Skip to content

[Perf] Choose the RDNA3 GEMM tile from the shape - #980

Open
vlluvia wants to merge 2 commits into
ROCm:mainfrom
vlluvia:feat/rdna3-gemm-tile-autotune
Open

[Perf] Choose the RDNA3 GEMM tile from the shape#980
vlluvia wants to merge 2 commits into
ROCm:mainfrom
vlluvia:feat/rdna3-gemm-tile-autotune

Conversation

@vlluvia

@vlluvia vlluvia commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Stacked on #979. Base had to be main because the base branch must live in this repo, so the diff currently also shows #979's commit. Review only the last commit, [Perf] Choose the RDNA3 GEMM tile from the shape — it touches 5 files and zero lines of rdna3_f16_gemm.py. The diff collapses to those 5 files once #979 merges.

Why

rdna3_f16_gemm builds whatever tile it is handed and defaults to 128x128x32. That tile is right once the problem fills the grid, but it cuts only 4 workgroups at 256x256 and 16 at 512x512, so on a 96-CU part most CUs idle no matter how good the inner loop is. Choosing the tile from the shape is worth up to 3.0x there.

How

rdna3_f16_gemm_autotune owns the decision in two layers, leaving the kernel untouched:

  • pick_tile is a heuristic fitted to a sweep of every feasible tile on 27 shapes. It needs no GPU and no measurement, and it is what a call resolves to with nothing configured — the wrapper benchmarks nothing by default.
  • Above it sits the shared autotuner: FLYDSL_AUTOTUNE=1 sweeps feasible_tiles for real, and the result can be frozen into an offline artifact.

feasible_tiles doubles as the search space: anything it excludes does not divide the shape, cannot fill the prefetch pipeline, or does not fit in LDS, so benchmarking it would only measure a build failure.

Why 64x64x64 as the default

Not the widest tile that covers the machine, which is the tempting rule. Measured on gfx1100, 64x64x64 is fastest on 16 of the 27 shapes and holds 50-59 TFLOP/s throughout, where 128x128x32 swings between 40 and 72. Taking the widest covering tile cost up to 37% and averaged 6.5%; against the per-shape fastest tile this heuristic averages 0.6%, worst case 8.1%.

Two limits worth knowing

  • NUM_CU is hard-coded for gfx1100, so the thresholds do not transfer to a gfx11 part with a different CU count, and shapes outside the fitted set are extrapolation. The search exists for both cases.
  • _graph_bench captures a CUDA graph to get under the ~90us launch overhead that would otherwise swamp these kernels, but it still reads the multi-wave tiles a few us high below about 50us. A tuned result for a short kernel is a hypothesis to confirm rather than a fact.

Tests

test_rdna3_tile_selection pins the heuristic against the measured shapes; test_rdna3_gemm_autotune checks that the untuned path resolves to pick_tile and that the default is reachable by the search. Both are GPU-free. test_rdna_gemm adds a device check that the wrapper agrees with the heuristic path.

Full suite on gfx1100 (Radeon Pro W7900): 2216 passed, 0 failed. #979 alone, with these files removed from disk, is 1606 passed, 0 failed.

Also points the gfx11 benchmark path at the wrapper so its numbers reflect the chosen tile rather than the default.

image

@skyguan92

Copy link
Copy Markdown
Contributor

Native gfx1100 current-main replay looks good in the scope of this PR.

I replayed the final PR commit patch-id-equivalently onto current main 3df2d7c70059b53f352d83226f9ee8edeceecb33 after #979 merged. The replay head is c7a6dddfa15587d2ad8d5b294ffba6b516b222f7; the final-commit patch ID is unchanged. Environment: Radeon PRO W7900, native gfx1100, ROCm/HIP 7.2, PyTorch 2.9.1+gitff65f5b, with HSA_OVERRIDE_GFX_VERSION unset.

Focused results:

  • GPU-free selection/autotune tests: 608 passed, 322 skipped.
  • Added device cases: 2 passed (256x256x4096, 1024x1024x1024).
  • Every feasible tile for the three rows below matched the FP32 torch reference at BF16 atol=rtol=0.05.

Kernel-only timing used HIP events around graphs containing 200 direct launches, 4 graph replays per round, 31 rounds, alternating full-ladder order. P50 selected vs pre-selection 128x128x32 default:

MxNxK selected p50 old default p50 speedup selected rank
256x256x4096 30.776 us 95.441 us 3.101x fastest of 4
512x512x1024 18.395 us 27.674 us 1.504x fastest of 5
1024x1024x1024 40.388 us 46.518 us 1.152x fastest of 5

I also checked the failing Navi workflow job. Its device test output reaches ALL TESTS PASSED; the job fails afterward at cat: bench_*.csv: No such file or directory, so I do not see a Navi kernel failure attributable to this change.

Scope boundary only, not a request to expand this PR: formal DeepSeek-V4-Flash decode rows with M={1,5,16} and (N,K) in {(4096,1024),(1024,4096),(4096,512),(4096,2048),(2048,4096)} all have feasible_tiles=[] and reject at the existing M % BLOCK_M assertion. I am treating small-M/tail handling as a separate follow-on rather than a blocker here.

@vlluvia
vlluvia force-pushed the feat/rdna3-gemm-tile-autotune branch 2 times, most recently from 76b2399 to 588c045 Compare August 12, 2026 01:59
@vlluvia

vlluvia commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

/rerun-ci

rdna3_f16_gemm builds whatever tile it is handed and defaults to
128x128x32. That tile is right once the problem fills the grid, but it cuts
only 4 workgroups at 256x256 and 16 at 512x512, so on a 96-CU part most CUs
idle no matter how good the inner loop is. Choosing the tile from the shape
is worth up to 3.0x there.

rdna3_f16_gemm_autotune owns that decision in two layers. pick_tile is a
heuristic fitted to a sweep of every feasible tile on 27 shapes; it needs no
GPU and no measurement, and it is what a call resolves to with nothing
configured, so the wrapper benchmarks nothing by default. Above it sits the
shared autotuner: FLYDSL_AUTOTUNE=1 sweeps feasible_tiles for real, and the
result can be frozen into an offline artifact.

The heuristic defaults to 64x64x64 rather than the widest tile that covers
the machine. Measured on gfx1100 it is fastest on 16 of the 27 shapes and
holds 50-59 TFLOP/s throughout, where 128x128x32 swings between 40 and 72.
Taking the widest covering tile cost up to 37% and averaged 6.5%; against
the per-shape fastest tile this averages 0.6%, worst case 8.1%.

Two limits worth knowing. NUM_CU is hard-coded for gfx1100, so the
thresholds do not transfer to a gfx11 part with a different CU count, and
shapes outside the fitted set are extrapolation -- the search exists for
both cases. And _graph_bench, which captures a CUDA graph to get under the
~90us launch overhead that would otherwise swamp these kernels, still reads
the multi-wave tiles a few us high below about 50us, so a tuned result for a
short kernel is a hypothesis to confirm rather than a fact.

feasible_tiles doubles as the search space: anything it excludes does not
divide the shape, cannot fill the prefetch pipeline, or does not fit in LDS,
so benchmarking it would only measure a build failure.

Points the gfx11 benchmark path at the wrapper so its numbers reflect the
chosen tile rather than the default.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vlluvia
vlluvia force-pushed the feat/rdna3-gemm-tile-autotune branch from 588c045 to 6c8e7c3 Compare August 13, 2026 03:51
@vlluvia

vlluvia commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

/rerun-ci

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