Skip to content

cuda/hip: optional wave64 for the quantized mat-vec kernels on RDNA - #89

Draft
mgehre-amd wants to merge 1 commit into
matthias.q4k-mmvq-gfx1151from
matthias.q4k-mmvq-wave64-gfx1151
Draft

cuda/hip: optional wave64 for the quantized mat-vec kernels on RDNA#89
mgehre-amd wants to merge 1 commit into
matthias.q4k-mmvq-gfx1151from
matthias.q4k-mmvq-wave64-gfx1151

Conversation

@mgehre-amd

Copy link
Copy Markdown
Collaborator

Adds GGML_HIP_MMVQ_WAVE64, off by default, which builds mmvq.cu with
-mwavefrontsize64.

@Annieren,
This is exploratory rather than a proposed default. RDNA can pick a wave size per kernel, and for
a couple of shapes wave64 is a clear win - but for more of them it is a clear loss. The point of
the PR is to expose the switch and record the measurements so the tradeoff is visible, rather
than have the next person rediscover it.

21 lines of change.

Which shapes benefit, and which don't

q4_K, rocprofv3 kernel time, median of 3 interleaved passes, gfx1151 / Radeon 8060S,
ROCm 7.15.0a20260728, GGML_CUDA_DQ_MMV=0:

shape wave32 wave64 delta
m=21504 n=1 k=5376 (gemma-4 fused FFN gate/up) 289.6 us 285.6 us -1.4% win
m=4096 n=4 k=14336 117.6 us 110.0 us -6.5% win
m=4096 n=8 k=14336 210.0 us 223.1 us +6.2% loss
m=4096 n=4 k=4096 35.1 us 40.5 us +15.4% loss
m=4096 n=1 k=4096 32.3 us 37.5 us +16.2% loss
m=4096 n=1 k=14336 54.1 us 69.6 us +28.7% loss

Two of six shapes benefit. The one that motivated the work - the gemma-4 fused FFN gate/up matvec

  • gains 1.4%. Before landing, we would need to investigate the other changes or guard the change to only the benefiting shapes.

The likely mechanism is the K-loop trip count. blocks_per_iter = vdr * nwarps * warp_size / qi,
so doubling the wave size halves the iteration count. At k=4096 with nwarps=2 that is two
iterations instead of four, and the 4x unroll from the parent PR has nothing left to pipeline.
The shapes that win are the ones with enough K to keep it fed.

Which types benefit, and which don't

All 21 mmvq types at m=4096 n=1 k=4096, median of 3 passes. Note this is the shape where q4_K
itself does worst, so read it as one slice, not a verdict:

improves >2% regresses >2%
iq2_xs -12.8% q4_K +18.7%
iq2_xxs -12.5% iq4_xs +15.5%
iq3_xxs -12.5% mxfp4 +9.8%
iq2_s -11.2% nvfp4 +9.1%
iq3_s -5.7% q2_K +7.7%
q4_0 +5.4%
iq1_s +4.7%
iq4_nl +4.4%
q5_0 +2.4%

Seven more (q6_K, q4_1, q5_K, q3_K, q8_0, q1_0, q5_1) are within +/-2%. The iq2/iq3 family is the
consistent winner; everything with a cheap vec_dot tends to lose.

Why it is a build flag and not something narrower

-mwavefrontsize64 is a per-translation-unit flag, so it flips all 265 kernels in mmvq.cu,
not just the q4_K one. It is not applied target-wide because fattn-mma-f16.cuh static_asserts on
wave32 tiling.

A function attribute cannot narrow it. __attribute__((target("wavefrontsize64"))) does work in
isolation - it emits .wavefront_size: 64 for that kernel alone - but clang then refuses to
inline any wave32 function into it:

error: always_inline function 'warp_reduce_sum' requires target feature 'wavefrontsize32',
       but would be inlined into function 'k64' that is compiled without support for
       'wavefrontsize32'

That extends to threadIdx accessors, and every kernel here is assembled from __forceinline__
helpers, so there is nothing left to attribute. Narrowing this properly means moving the q4_K
instantiation into its own translation unit compiled with the flag, which is a larger change and
is not attempted here.

Verification

test-backend-ops test -o MUL_MAT,MUL_MAT_ID,MUL_MAT_VEC_FUSION,MUL_MAT_ID_FUSION -b ROCm0
passes with the flag on, with and without GGML_CUDA_DQ_MMV=0.

Reproducing

cmake -B build64 -DGGML_HIP=ON -DAMDGPU_TARGETS=gfx1151 -DCMAKE_BUILD_TYPE=Release \
      -DGGML_HIP_MMVQ_WAVE64=ON
cmake --build build64 --target test-backend-ops -j$(nproc)

GGML_CUDA_DQ_MMV=0 ./build64/bin/test-backend-ops perf -o MUL_MAT -b ROCm0 \
    -p 'type_a=q4_K,type_b=f32,m=4096,n=1,k=14336'

Runs were serialised under exclusive GPU access at DPM level high and gated on the GPU edge
sensor at 52 C.

Adds GGML_HIP_MMVQ_WAVE64, off by default, which builds mmvq.cu with
-mwavefrontsize64. Exploratory: it is a clear win on a couple of shapes and a
clear loss on more of them, so it is exposed as a flag rather than turned on,
and the data is recorded here so the next person does not have to rediscover it.

Not applied target-wide because fattn-mma-f16.cuh static_asserts on wave32
tiling. -mwavefrontsize64 is per translation unit, so this flips all 265 kernels
in mmvq.cu, not only the q4_K one. A function attribute cannot narrow it:
clang refuses to inline any wave32 function into a target("wavefrontsize64")
one, down to the threadIdx accessors, and every kernel here is built from
__forceinline__ helpers. Narrowing it properly needs the q4_K instantiation
moved to its own translation unit.

q4_K, wave32 vs wave64, rocprofv3 kernel time, median of 3 passes, gfx1151:

    m=21504 n=1 k=5376     289.6 -> 285.6 us    -1.4%
    m=4096  n=4 k=14336    117.6 -> 110.0 us    -6.5%
    m=4096  n=8 k=14336    210.0 -> 223.1 us    +6.2%
    m=4096  n=4 k=4096      35.1 ->  40.5 us   +15.4%
    m=4096  n=1 k=4096      32.3 ->  37.5 us   +16.2%
    m=4096  n=1 k=14336     54.1 ->  69.6 us   +28.7%

Only the gemma-4-31B-it fused FFN gate/up shape and one batched shape benefit.
The k=4096 and k=14336 n=1 shapes lose badly. The likely mechanism is the K-loop
trip count: at nwarps=2 and wave64, blocks_per_iter doubles, so k=4096 gives two
iterations instead of four and the 4x unroll has nothing left to pipeline.

Across all 21 mmvq types at m=4096 n=1 k=4096, median of 3: 5 improve by more
than 2% (iq2_xs -12.8, iq2_xxs -12.5, iq3_xxs -12.5, iq2_s -11.2, iq3_s -5.7),
9 regress by more than 2% (q4_K +18.7, iq4_xs +15.5, mxfp4 +9.8, nvfp4 +9.1,
q2_K +7.7, q4_0 +5.4, iq1_s +4.7, iq4_nl +4.4, q5_0 +2.4), 7 are within +/-2%.

In-model impact on gfx1151 is narrower than that table suggests: with
GGML_CUDA_DQ_MMV on by default, q4_K/q5_K/q6_K at ne11==1 go to
mul_mat_vec_dq_*, which this does not touch. The only mmvq user left for
gemma-4-31B-it Q4_K_M is the fused GEGLU gate/up op, which is the 21504x5376
shape above. Models built from non-K quants route every n=1 matvec through
mmvq and would take the regressions.

Verification: test-backend-ops test -o
MUL_MAT,MUL_MAT_ID,MUL_MAT_VEC_FUSION,MUL_MAT_ID_FUSION passes with the flag on,
with and without GGML_CUDA_DQ_MMV=0. Note the flag miscompiles on ROCm
7.12.0a20260211 (clang 22): 371 of 1175 MUL_MAT cases fail, including the
benchmarked shape at ERR 0.47. 7.15.0a20260728 is clean. There is no version
guard; that is a reason to keep this opt-in.

Assisted-by: Claude Opus 5
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