Skip to content

vulkan: tune mat-vec rows for batched inference on Strix Halo - #27909

Merged
ggerganov merged 2 commits into
ggml-org:masterfrom
SimonTeixidor:strix-halo-vulkan-tuning
Aug 31, 2026
Merged

vulkan: tune mat-vec rows for batched inference on Strix Halo#27909
ggerganov merged 2 commits into
ggml-org:masterfrom
SimonTeixidor:strix-halo-vulkan-tuning

Conversation

@SimonTeixidor

Copy link
Copy Markdown
Contributor

Overview

I found that by tuning rm_kq_int and rm_stdq_int I'm able to get better performance for batch sizes 5-8 (mostly in that range, some operations get improvements on smaller batch sizes as well) on Strix Halo.

Additional information

This is my first attempt at this kind of tuning, so it's certainly possible that I've done something wrong. I've tried to be as rigorous as possible in my testing and attach my findings here.

I investigated if rm_int_n could be a function of the batch size, but it appears to be a shoulder between n=4 and n=5, at least on this hardware. It seems to me like even more performance could be squeezed out by tuning on a per type basis, but that would require some kind of pattern/infrastructure for that.

I have gated these changes on device->vendor_id == VK_VENDOR_ID_AMD && device->architecture == AMD_RDNA3 so it should not impact GPUs from other vendors/other generations of AMD GPUs. I only have a Strix Halo machine to test on though, so I can't say if these improvements hold up on other RDNA3 cards. Someone more experienced might be able to tell if this kind of tuning generally works across the whole architecture or not?

KLD and PPL figures

To ensure that these changes didn't break anything, I calculated KLD and PPL figures against master.

Method:

llama-perplexity -m /models/<MODEL>.gguf  -f /data/wikitext-2-raw/wiki.test.raw -c 512 -b 8 -ub 8 --chunks 16 -ngl 99 --kl-divergence-base /kld/<MODEL>.kld
llama-perplexity -m /models/<MODEL>.gguf -f /data/wikitext-2-raw/wiki.test.raw -c 512 -b 8 -ub 8 --chunks 16 -ngl 99 --kl-divergence --kl-divergence-base /kld/<MODEL>.kld

Results:

model Mean KLD Max KLD Min KLD Max Δp RMS Δp Same top token
qwen27b_q5ks 0.000000 0.000054 -0.000056 0.006% 0.001% 100.000%
qwen27b_mxfp4 0.000000 0.000063 -0.000062 0.005% 0.001% 100.000%
qwen36moe -0.000000 0.000057 -0.000059 0.005% 0.001% 100.000%
muse30b 0.000000 0.000059 -0.000055 0.005% 0.001% 100.000%
model Mean PPL(patched)/PPL(base)
qwen27b_q5ks 1.000588 ± 0.000374
qwen27b_mxfp4 1.000444 ± 0.000326
qwen36moe 1.000817 ± 0.000578
muse30b 1.002657 ± 0.001290

On these models, nothing appears broken.

Micro benchmarks

Method:
test-backend-ops perf -o MUL_MAT -b Vulkan0 on both branches, comparing results against each other.

Results:

type touched n=1 n=2 n=3 n=4 n=5 n=8 n=512
bf16 . -0.7% -0.2% -0.2% -2.8% -0.3% -1.3% -0.5%
f16 . -0.0% -0.3% -0.4% -0.5% -0.3% -1.1% -0.0%
f32 . +0.2% -0.8% -0.9% +0.4% -0.0% -1.6% +0.5%
iq1_m . +0.0% +0.4% +0.5% +2.6% +2.2% +3.1% +0.1%
iq1_s . -0.1% +0.5% +0.6% +0.1% -10.5% +2.8% -0.0%
iq2_s . +0.4% +0.5% -0.6% -1.3% -0.0% -0.5% +1.2%
iq2_xs . -0.3% +0.5% +0.3% -0.7% -0.4% -0.1% +0.6%
iq2_xxs . +0.2% +0.7% +0.8% +0.8% +0.2% +0.4% -0.9%
iq3_s . -0.3% -0.0% -1.6% -0.8% -0.2% +0.0% +1.9%
iq3_xxs . +0.5% +1.2% -0.1% -0.3% -1.9% -0.3% +0.5%
iq4_nl . +1.7% -3.8% +1.2% +0.1% -0.1% -0.2% +1.6%
iq4_xs . +1.7% +0.9% +0.8% -0.3% -1.0% -0.4% -0.9%
mxfp4 yes +0.6% -0.0% +0.0% -0.2% +22.5% +20.4% -0.2%
nvfp4 . +0.8% -0.2% +0.7% -0.4% -0.4% +0.8% +0.7%
q1_0 . +0.1% +0.2% +0.1% -0.0% -0.1% -0.4% +1.8%
q2_0 yes +0.1% -0.5% +0.4% -0.0% +16.0% +12.3% +0.3%
q2_K yes +0.3% +0.2% +0.2% -0.2% +16.2% +8.2% +1.3%
q3_K yes +1.4% -0.6% +0.5% -0.6% +20.3% +25.0% +1.4%
q4_0 yes -1.1% -0.4% +0.1% -0.3% +44.4% +46.2% -0.4%
q4_1 yes -6.2% -0.7% +0.5% +0.3% +42.4% +43.4% +1.2%
q4_K yes -0.8% -2.9% -0.1% -0.3% +22.2% +31.0% +2.5%
q5_0 yes +2.8% -4.4% -0.1% -0.3% +38.0% +42.6% -0.0%
q5_1 yes +1.2% -2.9% -0.8% -0.5% +15.4% +25.7% +0.1%
q5_K yes +1.2% +3.0% +4.4% +13.8% +6.2% +19.6% +2.1%
q6_K yes +0.1% -2.7% +3.3% -1.3% +0.2% +0.1% +1.6%
q8_0 yes +0.5% -1.5% -0.9% -1.3% -3.8% +1.8% +0.2%
tq2_0 . +0.2% -0.1% -0.6% +0.1% +0.2% +0.0% +0.4%

In this one, we expect speedup at n=5 and above.

Method:
test-backend-ops perf -o MUL_MAT_ID -b Vulkan0 on both branches, comparing results against each other.
Results:

type experts shape reached n=1 n=4 n=8 n=32 n=64 n=128 n=256 n=512
f16 128x8 m768 k2048 control +0.0% -2.1% -0.0% -5.2% -0.7% -1.1% +2.2% -1.9%
f16 32x4 m1792 k2048 control +0.4% -6.1% -0.7% +4.4% -7.5% +8.1% -3.7% +0.0%
f32 128x8 m768 k2048 control +0.6% -2.4% +3.5% +1.7% +1.6% +0.8% -0.3% -0.6%
f32 32x4 m1792 k2048 control +0.7% -4.7% -2.5% +3.1% -0.2% +0.8% +0.1% -1.4%
iq2_xs 128x8 m768 k2048 control -1.8% -0.7% +0.4% -2.8% -0.9% -0.1% -1.0% +0.5%
iq2_xs 32x4 m1792 k2048 control +0.6% -0.1% +0.7% +1.3% +0.1% +0.4% -0.4% +9.7%
mxfp4 32x4 m2880 k2880 yes +3.9% +16.5% +13.0% -- -- -- -- -4.3%
q4_0 128x8 m768 k2048 yes +12.2% +27.0% +24.6% +0.8% -2.5% -0.4% -0.9% -1.0%
q4_0 32x4 m1792 k2048 yes +16.9% +27.1% +27.0% -0.5% +4.3% +0.3% +3.5% -18.0%
q4_K 128x8 m768 k2048 yes +2.0% +14.4% +0.8% -11.8% -0.6% -1.0% -1.1% -0.7%
q4_K 32x4 m1792 k2048 yes +6.6% +13.4% +21.5% +0.4% +0.8% +0.8% +2.6% -0.4%
q6_K 128x8 m768 k2048 not used on this HW +0.1% +3.6% -20.7% -1.2% -1.2% -0.8% -0.8% -0.1%
q6_K 32x4 m1792 k2048 not used on this HW +0.6% +13.0% -18.8% +0.1% -0.4% -0.2% +0.1% -12.5%
q8_0 128x8 m768 k2048 yes +1.6% -2.1% +1.0% +3.4% -1.0% +0.6% -0.2% -0.1%
q8_0 32x4 m1792 k2048 yes +0.1% +8.1% +13.2% -2.9% -0.0% -0.4% +1.4% -9.3%

But a big caveat! First, the results here swing wildly so the noise floor is high. Notably, q6_k has huge variance between runs, but doesn't reach this kernel on this hardware in practise anyway (ggml_vk_should_use_mmvq(...) returns false for q6_K on AMD). Second, the results are not really comparable to real inference on these types. The direction seems to agree between the micro bench and real inference though. I'm not able to test real inference speed on real quants in all those formats, so I ran the micro benchmarks and real inference for pure quants in a few of these formats.

Batch inference performance

Method:
llama-batched-bench -npp 256 -ntg 256 -npl 1,2,4,8 -c 8192 -b 2048 -ub 512 -ngl 99 compared between master and branch.
Result:

model npl 1 npl 2 npl 4 npl 8 npl 8 t/s
Qwen3.8-27b pure Q4_K 27B +0.07% +0.23% +0.39% +18.10% 47.91 -> 56.59
Qwen3.8-27b pure Q2_K 27B -0.09% -0.04% -0.07% +3.85% 47.53 -> 49.36
Qwen3.8-27b pure Q8_0 27B -0.68% -0.08% -0.05% +9.46% 35.94 -> 39.34
Qwen3.8-27B pure MXFP4 +0.14% +0.17% +0.15% +11.47% 43.95 -> 48.99
Qwen3.8-27B Q5_K_S (mixed quant recipe) -0.06% -0.05% -0.05% +8.65% 40.78 -> 44.31
Qwen3.6-35B-A3B Q5_K_S (mixed quant recipe) -0.77% +1.16% +3.63% +8.70% 130.41 -> 141.76
muse-glimmer-30B-kquant-dynamic +0.03% +0.07% +0.08% +8.09% 50.62 -> 54.72
Qwen3.8-27B UD-Q5_K_XL (unsloth dynamic) +0.79% +0.02% +0.02% +5.33% 37.96 -> 39.99

Good speedup at batch size 8.

Spec. decoding performance

This was my goal all along, with better batched performance we should be able to speed up single batch inference with spec decoding. I test this with two prompts, one that gives very high draft acceptance (copy) and one that gives a modest/normal (normal) draft acceptance:

muse-glimmer-30B-kquant-dynamic with Dflash drafter:

n-max prompt draft accept base t/s patched t/s speedup
off copy -- 10.56 10.57 +0.09%
off normal -- 10.60 10.47 -1.21%
n2 copy 100.0% 25.86 26.07 +0.83%
n2 normal 70.2% 20.68 20.88 +0.96%
n4 copy 100.0% 35.14 38.11 +8.47%
n4 normal 48.5% 20.61 22.55 +9.40%
n7 copy 100.0% 40.97 46.16 +12.65%
n7 normal 33.5% 17.64 19.86 +12.57%

Qwen3.6 35B with MTP:

n-max prompt draft accept base t/s patched t/s speedup
off copy -- 47.54 47.50 -0.10%
off normal -- 47.60 47.66 +0.12%
n2 copy 93.3% 72.07 73.05 +1.37%
n2 normal 71.8% 63.42 64.34 +1.44%
n4 copy 85.3% 87.24 87.54 +0.35%
n4 normal 42.7% 55.68 56.34 +1.18%
n7 copy 88.9% 101.00 107.89 +6.82%
n7 normal 54.1% 70.51 75.00 +6.37%

So at 7 draft tokens we see a nice boost on both models, and on the dense model we see it already from 4 draft tokens.

Requirements

  • I have read and agree with the contributing guidelines
  • AI usage disclosure: Yes. Claude helped me write scripts to sweep the possible values for these parameters, and then implement what we found was close to optimal on this hardware.

@SimonTeixidor
SimonTeixidor requested a review from a team as a code owner August 28, 2026 21:16
@github-actions github-actions Bot added Vulkan Issues specific to the Vulkan backend ggml changes relating to the ggml tensor library for machine learning labels Aug 28, 2026
Comment thread ggml/src/ggml-vulkan/ggml-vulkan.cpp Outdated
const bool rm_abs = device->vendor_id == VK_VENDOR_ID_AMD && device->architecture == AMD_RDNA3;
auto const &rm_int_n = [&](uint32_t rows, uint32_t i) { return (rm_abs && i >= 4) ? 4u : rows; };
// RDNA3: Static 4 rows for all types bench faster than the default
auto const &rm_id = [&](uint32_t rows) { return rm_abs ? 4u : rows; };

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I did the simplest thing that worked for tuning this case here. If in the future this needs to be scaled differently for different hardware we need some better mechanism than my rm_abs flag with hard coded 4 as the only option. Not sure if I should bother to explore that now though?

@0cc4m 0cc4m left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

looks good otherwise, I can reproduce the perf increase.

Comment thread ggml/src/ggml-vulkan/ggml-vulkan.cpp Outdated
On RDNA3 above four columns a static 4 rows for all types benches faster than
the default.
mul_mat_vec_id has no column dimension to switch on. On my Strix Halo machine,
a static 4 is faster here than the defaults across types and batch sizes.
@SimonTeixidor
SimonTeixidor force-pushed the strix-halo-vulkan-tuning branch from 9970e84 to 6709f1e Compare August 30, 2026 16:00
@0cc4m

0cc4m commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

@ggerganov The Apple Vulkan CI is consistently broken with 40 - test-thread-safety (Bus error): https://github.com/ggml-org/llama.cpp/actions/runs/33321211000/job/99317859088?pr=27909

@0cc4m 0cc4m added the merge ready A maintainer can use this label to indicate that they consider the changes final and ready to merge. label Aug 31, 2026
@ggerganov

Copy link
Copy Markdown
Member

@ggerganov The Apple Vulkan CI is consistently broken with 40 - test-thread-safety (Bus error): https://github.com/ggml-org/llama.cpp/actions/runs/33321211000/job/99317859088?pr=27909

Hm, weird. Running it manually on that machine does not trigger the problem.

@ggerganov
ggerganov merged commit 2cdae80 into ggml-org:master Aug 31, 2026
25 of 30 checks passed
wdenejko added a commit to wdenejko/llama.cpp that referenced this pull request Aug 31, 2026
54 upstream commits since 1844325. Conflict resolutions keep the fork's
validated mmid stack authoritative and graft upstream additions alongside:
- mul_mat_id pc: union of our padded_N/use_row_lists/fusion_flags/tile_list_base
  and upstream's n_experts/hoist_row_ids (ggml-org#26686); mmq declares tile_list_base
  as layout filler so both shader blocks match the one host struct.
- row-id loading: our row-lists prepass first, upstream's hoisted path second
  (their gate is n_as <= 256, so qwen4exp n_as=512 never hoists), ballot scan
  as the fallback; hoisting yields the prepass buffer to our row lists.
- K-vs-N padding (ggml-org#27925): took upstream's y_staged_row_stride/K-pad scheme;
  padded_N stays as a pc-layout filler (no merged shader reads it).
- Strix mat-vec tuning (ggml-org#27909): took upstream's rm_id/rm_int_n RDNA3 lambdas
  alongside our GGML_VK_MMV_RM_* sweep knobs.
- kept our epilog-fusion ctx fields next to upstream's fused_topk_qsa (ggml-org#28032).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
wdenejko added a commit to wdenejko/llama.cpp that referenced this pull request Aug 31, 2026
…VK_RDNA3_MMV_TUNE

ggml-org#27909 benches faster on RDNA3 dGPUs but costs tg on gfx1151 (Strix Halo APU):
post-merge decode -5% shallow / -13% @32k at equal draft acceptance. Default
restores the pre-merge dispatch (generic 1/2 rows per WG); =1 re-enables the
upstream tuning. Launcher forwards the knob.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ggerganov

Copy link
Copy Markdown
Member

@0cc4m Strangely, the failures seem to start happening after this PR: #28032

For example, here I just ran the CI on the commit right before that PR and the CI passes: https://github.com/ggml-org/llama.cpp/actions/runs/33434794357/job/99628492302

Any ideas?

Nathanw1014 pushed a commit to Nathanw1014/llama.cpp that referenced this pull request Aug 31, 2026
…rg#27909)

* vulkan: RDNA3 static mat-vec rows above four columns

On RDNA3 above four columns a static 4 rows for all types benches faster than
the default.

* vulkan: RDNA3 static mat-vec-id rows

mul_mat_vec_id has no column dimension to switch on. On my Strix Halo machine,
a static 4 is faster here than the defaults across types and batch sizes.
@0cc4m

0cc4m commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

I think that might be coincidental, e.g. my PR #27952 also has the same failure, but it predates the radix PR and does not contain its commit yet: https://github.com/ggml-org/llama.cpp/actions/runs/33251797688/job/99098628443?pr=27952

@ggerganov

Copy link
Copy Markdown
Member

I see. I'll try to restart the mac runners today and clear their caches. See if it makes a difference.

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

Labels

ggml changes relating to the ggml tensor library for machine learning merge ready A maintainer can use this label to indicate that they consider the changes final and ready to merge. Vulkan Issues specific to the Vulkan backend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants