Skip to content

[ROCm][RDNA] Skinny GEMMs: no VGPR spills on gfx11/gfx12, wvSplitK launch rows for gfx1150/gfx1151 - #1401

Merged
roberteg16 merged 4 commits into
gfx11from
rogarcia.skinny-gemm-no-spill
Oct 26, 2026
Merged

roberteg16 merged 4 commits into
gfx11from
rogarcia.skinny-gemm-no-spill

Conversation

@roberteg16

@roberteg16 roberteg16 commented Oct 24, 2026 •

Copy link
Copy Markdown

Summary

The skinny GEMMs (wvSplitK*) spilled VGPRs in many instantiations on gfx11/gfx12. A spilling GEMM is slower, and its speed depends on earlier kernels: the runtime sizes every wave's scratch slot to the largest scratch used so far in the process, and with large slots the spills go to DRAM.

  • bf16/fp16 spills: bf16 multiplies with v_dot2_f32_bf16, and the launch rows below only use tiles that fit the wave's VGPRs. 0 VGPR spills on gfx1150, gfx1151, gfx1200, gfx1201 (78-102 before). The kernels compile to the same instructions as the same tiles upstream.
  • int4 spills: tiles only selected for N = 1 are no longer built for N = 2..5 (1684 -> 1204 kernels); on 1024-VGPR archs the spilling MoE tile for N = 1, K = 2048 is replaced. Kernel code unchanged.
  • wvSplitK launch rows: one tile per arch, from a sweep of every non-spilling launch configuration:
    • gfx1151: (YTILE 1, UNRL 4, A_CHUNK 16); (2, 4, 8) for K < 1024.
    • gfx1150/1152/1153: (2, 1, 16); (1, 2, 16) when M <= 32 * CUs.
    • A_CHUNK 8 when K % 16 != 0.
    • Other archs: generic rows.

gfx9 code is unchanged.

Results

Spill fixes against gfx11 (bench_rocm_skinny_gemm.py, 95 shape x N cells per dtype, N = 1..5; int4 W4A16 group 128, M = 1..5):

gfx1150 gfx1151 gfx1201
bf16 1.072x 1.025x 1.003x
fp16 1.006x 1.000x 1.000x
int4 MoE 1.18x 1.00x 1.00x
int4 dense 0.99x (same instructions) 1.00x 1.00x

Launch rows against the previous rows (interleaved A/B, cold caches, 43 shapes x N = 1..5, bf16/fp16, 13 shapes not used for tuning, odd M, K 768/1000/2880, bias): gfx1150 +0.6% to +1.1%, gfx1151 +0.2% to +1.3%. They reach 99.3-99.8% of the per-shape best tile (previous rows 98-99%, upstream's 96-98%).

The 4096x2048 bf16 GEMM on gfx1150 takes 188 us whatever scratch earlier kernels used (236-525 us before).

Tests

On gfx1150 and gfx1151:

  • test_rocm_skinny_gemms.py: 190 passed (new K % 16 != 0 and N = 5 cases). test_wvsplitk_fused_silu.py: 124 passed.
  • test_rdna_hybrid_w4a16.py: 102 passed. test_hybrid_w4a16_moe.py: 70 passed.
  • N = 5 bf16/fp16 and int4 against a reference: 54/54.

gfx1201 (spill commits): test_rocm_skinny_gemms.py 311 passed; its fp8 cases run out of memory with and without this PR.

pre-commit passes.

Overlap

#1246 (draft) also changes wvSplitK_hf_sml_ and the gfx11 dispatch; the two will conflict textually.

AI assistance was used; the submitter reviews every line.

@roberteg16
roberteg16 requested a review from mgehre-amd October 26, 2026 08:45
@roberteg16
roberteg16 marked this pull request as ready for review October 26, 2026 08:45
@roberteg16
roberteg16 removed the request for review from dllehr-amd October 26, 2026 08:45
Comment thread csrc/rocm/skinny_gemms.cu Outdated
roberteg16 and others added 3 commits October 26, 2026 12:16
wvSplitK_hf_sml_, wvSplitK_hf_ and wvSplitK_hf_big_ held every activation
chunk of an unrolled step in registers next to the weights, and bf16
widened the weights to f32. With 1024-thread workgroups a wave gets 128
VGPRs on gfx1150 (192 on gfx1151), and 102 of the 388 kernels spilled on
gfx1150, 78 on gfx1151. A spilling GEMM is slow, and its speed depends on
the largest scratch an earlier kernel of the process used (1.3x slower
after a 256 B/lane kernel, 2.1x after 4 KiB).

On gfx1x:
- bf16 multiplies with v_dot2_f32_bf16, as fp16 with v_dot2_f32_f16.
- A tile holds the activation chunks while (N + YTILE) * UNRL *
  A_CHUNK / 2 operand VGPRs plus 64 fit the wave's VGPRs; otherwise it
  reads each chunk from LDS where it is used.
gfx9 code is unchanged.

No VGPR spills in the 388 kernels on gfx1150, gfx1151 and gfx1201.
bench_rocm_skinny_gemm.py, N 1..4, against gfx11: gfx1150 bf16 1.050x,
fp16 1.008x; gfx1151 1.00x. The 4096x2048 bf16 GEMM keeps 186-210 us
whatever scratch an earlier kernel used (242-651 us before).

test_rocm_skinny_gemms.py: 172 passed on gfx1150 and gfx1151.

AI assistance was used for this patch.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Robert Esclapez Garcia <robert.garcia@amd.com>
…only reachable tiles built

The int4 dispatch macros expand every tile for N = 1..5, so tiles only
picked for N = 1 were also built for N = 2..5, where they are never
launched: 480 of the 1684 kernels, and most of the spilling ones. Those
arms are now `if constexpr (__N == 1)`; the dispatch is unchanged.

The MoE (YTILE 4, W 32, AC 32, UNRL 4) tile for N = 1, K = 2048 spills
with the 128 VGPRs a wave gets on a 1024-VGPR arch: gfx1102, gfx1103 and
gfx1150..gfx1153 take (4, 4, 16, 16) there; gfx1151 keeps it.

Kernel code is unchanged. Spilling kernels: 334 -> 24 on gfx1150 (the
tile above, not launched there), 293 -> 8 on gfx1151 (at the VGPR limit,
as before).

W4A16 group 128, M = 1..4, against gfx11: gfx1150 MoE 1.19x, dense
0.99x-1.00x; gfx1151 1.00x.

test_rdna_hybrid_w4a16.py (102 passed), test_hybrid_w4a16_moe.py (70
passed) and test_rocm_skinny_gemms.py (172 passed) on gfx1150 and
gfx1151.

AI assistance was used for this patch.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Robert Esclapez Garcia <robert.garcia@amd.com>
…151 and gfx1150

The wave32 wvSplitK dispatch was one chain for every gfx11 arch, tuned
shape by shape on gfx1151. A sweep of every non-spilling launch
configuration (YTILE 1-4, UNRL 1/2/4/8, A_CHUNK 8/16/32, WvPrGrp 16/32,
N = 1..5, bf16 and fp16) finds one tile per arch within ~1% of the best
for nearly every shape:

- gfx1151: (YTILE 1, UNRL 4, A_CHUNK 16); (2, 4, 8) for K < 1024.
- gfx1150, gfx1152, gfx1153: (2, 1, 16); (1, 2, 16) when M <= 32 * CUs.
- A_CHUNK 8 when K % 16 != 0: wider chunks give wrong results there.

Other archs take the generic rows. The sweep op covers every knob the
dispatcher uses, and sweep_bf16_kernel.py pads the weights as they are
padded at load.

Interleaved A/B against the previous rows, 43 shapes (13 not used for
tuning, odd M, K 768/1000/2880), N = 1..5, bf16/fp16 and bias:
gfx1151 +0.2% to +1.3%, gfx1150 +0.6% to +1.1%; against upstream's rows
+1.8% to +4.2% and +0.8% to +3.9%. 99.3-99.8% of the per-shape best.

test_rocm_skinny_gemms.py: 190 passed on gfx1150 and gfx1151, with new
K % 16 != 0 and N = 5 cases.

AI assistance was used for this patch.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Robert Esclapez Garcia <robert.garcia@amd.com>
@roberteg16
roberteg16 force-pushed the rogarcia.skinny-gemm-no-spill branch from 9eb172c to f993e6b Compare October 26, 2026 19:17
@roberteg16 roberteg16 changed the title [ROCm][RDNA] Skinny GEMMs: no VGPR spills on gfx11/gfx12 (bf16/fp16, int4) [ROCm][RDNA] Skinny GEMMs: no VGPR spills on gfx11/gfx12, wvSplitK launch rows for gfx1150/gfx1151 Oct 26, 2026
The fallback that reads each activation chunk from LDS when a tile's
operands exceed the wave's VGPRs only served the large tiles of the
previous rows. No launched tile needs it now (the tuned rows, the generic
rows and the fused epilogues all fit), so the kernels go back to holding
the activation chunks, as before. 0 VGPR spills on gfx1150 and gfx1151;
the launched kernels compile to the same instructions as the same tiles
upstream.

AI assistance was used for this patch.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Robert Esclapez Garcia <robert.garcia@amd.com>
@roberteg16
roberteg16 merged commit 513950f into gfx11 Oct 26, 2026
9 checks passed
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.

2 participants