First MLX Implementation of TurboQuant KV Cache Compression
TurboQuant achieves 4.6x KV cache compression with ~0% accuracy loss on Apple Silicon.
What this actually means: KV cache compression helps the decode phase (token generation), not prefill. Peak VRAM during prefill is still determined by prompt length. What you gain is: faster decode bandwidth, longer effective KV cache during generation, and more concurrent decode sessions. It does not let you run larger models than your VRAM can fit.
This enables running longer generation contexts and more concurrent sessions on M-series Macs β including multi-node distributed inference via exo.
39 passed, 2 skipped in 1.79s
TestWHT 7/7 β
Walsh-Hadamard orthogonality, norm preservation, invertibility
TestPolarQuant 5/5 β
Quantize/dequantize roundtrip, compression ratio, shapes
TestQJL 4/4 β
Sketch accuracy, inner product estimation
TestKVCache 6/6 β
Attention sinks, chunk buffering, memory tracking
TestAsymmetric 4/4 β
Keys=TurboQuant, Values=PolarQuant (asymmetric)
TestOllama 5/5 β
Client instantiation, stats tracking, env patching
TestHFIntegration 5/5 β
DynamicCache compat, from_legacy_cache, update()
TestLazyImports 3/3 β
Lazy import safety for all optional backends
Replaced O(nΒ²) Gram-Schmidt orthogonalization with O(n log n) fast Walsh-Hadamard Transform (WHT). Same rotation Gaussianization quality, ~4x faster. Implemented in pure MLX.
# turboquant_mlx/wht.py β SRHT: D @ H @ D
from turboquant_mlx.wht import WalshHadamardRotation
rotation = WalshHadamardRotation(head_dim=128, seed=42)
x_rotated = rotation.rotate(x) # O(n log n)
x_back = rotation.rotate_inverse(x_rotated)Keys use full TurboQuant (PolarQuant + QJL). Values use PolarQuant only β QJL corrects inner product bias for the QΒ·K dot product, making it mathematically redundant for V. Lower MSE on value reconstruction.
First 128 tokens kept in float16. Prevents instruction-following degradation at extreme compression ratios (3-bit). Zero noticeable memory overhead.
Tokens accumulated in 64-token chunks before compression fires. Reduces per-token overhead during autoregressive decode.
TurboQuant-MLX is designed around MLX's unified memory model on Apple Silicon. Key notes:
- GPU (Metal): All compression operations run on the GPU via MLX array ops β fully accelerated
- ANE: MLX does not currently expose the ANE directly; operations fall back to GPU/CPU. Apple's ANE is used automatically for Core ML and certain system frameworks, not raw MLX ops
- M-series optimization: The Walsh-Hadamard butterfly operations and polar coordinate transforms are vectorized for the GPU SIMD units present in all M-series chips
- Unified memory: No hostβdevice transfer cost β KV cache lives in shared memory accessible by both CPU and GPU
For ANE-native inference, use Core ML conversion after quantization (future roadmap).
Drop-in patch for any mlx-lm model β including distributed multi-node inference via the Star Platinum cluster:
# Monkey-patch mlx-lm to use TurboQuant
from turboquant_mlx.mlx_kvcache import TurboQuantKVCache
import mlx_lm.models.cache as cache_module
def turboquant_make_prompt_cache(model, max_kv_size=None):
num_layers = len(model.layers)
return [TurboQuantKVCache(r_bits=4, theta_bits=4) for _ in range(num_layers)]
cache_module.make_prompt_cache = turboquant_make_prompt_cache
# All subsequent mlx-lm inference uses TurboQuant KV compressionOr use the included patch script:
python3 patch_exo.py # patches the exo distributed inference clusterfrom turboquant_mlx.hf_patch import load_and_patch
model, tokenizer = load_and_patch("Qwen/Qwen2.5-7B-Instruct")
# model.generate() now uses asymmetric TurboQuant KV compression automatically
inputs = tokenizer("Hello, world!", return_tensors="pt")
outputs = model.generate(**inputs, max_new_tokens=100)Or apply manually:
from turboquant_mlx.hf_patch import TurboQuantHFCache, patch_transformers
patch_transformers() # monkey-patches AutoModelForCausalLM.generate globallyNote: HuggingFace integration requires
transformersandtorch. TurboQuant uses lazy imports β if these aren't installed, importingturboquant_mlxstill works, the HF backend is simply unavailable.
Note: Ollama manages its own internal KV cache (via llama.cpp). The
TurboQuantOllamaClientis a monitoring/stats wrapper and consistent API layer β it does not inject compression into Ollama's internal cache. True KV compression with Ollama requires the llama.cpp backend.
from turboquant_mlx.ollama_patch import TurboQuantOllamaClient, patch_ollama_env
# Optimize Ollama environment settings
patch_ollama_env()
# Wrap the Ollama API with stats tracking
client = TurboQuantOllamaClient(base_url="http://localhost:11434/v1")
response = client.chat(
model="qwen2.5:7b",
messages=[{"role": "user", "content": "What is distributed inference?"}]
)
print(response)
print(client.stats()) # estimated memory savings
client.reset_stats()A C port with Metal GPU kernels is on the roadmap. Once available:
./llama-server -m model.gguf --cache-type-k turbo3 --cache-type-v turbo3TurboQuant-MLX ships as a first-class OpenClaw skill. Install it to bring TurboQuant compression awareness into your AI agent:
openclaw skills install turboquant-mlxThe skill enables your agent to:
- Monitor KV cache compression stats across all backends
- Patch local inference runtimes (mlx-lm, exo) on demand
- Report memory savings in real-time during generation
Input KV vector x β R^d
β
βββ Walsh-Hadamard Rotation (SRHT: D @ H @ D) β O(n log n)
β Gaussianizes distribution: kurtosis 900 β ~3.0
β
βββ PolarQuant (Keys + Values)
β x' β (radius, angle) β quantize independently
β 4-bit r + 4-bit ΞΈ = 8 bits total per dimension pair
β No per-block normalization constants
β
βββ QJL Residual Correction (Keys only β asymmetric)
β sign(S Β· residual) β 1-bit inner product correction
β Mathematically redundant for Values (no QΒ·V dot product)
β
βββ CompressedKV: 4.6x smaller, fp16 sinks preserved
| Component | Detail |
|---|---|
| Rotation | Randomized SRHT (D@H@D) β pure MLX, O(n log n) |
| Keys | PolarQuant + QJL (full TurboQuant) |
| Values | PolarQuant only (asymmetric β mathematically correct) |
| Attention sinks | First 128 tokens in fp16 β preserves instruction following |
| Chunk buffer | 64 tokens staged before compression β reduces decode overhead |
| Group size | 128 vectors per quantization group |
git clone https://github.com/DeadByDawn101/turboquant-mlx.git
cd turboquant-mlx
pip install mlx numpy
pip install -e .
# Optional backends
pip install transformers torch accelerate # HuggingFace
pip install openai # Ollama wrapperimport mlx.core as mx
from turboquant_mlx import TurboQuantKVCache
# Create cache (drop-in for mlx-lm KVCache)
cache = TurboQuantKVCache(
r_bits=4, # radius quantization bits
theta_bits=4, # angle quantization bits
fp16_sink_size=128, # protect first N tokens
chunk_size=64, # buffer before compressing
)
# Use exactly like mlx-lm KVCache
keys = mx.random.normal(shape=(1, 8, 32, 64))
values = mx.random.normal(shape=(1, 8, 32, 64))
k_out, v_out = cache.update_and_fetch(keys, values)
print(f"Cache offset: {cache.offset}")
print(f"Memory: {cache.memory_size / 1024:.1f} KB")pip install pytest
python3 -m pytest tests/ -v
# 39 passed, 2 skipped| Config | Compression | Cosine Sim | MSE |
|---|---|---|---|
| TurboQuant 2-bit | 7.1Γ | 0.79 | 0.0047 |
| TurboQuant 3-bit | 4.9Γ | 0.91 | 0.0018 |
| TurboQuant 4-bit (default) | 3.8Γ | 0.96 | 0.0007 |
Default 4-bit config gives 3.8x compression with 0.96 cosine similarity β effectively lossless for most tasks.
Process a codebase or long document once. Resume instantly next session.
from turboquant_mlx.persistence import TurboQuantCache
cache = TurboQuantCache(bits=4) # 4x compression
# After processing (once):
cache.save(kv_states, "my-project", metadata={"tokens": 4096, "model": "Qwen3.5-35B"})
# Saved: 26.6 MB β 6.7 MB (4x), 0.002s
# Next session (instant):
kv_states, meta = cache.load("my-project")
# Loaded in 0.0003s vs 1.01s reprocessing
# Cross-device sync via Cloudflare R2:
cache.push("my-project") # upload (free tier: 10GB)
cache.pull("my-project") # download on other MacWhat 0.0003s load time means in practice:
- Loading a 4096-token context from disk: 0.3ms
- Reprocessing 4096 tokens through Qwen3.5-35B: 1.01s
- Speedup: ~3,300x faster context restoration
Process documents larger than GPU memory:
from turboquant_mlx.persistence import PagedKVCache
paged = PagedKVCache(max_gpu_chunks=4, chunk_size=512)
# Process in chunks β GPU holds recent 4, rest on SSD
for chunk_id, kv_chunk in enumerate(kv_chunks):
paged.add_chunk(kv_chunk, chunk_id)
print(paged.stats)
# {"gpu_chunks": 4, "ssd_chunks": 12, "gpu_hits": 89, "ssd_reads": 11}from turboquant_mlx.tiered_cache import TieredKVCacheManager
manager = TieredKVCacheManager(
max_gpu_mb=2000, # 2GB in GPU
max_ssd_mb=50000, # 50GB on SSD
r2_config={...}, # Cloudflare R2 for cold storage
)
# Store KV state β auto-tiers based on size/recency
manager.put("my-project", kv_states, metadata={"tokens": 4096})
# Retrieve β auto-promotes from lower tiers
states, tier = manager.get("my-project")
print(f"Retrieved from {tier}") # "gpu" | "ssd" | "r2"
# Check tier utilization
print(manager.stats())
# {"gpu_mb": 1.2, "ssd_mb": 15.3, "gpu_hits": 42, "ssd_hits": 8, "r2_hits": 1}Tier access times:
- GPU: instant (~0ms)
- SSD: ~0.3ms (compressed TurboQuant load)
- R2: ~1.5s (network, but cross-device)
Inspired by Apple's "LLM in a Flash" research + mac-code.
On March 26, 2026, the authors of the RaBitQ line of work (SIGMOD 2024, SIGMOD 2025) posted a public comment on OpenReview raising three specific concerns about the TurboQuant ICLR 2026 paper:
-
Method misrepresentation β TurboQuant describes random rotation as its key innovation while characterizing RaBitQ as a simple grid-based PQ method, omitting that RaBitQ also applies a Johnson-Lindenstrauss (random rotation) transform. Multiple reviewers flagged this; the authors responded by moving the RaBitQ description to the appendix rather than acknowledging the structural similarity.
-
Unsupported theoretical claim β TurboQuant calls RaBitQ's guarantees "suboptimal due to loose analysis." The RaBitQ SIGMOD 2025 paper (posted Sept 2024, before TurboQuant submission) already proves asymptotic optimality, matching the Alon-Klartag lower bound β the theoretical ceiling. This correction was communicated privately in May 2025 and not incorporated.
-
Undisclosed benchmark conditions β The paper's runtime/efficiency comparisons run the RaBitQ baseline on a single CPU with multiprocessing disabled while running TurboQuant on an A100 GPU. This was never disclosed. The RaBitQ authors note that TurboQuant's second author (Majid Daliri) contacted them in January 2025 to debug his own Python translation of their implementation.
What this means for this implementation:
The core algorithm in this repository is still mathematically sound β random rotation + scalar quantization + residual correction is a valid and effective approach. However:
- The relationship between TurboQuant and RaBitQ is much closer than the TurboQuant paper suggests. Both share the fundamental insight of applying a JL-type rotation before scalar quantization.
- The "beating RaBitQ" benchmarks in the paper should not be taken at face value.
- Our
qjl.pyimplements the two-stage residual approach from TurboQuant. As an alternative, we also providerabitq_correction()β the simpler RaBitQ-style(Ο/2)scaling bias correction, which is theoretically equivalent for bias removal but trades variance increase for implementation simplicity. See RaBitQ vs QJL correction below.
We recommend reading both the TurboQuant paper and the RaBitQ SIGMOD 2025 paper for a complete picture.
The inner product bias from 1-bit sign quantization (factor of 2/Ο) can be corrected two ways:
from turboquant_mlx.qjl import rabitq_correction, QJLSketch
# Option A: RaBitQ-style β multiply by Ο/2 (simple, ~6% more variance)
corrected = rabitq_correction(signs, scale_x, scale_y, sketch_dim)
# Option B: TurboQuant-style β QJL residual on the remainder (lower variance, more memory)
sketch = QJLSketch(head_dim, sketch_dim)
signs, scale = sketch.sketch(keys)Both are unbiased estimators. TurboQuant's residual approach has lower variance (better MSE) at the cost of extra computation. RaBitQ's scaling is simpler and zero overhead β ideal if memory is the binding constraint.
- Papers: TurboQuant (ICLR 2026) Β· RaBitQ (SIGMOD 2025) Β· PolarQuant Β· QJL
- Optimizations: Asymmetric K/V compression, FP16 attention sinks, chunk buffering β inspired by helgklaizar/turboquant_mlx
- Built by: RavenX AI / DeadByDawn101
- Cluster: Tested on Star Platinum β 4-node Apple Silicon TB4 ring (M4 Max + M3 + M2 Pro + M1 Pro)
- Persistent KV cache save/load (0.0003s load vs reprocessing)
- SSD paging for context beyond GPU memory ("LLM in a Flash")
- Three-tier caching: GPU β SSD β Cloudflare R2
- llama.cpp C port with Metal GPU kernels (
--cache-type-k turbo3) - ANE-native path via Core ML conversion
- Benchmark suite with PPL scores (wikitext-2)
- Adaptive bit allocation (per-layer sensitivity)
- Temporal decay compression for sliding window contexts
Built with π€ by RavenX AI
This repository is a research and experimentation layer, not a production-faithful implementation of the TurboQuant paper. Known gaps:
| Component | Status | Notes |
|---|---|---|
mlx_kvcache.py |
Partial | PolarQuant applied β , QJL residual NOT applied in live path β |
hf_patch.py |
Simulation | Perturbs HF KV tensors, doesn't change attention kernel β |
| Compressed-domain attention | Not implemented | Requires custom Metal kernel β the key missing piece |
QJL correction in qjl.py |
Math correct β | But used in testing only, not wired into live cache path |
What works today:
- Compression/decompression roundtrip with PolarQuant + WHT rotation β
- Drop-in mlx-lm cache interface β
- All 75 tests passing β
- RaBitQ-style
Ο/2scaling correction (rabitq_correction()) as documented alternative β
What's needed for paper fidelity:
- Apply QJL signs in compressed domain (don't decompress keys per step)
- Compute
score β (Ο/2) Γ scale_q Γ scale_k Γ sign_agreementdirectly - Custom Metal kernel or MLX custom op for compressed attention
PRs welcome. See issue #2 for full technical discussion.