Skip to content

metal: add TQ3_4S GPU support - #59

Merged
turbo-tan merged 9 commits into
mainfrom
pr39-metal-tq3_4s-clean-20260704
Jul 5, 2026
Merged

metal: add TQ3_4S GPU support#59
turbo-tan merged 9 commits into
mainfrom
pr39-metal-tq3_4s-clean-20260704

Conversation

@turbo-tan

Copy link
Copy Markdown
Owner

Clean replacement for PR #39, rebased onto current main and reduced to the Metal TQ3_4S implementation only.

Scope:

  • Adds Metal TQ3_4S get_rows / mat-vec support.
  • Adds RHT activation pre-pass for coalesced mat-vec.
  • Adds Metal TQ3_4S GEMM path for batched eval.
  • Adds docs/backend/Metal-TQ3.md.

Excluded from the old PR:

  • Historical sync commits.
  • CUDA/server/chat changes.
  • Artifacts, scratch files, and unrelated scripts.

Validation run locally on Linux:

  • cmake -S . -B build-pr39-clean-cpu -DCMAKE_BUILD_TYPE=Release -DGGML_CUDA=OFF -DGGML_METAL=OFF
  • cmake --build build-pr39-clean-cpu --target llama-cli test-backend-ops -j 8

Metal runtime validation still needs Apple Silicon hardware.

mromanuk and others added 5 commits July 4, 2026 14:28
TQ3_4S (ggml type 46) was CUDA-only; on Metal it aborted in the mul_mv
pipeline selector ("Asserting on type 46") and only ran on CPU at ~0.15
tok/s. This adds Metal support so TQ3_4S models run on the GPU.

TQ3_4S dequant is a per-32-block inverse randomized Hadamard transform
(8-level codebook lookup + E3M5 per-group scale, then a Walsh-Hadamard
butterfly). The block size QK_TQ3_0 == 32 maps to one 32-lane SIMD-group,
so the butterfly is done with simd_shuffle_xor, mirroring the CUDA kernel
in ggml-cuda/convert.cu.

- ggml-metal.metal: TQ3_0 centroid/sign constants, E3M5 scale decode and
  3-bit unpack helpers, kernel_get_rows_tq3_4s (one SIMD-group per row),
  and kernel_mul_mv_tq3_4s_f32. The mat-vec kernel exploits RHT
  orthogonality to apply the butterfly once per block to the activation
  (shared across rows) so weights only do a local codebook lookup.
- ggml-metal-impl.h: N_R0_TQ3_4S / N_SG_TQ3_4S tuning constants.
- ggml-metal-device.cpp: TQ3_4S case in the mul_mv pipeline selector.
- ggml-metal-ops.cpp: route TQ3_4S MUL_MAT through mat-vec (no mat-mat
  GEMM yet) and dispatch get_rows as one 32-lane SIMD-group per row.

No mul_mm GEMM or MoE _id kernels yet (the qwen35 test model is dense);
those are the next optimizations for prefill / MoE.

Validated on M3 Pro with osmQwopus-3.6-27B-V2 TQ3_4S: Metal greedy output
is byte-identical to CPU; decode ~4 tok/s, prefill ~4.6 tok/s (~27x over
the CPU baseline). Other quant types are unaffected (only TQ3_4S-specific
cases/kernels were added).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace per-byte loads of the 16-byte block with a single aligned uint4
load plus register-side extraction. The 3-bit index of element j lives at
bit 3*j of the 96-bit qs stream (groups pack 8x3 bits = 24 bits = 3 bytes
with no cross-group gap), so the index/scale helpers work directly off the
uint4. Cleaner than the switch-based unpack and reused by both kernels.
Performance-neutral on decode (~4 tok/s) but better-formed memory access.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The mat-vec was bottlenecked by its memory access pattern, not the math:
the lane=element mapping (needed for the in-kernel inverse RHT butterfly)
forced all 32 lanes to broadcast-read the same 16-byte block, wasting
bandwidth (~33% of peak). A diagnostic capping the K-loop to one block
jumped decode from 4 to 48 tok/s, confirming weight reads dominate.

Fix: move the randomized Hadamard transform out of the mat-vec into a
dedicated pre-pass over the activation (RHT orthogonality:
dot(W_dequant, x) == dot(centroid*scale, RHT_fwd(x))). The mat-vec then
needs only a local codebook lookup on the weights, freeing the lane
mapping so each lane owns whole K-blocks and the 32 lanes read 32
consecutive blocks per step -> fully coalesced loads.

- ggml-metal.metal: kernel_tq3_4s_rht_f32 (forward RHT of activation) and a
  rewritten kernel_mul_mv_tq3_4s_f32 (strided-K lanes, local dequant).
- ggml-metal-ops.cpp: dedicated ggml_metal_op_mul_mat_tq3_4s path
  (pre-pass -> barrier -> mat-vec) so the generic mul_mat code is untouched
  for other types; ggml_metal_op_mul_mat_extra_w1 reserves the scratch.
- ggml-metal.cpp: reserve the scratch via the alloc-size hook.
- ggml-metal-device.{h,cpp}: pipeline accessor for the pre-pass kernel.
- ggml-metal-impl.h: kargs_tq3_rht; retune N_R0/N_SG to 4/4.

Validated on M3 Pro (osmQwopus-3.6-27B-V2 TQ3_4S), Metal greedy output
byte-identical to CPU. Decode 4.06 -> 6.4 tok/s (~51% of bandwidth), prefill
4.7 -> 8.0 tok/s; ~43x over the CPU baseline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Batched evaluation (prefill, and speculative-decode verification) was stuck at
mat-vec speed because the mat-vec re-reads the full weights once per column.
This adds the simdgroup-matrix GEMM path.

The RHT activation pre-pass (already used by the mat-vec) lets the weights use a
purely local dequant, so a standard dequantize_tq3_4s plugs straight into the
stock kernel_mul_mm template (element ordering matches dequantize_q8_0). The
dedicated TQ3_4S mul_mat handler routes ne11>1 to mul_mm (reading the
pre-transformed activation) and keeps the coalesced mat-vec for decode (ne11==1).

- ggml-metal.metal: dequantize_tq3_4s (local, no butterfly) + kernel_mul_mm_tq3_4s_f32.
- ggml-metal-ops.cpp: GEMM branch in ggml_metal_op_mul_mat_tq3_4s.

Validated on M3 Pro (osmQwopus-3.6-27B-V2 TQ3_4S), greedy output byte-identical
to CPU. Prefill 14 tok: 8 -> 32 tok/s; 217 tok: ~6.6 -> 85 tok/s (~13x). Decode
unchanged at ~6.4 tok/s. This also makes speculative-decode verification cheap
(the K-token batch now amortizes weight reads), the prerequisite for draft-model
speculation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Covers the macOS build (incl. the util-linux uuid_string_t gotcha), running a
TQ3_4S model on Metal (the nomtp_trunk_only override), which ops are accelerated,
indicative M3 Pro throughput, and a tuned draft-model speculative command with
the hybrid-target (checkpoint) caveat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation ggml Apple Metal labels Jul 4, 2026
@turbo-tan
turbo-tan merged commit f8397b2 into main Jul 5, 2026
32 checks passed
@turbo-tan
turbo-tan deleted the pr39-metal-tq3_4s-clean-20260704 branch July 5, 2026 22:34
turbo-tan added a commit that referenced this pull request Aug 1, 2026
* metal: add TQ3_4S (TurboQuant 3-bit) GPU kernels for Apple Silicon

TQ3_4S (ggml type 46) was CUDA-only; on Metal it aborted in the mul_mv
pipeline selector ("Asserting on type 46") and only ran on CPU at ~0.15
tok/s. This adds Metal support so TQ3_4S models run on the GPU.

TQ3_4S dequant is a per-32-block inverse randomized Hadamard transform
(8-level codebook lookup + E3M5 per-group scale, then a Walsh-Hadamard
butterfly). The block size QK_TQ3_0 == 32 maps to one 32-lane SIMD-group,
so the butterfly is done with simd_shuffle_xor, mirroring the CUDA kernel
in ggml-cuda/convert.cu.

- ggml-metal.metal: TQ3_0 centroid/sign constants, E3M5 scale decode and
  3-bit unpack helpers, kernel_get_rows_tq3_4s (one SIMD-group per row),
  and kernel_mul_mv_tq3_4s_f32. The mat-vec kernel exploits RHT
  orthogonality to apply the butterfly once per block to the activation
  (shared across rows) so weights only do a local codebook lookup.
- ggml-metal-impl.h: N_R0_TQ3_4S / N_SG_TQ3_4S tuning constants.
- ggml-metal-device.cpp: TQ3_4S case in the mul_mv pipeline selector.
- ggml-metal-ops.cpp: route TQ3_4S MUL_MAT through mat-vec (no mat-mat
  GEMM yet) and dispatch get_rows as one 32-lane SIMD-group per row.

No mul_mm GEMM or MoE _id kernels yet (the qwen35 test model is dense);
those are the next optimizations for prefill / MoE.

Validated on M3 Pro with osmQwopus-3.6-27B-V2 TQ3_4S: Metal greedy output
is byte-identical to CPU; decode ~4 tok/s, prefill ~4.6 tok/s (~27x over
the CPU baseline). Other quant types are unaffected (only TQ3_4S-specific
cases/kernels were added).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* metal: TQ3_4S use aligned uint4 block loads in get_rows/mul_mv

Replace per-byte loads of the 16-byte block with a single aligned uint4
load plus register-side extraction. The 3-bit index of element j lives at
bit 3*j of the 96-bit qs stream (groups pack 8x3 bits = 24 bits = 3 bytes
with no cross-group gap), so the index/scale helpers work directly off the
uint4. Cleaner than the switch-based unpack and reused by both kernels.
Performance-neutral on decode (~4 tok/s) but better-formed memory access.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* metal: TQ3_4S RHT activation pre-pass + coalesced mat-vec (decode +60%)

The mat-vec was bottlenecked by its memory access pattern, not the math:
the lane=element mapping (needed for the in-kernel inverse RHT butterfly)
forced all 32 lanes to broadcast-read the same 16-byte block, wasting
bandwidth (~33% of peak). A diagnostic capping the K-loop to one block
jumped decode from 4 to 48 tok/s, confirming weight reads dominate.

Fix: move the randomized Hadamard transform out of the mat-vec into a
dedicated pre-pass over the activation (RHT orthogonality:
dot(W_dequant, x) == dot(centroid*scale, RHT_fwd(x))). The mat-vec then
needs only a local codebook lookup on the weights, freeing the lane
mapping so each lane owns whole K-blocks and the 32 lanes read 32
consecutive blocks per step -> fully coalesced loads.

- ggml-metal.metal: kernel_tq3_4s_rht_f32 (forward RHT of activation) and a
  rewritten kernel_mul_mv_tq3_4s_f32 (strided-K lanes, local dequant).
- ggml-metal-ops.cpp: dedicated ggml_metal_op_mul_mat_tq3_4s path
  (pre-pass -> barrier -> mat-vec) so the generic mul_mat code is untouched
  for other types; ggml_metal_op_mul_mat_extra_w1 reserves the scratch.
- ggml-metal.cpp: reserve the scratch via the alloc-size hook.
- ggml-metal-device.{h,cpp}: pipeline accessor for the pre-pass kernel.
- ggml-metal-impl.h: kargs_tq3_rht; retune N_R0/N_SG to 4/4.

Validated on M3 Pro (osmQwopus-3.6-27B-V2 TQ3_4S), Metal greedy output
byte-identical to CPU. Decode 4.06 -> 6.4 tok/s (~51% of bandwidth), prefill
4.7 -> 8.0 tok/s; ~43x over the CPU baseline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* metal: TQ3_4S GEMM (mul_mm) for batched eval — prefill up to ~13x

Batched evaluation (prefill, and speculative-decode verification) was stuck at
mat-vec speed because the mat-vec re-reads the full weights once per column.
This adds the simdgroup-matrix GEMM path.

The RHT activation pre-pass (already used by the mat-vec) lets the weights use a
purely local dequant, so a standard dequantize_tq3_4s plugs straight into the
stock kernel_mul_mm template (element ordering matches dequantize_q8_0). The
dedicated TQ3_4S mul_mat handler routes ne11>1 to mul_mm (reading the
pre-transformed activation) and keeps the coalesced mat-vec for decode (ne11==1).

- ggml-metal.metal: dequantize_tq3_4s (local, no butterfly) + kernel_mul_mm_tq3_4s_f32.
- ggml-metal-ops.cpp: GEMM branch in ggml_metal_op_mul_mat_tq3_4s.

Validated on M3 Pro (osmQwopus-3.6-27B-V2 TQ3_4S), greedy output byte-identical
to CPU. Prefill 14 tok: 8 -> 32 tok/s; 217 tok: ~6.6 -> 85 tok/s (~13x). Decode
unchanged at ~6.4 tok/s. This also makes speculative-decode verification cheap
(the K-token batch now amortizes weight reads), the prerequisite for draft-model
speculation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: how to run TQ3_4S on Metal (build, run, speculative)

Covers the macOS build (incl. the util-linux uuid_string_t gotcha), running a
TQ3_4S model on Metal (the nomtp_trunk_only override), which ops are accelerated,
indicative M3 Pro throughput, and a tuned draft-model speculative command with
the hybrid-target (checkpoint) caveat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ggml-metal: guard SET_ROWS to supported index types

* ggml-metal: guard missing set_rows pipeline in set_rows op encoding

* ggml-metal: tighten backend-op CI coverage

* ggml-metal: use available error log macro

---------

Co-authored-by: Martin Romañuk <mromanuk@me.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: charpdev <charpdev@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Apple Metal devops documentation Improvements or additions to documentation ggml testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants