Repository navigation
[Perf][Kernel] Single-launch fused scoring+top-k kernel for sparse-indexer prefill, without materializing logits - #60618
Conversation
|
This pull request has merge conflicts that must be resolved before it can be |
c84e033 to
27ab828
Compare
|
/ci run |
|
✅ Triggered Buildkite CI #94835 for commit ℹ️ This PR is 94 commits behind upstream |
|
/ci run |
|
✅ CI is already running for this commit: https://buildkite.com/vllm/ci/builds/94835 |
|
Hi @LopezCastroRoberto, I put together a three-way diagram to illustrate the relationship between the dense path, LiteTopK, and this PR, to help to understand this PR. The first two panels are preserved from SGLang PR #32094. The added third panel highlights my understanding of this PR: sampled threshold gating produces candidates, and top-k selection runs inside the persistent kernel as each 8-row group completes, before reusing the slot for the next group. Sharing it here in case it is useful for the PR description.
|
- Allocate the fused_mqa_topk scratch and block counter per call from the caching allocator instead of a persistent per-device workspace: nothing stays reserved between calls, and the profiled peak is the larger of the scratch and the dense logits budget (KV cache unchanged vs dense). - Rename plan_fused_mqa_topk to fused_mqa_topk_eligible. - Build the fused source next to the other _C_stable_libtorch kernels and move the DeepGEMM include there. Co-authored-by: LopezCastroRoberto <rocastro@redhat.com> Co-authored-by: Lucas Wilkinson <lwilkins@redhat.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: LopezCastroRoberto <rocastro@redhat.com>
Conflict in vllm/model_executor/layers/sparse_attn_indexer.py: the dense prefill path keeps the fused_mqa_topk row-window loop and takes upstream's DeepSelect BF16 ranking (vllm-project#62240) per window; the profiling branch takes upstream's logits reservation (vllm-project#62860, vllm-project#63047) next to the fused scratch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: LopezCastroRoberto <rocastro@redhat.com>
Co-authored-by: Ziqi Yin <62246771+Heisenberg-Yin@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: LopezCastroRoberto <rocastro@redhat.com>
|
/ci run |
|
✅ Triggered Buildkite CI #98755 for commit ℹ️ This PR is 13 commits behind upstream |
Signed-off-by: LopezCastroRoberto <rocastro@redhat.com>
|
/ci run |
|
✅ Triggered Buildkite CI #98776 for commit ℹ️ This PR is 3 commits behind upstream |
|
@LopezCastroRoberto, adding a concrete memory example to my earlier decode follow-up: the motivation is the remaining BF16 logits intermediate at high concurrency and large speculative K, beyond removing the FP32 copy. We have complementary PRs addressing redundant FP32 logits materialization: #62172 for cooperative top-k and #64138 for persistent top-k. Direct BF16 consumption still materializes the dense BF16 logits tensor, which contributes to retained CUDA graph pool memory. For a more representative capacity example, consider the DeepSeek-V4.1 dense indexer path with 1M logits columns, rather than V4's 4×-compressed width. The dense BF16 intermediate alone requires approximately
These are tensor-capacity calculations, not measured end-to-end allocations or fusion savings. The 8192-row example assumes capture coverage is extended to that size; #63406's current default uniform-decode token cap is 4096. The model-derived 1M-column dense-path capacity is documented in #64138; it does not describe candidate-consuming sparse-logits layers. The memory analysis in #63406 provides the measured graph-memory context. Importantly, this BF16 intermediate remains even after removing all redundant FP32 copies. I'm preparing an SM100 decode row-chunking PR, which is not pushed yet, to bound this allocation. Chunking may trade memory for additional scheduling/scoring/top-k launches and smaller execution batches; capturing those launches in a CUDA graph does not eliminate their GPU work. A decode extension of fused scoring + top-k could potentially avoid materializing the full BF16 matrix and offer a better memory/latency trade-off. The actual graph-pool savings and latency benefit still need matched decode benchmarks, including the fused implementation's own workspace. High-concurrency, large-K verification would be a useful follow-up case to evaluate. AI assistance was used to prepare this comment. |
|
Nice work. The single-launch design and the exact rescan fallback are a clean improvement over the dense path. One suggestion, as a reader: since the motivation (avoiding the materialized |
|
/ci run |
|
✅ Triggered Buildkite CI #98901 for commit |
Resolve conflicts with vllm-project#60618 (fused MQA+top-k prefill): keep its row-split dense path and pass logits_dtype through it, and update the new test_fused_mqa_topk.py callers of mqa_logits_mx_inputs to the new signature. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
…ariant Every causal row runs in one call on the unsplit sparse decode kernel, reading the fp8 SWA and nvfp4 compressed caches directly; sparse_paged_indices derives each token's slots and lengths from (request, position) only. Split out of vllm-project#62365 and rebased onto main 51c6dfd. sparse_attn_indexer: main moved the prefill top-k call into the per-chunk branch (vllm-project#60618, vllm-project#62240), so the BI top-k is selected there; prefill DeepSelect (new on main) is off under BI; decode keeps FP32 logits when main's takes_bf16_logits rule (vllm-project#64138) says so or under BI, and the decode logits budget counts the FP32 copy under BI. main's fused prefill top-k already refuses BI. flashmla: main's num_prefill_tokens. Tests: type fixes for main's Optional topk_indices_buffer and ClassVar PREFILL_CHUNK_SIZE; the decode-vs-prefill test's model config stub gets main's hf_text_config. Original commits: - 5fbd1ad [BI][DSv4.1] Make FlashMLA sparse attention and the indexer batch invariant - 4345c93 [BI][DSv4.1] Set PCP fields on the compressor stub in the BI chunk test - f1ff0f0 [DSv4.1][BI] Run FlashMLA sparse BI rows on the unsplit decode kernel (its attention and test part) - fe16faf [DSv4.1][BI] Fill the BI SWA test stubs' context parallel fields (in part) Signed-off-by: aoshen02 <aoshen@inferact.ai> Signed-off-by: 0z5a <0z5a@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

(this PR description was manually written by me)
Summary
[rows, keys]logits matrix.fp8_mqa_logits+top_k_per_row_prefill) for the same chunk.Acknowledgements
We'd like to acknowledge the authors of LiteTopK (arXiv:2607.11976, #48726), who were the first to fuse sparse-attention indexer scoring with top-k selection without materializing the full logits matrix, using classic sample-based threshold filtering. I open a separate PR because:
c7184685) I found an undersized shared-memory allocation (4100 memcheck errors), racecheck hazards in the candidate ring and the seed/radix-select kernels, and no recovery from candidate-buffer overflow (the mapping kernel traps). LiteTopK's selection is also approximate (99.998% recall); this PR matches dense up to boundary ties.However, most of this kernel is largely an adaptation of DeepSeek's code. This PR replaces the logits write-out with a sampled gate, candidate emission and in-kernel top-k selection.
Motivation: what materializing the scores costs
The dense prefill path writes the fp32 scores of every (query row, visible key) pair to HBM, then reads them back to select the top-k. One 16,384-row prefill chunk needs
16,384 × keys × 4 Bof logits:This produces the following pattern:

It gets worse with context, e.g. at ISL 512K:

Two consequences:
The logits budget trades KV-cache capacity against indexer speed. During memory profiling,
sparse_attn_indexerallocates a dummyVLLM_SPARSE_INDEXER_MAX_LOGITS_MBbuffer, so the logits budget is part of the peak memory the KV cache is sized from. On GLM-5.x with an FP8 KV cache, one token costs 47.7 KB per GPU (78 MLA layers × 576 B + 21 indexer layers × 132 B; MLA KV is replicated across TP ranks). Measured at startup (TP4, B300,--max-model-len 1048576):At the default budget the logits buffer fits under the profiling run's other peak allocations, so it costs no KV cache. The cost is speed: rows per dense call shrink as keys grow (2²⁷ / keys), so a 128K-token prefill in 16K-token steps needs 76 dense call pairs and a 1M-token prefill needs 4,217. With the fused path (top-2048), the first four steps, whose rows see fewer than 64K keys, stay dense (21 call pairs) and every later step is one fused call: 4 at 128K, 60 at 1M. Buying fewer dense calls means raising the budget, and that memory comes straight out of the KV cache: an 8 GiB budget (one dense call per 16K-row chunk at 128K keys) costs 144,640 tokens of KV cache. One dense call per chunk at 1M keys would need 64 GiB, more than the KV cache of the entire 1M-token sequence (46.6 GiB). The fused kernel serves long-context chunks in one call each with a fixed 296 MiB workspace.
The logits round trip is pure overhead. Only k of the
keysscores per row are needed. The fused kernel writes only a gated candidate set, about 1K–4.5K candidates per row depending on k (median on the captures below) at 6 B each, i.e. about 0.1–0.45 GB per 16K-row chunk, against 16–128 GiB of logits traffic at 128K–1M keys.What this PR does
Kernel (
csrc/fused_mqa_topk/). One CTA per SM; each CTA claims 8-row blocks from a global counter (heaviest first) and keeps one continuous Q/KV/TMEM pipeline across blocks:[k, 16384], a row with weights unsafe to scale, or a block whose candidates exceed the selection scratch is rescanned exactly in fp32 (the dense scoring pipeline with a streaming top-k merge), so the output never depends on the gate being right.Memory
Limitations of this PR -- TODOs
Anything outside these bounds takes the existing dense path, unchanged.
DeepSeek-V4.1 two-level candidate selection. DeepSeek-V4.1-Flash has 8 indexer layers (
index_source_layer_ids2, 8, 14, 20, 24, 28, 32, 36). Only layers 2, 8 and 14 use the kernel at this point, which is why its e2e gain is smaller than GLM-5.x (every GLM-5.x indexer layer uses the kernel). See figure below.indexer_sparse_logits(opt-in) already speeds up the consumers by scoring only the candidate blocks, but it requires the MXFP4 indexer cache. [Perf] Enable DeepGEMM sparse MQA logits on the FP8 indexer cache, DSv4.1 #62011 addresses this.Performance
Kernel. B300, real GLM-5.x indexer inputs captured from serving (MRCR 2- and 8-needle and random prompts, layers 0, 9 and 19), one 16,384-row prefill chunk, top-2048, median of
do_bench_cudagraph(rep=200)over captures. Dense is chunked by the 512 MiB budget exactly as in vLLM and captured in one CUDA graph."Exact top-k": the selected keys' scores equal the fp32 top-k scores. The current dense path rounds its logits to bf16, so it is not exact at the boundary. See #61149 (comment)
End to end. TP4, B300, FP8 KV cache, prefix caching off. Dense is the upstream base of this PR.
Up to 32K every chunk stays dense (below the dispatch threshold of 64K visible keys). At 64K only the last chunk of each prompt reaches it. At 1.04M, TTFT drops by 711 s (55%).
The two Flash models gain less in the same concurrency sweep, for different reasons:
Accuracy
MRCR shows no accuracy difference: fused and dense agree within noise in every context range. Measured before the latest refactors, which keep the output exact up to ties (real-capture validation below).