Skip to content

[ROCm][DSv4.1] Support the NVFP4 compressed KV cache on gfx950 - #57463

Open
JohnQinAMD wants to merge 1 commit into
vllm-project:mainfrom
JohnQinAMD:rocm-dsv41-nvfp4-kv
Open

JohnQinAMD wants to merge 1 commit into
vllm-project:mainfrom
JohnQinAMD:rocm-dsv41-nvfp4-kv

Conversation

@JohnQinAMD

@JohnQinAMD JohnQinAMD commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Purpose

--kv-cache-dtype nvfp4_ds_mla cannot be selected on ROCm at all today. The pack in _fp32x2_to_fp4x2 is PTX, f is not an AMDGPU asm constraint, and every worker aborts during startup with unknown asm constraint 'f'. Reading was missing too: only FlashMLA's SM100 sparse decode could decode the record, so the dtype was rejected everywhere else.

The V4.1 compressed record (#56935) packs 512 e2m1 values two per byte, then 32 e4m3 scales of 16 dims each, and sits beside an fp8 sliding-window cache — 288 B/token against the 584 B V4 record.

Approach

  • _fp32x2_to_fp4x2 gains a gfx950 branch using v_cvt_scalef32_pk_fp4_f32. That instruction takes the low nibble first where the PTX one takes the high nibble, divides by the E8M0 of a third operand (1.0 leaves the value alone), and writes only the destination's low byte.
  • _load_v41_nvfp4_gfx950_chunk decodes a nibble pair with v_cvt_scalef32_pk_f32_fp4. That instruction's scale operand is E8M0 too, so this record's e4m3 scale is applied separately, and its destination is a register pair, so the result comes back as one 64-bit value whose halves are the even and odd dim of the byte.
  • The gfx950 decode tile picks the record from the per-token width the KV-cache spec already publishes. The sliding-window record and the fp8 path are untouched.
  • reads_nvfp4_compressed_cache lets a layer whose kernel can decode the record opt in, replacing a gate on SM100 alone. gfx942 still rejects the dtype, now with an accurate message.

The PTX pack has two other callers — the indexer Q quantization and the V4 indexer's MXFP4 K cache — so the same fix unblocks those write paths on ROCm as well, though neither is reachable yet for other reasons.

Test plan

pytest tests/kernels/attention/test_rocm_sparse_mla_v41_records.py
pytest tests/kernels/test_fused_indexer_q_rope_quant.py -k fp32x2

The decode test packs known rows into each record, drives rocm_sparse_attn_decode, and compares against a torch softmax over the same dequantized rows, parametrized over the record pairs the model allocates so the V4 arm is the regression case. The pack test checks the gfx950 byte against cvt.rn.satfinite.e2m1x2.f32 over every level and its negation, both signed zeros, the exact ties, and values past the ends of the format.

Test result

MI355X (gfx950): tests pass, the pack is byte-exact, cosine 0.999998 against the torch reference for the NVFP4 arm and unchanged for the fp8 arm.

Decode latency at the shape this model runs on 4 ranks — 16 local heads (num_attention_heads 64 // TP 4), index_topk 512, sliding window 128 — captured in a HIP graph with prebuilt ragged indices:

decode tokens fp8 584 B NVFP4 288 B
1 0.0271 ms 0.0272 ms
8 0.0287 ms 0.0285 ms
32 0.0307 ms 0.0303 ms
64 0.0366 ms 0.0367 ms

Half the compressed-cache bytes at the same decode cost, within 1% across the range.

Accuracy. GSM8K over all 1319 questions, 5-shot, TP=4: 90.30% on the 584 B record against 89.92% here, inside the 0.83 pt standard error at that sample size.

Capacity. fp8_ds_mla is untouched, so nothing regresses — but the new dtype measures below it as things stand, 123.31x against 135.58x max concurrency at TP=4 and 65,536 context. The compressed cache is only ~10% of this model's per-block bytes (the sliding-window cache is 78%), and the smaller page also shrinks _get_packed_kv_cache_groups' anchor, which splits the unrelated state buckets from 9 groups into 17. That is a pre-existing, platform-independent issue in the grouping heuristic rather than something this PR introduces.

What makes the record pay off is narrowing the sliding-window cache alongside it. With the V4.1 MXFP8 record in place the pair reaches 11,020,825 tokens and 168.16x, +24.0% over the V4 baseline.

Known follow-up

  • Serving DeepSeek-V4.1 on ROCm additionally needs [Bugfix][ROCm] Handle MoE padding sentinel in AITER fused experts #57036.
  • The sliding-window half of the +24.0% is a separate PR.
  • The grouping-anchor behaviour above is worth fixing on its own; it is platform-independent and also affects SM100, where nvfp4_ds_mla keeps a wider sliding-window record beside the 288 B one.

AI assistance (Claude Code) was used in preparing this change.


Essential Elements of an Effective PR Description Checklist
  • The purpose of the PR, such as "Fix some issue (link existing issues this PR will resolve)".
  • The test plan, such as providing test command.
  • The test results, such as pasting the results comparison before and after, or e2e results
  • (Optional) The necessary documentation update, such as updating supported_models.md and examples for a new model.

DeepSeek-V4.1 stores its compressed KV as NVFP4 (288 B/token: 512 e2m1 values
packed two per byte, then 32 e4m3 scales of 16 dims each) next to an fp8
sliding-window cache. On ROCm neither end of that record worked: the pack is
PTX, and reading it was gated on SM100, so `--kv-cache-dtype nvfp4_ds_mla` was
rejected outright and ROCm kept the 584 B V4 record for both caches.

Writing: `_fp32x2_to_fp4x2` gains a gfx950 branch. `f` is not an AMDGPU asm
constraint, so the PTX pack aborted every worker at startup with `unknown asm
constraint 'f'`. `v_cvt_scalef32_pk_fp4_f32` replaces it -- it takes the low
nibble first, divides by the E8M0 of a third operand (1.0 leaves the value
alone) and writes only the destination's low byte.

Reading: an NVFP4 row loader in the gfx950 sparse decode tile, selected per
cache from the record width the KV-cache spec publishes, decoding a nibble pair
with `v_cvt_scalef32_pk_f32_fp4`. That instruction's scale operand is E8M0 too,
so the e4m3 scale is applied separately, and its destination is a register
pair, so the result is taken as one 64-bit value whose halves are the even and
odd dim of the byte. A layer whose kernel can read the record now opts in
rather than the gate testing for SM100 alone. The sliding-window record and the
fp8 path are unchanged.

Measured on MI355X (gfx950). The pack matches `cvt.rn.satfinite.e2m1x2.f32`
byte for byte over every level, both signed zeros, the exact ties and the
saturating ends. Decode, at the shape this model runs on 4 ranks (16 local
heads, index_topk 512, sliding window 128) captured in a HIP graph with
prebuilt ragged indices, stays within 1% of the 584 B record from 1 to 64
decode tokens, for a compressed cache half the size, and GSM8K over 1319
questions is 90.30% on the 584 B record against 89.92% on this one. Capacity
is a separate matter: the compressed cache is ~10% of this model's per-block
bytes, so the smaller record is worth +2.5% max concurrency, and realising even
that needs a fix to the KV-cache grouping heuristic that is filed separately.

This change was written with AI assistance (Claude Code).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: JohnQinAMD <yanyuan.qin@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 commented Sep 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, @JohnQinAMD.

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

@mergify mergify Bot added the needs-rebase label Sep 29, 2026

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

deepseek Related to DeepSeek models DSv4 DSv4.1 Related to DeepSeek-V4.1 models needs-rebase rocm Related to AMD ROCm

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

1 participant