Skip to content

[ROCm][Perf] Fuse DSA indexer QK preprocessing with AITER - #53094

Closed
sumin-hong wants to merge 1 commit into
vllm-project:mainfrom
moreh-dev:rocm/aiter-indexer-qk-fusion-upstream
Closed

sumin-hong wants to merge 1 commit into
vllm-project:mainfrom
moreh-dev:rocm/aiter-indexer-qk-fusion-upstream

Conversation

@sumin-hong

@sumin-hong sumin-hong commented Aug 20, 2026 •

Copy link
Copy Markdown

Purpose

Every DeepSeek-Sparse-Attention layer (DeepSeek-V3.2, GLM-5.x, and their MTP drafts) runs a lightweight indexer whose pre-processing costs five kernel launches per layer per step:

  1. LayerNorm on k
  2. RoPE on the leading rope dims of q and k
  3. per-token-group FP8 quantization of q
  4. a pointwise fold of the q scale into the indexer weights
  5. FP8 quantization of k plus the paged indexer K-cache write

At decode sizes this is launch-bound: the cost barely moves with token count. AITER already ships a kernel that does all five in one launch (indexer_qk_rope_quant_and_cache). This PR wires it in behind a new, default-off env flag VLLM_ROCM_USE_AITER_INDEXER_QK_FUSION.

The kernel writes the indexer K cache itself, so the sparse-attention indexer op skips its own insert (skip_k_cache_insert). Two data-movement optimizations ride along: the rope cos_sin_cache halves are registered once as strided views on the indexer's rotary embedding instead of being re-split every layer every step, and one zero-initialized q_fp8/weights output pair is shared by the indexer layers of a model (allocated next to topk_indices_buffer and sized the same way), with only the first indexer of a forward pass zero-filling it. The change is Python-only: the kernel is already in the AITER version the tree pins.

The fused path engages only when all of the following hold, and otherwise falls back silently to today's code:

  • VLLM_ROCM_USE_AITER=1 and VLLM_ROCM_USE_AITER_INDEXER_QK_FUSION=1
  • gfx942 / gfx950 (on_mi3xx()): AITER ships this kernel in its CK build, and the RDNA paths that SparseAttnIndexer.forward_hip now also serves have Triton-only AITER
  • the in-place-RoPE indexer path, i.e. the rotary_embedding custom op is enabled
  • DSA indexer shapes index_head_dim == 128 and qk_rope_head_dim == 64
  • no context parallelism (decode_context_parallel_size == 1 and prefill_context_parallel_size == 1). The fused kernel is driven by slot_mapping, which is PAD_SLOT_ID on ranks that do not own a token, so those ranks would skip the row and never produce its quantized query — while every CP rank still needs the query to score its own KV shard.

Related work

#51315: same kernel, different integration scope

#51315 integrates the same AITER fused indexer kernel and is the direct overlapping effort. The shared core idea should land only once. This PR's material additional scope is:

  • an explicit default-off feature gate;
  • MI3XX, shape, in-place-RoPE, and context-parallel guards;
  • graph-stable shared output buffers with one zero-fill per pass;
  • defined zero values for skipped and padded rows;
  • explicit MTP buffer ownership and wiring;
  • one-time split RoPE buffer registration;
  • broader correctness coverage, including skipped rows and custom-op schema;
  • model-level validation across both checkpoint formats and MTP settings.

This PR is ready for maintainer comparison and coordination: maintainers can choose this implementation or carry the safety and integration pieces above into #51315. It should not be interpreted as a second independent fused-kernel feature.

#51728: AITER dependency pin

Current vLLM main still pins AITER v0.1.19; #51728 proposes moving the pin to v0.1.19.post2. This PR does not duplicate that dependency update. The submitted code follows the currently pinned norm-parameter ABI. If the pin moves first, the fused call must follow the post2 FP32 norm-parameter contract and the op-level and model-accuracy tests must be rerun on that stack before merge.

Test Plan

  • Image: vllm/vllm-openai-rocm:nightly-5a4c8d99242e9e069b604d0e9b969e77f7dd501d.
  • 8× MI355X (gfx950) — GLM-5.2-FP8 and GLM-5.2-MXFP4: op test, kernel benchmark, coherence check, GSM8K (speculative decoding off and on), GPQA-Diamond, RULER/NIAH, serving throughput.
  • Serve configuration: the official recipe for GLM-5.2, AMD selection (https://recipes.vllm.ai/zai-org/GLM-5.2), plus --no-enable-prefix-caching for the throughput sweep. Accuracy leaves prefix caching at its default, since GSM8K's few-shot prompts share prefixes; both arms are identical per workload.

Serve — GLM-5.2-FP8.

export VLLM_ROCM_USE_AITER=1
export VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS=1
export VLLM_ROCM_USE_AITER_INDEXER_QK_FUSION=1   # the flag this PR adds

vllm serve /models/GLM-5.2-FP8 --port $PORT \
  --kv-cache-dtype fp8_e4m3 --tensor-parallel-size 8 \
  --linear-backend aiter --moe-backend aiter \
  --tool-call-parser glm47 --enable-auto-tool-choice --reasoning-parser glm45 \
  --no-enable-prefix-caching        # throughput sweep only; accuracy keeps the default

Serve — GLM-5.2-MXFP4.

export VLLM_ROCM_USE_AITER=1
export VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS=1
export VLLM_ROCM_USE_AITER_INDEXER_QK_FUSION=1
export VLLM_USE_V2_MODEL_RUNNER=1
export VLLM_ROCM_USE_AITER_FP8BMM=0
export VLLM_ROCM_USE_AITER_FP4BMM=0

vllm serve /models/GLM-5.2-MXFP4 --port $PORT \
  --kv-cache-dtype fp8_e4m3 --tensor-parallel-size 8 \
  --linear-backend aiter --moe-backend aiter \
  --tool-call-parser glm47 --enable-auto-tool-choice --reasoning-parser glm45 \
  --no-enable-prefix-caching --trust-remote-code

Speculative decoding. Append this to either serve command:

--speculative-config '{"method":"mtp","num_speculative_tokens":5}'

Op test

.venv/bin/python -m pytest \
  tests/kernels/attention/test_rocm_aiter_indexer_qk_fusion.py -v

Kernel benchmark

.venv/bin/python benchmarks/kernels/benchmark_indexer_qk_fusion.py --repeat 5

Accuracy — coherence check. One chat completion, to confirm the model is not producing garbage before spending hours on the rest.

curl -s http://127.0.0.1:$PORT/v1/chat/completions -H 'Content-Type: application/json' -d '{
  "model": "<model>", "messages": [{"role": "user", "content": "What is 17 times 23?"}],
  "temperature": 0.6, "top_p": 0.95, "max_tokens": 8192}'

Accuracy — GSM8K

lm_eval --model local-completions --tasks gsm8k \
  --model_args "model=<model>,base_url=http://127.0.0.1:$PORT/v1/completions,num_concurrent=64,max_retries=3,tokenized_requests=False,timeout=600"

Accuracy — GPQA-Diamond, 198 questions, zero-shot, against the running server:

.venv/bin/python -m sglang.test.run_eval --eval-name gpqa --model <model> \
  --host 127.0.0.1 --port $PORT --num-threads 32 --max-tokens 100000 \
  --temperature 1.0 --top-p 0.95 \
  --chat-template-kwargs '{"enable_thinking": true, "think_end_token": "</think>"}'

Accuracy — RULER / NIAH, 500 samples per context length, offline engine:

lm_eval --model vllm \
  --model_args 'pretrained=<model>,tensor_parallel_size=8,max_model_len=262144,gpu_memory_utilization=0.85,kv_cache_dtype=fp8,trust_remote_code=True,enable_thinking=True,think_end_token=</think>,max_gen_toks=8192,block_size=64' \
  --tasks niah_single_2 --metadata '{"max_seq_lengths":[65536,131072]}' \
  --batch_size auto --seed 1234 --log_samples --output_path ./niah

Serving throughput, for CONC in 4 8 16 32 64 128 256:

vllm bench serve --port $PORT --model <model> --dataset-name random \
  --random-input-len 8192 --random-output-len 1024 \
  --num-prompts $((3*CONC)) --max-concurrency $CONC \
  --num-warmups 8 --request-rate inf --ignore-eos \
  --percentile-metrics ttft,tpot,itl,e2el

Lint

uvx pre-commit run --files $(git diff --name-only HEAD~1 HEAD)

The op test scores the fused op and the unfused flow against an fp64 golden of the same math over 20 cases (num_tokens ∈ {1,7,32,257,1023} × block_size ∈ {1,64} × is_neox ∈ {True,False}): the fused path must be no less accurate than the unfused one, and the two must agree to within one fp8 code on every element above 1e-3 of the tensor maximum, for both q_fp8 * weights_out and the dequantized indexer K cache. Two further cases pin the rows the kernel skips to zero and opcheck the op's schema, for 22 in total.

Test Result

Current main rebase (2026-08-27) — based on vLLM fd57c4b7af; the conflict-free rebase preserves the submitted patch exactly (git range-diff: equal). The full vLLM pre-commit set passes. The ROCm GPU results below remain from the original image and have not been rerun after this rebase.

Op-level equivalence — 22 passed on 8× MI355X (gfx950).

Kernel speed — one indexer layer's pre-processing, median of --repeat 5 at block_size=64, n_head=32 (index_n_heads for both GLM-5.2 checkpoints) and the interleaved RoPE layout GLM-5.x selects, on 8× MI355X (gfx950):

tokens unfused fused speedup
1 70.29 µs 11.14 µs 6.31×
8 74.10 µs 11.31 µs 6.55×
32 74.17 µs 11.16 µs 6.65×
64 74.51 µs 11.17 µs 6.67×
256 74.93 µs 11.36 µs 6.59×
1024 73.00 µs 25.06 µs 2.91×

pre-commit — all hooks pass (ruff check, ruff format, typos, markdownlint, mypy 3.10, SPDX headers, root lazy imports, forbidden imports, torch.cuda API check, configuration validation, …), exit code 0.

Coherence check — both arms answer 391 with a non-empty content, i.e. the model closed its thinking block and answered:

Arm reasoning content Answer
Before 454 chars 23 chars 17 times 23 is **391**.
After 575 chars 19 chars 17 times 23 is 391.

GSM8K (1319 questions, lm_eval gsm8k)

8× MI355X, TP8. Reported as strict-match with lm_eval's standard error, flexible-extract in brackets.

Model MTP Before After
GLM-5.2-FP8 off 94.54% ±0.63 (94.54%) 94.16% ±0.65 (94.31%)
GLM-5.2-FP8 5 tokens 94.39% ±0.63 (94.39%) 94.39% ±0.63 (94.39%)
GLM-5.2-MXFP4 off 93.10% ±0.70 (93.10%) 93.40% ±0.68 (93.40%)
GLM-5.2-MXFP4 5 tokens 94.01% ±0.65 (94.01%) 93.63% ±0.67 (93.48%)

GPQA-Diamond (198 questions, zero-shot)

Model Before After
GLM-5.2-MXFP4 89.40% 91.90%

RULER / NIAH (niah_single_2, 500 samples per length)

Model Context Before After
GLM-5.2-MXFP4 65,536 100% (500/500) 100% (500/500)
GLM-5.2-MXFP4 131,072 100% (500/500) 100% (500/500)

Serving throughput, ISL 8192 / OSL 1024

GLM-5.2-FP8, MTP off, 8× MI355X TP8:

Conc Out tok/s before Out tok/s after Δ Mean TPOT ms
4 154.6 157.2 +1.66% 24.8 → 24.3
8 273.2 278.3 +1.84% 27.4 → 26.8
16 461.2 469.9 +1.88% 31.5 → 30.9
32 700.2 705.2 +0.72% 41.0 → 40.8
64 989.9 995.2 +0.54% 57.9 → 57.1
128 1308.4 1311.7 +0.25% 85.8 → 86.0
256 1547.1 1547.8 +0.05% 144.2 → 144.0

GLM-5.2-MXFP4, MTP off, 8× MI355X TP8:

Conc Out tok/s before Out tok/s after Δ Mean TPOT ms
4 187.5 193.9 +3.41% 20.3 → 19.7
8 324.8 330.3 +1.71% 23.0 → 22.7
16 562.4 564.5 +0.37% 26.0 → 25.9
32 853.8 864.1 +1.21% 33.9 → 33.4
64 1249.8 1259.7 +0.79% 45.7 → 45.6
128 1642.5 1650.0 +0.46% 69.0 → 68.7
256 1975.0 1980.8 +0.30% 113.1 → 112.5

Note: AI assistance was used to rebase, review, and prepare this contribution.


Essential Elements of an Effective PR Description Checklist
  • The purpose of the PR is described, including related work.
  • Test commands are provided.
  • Test and model evaluation results are provided with their tested base.
  • AI assistance is disclosed.

@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.

🚀

@mergify mergify Bot added deepseek Related to DeepSeek models performance Performance-related issues rocm Related to AMD ROCm labels Aug 20, 2026
@github-project-automation github-project-automation Bot moved this to Todo in AMD Aug 20, 2026
@sumin-hong
sumin-hong force-pushed the rocm/aiter-indexer-qk-fusion-upstream branch from 3db257f to 128745a Compare August 20, 2026 10:24
@sumin-hong
sumin-hong marked this pull request as ready for review August 20, 2026 10:46

@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.

Fuse LayerNorm, RoPE, FP8 Q/K quantization, scale folding, and K-cache writes behind a default-off ROCm flag. Add MI3XX shape and parallelism guards, shared outputs, MTP wiring, correctness tests, and a kernel benchmark.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Sumin Hong <sumin.hong@moreh.io>
@sumin-hong
sumin-hong force-pushed the rocm/aiter-indexer-qk-fusion-upstream branch from 128745a to edcea1e Compare August 27, 2026 08:49
@mergify

mergify Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @sumin-hong.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mjkvaak-amd

Copy link
Copy Markdown
Contributor

Hi @sumin-hong! We noticed that this PR overlaps substantially with #51315, which our team has been developing, has received review, and is now close to merging. Both PRs integrate the same AITER indexer_qk_rope_quant_and_cache kernel for DSA indexer preprocessing.

I see that #53094 includes several useful additional integration and correctness improvements beyond the core kernel wiring in #51315. We don’t have a preference about which implementation ultimately gets merged; since #51315 is further along in review, it may be worth merging it first and port the additional improvements later. In either case, we should avoid further parallel work on the shared core.

Pinging @dllehr-amd.

@sumin-hong

Copy link
Copy Markdown
Author

Hi @sumin-hong! We noticed that this PR overlaps substantially with #51315, which our team has been developing, has received review, and is now close to merging. Both PRs integrate the same AITER indexer_qk_rope_quant_and_cache kernel for DSA indexer preprocessing.

I see that #53094 includes several useful additional integration and correctness improvements beyond the core kernel wiring in #51315. We don’t have a preference about which implementation ultimately gets merged; since #51315 is further along in review, it may be worth merging it first and port the additional improvements later. In either case, we should avoid further parallel work on the shared core.

Pinging @dllehr-amd.

Thanks, @mjkvaak-amd. Your feedback makes sense to us.
We’ll prepare a new PR as follow-up work to #51315.

hgquannn added a commit to moreh-dev/vllm that referenced this pull request Sep 21, 2026
…se branch

Brings the changes tested on the tmp/v0.28.0 image series into moreh/v0.28.0.
Content is exactly that of the fork PRs #16 to #22 (all against this branch
or its tmp copy):

  #16  [ROCm][Perf] Fuse DSA indexer QK preprocessing with AITER (upstream vllm-project#53094)
  #17  [ROCm][Perf] Shard DSA prefill indexer rows across TP ranks (upstream vllm-project#56944)
  #18  [ROCm] Skip redundant sparse index remap on non-indexer layers
       (backport of upstream vllm-project/vllm vllm-project#51309)
  #19  [ROCm][MLA][Perf] Fuse decode QK-RoPE + Q-concat + KV-concat + KV-cache
       write for sparse MLA (backport of upstream vllm-project#47757)
  #20  [ROCm][Perf] Dispatch DSA prefill indexer logits to aiter's HIP kernel on gfx950
  #21  [ROCm][Perf] Use AITER tuned GEMM for the MoE router gate
       (backport of upstream vllm-project#50535)
  #22  [ROCm][Bugfix] Add `positions` to unified_mla_attention_with_output_fake

Verified as image vllm-forks:0.28.0-pr16-21-a4-9-260917 (this branch minus #22)
built with aiter moreh/v0.1.19 + PR #4 + PR #9 on MI355X.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@sumin-hong sumin-hong closed this Oct 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deepseek Related to DeepSeek models needs-rebase performance Performance-related issues rocm Related to AMD ROCm

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants