Skip to content

[ROCm][Perf][Qwen4Exp] Tune sparse GQA prefill profile on gfx942 and gfx950 - #62590

Open
tc3oliver wants to merge 3 commits into
vllm-project:mainfrom
tc3oliver:qsa-amd-sparse-gqa-gfx942-prefill
Open

tc3oliver wants to merge 3 commits into
vllm-project:mainfrom
tc3oliver:qsa-amd-sparse-gqa-gfx942-prefill

Conversation

@tc3oliver

@tc3oliver tc3oliver commented Oct 20, 2026 •

Copy link
Copy Markdown

Overview

Tunes the >512-program (prefill) launch profile of qsa_sparse_paged_attention on CDNA3 (gfx942) and CDNA4 (gfx950). This PR now includes #65341 by @eppaneamd, combined as proposed and measured by @mjkvaak-amd in this comment:

arch K/V BLOCK_M profile (BLOCK_N, warps)
gfx942 BF16 >= 16 (TP1, TP2) (32, 2), from the earlier commit of this PR
gfx942 FP8, or BF16 with BLOCK_M < 16 any (64, 1)
gfx950 BF16 / FP8 any (64, 1), from #65341
other ROCm archs, CUDA, others (64, 2), unchanged

BLOCK_N=32 with one warp faults on gfx942 (HSA_STATUS_ERROR_MEMORY_FAULT, BF16, BLOCK_M=16), so the two rules are exclusive. The FP8 condition is new: the kernel accepts fp8 K/V since #61529, and BLOCK_N=32 is only 1.2x there (0.78x at TP2 with 520 rows), while one warp is 1.7-1.8x.

get_cdna_version() returns 3 only on gfx942 and 4 only on gfx950. The branch was merged with main to pick up the fp8 path; the PR diff against main is these two files.

Measured results (by AMD, kernel level)

From @mjkvaak-amd's comment linked above. These were measured on a combined qsa.py built from the same hunk, not on this branch's commit: image vllm/vllm-openai-rocm:nightly-40941f2c4ba96fba0a7e282a5355e89cd74d9ac5, one MI300X and one MI355X. Speedup over main at 8,192 rows (min-max over 520 to 16,384 rows):

arch K/V TP1 (24q/2kv) TP2 (12q/1kv) TP4 (6q/1kv) TP8 (3q/1kv)
gfx942 bf16 1.94x (1.80-2.03) 1.92x (1.39-2.14) 1.19x (1.17-1.32) 1.19x (1.14-1.20)
gfx942 fp8 1.76x (1.69-1.79) 1.84x (1.07-1.84) 1.33x (1.17-1.36) 1.29x (1.15-1.43)
gfx950 bf16 1.63x 1.69x 1.37x 1.25x
gfx950 fp8 1.56x 1.59x 1.79x 1.24x

The combined rule was faster than either PR alone, or equal to the better one, in every measured cell. Outputs are bitwise equal to main except gfx942 BF16 at TP1/TP2 (BLOCK_N=32), which differs by at most 2.0e-3 absolute from accumulation order; rel_l2 against an fp64 reference matches main's for every variant. With that qsa.py, the reference test (main's cases plus tp2_split1, tp1_split1_prefill and two extra shapes, bf16 and fp8) gave 18 passed on both MI300X and MI355X.

Still to verify

  • This branch's tests/models/qwen4_exp/test_qsa_amd.py on gfx942 and gfx950. The test case list changed (below) and has not been run on GPU yet; the MI300 and MI355 CI steps select it.
  • End-to-end numbers for the combined rule. None exist yet: the earlier end-to-end runs in this PR (Qwen3.8-Flash-Next-FP8 on MI325X, below) measured only the previous BLOCK_N=32 commit.
  • MI325X and MI350X with the combined rule (not measured).

Checks run locally on this commit (macOS, no GPU): ruff check and ruff format --check (v0.14.0) on both files, and a CPU-only check of the dispatch ladder extracted from qsa.py (every platform/arch x BLOCK_M 4-32 x bf16/fp8 x 512/513/8,192 programs, including that BLOCK_N=32 with one warp is never selected and that non-CDNA3/4 platforms keep the old profile). No GPU test has been run on this commit.

Tests

test_qsa_sparse_paged_attention_matches_reference (bf16 and fp8 K/V, 24 cases):

Not covered

  • @mjkvaak-amd found BLOCK_N=16 with one warp a further 1.2-1.3x faster at >= 2,048 rows but slower at 520-1,024 rows; it would need a row-count threshold and is left for a separate PR.
  • The 24q/1kv shape (BLOCK_M=32) is not a Qwen3.8-Flash-Next shape and is not in the test list.
Earlier results for the previous commit (BLOCK_N=32 on gfx942 only, BF16)

These measured commit f87fca3, not the combined rule. They apply only to the gfx942 BF16 TP1/TP2 branch, which this commit keeps unchanged.

  • Kernel, MI325X, BF16 K/V, 2,051 selected tokens per row, head_dim 256: 1.5x at about 520 programs and 2.1-2.3x at 1K-8K programs, for TP1 and TP2. TP4 (BLOCK_M=8) was 5x slower with BLOCK_N=32, hence block_m >= 16.
  • End-to-end, Qwen3.8-Flash-Next-FP8, 8 x 32K concurrent prefill: TP2 on 2x MI325X, v0.32.0 image, 11,486 -> 12,272 tok/s (+6.8%); on main nightly 76c6f3f (ROCm 10.0), +3.4% at TP2 (11,907 -> 12,306) and +6.3% at TP1 (11,147 -> 11,854). No decode speedup claimed. The KV cache dtype of these runs is not recorded here, so they are not evidence for the fp8 K/V profile.
  • MI300X kernel numbers for that commit: this comment.

Pull Request Checklist
  • I used vLLM's /pr-checklist skill. (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.

@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 qwen Related to Qwen models rocm Related to AMD ROCm labels Oct 20, 2026
@github-project-automation github-project-automation Bot moved this to Todo in AMD Oct 20, 2026
@github-actions

Copy link
Copy Markdown

👋 Hi! Thank you for contributing to the vLLM project.

💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in #pr-reviews, coordinate on features in #feat- channels, or join special interest groups in #sig- channels.

PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment /ci run for upstream CI or /amd-ci run for AMD CI only whenever CI signals are needed.

Once the PR is approved or has the ready label, the PR author can also use the corresponding /ci run, /ci retry, and /ci cancel commands, or their /amd-ci variants. New commits do not start upstream CI automatically.

If you have any questions, please reach out to us on Slack at https://slack.vllm.ai.

Agent Guidelines

IMPORTANT: 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.

🚀

The >512-program sparse GQA profile (BLOCK_N=64, one split, two warps)
was tuned on GB300. On gfx942 with BLOCK_M=16 (the TP1 and TP2 shapes),
BLOCK_N=32 with the same splits, warps, and single pipelining stage is
1.5-2.3x faster. With BLOCK_M=8 (TP4) it is about 5x slower, so that
shape and other architectures keep the existing profile.

Signed-off-by: Oliver Yu <[email protected]>
@tc3oliver
tc3oliver force-pushed the qsa-amd-sparse-gqa-gfx942-prefill branch from 8a0113e to f87fca3 Compare October 20, 2026 02:50
@tc3oliver

Copy link
Copy Markdown
Author

MI300X (gfx942) numbers for #62590 @ f87fca3

Setup: MI300X, 304 CUs, one GPU (on_gfx942() is True). Images: vllm/vllm-openai-rocm:nightly sha256:5800b3b4cd79dc…c84f (0.33.0rc1.dev573, torch 2.13+rocm10.0, triton 3.8.0; the 7-line hunk applied to ops/qsa.py with patch, offset 14) and :v0.33.0-rocm72 sha256:4fe7bc121cbe2f…a182 (torch 2.13, HIP 7.2.53211). Host ROCm 7.0.2. The PR's bench_gqa.py, stock vs stock+hunk.

Dispatch: TP1 (24q/2kv) and TP2 (12q/1kv) have BLOCK_M=16 and use BLOCK_N=64 up to 512 programs, 32 above. TP4 (6q/1kv, BLOCK_M=8) stays at 64 at 512, 513 and 1024 rows.

Speedup on nightly (rocm72 within ~1%):

shape rows stock ms PR ms speedup
TP2 520 0.409 0.274 1.49x
TP2 1024 0.787 0.359 2.19x
TP2 4096 2.666 1.373 1.94x
TP1 260 0.413 0.277 1.49x
TP1 1024 1.538 0.727 2.11x
TP1 4096 5.350 2.794 1.92x
TP4 520 0.365 0.355 1.03x
TP4 4096 2.215 2.205 1.00x

Max abs diff vs stock: 4.9e-4 for TP1/TP2, 0 for TP4. Against an fp32 torch reference I ran 9 shapes x (bf16, fp8-quantised values stored as bf16) x (default dispatch, forced BLOCK_N=64). The BLOCK_N=32 path is off by at most 3.8e-4; the overall max is 5.07e-4 (TP4, which the PR doesn't touch). All within the 2e-2 tolerance.

The flaky CUDA-graph test I hit in the QSA tests on MI300X is unrelated to this PR; see the #62589 comment.

Caveats: the kernel has no native fp8 path (it asserts bf16), synthetic activations, single seed, host load 7-22 while timing (GPU use 0-2%), the PR's 5x TP4 slowdown at BLOCK_N=32 not re-measured, the PR branch itself not built on nightly.

@mjkvaak-amd

Copy link
Copy Markdown
Contributor

@eppaneamd @tc3oliver: #65341 and #62590 change the same base_programs > 512 branch of qsa_sparse_paged_attention. I measured both PRs, a few combinations, and the full launch-profile grid on MI300X (gfx942) and MI355X (gfx950), at kernel level only. Neither PR alone is the best profile on gfx942. A combined rule is faster than both PRs (or equal to the better one) in every cell I measured on both archs, and it passes the reference test. I suggest landing it as one PR.

Proposed combined hunk

@tc3oliver add @eppaneamd as a co-author in this PR and add the following change

    else:
        block_n, target_splits, partial_warps = 64, 1, 2
        if current_platform.is_rocm():
            from vllm.platforms.rocm import get_cdna_version

            # One warp per program is faster on CDNA3 and CDNA4, except BF16
            # K/V with BLOCK_M >= 16 on CDNA3, where BLOCK_N=32 is faster
            # (BLOCK_N=32 with one warp faults there).
            cdna_version = get_cdna_version()
            if cdna_version == 3 and block_m >= 16 and not is_fp8:
                block_n = 32
            elif cdna_version in (3, 4):
                partial_warps = 1

get_cdna_version() returns 3 exactly on gfx942 and 4 exactly on gfx950. On CDNA4 (gfx950) this is exactly #65341. On CDNA3 (gfx942) it is #62590 for BF16 K/V at TP1/TP2, and one warp everywhere else.

Results

Speedup over main at 8,192 rows, with the min-max over 520 to 16,384 rows in parentheses. "Bitwise" means the combined output equals main's.

arch K/V shape main ms #62590 #65341 combined bitwise
gfx942 bf16 TP1 (24q/2kv) 11.53 1.94x (1.81-2.02) 1.01x 1.94x (1.80-2.03) no (BLOCK_N=32)
gfx942 bf16 TP2 (12q/1kv) 5.39 1.92x (1.39-2.12) 1.00x 1.92x (1.39-2.14) no (BLOCK_N=32)
gfx942 bf16 TP4 (6q/1kv) 4.49 1.00x 0.99x 1.19x (1.17-1.32) yes
gfx942 bf16 TP8 (3q/1kv) 3.96 0.99x 0.99x 1.19x (1.14-1.20) yes
gfx942 fp8 TP1 (24q/2kv) 6.22 1.21x (1.17-1.21) 1.01x 1.76x (1.69-1.79) yes
gfx942 fp8 TP2 (12q/1kv) 3.05 1.25x (0.78-1.25) 1.00x 1.84x (1.07-1.84) yes
gfx942 fp8 TP4 (6q/1kv) 2.97 1.00x 1.00x 1.33x (1.17-1.36) yes
gfx942 fp8 TP8 (3q/1kv) 2.65 1.00x 1.00x 1.29x (1.15-1.43) yes
gfx950 bf16 TP1 (24q/2kv) 4.81 1.00x 1.63x (1.43-1.75) 1.63x (same as #65341) yes
gfx950 bf16 TP2 (12q/1kv) 2.50 1.00x 1.69x (1.55-1.73) 1.69x yes
gfx950 bf16 TP4 (6q/1kv) 2.24 1.00x 1.37x (1.24-1.45) 1.37x yes
gfx950 bf16 TP8 (3q/1kv) 1.96 1.00x 1.25x (1.12-1.31) 1.25x yes
gfx950 fp8 TP1 (24q/2kv) 4.11 1.00x 1.56x (1.18-1.59) 1.56x yes
gfx950 fp8 TP2 (12q/1kv) 2.11 1.00x 1.58x (1.41-1.58) 1.59x yes
gfx950 fp8 TP4 (6q/1kv) 2.94 1.00x 1.79x (1.64-1.81) 1.79x yes
gfx950 fp8 TP8 (3q/1kv) 1.86 1.00x 1.24x (1.15-1.40) 1.24x yes

What this means for each PR:

Method

Not covered and possible follow-up

  • No end-to-end numbers; this is kernel level only. MI325X and MI350X were not measured. The 24q/1kv shape (BLOCK_M=32, not a Qwen3.8-Flash-Next shape) is in the sweep only: there, one warp is 0.5x on gfx942 at bf16, which the block_m >= 16 arm also covers.
  • Headroom: BLOCK_N=16 with one warp at BLOCK_M=16 is a further 1.2-1.3x over the combined rule at ≥ 2,048 rows on both archs. On MI355X at bf16 TP2 it is 2.0-2.1x vs 1.7x, and on MI300X at bf16 TP2 2.3-2.6x vs 1.9-2.1x. It is slower at 520-1,024 rows (MI300X bf16 TP2: 1.12x vs 1.39x at 520), so it would need a row-count threshold. I'd leave it for a separate PR.

tc3oliver and others added 2 commits November 3, 2026 19:43
…ll profiles

vllm-project#62590 (BLOCK_N=32 on gfx942) and vllm-project#65341 (one warp on gfx950) change the
same >512-program branch of qsa_sparse_paged_attention. Kernel sweeps on
MI300X and MI355X showed one warp per program is faster on both CDNA3 and
CDNA4, except BF16 K/V with BLOCK_M >= 16 on CDNA3, where BLOCK_N=32 stays
faster. BLOCK_N=32 with one warp faults on gfx942, so the two rules are
exclusive. The BF16-only condition matters because the kernel takes fp8 K/V
since vllm-project#61529, and BLOCK_N=32 regressed the fp8 TP2 shape at 520 rows.
Other platforms and other ROCm architectures keep the GB300 profile.

The reference test drops the duplicate tp2_split1 case from vllm-project#65341, keeps
its TP1 split=1 case, and covers TP1/TP2/TP4/TP8 on both sides of the
512-program profile boundary, for BF16 and FP8 K/V.

Co-authored-by: Rony Leppänen <[email protected]>
Signed-off-by: Oliver Yu <[email protected]>
@tc3oliver tc3oliver changed the title [ROCm][Perf][Qwen4Exp] Use BLOCK_N=32 for sparse GQA prefill on gfx942 [ROCm][Perf][Qwen4Exp] Tune sparse GQA prefill profile on gfx942 and gfx950 Nov 3, 2026
@tc3oliver

tc3oliver commented Nov 3, 2026 •

Copy link
Copy Markdown
Author

@mjkvaak-amd @eppaneamd Thanks for the benchmarks, the analysis and the combined proposal.

I've folded both PRs into d902859 and added @eppaneamd as co-author. In the >512-program profile, gfx942 keeps BLOCK_N=32 for BF16 K/V with BLOCK_M >= 16 and uses one warp otherwise, and gfx950 uses one warp for every shape. Other platforms are unchanged. The reference test now covers BF16 and FP8 K/V for TP1/TP2/TP4/TP8, on both sides of the 512-program boundary, and has one copy of the duplicate TP2 case.

My earlier note that the kernel had no FP8 K/V path was wrong. The description is corrected.

So far I've only run ruff and a CPU check of the dispatch logic, on a Mac. The new commit hasn't run on an AMD GPU yet. I'll run the correctness test on gfx942 later and post the results here. Could you review it and, when you have time, check gfx950?

@tc3oliver

tc3oliver commented Nov 4, 2026 •

Copy link
Copy Markdown
Author

Ran d902859 on MI325X (gfx942), nightly-76c6f3f2 image, main vs this commit, same GPU and inputs, 3 rounds.

pytest -k sparse_paged_attention_matches_reference: 24 passed. Whole test_qsa_amd.py: 37 passed. No HSA fault or NaN.

Launch config matches the commit: BF16 TP2 gets BLOCK_N=32 with 2 warps, everything else above 512 programs gets BLOCK_N=64 with 1 warp (main: 64, 2 warps).

qsa_sparse_paged_attention latency in ms, median of 3 rounds (rounds within ~1%), FP8 with real fp8 K/V:

K/V shape rows main PR speedup
BF16 TP2 520 0.409 0.282 1.45x
BF16 TP2 1024 0.792 0.374 2.11x
BF16 TP2 4096 2.800 1.603 1.75x
FP8 TP2 520 0.213 0.190 1.12x
FP8 TP2 1024 0.394 0.225 1.75x
FP8 TP2 4096 1.423 0.833 1.71x
BF16 TP4 520 0.318 0.254 1.25x
BF16 TP4 4096 2.073 1.797 1.15x
FP8 TP4 520 0.312 0.246 1.27x
FP8 TP4 4096 1.433 1.104 1.30x
BF16 TP8 520 0.274 0.235 1.16x
BF16 TP8 4096 1.820 1.610 1.13x
FP8 TP8 520 0.271 0.200 1.36x
FP8 TP8 4096 1.251 0.995 1.26x

No cell is slower than main. Output matches main except BF16 TP2, which differs by up to 2e-3 (test tolerance is 2e-2). Kernel microbenchmark only, not end-to-end, gfx942 only.

Timed as 30 back-to-back launches per sample, so host launch overhead is included.

This branch has not been deployed

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

Labels

qwen Related to Qwen models rocm Related to AMD ROCm

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

2 participants