Skip to content

Commit 1c26f1f

Browse files
_aten_to_core, _utils: harden mixed-source SymInt lowerings under dynamic shapes
Six related fixes that surface together when exporting models whose FX graphs combine SymInt-derived shape arithmetic with mixed source element types and ranks: 1. _aten_to_core_resolver / replace_binary_ops _op_map: register a bare 'pow' entry alongside the variant-suffixed ones. Some torch.export rewrites leave ``aten.pow`` as the OpOverloadPacket target with no overload suffix; without this entry the converter raises ``Unsupported ATen op: pow``. 2. Same registries for bare 'round': torch.export can leave ``aten.round`` without a ``.default`` overload, mirroring the pow case. 3. upsample_build_output_shape_dynamic: ensure each (out_h, out_w) operand is rank-1 with int32 element type before the concat that builds the output shape — the dialect verifier rejects mixed-rank / mixed-element-type concat inputs. Hits when out_h/out_w are SymInts derived from ``round(SymFloat)`` arithmetic. 4. get_operand mixed-list path (SymInt + plain int): normalise each resolved Value to the same canonical rank-1 si32 form and emit plain-int constants with explicit ``dtype=np.int32`` so the dim-vector concat sees uniform operands. Hits ops like ``view``, ``expand``, ``reshape``, ``repeat`` whenever a dim list mixes SymInts with ints. 5. replace_cat: when one input has a dynamic non-concat axis and a sibling has a known static size for that axis, reshape the dynamic side to that static size before the concat. Localised shape inference using the fact that all non-concat dims must be equal — multiple distinct static sizes is left for the dialect verifier to reject. 6. replace_arange_start_step: unify start/end/step element types to the FX node's output dtype before ``coreai.range_``. Mirrors aten.arange's internal type promotion since coreai.range_'s verifier requires uniform element types. Adds a shared ``to_rank1_int32(v)`` helper in ``_utils.py`` so fixes 3 and 4 share one canonical normalization (rank-0 → rank-1, cast to signed int32 if needed); both call sites collapse to one line per operand. One regression test per non-trivial fix, each verified to FAIL without the fix and PASS with it (verified by reverting each fix individually): - TestRound: bare ``aten.round`` overload-packet target must lower. - TestUpsampleNearest2d / TestUpsampleBilinear2d::test_round_symfloat_size: ``round((num / aspect) ** 0.5) * 14`` output_size produces SymInts whose Value type doesn't match the int32 constants used elsewhere. Pre-fix: ``coreai.concat`` raises ``Operation creation failed``. - TestView::test_view_with_round_symfloat_dims: same trigger applied to a ``view([1, C, h, w])`` mixed list. Pre-fix: ``expected the same element type for all inputs to concat``. - TestCat::test_dynamic_vs_static_non_concat_axis: ``Dim.AUTO`` on one side + static sibling forces non-concat-axis promotion. - TestArange::test_symint_end_with_float_start_step: float ``arange`` with SymInt-derived end exercises the element-type unify.
1 parent 7171b3b commit 1c26f1f

4 files changed

Lines changed: 391 additions & 30 deletions

File tree

coreai_torch/_aten_to_core.py

Lines changed: 44 additions & 7 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,36 @@ 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+
inp = coreai.reshape(inp, new_shape)
978+
promoted.append(inp)
979+
inputs = promoted
980+
947981
return coreai.concat(dim, inputs)
948982

949983

@@ -2595,6 +2629,7 @@ def replace_unary_ops(
25952629
"log.default": coreai.log,
25962630
"relu.default": coreai.relu,
25972631
"round.default": coreai.round_,
2632+
"round": coreai.round_,
25982633
"rsqrt.default": coreai.rsqrt,
25992634
"sigmoid.default": coreai.sigmoid,
26002635
"silu.default": coreai.silu,
@@ -3434,13 +3469,15 @@ def sdpa_maskless(q: Value, k: Value, v: Value) -> Value:
34343469
"pow.Scalar": replace_binary_ops,
34353470
"pow.Tensor_Scalar": replace_binary_ops,
34363471
"pow.Tensor_Tensor": replace_binary_ops,
3472+
"pow": replace_binary_ops,
34373473
"prod.default": replace_prod_default,
34383474
"prod.dim_int": replace_prod_dim_int,
34393475
"reciprocal.default": replace_reciprocal,
34403476
"relu.default": replace_unary_ops,
34413477
"remainder.Tensor": replace_remainder,
34423478
"round.default": replace_unary_ops,
34433479
"round.decimals": replace_round_decimals,
3480+
"round": replace_unary_ops,
34443481
"repeat.default": replace_repeat,
34453482
"rsqrt.default": replace_unary_ops,
34463483
"scaled_dot_product_attention.default": replace_sdpa,

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]

0 commit comments

Comments
 (0)