Repository navigation
[Triton/Gluon] [gfx950] unified_attention: split-KV 3D configs for head_size <= 128 keyed on max_seqlen_q - #6983
Conversation
🏷️ CI GuideRuns automatically on every PR:
Extended tests (opt-in via labels):
One backend per PR: PR title tags & labels: |
|
@cagrikymk @vgokhale Could you review? It only touches the gfx950 triton unified-attention DEFAULT.json; q = 1 resolves to the same configs as main (lookup scan in the description). Fork PR, so the workflow needs a maintainer approval to run. Thanks! |
There was a problem hiding this comment.
🟡 Changes recommended
The wider query tile exposes out-of-bounds query-bias reads because the 3D kernel’s bias load does not mask padded query rows.
1 open issue
What changed in this PR
This PR tunes gfx950 Triton unified attention for multi-token queries on the split-KV path, while preserving single-token decode settings.
Changes:
- Adds query-length (
Q) matching to the 3D attention and KV-split schemas. - Increases query tile size for head sizes ≤128 when Q≥4.
- Adjusts KV splitting for head sizes 65–128 when Q≥2, preserving existing splits for smaller heads.
| File | Description |
|---|---|
aiter/ops/triton/configs/gfx950/triton/attention/unified_attention/DEFAULT.json |
Adds query-length-specific tile and split settings for gfx950. |
Review effort: Balanced
Thank you for the contribution! |
603961c to
9d1fd50
Compare
@cagrikymk Thanks for the review! Below are all the q >= 2 shapes that get slower: 16 of the 143 measured are below 0.97x. How the numbers were taken:
Where it regresses:
The push also fixes the out-of-bounds |
…_size <= 128 keyed on max_seqlen_q Add a Q axis to the attn_3d and kv_split schemas of the gfx950 triton unified-attention table: - attn_3d D_LEQ_128.Q_GEQ_4: BLOCK_M 32. - kv_split D_LEQ_128.Q_GEQ_2: 16 segments per CU with a 256-token segment floor. - kv_split D_LEQ_64: main's D_LEQ_128 split, so head_size <= 64 keeps it. max_seqlen_q == 1 resolves to the same configs as before. Mask padded query rows when loading qq_bias in the 2D and 3D triton kernels. A query length that is not a multiple of BLOCK_Q read qq_bias rows past the tensor; the wider BLOCK_M makes that reachable at q = 4. Add a qq_bias test with padded query rows on both paths.
9d1fd50 to
935301c
Compare
|
@cagrikymk The gfx1250 skip is in 935301c (thread above). Could you take another look when you have time? The workflows also still need a maintainer approval to run. Thanks! |
NOT READY FOR HUMAN REVIEWPR Self-ReviewRevision Waiting for the PR author. The readiness report will appear after the self-review answers are complete for this revision. Questions to answer in the PR description
Edit the Missing or invalid answers
Shadow mode reports what would block but keeps the status successful. Set |
|
@lishuoshuo-amd LGTM! Triggered the CI |
|
@cagrikymk Thanks for the approval! Triton Test and OPUS Test passed. The only failure is one Aiter Test shard (Standard Tests (1 GPU) ... 8, 7), where the self-hosted runner lost communication before the tests started, so the gate failed with it. Could you re-run the failed jobs? |
|
@azaidy @cagrikymk @Boss2002n This PR has been approved; could you please re-trigger the CI? Thanks! |
@lishuoshuo-amd Triggered the CI and merged main to the branch |
|
@cagrikymk The CI has finished running; could you please help with the merge? Thanks! |
- Resolve conflicts with ROCm#6889 (window-aware split) and ROCm#6983 (Q axis in the attn_3d and kv_split schemas). - Kernels: keep main's split_window_tiles carving next to the length-aware per-sequence segment count. - compute_segment_params: keep the num_segments helper and apply main's SPLIT_FIT_BUDGET / SPLIT_SEQS_PER_TILE in it. - Config: keep kv_split D_EQ_64.DT_bf16_fp8 next to main's new D_LEQ_64 and D_LEQ_128.Q_GEQ_2 entries. - Config: add SPLIT_RUNS_PER_XCD to the new attn_3d D_LEQ_128.Q_GEQ_4 entry so q>=4 length-aware calls do not assert.
- Main's attn_3d D_LEQ_128.Q_GEQ_4 entry (ROCm#6983) sets BLOCK_M 32, which pads q4 verify blocks to twice their rows. - A new SPLIT_BLOCK_M key cuts BLOCK_M to max(SPLIT_BLOCK_M, query rows) on the length-aware launch only. - The eligibility check still uses the tuned BLOCK_M, and the uniform split keeps BLOCK_M 32. - Test: a q=8 skewed case and q=1/4/8 config-key checks in test_length_aware_split_scope.
…usal - Pick up ROCm#7088's merge with main (ROCm#6983 Q-axis configs through ROCm#7302) so this stacked PR is conflict-free again. - Pick up ROCm#7088's SPLIT_BLOCK_M, which keeps q4 draft verify blocks at 16 rows under main's BLOCK_M 32 entry. - No textual conflicts; the CAUSAL constexpr composes with the merged split code.
…usal - Pick up ROCm#7088's merge with main (ROCm#6983 Q-axis configs through ROCm#7302) so this stacked PR is conflict-free again. - Pick up ROCm#7088's SPLIT_BLOCK_M, which keeps q4 draft verify blocks at 16 rows under main's BLOCK_M 32 entry. - No textual conflicts; the CAUSAL constexpr composes with the merged split code.

