Skip to content

Support 8-byte dtypes in assignment scatter on Metal - #4328

Open
erwinzhang7 wants to merge 1 commit into
ml-explore:mainfrom
erwinzhang7:scatter-64bit-none
Open

Support 8-byte dtypes in assignment scatter on Metal#4328
erwinzhang7 wants to merge 1 commit into
ml-explore:mainfrom
erwinzhang7:scatter-64bit-none

Conversation

@erwinzhang7

Copy link
Copy Markdown
Contributor

Proposed changes

Root cause identified: fixes #4300, supersedes the two closed workarounds, #4301 (eye) and #4309 (diag)

mx.eye and mx.diag fail on the GPU for int64, uint64 and complex64
because Metal scatter rejects every 8-byte output type. Both build their result
with an assignment scatter, so fixing the scatter fixes them, and put_along_axis
and anything else assigning 8-byte data with it.

Why they were rejected. Every scatter writes through
device mlx_atomic<T>* out. For a type with no native Metal atomic,
mlx_atomic<T> packs several values into a single atomic<uint>, which cannot
represent an 8-byte value, so Scatter::eval_gpu and the scatter op refused
them outright.

That is real for the reducing scatters and was never real for assignment.
Metal has no 8-byte atomic, so Sum, Prod, Max and Min do need
read-modify-write. Scattering duplicate indices under Scatter::None is
a race and the result is whichever thread writes last; a plain store has those
semantics.

The change. The output pointer type is chosen by the op: mlx_atomic<T> for
the reducing scatters, plain T for assignment. None gains a non-atomic
overload, scatter_impl deduces the pointer type, scatter_axis takes it as a
template parameter, and the JIT signature takes it as a placeholder. Three type
gates now reject only the reducing case: Scatter::eval_gpu,
ScatterAxis::eval_gpu, and the one in ops.cpp carrying
// TODO, remove when scatter supports 64-bit outputs. Kernel names already
encode the op, so the two pointer forms cannot collide in the library cache.

CUDA and CPU are unaffected; both already handled these dtypes, which is why the
old gate was GPU-only. complex64 + Sum keeps its existing path.

Verified

macOS 26.2, M5 Max. mx.eye and mx.diag for int64, uint64 and complex64
on both streams. Assignment scatter against the CPU across those three plus seven
control dtypes over four shapes. Duplicate indices land a valid candidate and
leave other slots untouched. Reducing scatter still raises for int64 and
uint64.

test_put_along_axis asserted the old limitation and now checks the result on
both streams. test_scatter_8_byte_types covers the reported ops and keeps the
invariant that a reducing scatter raises rather than returning something
plausible. Its indices are unique on purpose: assignment with a repeated index is
a race by definition, so the GPU and CPU need not agree.

Full python suite 839 passed, 11,814 subtests, 5 skipped.

Metal scatter rejected every 8-byte output type, so mx.eye and mx.diag both
failed on the GPU for int64, uint64 and complex64.

The cause is that all scatters write through mlx_atomic<T>. For a type with no
native Metal atomic that falls back to packing several values into one
atomic<uint>, which cannot represent an 8-byte value at all, so the dispatch
refused them outright.

Only the reducing scatters need read-modify-write. Assignment is a plain store:
scattering duplicate indices under Scatter::None is already a race whose result
is whichever thread writes last, and a plain store has exactly those semantics.
So the output pointer is now chosen by the op — mlx_atomic<T> for the reducing
scatters, plain T for assignment — and the two type gates only reject the
reducing case. Kernel names already encode the op, so the two forms cannot
collide in the library cache.

This fixes eye and diag as a consequence rather than as a special case, and it
covers put_along_axis and every other 8-byte assignment scatter with it.

test_put_along_axis asserted the old limitation and now verifies the result on
both streams. test_scatter_8_byte_types covers the reported cases, checks
assignment against the CPU with unique indices, and keeps the invariant that a
reducing scatter still raises rather than returning something plausible.

Indices are unique in the new test on purpose: assignment with a repeated index
is a race by definition, so the GPU and the CPU are both correct and need not
agree.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Metal scatter does not support 64bit dtype

1 participant