Repository navigation
Conversation
`fused_norm_rope_attn_rope_cast_decode` schedules one cluster per query
token, so a small decode batch leaves the GPU mostly idle: on one B200 the
kernel costs the same 26.6 us at s_q=1, 6 and 96 (topk 128+512, h_q=64).
The header explains why there is no split-KV -- "the fused RoPE + FP8-quant
epilogue cannot be combined across splits".
This adds an opt-in partial-output mode so the epilogue can move to a
combine kernel:
* `mega_num_splits` extends the job space from (query token) to
(query token, KV split); each job walks only its own KV block range and
the grid grows accordingly. A split with no blocks of its own re-runs the
last block with `contributes=false` so the pipeline and its barriers stay
uniform, and its result is discarded.
* When `mega_o_accum` / `mega_lse_accum` are provided, `store_o` writes an
fp32 partial normalised by that split's l_i plus a log2-space LSE, and
skips the FP8 cast. This is the protocol `smxx/decode/combine` already
consumes, including leaving the attention sink to the combine.
* The inverse RoPE stays in the main kernel: it is linear and every split
shares the query position, so it commutes with the cross-split weighted
sum.
Unsplit behaviour is unchanged -- without the partial buffers the fused FP8
epilogue runs exactly as before.
Measured on one B200, h_q=64, topk 128+512, CUDA graphs on, us:
s_q unsplit split2 split4 split8 split16
1 26.6 20.5 16.4 14.4 14.3
6 26.7 22.5 18.4 16.4 14.4
16 28.7 22.5 18.5 18.4 26.4
96 30.7 42.9 47.1 68.4 117.6
so the caller must stop splitting once the batch fills the GPU.
Limited to h_q == 64 (CLUSTER_SIZE == 1) for now; the API rejects the
partial buffers otherwise.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gcanlin
force-pushed
the
perf/mega-decode-splitkv
branch
from
September 19, 2026 18:54
0a5744d to
00e9393
Compare
gcanlin
added a commit
to gcanlin/vllm
that referenced
this pull request
Sep 20, 2026
The FlashMLA mega decode kernel runs one cluster per query token, so a small decode batch leaves most of the GPU idle -- on one B200 the kernel costs the same 26.6 us at 1, 6 and 96 query tokens. vllm-project/FlashMLA#25 adds an fp32 partial-output mode to that kernel so the KV range can be split across CTAs; this is the vLLM side of it. When enabled, `_forward_decode_mega` hands the kernel per-split partial buffers and merges them with a Triton combine that applies the attention sink once and runs the FP8 epilogue (per-32 ue8m0 scales, permuted wv-group layout). Off by default and capability-gated: if the linked FlashMLA predates the partial-output arguments the decode path is unchanged and a warning is logged, so this is a no-op until the pin is bumped. VLLM_DSV41_MEGA_SPLITKV max splits (0/1 disables, default 0) VLLM_DSV41_MEGA_SPLITKV_MAX_TOKENS only split at or below this token count (default 8) The token cap is load-bearing. The partials cost num_splits * s_q * h_q * d_v * 4 bytes per layer per step: 1.05 MB/layer at one decode token with 8 splits, but 16.8 MB/layer at 16 tokens, which saturates HBM and regressed that lane ~35% end to end even though a single-layer microbenchmark still showed a win there. DeepSeek-V4.1-Flash, TP4 + EP + MegaMoE + SP, FLASHMLA_MEGA_ATTN_DSV41, vllm bench serve random 4096/256, 3 seeds per lane, cold server each: lane metric off on delta C1 out tok/s 120.12/121.67/121.72 130.69/132.55/132.55 +8.9% C1 TPOT ms 7.77/7.78/7.78 7.11/7.10/7.10 -8.7% C16 out tok/s 1301.84/1304.29/1299.16 1299.77/1290.00/1290.72 noise Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Purpose
fused_norm_rope_attn_rope_cast_decodeschedules one cluster per query token, so a small decode batch leaves most of the GPU idle. On one B200 (h_q=64, topk 128+512, CUDA graphs) the kernel costs the same 26.6 us at s_q = 1, 6 and 96 — 96x the work for free. The kernel header explains why there is no split-KV today:This PR adds an opt-in fp32 partial-output mode so the epilogue can move to a combine kernel, and with it a KV split.
What changes
mega_num_splitsextends the job space from (query token) to (query token, KV split). Each job walks only its own KV block range; the grid grows accordingly. A split with no blocks of its own re-runs the last block withcontributes=falseso the pipeline and its barriers stay uniform, and its result is discarded.mega_o_accum/mega_lse_accumare supplied,store_owrites an fp32 partial normalised by that split'sl_iplus a log2-space LSE, and skips the FP8 cast. This is exactly the protocolsmxx/decode/combinealready consumes — including leaving the attention sink to the combine.Measured
Kernel alone, one B200, h_q=64, topk 128+512, CUDA graphs, us:
End to end through vLLM (DeepSeek-V4.1-Flash, TP4 + EP + MegaMoE + SP,
FLASHMLA_MEGA_ATTN_DSV41,vllm bench serve, random 4096/256, 3 seeds per lane, cold server per configuration), with a combine kernel on the caller side and splitting gated to <= 8 decode tokens:Callers must gate this tightly. The partials cost
num_splits * s_q * h_q * d_v * 4bytes per layer per step: 1.05 MB/layer at one decode token with 8 splits, but 16.8 MB/layer at 16 tokens. Splitting at 16 tokens regressed that lane by ~35% end to end even though the kernel microbenchmark above still shows a win there — the microbenchmark times one layer in isolation and cannot see the aggregate HBM pressure.Test plan
The new test's oracle is the kernel itself at
mega_num_splits=1, which takes the same fp32-partial path over the whole KV range — that isolates the split logic from the FP8 epilogue, so a real regression appears far below quantisation noise. It covers s_q in {1, 6, 16} x splits in {2, 4, 8} x sink {off, on}, plus a degenerate case with more splits than KV blocks, plus a guard that the unsplit path still produces FP8.Against the unsplit FP8 path: LSE max abs error 9.5e-07, and the FP8 codes are 99.6% bit-identical with non-cancelling KV values. With
randnKV they are 91-94% identical; that is not a logic error but fp32 re-association showing through —|O|is ~20x smaller there because the V terms cancel, while the absolute error stays ~6e-4, and the LSE (which sums every token's contribution) stays exact at every split count.tests/test_fused_norm_rope_attn_rope_cast.pyneedstile_kernels/tilelang/kernelkit, which I could not install in this environment; the new test deliberately depends only ontorchso it runs wherever the extension builds.Limitations / follow-ups
h_q == 64(CLUSTER_SIZE == 1) only. The API rejects the partial buffers otherwise.smxx/decode/combinewith PDL, which should also widen the end-to-end gain — that combine is currently launch-bound and does not overlap the main kernel's tail the way the existing one does.Duplicate-work check
Searched open PRs and issues on this repo for split-KV / partial-output work on the fused mega decode kernel and found none. The non-fused
sm100/decode/sparse/head64path already ships split and no-split instantiations; this brings the same capability to the fused kernel and reuses its partial/LSE protocol rather than inventing a new one.AI assistance
This change was developed with AI assistance (Claude Code). The submitter has reviewed every changed line.
🤖 Generated with Claude Code