Repository navigation
Conversation
🔗 Helpful Links🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/23398
Note: Links to docs will display an error until the docs builds have been completed. ❌ 4 New FailuresAs of commit ca3cc1c with merge base a6171a6 ( NEW FAILURES - The following jobs have failed:
This comment was automatically generated by Dr. CI and updates every 15 minutes. |
|
Hi @zsun6! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
|
|
|
Hi @zsun6, thanks for the PR! Could you rebase and ping for review when ready? This would ensure the CI workflows focus on this PR's code changes post some fixes from our end. |
aten.gather.default, aten.scatter.src and aten.scatter.value are core ATen, so they reach the partitioner undecomposed and a graph that uses them splits at that node today. gather lowers onto the existing TakeAlongAxisNode; the input is first narrowed to the index shape on the non-gather axes, because aten reads only that block while mlx broadcasts. scatter gets a PutAlongAxisNode, appended at the end of the OpNode union, whose interpreter kernel calls mlx put_along_axis; when the index is smaller than self on the other axes it scatters into the leading index-shaped block of self and writes it back with slice_update. A src larger than the index is narrowed the same way and a scalar value becomes a 0-D constant. 8-byte element types are declined since mlx ScatterAxis has no GPU kernel for them. 32 configurations in test_ops.py (1D-4D, every axis, negative dim, index smaller than input and longer along dim, src larger than index, scalar value, fp16/bf16/int inputs, gather from a transposed view, dynamic batch run at an unseen size) match eager exactly through the C++ op_test_runner; the full MLX op suite passes 982/982. AI-Assistance: OpenAI Codex (rebase preparation and validation)
e6b6332 to
0c37c45
Compare
|
@nil-is-all Rebased onto main in 0c37c45. The gather/scatter export tests and C++ syntax checks pass. @metascroy Could you take another look and approve MLX CI? |
|
@pytorchbot label "release notes: ops & kernels" |
|
@claude review this code. Do the test cases cover corner cases or odd configs? Doe the node fbs shema / Node closely follow the MLX op's contract like other op handlers (not aten). Does the schema hand dynamism appropriately, using Vid where it could |
|
Claude finished @metascroy's task in 2m 42s —— View job
Detailed reviewPR Review: #23398SummaryThe PR lowers Code Quality
API Design
TestingThe rank, axis, negative-
RecommendationNeeds Discussion. The main point is the design question @metascroy raised: keep |
| f"aten.scatter: index must have the same non-zero rank as input, got " | ||
| f"{len(index_meta.shape)} vs {ndim}" | ||
| ) | ||
| if x_meta.dtype.itemsize == 8: |
There was a problem hiding this comment.
compare to int8/unint8 directly?
| return out | ||
|
|
||
|
|
||
| def _slice_to_shape( |
There was a problem hiding this comment.
Could this move to op_helpers.py?
main now lowers aten.gather (pytorch#24598), so this branch's gather handler and gather test are dropped in favor of main's. PutAlongAxisNode goes after main's ScatterNode in the OpNode union.
- PutAlongAxisNode is now a plain mlx::put_along_axis call. The scatter handler narrows self with SliceNode, scatters into the block and writes it back with SliceUpdateNode. - Slice stops come from SymSizeNode Vids when the index size is symbolic (emit_leading_block in op_helpers.py). Mismatched symbols and a symbolic index against a static input now lower. - main's aten.gather handler uses emit_leading_block too, so it also lowers an index smaller than the input on a dynamic dim. - A SymInt scatter value (from .item()) goes through FullNode. - int64 is checked by dtype instead of itemsize. - PutAlongAxisNode clamps indices like main's ScatterNode and TakeAlongAxisNode, and returns x when the axis is empty. - Tests: gather_shape (12) for shapes main's gather test does not cover, scatter (29) with size-1 and dynamic axes, different symbols, bool, zero indices, .item() and out-of-range indices, node counts checked; partitioner tests for int64 and 0-D scatter falling back to CPU. AI-assisted (OpenAI Codex, Claude); reviewed and tested by the author.
main lowers aten.gather since pytorch#24598, so this restores its gather handler as on main and drops the gather_shape op test. The scatter handler, PutAlongAxisNode with clamped indices, emit_leading_block (used by scatter), the scatter op tests and the scatter decline tests stay. AI-assisted (OpenAI Codex, Claude); reviewed and tested by the author.
metascroy
left a comment
There was a problem hiding this comment.
Thanks for the PR! The scatter lowering looks right to me. Narrowing self and src to the index block and then writing back with SliceUpdateNode handles the MLX broadcast mismatch, and the tests cover it well. Two things to fix before landing are inline.
| f"aten.scatter: index must have the same non-zero rank as input, got " | ||
| f"{len(index_meta.shape)} vs {ndim}" | ||
| ) | ||
| if x_meta.dtype == torch.int64: |
There was a problem hiding this comment.
Please check x_meta.dtype.itemsize == 8 here, the same way _scatter_add_handler does. ScatterAxis::eval_gpu rejects every 8-byte dtype, not only int64 (mlx/backend/metal/indexing.cpp), so matching int64 exactly depends on the delegate never receiving float64 or another 8-byte type, and that could change.
| raise ValueError("aten.scatter requires src shape metadata") | ||
| src, _ = emit_leading_block(P, src, src_meta.shape, index, index_meta.shape) | ||
| else: | ||
| src = emit_lifted_constant(P, src, x_meta.dtype) |
There was a problem hiding this comment.
emit_lifted_constant caches constants under f"_scalar_{value}", and the dtype isn't part of the key. With the new float-into-int32 case (value=2.5, dtype=torch.int32), this lifts torch.tensor(2.5, dtype=torch.int32), which is 2, and caches it as _scalar_2.5. If a later op in the same graph lifts a float 2.5 (say y * 2.5), it gets the cached int tensor back and silently computes with 2.
The problem is in the helper, but this is the first handler that intentionally lifts a float value into an int dtype. Two ways to fix it:
- include the dtype in the cache key in
emit_lifted_constant, or - lift the scalar in its own Python type here, since
mlx::put_along_axisalreadyastypes values toa.dtype().
A test with a second float lift of the same value in the graph would cover this.
aten.gather.default,aten.scatter.srcandaten.scatter.valuehad no MLX handler. They're core ATen soto_edgedoesn't decompose them, and any graph with one of them splits at that node: the op runs on the portable CPU kernel with a delegate handoff on each side.Subgraphs from this repo, before/after (
to_edge_transform_and_lowerwithMLXPartitioner):rel_shift(examples/models/sortformer/export_sortformer.py:164, one per attention layer), x (1,8,64,127)gatheron CPUsampling_probabilities(examples/models/muse-glimmer/model/dflash_token_sampler.py:63), logits (4,512)scatter.src,sort.stableon CPUsort.stableon CPUverify_speculative(same file, R=3, V=512)gather,scatter.src,scatter.value,sort.stable, 2xsumon CPUsort.stable, 2xsumon CPUexamples/models/llama/llama_transformer.py:156(scores.gather(1, idx)) and HF gpt_oss / qwen3_vl_moe (zeros_like(logits).scatter_(1, idx, w)), (16,256) -> 8 expertsgather,scatter.srcon CPU(
sort.stableisn't core ATen and needs_core_aten_ops_exception_list;sumover bool is a separate gap. Both out of scope here.)How
gatherlowers onto the existingTakeAlongAxisNode. aten allowsindex.size(d) <= self.size(d)on the non-gather axes and only reads that leading block, whilemlx::take_along_axisbroadcasts, so the handler first narrowsselfto the index shape on those axes withSliceNodes (nothing is emitted when the sizes already agree, including a shared symbolic dim; a static index size is sliced to even if the input dim is symbolic). The output has the index shape, so the index can be longer thanselfalongdimfor free.scatteradds aPutAlongAxisNode(appended at the end of theOpNodeunion, so existing.ptefiles still load) and anexec_put_along_axisthat callsmlx::put_along_axis. Two aten freedoms need handling:srcmay be larger thanindexon any axis (only its leadingindex.shapeblock is read), which the handler narrows like gather; andindexmay be smaller thanselfon the non-scatter axes, which the kernel handles by scattering into the leading index-shaped block ofselfand writing it back withslice_update— broadcasting there would write every row instead of the firstindex.size(d). A scalarvaluebecomes a 0-D constant that MLX broadcasts, soscatter.valueshares the node.int64/float64inputs are declined at partition time becauseScatterAxis::eval_gpuhas no kernel for 8-byte element types. Duplicate indices alongdimare unspecified in aten; MLX keeps one of the writes. Indices aren't bounds-checked in the delegate, same as the existingtake_along_axis/scatter_addlowerings.Tests
Adds
gatherandscattertobackends/mlx/test/test_ops.py, 32 configs: 1D-4D, every axis and negativedim, index smaller than the input on the other axes and longer alongdim,srclarger thanindex, scalarvalue(including a float written into an int32self), fp16/bf16/int64 inputs with int64 indices, a gather from a transposed view, and a dynamic batch dim (shared by the operands, and on the input alone against a static index) exported at one size and run at another. Both ops are pure data movement, so the comparison is exact (rtol = atol = 0).M3 Pro, macOS 14.1, Xcode 15.3, torch 2.14.0, MLX 1f8e74e3. Only
schema.fbsis committed; the generated files come from the build, as in #23051.Written with AI assistance (Claude Code). I reviewed the design and the diff, ran the full MLX op suite locally and take responsibility for the change.
cc @nil-is-all @metascroy