Skip to content

Ptq1/vulkan port - #32

Open
LaurentZuijdwijk wants to merge 5 commits into
masterfrom
ptq1/vulkan-port
Open

LaurentZuijdwijk wants to merge 5 commits into
masterfrom
ptq1/vulkan-port

Conversation

@LaurentZuijdwijk

Copy link
Copy Markdown
Member

Summary

Optimize Vulkan PTQ1_0 decoding and matrix-vector multiplication by reusing packed quantization bytes instead of decoding them repeatedly.

• Add a PTQ1_0-specific matvec path using eight lanes per quantization block: six for packed qs words, one for the qh tail, and one inactive.
• Reuse activation loads across output rows and apply the block scale after accumulating each lane’s contribution.
• Preserve the existing reduction, batch/expert offsets, and fused-output handling.
• Extend PTQ test coverage and classify PTQ1_0 under the existing ternary quantization error limits.
• Add strict CPU dot-product checks against dequantized values to test arithmetic independently of quantization loss.

Benchmarks

Measured against the preceding local PTQ1_0 implementation on the same machine using Vulkan0.

End-to-end inference

Model: Ternary-Bonsai-2-27B-PTQ1_0.gguf

▎ sh
▎ llama-bench
▎ -m Ternary-Bonsai-2-27B-PTQ1_0.gguf
▎ -dev Vulkan0 -ngl 99
▎ -p 512 -n 128 -r 3

Workload Before After Change
Generation, 128 tokens 11.31 tokens/s 16.22 ± 0.01 tokens/s +43.4%
Prompt processing, 512 tokens 292.80 tokens/s 281.42 ± 0.87 tokens/s −3.9%

The generation improvement comes with an observed prompt-processing regression; its cause has not yet been isolated.

Matrix-vector kernel latency

PTQ1_0 weights, F32 input, one input vector. Dimensions are output rows × input columns.

Matrix dimensions Before After Speedup
17,408 × 5,120 248.11 µs 163.88 µs 1.51×
5,120 × 17,408 246.98 µs 159.58 µs 1.55×
6,144 × 5,120 92.94 µs 61.35 µs 1.51×
248,320 × 5,120 3,570.07 µs 2,297.16 µs 1.55×

These results compare the byte-reuse optimization with the preceding PTQ implementation, not with upstream master or another quantization format.

Validation

• Full configured Release/Vulkan build passed, including server, tools, and tests.
• All 45 focused PTQ backend correctness cases passed.
• All four model-shape correctness cases passed.
• All 16 selected CTest regression tests passed.
• Six strict PTQ dot-product checks passed at lengths 128, 256, 384, 4096, 5120, and 17408.
• git diff --check passed.

AI assistance

Implementation and validation were assisted by Marshall.

LaurentZuijdwijk and others added 4 commits September 18, 2026 09:36
…rnels

Ports GGML_TYPE_PTQ1_0 (143) from PrismML-Eng/llama.cpp, the packing used by
prism-ml/Ternary-Bonsai-2-27B-gguf. The format is upstream TQ1_0's base-3 trit
packing at group 128 instead of 256: block_ptq1_0 is 28 B per 128 weights
(24 B qs at 5 trits/byte + 2 B qh at 4 trits/byte + fp16 scale) = 1.75 bpw.

Covered: ggml type + ftype plumbing, the CPU reference codec and vec_dot, and
the full Vulkan path (dequant, get_rows, mul_mat_vec f32/f16, matmul and
matmul_id including subgroup and coopmat1 variants) -- 50 SPIR-V shaders.
coopmat2 is skipped deliberately: dequant_funcs_cm2.glsl has no PTQ1_0 decoder
and emitting cm2 for it fails the whole Vulkan build, so it falls back to the
scalar and coopmat1 paths.

PTQ1_0 has no arch-specific vec_dot on any target, so arch-fallback.h routes
every architecture to the generic one -- including x86, which is not covered by
the tq1_0 fallback list.

PQ2_0 (142) is not ported: it has no Vulkan kernels in the source fork either.

Known incomplete: the prism.hadamard.* weight-rotation runtime is NOT ported
yet. Bonsai weights are stored in a rotated basis, so a Bonsai GGUF currently
loads and reports its ftype correctly but generates garbage. The activation
transform (llama-model.cpp metadata + validation, the build_lora_mm hook in
llama-graph.cpp, and the token_embd inverse) is the remaining piece; the FWHT
primitive and llama_mul_mat_hadamard already exist here from the KV work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3NeoeVLYx9mNqMSEJHRvS
Completes the Bonsai path. Bonsai weights are stored in a blockwise-Hadamard
rotated basis (W x = (WQ)(Q^T x), Q folded into the weights offline), so the
runtime must apply the matching transform to activations or the model loads
cleanly and emits fluent garbage -- which is exactly what the previous commit
did on its own.

Ported from PrismML-Eng/llama.cpp:
  - llama-model.cpp: prism.hadamard.* metadata parsing and validation, plus
    construction of the per-(block_size, buffer type) rotation matrices and the
    per-width explicit sign vectors, after buffers are allocated.
  - llama-graph.{h,cpp}: llama_hadamard_transform, the transform hook in
    build_lora_mm and build_lora_mm_id (memoized per (activation, rotation) so
    weights sharing an input build it once), and the inverse applied to the
    token-embedding lookup, whose table stores rotated rows.
  - llama-context.{h,cpp}: llama_verify_hadamard_graph, run once on the first
    pristine graph. It throws if any folded weight reaches a matmul without its
    transform, so an unsupported graph fails loudly instead of computing wrong
    results.
  - llama-model-loader.cpp: the three string-key get_arr/get_key instantiations
    the metadata reads need.

Deviations from the source fork: LLM_ARCH_DSPARK is Prism-private and its three
special cases are dropped; the verified-architecture list keeps QWEN35 and the
rest, so a Bonsai GGUF (arch qwen35) is accepted.

No new kernels were needed -- llama_mul_mat_hadamard and fwht.comp already live
here from the KV-rotation work, and the Vulkan backend picks the FWHT kernel off
the GGML_HINT_SRC0_IS_HADAMARD hint.

Verified on Ternary-Bonsai-2-27B-PTQ1_0 (Radeon 8060S, RADV STRIX_HALO): loads,
reports 401 folded weights, and answers correctly on both CPU and Vulkan.
llama-bench matches the source fork within noise -- pp512 210.13 vs 206.38,
tg128 3.96 vs 4.00 -- so the port is faithful.

Known performance gap, not introduced here: tg128 is ~22 GiB/s effective against
~193 GiB/s for IQ3_XXS on the same box and build. PTQ1_0 decode is bound by the
serial dependent-multiply trit unpack in ptq1_0_trit (up to 4 iterations per
element, trip count varying by element position, so it diverges within a
subgroup), not by bandwidth. A LUT or position-unrolled decoder is the fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3NeoeVLYx9mNqMSEJHRvS
ptq1_0_trit walked to trit n with a loop of up to four dependent
v = (v*3) & 0xFF steps, and the trip count is a function of element position,
so lanes in a subgroup ran different iteration counts.

Mod-256 multiplication is associative, so that walk is just b * 3^n mod 256.
This is already how dequantize_row_ptq1_0 in ggml-quants.c decodes -- only the
GLSL kept the serial form. 3^n is computed with selects rather than a const
array, because a dynamically indexed local array can land in scratch on some
drivers and this is the hot path of every ternary matmul.

Equivalence was checked exhaustively over the whole input domain: all 256 byte
values x 5 trit positions agree with the loop form, and all 3^5 = 243 encoder
codes round-trip. Applied to both copies -- ptq1_0.glsl (mul_mat_vec, get_rows,
mul_mm) and the standalone decoder in dequant_ptq1_0.comp.

NOT YET VALIDATED ON GPU. The shaders compile and the arithmetic is proven
equivalent offline, but nothing has been run on the Radeon 8060S since the
change, so there is no measured speedup and no on-device correctness check yet.
Baseline to beat, from the same box and build: pp512 210.13, tg128 3.96, which
is ~22 GiB/s effective against ~193 GiB/s for IQ3_XXS on the same hardware.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3NeoeVLYx9mNqMSEJHRvS
Upstream replaced the per-type CREATE_MM/CREATE_MM2 pipeline macros with
keyed pipeline creation and generic non-LUT quantization loops. Keep that
architecture and re-register the dedicated PTQ1_0 shaders through the new
per-type macros (CM1, CM1_ID, SG, SG_ID, SG_ID_SUB, SG_FP32, SG_ID_FP32,
SG_ID_SUB_FP32), preserving PTQ1_0 support in both MUL_MAT and MUL_MAT_ID.

The PTQ1_0 packed 8-wide loader is kept in the refactored load_a_to_shmem,
and shader generation still emits the 8-wide LOAD_VEC_A per-type variants.
Coopmat2 remains unsupported for PTQ1_0 (dequant_funcs_cm2.glsl has no
PTQ1_0 entry), so those shaders are skipped instead of failing the build.

Assisted-by: Marshall
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.

1 participant