Title: Vulkan: PTQ1_0 decode is 6-119x slower than necessary (serial trit recurrence) + no MUL+FWHT fusion
Hardware context: Radeon 890M iGPU (Ryzen AI 9 HX 470), Windows, Vulkan backend, Ternary-Bonsai-2-27B PTQ1_0 (5.95 GB), full offload, single-sequence decode.
Findings while profiling why Bonsai 2 PTQ1_0 decode runs at only ~2 t/s on this class of iGPU (Q2_0 band runs ~8 t/s on the same graph):
-
ptq1_0_trit() in vulkan-shaders/ptq1_0.glsl (and the duplicate in dequant_ptq1_0.comp) decodes each weight with a serial loop of up to 119 multiply-mod iterations (v = v*3 & 0xFF). This runs per weight per matvec. Since 3^64 == 1 (mod 256), the whole recurrence collapses to a 64-entry lookup table + one multiply — bit-identical, dramatically cheaper.
-
dequantize4() for PTQ1_0 calls ptq1_0_trit four times, re-evaluating the 3-way region branch per element. The codec layout guarantees any 4-aligned group shares one exponent and reads consecutive bytes, so one branch + a vector multiply decodes the group.
-
Metal has ggml_metal_op_can_fuse_fwht_signed (fusing the Hadamard sign MUL into the FWHT kernel, metal-ops.cpp ~2454). Vulkan has the same MUL -> RESHAPE -> FWHT-hinted MUL_MAT pattern in Bonsai 2's rotated-basis graphs but no fusion, so every Hadamard site pays an extra elementwise launch + full activation round-trip.
Proposed fix (implemented, tested locally): lookup-table trit decode + vectorized dequantize4 + MUL_FWHT fusion with dedicated fwht_signed_* pipelines, following the Metal implementation including the width-from-matmul validation (the activation may be a flat concat of n_blk transform-width rows, e.g. 5120 = 40 x 128).
Measured: PTQ1_0 decode 1.85 -> 3.26 t/s (+76%) on 890M; correctness 7/7 generation checks, bit-identical decode for the table/vectorization changes.
Happy to open the PR — wanted to file the issue first per CONTRIBUTING.md.
Title: Vulkan: PTQ1_0 decode is 6-119x slower than necessary (serial trit recurrence) + no MUL+FWHT fusion
Hardware context: Radeon 890M iGPU (Ryzen AI 9 HX 470), Windows, Vulkan backend, Ternary-Bonsai-2-27B PTQ1_0 (5.95 GB), full offload, single-sequence decode.
Findings while profiling why Bonsai 2 PTQ1_0 decode runs at only ~2 t/s on this class of iGPU (Q2_0 band runs ~8 t/s on the same graph):
ptq1_0_trit() in vulkan-shaders/ptq1_0.glsl (and the duplicate in dequant_ptq1_0.comp) decodes each weight with a serial loop of up to 119 multiply-mod iterations (v = v*3 & 0xFF). This runs per weight per matvec. Since 3^64 == 1 (mod 256), the whole recurrence collapses to a 64-entry lookup table + one multiply — bit-identical, dramatically cheaper.
dequantize4() for PTQ1_0 calls ptq1_0_trit four times, re-evaluating the 3-way region branch per element. The codec layout guarantees any 4-aligned group shares one exponent and reads consecutive bytes, so one branch + a vector multiply decodes the group.
Metal has ggml_metal_op_can_fuse_fwht_signed (fusing the Hadamard sign MUL into the FWHT kernel, metal-ops.cpp ~2454). Vulkan has the same MUL -> RESHAPE -> FWHT-hinted MUL_MAT pattern in Bonsai 2's rotated-basis graphs but no fusion, so every Hadamard site pays an extra elementwise launch + full activation round-trip.
Proposed fix (implemented, tested locally): lookup-table trit decode + vectorized dequantize4 + MUL_FWHT fusion with dedicated fwht_signed_* pipelines, following the Metal implementation including the width-from-matmul validation (the activation may be a flat concat of n_blk transform-width rows, e.g. 5120 = 40 x 128).
Measured: PTQ1_0 decode 1.85 -> 3.26 t/s (+76%) on 890M; correctness 7/7 generation checks, bit-identical decode for the table/vectorization changes.
Happy to open the PR — wanted to file the issue first per CONTRIBUTING.md.