Skip to content

[Perf] Deepseek V41 Attention MegaKernel - #56344

Closed
zyongye wants to merge 28 commits into
dsv41-optimizedfrom
v41-megakernel
Closed

zyongye wants to merge 28 commits into
dsv41-optimizedfrom
v41-megakernel

Conversation

@zyongye

@zyongye zyongye commented Sep 10, 2026

Copy link
Copy Markdown
Member

Purpose

Integrate FlashMLA PR #221's fused norm + RoPE + sparse-attention + inverse-RoPE + FP8-cast kernels into DeepSeek V4.1 attention and add the nvfp4_ds_mla KV cache (V4.1 fp8 528 B SWA + V4.1 fp4 288 B compressed)..

  • flashmla.cmake pins vllm-project/FlashMLA@c112cc1 (the fork's sync of Add kernels for DeepSeek v4.1 deepseek-ai/FlashMLA#221 behind the stable torch.ops._flashmla_C ABI): same build structure as before, source list mirroring the fork's setup.py, both Python interface files vendored, include/cccl added for CUDA 13 host compiles. vllm/v1/attention/ops/flashmla.py adds thin wrappers for the fused prefill/decode ops.
  • New DeepseekV4FlashMLAFusedAttention (vllm/models/deepseek_v4_1/nvidia/flashmla_fused.py), selected by --attention-config '{"dsv4_fused_attention": true}'. It permutes wq_b rows and wo_a columns at load into the kernel's chunk-interleaved layouts, feeds the kernel's FP8 output straight into the DeepGEMM wo_a einsum, and keeps the split-KV path as a fallback below dsv4_fused_decode_min_tokens. The base layer gains _alloc_attn_out / _prepare_q_and_insert_kv / _finish_o_proj hooks so platform subclasses can change the eager-region output shape. Every decode token carries its own metadata row (positions, SWA indices, compressed-cache indices), so the kernel runs on the flattened token batch at any concurrency.
  • nvfp4_ds_mla KV layout table, SlidingWindowMLASpec plumbing, Triton insert/gather kernels for both V4.1 formats, SM100 gating, DSpark context insert.
  • Bug fix surfaced by the evals: combine_topk_swa_indices only wrote the valid prefix of each row and relied on the caller's -1 pre-fill, but both prefill paths reuse an out= workspace. flash_mla_sparse_fwd caps reads at topk_length; the upstream fused prefill reads the whole row and propagates NaN from stale indices into uninitialized gathered rows, which turned 72 % of gsm8k generations into 4096 tokens of NaN-argmax output. The kernel now writes the sentinels itself.
  • DeepseekV41ForConditionalGeneration joins the breakable-CUDA-graph default list; positions_int32 added to the sparse SWA metadata.

Performance

All numbers: GB200, TP4, DeepSeek V4.1 (ckpt20260903), fp8_ds_mla unless stated, same branch for fused and unfused.

Kernel level (benchmarks/kernels/benchmark_dsv41_fused_attention.py, h_q = 64, topk 128 SWA + 512 compressed, µs per call). The fused kernel replaces the split-KV attention kernel plus the separate inverse-RoPE + FP8 quant kernels; the unfused columns time the attention kernel alone and the whole chain.

decode s_q fused split-KV attention only split-KV attention + inv-RoPE + quant
1 (CUDA graph) 22.7 22.7 43.1
64 (CUDA graph) 24.7 24.7 59.7
256 (eager) 50.1 48.2 400
1024 (eager) 155 161 469
prefill s_q (topk 640, eager) fused sparse_fwd + inv-RoPE + quant
184 30 375
2123 163 790

Eager numbers include ~50 µs of stable-ABI boxed dispatch per op call (both fused and the fork's sparse_decode_fwd pay it); under CUDA graphs the kernels alone are 22.7 µs at s_q = 1.

Serving, bs1 (vllm bench serve, 20 random prompts, concurrency 1, no speculative decoding, --max-model-len 40960), TPOT mean / p50 ms:

variant 8k in / 1k out 32k in / 1k out TTFT p50 8k / 32k
unfused 6.20 / 6.14 6.24 / 6.17 230 / 591 ms
fused 6.46 / 6.45 6.49 / 6.49 227 / 552 ms

At bs1 the fused layer is ~4 % slower per token even though its kernel is faster: the attention runs in the eager region of the breakable CUDA graph, and the fused path adds the Q layout kernel, the op call with fresh output allocations and the DeepGEMM wo_a einsum to that region, whereas the base layer's o-projection is inside the captured graph. Follow-up: persistent out_fp8 / out_sf buffers and the einsum in _finish_o_proj. dsv4_fused_attention therefore defaults to off.

Serving, high concurrency (gsm8k, 1319 five-shot prompts sent at concurrency 1024, ~1000 decode tokens per step, 4096-token cap): unfused 67 s, fused 73 s, fused + nvfp4_ds_mla 58 s wall time for the whole eval.

KV capacity: nvfp4_ds_mla raises the KV token budget at 8k context from 2.95 M to 4.08 M tokens (1.38x).

Accuracy

gsm8k, 1319 problems, 5-shot, greedy:

variant flexible strict
unfused (dsv4_fused_attention: false) 0.889 0.889
fused, fp8_ds_mla 0.900 0.899
fused, nvfp4_ds_mla 0.905 0.905

Before the sentinel fix the fused run scored 0.229 (949 / 1319 generations hit the 4096-token cap with empty text). The long-context accuracy of the fp4 compressed path (gpqa, 32k task) is not yet measured.

Test Plan

.venv/bin/python -m pytest tests/v1/attention/test_deepseek_v4_swa_visible.py tests/kernels/attention/test_flashmla_sparse.py \
  tests/kernels/attention/test_flashmla.py tests/kernels/test_compressor_kv_cache.py \
  tests/kernels/attention/test_mla_cross_layer_kernel_equivalence.py tests/v1/attention/test_dspark_noncausal_sparse_mla.py -k "not builder"
pre-commit run --files <changed files>   # ruff, mypy, typos all pass
vigil -c recipe/dsv41/sra_vigil_tp4_gsm8k_fused.yaml            # + _fused_nvfp4 / _worktree_unfused variants
vigil -c recipe/dsv41/sra_vigil_tp4_8k1k_32k1k_bs1_fused.yaml   # + _worktree_unfused variant
.venv/bin/python -m benchmarks.kernels.benchmark_dsv41_fused_attention --decode-s-q 1 64 256 1024 [--cudagraph]

124 existing SWA / FlashMLA / compressor / DSpark tests pass on the new FlashMLA build. Development-time kernel tests (layout permutations, Q padding kernel, V4.1 fp8/fp4 insert + gather against a torch port of FlashMLA's reference quantizer, fused vs split-KV decode/prefill equivalence, combined-index sentinels) were run on GB200 and passed but are not part of this PR.

AI assistance: this PR was developed with Claude Code (Claude Fable 5.1); every change was reviewed and the tests and evals above were run by the submitter.

🤖 Generated with Claude Code

zyongye and others added 28 commits September 10, 2026 20:09
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
Index permutations between the standard [h, d] layout and the layouts the
FlashMLA fused sparse-attention kernel reads (Q, 16-element chunks
interleaved across heads) and writes (O, 32-element chunks interleaved
across the 8 heads of a wo_a group), plus in-place row/column permutation
of MXFP8 wq_b / wo_a shards and their per-32 scales.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
FlashMLA's fused sparse kernels take int32 token positions; copy the batch
positions once per step into a graph-stable buffer shared by all layers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
dsv4_fused_attention selects FlashMLA's fused Q-RoPE + sparse attention +
inverse-RoPE + FP8-cast kernel (None: auto when available); the fused decode
kernel has no split-KV, so dsv4_fused_decode_min_tokens routes small decode
batches to the split-KV kernel.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
… V4.1 kernels commit

Point flashmla.cmake at deepseek-ai/FlashMLA@07a1089
("Add kernels for DeepSeek v4.1 (#221)"): V4.1 fp8 / fp4 paged KV formats,
native head128 sparse decode and the fused norm + RoPE + sparse attention +
RoPE + FP8-cast kernel. Upstream is a pybind11 module, so _flashmla_C is built
as a regular extension linked against torch_python (no stable-ABI flags), the
vendored Python files import vllm._flashmla_C, and the SM90 dense FP8 decode
extension is only built when the source tree has csrc/extension. The
vllm-project fork's V3.2 nvfp4_ds_mla instantiation is not in upstream.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
flash_mla_fused_sparse_prefill / _decode call FlashMLA's fused Q-RoPE +
sparse attention + inverse-RoPE + FP8-cast kernel (DeepSeek V4.1 settings:
no Q norm, per-32 ue8m0 output scales in DeepGEMM's layout). The compiled
entry points resolve on either the upstream pybind11 module or the fork's
torch.ops namespace, the fork-only out= parameter of flash_mla_with_kvcache /
flash_mla_sparse_fwd is emulated with a copy when the vendored interface lacks
it, and the SM90 dense FP8 extension is no longer required for the sparse
paths. Tests check the fused kernel against the split-KV pipeline at the lse
and at the wo_a einsum output, including sliced groups (TP > 1).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
Pads the local-head Q that the permuted wq_b produces to the fused kernel's
head count, either keeping the fused chunk layout or un-permuting to the
standard layout with GPT-J RoPE for the split-KV fallback.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
permuted_output stores chunk (h, c) of each wo_a group at chunk position
c * G + h, values and packed ue8m0 scale bytes alike, so the split-KV fallback
feeds the same permuted wo_a as the fused kernel.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
forward() now asks the platform subclass for the buffer the eager attention
region writes (_alloc_attn_out), how Q is prepared and KV inserted
(_prepare_q_and_insert_kv) and how the buffer is projected afterwards
(_finish_o_proj); the defaults reproduce the existing behavior. Adds the
per-layer finalize_loaded_weights post-load hook, run before quant-method
packing so subclasses can permute raw MXFP8 shards.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
DeepseekV4FlashMLAFusedAttention runs Q RoPE, sparse attention, inverse RoPE
and the FP8 cast in FlashMLA's fused kernel and feeds the wo_a einsum
directly, writing the post-wo_a activation from the eager region. wq_b rows
and wo_a columns are permuted once at load (finalize_loaded_weights, keyed on
the parameters each load touched) so the MXFP8 GEMMs speak the kernel's
layouts; the SWA cache insert reuses rope_quant_insert. Decode steps below
dsv4_fused_decode_min_tokens fall back to the split-KV kernel with the
permuted-output O quant. Selected by attention_config.dsv4_fused_attention
(auto when the kernel is available and o_groups gives 8 heads per group).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
…peline

Decode (topk 128 SWA + 512 compressed, h_q 64) and prefill (topk 640), eager
and under CUDA graphs. On GB200 the fused decode kernel matches the split-KV
attention kernel at s_q = 1 and halves the segment once the Q-RoPE and O-quant
kernels are counted; prefill is 4-13x faster. Results recorded in the spec.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
…4 compressed)

--kv-cache-dtype nvfp4_ds_mla stores the sliding-window cache as FlashMLA's
V4.1 fp8 rows (528 B/token: all 512 dims e4m3, 16 ue8m0 scales per 32) and
the compressed cache as V4.1 fp4 rows (288 B/token: e2m1 pairs with 32 e4m3
scales per 16). A layout table (DSv4KVLayout) drives both cache specs;
rope_quant_insert and dequantize_and_gather_k_cache dispatch on bytes per
token to new Triton kernels (exact ceil-log2 ue8m0 via the fp32 bit pattern,
IEEE division before the e2m1 conversion so exact ties round like the
reference). Requires the fused attention layer (SM100); the DSpark context
insert now goes through rope_quant_insert for every packed layout. Tests
cover insert/gather against torch ports of FlashMLA's reference quantizer
and fused decode over V4.1 fp8 + fp4 caches against the split-KV kernel.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
DeepseekV41ForConditionalGeneration was missing from the auto-enable set
(the V4.0 wrapper is listed), so the LLM-class path hit the piecewise
CUDA-graph error that vllm serve avoids. Also ignore the vendored FlashMLA
fused-kernel Python file generated by cmake.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
…megakernel work

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
…ames

AutoWeightsLoader hands a child module's load_weights names relative to that
child, so under the DeepseekV41ForConditionalGeneration wrapper the loaded
names (model.layers.N...) never matched the attention prefix
(language_model.model.layers.N.attn) and wq_b / wo_a were left unpermuted
while the fused kernel read Q as permuted, producing garbage. Match on the
layers.<id>.attn tail instead, record that each permutation ran, and refuse to
run forward_mqa on a layer whose weights were not permuted.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
…nt fixes

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…_indices

The prefill paths pass a reused workspace through `out=`, so slots past
`combined_lens` kept stale indices. `flash_mla_sparse_fwd` caps reads at
`topk_length`, but the FlashMLA fused prefill reads the whole row and
propagates NaN from any stale index that names a NaN row of the gathered
workspace. With the fused layer this turned 72 % of gsm8k generations into
4096 tokens of NaN-argmax output (0.229 vs 0.889 unfused). The kernel now
fills the tail of every row with -1, matching the `out=None` path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
… the spec

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…r cache test

DeepseekV4Attention._prepare_and_attn now reaches the SWA insert through
_prepare_q_and_insert_kv (platform subclasses override it); the stubbed
attention layer in test_v41_attention_joins_cache_writes_before_consumption
must provide that name.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ed layer

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n items

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Remove the kernel/config test files, the torch reference quantizer, the
integration spec and plan, and the inverse-permutation helpers that only the
tests used. The benchmark and the layer keep the remaining helpers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ynced)

The fork now carries deepseek-ai/FlashMLA#221 (commit 07a1089) behind its
ABI-stable torch.ops._flashmla_C registration, so the pybind11 workarounds
go away: no torch_python link, USE_SABI 3 and TORCH_TARGET_VERSION restored,
_flashmla_extension_C built unconditionally, the vendored Python interfaces
only need the torch.ops shim, and the V3.2 nvfp4 decode instantiation is
back. The op wrapper drops the pybind/torch.ops dual lookup and the out=
emulation; the fused prefill/decode wrappers call torch.ops._flashmla_C
directly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

@mergify

mergify Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Documentation preview: https://vllm--56344.org.readthedocs.build/en/56344/

@mergify mergify Bot added documentation Improvements or additions to documentation ci/build deepseek Related to DeepSeek models performance Performance-related issues DSv4 labels Sep 10, 2026
@jcotant-inferact jcotant-inferact added DSv4.1 Related to DeepSeek-V4.1 models and removed DSv4 labels Sep 10, 2026
@mergify mergify Bot added the DSv4 label Sep 10, 2026
@Harry-Chen

Copy link
Copy Markdown
Member

Kernel level (benchmarks/kernels/benchmark_dsv41_fused_attention.py, h_q = 64, topk 128 SWA + 512 compressed, µs per call). The fused kernel replaces the split-KV attention kernel plus the separate inverse-RoPE + FP8 quant kernels; the unfused columns time the attention kernel alone and the whole chain.

decode s_q fused split-KV attention only split-KV attention + inv-RoPE + quant
1 (CUDA graph) 22.7 22.7 43.1
64 (CUDA graph) 24.7 24.7 59.7
256 (eager) 50.1 48.2 400
1024 (eager) 155 161 469
prefill s_q (topk 640, eager) fused sparse_fwd + inv-RoPE + quant
184 30 375
2123 163 790
Eager numbers include ~50 µs of stable-ABI boxed dispatch per op call (both fused and the fork's sparse_decode_fwd pay it); under CUDA graphs the kernels alone are 22.7 µs at s_q = 1.

Stable ABI dispatch should not be so expensive: #26946 (comment). And if it is 50us per call, then 50.1us does not make sense. Any misunderstanding here?

@zyongye

zyongye commented Sep 11, 2026

Copy link
Copy Markdown
Member Author

Kernel level (benchmarks/kernels/benchmark_dsv41_fused_attention.py, h_q = 64, topk 128 SWA + 512 compressed, µs per call). The fused kernel replaces the split-KV attention kernel plus the separate inverse-RoPE + FP8 quant kernels; the unfused columns time the attention kernel alone and the whole chain.
decode s_q fused split-KV attention only split-KV attention + inv-RoPE + quant
1 (CUDA graph) 22.7 22.7 43.1
64 (CUDA graph) 24.7 24.7 59.7
256 (eager) 50.1 48.2 400
1024 (eager) 155 161 469
prefill s_q (topk 640, eager) fused sparse_fwd + inv-RoPE + quant
184 30 375
2123 163 790
Eager numbers include ~50 µs of stable-ABI boxed dispatch per op call (both fused and the fork's sparse_decode_fwd pay it); under CUDA graphs the kernels alone are 22.7 µs at s_q = 1.

Stable ABI dispatch should not be so expensive: #26946 (comment). And if it is 50us per call, then 50.1us does not make sense. Any misunderstanding here?

Yea I was originally use the upstream flashmla branch for initial integration and later switch to our branch after that's done. So will update the number here.

@Fangzhou-Ai

Copy link
Copy Markdown
Collaborator

Hi @zyongye nice work! Can you also add mega kernel support to ROCm path? cc @ChuanLi1101

Comment thread vllm/config/attention.py
"""With ``dsv4_fused_attention``, decode steps with fewer query tokens than
this use the split-KV FlashMLA decode kernel instead (the fused kernel runs
one query token per CTA and has no split-KV). 0 always uses the fused
kernel."""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we rethink how we do model specific attention configs? this feels like it could get out of hand very fast

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea lemme work on this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/build deepseek Related to DeepSeek models documentation Improvements or additions to documentation DSv4 DSv4.1 Related to DeepSeek-V4.1 models performance Performance-related issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants