Skip to content

Commit debf6ba

Browse files
Merge branch 'main' into dev/gokul/smolvlm-masked-scatter
2 parents 5131d6e + ced5268 commit debf6ba

19 files changed

Lines changed: 697 additions & 153 deletions

.github/workflows/ci.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
branches: [main]
6+
push:
7+
branches: [main]
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.ref }}
11+
cancel-in-progress: true
12+
13+
jobs:
14+
lint:
15+
if: github.repository == 'apple/coreai-torch'
16+
runs-on: [self-hosted, macos, tahoe, ARM64]
17+
timeout-minutes: 15
18+
steps:
19+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
20+
- name: Ensure uv
21+
run: |
22+
command -v uv >/dev/null 2>&1 || curl -LsSf https://astral.sh/uv/install.sh | sh
23+
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
24+
- run: uv run --extra dev ruff check .
25+
- run: uv run --extra dev ruff format --check .
26+
27+
python-test:
28+
if: github.repository == 'apple/coreai-torch'
29+
runs-on: [self-hosted, macos, tahoe, ARM64]
30+
timeout-minutes: 60
31+
steps:
32+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
33+
- name: Ensure uv
34+
run: |
35+
command -v uv >/dev/null 2>&1 || curl -LsSf https://astral.sh/uv/install.sh | sh
36+
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
37+
- run: uv run --extra test pytest tests/ -n auto -m "not slow"

coreai_torch/_aten_to_core.py

Lines changed: 84 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -644,17 +644,20 @@ def replace_arange_start_step(
644644
else coreai.constant(1, dtype=start.type.element_type)
645645
)
646646

647-
# Squeeze rank-1 inputs to 0D scalars for coreai.range_.
647+
# coreai.range_ requires scalar (rank-0) operands that share an element
648+
# type. aten.arange promotes mixed-type scalars internally; we replicate
649+
# that here by squeezing each operand to rank-0 and casting to the FX
650+
# node's output dtype before the op.
651+
target_type = get_output_element_type_from_node(node)
652+
648653
def to_scalar(v: Value) -> Value:
649654
if v.type.rank > 0:
650-
return coreai.shrink_dims(v, list(range(v.type.rank)))
655+
v = coreai.shrink_dims(v, list(range(v.type.rank)))
656+
if v.type.element_type != target_type:
657+
v = coreai.cast(v, target_type)
651658
return v
652659

653-
result = coreai.range_(to_scalar(start), to_scalar(end), to_scalar(step))
654-
target_type = get_output_element_type_from_node(node)
655-
if result.type.element_type != target_type:
656-
result = coreai.cast(result, target_type)
657-
return result
660+
return coreai.range_(to_scalar(start), to_scalar(end), to_scalar(step))
658661

659662

660663
def replace_batch_norm(
@@ -814,6 +817,7 @@ def replace_binary_ops(
814817
"pow.Scalar": coreai.broadcasting_pow,
815818
"pow.Tensor_Tensor": coreai.broadcasting_pow,
816819
"pow.Tensor_Scalar": coreai.broadcasting_pow,
820+
"pow": coreai.broadcasting_pow,
817821
"sub.Tensor": coreai.broadcasting_sub,
818822
"sub.Scalar": coreai.broadcasting_sub,
819823
"sub": coreai.broadcasting_sub,
@@ -944,6 +948,55 @@ def replace_cat(values_map: dict[str, Value], node: fx.Node, loc: Location) -> V
944948

945949
rank = inputs[0].type.rank
946950
dim = dim + rank if dim < 0 else dim
951+
952+
# coreai.concat requires all non-concat dims to be provably equal across
953+
# inputs. Under dynamic shapes, one branch can carry a dynamic non-concat
954+
# axis while a sibling has a static size for the same axis — the dynamic
955+
# side must in fact equal that static size, but the type system doesn't
956+
# know it. Reshape such inputs to the known static size before the concat.
957+
# Multiple distinct static sizes on one axis is a real mismatch and is
958+
# left for the dialect verifier to reject.
959+
dyn = ShapedType.get_dynamic_size()
960+
961+
def known_static(axis: int) -> int | None:
962+
if axis == dim:
963+
return None
964+
sizes = {inp.type.shape[axis] for inp in inputs if inp.type.shape[axis] != dyn}
965+
return next(iter(sizes)) if len(sizes) == 1 else None
966+
967+
statics = [known_static(a) for a in range(rank)]
968+
promoted: list[Value] = []
969+
for inp in inputs:
970+
new_shape = [
971+
statics[a]
972+
if statics[a] is not None and inp.type.shape[a] == dyn
973+
else inp.type.shape[a]
974+
for a in range(rank)
975+
]
976+
if new_shape != list(inp.type.shape):
977+
if all(s != dyn for s in new_shape):
978+
# All axes static post-promotion: list-form reshape packs
979+
# the shape into an int32 constant tensor.
980+
inp = coreai.reshape(inp, new_shape)
981+
else:
982+
# Mixed static / dynamic post-promotion: build the shape
983+
# vector at runtime by mixing the input's actual sizes
984+
# (via coreai.get_shape) for the still-dynamic axes with
985+
# constants for the promoted axes.
986+
runtime_shape = coreai.cast(coreai.get_shape(inp), dtype=np.int32)
987+
parts = [
988+
coreai.constant([s], dtype=np.int32)
989+
if s != dyn
990+
else coreai.slice_(runtime_shape, [a], [a + 1], [1])
991+
for a, s in enumerate(new_shape)
992+
]
993+
result_type = RankedTensorType.get(new_shape, inp.type.element_type)
994+
inp = coreai.ReshapeOp(
995+
inp, coreai.concat(0, parts), results=[result_type]
996+
).result
997+
promoted.append(inp)
998+
inputs = promoted
999+
9471000
return coreai.concat(dim, inputs)
9481001

9491002

@@ -2232,11 +2285,29 @@ def replace_remainder(
22322285

22332286
def replace_repeat(values_map: dict[str, Value], node: fx.Node, loc: Location) -> Value:
22342287
x = _get_operand(values_map, node, 0)
2235-
repeats = np.array(node.args[1], dtype=np.uint32)
2236-
extra_dims = len(repeats) - x.type.rank
2288+
repeat_args = list(node.args[1])
2289+
extra_dims = len(repeat_args) - x.type.rank
22372290
if extra_dims > 0:
22382291
x = coreai.expand_dims(x, list(range(extra_dims)))
2239-
return coreai.tile(x, repeats)
2292+
2293+
if all(isinstance(r, int) for r in repeat_args):
2294+
return coreai.tile(x, np.array(repeat_args, dtype=np.uint32))
2295+
2296+
# At least one repeat is a SymInt fx.Node — build a rank-1 uint32 dim
2297+
# vector at runtime, with per-axis constants for plain ints and the
2298+
# resolved Value (cast to uint32, lifted to rank-1 if scalar) for
2299+
# SymInts. coreai.tile accepts a runtime Value for its dims.
2300+
chunks: list[Value] = []
2301+
for r in repeat_args:
2302+
if isinstance(r, int):
2303+
chunks.append(coreai.constant([r], dtype=np.uint32))
2304+
else:
2305+
assert isinstance(r, fx.Node)
2306+
v = coreai.cast(values_map[r.name], dtype=np.uint32)
2307+
if v.type.rank == 0:
2308+
v = coreai.reshape(v, [1])
2309+
chunks.append(v)
2310+
return coreai.tile(x, coreai.concat(0, chunks))
22402311

22412312

22422313
def replace_round_decimals(
@@ -2635,6 +2706,7 @@ def replace_unary_ops(
26352706
"log.default": coreai.log,
26362707
"relu.default": coreai.relu,
26372708
"round.default": coreai.round_,
2709+
"round": coreai.round_,
26382710
"rsqrt.default": coreai.rsqrt,
26392711
"sigmoid.default": coreai.sigmoid,
26402712
"silu.default": coreai.silu,
@@ -3475,13 +3547,15 @@ def sdpa_maskless(q: Value, k: Value, v: Value) -> Value:
34753547
"pow.Scalar": replace_binary_ops,
34763548
"pow.Tensor_Scalar": replace_binary_ops,
34773549
"pow.Tensor_Tensor": replace_binary_ops,
3550+
"pow": replace_binary_ops,
34783551
"prod.default": replace_prod_default,
34793552
"prod.dim_int": replace_prod_dim_int,
34803553
"reciprocal.default": replace_reciprocal,
34813554
"relu.default": replace_unary_ops,
34823555
"remainder.Tensor": replace_remainder,
34833556
"round.default": replace_unary_ops,
34843557
"round.decimals": replace_round_decimals,
3558+
"round": replace_unary_ops,
34853559
"repeat.default": replace_repeat,
34863560
"rsqrt.default": replace_unary_ops,
34873561
"scaled_dot_product_attention.default": replace_sdpa,

coreai_torch/_compression/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ def repeat_tensor_as(tensor: torch.Tensor, target_shape: torch.Size) -> torch.Te
9999
)
100100
return repeated_tensor
101101

102+
102103
def wrap_for_parametrization(
103104
compression_module_class: type[torch.nn.Module],
104105
) -> type[torch.nn.Module]:

coreai_torch/_utils.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,23 @@ def __exit__(self, *exc: Any) -> None:
175175
self.close()
176176

177177

178+
def to_rank1_int32(v: Value) -> Value:
179+
"""Coerce a SymInt-derived Value to canonical rank-1 si32 form.
180+
181+
Dim-vector concats (used to build shape operands for ``coreai.reshape``,
182+
``coreai.interpolate``, etc.) require all inputs to share rank and
183+
element type. SymInt values can arrive rank-0 (e.g. from
184+
``aten._local_scalar_dense``) or with a different int variant. This
185+
helper produces the form that aligns with ``coreai.constant([i],
186+
dtype=np.int32)`` and ``replace_sym_size_int``.
187+
"""
188+
if v.type.rank == 0:
189+
v = coreai.reshape(v, [1])
190+
if v.type.element_type != IntegerType.get_signed(32):
191+
v = coreai.cast(v, np.int32)
192+
return v
193+
194+
178195
def upsample_build_output_shape_dynamic(
179196
x: Value, out_h: int | Value, out_w: int | Value
180197
) -> Value:
@@ -192,8 +209,8 @@ def upsample_build_output_shape_dynamic(
192209
)
193210
shape = coreai.cast(coreai.get_shape(x), dtype=np.int32)
194211
non_spatial = coreai.slice_(shape, [0], [2], [1])
195-
h = [out_h] if isinstance(out_h, int) else out_h
196-
w = [out_w] if isinstance(out_w, int) else out_w
212+
h = [out_h] if isinstance(out_h, int) else to_rank1_int32(out_h)
213+
w = [out_w] if isinstance(out_w, int) else to_rank1_int32(out_w)
197214
return coreai.concat(0, [non_spatial, h, w])
198215

199216

@@ -986,9 +1003,13 @@ def get_operand(
9861003
if isinstance(arg, fx.Node):
9871004
return values_map[arg.name]
9881005
if isinstance(arg, list) and any(isinstance(e, fx.Node) for e in arg):
989-
# Mixed list: resolve fx.Node elements via values_map, keep ints as constants.
1006+
# Mixed list: SymInt fx.Nodes + plain ints. Concat the two sources
1007+
# into a single rank-1 si32 dim vector. Both branches must produce
1008+
# the same canonical form so the concat verifier accepts them.
9901009
dim_vals = [
991-
values_map[e.name] if isinstance(e, fx.Node) else coreai.constant([e])
1010+
to_rank1_int32(values_map[e.name])
1011+
if isinstance(e, fx.Node)
1012+
else coreai.constant([e], dtype=np.int32)
9921013
for e in arg
9931014
]
9941015
return coreai.concat(0, dim_vals) if len(dim_vals) > 1 else dim_vals[0]

docs/coreai-core/tutorials/construct-a-graph.ipynb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,10 @@
7676
"\n",
7777
"import numpy as np\n",
7878
"\n",
79-
"from coreai.authoring import AIModelAsset, AIProgram, Module, TensorSpec\n",
80-
"\n",
8179
"# Graph-building primitives — pending re-export from coreai.authoring.\n",
8280
"from coreai._compiler.dialects import coreai as ops\n",
83-
"from coreai._compiler.ir import Value"
81+
"from coreai._compiler.ir import Value\n",
82+
"from coreai.authoring import AIModelAsset, AIProgram, Module, TensorSpec"
8483
]
8584
},
8685
{
@@ -175,6 +174,7 @@
175174
" ) -> Annotated[Value, output_spec]:\n",
176175
" return ops.add(x, x)\n",
177176
"\n",
177+
"\n",
178178
"module.verify()\n",
179179
"print(\"Module verified.\")"
180180
]

docs/coreai-core/tutorials/run-an-aimodel.ipynb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@
6464
"from pathlib import Path\n",
6565
"\n",
6666
"import numpy as np\n",
67-
"\n",
6867
"from coreai.authoring import AIModelAsset\n",
6968
"from coreai.runtime import InferenceFunction, NDArray\n",
7069
"\n",
@@ -108,10 +107,10 @@
108107
"from shutil import rmtree\n",
109108
"from typing import Annotated\n",
110109
"\n",
111-
"from coreai.authoring import AIProgram, Module, TensorSpec\n",
112110
"# Pending re-export from coreai.authoring; see the previous tutorial.\n",
113111
"from coreai._compiler.dialects import coreai as ops\n",
114112
"from coreai._compiler.ir import Value\n",
113+
"from coreai.authoring import AIProgram, Module, TensorSpec\n",
115114
"\n",
116115
"# Reconstruct asset.\n",
117116
"if asset_path.exists():\n",
@@ -126,6 +125,7 @@
126125
" ) -> Annotated[Value, TensorSpec(shape=[2, 3], dtype=np.float32, name=\"y\")]:\n",
127126
" return ops.add(x, x)\n",
128127
"\n",
128+
"\n",
129129
"AIProgram(module).save_asset(asset_path)\n",
130130
"print(f\"created {asset_path}\")"
131131
]

docs/getting-started/quickstart.ipynb

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,6 @@
6868
"metadata": {},
6969
"outputs": [],
7070
"source": [
71-
"import torch\n",
72-
"\n",
7371
"example_input = (torch.randn(1, 10),)\n",
7472
"exported = torch.export.export(model, args=example_input)"
7573
]
@@ -145,10 +143,10 @@
145143
"outputs": [],
146144
"source": [
147145
"import tempfile\n",
148-
"import torch\n",
149146
"from pathlib import Path\n",
150147
"\n",
151148
"import numpy as np\n",
149+
"import torch\n",
152150
"from coreai.runtime import NDArray\n",
153151
"\n",
154152
"\n",
@@ -240,9 +238,6 @@
240238
"metadata": {},
241239
"outputs": [],
242240
"source": [
243-
"import tempfile\n",
244-
"\n",
245-
"\n",
246241
"async def run():\n",
247242
" with tempfile.TemporaryDirectory() as tmpdir:\n",
248243
" asset = coreai_program.save_asset(Path(tmpdir) / \"mobilenet_v2_example.aimodel\")\n",
@@ -274,6 +269,7 @@
274269
"outputs": [],
275270
"source": [
276271
"import torch\n",
272+
"\n",
277273
"import coreai_torch\n",
278274
"\n",
279275
"model = SimpleModel().eval()\n",

docs/guides/composite-ops.ipynb

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,6 @@
9797
"metadata": {},
9898
"outputs": [],
9999
"source": [
100-
"import torch\n",
101-
"import torch.nn as nn\n",
102-
"from coreai_torch.composite_ops import RMSNormImpl\n",
103-
"\n",
104-
"\n",
105100
"class RMSNorm(nn.Module):\n",
106101
" \"\"\"Convenience wrapper that owns the learnable scale parameter.\"\"\"\n",
107102
"\n",
@@ -149,7 +144,6 @@
149144
"outputs": [],
150145
"source": [
151146
"import torch\n",
152-
"import coreai_torch\n",
153147
"\n",
154148
"coreai_program = (\n",
155149
" TorchConverter()\n",
@@ -185,10 +179,10 @@
185179
"outputs": [],
186180
"source": [
187181
"import tempfile\n",
188-
"import torch\n",
189182
"from pathlib import Path\n",
190183
"\n",
191184
"import numpy as np\n",
185+
"import torch\n",
192186
"from coreai.runtime import NDArray\n",
193187
"\n",
194188
"\n",

docs/guides/conversion-workflows.ipynb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
"outputs": [],
8787
"source": [
8888
"import torch\n",
89+
"\n",
8990
"import coreai_torch\n",
9091
"\n",
9192
"model = MyModel().eval()\n",
@@ -133,7 +134,7 @@
133134
"source": [
134135
"import torch\n",
135136
"import torch.nn as nn\n",
136-
"import coreai_torch\n",
137+
"\n",
137138
"from coreai_torch import ExternalizeSpec, TorchConverter\n",
138139
"from coreai_torch.composite_ops import RMSNormImpl\n",
139140
"\n",

0 commit comments

Comments
 (0)