Emit scalar Range bounds in aten_unfold - #3013
Conversation
aten_unfold read the unfolded dimension with op.Gather(input_shape, op.Constant(value_ints=[dimension])), which produces an int64[1] tensor rather than a scalar. op.Sub preserves that rank, so the rank 1 value reached the limit input of Range. The ONNX Range spec requires all three inputs to be rank 0, and onnxruntime rejects the model at session creation: Node (node_Range_6) Op (Range) [ShapeInferenceError] Input to 'Range' op should be scalars (Tensor with only one element and shape empty) With the default optimize=True and a static unfold dimension the constant folder collapses Shape/Gather/Sub/Range into one index constant, so no Range node survives and those exports are unaffected. The invalid node shows up with optimize=False, and on a dynamic unfold dimension where Range survives but its inputs cannot be folded. Squeeze the Gather result to a scalar instead of switching the index to value_int. A scalar index would also satisfy Range, but the Shape/Gather folding rule in onnxscript/optimizer/_constant_folding.py bails out on any index whose ndim is not 1, so that spelling quietly disables the folding. Squeeze is registered as a shape value propagator there, so the folded path keeps working: a dynamic batch with a static unfold dimension still optimizes down to ops=['Gather'].
|
Thanks! |
There was a problem hiding this comment.
Pull request overview
This PR fixes an ONNX spec-compliance issue in the aten_unfold lowering where the limit input to Range could become a rank-1 tensor (int64[1]) due to Gather returning [1]-shaped output for a rank-1 index. The change ensures Range receives rank-0 scalar inputs (as required by the ONNX spec), preventing onnxruntime from rejecting unoptimized exports.
Changes:
- Update
aten_unfoldtoSqueezethe[1]-shapedGatherresult into a scalar before feeding it toRange. - Add an end-to-end regression test that exports with
optimize=Falseto ensure the scalar-Range-bounds behavior is exercised.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
onnxscript/function_libs/torch_lib/ops/core.py |
Squeezes the gathered dimension size to a scalar to satisfy Range’s scalar-input requirement. |
tests/function_libs/torch_lib/e2e_ops_tests.py |
Adds an optimize=False export regression test for unfold to catch invalid Range inputs. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #3013 +/- ##
=======================================
Coverage 72.63% 72.63%
=======================================
Files 265 265
Lines 32218 32218
Branches 3044 3044
=======================================
Hits 23403 23403
Misses 7781 7781
Partials 1034 1034 ☔ View full report in Codecov by Harness. |
f78d33b
into
microsoft:main
|
Thanks! Those two red checks are the py311 torch nightly jobs, they're red on main and on every other open PR at the moment so there's nothing to chase there. |
aten_unfoldreads the size of the unfolded dimension like this:A rank 1 index makes
Gatherreturnint64[1], not a scalar.Subpreserves that rank, so a rank 1 value reaches thelimitinput ofRange. The ONNX spec requires all threeRangeinputs to be rank 0, and onnxruntime rejects the model at session creation:Scope
This is narrower than it first looks. With
x.unfold(1, 2, 1)on a(3, 4)input:optimize=True(default)['Gather']optimize=FalseRangesurvivesRangesurvivesOn the default path the constant folder collapses
Shape/Gather/Sub/Rangeinto a single index constant, so noRangenode survives and those exports are fine and numerically correct. The bad node is reachable withoptimize=False, and on a dynamic unfold dimensionRangesurvives with a rank 1limitbut onnxruntime cannot fold its inputs, so the scalar check never runs and the spec violation is silently tolerated.So: a spec compliance fix on an unoptimized path, not a broken operator.
The fix
Why not just
value_int=dimensionThat one token change also gives
Rangea scalar and it does fix onnxruntime, but it quietly defeats theShape/Gatherconstant folding. The rule inonnxscript/optimizer/_constant_folding.pybails out on any index whosendimis not 1:Measured on a dynamic batch dimension with a static unfold dimension, default
optimize=True:Squeezeis registered as a shape value propagator in that same file, so the symbolic shape value survives the squeeze and the folded path keeps working. The seven node chain would otherwise be left in the graph at runtime.Testing
Added
test_unfold_emits_scalar_range_boundstotests/function_libs/torch_lib/e2e_ops_tests.py, following theoptimize=Falsepattern already used by the tests around it.With only the
core.pychange reverted, the new test fails with the error quoted above. With the fix it passes. Rest ofe2e_ops_tests.pyand theunfoldcases inops_test.pyare unchanged.Same class of fix as #2943 (rank 1 where a scalar was required, same file) and #3006 (invalid ONNX graph out of a torchlib lowering).