Skip to content

feat(attention): per-token fp8 KV and split-K verify in gfx1250 unified attention - #6660

Closed
lirui927 wants to merge 4 commits into
mainfrom
lirui/ua-gfx1250-per-token-kv-scale
Closed

lirui927 wants to merge 4 commits into
mainfrom
lirui/ua-gfx1250-per-token-kv-scale

Conversation

@lirui927

@lirui927 lirui927 commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Draft. Ready for review after the ATOM side lands; no blocking issues in this PR.

Motivation

MiniMax-M3 on gfx1250 decodes through the Gluon unified attention kernels with a page-shuffled fp8 KV cache that has one K and one V scale per cached token (LLM-Booster #211). Two gaps block it:

  1. The gfx1250 3d kernel reads only the first element of k_descale and v_descale. With a per-token cache the output is wrong without an error.
  2. A speculative verify (2 to 4 tokens per request) is not all-decode, so gfx1250 runs it on the 2d kernel. The 2d grid has no KV split. Three requests at 227K context take 16.5 ms per layer, which is 83% of the M3 target-verify time in the AgentX profile.

Technical details

  • Per-token KV scales (PER_TOKEN_KV_SCALE). The scales use the cache page layout [num_blocks, num_kv_heads, block_size], float32, contiguous. Each tile is one page and loads the scales of its tokens. The K scales multiply the QK columns. The V scales multiply the P columns after the softmax row sum. Slots past seq_len get scale 0.
  • P precision. P is split into a bf16 high part and a bf16 low part, with one WMMA for each. One truncated bf16 P biased the output low by about 0.3%. With the split the error is the same as an fp32 P @ V.
  • Short queries on the 3d kernel. On gfx1250 a batch that is not all-decode, with at most 4 tokens per request and one query position per query block (BLOCK_Q == 1), takes the 3d split-K kernel. A query block that ends before a segment stores an empty partial (M = -inf), so the reduce gives it weight 0. Other batches keep the 2d kernel.
  • The wrapper finds per-token scales by their shape. It raises NotImplementedError on paths that do not read them. Per-tensor scales use the same code as before.

Test plan

  • test_unified_attention_per_token_kv_scale: decode, GQA 16 and 8, head 128, page 16 and 128, context 1 to 4096, token magnitudes that differ by up to 20x, NaN in unused scale slots. Checks against the dequantized reference, the unquantized reference, and the output bias.
  • test_unified_attention_short_query_split: 2 to 4 tokens per request, bf16 and per-token fp8 KV, tails that end 1 to 4 tokens into a page, context up to 65537.
  • test_unified_attention_per_token_kv_scale_rejects_prefill.
  • Existing test_triton_unified_attn_3d.

Test results

gfx1250 (MI455), this branch on main @ 84077b3:

  • New and 3d tests: 99 passed, 16 skipped. ruff and black clean.
  • Full test_unified_attention.py: no test changes outcome against the same tree without the routing commit. Main already has failures in test_triton_unified_attn and a process abort in that file; both runs show them.
  • MiniMax-M3-MXFP4, TP4, 4 x MI455, full GSM8K 5-shot, with the ATOM change (feat(minimax-m3): per-token fp8 KV cache on gfx1250 through unified attention ATOM#2628):
KV cache flexible strict 8K/1K c4 output TPOT
bf16 0.9386 0.9393 375.79 tok/s 9.84 ms
per-token fp8, this PR 0.9409, 0.9401 (2 runs) 0.9416, 0.9409 358.33 tok/s 10.21 ms
Kernel accuracy and time (GQA 16, head 128, CUDA graph timing)

Accuracy against an fp32 reference on the same fp8 cache:

P @ V relative error output bias
one bf16 P, truncated 3.6e-3 -2.8e-3
bf16 high + low P (this PR) 1.65e-3 about 0
fp32 P (reference Triton kernel) 1.65e-3 about 0

Decode time, per-token fp8 vs bf16 KV:

batch, context, page fp8 (this PR) bf16
4, 8K, 128 17.8 us 12.3 us
4, 32K, 128 28.3 us 26.9 us
32, 8K, 128 22.6 us 15.6 us
4, 2K, 16 12.6 us 11.6 us
64, 2K, 16 16.4 us 13.3 us

Speculative verify, 3 requests x 4 tokens:

context 2d (before) 3d bf16 3d fp8
32K 2264 us 23.2 us 40.4 us
128K 9382 us 53.4 us 108.8 us
227K 16461 us 75.2 us 176.5 us

🤖 Generated with Claude Code

lirui927 and others added 3 commits October 19, 2026 06:08
… decode

The gfx1250 Gluon 3d kernel read only the first element of k_descale and
v_descale. A per-token KV cache (one scale per cached token and KV head,
[num_blocks, num_kv_heads, block_size]) then used one scale for all tokens.

Add PER_TOKEN_KV_SCALE. Each tile is one page and loads the K and V scales
of its tokens. The K scales multiply the QK columns. The V scales multiply
the P columns after the row sum. Slots past seq_len get scale 0.

The wrapper finds per-token scales by their page shape. It raises
NotImplementedError on paths that cannot read them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
P, scaled by the per-token V scales, was truncated to bf16 before P @ V.
The truncation biases the output low by about 0.3% against the fp32 row
sum. Split P into a bf16 high part and a bf16 low part and run two WMMAs.
The error then matches an fp32 PV, and decode time changes by 0-3%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ernel

On gfx1250 every batch that was not all-decode took the 2d kernel. The 2d
grid has one program per (KV head, query block), so a speculative verify of
a few requests over a long context leaves most CUs idle: 3 requests x 4
tokens at 227K context take 16.5 ms per layer.

Route queries of at most 4 tokens to the 3d split-K kernel when each query
block holds one query position (BLOCK_Q == 1). A query block that ends
before a segment now stores an empty partial, so the reduce weighs it 0.
The same shape takes 75 us (bf16 KV) or 177 us (per-token fp8 KV).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@lirui927 lirui927 self-assigned this Oct 19, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🏷️ CI Guide

Runs automatically on every PR:

  • ✅ Pre-checks (submodule verification, code formatting)
  • ✅ Aiter op tests (gfx942 + gfx950)
  • ✅ Triton tests on MI35X (only when aiter/ops/triton/** or related paths are changed)

Extended tests (opt-in via labels):

Label Tests
ci:gfx1250-ffm-triton Run the five-shard gfx1250 FFM Triton test suite
ci:triton-300x Run an additional Triton test job on MI300X in PRs (added automatically when gfx942 configs change); main branch always runs both MI35X and MI300X
ci:triton-355 Run the full Triton test suite on MI35X, not only the tests the change affects
ci:windows Windows JIT op smoke test on the gfx1201 ROCm runner
ci:multi-gpu Aiter multi-GPU tests on the 8-GPU runner
ci:sglang SGLang integration tests: DeepSeek-R1-MXFP4 accuracy, Qwen 3.5 accuracy
ci:atom ATOM benchmark: DeepSeek-R1-0528, GPT-OSS-120B
ci:atom_full ATOM accuracy suite for PR and main models from ATOM models_accuracy.json
ci:vllm vLLM benchmark: DeepSeek-V4-Pro, MiniMax-M3-MXFP4
ci:all All standard extended tests (excludes ci:atom_full)

Only add ci:atom_full for FlyDSL or Triton upgrades.
Add labels via the sidebar or gh pr edit 6660 --add-label <label>

One backend per PR:
A PR changes one kernel backend: [Triton/Gluon] (Triton and Gluon count as one), [HIP], [ASM], [CK], [OPUS] or [FlyDSL]. If the title ends up with two backend tags, split the PR -- as stacked pull requests when one part cannot merge without the other.

PR title tags & labels:
Component tags ([Triton/Gluon], [HIP], [CK], [ASM], ...) are added to the PR title and as PR labels automatically from the changed files and re-synced on every push — change-type tags like [fix]/[Perf], op tags like [MLA], and human labels (ci:*) are left untouched. Add the no-auto-title label to stop the title rewrites; labels stay in sync either way.

Rule 18 of aiter/ops/triton/README.md allows one or two sentences in a
comment. Shorten the docstrings of compute_pv_per_token and
_gfx1250_split_short_queries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI 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.

🟡 Changes recommended

Short-query routing admits unsupported dtype pairs, unsafe query blocks and strided outputs that the direct store cannot handle.

3 open issues
What changed in this PR

Adds per-token fp8 KV scales and short-query split-K execution to gfx1250 Gluon unified attention.

Changes:

  • Applies per-token scales with higher-precision probability accumulation.
  • Routes eligible short queries through the 3d kernel.
  • Adds accuracy, boundary and unsupported-path tests.
File Description
op_tests/​triton_tests/​attention/​test_unified_attention.py Tests per-token scales and short-query execution.
aiter/​ops/​triton/​attention/​unified_attention.py Validates scales and selects short-query routing.
aiter/​ops/​triton/​_gluon_kernels/​gfx1250/​attention/​unified_attention_3d.py Applies token scales and handles empty segments.

Review effort: Balanced

Comment on lines +184 to +185
backend != "gluon"
or not params.shuffled_kv_cache
backend != "gluon"
or not params.shuffled_kv_cache
or params.max_seqlen_q > _GFX1250_SPLIT_MAX_QUERY_LEN
or not is_3d_gluon_available(params, backend)
return False
config = get_unified_attention_config("attn_3d", params, backend="gluon")
block_m = max(config["BLOCK_M"], triton.next_power_of_2(params.num_queries_per_kv))
return block_m // params.num_queries_per_kv == 1
@lirui927

Copy link
Copy Markdown
Contributor Author

Closing. MiniMax-M3 on gfx1250 now takes the FlyDSL paged decode from #6462 (merged), which reads per-token FP8 KV scales itself, so unified attention no longer needs them. The ATOM side is on branch lirui/m3-gfx1250-flydsl-pa-decode.

@lirui927 lirui927 closed this Oct 26, 2026
@lirui927

Copy link
Copy Markdown
Contributor Author

Not needed: MiniMax-M3 decode on gfx1250 now runs on FlyDSL paged decode (#6462), see ROCm/ATOM#2710.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants