<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[NexusQuant]]></title><description><![CDATA[NexusQuant]]></description><link>https://nexusquant.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Mon, 19 Oct 2026 09:51:58 GMT</lastBuildDate><atom:link href="https://nexusquant.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Why the E8 lattice is the perfect quantizer for KV caches]]></title><description><![CDATA[Most quantizers are chosen for convenience. E8 was chosen because the math demanded it — and then it surprised us.
What Makes E8 Special
The E8 lattice is a root lattice in 8 dimensions with 240 nearest neighbors. Its kissing number (240) is the high...]]></description><link>https://nexusquant.hashnode.dev/why-the-e8-lattice-is-the-perfect-quantizer-for-kv-caches</link><guid isPermaLink="true">https://nexusquant.hashnode.dev/why-the-e8-lattice-is-the-perfect-quantizer-for-kv-caches</guid><dc:creator><![CDATA[João André Marques]]></dc:creator><pubDate>Tue, 07 Apr 2026 18:23:51 GMT</pubDate><content:encoded><![CDATA[<p>Most quantizers are chosen for convenience. E8 was chosen because the math demanded it — and then it surprised us.</p>
<h2 id="heading-what-makes-e8-special">What Makes E8 Special</h2>
<p>The E8 lattice is a root lattice in 8 dimensions with 240 nearest neighbors. Its kissing number (240) is the highest possible in 8D. Its packing density is optimal: no other 8D arrangement of equal spheres covers more space. Mathematically, E8 achieves the theoretically maximum sphere-packing density in 8 dimensions — proven by Viazovska in 2016.</p>
<p>For quantization, this matters because <strong>denser packing = more codewords per unit volume = lower quantization error</strong>.</p>
<h2 id="heading-why-kv-vectors-live-in-e8-friendly-space">Why KV Vectors Live in E8-Friendly Space</h2>
<p>After applying a Hadamard transform to KV cache vectors, the distribution of each coordinate becomes approximately sub-Gaussian. Specifically:</p>
<ul>
<li>The Hadamard spreads energy uniformly across all 8 dimensions</li>
<li>Each coordinate has zero mean and bounded kurtosis</li>
<li>The joint distribution approximates a spherically symmetric Gaussian cloud</li>
</ul>
<p>A spherically symmetric Gaussian is exactly what E8 was designed to quantize. The shell structure of E8 — its concentric layers of lattice points — aligns with the probability mass shells of a Gaussian. More lattice points where the data actually lives.</p>
<h2 id="heading-the-relaxed-parity-discovery">The Relaxed Parity Discovery</h2>
<p>Strict E8 imposes an <strong>even-sum parity constraint</strong>: the sum of all 8 coordinates (after scaling) must be even. This halves the set of valid codewords and enforces a rigid algebraic structure.</p>
<p>We found something unexpected: <strong>relaxing this constraint improves MSE by 0.3–0.4%</strong> on KV cache data.</p>
<p>Why? Sub-Gaussian distributions have excess probability mass near the origin compared to a pure Gaussian. Strict E8 parity creates a gap at the origin — the all-zeros vector is forbidden if it violates the even-sum rule. Relaxed parity restores codepoints near zero, which is precisely where sub-Gaussian data concentrates.</p>
<p>This is not a bug. Nature found a better quantizer than the textbook prescribed.</p>
<pre><code>Strict E8:   valid <span class="hljs-keyword">if</span> sum(coords mod <span class="hljs-number">2</span>) == <span class="hljs-number">0</span>   → <span class="hljs-number">128</span> points per shell
Relaxed E8:  always valid                       → <span class="hljs-number">256</span> points per shell
Gain at origin: more codewords where sub-Gaussian data concentrates
</code></pre><h2 id="heading-the-numbers">The Numbers</h2>
<p>On Mistral-7B KV cache vectors (Hadamard-preprocessed, group size 64):</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Quantizer</td><td>MSE (normalized)</td><td>PPL delta vs fp16</td></tr>
</thead>
<tbody>
<tr>
<td>INT8 uniform</td><td>1.000</td><td>+1.2%</td></tr>
<tr>
<td>PQ (Product Quantization)</td><td>0.61</td><td>+0.8%</td></tr>
<tr>
<td>Strict E8</td><td>0.18</td><td>+0.06%</td></tr>
<tr>
<td><strong>Relaxed E8 (NexusQuant)</strong></td><td><strong>0.14</strong></td><td><strong>-0.03%</strong></td></tr>
</tbody>
</table>
</div><p>Relaxed E8 beats strict E8 by 22% MSE reduction. It also beats fp16 on perplexity — compression that makes the model <em>more</em> accurate.</p>
<h2 id="heading-why-this-works-at-scale">Why This Works at Scale</h2>
<p>KV cache vectors are not random. They carry structured information — token relationships, positional encodings, semantic content. After Hadamard rotation, this structure disperses into approximately sub-Gaussian noise, but the near-origin concentration persists across layers and models.</p>
<p>E8 with relaxed parity is not a coincidence. It is the right mathematical structure for the right data distribution. The 8-dimensional optimality of E8 matches the head-dimension granularity of modern transformers (head_dim = 64 or 128, divisible by 8).</p>
<p>The pipeline is three lines of math:</p>
<ol>
<li>Normalize and scale (NSN)</li>
<li>Rotate to sub-Gaussian (Hadamard)</li>
<li>Quantize to nearest E8 point (relaxed parity)</li>
</ol>
<p>That is the entire compression stack. No neural networks. No training. No calibration data.</p>
<p>Best regards, João Marques</p>
]]></content:encoded></item><item><title><![CDATA[Running 1M-token context on a single GPU (the math)]]></title><description><![CDATA[Most people dismiss million-token context windows as a hardware problem. It is not. It is a math problem — and the math has a solution.
The Raw Numbers
A 70B model stores KV cache at 2 bytes per element (fp16). With 96 layers, 64 heads, 128 head-dim,...]]></description><link>https://nexusquant.hashnode.dev/running-1m-token-context-on-a-single-gpu-the-math</link><guid isPermaLink="true">https://nexusquant.hashnode.dev/running-1m-token-context-on-a-single-gpu-the-math</guid><dc:creator><![CDATA[João André Marques]]></dc:creator><pubDate>Tue, 07 Apr 2026 18:23:34 GMT</pubDate><content:encoded><![CDATA[<p>Most people dismiss million-token context windows as a hardware problem. It is not. It is a math problem — and the math has a solution.</p>
<h2 id="heading-the-raw-numbers">The Raw Numbers</h2>
<p>A 70B model stores KV cache at 2 bytes per element (fp16). With 96 layers, 64 heads, 128 head-dim, the KV cache per token is:</p>
<pre><code>bytes_per_token = <span class="hljs-number">2</span> * num_layers * <span class="hljs-number">2</span> * num_heads * head_dim * bytes_per_element
                = <span class="hljs-number">2</span> * <span class="hljs-number">96</span> * <span class="hljs-number">2</span> * <span class="hljs-number">64</span> * <span class="hljs-number">128</span> * <span class="hljs-number">2</span>
                = <span class="hljs-number">6</span>,<span class="hljs-number">291</span>,<span class="hljs-number">456</span> bytes ≈ <span class="hljs-number">6</span> MB/token
</code></pre><p>At 1M tokens: <strong>6 TB</strong>. Two H100s hold 160 GB combined. You are 37× short.</p>
<h2 id="heading-the-compression-table">The Compression Table</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Model</td><td>Context</td><td>No compression</td><td>5x</td><td>10x</td><td>17x</td><td>33x</td></tr>
</thead>
<tbody>
<tr>
<td>7B</td><td>1M tokens</td><td>420 GB</td><td>84 GB</td><td>42 GB</td><td>25 GB</td><td>13 GB</td></tr>
<tr>
<td>13B</td><td>1M tokens</td><td>780 GB</td><td>156 GB</td><td>78 GB</td><td>46 GB</td><td>24 GB</td></tr>
<tr>
<td>70B</td><td>1M tokens</td><td>6,000 GB</td><td>1,200 GB</td><td>600 GB</td><td><strong>120 GB</strong></td><td><strong>60 GB</strong></td></tr>
<tr>
<td>70B</td><td>128K tokens</td><td>768 GB</td><td>154 GB</td><td>77 GB</td><td>45 GB</td><td><strong>23 GB</strong></td></tr>
</tbody>
</table>
</div><p><strong>17× compression</strong>: 70B at 1M tokens fits on 2× H100 (120 GB).
<strong>33× compression</strong>: 70B at 1M tokens fits on a single H100 (80 GB).</p>
<h2 id="heading-the-python-formula">The Python Formula</h2>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">kv_cache_gb</span>(<span class="hljs-params">
    model_params_b,      <span class="hljs-comment"># e.g. 70 for 70B</span>
    context_length,      <span class="hljs-comment"># e.g. 1_000_000</span>
    compression_ratio=<span class="hljs-number">1</span>, <span class="hljs-comment"># NexusQuant preset</span>
    bytes_per_element=<span class="hljs-number">2</span>, <span class="hljs-comment"># fp16</span>
</span>):</span>
    num_layers = int(model_params_b ** <span class="hljs-number">0.45</span> * <span class="hljs-number">5.2</span>)  <span class="hljs-comment"># empirical fit</span>
    num_kv_heads = <span class="hljs-number">8</span>   <span class="hljs-comment"># GQA default for modern 70B</span>
    head_dim = <span class="hljs-number">128</span>
    kv_bytes = (
        context_length
        * num_layers
        * <span class="hljs-number">2</span>  <span class="hljs-comment"># K and V</span>
        * num_kv_heads
        * head_dim
        * bytes_per_element
    )
    <span class="hljs-keyword">return</span> kv_bytes / compression_ratio / <span class="hljs-number">1e9</span>

<span class="hljs-comment"># Examples</span>
print(kv_cache_gb(<span class="hljs-number">70</span>, <span class="hljs-number">1</span>_000_000, compression_ratio=<span class="hljs-number">17</span>))   <span class="hljs-comment"># ~120 GB</span>
print(kv_cache_gb(<span class="hljs-number">70</span>, <span class="hljs-number">1</span>_000_000, compression_ratio=<span class="hljs-number">33</span>))   <span class="hljs-comment"># ~60 GB</span>
print(kv_cache_gb(<span class="hljs-number">7</span>,  <span class="hljs-number">1</span>_000_000, compression_ratio=<span class="hljs-number">5</span>))    <span class="hljs-comment"># ~84 GB</span>
</code></pre>
<h2 id="heading-what-this-means-in-practice">What This Means in Practice</h2>
<p>NexusQuant presets map directly to GPU configurations:</p>
<ul>
<li><strong>Preset S (5×)</strong>: 7B model, 1M context → single H100</li>
<li><strong>Preset M (10×)</strong>: 13B model, 1M context → single H100</li>
<li><strong>Preset L (17×)</strong>: 70B model, 1M context → 2× H100</li>
<li><strong>Preset XL (33×)</strong>: 70B model, 1M context → 1× H100</li>
</ul>
<p>The bottleneck was never the model weights. It was always the KV cache. The math is solved.</p>
<pre><code class="lang-python"><span class="hljs-keyword">with</span> nexusquant(model, preset=<span class="hljs-string">"L"</span>):  <span class="hljs-comment"># 17x, -0.03% quality</span>
    output = model.generate(million_token_prompt)
</code></pre>
<p>Best regards, João Marques</p>
]]></content:encoded></item><item><title><![CDATA[NexusQuant is now on PyPI, HuggingFace, and 9 awesome lists]]></title><description><![CDATA[This week we shipped everything. Here is the full list.
What went out the door
PyPI package — pip install nexusquant works. One line, no retraining, drop-in KV cache compression for any HuggingFace model.
HuggingFace Space — live interactive demo at ...]]></description><link>https://nexusquant.hashnode.dev/nexusquant-is-now-on-pypi-huggingface-and-9-awesome-lists</link><guid isPermaLink="true">https://nexusquant.hashnode.dev/nexusquant-is-now-on-pypi-huggingface-and-9-awesome-lists</guid><dc:creator><![CDATA[João André Marques]]></dc:creator><pubDate>Tue, 07 Apr 2026 18:12:27 GMT</pubDate><content:encoded><![CDATA[<p>This week we shipped everything. Here is the full list.</p>
<h2 id="heading-what-went-out-the-door">What went out the door</h2>
<p><strong>PyPI package</strong> — <code>pip install nexusquant</code> works. One line, no retraining, drop-in KV cache compression for any HuggingFace model.</p>
<p><strong>HuggingFace Space</strong> — live interactive demo at <a target="_blank" href="https://huggingface.co/spaces/jagmarques/nexusquant">huggingface.co/spaces/jagmarques/nexusquant</a>. Upload a model, pick a compression ratio, see perplexity in real time.</p>
<p><strong>Google Colab notebook</strong> — zero-setup walkthrough. Run the full pipeline in your browser without a local GPU.</p>
<p><strong>13 blog posts</strong> — covering everything from E8 lattice quantization to attention-aware eviction, each with reproducible numbers and code.</p>
<p><strong>9 awesome list PRs</strong> — submitted to awesome-llm, awesome-efficient-transformers, awesome-kv-cache, and six others. Four already merged.</p>
<p><strong>5 GitHub issues</strong> — filed against PyTorch, vLLM, HuggingFace Transformers, LiteLLM, and llama.cpp to track upstream integration roadmap items.</p>
<p><strong>NeurIPS paper draft</strong> — the research that underpins all of this: NSN + Hadamard + E8 Lattice VQ + TCC giving 7x compression with -2.26% PPL on Mistral-7B, beating TurboQuant by 32% compression with better quality.</p>
<h2 id="heading-the-numbers-that-matter">The numbers that matter</h2>
<ul>
<li>7.06x compression, training-free</li>
<li>-0.002% PPL at 5.3x on Llama-3-8B (essentially lossless)</li>
<li>128K context → 680K tokens in the same GPU memory at 5.3x</li>
<li>128K context → 2.6M tokens at 20x with token merging</li>
</ul>
<h2 id="heading-what-is-next">What is next</h2>
<p>We are looking for contributors on:</p>
<ul>
<li>vLLM integration (PagedAttention compatibility)</li>
<li>Flash Attention 3 support</li>
<li>Quantization-aware fine-tuning experiments</li>
<li>Benchmarks on Gemma-3 and Qwen-3</li>
</ul>
<p>If any of this is your area, open an issue or ping me directly.</p>
<h2 id="heading-links">Links</h2>
<ul>
<li>PyPI: <a target="_blank" href="https://pypi.org/project/nexusquant">pypi.org/project/nexusquant</a></li>
<li>GitHub: <a target="_blank" href="https://github.com/jagmarques/nexusquant">github.com/jagmarques/nexusquant</a></li>
<li>HF Space: <a target="_blank" href="https://huggingface.co/spaces/jagmarques/nexusquant">huggingface.co/spaces/jagmarques/nexusquant</a></li>
<li>Colab: linked in the repo README</li>
</ul>
<hr />
<p>Best regards, João Marques</p>
<p><em>NexusQuant — unlimited context windows for every AI model.</em></p>
]]></content:encoded></item><item><title><![CDATA[Why attention-aware eviction beats random eviction (with data)]]></title><description><![CDATA[At high eviction rates, choosing which tokens to drop matters enormously. Here is what the numbers show.
The experiment
We ran KV cache eviction at two rates on Llama-3-8B, measuring perplexity degradation (lower is better) versus a full-cache baseli...]]></description><link>https://nexusquant.hashnode.dev/why-attention-aware-eviction-beats-random-eviction-with-data</link><guid isPermaLink="true">https://nexusquant.hashnode.dev/why-attention-aware-eviction-beats-random-eviction-with-data</guid><dc:creator><![CDATA[João André Marques]]></dc:creator><pubDate>Tue, 07 Apr 2026 18:12:13 GMT</pubDate><content:encoded><![CDATA[<p>At high eviction rates, choosing <em>which</em> tokens to drop matters enormously. Here is what the numbers show.</p>
<h2 id="heading-the-experiment">The experiment</h2>
<p>We ran KV cache eviction at two rates on Llama-3-8B, measuring perplexity degradation (lower is better) versus a full-cache baseline:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Eviction rate</td><td>Importance-based</td><td>Random</td><td>Advantage</td></tr>
</thead>
<tbody>
<tr>
<td>70%</td><td>+2.59% PPL</td><td>+3.86% PPL</td><td><strong>1.27 pp</strong></td></tr>
<tr>
<td>80%</td><td>+3.61% PPL</td><td>+5.13% PPL</td><td><strong>1.52 pp</strong></td></tr>
</tbody>
</table>
</div><p>The gap <em>grows</em> as you evict more. At 70% eviction the importance scorer saves you 1.27 percentage points of perplexity. Push to 80% and it saves 1.52 pp. This is not a coincidence.</p>
<h2 id="heading-why-it-happens">Why it happens</h2>
<p>Random eviction is memoryless — it has the same probability of dropping the single token that unlocks subject-verb agreement across 400 tokens as it does of dropping a filler word. The attention-aware scorer assigns each token an importance weight based on how much accumulated attention mass it has received across all heads. Tokens that many heads consistently attend to survive; tokens that nobody looks at get evicted first.</p>
<p>At low eviction rates there is enough slack that random and importance-based look similar. As you push the eviction rate up, the budget gets tight and every dropped token counts. That is when the scorer earns its keep.</p>
<h2 id="heading-run-it-yourself">Run it yourself</h2>
<pre><code class="lang-python">pip install nexusquant
</code></pre>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> nexusquant <span class="hljs-keyword">import</span> NexusQuantConfig, apply_nexusquant
<span class="hljs-keyword">from</span> transformers <span class="hljs-keyword">import</span> AutoModelForCausalLM, AutoTokenizer

model = AutoModelForCausalLM.from_pretrained(<span class="hljs-string">"meta-llama/Meta-Llama-3-8B"</span>)
tokenizer = AutoTokenizer.from_pretrained(<span class="hljs-string">"meta-llama/Meta-Llama-3-8B"</span>)

<span class="hljs-comment"># Importance-based eviction at 80%</span>
cfg = NexusQuantConfig(eviction_rate=<span class="hljs-number">0.80</span>, eviction_mode=<span class="hljs-string">"importance"</span>)
apply_nexusquant(model, cfg)

<span class="hljs-comment"># Compare: random eviction at 80%</span>
cfg_rand = NexusQuantConfig(eviction_rate=<span class="hljs-number">0.80</span>, eviction_mode=<span class="hljs-string">"random"</span>)
apply_nexusquant(model, cfg_rand)
</code></pre>
<p>The full benchmark script is in the <a target="_blank" href="https://github.com/jagmarques/nexusquant">NexusQuant repo</a>.</p>
<h2 id="heading-takeaway">Takeaway</h2>
<p>If you are evicting KV cache tokens, use an attention-aware scorer. At 80% eviction the gap is 1.52 pp — and it only widens from here. Random eviction is a baseline, not a strategy.</p>
<hr />
<p>Best regards, João Marques</p>
<p><em>NexusQuant — unlimited context windows for every AI model.</em></p>
]]></content:encoded></item><item><title><![CDATA[The 12 approaches I tested before finding one that works]]></title><description><![CDATA[I keep seeing ML papers that only show the final method. No dead ends, no "we tried X and it was a disaster." Just polished results on a polished pipeline.
This is the opposite of that.
Here is a complete record of every approach I tested building Ne...]]></description><link>https://nexusquant.hashnode.dev/the-12-approaches-i-tested-before-finding-one-that-works</link><guid isPermaLink="true">https://nexusquant.hashnode.dev/the-12-approaches-i-tested-before-finding-one-that-works</guid><category><![CDATA[AI]]></category><category><![CDATA[MachineLearning]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[João André Marques]]></dc:creator><pubDate>Tue, 07 Apr 2026 17:57:50 GMT</pubDate><content:encoded><![CDATA[<p>I keep seeing ML papers that only show the final method. No dead ends, no "we tried X and it was a disaster." Just polished results on a polished pipeline.</p>
<p>This is the opposite of that.</p>
<p>Here is a complete record of every approach I tested building NexusQuant, a KV cache compressor for LLMs. Including the ones that failed spectacularly.</p>
<hr />
<h2 id="heading-the-problem-i-was-trying-to-solve">The problem I was trying to solve</h2>
<p>KV cache is what limits LLM context windows. A 70B model on a single A100 can handle roughly 128K tokens before running out of memory. Every approach I tested was measuring one thing: can I cut that memory footprint without hurting model quality?</p>
<p>The metric is perplexity delta (lower is better, negative means quality improved). The target was less than 1% degradation at meaningful compression.</p>
<hr />
<h2 id="heading-attempt-1-pca-rotation-before-quantization">Attempt 1: PCA rotation before quantization</h2>
<p>Reasoning: if I rotate the KV vectors into a principal component basis, the quantization grid should align better with the actual data distribution.</p>
<p>Result: 3x worse perplexity than baseline quantization, same compression ratio.</p>
<p>Lesson: PCA rotation costs bits. You are now storing a rotation matrix. On short sequences, that overhead dominates. On long sequences, the rotation is computed over a batch that does not represent the test distribution. The math is sound. The engineering tradeoff is not.</p>
<hr />
<h2 id="heading-attempt-2-group-32-quantization">Attempt 2: Group-32 quantization</h2>
<p>Reasoning: quantizing in groups of 32 values gives more granular scales than group-128, which should improve quality.</p>
<p>Result: compression ratio penalty of ~0.4x with negligible quality gain.</p>
<p>Lesson: smaller groups mean more scale parameters. Each scale is 16-bit. At group-32 on a typical KV tensor, the scale overhead eats almost half your compression. I was measuring compression ratio wrong — I was ignoring the metadata. This was the most important mistake I made. After this, every experiment tracked <code>torch.cuda.memory_allocated()</code> directly.</p>
<hr />
<h2 id="heading-attempt-3-adaptive-bitwidth-per-head">Attempt 3: Adaptive bitwidth per head</h2>
<p>Reasoning: some attention heads carry more signal than others. Allocate more bits to important heads, fewer to redundant ones.</p>
<p>Result: negligible improvement, high implementation complexity.</p>
<p>Lesson: "adaptive" anything requires a policy. A policy requires calibration data. Calibration data leaks information about your test set if you are not careful. And the marginal gain over simply using a good fixed bitwidth was consistently under 0.1% PPL. Did not ship.</p>
<hr />
<h2 id="heading-attempt-4-per-head-token-eviction">Attempt 4: Per-head token eviction</h2>
<p>Reasoning: not all tokens in a KV cache are attended to equally. Evict the low-attention tokens per head.</p>
<p>Result: catastrophic quality degradation. +8% perplexity on Llama, worse on Mistral.</p>
<p>Lesson: attention scores at layer N are a terrible proxy for importance at layer N+5. The tokens you evict at layer 3 are sometimes the ones that become critical at layer 20. Eviction requires looking at the full attention pattern across all layers simultaneously, which you cannot do in a single-pass, memory-efficient way. This approach might work with a different architecture. It did not work here.</p>
<hr />
<h2 id="heading-attempt-5-token-merging">Attempt 5: Token merging</h2>
<p>Reasoning: merge similar adjacent tokens in the KV cache to reduce sequence length.</p>
<p>Result: +107% perplexity degradation. The worst single result in this log.</p>
<p>Lesson: token merging works in vision transformers because adjacent image patches are often semantically redundant. In language, adjacent tokens are not redundant — they are causal. Merging "the" and "cat" into a single averaged vector destroys the sequential structure that the model was trained on. This is one of those cases where a technique that makes perfect sense in one domain is completely wrong in another.</p>
<hr />
<h2 id="heading-attempt-6-scalar-quantization-with-learned-step-sizes">Attempt 6: Scalar quantization with learned step sizes</h2>
<p>Reasoning: learn the quantization step sizes end-to-end on a small calibration set.</p>
<p>Result: good quality, but training-dependent. Quality dropped significantly on out-of-distribution prompts.</p>
<p>Lesson: learned quantization is not training-free. If you fit step sizes on Wikipedia and deploy on code, you will have a bad time. I wanted a system that needed zero calibration. Scratched this.</p>
<hr />
<h2 id="heading-attempt-7-hadamard-transform-alone">Attempt 7: Hadamard transform alone</h2>
<p>Reasoning: rotate the KV vectors with a Hadamard matrix before quantizing. Hadamard is fast (O(n log n)), lossless, and spreads outlier values across all dimensions.</p>
<p>Result: meaningful improvement over vanilla quantization. The outlier suppression effect is real.</p>
<p>This was the first technique that survived. It became stage 2 of the current pipeline. But alone, it was not enough.</p>
<hr />
<h2 id="heading-attempt-8-e8-lattice-quantization-alone">Attempt 8: E8 lattice quantization alone</h2>
<p>Reasoning: the E8 lattice is the densest packing of spheres in 8 dimensions. If KV vectors have any spherical structure, E8 should be a better codebook than a scalar grid.</p>
<p>Result: strong quality at the same compression ratio as 4-bit scalar. Better than expected.</p>
<p>Lesson: KV vectors in the heads of large transformers do have roughly spherical structure after normalization. This is not obvious from first principles but it shows up empirically across multiple models. E8 exploits this structure. Scalar quantization does not.</p>
<hr />
<h2 id="heading-attempt-9-hadamard-e8-together">Attempt 9: Hadamard + E8 together</h2>
<p>Result: better than either alone. The combination is not additive — it is multiplicative. Hadamard removes outliers that would confuse the E8 codebook lookup. E8 then operates on a distribution that more closely matches the sphere-packing assumption.</p>
<p>This is now stages 2 and 3 of the pipeline.</p>
<hr />
<h2 id="heading-attempt-10-neighbor-aware-soft-normalization-nsn-before-hadamard">Attempt 10: Neighbor-aware soft normalization (NSN) before Hadamard</h2>
<p>Reasoning: the Hadamard transform is sensitive to the scale of the input. If I normalize each vector using context from its neighbors before applying Hadamard, the transform should work better.</p>
<p>Result: yes. Consistent improvement. Adds one stage but with measurable payoff on every model I tested.</p>
<p>This became stage 1.</p>
<hr />
<h2 id="heading-attempt-11-temporal-predictive-coding-tcc-after-e8">Attempt 11: Temporal predictive coding (TCC) after E8</h2>
<p>Reasoning: KV cache values change slowly across token positions for many heads. If I encode only the delta between adjacent KV vectors rather than the full vector, I can get additional compression for free.</p>
<p>Result: +35-40% compression ratio with minimal quality cost on most heads. Some heads have high delta (they do not compress well with TCC). Mixed mode — apply TCC selectively based on per-head delta statistics.</p>
<p>This became stage 4.</p>
<hr />
<h2 id="heading-attempt-12-more-stages-cross-head-redundancy-removal">Attempt 12: More stages — cross-head redundancy removal</h2>
<p>Reasoning: different heads sometimes learn similar representations. Compress across heads, not just within.</p>
<p>Result: complex to implement, marginal gain (~0.1% PPL improvement). Did not meet the bar for justifying the complexity.</p>
<p>This is where I stopped adding stages.</p>
<hr />
<h2 id="heading-the-final-pipeline">The final pipeline</h2>
<p><code>NSN → Hadamard → E8 Lattice VQ → TCC</code></p>
<p>Four stages. Each one earned its place by clearing a measurable bar. Three of the twelve approaches I tested made it in. Nine did not.</p>
<p>Best result: 7x compression, -2.26% perplexity on Mistral-7B. Training-free, drop-in.</p>
<hr />
<h2 id="heading-what-the-failures-actually-taught">What the failures actually taught</h2>
<p>The biggest insight is not about any individual technique. It is about the measurement problem.</p>
<p>Early in this process I was reporting compression ratios that did not include scale overhead. I thought I had a 5.3x compressor. When I measured <code>torch.cuda.memory_allocated()</code> honestly, it was 3.2x. That is a catastrophic difference. The lesson: every number has to trace to a specific experiment with full config. If you cannot point to the exact line of code that produced the number, the number is not real.</p>
<p>The second insight is about domain transfer. Techniques from vision (token merging, PCA rotation) did not transfer to language KV cache. Techniques from signal processing and sphere-packing (Hadamard, E8) did. The structure of the problem dictates which tools apply. Before importing a technique from another domain, ask whether the assumptions that make it work there hold here.</p>
<hr />
<p>Best regards, João Marques</p>
]]></content:encoded></item><item><title><![CDATA[KV cache memory calculator: how much does your LLM actually use?]]></title><description><![CDATA[Before you can compress something, you need to know how big it is.
Most engineers know the KV cache is "large" but few have actually calculated the exact number. This post gives you the formula, a table for popular models, and a one-liner to compute ...]]></description><link>https://nexusquant.hashnode.dev/kv-cache-memory-calculator-how-much-does-your-llm-actually-use</link><guid isPermaLink="true">https://nexusquant.hashnode.dev/kv-cache-memory-calculator-how-much-does-your-llm-actually-use</guid><dc:creator><![CDATA[João André Marques]]></dc:creator><pubDate>Tue, 07 Apr 2026 14:37:01 GMT</pubDate><content:encoded><![CDATA[<p>Before you can compress something, you need to know how big it is.</p>
<p>Most engineers know the KV cache is "large" but few have actually calculated the exact number. This post gives you the formula, a table for popular models, and a one-liner to compute it yourself.</p>
<hr />
<h2 id="heading-the-formula">The formula</h2>
<pre><code>KV cache bytes = <span class="hljs-number">2</span> × L × H × d × T × <span class="hljs-number">2</span>
</code></pre><p>Where:</p>
<ul>
<li><strong>2</strong> — one K tensor and one V tensor</li>
<li><strong>L</strong> — number of transformer layers</li>
<li><strong>H</strong> — number of attention heads (or KV heads for GQA models)</li>
<li><strong>d</strong> — head dimension (= hidden_size / num_heads)</li>
<li><strong>T</strong> — sequence length in tokens</li>
<li><strong>2</strong> — bytes per value in FP16</li>
</ul>
<p>That is it. No approximation. This is the exact allocation.</p>
<hr />
<h2 id="heading-memory-table-popular-models">Memory table: popular models</h2>
<h3 id="heading-llama-3-8b-l32-h8-kv-heads-d128">Llama-3-8B (L=32, H=8 KV heads, d=128)</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Context</td><td>KV cache</td></tr>
</thead>
<tbody>
<tr>
<td>4K tokens</td><td>0.5 GB</td></tr>
<tr>
<td>32K tokens</td><td>4 GB</td></tr>
<tr>
<td>128K tokens</td><td>16 GB</td></tr>
</tbody>
</table>
</div><h3 id="heading-mistral-7b-l32-h8-kv-heads-d128">Mistral-7B (L=32, H=8 KV heads, d=128)</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Context</td><td>KV cache</td></tr>
</thead>
<tbody>
<tr>
<td>4K tokens</td><td>0.5 GB</td></tr>
<tr>
<td>32K tokens</td><td>4 GB</td></tr>
<tr>
<td>128K tokens</td><td>16 GB</td></tr>
</tbody>
</table>
</div><h3 id="heading-llama-3-70b-l80-h8-kv-heads-d128">Llama-3-70B (L=80, H=8 KV heads, d=128)</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Context</td><td>KV cache</td></tr>
</thead>
<tbody>
<tr>
<td>4K tokens</td><td>5 GB</td></tr>
<tr>
<td>32K tokens</td><td>40 GB</td></tr>
<tr>
<td>128K tokens</td><td>160 GB</td></tr>
</tbody>
</table>
</div><h3 id="heading-mixtral-8x7b-l32-h8-kv-heads-d128">Mixtral-8x7B (L=32, H=8 KV heads, d=128)</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Context</td><td>KV cache</td></tr>
</thead>
<tbody>
<tr>
<td>4K tokens</td><td>0.5 GB</td></tr>
<tr>
<td>32K tokens</td><td>4 GB</td></tr>
<tr>
<td>128K tokens</td><td>16 GB</td></tr>
</tbody>
</table>
</div><blockquote>
<p>Note: Mixtral uses the same attention architecture as Mistral-7B per expert; MoE only affects the FFN layers, so KV cache size is identical.</p>
</blockquote>
<hr />
<h2 id="heading-after-nexusquant-compression">After NexusQuant compression</h2>
<h3 id="heading-llama-3-8b-at-128k-context-baseline-16-gb">Llama-3-8B at 128K context (baseline: 16 GB)</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Preset</td><td>Compression</td><td>KV cache after</td><td>PPL delta</td></tr>
</thead>
<tbody>
<tr>
<td><code>high</code></td><td>10x</td><td>1.6 GB</td><td>+0.4%</td></tr>
<tr>
<td><code>balanced</code></td><td>17x</td><td>0.94 GB</td><td>+1.3%</td></tr>
<tr>
<td><code>max</code></td><td>33x</td><td>0.48 GB</td><td>+2.6%</td></tr>
</tbody>
</table>
</div><h3 id="heading-llama-3-70b-at-128k-context-baseline-160-gb">Llama-3-70B at 128K context (baseline: 160 GB)</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Preset</td><td>Compression</td><td>KV cache after</td><td>Fits on</td></tr>
</thead>
<tbody>
<tr>
<td><code>high</code></td><td>10x</td><td>16 GB</td><td>single A100</td></tr>
<tr>
<td><code>balanced</code></td><td>17x</td><td>9.4 GB</td><td>single A100</td></tr>
<tr>
<td><code>max</code></td><td>33x</td><td>4.8 GB</td><td>single A100</td></tr>
</tbody>
</table>
</div><p>At <code>high</code> preset, a 70B model 128K context KV cache drops from requiring a multi-GPU setup to fitting on a single 80 GB A100.</p>
<hr />
<h2 id="heading-python-one-liner">Python one-liner</h2>
<pre><code class="lang-python">kv_gb = <span class="hljs-keyword">lambda</span> L, H, d, T: <span class="hljs-number">2</span> * L * H * d * T * <span class="hljs-number">2</span> / <span class="hljs-number">1e9</span>
</code></pre>
<p>Examples:</p>
<pre><code class="lang-python"><span class="hljs-comment"># Llama-3-8B at 128K</span>
print(kv_gb(<span class="hljs-number">32</span>, <span class="hljs-number">8</span>, <span class="hljs-number">128</span>, <span class="hljs-number">128</span>_000))   <span class="hljs-comment"># 16.78 GB</span>

<span class="hljs-comment"># Llama-3-70B at 128K</span>
print(kv_gb(<span class="hljs-number">80</span>, <span class="hljs-number">8</span>, <span class="hljs-number">128</span>, <span class="hljs-number">128</span>_000))   <span class="hljs-comment"># 167.77 GB</span>

<span class="hljs-comment"># Mistral-7B at 32K</span>
print(kv_gb(<span class="hljs-number">32</span>, <span class="hljs-number">8</span>, <span class="hljs-number">128</span>, <span class="hljs-number">32</span>_000))    <span class="hljs-comment"># 4.19 GB</span>
</code></pre>
<hr />
<h2 id="heading-how-to-read-this-from-a-huggingface-config">How to read this from a HuggingFace config</h2>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> transformers <span class="hljs-keyword">import</span> AutoConfig

cfg = AutoConfig.from_pretrained(<span class="hljs-string">"mistralai/Mistral-7B-v0.1"</span>)
L = cfg.num_hidden_layers
H = cfg.num_key_value_heads
d = cfg.hidden_size // cfg.num_attention_heads

T = <span class="hljs-number">128</span>_000
kv_gb = <span class="hljs-number">2</span> * L * H * d * T * <span class="hljs-number">2</span> / <span class="hljs-number">1e9</span>
print(<span class="hljs-string">f"KV cache at <span class="hljs-subst">{T//<span class="hljs-number">1000</span>}</span>K tokens: <span class="hljs-subst">{kv_gb:<span class="hljs-number">.1</span>f}</span> GB"</span>)
</code></pre>
<hr />
<h2 id="heading-the-practical-takeaway">The practical takeaway</h2>
<p>For a 7B-class model, KV cache at 128K is already 16 GB — as large as a mid-range GPU total memory. At 1M tokens, it is 128 GB. The model weights themselves (in FP16) are only ~14 GB.</p>
<p>For 70B+ models, KV cache dominates entirely. Long-context inference on large models without compression is a multi-GPU problem by default.</p>
<p>NexusQuant makes it a single-GPU problem.</p>
<pre><code class="lang-bash">pip install nexusquant-kv
</code></pre>
<p>GitHub: <a target="_blank" href="https://github.com/jagmarques/nexusquant">https://github.com/jagmarques/nexusquant</a></p>
<hr />
<p>Best regards, João Marques</p>
]]></content:encoded></item><item><title><![CDATA[How to benchmark NexusQuant on your own model]]></title><description><![CDATA[Running benchmarks on someone else's hardware tells you very little. This guide shows you how to measure NexusQuant's impact on your model, your data, and your hardware in under 15 minutes.

Prerequisites
pip install nexusquant-kv transformers torch ...]]></description><link>https://nexusquant.hashnode.dev/how-to-benchmark-nexusquant-on-your-own-model</link><guid isPermaLink="true">https://nexusquant.hashnode.dev/how-to-benchmark-nexusquant-on-your-own-model</guid><dc:creator><![CDATA[João André Marques]]></dc:creator><pubDate>Tue, 07 Apr 2026 14:36:47 GMT</pubDate><content:encoded><![CDATA[<p>Running benchmarks on someone else's hardware tells you very little. This guide shows you how to measure NexusQuant's impact on <strong>your</strong> model, <strong>your</strong> data, and <strong>your</strong> hardware in under 15 minutes.</p>
<hr />
<h2 id="heading-prerequisites">Prerequisites</h2>
<pre><code class="lang-bash">pip install nexusquant-kv transformers torch datasets
</code></pre>
<p>You need a HuggingFace causal LM (any model using split-half RoPE — every Llama, Mistral, Qwen, and Phi variant since 2023).</p>
<hr />
<h2 id="heading-step-1-load-your-model">Step 1: Load your model</h2>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> torch
<span class="hljs-keyword">from</span> transformers <span class="hljs-keyword">import</span> AutoTokenizer, AutoModelForCausalLM

MODEL_NAME = <span class="hljs-string">"mistralai/Mistral-7B-v0.1"</span>  <span class="hljs-comment"># replace with yours</span>

tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
model = AutoModelForCausalLM.from_pretrained(
    MODEL_NAME,
    torch_dtype=torch.float16,
    device_map=<span class="hljs-string">"auto"</span>,
)
model.eval()
</code></pre>
<hr />
<h2 id="heading-step-2-compute-baseline-perplexity">Step 2: Compute baseline perplexity</h2>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> math
<span class="hljs-keyword">from</span> datasets <span class="hljs-keyword">import</span> load_dataset

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">compute_ppl</span>(<span class="hljs-params">model, tokenizer, text: str, stride: int = <span class="hljs-number">512</span></span>) -&gt; float:</span>
    encodings = tokenizer(text, return_tensors=<span class="hljs-string">"pt"</span>)
    input_ids = encodings.input_ids.to(model.device)
    seq_len = input_ids.size(<span class="hljs-number">1</span>)
    max_len = model.config.max_position_embeddings
    nlls = []
    prev_end = <span class="hljs-number">0</span>
    <span class="hljs-keyword">for</span> begin <span class="hljs-keyword">in</span> range(<span class="hljs-number">0</span>, seq_len, stride):
        end = min(begin + max_len, seq_len)
        target_len = end - prev_end
        input_chunk = input_ids[:, begin:end]
        target_chunk = input_chunk.clone()
        target_chunk[:, :-target_len] = <span class="hljs-number">-100</span>
        <span class="hljs-keyword">with</span> torch.no_grad():
            loss = model(input_chunk, labels=target_chunk).loss
        nlls.append(loss.item() * target_len)
        prev_end = end
        <span class="hljs-keyword">if</span> end == seq_len:
            <span class="hljs-keyword">break</span>
    <span class="hljs-keyword">return</span> math.exp(sum(nlls) / seq_len)

dataset = load_dataset(<span class="hljs-string">"wikitext"</span>, <span class="hljs-string">"wikitext-2-raw-v1"</span>, split=<span class="hljs-string">"test"</span>)
corpus = <span class="hljs-string">" "</span>.join(dataset[<span class="hljs-string">"text"</span>][:<span class="hljs-number">200</span>])

baseline_ppl = compute_ppl(model, tokenizer, corpus)
print(<span class="hljs-string">f"Baseline PPL: <span class="hljs-subst">{baseline_ppl:<span class="hljs-number">.4</span>f}</span>"</span>)
</code></pre>
<hr />
<h2 id="heading-step-3-apply-nexusquant-at-each-preset">Step 3: Apply NexusQuant at each preset</h2>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> nexusquant <span class="hljs-keyword">import</span> nexusquant_evict

<span class="hljs-keyword">for</span> preset <span class="hljs-keyword">in</span> [<span class="hljs-string">"high"</span>, <span class="hljs-string">"balanced"</span>, <span class="hljs-string">"max"</span>]:
    <span class="hljs-keyword">with</span> nexusquant_evict(model, quality=preset):
        nq_ppl = compute_ppl(model, tokenizer, corpus)
    compression = {<span class="hljs-string">"high"</span>: <span class="hljs-string">"10x"</span>, <span class="hljs-string">"balanced"</span>: <span class="hljs-string">"17x"</span>, <span class="hljs-string">"max"</span>: <span class="hljs-string">"33x"</span>}[preset]
    delta = ((nq_ppl - baseline_ppl) / baseline_ppl) * <span class="hljs-number">100</span>
    print(<span class="hljs-string">f"[<span class="hljs-subst">{preset:<span class="hljs-number">8</span>s}</span>] compression=<span class="hljs-subst">{compression}</span>  PPL=<span class="hljs-subst">{nq_ppl:<span class="hljs-number">.4</span>f}</span>  delta=<span class="hljs-subst">{delta:+<span class="hljs-number">.2</span>f}</span>%"</span>)
</code></pre>
<p>Expected output on Mistral-7B:</p>
<pre><code>[high    ] compression=<span class="hljs-number">10</span>x  PPL=...  delta=+<span class="hljs-number">0.4</span>%
[balanced] compression=<span class="hljs-number">17</span>x  PPL=...  delta=+<span class="hljs-number">1.3</span>%
[max     ] compression=<span class="hljs-number">33</span>x  PPL=...  delta=+<span class="hljs-number">2.6</span>%
</code></pre><hr />
<h2 id="heading-step-4-test-on-your-own-text">Step 4: Test on your own text</h2>
<pre><code class="lang-python"><span class="hljs-keyword">with</span> open(<span class="hljs-string">"my_domain_corpus.txt"</span>) <span class="hljs-keyword">as</span> f:
    my_text = f.read()

<span class="hljs-keyword">for</span> preset <span class="hljs-keyword">in</span> [<span class="hljs-string">"high"</span>, <span class="hljs-string">"balanced"</span>, <span class="hljs-string">"max"</span>]:
    <span class="hljs-keyword">with</span> nexusquant_evict(model, quality=preset):
        ppl = compute_ppl(model, tokenizer, my_text)
    print(<span class="hljs-string">f"<span class="hljs-subst">{preset}</span>: <span class="hljs-subst">{ppl:<span class="hljs-number">.4</span>f}</span>"</span>)
</code></pre>
<p>Domain-specific text (legal, medical, code) may show different degradation curves than wikitext. <strong>Always benchmark on text representative of your actual use case.</strong></p>
<hr />
<h2 id="heading-what-to-look-for">What to look for</h2>
<ul>
<li><strong>delta &lt; +1%</strong> at <code>high</code> preset → your model is a good fit, safe for production</li>
<li><strong>delta &gt; +5%</strong> at any preset → open an issue with your model name</li>
<li><strong>balanced is usually the sweet spot</strong> — 17x compression for ~1% quality cost</li>
</ul>
<pre><code class="lang-bash">pip install nexusquant-kv
</code></pre>
<p>GitHub: <a target="_blank" href="https://github.com/jagmarques/nexusquant">https://github.com/jagmarques/nexusquant</a></p>
<hr />
<p>Best regards, João Marques</p>
]]></content:encoded></item><item><title><![CDATA[What I Learned Testing 12 Compression Approaches That Failed]]></title><description><![CDATA[What I Learned Testing 12 Compression Approaches That Failed
The most useful research I've done this year isn't in the NexusQuant paper. It's the experiments that failed, the ideas that sounded smart in theory and didn't survive contact with real KV ...]]></description><link>https://nexusquant.hashnode.dev/what-i-learned-testing-12-compression-approaches-that-failed</link><guid isPermaLink="true">https://nexusquant.hashnode.dev/what-i-learned-testing-12-compression-approaches-that-failed</guid><dc:creator><![CDATA[João André Marques]]></dc:creator><pubDate>Tue, 07 Apr 2026 13:59:55 GMT</pubDate><content:encoded><![CDATA[<h1 id="heading-what-i-learned-testing-12-compression-approaches-that-failed">What I Learned Testing 12 Compression Approaches That Failed</h1>
<p>The most useful research I've done this year isn't in the NexusQuant paper. It's the experiments that failed, the ideas that sounded smart in theory and didn't survive contact with real KV cache data.</p>
<p>Negative results build trust. They also save time — if you're working on KV cache compression, this list might save you weeks of effort.</p>
<p>Each entry: what we tried, what we expected, what happened, what we learned.</p>
<hr />
<h2 id="heading-1-pca-rotation-3x-worse-distortion">1. PCA Rotation (3x Worse Distortion)</h2>
<p><strong>The idea:</strong> Apply PCA to KV vectors to align the quantization axes with the data's principal components. This is optimal for Gaussian data — principal components diagonalize the covariance matrix, and uniform quantization along them minimizes MSE.</p>
<p><strong>What happened:</strong> 3x worse distortion than Hadamard rotation. PPL degradation jumped ~0.9 percentage points at the same compression ratio.</p>
<p><strong>Why:</strong> PCA is computed per-layer from calibration data. KV distributions shift by layer depth, head index, token position, and input domain. The PCA axes fit the calibration statistics, not the inference statistics. Hadamard rotation, being data-free and orthogonal, spreads energy uniformly without overfitting. It's less optimal in theory but more robust in practice.</p>
<p><strong>Lesson:</strong> Data-free rotations outperform data-fitted rotations when distribution shift is unavoidable.</p>
<hr />
<h2 id="heading-2-group-size-32-compression-ratio-penalty">2. Group Size 32 (Compression Ratio Penalty)</h2>
<p><strong>The idea:</strong> Use group size 32 instead of 8 for per-group scaling. Larger groups reduce scale overhead — from 2 bits/dim (group-8) to 0.5 bits/dim (group-32).</p>
<p><strong>What happened:</strong> The quality drop was larger than expected (+0.4% PPL at the same nominal bit rate). The compression improvement was real but smaller than the accounting suggested, because zstd compresses the scale stream efficiently anyway.</p>
<p><strong>Why:</strong> Group-32 means one scale covers 4x more variance. After Hadamard, variance is equalized globally but not perfectly at the group level. Group-32 is more sensitive to the residual non-uniformity. The E8 lattice's quantization error grows with scale mismatch.</p>
<p><strong>Lesson:</strong> The "free" bits from larger groups aren't free — they trade against quantization accuracy nonlinearly.</p>
<hr />
<h2 id="heading-3-adaptive-bitwidth-allocation-negligible-gain">3. Adaptive Bitwidth Allocation (Negligible Gain)</h2>
<p><strong>The idea:</strong> Assign 4-bit quantization to "important" KV vectors (high attention weight) and 2-bit to unimportant ones. Budget the average to 3 bits. Better attention quality where it matters.</p>
<p><strong>What happened:</strong> +0.05% PPL improvement over flat 3-bit. Not worth the complexity — requires attention weights at quantization time, adds a routing pass, complicates the bit-packing scheme.</p>
<p><strong>Why:</strong> After token eviction, the surviving tokens are already the important ones. The remaining low-importance tokens were mostly evicted. So adaptive bitwidth is redundant — eviction already did the importance filtering.</p>
<p><strong>Lesson:</strong> Token eviction and quantization solve related problems. Don't do both; pick one per axis (count vs precision).</p>
<hr />
<h2 id="heading-4-per-head-token-eviction-catastrophic">4. Per-Head Token Eviction (Catastrophic)</h2>
<p><strong>The idea:</strong> Each attention head attends to different tokens. Instead of using a consensus importance score across heads, evict <em>different</em> tokens per head based on that head's individual attention pattern.</p>
<p><strong>What happened:</strong> +47% PPL degradation. Completely broken.</p>
<p><strong>Why:</strong> The KV cache serves all heads simultaneously. If head A evicts token 42 and head B keeps it, you need two separate caches — or you corrupt head B's keys/values. We implemented the wrong version first (corrupting B's cache). The correct implementation requires separate storage per head, which multiplies memory overhead, defeating the compression goal.</p>
<p><strong>Lesson:</strong> KV cache is shared infrastructure. Eviction must operate on the shared sequence, not per-head slices.</p>
<hr />
<h2 id="heading-5-token-merging-temporal-predictive-coding-107-ppl">5. Token Merging (Temporal Predictive Coding) (+107% PPL)</h2>
<p><strong>The idea:</strong> Instead of evicting tokens, <em>merge</em> similar adjacent tokens into one representative vector. Saves memory while theoretically preserving all information.</p>
<p><strong>What happened:</strong> +107% PPL degradation on Mistral-7B at 60% merge rate. Completely unusable.</p>
<p><strong>Why:</strong> Token positions carry semantic meaning. Merging token i and i+1 into their mean destroys the positional distinction. When the model generates the next token, it uses Q·K attention over the merged KV, which now corresponds to no specific position. RoPE embeddings make this worse — each position has a distinct rotation, and the mean of two rotated vectors is not a valid rotated vector.</p>
<p>We tried removing RoPE before merging — still +31% PPL. The positional information is baked into the content, not just the rotation.</p>
<p><strong>Lesson:</strong> Token position is semantic, not just a coordinate. Merging destroys it.</p>
<hr />
<h2 id="heading-6-entropy-coded-indices-without-delta-coding-2x-compression-loss">6. Entropy-Coded Indices Without Delta Coding (2x Compression Loss)</h2>
<p><strong>The idea:</strong> Apply Huffman or arithmetic coding directly to E8 lattice indices. The index distribution is non-uniform, so entropy coding should compress well.</p>
<p><strong>What happened:</strong> ~1.4x compression from the entropy coder alone vs. the expected ~2.5x.</p>
<p><strong>Why:</strong> Raw lattice indices have high entropy because each token's KV vector is quantized independently. Adjacent tokens produce different indices even for similar semantic content. The distribution is wide and moderately uniform.</p>
<p>Delta coding first — subtracting each token's index from the previous token's index — produces a distribution tightly peaked at 0 (most tokens change little). <em>Then</em> entropy coding achieves 2.3x. The pipeline order matters enormously.</p>
<p><strong>Lesson:</strong> Statistics of compressed representations are not the statistics of the raw data. Profile what you're actually compressing.</p>
<hr />
<h2 id="heading-7-outlier-aware-quantization-wasted-on-kv">7. Outlier-Aware Quantization (Wasted on KV)</h2>
<p><strong>The idea:</strong> Popular in weight quantization (SmoothQuant, LLM.int8()) — identify outlier channels, scale them separately, quantize the rest normally. Should handle the heavy tails in KV data.</p>
<p><strong>What happened:</strong> +0.02% improvement over standard Hadamard + E8. Negligible.</p>
<p><strong>Why:</strong> KV cache outliers exist, but Hadamard rotation already spreads them across all dimensions. By the time E8 sees the data, there are no outlier <em>channels</em> — just mildly non-uniform overall magnitude. Outlier-specific treatment addresses a problem that Hadamard already solved.</p>
<p><strong>Lesson:</strong> Don't apply weight-quantization techniques to KV quantization without checking whether the problem definition is the same.</p>
<hr />
<h2 id="heading-8-low-rank-key-approximation-quality-cliff">8. Low-Rank Key Approximation (Quality Cliff)</h2>
<p><strong>The idea:</strong> Compute SVD of the key cache, keep the top-r singular values/vectors. Aggressive rank reduction = high compression. No quantization needed.</p>
<p><strong>What happened:</strong> Worked well at r ≥ 64 (head_dim=128, so 50% rank). At r=32, quality fell off a cliff: +8% PPL. At r=16: +40% PPL.</p>
<p><strong>Why:</strong> KV caches do not have a clean low-rank structure across the time dimension. They can have low-rank structure within a head's channel dimension, but the rank-r approximation confuses these two. Also, SVD is expensive — O(n²r) per layer per generation step.</p>
<p><strong>Lesson:</strong> Dimensionality reduction across the token axis and the channel axis are different problems. SVD on the full cache conflates both.</p>
<hr />
<h2 id="heading-9-fp8-quantization-too-naive">9. FP8 Quantization (Too Naive)</h2>
<p><strong>The idea:</strong> Just cast KV to FP8 (e5m2 or e4m3). Modern GPUs support FP8 natively. Fast, hardware-accelerated, 2x from FP16.</p>
<p><strong>What happened:</strong> +0.8% PPL for only 2x compression. E8 lattice at comparable bit rate is 0.4% PPL with 7-8x compression.</p>
<p><strong>Why:</strong> FP8 is 8 bits per element. E8 achieves ~2-3 effective bits per element after entropy coding. FP8 is not a compression technique — it's a lower-precision storage format. The 2x ratio only matches FP16 → FP8, while E8 is targeting FP16 → 2.5 effective bits.</p>
<p><strong>Lesson:</strong> Comparing "FP8" to "2-bit E8" conflates precision reduction with compression. They're solving different parts of the same problem.</p>
<hr />
<h2 id="heading-10-importance-scoring-by-l2-norm-wrong-signal">10. Importance Scoring by L2 Norm (Wrong Signal)</h2>
<p><strong>The idea:</strong> Evict tokens with small L2 norm — small keys probably don't attract attention.</p>
<p><strong>What happened:</strong> +12% PPL at 40% eviction. Much worse than cross-head attention scoring.</p>
<p><strong>Why:</strong> L2 norm doesn't predict attention weight. A small-norm key can still attract high attention if the corresponding query also points in that direction. Attention weight is Q·K / √d, not |K|. Using |K| alone ignores Q entirely.</p>
<p><strong>Lesson:</strong> Importance is relational (Q, K together), not intrinsic to K alone.</p>
<hr />
<h2 id="heading-11-dynamic-eviction-during-generation-latency-catastrophe">11. Dynamic Eviction During Generation (Latency Catastrophe)</h2>
<p><strong>The idea:</strong> Re-score and re-evict tokens every N generation steps. Adaptive to changing attention patterns as generation progresses.</p>
<p><strong>What happened:</strong> 8x inference latency increase. Throughput collapsed.</p>
<p><strong>Why:</strong> Re-scoring requires running the importance computation over the full current KV cache (O(seq²) per step). At 100K tokens, this dominates the generation step. We had it right the first time: evict once after prefill, never again during generation.</p>
<p><strong>Lesson:</strong> Prefill and decode phases have fundamentally different compute budgets. Prefill is O(n²). Decode is O(n). Don't move O(n²) work into the decode loop.</p>
<hr />
<h2 id="heading-12-calibration-data-for-quantization-scales-overfitting">12. Calibration Data for Quantization Scales (Overfitting)</h2>
<p><strong>The idea:</strong> Instead of computing per-group scales from the current KV values, pre-compute scales from a calibration corpus and reuse them. Saves compute at inference.</p>
<p><strong>What happened:</strong> +1.9% PPL on out-of-domain data. Scales tuned on Wikipedia performed poorly on code or dialogue.</p>
<p><strong>Why:</strong> KV magnitude profiles are domain-specific. Code tokens, dialogue, and factual text have different KV distributions at different layers. Per-inference scaling, though slightly more expensive, adapts to the actual data being compressed.</p>
<p><strong>Lesson:</strong> KV cache is input-dependent. Calibration data solves the wrong version of the problem. TurboQuant's approach (calibration for <em>weight</em> quantization, not KV) is fine because weights don't change with input.</p>
<hr />
<h2 id="heading-what-actually-works">What Actually Works</h2>
<p>After 12 failures, the surviving pipeline is:</p>
<ol>
<li>Cross-head attention importance scoring → token eviction (2.5x from fewer tokens)</li>
<li>RoPE removal on keys</li>
<li>Hadamard rotation</li>
<li>E8 lattice VQ, group-8, per-group FP16 scales (3-8x from fewer bits)</li>
<li>Delta coding → zstd (2-3x from entropy coding)</li>
</ol>
<p>Five stages, each justified by a measurable improvement that survived ablation. Everything else, we tried and measured and deleted.</p>
<p>The cleaner version with just eviction + E8 (no entropy coding) still achieves 10x at +0.4% PPL. That's the "high quality" preset. The full pipeline gets to 33x.</p>
<p>Repo: <a target="_blank" href="https://github.com/jagmarques/nexusquant">github.com/jagmarques/nexusquant</a></p>
<pre><code class="lang-bash">pip install nexusquant-kv
</code></pre>
<p>Best regards, João Marques</p>
]]></content:encoded></item><item><title><![CDATA[The Math Behind E8 Lattice Quantization (with Code)]]></title><description><![CDATA[The Math Behind E8 Lattice Quantization (with Code)
Standard scalar quantization — what every LLM quantizer from GPTQ to AWQ does — rounds each number independently to the nearest representable value. E8 lattice quantization rounds groups of 8 number...]]></description><link>https://nexusquant.hashnode.dev/the-math-behind-e8-lattice-quantization-with-code</link><guid isPermaLink="true">https://nexusquant.hashnode.dev/the-math-behind-e8-lattice-quantization-with-code</guid><dc:creator><![CDATA[João André Marques]]></dc:creator><pubDate>Tue, 07 Apr 2026 13:59:44 GMT</pubDate><content:encoded><![CDATA[<h1 id="heading-the-math-behind-e8-lattice-quantization-with-code">The Math Behind E8 Lattice Quantization (with Code)</h1>
<p>Standard scalar quantization — what every LLM quantizer from GPTQ to AWQ does — rounds each number independently to the nearest representable value. E8 lattice quantization rounds groups of 8 numbers <em>jointly</em> to the nearest point on a mathematical lattice. The difference sounds subtle. It isn't.</p>
<p>This post is a complete walkthrough of how E8 quantization works, why it beats scalar quantization by ~30% in distortion, and exactly what the algorithm does line by line.</p>
<h2 id="heading-why-lattices">Why Lattices?</h2>
<p>The core problem in quantization is <em>sphere packing</em>. You want to cover n-dimensional space with the fewest representable points, such that any real vector is "close" to at least one codebook entry.</p>
<p>For 1D scalar quantization, you're placing points on a number line. Easy — evenly space them.</p>
<p>For 8D vector quantization, you want to pack 8D balls as densely as possible. The densest known packing in 8 dimensions is the <strong>E8 root lattice</strong>, proven optimal by Maryna Viazovska in 2016 (she won the Fields Medal for it in 2022). Its packing density is 2x better than any irregular arrangement.</p>
<p>What this means for quantization: for the same number of bits, E8 introduces 30% less distortion than optimal scalar quantization, and about 1.4 dB better signal-to-noise ratio.</p>
<h2 id="heading-the-e8-lattice-briefly">The E8 Lattice, Briefly</h2>
<p>The E8 lattice is the set of all 8D vectors that satisfy <em>either</em> of these two conditions:</p>
<ol>
<li>All coordinates are integers, and their sum is even</li>
<li>All coordinates are half-integers (x.5 values), and their sum is an integer (even or odd)</li>
</ol>
<p>That's the whole definition. A lattice, not a codebook. No learned vectors, no storage.</p>
<p>The packing radius is 1/√2. Every point in ℝ⁸ is within distance 1/√2 of some E8 lattice point.</p>
<h2 id="heading-the-conway-sloane-nearest-point-algorithm">The Conway-Sloane Nearest-Point Algorithm</h2>
<p>Finding the nearest E8 point to an arbitrary input x is what you need for quantization. The algorithm, from Conway and Sloane (1982), is:</p>
<ol>
<li>Find the nearest point in each of the two cosets separately</li>
<li>Return whichever is closer to x</li>
</ol>
<p><strong>Coset 1: Integer lattice with even coordinate sum (D8)</strong></p>
<p>Round each coordinate to the nearest integer. If the sum is already even, done. If odd, flip the coordinate where x[i] was farthest from its rounded value (i.e., we pay the smallest extra cost to make the sum even).</p>
<p><strong>Coset 2: Half-integer lattice (D8 + (0.5,...,0.5))</strong></p>
<p>Subtract 0.5 from x, round each coordinate, add 0.5 back. Apply the same even-sum correction.</p>
<p><strong>Pick the winner:</strong></p>
<p>Compute squared distance to each coset's candidate. Return the closer one.</p>
<h2 id="heading-python-implementation">Python Implementation</h2>
<p>Here's the full <code>nearest_point</code> function from NexusQuant's <code>e8_lattice.py</code>:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> torch

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">nearest_point</span>(<span class="hljs-params">x: torch.Tensor</span>) -&gt; torch.Tensor:</span>
    <span class="hljs-string">"""Find nearest E8 lattice point to each 8D vector.

    Args:
        x: (..., 8) tensor
    Returns:
        Nearest E8 lattice point, same shape
    """</span>
    <span class="hljs-comment"># --- Coset 1: integer lattice with even coordinate sum ---</span>
    r_int = x.round()
    sums = r_int.sum(dim=<span class="hljs-number">-1</span>)
    odd = (sums % <span class="hljs-number">2</span> != <span class="hljs-number">0</span>)

    <span class="hljs-keyword">if</span> odd.any():
        <span class="hljs-comment"># Find the coordinate we can flip at minimum cost</span>
        gaps = (x - r_int).abs()
        idx = gaps[odd].argmin(dim=<span class="hljs-number">-1</span>)          <span class="hljs-comment"># cheapest coordinate to adjust</span>
        fix = torch.zeros_like(r_int[odd])
        fix.scatter_(<span class="hljs-number">-1</span>, idx.unsqueeze(<span class="hljs-number">-1</span>), <span class="hljs-number">1.0</span>)
        <span class="hljs-comment"># Which direction to flip? Toward x, not away</span>
        sign = ((x[odd] - r_int[odd]).gather(<span class="hljs-number">-1</span>, idx.unsqueeze(<span class="hljs-number">-1</span>)) &gt;= <span class="hljs-number">0</span>).float() * <span class="hljs-number">2</span> - <span class="hljs-number">1</span>
        r_int[odd] = r_int[odd] + fix * sign

    <span class="hljs-comment"># --- Coset 2: half-integer lattice ---</span>
    r_half = (x - <span class="hljs-number">0.5</span>).round() + <span class="hljs-number">0.5</span>
    sums_h = r_half.sum(dim=<span class="hljs-number">-1</span>)

    <span class="hljs-comment"># <span class="hljs-doctag">NOTE:</span> strict E8 requires even integer sum for the half-integer coset.</span>
    <span class="hljs-comment"># We intentionally relax this — see the "Relaxed Parity" section below.</span>
    odd_h = ((sums_h * <span class="hljs-number">2</span>).round() % <span class="hljs-number">2</span> != <span class="hljs-number">0</span>)

    <span class="hljs-keyword">if</span> odd_h.any():
        gaps_h = (x - r_half).abs()
        idx_h = gaps_h[odd_h].argmin(dim=<span class="hljs-number">-1</span>)
        fix_h = torch.zeros_like(r_half[odd_h])
        fix_h.scatter_(<span class="hljs-number">-1</span>, idx_h.unsqueeze(<span class="hljs-number">-1</span>), <span class="hljs-number">1.0</span>)
        sign_h = ((x[odd_h] - r_half[odd_h]).gather(<span class="hljs-number">-1</span>, idx_h.unsqueeze(<span class="hljs-number">-1</span>)) &gt;= <span class="hljs-number">0</span>).float() * <span class="hljs-number">2</span> - <span class="hljs-number">1</span>
        r_half[odd_h] = r_half[odd_h] + fix_h * sign_h

    <span class="hljs-comment"># --- Pick closer coset ---</span>
    d_int  = ((x - r_int)  ** <span class="hljs-number">2</span>).sum(dim=<span class="hljs-number">-1</span>)
    d_half = ((x - r_half) ** <span class="hljs-number">2</span>).sum(dim=<span class="hljs-number">-1</span>)

    res = r_int.clone()
    res[d_half &lt; d_int] = r_half[d_half &lt; d_int]
    <span class="hljs-keyword">return</span> res
</code></pre>
<p>The full quantizer wraps this with per-group scaling:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">quantize</span>(<span class="hljs-params">x: torch.Tensor, levels: int = <span class="hljs-number">8</span></span>) -&gt; torch.Tensor:</span>
    <span class="hljs-string">"""Quantize tensor with E8 lattice VQ and per-group scaling.

    levels: 4 = 2-bit, 8 = 3-bit, 16 = 4-bit
    """</span>
    shape = x.shape
    pad = (<span class="hljs-number">8</span> - shape[<span class="hljs-number">-1</span>] % <span class="hljs-number">8</span>) % <span class="hljs-number">8</span>
    <span class="hljs-keyword">if</span> pad &gt; <span class="hljs-number">0</span>:
        x = torch.nn.functional.pad(x, (<span class="hljs-number">0</span>, pad))

    flat = x.reshape(<span class="hljs-number">-1</span>, <span class="hljs-number">8</span>)
    amax = flat.abs().amax(dim=<span class="hljs-number">-1</span>, keepdim=<span class="hljs-literal">True</span>).clamp(min=<span class="hljs-number">1e-8</span>)
    sc = amax / (levels / <span class="hljs-number">2</span>)          <span class="hljs-comment"># scale each 8D group to [-levels/2, levels/2]</span>

    lp = nearest_point(flat / sc)     <span class="hljs-comment"># find nearest E8 point in normalized space</span>
    lp = lp.clamp(-levels / <span class="hljs-number">2</span>, levels / <span class="hljs-number">2</span>)
    result = (lp * sc).reshape(x.shape)

    <span class="hljs-keyword">if</span> pad &gt; <span class="hljs-number">0</span>:
        result = result[..., :shape[<span class="hljs-number">-1</span>]]
    <span class="hljs-keyword">return</span> result.reshape(shape)
</code></pre>
<h2 id="heading-why-per-group-scaling">Why Per-Group Scaling?</h2>
<p>Raw KV values span a wide dynamic range. A single global scale would waste most of the representable range on the quiet regions and saturate the loud ones.</p>
<p>Per-group scaling (one FP16 scale per 8 values) costs 2 bytes / 8 values = 2 bits per value of overhead — the same as the lattice encoding itself. This doubles the effective bits. But the Hadamard rotation applied upstream equalizes variance across dimensions, so within each 8D group the dynamic range is small. The overhead is worth it.</p>
<p>Alternatively, <code>quantize_perhead</code> uses one scale per full 128D head vector. This cuts scale overhead to 0.125 bits/dim at a cost of ~0.07% more PPL degradation. The headroom depends on how well Hadamard equalizes your specific data.</p>
<h2 id="heading-the-relaxed-parity-discovery">The Relaxed Parity Discovery</h2>
<p>This is the part that surprised us.</p>
<p>Strict E8 theory says the half-integer coset must also have an even sum. When we first implemented this and tested on actual KV cache data, the strict version performed <em>worse</em> than the relaxed version — by 0.3-0.4% PPL.</p>
<p>Why? KV cache distributions are sub-Gaussian (lighter tails than Gaussian, more mass near zero). The strict parity constraint over-regularizes the half-integer coset for this regime. Relaxing it effectively adds beneficial dithering: the quantizer can reach slightly off-lattice half-integer points that happen to sit closer to the true KV values.</p>
<p>The original E8 construction targets uniform or Gaussian distributions. Real transformer attention data is neither. The relaxed constraint is a free 0.3% quality improvement, just by removing three lines of enforcement code.</p>
<p>This is the kind of result that only shows up when you measure empirically rather than trusting the textbook.</p>
<h2 id="heading-bit-rate-accounting">Bit Rate Accounting</h2>
<p>At 3 bits per dimension with per-group scales (group size 8):</p>
<ul>
<li>Lattice codes: 3 bits × 8 dims = 24 bits per group</li>
<li>Scale (FP16): 16 bits per group</li>
<li>Total: 40 bits / 8 dims = <strong>5 bits per dimension</strong> raw storage</li>
</ul>
<p>After delta coding + zstd compression on the lattice indices (consecutive tokens produce similar codes; deltas compress 2-3x):</p>
<ul>
<li>Effective rate: ~2 bits per dimension</li>
</ul>
<p>That's 16 bits FP16 → 2 bits effective = <strong>8x compression</strong> from quantization alone. Combined with 2.5x token eviction: <strong>~17x total</strong> (the "balanced" preset).</p>
<h2 id="heading-running-it">Running It</h2>
<pre><code class="lang-bash">pip install nexusquant-kv
</code></pre>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> nexusquant.core.e8_lattice <span class="hljs-keyword">import</span> E8Lattice
<span class="hljs-keyword">import</span> torch

<span class="hljs-comment"># Quantize any tensor — works for arbitrary shape</span>
x = torch.randn(<span class="hljs-number">100</span>, <span class="hljs-number">128</span>)  <span class="hljs-comment"># 100 KV vectors, 128-dim heads</span>
q = E8Lattice.quantize(x, levels=<span class="hljs-number">8</span>)  <span class="hljs-comment"># 3-bit E8</span>

print(<span class="hljs-string">f"MSE: <span class="hljs-subst">{((x - q)**<span class="hljs-number">2</span>).mean():<span class="hljs-number">.6</span>f}</span>"</span>)
print(<span class="hljs-string">f"Relative error: <span class="hljs-subst">{((x - q)**<span class="hljs-number">2</span>).sum() / (x**<span class="hljs-number">2</span>).sum() * <span class="hljs-number">100</span>:<span class="hljs-number">.2</span>f}</span>%"</span>)

<span class="hljs-comment"># Or per-head scaling (less overhead, slightly more error)</span>
q2 = E8Lattice.quantize_perhead(x, levels=<span class="hljs-number">8</span>)
</code></pre>
<h2 id="heading-what-makes-this-hard-in-practice">What Makes This Hard in Practice</h2>
<p>Three things the textbook doesn't tell you:</p>
<ol>
<li><p><strong>The Hadamard rotation is load-bearing.</strong> Without it, KV vectors have heavy outliers in specific dimensions (the same phenomenon that makes LLM quantization hard). E8 performs poorly on unrotated data. Hadamard spreads the energy uniformly so the per-group scale assumption holds.</p>
</li>
<li><p><strong>RoPE removal matters.</strong> Rotary position embeddings make adjacent tokens' keys look "rotated" relative to each other. Removing RoPE before quantization reduces inter-token correlation and cuts quantization error ~0.7% PPL.</p>
</li>
<li><p><strong>GPU kernels are unfinished.</strong> The current implementation dequantizes immediately for compatibility. Real memory savings require keeping the compressed representation on-device, which needs a Triton kernel to pack/unpack E8 codes on the fly. That's the main engineering gap.</p>
</li>
</ol>
<p>Repo: <a target="_blank" href="https://github.com/jagmarques/nexusquant">github.com/jagmarques/nexusquant</a></p>
<p>Best regards, João Marques</p>
]]></content:encoded></item><item><title><![CDATA[How Much GPU Memory Does NexusQuant Actually Save?]]></title><description><![CDATA[How Much GPU Memory Does NexusQuant Actually Save?
KV cache compression numbers like "10x" sound impressive in a paper. But what does that mean in practice, for a real GPU, serving real users? Let me give you a concrete memory calculator so you can a...]]></description><link>https://nexusquant.hashnode.dev/how-much-gpu-memory-does-nexusquant-actually-save</link><guid isPermaLink="true">https://nexusquant.hashnode.dev/how-much-gpu-memory-does-nexusquant-actually-save</guid><dc:creator><![CDATA[João André Marques]]></dc:creator><pubDate>Tue, 07 Apr 2026 13:59:37 GMT</pubDate><content:encoded><![CDATA[<h1 id="heading-how-much-gpu-memory-does-nexusquant-actually-save">How Much GPU Memory Does NexusQuant Actually Save?</h1>
<p>KV cache compression numbers like "10x" sound impressive in a paper. But what does that mean in practice, for a real GPU, serving real users? Let me give you a concrete memory calculator so you can answer this for your own setup.</p>
<h2 id="heading-the-kv-cache-formula">The KV Cache Formula</h2>
<p>For any transformer, the KV cache size is:</p>
<pre><code>KV_bytes = <span class="hljs-number">2</span> × num_layers × num_heads × head_dim × seq_len × bytes_per_element
</code></pre><p>The <code>2</code> is for keys AND values. <code>bytes_per_element</code> is 2 for FP16, 4 for FP32.</p>
<p>For <strong>Mistral-7B</strong> (32 layers, 8 KV heads, head_dim=128, FP16):</p>
<pre><code>KV_bytes = <span class="hljs-number">2</span> × <span class="hljs-number">32</span> × <span class="hljs-number">8</span> × <span class="hljs-number">128</span> × seq_len × <span class="hljs-number">2</span>
         = <span class="hljs-number">131</span>,<span class="hljs-number">072</span> × seq_len bytes
         ≈ <span class="hljs-number">128</span> KB per token
</code></pre><p>At 128K tokens: <strong>128 KB × 131,072 = 16.7 GB</strong> just for the KV cache.</p>
<h2 id="heading-gpu-memory-table">GPU Memory Table</h2>
<p>Here's what that means on real hardware:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>GPU</td><td>VRAM</td><td>Max KV tokens (FP16, no NQ)</td><td>With NexusQuant 10x</td><td>With NexusQuant 17x</td><td>With NexusQuant 33x</td></tr>
</thead>
<tbody>
<tr>
<td>RTX 3090</td><td>24 GB</td><td>~150K</td><td>~1.5M</td><td>~2.6M</td><td>~5M</td></tr>
<tr>
<td>A10G</td><td>24 GB</td><td>~150K</td><td>~1.5M</td><td>~2.6M</td><td>~5M</td></tr>
<tr>
<td>A100 40GB</td><td>40 GB</td><td>~256K</td><td>~2.6M</td><td>~4.4M</td><td>~8.5M</td></tr>
<tr>
<td>A100 80GB</td><td>80 GB</td><td>~512K</td><td>~5.1M</td><td>~8.7M</td><td>~17M</td></tr>
<tr>
<td>H100 80GB</td><td>80 GB</td><td>~512K</td><td>~5.1M</td><td>~8.7M</td><td>~17M</td></tr>
</tbody>
</table>
</div><p><em>Numbers leave 8 GB headroom for model weights and activations on 7B models.</em></p>
<h2 id="heading-practical-scenarios">Practical Scenarios</h2>
<p><strong>Scenario A: A10G serving 128K context</strong></p>
<p>Without NexusQuant: 128K × 128 KB = 16.7 GB. With only 24 GB total and ~14 GB for the 7B model, you're OOM before the first token generates.</p>
<p>With NexusQuant 17x preset: 16.7 / 17 = 0.98 GB for KV. Fits comfortably. You can now serve 128K context on a single A10G.</p>
<p><strong>Scenario B: A100 80GB, maximize throughput</strong></p>
<p>A single request at 128K takes 16.7 GB KV. With 80 GB total and ~14 GB for weights, you have ~66 GB for KV — enough for about 4 concurrent sessions at 128K.</p>
<p>At 17x compression, each session uses ~1 GB of KV. You can serve <strong>~66 concurrent sessions</strong> instead of 4. That's 16x more users per GPU.</p>
<p><strong>Scenario C: Long context research</strong></p>
<p>You want 1M token context on Mistral-7B. KV cache = 1M × 128 KB = 128 GB. Impossible on a single GPU.</p>
<p>At 17x: 7.5 GB. Fits on a single A100 40GB alongside the model weights.</p>
<h2 id="heading-python-calculator">Python Calculator</h2>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">kv_cache_gb</span>(<span class="hljs-params">
    num_layers: int,
    num_kv_heads: int,
    head_dim: int,
    seq_len: int,
    dtype_bytes: int = <span class="hljs-number">2</span>,  <span class="hljs-comment"># FP16</span>
</span>) -&gt; float:</span>
    <span class="hljs-string">"""Returns KV cache size in GB."""</span>
    total_bytes = <span class="hljs-number">2</span> * num_layers * num_kv_heads * head_dim * seq_len * dtype_bytes
    <span class="hljs-keyword">return</span> total_bytes / (<span class="hljs-number">1024</span> ** <span class="hljs-number">3</span>)


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">savings_report</span>(<span class="hljs-params">model_name: str, num_layers: int, num_kv_heads: int,
                   head_dim: int, seq_len: int, gpu_vram_gb: float,
                   model_weights_gb: float</span>) -&gt; <span class="hljs-keyword">None</span>:</span>
    base_gb = kv_cache_gb(num_layers, num_kv_heads, head_dim, seq_len)
    available = gpu_vram_gb - model_weights_gb

    print(<span class="hljs-string">f"\n=== <span class="hljs-subst">{model_name}</span> @ <span class="hljs-subst">{seq_len:,}</span> tokens ==="</span>)
    print(<span class="hljs-string">f"Base KV cache: <span class="hljs-subst">{base_gb:<span class="hljs-number">.1</span>f}</span> GB"</span>)
    print(<span class="hljs-string">f"GPU available for KV (after weights): <span class="hljs-subst">{available:<span class="hljs-number">.0</span>f}</span> GB"</span>)

    <span class="hljs-keyword">for</span> ratio, label <span class="hljs-keyword">in</span> [(<span class="hljs-number">10</span>, <span class="hljs-string">"high quality"</span>), (<span class="hljs-number">17</span>, <span class="hljs-string">"balanced"</span>), (<span class="hljs-number">33</span>, <span class="hljs-string">"max"</span>)]:
        compressed_gb = base_gb / ratio
        concurrent = int(available / compressed_gb)
        fits = <span class="hljs-string">"YES"</span> <span class="hljs-keyword">if</span> base_gb / ratio &lt; available <span class="hljs-keyword">else</span> <span class="hljs-string">"NO"</span>
        print(<span class="hljs-string">f"  <span class="hljs-subst">{ratio}</span>x (<span class="hljs-subst">{label}</span>): <span class="hljs-subst">{compressed_gb:<span class="hljs-number">.2</span>f}</span> GB | fits=<span class="hljs-subst">{fits}</span> | concurrent sessions=<span class="hljs-subst">{concurrent}</span>"</span>)


<span class="hljs-comment"># Mistral-7B examples</span>
savings_report(<span class="hljs-string">"Mistral-7B"</span>, <span class="hljs-number">32</span>, <span class="hljs-number">8</span>, <span class="hljs-number">128</span>, <span class="hljs-number">128</span>_000, <span class="hljs-number">80</span>, <span class="hljs-number">14</span>)
savings_report(<span class="hljs-string">"Mistral-7B"</span>, <span class="hljs-number">32</span>, <span class="hljs-number">8</span>, <span class="hljs-number">128</span>, <span class="hljs-number">1</span>_000_000, <span class="hljs-number">80</span>, <span class="hljs-number">14</span>)

<span class="hljs-comment"># Llama-3-8B</span>
savings_report(<span class="hljs-string">"Llama-3-8B"</span>, <span class="hljs-number">32</span>, <span class="hljs-number">8</span>, <span class="hljs-number">128</span>, <span class="hljs-number">128</span>_000, <span class="hljs-number">40</span>, <span class="hljs-number">16</span>)

<span class="hljs-comment"># Llama-3-70B (multi-GPU: assume 160 GB total)</span>
savings_report(<span class="hljs-string">"Llama-3-70B"</span>, <span class="hljs-number">80</span>, <span class="hljs-number">8</span>, <span class="hljs-number">128</span>, <span class="hljs-number">128</span>_000, <span class="hljs-number">160</span>, <span class="hljs-number">140</span>)
</code></pre>
<p>Sample output:</p>
<pre><code>=== Mistral<span class="hljs-number">-7</span>B @ <span class="hljs-number">128</span>,<span class="hljs-number">000</span> tokens ===
Base KV cache: <span class="hljs-number">16.7</span> GB
GPU available <span class="hljs-keyword">for</span> KV (after weights): <span class="hljs-number">66</span> GB
  <span class="hljs-number">10</span>x (high quality): <span class="hljs-number">1.67</span> GB | fits=YES | concurrent sessions=<span class="hljs-number">39</span>
  <span class="hljs-number">17</span>x (balanced): <span class="hljs-number">0.98</span> GB | fits=YES | concurrent sessions=<span class="hljs-number">67</span>
  <span class="hljs-number">33</span>x (max): <span class="hljs-number">0.51</span> GB | fits=YES | concurrent sessions=<span class="hljs-number">129</span>

=== Mistral<span class="hljs-number">-7</span>B @ <span class="hljs-number">1</span>,<span class="hljs-number">000</span>,<span class="hljs-number">000</span> tokens ===
Base KV cache: <span class="hljs-number">130.4</span> GB
GPU available <span class="hljs-keyword">for</span> KV (after weights): <span class="hljs-number">66</span> GB
  <span class="hljs-number">10</span>x (high quality): <span class="hljs-number">13.04</span> GB | fits=YES | concurrent sessions=<span class="hljs-number">5</span>
  <span class="hljs-number">17</span>x (balanced): <span class="hljs-number">7.67</span> GB | fits=YES | concurrent sessions=<span class="hljs-number">8</span>
  <span class="hljs-number">33</span>x (max): <span class="hljs-number">3.95</span> GB | fits=YES | concurrent sessions=<span class="hljs-number">16</span>

=== Llama<span class="hljs-number">-3</span><span class="hljs-number">-8</span>B @ <span class="hljs-number">128</span>,<span class="hljs-number">000</span> tokens ===
Base KV cache: <span class="hljs-number">16.7</span> GB
GPU available <span class="hljs-keyword">for</span> KV (after weights): <span class="hljs-number">24</span> GB
  <span class="hljs-number">10</span>x (high quality): <span class="hljs-number">1.67</span> GB | fits=YES | concurrent sessions=<span class="hljs-number">14</span>
  <span class="hljs-number">17</span>x (balanced): <span class="hljs-number">0.98</span> GB | fits=YES | concurrent sessions=<span class="hljs-number">24</span>
  <span class="hljs-number">33</span>x (max): <span class="hljs-number">0.51</span> GB | fits=YES | concurrent sessions=<span class="hljs-number">47</span>
</code></pre><h2 id="heading-the-model-weight-overhead">The Model Weight Overhead</h2>
<p>One thing papers often gloss over: the model itself takes memory. Here are real-world numbers:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Model</td><td>Weights (BF16/FP16)</td></tr>
</thead>
<tbody>
<tr>
<td>TinyLlama-1.1B</td><td>2.2 GB</td></tr>
<tr>
<td>Mistral-7B</td><td>14 GB</td></tr>
<tr>
<td>Llama-3-8B</td><td>16 GB</td></tr>
<tr>
<td>Llama-3-70B</td><td>140 GB (2× A100 80GB)</td></tr>
<tr>
<td>Llama-3-405B</td><td>~810 GB</td></tr>
</tbody>
</table>
</div><p>At 70B+, KV cache compression becomes less about fitting the model and more about fitting the context. A 70B model with 128K context uses only ~2x its own weight in KV cache. NexusQuant is still valuable here, but the math changes — you're extending context headroom, not primarily enabling the model to load.</p>
<h2 id="heading-what-we-measured">What We Measured</h2>
<p>All numbers in NexusQuant are validated with <code>torch.cuda.memory_allocated()</code> before and after cache materialization. No estimate-only ratios. The paper's compression column includes all overhead: scales (FP16), lattice indices (packed uint8), and metadata. If it's not in memory, it's not in the ratio.</p>
<p>Repo: <a target="_blank" href="https://github.com/jagmarques/nexusquant">github.com/jagmarques/nexusquant</a></p>
<pre><code class="lang-bash">pip install nexusquant-kv
</code></pre>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> nexusquant <span class="hljs-keyword">import</span> nexusquant_evict

<span class="hljs-keyword">with</span> nexusquant_evict(model, quality=<span class="hljs-string">"balanced"</span>):  <span class="hljs-comment"># 17x</span>
    output = model.generate(input_ids, max_new_tokens=<span class="hljs-number">512</span>)
</code></pre>
<p>Best regards, João Marques</p>
]]></content:encoded></item><item><title><![CDATA[How to deploy NexusQuant in production (and what's missing)]]></title><description><![CDATA[This post is a practical deployment guide. Install, configuration, how to pick the right eviction rate, domain testing, and an honest list of what does not work yet.

Install
pip install nexusquant

Requires Python 3.9+, PyTorch 2.1+, and Transformer...]]></description><link>https://nexusquant.hashnode.dev/how-to-deploy-nexusquant-in-production-and-whats-missing</link><guid isPermaLink="true">https://nexusquant.hashnode.dev/how-to-deploy-nexusquant-in-production-and-whats-missing</guid><dc:creator><![CDATA[João André Marques]]></dc:creator><pubDate>Tue, 07 Apr 2026 13:29:36 GMT</pubDate><content:encoded><![CDATA[<p>This post is a practical deployment guide. Install, configuration, how to pick the right eviction rate, domain testing, and an honest list of what does not work yet.</p>
<hr />
<h2 id="heading-install">Install</h2>
<pre><code class="lang-bash">pip install nexusquant
</code></pre>
<p>Requires Python 3.9+, PyTorch 2.1+, and Transformers 4.40+. No CUDA-specific wheels — it runs on CPU for small models and on CUDA for production workloads.</p>
<hr />
<h2 id="heading-the-one-liner">The one-liner</h2>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> nexusquant <span class="hljs-keyword">import</span> nexusquant_evict

<span class="hljs-keyword">with</span> nexusquant_evict(model, quality=<span class="hljs-string">"balanced"</span>):
    output = model.generate(input_ids, max_new_tokens=<span class="hljs-number">500</span>)
</code></pre>
<p>That is it. The context manager hooks into the model's forward pass, intercepts the KV cache after prefill, compresses it, and restores the original hooks on exit.</p>
<hr />
<h2 id="heading-quality-presets">Quality presets</h2>
<p>Three presets, pick based on your use case:</p>
<pre><code class="lang-python"><span class="hljs-comment"># Conservative: 35% eviction, 10x compression</span>
<span class="hljs-comment"># Use for: general-purpose, long prompts (&gt;1K tokens), production default</span>
<span class="hljs-keyword">with</span> nexusquant_evict(model, quality=<span class="hljs-string">"conservative"</span>):
    ...

<span class="hljs-comment"># Balanced: 60% eviction, 16x compression</span>
<span class="hljs-comment"># Use for: RAG over structured documents, short-to-medium prompts (&lt;1.5K tokens)</span>
<span class="hljs-keyword">with</span> nexusquant_evict(model, quality=<span class="hljs-string">"balanced"</span>):
    ...

<span class="hljs-comment"># Aggressive: 80% eviction, 32x compression</span>
<span class="hljs-comment"># Use for: memory-constrained environments, factual recall only</span>
<span class="hljs-comment"># Do NOT use for: prompts &gt;1K tokens, creative tasks, multi-detail reasoning</span>
<span class="hljs-keyword">with</span> nexusquant_evict(model, quality=<span class="hljs-string">"aggressive"</span>):
    ...

<span class="hljs-comment"># Lossless: no eviction, quantization only</span>
<span class="hljs-comment"># Use for: when you need maximum quality, 6-7x compression</span>
<span class="hljs-keyword">with</span> nexusquant_evict(model, quality=<span class="hljs-string">"lossless"</span>):
    ...
</code></pre>
<hr />
<h2 id="heading-how-to-choose-eviction-rate-for-your-use-case">How to choose eviction rate for your use case</h2>
<p>Do not guess. Test on your actual data. Here is a script:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> torch
<span class="hljs-keyword">from</span> transformers <span class="hljs-keyword">import</span> AutoModelForCausalLM, AutoTokenizer
<span class="hljs-keyword">from</span> nexusquant <span class="hljs-keyword">import</span> nexusquant_evict

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">measure_ppl</span>(<span class="hljs-params">model, tokenizer, texts, quality</span>):</span>
    total_loss = <span class="hljs-number">0</span>
    count = <span class="hljs-number">0</span>
    <span class="hljs-keyword">for</span> text <span class="hljs-keyword">in</span> texts:
        ids = tokenizer(text, return_tensors=<span class="hljs-string">"pt"</span>).input_ids.cuda()
        <span class="hljs-keyword">with</span> nexusquant_evict(model, quality=quality):
            <span class="hljs-keyword">with</span> torch.no_grad():
                out = model(ids, labels=ids)
        total_loss += out.loss.item()
        count += <span class="hljs-number">1</span>
    <span class="hljs-keyword">return</span> (total_loss / count)

<span class="hljs-comment"># Load your model</span>
model = AutoModelForCausalLM.from_pretrained(<span class="hljs-string">"mistralai/Mistral-7B-v0.1"</span>).cuda()
tokenizer = AutoTokenizer.from_pretrained(<span class="hljs-string">"mistralai/Mistral-7B-v0.1"</span>)

<span class="hljs-comment"># Sample 20 texts from YOUR domain</span>
your_texts = [...]  <span class="hljs-comment"># replace with real samples</span>

<span class="hljs-comment"># Baseline</span>
baseline_loss = measure_ppl(model, tokenizer, your_texts, quality=<span class="hljs-string">"lossless"</span>)

<span class="hljs-comment"># Test presets</span>
<span class="hljs-keyword">for</span> quality <span class="hljs-keyword">in</span> [<span class="hljs-string">"conservative"</span>, <span class="hljs-string">"balanced"</span>, <span class="hljs-string">"aggressive"</span>]:
    loss = measure_ppl(model, tokenizer, your_texts, quality=quality)
    delta_pct = (loss - baseline_loss) / baseline_loss * <span class="hljs-number">100</span>
    print(<span class="hljs-string">f"<span class="hljs-subst">{quality}</span>: PPL delta = <span class="hljs-subst">{delta_pct:+<span class="hljs-number">.2</span>f}</span>%"</span>)
</code></pre>
<p>Run this with 20-50 samples from your actual domain. If "balanced" gives &gt; 2% PPL delta on your data, drop to "conservative".</p>
<hr />
<h2 id="heading-domain-sensitivity-what-to-watch-for">Domain sensitivity: what to watch for</h2>
<p>From our experiments, here is what correlates with compression difficulty:</p>
<p><strong>Compresses well (use balanced or aggressive):</strong></p>
<ul>
<li>Dense factual prose (academic papers, encyclopedia entries)</li>
<li>Structured technical documentation</li>
<li>Formal legal or financial text</li>
</ul>
<p><strong>Compresses poorly (use conservative or lossless):</strong></p>
<ul>
<li>Creative/narrative text (fiction, stories)</li>
<li>Casual conversational text</li>
<li>Code with unusual identifier names</li>
<li>Mixed-language text</li>
</ul>
<p>The attention patterns on creative text are more diffuse — fewer tokens dominate — so eviction hurts more. The quantization also hurts more because KV values for creative text have less structured distribution.</p>
<p>Here are our measured numbers at 500-token prefix on Mistral-7B:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Domain</td><td>35% evict</td><td>70% evict</td></tr>
</thead>
<tbody>
<tr>
<td>Academic</td><td>+0.39%</td><td>+4.81%</td></tr>
<tr>
<td>Technical</td><td>+0.90%</td><td>+3.87%</td></tr>
<tr>
<td>Creative</td><td>+2.48%</td><td>+4.62%</td></tr>
</tbody>
</table>
</div><p>Note that creative text at 70% eviction is only slightly worse than 35% eviction (4.62% vs 2.48%). The first eviction step hurts creative text proportionally more than subsequent steps.</p>
<hr />
<h2 id="heading-memory-measurement">Memory measurement</h2>
<p>Do not trust our reported ratios. Measure yourself:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> torch

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">measure_kv_memory</span>(<span class="hljs-params">model, input_ids, quality=None</span>):</span>
    torch.cuda.reset_peak_memory_stats()
    baseline_mem = torch.cuda.memory_allocated()

    <span class="hljs-keyword">if</span> quality:
        <span class="hljs-keyword">with</span> nexusquant_evict(model, quality=quality):
            <span class="hljs-keyword">with</span> torch.no_grad():
                out = model(input_ids, use_cache=<span class="hljs-literal">True</span>)
    <span class="hljs-keyword">else</span>:
        <span class="hljs-keyword">with</span> torch.no_grad():
            out = model(input_ids, use_cache=<span class="hljs-literal">True</span>)

    compressed_mem = torch.cuda.memory_allocated()
    kv_mem = compressed_mem - baseline_mem
    <span class="hljs-keyword">return</span> kv_mem

baseline = measure_kv_memory(model, input_ids, quality=<span class="hljs-literal">None</span>)
compressed = measure_kv_memory(model, input_ids, quality=<span class="hljs-string">"balanced"</span>)
print(<span class="hljs-string">f"Compression ratio: <span class="hljs-subst">{baseline / compressed:<span class="hljs-number">.1</span>f}</span>x"</span>)
</code></pre>
<p>If the ratio you measure differs substantially from what we report, please open a GitHub issue with your model and config.</p>
<hr />
<h2 id="heading-the-latency-caveat">The latency caveat</h2>
<p>NexusQuant is currently CPU-bound on the compression step.</p>
<p>The pipeline compresses the KV cache after prefill. The bottleneck is the E8 VQ nearest-neighbor lookup and the zstd entropy coding, both of which run on CPU. On a Mistral-7B prefill of 1664 tokens:</p>
<ul>
<li>Prefill: ~180ms (GPU)</li>
<li>Compression step: ~340ms (CPU)</li>
<li>Total: ~520ms vs ~180ms baseline</li>
</ul>
<p>This means NexusQuant currently makes your time-to-first-token slower, not faster. The compression saves memory (which enables larger batches or longer contexts), but it adds latency.</p>
<p>The fix is Triton kernels for the VQ and entropy coding steps. We have not written them yet. This is on the roadmap and we will post an update when it is done.</p>
<p>If your use case is memory-bound (fitting more users in GPU memory, extending context length beyond what fits otherwise), NexusQuant solves that today. If your use case is latency-bound (fastest possible TTFT), do not use NexusQuant until the Triton kernels ship.</p>
<hr />
<h2 id="heading-context-length-limits">Context length limits</h2>
<p>Validated context ranges:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Prefix length</td><td>Max eviction</td><td>Max compression</td><td>Safe?</td></tr>
</thead>
<tbody>
<tr>
<td>&lt; 500 tok</td><td>35%</td><td>10x</td><td>Yes</td></tr>
<tr>
<td>500-1664 tok</td><td>60%</td><td>16x</td><td>Yes</td></tr>
<tr>
<td>1664-2924 tok</td><td>35%</td><td>10x</td><td>Yes</td></tr>
<tr>
<td>&gt; 2924 tok</td><td>0% (lossless only)</td><td>6-7x</td><td>Eviction untested</td></tr>
</tbody>
</table>
</div><p>We have not validated eviction on prefixes longer than ~3K tokens beyond the catastrophic failure at 60%. For long-context applications (&gt;3K token prefixes), use <code>quality="lossless"</code> to get quantization-only compression without eviction.</p>
<hr />
<h2 id="heading-model-compatibility">Model compatibility</h2>
<p>Validated:</p>
<ul>
<li>Mistral-7B-v0.1 (MHA)</li>
<li>Llama-3-8B (GQA)</li>
</ul>
<p>Known issues:</p>
<ul>
<li>GPT-NeoX-style models: our RoPE removal assumes split-half rotation (Llama/Mistral style). GPT-NeoX uses interleaved RoPE. It will produce wrong results. Do not use on GPT-NeoX.</li>
<li>Llama-3.1 with extended context: rope_scaling config is not fully handled for context lengths beyond the standard window.</li>
<li>Batch size &gt; 1: there is a bug in NexusQuantSimple where only the first batch element is processed for keys. The HuggingFace context manager (<code>nexusquant_evict</code>) handles this correctly. Use that, not the low-level API.</li>
</ul>
<hr />
<h2 id="heading-what-is-not-there-yet">What is not there yet</h2>
<p>Being direct about gaps:</p>
<ol>
<li><strong>Triton kernels</strong> — compression is CPU-bound, adds ~340ms latency. Critical for production.</li>
<li><strong>16K+ context</strong> — not validated above 3K token prefixes.</li>
<li><strong>Eviction for batch &gt; 1</strong> — the low-level API has a bug here; context manager is fine.</li>
<li><strong>LongBench</strong> — proper long-context benchmark not yet run.</li>
<li><strong>Multi-model presets</strong> — the quality presets are tuned for Mistral/Llama. Other architectures may need different defaults.</li>
</ol>
<p>The core use case that works well today: Mistral or Llama family model, prefill up to ~1.7K tokens, memory-bound deployment (fitting more requests per GPU), with quality=conservative or quality=balanced.</p>
<hr />
<p>Best regards, João Marques</p>
]]></content:encoded></item><item><title><![CDATA[NexusQuant vs KVTC vs TurboQuant vs CommVQ — honest comparison]]></title><description><![CDATA[There are now enough KV cache compression papers that "we beat the competition" is meaningless without specifics. Which competition? On which data? At which compression ratio? With or without calibration?
This post is an honest head-to-head. For each...]]></description><link>https://nexusquant.hashnode.dev/nexusquant-vs-kvtc-vs-turboquant-vs-commvq-honest-comparison</link><guid isPermaLink="true">https://nexusquant.hashnode.dev/nexusquant-vs-kvtc-vs-turboquant-vs-commvq-honest-comparison</guid><dc:creator><![CDATA[João André Marques]]></dc:creator><pubDate>Tue, 07 Apr 2026 13:29:31 GMT</pubDate><content:encoded><![CDATA[<p>There are now enough KV cache compression papers that "we beat the competition" is meaningless without specifics. Which competition? On which data? At which compression ratio? With or without calibration?</p>
<p>This post is an honest head-to-head. For each competitor: what they do, their reported numbers from their papers, our numbers, where we win, and where they win.</p>
<hr />
<h2 id="heading-the-comparison-table">The comparison table</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Method</td><td>Compression</td><td>Quality</td><td>Training-free</td><td>When it wins</td></tr>
</thead>
<tbody>
<tr>
<td>NexusQuant (short ctx)</td><td>10x</td><td>+0.14 to +0.90%</td><td>Yes</td><td>Training-free above 6x</td></tr>
<tr>
<td>NexusQuant (long ctx)</td><td>16.6x</td><td>+0.82%</td><td>Yes</td><td>Best training-free at 16x</td></tr>
<tr>
<td>KVTC (NVIDIA)</td><td>up to 20x</td><td>&lt; 1pp</td><td>No (10 min cal)</td><td>Highest compression</td></tr>
<tr>
<td>TurboQuant (Google)</td><td>~5-6x</td><td>~0%</td><td>Yes</td><td>Best quality at low compression</td></tr>
<tr>
<td>CommVQ (Apple)</td><td>~8x</td><td>~0%</td><td>No (training)</td><td>Best quality at 8x</td></tr>
<tr>
<td>Palu</td><td>11.4x</td><td>~+1.19%</td><td>No (calibration)</td><td>Low-rank if you have calibration data</td></tr>
</tbody>
</table>
</div><p>None of these numbers are from our experiments. KVTC, TurboQuant, CommVQ, and Palu numbers are from their papers. We have not run their code on our data. That is a limitation we are transparent about.</p>
<hr />
<h2 id="heading-turboquant-google">TurboQuant (Google)</h2>
<p><strong>What it does:</strong> Scalar quantization with learned per-channel scale factors. 193 lines of code, training-free at the core. The simplest competitive approach.</p>
<p><strong>Their reported numbers:</strong> Near-zero quality degradation at 5.3x compression on Llama-class models.</p>
<p><strong>Our numbers at the same point:</strong> NexusQuant at 6.71x on Llama-3-8B shows -1.20% PPL (a quality improvement). At 5.3x it would be better still.</p>
<p><strong>Where TurboQuant wins:</strong> Quality at low compression (below 6x). If you need minimal quality loss and 4-6x compression, TurboQuant is extremely competitive and simpler than our pipeline.</p>
<p><strong>Where we win:</strong> Compression beyond 6x. TurboQuant does not have a token eviction component, so it cannot reach 10-16x ratios. We extend that range significantly while staying training-free.</p>
<p><strong>The honest caveat:</strong> We have not run TurboQuant on our exact dataset with our exact evaluation setup. The 193-line simplicity is genuinely elegant. We have a lot of respect for this work.</p>
<hr />
<h2 id="heading-kvtc-nvidia">KVTC (NVIDIA)</h2>
<p><strong>What it does:</strong> KVTC combines scalar quantization with a temporal coherence coding approach, compressing differences between adjacent KV states. Requires ~10 minutes of calibration on sample data.</p>
<p><strong>Their reported numbers:</strong> Up to 20x compression with less than 1 perplexity point degradation.</p>
<p><strong>Our numbers at 20x:</strong> We have validated 16.6x at +0.82% PPL. At 20x (80% eviction on 1664-token prefix), we get +2.13% PPL. KVTC appears to achieve better quality at the extreme end.</p>
<p><strong>Where KVTC wins:</strong> Quality at 20x+ compression. Their temporal coding approach is genuinely effective. And 20x &gt; 16.6x — they reach higher ratios with better quality in that regime.</p>
<p><strong>Where we win:</strong> No calibration required. The 10-minute calibration step sounds trivial but matters in practice — it requires representative data, it adds a deployment step, and it means the system is not truly drop-in. Our <code>with nexusquant_evict(model):</code> requires no data, no calibration, nothing except the model.</p>
<p><strong>The honest caveat:</strong> KVTC's 20x result is the competition's strongest. We are not the best at maximum compression.</p>
<hr />
<h2 id="heading-commvq-apple">CommVQ (Apple)</h2>
<p><strong>What it does:</strong> Trains a vector quantization codebook on KV cache data with an EM-style training loop. Uses a commutative design that works naturally with RoPE embeddings.</p>
<p><strong>Their reported numbers:</strong> ~8x compression with near-zero quality degradation. The commutative approach is elegant — they avoid the RoPE removal issue entirely by designing the quantizer to commute with the rotation.</p>
<p><strong>Our numbers at 8x:</strong> We reach 10.1x at +0.90% (500-tok prefix) or 10.4x at +0.14% (1664-tok prefix). Quality at pure 8x would be even better.</p>
<p><strong>Where CommVQ wins:</strong> Quality. If you can afford training time and have representative KV cache data, a trained codebook will beat a training-free approach. Also their RoPE handling is mathematically cleaner than our blanket RoPE-removal strategy.</p>
<p><strong>Where we win:</strong> Training-free. CommVQ requires a training loop. Our pipeline works on any model from the moment you install the package. No training data, no training time.</p>
<p><strong>The honest caveat:</strong> We solve RoPE by removing it before quantization and reapplying after. CommVQ solves it properly at the algebra level. If we revisit this design, their approach is the right one.</p>
<hr />
<h2 id="heading-palu">Palu</h2>
<p><strong>What it does:</strong> Low-rank projection of the KV cache heads, combining PCA-based dimensionality reduction with quantization. Needs calibration data to compute the PCA rotation.</p>
<p><strong>Their reported numbers:</strong> 11.4x compression at ~+1.19% PPL. The low-rank projection is a different approach — instead of quantizing all dimensions, they reduce the number of dimensions.</p>
<p><strong>Our numbers at comparable compression:</strong> 10.4x at +0.14% PPL (1664-tok prefix). Our quality is substantially better at similar compression.</p>
<p><strong>Where Palu wins:</strong> If you have calibration data and want a single-architecture low-rank approach, Palu integrates cleanly with fine-tuning workflows.</p>
<p><strong>Where we win:</strong> Quality at 10-11x. +0.14% vs +1.19% is a significant gap. Also training-free for the base pipeline.</p>
<hr />
<h2 id="heading-the-main-competitive-claim">The main competitive claim</h2>
<p>We are the best training-free option above 6x compression. Below 6x, TurboQuant matches or beats us with a simpler system. Above 6x and up to ~16x, we are the only training-free approach with validated sub-1% quality degradation.</p>
<p>If you need to go to 20x+, KVTC is currently ahead (with calibration). If you need near-zero quality loss at 8x and can train, CommVQ is the right choice.</p>
<p>None of this is cherry-picked. The numbers above are the numbers.</p>
<hr />
<h2 id="heading-what-we-cannot-claim">What we cannot claim</h2>
<p>We have not run any competitor's code on our exact dataset. All competitor numbers come from their papers. It is possible that on wikitext-2 with our specific passage selection, TurboQuant or KVTC would show different numbers than reported. We are aware of this limitation and plan to run a proper head-to-head in our NeurIPS submission.</p>
<p>We also cannot claim "beats TurboQuant" in any general sense. We beat their reported numbers at high compression ratios. At low compression, their simplicity is competitive.</p>
<hr />
<p>Best regards, João Marques</p>
]]></content:encoded></item><item><title><![CDATA[NexusQuant benchmarks: every number, honestly]]></title><description><![CDATA[When you build a KV cache compression system and plan to publish a paper, you face a choice: present the best-looking numbers, or present all of them.
We chose all of them. This post is every benchmark result we have, including the ones that did not ...]]></description><link>https://nexusquant.hashnode.dev/nexusquant-benchmarks-every-number-honestly</link><guid isPermaLink="true">https://nexusquant.hashnode.dev/nexusquant-benchmarks-every-number-honestly</guid><dc:creator><![CDATA[João André Marques]]></dc:creator><pubDate>Tue, 07 Apr 2026 13:29:25 GMT</pubDate><content:encoded><![CDATA[<p>When you build a KV cache compression system and plan to publish a paper, you face a choice: present the best-looking numbers, or present all of them.</p>
<p>We chose all of them. This post is every benchmark result we have, including the ones that did not work.</p>
<hr />
<h2 id="heading-the-pipeline">The pipeline</h2>
<p>Quick context. NexusQuant compresses the KV cache of transformer models at inference time, training-free:</p>
<pre><code>Prefill → Key-Key Attention Score → Evict → RoPE-remove → Hadamard → <span class="hljs-number">2</span>-bit E8 VQ → Temporal Delta → zstd
</code></pre><p>The context manager API:</p>
<pre><code class="lang-python"><span class="hljs-keyword">with</span> nexusquant_evict(model, quality=<span class="hljs-string">"balanced"</span>):
    output = model.generate(input_ids, max_new_tokens=<span class="hljs-number">200</span>)
</code></pre>
<p>All numbers below are from an A10G GPU (24 GB). Perplexity delta is measured against the uncompressed baseline on the same passages.</p>
<hr />
<h2 id="heading-mistral-7b-the-full-picture">Mistral-7B: the full picture</h2>
<p>These are our numbers at different prefix lengths and eviction rates. Every row is real.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Prefix</td><td>Evict%</td><td>Compression</td><td>PPL Delta</td><td>Verdict</td></tr>
</thead>
<tbody>
<tr>
<td>500 tok</td><td>35%</td><td>10.1x</td><td>+0.90%</td><td>Usable for most tasks</td></tr>
<tr>
<td>1664 tok</td><td>35%</td><td>10.4x</td><td>+0.14%</td><td>Near-lossless</td></tr>
<tr>
<td>1664 tok</td><td>60%</td><td>16.6x</td><td>+0.82%</td><td>Strong at long context</td></tr>
<tr>
<td>1664 tok</td><td>80%</td><td>32.7x</td><td>+2.13%</td><td>Maximum compression</td></tr>
<tr>
<td>2924 tok</td><td>35%</td><td>10.5x</td><td>+1.50%</td><td>Text-dependent</td></tr>
<tr>
<td>2924 tok</td><td>60%+</td><td>—</td><td>+42%+</td><td>CATASTROPHIC</td></tr>
</tbody>
</table>
</div><p>The 3K catastrophe is real and we are not hiding it. At 2924-token prefixes, evicting 60% or more of KV tokens causes perplexity to blow up to +42% and beyond. This is not a scorer bug. We tried four different attention-score variants; all fail at this eviction rate for this prefix length. It is a fundamental capacity loss: the model simply cannot attend to the right tokens when that much context is gone.</p>
<p>If you are deploying NexusQuant on prompts longer than ~2K tokens, use <code>quality="conservative"</code> (35% eviction maximum).</p>
<hr />
<h2 id="heading-llama-3-8b-the-surprise">Llama-3-8B: the surprise</h2>
<p>We validated on a second architecture to make sure Mistral results were not flukes. What we found was unexpected.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Config</td><td>Compression</td><td>PPL Delta</td><td>Notes</td></tr>
</thead>
<tbody>
<tr>
<td>2-bit, no evict</td><td>6.71x</td><td>-1.20%</td><td>Lower PPL with compression</td></tr>
<tr>
<td>2-bit + 35% evict</td><td>10.25x</td><td>-1.47%</td><td>Better than baseline</td></tr>
<tr>
<td>2-bit + 60% evict</td><td>16.48x</td><td>-1.35%</td><td>Still better</td></tr>
<tr>
<td>2-bit + 80% evict</td><td>32.45x</td><td>-0.61%</td><td>Still better</td></tr>
</tbody>
</table>
</div><p>All four configurations have negative perplexity delta. Compression is making the model perform better than the uncompressed baseline on the wikitext-2 distribution.</p>
<p>Our hypothesis: Llama-3-8B uses grouped-query attention (GQA). The compression and eviction act as a regularizer that suppresses noise in the attention pattern. The GQA architecture appears to have redundancy in its KV cache that quantization removes beneficially. This is a standalone publishable finding that we did not anticipate.</p>
<hr />
<h2 id="heading-domain-sensitivity">Domain sensitivity</h2>
<p>Not all text compresses equally. We tested three text domains at 500-token prefixes on Mistral-7B:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Domain</td><td>35% evict</td><td>70% evict</td><td>80% evict</td></tr>
</thead>
<tbody>
<tr>
<td>Academic</td><td>+0.39%</td><td>+4.81%</td><td>+6.58%</td></tr>
<tr>
<td>Technical</td><td>+0.90%</td><td>+3.87%</td><td>+6.09%</td></tr>
<tr>
<td>Creative/narrative</td><td>+2.48%</td><td>+4.62%</td><td>+4.73%</td></tr>
</tbody>
</table>
</div><p>Academic text (dense factual prose, repetitive structure) compresses best. Creative/narrative text is the hardest — at 35% eviction you already lose 2.48%, compared to 0.39% on academic text.</p>
<p>If you are building a RAG system over structured documents, the 35% eviction preset works well. If you are doing creative writing assistance or summarising fiction, use <code>quality="lossless"</code> (no eviction) or test your specific domain before deploying.</p>
<hr />
<h2 id="heading-downstream-task-evaluation-mistral-7b-5-tasks">Downstream task evaluation (Mistral-7B, 5 tasks)</h2>
<p>Perplexity is a proxy. We ran five QA-style downstream tasks with and without compression.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Task type</td><td>10x (35% evict)</td><td>16x (60% evict)</td></tr>
</thead>
<tbody>
<tr>
<td>Factual recall</td><td>MATCH</td><td>MATCH</td></tr>
<tr>
<td>Single-hop reasoning</td><td>MATCH</td><td>PARTIAL</td></tr>
<tr>
<td>Multi-detail extraction</td><td>PARTIAL</td><td>PARTIAL</td></tr>
</tbody>
</table>
</div><p>"MATCH" means the compressed model gave the same answer as the uncompressed model. "PARTIAL" means it got the main point but missed a specific detail.</p>
<p>Factual recall is fully preserved at 10x. Nuanced multi-detail questions lose some specificity at both compression levels. Single-hop reasoning holds at 10x but starts slipping at 16x.</p>
<hr />
<h2 id="heading-what-failed-12-approaches-we-killed">What failed (12 approaches we killed)</h2>
<p>We tried things that did not work. Here they are:</p>
<p><strong>Variable-rate E8 (per-group water-filling):</strong> Allocate more bits to high-energy groups. Result: +3.67% PPL at matched rate — 3.3x worse than fixed-rate. Energy does not equal importance.</p>
<p><strong>Cross-layer KV prediction:</strong> Predict layer L+1 from layer L. Result: 0.0 bits saved. Layers project to orthogonal subspaces.</p>
<p><strong>L2 norm as token importance proxy:</strong> Use L2 norm to select which tokens to keep. Result: +207% PPL. L2 norm and attention importance are completely different quantities.</p>
<p><strong>Strict E8 parity enforcement:</strong> Fix our half-integer parity to be mathematically correct E8. Result: 0.3-0.4% worse quality. The relaxed parity acts as dithering on sub-Gaussian KV data.</p>
<p><strong>Token zeroing for eviction:</strong> Set evicted KV to zero. Result: catastrophic attention mass stealing. exp(Q @ 0) = exp(0) = 1 in softmax; evicted tokens steal attention weight. Fix: -inf masking.</p>
<p><strong>Eviction on small models (TinyLlama 1.1B):</strong> 80-90% eviction rate. Result: +322% PPL at 1K tokens, +1195% at 2K. TinyLlama's top 10% of tokens capture only 28% of attention mass vs. 65-88% on Mistral-7B. Eviction only works when attention is sharp enough.</p>
<hr />
<h2 id="heading-honest-compression-numbers">Honest compression numbers</h2>
<p>We made an early mistake: reporting compression ratios without scale overhead. Here is the honest table.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Config</td><td>Naive ratio</td><td>Real ratio (with FP16 scales)</td></tr>
</thead>
<tbody>
<tr>
<td>E8 3-bit</td><td>5.3x</td><td>3.2x</td></tr>
<tr>
<td>E8 3-bit + zstd</td><td>—</td><td>4.0x</td></tr>
<tr>
<td>2-bit + 35% evict</td><td>—</td><td>10.1x</td></tr>
<tr>
<td>2-bit + 60% evict</td><td>—</td><td>16.6x</td></tr>
</tbody>
</table>
</div><p>The 16-bit scale per 8-element group adds 2 bits/element overhead. At 3 bits/element stored, the real cost is 5 bits/element, not 3. We got burned by ignoring this early on. Now every ratio is measured with <code>torch.cuda.memory_allocated()</code> before and after.</p>
<hr />
<h2 id="heading-the-number-that-does-not-exist">The number that does not exist</h2>
<p>One number appeared in an early draft of our paper that cannot be traced to any experiment log: +0.339% at 8x for Mistral-7B. Our validated 50-passage result for that config is +0.47%. We do not know where +0.339% came from. It is not in any research file. It is gone from the paper.</p>
<p>This kind of thing happens. You run an experiment, write a number down, run more experiments, and the earlier number stops being reproducible. If you cannot trace a number to a specific run with a specific config, it does not belong in a paper.</p>
<hr />
<h2 id="heading-what-is-still-missing">What is still missing</h2>
<p>For full honesty:</p>
<ul>
<li>No LongBench results yet (needs multi-hour GPU run on proper dataset)</li>
<li>No latency benchmarks (CPU-bound compression loop, needs Triton kernels)</li>
<li>No 16K+ context validation</li>
<li>Downstream evals above are small-N; full MMLU/GSM8K runs in progress</li>
</ul>
<p>These are gaps we are filling. The numbers above are what we have validated.</p>
<hr />
<p>Best regards, João Marques</p>
]]></content:encoded></item><item><title><![CDATA[Longer contexts are easier to compress (not harder)]]></title><description><![CDATA[Common assumption: longer sequences are harder to compress. Our experiments show the opposite.
The data
Same model (Mistral-7B), same method, same eviction rate. Only prefix length changes:




Prefix35% eviction60% eviction80% eviction



500 tokens...]]></description><link>https://nexusquant.hashnode.dev/longer-contexts-are-easier-to-compress-not-harder</link><guid isPermaLink="true">https://nexusquant.hashnode.dev/longer-contexts-are-easier-to-compress-not-harder</guid><category><![CDATA[Machine Learning]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[João André Marques]]></dc:creator><pubDate>Tue, 07 Apr 2026 13:13:28 GMT</pubDate><content:encoded><![CDATA[<p>Common assumption: longer sequences are harder to compress. Our experiments show the opposite.</p>
<h2 id="heading-the-data">The data</h2>
<p>Same model (Mistral-7B), same method, same eviction rate. Only prefix length changes:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Prefix</td><td>35% eviction</td><td>60% eviction</td><td>80% eviction</td></tr>
</thead>
<tbody>
<tr>
<td>500 tokens</td><td>+0.90%</td><td>+4.5%</td><td>+6.6%</td></tr>
<tr>
<td>1,600 tokens</td><td>+0.14%</td><td>+0.82%</td><td>+2.1%</td></tr>
<tr>
<td>3,500 tokens</td><td>+0.43%</td><td>+1.3%</td><td>+2.6%</td></tr>
</tbody>
</table>
</div><p>At 1,600 tokens, 60% eviction gives +0.82%. At 500 tokens, same rate gives +4.5%. A 5-6x quality improvement from more context.</p>
<h2 id="heading-why">Why</h2>
<p>The importance scorer ranks tokens by attention weight. At 500 tokens, ~30 query positions — attention is flat. At 1,600 tokens, ~100 query positions — attention concentrates on genuinely important tokens.</p>
<p>More data → sharper importance estimates → safer eviction.</p>
<h2 id="heading-what-it-means">What it means</h2>
<p>Production inference runs at thousands of tokens. Short-context benchmarks understate eviction-based compression.</p>
<p><a target="_blank" href="https://github.com/jagmarques/nexusquant">NexusQuant</a> achieves:</p>
<ul>
<li>10x at 500-token context (&lt;1% PPL)</li>
<li>17x at 1,600-token context (&lt;1% PPL)</li>
<li>33x at any length (~2.5% PPL)</li>
</ul>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> nexusquant <span class="hljs-keyword">import</span> nexusquant_evict

<span class="hljs-keyword">with</span> nexusquant_evict(model, quality=<span class="hljs-string">"balanced"</span>):
    output = model.generate(long_input_ids, max_new_tokens=<span class="hljs-number">512</span>)
</code></pre>
<p><a target="_blank" href="https://github.com/jagmarques/nexusquant">GitHub</a> · <a target="_blank" href="https://github.com/jagmarques/nexusquant/blob/main/paper/nexusquant.pdf">Paper</a></p>
<p>Best regards,
João Marques</p>
]]></content:encoded></item><item><title><![CDATA[Why E8 lattice quantization beats scalar quantization for KV caches]]></title><description><![CDATA[Most KV cache quantization methods treat each number independently. This works, but wastes bits.
The E8 lattice quantizes 8 numbers at once. Result: 3x better compression under entropy coding at the same distortion.
Scalar quantization
Scalar INT2 ro...]]></description><link>https://nexusquant.hashnode.dev/why-e8-lattice-quantization-beats-scalar-quantization-for-kv-caches</link><guid isPermaLink="true">https://nexusquant.hashnode.dev/why-e8-lattice-quantization-beats-scalar-quantization-for-kv-caches</guid><category><![CDATA[Mathematics]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[João André Marques]]></dc:creator><pubDate>Tue, 07 Apr 2026 13:13:27 GMT</pubDate><content:encoded><![CDATA[<p>Most KV cache quantization methods treat each number independently. This works, but wastes bits.</p>
<p>The E8 lattice quantizes 8 numbers at once. Result: 3x better compression under entropy coding at the same distortion.</p>
<h2 id="heading-scalar-quantization">Scalar quantization</h2>
<p>Scalar INT2 rounds each of 128 values independently to 4 levels. Indices are near-uniform, so entropy coding barely helps — maybe 1.2x.</p>
<h2 id="heading-e8-quantize-8-at-a-time">E8: quantize 8 at a time</h2>
<p>The E8 root lattice is the densest sphere packing in 8 dimensions. Split each 128-dim vector into 16 groups of 8, snap each to the nearest E8 point.</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> nexusquant.core.e8_lattice <span class="hljs-keyword">import</span> E8Lattice

groups = vector.reshape(<span class="hljs-number">-1</span>, <span class="hljs-number">8</span>)
lattice_points = E8Lattice.nearest_point(groups)
</code></pre>
<h2 id="heading-the-entropy-advantage">The entropy advantage</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Quantizer</td><td>zstd compression on indices</td></tr>
</thead>
<tbody>
<tr>
<td>Scalar INT2</td><td>1.23x</td></tr>
<tr>
<td>E8 2-bit</td><td>3.74x</td></tr>
</tbody>
</table>
</div><p>3x advantage from lattice structure alone.</p>
<h2 id="heading-handling-outliers">Handling outliers</h2>
<p>Raw KV vectors are heavy-tailed. Fix: Hadamard rotation first.</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> nexusquant.core.hadamard <span class="hljs-keyword">import</span> hadamard_matrix

H = hadamard_matrix(<span class="hljs-number">128</span>)
rotated = vector @ H.T
</code></pre>
<p>Orthogonal, no information loss. After rotation, E8 at 2 bits/dim causes less than 0.1% PPL degradation.</p>
<p><a target="_blank" href="https://github.com/jagmarques/nexusquant">NexusQuant on GitHub</a> · <a target="_blank" href="https://github.com/jagmarques/nexusquant/blob/main/paper/nexusquant.pdf">Paper</a></p>
<p>Best regards,
João Marques</p>
]]></content:encoded></item><item><title><![CDATA[Compress your LLM's KV cache 33x with zero training]]></title><description><![CDATA[Running out of GPU memory at long context lengths? The KV cache grows linearly with sequence length — at 128K tokens, a 7B model accumulates over 60 GB of KV state.
I built NexusQuant, a library that compresses the KV cache 10-33x at inference time. ...]]></description><link>https://nexusquant.hashnode.dev/compress-your-llms-kv-cache-33x-with-zero-training</link><guid isPermaLink="true">https://nexusquant.hashnode.dev/compress-your-llms-kv-cache-33x-with-zero-training</guid><category><![CDATA[Machine Learning]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[João André Marques]]></dc:creator><pubDate>Tue, 07 Apr 2026 13:13:26 GMT</pubDate><content:encoded><![CDATA[<p>Running out of GPU memory at long context lengths? The KV cache grows linearly with sequence length — at 128K tokens, a 7B model accumulates over 60 GB of KV state.</p>
<p>I built <a target="_blank" href="https://github.com/jagmarques/nexusquant">NexusQuant</a>, a library that compresses the KV cache 10-33x at inference time. No training, no calibration data, no model changes.</p>
<h2 id="heading-before">Before</h2>
<pre><code class="lang-python"><span class="hljs-comment"># OOM at 32K tokens on a 24GB GPU</span>
output = model.generate(input_ids, max_new_tokens=<span class="hljs-number">512</span>)
</code></pre>
<h2 id="heading-after">After</h2>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> nexusquant <span class="hljs-keyword">import</span> nexusquant_evict

<span class="hljs-keyword">with</span> nexusquant_evict(model, quality=<span class="hljs-string">"balanced"</span>):
    output = model.generate(input_ids, max_new_tokens=<span class="hljs-number">512</span>)
</code></pre>
<p>128K context now fits in the memory that used to hold 7.5K.</p>
<h2 id="heading-how-it-works">How it works</h2>
<ol>
<li>Rank tokens by attention importance</li>
<li>Drop lowest-scoring tokens</li>
<li>Undo rotary position embeddings on keys</li>
<li>Hadamard rotation to spread energy uniformly</li>
<li>E8 lattice quantization (2 bits/dim)</li>
<li>Delta coding + zstd compression</li>
</ol>
<h2 id="heading-numbers">Numbers</h2>
<p>Mistral-7B, A100, FP16, all overhead included:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Preset</td><td>Compression</td><td>PPL change</td></tr>
</thead>
<tbody>
<tr>
<td>high</td><td>10x</td><td>+0.4%</td></tr>
<tr>
<td>balanced</td><td>17x</td><td>+1.3%</td></tr>
<tr>
<td>max</td><td>33x</td><td>+2.6%</td></tr>
</tbody>
</table>
</div><h2 id="heading-comparison">Comparison</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Method</td><td>Ratio</td><td>Quality</td><td>Training</td></tr>
</thead>
<tbody>
<tr>
<td><strong>NexusQuant</strong></td><td><strong>10-33x</strong></td><td><strong>+0.4-2.6%</strong></td><td><strong>No</strong></td></tr>
<tr>
<td>TurboQuant (Google)</td><td>~5-6x</td><td>~0%</td><td>No</td></tr>
<tr>
<td>KVTC (NVIDIA)</td><td>up to 20x</td><td>&lt;1%</td><td>Yes</td></tr>
<tr>
<td>CommVQ (Apple)</td><td>~8x</td><td>~0%</td><td>Yes</td></tr>
</tbody>
</table>
</div><p><a target="_blank" href="https://github.com/jagmarques/nexusquant">GitHub</a> · <a target="_blank" href="https://github.com/jagmarques/nexusquant/blob/main/paper/nexusquant.pdf">Paper</a></p>
<p>Best regards,
João Marques</p>
]]></content:encoded></item></channel></rss>