Skip to content

[Bugfix] Make DSV4 sparse MLA work end-to-end for plain decode, MTP, and DSpark - #51538

Merged
WoosukKwon merged 8 commits into
vllm-project:mainfrom
lucifer1004:pr/dspark-consolidated
Aug 15, 2026
Merged

WoosukKwon merged 8 commits into
vllm-project:mainfrom
lucifer1004:pr/dspark-consolidated

Conversation

@lucifer1004

@lucifer1004 lucifer1004 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Purpose

DeepSeek-V4-Flash-0731 could not run reliably through the SM120 sparse MLA backend. This fixes the seven defects that blocked it across all three decode modes -- plain decode, MTP, and DSpark -- verified end-to-end on 8xRTX PRO 6000 Blackwell across in-flight batching and prefill/decode disaggregation.

Commits 1-5 unblock DSpark. Commits 6-7 fix a hang that is not DSpark-specific: it strands any MTP (next_n > 1) server on this backend once the batch drains, and is a pre-existing defect on main rather than a regression from this PR. It is filed as #51593, with the full root-cause analysis in this comment.

Why this is not a duplicate

This consolidates #51042 after coordination with the maintainers (see #51042 (comment)). @ilmarkov is Co-authored-by on the SWA-width commit, whose amd/rocm.py hunk and decode_swa_width field are his work. Two deliberate design differences are described in that comment.

Duplicate checks run for dspark, sparse MLA SWA width, mxfp4 gemm1_alpha, and deepseek v4 expert placement; no other open PR covers the remaining four commits.

For commits 6-7, checks run for persistent_topk, indexer seq_lens spec decode, and 51593 in:body. Two open PRs are adjacent but do not overlap:

  • [Bugfix] Align MLA indexer block table with MTP speculative decode #43970 (MLA indexer / MTP) touches the same two files. Its "drop padded MTP decode slots" applies to the variable-length flatten branch, which computes seq_len - decode_len = 0 - 0 = 0 and is already safe. The uniform and native spec-decode paths that produce the negative length are untouched by it. The two changes are complementary and do not conflict.
  • [Bugfix][Kernel] Fix persistent top-k histogram reuse after short rows #49139 fixes a different bug in persistent_topk.cuh (radix histogram reuse after short rows). Worth flagging the interaction: its precondition is a CTA group processing rows that cross RADIX_THRESHOLD, and the out-of-range lengths fixed here cause exactly that spuriously. That PR is still needed for genuinely long rows.

What is fixed

  1. SWA widths — non-causal draft batches allocate decode_swa_indices wider than window_size, but the FlashInfer DSV4 path reshaped with window_size and crashed the draft. The dense width is now carried on the metadata. The non-causal width pads to a multiple of 64 (192 for the K=5 shape: 128 sliding-window + 5 draft entries) rather than 128 (256), matching the kernel's 64-entry tile; both dispatch after feat(sm120): consolidate DSV4 sparse MLA top-k 192/256 support flashinfer-ai/flashinfer#4380, and 192 measures 13-16% faster at >=8 tokens.

  2. Workspace lanes — the DSpark target and draft CUDA graphs retain workspace views concurrently, so one buffer per ubatch let a resize for one orphan the other's live tensor. The second lane is allocated only for V2 DSpark.

  3. Graph replay and draft KVsample_idx_mapping was zero-filled, so capture executed padding rows that scattered into request slot 0; captured backbone outputs could be freed before replay read their storage; draft KV could be written into physical block 0, the null block. Draft sampling also moves to a disjoint Philox counter range, since the rejection sampler keys both its acceptance uniform and its recovery Gumbel noise by token position.

  4. MXFP4 SwiGLU parametersFlashInferExperts injected the GPT-OSS activation constants (gemm1_alpha=1.702, gemm1_beta=1.0, gemm1_clamp_limit=7.0) for every mxfp4 weight dtype. DeepSeek V4 uses this path under --moe-backend flashinfer_cutlass, so its SwiGLU was evaluated with GPT-OSS constants and generation collapsed. GPT-OSS is unaffected: GptOssMxfp4MoEMethod supplies the same constants through its quant config, which the added test pins.

  5. SM120 gate — a FlashInfer build can expose the sparse MLA decode API without carrying the DSV4 specialization a configuration needs. That now fails at model init with the required (num_q_heads, top_k) shape instead of an opaque kernel launch failure at the first decode.

  6. Negative indexer context lengths under MTP — padded decode slots carry seq_len == 0, and with next_n > 1 both spec-decode paths computed a negative per-token context length for the first token of each padded request (0 - 2 + 0 + 1 = -1). The sparse-MLA top-k kernels consume lengths as uint32, so -1 is read as ~4.29e9. Clamped at 0 in both paths, matching the variable-length path which already yields 0. With next_n == 1 the expression collapses to seq_len, which is why plain decode never hit this.

  7. Top-k kernels hardened against out-of-range lengthspersistent_topk_kernel cast lengths to uint32 before testing RADIX_THRESHOLD, so the bogus ~4.29e9 forced the row onto the multi-CTA radix path. Because the cta_in_group != 0 early exit is decided from the host-side max_seq_len while the per-row branch reads device memory, the two disagreed and stranded the group leader on the inter-CTA barrier forever — the kernel never retired, the async output-copy event never fired, and the engine hung waiting for a response that was never sent. The row length is now clamped to min(stride, max_seq_len) before any decision, which also removes an out-of-bounds read into the next row. cooperative_topk compares signed (so it cannot hang) but then casts to uint32, emitting indices 0..TopK-1 instead of -1 padding; clamped as well.

