Skip to content

[ROCm][Attention] Wide MHA decode attention kernel for gfx1151 - #1087

Draft
roberteg16 wants to merge 3 commits into
gfx11from
rogarcia.wide-decode-attn
Draft

[ROCm][Attention] Wide MHA decode attention kernel for gfx1151#1087
roberteg16 wants to merge 3 commits into
gfx11from
rogarcia.wide-decode-attn

Conversation

@roberteg16

@roberteg16 roberteg16 commented Aug 12, 2026

Copy link
Copy Markdown

Summary

Adds rdna35_causal_mha_attn, a split/reduce decode-attention kernel for MHA
(num_heads == num_kv_heads) on gfx1151, as a fast path inside TRITON_ATTN.
Enabled by default on gfx1151; VLLM_ROCM_RDNA35_CAUSAL_MHA=0 forces Triton and
=1 forces it on. Anything the host predicate declines falls through to
unified_attention unchanged, so the path is A/B-able at runtime.

One wave owns each (sequence, KV-head-group, KV segment) and writes fp32
partials; a reduce pass merges them with the usual log-sum-exp rescale. fp16 and
bf16, using v_dot2_f32_f16 / v_dot2_f32_bf16. The body is gfx11-only with
stubs elsewhere, so the TU links in multi-arch and CDNA builds.

End-to-end

TheBloke/Llama-2-7B-AWQ (32×32×128), input 1920 / output 128, --max-num-seqs 1,
three repetitions per mode — all three identical, so this is not run-to-run noise:

Triton this kernel
Decode 42.2 tok/s 44.7 tok/s +5.9%
TPOT 23.71 ms 22.38 ms −5.6%
Prefill 2266 tok/s 2264 tok/s unchanged

Prefill is untouched by construction — the fast path is decode-only. The fast
path was confirmed to actually fire (64 invocations with it on, 0 with it off,
identical generated text).

Kernel benchmarks

312 cells: head counts 8/16/32/64 × head_dim 64/128/256/512 × M 1/4 × ctx
128..65536, fp16, block_size 16, against unified_attention's 2D and 3D paths
on the same inputs. Geomean 1.87× vs 2D and 1.79× vs 3D; 283/312 cells beat
both paths; median 88% of the 238 GiB/s streaming peak.

shape (Hq×Hkv×D) vs Triton 2D vs Triton 3D % peak BW (median)
8x8x64 2.27× 1.73× 71%
16x16x64 1.50× 1.38× 79%
32x32x64 1.11× 1.18× 84%
64x64x64 1.11× 1.08× 87%
8x8x128 2.14× 1.48× 78%
16x16x128 1.60× 1.37× 84%
32x32x128 1.49× 1.46× 87%
64x64x128 1.39× 1.43× 90%
8x8x256 3.06× 2.02× 84%
16x16x256 1.79× 1.51× 88%
32x32x256 1.56× 1.90× 91%
64x64x256 1.36× 1.72× 92%
8x8x512 4.24× 2.92× 89%
16x16x512 3.47× 2.66× 91%
32x32x512 2.34× 3.38× 92%
64x64x512 1.72× 2.91× 91%
Per-cell detail (160 rows, ctx 128 / 1024 / 4096 / 16384 / 65536)
shape M ctx Triton 2D Triton 3D this kernel vs 2D vs 3D GiB/s % peak
8x8x128 1 128 10.1µs 12.9µs 10.4µs 0.97× 1.24× 47.2 20%
8x8x128 1 1024 53.6µs 28.0µs 27.8µs 1.93× 1.01× 140.6 59%
8x8x128 1 4096 200.5µs 84.5µs 86.5µs 2.32× 0.98× 180.7 76%
8x8x128 1 16384 775.5µs 294.2µs 298.1µs 2.60× 0.99× 209.7 88%
8x8x128 1 65536 3061.7µs 1137.0µs 1167.4µs 2.62× 0.97× 214.2 90%
8x8x128 4 128 11.7µs 11.7µs 11.1µs 1.06× 1.05× 45.5 19%
8x8x128 4 1024 53.7µs 53.6µs 27.4µs 1.96× 1.96× 143.1 60%
8x8x128 4 4096 200.5µs 200.6µs 83.1µs 2.41× 2.41× 188.3 79%
8x8x128 4 16384 776.0µs 776.1µs 289.0µs 2.68× 2.69× 216.3 91%
8x8x128 4 65536 3059.5µs 3070.4µs 1117.3µs 2.74× 2.75× 223.8 94%
8x8x256 1 128 23.2µs 27.3µs 13.1µs 1.76× 2.07× 74.9 31%
8x8x256 1 1024 139.7µs 62.7µs 45.7µs 3.06× 1.37× 171.2 72%
8x8x256 1 4096 516.8µs 192.5µs 155.7µs 3.32× 1.24× 200.8 84%
8x8x256 1 16384 2038.8µs 715.0µs 582.1µs 3.50× 1.23× 214.7 90%
8x8x256 1 65536 8101.9µs 2733.7µs 2289.4µs 3.54× 1.19× 218.4 92%
8x8x256 4 128 23.2µs 23.3µs 13.9µs 1.67× 1.68× 72.6 31%
8x8x256 4 1024 139.2µs 139.2µs 46.7µs 2.98× 2.98× 168.1 71%
8x8x256 4 4096 518.2µs 519.0µs 159.9µs 3.24× 3.25× 195.7 82%
8x8x256 4 16384 2038.8µs 2038.2µs 587.2µs 3.47× 3.47× 212.9 89%
8x8x256 4 65536 8107.2µs 8101.0µs 2315.6µs 3.50× 3.50× 215.9 91%
8x8x512 1 128 51.3µs 37.3µs 17.8µs 2.87× 2.09× 110.4 46%
8x8x512 1 1024 351.3µs 162.4µs 83.3µs 4.22× 1.95× 187.8 79%
8x8x512 1 4096 1335.6µs 589.7µs 294.7µs 4.53× 2.00× 212.1 89%
8x8x512 1 16384 5279.5µs 2292.1µs 1146.3µs 4.61× 2.00× 218.1 92%
8x8x512 1 65536 21246.0µs 9450.6µs 4538.5µs 4.68× 2.08× 220.3 93%
8x8x512 4 128 51.5µs 51.5µs 18.8µs 2.73× 2.73× 106.9 45%
8x8x512 4 1024 351.3µs 351.2µs 85.2µs 4.13× 4.12× 184.2 77%
8x8x512 4 4096 1335.5µs 1338.1µs 299.1µs 4.46× 4.47× 209.1 88%
8x8x512 4 16384 5301.8µs 5309.4µs 1152.0µs 4.60× 4.61× 217.1 91%
8x8x512 4 65536 21260.2µs 21351.3µs 4561.2µs 4.66× 4.68× 219.3 92%
16x16x128 1 128 14.7µs 15.5µs 13.0µs 1.13× 1.19× 75.6 32%
16x16x128 1 1024 76.4µs 53.6µs 45.7µs 1.67× 1.17× 171.0 72%
16x16x128 1 4096 267.0µs 186.7µs 156.3µs 1.71× 1.19× 199.9 84%
16x16x128 1 16384 1038.2µs 700.4µs 583.7µs 1.78× 1.20× 214.2 90%
16x16x128 1 65536 4110.6µs 2729.9µs 2297.9µs 1.79× 1.19× 217.6 91%
16x16x128 4 128 14.7µs 14.7µs 13.7µs 1.07× 1.07× 73.3 31%
16x16x128 4 1024 76.4µs 76.3µs 46.7µs 1.63× 1.63× 167.8 71%
16x16x128 4 4096 267.0µs 266.8µs 161.0µs 1.66× 1.66× 194.3 82%
16x16x128 4 16384 1039.1µs 1038.4µs 597.7µs 1.74× 1.74× 209.2 88%
16x16x128 4 65536 4106.2µs 4109.0µs 2346.7µs 1.75× 1.75× 213.1 90%
16x16x256 1 128 26.0µs 32.3µs 17.6µs 1.48× 1.83× 111.6 47%
16x16x256 1 1024 149.3µs 108.1µs 84.1µs 1.78× 1.29× 186.0 78%
16x16x256 1 4096 551.9µs 372.9µs 299.1µs 1.85× 1.25× 209.0 88%
16x16x256 1 16384 2167.5µs 1387.0µs 1165.4µs 1.86× 1.19× 214.5 90%
16x16x256 1 65536 8608.4µs 5402.0µs 4568.6µs 1.88× 1.18× 218.9 92%
16x16x256 4 128 26.0µs 25.9µs 18.6µs 1.40× 1.40× 108.5 46%
16x16x256 4 1024 148.8µs 149.1µs 85.9µs 1.73× 1.74× 182.6 77%
16x16x256 4 4096 554.9µs 551.4µs 304.2µs 1.82× 1.81× 205.7 86%
16x16x256 4 16384 2167.2µs 2160.9µs 1176.7µs 1.84× 1.84× 212.5 89%
16x16x256 4 65536 8581.6µs 8602.9µs 4628.6µs 1.85× 1.86× 216.1 91%
16x16x512 1 128 76.4µs 52.5µs 26.1µs 2.92× 2.01× 150.7 63%
16x16x512 1 1024 501.1µs 302.6µs 151.2µs 3.31× 2.00× 206.9 87%
16x16x512 1 4096 1977.0µs 1165.8µs 566.1µs 3.49× 2.06× 220.8 93%
16x16x512 1 16384 8021.6µs 4612.1µs 2238.5µs 3.58× 2.06× 223.4 94%
16x16x512 1 65536 41343.7µs 20678.3µs 8888.2µs 4.65× 2.33× 225.0 95%
16x16x512 4 128 76.4µs 76.3µs 27.1µs 2.82× 2.82× 148.7 62%
16x16x512 4 1024 511.7µs 505.9µs 158.4µs 3.23× 3.19× 198.1 83%
16x16x512 4 4096 2055.0µs 1988.6µs 583.6µs 3.52× 3.41× 214.4 90%
16x16x512 4 16384 8032.3µs 8189.3µs 2295.7µs 3.50× 3.57× 217.9 92%
16x16x512 4 65536 41417.5µs 41154.3µs 9030.3µs 4.59× 4.56× 221.5 93%
32x32x128 1 128 21.4µs 23.8µs 18.0µs 1.19× 1.32× 109.1 46%
32x32x128 1 1024 126.4µs 116.8µs 84.9µs 1.49× 1.38× 184.2 77%
32x32x128 1 4096 473.6µs 437.3µs 302.6µs 1.57× 1.45× 206.6 87%
32x32x128 1 16384 1866.8µs 1766.1µs 1175.5µs 1.59× 1.50× 212.7 89%
32x32x128 1 65536 7413.4µs 7086.9µs 4602.7µs 1.61× 1.54× 217.3 91%
32x32x128 4 128 21.4µs 21.4µs 18.7µs 1.14× 1.15× 107.6 45%
32x32x128 4 1024 126.6µs 126.6µs 87.4µs 1.45× 1.45× 179.5 75%
32x32x128 4 4096 474.3µs 473.2µs 309.9µs 1.53× 1.53× 201.9 85%
32x32x128 4 16384 1864.1µs 1862.6µs 1200.0µs 1.55× 1.55× 208.4 88%
32x32x128 4 65536 7429.9µs 7392.5µs 4699.4µs 1.58× 1.57× 212.8 89%
32x32x256 1 128 34.6µs 59.8µs 25.9µs 1.33× 2.31× 151.8 64%
32x32x256 1 1024 226.9µs 352.1µs 151.8µs 1.49× 2.32× 206.0 87%
32x32x256 1 4096 882.4µs 1335.9µs 566.2µs 1.56× 2.36× 220.8 93%
32x32x256 1 16384 3479.0µs 5290.3µs 2233.5µs 1.56× 2.37× 223.9 94%
32x32x256 1 65536 20411.7µs 22752.1µs 8881.2µs 2.30× 2.56× 225.2 95%
32x32x256 4 128 34.4µs 34.5µs 27.3µs 1.26× 1.26× 147.6 62%
32x32x256 4 1024 225.7µs 228.7µs 157.4µs 1.43× 1.45× 199.3 84%
32x32x256 4 4096 883.4µs 889.7µs 592.1µs 1.49× 1.50× 211.3 89%
32x32x256 4 16384 3558.5µs 3511.7µs 2282.3µs 1.56× 1.54× 219.1 92%
32x32x256 4 65536 20393.3µs 20339.3µs 9036.4µs 2.26× 2.25× 221.3 93%
32x32x512 1 128 96.1µs 187.6µs 43.2µs 2.23× 4.34× 182.2 77%
32x32x512 1 1024 654.5µs 1382.1µs 290.1µs 2.26× 4.76× 215.7 91%
32x32x512 1 4096 2604.8µs 5511.4µs 1125.1µs 2.32× 4.90× 222.3 93%
32x32x512 1 16384 10678.3µs 22080.2µs 4432.2µs 2.41× 4.98× 225.6 95%
32x32x512 1 65536 49900.2µs 105055.7µs 17741.4µs 2.81× 5.92× 225.5 95%
32x32x512 4 128 94.5µs 94.3µs 45.2µs 2.09× 2.08× 178.1 75%
32x32x512 4 1024 646.9µs 646.0µs 298.5µs 2.17× 2.16× 210.2 88%
32x32x512 4 4096 2534.2µs 2563.0µs 1143.5µs 2.22× 2.24× 218.8 92%
32x32x512 4 16384 10279.9µs 10274.7µs 4579.7µs 2.24× 2.24× 218.4 92%
32x32x512 4 65536 49973.2µs 49801.0µs 17888.2µs 2.79× 2.78× 223.6 94%
64x64x128 1 128 33.3µs 36.0µs 26.4µs 1.26× 1.37× 149.3 63%
64x64x128 1 1024 216.7µs 223.6µs 153.5µs 1.41× 1.46× 203.7 86%
64x64x128 1 4096 828.7µs 882.7µs 575.4µs 1.44× 1.53× 217.3 91%
64x64x128 1 16384 3254.3µs 3496.0µs 2260.7µs 1.44× 1.55× 221.2 93%
64x64x128 1 65536 12224.8µs 13938.5µs 8958.0µs 1.36× 1.56× 223.3 94%
64x64x128 4 128 33.5µs 33.5µs 27.5µs 1.22× 1.22× 146.7 62%
64x64x128 4 1024 218.1µs 217.9µs 159.8µs 1.36× 1.36× 196.3 82%
64x64x128 4 4096 829.1µs 830.6µs 592.0µs 1.40× 1.40× 211.4 89%
64x64x128 4 16384 3258.5µs 3257.4µs 2337.1µs 1.39× 1.39× 214.0 90%
64x64x128 4 65536 12228.8µs 12206.5µs 9169.7µs 1.33× 1.33× 218.1 92%
64x64x256 1 128 57.4µs 95.3µs 43.8µs 1.31× 2.18× 179.9 76%
64x64x256 1 1024 386.7µs 633.8µs 292.7µs 1.32× 2.17× 213.7 90%
64x64x256 1 4096 1501.7µs 2461.6µs 1137.5µs 1.32× 2.16× 219.8 92%
64x64x256 1 16384 5952.8µs 9720.9µs 4442.8µs 1.34× 2.19× 225.1 95%
64x64x256 1 65536 27649.9µs 40667.9µs 17768.0µs 1.56× 2.29× 225.1 95%
64x64x256 4 128 57.2µs 57.2µs 44.8µs 1.28× 1.28× 180.0 76%
64x64x256 4 1024 387.7µs 386.4µs 297.8µs 1.30× 1.30× 210.7 89%
64x64x256 4 4096 1517.6µs 1509.2µs 1144.2µs 1.33× 1.32× 218.7 92%
64x64x256 4 16384 5918.5µs 5946.7µs 4462.6µs 1.33× 1.33× 224.1 94%
64x64x256 4 65536 27643.4µs 27214.9µs 17970.3µs 1.54× 1.51× 222.6 94%
64x64x512 1 128 147.4µs 343.7µs 83.0µs 1.78× 4.14× 189.6 80%
64x64x512 1 1024 1021.0µs 2740.9µs 586.3µs 1.74× 4.67× 213.4 90%
64x64x512 1 4096 3986.3µs 11060.2µs 2297.7µs 1.73× 4.81× 217.7 91%
64x64x512 1 16384 15866.0µs 53010.7µs 9122.3µs 1.74× 5.81× 219.3 92%
64x64x512 1 65536 63417.1µs 212990.7µs 36535.8µs 1.74× 5.83× 219.0 92%
64x64x512 4 128 148.0µs 147.8µs 86.5µs 1.71× 1.71× 186.3 78%
64x64x512 4 1024 1023.2µs 1022.0µs 600.9µs 1.70× 1.70× 208.8 88%
64x64x512 4 4096 3986.3µs 3983.4µs 2356.5µs 1.69× 1.69× 212.4 89%
64x64x512 4 16384 15896.9µs 15829.9µs 9288.2µs 1.71× 1.70× 215.4 90%
64x64x512 4 65536 63327.9µs 63391.0µs 36877.0µs 1.72× 1.72× 217.0 91%
8x8x64 1 128 8.0µs 10.2µs 9.7µs 0.83× 1.05× 25.5 11%
8x8x64 1 1024 29.7µs 19.6µs 18.5µs 1.61× 1.06× 105.8 44%
8x8x64 1 4096 103.6µs 50.5µs 48.3µs 2.15× 1.05× 162.0 68%
8x8x64 1 16384 381.6µs 173.2µs 162.0µs 2.36× 1.07× 193.0 81%
8x8x64 1 65536 1497.6µs 655.1µs 601.6µs 2.49× 1.09× 207.8 87%
8x8x64 4 128 10.4µs 10.4µs 10.5µs 0.99× 0.98× 23.9 10%
8x8x64 4 1024 43.8µs 43.8µs 18.9µs 2.32× 2.32× 103.9 44%
8x8x64 4 4096 162.6µs 162.1µs 46.3µs 3.52× 3.51× 169.1 71%
8x8x64 4 16384 624.6µs 625.1µs 156.7µs 3.98× 3.99× 199.4 84%
8x8x64 4 65536 2460.8µs 2456.5µs 579.4µs 4.25× 4.24× 215.8 91%
16x16x64 1 128 9.4µs 10.9µs 10.4µs 0.90× 1.05× 47.2 20%
16x16x64 1 1024 30.7µs 27.8µs 27.8µs 1.11× 1.00× 140.8 59%
16x16x64 1 4096 108.1µs 85.8µs 85.8µs 1.26× 1.00× 182.1 77%
16x16x64 1 16384 402.4µs 307.0µs 297.7µs 1.35× 1.03× 210.0 88%
16x16x64 1 65536 1591.8µs 1196.9µs 1168.0µs 1.36× 1.02× 214.1 90%
16x16x64 4 128 10.9µs 10.9µs 11.1µs 0.98× 0.98× 45.5 19%
16x16x64 4 1024 46.8µs 46.8µs 27.5µs 1.70× 1.70× 142.6 60%
16x16x64 4 4096 174.0µs 174.0µs 83.6µs 2.08× 2.08× 187.1 79%
16x16x64 4 16384 669.8µs 670.0µs 289.8µs 2.31× 2.31× 215.8 91%
16x16x64 4 65536 2637.2µs 2635.2µs 1131.7µs 2.33× 2.33× 220.9 93%
32x32x64 1 128 11.1µs 13.9µs 13.0µs 0.85× 1.07× 75.8 32%
32x32x64 1 1024 41.2µs 46.8µs 46.3µs 0.89× 1.01× 169.1 71%
32x32x64 1 4096 149.2µs 164.0µs 156.4µs 0.95× 1.05× 199.9 84%
32x32x64 1 16384 577.2µs 622.9µs 600.5µs 0.96× 1.04× 208.2 87%
32x32x64 1 65536 2265.7µs 2448.2µs 2353.8µs 0.96× 1.04× 212.4 89%
32x32x64 4 128 13.6µs 13.6µs 14.2µs 0.96× 0.96× 70.8 30%
32x32x64 4 1024 61.3µs 61.2µs 48.0µs 1.28× 1.27× 163.4 69%
32x32x64 4 4096 228.1µs 227.6µs 160.3µs 1.42× 1.42× 195.1 82%
32x32x64 4 16384 878.6µs 878.1µs 601.5µs 1.46× 1.46× 207.9 87%
32x32x64 4 65536 3473.8µs 3474.6µs 2364.3µs 1.47× 1.47× 211.5 89%
64x64x64 1 128 16.8µs 18.4µs 17.5µs 0.96× 1.05× 112.4 47%
64x64x64 1 1024 93.5µs 88.2µs 84.0µs 1.11× 1.05× 186.2 78%
64x64x64 1 4096 335.3µs 307.4µs 303.6µs 1.10× 1.01× 205.9 87%
64x64x64 1 16384 1293.0µs 1184.7µs 1172.3µs 1.10× 1.01× 213.3 90%
64x64x64 1 65536 5129.2µs 4673.4µs 4632.4µs 1.11× 1.01× 215.9 91%
64x64x64 4 128 17.9µs 17.9µs 19.1µs 0.94× 0.94× 105.4 44%
64x64x64 4 1024 98.6µs 98.7µs 87.2µs 1.13× 1.13× 179.9 76%
64x64x64 4 4096 358.3µs 357.6µs 304.0µs 1.18× 1.18× 205.8 86%
64x64x64 4 16384 1393.6µs 1392.0µs 1174.3µs 1.19× 1.19× 212.9 89%
64x64x64 4 65536 5519.2µs 5520.5µs 4620.1µs 1.19× 1.19× 216.5 91%

The 29 cells that do not beat both paths are all head_dim 64, where a lane holds
the least work. The worst is 32 heads at M=1: 0.90–0.96× of the 2D path, though
still 1.05–1.07× of the 3D one, and 1.19–1.42× at M=4. Those shapes are routed
here anyway — one predicate covering every supported shape is easier to reason
about than a table of exceptions, and the loss is bounded and small.

Restrictions

Every one is a property of the kernel, and routing a case that violates one
produces wrong output rather than an error, so the host predicate
(rocm_wide_decode_attn.can_run) enforces all of them:

  • MHA only — one query head per KV head is baked into the wave mapping
  • head_size in {64, 128, 256, 512}
  • fp16 / bf16 with an unquantized KV cache — there is no scale in the ABI
  • causal, no sliding window, no ALiBi, no sinks, no fused output quant
  • AttentionType.DECODER only — ENCODER_DECODER cross-attention is not causal
  • NHD cache layout — HND permutes block_size and num_kv_heads within a block
  • decode-only batches (max_query_len <= 4)

The kernel's own knobs (KV segment count, unroll, KV heads per wave) are chosen
by a rule in the op, keyed on head size, head count and query length; callers
never pick a configuration.

Query lengths 2 and 3 are padded up to the M=4 kernel rather than instantiated.
Padding costs a measured geomean 1.024× and keeps every launch on one of the two
M values the tuning sweep covered; it also removes any need to reorder the
batch, which would have changed batch ordering for every TRITON_ATTN user on
every platform.

Test plan

  • pytest tests/kernels/attention/test_rdna35_causal_mha_attn.py — 82 passed
    on gfx1151 (shapes, dtypes, batches with unequal lengths, context edges,
    softcap, the predicate, and the gather/scatter roundtrip)
  • Fast path vs unified_attention on identical state: 1.2e-04 max abs diff
    over decode-only, irregular [1,3,2,4] and uniform M=4 batches
  • CUDA graph capture and replay on all three, zero deviation from eager
  • End-to-end A/B above, plus SmolLM2-1.7B in fp16: 16/16 sequences
    bit-identical to Triton
  • The C++ tuned rule and its Python mirror agree on all 4096
    (M, head_size, num_kv_heads) combinations
  • CI on a non-gfx11 build, to confirm the stub links

AI assistance (Claude) was used; every number above comes from a run on this
machine, and a human has reviewed the change.

@roberteg16
roberteg16 force-pushed the rogarcia.wide-decode-attn branch 4 times, most recently from 8fafabb to c9c3258 Compare August 12, 2026 14:43
roberteg16 and others added 3 commits August 12, 2026 09:28
Adds a split/reduce decode-attention kernel for MHA (num_heads == num_kv_heads)
on AMD RDNA3.5, a faster alternative to Triton's unified attention for that
shape. Enabled by default on gfx1151 (VLLM_ROCM_WIDE_DECODE_ATTN=0 forces
Triton, =1 forces on).

Measured against Triton over 120 cells (head counts 8/16/32/64 x head_dim
128/256/512 x M 1/4 x ctx 128..65536): 1.0-4.7x on the 2D path, 0.98-6.0x on
the 3D one, reaching 90-95% of the 238 GiB/s streaming peak at long context.
One wave owns each (sequence, KV head, KV segment) and writes fp32 partials; a
reduce pass merges them with the usual log-sum-exp rescale.

- csrc/rocm/wide_decode_attn.cu: the kernels, ported from the standalone
  attn_decode_hip/ harness where they were developed and tuned. fp16 and bf16
  (v_dot2_f32_f16 / v_dot2_f32_bf16); the body is gfx11-only with stubs on
  other arches so the TU links in multi-arch builds. Registered as
  torch.ops._rocm_C.wide_decode_attn; every constraint is a TORCH_CHECK, since
  the alternative failure mode is an unwritten output tensor.
- vllm/envs.py: VLLM_ROCM_WIDE_DECODE_ATTN tri-state (unset = default-on on
  gfx1151).
- vllm/v1/attention/ops/rocm_wide_decode_attn.py: the host-side predicate, a
  graph-safe custom op wrapper with a no-op fake, and the varlen -> fixed-M
  gather/scatter.
- vllm/v1/attention/backends/triton_attn.py: workspace allocation in the
  builder, and a fast path in forward() that falls through to
  unified_attention for anything the predicate declines.
- tests/kernels/attention/test_rocm_wide_decode_attn.py: kernel vs an explicit
  fp32 paged reference across shapes, dtypes, batches and context edges, plus
  the predicate and the gather/scatter roundtrip.

The predicate enforces every restriction the kernel has, because violating one
produces wrong output rather than an error: MHA only (one query head per KV
head is baked into the wave mapping), head_size in {64,128,256,512}, fp16/bf16
with an unquantized cache (there is no scale in the ABI), causal with no
sliding window / ALiBi / sinks, no fused output quant, and the NHD cache layout
(HND permutes block_size and num_kv_heads inside a block).

Only M=1 and M=4 are instantiated. Query lengths 2 and 3 are padded up to the
M=4 kernel rather than built: padding costs a measured geomean 1.024x (worst
1.081x, and in 8 of 120 cells M=4 is outright faster), and it keeps every
launch on one of the two M values the tuning sweep actually covered -- the
tuned rule steps UNROLL between M=2 and M=3 on 7 of 12 shapes, so instantiating
them would mean shipping configs fitted to no measurement. Padding goes at the
front because the mask is `pos > ctx_len + m`: real token j lands at row
M-len+j and sees exactly [0, num_computed+j], so seq_lens passes through
unmodified.

Padding also removes the need for a uniform-length decode block, so the batch
does not have to be reordered. That matters because reorder_batch_threshold is
taken as a min across every attention group -- setting it here would change
batch ordering for every TRITON_ATTN user on every platform, and would make
AttentionCGSupport.ALWAYS untrue. The fast path is confined to decode-only
batches and leaves all of that alone.

MEMORY AND CAPTURE CORRECTNESS. Four things the integration had to get right,
each of which fails only on hardware:

  The M=1 path must not slice the M dimension off the shared M=4 workspace:
  the shape is right but the strides are not (131072 where a dense block wants
  32768), and the kernels index the partials with raw pointer arithmetic, so
  it would read the wrong elements silently rather than fault. A dense view is
  carved over the front of the storage instead. Contiguity of the partials,
  block_table and seq_lens is checked at the op boundary; the KV caches
  deliberately are NOT required to be contiguous (they are k/v views into one
  [num_blocks, 2, ...] allocation), only dense within a block -- which also
  rejects HND.

  No device sync in the guard. Comparing num_actual_tokens against
  query_start_loc[-1] would call __bool__ on a device tensor, syncing per layer
  per forward and making the branch uncapturable. max_query_len <= 4 already
  bounds every request in the batch.

  No data-dependent shapes in the scatter. Selecting real rows with a boolean
  mask materializes via nonzero(), which syncs and cannot be captured, and
  TRITON_ATTN declares AttentionCGSupport.ALWAYS. build_gather_index returns a
  fixed num_seqs*M index that aims padding rows at a scratch slot instead.

  Padding counts are independent. gpu_model_runner pads seq_lens and
  num_actual_tokens separately under capture, so num_reqs is passed in
  explicitly and drives every slice; the workspace is sized for the larger of
  max_num_seqs and max_cudagraph_capture_size.

  Also: ENCODER_DECODER reaches forward() and its cross-attention is not
  causal, so the path is gated on attn_type == DECODER; and a sliding_window
  of 1 is stored as (0, 0) by this Impl, so the predicate receives W itself
  rather than testing the stored extent against 0.

TUNING. Three rules ported from the standalone harness, each fitted on
measurement:

  Unrolled reduce loops. Making the segment count a runtime argument dropped
  the unroll the templated version got for free -- geomean 1.011 over 240
  cells, up to 1.16x at short context where the reduce is 13% of the traffic.
  Unrolling both loops by 4 with independent accumulators brings it to 1.0026.

  KV segments capped at 32 when head_dim is 64. The cap of 16 was fitted over
  head_dim 128/256/512 where the quotient rarely reaches it; at head_dim 64
  vec2 is 1, so it binds on every shape. Over 136 cells the conditional cap
  takes the worst case from 1.424x to 1.123x, while raising it everywhere
  would regress Hq=8 at head_dim 128.

  Head grouping (HEADS_PER_WAVE). At head_dim 64 a lane holds 4 bytes and the
  wave reduces one dot2 per token, so the 5-step DPP chain becomes most of the
  inner loop. Adjacent KV heads are contiguous in the paged layout, so a wave
  takes 2 or 4 as one run: b128 loads and a 3-step reduction. The rule is 2
  heads per wave below 16 heads and 4 at or above, with a segment numerator of
  256 at head_dim 64 against 512 above -- both conditioned on head_dim 64,
  since applying either everywhere costs geomean 1.070 at head_dim >= 128
  against 1.012.

  P.V accumulation goes through elem_traits::accum_pv, emitting VOPD-pairable
  dot2 rather than v_fma_mix_f32. It removes all 64 fma_mix and changes the
  runtime by nothing -- the kernel sits at 97% of this machine's streaming
  ceiling and waits on memory, not issue -- but costs one VGPR less.

The C++ and Python copies of the tuned rule agree on all 4096
(M, head_size, num_kv_heads) combinations; the partials are indexed
[num_seqs, num_heads, nseg, M, head_size], so a disagreement would be a memory
fault, and the op re-derives the count and TORCH_CHECKs it.

Verified so far without a GPU: the source compiles for gfx1151 with both dtypes
instantiated symmetrically (112 kernels, zero scratch, occupancy 7-16), bf16
lowers to v_dot2_f32_bf16 rather than an fp32 fallback, the predicate rejects
all twelve unsupported cases, the M=4-padded mask reproduces Triton's
context_len = seq_len - cur_batch_query_len exactly for every real query
length, and the fixed-shape scatter roundtrips exactly on irregular lengths.
The test suite last ran green at 82 passed; it and an end-to-end A/B need to
run again on hardware.

AI assistance (Claude) was used.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Robert Esclapez-Garcia <rogarcia@amd.com>
…ters

Review feedback. No behaviour change except the last item.

NAMING. The kernel, the registered op, the source file, the Python module and
the env var are named for what they are: causal MHA decode attention on
RDNA3.5. VLLM_ROCM_WIDE_DECODE_ATTN becomes VLLM_ROCM_RDNA35_CAUSAL_MHA.

COMMENTS. Those that narrated how the code got here -- what was tried, what
regressed, what was removed and re-added -- are gone. What remains explains the
code as it stands, keeping the measurements that justify a constant or a
restriction. The file header now walks the seven phases of the split kernel in
order, from task decomposition to the partial write.

DEAD PARAMETERS. Three mode switches had a default that nothing ever overrode,
so each carried an unused arm through every instantiation:

  PV_DOT2_MODE   chose dot2 against a one-hot weight over the fma_mix form.
                 On by default; the fma_mix arm is deleted.
  BURST_MAX_MODE chose rescaling once per burst over once per token. Swept over
                 40 cells with both values compiled and timed, per-burst wins
                 38 of 40: forcing it on costs geomean 1.0005 against the best
                 config per cell, forcing it off costs 1.028-1.038 with a worst
                 case of 1.11.
  FAST_ADDR_MODE chose whether to emit a second address chain for bursts inside
                 one page. Its -1 mode resolved to VEC2_PER_LANE <= 8, and every
                 instantiated combination satisfies that -- head_dim 64 reaches
                 4 at most, and head_dim >= 128 only runs ungrouped, so
                 VEC2_PER_LANE is 2, 4 or 8. The condition was constant-true in
                 every build; the sweep agrees it should be (on wins 35 of 40).

The template goes from eight parameters to five. Resource usage is unchanged
across all 160 instantiations: no scratch, VGPR 18-196, occupancy 7-16.

ROUTING. head_size 64 with 32 heads at M=1 is no longer declined. It runs
0.90-0.96x of Triton's 2D path there (1.05-1.07x of the 3D one) and 1.19-1.42x
at M=4; one predicate covering every supported shape is easier to reason about
than a table of exceptions, and the loss is bounded and small.

Verified on gfx1151: pytest 82/82; the fast path matches unified_attention to
1.2e-04 over decode-only, irregular and uniform M=4 batches; all three capture
and replay under cudagraph with zero deviation; Llama-2-7B-AWQ end-to-end decode
44.7 tok/s against Triton's 42.2.

Signed-off-by: Robert Esclapez-Garcia <rogarcia@amd.com>
6672106e1 dropped the losing-shape table, so can_run() no longer looks at
max_query_len beyond the range check and its verdict is uniform over M=1..4
(checked over the 80 (num_heads, head_size) combinations). The builder's any()
probe existed only to catch a shape declined at M=1 and accepted at M=4 --
head_size 64 with 32 heads was the one entry -- so it collapses to a single
call, and the comment saying otherwise goes with it.

No behaviour change: the allocation is made for exactly the same layers.

Not run on device -- this session has no GPU access (not in the render group,
so /dev/kfd is unreadable). Verified by calling can_run() directly over the
shape matrix.

Signed-off-by: Robert Esclapez-Garcia <rogarcia@amd.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@roberteg16
roberteg16 force-pushed the rogarcia.wide-decode-attn branch from c9c3258 to 59e75e0 Compare August 12, 2026 15:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant