Repository navigation
[Fix][Relax][Frontend][ONNX] Fix Scatter with indices smaller than data - #20187
Merged
tlopex merged 8 commits intoAug 27, 2026
Merged
Conversation
Scatter._impl_v9 forwarded indices/updates to relax.op.scatter_elements regardless of their shape. ONNX Scatter (opset 9/10) iterates over indices' own shape: for each entry idx, output[idx[:axis] + (indices[idx],) + idx[axis+1:]] = updates[idx], which matches scatter_elements only when indices has data's exact shape. For a legal model whose indices/updates are smaller than data (e.g. size-1 broadcast dims), the scatter_elements lowering silently produced wrong values, diverging from onnxruntime. Lower the shape-mismatched case as scatter_nd with explicit per-entry target positions: a constant coordinate grid of indices' shape with the axis column replaced by the flattened indices values. This is exact for every axis and any indices shape; the scatter_elements path is kept for the statically-known exact-shape and dynamic-shape cases. The fix also makes Scatter with mismatched indices work for axis 0 and 2, which test_scatter previously skipped, so restrict that skip to ScatterElements (still lowered via scatter_elements), and add test_scatter_broadcast covering the size-1 broadcast cases across axes.
tlopex
requested changes
Aug 26, 2026
| "bool", | ||
| ), | ||
| relax.op.reshape(indices, (n_entries, 1)), | ||
| relax.const(grid.astype("int64"), "int64"), |
Member
There was a problem hiding this comment.
This hard-codes the coordinate grid as int64, but ONNX Scatter permits int32 indices as well. For a valid int32 model, the two branches of relax.op.where have different dtypes, causing import to fail. Please use the indices dtype and add an int32 test. Also, dynamic shapes still fall back to the old lowering that can silently produce incorrect results.
Contributor
Author
There was a problem hiding this comment.
Hi @tlopex, all requested changes have been implemented:
- Dtype Match: Dynamically bound the coordinate grid dtype to
indices.dtype(supporting bothint32andint64). - Dynamic Shape Safety: Added an explicit check/ValueError for dynamic shapes that cannot be proven equal, preventing fallback to silent incorrect lowering.
- Tests: Added
int32/int64test coverage (test_scatter_broadcast) and dynamic shape boundary test (test_scatter_dynamic_shape) withir_version=8for ONNXRuntime compatibility.
All CI checks have passed. Could you please re-review when you have time? Thanks!
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: #20182
Summary
The Relax ONNX frontend imported legal opset-9/10 Scatter models whose
indices/updatesare smaller thandata(e.g. size-1 broadcast dims) butsilently produced wrong numeric output. The spec iterates over
indices'own shape —
output[idx[:axis] + (indices[idx],) + idx[axis+1:]] = updates[idx]for each entry
idx— while the frontend forwardedindices/updatesstraightto
relax.op.scatter_elements, which only matches that semantics whenindices.shape == data.shape.Root cause
Scatter._impl_v9was a trivial forwarding torelax.op.scatter_elements:scatter_elements(torch-scatter_semantics) writes for every position of thebroadcast
indices; ONNX Scatter instead iteratesindices'own shape. Whenindicesis smaller thandata, the two disagree, and the lower-level opsilently emits wrong values for the size-1-dim (broadcast) cases — e.g.
data(2,3,4),indices(1,3,1),axis=0writes updates to the wrong cells(
max|diff| = 298in the minimal repro). A 72-case sweep (3 axes x indices dimsin {1, dim} x 3 seeds) showed 12/72 wrong, all with non-axis-1 broadcast dims.
Fix
When
indicesis statically known and its shape differs fromdata's, lower theper-entry semantics exactly as
scatter_ndwith explicit target positions: aconstant coordinate grid of
indices'own shape with the axis column replaced bythe flattened
indicesvalues.scatter_ndhas no axis (coordinates are explicit), so negative axes arenormalized with
axis % rank. The exact-shape and dynamic-shape cases keep theoriginal
scatter_elementspath, so conventional models are unchanged.This also makes Scatter with mismatched
indicescorrect foraxis=0/2(whichtest_scatterpreviously skipped), so that skip is now restricted toScatterElements(whose_impl_v11still usesscatter_elements).Validation
Differential test (Relax
from_onnx+relax.build+VirtualMachinevsonnxruntime) over 72 legal Scatter models:
data(2,3,4)xindiceseach dim in{1, dim} x axes {0,1,2} x 3 seeds, plus the minimal repro,
(16,16,16)+ (8,8,8)all axes, negative-axis and opset-10 cases. Verified on the familyfuzzlocked build
262c6d2e0via runtime monkey-patch (no source files modified).indices(incl. size-1 dims)max|diff| = 0indices.shape == data.shape(all axes incl. -1)In-tree tests added to
tests/python/relax/test_frontend_onnx.py:test_scatter_broadcast— 4 axes x 4 broadcastindicesshapes (16 cases),check_correctnessvs onnxruntime withcheck_dtypes=True: 16/16 pass.test_scatter— theaxis != 1skip is now restricted toScatterElements;Scatter runs for
axis=0/1/2and passes.Run:
pytest tests/python/relax/test_frontend_onnx.py::test_scatter \ tests/python/relax/test_frontend_onnx.py::test_scatter_broadcastFiles changed
python/tvm/relax/frontend/onnx/onnx_frontend.py—Scatter._impl_v9lowersshape-mismatched (broadcastable)
indicesas exact per-entryscatter_nd.tests/python/relax/test_frontend_onnx.py— addtest_scatter_broadcast;narrow the
test_scatterskip toScatterElements.