Model evaluation

DeepSeek-V4-Flash-0731, --moe-backend flashinfer_cutlass, --attention-backend FLASHINFER_MLA_SPARSE_DSV4, gsm8k n=1319:

configuration strict-match flexible-extract acceptance FlashInfer
target-only, TP4 0.9454 0.9454 main @ 7f614b86
DSpark, TP4 0.9477 0.9477 65.5% main @ 7f614b86
DSpark, TP4+EP4 0.9462 0.9462 67.1% dc963cc0
DSpark + P/D (mooncake) 0.9492 0.9500 dc963cc0

Standard rejection sampling is distribution-preserving, and DSpark lands 0.0023 from target-only against a difference-stderr of ~0.009 (0.26 sigma), on the same checkpoint and the same tree.

MTP on the base checkpoint (num_nextn_predict_layers = 1), the configuration that #51593 hangs on, gsm8k n=1319:

configuration strict-match
MTP, TP4 (commits 6-7 applied) 0.9477

That sits inside the 0.9454-0.9492 band spanned by the target-only and DSpark rows above, on the same checkpoint and tree, so the clamp does not move accuracy. No unfixed baseline is quoted for this row: without commits 6-7 the same server wedges under the drain reproducer within 30-180 s (see Test plan), and I did not obtain a clean unfixed eval run to compare against.

Isolating fix 4 — identical configuration with only that commit removed:

configuration strict-match flexible-extract
target-only, TP4 0.0000 0.0220
DSpark, TP4 0.0000 0.0174

The TP4 pair was measured on both FlashInfer revisions with no significant change (DSpark 0.9454 -> 0.9477, target-only 0.9462 -> 0.9454), so the two rows measured on the earlier revision are directly comparable.

Test plan

pytest tests/v1/worker/test_workspace.py \
       tests/v1/spec_decode/test_dflash_prepare_inputs.py \
       tests/v1/worker/gpu/spec_decode/test_utils.py \
       tests/kernels/attention/test_flashmla_sparse.py \
       tests/kernels/test_compressor_kv_cache.py \
       tests/v1/attention/test_flashinfer_sparse_mla_sm120_api.py \
       tests/kernels/moe/test_ocp_mx_moe.py

Result: 71 passed, 141 skipped.

For commits 6-7, a drain-shaped reproducer (waves of concurrent completions that retire unevenly, wedging the unfixed build in 30-180 s) against deepseek-v4-flash with --speculative-config '{"method":"mtp","num_speculative_tokens":1}', TP4:

build runs result
unfixed hangs within 30-180 s; all 4 GPUs pinned at 100% util / 0% memory util
commit 6 only 13 all clean
commit 7 only (commit 6 reverted, so negative lengths still reach the kernel) 3 all clean

The commit-7-only run is the one that shows the kernel guard is load-bearing rather than dead code: with commit 6 in place nothing negative ever reaches the kernel, so it would pass either way.

Root cause was confirmed on a live hang with cuda-gdb: exactly one of 16 CTAs resident, spinning in wait_ge, with arrival_counter == 1 against target_val == 2 — a group leader waiting on a peer that had already taken the max_seq_len early exit.

AI assistance

This work was produced with AI assistance. Every changed line has been reviewed by the submitter, and the tests and evaluations above were executed on the hardware described, with results reported as measured.

