Repository navigation
vulkan : run small-M mul_mat as a swapped mat-vec - #129
Conversation
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. Assisted-by: Claude Opus 5.5
|
Independent verification on a second Strix Halo (strix-halo-002, gfx1151, RADV) Built Performance (isolated op,
|
shape (type_a=f32,type_b=f32) |
old path | this PR | speedup |
|---|---|---|---|
| m=4, n=2048, k=10240 | 2006.9 µs (83.6 GFLOPS) | 413.9 µs (405 GFLOPS) | 4.85x |
| m=4, n=512, k=10240 | 468.6 µs (89.5 GFLOPS) | 25.1 µs (1.67 TFLOPS) | 18.6x |
Both runs report OK; the baseline numbers are the same harness on the same machine, so the route is genuinely taken. (I did not reproduce the end-to-end prefill %; the per-op gain is large and in the direction you describe.)
Correctness
Targeted runs of the new shapes on Vulkan0, all pass:
m=4,n=2048,k=10240→ 1/1m in {2,3,4,5,8} x n in {9,16,509,2048}, k=2048→ 20/20m=4, n in {1,4,8,9,512,2048}, k=10240→ 6/6 (n <= 8 stays on the plain mat-vec, n = 9 enters the new path)m=4,n=509,k=2051(F16/F32, F16/F16, batchedbs=[2,1]) → 3/3m=9, n in {9,16,509,2048}, k=2048→ 4/4 (boundary, falls through)
Full suite: test-backend-ops test -b Vulkan0 -o MUL_MAT → 7443/7446. The three failures are all [CONCAT] ERR = ... on m=128 shapes (type_a=bf16,n=9/16,k=5120 and type_a=q8_0,n=4,k=5120). m=128 > mul_mat_vec_max_cols (8), so they cannot come from this change, and I reproduced them identically on the unmodified baseline. So this PR introduces no regression across the MUL_MAT suite.
Code review
The transpose is wired up correctly. The mat-vec writes data_d[j*batch_stride_d + row] with batch_stride_d = ne20 = n (the vector index is slow), so the scratch is [m, n] row-major; dst_t (nb[0]=n*4, nb[1]=4) describes exactly that source, and ggml_vk_cpy_to_contiguous writes pc.nb10=1, nb11=m — a contiguous [m, n] with nb0=4, i.e. the real destination. The scratch is sized to ggml_nbytes(dst) = m*n*4, which the kernel fills exactly. The prealloc_split_k growth, the pre-write need_sync check and the trailing prealloc_split_k_need_sync = true match the existing scratch pattern. The m=1 path is unchanged (transpose_d is false), the non-swap column-chunking path never sets it, and decode/speculative (n <= 8) stays on the old branches.
Nits, none blocking:
- The dispatch guard does not test
dst->type, but the new codeGGML_ASSERT(dst->type == GGML_TYPE_F32 ...). That is safe today sinceggml_mul_matalways allocates an F32 result, but it would abort (not fall through) if that ever changed. Addingdst->type == GGML_TYPE_F32to the branch condition would make it a clean fallback. - The branch allows
src1to be BF16, which makes the BF16 tensor the mat-vec matrix after the swap. That allowance already existed for the m=1 route, but there is notype_b=BF16case anywhere in the suite, so this specific combination has no dedicated test. Same for a quantizedsrc1. Worth a case or two if you want the new small-m route exercised for those types. - The copy passes
dst = nullptrtoggml_vk_get_cpy_pipeline, so it takes the generic strided shader rather than the fast transpose pipeline. Irrelevant at a few KB, just noting it.
CI
All three red jobs are pre-existing and unrelated: ubuntu-llvmpipe fails the same test-recurrent-state-rollback (x2) and test-save-load-state as master b79812d59 (confirmed identical), windows is the test-qsa-fast.cpp setenv error fixed by #138, and metal hit the 24h timeout.
LGTM. Correct on gfx1151, no MUL_MAT regressions, and a 4.8–18x improvement on the targeted small-M shapes.
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. Assisted-by: Claude Opus 5.5
|
Thanks for the careful check, especially building the baseline harness for the perf cases. Pushed
I left the copy on the generic strided shader: it moves a few KB. Re-checked on gfx1151, stock Mesa 25.2.8: |
|
Code review of I found no correctness problems. LGTM, with a few optional follow-ups below. Verified
Optional follow-ups
|
Flash-Next's hyper-connection inject projection is a matmul with only 4 output rows (
f32 m=4 n=2048 k=10240, 95 times per prompt batch). On Vulkan it went through the regular matmul kernel, padded out to a full tile, and cost about 145 ms per 2048-token batch.This extends the existing m=1 trick (ggml-org#28457 upstream) to small m: run it as a mat-vec with the operands swapped, then copy the small transposed result into place. Only applies to plain 2-D cases with at most 8 output rows and more than 8 columns, so decode and everything else keep their current paths.
On a Strix Halo (gfx1151, RADV) that op drops to 54 ms, and Flash-Next prefill goes up about 4% at depth 0 and 3.4% at 12k (measured with #115 and #119 applied). Decode is unchanged, and 27B and 35B never hit this path. Divergence from a CPU reference is the same as before. New test cases cover the exact shape and a few neighbours; skipping the copy makes them fail, so they do exercise the route.
Written with Claude, tested by me.
Assisted-by: Claude Opus 5.5