feat(prezero): wire split-K GEMM prezero into Kimi MLA/MoE decode via a torch.compile pass#1490
feat(prezero): wire split-K GEMM prezero into Kimi MLA/MoE decode via a torch.compile pass#1490ColorsWind wants to merge 1 commit into
Conversation
…idate flag - pass -> atom/models/deepseek_v2_prezero.py (drop aiter/compile; layering) - single ATOM_ENABLE_PREZERO flag (drop dead ATOM_ENABLE_SPLITK_PREZERO) - per-call local stats in the pass (was a leaky module global)
|
i prefer reject this one, as prezero is not must for our splitk gemm |
|
Hi @valarLip, We understand your concern, but we would appreciate the chance to discuss whether there is a reasonable path forward, as this PR provides meaningful performance value based on our measurements. In our Kimi-K2.5-MXFP4 tests, this PR gives +1–3.7% decode throughput by moving the per-GEMM zero-init overhead into the preceding AR-RMSNorm kernel, which has idle CUs during decode and can absorb the output-buffer zeroing at nearly zero cost. More specifically:
The change is fully gated behind Please let us know if there are any concerns we may have missed. We are happy to keep improving our implementation on this. Thanks! |
Split-K GEMM prezero as a torch.compile pass
Implements the split-K GEMM prezero decode optimization from #1421 as a
torch.compile pass instead of hand-wiring it into the model. Output is bit-identical, the
kernel speedup is the same, and it covers one more consumer (o_proj). Off by default
(
ATOM_ENABLE_PREZERO=1).What prezero does
A split-K GEMM zero-inits its output before atomic-accumulating into it — a device-wide
sync worth ~2–4 µs/layer in decode. Prezero drops that in-kernel zeroing and hands it to
the preceding fused AllReduce+RMSNorm, which has idle CUs in decode; the GEMM then runs
zero_init=Falseinto the pre-zeroed buffer.is_prezero_freedecides when the fill fitsthe AR's spare CUs.
Why a compile pass
#1421 threads prezero buffers by hand through
attention_mla.py,base_attention.py,paged_attention.pyand the block forward. This PR does the same rewrite on the fx graphand leaves the model forward alone.
How the pass works
Runs on the full pre-split dynamo graph — the only point where the attention op is a single
node, so its internal buffer slots are reachable. Donor-driven: iterate the AR donors, walk
forward to the GEMM.
fused_allreduce_rmsnorm_, read its layer + kind (input vs post-attn) from thenorm weight, then follow
getitem[0] → gemm_a16w16to the consumer (qkv_a / gate).allocate one
[m, n_total]buffer, slicedqkv_a=[0, n_qkv_a),q_b=[n_qkv_a, n_base),o_proj=[n_base, n_total), withn_base = n_qkv_a + n_q_b.ar_rmsnorm_maybe_prezero_(…, n_total, n_base)(zeros the whole bufferwhen free, else just the
n_baseprefix — o_proj self-zeros); qkv_a GEMM →mm_maybe_prezero_(qkv_a slice, …, n_base); pass the q_b / o_proj slices into theattention op's
qb_prezero(arg 8) /oproj_prezero(arg 9).The gate (
is_prezero_free) lives in the ops, so it re-runs per cudagraph batch sizeinstead of being frozen at compile time.
Not general yet: it keys on the DeepSeek-V2 / Kimi layout (these donors, the
qkv_a | q_b | o_proj/ gate consumers, the attention op's slots), hence the model-specificdeepseek_v2_prezero.py. Other models are future work.Coverage
mm_maybe_prezero_qb_prezeroslotoproj_prezeroslotq_b / o_proj are best-effort — they no-op when the tuner picks a non-split-K kernel or
is_prezero_freerejects the wide buffer.Changes
atom/models/deepseek_v2_prezero.py(new) — the pass.atom/utils/backends.py— run it beforesplit_graph.atom/model_ops/prezero.py— the ops (mm_maybe_prezero_,ar_rmsnorm_maybe_prezero_,prezero_active).atom/model_ops/{attention_mla,base_attention}.py—qb_prezero/oproj_prezeroslotson the attention op. q_b and o_proj are eager GEMMs inside the opaque op, so no graph pass
can reach them; a small hook there consumes the threaded buffer.
rf-prezero2) —is_prezero_free, the ARzero_fill=path, the GEMMout=/zero_init=Falsepath.The
deepseek_v2model forward is otherwise untouched.Results
Kimi-K2.5-MXFP4, TP=4, ISL/OSL 8192/1024. Full breakdown:
prezero_pr_data.md.Serving throughput (per-GPU, ON vs OFF):
Decode kernel Δ (ON − OFF, µs/layer; negative = saved):
gsm8k exact_match (strict), 5-shot, full 1319-question set:
Enabling
ATOM_ENABLE_PREZERO=1, off by default.prezero_compare.py