Skip to content

[Perf][DCP] DeepSeek-V3.2: skip other ranks' top-k slots in FP8 sparse attention on Hopper - #58985

Draft
LoongPei wants to merge 2 commits into
vllm-project:mainfrom
LoongPei:dcp-sparse-mla-topk-length
Draft

LoongPei wants to merge 2 commits into
vllm-project:mainfrom
LoongPei:dcp-sparse-mla-topk-length

Conversation

@LoongPei

@LoongPei LoongPei commented Sep 28, 2026 •

Copy link
Copy Markdown

Purpose

Fixes #58980. Under DCP, each rank owns about 2048/dcp slots of the indexer's 2048-wide top-k row. _forward_fp8_kv_mixed_batch kept the other ranks' slots as -1 and ran FlashMLA on the full width, so ~(dcp-1)/dcp of every rank's sparse attention went to masked slots, for decode rows and, under DCP, prefill rows alike.

This PR:

  • Compacts the rank's slots into a prefix with the existing in-kernel compaction (compact_valid_to_front, already used by the trtllm-gen path) and passes their count as topk_length.
  • Calls FlashMLA with one batch entry per token, since topk_length is per batch entry (the layout the DeepSeek-V4 decode path uses). Each call gets a fresh FlashMLASchedMeta because the lengths, and so the plan, differ per layer. Tokens are split across calls so that FlashMLA's fp32 split-KV accumulators stay within 256 MiB per call (2048 tokens at 64 heads).
  • Neutralizes empty rows by topk_length == 0 instead of an all--1 scan.
  • Passes HY V4's attention sink to the new call as well, since its _fp8_flash_mla_kernel override is no longer on the DCP path.
  • Moves the query head padding into a helper shared by both FP8 wrappers.
  • Bumps FlashMLA for SM90 V3.2 topk_length support ([SM90] Sparse decode: honor topk_length for DeepSeek-V3.2 FlashMLA#27) and the faster planner (Faster decoding schedule planner with identical plans FlashMLA#28), and adds the two new kernel files to cmake/external_projects/flashmla.cmake.

The attended token set is exactly the same as before; only the kernel's accumulation order changes.

This does not duplicate an open PR: the masked call comes from #46514, written when the SM90 V3.2 kernel did not accept topk_length, and no open PR changes it. This stays a draft until vllm-project/FlashMLA#27 and vllm-project/FlashMLA#28 merge; the GIT_TAG bump will then point at the merge commit.

Test Plan

  • Unit: pytest tests/v1/attention/test_sparse_mla_backends.py tests/v1/attention/test_indexer_dcp_localize.py -k "flashmla or FlashMLA or dcp or fp8 or hyv4"
    • test_fp8_mixed_batch_dcp_neutralizes_empty_rows is updated to the new filter return value.
    • New test_fp8_dcp_topk_length_matches_masked_rows (GPU) checks the new path against the masked call for DCP 2 and 4 with KV interleave 1 and 16. It covers rows with fewer than top-k candidates, rows the rank owns nothing of, and batches split across several plans.
    • New test_hyv4_fp8_per_token_kernel_passes_sink checks that HY V4's sink reaches the new call.
  • Accuracy:
    • GSM8K with tests/evals/gsm8k/gsm8k_eval.py (1319 questions, 5-shot), without and with MTP (num_speculative_tokens=3).
    • Passkey retrieval at 16K-120K tokens.
    • Teacher-forced log-likelihood of 8 natural documents of ~32K tokens.
  • Performance, on prompts of random tokens:
    • Decode step time over a 6 s window in which every request is decoding, at 32K and 128K context with 8, 16 and 32 concurrent requests.
    • Prefill wall time of fresh prompts (no prefix-cache hits) with one output token, for 8 x 32K and 2 x 128K prompts.

Setup: 8x H20, vLLM main (a9eafde), DeepSeek-V3.2, --tensor-parallel-size 8 --decode-context-parallel-size 4 --enable-expert-parallel --kv-cache-dtype fp8_ds_mla --max-num-batched-tokens 16384 --max-num-seqs 64, CUDA graphs on.

Test Result

Unit: 219 passed, 0 failed on H20 (FlashMLA with vllm-project/FlashMLA#27).

pre-commit run --files on the four changed files: all hooks pass.

Decode step time (ms):

context x concurrency before after change change with both FlashMLA PRs
32K x 8 22.80 22.36 -1.9% -3.4%
32K x 16 28.93 26.96 -6.8% -9.5%
32K x 32 41.18 37.78 -8.3% -10.8%
128K x 8 23.30 23.48 +0.8% -2.6%
128K x 16 30.85 29.65 -3.9% -7.7%

"after" uses vllm-project/FlashMLA#27 only; the last column adds vllm-project/FlashMLA#28. All columns come from one session. With the first alone, planning every layer (FlashMLA's planner takes ~16 us per call, and V3.2 has 61 layers) takes back what the kernel saves at 8 concurrent requests; the second cuts the planner to ~4 us.

Prefill wall time of fresh prompts (s, mean of two runs):

prompts before after change
8 x 32K 90.85 65.44 -28.0%
2 x 128K 104.58 79.30 -24.2%

"after" is with the 256 MiB accumulator bound. Planning up to 8192 tokens per call measured 65.17 and 79.10 s; in a 16K-token batch the bound makes the attention call 1.4-1.8% slower on H20.

Accuracy:

before after
GSM8K (5-shot, 1319) 0.951 0.955
GSM8K with MTP (3 tokens) 0.955 0.957
passkey 16K / 32K / 64K / 120K (10 each) 10 / 10 / 10 / 10 9 / 10 / 10 / 10
mean NLL over 8 x 32K-token documents 0.60493 0.60485

The mean MTP acceptance length is 3.11 before and 3.12 after. 38 of the 40 passkey completions are byte-identical before and after. The one miss answers "The pass key is" with "the piece of information you were supposed to find and memorize." instead of the digits. The per-document NLL differs by at most 0.0017 nats per token, also beyond the first 4K tokens.

GSM8K without MTP, passkey and NLL ran on v0.30.0 with the same backend code; everything else ran on main. The accuracy runs used a FlashMLA build with both FlashMLA PRs; the planner PR's plans are identical, so the outputs are the same as with vllm-project/FlashMLA#27 alone.

AI assistance (Claude) was used for parts of the implementation, the tests and the benchmark scripts. I reviewed every changed line and ran the tests and evaluations above.

…e attention

Under DCP, compact this rank's top-k slots into a prefix and pass their
count to FlashMLA as topk_length, with one batch entry per token, instead
of running the kernel on the full row with the other ranks' slots masked.

Assisted-by: Claude
Signed-off-by: LoongPei <3136347099@qq.com>
@mergify mergify Bot added ci/build deepseek Related to DeepSeek models labels Sep 28, 2026
@github-actions

Copy link
Copy Markdown

👋 Hi! Thank you for contributing to the vLLM project.

💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in #pr-reviews, coordinate on features in #feat- channels, or join special interest groups in #sig- channels.

PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment /ci run for upstream CI or /amd-ci run for AMD CI only whenever CI signals are needed.

Once the PR is approved or has the ready label, the PR author can also use the corresponding /ci run, /ci retry, and /ci cancel commands, or their /amd-ci variants. New commits do not start upstream CI automatically.

If you have any questions, please reach out to us on Slack at https://slack.vllm.ai.

Agent Guidelines

IMPORTANT: 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.

🚀

@drakosha

Copy link
Copy Markdown
Contributor

The masked call came from #46514, written when the SM90 V3.2 kernel did not take topk_length, so this is the change we would have wanted. We run that path in production (GLM-5.3, TP4 + DCP4, MTP 3, fp8_ds_mla, 4x H200 NVL) and tested the PR on it.

Kernel. vllm-project/FlashMLA#27 and #28 cherry-pick cleanly onto vLLM's current pin 0eee43b. On one H200 NVL we compared the masked call against the compacted call with topk_length: a DCP=4 ownership pattern, the tail of each compacted row filled with other valid slots (so a kernel that ignored topk_length would diverge), plus a row the rank owns nothing of and a row with under 100 candidates. Max |Δout| is 3.1e-5 and max |Δlse| 1.4e-6 for h_q 64 and 128 at every size. Time per call, planning included on the topk_length side:

tokens h_q=64 h_q=128
1 -15% -11%
8 -9% -27%
128 -44% -60%
1024 -59% -66%

End to end. Nightly af7f9488c with this PR and the two FlashMLA PRs, Inferact/GLM-5.3-NVFP4, TP4 + DCP4 + EP, MTP 3, fp8_ds_mla, --max-num-batched-tokens 8192, --gpu-memory-utilization 0.945, OffloadingConnector. Main needs two local workarounds to start this model on Hopper, filed as #59306. All three columns are the same image; the masked call is restored behind an env switch, and the middle column caps the per-token chunk at 1024 tokens.

masked (before) this PR, chunk 1024 this PR as is
decode, 1 request 109.2 tok/s 108.8 110.8
decode, 8 concurrent 263.5 tok/s 280.3 (+6%) 281.8 (+7%)
decode, 32 concurrent 773.2 tok/s 826.9 (+7%) 836.4 (+8%)
fresh prefill, 8 x 32K 4157 tok/s 5145 (+24%) 5218 (+26%)
fresh prefill, 2 x 128K 4053 tok/s 4973 (+23%) 5064 (+25%)
needle at 230K, 3 depths 3/3 3/3 3/3
3 x 544K cold prompts 3/3, 444 s 3/3, 371 s 3/3, 366 s
same 3 prompts again, from CPU offload 3/3, 2.3 s 3/3, 2.3 s 3/3, 2.2 s

One run per point. The prefill gain shows up the same way in all four prefill-bound measurements (both prefill rows, the needle and the cold prompts); the decode differences at 8 and 32 are single samples. That is in line with your H20 numbers, slightly smaller on prefill.

Chunk size. In the per-token layout each call allocates split accumulators of (b + num_sm_parts) * s_q * h_q * d_v * 4 bytes: 1043 MiB at 8192 tokens and 145 MiB at 1024, measured. The 8192 chunk passed our 544K prompts, but on this image the KV cache came out at 914K tokens against 1.2M on our production image, so there was more free memory than we normally run with, and we have lost an engine before to a 2 GiB version of the same allocation (the masked layout at 8192 tokens, fragmented free memory). The 1024 chunk costs nothing measurable in the table above. Bounding the chunk by bytes rather than by the planner's shared-memory limit would make it independent of how much memory is left free.

Separately, and not caused by this PR: on the current pin the masked mixed-batch call allocates the same accumulators again (2 GiB at 8192 tokens, 8 GiB at 32768) because the FlashMLA V4.1 sync dropped the num_sm_parts > 1 guard that fixed #53413. Filed as #59305.

AI assistance was used for this work.

@drakosha

Copy link
Copy Markdown
Contributor

For anyone who wants to try this on Hopper before it lands: ghcr.io/drakosha/vllm-openai:glm53-dcp-hopper-20260930 is nightly af7f9488c with #58985, vllm-project/FlashMLA#27, #28 and #29, #58209, #59310, #59319 and #49249 on top, plus our local tiling of the fp8 mixed-batch call (VLLM_FLASHMLA_SPARSE_MIXED_TILE_TOKENS, default 1024). It has been serving Inferact/GLM-5.3-NVFP4 on 4x H200 NVL (TP4, DCP4, EP, MTP 3, fp8_ds_mla, 786K context) since 2026-09-30 and starts there without extra DCP flags. Not an official build.

Split the per-token FlashMLA call so that its fp32 split-KV accumulators
stay within 256 MiB (2048 tokens at 64 heads), instead of planning up to
8192 tokens per call, which needed about 1 GiB.

Assisted-by: Claude
Signed-off-by: LoongPei <3136347099@qq.com>
@LoongPei

Copy link
Copy Markdown
Author

Thanks a lot for testing this on H200 NVL with GLM-5.3, and for the kernel check. Filling the tail of the compacted rows with other valid slots is a stronger test than mine.

Agreed on the chunk size. fb94721 bounds the split-KV accumulators by bytes instead: each call now takes as many tokens as fit in 256 MiB of fp32 accumulators, which is 2048 tokens at 64 heads and 1024 at 128 heads. That is about 270 MiB per call including the num_sm_parts extra, down from about 1 GiB at 8192.

On 8x H20 (TP8 + DCP4, V3.2) the cost is small. In a 16K-token mixed batch the attention call is 1.4-1.8% slower than with 8192-token chunks (3.5-4.6% with 1024). Fresh prefill goes from 65.17 / 79.10 s to 65.44 / 79.30 s, which is -28.0% / -24.2% against stock instead of -28.3% / -24.4%; I updated the table in the description. Decode-only steps fit in one call either way. Unit tests and pre-commit pass.

Thanks also for #59305 and vllm-project/FlashMLA#29.

This branch has not been deployed

No deployments
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Performance]: DeepSeek-V3.2 under DCP: FP8 sparse attention processes other ranks' masked top-k slots on Hopper

2 participants