Skip to content

[ROCm][DSv4] Fix sparse-indexer logits collapse on gfx950/gfx942 - #50455

Merged
tjtanaa merged 27 commits into
vllm-project:mainfrom
frida-andersson:dsv4-gfx950-sparse-logits-fix
Sep 18, 2026
Merged

tjtanaa merged 27 commits into
vllm-project:mainfrom
frida-andersson:dsv4-gfx950-sparse-logits-fix

Conversation

@frida-andersson

@frida-andersson frida-andersson commented Jul 30, 2026 •

Copy link
Copy Markdown
Contributor

On gfx950 and gfx942, AITER's deepgemm_fp8_paged_mqa_logits mis-scores the DSv4 sparse indexer during decode (wrong paged-cache layout), collapsing long-context retrieval. VLLM_DSV4_LOGITS_FIX=1 (off by default) reroutes decode + MTP through a Triton kernel instead

The replacement kernel (_fp8_paged_mqa_logits_decode_kernel) reads the fp8 KV cache in its true block-flat layout and computes relu(q·k)·weight·scale per key position, splitting each query's key range across programs so there is no cross-program reduction or host sync — which keeps it capturable in a full CUDA graph

The decode kernel computes its q·k dot directly in fp8 (fp8 MFMA with f32 accumulation) rather than upcasting q and k to f32 first. Against that f32-upcast path the dot is 4.4–4.7× faster on gfx942 and up to ~9× on gfx950 at serving batch sizes, for the same result — bit-identical on gfx942, and on gfx950 within a ~1e-5 rounding difference that leaves the selected keys unchanged (GSM8K, RULER and MTP all agree).

Env: vLLM 0.26.1rc1.dev124, AITER 0.1.16.post5, DeepSeek-V4-Flash, TP=4, fp8 KV, block_size=256, max_model_len=40960, 4x gfx950

RULER NIAH

Accuracy % as stock → fix (50 samples/cell, temp=0, concurrency 16)

context niah_1 niah_2 niah_3
4K 62 → 90 60 → 58 8 → 54
8K 40 → 96 18 → 66 10 → 38
16K 24 → 86 16 → 68 4 → 40
32K 12 → 92 0 → 80 2 → 48

At 4K the indexer prunes little, so the fix has little to correct there; niah_2 60 → 58 is within run-to-run noise. The fix's effect grows with context, where stock collapses and the fix holds

As a sanity check, I also ran niah_1 through the torch reference - the slow but known-correct version of the decode-logits calculation that the fix's Triton kernel is designed to reproduce. It recovered accuracy to 81% at 16K and 92% at 32K, essentially matching the fix's own niah_1 numbers

GSM8K

flexible-extract accuracy (1319 problems), prefix caching disabled. The gap widens with shot count: more few-shot context means the indexer prunes more, so stock's mis-ranking costs more and the fix recovers more. 5-shot is too short to engage the indexer (within noise)

shots stock fix Δ
5 0.9477 0.9492 +0.002
20 0.9212 0.9553 +0.034
60 0.8772 0.9507 +0.074

Throughput

Decode throughput on the committed kernel (with the fp8 dot), prefix caching disabled, 512 output tokens, ignore_eos, 1 warmup + 2 reps. stock = AITER fused paged-MQA-logits kernel, fix = Triton replacement

in / conc stock tok/s fix tok/s stock TPOT fix TPOT
16K / 1 72.5 74.8 12.72 12.29
16K / 32 652.8 696.9 41.26 37.93
32K / 1 69.3 69.0 12.28 12.35

The fix runs at parity with AITER's fused kernel — differences are within run-to-run noise, since the logits step is only a few percent of decode

Kernel occupancy tuning

The kernel is memory-bound over the KV range, so the number of KV-range splits per query sets occupancy. A fixed 32 leaves most CUs idle at low batch / long context, so it scales as min(256, tiles, 1024 / rows) (all static at launch, so still CUDA-graph-safe). Isolated kernel latency vs the fixed-32 version, output unchanged:

batch ctx fixed 32 tuned speedup
1 16K 62.9 us 34.9 us 1.8x
1 32K 115 us 36.5 us 3.2x
8 16K 64.4 us 60.5 us 1.06x
32 16K 200 us 192 us 1.04x

End-to-end the effect is small — ~3-5% TPOT at concurrency 1, within noise at c8/c32 (where the split count is back to 32). No regression anywhere

gfx942 (MI300-class): same bug, same fix

The identical collapse reproduces on gfx942 and the Triton reroute fixes it there too. The kernel is unchanged — the fp8 KV-cache layout and torch reference are identical across both archs — so only the arch gate widens (_ON_GFX950 → _ON_GFX950 or _ON_GFX942). RULER NIAH stock → fix (TP8, fp8 KV, 8x gfx942, 50 samples/cell, temp=0, concurrency 16):

context niah_1 niah_2 niah_3
4K 60 → 86 64 → 84 14 → 56
8K 44 → 92 26 → 60 6 → 54
16K 22 → 76 14 → 68 4 → 52
32K 6 → 72 4 → 82 0 → 58

Same signature as gfx950: stock collapses toward ~0 by 32K, the fix recovers and holds. MTP (num_speculative_tokens=3) verified, consistent with the non-MTP fix

On gfx950, AITER's deepgemm_fp8_paged_mqa_logits mis-scores the DeepSeek-V4
sparse indexer during decode (paged MQA cache read with the wrong layout),
collapsing long-context retrieval (RULER niah_single_1 ~60% at 4K -> ~8% at 32K).

Behind VLLM_DSV4_LOGITS_FIX=1 (off by default), reroute decode and MTP paged
MQA-logits through a Triton kernel that reads the fp8 KV cache in its true
block-flat layout and computes relu(q.k)*weight*scale per key position. Each
query's key range is split across programs so there is no cross-program
reduction or host sync, keeping it capturable in a full CUDA graph. The write
range is clamped to max_model_len and the block-table gather is bounds-guarded.

Validated on DeepSeek-V4-Flash (TP4, fp8 KV, gfx950): RULER long-context
retrieval recovered across 4K-32K, GSM8K 20-shot +3.4pts flexible-extract,
throughput within ~1.6% at long-context decode, verified under MTP
(num_speculative_tokens=3). Kernel matches the torch reference to 1.4e-6.

Signed-off-by: Frida Andersson <fanderss@amd.com>

@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 rocm Related to AMD ROCm v1 labels Jul 30, 2026
@github-project-automation github-project-automation Bot moved this to Todo in AMD Jul 30, 2026
Scale the decode kernel's KV-range split count as min(256, tiles, 1024 / rows)
instead of a fixed 64/32, and pipeline the launch (num_warps=4, num_stages=2).
All terms are static at launch, so the grid stays fixed under CUDA graph
capture.

The kernel is memory-bound over the KV range; a fixed split count leaves most
CUs idle at low batch / long context. This is 1.7-3.2x faster in isolation and
~3-5% lower TPOT at concurrency 1, collapsing back to 32 splits (identical to
before) at high batch, so no regression there. Output is bit-identical
(~1e-7 vs torch ref); decode and MTP (next_n>1) both verified.

Also drop an unused local and reformat to satisfy ruff.

Signed-off-by: Frida Andersson <fanderss@amd.com>
The same paged-MQA-logits collapse reproduces on gfx942, and the Triton reroute
fixes it there too: the fp8 KV-cache layout and torch reference are identical
across both archs, so the kernel is unchanged and only the arch gate widens
(_ON_GFX950 -> _ON_GFX950 or _ON_GFX942). Validated on 8x gfx942 (TP8): RULER
NIAH recovers the long-context collapse, MTP (num_speculative_tokens=3) consistent
with the non-MTP fix.