@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 pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@mergify mergify Bot added nvidia speculative-decoding mrv2 Model Runner V2 specific labels Aug 9, 2026
@mergify mergify Bot added the bug Something isn't working label Aug 9, 2026
@ZJY0516 ZJY0516 added the ready ONLY add when PR is ready to merge/full CI is needed label Aug 9, 2026
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

@lucifer1004, CI is now available for this PR.

  • /ci run starts a CI build.
  • /ci retry retries failed jobs in the CI build for the current PR head. If the current head has no CI build, it starts a new CI build for the current head containing only jobs that failed in the latest earlier CI build for this PR.

@ZJY0516

ZJY0516 commented Aug 9, 2026

Copy link
Copy Markdown
Member

/ci run

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

✅ Triggered Buildkite CI #83052 for commit 8d5140e5ee60.

@lucifer1004 lucifer1004 changed the title [Bugfix] Make DSpark work end-to-end with FLASHINFER_MLA_SPARSE_DSV4 [Bugfix] Make DSV4 sparse MLA work end-to-end for plain decode, MTP, and DSpark Aug 10, 2026
@lucifer1004

Copy link
Copy Markdown
Contributor Author

/ci retry

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #83127 for commit db39e6751323, running 4 failed step(s) from Buildkite CI #83052.

@mergify

mergify Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @lucifer1004.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@Mirrdhyn

Copy link
Copy Markdown

Validated on SM121 (GB10), two nodes, tensor-parallel.

Platform

Hardware 2× NVIDIA DGX Spark (GB10), SM121, aarch64
Interconnect direct QSFP56, RoCEv2, dual HCA (NCCL_IB_MERGE_NICS=1)
CUDA / driver 13.2 / 595.84
torch 2.13.0+cu132
flashinfer 0.6.18.dev20260811
vLLM this PR at db39e675, on top of main
Model DeepSeek-V4-Flash-0731 (hc_mult=4, index_topk=512, 64 attention heads)
Serving --tensor-parallel-size 2 --distributed-executor-backend ray, --speculative-config '{"method":"dspark","num_speculative_tokens":5,...}', --kv-cache-dtype fp8 --gpu-memory-utilization 0.80

At TP=2 the sparse-MLA decode shape is (num_heads=32, top_k=192) — 32 heads from the
split, and a top_k that comes from the sliding window rather than from index_topk,
as detailed at the end. That entry works. We could not tell from the PR description
which shapes and which parts you had already covered, so take this as one more data
point rather than a claim about your coverage.

The server starts and serves, and speculative decoding stays active. No crash across
the runs below: roughly 60 requests, contexts from 4 k to 131 k tokens.

Speculation

Read from /metrics deltas per series. k=5, single stream, 5 draws per row,
acceptance = accepted / drafted tokens.

series tok/s acceptance mean accepted length per-position acceptance (%)
code, reasoning off 72.77 90.1 % 5.51 99.6 · 97.1 · 91.4 · 86.4 · 76.1
code, reasoning on 49.45 54.1 % 3.71 82.3 · 64.9 · 51.5 · 40.0 · 32.0
prose, reasoning off 43.31 44.6 % 3.23 79.1 · 57.4 · 39.2 · 27.6 · 19.5
prose, reasoning on 37.27 34.8 % 2.74 72.2 · 46.9 · 28.8 · 16.3 · 9.7

Throughput divided by mean accepted length gives 13.22, 13.34, 13.42 and 13.60 steps
per second across those four rows, plus 13.62 and 13.58 on two further runs: six
measurements spanning 3 %. The verify step therefore costs about 74 ms whatever the
model is writing, and all the throughput variation comes from acceptance. A step cost
that stays flat across content types seemed worth reporting alongside the absence of
crashes.

Long context

Isolated single request per point, 320 output tokens.

input tokens tok/s acceptance
4 240 77.50 97.8 %
16 520 77.95 100.0 %
32 880 76.24 97.8 %
65 680 76.81 97.8 %
80 120 75.02 97.8 %
100 120 73.31 95.6 %
131 200 77.10 100.0 %

One change outside this PR

The DeepGEMM revision pinned by cmake/external_projects/deepgemm.cmake ships no
sm120_* kernels, so csrc/apis/hyperconnection.hpp reaches
DG_HOST_UNREACHABLE("Unsupported architecture") on family-12 Blackwell as soon as a
model uses hyperconnections. With hc_mult=4 that happens on the first forward pass.
We repointed the pin at deepseek-ai/DeepGEMM a6b593d and filed it separately. It is
orthogonal to this PR, but anyone reproducing on SM12x will hit it first.

FlashInfer requirement, measured rather than assumed

We downgraded to flashinfer 0.6.17 stable on this setup to check whether the nightly is
actually needed. It is:

RuntimeError: FLASHINFER_MLA_SPARSE_DSV4 on SM120 requires a FlashInfer DSV4 sparse
MLA decode specialization for (num_q_heads=32, top_k=192).
Install a FlashInfer build containing flashinfer-ai/flashinfer#4380.

Worth spelling out because the required top_k is not the model's index_topk. It
comes from get_dspark_swa_index_width:

top_k = ceil((sliding_window + num_speculative_tokens) / 64) * 64

With sliding_window=128, every k from 1 to 64 lands on 192, and only k=0 stays at
128. Stable 0.6.16 and 0.6.17 carry 128, 512 and 1024 only. So any DSpark speculation
at all requires a flashinfer build containing PR #4380 (merged 2026-08-08). Since vLLM
pins flashinfer-python==0.6.15.post1, a clean install will not have it.

What we cannot claim

We measured throughput and acceptance, not accuracy: no evaluation was run. And we have
no SM120 consumer part, so we cannot separate SM120 from SM121.

Raw draws, the benchmark harness and the serve/cluster scripts are here, in case any of
it is useful: https://github.com/Mirrdhyn/dsv4-flash-dgx-spark

Happy to re-run anything specific on this hardware if it helps land the PR.

SamMausberg pushed a commit to SamMausberg/flashinfer that referenced this pull request Aug 16, 2026
…ry API

Since flashinfer-ai#4380, a decode-form call (num_tokens <= 64) that matches no
decode instantiation raises in Python instead of tripping the prefill
orchestrator's C++ "num_tokens > 64" assert, but the error is a flat
parameter dump pointing at private dispatch tables, so the reader
still has to open the source to work out which parameter to change
(flashinfer-ai#4541).

Make the dispatch-miss error name the mismatch: which of topk,
num_heads, page_block_size, or d_qk missed the instantiated set, and
which values are available. The old message prefix and shape summary
are kept so callers matching on them are unaffected.

Add flashinfer.mla.supported_sparse_mla_sm120_configs() so serving
frameworks can validate (num_heads, topk, page_block_size) at init
time instead of on the first decode request; vLLM currently reads the
private tables for this (vllm-project/vllm#51538).

Dispatch behavior is unchanged: shapes that previously reached a
kernel still do; only the no-kernel error message and the new query
API changed.

Developed in combination with Claude Fable 5.

Closes flashinfer-ai#4541
SamMausberg pushed a commit to SamMausberg/flashinfer that referenced this pull request Aug 16, 2026
…ry API

Since flashinfer-ai#4380, a decode-form call (num_tokens <= 64) that matches no
decode instantiation raises in Python instead of tripping the prefill
orchestrator's C++ "num_tokens > 64" assert, but the error is a flat
parameter dump pointing at private dispatch tables, so the reader
still has to open the source to work out which parameter to change
(flashinfer-ai#4541).

Make the dispatch-miss error name the mismatch: which of topk,
num_heads, page_block_size, or d_qk missed the instantiated set, and
which values are available. The old message prefix and shape summary
are kept so callers matching on them are unaffected.

Add flashinfer.mla.supported_sparse_mla_sm120_configs() so serving
frameworks can validate (num_heads, topk, page_block_size) at init
time instead of on the first decode request; vLLM currently reads the
private tables for this (vllm-project/vllm#51538).

Dispatch behavior is unchanged: shapes that previously reached a
kernel still do; only the no-kernel error message and the new query
API changed.

Developed in combination with Claude Fable 5.

Closes flashinfer-ai#4541
SamMausberg added a commit to SamMausberg/flashinfer that referenced this pull request Aug 16, 2026
…ry API

Since flashinfer-ai#4380, a decode-form call (num_tokens <= 64) that matches no
decode instantiation raises in Python instead of tripping the prefill
orchestrator's C++ "num_tokens > 64" assert, but the error is a flat
parameter dump pointing at private dispatch tables, so the reader
still has to open the source to work out which parameter to change
(flashinfer-ai#4541).

Make the dispatch-miss error name the mismatch: which of topk,
num_heads, page_block_size, or d_qk missed the instantiated set, and
which values are available. The old message prefix and shape summary
are kept so callers matching on them are unaffected.

Add flashinfer.mla.supported_sparse_mla_sm120_configs() so serving
frameworks can validate (num_heads, topk, page_block_size) at init
time instead of on the first decode request; vLLM currently reads the
private tables for this (vllm-project/vllm#51538).

Dispatch behavior is unchanged: shapes that previously reached a
kernel still do; only the no-kernel error message and the new query
API changed.

Developed in combination with Claude Fable 5.

Closes flashinfer-ai#4541
@WoosukKwon

Copy link
Copy Markdown
Collaborator

I feel the workspace lane change in MRV2 might not be necessary 🤔

randomvariable pushed a commit to randomvariable/vllm that referenced this pull request Aug 19, 2026
zyp2014 pushed a commit to zyp2014/vllm that referenced this pull request Aug 21, 2026
wyettzeng pushed a commit to wyettzeng/vllm that referenced this pull request Aug 21, 2026
randomvariable pushed a commit to randomvariable/vllm that referenced this pull request Aug 23, 2026
zufangzhu pushed a commit to zufangzhu/vllm that referenced this pull request Aug 24, 2026
…and DSpark (vllm-project#51538)

Signed-off-by: Zhu, Zufang <zufang.zhu@intel.com>
randomvariable pushed a commit to randomvariable/vllm that referenced this pull request Aug 25, 2026
randomvariable pushed a commit to randomvariable/vllm that referenced this pull request Aug 29, 2026
randomvariable pushed a commit to randomvariable/vllm that referenced this pull request Aug 29, 2026
khushali9 pushed a commit to khushali9/vllm that referenced this pull request Aug 29, 2026
…and DSpark (vllm-project#51538)

Signed-off-by: khushali9 <khushali.desai9@gmail.com>
azmiao pushed a commit to azmiao/dspark-vllm-gx10 that referenced this pull request Sep 3, 2026
Apply the upstream vLLM DSpark fix at build time without changing the pinned vLLM revision. Validate patch applicability against the exact lock in CI.

Refs Anemll#11

Upstream: vllm-project/vllm#51538
Signed-off-by: code4me2 <velvetmoon222999@gmail.com>
leinasi2014 pushed a commit to leinasi2014/deepseek-v4-vision-cmp170hx that referenced this pull request Sep 3, 2026
… PR#51538 dfecbb5)

Root cause of production Xid 31 (MMU VIRT_WRITE) on the last PP rank:
draft KV slots were computed as block_id*block_size+offset without a
null-block guard, so sliding-window block-table entries that carried
evicted/padding block 0 produced writes into physical block 0.
Also -1-fill sample_idx_mapping so CUDA graph capture padding rows do
not scatter into request slot 0.

Upstream-PR: vllm-project/vllm#51538
Upstream-Commit: dfecbb52ce1
200lz added a commit to 200lz/vllm that referenced this pull request Sep 9, 2026
A FULL-CUDA-graph replay of an MTP batch pads the request count; the padded
request has seq_len == 0, and with next_n > 1 the DSA indexer's per-token
context length for its first row is seq_len - next_n + 1 < 0 unless clamped.
The sparse top-k kernels consume that length as uint32, and the pre-vllm-project#51538
persistent_topk wedged on it (vllm-project#51593). vllm-project#51538 fixed both producer paths and
the kernel guard, but the producer clamp had no direct regression test.

A downstream fork that predates vllm-project#51538 reproduced the wedge while serving
with FlashInfer sparse MLA on SM12x, where it presented as a FlashInfer
decode hang (flashinfer-ai/flashinfer#5015): the FlashInfer launch was
queued on the same stream behind the already-wedged top-k.

Add one parametrised test that builds DeepseekV32IndexerMetadataBuilder for
three live requests plus one seq_len-0 padding request with next_n == 2 and
requires the padded request's per-token context lengths to be [0, 0] on
both the native (B, next_n) path and the flattened uniform-decode kernel
path. No production change; this is coverage for the invariant vllm-project#51538
already enforces.

Signed-off-by: Linzhang Chen <chenlinzhang1-c@toki.waseda.jp>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working mrv2 Model Runner V2 specific nvidia ready ONLY add when PR is ready to merge/full CI is needed speculative-decoding verified Run pre-commit for new contributors without triggering other tests

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

7 participants