Skip to content

Commit e8dac1d

Browse files
fix(arange): preserve static output shape for float-dtype arange with integer bounds (#25)
* _aten_to_core: fix arange lowering to preserve static shape for float dtypes Cast range_ operands to si32 instead of the output dtype, so the compiler can infer a static element count from constant int bounds. Cast the result to the requested dtype afterward. Previously casting operands to float caused range_ to return tensor<?xT> even for compile-time constants, breaking composite signatures that expected a static dimension. * _aten_to_core: fix arange lowering — use si32 only for integer operands Float operands (e.g. arange(0.5, 5.0, 0.5)) must not be cast to si32 as that truncates the values. Only apply the integer-path optimisation when start has an integer element type; fall back to target_type for float operands where a dynamic shape is correct anyway.
1 parent ea728d6 commit e8dac1d

2 files changed

Lines changed: 56 additions & 22 deletions

File tree

coreai_torch/_aten_to_core.py

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

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.
647+
# When operands are integer-typed, keep them as si32 so coreai.range_ can
648+
# infer a static output shape, then cast the result to the requested dtype.
649+
# For float operands the count isn't statically determinable anyway, so
650+
# cast everything to target_type and let range_ return a dynamic shape.
651651
target_type = get_output_element_type_from_node(node)
652+
si32 = IntegerType.get_signed(32)
653+
range_type = (
654+
si32 if isinstance(start.type.element_type, IntegerType) else target_type
655+
)
652656

653657
def to_scalar(v: Value) -> Value:
654658
if v.type.rank > 0:
655659
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)
660+
if v.type.element_type != range_type:
661+
v = coreai.cast(v, range_type)
658662
return v
659663

660-
return coreai.range_(to_scalar(start), to_scalar(end), to_scalar(step))
664+
result = coreai.range_(to_scalar(start), to_scalar(end), to_scalar(step))
665+
if result.type.element_type != target_type:
666+
result = coreai.cast(result, target_type)
667+
return result
661668

662669

663670
def replace_batch_norm(

tests/ops/test_ops_ir.py

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -995,36 +995,63 @@ def forward(self, x: Tensor) -> Tensor:
995995
dynamic_shapes={"x": {0: torch.export.Dim("batch", min=1)}},
996996
)
997997
# ``end`` is a SymInt (rank-1 si32) sliced from x.shape[0];
998-
# ``start``/``step`` are scalar (rank-0) si32 constants. The
999-
# lowering casts each operand to the FX node's output dtype (f32)
1000-
# before ``coreai.range_`` so the op sees uniform-typed scalars;
1001-
# the optimizer then constant-folds the casts on start/step into
1002-
# f32 constants directly.
998+
# ``start``/``step`` are scalar (rank-0) si32 constants.
999+
# The lowering keeps all operands as si32 so that coreai.range_
1000+
# can infer a static shape from constant int operands, then casts
1001+
# the result to the requested output dtype. (Casting operands to
1002+
# float *before* range_ causes it to return tensor<?> even when the
1003+
# bounds are compile-time constants.)
10031004
filecheck_pattern(
10041005
ir,
10051006
check_file="""
10061007
// CHECK-LABEL: coreai.graph @main
10071008
// CHECK-SAME: %[[ARG0:.*]]: tensor<?x3xf32>
10081009
// CHECK-SAME: -> (tensor<?xf32>
10091010
//
1010-
// start and step land as f32 constants directly (the si32
1011-
// constant + cast-to-f32 pair gets folded by the optimizer):
1012-
// CHECK-DAG: %[[STEP:.+]] = coreai.constant dense<1.000000e+00> : tensor<f32>
1013-
// CHECK-DAG: %[[START:.+]] = coreai.constant dense<0.000000e+00> : tensor<f32>
1011+
// start and step remain as si32 constants; no pre-range cast:
1012+
// CHECK-DAG: %[[STEP:.+]] = coreai.constant dense<{{.*}}> : tensor<si32>
1013+
// CHECK-DAG: %[[START:.+]] = coreai.constant dense<{{.*}}> : tensor<si32>
10141014
//
1015-
// end: get_shape -> slice -> cast(ui32->si32) -> reshape(rank-1 to rank-0) -> cast(si32->f32):
1015+
// end: get_shape -> slice -> cast(ui32->si32) -> reshape(rank-1 to rank-0);
1016+
// stays si32, no cast to f32 before range_:
10161017
// CHECK: %[[END_RANK1:.+]] = coreai.cast {{.*}} : tensor<1xui32> to tensor<1xsi32>
10171018
// CHECK: %[[END_RANK0:.+]] = coreai.reshape %[[END_RANK1]], {{.*}} : (tensor<1xsi32>, tensor<0xui32>) -> tensor<si32>
1018-
// CHECK: %[[END_F32:.+]] = coreai.cast %[[END_RANK0]] : tensor<si32> to tensor<f32>
10191019
//
1020-
// range called with all-f32 scalars; result is f32 directly,
1021-
// no post-range cast on the result:
1022-
// CHECK: %[[OUT:.+]] = coreai.range %[[START]], %[[END_F32]], %[[STEP]] : (tensor<f32>, tensor<f32>, tensor<f32>) -> tensor<?xf32>
1023-
// CHECK-NOT: coreai.cast %[[OUT]]
1020+
// range_ called with all-si32 scalars; result is si32 with dynamic shape:
1021+
// CHECK: %[[RANGE_OUT:.+]] = coreai.range %[[START]], %[[END_RANK0]], %[[STEP]] : (tensor<si32>, tensor<si32>, tensor<si32>) -> tensor<?xsi32>
1022+
//
1023+
// post-range cast to the requested f32 output dtype:
1024+
// CHECK: %[[OUT:.+]] = coreai.cast %[[RANGE_OUT]] : tensor<?xsi32> to tensor<?xf32>
10241025
// CHECK: coreai.output %[[OUT]] : tensor<?xf32>
10251026
""",
10261027
)
10271028

1029+
def test_static_float_dtype_preserves_shape(self) -> None:
1030+
"""Regression: arange with a float dtype must keep a static output shape.
1031+
1032+
Casting operands to float *before* coreai.range_ causes the op to
1033+
return tensor<?xf32> even when all bounds are compile-time constants.
1034+
The fix is to run range_ on int operands and cast the result.
1035+
"""
1036+
1037+
class ArangeFloat(nn.Module):
1038+
def forward(self, x: Tensor) -> Tensor:
1039+
# Constant int bounds, float output dtype — the problematic case.
1040+
return torch.arange(0, 8, 2, dtype=torch.float32, device=x.device)
1041+
1042+
ir = get_ir(ArangeFloat().eval(), x=torch.rand(4))
1043+
# The output shape must be static (4 elements: 0,2,4,6).
1044+
filecheck_pattern(
1045+
ir,
1046+
check_file="""
1047+
// CHECK-LABEL: module {
1048+
// CHECK-NEXT: coreai.graph @main(%[[ARG0:.*]]: tensor<4xf32>
1049+
// CHECK-SAME: -> (tensor<4xf32>
1050+
// The optimizer constant-folds the whole thing to a dense literal:
1051+
// CHECK: coreai.constant dense<{{.*}}> : tensor<4xf32>
1052+
""",
1053+
)
1054+
10281055

10291056
class TestArgmaxIR:
10301057
def test_static(self) -> None:

0 commit comments

Comments
 (0)