Skip to content

Add fused Metal cross entropy kernel - #4545

Closed
DIvkov575 wants to merge 2 commits into
ml-explore:mainfrom
DIvkov575:metal-cross-entropy-losses
Closed

DIvkov575 wants to merge 2 commits into
ml-explore:mainfrom
DIvkov575:metal-cross-entropy-losses

Conversation

@DIvkov575

Copy link
Copy Markdown

Summary

  • Add fused Metal cross-entropy forward and VJP kernels for float32, float16, and bfloat16 logits.
  • Use stable max-shifted accumulation with a looped path for large vocabularies.
  • Route the indexed, no-smoothing GPU loss path through the fused primitive.
  • Handle non-contiguous inputs and donate logits storage for gradient computation when safe.
  • Add correctness tests and forward/backward benchmark coverage.

Validation

  • macOS arm64 Metal build completed successfully.
  • Focused GPU forward/backward smoke test passed.
  • pytest was unavailable in the local environment.

CrossEntropy and CrossEntropyVJP had a fused CUDA kernel but threw
"Metal cross entropy NYI" on Metal, where use_fallback always returned
true and the op decomposed into logsumexp + take_along_axis.

Port the CUDA kernel to Metal, following the logsumexp.h idiom: a block
variant for rows up to 4096 and a looped variant above that, both
reducing with simd_max/simd_sum and a cross-simdgroup pass through
threadgroup memory. The accumulation is in float32 regardless of input
type, so half precision inputs no longer materialize a float32 copy of
the logits.

The vjp writes the gradient into the logits buffer when that buffer is
donatable. Each thread reads the target score into a register and the
threadgroup barriers before any of them overwrite the row.

Fusing also avoids a catastrophic cancellation the decomposed path
suffers: forming (max - x_target) before adding log(normalizer) keeps
the loss when the logits are large, where computing the full logsumexp
first rounds it away.

On an M2 Max, forward + backward is 2.4-6.1x faster and peak memory for
4096 x 128256 bfloat16 drops from 4.89 GiB to 1.96 GiB. Forward alone is
2.2-4.1x faster in half precision and roughly at parity in float32,
where the fallback needs no cast.

Validated against the decomposed fallback for float32/float16/bfloat16
across vocabularies spanning the block/looped boundary, degenerate rows
(+inf, fully masked), non-contiguous logits and targets, donated
buffers, and with both MLX_METAL_JIT on and off.
The use_fast gate checked mx.cuda.is_available(), so the fused primitive
was unreachable from nn.losses.cross_entropy on Metal even once the
Metal kernel existed. Both GPU backends implement it now, so gate on the
device rather than on the backend.

The fast branch does not apply the max shift the fallback branch does,
because the fused kernel shifts by the row max internally. The Metal
kernel has that property, so the existing shift invariance tests hold on
Metal: the loss stays log(n) for equal logits from 1e0 to 1e20, and
matches the cpu at every offset.

Add a test that the two devices agree. In float32 they agree exactly. In
half precision they are allowed to differ, because the fused kernel
accumulates in float32 while the fallback reduces in the dtype of the
logits, so the test asserts the stronger property instead: the fused
result is never further from the float32 answer. It is a regression
guard rather than a demonstration of this change, since routing to a
more accurate kernel cannot make an equivalence test fail.

Also correct the docstring, which said Metal reduces in the dtype of the
logits, and the dtype in the half precision example, which is the dtype
of the logits rather than float32.
@nastya236

Copy link
Copy Markdown
Collaborator

I believe this pull request is a duplication of #4520, is not it?

@nastya236 nastya236 added the await response This pull request is waiting for response from the author. label Sep 22, 2026
@DIvkov575 DIvkov575 closed this Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

await response This pull request is waiting for response from the author.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants