Repository navigation
[Perf][Attention] FA4 sparse MLA prefill with query groups for 16-64 heads per rank - #63104
XinyuanQiao wants to merge 7 commits into
Conversation
…heads per rank FLASH_ATTN_MLA_SPARSE_FA4 sent sparse MQA prefill to trtllm-gen one query at a time. Run it on FA4 instead: 2, 4 or 8 consecutive queries of a request share each 128-head row (gather_kv_query_group_size from flash-attention#205), and a Triton kernel builds each group's top-k union once per index group. Remainder, decode and padding rows use the existing one-query-per-row FA4 kernel; DCP, PCP, HiSparse and other head counts keep the trtllm-gen prefill. The flash-attention pin points at vllm-project#205's commit until that PR merges. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Xinyuan Qiao <82081555+XinyuanQiao@users.noreply.github.com>
|
👋 Hi! Thank you for contributing to the vLLM project. 💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment Once the PR is approved or has the If you have any questions, please reach out to us on Slack at https://slack.vllm.ai. Agent GuidelinesIMPORTANT: 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. 🚀 |
Grouped prefill built the unions of a whole step at once and kept them for the rest of the index group, and copied the step's head-major q_nope on every layer. None of it was counted by memory profiling: 24 KB per token at 16 heads, 80 KB at 64. Grouped rows now go in calls of at most 1,024 packed rows, with the packed query, the unions and the union bitmap in workspace scratch reserved at init (272 MB at 16 heads). Calls start on group boundaries, so every union, and the output, is unchanged. Each layer now builds the unions of its own calls instead of reusing the index group's, which makes prefill 1-2.5% slower. Also type forward_mqa's metadata as the parent's FlashInferMLASparseMetadata. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Xinyuan Qiao <82081555+XinyuanQiao@users.noreply.github.com>
The grouped FA4 kernel has two compile keys: above 256 rows per call, FA4's varlen scheduler adds a batch-lookup table. The startup dummy runs reach at most the small one, so a process compiled the other on its first long prefill, 3.5-7.5 s on GLM-5.2 at TP4. FA4GroupedPrefillKernel registers both keys with the JIT warmup and compiles them compile-only; compile_flash_attn_varlen_func_from_specs now takes qv and the top-k gather specs. Layers that never group (DCP, PCP, HiSparse, decode LSE) skip the warmup and the scratch reservation. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Xinyuan Qiao <82081555+XinyuanQiao@users.noreply.github.com>
|
This pull request has merge conflicts that must be resolved before it can be |
Conflict in cmake/external_projects/vllm_flash_attn.cmake: main moved the FA pin to vllm-project/flash-attention d8dca8f. The pin is now 814171c on the FA PR branch, which merges FA main (and so contains d8dca8f) into the grouped prefill kernel and its review fixes. Signed-off-by: Xinyuan Qiao <82081555+XinyuanQiao@users.noreply.github.com>
Pin flash-attention#205 on top of the FA commit main pins (d8dca8f), as e7352c9. The merged PR head 814171c also carries FA main's Dao-AILab sync (vllm-project#213), whose _flash_attn_fwd returns five values where vLLM's wrapper unpacks four, so every FA4 call failed. Layers that group turn ShardQ off: grouping packs consecutive queries of the rank's heads, while ShardQ hands prefill a token slice with every head. _query_group_spans also falls back to one query per row when the metadata does not start at the top of the top-k buffer or cover every prefill token, as ShardQ's narrowed metadata does. The prefill test inputs' metadata gains main's topk_start. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Xinyuan Qiao <82081555+XinyuanQiao@users.noreply.github.com>
…fill-fp8 Brings in vllm-project#63104's fixes after the main merge: the flash-attention pin on main's FA commit, ShardQ off for grouped layers, and the narrowed- metadata fallback, kept alongside the FP8 path's KV-row limit. Signed-off-by: Xinyuan Qiao <82081555+XinyuanQiao@users.noreply.github.com>
ShardQ (vllm-project#63956) hands each TP rank its tokens with every head, token-major. Instead of turning ShardQ off, group 128 // heads consecutive tokens of the rank per FA4 row: the rows are views of the exchanged query, and groups may span requests, since each query keeps to its own top-k through the membership bits. - build_query_group_row_union builds the unions from the converted KV rows. Groups of two sort one list and look the other's rows up in it; larger groups sort all their rows. Neither grows with the sequence length. - A ShardQ step takes one grouped call, its unions in workspace sized by ShardQ's per-rank token bound. The last group repeats the rank's last token to fill its row. - Layers that use ShardQ group only its steps. The steps it leaves are below its minimum token count, where grouping cost more than it saved. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Xinyuan Qiao <82081555+XinyuanQiao@users.noreply.github.com>
Overview
FLASH_ATTN_MLA_SPARSE_FA4sends sparse MQA prefill to FlashInfer's trtllm-gen kernel, one query at a time. This PR runs it on FA4 instead, with 2, 4 or 8 consecutive queries sharing each 128-head kernel row (the grouped mode of vllm-project/flash-attention#205) and a Triton kernel building each group's top-k union. It covers both prefill layouts:128 / heads per rankconsecutive queries of a request per row, as before.Depends on vllm-project/flash-attention#205 (approved).
cmake/external_projects/vllm_flash_attn.cmakepins e7352c9, which is #205 merged onto d8dca8f (main's current pin) and differs from d8dca8f only in #205's four files. flash-attention main has since taken the Dao-AILab sync (#213), whose_flash_attn_fwdreturns five values where vLLM main's wrapper unpacks four. Once #205 is merged and vLLM moves to a flash-attention commit with it, the pin goes back to vllm-project/flash-attention.Related: #48726 (LiteDSA, under review) groups neighbouring queries in the same way for the FP8 path in the FlashInfer sparse backend; the two PRs touch no common files. The same idea is merged in SGLang for SM90 and SM120 (sgl-project/sglang#32779). #63328 adds an FP8 KV cache on top of this PR.
Claims
nvidia/GLM-5.2-NVFP4, TP4, B300, BF16 KV). Steps that ShardQ does not shard (fewer than 1024 prefill tokens) take main's path and are unchanged.Validation
All runs: B300. This branch (merged with main 86fdabe) is installed editable over main's precompiled wheel for d60c440, which has no
csrc/change up to 86fdabe, with theflash_attn/cutefiles of the pinned flash-attention commit copied in as the cmake build does. The baseline is main 86fdabe installed the same way, with flash-attention d8dca8f. I did not compile this branch from source.Both servers ran with
--kernel-config '{"enable_flashinfer_autotune": false}'. With FlashInfer autotuning on, main's server crashed at startup in my environment (an illegal memory access during the autotune dummy runs, whenever their prefill went through trtllm-gen, e.g. under ShardQ), which I have not investigated further.Tests
pytest tests/v1/attention/test_sparse_mla_backends.py # 551 passed, 1432 skipped (split over 8 GPUs)test_build_query_group_row_union_matches_reference[2, 4]: the ShardQ union builder against a Python reference, with an empty group, rows past each query's count, negative rows and rows up to the 24-bit limit.test_fa4_shardq_prefill_groups_rank_tokens[32, 64 heads]: every rank of a 4-way ShardQ split runs the realforward_mqawith narrowed metadata, in calls of 2 rows, with tails; together the ranks match an fp32 reference of the whole step.test_fa4_grouped_prefill_warmup_covers_runtime_keys: after the compile-only warmup, grouped calls on both sides of FA4's 256-row scheduler threshold, with and without ShardQ, compile nothing (cute.compileraises inside them).test_build_query_group_union_matches_reference,test_fa4_sparse_grouped_prefill_correctness[16, 32, 64]andtest_fa4_sparse_grouped_prefill_sub_chunks[16, 64]cover the path without ShardQ, as before.Dropping the second query's membership bit, or the tail's output, fails the new tests; I checked both. The grouped tests need SM100 and an FA build with flash-attention#205, so CI covers them once the pin includes it.
End-to-end prefill latency. Baseline main on GPUs 0-3 and this PR on GPUs 4-7 of one machine, with:
Each request asks for one token on public-domain text (Project Gutenberg #1342 and #2701). Each cell is the median of 5 trials, with the servers alternating, from the second of two runs: in the first, both servers were up to 3x slower on prompt lengths they had not seen yet, including on paths this PR does not touch. Every prompt starts with its own salt, so no request hits an earlier one's prefix cache; for the cached rows the prefix is sent once, untimed, first.
The ShardQ-off column ran with FlashInfer autotuning on, on the commit before this PR's ShardQ path; the path it measures is unchanged since. Under ShardQ, main routes every sharded step to MQA, so short prompts there do not use masked MHA.
Accuracy. Default configuration, so the ~8K-token prompts go through ShardQ and the grouped path. Teacher-forced
prompt_logprobsover six public-text prompts, two runs per server:python3 prompt_logprob_probe.py --base-url http://127.0.0.1:8400 --cache-salt r1 --out base_r1.json # also r2, and :8500 python3 prompt_logprob_probe.py --compare base_r1.json new_r1.jsonAgreement between the servers is a little below each server's run-to-run agreement, as expected from kernels that round differently. Before the main merge, the path without ShardQ also kept GSM8K 5-shot unchanged with every prefill forced through it (0.9363 ± 0.0067 → 0.9386 ± 0.0066).
Where a ShardQ rank's time goes. One layer on one rank of a 4-way split of a ~15K-token prompt (real GLM-5.2 top-k, ~3.7K tokens per rank, L2 flushed,
bench_shardq_rank.py):At 64 heads FA4's grouped kernel alone is 1.4-1.6x faster than trtllm-gen (at 16 heads, without ShardQ, it is 4x), which is why the gain under ShardQ is small.
Scripts:
e2e_prefill.py,prompt_logprob_probe.py,bench_shardq_rank.pyDetails
Why. FA4's sparse MLA kernel needs 128 query heads per KV head. GLM-5.x has 64 heads and DeepSeek-V3.2 128, so under TP a rank holds 8-64; this backend therefore runs prefill on trtllm-gen, one query at a time, each streaming its own 2,048 KV rows. Consecutive queries select mostly the same rows: on GLM-5.2 the union of 2 neighbours' top-k is 1.15x one list, and of 8 neighbours 1.2-1.7x. flash-attention#205 lets each 128-head row hold
128 / headsqueries that gather their union once, with a membership mask per query.Changes
_forward_row_groups): the rank's query is token-major with every head, so groups are views of it, and a group may span requests, since each query keeps to its own top-k through its membership bit.sparse_utils.build_query_group_row_unionbuilds the unions from the converted KV rows: groups of two sort one list and look the other's rows up in it, larger groups sort all their rows, so the cost does not depend on the sequence length. A step takes one call, its unions in workspace that__init__reserves from ShardQ's per-rank token bound (134 MB at 65,536 batched tokens and TP4). The last group repeats the rank's last token to fill its row.sparse_mla_shard_q_min_tokens, where grouping cost more than it saved (16-32K cached + 256 new tokens: 0.87-0.88x with grouping)._forward_query_groups): the firstn // G * Gqueries of each request run grouped, the rest on the one-query-per-row FA4 kernel this backend uses for decode. The unions come fromsparse_utils.build_query_group_union(a position bitmap per group), in calls of at most 1,024 packed rows whose scratch (272 MB at 16 heads)__init__reserves. The DSA model's absorbedq_nopeis head-major, so each call copies its rows token-major._forward_mqa_kerneldoesn't receive the metadata, soforward_mqakeeps it on the impl for the duration of the call.flash_attn_varlen_funcpassesgather_kv_query_group_sizethrough (default 1, no change), andcompile_flash_attn_varlen_func_from_specstakesqvand the top-k gather specs, soFA4GroupedPrefillKernelregisters the grouped kernel's two compile keys with the JIT warmup (compile-only). ShardQ's strided query views reuse them.Tradeoffs and limitations
AI assistance (Claude) was used to write this change, its tests and the benchmarks, which ran on my machine; I reviewed every changed line.
Pull Request Checklist
I used vLLM's
/pr-checklistskill. (Mandatory for agents, optional for humans).AI assistance was used during the creation of this PR.
Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.
Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.
Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.
Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.