M11: Fused MLX transformer kernels - #21
Merged
Merged
Conversation
Wires MLX's handwritten mx::fast::* kernels (RMSNorm, LayerNorm, RoPE,
scaled-dot-product attention) into Emily as defn-callable helpers, and
ships a Bumblebee shim that swaps them in for the stock composed-defn
implementations. Closes the fusion gap M10.5 noted: the two-kernel
dequantize+dot cost on quantized matmuls, plus the ~5-dispatch
attention chain, collapse to one kernel dispatch each where the fused
kernel applies.
Mechanism: Emily.Fast.* helpers emit Nx.Defn.Expr.optional/3 nodes
whose op name matches a custom callback on Emily.Backend. At eval
time Nx.Defn.Evaluator calls the backend-exported function directly;
when the backend doesn't export it (BinaryBackend, EXLA) the defn
fallback composition runs, so conformance oracles still work.
Rejected the pattern-matched subgraph-fusion approach (PLAN.md M11
defers it — compiler-level work).
- Native NIFs (c_src/ops/fast.cpp) over mx::fast::rms_norm,
mx::fast::layer_norm, mx::fast::rope, and
mx::fast::scaled_dot_product_attention. Nullable weight / bias /
freqs arguments marshal via std::optional; SDPA mask arrays via
std::vector<fine::ResourcePtr<Tensor>>.
- Emily.Fast (lib/emily/fast.ex) — rms_norm/3, layer_norm/4, rope/3,
rope_with_freqs/4, scaled_dot_product_attention/4 and the _with_mask
variant. Each opt-arg contract follows Nx.Defn.Expr.optional/3's
split-at-first-list convention (all tensor inputs first, one trailing
keyword list).
- Emily.Backend.fast_* — six custom callbacks (outside the Nx.Backend
behaviour) that unwrap refs and call Native directly.
- Emily.Bumblebee.FastKernels (test/support/) — Axon graph rewriter
mirroring the M10.5 Emily.Quantization.Transform pattern. Rewrites
:rms_norm and :layer_norm via Axon.map_nodes,
Bumblebee.Layers.apply_rotary_embedding/5 by MFA match, and
coalesces attention_weights_impl + attention_output_impl into one
fused SDPA layer via Axon.rewrite_nodes. RoPE supports all four
Bumblebee scaling strategies (:linear, :dynamic, :longrope, :llama3)
by precomputing the inverse-frequency table Elixir-side and passing
it to MLX via the freqs-override overload. Lives under test/support/
because Bumblebee + Axon are only: :test.
Scope & trade-offs:
- Attention rewrite leaves the unfused attention_weights_impl node in
the graph because it's referenced from Bumblebee's {output, weights}
tuple. The new fused layer consumes raw Q/K/V/mask bypassing it; if
output_attentions is off, dead-code elimination drops the orphan.
For output_attentions=true the un-fused weights still compute. Not
a regression vs M10.5; the common inference path wins.
- head_mask fusion is approximate: applies the mask to per-head
outputs post-attention (equivalent to a 0/1 mask but diverges on
fractional values). Bumblebee's built-in usage is 0/1 only.
- Non-default channel_index on norm layers skipped (vision-CNN heads;
no transformer hits this path).
Tests:
- test/emily/fast/{rms_norm,layer_norm,rope,sdpa}_test.exs — 16 cases
covering native unit vectors, defn-composability inside a jitted
function, and fused-vs-composed equivalence for f32 + bf16.
- test/emily/bumblebee/fast_kernels_test.exs — 3 shim unit tests on
handcrafted Axon models; asserts rewrites land, init_fn succeeds,
and predict output matches the unrewritten path within tolerance.
- :fast_kernels_full tagged variants of every existing *_full
conformance suite (Qwen3 dense, Qwen3 quantized, ViT, Whisper) plus
a tiny-random DistilBERT smoke. Tag excluded by default like the
other *_full tags; opt in with `mix test --only fast_kernels_full`.
Bench: bench/qwen3_tokens_per_sec.exs gains EMILY_BENCH_FAST_KERNELS=1
for baseline-vs-fused side-by-side reporting, and EMILY_BENCH_PIN=<n>
for a hard speedup-multiplier floor that exits non-zero on miss.
mix precommit: 349 tests, 0 failures, credo clean.
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
Wires MLX's handwritten
mx::fast::*kernels (RMSNorm, LayerNorm, RoPE, scaled-dot-product attention) into Emily asdefn-callable helpers, and ships a Bumblebee shim that swaps them in for the stock composed-defn implementations. Closes the fusion gap M10.5 noted: the two-kerneldequantize + dotcost on quantized matmuls, plus the ~5-dispatch attention chain, collapse to one kernel dispatch each.c_src/ops/fast.cpp) overmx::fast::rms_norm,mx::fast::layer_norm,mx::fast::rope, andmx::fast::scaled_dot_product_attention. Nullable weight/bias/freqs args marshal viastd::optional.Emily.Fast(lib/emily/fast.ex) —rms_norm/3,layer_norm/4,rope/3,rope_with_freqs/4,scaled_dot_product_attention/4and the_with_maskvariant. Each emits aNx.Defn.Expr.optional/3node whose op name matches a custom callback onEmily.Backend; the Evaluator dispatches to the fused kernel under Emily and falls back to a defn composition on any other backend (BinaryBackend / EXLA conformance stays green without a re-plumb).Emily.Backend.fast_*— six custom callbacks (outside theNx.Backendbehaviour) that unwrap refs and call Native directly.Emily.Bumblebee.FastKernels(test/support/) — Axon graph rewriter mirroring the M10.5Emily.Quantization.Transformpattern. Rewrites:rms_normand:layer_normviaAxon.map_nodes,Bumblebee.Layers.apply_rotary_embedding/5by MFA match, and coalescesattention_weights_impl + attention_output_implinto one fused SDPA layer viaAxon.rewrite_nodes. RoPE supports all four Bumblebee scaling strategies (:linear,:dynamic,:longrope,:llama3) by precomputing the inverse-frequency table at rewrite time and passing it to MLX via thefreqs-override overload — no per-token BEAM-side recomputation.Mechanism:
Nx.Defn.Expr.optional/3is Nx's documented extension point for vendor-fused kernels (the pattern EXLA uses too). Rejected the pattern-matched subgraph-fusion approach — PLAN.md M11 explicitly defers that as compiler-level work.Scope & trade-offs
attention_weights_implnode in the graph because it's referenced from Bumblebee's{output, weights}tuple. On the generation path (output_attentions=false),Nx.Defn.Evaluator.precompilewalks IDs only from the returned expression — unreachable nodes never execute, so the fused SDPA is a pure win. Foroutput_attentions=truethe orphan still runs; accepted.head_maskfusion is approximate: applies the mask to per-head outputs post-attention. Equivalent to a 0/1 mask; fractional values diverge slightly. Bumblebee's built-in usage is 0/1 only.channel_index != -1on norm layers skipped (vision-CNN heads; no transformer hits it).Test plan
mix precommit— 349 tests, 0 failures, credo cleanmix test --only conformance— 17 tiny-random conformance tests greenmix test --only fast_kernels_full test/emily/conformance/distilbert_test.exs— tiny-random DistilBERT with fused kernels matches the dense-path pinned slicemix test --only fast_kernels_full— full-model:fast_kernels_fullvariants (Qwen3 dense, Qwen3 quantized, ViT, Whisper) on a machine with the checkpoints cachedEMILY_BENCH_FAST_KERNELS=1 mix run bench/qwen3_tokens_per_sec.exs— verify tokens/sec speedup on M3 hardware; tighten theEMILY_BENCH_PIN=<multiplier>floor after first measurementNew test surface
test/emily/fast/{rms_norm,layer_norm,rope,sdpa}_test.exs— 16 cases across native unit vectors, defn-composability inside jitted functions, and fused-vs-composed equivalence for f32 + bf16test/emily/bumblebee/fast_kernels_test.exs— 3 shim unit tests on handcrafted Axon models; asserts rewrites land,init_fnsucceeds, and predict output matches the unrewritten path within tolerance:fast_kernels_fulltagged variants of every existing*_fullconformance suite plus a tiny-random DistilBERT smoke. Tag excluded by default like the other*_fulltagsFull details in PLAN.md §M11.