Skip to content

[Perf][DSA] Grouped sparse prefill with the FlashMLA head-group-mask kernel - #6

Open
JaredforReal wants to merge 1 commit into
perf/dsa-fused-q-packed-queryfrom
perf/dsa-grouped-sparse-prefill
Open

JaredforReal wants to merge 1 commit into
perf/dsa-fused-q-packed-queryfrom
perf/dsa-grouped-sparse-prefill

Conversation

@JaredforReal

Copy link
Copy Markdown
Owner

Summary

Grouped sparse prefill for DSA models (GLM-5.3 / DeepSeek V3.2) on SM100, stacked on #5. Off by default (VLLM_DSA_GROUPED_SPARSE_PREFILL=1), requires a FlashMLA build with the head_group_mask argument (JaredforReal/FlashMLA#1).

Why

Profiling GLM-5.3-NVFP4 prefill (16k-token chunks) showed the FlashInfer per-token sparse MLA kernel at ~4.8 ms/layer = 45% of the step: every query token gathers its 2048 × 1152 B of KV, and with TP4 all four ranks gather the same rows — it is HBM-gather bound, not compute bound. Measured on real prompts, 8 adjacent tokens' top-2048 sets have a union of only ~2.4k keys (tools/topk_locality.py).

How

  • nvidia/ops/grouped_sparse_prefill.py: three Triton kernels build, per group of 128 // num_heads consecutive tokens of a request, the sorted union of their top-k (bitmap → prefix-sum compaction) and a per-token membership bitmask in FlashMLA's head_group_mask layout. 0.56 ms per 16k tokens incl. Python. Torch reference + tests/kernels/test_grouped_sparse_prefill.py.
  • flashinfer_mla_sparse.py: forward_mqa splits decode tokens (unchanged FlashInfer path) from prefill tokens, which go through _forward_grouped_prefill: union → physical rows via triton_convert_req_index_to_global_index, q gathered into [groups, 128, 576], flash_mla_sparse_fwd(..., topk_length, head_group_mask), padding rows dropped.
  • mla_attention.py: VLLM_DSA_GROUPED_SKIP_MASKED_MHA=1 keeps the dense-MHA shortcut for prompts within the top-k budget but routes everything longer through the grouped kernel instead of masked MHA (masked MHA loses to it at every length).

Results (GLM-5.3-NVFP4, TP4, 4× GB300, bf16 KV, prefix caching off; same script, same window)

Kernel only (real layer-10 top-k, 21k queries, 32k KV): FlashInfer per-token 14.25 ms → FlashMLA grouped 2.86 ms (5.0×).

case base (#5) grouped grouped + skip masked MHA
16k prefill c=16 mean TTFT 8089 ms 8106 ms (masked-MHA path unchanged) 7319 ms (−10%)
32k prefill c=8 mean/median TTFT 10382 / 10290 8776 / 8592 (−15%) 7700 / 8130 (−26% / −21%)
64k prefill c=2 mean TTFT 5552 4348 (−22%) 4286 (−23%)
32k ctx / 256 out c=16 tok/s / median TPOT 163.6 / 72.2 ms 204.6 / 57.8 207.0 (+27%) / 58.9 ms
1k/512 c=64 TPOT 21.13 ms 21.23 21.33
vs main: 32k TTFT −36%, 64k −32%, 32k-ctx c=16 throughput +42%.

Correctness

  • FlashMLA kernel vs exact per-token reference (random data, G=8): rel err 2.2e-3 (bf16 level); without the mask 0.34 → mask verified.
  • Prompt-logprob A/B on a real 4k/12k/30k/60k document: grouped vs baseline mean |Δlogprob| 0.038 / 0.18 / 0.23 / 0.33, identical to the baseline's own run-to-run noise (0.039 / 0.16 / 0.23 / 0.32); document log-PPL unchanged within noise. Greedy continuations are not reproducible even between two baseline runs, so they were not used.
  • Mixed decode+prefill batches exercised (the first version crashed there; fixed by only taking the grouped path when q carries the prefill tokens).

Tests

pytest tests/kernels/test_grouped_sparse_prefill.py (6 passed), tests/kernels/test_fused_deepseek_v32_norm_rope.py -k fused_q (30 passed). E2E scripts and logs under ~/notes/glm53-nvfp4-perf/ (tools/e2e_grouped.sh, tools/logprob_ab.sh).

Open points for review

  • Env-var gating is for evaluation; for upstream this should become an attention-config option and the masked-MHA threshold table should be revisited.
  • fp8 / nvfp4 KV and DCP are not supported by this path (falls back).
  • The FlashMLA dependency must land first; vllm/third_party/flashmla/flash_mla_interface.py is regenerated from it at build time.
  • AI assistance (Claude Code) was used for design, implementation and measurement; the submitter reviewed the change. Model quality eval (gsm8k/aime) still to be run.

…kernel

The per-token sparse MLA prefill kernel re-gathers every selected KV row for
every query token (2048 x 1152 B per token); with TP4 all four ranks gather the
same rows, so prefill attention is HBM-gather bound (~4.8 ms/layer for a 16k
chunk, ~45% of the prefill step on GLM-5.3-NVFP4). Consecutive queries select
heavily overlapping top-k sets: on real prompts the union of 8 adjacent
tokens' top-2048 is ~2.4k keys.

Present 128 // num_heads consecutive tokens of a request as 128 pseudo-heads
that attend over the union of their top-k rows with FlashMLA's SM100 sparse
prefill kernel, using its new `head_group_mask` argument to restrict each
token's heads to its own top-k, which reproduces per-token sparse attention
exactly (kernel test vs. an exact reference: rel err 2e-3). Three Triton
kernels build the sorted union list and the membership bitmask from the
indexer top-k (0.56 ms per 16k tokens).

Opt-in via VLLM_DSA_GROUPED_SPARSE_PREFILL=1 (bf16 KV, no DCP);
VLLM_DSA_GROUPED_SKIP_MASKED_MHA=1 additionally routes every prefill longer
than the top-k budget through it instead of the masked-MHA path. Requires a
FlashMLA build with `head_group_mask` support.

Attention kernel time on a real 32k prompt (layer 10 top-k, 21k queries):
14.25 ms -> 2.86 ms. End to end on GLM-5.3-NVFP4 TP4 / 4x GB300, on top of the
fused_q change: P16k TTFT -10%, P32k -21..26%, P64k -23%, 32k-context c=16
+27% tok/s (-36% / -32% / +42% vs. main); prompt-logprob differences vs. the
per-token path are within the baseline's run-to-run noise.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Jared Wen <jaredwen@inferact.ai>
Copilot AI lite review requested due to automatic review settings September 6, 2026 05:15

Copilot AI 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.

🟡 Changes recommended

There is a confirmed crash path when the env var is enabled without FlashMLA being available, plus test/perf issues that should be addressed before landing.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds an experimental “grouped sparse prefill” path for DSA models that batches consecutive prefill tokens to share a union of top‑k rows and uses FlashMLA’s head-group-mask support to preserve per-token sparsity semantics. This is gated by env vars and integrates into the FlashInfer sparse MLA backend selection logic.

Changes:

  • Add a grouped-prefill execution path in FlashInferMLASparseImpl.forward_mqa that routes prefill tokens through FlashMLA with a head-group mask.
  • Introduce Triton kernels + reference implementation to build per-group union index lists and per-token membership masks.
  • Add a CUDA kernel test validating Triton metadata builder vs the reference, plus env-var behavior to skip masked-MHA when enabled.
File summaries
File Description
vllm/v1/attention/backends/mla/flashinfer_mla_sparse.py Adds env-var gated grouped-prefill path that calls FlashMLA sparse kernel with head-group masks.
vllm/models/deepseek_v32/nvidia/ops/grouped_sparse_prefill.py New Triton implementation for grouped union + mask metadata generation.
vllm/model_executor/layers/attention/mla_attention.py Adds env-var switch to disable masked-MHA when grouped sparse prefill is enabled.
tests/kernels/test_grouped_sparse_prefill.py New CUDA test comparing Triton metadata builder to a torch reference.
Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +194 to +197
union_pad = triton.cdiv(group * num_topk, UNION_BLOCK) * UNION_BLOCK
words = triton.cdiv(max_seq_len, 32)
nblk = union_pad // UNION_BLOCK

Comment on lines +393 to +403
def _use_grouped_prefill(self, attn_metadata: FlashInferMLASparseMetadata) -> bool:
return (
_GROUPED_SPARSE_PREFILL
and attn_metadata.num_prefills > 0
and attn_metadata.prefill is not None
and self.dcp_world_size <= 1
and not is_quantized_kv_cache(self.kv_cache_dtype)
and not self.need_to_return_lse_for_decode
and _GROUPED_PSEUDO_HEADS % self.num_heads == 0
and _GROUPED_PSEUDO_HEADS // self.num_heads <= 8
)
Comment on lines +34 to +35
scores = torch.rand(num_tokens, max_seq_len, device="cuda")
idx = scores.argsort(dim=1)[:, :topk].int().contiguous()
@JaredforReal

Copy link
Copy Markdown
Owner Author

GLM-5.3-Flash (zai-org, FP8, TP4 on 4x GB300) results with this branch cherry-picked onto the perf/glm53-flash stack (#7 + #8 + #9) and VLLM_DSA_GROUPED_SPARSE_PREFILL=1, prefix caching off, bench warmups (~/notes/glm53flash-perf/logs/e2e/dev2_grouped/):

point main stack stack + grouped
8 x 32768 prefill, c=8, mean TTFT 5254 ms 5039 / 4341 ms (two runs) 4120 ms (-21.6%)
2 x 65536 prefill, c=2, mean TTFT 2737 ms 2365 ms 2178 ms (-20.4%)
32k/256 c=16 decode tok/s / TPOT 294 / 40.8 ms 325 / 37.1 ms 312 / 35.4 ms
8 x 2048 / 2 x 8192 prefill 462 / 382 ms 363 / 317 ms 358 / 299 ms (masked-MHA path, grouped not used)

No code change was needed for GLM: the FlashMLA sm100 sparse prefill kernel and the head-group-mask patch are templated on D_QK (512 here), and the union builder already handles the kpool -1 padding and the wider (2176-column) top-k buffer. tests/kernels/test_grouped_sparse_prefill.py: 6 passed on the dev worktree with the head-group-mask FlashMLA build. Quality: gsm8k 1319 questions on the stack (without grouped) 92.65% vs main 93.03% (< 1 sigma); prompt-logprob A/B with grouped enabled was not re-run for GLM.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants