Repository navigation
[Fix][Relax][Frontend][Torch] Validate flatten dims in from_fx - #20245
Merged
tlopex merged 1 commit intoSep 8, 2026
Merged
Conversation
Fixes: #20227 `_flatten_impl` normalized negative flatten dims but never checked their range or their ordering, so `from_fx` crashed with an internal `TypeError: reduce() of empty iterable with no initial value` on a traced model whose `start_dim` came after its `end_dim`. Out-of-range dims leaked an `IndexError`, and an out-of-range negative `start_dim` silently computed a wrong shape that only failed later inside `relax.op.reshape`. Normalize both dims against `max(rank, 1)`, validate each against `[-r, r-1]`, and reject `start_dim > end_dim` with a message matching torch's own. A 0-d input is now handled explicitly, matching `torch.flatten` on a scalar. Co-authored-by: Claude <noreply@anthropic.com>
cchung100m
approved these changes
Sep 5, 2026
cchung100m
left a comment
Contributor
There was a problem hiding this comment.
LGTM ,thanks to @hiyufan
tlopex
approved these changes
Sep 8, 2026
tlopex
pushed a commit
that referenced
this pull request
Sep 15, 2026
…0255) ### Problem PyTorch reads a literal `0` in a target shape as a real zero-sized dimension. `relax.op.reshape` reads it as *"copy the corresponding input dimension"* — ONNX `Reshape` with `allowzero=0`. The torch frontend forwards torch's shape unchanged, so any target shape holding a literal `0` is silently reinterpreted. ```python import torch x = torch.randn(2, 0, 4) x.reshape(0, 4) # torch (0, 4) x.view(0, 4) # torch (0, 4) torch.flatten(x) # torch (0,) torch.randn(2, 0).unflatten(0, (2, -1)) # torch (2, 1, 0) ``` On `main` these import as: | expression | input | torch | frontend on `main` | | --- | --- | --- | --- | | `x.reshape(0, 4)` | `(2, 0, 4)` | `(0, 4)` | `ValueError: Reshape expects the new shape to be convertible…` | | `x.view(0, 4)` | `(2, 0, 4)` | `(0, 4)` | same `ValueError` | | `x.reshape(0)` | `(2, 0, 4)` | `(0,)` | same `ValueError` | | `x.reshape(3, 0)` | `(0, 3)` | `(3, 0)` | same `ValueError` | | `torch.flatten(x)` | `(2, 0, 4)` | `(0,)` | same `ValueError` | | `torch.flatten(x)` | `(2, 3, 0)` | `(0,)` | same `ValueError` | | `x.unflatten(0, (2, -1))` | `(2, 0)` | `(2, 1, 0)` | `IndexError: Index 2 out of bounds 2` | | `torch.flatten(x)` | `(0, 3)` | `(0,)` | `(0,)` — happens to work | The last row is why this is easy to miss: copying input dim 0 there gives back the same `0` the literal asked for, so the one case people usually try looks fine. The `IndexError` comes from the same rule. `ConvertNewShapeToExpr` resolves a zero with `array_ref.Set(i, shape_ty->values.value()[i])`, indexing the *input* shape at the new shape's position, so a target of higher rank than the input reads past the end. Zero-sized tensors are not exotic in exported models — a detector with no proposals, an empty batch, an empty mask — and they reach `reshape`/`view`/`flatten` on ordinary code paths. ### Fix When the input is statically empty, the dimension torch asks for can be written as `-1` instead, whose inference yields `0`. `_torch_reshape_dims` does that rewrite, applied where a torch-supplied target shape reaches `relax.op.reshape`: `_reshape`, `_reshape_as`, `_flatten_impl`, `_unflatten`, `_as_strided`. The other `relax.op.reshape` call sites in the frontend derive their target from the input's own shape, where "copy input dim" and the literal agree, so they are left alone. **The rewrite is deliberately narrow.** It only fires when the input is statically empty. For a non-empty input torch rejects a zero in the target outright, and rewriting it to `-1` there would turn an error into a silently wrong shape: ``` input (2, 3), target [0, 2] torch: rejects today raises ValueError <- correct unconditional 0 -> -1 R.Tensor((3, 2)) <- wrong, and silent this PR (guard declines) raises ValueError <- unchanged ``` ### Verification All 17 shape cases I exercised now agree with PyTorch (6 previously raised). Zero-sized behaviour of `squeeze`, `permute`, `expand`, `cat` and `sum` was already correct and is unchanged. Built with LLVM and ran the imported module: `x.reshape(0, 4)` on `(2, 0, 4)` builds, runs, and returns shape `(0, 4)`. `tests/python/relax/test_frontend_from_fx.py` + `tests/python/relax/test_frontend_from_exported_program.py`: - clean `main`: 24 failed, 412 passed, 3 skipped - with this change: 24 failed, **415** passed, 3 skipped The 24 failures are pre-existing on `main` in my environment (`test_dtypes` and friends), identical before and after. The three additional passes are the new tests, which fail on `main` and pass with the fix. `ruff format --check` and `ruff check` are clean. ### One thing I want to flag The three tests run the imported module instead of comparing against an expected TVMScript module, because **the resulting `IRModule` cannot be written as TVMScript**. The frontend emits ``` lv: R.Tensor((0, 4), dtype="float32") = R.reshape(x, R.shape([0, 4])) ``` which executes correctly, but re-parsing it applies the copy rule again and infers `(2, 4)`, so the annotation no longer matches and the module is rejected as not well-formed. That round-trip gap lives in `relax.op.reshape`, not in the frontend, and this PR does not try to close it. So this fixes the observable behaviour but leaves the underlying ambiguity in place. **The more complete fix is probably an `allowzero`-style option on `relax.op.reshape`** (the ONNX frontend already carries `allowzero` and works around the same rule by routing through a dynamic shape expression), with the torch frontend opting in — that would also make the emitted IR round-trip. That is a change to a core op's interface, so I did not want to make that call unilaterally. **If you would prefer that shape, I am happy to implement it instead and close this.** Also worth noting: `_flatten_impl` is touched here and also by #20245. The hunks are independent and should merge cleanly; happy to rebase either way. --- This change was prepared with AI assistance (Claude). I have reviewed and verified it, and can speak to it in review. --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes: #20227
Summary
tvm.relax.frontend.torch.from_fxcrashed with an internalTypeError: reduce() of empty iterable with no initial valuewhen a traced modelcontained a
flattenwhosestart_dimcomes after itsend_dim.torch rejects such a
flattenwith a clearRuntimeError, but only when the model isexecuted.
fx.symbolic_tracedoes not execute the model, so the invalid node reachesthe frontend as a perfectly traceable graph and has to be rejected there. This PR
validates the dims in
_flatten_impl, mirroring whatfrom_onnxnow does forFlatten(#20145).Only the
from_fx/TorchFXImporterpath is affected.from_exported_programrunsrun_decompositions()by default, which lowersaten.flatten.using_intstoaten.view, so_flatten_implis never reached there.Root cause
_flatten_implinpython/tvm/relax/frontend/torch/base_fx_graph_translator.pynormalized negative dims but never checked their range or their ordering:
For
start_dim=2, end_dim=1therangeis empty, sofunctools.reduceis called overan empty iterable with no initial value. Out-of-range dims (
flatten(x, 0, 3)on arank-3 input) leaked an
IndexErrorout ofshape[i]instead.Both entry points that reach this helper are affected:
torch.flatten(dispatched via_flatten) andtorch.nn.Flatten(via_flatten_module).Fix
Normalize both dims against
max(rank, 1), validate each against[-r, r-1], andreject
start_dim > end_dimwith torch's own wording:A 0-d input is handled explicitly: torch normalizes flatten dims against a rank of at
least one, so
torch.flatten(scalar)is valid and returns a 1-d tensor holding thesingle element. The old code hit the same empty-
reducecrash for that input.Validation
Built from source (CPU-only,
USE_LLVM=OFF) on Linux, torch 2.13.0+cpu, Python 3.14.Behavior on the reported cases, measured before and after the change:
(2,3,4)(2,1)TypeError: reduce() of empty iterable with no initial valueValueError: flatten() has invalid args: start_dim cannot come after end_dim(2,3,4)(0,3)IndexError: ShapeExpr index out of rangeValueError: flatten end_dim 3 is out of range [-3, 2] for an input of rank 3(2,3,4)(-4,2)ValueError: Reshape expects the new shape to be convertible from the old shapeValueError: flatten start_dim -4 is out of range [-3, 2] for an input of rank 3()(0-d)(0,-1)TypeError: reduce() of empty iterable with no initial value(1,)The
(-4,2)row is worth calling out: an out-of-range negativestart_dimdid not fail in_flatten_implat all. It normalized to-1, sorange(-1, 3)silently folded the lastdimension into the product and emitted a
reshapeto(96,)for a 24-element tensor, whichonly failed later inside
relax.op.reshape. That is a mis-computation, not just a crash.Valid dims are unaffected —
(1,3,10,10)with(2,-1), and(2,3,4)with(1,2),(0,-1),(-3,-1)and(2,2), all convert to the same shapes as before.Regression run of the whole
from_fxsuite, base commit vs. this branch:HEAD~1)The failure sets are identical line for line — the 16 failures (
test_dtypes[*],test_round)are pre-existing on the base commit in this environment and unrelated to this change.
Tests added to
tests/python/relax/test_frontend_from_fx.py:test_flatten_invalid_dims—start_dim > end_dimthrough bothtorch.flattenandtorch.nn.Flatten, plus an out-of-rangeend_dimtest_flatten_scalar_input— 0-d input flattens to shape(1,)Files changed
python/tvm/relax/frontend/torch/base_fx_graph_translator.py—_flatten_impl:normalize and validate dims, handle a 0-d input.
tests/python/relax/test_frontend_from_fx.py— regression tests.Note for reviewers
The 0-d handling (
dim_post_expr = max(rank, 1)plus therank == 0branch) isseparable from the reported bug. It fixes the same empty-
reducecrash for scalarinputs and keeps the new range check from rejecting a valid
torch.flatten(scalar),but if you would rather keep this PR to exactly the reported case I am happy to drop
that hunk and its test.
This change was developed with AI assistance. The build, the before/after comparison
and the test runs reported above were all executed locally against this branch as
submitted.