dbf85683e6
KV cache compression for LLM inference (ICLR 2026, arXiv:2504.19874). Core: - TurboQuantProd: 3-bit keys (MSE + QJL), 2-bit/4-bit values (group quant) - Modular architecture: capture, store, score, integration/vllm - vLLM monkey-patch with free_kv_cache and hybrid decode - 3 fused Triton kernels for decode attention Validated on: - RTX 5090: Qwen3.5-27B-AWQ, 30GB KV freed, 2x context capacity - 8x RTX 3090: Qwen3.5-35B-A3B MoE at 131k context - 8,238 tok/s prefill, 98 tok/s decode, 15.9s TTFT - 30.9% KV savings (4.4x on full-attn layers, 1.45x overall) - 5/5 needle retrieval at max context 35 tests pass (19 modular + 7 core + 9 paper validation). Adversarial audit included with honest assessment of all claims.
299 lines
17 KiB
HTML
299 lines
17 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>TurboQuant — How It Works</title>
|
|
<style>
|
|
:root { --bg: #0f0f1a; --card: #1a1a2e; --accent: #4fc3f7; --green: #66bb6a; --red: #ef5350; --yellow: #ffd54f; --text: #e0e0e0; --dim: #888; }
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; background: var(--bg); color: var(--text); line-height: 1.6; }
|
|
.container { max-width: 960px; margin: 0 auto; padding: 40px 24px; }
|
|
h1 { font-size: 2.8rem; text-align: center; margin-bottom: 8px; }
|
|
h1 span { color: var(--accent); }
|
|
.subtitle { text-align: center; color: var(--dim); font-size: 1.1rem; margin-bottom: 48px; }
|
|
h2 { font-size: 1.6rem; margin: 48px 0 20px; padding-bottom: 8px; border-bottom: 2px solid #333; }
|
|
h3 { font-size: 1.15rem; color: var(--accent); margin: 16px 0 8px; }
|
|
p, li { font-size: 0.95rem; }
|
|
ul { padding-left: 20px; margin: 8px 0; }
|
|
li { margin: 4px 0; }
|
|
|
|
.hero-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin: 32px 0; }
|
|
.stat { background: var(--card); border-radius: 12px; padding: 24px; text-align: center; border: 1px solid #333; }
|
|
.stat .num { font-size: 2.4rem; font-weight: 800; }
|
|
.stat .label { font-size: 0.85rem; color: var(--dim); margin-top: 4px; }
|
|
.stat.green .num { color: var(--green); }
|
|
.stat.red .num { color: var(--red); }
|
|
.stat.blue .num { color: var(--accent); }
|
|
|
|
.flow { display: flex; align-items: stretch; gap: 0; margin: 24px 0; flex-wrap: wrap; justify-content: center; }
|
|
.flow-step { background: var(--card); border: 1px solid #333; border-radius: 10px; padding: 16px; flex: 1; min-width: 140px; max-width: 200px; text-align: center; position: relative; }
|
|
.flow-step .icon { font-size: 1.8rem; margin-bottom: 6px; }
|
|
.flow-step .title { font-weight: 700; font-size: 0.9rem; }
|
|
.flow-step .desc { font-size: 0.78rem; color: var(--dim); margin-top: 4px; }
|
|
.flow-arrow { display: flex; align-items: center; font-size: 1.4rem; color: var(--dim); padding: 0 6px; }
|
|
|
|
.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; margin: 16px 0; }
|
|
@media (max-width: 700px) { .two-col { grid-template-columns: 1fr; } .hero-stats { grid-template-columns: 1fr; } }
|
|
|
|
.card { background: var(--card); border: 1px solid #333; border-radius: 10px; padding: 20px; }
|
|
.card h3 { margin-top: 0; }
|
|
|
|
.diagram { background: #111; border: 1px solid #333; border-radius: 10px; padding: 24px; margin: 20px 0; font-family: 'Courier New', monospace; font-size: 0.82rem; line-height: 1.8; overflow-x: auto; white-space: pre; color: #ccc; }
|
|
.diagram .hl { color: var(--accent); font-weight: 700; }
|
|
.diagram .gr { color: var(--green); }
|
|
.diagram .rd { color: var(--red); }
|
|
.diagram .yw { color: var(--yellow); }
|
|
|
|
table { width: 100%; border-collapse: collapse; margin: 16px 0; font-size: 0.9rem; }
|
|
th { background: #222; padding: 10px 12px; text-align: left; font-weight: 600; border-bottom: 2px solid #444; }
|
|
td { padding: 8px 12px; border-bottom: 1px solid #2a2a2a; }
|
|
tr:hover td { background: #1a1a2e; }
|
|
.g { color: var(--green); font-weight: 700; }
|
|
.r { color: var(--red); }
|
|
|
|
.math { font-family: 'Times New Roman', serif; font-style: italic; font-size: 1.05em; }
|
|
.code { background: #222; padding: 2px 6px; border-radius: 4px; font-family: monospace; font-size: 0.85em; }
|
|
|
|
.section-num { display: inline-block; background: var(--accent); color: #000; font-weight: 800; width: 28px; height: 28px; border-radius: 50%; text-align: center; line-height: 28px; font-size: 0.85rem; margin-right: 8px; vertical-align: middle; }
|
|
|
|
.tag { display: inline-block; padding: 2px 10px; border-radius: 12px; font-size: 0.75rem; font-weight: 600; }
|
|
.tag-green { background: #1b5e20; color: #a5d6a7; }
|
|
.tag-red { background: #b71c1c; color: #ef9a9a; }
|
|
.tag-blue { background: #0d47a1; color: #90caf9; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
|
|
<h1>Turbo<span>Quant</span></h1>
|
|
<div class="subtitle">KV Cache Compression for vLLM — 2× context, 0% overhead, 30 GB freed</div>
|
|
|
|
<div class="hero-stats">
|
|
<div class="stat green"><div class="num">30 GB</div><div class="label">KV cache freed (4 GPUs)</div></div>
|
|
<div class="stat blue"><div class="num">2.0×</div><div class="label">context capacity</div></div>
|
|
<div class="stat red"><div class="num">0%</div><div class="label">throughput overhead</div></div>
|
|
</div>
|
|
|
|
<!-- ─── THE PROBLEM ─── -->
|
|
<h2><span class="section-num">1</span> The Problem</h2>
|
|
|
|
<div class="two-col">
|
|
<div class="card">
|
|
<h3>KV cache grows with every token</h3>
|
|
<p>During generation, every layer stores a <strong>Key</strong> and <strong>Value</strong> vector for every previous token. For long contexts this dominates GPU memory.</p>
|
|
<div class="diagram">Token 1 → K₁ V₁
|
|
Token 2 → K₁ V₁ K₂ V₂
|
|
Token 3 → K₁ V₁ K₂ V₂ K₃ V₃
|
|
...
|
|
<span class="rd">Token N → K₁..Kₙ V₁..Vₙ ← O(N) memory</span></div>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Qwen3.5-27B on 4× RTX 3090</h3>
|
|
<ul>
|
|
<li>Model weights: ~13 GB / GPU</li>
|
|
<li><strong>KV cache: ~7.5 GB / GPU</strong> (30 GB total)</li>
|
|
<li>Max context: 457,072 tokens</li>
|
|
<li>Want longer? Need more GPUs — expensive</li>
|
|
</ul>
|
|
<p style="margin-top:12px; color: var(--yellow);">What if we could compress the KV cache to 39% of its size?</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ─── HIGH-LEVEL PIPELINE ─── -->
|
|
<h2><span class="section-num">2</span> The TurboQuant Pipeline</h2>
|
|
|
|
<p>Each KV pair goes through a compression pipeline that reduces <strong>512 bytes → 198 bytes</strong> per token (2.6× compression):</p>
|
|
|
|
<div class="flow">
|
|
<div class="flow-step"><div class="icon">🔑</div><div class="title">Raw Key</div><div class="desc">256-dim bf16<br>512 bytes</div></div>
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-step" style="border-color: var(--accent);"><div class="icon">🔄</div><div class="title">Rotate</div><div class="desc">Random orthogonal<br>matrix Π</div></div>
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-step" style="border-color: var(--accent);"><div class="icon">📊</div><div class="title">Quantize</div><div class="desc">3-bit Lloyd-Max<br>8 centroids</div></div>
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-step" style="border-color: var(--accent);"><div class="icon">±</div><div class="title">QJL Signs</div><div class="desc">Residual direction<br>sign bits</div></div>
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-step" style="border-color: var(--green);"><div class="icon">✅</div><div class="title">Compressed</div><div class="desc">~110 bytes<br><strong>4.7× smaller</strong></div></div>
|
|
</div>
|
|
|
|
<div class="flow" style="margin-top: 16px;">
|
|
<div class="flow-step"><div class="icon">💎</div><div class="title">Raw Value</div><div class="desc">256-dim bf16<br>512 bytes</div></div>
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-step" style="border-color: var(--yellow);"><div class="icon">📦</div><div class="title">Group Quantize</div><div class="desc">2-bit per value<br>groups of 32</div></div>
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-step" style="border-color: var(--yellow);"><div class="icon">🗜️</div><div class="title">Bit-Pack</div><div class="desc">4 values per byte<br>+ scales & zeros</div></div>
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-step" style="border-color: var(--green);"><div class="icon">✅</div><div class="title">Compressed</div><div class="desc">~88 bytes<br><strong>5.8× smaller</strong></div></div>
|
|
</div>
|
|
|
|
<!-- ─── KEY COMPRESSION DETAIL ─── -->
|
|
<h2><span class="section-num">3</span> Key Compression (3-bit)</h2>
|
|
|
|
<div class="two-col">
|
|
<div class="card">
|
|
<h3>Step A: Random Rotation</h3>
|
|
<p>Multiply keys by a random orthogonal matrix <span class="math">Π</span>. This spreads information uniformly across dimensions so no single dimension carries disproportionate signal.</p>
|
|
<p style="margin-top:8px;"><span class="math">K<sub>rot</sub> = K · Π</span> (norm-preserving)</p>
|
|
<h3 style="margin-top:16px;">Step B: Lloyd-Max Quantization</h3>
|
|
<p>After rotation, each dimension follows a <strong>Beta distribution</strong>. Lloyd-Max finds the <strong>MSE-optimal</strong> 8 centroids for this distribution.</p>
|
|
<p style="margin-top:8px;">Codebooks are <strong>pre-computed offline</strong> for d=256 — zero runtime overhead.</p>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Step C: QJL Residual Signs</h3>
|
|
<p>The quantization residual <span class="math">r = K<sub>rot</sub> - K̂</span> still contains directional information. QJL (Quantized Johnson-Lindenstrauss) projects it into a compact sign vector:</p>
|
|
<p style="margin-top:8px;"><span class="math">signs = sign(S · r)</span></p>
|
|
<p>where <span class="math">S</span> is a random ±1 matrix. This preserves inner-product relationships with high probability.</p>
|
|
<h3 style="margin-top:16px;">Storage Breakdown</h3>
|
|
<table>
|
|
<tr><td>3-bit indices (256 dims)</td><td style="text-align:right;">96 B</td></tr>
|
|
<tr><td>Norm (fp32)</td><td style="text-align:right;">4 B</td></tr>
|
|
<tr><td>Residual norm (fp32)</td><td style="text-align:right;">4 B</td></tr>
|
|
<tr><td>QJL signs</td><td style="text-align:right;">~8 B</td></tr>
|
|
<tr><th>Total per token per head</th><th style="text-align:right;">~110 B</th></tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ─── VALUE COMPRESSION DETAIL ─── -->
|
|
<h2><span class="section-num">4</span> Value Compression (2-bit)</h2>
|
|
|
|
<div class="card">
|
|
<div class="two-col">
|
|
<div>
|
|
<h3>Group Quantization</h3>
|
|
<p>Values are split into groups of 32 dimensions. Each group gets its own scale and zero point, then each value is mapped to one of <strong>4 levels</strong> (2 bits).</p>
|
|
<div class="diagram"><span class="yw">For group [d₀ .. d₃₁]:</span>
|
|
scale = (max - min) / 3
|
|
zero = min
|
|
idx_i = round((val_i - zero) / scale) <span class="hl">← 0,1,2,3</span>
|
|
|
|
<span class="yw">Bit-packing (4 values per byte):</span>
|
|
byte = idx₀ | (idx₁<<2) | (idx₂<<4) | (idx₃<<6)
|
|
|
|
<span class="gr">256 dims → 64 packed bytes + 16B scales + 16B zeros = 88 bytes</span></div>
|
|
</div>
|
|
<div>
|
|
<h3>Why 2-bit works for values</h3>
|
|
<ul>
|
|
<li>Values are <strong>summed with softmax weights</strong> during attention — small errors average out across many tokens</li>
|
|
<li>Per-group scales preserve the <strong>dynamic range</strong> of each dimension cluster</li>
|
|
<li>4× denser than 8-bit quantization</li>
|
|
</ul>
|
|
<h3 style="margin-top:16px;">Combined Compression</h3>
|
|
<table>
|
|
<tr><th>Component</th><th>Original</th><th>Compressed</th></tr>
|
|
<tr><td>Key</td><td class="r">512 B</td><td class="g">110 B</td></tr>
|
|
<tr><td>Value</td><td class="r">512 B</td><td class="g">88 B</td></tr>
|
|
<tr><th>Total</th><th class="r">1,024 B</th><th class="g">198 B (2.6×)</th></tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ─── VLLM INTEGRATION ─── -->
|
|
<h2><span class="section-num">5</span> vLLM Integration</h2>
|
|
|
|
<div class="diagram" style="font-size: 0.78rem; line-height: 2;">
|
|
<span class="yw">═══ PREFILL (processing the prompt) ═════════════════════════════════════════</span>
|
|
|
|
User prompt → vLLM tokenizer → <span class="hl">FlashAttention forward(Q, K, V)</span>
|
|
↓
|
|
Writes K,V to paged KV cache (normal)
|
|
↓
|
|
<span class="gr">TQ Hook intercepts K,V → Rotate → Quantize → Store compressed</span>
|
|
↓
|
|
Returns attention output (from flash)
|
|
|
|
|
|
<span class="yw">═══ DECODE (generating each new token) ══════════════════════════════════════</span>
|
|
|
|
New token → vLLM → <span class="gr">TQ ACTIVE mode intercepts forward()</span>
|
|
↓
|
|
<span class="gr">Reconstruct K,V from compressed store (Triton kernel)</span>
|
|
<span class="gr">Compute attention entirely in TQ</span>
|
|
<span class="gr">Flash attention SKIPPED — paged cache not read</span>
|
|
↓
|
|
Returns attention output
|
|
|
|
|
|
<span class="yw">═══ FREE KV CACHE ═══════════════════════════════════════════════════════════</span>
|
|
|
|
After prefill, TQ compressed store has all token data.
|
|
<span class="rd">Replace paged KV cache tensors with 1-byte dummies.</span>
|
|
<span class="gr">torch.cuda.empty_cache() → 30 GB VRAM returned to pool!</span>
|
|
Decode continues using only TQ compressed store.
|
|
</div>
|
|
|
|
<!-- ─── DECODE KERNEL ─── -->
|
|
<h2><span class="section-num">6</span> Fused Triton Decode Kernel</h2>
|
|
|
|
<div class="two-col">
|
|
<div class="card">
|
|
<h3>What happens every decode step</h3>
|
|
<p>For each new query token <span class="math">q</span>, against all <span class="math">N</span> cached tokens:</p>
|
|
<ol style="padding-left: 20px; margin-top: 8px;">
|
|
<li><strong>Dequantize keys:</strong> indices → centroids, undo rotation via <span class="math">Π<sup>T</sup></span></li>
|
|
<li><strong>Attention scores:</strong> <span class="math">α<sub>i</sub> = q · k<sub>i</sub> / √d</span></li>
|
|
<li><strong>Dequantize values:</strong> unpack 2-bit → scale · idx + zero</li>
|
|
<li><strong>Weighted sum:</strong> <span class="math">out = ∑ softmax(α<sub>i</sub>) · v<sub>i</sub></span></li>
|
|
</ol>
|
|
<p style="margin-top: 12px;"><strong>All 4 steps fused into ONE Triton kernel</strong> — no intermediate tensors allocated.</p>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Why zero overhead</h3>
|
|
<ul>
|
|
<li>Decode is <strong>compute-bound</strong>, not memory-bound</li>
|
|
<li>Reading compressed data is <strong>faster</strong> (less memory bandwidth)</li>
|
|
<li>Dequantization is cheap ALU ops fused into the attention loop</li>
|
|
<li>Works with <strong>GQA</strong> (Grouped-Query Attention)</li>
|
|
</ul>
|
|
<table style="margin-top: 16px;">
|
|
<tr><th>Metric</th><th>Baseline</th><th>TQ</th></tr>
|
|
<tr><td>Throughput</td><td>6.0 tok/s</td><td class="g">6.0 tok/s</td></tr>
|
|
<tr><td>Overhead</td><td>—</td><td class="g">0%</td></tr>
|
|
<tr><td>Output</td><td>reference</td><td class="g">identical</td></tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ─── RESULTS ─── -->
|
|
<h2><span class="section-num">7</span> Results</h2>
|
|
|
|
<p style="text-align:center; color: var(--dim); margin-bottom: 16px;">Qwen3.5-27B · 4× RTX 3090 · TP=4 · bf16 · gpu_mem=0.90</p>
|
|
|
|
<table>
|
|
<tr><th>Metric</th><th>Baseline vLLM</th><th>TurboQuant</th><th>Change</th></tr>
|
|
<tr><td>KV cache blocks</td><td>583</td><td>583 → freed</td><td class="g">−30 GB</td></tr>
|
|
<tr><td>VRAM / GPU</td><td>21,539 MB</td><td>21,299 MB</td><td class="g">−240 MB visible</td></tr>
|
|
<tr><td>Tensor bytes freed / GPU</td><td>—</td><td>7,489 MB</td><td class="g">available in CUDA pool</td></tr>
|
|
<tr><td>Total freed</td><td>—</td><td><strong>30.0 GB</strong></td><td></td></tr>
|
|
<tr><td>Max token capacity</td><td>457,072</td><td><strong>914,144</strong></td><td class="g"><strong>2.0×</strong></td></tr>
|
|
<tr><td>Throughput</td><td>6.0 tok/s</td><td>6.0 tok/s</td><td class="g">0% overhead</td></tr>
|
|
<tr><td>Output quality</td><td>reference</td><td>identical</td><td>—</td></tr>
|
|
</table>
|
|
|
|
<!-- ─── ARCHITECTURE ─── -->
|
|
<h2><span class="section-num">8</span> Code Architecture</h2>
|
|
|
|
<div class="diagram" style="line-height: 2.2; font-size: 0.8rem;">turboquant/
|
|
<span class="hl">codebook.py</span> ← Lloyd-Max optimal quantizer for Beta distribution
|
|
<span class="hl">codebooks/</span> ← Pre-generated codebook files (d=256, bits 2/3/4)
|
|
<span class="hl">rotation.py</span> ← Random orthogonal Π + QJL projection S
|
|
<span class="hl">quantizer.py</span> ← TurboQuantMSE + TurboQuantProd pipeline
|
|
<span class="hl">kv_cache.py</span> ← KV cache manager, value bit-packing
|
|
<span class="gr">triton_kernels.py</span> ← 3 fused Triton kernels (decode attention)
|
|
<span class="yw">vllm_attn_backend.py</span> ← Monkey-patch hooks, free_kv_cache(), enable_no_alloc()
|
|
|
|
<span class="rd">proof.py</span> ← Definitive A/B benchmark (separate processes)
|
|
<span class="rd">benchmark.py</span> ← Throughput + quality + VRAM benchmark</div>
|
|
|
|
<div style="text-align: center; margin-top: 48px; padding: 32px; border-top: 1px solid #333;">
|
|
<div style="font-size: 1.4rem; font-weight: 700;">Same model. Same GPUs. <span style="color: var(--accent);">2× the context.</span></div>
|
|
<div style="margin-top: 12px;"><a href="https://github.com/0xSero/turboquant" style="color: var(--accent); text-decoration: none;">github.com/0xSero/turboquant</a></div>
|
|
</div>
|
|
|
|
</div>
|
|
</body>
|
|
</html>
|