Skip to content

feat: ROCm/HIP support for turbo3 KV cache (gfx1100/RDNA3)#5

Open
apollosenvy wants to merge 115 commits intoTheTom:feature/turboquant-kv-cachefrom
apollosenvy:rocm-turbo3-pr
Open

feat: ROCm/HIP support for turbo3 KV cache (gfx1100/RDNA3)#5
apollosenvy wants to merge 115 commits intoTheTom:feature/turboquant-kv-cachefrom
apollosenvy:rocm-turbo3-pr

Conversation

@apollosenvy
Copy link
Copy Markdown

Summary

Adds complete ROCm/HIP backend support for turbo3 KV cache quantization, enabling 4.6x KV compression on AMD GPUs.

Tested on: AMD Radeon RX 7900 XTX (gfx1100), ROCm 7.1, Qwen3.5-27B Q4_K_M

What's included

  • GPU kernels: dequant, vec_dot, Walsh-Hadamard Transform, quantize, GET_ROWS, CPY
  • Flash attention: F16 dequant bridge (turbo3 -> F16 temp buffer -> F16 FA kernel). The native turbo3 FA template compiles but produces broken kernels on HIP/RDNA3, so we route through the existing need_f16_K/V conversion path in launch_fattn.
  • CPU fallback: vec_dot + type traits for graph scheduler fallback paths
  • All dispatch wiring: supports_op for MUL_MAT, GET_ROWS, SET_ROWS, CPY, FLASH_ATTN_EXT, TURBO_WHT

Critical bugfix: K/V rotation before cache write

Found and fixed a quality issue: K and V vectors were being quantized to turbo3 without WHT rotation. The graph rotated Q (forward) and inverse-rotated the attention output, but the K/V write path in cpy_k()/cpy_v() was missing the forward rotation step.

Before fix: <accepts high pressure "Itself" - ount>& & nsp; a- xii'
After fix: 7 x 8 = 56. This is one of the most common multiplication facts.

This fix is in src/llama-kv-cache.cpp and may also benefit the Metal path if it has the same omission.

Performance

Metric FP16 KV turbo3 KV Ratio
Prompt 90 tok/s 82 tok/s 91%
Generation 21 tok/s 12 tok/s 57%*
KV cache (32K ctx) 2048 MB 448 MB 4.6x

*Generation speed regression is from AMD_SERIALIZE_KERNEL=3 requirement (see below).

Known limitation

Requires AMD_SERIALIZE_KERNEL=3 environment variable. Without it, the multi-stream graph scheduler has a race condition between KV cache write and FA read ops. Per-op cudaStreamSynchronize is not sufficient. This is a ROCm runtime issue, not a turbo3 bug.

Files changed (24 files, +595 lines)

New files (5):

  • dequantize-turbo.cuh - Device dequant (centroid LUT)
  • vecdot-turbo.cuh - MMVQ vec_dot
  • turbo-wht.cu/cuh - WHT butterfly kernel
  • fattn-vec-instance-turbo3_0-turbo3_0.cu - FA template (D=256)

Modified (19): common.cuh, mmvq.cu, fattn-common.cuh, fattn-vec.cuh, fattn.cu, convert.cu, cpy-utils.cuh, cpy.cu, set-rows.cu, getrows.cu, ggml-cuda.cu, CMakeLists.txt (cuda+hip), ggml-turbo-quant.c, ggml-quants.h, ggml-cpu.c, quants.h, llama-kv-cache.cpp

Test plan

  • Standalone GPU kernel test (quantize/dequant round-trip)
  • Server starts with --cache-type-k turbo3 --cache-type-v turbo3
  • KV cache allocates at expected compression ratio
  • Inference produces coherent output
  • Math reasoning correct (7*8=56)
  • Perplexity benchmark vs upstream Metal numbers
  • Multi-turn conversation stability

TheTom and others added 30 commits March 26, 2026 12:15
New types: GGML_TYPE_TURBO3_0 (3-bit) and GGML_TYPE_TURBO4_0 (4-bit)
Implements PolarQuant + QJL compression per the ICLR 2026 paper.

Block size = 128 (matching head_dim for optimal rotation Gaussianization)
turbo3: 52 bytes per 128 values = 3.25 bits/value (4.9× vs fp16)
turbo4: 68 bytes per 128 values = 4.25 bits/value (3.8× vs fp16)

Status:
- ✅ Type definitions in ggml.h
- ✅ Block structures in ggml-common.h
- ✅ Quantize/dequantize C implementation in ggml-turbo-quant.c
- ✅ Registered in ggml.c type traits
- ✅ Added to kv_cache_types in arg.cpp
- ✅ Builds successfully
- ✅ Shows in --help output
- ❌ Metal SET_ROWS kernel not implemented (blocks GPU inference)
- ❌ Needs Metal dequantize kernels for attention computation

Co-Authored-By: tturney@psyguard.ai
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Added Metal shader implementations:
- quantize_turbo3_0 / quantize_turbo4_0 (per-block quantization)
- dequantize_turbo3_0 / dequantize_turbo4_0 (type4x4 and type4 variants)
- kernel_set_rows_turbo template (128-element block size)
- Flash attention instantiations for all dk/dv variants

Added TURBO3_0/TURBO4_0 to Metal device SET_ROWS validation.

Builds successfully. Testing with Qwen 3.5 35B-A3B MoE on M5 Max.

Note: Initial version uses simplified quantization (no rotation matrix)
for Metal compatibility. Full rotation requires custom kernel with extra
buffer bindings — tracked for follow-up.

Co-Authored-By: tturney@psyguard.ai
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Embedded pre-computed 128×128 rotation and QJL matrices (256KB constant
memory) directly in the Metal shader. Both quantize and dequantize now
perform the full TurboQuant algorithm:

Quantize: normalize → rotate → codebook → inverse rotate → residual → QJL
Dequantize: codebook → inverse rotate → QJL correction → rescale

Previous version (no rotation) produced garbage. This should produce
meaningful output since the rotation Gaussianizes the KV distribution.

Note: dequantize does full 128-element rotation per chunk (8× work).
Optimization possible with caching or restructured kernel in follow-up.

Co-Authored-By: tturney@psyguard.ai
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…eTom#21

- Inlined turbo-matrices.h directly into ggml-metal.metal (256KB)
  to fix JIT compilation failure with #include
- Added C round-trip test (test-turbo-quant.c):
  turbo3 cosine=0.906, turbo4 cosine=0.966 — matches Python prototype
- Metal library loads successfully ("loaded in 5.9 sec")
- Model runs on Metal but output quality needs debugging
  (Metal quantize/dequantize may have a bug vs the working C version)

C round-trip PROVES the algorithm works in C. Metal shader needs
debugging — likely an issue with the dequantize chunk addressing
or the large constant arrays in thread-local memory.

Co-Authored-By: tturney@psyguard.ai
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…m#23

Codex review found:
1. Stale duplicate code in dequantize_turbo3_0_t4 (compile would fail)
2. thread static is risky/non-portable in MSL

Fixed: removed thread static caching, using plain thread locals.
Speed unchanged (2.4 tok/s) — the static caching wasn't actually working
on Metal. True optimization needs architectural change in flash attention
kernel to dequantize once per block, not per chunk.

Co-Authored-By: tturney@psyguard.ai
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…heTom#26

Massive reduction in constant memory and compute:
- 256KB of dense matrices → 512 bytes of sign arrays
- O(d²) = 16,384 ops → O(d log d) = 896 ops per rotation
- Metal shader file: 1.5MB → 432KB

Speed: still 2.4 tok/s. WHT reduced per-rotation cost but the
bottleneck is redundant calls (8-32× per block from flash attention).
The dequantize function is called per 4/16-element chunk, each time
doing the full 128-element WHT. Need to modify the flash attention
kernel to dequantize once per block.

Quality: WHT+signs gives BETTER quality than dense QR on real KV
tensors (cosine 0.94 vs 0.79 at 2-bit). Sub-Gaussian distribution
(kurtosis 1.53) means fewer outliers hitting extreme centroids.

Reviewed by Codex: WHT butterfly correct, inverse order verified,
QJL correction matches reference C implementation.

Co-Authored-By: tturney@psyguard.ai
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…heTom#23

Root cause analysis: 8-32× redundant full-block dequantize per block
from flash attention template. Four approaches documented with expected
speedups and risk levels.

Plan: D (reduce overhead) → A/B (eliminate redundant calls)
Target: 2.4 tok/s → 20-40 tok/s

Co-Authored-By: tturney@psyguard.ai
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: tturney@psyguard.ai
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…om#23

Co-Authored-By: tturney@psyguard.ai
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…heTom#23

No-op dequant test: even returning all zeros from dequantize, turbo3
runs at 2.4 tok/s (same as with full WHT rotation). The bottleneck is
NOT in the attention dequantize path.

New hypothesis: the SET_ROWS (quantize) path is the bottleneck. The
Metal quantize_turbo3_0 function does 3 WHT rotations per KV write,
totaling ~3200 ops per block × 224 blocks per token.

Co-Authored-By: tturney@psyguard.ai
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>


CRITICAL BUG: The #include "turbo-wht.h" caused Metal JIT compilation
to fail at runtime. The model silently fell back to CPU for ALL ops.
ALL previous benchmarks (2.4 tok/s) were measuring CPU, not Metal GPU.

After inlining the header:
- MoE gen: 2.4 → 10.7 tok/s (4.5× improvement, now actually on Metal)
- MoE prompt: 4.2 → 60.9 tok/s (14.5× improvement)

Remaining gap vs q8_0: 85 → 10.7 tok/s (8× slower, down from 35×)

This is the SAME bug we hit with turbo-matrices.h earlier.
Rule: NEVER use #include in ggml-metal.metal — always inline.

Co-Authored-By: tturney@psyguard.ai
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…m#23

Previous 2.4 tok/s was CPU fallback. Real Metal numbers:
MoE: 10.7 tok/s gen (8× slower than q8_0, was thought to be 35×)
Qwopus: 5.3 tok/s gen (3.3× slower than q8_0)

Co-Authored-By: tturney@psyguard.ai
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…m#27

Full investigation log with all tests, results, and the root cause.
Upstream TurboQuant activity tracked in TheTom#27.

Co-Authored-By: tturney@psyguard.ai
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…om#28

Key findings from Dejan.ai, unixsysdev, and mudler:
1. QJL naively added back destroys quality (cosine 0.69)
2. Pre-rotate queries eliminates rotation from dequant path
3. WHT abandoned by everyone — dense QR or no rotation preferred
4. unixsysdev gets -0.8% speed loss with fused CUDA kernel
5. We're the only Metal implementation

Co-Authored-By: tturney@psyguard.ai
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…in) TheTom#23

Removing WHT rotation from dequant (quality broken, speed test only):
  gen: 10.7 → 49.1 tok/s (4.6× improvement, 57% of q8_0)
  prompt: 67.3 → 162.6 tok/s

Confirms pre-rotate-queries would deliver ~49 tok/s.
Remaining gap (49 vs 85) is block size + QJL overhead.

Co-Authored-By: tturney@psyguard.ai
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Speed ceiling confirmed: stripping rotation from dequant gives 49.1 tok/s
(vs 10.7 with rotation, vs 85.5 q8_0 baseline).

Implementation plan: store rotation matrix in KV cache, apply to Q in
graph builder, strip from Metal dequant. 6 files to modify.

Co-Authored-By: tturney@psyguard.ai
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…m#23

Instead of inverse-rotating every K during dequant, rotate Q once
before attention. Math: <q, R^T*c[idx]> = <R*q, c[idx]>.

Changes:
- Store rotation matrix (R^T) in KV cache, filled after buffer clear
- Apply ggml_mul_mat(R_T, q) in build_attn_mha after permute
- Strip turbo_rotate_inverse from Metal dequant
- Dynamic cast to access rotation from mctx

Results:
- MoE gen: 10.7 → 51.4 tok/s (4.8× speedup)
- MoE prompt: 67.3 → 160.3 tok/s (2.4× speedup)
- Now at 60% of q8_0 speed with 4.9× compression
- Model produces coherent output

Codex review: fixed buffer clear ordering (was zeroing rotation after init).
Verified: rotation point is correct (after 4d reshape + permute, ne[0]=128).

Co-Authored-By: tturney@psyguard.ai
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…heTom#23

Full investigation log documenting every test, every dead end, and every
breakthrough. 21× total improvement from CPU fallback to pre-rotate-queries.

Key lessons: no #include in Metal, no-op testing, pre-rotate-queries,
buffer clear ordering, codex+roast catch real bugs.

Co-Authored-By: tturney@psyguard.ai
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Validated on real Qwen3 KV tensors: cosine sim 0.9508 → 0.9831 (+3.2%)
MSE-only better on 99.3% of vectors including p1 tails.

3-bit index split: lower 2 bits in qs[], upper 1 bit in signs[].
No QJL stage in quantize or dequant.

Results:
- MoE gen: 51.4 → 62.2 tok/s (73% of q8_0, was 60%)
- MoE prompt: 160 → 200 tok/s (90% of q8_0)
- Qwopus gen: 14.6 → 15.5 tok/s (88% of q8_0, was 83%)
- Qwopus prompt: 67 → 83 tok/s (100% of q8_0!)

Codex verified: bit packing correct, quantize/dequant consistent.

Co-Authored-By: tturney@psyguard.ai
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Speed ceiling without Q rotation: 61.3 tok/s (vs 62.2 with it).
The 128×128 ggml_mul_mat adds <1% overhead on Metal.

Remaining gap is structural (block size + dequant complexity).
Final: MoE 62.2 tok/s (73%), Qwopus 15.5 tok/s (88%).

Co-Authored-By: tturney@psyguard.ai
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Diagnostic benchmark proves the 26% gap is entirely from block size 128.
q4_0 (block 32, 4-bit quantization) runs at 84.2 tok/s = identical to q8_0.

Next: turbo3 with block size 32.

Co-Authored-By: tturney@psyguard.ai
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Changed QK_TURBO3 from 128 to 32 (storage block size).
Rotation still operates on 128-element groups (QK_TURBO3_GROUP=128).
SET_ROWS kernel processes 4 blocks per rotation group.
Flash attention nl_k changed from 32 to 8 (matching q4_0).

Block struct: 14 bytes per 32 values = 3.5 bits/val → 4.6× compression.

Results:
- MoE gen: 62.2 → 77.7 tok/s (91% of q8_0 at 85.5)
- MoE prompt: 200 → 218.5 tok/s (98% of q8_0)
- Qwopus gen: 15.5 → 17.0 tok/s (97% of q8_0 at 17.6)
- Qwopus prompt: 83 → 89.5 tok/s (108% of q8_0 — FASTER)

Target was 75+ tok/s. Exceeded.

Co-Authored-By: tturney@psyguard.ai
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Codex post-commit review found:
1. TURBO_D was QK_TURBO3 (now 32) — broke turbo4 C array sizes
2. SET_ROWS kernel turbo3-specific but instantiated for turbo4
3. Tail block drop for non-128 head dims

Fixed TheTom#3 (TURBO_D). TheTom#1 and TheTom#2 don't affect turbo3+dk128 path.

Co-Authored-By: tturney@psyguard.ai
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: tturney@psyguard.ai
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…Tom#30

Perplexity benchmarking reveals catastrophic quality failure:
- f16: 6.121, q8_0: 6.111, q4_0: 6.142
- turbo3: 165.6 (27× worse)

Speed benchmarks were meaningless — fast garbage.
Root cause investigation needed before any quality claims.

Co-Authored-By: tturney@psyguard.ai
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. V cache returns rotated-space values (cosine=0.02 vs correct 0.987)
2. dynamic_cast to llama_kv_cache_context fails for MoE models
   (uses llama_memory_hybrid_context, not kv_cache_context)
   → Q rotation and V inverse rotation NEVER executed

Fix: store rotation tensors in llm_graph_context, not KV cache.
Or access through hybrid memory interface.

Co-Authored-By: tturney@psyguard.ai
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…heTom#31

Block 128: PPL=165.6 (same as block 32)
Disabled Q rotation: PPL=165.6 (same)
Root cause: dynamic_cast fails for MoE hybrid memory context.
Q rotation and V inverse rotation never execute.

Co-Authored-By: tturney@psyguard.ai
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…eTom#31 TheTom#30

ROOT CAUSE: pre-rotate-queries never executed because:
1. Q ne[0]=256 (GQA concatenated heads), rotation matrix ne[0]=128
2. mctx dynamic_cast failed for MoE hybrid memory

FIX: put inverse WHT rotation back in dequantize_full_block.
This is slower (10.7 tok/s vs 77.7) but produces CORRECT results.

PERPLEXITY RESULTS:
- f16:     6.121
- q8_0:    6.111
- q4_0:    6.142
- turbo3:  6.194 (+1.2% vs q8_0) ✅

The speed optimization (pre-rotate-queries) needs to be reimplemented
to work with GQA head layout and hybrid memory types.

Co-Authored-By: tturney@psyguard.ai
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: tturney@psyguard.ai
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Quality confirmed: PPL 6.194 (+1.4% of q8_0)
Speed: 10.7 tok/s (inverse rotation in dequant, no pre-rotate-queries)
Previous speed claims (51-77 tok/s) were invalid — measured garbage output speed.

Key lessons documented for future reference.

Co-Authored-By: tturney@psyguard.ai
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@apollosenvy
Copy link
Copy Markdown
Author

Synced to TOT — HIP port on rocm-turbo3-v2

Rebased onto your latest feature/turboquant-kv-cache (172fc85). Clean single commit: HIP/ROCm porting for your warp-cooperative turbo3 kernel + turbo2/turbo3 FA templates.

What was needed for HIP

  1. Vendor header (hip.h): Added cudaMemcpyToSymbol/FromSymbol mappings. Fixed __shfl_sync, __shfl_xor_sync, __shfl_up_sync, __shfl_down_sync to support 3-arg calls (CUDA defaults width to warpSize, old HIP macros required 4 args). Added __ballot_sync -> __ballot with uint32_t cast.

  2. HIP CMakeLists: Added turbo3 and turbo2 FA template instances (turbo3-turbo3, turbo3-q8_0, q8_0-turbo3, turbo2-turbo2, turbo2-q8_0, q8_0-turbo2).

PPL Confirmed

You were right about the pipeline bugs. With the CPU quantize stub fix (53f1298):

Model KV Type PPL vs F16
Mistral-Small-24B (hd128) F16 5.16 baseline
Mistral-Small-24B (hd128) turbo3 5.28 +2.4%
Mistral-Small-24B (hd128) q4_0 5.26 +1.9%

PolarQuant IS sound. The catastrophic PPL was 100% the CPU quantize stub zeroing qs/signs on layer 0. My earlier "fundamental limitation" analysis was wrong — thanks for pushing back on that.

Branch is rocm-turbo3-v2 on fork. Ready for review.

Tuklus and others added 8 commits March 28, 2026 22:12
Two-dimensional KV cache compression: static per-layer type selection
(vertical, existing TURBO_LAYER_ADAPTIVE) combined with dynamic
per-position quality degradation (horizontal, new TURBO_DECAY).

Old tokens get their QJL refinement bits zeroed in-place, reducing
effective precision without changing the ggml tensor type. Tier
boundaries shift dynamically with context length. Re-quantization
piggybacks on SET_ROWS dispatch -- one memset per block per token.

Three presets: conservative (25/40/35), balanced (15/35/50),
aggressive (5/25/70). Custom percentages via TURBO_DECAY=H,W,C.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Task 1: Config struct + env var parsing
Task 2: ggml TURBO_DECAY op declaration
Task 3: CUDA/HIP/CPU decay kernel (signs zeroing)
Task 4: Wire into cpy_k/cpy_v graph dispatch
Task 5: PPL validation benchmarks
Task 6: Long-context stress test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- turbo3 signs field is 3rd centroid bit, not QJL. Zeroing reduces
  8-level to 4-level reconstruction (corrected description)
- turbo4 has two compile-time variants: 4-bit (no QJL, skip decay)
  vs legacy 3-bit+QJL (zero rnorm). Both documented.
- Boundaries computed in position space, not cell index space
  (ring buffer ordering is non-monotonic)
- Multi-sequence policy: conservative (cold for ALL sequences)
  Phase 1 single-sequence only.
- VRAM claim removed (decay doesn't reduce VRAM, it improves
  quality-at-equal-VRAM)
- Added edge cases: SWA, seq_rm, defrag, serialization, idempotency
- Clarified graph execution model (ggml op, not side-effect)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds the configuration infrastructure for TurboQuant+ temporal decay:
- turbo_decay_config struct (public, in llama_kv_cache) with hot/warm/cold
  percentage splits and hot_promote flag
- parse_turbo_decay() static function reads TURBO_DECAY env var with named
  presets (conservative/balanced/aggressive) or custom h,w,c splits
- TURBO_DECAY_HOT_PROMOTE env var for phase 2 q8_0 promotion
- Per-layer last_cold_boundary tracking vector initialized in constructor

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds the GGML_OP_TURBO_DECAY enum value, ggml_turbo_decay() factory
function, and name/symbol table entries. The op records a position
range [cold_start, cold_end) in op_params; the backend kernel (Task 3)
will zero the signs field of turbo3/turbo4 KV cache blocks in that
range. GGML_OP_COUNT static_assert bumped to 98.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implements GGML_OP_TURBO_DECAY backend kernels:
- CUDA/HIP: k_turbo3_decay zeros signs[] to collapse 3-bit to 2-bit
  reconstruction; k_turbo4_decay (legacy only) also zeros rnorm
- CPU: single-threaded memset fallback for both turbo3 and turbo4
- Properly gated behind TURBO4_USE_4BIT for turbo4 (4-bit variant
  has no signs field to zero)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After writing new KV entries via ggml_set_rows, append a
ggml_turbo_decay node that demotes old positions past the cold
boundary. Only fires for turbo3/turbo4 caches in single-sequence
mode when TURBO_DECAY is enabled. The last_cold_boundary vector
is made mutable so const cpy_k/cpy_v methods can track boundary
advancement per layer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Qwen3.5-27B Q4_K_M, turbo3 KV, 7900 XTX, ctx=2048, 4 chunks:

PPL:
  no decay:   7.58 (baseline)
  balanced:   8.43 (+11.2%)
  aggressive: 9.81 (+29.4%)

Speed (pp128 / tg128):
  no decay:   528 / 19.07 t/s
  balanced:   530 / 18.62 t/s (within noise)

PPL regression is expected at short context (50-70% of 2K tokens
get degraded). At longer contexts the cold tier covers older tokens
with lower attention weight, so the quality impact decreases.

Zero measurable speed overhead from decay op.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@apollosenvy
Copy link
Copy Markdown
Author

TurboQuant+ Temporal Decay -- Implemented

Pushed to rocm-turbo3-v2: position-aware KV cache quality degradation. Old tokens get their signs field zeroed in-place, reducing turbo3 from 8-level to 4-level centroid reconstruction.

How it works

Three dynamic tiers with proportional boundaries:

  • Hot (newest 15%): full quality
  • Warm (middle 35%): base turbo3 quality
  • Cold (oldest 50%): signs zeroed, 4-level reconstruction

Boundaries shift proportionally as context grows. Demotion is a single ggml_turbo_decay op appended to the graph after SET_ROWS. One kernel launch per layer per boundary advance.

Configuration

TURBO_DECAY=balanced          # 15/35/50 (default)
TURBO_DECAY=conservative      # 25/40/35
TURBO_DECAY=aggressive        # 5/25/70
TURBO_DECAY=10,30,60          # custom percentages

Benchmark (Qwen3.5-27B Q4_K_M, 7900 XTX)

PPL (ctx=2048, 4 chunks):

Mode PPL vs No-Decay
off 7.58 baseline
balanced 8.43 +11.2%
aggressive 9.81 +29.4%

Speed: Zero measurable overhead (within noise).

The PPL hit at 2K is expected -- at short context, the cold tier covers tokens the model still actively attends to. At longer contexts (16K+), the cold tier covers much older tokens with naturally lower attention weight, so the quality impact should decrease. This is the design tradeoff: sacrifice some short-context quality for better long-context compression efficiency.

Implementation (8 commits)

  1. Design spec + reviewer feedback fixes
  2. Implementation plan
  3. Config struct + env var parsing
  4. GGML_OP_TURBO_DECAY op
  5. CUDA/HIP/CPU decay kernels
  6. Wire into cpy_k/cpy_v graph
  7. PPL validation
  8. Push

Phase 2 (hot tier q8_0 promotion) and Phase 3 (attention-guided decay) designed but deferred.

@github-actions github-actions bot added documentation Improvements or additions to documentation Nvidia GPU ggml labels Mar 29, 2026
@TheTom
Copy link
Copy Markdown
Owner

TheTom commented Mar 30, 2026

Tested on M5 Max 128GB (Metal) and M2 Pro 32GB (Metal). Thanks for the work here, some notes below.

What passes (head_dim=128 models)

PPL and speed are clean for standard models:

Config PPL (8-chunk) pp512 t/s tg128 t/s Known baseline
turbo3 6.1756 2728 75.14 6.1756 (match)
turbo4 6.1250 6.1250 (match)

Build passes on Metal with no warnings.

Issue 1: TURBO_DECAY crashes on Metal

The temporal decay op has CUDA/HIP/CPU kernels but no Metal kernel. When TURBO_DECAY=balanced is set with a GPU-offloaded model, it aborts:

pre-allocated tensor (cache_k_l3 (view) (view)) in a buffer (MTL0) that cannot run the operation (TURBO_DECAY)

Repro:

TURBO_DECAY=balanced ./build/bin/llama-cli \
  -m model.gguf -ngl 99 -fa on \
  -ctk turbo3 -ctv turbo3 \
  -p "Write a 500 word essay about the history of computing" -n 500

This is a crash, not a graceful fallback. The decay op needs either a Metal kernel or a supports_op guard that prevents dispatch to the Metal backend. As-is, any Metal user who sets the env var will hit an abort.

Issue 2: Boundary V modes 5/6/7 removed

The PR removes layer-adaptive modes 5, 6, and 7 (Boundary V) from the constructor. These were shipped in PR #30 and are part of our current feature set. TURBO_LAYER_ADAPTIVE=5 no longer produces a "Boundary V" log line, confirming the code path is gone.

Suggestion

Consider splitting this into separate PRs:

  1. ROCm/HIP kernel support (the core contribution, looks solid)
  2. Temporal decay (experimental feature, needs Metal kernel + more validation)
  3. Non-128 head_dim q8_0 fallback (architectural decision worth discussing separately)

Happy to re-review individual pieces. The ROCm work is great and would be straightforward to land on its own.

@apollosenvy
Copy link
Copy Markdown
Author

Split per your suggestion:

  • PR feat: HIP/ROCm support for turbo3/turbo2 (7900 XTX) #31 — ROCm/HIP port only. Single commit, minimal. Rebased on your latest (includes Boundary V). Also excludes D>=576 fattn-tile instances that exceed HIP's 64KB local memory limit.

  • Temporal decay — Will open as separate PR once Metal kernel or supports_op guard is added. The decay op shouldn't dispatch to backends that don't implement it.

  • head_dim fallback — Your commit 4c4511c14 already handles this. No separate PR needed from our side.

Re: modes 5/6/7 — we didn't remove them. Our branch was based on a commit before PR #30. The new PR #31 is rebased on your latest which includes Boundary V, so modes 5/6/7 are intact.

TheTom added a commit that referenced this pull request Apr 2, 2026
Complete experiment log:
  #1  4-mag LUT:           15.1 at 8K (BEST, +38%)
  #2  Batched extract:     13.7 (+25%)
  #3  Inline FA block:     13.5 (I-cache pressure)
  #4  Deferred norm:       12.9 (loses ILP)
  #5  2-pair half2:        12.0 (ternary overhead)
  #6  Select chain:        11.9 (branches kill)
  #7  Bit-arithmetic:      11.6 (ALU too heavy)
  #8  FMA branchless:      11.4 (ALU still too heavy)
  #9  Named-reg ternary:   10.3 (branches worst)
  #10 Main (8-LUT):        10.95 (baseline)
  #11 Non-vec FA:          10.2 (wrong kernel)
  Ceiling:                 24.5 (no dequant)

Apple8 hardware truth:
  1 divergent constant read < 7 ALU ops (even with fma)
  Branches cost MORE than divergent constant reads
  Array indexing ALWAYS spills on Metal
  4 constant addresses is the sweet spot

The 4-mag LUT is the dequant-level ceiling on Apple Silicon.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: tturney@psyguard.ai
TheTom added a commit that referenced this pull request Apr 2, 2026
Complete experiment log:
  #1  4-mag LUT:           15.1 at 8K (BEST, +38%)
  #2  Batched extract:     13.7 (+25%)
  #3  Inline FA block:     13.5 (I-cache pressure)
  #4  Deferred norm:       12.9 (loses ILP)
  #5  2-pair half2:        12.0 (ternary overhead)
  #6  Select chain:        11.9 (branches kill)
  #7  Bit-arithmetic:      11.6 (ALU too heavy)
  #8  FMA branchless:      11.4 (ALU still too heavy)
  #9  Named-reg ternary:   10.3 (branches worst)
  #10 Main (8-LUT):        10.95 (baseline)
  #11 Non-vec FA:          10.2 (wrong kernel)
  Ceiling:                 24.5 (no dequant)

Apple8 hardware truth:
  1 divergent constant read < 7 ALU ops (even with fma)
  Branches cost MORE than divergent constant reads
  Array indexing ALWAYS spills on Metal
  4 constant addresses is the sweet spot

The 4-mag LUT is the dequant-level ceiling on Apple Silicon.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: tturney@psyguard.ai
@TheTom TheTom force-pushed the feature/turboquant-kv-cache branch from 63b832b to e9c54d5 Compare April 3, 2026 16:14
iamwavecut pushed a commit to iamwavecut/llama-cpp-turboquant that referenced this pull request Apr 8, 2026
Complete experiment log:
  TheTom#1  4-mag LUT:           15.1 at 8K (BEST, +38%)
  TheTom#2  Batched extract:     13.7 (+25%)
  TheTom#3  Inline FA block:     13.5 (I-cache pressure)
  TheTom#4  Deferred norm:       12.9 (loses ILP)
  TheTom#5  2-pair half2:        12.0 (ternary overhead)
  TheTom#6  Select chain:        11.9 (branches kill)
  TheTom#7  Bit-arithmetic:      11.6 (ALU too heavy)
  TheTom#8  FMA branchless:      11.4 (ALU still too heavy)
  TheTom#9  Named-reg ternary:   10.3 (branches worst)
  TheTom#10 Main (8-LUT):        10.95 (baseline)
  TheTom#11 Non-vec FA:          10.2 (wrong kernel)
  Ceiling:                 24.5 (no dequant)

Apple8 hardware truth:
  1 divergent constant read < 7 ALU ops (even with fma)
  Branches cost MORE than divergent constant reads
  Array indexing ALWAYS spills on Metal
  4 constant addresses is the sweet spot

The 4-mag LUT is the dequant-level ceiling on Apple Silicon.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: tturney@psyguard.ai
@TheTom TheTom force-pushed the feature/turboquant-kv-cache branch from 10cb187 to 0d6b38a Compare April 8, 2026 23:49
KGardevoir pushed a commit to KGardevoir/llama-cpp-turboquant that referenced this pull request Apr 9, 2026
Complete experiment log:
  TheTom#1  4-mag LUT:           15.1 at 8K (BEST, +38%)
  TheTom#2  Batched extract:     13.7 (+25%)
  TheTom#3  Inline FA block:     13.5 (I-cache pressure)
  TheTom#4  Deferred norm:       12.9 (loses ILP)
  TheTom#5  2-pair half2:        12.0 (ternary overhead)
  TheTom#6  Select chain:        11.9 (branches kill)
  TheTom#7  Bit-arithmetic:      11.6 (ALU too heavy)
  TheTom#8  FMA branchless:      11.4 (ALU still too heavy)
  TheTom#9  Named-reg ternary:   10.3 (branches worst)
  TheTom#10 Main (8-LUT):        10.95 (baseline)
  TheTom#11 Non-vec FA:          10.2 (wrong kernel)
  Ceiling:                 24.5 (no dequant)

Apple8 hardware truth:
  1 divergent constant read < 7 ALU ops (even with fma)
  Branches cost MORE than divergent constant reads
  Array indexing ALWAYS spills on Metal
  4 constant addresses is the sweet spot

The 4-mag LUT is the dequant-level ceiling on Apple Silicon.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: tturney@psyguard.ai
KGardevoir pushed a commit to KGardevoir/llama-cpp-turboquant that referenced this pull request Apr 10, 2026
Complete experiment log:
  TheTom#1  4-mag LUT:           15.1 at 8K (BEST, +38%)
  TheTom#2  Batched extract:     13.7 (+25%)
  TheTom#3  Inline FA block:     13.5 (I-cache pressure)
  TheTom#4  Deferred norm:       12.9 (loses ILP)
  TheTom#5  2-pair half2:        12.0 (ternary overhead)
  TheTom#6  Select chain:        11.9 (branches kill)
  TheTom#7  Bit-arithmetic:      11.6 (ALU too heavy)
  TheTom#8  FMA branchless:      11.4 (ALU still too heavy)
  TheTom#9  Named-reg ternary:   10.3 (branches worst)
  TheTom#10 Main (8-LUT):        10.95 (baseline)
  TheTom#11 Non-vec FA:          10.2 (wrong kernel)
  Ceiling:                 24.5 (no dequant)

Apple8 hardware truth:
  1 divergent constant read < 7 ALU ops (even with fma)
  Branches cost MORE than divergent constant reads
  Array indexing ALWAYS spills on Metal
  4 constant addresses is the sweet spot

The 4-mag LUT is the dequant-level ceiling on Apple Silicon.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: tturney@psyguard.ai
KGardevoir pushed a commit to KGardevoir/llama-cpp-turboquant that referenced this pull request Apr 13, 2026
Complete experiment log:
  TheTom#1  4-mag LUT:           15.1 at 8K (BEST, +38%)
  TheTom#2  Batched extract:     13.7 (+25%)
  TheTom#3  Inline FA block:     13.5 (I-cache pressure)
  TheTom#4  Deferred norm:       12.9 (loses ILP)
  TheTom#5  2-pair half2:        12.0 (ternary overhead)
  TheTom#6  Select chain:        11.9 (branches kill)
  TheTom#7  Bit-arithmetic:      11.6 (ALU too heavy)
  TheTom#8  FMA branchless:      11.4 (ALU still too heavy)
  TheTom#9  Named-reg ternary:   10.3 (branches worst)
  TheTom#10 Main (8-LUT):        10.95 (baseline)
  TheTom#11 Non-vec FA:          10.2 (wrong kernel)
  Ceiling:                 24.5 (no dequant)

Apple8 hardware truth:
  1 divergent constant read < 7 ALU ops (even with fma)
  Branches cost MORE than divergent constant reads
  Array indexing ALWAYS spills on Metal
  4 constant addresses is the sweet spot

The 4-mag LUT is the dequant-level ceiling on Apple Silicon.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: tturney@psyguard.ai
KGardevoir pushed a commit to KGardevoir/llama-cpp-turboquant that referenced this pull request Apr 14, 2026
Complete experiment log:
  TheTom#1  4-mag LUT:           15.1 at 8K (BEST, +38%)
  TheTom#2  Batched extract:     13.7 (+25%)
  TheTom#3  Inline FA block:     13.5 (I-cache pressure)
  TheTom#4  Deferred norm:       12.9 (loses ILP)
  TheTom#5  2-pair half2:        12.0 (ternary overhead)
  TheTom#6  Select chain:        11.9 (branches kill)
  TheTom#7  Bit-arithmetic:      11.6 (ALU too heavy)
  TheTom#8  FMA branchless:      11.4 (ALU still too heavy)
  TheTom#9  Named-reg ternary:   10.3 (branches worst)
  TheTom#10 Main (8-LUT):        10.95 (baseline)
  TheTom#11 Non-vec FA:          10.2 (wrong kernel)
  Ceiling:                 24.5 (no dequant)

Apple8 hardware truth:
  1 divergent constant read < 7 ALU ops (even with fma)
  Branches cost MORE than divergent constant reads
  Array indexing ALWAYS spills on Metal
  4 constant addresses is the sweet spot

The 4-mag LUT is the dequant-level ceiling on Apple Silicon.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: tturney@psyguard.ai
KGardevoir pushed a commit to KGardevoir/llama-cpp-turboquant that referenced this pull request Apr 15, 2026
Complete experiment log:
  TheTom#1  4-mag LUT:           15.1 at 8K (BEST, +38%)
  TheTom#2  Batched extract:     13.7 (+25%)
  TheTom#3  Inline FA block:     13.5 (I-cache pressure)
  TheTom#4  Deferred norm:       12.9 (loses ILP)
  TheTom#5  2-pair half2:        12.0 (ternary overhead)
  TheTom#6  Select chain:        11.9 (branches kill)
  TheTom#7  Bit-arithmetic:      11.6 (ALU too heavy)
  TheTom#8  FMA branchless:      11.4 (ALU still too heavy)
  TheTom#9  Named-reg ternary:   10.3 (branches worst)
  TheTom#10 Main (8-LUT):        10.95 (baseline)
  TheTom#11 Non-vec FA:          10.2 (wrong kernel)
  Ceiling:                 24.5 (no dequant)

Apple8 hardware truth:
  1 divergent constant read < 7 ALU ops (even with fma)
  Branches cost MORE than divergent constant reads
  Array indexing ALWAYS spills on Metal
  4 constant addresses is the sweet spot

The 4-mag LUT is the dequant-level ceiling on Apple Silicon.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: tturney@psyguard.ai
TheTom added a commit that referenced this pull request Apr 15, 2026
Complete experiment log:
  #1  4-mag LUT:           15.1 at 8K (BEST, +38%)
  #2  Batched extract:     13.7 (+25%)
  #3  Inline FA block:     13.5 (I-cache pressure)
  #4  Deferred norm:       12.9 (loses ILP)
  #5  2-pair half2:        12.0 (ternary overhead)
  #6  Select chain:        11.9 (branches kill)
  #7  Bit-arithmetic:      11.6 (ALU too heavy)
  #8  FMA branchless:      11.4 (ALU still too heavy)
  #9  Named-reg ternary:   10.3 (branches worst)
  #10 Main (8-LUT):        10.95 (baseline)
  #11 Non-vec FA:          10.2 (wrong kernel)
  Ceiling:                 24.5 (no dequant)

Apple8 hardware truth:
  1 divergent constant read < 7 ALU ops (even with fma)
  Branches cost MORE than divergent constant reads
  Array indexing ALWAYS spills on Metal
  4 constant addresses is the sweet spot

The 4-mag LUT is the dequant-level ceiling on Apple Silicon.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: tturney@psyguard.ai
@TheTom TheTom force-pushed the feature/turboquant-kv-cache branch from 45f8a06 to 1073622 Compare April 16, 2026 01:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation ggml Nvidia GPU

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants