Conversation
Cohere2 blocks end with `(attn + ff) + x`, which MLX runs as two dependent elementwise kernels, each one more barrier level on the decode critical path. `compiled_add3` compiles the same expression, with the same association order, into one kernel, so the result is byte-identical and every layer drops one dispatch and one barrier. Validated on c4ai-command-r7b-12-2024 4-bit on M1 Ultra: greedy generations for three prompts at 200 tokens match main exactly, and eight interleaved ABBA pairs (500-token prompt, 128 tokens, command-buffer input budget pinned on both arms) read decode median 110.54 vs 109.35 tok/s, +1.1%.
A Cohere2 block ends in `(attn + mlp) + x` and the next block starts with a LayerNorm of that sum, so every layer boundary on the decode critical path is two dependent dispatches, a compiled add and MLX's `layer_norm_single_row`. `fused_add3_layer_norm` is one Metal kernel that writes the residual and its normalization. It copies the pinned MLX kernel's threadgroup size, read pattern, two-stage reductions, `precise::rsqrt` and affine step (bias read from memory, as `fast::layer_norm` passes it), so both outputs are byte-identical to the unfused pair rather than close to it. `layers::residual_add3_layer_norm` falls back to the pair off Metal, above a 6656-wide row, or on mixed dtypes, and `MLXCEL_FUSED_ADD_NORM=0` forces it. The Cohere2 layer loop now fuses block i's add with block i+1's input norm, and the last block's add with the final norm. A unit test asserts exact equality across f16 and bf16, with and without bias, several rows and a width off the 8-read boundary; perturbing the kernel's rsqrt by 0.1% makes it fail. Greedy generations for three prompts at 200 tokens match main. command-r7b 4-bit on M1 Ultra, eight ABBA pairs in one binary: decode 112.45 to 113.72 tok/s (+1.1%), prefill unchanged.
This branch has not been deployed
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.
Summary
A Cohere2 block ends in
(attn + mlp) + xand the next block starts with a LayerNorm of that sum, so on the decode critical path every layer boundary is three dependent kernels: two adds and MLX'slayer_norm_single_row. Two commits remove two of them, both byte-identical to the current output:compiled_add3runs the two adds as one compiled kernel with the same association order.fused_add3_layer_normis one Metal kernel that writes the residual and its LayerNorm. It copies the pinned MLX single-row kernel (threadgroup size, 8 reads per thread, two-stage simd and threadgroup reductions,precise::rsqrt, the affine step with the bias read from memory asfast::layer_normpasses it), so both outputs match the unfused pair bit for bit.layers::residual_add3_layer_normfalls back to the pair off Metal, above a 6656-wide row, or on mixed dtypes, andMLXCEL_FUSED_ADD_NORM=0forces the fallback. The Cohere2 layer loop fuses block i's add with block i+1's input norm, and the last block's add with the final norm.Measurements
command-r7b 4-bit on M1 Ultra, 500-token prompt, 128 tokens, eight interleaved ABBA pairs, measured with #1947 applied:
compiled_add3(budget pinned on both arms)MLXCEL_FUSED_ADD_NORM=0vs unset)Prefill is unchanged.
Validation
compiled_add3followed byLayerNorm::forwardacross f16 and bf16, with and without a bias, several rows, and a width that is not a multiple of 8. Multiplying the kernel's rsqrt by 1.001 makes it fail, which shows the fused path runs and the test discriminates.make verify-test, 11,412 passed), clippy and fmt pass; on this branch, clippy, fmt and the new test.Two
mx.fast.metal_kerneldetails worth knowing: a 0-d input is passed to the kernel as a scalar rather than a pointer, and a small input can land in the constant address space, so the kernel takes the missing bias as a one-element array and usesautopointers. Upstream's kernel starts one tail loop at a negative index for threads past the end of a narrow row; the copy clamps the start to 0, which leaves the result unchanged.