Signed-off-by: Frida Andersson <fanderss@amd.com>
@frida-andersson frida-andersson changed the title [ROCm][DSv4] Fix gfx950 sparse-indexer logits collapse [ROCm][DSv4] Fix sparse-indexer logits collapse on gfx950/gfx942 Aug 3, 2026
@frida-andersson frida-andersson changed the title [ROCm][DSv4] Fix sparse-indexer logits collapse on gfx950/gfx942 [ROCm][DSv4] Fix gfx950/gfx942 sparse-indexer logits collapse Aug 3, 2026
@frida-andersson frida-andersson changed the title [ROCm][DSv4] Fix gfx950/gfx942 sparse-indexer logits collapse [ROCm][DSv4] Fix sparse-indexer logits collapse on gfx950/gfx942 Aug 3, 2026
@frida-andersson
frida-andersson force-pushed the dsv4-gfx950-sparse-logits-fix branch from eab2f97 to b6a29ad Compare August 3, 2026 12:14
Signed-off-by: Frida Andersson <fanderss@amd.com>
@frida-andersson
frida-andersson force-pushed the dsv4-gfx950-sparse-logits-fix branch from b6a29ad to ac65d1f Compare August 3, 2026 12:15
Run the indexer decode QK dot on fp8 operands (fp8 MFMA, f32 accum) instead of
upcasting q/kv to f32 with input_precision="ieee". fp8 upcasts exactly, so
products are unchanged. Add tl.static_assert(HEAD_SIZE % 4 == 0) for the
block_size*D//4 scale offset (D=128, compile-time only).

Kernel 4.4-4.7x faster at decode batch 256-512 on gfx942, 8-9x on gfx950.
gfx942: bit-identical to the f32 dot. gfx950: ~1e-5 rel (fp8-MFMA accum order),
top-k preserved; GSM8K/RULER A/B incl. MTP=3 show no regression.

Signed-off-by: Frida Andersson <fanderss@amd.com>
@hellozhuo-amd

Copy link
Copy Markdown
Contributor

I have reviewed the code under ticket.
The original fp32 tl.dot was changed to fp8 version, and additional assert regarding head_size was added. Performance and accuracy looks sound, by comparing with the stock kernel and the torch reference respectively.
The code looks good to me!
A few things to mention:

  1. "VLLM_DSV4_LOGITS_FIX" causes additional env var to the original vllm, we may need additional changes in the code to avoid that (but let's first wait for the outputs from the vllm experts).
  2. There is a gap in accuracy (86% at 16K context length and 92% at 32K from the PR) compared with SGLang (100% at 16K and 98% at 32K). However, the accuracy matches the numbers using torch reference. This may need to be further investigated (i.e., vLLM vs. SGLang).

Comment thread vllm/v1/attention/ops/rocm_aiter_mla_sparse.py Outdated
Route the gfx942/gfx950 decode/MTP paged MQA-logits path through the
Triton kernel unconditionally instead of behind VLLM_DSV4_LOGITS_FIX,
per review feedback to keep the OOTB experience flag-free.

Signed-off-by: frida-andersson <fanderss@amd.com>
@frida-andersson

Copy link
Copy Markdown
Contributor Author

Thanks @tjtanaa for the review! I've addressed the comment now by removing the VLLM_DSV4_LOGITS_FIX env var so the Triton indexer path is now always-on for gfx942/gfx950 (no flags needed). Re-validated that it matches the torch reference and serves cleanly on the standard recipe. PTAL when you get a chance

# gfx942/gfx950: AITER's paged MQA-logits decode kernel scores the sparse
# indexer incorrectly and collapses long-context retrieval, so route the
# decode/MTP path through the Triton kernel instead.
if _ON_GFX950 or _ON_GFX942:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So AITER's paged MQA-logits decode kernel has issue for all cases? If we do this, we will be introducingperf regression to all models as it is going to use triton or even torch, rather than gluon kernel from aiter.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the comment! AITER's kernel is fine for the SHUFFLE layout DSv32 uses, the bug is DSv4-specific, where the compressor writes block-flat but deepgemm_fp8_paged_mqa_logits reads shuffled. This only hits sparse-indexer decode, not other models. The gating is too wide though, I'll tighten it so DSv32/GLM keep the current gluon path

@frida-andersson
frida-andersson force-pushed the dsv4-gfx950-sparse-logits-fix branch 4 times, most recently from f78e56b to ef2eaae Compare August 19, 2026 13:29
@frida-andersson
frida-andersson force-pushed the dsv4-gfx950-sparse-logits-fix branch from f8dfd17 to eb09a3c Compare September 10, 2026 13:20
@frida-andersson

Copy link
Copy Markdown
Contributor Author

@tjtanaa the skip_k_cache_insert gate was still too broad as DSv3.2’s ROCm writer sets that too, so it was hitting the Triton kernel even though its cache is shuffled, not block-flat. I switched it to compress_ratio > 1 in eb09a3c. That arg was already on the indexer op: DSv4’s C4A indexer passes 4, DSv3.2 never sets it (stays 1). GLM goes through kpool and also doesn’t pass it, so it stays on AITER.

Flash NIAH is 100% through 32K, DSv32 GSM8K stays at stock (~0.96). PTAL

@frida-andersson
frida-andersson marked this pull request as ready for review September 10, 2026 13:30

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

@simondanielsson simondanielsson left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work :)

Comment thread vllm/v1/attention/ops/rocm_aiter_mla_sparse.py Outdated
Comment thread vllm/v1/attention/ops/rocm_aiter_mla_sparse.py
@frida-andersson

Copy link
Copy Markdown
Contributor Author

@tjtanaa ping for re-review. AMD CI is red on the NIXL test _failed_recv_reqs, same failure is on today’s main nightly (build 12959)

@frida-andersson
frida-andersson force-pushed the dsv4-gfx950-sparse-logits-fix branch from 5db60ee to 5ff9782 Compare September 17, 2026 13:11
@frida-andersson

Copy link
Copy Markdown
Contributor Author

@simondanielsson @tjtanaa - gate change since your last look: Triton/NORMAL only when compress_ratio == 4 (V4.0 C4A). V4.1-Flash is SHUFFLE at ratio 1 and 2; the old >1 check sent ratio-2 layers to the wrong reader. Verified gfx950 TP4: V4-Flash → Triton C4A, V4.1 and DSv3.2 → AITER. PTAL

@mergify

mergify Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Hi @frida-andersson, the pre-commit checks have failed. Please run:

uv pip install pre-commit>=4.5.1
pre-commit install
pre-commit run --all-files

Then, commit the changes and push to your branch.

For future commits, pre-commit will run automatically on changed files before each commit.

V4.1-Flash writes SHUFFLE and has indexer ratios 1 and 2. compress_ratio>1
sent ratio-2 layers down the Triton block-flat reader while ratio-1 stayed
on AITER. Only V4.0 C4A (ratio 4) is values-then-scales.

Signed-off-by: Frida Andersson <fanderss@amd.com>
@frida-andersson
frida-andersson force-pushed the dsv4-gfx950-sparse-logits-fix branch from 5ff9782 to da0c592 Compare September 17, 2026 13:27

@tjtanaa tjtanaa left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@tjtanaa

tjtanaa commented Sep 18, 2026

Copy link
Copy Markdown
Member

/ci run

@tjtanaa
tjtanaa enabled auto-merge (squash) September 18, 2026 10:25
@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #89826 for commit 25feae92ff52.

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

Labels

deepseek Related to DeepSeek models DSv4 ready ONLY add when PR is ready to merge/full CI is needed rocm Related to AMD ROCm v1

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

6 participants