Repository navigation
perf(hip): keep prefill GEMMs spill-free on clang 23 - #459
Conversation
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The resource test can silently succeed when it decodes zero kernels.
Review effort: Balanced
Findings: 1
What changed in this PR
Eliminates clang 23 register spills in Qwen prefill kernels and adds regression detection.
Changes:
- Constrains kernel scheduling, unrolling, and SLP vectorization.
- Reworks Flash-Next cache selection to avoid scratch memory.
- Adds kernel-resource validation and performance documentation.
| File | Description |
|---|---|
CMakeLists.txt |
Registers the kernel-resource CTest. |
src/models/qwen/CMakeLists.txt |
Disables SLP for affected HIP units. |
src/models/qwen/hip/kernels/prefill_quant_gemm.hip |
Bounds unrolling and scheduling. |
src/models/qwen/hip/kernels/prefill_quant_gemm.hpp |
Bounds K-block unrolling. |
src/models/qwen38_flash_next/kernels/rocm/kernels.hip.cpp |
Replaces indexed cache selection with mask blending. |
tools/ci/check-kernel-resources.py |
Implements resource extraction and comparison. |
tools/ci/kernel-resources.json |
Defines accepted scratch-memory limits. |
docs/PERFORMANCE.md |
Documents resource-check usage. |
docs/models/qwen3.8-27b/EXPERIMENTS.md |
Records Qwen performance results. |
docs/models/qwen3.8-flash-next/EXPERIMENTS.md |
Records Flash-Next results. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Interesting, similar to #255 . It looks like ROCm 10 has also some small benefit of around ~20 tok/s on Qwen 27B |
|
@francescobozzo is there anything else you'd like me to address to get this merged? |
|
Overall looks good, but my Strix Halo is currently doing some long running benchmarks and I'd like to verify locally a few of these numbers on it. About DeepSeek V4 Flash, ASR, TTS, Qwen Image 2.1: are we spill-free there or you didn't check these models yet? |
|
VGPR spills by model:
Qwen-Image-2.1, MiniMax-H3, ASR and TTS are spill-free with the two compilers.
DeepSeek V4 Flash is not spill-free with either compiler. Its
The three worse kernels are the DS4 items in "Remaining clang 23 items" of the PR description:
Since this is not a regression, it's probably better to fix in a separate PR. |
Add error handling for missing AMDGPU kernels. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
ec89536 to
9bccebc
Compare
|
LGTM, thanks! This will make the migration to ROCm 10 easier. Also, nice findings on ds4 flash |

Summary
AMD clang 23 is the HIP compiler of ROCm 7.14 and ROCm 10.0. With clang 23, the
blocked WMMA prefill GEMMs of Qwen3.8-27B spill registers. One Flash-Next GEMM keeps a private
array in scratch memory. On current
main(05688f9), a clang 23 build has these results,compared with the published 0.8.1 image (clang 22):
gufo bench, tok/sWithout this PR, a clang 23 build makes Q8 prefill of 2048 tokens 36% slower, and prompts of
9 to 95 tokens about 3 times slower. Decode does not change.
This PR removes the spills with source constraints. It does not use hidden compiler options or
a compiler version test. It also adds a kernel resource check: a CTest that fails if a kernel
uses more scratch memory than a baseline allows. Thus a later compiler cannot add a spill and
go unseen.
The greedy output does not change. On clang 22, the pinned toolchain, pp2048 and decode
change by 1.1% or less, and prefill of 16 to 64 tokens is 1.0% to 1.9% slower (see "Cost on
clang 22").
Changes
src/models/qwen/hip/kernels/prefill_quant_gemm.hipW8A8BlockedWmmaGEMMKernel: one__builtin_amdgcn_sched_barrier(0)at the end of each K block. The K-block loop uses#pragma unroll(BK <= 2 ? BK : 1).src/models/qwen/hip/kernels/prefill_quant_gemm.hppWKQuantA8BlockedWmmaGEMMKernel: the same#pragma unroll(BK <= 2 ? BK : 1). No fence: the fence makes clang 22 spill in the Q3_K tile.src/models/qwen/CMakeLists.txt-fno-slp-vectorizeforprefill_quant_gemm.hipandprefill_quant_wave64.hip, on all compilers.src/models/qwen38_flash_next/kernels/rocm/kernels.hip.cppRoutedF16GEMMKernel: blend the four cached code chunks with masks. Before, a conditional overuint4lvalues selected one chunk.tools/ci/check-kernel-resources.py,tools/ci/kernel-resources.jsonCMakeLists.txtkernel_resources_test(labelscpu;kernels, HIP builds only).docs/PERFORMANCE.md,docs/models/qwen3.8-27b/EXPERIMENTS.md,docs/models/qwen3.8-flash-next/EXPERIMENTS.mdThe arithmetic order does not change.
Causes
RoutedF16GEMMKernelpair tilesuint4lvalues becomes an address select, andcode_cachestays in scratch. Anif/elsechain does not help: clang 23 merges it into one load through a pointer phi.To find the stage of the W8A8 spill, we gave the optimized IR of clang 22 to the backend of
clang 23 (
-Xclang -disable-llvm-optzns). The spill occurred again with the same numbers.Why source constraints
A fix must continue to work with the next compiler. These constraints do not depend on one
compiler:
__builtin_amdgcn_sched_barrieris a documented builtin.-fno-slp-vectorizeis a documented driver flag.We rejected these alternatives. We measured them at
8a46cd9with clang 23 (Q8_K_XL). TheQwen3.8-27B kernel files are the same at
05688f9.-mllvm -amdgpu-load-store-vectorizer=false-fno-slp-vectorize -mllvm -amdgpu-use-amdgpu-trackers=1-fno-slp-vectorizealone (from #228)#pragma unroll 1on all K-block loops#if __clang_major__ >= 23(as #255)If a later compiler adds a spill,
kernel_resources_testfails and names the kernel.Results
All numbers are
gufo bench -r 3in tok/s, at05688f9. Two rounds, builds alternated. Themachine and the builds are in "Test setup". The DFlash2 columns use the Q4_K_M draft. "Before"
is
05688f9; "after" is05688f9with this PR.Qwen3.8-27B UD-Q8_K_XL, DFlash2:
Qwen3.8-27B UD-Q4_K_XL, DFlash2:
Qwen3.8 Flash-Next UD-Q4_K_XL, MTP:
Each cell shows the two rounds. The rows in italics give the change of the mean.
With this PR, clang 23 prefill is faster than the 0.8.1 image in all columns except Q8 pp64
(-4.4%) and Q8 pp16 (-1.4%). Decode is within 1.3% of the image, except Flash-Next MTP tg128
(+2.3%). That difference comes from the text, not from the kernels: the drafts follow the text,
and clang 23 gives a different Flash-Next text (see "Numerics").
Q4 pp2048 was not slow before this PR. At pp2048, Q4_K_XL uses
HalfPrefillGemmKernelfor mostof its GPU time (rocprofv3 at
8a46cd9), and clang 23 compiles that kernel as well as clang 22.The Q4 short prompts use the BK=4 tiles that spilled.
Cost on clang 22
Upstream builds with clang 22. Thus the "clang 22, after vs before" rows above are the cost of
this PR for the current toolchain:
The cause is in the register use (see "Register use"). With clang 22, four WKQuantA8 128x64
BK=4 tiles use more VGPRs without the K-block unroll: Q3_K (179 to 230 VGPRs, 8 to 6 waves),
and IQ4_NL, IQ3_S and IQ4_XS (9 to 8 waves). No kernel spills. Eleven kernels are better.
We chose constraints that do not depend on one compiler, and they have this small cost. If you
do not want this cost on clang 22, you can put the BK=4 unroll and the Flash-Next blend behind
a gate. The BK=2 fence does not change the occupancy of the W8A8 128x128 tiles on clang 22
(180 to 183 VGPRs, 8 waves), and Q8 pp2048 does not become slower.
kernel_resources_testprotects the kernels in all cases.
The clang 22 build over the ROCm 10 userspace is faster than the published 0.8.1 image (Q8
pp2048 476 against 454, without this PR), and its output is the same. Thus the ROCm 10
libraries are not the cause of the clang 23 slowdown.
Numerics
Greedy output: the first 12 hex digits of the SHA-256 of 160 generated tokens
(
gufo prompt -t 0, a 553-word prompt).1e504ca34bf6821f519124deea43a269d4aa4b063b5eeb2c4b063b5eeb2cea43a269d4aaea43a269d4aa--validate-prefill 1024gives the same result on all five builds(max_abs_diff 0.78307986, rmse 0.09793035, cosine 0.99944276, top-1 match).
Flash-Next unit compiles with
-O3 -ffast-math, and clang 23 makes different arithmetic. Aclang 22 build over the ROCm 10 libraries gives the text of the image. Thus the compiler causes
the difference, not the libraries. A move to clang 23 needs the Flash-Next and DeepSeek V4
Flash quality gates. This PR does not change that.
Kernel resource check
tools/ci/check-kernel-resources.pyreads the.hip_fatbinsection of a binary. It decodesthe AMDGPU metadata note of each code object. It needs no GPU. It needs no LLVM tool, except
clang-offload-bundlerwhen the bundles are compressed and Python has nozstd. It runs inabout 1.5 seconds on the 228 MB release binary.
check-kernel-resources.py BINARY --baseline tools/ci/kernel-resources.jsoncheck-kernel-resources.py NEW --compare OLDcheck-kernel-resources.py BINARY --report [FILTER]check-kernel-resources.py BINARY --write-baseline FILE --note TEXTThe baseline comes from the release binary of the published clang 22 image. It allows the
scratch of the 247 kernels that use scratch in that build. Mangled kernel names are the keys,
and the tool removes the version from the rocPRIM inline namespace. Thus a ROCm update does not
change the key of most kernels. An allowance for a kernel that no longer exists gives a note,
not a failure. Results at
05688f9:Thus
kernel_resources_testpasses on the current toolchain. On a clang 23 build it fails,also with this PR: 13 kernels remain (see "Remaining clang 23 items"). That is intended. The
test shows the work that a move to clang 23 needs. A clang 23 build can keep its own baseline
(
--write-baseline).--compareof the "after" build with the "before" build:scratch, but they lose one occupancy step (9 to 8 and 12 to 10 waves). The Flash-Next results
include this effect.
Not verified
nix build .#checks.x86_64-linux.prand the model quality gates. The machine has no Nix.We ran
tools/ci/check-docs.py,tools/ci/check-dependencies.pyandtools/ci/check-pr-title.py(all pass), andclang-format --dry-run --Werror21.1.8 on thethree changed C++ files (clean).
userspace, and the published image.
kernel_resources_testthrough CTest. We ran the same command by hand on all five builds.(checked in the source), so DeepSeek V4 Flash, ASR, TTS and image models do not change.
Related work
W8A8BlockedWmmaGEMMKernel, with#if __clang_major__ >= 23. The Qwen3.8-27B copy did notget the fence. This PR adds it without the version test.
-fno-slp-vectorizechange is in this PR, for two unitsand on all compilers. Its
roundfto__float2int_rnchange is not in this PR, because itchanges the numerics. A comment on ROCm 10 evaluation for gfx1151, and a clang 23 prefill fix #228 asks for a ROCm 10.1 test. We did not test ROCm 10.1.
kernel_resources_testgives the result for any compiler without a GPU.Test setup
profile was
accelerator-performance. No other GPU process ran; each run recorded this.Flash-Next UD-Q4_K_XL with the shared MTP Q8_0 head. All at the Hub revisions that the model
cards pin.
--device /dev/kfd --device /dev/dri/renderD128 --group-add keep-groups, models mounted read-only.ghcr.io/gufo-org/toolboxes/gufo-runtime:0.8.1(543300e).05688f9adds one commit to it, #449, which changes server code only.05688f9without / with this PR. CMakereleasepreset in AMD'srocm/dev-ubuntu-26.04:10.0.0-fullimage.CMAKE_HIP_COMPILER, over the ROCm 10.0.0 userspace.Commands (
M,D,FandTare the model, draft, Flash-Next and MTP files):Method
gufo benchwith a clang 22 and a clang 23 build. We used the same flags, the samemodel files and alternate runs. We measured pp2048, pp64, pp16 and pp8, because batches of
9 to 95 tokens and of 8 tokens or fewer use different kernel tiles.
rocprofv3 --kernel-trace. One kernel,W8A8BlockedWmmaGEMMKernel<128,128,2,4,2,true,true>, used 47% of the GPU time of Q8 pp2048.kernel used 256 VGPRs, 149 spilled VGPRs and 480 bytes of scratch. With clang 22 it used
181 VGPRs and no scratch. Its clang 23 code has 150
scratch_loadand 120scratch_storeinstructions. The clang 22 code has none.
(
--offload-device-only). With clang 23, 33 kernels have new or larger spills, 95 kernelshave less VGPR occupancy, and 355 kernels are better.
Thus the backend scheduler causes the W8A8 spill, not the optimizer.
ds_load_b32instructions into 32ds_load_2addr_stride64_b32instructions, and it moves them above the WMMAs of the currentK block.
sizes, speculative decode and the greedy output.
libraries. It is faster than the published image, and its output is the same. Thus the
slowdown comes only from clang 23.
Steps 1 to 8 were done at
8a46cd9. The Qwen3.8-27B kernel files did not change from8a46cd9to05688f9. The measurements in "Results" are new, at05688f9.Candidates (measured at 8a46cd9, clang 23, Q8_K_XL)
W8A8 kernel only. The K-quant changes are not in these builds, except in the last row.
#pragma unroll 1on all K-block loopsunroll(BK <= 2 ? BK : 1)unroll(BK <= 2 ? BK : 1)(chosen)-fno-slp-vectorize(this PR)K-quant kernels in
prefill_quant_gemm.hip, total spilled VGPRs of the 33 WKQuantA8instantiations:
unroll(BK <= 2 ? BK : 1)unroll(BK <= 2 ? BK : 1)+-fno-slp-vectorize(chosen)unroll(BK <= 2 ? BK : 1)unroll(...)+-fno-slp-vectorizeAn unconditional fence in
SmallBatchKQuantExactFp32GEMMKernelalso made one clang 22instantiation spill (30 VGPRs). Thus this PR does not change that kernel.
Flash-Next
RoutedF16GEMMKernelpair tiles, clang 23 scratch:uint4lvalues)if/elsechainext_vector_typestorageRegister use
VGPRs / spilled VGPRs at
05688f9, from the release binaries. A value in brackets isscratch without spills.
Remaining clang 23 items
With this PR, a clang 23 build has 13 kernels over the clang 22 baseline. In the measurements
above, we saw no cost from them. They are a list for a move to ROCm 10 (#228):
qfn_mmq::mul_mat_q<type 7, 96|112|128>(6 tiles, up to 58 spilled VGPRs). The Flash-NextQ4 workloads do not use them.
mul_mat_q<Q2_K>andmul_mat_q<IQ2_XXS>(2 tiles). They spill with clang 22 too.Clang 23 spills more.
HalfPrefillGemmKernel<256,256,8,4>Q4_K, Q6_K and IQ4_NL tiles (2 to 6 spilled VGPRs).SmallBatchKQuantExactFp32GEMMKernel<16,7,3,Q4_K>(6 spilled VGPRs).rms_norm_plain_regs_kernel<64>(1 spilled VGPR).Also, many
BatchedExactBf16GEMMFp32VecKernelinstances use more VGPRs with clang 23. Forexample,
<16,12,2,true,32,1,false>uses 61 VGPRs with clang 22 and 100 with clang 23 (16waves against 12). They do not spill. Decode and verification use them too, so this PR does
not change them.