Summary
On gfx950, a triton unified-attention call with
head_size <= 128that takes the split-KV (3D) path resolves toattn_3d.any(BLOCK_M 16) andkv_split.D_LEQ_128(SEGMENTS_PER_CU 4). For multi-token queries at long context, as in speculative-decode verify, the grid is too small: a 16q/1kv head, 23 sequences, q=4, context p50 114K gets 16 KV segments and takes 0.774 ms per call; with this change it takes 0.419 ms.This PR adds a
Qaxis to theattn_3dandkv_splitschemas and three entries:attn_3dD_LEQ_128.Q_GEQ_4BLOCK_M 32, rest asanykv_splitD_LEQ_128.Q_GEQ_2SEGMENTS_PER_CU 16,SEGMENT_TILE_MIN/MAX 256, rest asD_LEQ_128kv_splitD_LEQ_64D_LEQ_128on mainmax_seqlen_q == 1resolves to the same configs as on main, so decode does not change. In SGLang with speculative decoding, total throughput goes up 11-22% on Llama-3.1-8B and Qwen3-14B-FP8.It also masks padded query rows when the 2D and 3D triton kernels load
qq_bias(see "qq_bias padded query rows" below). That out-of-bounds read already exists on main for query lengths that are not a multiple ofBLOCK_Q.BLOCK_M 32makes it reachable at q = 4 as well.Change
Lookup scan with
unified_attention_utils._lookupover head_size {32, 64, 96, 128, 192, 256, 512} x Q {1, 2, 3, 4, 8, 16, 64, 300, 4096} x q/kv dtype {bf16/fp8, bf16/bf16, fp8/fp8} x sliding window {off, on}: the resolved config changes only forattn_3dat head_size <= 128 with Q >= 4, and forkv_splitat 64 < head_size <= 128 with Q >= 2. Q = 1, head_size > 128,attn_2dandreduceresolve to the same configs as on main. head_size 96 falls in the changed range; I did not measure a head_size 96 model.How the values were picked: a sweep of 57 variants (
BLOCK_M16/32 xSEGMENTS_PER_CU4/8/16/32 xSEGMENT_TILE64/128/256/512 xMAX_SEGMENTS128/256) over 143 q >= 2 shapes, two GPU replicas (median disagreement 0.4%), choosing per keyable cell (head_size, q = 2 vs q >= 4). Findings that shaped the entries:BLOCK_M 32at q >= 4. At q = 2 it halves the query blocks without filling them, so q = 2 keeps 16.MAX_SEGMENTS 256made batch-1 shapes slower (down to 0.58x).BLOCK_M 32and main's split.BLOCK_M 32and 32 segments per CU to every q >= 2 call instead gives a 1.17x geomean with worst case 0.63x; the entries here give 1.23x with worst case 0.81x.When these entries are read on gfx950:
unified_attentiontakes the gluon kernel when_gfx950_gluon_supportedholds: q and kv dtypes match (bf16/bf16 or fp8/fp8), head_size is a power of two <= 256, and there is no softcap, alibi or qq_bias. Otherwise it reads this triton table, for example with bf16 q and an fp8 KV cache, a softcap, or head_size 96/192.use_2d_kerneltakes the 2D kernel when there is a sliding window, whenmax_seqlen_k <= 512, or when the 2D grid (num_2d_prgms, about (tokens / BLOCK_Q + seqs) x kv heads) exceeds4 x CU(1024 on MI355X). So the 3D entries serve long-context calls with a small or medium batch; for Llama-8B (8 kv heads) at q = 4 that is up to about 64 sequences, and gpt-oss at 64 sequences already takes 2D.In SGLang (aiter backend, fp8 KV) this is the speculative-decode verify/extend step; q = 1 decode quantizes q to fp8 and goes to gluon.
I measured only gfx950.
qq_bias padded query rows
Both triton kernels compute
query_pos = q_block_local_idx * BLOCK_Q + offs_m // num_queries_per_kvand loadqq_bias_ptr + query_pos[:, None] * qq_bias_stride_0 + key_rel_pos[None, :]withmask=is_query_key[None, :]. Rows past the sequence's query length are not masked. When the query length is not a multiple ofBLOCK_Q, the last query block therefore readsqq_biasrows that do not exist. On main this happens withBLOCK_M 16and 4 query heads per KV head at q = 3, 5, 6 and 7. Withattn_3dBLOCK_M 32,BLOCK_Qbecomes 8 and q = 4 reads past the tensor too; this is the case Copilot flagged. The padded rows are already-infand are not stored, so valid outputs do not change, but the read itself is out of bounds.The fix adds
query_mask_0[:, None]to the threeqq_biasloads: two in the 2D kernel and one in the 3D kernel.USE_QQ_BIASis atl.constexpr, so calls withoutqq_biascompile to the same code, and none of the measurements below change.To show the read, a probe places
qq_biasat the end of a mapped HIP VMM page whose next page is reserved but unmapped. Any read pastqq_biasthen faults. The probe uses 32q/8kv, D 128, bf16, page 16, 1 sequence, and compares the output against a torch reference with the bias:The new unit test checks
qq_biasresults with padded rows on both paths. It cannot detect the read itself, because the read does not change the output.Results (MI355X, gfx950, ROCm 10.0.0, Triton 3.8.0)
Micro, aiter main be0237f source, CUDA-graph timing over distinct KV "layers", speedup = main / this PR, geo-mean of two GPU replicas:
13 of the 16 slower shapes run 1 or 2 sequences. The worst are 16q/1kv q=4 at batch 1 (0.81-0.86x at 64K-150K):
BLOCK_M 32halves the query blocks and the split is already atMAX_SEGMENTS, so the grid shrinks. The others are Llama q=4 at 4K with 2-4 sequences (0.86-0.87x) and 16q/1kv q=2 with 16 sequences (0.89-0.91x). These calls take 10-96 us. The config lookup has no batch-size axis, so a table entry cannot separate batch 1 from batch 16; a batch-aware key would be a separate change to the lookup.All 16 shapes below 0.97x. Times are per call, averaged over the two GPU replicas, and
main / PRis the geomean of the two per-GPU ratios. Page size 1 is the SGLang layout and 16 the vLLM layout. The last column is the 3D kernel'sNUM_SEGMENTSandBLOCK_Mon each side:End to end, SGLang 52a6f70 (
--attention-backend aiter --kv-cache-dtype fp8_e4m3, TP1, CUDA graphs on), real-text prompts, OSL 1000. For models without sliding window, SGLang's aiter backend takes this kernel only withSGLANG_USE_AITER_UNIFIED_ATTN=1(not the default), which these runs set. Speculative decoding is either an EAGLE3 draft (--speculative-num-steps 3 --speculative-eagle-topk 1 --speculative-num-draft-tokens 4) or NGRAM as a linear chain (--speculative-ngram-max-bfs-breadth 1 --speculative-num-draft-tokens 4), so verify runs q = 4 in both. The arms differ only in thisDEFAULT.json, swapped through the aiter config directory on the image's aiter build. Each arm: 4 consecutive runs, a fresh server for each run, run 1 discarded, median of runs 2-4; order main -> this PR -> main repeat. A per-process probe confirmed that verify calls resolve to the new entries and that prefill stays on the 2D kernel.Within-arm spread of total tok/s is 0.2-3.7% (gpt-oss up to 4.4%). Speculative acceptance length is the same in both arms of every row (Qwen 1.5 / 1.4, Llama NGRAM 1.8, EAGLE3 1.1, gpt-oss 1.5), so the gain is attention time in verify. With the low acceptance on these prompts, speculative decoding is itself slower than plain decode here; every row compares the two configs with the same speculative setting. The gemma-4 row compares main's table with that all-q >= 2 variant on the same SGLang commit; head_size 256/512 reaches neither. MiniMax-M3 TP4 + EAGLE3, measured with the same variant on SGLang 3ea7e210, was flat: only 4 of its 61 attention call sites per step take this kernel.
Accuracy, GSM8K with the Llama-8B + EAGLE3 server, chat API, greedy, 1319 questions: main 0.8021, this PR 0.8044, main repeat 0.7953.
Test plan
On gfx950, rebased on main 036a6ce:
test_unified_attention.py::test_triton_unified_attn, triton backend ids (2048): 1024 passed, 1024 skipped (skip shuffled_kv_cache, 2d gluon not available), the same counts as before the rebase and as with main's table.test_unified_attention.py::test_triton_unified_attn_3d: 64 passed, 32 skipped.test_triton_unified_attn_qq_bias_padded_query_rows: 5 passed. It covers 32q/8kv at q = 3 and 4 and 16q/1kv at q = 3 with KV 4096 on the 3D path, and 32q/8kv at q = 3 and 5 with KV 256 on the 2D path.ref_paged_attngains an optionalqq_bias.The micro and end-to-end numbers were measured on main be0237f. Between that commit and 036a6ce, this table, the lookup and the triton kernels are unchanged. The wrapper changed in two ways: #6581 casts
NUM_SEGMENTSandTILE_SIZEto Python ints, which gives the same launch values, and #6896 is gfx1250 only. The new mask only compiles in whenqq_biasis passed.Not covered: vLLM end to end (no vLLM environment was available), and head_size 96, which falls in the changed range.
Powered by Hyperloom, with human review, writing and testing.