Repository navigation
[Perf][Kernel][DSv4.1] Swap-AB CuTe DSL decode attention for small TP2/TP4 steps - #62633
Conversation
…2/TP4 steps FlashMLA's mega-attention decode kernel takes 64 or 128 query heads. At TP2 and TP4 a rank holds 32 and 16 live heads of the 64-head model, so on small decode, verify and draft steps half or three quarters of the kernel's MMA work goes to padding heads. Add SwapABDecodeKernel, a CuTe DSL kernel adapted from SGLang's swap-AB DSv4 decode attention (Apache-2.0), in vllm/models/deepseek_v41/nvidia/ops/swapab_decode_attn.py. It puts the KV slots on the tcgen05 MMA's M side and the live heads on N. One CTA takes a 64-slot tile of a token's SWA list or a 128-slot tile of its compressed list: it gathers and dequantizes the V4.1 records (fp8 528 B, NVFP4 288 B) into SMEM, RoPEs Q and computes S = KV Q^T and O = KV^T P, with P split into bf16 hi and lo parts. A token's tiles form one thread-block cluster that merges the partials and the attention sink over DSMEM in a fixed order, applies the inverse RoPE and writes the MXFP8 output the mega kernel writes, for the live heads' groups only. It is a single PDL launch with no workspace and no host sync, CUDA-graph safe and deterministic. _forward_decode_mega takes the kernel automatically, with no new flag, when all of these hold: the GPU is SM10x; the layer has a compressed cache and 16 or 32 live heads; a token's tiles fit one portable cluster (ceil(window / 64) + ceil(index_topk / 128) <= 8); the CuTe DSL imports and is built with CUDA 13.2+, as the direct fp8 / fp4 -> bf16x2 conversions need PTX ISA 9.2; VLLM_BATCH_INVARIANT is off; the step has at most 16 decode tokens; and its SWA list is the window, which DSpark's non-causal draft steps widen. Every other step, layer and TP size keeps the mega kernel; prefill and other platforms are unchanged. The compile key holds the compressed page size, which is final only once the KV cache is allocated. So kv-source layers register it in bind_kv_cache, which runs while the runner's JIT warmup registry is active, and kernel_warmup compiles it before CUDA graph capture. The output is not bit-identical to the mega kernel's, whose fp32 sums are ordered differently. Its error against an fp64 reference is held to at most 1.1x the mega kernel's. tests/kernels/test_dsv41_swapab_decode.py checks the kernel against an fp64 reference and the mega kernel on caches written by V4.1's insert kernels; checks that padding heads' output is untouched and that eager calls and a CUDA graph replay agree bit for bit; and checks the dispatch rule and that the warmup key is the launched key. benchmarks/kernels/benchmark_dsv41_mega_attn.py gains a swap-AB arm. Signed-off-by: bofengl <bofengl@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve the import conflict with vllm-project#60118 in flash_mla_mega_attn.py: keep main's `cdiv` import next to this PR's `has_cutedsl` import. Signed-off-by: bofengl <bofengl@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
👋 Hi! Thank you for contributing to the vLLM project. 💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment Once the PR is approved or has the If you have any questions, please reach out to us on Slack at https://slack.vllm.ai. Agent GuidelinesIMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban. 🚀 |
LopezCastroRoberto
left a comment
There was a problem hiding this comment.
Could you share some e2e numbers (TPOT at TP2/TP4) and accuracy evals?
|
Will do. Planned data:
|
TPOT and accuracy, as promised. Setup. B300 (8-GPU node), vLLM nightly image at commit 00b6c4c; arm A is that commit stock, arm B is this PR's runtime hunks applied on top (they apply cleanly). Per TP config the two arms ran back to back on the same node. Server: Median TPOT (ms), stock → PR:
Inside the gate the win shrinks as concurrency grows, as expected (attention is a shrinking share of the step). At c64 both arms run only the mega kernel; the +1.1–1.6% there reads as run noise — two reruns of the PR arm's TP2 c64 on another node gave 16.81/16.83 ms with clean means, and I'm happy to pin it down further if you'd like. E2E TPOT gains are, as expected, much smaller than the 1.6–2.15x per-layer attention-step numbers in the description. GSM8K (lm-eval
Both deltas are within one standard error and within this model's run-to-run spread on GSM8K (e.g. the |
| @@ -206,6 +250,25 @@ def packed_kv_cache_dtype(self) -> CacheDType: | |||
| # it is what an unspecific --kv-cache-dtype resolves to here. | |||
| return "nvfp4_ds_mla" | |||
|
|
|||
| def bind_kv_cache(self, kv_cache: torch.Tensor) -> None: | |||
There was a problem hiding this comment.
don't think this is the best place to do the warmup
There was a problem hiding this comment.
Agreed, moved in 0300f89. The key is now registered in __init__ (_register_swapab_warmup), next to the layer's other register_warmup calls, and the bind_kv_cache override is gone.
I had deferred it because I took the compressed page to be known only once the cache was allocated. It is fixed earlier: FlashMLAMegaAttnBackend supports one kernel block size (128), and the compressed cache is always viewed in kernel blocks of 128 // compress_ratio slots (get_num_kernel_states), whatever the manager's block size. So every key field comes from construction-time state.
test_warmup_key_matches_launchnow checks the construction-time key against the key a decode step launches on a compressed cache shaped byMLAAttentionSpec.get_num_kernel_states, for both record formats and compress ratios 1/2.- B300, TP4 NVFP4 server: each rank compiled exactly the two expected keys (
extra_page128 and 64). There were no swap-AB compiles once requests were being served (c1/c4/c16).
Every field of the compile key is fixed when the layer is built: the compressed cache is read in the mega backend's 128-token kernel blocks, so its page is 128 // compress_ratio slots whatever the manager block size is. Registering it next to the layer's other JIT warmups removes the bind_kv_cache override. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: bofengl <bofengl@nvidia.com>
Signed-off-by: bofengl <bofengl@nvidia.com>
Move tests/kernels/test_dsv41_swapab_decode.py to tests/models/deepseek_v41/nvidia/test_swapab_decode.py and run it in a single-GPU B200 step. test_warmup_key_matches_launch now checks the key registered at construction against the key a decode step launches on a compressed cache shaped by MLAAttentionSpec.get_num_kernel_states. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: bofengl <bofengl@nvidia.com>
…1971 vllm-project#61971 moved the mega attention layer to int64 RoPE positions and dropped positions_int32, so the swap-AB call merged in from main named a variable that no longer exists. The kernel now reads int64 positions like the mega kernel (it only uses them as an int64 row offset), and the benchmark and tests pass int64 positions. The CPU tests build the layer with object.__new__; initialize its nn.Module bookkeeping so monkeypatch can undo instance attributes on current torch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: bofengl <bofengl@nvidia.com>
| - pytest -v -s models/deepseek_v41/nvidia/specialized_decode/test_numerics.py | ||
| - pytest -v -s models/deepseek_v41/nvidia/specialized_decode/test_layers.py | ||
|
|
||
| - label: ":nvidia: (B200) DeepSeek-V4.1 Swap-AB Decode" |
There was a problem hiding this comment.
how about merging this with nvidia: (4xB200) DeepSeek-V4.1 Specialized Decode and make it Deepseek v4.1 nvidia kernels test? What do you think?
There was a problem hiding this comment.
Sounds good, done in 0490667. The swap-AB tests now run in that step, renamed :nvidia: (4xB200) DeepSeek-V4.1 NVIDIA Kernels (key dsv41-nvidia-kernels-4xb200). Its dependencies now also cover vllm/model_executor/warmup/ and the swap-AB test file, and the timeout went from 40 to 50 minutes. One trade-off: the step is optional, so the swap-AB tests now run nightly rather than on every PR that touches these files.
wzhao18
left a comment
There was a problem hiding this comment.
lgtm. left a nit suggestion.
|
✅ @roborluo, CI is now available for this PR.
|
|
/ci run |
|
✅ Triggered Buildkite CI #99430 for commit ℹ️ This PR is 28 commits behind upstream |
Fold the single-GPU swap-AB step into the 4xB200 specialized-decode step and rename it to "DeepSeek-V4.1 NVIDIA Kernels", per review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: bofengl <bofengl@nvidia.com>
|
/ci run |
|
✅ Triggered Buildkite CI #99459 for commit ℹ️ This PR is 11 commits behind upstream |
|
@roborluo Not sure why it is not merging. Maybe ask in pr-merge-request channel in vllm to ask for help merging. |
|
Correction to the 10/28 numbers, and a re-run on current main.
Re-run. B300, vLLM nightly Median TPOT (ms), stock → PR:
Δ is the change in the summed TPOT of both repeats.
GSM8K, fp8_ds_mla at TP2 (same settings as 10/28: 5-shot strict-match, all 1319 questions, Kernel level on B300. The description's benchmark gives 1.83-2.0x with 16 heads and 1.41-1.64x with 32 heads at 1-16 tokens, for both record formats. That is below the 1.6-1.8x the description claimed for TP2, so I've updated the description to give the B300 range. |
Overview
Makes DeepSeek-V4.1's per-layer decode attention step 1.4-2.15x faster for decode steps of up to 16 tokens at TP2 / TP4, by adding a swap-AB CuTe DSL decode attention kernel to the FlashMLA mega-attention layer (#56935) on SM10x. Steps of up to 16
tokens with 16 or 32 live heads per rank (TP4 / TP2 of the 64-head model) now take it instead of the mega kernel, which pads the heads to 64.
Part of #57448; it reads the interleaved V4.1 KV records from #61371. Related work that this PR does not duplicate (see Details): #59418, #61615, #57711, #61868.
Claims
query heads to the MMA's M dimension, which is at least 64, so at TP2 and TP4 half or three quarters of its MMA work goes to padding heads; swap-AB puts the KV slots on M and only the live heads on N.
and adds no host sync.
Validation
Performance. This PR adds a swap-AB arm to the existing
benchmarks/kernels/benchmark_dsv41_mega_attn.py. Each arm's per-step ops (Q pad + KV insert + attention) are timed as a CUDA graph replay, the same harness that produced the mega vs split-KVtable in
flash_mla_mega_attn.py.Speedup = mega step time / swap-AB step time (median of two runs, which differ by at most 9% per cell):
At 32 tokens swap-AB still wins with 16 heads (1.22-1.25x) but only breaks even with 32 (0.98-1.02x), and at 64 it loses with both, so
_SWAPAB_MAX_TOKENS = 16is the largest step size where it wins for both head counts. On B300 (nightly 76c6f3f) the same harness gives 1.83-2.0x with 16 heads and 1.41-1.64x with 32 heads at 1-16 tokens, for both record formats. As in the existing harness,every replay reuses the same buffers, so the KV reads hit a warm L2 for both kernels; timed alone with the L2 flushed before every call, the attention kernel is 1.6-2.3x faster than the mega kernel over 48 shapes with 1-16 tokens (geomean 1.94x).
Correctness: unit tests. The kernel tests skip unless the GPU is SM10x and the CuTe DSL is built with CUDA 13.2+.
.venv/bin/python -m pytest -v tests/models/deepseek_v41/nvidia/test_swapab_decode.py .venv/bin/python -m pytest -v "tests/kernels/test_compressor_kv_cache.py::test_v41_cache_writers_match_flashmla_decode"test_matches_reference(64)test_untouched_padding(2)test_cuda_graph_and_determinism(12)test_dispatch_rule(9, CPU)test_warmup_key_matches_launch(4, CPU)test_v41_cache_writers_match_flashmla_decode(3, existing)Details
Motivation. Mega attention is the default DeepSeek V4.1 decode path on SM10x. Its FP8 decode kernel takes 64 or 128 query heads, so at TP2 and TP4 half or three quarters of its MMA work goes to padding heads. The mega vs split-KV note in
flash_mla_mega_attn.pyalready records that this padded-head cost dominates small decode steps. Small steps are what low-concurrency decode and spec-decode verify and draft steps run.Why a separate kernel, not a FlashMLA change. FlashMLA's decode maps query heads to the MMA's M dimension, which is at least 64. Serving 16 or 32 heads without padding needs the opposite mapping, with KV slots on M and heads on N. That is a
different kernel, not a smaller tile. In FlashMLA it would be CUDA C++ shipped through a vllm-project/FlashMLA release and a vLLM bump, as #61371 was. The CuTe DSL kernel instead compiles from Python at JIT warmup, needs no build change, and lives next
to its only caller in the DSv4.1 model package. It is adapted from SGLang's swap-AB DSv4 decode attention (Apache-2.0, attribution in the file header).
Kernel design
SwapABDecodeKernel(vllm/models/deepseek_v41/nvidia/ops/swapab_decode_attn.py) puts the KV slots on the tcgen05 MMA's M side (128 rows) and the live heads on N (16 or 32).inverse RoPE and writes MXFP8 with the mega kernel's rounding rule into the o-fused buffers
wo_areads, for the live heads' groups only.128 // compress_ratioslots, whatever the KV cache manager's block size. The layer registers the key in__init__, next to its other JIT warmups, andkernel_warmupcompiles it before CUDA graph capture. V4.1-Flash needs two keys, one per compressed page size.Dispatch.
DeepseekV4MegaAttnAttention._forward_decode_megatakes the swap-AB kernel only when all of these hold:ceil(window / 64) + ceil(index_topk / 128) <= 8;VLLM_BATCH_INVARIANTis off;_SWAPAB_MAX_TOKENS). Under CUDA graphs this is the padded count, so each captured graph holds exactly one of the two kernels;The first six conditions are fixed when the layer is built. The last two are host-side shape checks on each step. If any condition fails, the step runs the mega kernel exactly as before.
Complexity. The change adds about 1,260 lines of kernel code (about 400 of them are 23 small inline-PTX wrappers) and about 100 lines in the layer. Nothing changes in the model runner, the scheduler, FlashMLA or shared attention code. At runtime the
kernel module is imported only by SM10x layers with a compressed cache, 16 or 32 heads and an importable CuTe DSL. Most of the maintenance cost is the inline PTX and the SMEM / TMEM layout math. Both are documented next to the code, and the tests pin
them to an fp64 reference and to the mega kernel.
Tradeoffs and limitations.
least 1.6x faster at every step size up to 16 tokens (at least 1.4x on B300).
Compatibility.
DeepseekV4MegaAttnAttentionchanges. The FlashMLA sparse layer, FlashInfer sparse MLA, and the SM90, SM120 and ROCm paths are untouched. TP1 (64 heads) and TP8 (8 heads) keep the mega kernel.CI. The new tests run in the optional (nightly) 4xB200 step, renamed "(4xB200) DeepSeek-V4.1 NVIDIA Kernels" (
models_basic.yaml, keydsv41-nvidia-kernels-4xb200), next to the specialized-decode tests. The kernel tests need SM10x, the FlashMLA mega ops and a CuTe DSL built with CUDA 13.2+, and skip otherwise; with vLLM's pinnednvidia-cutlass-dsl[cu13]==4.8.0they run. The optional "(B200)LM Eval Spec Decode" step runs the V4.1-Flash NVFP4 DSpark TP4 GSM8K config. It runs at full concurrency, where few steps are small enough for swap-AB.
Not a duplicate. On 2026-10-18 I searched GitHub (open PRs plus PRs since 2026-08-15) for "swap-AB", "swapab", "small-head decode", "heads padded 64 decode", "mega attention DSv4.1" and "V4.1 decode attention". No PR adds an SM10x small-head decode
kernel for the mega-attention path. Related work:
fp8_ds_mlarecords used off SM10x. This PR targets SM10x tcgen05 and 16 or 32 headsin the mega-attention layer, which reads the V4.1 records and fuses the inverse RoPE and MXFP8 output.
VLLM_DSV41_MEGA_SPLITKVenv flags and blocked on Add split-KV to the fused mega decode kernel FlashMLA#25. That kernel still pads the heads to 64. This PR leavesFlashMLA alone, removes the padded-head work and adds no flags.
trtllm_batch_decode_sparse_mla_dsv4. That is a separate opt-in path; this PR changes the default mega-attention path.AI assistance. This PR was prepared with AI assistance (Claude Code, Claude Opus 5.5). It drafted the port of the kernel onto main, the tests, the benchmark arm, the
/pr-checklistpass and this description. The submitter reviewed every changedline and ran the validation above.
Pull Request Checklist
I used vLLM's
/pr-checklistskill. (Mandatory for agents, optional for humans).AI assistance was used during the creation of this PR.
Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.
Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.
Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.
Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.
Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.