Measured on Apple Silicon.
Verified across 12 models.
Real causal-LM generation throughput, attention correlation accuracy, and perplexity measurements on Apple Silicon hardware (M1 through M4). Zero synthetic projections — every result below is measured and reproducible, not estimated.
Metal GPU Kernel Acceleration
Hand-written Metal Shading Language (MSL) kernels vs. pure MLX operations across sequence lengths on Apple Silicon unified memory.
Metal threadgroup caching scales with context length: While unquantized attention stalls memory bandwidth on long sequences, our fused Metal kernels achieve 6.91× speedup at 128 tokens and scale up to 14.68× speedup at 8,192 tokens.
Eviction & Quantization Kernels vs. Pure MLX
Fused Metal kernels for the H2O / Keyformer eviction scoring loop and the KIVI / RVQ quantize+pack path, measured against the equivalent pure-MLX implementation on the same Apple M4. Each speedup is a range across repeated runs (median of interleaved samples per run) rather than a single reading — these are small, fast kernels where run-to-run scheduling noise is visible at larger sizes.
| Kernel | Python loop | Fused Metal | Speedup |
|---|---|---|---|
| H2O fused eviction | ~0.60–0.69 ms/call | ~0.28–0.42 ms/call | 1.65×–2.42× |
| Keyformer fused eviction | ~0.72–0.89 ms/call | ~0.28–0.33 ms/call | 2.45×–2.69× |
Reproduce: pytest veloxquant_mlx/tests/metal/test_h2o_evict.py veloxquant_mlx/tests/metal/test_keyformer_evict.py -k benchmark -s. Both eviction paths replace a per-token Python score-and-compact loop with a single fused dispatch.
| S | Scheme | Speedup |
|---|---|---|
| 32 | keys (per-channel) | 1.30×–1.35× |
| 32 | values (per-token) | 1.47×–1.50× |
| 512 | keys (per-channel) | 2.05×–2.12× |
| 512 | values (per-token) | 1.65×–1.71× |
| 2048 | keys (per-channel) | 4.30×–4.34× typical, up to 6.4× in one noisy run |
| 2048 | values (per-token) | 2.85×–3.66× typical, up to 4.9× in one noisy run |
Medians of interleaved samples over a rotating input pool. The win grows with block size — these kernels replace several full-size MLX intermediates with one pass, so they scale with bytes moved. Prefill (one large group-aligned flush) is where this matters; decode flushes only group_size tokens at a time. Reproduce: pytest veloxquant_mlx/tests/metal/test_kivi_quant.py -k benchmark -s.
| N | Speedup | Fused GB/s (nominal) |
|---|---|---|
| 64 | 1.34×–1.80× | 0.09–0.14 GB/s |
| 1,024 | 1.46×–1.81× | 1.14–2.26 GB/s |
| 8,192 | 2.38×–2.40× typical, one 1.31× low outlier | 2.65–7.18 GB/s |
GB/s is nominal traffic (input read once + packed streams written), not a hardware counter; the MLX baseline additionally charges the two full-size uint8 index intermediates the fused kernel never materializes. Reproduce: pytest veloxquant_mlx/tests/metal/test_rvq_quant_pack.py -k benchmark -s.
SnapKV: MLX vs. Metal Selection and Gather
Exact sink-protected selection, temporal ordering, and K/V gathering on an Apple M4. The Metal route uses threshold/tie compaction plus a combined K/V gather; scoring and threshold calculation remain in MLX for numerical parity.
Metal wins the cache microbenchmark by 5–7% after fusion, while selection alone is 31–39% faster. Real-model results are mixed, so MLX remains the automatic backend. Preserving native BF16 K/V dtype removed a separate Qwen decode regression.
| Candidate N | MLX | Metal | Metal delta |
|---|---|---|---|
| 512 | 0.442 ms | 0.412 ms | 6.7% faster |
| 2,048 | 0.849 ms | 0.802 ms | 5.6% faster |
| 8,192 | 2.323 ms | 2.201 ms | 5.3% faster |
Synchronized median timings from 20 measured repetitions after five warmups. This is a synthetic complete-cache update, not end-to-end generation throughput. Selection-only Metal medians were 0.246 / 0.249 / 0.783 ms versus MLX 0.390 / 0.410 / 1.140 ms at N=512 / 2,048 / 8,192. Raw data: snapkv_selection_after_fused.json.
| Model | Scenario | MLX prefill | Metal prefill | Parity |
|---|---|---|---|---|
| Qwen3-8B | budget 64, chunk 192 | 1,200 ms | 1,197 ms | Exact |
| Qwen3-8B | budget 64, chunk 48 | 1,966 ms | 1,816 ms | Exact |
| GLM-4-9B | budget 64, chunk 192 | 1,813 ms | 1,952 ms | Exact |
| GLM-4-9B | budget 64, chunk 48 | 2,647 ms | 2,586 ms | Exact |
One warmup plus three measured trials per route in the post-BF16-fix follow-up. “Exact” means zero measured first-prefill logit error against the SnapKV reference and identical greedy tokens. Metal is mixed at model level and remains opt-in. Full methodology and raw runs: SNAPKV_8B_9B_MODEL_RESULTS.md.
PyramidKV: Faster Compaction, Mixed Model Results
Apple M4 · FP16 cache K/V, FP32 scores · batched append and eviction with reference per-head scoring. Lower latency is better.
Contiguous element copying reverses the large-budget Metal regression. Model results remain mixed: Qwen decode improves about 3%, while GLM prefill improves about 11%. Metal remains explicitly selectable; the reference default is unchanged and auto uses MLX.
| Retained budget | MLX median range | Metal median range |
|---|---|---|
| 4,096 | 0.819–0.828 ms | 0.692–0.726 ms |
| 8,192 | 1.276–1.288 ms | 1.033–1.042 ms |
Ranges span three run medians, each with 10 warmups and 100 alternating-order trials. Synchronized host-wall timing includes dispatch and evaluates all outputs. Resident random finite scores; exact output parity passed. Scoring, append, and model execution are excluded. These ranges are not confidence intervals.
| Model | Backend | Prefill median | Decode median / step |
|---|---|---|---|
| Qwen3-8B-4bit | Reference | 8,131.4 ms | 173.64 ms |
| Qwen3-8B-4bit | MLX | 3,346.7 ms | 88.96 ms |
| Qwen3-8B-4bit | Metal | 3,382.6 ms | 86.36 ms |
| GLM-4-9B-0414-4bit | Reference | 3,676.2 ms | 92.16 ms |
| GLM-4-9B-0414-4bit | MLX | 2,697.2 ms | 70.72 ms |
| GLM-4-9B-0414-4bit | Metal | 2,387.6 ms | 71.51 ms |
One warmup and three measured trials per backend, fresh caches, alternating backend order, and 16 greedy decode steps. Timings include Python dispatch and decode token reads. All accelerated runs matched reference final-prefill logits exactly and generated identical tokens. Metal invocation counts confirmed execution. This single repeated-prompt workload establishes backend parity, not general model quality or long-context mask correctness. Qwen Metal prefill was about 1% slower; GLM Metal decode was about 1% slower.
Reproduction, exact model revisions, validation, and limitations: PyramidKV findings. Raw latest runs: Qwen JSON · GLM JSON.
KV-Cache Kernel Roofline: What Actually Limits Decode
A self-calibrated bandwidth roofline (not spec-sheet numbers) across all four KV-cache quantize/dequantize/fused-attend Metal kernels — two are already within a few percent of this hardware's measured bandwidth ceiling, and one never becomes bandwidth-bound at all for a different, more interesting reason.
The fused decode+attend kernel's bottleneck isn't memory bandwidth — it's GPU occupancy. At a realistic single-token decode shape it tops out at just 6.1% of peak bandwidth, even at 16,384 cached tokens, because too few threadgroups are dispatched to fill a 10-core GPU. Routing multiple concurrent requests through that same kernel — verified end-to-end on a real model — measured a real 1.50×–3.83× decode speedup that grows with batch size.
| Dispatch shape | Threadgroups | % of peak bandwidth |
|---|---|---|
| B=1, H=8, S_q=1 (realistic single request) | 8 | 6.1% |
| B=1, H=32, S_q=1 | 32 | 21.8% |
| B=4, H=32, S_q=1 | 128 | 36.7% |
Bandwidth utilization scales with threadgroup count, not with S_kv or bytes moved per threadgroup — direct evidence this kernel is occupancy/dispatch-bound, not memory- or compute-bound, at the shapes that matter for real single-token decode.
| Concurrent requests (B) | Baseline decode tok/s | Fused-kernel decode tok/s | Speedup |
|---|---|---|---|
| 1 | 17.1 | 25.5 | 1.50× |
| 4 | 25.3 | 48.1 | 1.90× |
| 16 | 34.3 | 108.1 | 3.16× |
| 32 | 39.1 | 149.9 | 3.83× |
Real forward passes on real hardware (Apple M4, prompt_len=256, 30 decode steps) — not an isolated kernel call. Both arms decode from the identical quantized KV state and were verified to produce bit-identical tokens before timing was trusted, so this measures the fused kernel's speedup over dequantize+MLX-SDPA on the same compressed state — not a general "KIVI is faster in production" claim. The test harness behind this table also has no fp16 residual window (unlike shipping KIVI), so its generation quality isn't production-representative; only the timing comparison is.
| Approach | Result | Disposition |
|---|---|---|
| GQA head-packing (share K/V decode across query heads) | Depends on heads_per_kv and S_kv — re-measured after a later occupancy fix changed the picture. 0.4×–0.9× (slower) at heads_per_kv=4-8 and short context, but 1.1×–1.7× faster at heads_per_kv=4 with few kv-heads or long context. |
No longer a uniform rejection, but still situational and not adopted as the default — see the doc for the full per-ratio breakdown. |
| Two-pass "decode-once" (separate decode + attend kernels) | 1.1×–2.4× faster at short context, 0.6×–0.9× (slower) at long context — exact crossover point shifts per head ratio (S_kv≈1.5k–8k) | Real crossover, but this project's realistic decode targets sit past it — not adopted as the default. |
| Cross-layer batched dispatch | 1.0×–4.5× faster in isolation | Structural dead end for real single-request serving — a standard transformer's residual stream makes layer L+1's attention depend on layer L's full output, so layers can't be batched together without changing the model. |
The head-packing numbers above were revised after this table's original measurement (before issue #317's occupancy fix changed the packed kernel's threadgroup-memory footprint) turned out to be superseded, not simply wrong — full re-measurement and methodology → re-measured after nsg autotune addendum.
python scripts/kv_kernel_roofline_bench.py
Full methodology, self-calibration approach, and all four follow-up experiments (including the ones summarized above) → KV_KERNEL_ROOFLINE_FINDINGS.md on GitHub.
GQA Head-Packing at Prefill: A Different Regime from Decode
Multi-query (prefill-shaped) grouped-query attention, distinct from the single-token decode kernel benchmarked above — B=1, H_q=32, H_kv=4, heads_per_kv=8, D=128 on Apple Silicon, MLX 0.32.2. Unpacked broadcasts each shared KV head out to every query head before attention runs; packed reads each KV head once per query group; two-pass splits the computation into a decode-once pass and a predecoded-attend pass to remove redundant work packing still leaves behind.
At prefill shapes, two-pass wins outright and the margin grows with context — 10.1× over unpacked at S_kv=512, climbing to 22.5× at S_kv=16,384. This is the opposite trend from the single-token decode kernel in the section above, where two-pass is situational and loses at long context for most head ratios — decode and prefill dispatch different threadgroup counts for the same S_kv, so the same technique lands on different sides of its occupancy crossover.
| S_kv | Packed | Unpacked | Speedup |
|---|---|---|---|
| 512 | 3.792 ms | 26.464 ms | 6.98× |
| 2,048 | 9.385 ms | 70.351 ms | 7.50× |
| 8,192 | 35.169 ms | 333.665 ms | 9.49× |
| 16,384 | 71.276 ms | 646.657 ms | 9.07× |
At this shape and ratio (heads_per_kv=8), packing alone already removes most of the redundant KV re-decode unpacked pays for at every S_kv tested.
| S_kv | Packed | Unpacked | Two-pass | Two-pass vs. unpacked |
|---|---|---|---|---|
| 512 | 4.322 ms | 19.901 ms | 1.966 ms | 10.12× |
| 2,048 | 9.518 ms | 66.829 ms | 4.135 ms | 16.16× |
| 8,192 | 34.099 ms | 324.979 ms | 17.582 ms | 18.48× |
| 16,384 | 68.195 ms | 646.804 ms | 28.753 ms | 22.50× |
The two-pass margin over unpacked widens monotonically with S_kv at this prefill shape — the reverse of the decode-shape crossover in the Decode Occupancy section, where two-pass's advantage shrinks and flips negative past a few thousand cached tokens for the same heads_per_kv=8 ratio. The two kernels dispatch different threadgroup counts for the same S_kv (one query token vs. many), which is why the same code change lands on opposite sides of its occupancy crossover depending on whether it runs during prefill or decode. These figures characterize the prefill path only; do not read them as superseding the decode-step verdict in KV_KERNEL_ROOFLINE_FINDINGS.md.
12-Model Quality Matrix (KIVI 4-Bit vs. 2-Bit)
Long-context causal generation (~430–500 tokens, well beyond the 128-token residual cache window) tested across 12 models spanning 135M to 8B parameters.
4-bit is safe across every model tested (perplexity within ~1 point of fp16). 2-bit provides an 8× compression win on 7B–8B models, but causes significant degradation under ~3B (Llama-3.2-1B perplexity triples from 22.8 → 81.3). Choose bit-width based on parameter size!
View Complete Numerical Data Table (All 12 Models)
| Model Architecture | fp16 Perplexity | KIVI 4-bit (4×) | KIVI 2-bit (8×) | Decode tok/s |
|---|---|---|---|---|
| SmolLM2-135M | 28.87 | 29.63 | 44.85 | — |
| Qwen2.5-0.5B | n/a† | 22.22 | 49.08 | 244.3 tok/s |
| Llama-3.2-1B | 22.77 | 23.27 | 81.30 | 119.7 tok/s |
| gemma-3-4B | 16.00 | 16.37 | 17.72 | — |
| Qwen3-4B | n/a† | 15.07 | 17.21 | — |
| Llama-3.2-3B | 15.47 | 15.82 | 20.93 | 46.2 tok/s |
| Mistral-7B-v0.3 | 8.01 | 7.96 | 8.05 | 22.8 tok/s |
| Qwen2.5-7B | n/a† | 9.30 | 10.75 | 20.9 tok/s |
| Llama-3.1-8B | 10.88 | 11.02 | 13.57 | 18.5 tok/s |
| Falcon3-7B | 12.69 | 12.63 | 14.44 | 16.1 tok/s |
| Qwen3-8B | n/a† | 9.92 | 10.57 | — |
| DeepSeek-V2-Lite (MoE·MLA) | n/a† | 15.34 | 17.87 | 82.5 tok/s |
Attention Scorer Correlation (Q-Filters)
Spearman rank-order correlation with genuine model attention weights. Tests whether the scorer correctly identifies which tokens are critical to retain.
100% sign-accuracy across all models tested. Q-Filters achieves roughly 2× the correlation of naive K-norm / L2Norm baselines (+0.863 vs +0.410 on Llama-3.2-3B).
Generation Perplexity & Quality Gap Closed
Percentage of the fp16 degradation gap closed by Q-Filters calibration relative to naive eviction at varying memory compression budgets.
Measurements on Llama-3.2-1B (fp16 baseline 4.050 ppl). Llama-3.2-3B closes 55% / 42% at Budgets 256 / 128.
Real KV Cache Memory Scaling (RaBitQ on Falcon3-7B)
Measured RAM footprint of the KV cache across context lengths from 256 to 4,096 tokens.
| Sequence Length | Standard fp16 Cache | RaBitQ MSE-4V Cache | Memory Saved | Compression Ratio |
|---|---|---|---|---|
| 256 tokens | 29.4 MB | 4.9 MB | 24.5 MB freed | 5.95× |
| 512 tokens | 58.7 MB | 9.9 MB | 48.8 MB freed | 5.95× |
| 1,024 tokens | 117.4 MB | 19.7 MB | 97.7 MB freed | 5.95× |
| 2,048 tokens | 234.9 MB | 39.5 MB | 195.4 MB freed | 5.95× |
| 4,096 tokens | 469.8 MB | 78.9 MB | 390.9 MB freed | 5.95× |
pip install veloxquant-mlx
veloxquant benchmark --all