Skip to content

vulkan, hip : Strix Halo prefill for qwen4exp and qwen35 (upstream catch-up + strix-llama.cpp ports) - #393

Merged
gianni-cor merged 22 commits into
temp-11018from
qwen4exp-opt-temp-11018
Oct 16, 2026
Merged

gianni-cor merged 22 commits into
temp-11018from
qwen4exp-opt-temp-11018

Conversation

@amangupta-tether

@amangupta-tether amangupta-tether commented Oct 15, 2026 •

Copy link
Copy Markdown

Overview

Prefill speedups for Strix Halo (Ryzen AI MAX+ 395, gfx1151), mainly on Vulkan, measured on Qwen3.8 Flash-Next (qwen4exp) and Qwen3.8 27B (qwen35). This covers the work that pays off at short context; attention at depth (QSA selected-key attention, multi-row prefill flash attention) will follow in a separate PR.

Three kinds of change, one commit per optimization:

ggml-org#29182 is picked and then reverted by ggml-org#29936, as upstream did. The revert is neutral here (Flash-Next pp2048 672.5 vs 669.6 t/s) and is kept to match upstream.

Additional information

Results

GMKtec EVO-X2: Ryzen AI MAX+ 395 / Radeon 8060S (gfx1151), 64 GiB GPU carve-out, Mesa 26.0.8 RADV, ROCm 10.0. llama-bench -ngl 999 -fa 1 -b 2048 -ub 2048 -p 2048 -n 128; Flash-Next adds -lzm on.

Vulkan, pp2048 at empty context (t/s):

model temp-11018 this PR upstream a0e6dc738 strix-llama.cpp deb5e2f
Qwen3.8 Flash-Next UD-IQ4_XS 233 684 486 730
Qwen3.8 27B UD-Q8_K_L 234 328 267 335
Qwen3.8 27B Q4_K_M 248 305 253 309

ROCm at empty context (t/s), same builds, -p 2048 -n 512, 2 runs:

model test temp-11018 this PR upstream a0e6dc738 strix-llama.cpp deb5e2f
Qwen3.8 Flash-Next UD-IQ4_XS pp2048 256.1 525.2 518.8 1189.7
tg512 21.50 22.95 22.98 27.23
Qwen3.8 27B Q4_K_M pp2048 309.1 308.0 320.7 349.5
tg512 12.31 12.31 12.30 12.42

On ROCm, this PR brings Flash-Next level with upstream, through the same first five commits. The 27B Q4_K_M is unchanged: its matmuls are Q4_K/Q6_K, which the new Q8_0 hipBLAS rule (e10627480) does not cover. With HIP 7.15 that rule takes Qwen3.8 27B UD-Q8_K_L from 310 to 354 t/s. On a ROCm 7.2.2 box it measured 317 -> 288 t/s, so it only applies from HIP 7.15 on. strix-llama.cpp's ROCm lead comes from its BF16 WMMA matmul (MMB) and fused qwen4exp HIP kernels, which are not part of this PR. Decode is unchanged or slightly faster in every case.

Per commit, Vulkan pp2048 unless noted. Each number comes from the same build with and without the commit, or the commit's env switch off/on, interleaved:

commit change Flash-Next 27B Q8
6a349d6ef llama_prefetch_rows (ggml-org#29599), with the four commits before it 233 -> 429 (ROCm 257 -> 528) —
e10627480 RDNA3.5 Q8_0 -> hipBLAS above 512 rows (ROCm, HIP >= 7.15 after f2df7a3c9) unchanged 310 -> 354
8ee3e0848 tiled concat transpose 432 -> 484 240 -> 258
e092d0386 direct GDN conv 483 -> 491 260 -> 261
f964031f4 int8 coopmat1 matmul (ggml-org#27952) 492 -> 520 262 -> 320
105e1ad7f int8 MMQ vec4 stores 519 -> 535 321 -> 326
1be77e089 router weights in the MMQ MUL_MAT_ID epilogue 540 -> 557 —
6f69f4bd1 hc_post gate fusion (ggml-org#29520) prefill flat, tg 25.94 -> 26.22 —
854e8164d short-row RMS norm fusions 551 -> 565 flat
672f167ed 512-expert row-id hoist (ggml-org#28501) 575 -> 677 —
2f3fcbe9d small-M operand swap 670 -> 682 (inject matmul 1805 -> 557 us/call) flat

Fabric-specific adaptations

  • vulkan: int8 coopmat1 matmul implementation for AMD RDNA3 and RDNA4 ggml-org/llama.cpp#27952: a non-contiguous quantized src0, which fabric supports by dequantizing it to f16, no longer takes the int8 path. Without this, 3 MUL_MAT cases failed (src1 was quantized to q8_1 for the f16 pipeline). Also AMD_RDNA4 goes into the enum in ggml-vulkan.cpp, and fabric's small-tile warp width (30b7e5162) is kept.
  • vulkan: fuse qwen4exp's SCALE -> SIGMOID -> SCALE -> hc_post chain ggml-org/llama.cpp#29520 and the strix commits target upstream's split Vulkan headers (ggml-vulkan-types.h and the others). Their declarations go into ggml-vulkan.cpp here.
  • The small-M swap keeps fabric's wider src1 types (BF16, quantized) on the existing M = 1 route; only the new transposed route is restricted.
  • The strix RMS_NORM_SCALE test is test_rms_norm_scale_short, because fabric already has a test_rms_norm_scale. Fabric's harness has no scheduler-allocation mode, so the strix tests that used it run on the plain allocator.

Testing

On gfx1151, Vulkan0: test-backend-ops MUL_MAT 2123/2123, MUL_MAT_ID 1199/1199, MUL_MAT_ID_FUSION 22/22, CONCAT 226/226, CONCAT_SSM_CONV_SILU 12/12, SSM_CONV 53/53, DSV4_HC_POST 9/9, RMS_NORM 60, RMS_NORM_MUL_MUL 10, RMS_NORM_SCALE 12, RMS_NORM_MUL_ADD 36, L2_NORM 30 and NORM 60, all passing. The env-gated changes were also tested with their gate off.

test-recurrent-state-rollback (qwen35, deepseek4, nemotron-h and kimi-k3 test models) passes on Vulkan0 with GGML_SCHED_DEBUG_REALLOC=1 after 587d94865. On ROCm0: MUL_MAT 2279/2279, TOPK_MOE 416/416 and ARGSORT 46/46.

Perplexity (wikitext, c = 2048):

Not tested: CUDA, Metal, OpenCL and Vulkan on other vendors. The int8 coopmat1 path, the vec4 stores and the router-weight epilogue only engage on RDNA3/RDNA4. The concat transpose, the direct GDN conv, the small-M swap and the row-id hoist apply on every Vulkan device but were only run on RADV. The CUDA-side commits (ggml-org#28432, ggml-org#29599) and the HIP rule were only built and run on gfx1151.

Requirements

  • I have read and agree with the contributing guidelines
  • AI usage disclosure: YES. Claude Code (Claude Opus 5.5) did the cherry-picks, conflict resolution and hand ports, built and ran the benchmarks and test-backend-ops on the EVO-X2, and drafted this description; the commits carry Co-Authored-By trailers.

🤖 Generated with Claude Code

am17an and others added 16 commits October 15, 2026 11:02
* vulkan: add IQ3_S MMQ matmul kernels

* Make block_a_to_shmem do 2-byte loads (110 bytes is divisible by 2)

* Align the check, IQ3_S is also using K tile size

(cherry picked from commit bbd488c)
* vulkan: optimize IQ4_XS matmul kernels

Assisted-by: OpenAI Codex

* vulkan: address IQ4_XS review nits

- drop the dead LOAD_VEC_A != 8 branch in the IQ4_XS shmem load; iq4_xs is
  in lut_load_vec_a()'s "8" list, so that path is never generated
- disable MMVQ for IQ4_XS on Intel (27.3% tg regression on A770)
- remove a stray empty line in types.glsl

Assisted-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit b1ff4ca)
mut_mul_id selected its matmul tile with total token count.
For MoE dispatch grid the true N per workgroup is per-expert rows.
At pp128 on Sarvam 30B that is 6, not 128, so the picker took the l-tile for ~6 live rows.
Most workers in each group had nothing to do.
This wasted time. The slow part was 55% of the whole job.

(cherry picked from commit 94a0ae3)
* llama: llama_prefetch_rows

* llama: support row prefetch on Windows

Apply the Windows port contributed by @praneshgo unchanged.

Source: ggml-org#29599 (comment)

* avoid exposing llama-mmap in model code, route via llama-impl

* add windows check, only prefetch in lazy mode

* cont : clean-up

* cont : fix build

* cont : clarify padding token for gemma4

---------

Co-authored-by: Pranesh Gonegandla <pranesh.iitp@gmail.com>
Co-authored-by: Georgi Gerganov <ggerganov@gmail.com>
(cherry picked from commit 185103d)
On gfx1151 MMQ for Q8_0 tops out at about 18 TFLOPS on the dense
Qwen3.8-27B shapes, while dequantization + hipBLAS reaches 18-24.
Route dense Q8_0 matmuls with more than 512 rows to hipBLAS; MoE
(n_experts >= 64) keeps MMQ.

Qwen3.8-27B UD-Q8_K_L, llama-bench -fa 1 -ub 2048, t/s:
  pp512   312 -> 301 (noise, MMQ kept here)
  pp1024  291 -> 320
  pp2048  310 -> 354
  tg128   7.9 -> 7.9
Flash-Next UD-IQ4_XS pp2048 unchanged (529 -> 531).

GGML_CUDA_RDNA3_5_Q8_0_MMQ_MAX overrides the row limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The delta-net conv input is CONCAT(state, transpose(x)) along dim 0. The
generic concat kernel walks the transposed src1 with a channels * 4 byte
stride (40960 B on qwen35), so every read lands on the same memory channel.
Route that exact shape through a 32x32 shared-memory tile transpose.
Ported from halo-box/strix-llama.cpp (f4d6f9e, e249248).

gfx1151 RADV, llama-bench pp2048 -ub 2048 -fa 1, t/s:
  Qwen3.8-27B UD-Q8_K_L          239.5 -> 258.0
  Qwen3.8-Flash-Next UD-IQ4_XS   431.7 -> 483.5
tg128 unchanged. GGML_VK_CONCAT_TRANSPOSE=0 opts out.

test-backend-ops: CONCAT cases with the delta-net shape (3 x 77 / 10240
channels, 1..2048 tokens, transposed src1), 226/226 on Vulkan0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Gated DeltaNet layers build CONCAT(state, transpose(x)) -> SSM_CONV -> SILU,
plus copies of the last 3 concat columns into the recurrent conv state. When
the concat's only readers are that conv and the tail copies, the concat node
writes only the tail columns, and the conv + SiLU reads x and the state
directly (4-tap window in registers, same dot() as the nc == 4 path, so the
output is identical). graph_optimize keeps x and the state allocated through
the SiLU; the runtime match re-checks the addresses. Ubatches under 8 tokens
keep the concat. Ported from halo-box/strix-llama.cpp (2dfe8e5).

gfx1151 RADV, llama-bench pp2048 -ub 2048 -fa 1, on top of the tiled concat:
  Qwen3.8-27B UD-Q8_K_L          259.5 -> 260.8
  Qwen3.8-Flash-Next UD-IQ4_XS   483.2 -> 490.5
Qwen3.8-27B wikitext PPL (4 x 2048) identical per chunk with both changes off
and on. GGML_VK_SSMCONV_DIRECT=0 opts out; GGML_VK_FUSION_DEBUG=1 prints why a
concat did not match.

test-backend-ops: CONCAT_SSM_CONV_SILU cases (ragged channels and token
tiles, two sequences, 1/4/8 rollback slots, 10240 channels, an extra
whole-concat reader that must not fuse), 12/12 with the direct conv on and off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gml-org#27952)

* vulkan: add int8 coopmat quantized matmul shader

* apply scales inline

* use scalar sums

* probe and directly access coopmat values instead of going through shmem

* add q8_0 support

* add BK_STEP to shader, default to 2

* use larger workgroups

* double buffering

* preload scales

* coopmat load first, then wmma

* use float for scales

* add faster RDNA int->float conversion

* workgroup scheduling for cache proximity

* clean up

* use wave32

* restructure for vgpr use

* skip computation for inactive tiles

* only force subgroup size 32 on AMD RDNA

* use BK_STEP 4

* fix compilation

* move quant-specific prefetch function out of main file

* add q4_1, q5_0, q5_1 support

* restructure mmq cm1 functions

* enable mul_mat_id support

* fix segfault

* fix mul_mat_id bug

* support iq4_nl and mxfp4

* remove elem row/col fast path, invalid for RDNA4

* use shmem arrays for LUTs

* use 4-byte loads where possible

* add q3_k, q4_k, q5_k, q6_k and nvfp4 support

* fix l warptile

* improve performance

* improve performance

* improvements

* dedup b scales

* merge shmem arrays

* undo uint8_t, gate to RDNA3/4

* add RDNA4 architecture, use for hardcoded coopmat elem thread access, set BK_STEP back to 4

* improve offset application

* clean up

* fix iq4_nl and nvfp4 performance

* rdna4 tuning

* use BK_STEP 2 on MUL_MAT_ID

* adapt to upstream changes

* fix shmem support function, clean up comments

* fix warptile logic

Co-authored-by: Piotr Wilkin (ilintar) <piotr.wilkin@syndatis.com>

* vulkan: add IQ4_XS support to the coopmat1 integer matmul shader (ggml-org#28440)

Adds IQ4_XS to mul_mmq_cm1: dedicated block_a_load/block_a_to_shmem that
expand both nibbles of each packed32 word through cm1_kvalues, LOAD_VEC_A 8
and an IQ4_XS-sized a_panel_bytes estimate for the L2-friendly scheduling.

Assisted-by: OpenAI Codex

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* avoid compiling f16 acc shader variants

---------

Co-authored-by: Piotr Wilkin (ilintar) <piotr.wilkin@syndatis.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 70c4e15)

fabric adaptations: the AMD_RDNA4 architecture value goes into the enum in
ggml-vulkan.cpp (fabric has no ggml-vulkan-types.h); fabric's own small-tile
warp width (30b7e51) is kept; and a non-contiguous quantized src0, which fabric
supports by dequantizing it to f16 first, no longer takes the int8 path
(otherwise src1 was quantized to q8_1 for the f16 pipeline: 3 MUL_MAT failures).
gfx1151 RADV, llama-bench pp2048 -ub 2048 -fa 1 (4 runs each): Qwen3.8-27B
UD-Q8_K_L 262.4 -> 319.9 t/s, Flash-Next UD-IQ4_XS 491.8 -> 519.6 t/s, tg128
unchanged. Qwen3.8-27B wikitext PPL (4 x 2048) now matches upstream Vulkan per
chunk. test-backend-ops MUL_MAT 2065/2065, MUL_MAT_ID 1168/1168 on Vulkan0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cm1 int8 MMQ epilogue wrote its f32 result one element at a time. On
the RDNA3/3.5 wave32 accumulator layout lane l holds column l % 16 and rows
(l / 16) + 2e, so every store instruction scattered 4-byte writes over 16
columns. Two subgroupShuffleXor(16) per four elements now give each lane four
adjacent rows of its column, and the lane writes them as one vec4. This
covers MUL_MAT and MUL_MAT_ID.

- The packed path needs stride_d (and batch_stride_d or the batch offset) to
  be a multiple of 4. Otherwise the per-element store runs, for example with
  odd M. A vec4 that crosses the row end falls back to scalar stores, so
  nothing is written past M.
- It is enabled by spec constant 12 (PACKSTORE) of mul_mmq_cm1.comp.
  GGML_VK_MMQ_PACKSTORE=0 turns it off. RDNA4 (blocked layout) and other
  tile shapes keep the old store.
- test-backend-ops: new int8 MMQ cases with ragged M/N (M = 20, 36, 132,
  199, 68x2 batch; N = 67, 129) for MUL_MAT and MUL_MAT_ID.

Results on gfx1151 (RADV):
- MUL_MAT + MUL_MAT_ID 14980/14983 with the gate on and off; the 3 failures
  are the same pre-existing CONCAT k=5120 cases in both arms.
- Positive controls: swapping lanes inside the vec4 fails thousands of
  MUL_MAT and MUL_MAT_ID cases with the gate on and none with it off.
  Dropping the row-end check fails the ragged-M cases, while odd M = 199
  passes because it takes the scalar fallback.
- llama-bench, same binary with the gate off/on, ABBA x2, -r 2 (mean of 8):
  Flash-Next REAP-320 pp2048 -ub 2048 769.5 -> 796.3 t/s (+3.5%),
  Qwen3.6-35B-A3B pp2048 -ub 2048 2100.5 -> 2187.1 (+4.1%),
  Qwen3.8-27B pp512 -ub 256 430.9 -> 441.5 (+2.4%). Every gate-on sample
  beat every gate-off sample.
- Flash-Next perplexity is identical per chunk with the gate on and off.

Assisted-by: Claude Opus 5.5
(cherry picked from commit 22c9ae8)
fabric, gfx1151 RADV, same binary with GGML_VK_MMQ_PACKSTORE=0/1, interleaved x2, -r 2:
pp2048 -ub 2048 Qwen3.8-27B UD-Q8_K_L 320.7 -> 325.9 t/s, Flash-Next UD-IQ4_XS
518.8 -> 534.7 t/s, tg128 unchanged. test-backend-ops MUL_MAT 2090/2090 and
MUL_MAT_ID 1183/1183 with the gate on and off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MUL_MAT_ID followed by MUL with the router weights ([1, n_expert_used,
n_tokens]) was fused only on the mat-vec path. At prefill the expert outputs
were written as f32, read back by a separate MUL, scaled and written again.

The int8 MMQ coopmat1 MUL_MAT_ID pipelines get a sixth binding holding the
weights. Bit 1 of the hoist_row_ids push constant tells the epilogue to scale
each output row by its (expert slot, token) weight before the store. The
product is the same f32 multiply the separate MUL does, so results are
identical.

The fusion is admitted only when ggml_vk_mul_mat_id_q_f16 will take the int8
MMQ path (checked against the pipeline's binding count); other MUL_MAT_ID
GEMMs keep the separate MUL. GGML_VK_MMID_EPILOGUE=0 disables it. The packed vec4
store path scales its four rows by the same column weight.

test-backend-ops: 9 new MUL_MAT_ID_FUSION cases (q8_0, iq4_nl, q4_K; n_used
10/4; ragged and odd M). MUL_MAT_ID + MUL_MAT_ID_FUSION + MOE_PREFILL
7561/7561 with the gate on and off. Scaling by twice the weight in the new
epilogue fails every GEMM-path fusion case with the gate on and none with
it off.

gfx1151 (RADV), pp2048 -ub 2048, same binary with the gate off/on, ABBA x2,
-r 2 (mean of 8): Flash-Next REAP-320 775.7 -> 800.2 t/s (+3.2%),
Qwen3.6-35B-A3B 2100.4 -> 2207.5 (+5.1%). Every gate-on sample beat every
gate-off sample. Perplexity is identical in every chunk.

Assisted-by: Claude Opus 5.5
(cherry picked from commit 30ef039)
fabric, gfx1151 RADV, same binary with GGML_VK_MMID_EPILOGUE=0/1, interleaved x2, -r 2:
Flash-Next UD-IQ4_XS pp2048 -ub 2048 539.9 -> 556.6 t/s, tg128 unchanged; wikitext
PPL (3 x 2048) identical per chunk. test-backend-ops MUL_MAT_ID 1183/1183,
MUL_MAT_ID_FUSION 22/22, MUL_MAT 2090/2090 with the gate on and off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gml-org#29520)

Ported by hand from upstream 03a667a: upstream had split the Vulkan backend
into headers, so the pattern, edges, context flag and push constants go into
ggml-vulkan.cpp here.

fabric, gfx1151 RADV, Flash-Next UD-IQ4_XS, -ub 2048 -fa 1, interleaved x2 -r 2:
pp2048 556.8 -> 554.7 t/s (noise), tg128 25.94 -> 26.22 t/s (every sample
above the old maximum). test-backend-ops DSV4_HC_POST 9/9, including the
gated cases, with fusion on and off.

(cherry picked from commit 03a667a)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… fusions

rms_norm.comp runs one 512-thread workgroup per row, so on 128-wide rows (the GDN
head norms) 3/4 of the lanes idle and every row pays a shared-memory tree reduction.
rms_norm_small.comp gives each row one subgroup (subgroupAdd), for ne00 <= 256,
f32 in/out, contiguous rows. It handles plain RMS_NORM and RMS_NORM_MUL, plus two
fusions only it has:

- RMS_NORM_MUL_MUL: the gated norm, normalised * gamma * gate (qwen3.5/qwen4exp
  build_norm_gated). The gate chain (sigmoid/silu of z) sits between the two MULs
  in the graph, so graph_optimize now schedules it ahead of the norm and keeps the
  three nodes adjacent. The products are the same f32 multiplies as the separate ops.
- RMS_NORM_SCALE: build_gdn_l2_norm, scale(rms_norm(x, eps/n), 1/sqrt(n)). The
  factor is applied on the store after rounding the normalised value to f32, the
  same two roundings as the SCALE op. graph_optimize keeps the SCALE behind its norm.

The kernel strides over rows, so the grid can be capped at 65535 workgroups and a
subgroup size other than the one the host assumed still covers every row.

GGML_VK_NORM_SMALL=0 restores the old path (and disables both fusions);
GGML_VK_FUSE_NORM_EXTRA=0 keeps the kernel but disables the two fusions.

gfx1151 (RADV), same binary with GGML_VK_NORM_SMALL off/on, ABBA x2, -r 2
(mean of 8): Flash-Next REAP-320 pp2048 -ub 2048 773.3 -> 785.0 t/s (+1.5%),
Qwen3.6-35B-A3B pp2048 -ub 2048 2100.4 -> 2145.5 (+2.1%), Qwen3.8-27B pp512
-ub 256 430.5 -> 431.6 (flat). Norm ops 640/640 in all three gate settings.
PPL moves only by the reduction order (subgroupAdd vs the shared-memory tree).
Only tested on gfx1151.

Assisted-by: Claude Opus 5.5
(cherry picked from commit 5206263)
fabric port notes: the rms_norm_mode values and pipelines go into ggml-vulkan.cpp
(no ggml-vulkan-types.h); the RMS_NORM_SCALE test is test_rms_norm_scale_short
(fabric already has a test_rms_norm_scale); fabric's test harness has no
scheduler-allocation mode, so those two tests run on the plain allocator.

fabric, gfx1151 RADV, same binary with GGML_VK_NORM_SMALL=0/1, interleaved x2, -r 2,
pp2048 -ub 2048: Flash-Next UD-IQ4_XS 551.4 -> 564.9 t/s, Qwen3.8-27B UD-Q8_K_L
326.6 -> 327.7 t/s (flat); tg128 26.24 -> 26.21 and 7.94 -> 7.92. Qwen3.8-27B
wikitext PPL (4 x 2048) 6.2256 -> 6.2192 (reduction order). test-backend-ops
RMS_NORM 60, RMS_NORM_MUL_MUL 10, RMS_NORM_SCALE 12, RMS_NORM_MUL_ADD 36,
L2_NORM 30, NORM 60, all passing with the gate on and off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… experts (ggml-org#28501)

* vulkan: raise the hoisted row-id limit for mul_mat_id to 512 experts

The expert-count shader (count_experts.comp) sizes its shared arrays
with BLOCK_SIZE, which is 256. Because of that, row-id hoisting is
switched off for any model with more than 256 experts, and every
mul_mat_id workgroup has to rescan the whole ids tensor on its own.
Qwen3.8-Flash-Next has 512 experts and was quietly running on that
slow path.

This change sizes the arrays with a separate MAX_EXPERTS constant (512),
clears them in a loop instead of one entry per thread, and raises the
matching limit on the host side.

On Strix Halo at batch 2048 the expert matmuls drop from 12.5 to 9.5 ms
(iq3_s) and from 14.0 to 7.5 ms (iq4_nl) per op, and prompt processing
gets about 19 % faster at 8k tokens. test-backend-ops MUL_MAT_ID passes
(891/891) with new 512-expert test cases.

Assisted-by: Claude Fable 5.1

* vulkan: raise the hoisted row-id limit for mul_mat_id to 1024 experts

Follow-up to review feedback: 1024 matches LLAMA_MAX_EXPERTS instead of
stopping at 512. The three shared arrays in count_experts.comp grow to
3 * 1024 * 4 = 12 KiB, which fits the 16 KiB that Vulkan guarantees for
maxComputeSharedMemorySize.

Adds mul_mat_id test cases at 1024 experts alongside the existing 512
ones. test-backend-ops MUL_MAT_ID passes on Vulkan (RADV, Strix Halo,
Radeon 8060S): 889/889.

(cherry picked from commit 5c53396)
fabric, gfx1151 RADV, Flash-Next UD-IQ4_XS, pp2048 -ub 2048 -fa 1, before/after
interleaved x2 -r 2: 575.0 -> 676.9 t/s, tg128 26.23 -> 26.26. test-backend-ops
MUL_MAT_ID 1199/1199, MUL_MAT_ID_FUSION 22/22, MUL_MAT 2090/2090 on Vulkan0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…29936)

* Fix Intel prefill regression on MoE models

* Revert the n_per_expert change back to nei1

(cherry picked from commit 3c9e747)
fabric, gfx1151 RADV, Flash-Next UD-IQ4_XS, pp2048 -ub 2048, before/after interleaved
x2 -r 2: 672.5 -> 669.6 t/s (noise), tg128 26.23 -> 26.19. Kept to match upstream.
test-backend-ops MUL_MAT_ID 1199/1199, MUL_MAT_ID_FUSION 22/22.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The m == 1 operand swap (ggml-org#28457) runs B^T*A as a mat-vec when dst has one
row. Extend it to dst->ne[0] <= mul_mat_vec_max_cols: the src0 rows become
the mat-vec columns (NUM_COLS = m), the mat-vec writes the result transposed
into the split_k scratch buffer, and a strided f32 copy of m*n floats puts it
in place. Without this, a [k, m] f32 weight with tiny m goes through mul_mm
and is padded to a full tile.

The motivating shape is the Flash-Next (qwen4exp) hyper-connection inject
projection, MUL_MAT f32 m=4 n=2048 k=10240, 95 calls per 2048-token ubatch.
Decode (n <= 8) and batched / broadcast cases keep their existing paths.

Tests: small-m cases (m = 2..5, 8, 9; n = 9..2048; the k = 10240 inject
shape at n = 1, 4, 8, 9, 512, 2048; f16 src0; a batched case that must not
take the route) added to test-backend-ops, plus two perf cases. Skipping the
transposing copy fails all 25 routed cases, so the tests see the route.

Tested on gfx1151 (RADV) only.

Follow-up (keep the small-M swapped mat-vec to F32 dst and f32/f16 src1):
The transposed write handles only an F32 dst, so require it in the dispatch
condition instead of asserting later. After the swap src1 is the mat-vec
matrix, and the CPU reference does not compute mul_mat with a BF16 or
quantized src1, so test-backend-ops cannot check those on this route; limit
it to f32/f16 src1. Anything else falls through to the regular paths. The
M = 1 route, which does not transpose, is unchanged.

fabric: the M = 1 route keeps fabric's wider src1 types (BF16 and quantized);
only the new transposed route (M > 1) is restricted.

fabric, gfx1151 RADV, Flash-Next UD-IQ4_XS: the inject projection MUL_MAT f32
m=4 n=2048 k=10240 goes from 1804.8 to 556.7 us per call (95 calls per 2048-token
ubatch, about 119 ms of GPU time saved); llama-bench pp2048 -ub 2048, before/after
interleaved x2 -r 2: 670.3 -> 682.1 t/s (within noise of the per-op saving).
Qwen3.8-27B UD-Q8_K_L and Q4_K_M unchanged. test-backend-ops MUL_MAT 2123/2123.

(cherry picked from commit 4fa5cc3)
(cherry picked from commit 109119a)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@aegioscy aegioscy left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Focused review of the two areas in code_review_and_refactoring_instructions.md: two refactoring suggestions and one adjacent CUDA allocation-boundary issue. Static source/index checks only; no GPU build or benchmark was run for this review.

Comment on lines +6065 to +6081
if (args.sigmoid) {
ops.insert(ops.end(), { GGML_OP_UNARY });
} else if (args.sqrt_softplus) {
ops.insert(ops.end(), { GGML_OP_UNARY, GGML_OP_SQRT });
} else {
ops.insert(ops.end(), { GGML_OP_SOFT_MAX });
}
const int i_probs = i + (int) ops.size() - 1; // last node of the gating activation

if (args.prob_bias) {
ops.insert(ops.end(), { GGML_OP_RESHAPE, GGML_OP_ADD, GGML_OP_ARGSORT, GGML_OP_VIEW,
GGML_OP_GET_ROWS });
out_nodes[0] = i_probs + 4;
} else {
ops.insert(ops.end(), { GGML_OP_RESHAPE, GGML_OP_ARGSORT, GGML_OP_VIEW, GGML_OP_GET_ROWS });
out_nodes[0] = i_probs + 3;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Flatten the activation and common top-k suffix. The bias branches repeat the reshape/argsort/view/gather sequence. Select the activation once, insert the optional ADD between the shared operations, and calculate the retained VIEW index in one place. Keep out_nodes: these outputs are needed by the fused path and cannot be elided. This preserves the existing operation order for sigmoid, sqrt-softplus, and softmax.

Suggested change
if (args.sigmoid) {
ops.insert(ops.end(), { GGML_OP_UNARY });
} else if (args.sqrt_softplus) {
ops.insert(ops.end(), { GGML_OP_UNARY, GGML_OP_SQRT });
} else {
ops.insert(ops.end(), { GGML_OP_SOFT_MAX });
}
const int i_probs = i + (int) ops.size() - 1; // last node of the gating activation
if (args.prob_bias) {
ops.insert(ops.end(), { GGML_OP_RESHAPE, GGML_OP_ADD, GGML_OP_ARGSORT, GGML_OP_VIEW,
GGML_OP_GET_ROWS });
out_nodes[0] = i_probs + 4;
} else {
ops.insert(ops.end(), { GGML_OP_RESHAPE, GGML_OP_ARGSORT, GGML_OP_VIEW, GGML_OP_GET_ROWS });
out_nodes[0] = i_probs + 3;
}
if (args.sigmoid) {
ops.push_back(GGML_OP_UNARY);
} else if (args.sqrt_softplus) {
ops.insert(ops.end(), { GGML_OP_UNARY, GGML_OP_SQRT });
} else {
ops.push_back(GGML_OP_SOFT_MAX);
}
const int i_probs = i + static_cast<int>(ops.size()) - 1;
ops.push_back(GGML_OP_RESHAPE);
if (args.prob_bias) {
ops.push_back(GGML_OP_ADD);
}
ops.insert(ops.end(), { GGML_OP_ARGSORT, GGML_OP_VIEW, GGML_OP_GET_ROWS });
out_nodes[0] = i_probs + (args.prob_bias ? 4 : 3);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in 6d95fca, as suggested: the reshape, the optional ADD and the shared top-k suffix are pushed once, out_nodes[0] = i_probs + (args.prob_bias ? 4 : 3), and out_nodes is kept. The op order is unchanged; TOPK_MOE 416/416 on ROCm.

Comment on lines +14 to +43
void main() {
const uint tx = gl_LocalInvocationID.x;
const uint ty = gl_LocalInvocationID.y;

const uint row = gl_WorkGroupID.x * 32 + tx;

// src0 is already contiguous, copy it straight through.
if (gl_WorkGroupID.y == 0 && row < p.ne01) {
for (uint i0 = ty; i0 < p.ne00; i0 += 8) {
data_d[get_doffset() + row*p.nb21 + i0*p.nb20] = D_TYPE(data_a[get_aoffset() + row*p.nb01 + i0*p.nb00]);
}
}

[[unroll]] for (uint j = 0; j < 32; j += 8) {
const uint c = gl_WorkGroupID.y * 32 + ty + j;
if (c < p.ne10 && row < p.ne11) {
tmp[ty + j][tx] = A_TYPE(data_b[get_boffset() + c*p.nb10 + row*p.nb11]);
}
}

barrier();

const uint col = gl_WorkGroupID.y * 32 + tx;
[[unroll]] for (uint j = 0; j < 32; j += 8) {
const uint r = gl_WorkGroupID.x * 32 + ty + j;
if (col < p.ne10 && r < p.ne11) {
data_d[get_doffset() + r*p.nb21 + (p.ne00 + col)*p.nb20] = D_TYPE(tmp[tx][ty + j]);
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hoist the invariant offsets and outer bounds checks. Cache the three buffer offsets once, compute row/column bases outside the loops, and move the fixed row/column guards around the unrolled loops. Keep the per-iteration c/r checks and the unconditional barrier for partial tiles. I kept p.ne11 for the src1 guard; the dispatch predicate currently guarantees ne01 == ne11, but this makes the source extent explicit. Any performance gain should be confirmed from generated code or a benchmark; this is a source-level refactor.

Suggested change
void main() {
const uint tx = gl_LocalInvocationID.x;
const uint ty = gl_LocalInvocationID.y;
const uint row = gl_WorkGroupID.x * 32 + tx;
// src0 is already contiguous, copy it straight through.
if (gl_WorkGroupID.y == 0 && row < p.ne01) {
for (uint i0 = ty; i0 < p.ne00; i0 += 8) {
data_d[get_doffset() + row*p.nb21 + i0*p.nb20] = D_TYPE(data_a[get_aoffset() + row*p.nb01 + i0*p.nb00]);
}
}
[[unroll]] for (uint j = 0; j < 32; j += 8) {
const uint c = gl_WorkGroupID.y * 32 + ty + j;
if (c < p.ne10 && row < p.ne11) {
tmp[ty + j][tx] = A_TYPE(data_b[get_boffset() + c*p.nb10 + row*p.nb11]);
}
}
barrier();
const uint col = gl_WorkGroupID.y * 32 + tx;
[[unroll]] for (uint j = 0; j < 32; j += 8) {
const uint r = gl_WorkGroupID.x * 32 + ty + j;
if (col < p.ne10 && r < p.ne11) {
data_d[get_doffset() + r*p.nb21 + (p.ne00 + col)*p.nb20] = D_TYPE(tmp[tx][ty + j]);
}
}
}
void main() {
const uint tx = gl_LocalInvocationID.x;
const uint ty = gl_LocalInvocationID.y;
const uint row = gl_WorkGroupID.x * 32 + tx;
const uint d_offset_base = get_doffset();
const uint a_offset_base = get_aoffset();
const uint b_offset_base = get_boffset();
if (gl_WorkGroupID.y == 0 && row < p.ne01) {
const uint d_row_ptr = d_offset_base + row * p.nb21;
const uint a_row_ptr = a_offset_base + row * p.nb01;
for (uint i0 = ty; i0 < p.ne00; i0 += 8) {
data_d[d_row_ptr + i0 * p.nb20] = D_TYPE(data_a[a_row_ptr + i0 * p.nb00]);
}
}
if (row < p.ne11) {
const uint b_row_ptr = b_offset_base + row * p.nb11;
[[unroll]] for (uint j = 0; j < 32; j += 8) {
const uint c = gl_WorkGroupID.y * 32 + ty + j;
if (c < p.ne10) {
tmp[ty + j][tx] = A_TYPE(data_b[b_row_ptr + c * p.nb10]);
}
}
}
barrier();
const uint col = gl_WorkGroupID.y * 32 + tx;
if (col < p.ne10) {
const uint d_col_ptr = d_offset_base + (p.ne00 + col) * p.nb20;
[[unroll]] for (uint j = 0; j < 32; j += 8) {
const uint r = gl_WorkGroupID.x * 32 + ty + j;
if (r < p.ne11) {
data_d[d_col_ptr + r * p.nb21] = D_TYPE(tmp[tx][ty + j]);
}
}
}
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in c3fe78c, as suggested: offsets read once, row/column bases and the fixed row/column guards moved outside the unrolled loops, the per-iteration c/r checks and the unconditional barrier kept. CONCAT 226/226, CONCAT_SSM_CONV_SILU 12/12 and SSM_CONV 53/53 on Vulkan0. Perf is neutral: with the direct GDN conv off (GGML_VK_SSMCONV_DIRECT=0), so the transpose kernel actually runs, Qwen3.8 27B pp2048 measures 1.47-1.59 ms per CONCAT before and 1.49-1.52 ms after (48 calls, 2 runs each). In the default path the concat is fused into CONCAT_SSM_CONV_SILU and only its tail runs (8.5 us either way).

Comment thread ggml/src/ggml-cuda/ggml-cuda.cu Outdated
if (ggml_can_fuse_subgraph(cgraph, i, ops.size(), ops.data(), out_nodes, 2) &&
ggml_cuda_should_use_topk_moe(node, logits, weights, ids)) {

add_alloc_deps(i, i + ops.size());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P1] Anchor allocation dependencies to the final fused node. add_alloc_deps uses its second argument as an actual cgraph->nodes[last_node] index. A match of ops.size() nodes ends at i + ops.size() - 1 (already stored in out_nodes[1]), so the current call points one node past the match. If the fusion ends at the graph boundary it reads outside the valid node range, and dependencies can be keyed to a node the scheduler never visits, losing the intended lifetime protection. Use the retained weights node and cover a top-k fusion at the end of the graph.

Suggested change
add_alloc_deps(i, i + ops.size());
add_alloc_deps(i, out_nodes[1]);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 28dc651: add_alloc_deps(i, out_nodes[1]), so the deps are keyed on the weights node that ends the match. ROCm (gfx1151): TOPK_MOE 416/416 and ARGSORT 46/46; Flash-Next pp2048 525.7 t/s, unchanged. Upstream master has the same i + ops.size() call, so this should go upstream as well.

amangupta-tether and others added 3 commits October 16, 2026 12:23
add_alloc_deps(start, last_node) keys every dependency on
cgraph->nodes[last_node], so it takes the index of the last node of the
fusion. The top-k MoE call passed i + ops.size(), one past the match:
the deps were kept alive until the node after the weights, and a fusion
ending at the end of a split graph indexed past n_nodes, so the deps
were keyed to a node outside the split and lost. Use out_nodes[1], the
weights node that ends the match.

Upstream master has the same call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bias and no-bias branches repeated the reshape/argsort/view/get_rows
sequence. Push the reshape, the optional ADD and the shared suffix once
and derive the ids VIEW index from prob_bias. The op order is unchanged
for sigmoid, sqrt-softplus and softmax gating.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…spose

Read the three buffer offsets once, compute the row and column bases
outside the loops, and test the fixed row (src1 load) and column (dst
store) bounds around the unrolled loops instead of in every iteration.
The per-iteration c / r checks stay, and the barrier is still reached by
every invocation for partial tiles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
static bool ggml_vk_ssm_conv_direct_enabled() {
static const bool enabled = [] {
const char * e = getenv("GGML_VK_SSMCONV_DIRECT");
return e == nullptr || atoi(e) != 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P1] Keep the direct SSM conv path opt-in until recurrent graph allocation is stable. On this PR head, GGML_SCHED_DEBUG_REALLOC=1 ctest -R ^test-recurrent-state-rollback$ aborts with unexpected graph reallocation (graph size = 170, nodes = 170, leafs = 58) in the Vulkan jobs. Running the same test with GGML_VK_SSMCONV_DIRECT=0 passes, which isolates the regression to this default-enabled path. This currently fails both Vulkan NVIDIA jobs and the Apple Vulkan job. Please correct the allocation dependencies so recurrent replays retain an identical graph allocation, or leave the optimization disabled by default until that is fixed.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 587d948 without turning the path off. Vulkan's graph_optimize registered the x/state alloc deps only for concats that passed the full direct-conv check, which rejects ubatches under 8 tokens. So the graph gained dependency nodes only for long ubatches, and its node count changed with the ubatch size. After the scheduler reserved for the largest ubatch, a short ubatch needed a fresh allocation, and the next short ubatch with more tokens but the same node count tripped the debug check. The minimum token count now applies only to the runtime match, so the deps are registered for every ubatch size and the graph no longer changes shape. Short ubatches still run the plain concat + conv, with x and the state kept alive through the SiLU. Reproduced on RADV (gfx1151) first: GGML_SCHED_DEBUG_REALLOC=1 test-recurrent-state-rollback with the qwen35 test model aborted before the fix and passes after, as do the deepseek4, nemotron-h and kimi-k3 variants. CONCAT 226/226, CONCAT_SSM_CONV_SILU 12/12, SSM_CONV 53/53. Vulkan pp2048/tg128 is unchanged (27B Q8 328/7.93 both ways; Flash-Next tg 26.19 vs 26.18).

test_cases.emplace_back(new test_rms_norm(GGML_TYPE_F32, {254, 6, 10, 2}, true, 1e-5f)); // view: {127, 3, 5, 1}
test_cases.emplace_back(new test_rms_norm(GGML_TYPE_F32, {256, 1, 1, 1}, false, 1e-6f));
test_cases.emplace_back(new test_rms_norm(GGML_TYPE_F32, {128, 4097, 1, 1}, false, 1e-6f));
test_cases.emplace_back(new test_rms_norm(GGML_TYPE_F32, {16, 270001, 1, 1}, false, 1e-6f));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P1] Do not add this shape unconditionally while WebGPU cannot dispatch it. This case generates 270001 workgroups in X, but WebGPU limits each dispatch dimension to 65535. Both WebGPU CI jobs abort at DispatchWorkgroups(270001, 1, 1) rather than reporting an unsupported case. Please gate this case out for WebGPU, or implement a split/strided dispatch before enabling it in the generic backend test suite.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in e1cbcda: test_rms_norm now skips WebGPU when the case has more than 65535 rows (WebGPU dispatches one workgroup per row in X). The {16, 270001} case still runs on every other backend, since it is there to cover the Vulkan grid past 65535 workgroups. RMS_NORM 60/60 on Vulkan0.

Comment thread ggml/src/ggml-cuda/mmq.cu
const char * env = getenv("GGML_CUDA_RDNA3_5_Q8_0_MMQ_MAX");
return env ? (int64_t) atoll(env) : 512;
}();
return ne11 <= q8_0_mmq_max;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P1] Revisit this gfx1151 cutoff because it regresses the target machine with the exact PR model. On qvac-dev-strix-1, Qwen3.8-27B-UD-Q8_K_L with -ngl 999 -fa 1 -b 2048 -ub 2048 -p 2048 -n 512 -r 2 gives pp2048 288.33 +/- 0.80 t/s on the new default hipBLAS path versus 316.71 +/- 4.04 t/s when the prior MMQ path is forced with GGML_CUDA_RDNA3_5_Q8_0_MMQ_MAX=100000. That is about a 9% prompt-processing regression, while generation is effectively unchanged (7.96 vs 8.00 t/s). This is opposite to the improvement stated in the added comment, so the global 512-row cutoff should be revised or gated more narrowly.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks for measuring this. The difference is the ROCm version, not the hardware: on our gfx1151 box (TheRock ROCm 10.0, HIP 7.15) the same model and settings still give pp2048 354 t/s with hipBLAS against 308 t/s with MMQ forced (2 interleaved runs each). Your 288 vs 317 is ROCm 7.2.2 (HIP 7.2). f2df7a3 enables the 512-row default only for HIP_VERSION >= 7.15, the version where it was measured, and keeps MMQ for all row counts before that, so your setup gets the 317 t/s path by default. GGML_CUDA_RDNA3_5_Q8_0_MMQ_MAX still overrides it either way. I don't have a HIP version between 7.2 and 7.15 to find the actual crossover, so the cutoff is conservative.

@gianni-cor

Copy link
Copy Markdown

Latest benchmark results from qvac-dev-strix-1 using the same model requested for the PR comparison:

  • Model: Qwen3.8-27B-UD-Q8_K_L
  • SHA-256: 2a13bba36d2efa213f9275abc430c40e4d914b145bfde976f30f1bb5b7e23ac2
  • Size: 28,045,695,904 bytes
  • GPU target: gfx1151
  • ROCm: 7.2.2
  • HIP command parameters: -ngl 999 -fa 1 -b 2048 -ub 2048 -p 2048 -n 512 -r 2
Path pp2048 tg512
New default hipBLAS path 288.33 +/- 0.80 t/s 7.96 +/- 0.01 t/s
Prior MMQ path, forced with GGML_CUDA_RDNA3_5_Q8_0_MMQ_MAX=100000 316.71 +/- 4.04 t/s 8.00 +/- 0.00 t/s

The new default is about 9.0% slower for prompt processing on this machine; generation speed is effectively unchanged. This does not reproduce the pp2048 310 -> 354 t/s improvement stated in the source comment.

For the same Q8_K_L model through Vulkan, the latest result was pp2048 364.32 +/- 0.99 t/s, with tg128 8.06 t/s.

amangupta-tether and others added 3 commits October 16, 2026 18:50
graph_optimize asked the allocator to keep x and the state alive only for
concats that passed the full direct-conv check, which rejects ubatches
under 8 tokens. The graph therefore gained dependency nodes for long
ubatches only, and its node count changed with the ubatch size: after
the scheduler reserved for the largest ubatch, a short ubatch needed a
fresh allocation, and the next short ubatch with more tokens but the same
node count needed another one. test-recurrent-state-rollback aborts on
that with GGML_SCHED_DEBUG_REALLOC=1 (unexpected graph reallocation).

Apply the minimum token count only in the runtime match. graph_optimize
now registers the dependencies for every token count, so the graph is the
same for every ubatch size; short ubatches still run the plain concat and
conv, with x and the state kept alive through the SiLU.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The short-row RMS norm cases include a {16, 270001} shape to cover a
Vulkan grid past 65535 workgroups. WebGPU dispatches one workgroup per
row in X and caps a dispatch dimension at 65535, so both WebGPU CI jobs
aborted at DispatchWorkgroups(270001, 1, 1). Skip RMS norm cases with
more than 65535 rows on WebGPU; every other backend still runs them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dequantization + hipBLAS rule for Q8_0 above 512 rows was measured
with the HIP 7.15 (ROCm 10.0) BLAS libraries, where Qwen3.8-27B
UD-Q8_K_L pp2048 goes from 308 to 354 t/s on gfx1151. A review run on
another gfx1151 box with ROCm 7.2.2 (HIP 7.2) measured the opposite,
317 t/s on MMQ against 288 t/s on hipBLAS. Enable the 512-row default
only for HIP >= 7.15 and keep MMQ for every row count before that.
GGML_CUDA_RDNA3_5_Q8_0_MMQ_MAX still overrides the limit either way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@amangupta-tether

Copy link
Copy Markdown
Author

Thanks for the runs. All three findings are addressed in 587d948, e1cbcda and f2df7a3; details are on each thread.

On the Q8_0 numbers: with ROCm 7.2.2 the new default is now MMQ again (the 317 t/s path). The hipBLAS rule only applies from HIP 7.15 (ROCm 10.0) on, where it measures 308 -> 354 t/s on our gfx1151.

The one gpu-metal failure on this run (MUL_MAT_VEC_FUSION with ptq1_0, Metal only) is not touched by this PR. The same job passed twice on #396, which contains all of these commits, so it looks flaky. #396 is rebased onto the new head.

@gianni-cor
gianni-cor merged commit bc47470 into temp-11018 Oct 16, 2026
48 of 52 checks passed
gagallo7 pushed a commit that referenced this pull request Oct 19, 2026
…tch-up + strix-llama.cpp ports) (#393)

(cherry picked from commit bc47470)
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.