Skip to content

HIP : RDNA3.5 MMVQ/MMQ kernels and per-type dispatch thresholds for quantised matmul - #56

Open
SimonTeixidor wants to merge 15 commits into
halo-box:masterfrom
SimonTeixidor:pr/rdna35-quant-matmul
Open

SimonTeixidor wants to merge 15 commits into
halo-box:masterfrom
SimonTeixidor:pr/rdna35-quant-matmul

Conversation

@SimonTeixidor

Copy link
Copy Markdown

Branch pr/rdna35-quant-matmul (79194d304), 15 commits on halo/master 69946438a.

Overview

Quantised matmul at small batch widths (decode, speculative verify, 2-16 columns).

  • Kernels (MMVQ/MMQ, HIP only)
    • 2ae2ee95f, 66efda4f6, e1430b151: branch-free SWAR __vsub4/__vcmpne4/__vcmpeq4 and IQ2/IQ3 sign path (CUDA semantics; __vsub4 no longer saturates).
    • 856d62f47, 9a08368a9, dada19e0c: two output rows per thread block (four for codebook types) in MMVQ, bit-exact.
    • c6e682ce0: 24-bit signed multiply for the IQ2_XS/IQ2_S/IQ3_S scale epilogue, bit-exact.
    • 7ed89d9e5: fold block scale and divisor into the float scale for IQ2_XXS/IQ2_XS/IQ2_S/IQ3_XXS/IQ3_S, removing a truncating divide (changes results); carry-free apply_signs4_nz.
    • 151cdf1f7: MMQ prefetch for the J = 16 tile on Q4_K/Q5_K/Q6_K.
  • Dispatch
    • df3e4d79b, 16730c2c8, 79194d304: re-measured MMVQ/MMQ crossover thresholds, all types but Q1_0/Q2_0 (e.g. IQ3_S/IQ4_XS/IQ3_XXS 6 -> 8, Q6_K 3 -> 4, Q5_1 5 -> 2, NVFP4 8 -> 4).
    • b9bbaa7ab: vector kernel when ne01 is below one MMQ tile (e.g. 48-row GDN alpha/beta projections).
    • b35847506: IQ2_XS/IQ2_S use hipBLAS above ne11 = 128.
  • c4d485be3: GGML_MMVQ_THR env override of the threshold table (unset = no change).

Measurements

Device:     HP ZBook Ultra G1a 14 inch Mobile Workstation, AMD RYZEN AI MAX+ PRO 395 w/ Radeon 8060S (gfx1151)
Memory:     64 GB LPDDR5X unified (62 GiB visible to Linux; speed not read, no root)
Power:      AC, ACPI platform_profile=balanced, amdgpu power_dpm_force_performance_level=auto (GPU clocks not pinned)
BIOS:       UMA carve-out 512 MiB VRAM, GTT 60 GiB
Kernel:     7.1.13-200.fc44.x86_64, cmdline amdgpu.gttsize=61440 ttm.pages_limit=15728640 amd_iommu=off
Backend:    ROCm 10.0.0 + patched HIP runtime 7.16.26353-c4b77ac5cb (pwilkin rocm-systems, retained PM4), image localhost/strix-halo:stackv2-2d7825b37 (df09292858be); env HSA_OVERRIDE_GFX_VERSION=11.5.1 GGML_HIP_ENABLE_UNIFIED_MEMORY=1 DEBUG_HIP_GRAPH_PM4=1; HIP_LAUNCH_BLOCKING unset
Build:      localhost/strix-halo:builder (f1db10cdcd29), CMAKE_BUILD_TYPE=Release GGML_HIP=ON GPU_TARGETS=gfx1151 GGML_HIP_GRAPHS=ON GGML_HIP_MMQ_MFMA=ON GGML_HIP_NO_VMM=ON GGML_CUDA_FA=ON LLAMA_BUILD_TESTS/EXAMPLES/TOOLS=ON, -j 20
Baseline:   654803517 (halo/master), built and run in this session
Change:     79194d304 (pr/rdna35-quant-matmul)
Model:      Qwen3.8-27B-UD-IQ4_XS.gguf (sha256 40fac4050e94...); weight bytes: IQ4_XS 50.7 %, Q5_K 14.7 %, IQ3_S 11.3 %, Q4_K 8.9 %, Q3_K 5.6 %, IQ3_XXS 3.8 %, Q6_K 2.4 %, IQ2_S 1.4 %, IQ2_XS 0.4 %, Q8_0 0.3 %, IQ4_NL 0.2 %, Q2_K 0.1 %

Stock: 654803517, same flags, image and session. Mirrored blocks, n = 6 (width sweep) or 4 per build.

test stock t/s (sd, n) PR t/s (sd, n) gain % 95 % CI %
pp128, ub 1 13.75 (0.07, 6) 15.48 (0.02, 6) +12.58 +12.06 .. +13.09
pp128, ub 4 36.34 (0.26, 6) 47.84 (0.19, 6) +31.64 +30.82 .. +32.47
pp128, ub 6 41.69 (0.28, 6) 64.64 (0.44, 6) +55.04 +53.87 .. +56.20
pp128, ub 16 124.52 (0.61, 6) 133.44 (1.18, 6) +7.16 +6.13 .. +8.19
tg128 12.94 (0.06, 4) 14.50 (0.08, 4) +12.09 +11.11 .. +13.07
tg128 @ d32000 11.13 (0.28, 4) 12.28 (0.35, 4) +10.42 +5.20 .. +15.64
batched decode, npl 6 35.26 (0.67, 4) 49.99 (0.49, 4) +41.76 +38.74 .. +44.79
batched decode, npl 8 37.70 (0.77, 4) 53.59 (0.48, 4) +42.15 +39.04 .. +45.25

All width-sweep (ub 1-512), TG and batched-decode cells resolve; pp2048 at ub 1024 is unresolved at every depth.

Correctness:

  • test-backend-ops -b ROCm0: MUL_MAT 7438/7438, MUL_MAT_ID 7097/7097, GET_ROWS 215/215. Full suite: 22599 OK, 0 FAIL, then a SOLVE_TRI abort stock shares; remaining ops 352/352.
  • Qwen3.8-27B output differs from stock (width-dependent kernels; 7ed89d9e5 by design) by no more KLD than stock's own ub 8 vs ub 512, with PPL deltas inside the error bars (prose +/- 0.355):
corpus PPL stock / PR (-c 512 -b/-ub 512, 4 chunks) KLD PR vs stock, ub 512: mean (max) same top-1 reference stock ub 8 vs stock ub 512: mean KLD (max), same top-1
prose 5.4246 / 5.4301 0.000849 (0.0103) 98.82 % 0.001027 (0.0115), 98.14 %
code 1.9202 / 1.9228 0.000367 (0.0128) 99.31 % 0.000670 (0.0140), 99.51 %
structured 2.4814 / 2.4660 0.005319 (2.89) 98.92 % 0.010257 (6.58), 98.92 %
numeric 1.9349 / 1.9329 0.000410 (0.0120) 99.41 % not run
  • Prefill logits: token ids and top-1 identical 5/5, KLD 1.3e-4 .. 8.2e-4 (1.8e-2 on the 13480-token prompt). Greedy decode identical on prose, code, numeric; diverges at step 103 (structured) and 97 (13480-token prompt).
Raw results

Batch width: llama-bench -p 128 -b 128 -ub 1,2,3,4,6,8,12,16 -n 0 -r 3 and -p 512 -b 512 -ub 64,512 -r 3, both with -ngl 99 -fa on -ctk f16 -ctv f16 --load-mode none; blocks stock, PR, PR, stock.

test ub stock t/s (n, sd) build candidate t/s (n, sd) gain % 95 % CI % Welch t resolved
pp128 1 13.75 (6, 0.07) matmul 15.48 (6, 0.02) +12.58 +12.06 .. +13.09 +63.01 yes
pp128 2 23.78 (6, 0.20) matmul 28.08 (6, 0.03) +18.05 +17.14 .. +18.96 +51.06 yes
pp128 3 30.60 (6, 0.22) matmul 37.67 (6, 0.08) +23.11 +22.36 .. +23.86 +75.27 yes
pp128 4 36.34 (6, 0.26) matmul 47.84 (6, 0.19) +31.64 +30.82 .. +32.47 +86.95 yes
pp128 6 41.69 (6, 0.28) matmul 64.64 (6, 0.44) +55.04 +53.87 .. +56.20 +109.08 yes
pp128 8 45.99 (6, 0.55) matmul 72.02 (6, 0.64) +56.59 +54.90 .. +58.29 +75.54 yes
pp128 12 88.66 (6, 0.73) matmul 99.21 (6, 0.91) +11.90 +10.68 .. +13.12 +22.04 yes
pp128 16 124.52 (6, 0.61) matmul 133.44 (6, 1.18) +7.16 +6.13 .. +8.19 +16.40 yes
pp512 64 220.73 (6, 0.53) matmul 228.69 (6, 0.67) +3.61 +3.25 .. +3.97 +22.75 yes
pp512 512 274.03 (6, 1.64) matmul 281.36 (6, 3.25) +2.68 +1.39 .. +3.96 +4.94 yes
invocation test ub t/s sd n mean GPU power/temp
A-1-base pp128 1 13.69 0.01 3 44.4W/69C
A-1-base pp128 2 23.65 0.13 3 44.4W/69C
A-1-base pp128 3 30.44 0.02 3 44.4W/69C
A-1-base pp128 4 36.17 0.27 3 44.4W/69C
A-1-base pp128 6 41.49 0.19 3 44.4W/69C
A-1-base pp128 8 45.58 0.35 3 44.4W/69C
A-1-base pp128 12 88.19 0.76 3 44.4W/69C
A-1-base pp128 16 124.52 0.49 3 44.4W/69C
A-1-base pp512 64 220.49 0.50 3 44.4W/69C
A-1-base pp512 512 273.55 2.09 3 44.4W/69C
A-2-matmul pp128 1 15.47 0.00 3 44.1W/68C
A-2-matmul pp128 2 28.07 0.04 3 44.1W/68C
A-2-matmul pp128 3 37.64 0.03 3 44.1W/68C
A-2-matmul pp128 4 47.70 0.03 3 44.1W/68C
A-2-matmul pp128 6 64.47 0.45 3 44.1W/68C
A-2-matmul pp128 8 71.77 0.42 3 44.1W/68C
A-2-matmul pp128 12 98.47 0.47 3 44.1W/68C
A-2-matmul pp128 16 133.16 1.41 3 44.1W/68C
A-2-matmul pp512 64 228.51 0.61 3 44.1W/68C
A-2-matmul pp512 512 280.34 2.60 3 44.1W/68C
B-1-matmul pp128 1 15.49 0.02 3 44.0W/69C
B-1-matmul pp128 2 28.08 0.03 3 44.0W/69C
B-1-matmul pp128 3 37.70 0.11 3 44.0W/69C
B-1-matmul pp128 4 47.98 0.18 3 44.0W/69C
B-1-matmul pp128 6 64.81 0.43 3 44.0W/69C
B-1-matmul pp128 8 72.27 0.82 3 44.0W/69C
B-1-matmul pp128 12 99.95 0.46 3 44.0W/69C
B-1-matmul pp128 16 133.71 1.14 3 44.0W/69C
B-1-matmul pp512 64 228.87 0.81 3 44.0W/69C
B-1-matmul pp512 512 282.39 4.06 3 44.0W/69C
B-2-base pp128 1 13.81 0.02 3 44.7W/69C
B-2-base pp128 2 23.92 0.19 3 44.7W/69C
B-2-base pp128 3 30.76 0.19 3 44.7W/69C
B-2-base pp128 4 36.51 0.10 3 44.7W/69C
B-2-base pp128 6 41.89 0.19 3 44.7W/69C
B-2-base pp128 8 46.41 0.34 3 44.7W/69C
B-2-base pp128 12 89.13 0.33 3 44.7W/69C
B-2-base pp128 16 124.52 0.83 3 44.7W/69C
B-2-base pp512 64 220.97 0.53 3 44.7W/69C
B-2-base pp512 512 274.51 1.27 3 44.7W/69C

Token generation: llama-bench -p 0 -n 128 -d 0,32000 -b 1024 -ub 1024 -r 2.

invocation test ub t/s sd n mean GPU power/temp
A-1-base tg128 1024 12.97 0.07 2 45.4W/70C
A-1-base tg128 @ d32000 1024 11.00 0.42 2 45.4W/70C
A-4-matmul tg128 1024 14.50 0.10 2 45.1W/69C
A-4-matmul tg128 @ d32000 1024 12.12 0.52 2 45.1W/69C
B-1-matmul tg128 1024 14.51 0.10 2 45.1W/69C
B-1-matmul tg128 @ d32000 1024 12.45 0.02 2 45.1W/69C
B-4-base tg128 1024 12.91 0.05 2 45.1W/70C
B-4-base tg128 @ d32000 1024 11.25 0.04 2 45.1W/70C
test ub stock t/s (n, sd) build candidate t/s (n, sd) gain % 95 % CI % Welch t resolved
tg128 1024 12.94 (4, 0.06) matmul 14.50 (4, 0.08) +12.09 +11.11 .. +13.07 +31.66 yes
tg128 @ d32000 1024 11.13 (4, 0.28) matmul 12.28 (4, 0.35) +10.42 +5.20 .. +15.64 +5.13 yes

Batched decode: llama-batched-bench -c 8192 -b 4096 -ub 512 -npp 512 -ntg 128 -npl 1,2,4,6,8, decode from speed_tg, one sample per invocation (A/C: stock, gdn PR, this PR; B/D reversed).

npl stock decode t/s (sd, n) build candidate decode t/s (sd, n) gain % 95 % CI % Welch t
1 13.05 (0.16, 4) matmul 14.47 (0.03, 4) +10.84 +8.80 .. +12.88 +16.90
2 21.45 (0.55, 4) matmul 25.26 (0.05, 4) +17.74 +13.65 .. +21.84 +13.79
4 31.32 (0.68, 4) matmul 38.48 (1.26, 4) +22.83 +16.48 .. +29.18 +9.98
6 35.26 (0.67, 4) matmul 49.99 (0.49, 4) +41.76 +38.74 .. +44.79 +35.48
8 37.70 (0.77, 4) matmul 53.59 (0.48, 4) +42.15 +39.04 .. +45.25 +34.85
invocation npl S_PP t/s S_TG t/s
A-1-base 1 220.80 13.29
A-1-base 2 285.97 22.22
A-1-base 4 276.18 32.28
A-1-base 6 277.49 36.25
A-1-base 8 280.78 38.77
A-3-matmul 1 198.28 14.50
A-3-matmul 2 285.96 25.30
A-3-matmul 4 285.74 39.21
A-3-matmul 6 278.01 50.65
A-3-matmul 8 282.21 54.09
B-1-matmul 1 194.34 14.47
B-1-matmul 2 283.46 25.24
B-1-matmul 4 283.88 39.11
B-1-matmul 6 276.85 50.05
B-1-matmul 8 280.00 53.87
B-3-base 1 193.60 12.97
B-3-base 2 276.93 21.13
B-3-base 4 268.11 30.79
B-3-base 6 268.39 34.81
B-3-base 8 267.88 37.14
C-1-base 1 238.48 13.04
C-1-base 2 278.03 21.48
C-1-base 4 274.82 31.32
C-1-base 6 271.13 34.90
C-1-base 8 274.60 37.77
C-3-matmul 1 190.40 14.42
C-3-matmul 2 282.88 25.29
C-3-matmul 4 282.45 39.00
C-3-matmul 6 274.77 49.74
C-3-matmul 8 279.23 53.39
D-1-matmul 1 249.29 14.47
D-1-matmul 2 282.80 25.20
D-1-matmul 4 283.16 36.58
D-1-matmul 6 271.49 49.53
D-1-matmul 8 278.79 53.02
D-3-base 1 241.11 12.92
D-3-base 2 274.05 20.98
D-3-base 4 270.83 30.91
D-3-base 6 267.23 35.09
D-3-base 8 266.42 37.13

Prompt processing: llama-bench -p 2048 -d 0,12000,32000,64000 -b 1024 -ub 1024 -n 0 -r 2.

invocation test ub t/s sd n mean GPU power/temp
A-1-base pp2048 1024 285.38 0.37 2 46.2W/71C
A-1-base pp2048 @ d12000 1024 250.03 5.93 2 46.2W/71C
A-1-base pp2048 @ d32000 1024 211.80 0.28 2 46.2W/71C
A-1-base pp2048 @ d64000 1024 166.23 5.45 2 46.2W/71C
A-4-matmul pp2048 1024 281.36 0.84 2 45.3W/70C
A-4-matmul pp2048 @ d12000 1024 249.18 6.14 2 45.3W/70C
A-4-matmul pp2048 @ d32000 1024 209.91 0.78 2 45.3W/70C
A-4-matmul pp2048 @ d64000 1024 166.51 4.59 2 45.3W/70C
B-1-matmul pp2048 1024 273.05 0.43 2 45.4W/70C
B-1-matmul pp2048 @ d12000 1024 246.64 5.42 2 45.4W/70C
B-1-matmul pp2048 @ d32000 1024 213.44 0.30 2 45.4W/70C
B-1-matmul pp2048 @ d64000 1024 167.34 4.46 2 45.4W/70C
B-4-base pp2048 1024 276.30 0.06 2 45.7W/70C
B-4-base pp2048 @ d12000 1024 244.44 4.55 2 45.7W/70C
B-4-base pp2048 @ d32000 1024 210.22 0.66 2 45.7W/70C
B-4-base pp2048 @ d64000 1024 162.60 7.64 2 45.7W/70C
test ub stock t/s (n, sd) build candidate t/s (n, sd) gain % 95 % CI % Welch t resolved
pp2048 1024 280.84 (4, 5.25) matmul 277.21 (4, 4.83) -1.29 -4.56 .. +1.97 -1.02 no
pp2048 @ d12000 1024 247.24 (4, 5.39) matmul 247.91 (4, 4.95) +0.27 -3.53 .. +4.08 +0.18 no
pp2048 @ d32000 1024 211.01 (4, 1.00) matmul 211.68 (4, 2.09) +0.32 -1.21 .. +1.84 +0.58 no
pp2048 @ d64000 1024 164.41 (4, 5.81) matmul 166.93 (4, 3.72) +1.53 -3.87 .. +6.93 +0.73 no

Correctness: llama-perplexity -c 512 -b/-ub 512, 4 chunks, --kl-divergence-base/--kl-divergence (base file quantises log-probs to 16 bits); llama-debug --save-logits -b 16384 -ub 512; llama-server greedy /completion, 256 tokens (128 on the long prompt), n_probs 10. Same -b/-ub, context, KV types and FA on both builds.

At -ub 8 (LLAMA_GRAPH_REUSE_DISABLE=1, 2 chunks), PR vs stock: prose KLD 0.000937, top-1 98.24 %, PPL 5.5569 -> 5.5752; code KLD 0.000610, top-1 99.61 %, PPL 2.2002 -> 2.2055.

Greedy decode: top-10 log-probs differ from step 0 on all prompts; max |d logprob| of the chosen token before divergence 0.028 .. 0.53.

Threshold case files (explicit crossing cases) passed 6960/6960 and 1160/1160 on earlier builds with a fixed test-backend-ops (results/2026-09-10-crossover-retune/); the default grid's MUL_MAT cases use m = 16, below the ne01 < 64 early return.

Full suite log: SOLVE_TRI abort at SOLVE_TRI(type=f32,ne_lhs=[64,64,2,2],...); stock aborts the same way on -o SOLVE_TRI. Ops run by name: -o ADD1,ADD_REL_POS,CROSS_ENTROPY_LOSS,...,GATED_DELTA_NET,...,WIN_UNPART.

Additional information

  • Stock sends every quantised type to MMVQ up to 8 columns, including k-quants whose MMVQ cost grows fastest per column; that this drives the ub 6-8 gain is inferred, not attributed per commit.

Requirements

  • I have read and agree with the contributing guidelines
  • This change is justified by measurements on Strix Halo (gfx1151)
  • AI usage disclosure: AGENT-AUTHORED. Claude (Claude Code) wrote the commits and this description and ran the measurements; the owner reviews.
  • What was NOT verified:
    • Measured on 654803517. The branch was then rebased onto 69946438a, whose 6 new commits touch only ggml/rocmfpx, one Vulkan shader and tests/CMakeLists.txt; not re-measured.
    • Threshold-crossing case files (ne11 1-8, m in {66, 67, 130, 1026}): not re-run, --test-file false FAIL on this tree.
    • FLASH_ATTN_EXT (stock traps at hs 192, fixed by pr/hip-rebase-fixes) and SOLVE_TRI (aborts on stock too).
    • Q5_1, NVFP4, Q4_0, Q4_1, Q5_0, MXFP4, IQ1_S, IQ2_XXS: absent from the model, no end-to-end check; Q2_K, IQ4_NL in one tensor each.
    • Per-commit attribution: the 15 commits were measured together.
    • Speculative decoding: not run; width sweep and batched decode stand in.
    • Repeats: one session, -r 2/-r 3 over mirrored blocks, not one -r 4.
    • Vulkan and NVIDIA CUDA builds of shared vecdotq.cuh/mmvq.cu: not compiled.

https://claude.ai/code/session_01MkuKwgPNgpAg7EbxKR2iWC

The RDNA3.5 short-circuit forces IQ2_XS and IQ2_S onto MMQ at every batch
size. On gfx1151 hipBLAS is faster than MMQ for both types well below the
ubatch sizes used for prefill. At ne11 <= 128 MMQ still wins, so that
threshold is kept.

The MMQ/hipBLAS time ratio is monotone in ne11 for every quantised type, so
the "ne11 <= T" form is right; only T was wrong for these two.

Mechanism: IQ2_XS/IQ2_S carry one scale per 16 weights (IQ4_XS: per 32),
forcing a doubled WMMA epilogue that no config change can remove, while their
dequant kernels are the cheapest of any quantised type. Worst MMQ plus
cheapest dequant gives the earliest crossover of any type.

This reverts one clause of PR ggml-org#18666 to what PR ggml-org#18537 shipped four days
earlier. The IQ2_XS/IQ2_S config rows in this tree are byte-identical to
upstream's; this tree additionally carries an IQ2_S prefetch that makes its
MMQ faster than upstream's.

Assisted-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_01VdFZgAf86WFrMQjttNagfW
__vsub4 was forwarded to __vsubss4, which saturates; CUDA's __vsub4 wraps.
The two disagree on every input where a byte subtraction overflows.

Assisted-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_01VdFZgAf86WFrMQjttNagfW
(cherry picked from commit ae8d32eebe49107d7b0d6f6b53013ddbf706e0be)
RDNA has no packed-byte compare or subtract, so hip.h emulates both per
byte. The IQ2_XXS/IQ2_XS/IQ2_S/IQ3_XXS/IQ3_S unpacks call that pair once
per 4 weights. Replacing it removes the emulation from every call site in
the MMVQ and MMQ kernels.

The replacement is bit-identical to CUDA's __vcmpne4/__vsub4 for every
input, checked exhaustively over grid words and sign nibbles on the host
and on gfx1151. Adding the two's complement +1 only into the low 7 bits of
each byte, then folding bit 7 back with an XOR, keeps the carry inside its
byte.

Note hip.h routes __vsub4 to the saturating __vsubss4, so master's HIP
path already differs from CUDA whenever a grid byte is 0x80. This follows
CUDA's semantics on both vendors and removes that divergence; it is
unreachable for the five grids in any case, whose bytes are 0x01..0x3e.

unpack_ksigns() now returns the 8 sign bits as a byte rather than
broadcast, and is renamed unpack_ksigns8 so stale callers cannot compile.

(cherry picked from commit 81dd22156a1d2ae2aebb74de9e6619d6f482bb6c)
Assisted-by: Claude Opus 5
The upstream F1 change only covers the call sites that exist upstream. This
fork carries three more copies of the same __vcmpne4/__vsub4 sign idiom, in
code that does not exist upstream:

  mmvq.cu  mul_mat_vec_q4_columns_rdna3_5   (IQ3_S, 4-column RDNA3.5 kernel)
  mmvq.cu  vec_dot_iq3_s_q8_1_grid
  vecdotq.cuh vec_dot_iq3_s_q8_1_pair

Left alone they would keep the emulated pair in the hottest IQ3_S decode
kernels, i.e. lose most of the win on this fork.

The nibble mapping is the same one apply_signs4 takes: ((s & 0x03) << 7) |
((s & 0x0C) << 21) makes byte i of the selector word nonzero exactly for bit
i of s & 0x0f, and ((s & 0x30) << 3) | ((s & 0xC0) << 17) does the same for
s >> 4. apply_signs4 is bit-exact with that idiom under CUDA's __vsub4, so
vec_dot_iq3_s_q8_1_pair stays bit-identical to two scalar
vec_dot_iq3_s_q8_1 calls, which is the invariant its comment states.

Assisted-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_01VdFZgAf86WFrMQjttNagfW
ggml_cuda_should_use_mmvq falls through to ne11 <= MMVQ_MAX_BATCH_SIZE (8) for
every quantized type on AMD; the per-type tables are only filled in for Ada,
Blackwell, DGX Spark and CDNA. On gfx1151 8 is too high for most types.

MMVQ re-reads the src1 column and redoes the dot per output column while the
weight unpack is hoisted out of the ncols_dst loop, so its cost is A + ncols_dst*D
with D dominated by the type's vec_dot. MMQ's smallest instantiated tile on
RDNA3.5 is J = 16, so one column tile covers the whole ne11 = 1..16 range and
MMQ's cost is flat across it. The crossover is therefore a per-type constant.

Both paths were timed at the same ne11 on real model shapes with a temporary
dispatch override, so MMQ's cost below ne11 = 9 is measured rather than
extrapolated. The crossover does not move with m or k once MMQ fills a wave.

Q1_0, Q2_0, IQ2_XXS, IQ2_XS, IQ2_S and NVFP4 keep the existing bound of 8.

This replaces an earlier table that covered only four k-quants and
extrapolated the other types from an assumption that turned out to be false.

This fork has GGML_HINT_EXACT_BATCH. The hint promises that a batched mul_mat
equals n single-column mul_mats exactly, which any ne11-dependent kernel switch
inside [1, MMVQ_MAX_BATCH_SIZE] breaks, so the exact_batch bypass is threaded
through mmvq.cuh / ggml-cuda.cu.

Assisted-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_01VdFZgAf86WFrMQjttNagfW
calc_rows_per_block returns 1 unconditionally for MMVQ_PARAMETERS_RDNA3_5,
while GENERIC, GCN, TURING and GB10 all return 2 for ncols_dst 2..8. The
RDNA3.5 entry was added alongside the nwarps = 1 entry and inherited its value
without a separate measurement.

On RDNA3.5 nwarps is 1, so a thread block is a single wave and every lane
re-reads the whole q8_1 activation block for the one row the block owns. At
k = 5120 and ncols_dst = 6 that is 6 * 5120 * 1.125 B = 34.5 KB of activation
traffic per output row, through the vector memory unit, as 4-byte loads
(block_q8_1 is 36 B so qs is only 4-byte aligned). With two rows per block the
same loaded activation words feed two rows, halving that traffic per row.

Bit-exact: rows_per_cuda_block only changes which rows a block owns and how
many accumulators it carries. The per-lane K decomposition (kbx stride
blocks_per_iter, kqs) and therefore the dot summation order per row is
unchanged, and the cross-lane reduction is the same warp_reduce_sum.

The rest of the kernel is already written for rows_per_cuda_block > 1 and is
exercised there by the four other tables: calc_launch_params divides nrows_x
by rpb, tmp/tmp_gate are [ncols_dst][rpb], the fusion bias prefetch and the
write-back both guard with threadIdx.x < rpb and row0 + i < stride_col_dst,
and the tmp_shared reduction is indexed by rpb. The RDNA3.5-specific
specialisations (the q4_columns kernels at ncols_dst = 4, the IQ3_S ncols_dst
= 1 kernels, the MUL_MAT_ID multi-token kernel) compute their own launch
geometry and do not go through calc_rows_per_block.

Register pressure: at ncols_dst = 6 the accumulator array grows from 6 to 12
floats, and to 24 when a fused gate is present.

Assisted-by: Claude Opus 5
The MMVQ/MMQ and MMVF/MMF thresholds are per-type ne11 crossovers measured at
m >= 5120, where MMQ fills the device. They say nothing about small m. The
smallest MMQ tile instantiated on RDNA3.5 is I = 64 rows, so at ne01 = 48 --
the GDN ssm_alpha and ssm_beta projections, 96 dispatches per decode step on
the 27B models -- MMQ launches ceil(48/64) = 1 block and 39 of the 40 CUs sit
idle for the whole call, while MMVQ launches a block per row (or row pair) and
uses the device. The same holds for MMF against MMVF.

So: on RDNA3.5, when ne01 is below one tile, take the vector kernel for any
ne11 the vector kernel supports. ggml_cuda_should_use_mmvq did not receive
ne01, so it is threaded in from ggml_cuda_mul_mat; ggml_cuda_should_use_mmvf
already had src0_ne.

Guarded to RDNA3.5 because that is where these tile sizes apply.

This changes which kernel runs, so the result can differ in the last ulp the
same way the existing per-type thresholds already make it depend on ne11.
Under GGML_HINT_EXACT_BATCH the RDNA3.5 MMVQ path already returns
ne11 <= MMVQ_MAX_BATCH_SIZE, so the new rule agrees with it.

Assisted-by: Claude Opus 5
These three vec_dots finish by multiplying an accumulated dp4a sum by its
integer block scale: sumi0*ls0 + sumi1*ls1 for iq2_xs and iq2_s, sumi *= 1+2*sc
for iq3_s. On RDNA3 a 32-bit integer multiply is quarter rate; the 24-bit form
v_mul_i32_i24 is full rate.

The compiler does not find this on its own: in a built gfx1151 library the
MMVQ kernels for these types multiply each block scale with v_mul_lo_u32. It
does use the unsigned 24-bit form elsewhere in the same file when it can prove
the operands are small, but it cannot bound a signed dp4a accumulator.

The multiply must be signed. The codebook bytes reach the dp4a with their signs
already applied, so sumi is genuinely negative about half the time and
__umul24 would be wrong.

Range proof, so the 24-bit form is bit-exact. __mul24 sign-extends the low 24
bits of each operand, so it agrees with a full multiply when both operands lie
in [-2^23, 2^23) and the product fits in int.

  iq2_xs, iq2_s: sumi0 and sumi1 each accumulate 4 dp4a, i.e. 16 int8 products.
  The iq2xs and iq2s codebook byte alphabet is {8, 25, 43}, so a signed grid
  byte is at most 43 in absolute value; a q8_1 byte is at most 128. So
  |sumi| <= 16*43*128 = 88064. The scales are nibbles, 0..15. Product at most
  1320960.

  iq3_s: sumi accumulates 8 dp4a, i.e. 32 int8 products, over the iq3s codebook
  alphabet {1, 3, ..., 15}, so |sumi| <= 32*15*128 = 61440. The scale is
  1 + 2*nibble, at most 31. Product at most 1904640.

Both are within 2^23 = 8388608 by more than a factor of 90, and both products
are far inside int. The remaining epilogue arithmetic ((sumi0 + sumi1)/2, the
/4) is adds and shifts and is untouched.

Applied at every site these three types reach: vec_dot_iq2_xs_q8_1,
vec_dot_iq2_s_q8_1, vec_dot_iq3_s_q8_1 and vec_dot_iq3_s_q8_1_pair in
vecdotq.cuh, and vec_dot_iq3_s_q8_1_grid and the IQ3_S branch of
mul_mat_vec_q4_columns_rdna3_5 in mmvq.cu.

Guarded to HIP: __mul24 is full rate on RDNA3 but emulated, and slower than a
plain multiply, on every NVIDIA architecture since Fermi.

Not applied to MMQ: load_tiles for all three types folds the integer scale into
the float block scale (x_df) instead of multiplying the accumulated sum, so
there is no integer multiply on that path to convert.

Assisted-by: Claude Opus 5
mul_mat_vec_q4_columns_rdna3_5 and mul_mat_vec_q4_columns compute their own
launch geometry (row = blockIdx.x) and never consult calc_rows_per_block, so the
two-rows-per-block change misses them and ncols_dst == 4 keeps re-reading the
four q8_1 activation columns once per output row. Give both kernels the same
treatment directly: a block owns rows row0 and row0 + 1, each row's weights are
decoded into its own registers, and the four activation columns are loaded once
and dotted against both.

Launch geometry becomes ceil(nrows_x / rows_per_block) blocks, the accumulator
and the cross-warp staging array gain a row dimension, and the write-back moves
from lane 0 to lane r under the same row0 + r < stride_col_dst guard the generic
kernel uses.

The per-lane K decomposition, the dp4a order within a column and the cross-lane
reduction are unchanged for every (row, column) pair, so the results are
bit-exact. The ids and fused-gate paths never reach these kernels: the dispatch
site takes them only for ids == nullptr with no fusion arguments.

Assisted-by: Claude Opus 5
calc_rows_per_block returned 2 for every type at ncols_dst 2..8 on the
RDNA3.5 parameter table. Four rows per block lowers the per-column cost
for the codebook types, does nothing for q1_0/q2_0/q8_0, and regresses
q5_1. A blanket 4 also drives mul_mat_vec_q<q2_K,8>, <NVFP4,7> and
<NVFP4,8> into register spills.

Take the subset that measured well and leave the rest alone:

  iq1_s iq1_m iq2_xxs iq2_xs iq2_s iq3_s iq4_xs   4 rows, ncols_dst <= 6
  q2_K                                            4 rows, ncols_dst <= 4
  everything else                                 2 rows, as before

q2_K stops at 4 because that is also its MMVQ threshold on this arch, so
5..8 is reachable only through the ne01 < 64 path, and it is the type with
the worst register growth. No type is promoted at ncols_dst 7 or 8, which
is where the spills are.

calc_rows_per_block gains a ggml_type parameter; both call sites already
have type as a template parameter inherited from the same
mul_mat_vec_q_switch_ncols_dst<type> instantiation, so the host grid
calculation and the device kernel cannot see different types. The type
argument is read only inside the RDNA3.5 arm; every other parameter table
returns exactly what it returned before, pinned by static_asserts.

Also assert rows_per_cuda_block <= warp_size in mul_mat_vec_q: the
epilogue writes row row0+i from lane i, an invariant that was never close
at two rows.

Assisted-by: Claude Opus 5
… quants

Two changes to the MMVQ vector-dot path on the IQ codebook types, both aimed at
mul_mat_vec_q<IQ2_S, 6> and <IQ3_S, 6>, the largest consumers of GPU time in a
six-token decode step on models built from those types.

L-EPI: IQ2_XXS / IQ2_XS / IQ2_S / IQ3_XXS / IQ3_S scaled the integer dot product with a
truncating signed integer divide inside the ncols_dst loop. Folding the block scale and the
divisor into the float scale moves all of it to per-weight-block work that hoists out of that
loop. It is also exact where the integer form truncated.
The IQ3_S four-column kernel and the LDS-codebook variant carry the same transform so the
n-column and single-column paths still agree (test_mul_mat_exact_batch checks that to 1e-7).

L-SGN: apply_signs4 spends three instructions containing a carry that cannot occur for any
grid apply_signs4 is used with -- iq2*_grid bytes are {8, 25, 43}, iq3xxs_grid {4..62},
iq3s_grid {1..15}, none zero, so 0xff ^ g <= 0xfe and the +1 never leaves its byte. MMVQ
gets apply_signs4_nz; the MMQ callers keep the general form.

IQ4_XS and IQ1_S kernels are unchanged, and no kernel gains a register spill.

Assisted-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_016inZWVhdrrrkrjh2djvH7N
GGML_MMVQ_THR="<ggml type id>:<max ne11 kept on MMVQ>[,...]" overrides
ggml_cuda_should_use_mmvq's per-type RDNA3.5 thresholds at runtime, so one
binary can serve both arms of a crossover comparison. Unset (the default)
changes nothing, and the override is placed after the GGML_HINT_EXACT_BATCH
early return so exact-batch semantics are untouched.

The existing thresholds predate the rows-per-block and epilogue changes,
which lowered the MMVQ per-column cost, so they need re-measuring.

Assisted-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_016inZWVhdrrrkrjh2djvH7N
…/Q6_K

J = 16 is the only MMQ tile instantiated for ne11 <= 16 on RDNA3.5, so it is
the tile every decode, small-batch decode and speculative-verification step
runs on, and it was not in the sweep the original whitelist was tuned on. The
motivation for prefetching does not depend on J: Q4_K at J = 16 needs 22080 B
of LDS against 26816 B at the already-whitelisted J = 48, so both sit at
exactly 2 blocks resident per 64 KiB CU, while staging the activation tile
costs fewer registers at J = 16 than at the larger tiles.

No kernel gains a spill. IQ4_XS and IQ3_S at J = 16 are left out: they showed
no per-call effect and would bring their kernels close to the register limit.

Assisted-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_016inZWVhdrrrkrjh2djvH7N
Every bound in this table except IQ3_XXS was set before the rows_per_block and
epilogue work lowered MMVQ's per-column cost. This re-measures all of it, using
GGML_MMVQ_THR so one binary serves both arms, at the (ne00 x ne01) pairs each
type takes in Qwen3.8-27B-UD-IQ4_XS.

  IQ3_S   6 -> 8      Q6_K 3 -> 4      Q3_K 3 (re-measured, unchanged)
  IQ4_XS  6 -> 8      Q8_0 4 -> 5      Q4_K 2 (re-measured, unchanged)
  IQ3_XXS 6 -> 8                       Q5_K 2 (re-measured, unchanged)
                                       IQ2_S 8 (re-measured, unchanged)

The k-quant bounds stay low because their per-column decode cost is several
times that of the IQ types at the same shape, and MMVQ redoes it per column.

For IQ3_S, IQ4_XS and IQ3_XXS MMVQ stays faster than MMQ through ne11 = 8, so
8 here is MMVQ_MAX_BATCH_SIZE rather than a measured meeting point. These three
types are most of a UD-IQ4_XS file's matmul weights, and all three left the
vector path at the same width, which moved nearly the whole file onto MMQ at a
speculative verification width of 7.

Not measured, left alone, and said so in the comment: Q2_K, Q4_0, Q4_1, Q5_0,
Q5_1, MXFP4, IQ4_NL, IQ1_S, IQ2_XS, IQ2_XXS, NVFP4, Q1_0, Q2_0.

Known incomplete, and recorded in the comment: the crossover is a per-(type,
ne01) constant, not a per-type one. At ne01 = 1024 (attn_k, attn_v) MMQ does
not fill the device and the crossovers differ. The bounds here are the
ne01 >= 5120 values, which is the conservative choice; expressing the other
regime needs a code change.

The default MUL_MAT test grid cannot reach this code: its cases are m = 16 and
should_use_mmvq returns early when ne01 < 64.

Assisted-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_016inZWVhdrrrkrjh2djvH7N
The previous commit re-measured the nine types a local GGUF contains and left
the other eleven alone. This measures those eleven the same way, at generic 27B
shapes (5120x17408, 17408x5120, 5120x6144).

  Q5_1  5 -> 2      Q2_K   4 -> 6      IQ2_XS  8 (re-measured, unchanged)
  NVFP4 8 -> 4      Q4_1   5 -> 6      IQ2_XXS 8 (re-measured, unchanged)
                    MXFP4  5 -> 6
                    IQ4_NL 5 -> 6
                    Q4_0   6 -> 8
                    Q5_0   6 -> 8
                    IQ1_S  6 -> 8

These bounds come from representative shapes, not from a real file, and have no
end-to-end check, because no model on the tuning box contains these types. The
comment says so as well.

The two lowerings matter most, because the old bounds sent these types to MMVQ
where it is slower:

  Q5_1 has one of the largest per-column costs in this function, above Q4_K and
  Q5_K, and crosses between ne11 = 2 and 3. Its bound of 5 kept it on MMVQ at
  ne11 = 3..5.

  NVFP4 crosses between ne11 = 4 and 5. Its bound of 8 kept it on MMVQ at
  ne11 = 5..8. The previous comment's "NVFP4 does not cross below 9" does not
  reproduce. This fits rdna3_5_rows4_max_ncols_dst already excluding NVFP4
  because its wide kernels spill.

Q1_0 and Q2_0 are now the only two types in this function that have never been
measured on gfx1151.

Assisted-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_016inZWVhdrrrkrjh2djvH7N

@dzannotti dzannotti left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validation: a Release HIP gfx1151 build completed. test-backend-ops MUL_MAT for q4_0 passed 1273/1273 ROCm cases.

Benchmark attempt: against current master on the locally available Qwen3.6-35B-A3B Q4_K model, pp128 measured 177.69 versus 177.84 t/s at ubatch 6 (+0.08%) and 64.89 versus 65.17 t/s at ubatch 1 (+0.44%), six samples per arm with alternating builds. This does not reproduce the reported large gain, but the PR used a different Qwen3.8 IQ4_XS model and quant mix, so it is not a direct refutation.

Please split by coherent kernel/type family, add boundary tests around every changed crossover, run all changed quant types, and provide a reproducible gfx1151 benchmark using the published fixture or an accessible equivalent.

Scope/reviewability: 15 commits and broad dispatch changes are too much for one acceptance decision. Much of the edited CUDA/HIP shared backend is generic; keep any Strix-only policy narrowly gated or stage it upstream.

comment generated by my clanker Codex

@SimonTeixidor

Copy link
Copy Markdown
Author

Hi! To validate the speedup, please try an IQ-type quant. This is the specific model I tested with: https://huggingface.co/unsloth/Qwen3.8-27B-GGUF/blob/main/Qwen3.8-27B-UD-IQ4_XS.gguf.

I am trying to upstream these things but I'm getting little response so far:
ggml-org#28613
ggml-org#28616

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants