Repository navigation
vulkan, hip : Strix Halo prefill for qwen4exp and qwen35 (upstream catch-up + strix-llama.cpp ports) - #393
Conversation
(cherry picked from commit 1a67982)
* 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
left a comment
There was a problem hiding this comment.
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.
| 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; | ||
| } |
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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.
| 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]); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| 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]); | |
| } | |
| } | |
| } | |
| } |
There was a problem hiding this comment.
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).
| 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()); |
There was a problem hiding this comment.
[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.
| add_alloc_deps(i, i + ops.size()); | |
| add_alloc_deps(i, out_nodes[1]); |
There was a problem hiding this comment.
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.
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; |
There was a problem hiding this comment.
[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.
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
[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.
There was a problem hiding this comment.
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.
| 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; |
There was a problem hiding this comment.
[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.
There was a problem hiding this comment.
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.
|
Latest benchmark results from
The new default is about 9.0% slower for prompt processing on this machine; generation speed is effectively unchanged. This does not reproduce the For the same Q8_K_L model through Vulkan, the latest result was |
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>
|
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 |
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:
temp-11018(last merged Sep 17) was missing. Flash-Next prefill doubles on both ROCm and Vulkan with the first five commits, most likely fromllama_prefetch_rows(llama: llama_prefetch_rows ggml-org/llama.cpp#29599): upstream built at the merge basec9a5eeeb3runs at fabric's speed, and the gain appears between that commit and Oct 1, the window that contains llama: llama_prefetch_rows ggml-org/llama.cpp#29599. The commit was not measured on its own. Also the int8 coopmat1 matmul for RDNA3/4 (vulkan: int8 coopmat1 matmul implementation for AMD RDNA3 and RDNA4 ggml-org/llama.cpp#27952), the 512-expert MUL_MAT_ID row-id hoist (vulkan: raise the hoisted row-id limit for mul_mat_id from 256 to 512 experts ggml-org/llama.cpp#28501), the IQ3_S/IQ4_XS Vulkan MMQ kernels (vulkan: add IQ3_S MMQ matmul kernels ggml-org/llama.cpp#28822, vulkan: add IQ4_XS MMQ/MMV matmul kernels ggml-org/llama.cpp#28415), the qwen4exp hc_post gate fusion (vulkan: fuse qwen4exp's SCALE -> SIGMOID -> SCALE -> hc_post chain ggml-org/llama.cpp#29520) and top-k MoE fusion (cuda: top-k MoE should always fire ggml-org/llama.cpp#28432).8ee3e0848,e092d0386) are by Nathan Wilson (strixf4d6f9e8d,e24924856,2dfe8e521), ported by hand.e10627480, narrowed inf2df7a3c9). With ROCm 7.2.2 hipBLAS is slower for these matmuls, so older HIP keeps MMQ.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):
temp-11018a0e6dc738deb5e2fROCm at empty context (t/s), same builds,
-p 2048 -n 512, 2 runs:temp-11018a0e6dc738deb5e2fOn 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:
6a349d6efe10627480f2df7a3c9)8ee3e0848e092d0386f964031f4105e1ad7f1be77e0896f69f4bd1854e8164d672f167ed2f3fcbe9dFabric-specific adaptations
AMD_RDNA4goes into the enum inggml-vulkan.cpp, and fabric's small-tile warp width (30b7e5162) is kept.ggml-vulkan-types.hand the others). Their declarations go intoggml-vulkan.cpphere.test_rms_norm_scale_short, because fabric already has atest_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 withGGML_SCHED_DEBUG_REALLOC=1after587d94865. 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
🤖 Generated with Claude Code