EMPIRICAL HARDWARE BENCHMARKS

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 Kernels 14.7× Peak speedup over pure MLX at S=8,192
Attention Prediction 100% Sign accuracy across all models tested
KIVI 4-Bit Quant <1.0 pt Perplexity delta from fp16 on 135M to 8B
RaBitQ Cache Footprint 5.95× Exact memory reduction on 7B models

Metal GPU Kernel Acceleration

Hand-written Metal Shading Language (MSL) kernels vs. pure MLX operations across sequence lengths on Apple Silicon unified memory.

KEY INSIGHT

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.

Execution Latency by Sequence Length (Lower is Better) Apple M3 / M4 Metal Engine
S = 128 6.91× faster
Pure MLX
3.64 ms
Metal MSL
0.53 ms
S = 512 10.70× faster
Pure MLX
13.47 ms
Metal MSL
1.26 ms
S = 1,024 12.09× faster
Pure MLX
26.97 ms
Metal MSL
2.23 ms
S = 2,048 13.17× faster
Pure MLX
55.10 ms
Metal MSL
4.18 ms
S = 4,096 13.63× faster
Pure MLX
108.82 ms
Metal MSL
7.98 ms
S = 8,192 14.68× faster
Pure MLX
228.56 ms
Metal MSL
15.57 ms

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.

Eviction Scoring: Python Loop vs. Fused Metal Kernel n_kept=512, D=128 · Apple M4
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.

KIVI Group-Affine Quantization: MLX vs. Fused Metal B=1 H=8 D=128 b=2 g=32 · Apple M4
S Scheme Speedup
32keys (per-channel)1.30×–1.35×
32values (per-token)1.47×–1.50×
512keys (per-channel)2.05×–2.12×
512values (per-token)1.65×–1.71×
2048keys (per-channel)4.30×–4.34× typical, up to 6.4× in one noisy run
2048values (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.

RVQ Quantize+Pack: MLX vs. Fused Metal D=128, bits=2 · Apple M4
N Speedup Fused GB/s (nominal)
641.34×–1.80×0.09–0.14 GB/s
1,0241.46×–1.81×1.14–2.26 GB/s
8,1922.38×–2.40× typical, one 1.31× low outlier2.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.

RESULT

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.

Complete SnapKV Cache Update (Lower is Better) B=1, H=8, D=128 · Apple M4
Candidate NMLXMetalMetal delta
5120.442 ms0.412 ms6.7% faster
2,0480.849 ms0.802 ms5.6% faster
8,1922.323 ms2.201 ms5.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.

Real Open-Model Validation Qwen3-8B · GLM-4-9B · 4-bit MLX
ModelScenarioMLX prefillMetal prefillParity
Qwen3-8Bbudget 64, chunk 1921,200 ms1,197 msExact
Qwen3-8Bbudget 64, chunk 481,966 ms1,816 msExact
GLM-4-9Bbudget 64, chunk 1921,813 ms1,952 msExact
GLM-4-9Bbudget 64, chunk 482,647 ms2,586 msExact

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.

RESULT

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.

Eviction Primitive: Selection and K/V/Score Compaction 8 heads · D=128 · 4 sinks
Retained budgetMLX median rangeMetal median range
4,0960.819–0.828 ms0.692–0.726 ms
8,1921.276–1.288 ms1.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.

Latest Warmed Model Runs 128 prompt tokens · chunk 64 · average budget 64
ModelBackendPrefill medianDecode median / step
Qwen3-8B-4bitReference8,131.4 ms173.64 ms
Qwen3-8B-4bitMLX3,346.7 ms88.96 ms
Qwen3-8B-4bitMetal3,382.6 ms86.36 ms
GLM-4-9B-0414-4bitReference3,676.2 ms92.16 ms
GLM-4-9B-0414-4bitMLX2,697.2 ms70.72 ms
GLM-4-9B-0414-4bitMetal2,387.6 ms71.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.

COUNTER-INTUITIVE RESULT

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.

% of Calibrated Bandwidth Peak, Largest Tested Size (Higher = Closer to Memory-Bound) Apple M4 · self-calibrated ~97–99 GB/s ceiling
kivi_group_quant_dequant 32×4096×128, group_size=32
102.2%
turboquant_scalar_dequantize N=16,000,000, b=2 (pure gather)
61.8%
turboquant_scalar_quantize N=16,000,000, b=4 (argmin, 16 centroids)
54.9%
scalar_fused_decode_attend S_kv=16,384, B=1 H=8 S_q=1 (realistic decode)
6.1%
Memory-bound (≥60% of peak) Occupancy-limited, trending toward memory-bound Occupancy/dispatch-bound — not a bandwidth problem
Occupancy Sweep — Same Kernel, Same S_kv, Only Threadgroup Count Changes scalar_fused_decode_attend, S_kv=16,384, D=128
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.

The Fix That Worked: Batch Concurrent Requests, Not Layers mlx-community/Qwen3-4B-4bit · 36 layers · real end-to-end decode
Concurrent requests (B) Baseline decode tok/s Fused-kernel decode tok/s Speedup
117.125.51.50×
425.348.11.90×
1634.3108.13.16×
3239.1149.93.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.

Also Measured, Not Adopted as the Default
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.

Reproduce the roofline measurements CLI
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.

RESULT — PREFILL ONLY

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.

GQA Head-Packing: Packed vs. Unpacked (Lower is Better) H_q=32, H_kv=4, heads_per_kv=8, D=128 · b=2, g=32, nsg=2
S_kvPackedUnpackedSpeedup
5123.792 ms26.464 ms6.98×
2,0489.385 ms70.351 ms7.50×
8,19235.169 ms333.665 ms9.49×
16,38471.276 ms646.657 ms9.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.

Two-Pass Decode-Once + Predecoded-Attend vs. Unpacked (issue #308) H_q=32, H_kv=4, heads_per_kv=8, D=128 · b=2, g=32, nsg=2
S_kvPackedUnpackedTwo-passTwo-pass vs. unpacked
5124.322 ms19.901 ms1.966 ms10.12×
2,0489.518 ms66.829 ms4.135 ms16.16×
8,19234.099 ms324.979 ms17.582 ms18.48×
16,38468.195 ms646.804 ms28.753 ms22.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.

CRITICAL RULE

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!

FILTER BY SCALE:
✓ Safe at 2-bit 7B Dense
Mistral-7B-v0.3
fp168.01
KIVI 4-bit7.96
KIVI 2-bit8.05
Throughput22.8 tok/s
✓ Safe at 2-bit 7B Dense
Qwen2.5-7B
fp16n/a
KIVI 4-bit9.30
KIVI 2-bit10.75
Throughput20.9 tok/s
✓ Safe at 2-bit 8B Dense
Llama-3.1-8B
fp1610.88
KIVI 4-bit11.02
KIVI 2-bit13.57
Throughput18.5 tok/s
✓ Safe at 2-bit 7B Dense
Falcon3-7B
fp1612.69
KIVI 4-bit12.63
KIVI 2-bit14.44
Throughput16.1 tok/s
✓ Safe at 2-bit 8B Dense
Qwen3-8B
fp16n/a
KIVI 4-bit9.92
KIVI 2-bit10.57
ThroughputValidated
✓ Safe at 2-bit MoE · MLA
DeepSeek-V2-Lite
fp16n/a
KIVI 4-bit15.34
KIVI 2-bit17.87
Throughput82.5 tok/s
✓ Safe at 4-bit 4B Dense
Gemma-3-4B
fp1616.00
KIVI 4-bit16.37
KIVI 2-bit17.72
ThroughputValidated
✓ Safe at 4-bit 4B Dense
Qwen3-4B
fp16n/a
KIVI 4-bit15.07
KIVI 2-bit17.21
ThroughputValidated
✓ Safe at 4-bit 3B Dense
Llama-3.2-3B
fp1615.47
KIVI 4-bit15.82
KIVI 2-bit20.93
Throughput46.2 tok/s
▲ Caution on 2-bit 1B Dense
Llama-3.2-1B
fp1622.77
KIVI 4-bit23.27
KIVI 2-bit81.30
Throughput119.7 tok/s
▲ Caution on 2-bit 0.5B Dense
Qwen2.5-0.5B
fp16n/a
KIVI 4-bit22.22
KIVI 2-bit49.08
Throughput244.3 tok/s
▲ Caution on 2-bit 135M Dense
SmolLM2-135M
fp1628.87
KIVI 4-bit29.63
KIVI 2-bit44.85
ThroughputValidated
View Complete Numerical Data Table (All 12 Models)
Model Architecture fp16 Perplexity KIVI 4-bit (4×) KIVI 2-bit (8×) Decode tok/s
SmolLM2-135M28.8729.6344.85—
Qwen2.5-0.5Bn/a†22.2249.08244.3 tok/s
Llama-3.2-1B22.7723.2781.30119.7 tok/s
gemma-3-4B16.0016.3717.72—
Qwen3-4Bn/a†15.0717.21—
Llama-3.2-3B15.4715.8220.9346.2 tok/s
Mistral-7B-v0.38.017.968.0522.8 tok/s
Qwen2.5-7Bn/a†9.3010.7520.9 tok/s
Llama-3.1-8B10.8811.0213.5718.5 tok/s
Falcon3-7B12.6912.6314.4416.1 tok/s
Qwen3-8Bn/a†9.9210.57—
DeepSeek-V2-Lite (MoE·MLA)n/a†15.3417.8782.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.

PERFECT PREDICTION

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).

Q-Filters (Query-SVD Calibrated)
Naive K-Norm / L2Norm Baseline
Llama-3.2-1B
Q-Filters
+0.783 (100% sign)
K-norm
+0.460
Llama-3.2-3B
Q-Filters
+0.863 (100% sign)
K-norm
+0.410
Qwen2.5-7B
Q-Filters
+0.850 (100% sign)
K-norm
+0.402

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.

52%
Budget 256 (~4×)
8.48 vs 13.36 ppl
37%
Budget 128 (~8×)
16.31 vs 23.65 ppl
20%
Budget 64 (~16×)
25.93 vs 31.27 ppl

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×
Reproduce these benchmarks on your Mac CLI
pip install veloxquant-mlx
veloxquant benchmark --all