<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://sridhar-3009.github.io/feed.xml" rel="self" type="application/atom+xml"/><link href="https://sridhar-3009.github.io/" rel="alternate" type="text/html" hreflang="en"/><updated>2026-06-27T07:24:17+00:00</updated><id>https://sridhar-3009.github.io/feed.xml</id><title type="html">Sai Sridhar Tarra</title><subtitle>Machine Learning Engineer at Accenture. Founder of MailAir. Building LLM-powered products and production ML systems. </subtitle><entry><title type="html">Byte-Latent Transformers: Training AI Without a Tokenizer</title><link href="https://sridhar-3009.github.io/blog/2026/byte-latent-transformers/" rel="alternate" type="text/html" title="Byte-Latent Transformers: Training AI Without a Tokenizer"/><published>2026-05-14T00:00:00+00:00</published><updated>2026-05-14T00:00:00+00:00</updated><id>https://sridhar-3009.github.io/blog/2026/byte-latent-transformers</id><content type="html" xml:base="https://sridhar-3009.github.io/blog/2026/byte-latent-transformers/"><![CDATA[<h1 id="byte-latent-transformers-training-ai-without-a-tokenizer">Byte-Latent Transformers: Training AI Without a Tokenizer</h1> <p>Every language model you’ve ever used — GPT-4, Claude, Gemini, LLaMA — starts with the same preprocessing step before a single parameter is touched.</p> <p>A <strong>tokenizer</strong> runs over your input and converts it into a sequence of integer IDs. <code class="language-plaintext highlighter-rouge">"Hello world"</code> becomes something like <code class="language-plaintext highlighter-rouge">[9906, 1917]</code>. The model never sees characters. It sees token IDs drawn from a fixed vocabulary, usually 32K to 128K entries, built offline by running BPE or SentencePiece on a large corpus.</p> <p>This has been the unquestioned default for five years.</p> <p>In late 2024, Meta AI published <strong>“Byte Latent Transformer: Patches Scale Better Than Tokens”</strong> — and questioned it.</p> <p>The core claim: you can train a competitive language model directly on raw bytes, no tokenizer required, and at the same compute budget it matches or beats LLaMA 3 on most benchmarks. On tasks that expose tokenization’s weaknesses — character manipulation, rare languages, structured data — it wins clearly.</p> <p>This post covers why tokenization has always been a compromise, how BLT replaces it, and why this architecture matters.</p> <hr/> <h2 id="tldr">TL;DR</h2> <ul> <li>Tokenizers are a <strong>lossy, brittle preprocessing step</strong> baked into every modern LLM</li> <li>BLT processes <strong>raw bytes</strong> — no vocabulary, no OOV, no tokenization artifacts</li> <li>Groups bytes into <strong>variable-length patches</strong> based on information content (entropy)</li> <li>Architecture: Local Encoder → Latent Transformer → Local Decoder</li> <li><strong>Matches LLaMA 3</strong> at equal FLOP budget; beats it on character-level tasks</li> <li>Especially strong on <strong>multilingual text, code, structured data, and novel formats</strong></li> <li>Patches scale better than tokens as model size increases</li> </ul> <hr/> <h2 id="the-tokenizer-problem">The Tokenizer Problem</h2> <p>Before explaining what BLT does, it’s worth being specific about what tokenizers get wrong.</p> <h3 id="problem-1-the-vocabulary-is-fixed-and-arbitrary">Problem 1: The Vocabulary Is Fixed and Arbitrary</h3> <p>BPE (Byte-Pair Encoding) builds a vocabulary by iteratively merging the most frequent character pairs in a training corpus. The result is deeply English-centric. Common English words get single tokens. Rare words, names, and most non-Latin scripts get shredded into fragments.</p> <p>The word <code class="language-plaintext highlighter-rouge">"unfortunately"</code> → 1 token. The Turkish word <code class="language-plaintext highlighter-rouge">"değiştiremeyebilirsiniz"</code> → 12+ tokens.</p> <p>Same information density, wildly different token cost. Multilingual models pay a massive efficiency penalty on non-English text simply because of vocabulary construction choices made before training started.</p> <h3 id="problem-2-tokenization-creates-invisible-bugs">Problem 2: Tokenization Creates Invisible Bugs</h3> <p>Because models operate on token IDs rather than characters, they have no direct access to spelling. This causes failures that look baffling from the outside:</p> <ul> <li><code class="language-plaintext highlighter-rouge">9.11 &gt; 9.9</code> — GPT-4 originally said false. The numbers are tokenized as whole units; the model sees <code class="language-plaintext highlighter-rouge">9</code> and <code class="language-plaintext highlighter-rouge">.11</code> vs <code class="language-plaintext highlighter-rouge">9</code> and <code class="language-plaintext highlighter-rouge">.9</code> without clean positional structure.</li> <li>Reverse a string → often wrong, because reversing happens at the character level but the model thinks in tokens.</li> <li>Count the letter <code class="language-plaintext highlighter-rouge">r</code> in <code class="language-plaintext highlighter-rouge">"strawberry"</code> → famously hard for tokenized models, trivial for humans.</li> <li>Rhyming, anagram detection, wordplay → systematically poor.</li> </ul> <p>These aren’t reasoning failures. They’re tokenization artifacts.</p> <h3 id="problem-3-tokenization-is-brittle-at-boundaries">Problem 3: Tokenization Is Brittle at Boundaries</h3> <p>Adding a space changes the token. Capitalization changes the token. A URL with an unusual structure might produce a token sequence no model has seen during training. Code with uncommon variable names gets fragmented unpredictably.</p> <p>Models can recover from this because they have enough capacity to memorize patterns — but they’re burning parameters on compensating for preprocessing noise.</p> <h3 id="problem-4-the-vocabulary-cant-adapt">Problem 4: The Vocabulary Can’t Adapt</h3> <p>If a new domain emerges after the tokenizer is built — a new programming language, a new file format, a new script — the model handles it poorly. The vocabulary is frozen. There’s no mechanism to learn better representations for new patterns without retraining the tokenizer and rebuilding the vocabulary from scratch.</p> <hr/> <h2 id="what-bytes-give-you">What Bytes Give You</h2> <p>At the other extreme: raw bytes.</p> <p>Every piece of digital information — text in any language, code, HTML, JSON, images, audio — can be represented as a sequence of bytes. A byte is a number from 0 to 255. There are exactly 256 possible values.</p> <p>A byte-level model has a vocabulary of 256. It never needs to be retrained. It never has an OOV (out-of-vocabulary) problem. Every string in any language on any format is representable without special handling.</p> <p>The catch: raw bytes produce very long sequences. <code class="language-plaintext highlighter-rouge">"Hello world"</code> is 11 bytes. A 10,000-word document is ~60,000 bytes. Transformers scale quadratically with sequence length — so naive byte-level transformers are expensive, slow, and don’t scale well.</p> <p>This is why the field defaulted to tokenization in the first place. Tokens compress the sequence. Shorter sequence → cheaper attention → more compute-efficient training.</p> <p>BLT’s contribution is solving the byte-length problem without reintroducing a fixed vocabulary.</p> <hr/> <h2 id="the-blt-architecture">The BLT Architecture</h2> <p>BLT replaces the tokenizer with a learned, dynamic <strong>patching</strong> mechanism. Instead of a fixed vocabulary, it groups bytes into variable-length <strong>patches</strong> based on how predictable the bytes are.</p> <p>The full model has three components:</p> <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Raw bytes
    ↓
Local Encoder  (lightweight byte-level transformer)
    ↓
Patch representations  (dynamic, variable-length groups)
    ↓
Latent Transformer  (large, expensive — operates on patches)
    ↓
Patch predictions
    ↓
Local Decoder  (reconstructs byte-level output from patches)
    ↓
Output bytes
</code></pre></div></div> <h3 id="component-1-local-encoder">Component 1: Local Encoder</h3> <p>A small, lightweight transformer that reads raw bytes and produces byte-level representations. It doesn’t try to understand the whole document — it just builds local context around each byte.</p> <p>The output feeds into the patching step.</p> <h3 id="component-2-entropy-based-dynamic-patching">Component 2: Entropy-Based Dynamic Patching</h3> <p>This is the core idea. Instead of splitting text at fixed boundaries (like BPE does), BLT groups bytes into patches based on <strong>next-byte entropy</strong> — a measure of how surprising the next byte is.</p> <p>High entropy = the model is uncertain what comes next. This byte starts something complex. Start a new patch here.</p> <p>Low entropy = the next byte is predictable. This byte is part of a familiar pattern. Extend the current patch.</p> <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Entropy-based patching example:

Input:   " t h e   q u i c k   b r o w n   f o x "
Entropy: [H  L  L  H  L  L  L  L  H  L  L  L  L  H  L  L  L]
Patches: [_the] [_quick] [_brown] [_fox]
</code></pre></div></div> <p>Common words and phrases get compressed into single patches. Rare words, code snippets, unusual names, and multilingual text get finer-grained patches — more attention per byte, exactly where it’s needed.</p> <p>This is the key insight: <strong>allocate compute proportional to information content</strong>. Predictable bytes get bundled cheaply. Surprising bytes get resolved carefully.</p> <p>The patch boundaries are dynamic — they change based on the actual input. A technical paper about nuclear physics gets different patches than a children’s story, even at the same byte length.</p> <h3 id="component-3-latent-transformer">Component 3: Latent Transformer</h3> <p>The main, expensive transformer. It operates on <strong>patch representations</strong>, not individual bytes. Because patches compress the sequence — typically by a factor of 4–8× compared to raw bytes — the latent transformer sees much shorter sequences.</p> <p>This is where the bulk of the model’s parameters live and where the “understanding” happens. It uses standard transformer architecture — multi-head attention, feed-forward layers — but on patch-level units.</p> <p>Critically, patch representations carry context from the Local Encoder via <strong>cross-attention</strong>. The latent transformer can look back at individual byte-level details when it needs to.</p> <h3 id="component-4-local-decoder">Component 4: Local Decoder</h3> <p>Reconstructs byte-level output from the patch-level predictions. Given a predicted patch, it generates the actual bytes that form the output — one byte at a time, conditioned on both the patch prediction and previously generated bytes.</p> <p>The decoder is also lightweight. Most of the compute stays in the Latent Transformer.</p> <hr/> <h2 id="why-patches-scale-better-than-tokens">Why Patches Scale Better Than Tokens</h2> <p>The paper’s title makes a specific claim: <strong>patches scale better than tokens</strong>.</p> <p>Here’s the intuition.</p> <p>As models get larger, they get better at predicting common patterns — frequent words, standard syntax, typical phrases. With fixed tokenization, every token has the same representation cost regardless of how predictable it is. A token for <code class="language-plaintext highlighter-rouge">"the"</code> takes the same slot in the attention matrix as a token for <code class="language-plaintext highlighter-rouge">"Schrödinger"</code>.</p> <p>With entropy-based patching, as the model improves:</p> <ul> <li>More bytes become predictable → they get compressed into larger patches</li> <li>The effective sequence length decreases</li> <li>The same latent transformer capacity gets focused on harder problems</li> <li>Scaling the model → automatically better compression → more efficient use of compute</li> </ul> <p>This creates a virtuous loop that fixed tokenization can’t replicate. Tokenization is static — it doesn’t adapt to what the model has learned. BLT’s patching is dynamic — it improves with the model.</p> <p>The paper shows this empirically: at the same compute budget, BLT matches LLaMA 3. At larger scales, the efficiency gap favors BLT more.</p> <hr/> <h2 id="where-blt-clearly-wins">Where BLT Clearly Wins</h2> <p>Some tasks are structurally impossible to do well with tokenized models. BLT handles all of them naturally.</p> <h3 id="character-level-manipulation">Character-Level Manipulation</h3> <p>Reverse a string, count specific characters, detect palindromes, anagram matching. BLT operates at the byte level — these tasks are as natural for it as scanning a character array is for you.</p> <h3 id="multilingual-text">Multilingual Text</h3> <p>No vocabulary bias. Turkish, Arabic, Hindi, Chinese, code-switching mid-sentence — all represented at the same byte-level granularity. No 10× token penalty for non-Latin scripts.</p> <h3 id="structured-formats">Structured Formats</h3> <p>JSON, CSV, XML, HTML, source code with unusual variable names — all handled without tokenization artifacts. The entropy-based patcher naturally segments these formats at structurally meaningful boundaries.</p> <h3 id="robustness-to-typos-and-noise">Robustness to Typos and Noise</h3> <p>A tokenizer turns a misspelled word into a completely different token sequence. BLT sees character-level corruption as a byte-level signal and can reason about it directly. This matters for real-world noisy input.</p> <h3 id="novel-formats-and-future-proofing">Novel Formats and Future-Proofing</h3> <p>A new programming language, a new markup format, a new domain — BLT handles it without retraining the vocabulary. There’s no concept of OOV. The 256-byte vocabulary is universal.</p> <hr/> <h2 id="the-numbers">The Numbers</h2> <table> <thead> <tr> <th>Benchmark</th> <th>LLaMA 3 (tokens)</th> <th>BLT (bytes)</th> </tr> </thead> <tbody> <tr> <td>FLOP budget</td> <td>Matched</td> <td>Matched</td> </tr> <tr> <td>Overall performance</td> <td>Baseline</td> <td>Matches or beats</td> </tr> <tr> <td>Character tasks</td> <td>Weak</td> <td>Significantly better</td> </tr> <tr> <td>Multilingual</td> <td>Penalized by vocab</td> <td>Equal cost per byte</td> </tr> <tr> <td>Robustness</td> <td>Token-brittle</td> <td>Byte-robust</td> </tr> <tr> <td>Vocabulary size</td> <td>128K</td> <td>256 (universal)</td> </tr> <tr> <td>OOV handling</td> <td>Fragmentation</td> <td>None</td> </tr> </tbody> </table> <p>At equal compute: BLT is competitive. At tasks that expose tokenization’s weaknesses: BLT wins clearly.</p> <hr/> <h2 id="what-blt-doesnt-solve">What BLT Doesn’t Solve</h2> <p>BLT is a compelling paper but not a solved problem.</p> <p><strong>Inference speed is still a challenge.</strong> The Local Encoder and Decoder add overhead at inference time that pure token-based models don’t have. The latent transformer may be cheaper due to shorter sequences, but the total wall-clock cost per output byte needs careful engineering.</p> <p><strong>The compression factor varies.</strong> On highly predictable text — boilerplate, repetitive formats — patches get large and the latent transformer gets efficient. On information-dense text like dense code or technical writing, patches stay small and the efficiency advantage shrinks.</p> <p><strong>Training stability at scale is unproven.</strong> The BLT paper demonstrates results up to a certain model size. Whether the architecture scales to 70B+ parameters with the same efficiency properties is an open question.</p> <p><strong>The field hasn’t adopted it yet.</strong> LLaMA 4, GPT-5, Gemini 2 all still use tokenizers. BLT is research. The engineering investment to replace tokenization infrastructure at production scale is non-trivial.</p> <hr/> <h2 id="why-this-matters">Why This Matters</h2> <p>The reason BLT is interesting isn’t just the benchmarks.</p> <p>It’s that tokenization has been treated as an unquestioned given — an implementation detail inherited from the NLP of the early 2010s, carried forward because it worked well enough and changing it was hard.</p> <p>BLT demonstrates that the “work well enough” part was covering for real weaknesses, and that a principled alternative is achievable. It shows that you can beat a carefully tuned tokenized baseline without a fixed vocabulary, using only 256 byte values and a smarter way of grouping them.</p> <p>That’s a meaningful result even if BLT itself never ships in a production model.</p> <p>The broader lesson: <strong>the preprocessing stack is not fixed</strong>. Tokenization was an engineering convenience that became an invisible constraint. BLT makes the constraint visible — and shows a path around it.</p> <p>The next generation of models might not start by running your input through a frozen vocabulary lookup. They might start by reading bytes — the way your CPU always has.</p> <hr/> <h2 id="further-reading">Further Reading</h2> <ul> <li><a href="https://arxiv.org/abs/2412.09871">Byte Latent Transformer: Patches Scale Better Than Tokens</a> — Meta AI, 2024</li> <li><a href="https://huggingface.co/learn/nlp-course/chapter6/5">BPE tokenization explained — Hugging Face NLP Course</a></li> <li><a href="https://arxiv.org/abs/2305.07185">MegaByte: Predicting Million-byte Sequences with Multiscale Transformers</a> — earlier byte-level work from Meta</li> <li><a href="https://arxiv.org/abs/2105.13626">ByT5: Towards a Token-Free Future with Pre-trained Byte-to-Byte Models</a> — Google’s earlier byte model</li> </ul>]]></content><author><name></name></author><category term="deep-learning"/><category term="BLT"/><category term="Tokenization"/><category term="Transformers"/><category term="Meta AI"/><category term="Byte Models"/><category term="LLMs"/><category term="Architecture"/><summary type="html"><![CDATA[Every LLM you've ever used runs on a tokenizer that chops text into chunks before the model sees a single character. Meta's Byte-Latent Transformer throws that away entirely — and matches LLaMA 3 performance. Here's how.]]></summary></entry><entry><title type="html">Hopfield Networks → Modern Associative Memory: The 1982 Idea Hidden Inside Every Transformer</title><link href="https://sridhar-3009.github.io/blog/2026/hopfield-networks/" rel="alternate" type="text/html" title="Hopfield Networks → Modern Associative Memory: The 1982 Idea Hidden Inside Every Transformer"/><published>2026-05-05T00:00:00+00:00</published><updated>2026-05-05T00:00:00+00:00</updated><id>https://sridhar-3009.github.io/blog/2026/hopfield-networks</id><content type="html" xml:base="https://sridhar-3009.github.io/blog/2026/hopfield-networks/"><![CDATA[<h1 id="hopfield-networks--modern-associative-memory-the-1982-idea-hidden-inside-every-transformer">Hopfield Networks → Modern Associative Memory: The 1982 Idea Hidden Inside Every Transformer</h1> <p>In 2020, a group of researchers from JKU Linz published a paper with one of the boldest titles in recent ML history: <strong>“Hopfield Networks is All You Need.”</strong></p> <p>A deliberate nod to the transformer paper. A provocation. And, it turns out, a theorem.</p> <p>They proved that the update rule of a modern Hopfield network — a theoretical model of associative memory from statistical physics — is <strong>mathematically identical</strong> to the attention mechanism in transformers. The softmax you use every day is a Hopfield retrieval step. Every attention head is running associative memory.</p> <p>This is the story of how a 1982 idea about how brains might store memories quietly became the core of the most powerful AI systems in history.</p> <hr/> <h2 id="the-memory-problem">The Memory Problem</h2> <p>How do you recognize a face you haven’t seen in ten years?</p> <p>You don’t store a perfect photograph in your head. You store something compressed, something fragmentary. When you see that person again — older, different haircut, different context — your brain fills in the gaps. It retrieves the full memory from a partial cue.</p> <p>This is <strong>associative memory</strong>. Not lookup-by-address (like a computer reading <code class="language-plaintext highlighter-rouge">memory[0x7FFF]</code>). Lookup-by-content. You give a noisy, incomplete query — and the system finds the closest stored pattern and returns it complete.</p> <p>Building a mathematical model of this was John Hopfield’s goal in 1982.</p> <hr/> <h2 id="part-1-the-classical-hopfield-network">Part 1: The Classical Hopfield Network</h2> <h3 id="the-setup">The Setup</h3> <p>Imagine N light switches arranged in a grid. Each switch is either ON (+1) or OFF (-1). Every switch is connected to every other switch by a wire with a weight on it. The whole thing is fully connected, symmetric (the weight from switch A to B equals the weight from B to A), and has no self-connections.</p> <p>This is the Hopfield network. The state of the system at any moment is the pattern of ON/OFF switches — a vector <code class="language-plaintext highlighter-rouge">s = [s1, s2, ..., sN]</code> where each value is either -1 or +1.</p> <h3 id="the-energy-function">The Energy Function</h3> <p>Hopfield borrowed an idea from physics: spin glasses. In physics, a spin glass is a magnet where atoms are frozen in random orientations. The system has an <strong>energy function</strong> that it naturally minimizes over time.</p> <p>For the Hopfield network, the energy is:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>E = -½ × Σ(i≠j) w_ij × s_i × s_j
</code></pre></div></div> <p>Think of it this way. If two connected switches <code class="language-plaintext highlighter-rouge">i</code> and <code class="language-plaintext highlighter-rouge">j</code> have a large positive weight <code class="language-plaintext highlighter-rouge">w_ij</code>, and they’re both ON (+1), the product <code class="language-plaintext highlighter-rouge">s_i × s_j = +1</code> contributes a large negative term to <code class="language-plaintext highlighter-rouge">E</code> — lowering the energy. The system “likes” when highly-connected switches agree. Energy decreases when the network settles into its preferred configurations.</p> <p>The key property: <strong>every single-switch update either lowers the energy or leaves it unchanged</strong>. The network can never get stuck in an endless loop. It always converges.</p> <h3 id="storing-memories-with-hebbian-learning">Storing Memories with Hebbian Learning</h3> <p>To store <code class="language-plaintext highlighter-rouge">p</code> patterns, you set the weights using the Hebbian rule:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>w_ij = (1/N) × Σ(μ=1 to p) [ ξ_i^μ × ξ_j^μ ]
</code></pre></div></div> <p>“Neurons that fire together, wire together.” For each stored pattern <code class="language-plaintext highlighter-rouge">μ</code>, if neuron <code class="language-plaintext highlighter-rouge">i</code> and neuron <code class="language-plaintext highlighter-rouge">j</code> are both ON (or both OFF), you strengthen the connection between them. Do this for all patterns, and each pattern carves out a <strong>local minimum</strong> in the energy landscape.</p> <h3 id="the-energy-landscape-as-terrain">The Energy Landscape as Terrain</h3> <p>Think of the energy landscape as a mountainous terrain. Each valley is a stored memory — a stable fixed point the system is attracted to. Corrupted or partial inputs are hikers dropped somewhere on a hillside. The update rule is gravity: the hiker slides downhill and settles in the nearest valley, retrieving the closest stored memory.</p> <p>Give the network a photo of a face with half the pixels blacked out. It slides down to the nearest energy minimum — the fully-complete stored face. The gap gets filled.</p> <h3 id="retrieval-in-action">Retrieval in Action</h3> <p>Start from a query (a noisy or partial pattern). Apply the update rule repeatedly:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>s_i ← sign( Σ_j w_ij × s_j )
</code></pre></div></div> <p>One neuron updates at a time. Each flip lowers <code class="language-plaintext highlighter-rouge">E</code>. Eventually you converge to the nearest stored pattern. The network has retrieved a memory from a fragment.</p> <hr/> <h2 id="part-2-the-capacity-wall-and-the-ghost-memory-problem">Part 2: The Capacity Wall and the Ghost Memory Problem</h2> <h3 id="the-0138n-limit">The 0.138N Limit</h3> <p>Here’s where classical Hopfield networks hit a wall.</p> <p>For a network of <code class="language-plaintext highlighter-rouge">N</code> neurons trained with Hebbian weights, reliable retrieval works only up to approximately:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>C ≈ 0.138 × N
</code></pre></div></div> <p>Roughly 14 patterns per 100 neurons. For 1000 neurons, 138 memories max. Beyond this threshold, retrieval collapses catastrophically.</p> <p>The intuition: when you store a new pattern by strengthening certain connections, you also slightly perturb all existing stored patterns. Each new memory leaves a ghost in every other basin. Up to 0.138N patterns, these ghosts cancel out via averaging. Above it, they accumulate and corrupt retrieval.</p> <blockquote> <p>A library where every new book smears a tiny bit of ink across every existing book. Up to 138 books per 1000 pages you can still read them. Add the 139th and the smearing cascades into noise.</p> </blockquote> <h3 id="spurious-states">Spurious States</h3> <p>The second failure mode is subtler and stranger: <strong>spurious attractors</strong> — energy minima that aren’t any stored pattern.</p> <p>The most common type is a <strong>mixture state</strong>. If you store patterns A, B, and C, the combination <code class="language-plaintext highlighter-rouge">sign(A + B - C)</code> is often also a stable minimum. It’s not a real memory — it’s a hallucination, a weighted blend of three real ones.</p> <p>Another guaranteed spurious state: the bitwise inverse of every stored pattern is always also a stable fixed point. The network converges — but to somewhere that was never stored. The hiker finds a phantom valley that shouldn’t exist.</p> <hr/> <h2 id="part-3-the-2020-resurrection">Part 3: The 2020 Resurrection</h2> <p>For 30 years, Hopfield networks were mostly taught as historical curiosities. Then three papers rewrote the story:</p> <p><strong>Krotov &amp; Hopfield (2016):</strong> Replace the quadratic interaction in the energy with a higher-order polynomial. Capacity scales as <code class="language-plaintext highlighter-rouge">N^(n-1)</code>.</p> <p><strong>Demircigil et al. (2017):</strong> Use an exponential interaction <code class="language-plaintext highlighter-rouge">e^z</code>. Capacity becomes <code class="language-plaintext highlighter-rouge">2^(N/2)</code> — exponential in N.</p> <p><strong>Ramsauer et al. (2020):</strong> Extend to <strong>continuous states</strong> (not binary ±1), derive the update rule cleanly, and discover it is exactly transformer attention.</p> <h3 id="the-new-energy-function">The New Energy Function</h3> <p>The continuous modern Hopfield network has two competing terms in its energy:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>E = -lse(β, X^T × ξ)  +  ½ × ||ξ||²
</code></pre></div></div> <p>The first term — <code class="language-plaintext highlighter-rouge">lse</code>, the log-sum-exp function — pulls the query <code class="language-plaintext highlighter-rouge">ξ</code> toward stored patterns <code class="language-plaintext highlighter-rouge">X</code>. The second term is a regularizer that prevents <code class="language-plaintext highlighter-rouge">ξ</code> from running off to infinity to maximize the first.</p> <p>Log-sum-exp is a smooth version of the max function:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>lse(β, z) = (1/β) × log( Σ_i exp(β × z_i) )
</code></pre></div></div> <p>At large <code class="language-plaintext highlighter-rouge">β</code>, it becomes a hard argmax — pick the single closest pattern. At small <code class="language-plaintext highlighter-rouge">β</code>, it averages everything. The <code class="language-plaintext highlighter-rouge">β</code> parameter is the sharpness knob.</p> <h3 id="the-update-rule">The Update Rule</h3> <p>One step of gradient descent on this energy gives:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ξ_new = X × softmax(β × X^T × ξ)
</code></pre></div></div> <p>Unpack this:</p> <ul> <li><code class="language-plaintext highlighter-rouge">X^T × ξ</code> computes the dot-product similarity between the query and every stored pattern</li> <li><code class="language-plaintext highlighter-rouge">softmax(β × ...)</code> turns these into weights that sum to 1, concentrating weight on the most similar patterns</li> <li><code class="language-plaintext highlighter-rouge">X × [weights]</code> returns a weighted combination of stored patterns — a retrieved memory</li> </ul> <p>This is <strong>not a local flip of one neuron</strong>. It’s a global, continuous update that simultaneously considers all stored patterns and returns a smooth interpolation weighted by similarity.</p> <h3 id="exponential-storage-capacity">Exponential Storage Capacity</h3> <p>The modern Hopfield network can store approximately <code class="language-plaintext highlighter-rouge">2^(N/2)</code> patterns with exponentially small retrieval error.</p> <p>For <code class="language-plaintext highlighter-rouge">N = 100</code> dimensions: classical capacity ≈ 14 patterns. Modern capacity ≈ 2^50 ≈ one quadrillion patterns.</p> <p>The reason: with exponential interaction, each stored pattern carves out a deep, narrow basin. Even when you pack in <code class="language-plaintext highlighter-rouge">2^(N/2)</code> patterns, the basins stay separated. Spurious states essentially vanish because the network almost always converges in <strong>a single update step</strong> directly to the closest true memory.</p> <hr/> <h2 id="part-4-the-attention-mechanism-is-hopfield-retrieval">Part 4: The Attention Mechanism is Hopfield Retrieval</h2> <p>Now for the twist.</p> <p>Generalize the update rule to handle multiple simultaneous queries — a batch of <code class="language-plaintext highlighter-rouge">M</code> query vectors processed in parallel:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Ξ_new = X × softmax(β × X^T × Ξ)
</code></pre></div></div> <p>Now add learnable linear projections — a standard trick for adding expressive power:</p> <table> <thead> <tr> <th>Hopfield concept</th> <th>Transformer equivalent</th> </tr> </thead> <tbody> <tr> <td>Stored patterns X</td> <td>Keys: K = X × W_K</td> </tr> <tr> <td>Query states Ξ</td> <td>Queries: Q = Ξ × W_Q</td> </tr> <tr> <td>Patterns in output space</td> <td>Values: V = X × W_V</td> </tr> <tr> <td>Inverse temperature β</td> <td>Scaling factor: 1 / sqrt(d_k)</td> </tr> </tbody> </table> <p>Substitute all of that in and you get:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Z = softmax( Q × K^T / sqrt(d_k) ) × V
</code></pre></div></div> <p>This is the <strong>exact transformer self-attention formula</strong>. Not similar. Not analogous. Identical.</p> <p>Every time a transformer runs attention, it is performing one step of Modern Hopfield retrieval. The key matrix is a store of patterns. The query is a partial, noisy signal. The softmax computes similarity scores. The value matrix is the content being retrieved.</p> <h3 id="the-β-knob-and-layer-behavior">The β Knob and Layer Behavior</h3> <p>The scaling factor <code class="language-plaintext highlighter-rouge">1/sqrt(d_k)</code> is the Hopfield inverse temperature <code class="language-plaintext highlighter-rouge">β</code>. This has a direct interpretation for what different transformer layers are doing:</p> <p><strong>Low β → diffuse softmax → global averaging.</strong> The softmax spreads weight across many keys. The retrieved output is a blend of many stored patterns. This is what lower transformer layers do: aggregate broad contextual information.</p> <p><strong>High β → sharp softmax → single-pattern retrieval.</strong> The softmax concentrates almost all weight on the single most-similar key. The output is essentially a copy of that one stored pattern. This is what upper transformer layers do: retrieve specific entities, facts, syntactic structures.</p> <p>The temperature isn’t fixed — learned attention heads discover the right <code class="language-plaintext highlighter-rouge">β</code> for their role in the hierarchy.</p> <h3 id="why-spurious-states-dont-matter-in-practice">Why Spurious States Don’t Matter in Practice</h3> <p>Modern transformers operate far below the exponential storage capacity of their attention heads. The number of tokens in context (say, 4096) is astronomically smaller than <code class="language-plaintext highlighter-rouge">2^(d_k/2)</code> for typical <code class="language-plaintext highlighter-rouge">d_k = 64</code> — capacity around 4 billion patterns. The network is running at a fraction of a percent of its storage limit. Every basin is clean. Retrieval converges in one step. Spurious states don’t appear.</p> <blockquote> <p>The classical library where books smear each other is replaced by a perfect digital index. Each book has its own exact address. One lookup, no cross-contamination.</p> </blockquote> <hr/> <h2 id="part-5-what-this-means-for-transformer-internals">Part 5: What This Means for Transformer Internals</h2> <p>The Hopfield lens gives new language for mechanistic interpretability.</p> <h3 id="attention-heads-as-memories">Attention Heads as Memories</h3> <p>Every attention head in every layer is storing patterns in its key matrix and retrieving content via its value matrix. The “memories” aren’t fixed — they’re computed fresh from the input on each forward pass. But they behave like memories: the softmax retrieval selects which contextual information to bring forward.</p> <h3 id="three-types-of-attention-fixed-points">Three Types of Attention Fixed Points</h3> <p>Ramsauer et al. classify transformer behavior into three retrieval regimes:</p> <ol> <li> <p><strong>Global averaging</strong> (lower layers): The output is close to the mean of all value vectors. The head is pooling global context. No specific memory retrieved.</p> </li> <li> <p><strong>Metastable retrieval</strong> (middle layers): The output averages a cluster of similar tokens. Semantic grouping — retrieving “all mentions of the subject” rather than one specific occurrence.</p> </li> <li> <p><strong>Sharp retrieval</strong> (upper layers): The output is essentially a single value vector. The head has found an exact match. This is where induction heads, factual recall, and entity tracking happen.</p> </li> </ol> <h3 id="the-hopfield-layers-library">The <code class="language-plaintext highlighter-rouge">hopfield-layers</code> Library</h3> <p>Ramsauer’s team shipped three PyTorch modules that wrap attention as explicit associative memory:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">hflayers</span> <span class="kn">import</span> <span class="n">Hopfield</span><span class="p">,</span> <span class="n">HopfieldLayer</span><span class="p">,</span> <span class="n">HopfieldPooling</span>

<span class="c1"># Full associative memory — both queries and stored patterns learnable
</span><span class="n">layer</span> <span class="o">=</span> <span class="nc">Hopfield</span><span class="p">(</span><span class="n">input_size</span><span class="o">=</span><span class="mi">256</span><span class="p">,</span> <span class="n">hidden_size</span><span class="o">=</span><span class="mi">64</span><span class="p">,</span> <span class="n">num_heads</span><span class="o">=</span><span class="mi">8</span><span class="p">)</span>

<span class="c1"># Fixed stored patterns — like a learned prototype bank / lookup table
</span><span class="n">lookup</span> <span class="o">=</span> <span class="nc">HopfieldLayer</span><span class="p">(</span><span class="n">input_size</span><span class="o">=</span><span class="mi">256</span><span class="p">,</span> <span class="n">quantity</span><span class="o">=</span><span class="mi">512</span><span class="p">)</span>

<span class="c1"># Single query vector aggregating over variable-length sets
# Replaces CLS token pooling or mean pooling
</span><span class="n">pooler</span> <span class="o">=</span> <span class="nc">HopfieldPooling</span><span class="p">(</span><span class="n">input_size</span><span class="o">=</span><span class="mi">256</span><span class="p">)</span>
</code></pre></div></div> <p>They used <code class="language-plaintext highlighter-rouge">HopfieldPooling</code> to classify immune repertoires — sets of hundreds of thousands of antibody sequences — and hit state-of-the-art. The network retrieved diagnostic antibody patterns from a massive noisy pool in a single forward pass.</p> <hr/> <h2 id="the-bigger-picture">The Bigger Picture</h2> <p>When Vaswani et al. wrote “Attention is All You Need” in 2017, they described attention as a lookup mechanism. Queries, keys, values. Soft selection. Practical and effective.</p> <p>What Ramsauer et al. revealed in 2020 is that this “practical mechanism” has a 40-year theoretical foundation in statistical physics and neuroscience. It is not just a useful trick — it is the provably optimal way to do associative memory retrieval with continuous states and exponential capacity.</p> <p>Every time GPT-4 recalls a fact, every time a code model identifies a variable name, every time a vision transformer detects an object — they are all running a Hopfield retrieval step. Storing patterns in keys, retrieving content from values, computing similarity with softmax.</p> <p>Hopfield’s 1982 paper was about how brains might work. It turned out to describe, precisely, how the best artificial minds we’ve built actually work.</p> <p>The 1982 idea wasn’t wrong. It was just 38 years early.</p> <hr/> <h2 id="further-reading">Further Reading</h2> <ul> <li><a href="https://arxiv.org/abs/2008.02217">Hopfield Networks is All You Need</a> — Ramsauer et al. 2020 (the paper)</li> <li><a href="https://ml-jku.github.io/hopfield-layers/">Official Blog &amp; hopfield-layers library</a></li> <li><a href="https://en.wikipedia.org/wiki/Modern_Hopfield_network">Modern Hopfield Network — Wikipedia</a></li> <li><a href="https://en.wikipedia.org/wiki/Hopfield_network">Hopfield network — Wikipedia</a></li> </ul>]]></content><author><name></name></author><category term="deep-learning"/><category term="Hopfield Networks"/><category term="Attention"/><category term="Transformers"/><category term="Associative Memory"/><category term="Energy Models"/><category term="Neural Networks"/><summary type="html"><![CDATA[A forgotten idea from 1982 about how brains store memories turned out to be the exact math behind transformer attention. Here's the full journey — from energy landscapes to softmax.]]></summary></entry><entry><title type="html">KV Cache Compression: How LLMs Handle Million-Token Contexts Without Running Out of Memory</title><link href="https://sridhar-3009.github.io/blog/2026/kv-cache-compression/" rel="alternate" type="text/html" title="KV Cache Compression: How LLMs Handle Million-Token Contexts Without Running Out of Memory"/><published>2026-05-02T00:00:00+00:00</published><updated>2026-05-02T00:00:00+00:00</updated><id>https://sridhar-3009.github.io/blog/2026/kv-cache-compression</id><content type="html" xml:base="https://sridhar-3009.github.io/blog/2026/kv-cache-compression/"><![CDATA[<h1 id="kv-cache-compression-how-llms-handle-million-token-contexts-without-running-out-of-memory">KV Cache Compression: How LLMs Handle Million-Token Contexts Without Running Out of Memory</h1> <p>Gemini 1.5 has a 1M token context window. Claude 3.5 handles 200K. GPT-4 Turbo runs 128K. These numbers sound like marketing — until you realize what they actually require in memory.</p> <p>Every token the model processes stores two vectors in GPU memory: a <strong>key</strong> and a <strong>value</strong>, for every attention head, in every layer. These stay resident in VRAM for the entire generation session. With a 70B model running at 128K context, the KV cache alone consumes <strong>over 100GB of VRAM</strong> — more than most GPU clusters have available.</p> <p>Long context isn’t free. It’s a memory engineering problem. The techniques that solve it — GQA, quantization, eviction policies, sliding windows — are what separate a research demo from something you can actually deploy.</p> <p>This post builds from first principles: what the KV cache is and why it matters, then every major compression technique with the intuition before the code.</p> <hr/> <h2 id="what-is-the-kv-cache-and-why-does-it-exist">What Is the KV Cache, and Why Does It Exist?</h2> <p>To understand the cache, you need to understand the cost without it.</p> <p>When a transformer generates text, it works <strong>one token at a time</strong>. To generate token number 500, it needs to run attention over all 499 previous tokens — computing how much each prior token should influence the current one. That requires the <strong>key</strong> and <strong>value</strong> vectors for all 499 previous tokens.</p> <p>Here’s the problem: those key and value vectors were already computed when those tokens were first processed. Without a cache, you’d recompute them from scratch every single step. To generate a 1000-token response, you’d compute the keys and values for token 1 a thousand times, for token 2 a total of 999 times, and so on — a staggering amount of redundant work.</p> <p>The <strong>KV cache</strong> solves this by storing the key and value vectors as they’re computed, and reusing them on every subsequent step. It’s the difference between O(n²) recomputation and O(n) incremental updates. Without it, even a 4K-token generation would be unbearably slow.</p> <p>Think of it like a notepad. Every time you read a new sentence in a book, instead of re-reading every previous sentence to remember what happened, you jot down the key facts. The notepad grows with every page — but you never have to go back to re-read.</p> <p>Here’s a minimal implementation to make it concrete:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">torch</span>
<span class="kn">import</span> <span class="n">torch.nn</span> <span class="k">as</span> <span class="n">nn</span>
<span class="kn">import</span> <span class="n">torch.nn.functional</span> <span class="k">as</span> <span class="n">F</span>
<span class="kn">import</span> <span class="n">math</span>

<span class="k">class</span> <span class="nc">AttentionWithKVCache</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">d_model</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">num_heads</span><span class="p">:</span> <span class="nb">int</span><span class="p">):</span>
        <span class="nf">super</span><span class="p">().</span><span class="nf">__init__</span><span class="p">()</span>
        <span class="n">self</span><span class="p">.</span><span class="n">num_heads</span> <span class="o">=</span> <span class="n">num_heads</span>
        <span class="n">self</span><span class="p">.</span><span class="n">d_k</span> <span class="o">=</span> <span class="n">d_model</span> <span class="o">//</span> <span class="n">num_heads</span>

        <span class="n">self</span><span class="p">.</span><span class="n">W_q</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="nc">Linear</span><span class="p">(</span><span class="n">d_model</span><span class="p">,</span> <span class="n">d_model</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">W_k</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="nc">Linear</span><span class="p">(</span><span class="n">d_model</span><span class="p">,</span> <span class="n">d_model</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">W_v</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="nc">Linear</span><span class="p">(</span><span class="n">d_model</span><span class="p">,</span> <span class="n">d_model</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">W_o</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="nc">Linear</span><span class="p">(</span><span class="n">d_model</span><span class="p">,</span> <span class="n">d_model</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">kv_cache</span><span class="p">:</span> <span class="nb">dict</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="bp">None</span><span class="p">):</span>
        <span class="n">B</span><span class="p">,</span> <span class="n">T</span><span class="p">,</span> <span class="n">C</span> <span class="o">=</span> <span class="n">x</span><span class="p">.</span><span class="n">shape</span>

        <span class="n">Q</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nc">W_q</span><span class="p">(</span><span class="n">x</span><span class="p">).</span><span class="nf">view</span><span class="p">(</span><span class="n">B</span><span class="p">,</span> <span class="n">T</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">num_heads</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">d_k</span><span class="p">).</span><span class="nf">transpose</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
        <span class="n">K</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nc">W_k</span><span class="p">(</span><span class="n">x</span><span class="p">).</span><span class="nf">view</span><span class="p">(</span><span class="n">B</span><span class="p">,</span> <span class="n">T</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">num_heads</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">d_k</span><span class="p">).</span><span class="nf">transpose</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
        <span class="n">V</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nc">W_v</span><span class="p">(</span><span class="n">x</span><span class="p">).</span><span class="nf">view</span><span class="p">(</span><span class="n">B</span><span class="p">,</span> <span class="n">T</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">num_heads</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">d_k</span><span class="p">).</span><span class="nf">transpose</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>

        <span class="c1"># If cache exists, prepend previous K/V to current ones
</span>        <span class="k">if</span> <span class="n">kv_cache</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span>
            <span class="k">if</span> <span class="sh">"</span><span class="s">K</span><span class="sh">"</span> <span class="ow">in</span> <span class="n">kv_cache</span><span class="p">:</span>
                <span class="n">K</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">cat</span><span class="p">([</span><span class="n">kv_cache</span><span class="p">[</span><span class="sh">"</span><span class="s">K</span><span class="sh">"</span><span class="p">],</span> <span class="n">K</span><span class="p">],</span> <span class="n">dim</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
                <span class="n">V</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">cat</span><span class="p">([</span><span class="n">kv_cache</span><span class="p">[</span><span class="sh">"</span><span class="s">V</span><span class="sh">"</span><span class="p">],</span> <span class="n">V</span><span class="p">],</span> <span class="n">dim</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
            <span class="n">kv_cache</span><span class="p">[</span><span class="sh">"</span><span class="s">K</span><span class="sh">"</span><span class="p">]</span> <span class="o">=</span> <span class="n">K</span>
            <span class="n">kv_cache</span><span class="p">[</span><span class="sh">"</span><span class="s">V</span><span class="sh">"</span><span class="p">]</span> <span class="o">=</span> <span class="n">V</span>

        <span class="n">scale</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">d_k</span> <span class="o">**</span> <span class="o">-</span><span class="mf">0.5</span>
        <span class="n">scores</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">matmul</span><span class="p">(</span><span class="n">Q</span><span class="p">,</span> <span class="n">K</span><span class="p">.</span><span class="nf">transpose</span><span class="p">(</span><span class="o">-</span><span class="mi">2</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">))</span> <span class="o">*</span> <span class="n">scale</span>
        <span class="n">weights</span> <span class="o">=</span> <span class="n">F</span><span class="p">.</span><span class="nf">softmax</span><span class="p">(</span><span class="n">scores</span><span class="p">,</span> <span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">)</span>
        <span class="n">out</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">matmul</span><span class="p">(</span><span class="n">weights</span><span class="p">,</span> <span class="n">V</span><span class="p">)</span>

        <span class="n">out</span> <span class="o">=</span> <span class="n">out</span><span class="p">.</span><span class="nf">transpose</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">).</span><span class="nf">contiguous</span><span class="p">().</span><span class="nf">view</span><span class="p">(</span><span class="n">B</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">num_heads</span> <span class="o">*</span> <span class="n">self</span><span class="p">.</span><span class="n">d_k</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="nc">W_o</span><span class="p">(</span><span class="n">out</span><span class="p">),</span> <span class="n">kv_cache</span>
</code></pre></div></div> <p>The key line is <code class="language-plaintext highlighter-rouge">K = torch.cat([kv_cache["K"], K], dim=2)</code> — we’re prepending everything we computed before. The cache just grows, one token at a time, for the entire session.</p> <hr/> <h2 id="the-memory-problem-at-scale">The Memory Problem at Scale</h2> <p>Here’s where things get alarming. Each cached token stores:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2 vectors (K and V) × num_layers × num_heads × head_dim × bytes_per_float
</code></pre></div></div> <p>For <strong>Llama 3 70B</strong> in fp16 (2 bytes per float):</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2 × 80 layers × 64 heads × 128 head_dim × 2 bytes = ~2.5 MB per token
</code></pre></div></div> <p>That’s 2.5 megabytes for a <strong>single token</strong>. Now scale it:</p> <ul> <li>At 4K tokens (a short conversation): <strong>10 GB</strong></li> <li>At 32K tokens (a long document): <strong>80 GB</strong></li> <li>At 128K tokens (a book): <strong>320 GB</strong></li> </ul> <p>The model weights themselves are only ~140GB. At 128K context, the <strong>KV cache is bigger than the model</strong>. This is why long context is hard — it’s not an architecture problem, it’s a memory engineering problem.</p> <p>Every technique in this post is an attack on that 2.5 MB/token number from a different angle.</p> <hr/> <h2 id="technique-1-multi-query-attention-mqa--one-keyvalue-for-all">Technique 1: Multi-Query Attention (MQA) — One Key/Value for All</h2> <p>The most aggressive fix is also the most obvious: instead of storing separate key and value vectors for every attention head, store <strong>just one shared pair</strong> that all heads use.</p> <p>In standard multi-head attention (MHA), if you have 64 heads, you have 64 independent K/V projections. Each head learns to attend differently — one head might focus on syntax, another on entity references, another on recency. That diversity is valuable, but it’s expensive: 64 heads means 64× the cache storage.</p> <p><strong>MQA says</strong>: all heads share one K/V. They still each have their own queries (so they can ask different questions), but they all look up the same keys and values. Like a classroom where every student asks different questions, but they’re all reading from the same textbook.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">MultiQueryAttention</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">d_model</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">num_heads</span><span class="p">:</span> <span class="nb">int</span><span class="p">):</span>
        <span class="nf">super</span><span class="p">().</span><span class="nf">__init__</span><span class="p">()</span>
        <span class="n">self</span><span class="p">.</span><span class="n">num_heads</span> <span class="o">=</span> <span class="n">num_heads</span>
        <span class="n">self</span><span class="p">.</span><span class="n">d_k</span> <span class="o">=</span> <span class="n">d_model</span> <span class="o">//</span> <span class="n">num_heads</span>

        <span class="n">self</span><span class="p">.</span><span class="n">W_q</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="nc">Linear</span><span class="p">(</span><span class="n">d_model</span><span class="p">,</span> <span class="n">d_model</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>   <span class="c1"># full — one per head
</span>        <span class="n">self</span><span class="p">.</span><span class="n">W_k</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="nc">Linear</span><span class="p">(</span><span class="n">d_model</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">d_k</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>  <span class="c1"># single shared key
</span>        <span class="n">self</span><span class="p">.</span><span class="n">W_v</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="nc">Linear</span><span class="p">(</span><span class="n">d_model</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">d_k</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>  <span class="c1"># single shared value
</span>        <span class="n">self</span><span class="p">.</span><span class="n">W_o</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="nc">Linear</span><span class="p">(</span><span class="n">d_model</span><span class="p">,</span> <span class="n">d_model</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">kv_cache</span><span class="p">:</span> <span class="nb">dict</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="bp">None</span><span class="p">):</span>
        <span class="n">B</span><span class="p">,</span> <span class="n">T</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">x</span><span class="p">.</span><span class="n">shape</span>

        <span class="c1"># Full queries: each head asks its own question
</span>        <span class="n">Q</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nc">W_q</span><span class="p">(</span><span class="n">x</span><span class="p">).</span><span class="nf">view</span><span class="p">(</span><span class="n">B</span><span class="p">,</span> <span class="n">T</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">num_heads</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">d_k</span><span class="p">).</span><span class="nf">transpose</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>

        <span class="c1"># Single K and V: shape (B, 1, T, d_k)
</span>        <span class="n">K</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nc">W_k</span><span class="p">(</span><span class="n">x</span><span class="p">).</span><span class="nf">unsqueeze</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
        <span class="n">V</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nc">W_v</span><span class="p">(</span><span class="n">x</span><span class="p">).</span><span class="nf">unsqueeze</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>

        <span class="k">if</span> <span class="n">kv_cache</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span>
            <span class="k">if</span> <span class="sh">"</span><span class="s">K</span><span class="sh">"</span> <span class="ow">in</span> <span class="n">kv_cache</span><span class="p">:</span>
                <span class="n">K</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">cat</span><span class="p">([</span><span class="n">kv_cache</span><span class="p">[</span><span class="sh">"</span><span class="s">K</span><span class="sh">"</span><span class="p">],</span> <span class="n">K</span><span class="p">],</span> <span class="n">dim</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
                <span class="n">V</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">cat</span><span class="p">([</span><span class="n">kv_cache</span><span class="p">[</span><span class="sh">"</span><span class="s">V</span><span class="sh">"</span><span class="p">],</span> <span class="n">V</span><span class="p">],</span> <span class="n">dim</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
            <span class="n">kv_cache</span><span class="p">[</span><span class="sh">"</span><span class="s">K</span><span class="sh">"</span><span class="p">]</span> <span class="o">=</span> <span class="n">K</span>
            <span class="n">kv_cache</span><span class="p">[</span><span class="sh">"</span><span class="s">V</span><span class="sh">"</span><span class="p">]</span> <span class="o">=</span> <span class="n">V</span>

        <span class="c1"># Broadcast the single K/V across all query heads
</span>        <span class="n">K</span> <span class="o">=</span> <span class="n">K</span><span class="p">.</span><span class="nf">expand</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">num_heads</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span>
        <span class="n">V</span> <span class="o">=</span> <span class="n">V</span><span class="p">.</span><span class="nf">expand</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">num_heads</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span>

        <span class="n">scores</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">matmul</span><span class="p">(</span><span class="n">Q</span><span class="p">,</span> <span class="n">K</span><span class="p">.</span><span class="nf">transpose</span><span class="p">(</span><span class="o">-</span><span class="mi">2</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">))</span> <span class="o">/</span> <span class="n">math</span><span class="p">.</span><span class="nf">sqrt</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">d_k</span><span class="p">)</span>
        <span class="n">out</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">matmul</span><span class="p">(</span><span class="n">F</span><span class="p">.</span><span class="nf">softmax</span><span class="p">(</span><span class="n">scores</span><span class="p">,</span> <span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">),</span> <span class="n">V</span><span class="p">)</span>
        <span class="n">out</span> <span class="o">=</span> <span class="n">out</span><span class="p">.</span><span class="nf">transpose</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">).</span><span class="nf">contiguous</span><span class="p">().</span><span class="nf">view</span><span class="p">(</span><span class="n">B</span><span class="p">,</span> <span class="n">T</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="nc">W_o</span><span class="p">(</span><span class="n">out</span><span class="p">),</span> <span class="n">kv_cache</span>
</code></pre></div></div> <p>Notice <code class="language-plaintext highlighter-rouge">W_k</code> and <code class="language-plaintext highlighter-rouge">W_v</code> project to <code class="language-plaintext highlighter-rouge">d_k</code> (one head’s worth) instead of <code class="language-plaintext highlighter-rouge">d_model</code> (all heads’ worth). The <code class="language-plaintext highlighter-rouge">.expand()</code> call broadcasts that single K/V to all heads without actually copying memory — efficient but conceptually it’s one shared lookup.</p> <p><strong>Memory reduction</strong>: 64× on the KV cache for a 64-head model. From 320GB to 5GB at 128K context for Llama 70B. Dramatic.</p> <p><strong>The cost</strong>: measurable quality degradation. When all heads share the same keys and values, they lose their ability to specialize in different types of information. A head that wants to focus on syntactic structure and a head that wants to focus on entity coreference are now forced to look at the same representation — they can ask different questions, but they’re looking at the same answers. Some tasks suffer meaningfully.</p> <p>Used by: <strong>PaLM</strong>, <strong>Falcon</strong>, <strong>StarCoder</strong>. Mostly superseded by GQA.</p> <hr/> <h2 id="technique-2-grouped-query-attention-gqa--the-goldilocks-solution">Technique 2: Grouped Query Attention (GQA) — The Goldilocks Solution</h2> <p>MQA went too far. Standard MHA is too expensive. <strong>GQA</strong> finds the middle ground: instead of one K/V group or 64 K/V groups, use somewhere in between — say, 8 groups for a 64-head model.</p> <p>Each group of 8 query heads shares one K/V pair. You get 8× memory reduction (vs MHA’s baseline) while giving each group of heads some independence. The heads within a group share a lookup table, but different groups have different lookup tables.</p> <p>Imagine 64 students split into 8 study groups of 8. Each group shares one set of notes. Students within a group see the same notes, but groups can take different notes. It’s not as rich as everyone having private notes, but it’s much better than the entire class sharing one set.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">GroupedQueryAttention</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">d_model</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">num_heads</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">num_kv_heads</span><span class="p">:</span> <span class="nb">int</span><span class="p">):</span>
        <span class="nf">super</span><span class="p">().</span><span class="nf">__init__</span><span class="p">()</span>
        <span class="k">assert</span> <span class="n">num_heads</span> <span class="o">%</span> <span class="n">num_kv_heads</span> <span class="o">==</span> <span class="mi">0</span>
        <span class="n">self</span><span class="p">.</span><span class="n">num_heads</span> <span class="o">=</span> <span class="n">num_heads</span>
        <span class="n">self</span><span class="p">.</span><span class="n">num_kv_heads</span> <span class="o">=</span> <span class="n">num_kv_heads</span>
        <span class="n">self</span><span class="p">.</span><span class="n">num_queries_per_kv</span> <span class="o">=</span> <span class="n">num_heads</span> <span class="o">//</span> <span class="n">num_kv_heads</span>  <span class="c1"># heads per group
</span>        <span class="n">self</span><span class="p">.</span><span class="n">d_k</span> <span class="o">=</span> <span class="n">d_model</span> <span class="o">//</span> <span class="n">num_heads</span>

        <span class="n">self</span><span class="p">.</span><span class="n">W_q</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="nc">Linear</span><span class="p">(</span><span class="n">d_model</span><span class="p">,</span> <span class="n">num_heads</span> <span class="o">*</span> <span class="n">self</span><span class="p">.</span><span class="n">d_k</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">W_k</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="nc">Linear</span><span class="p">(</span><span class="n">d_model</span><span class="p">,</span> <span class="n">num_kv_heads</span> <span class="o">*</span> <span class="n">self</span><span class="p">.</span><span class="n">d_k</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">W_v</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="nc">Linear</span><span class="p">(</span><span class="n">d_model</span><span class="p">,</span> <span class="n">num_kv_heads</span> <span class="o">*</span> <span class="n">self</span><span class="p">.</span><span class="n">d_k</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">W_o</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="nc">Linear</span><span class="p">(</span><span class="n">d_model</span><span class="p">,</span> <span class="n">d_model</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">kv_cache</span><span class="p">:</span> <span class="nb">dict</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="bp">None</span><span class="p">):</span>
        <span class="n">B</span><span class="p">,</span> <span class="n">T</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">x</span><span class="p">.</span><span class="n">shape</span>

        <span class="n">Q</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nc">W_q</span><span class="p">(</span><span class="n">x</span><span class="p">).</span><span class="nf">view</span><span class="p">(</span><span class="n">B</span><span class="p">,</span> <span class="n">T</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">num_heads</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">d_k</span><span class="p">).</span><span class="nf">transpose</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
        <span class="n">K</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nc">W_k</span><span class="p">(</span><span class="n">x</span><span class="p">).</span><span class="nf">view</span><span class="p">(</span><span class="n">B</span><span class="p">,</span> <span class="n">T</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">num_kv_heads</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">d_k</span><span class="p">).</span><span class="nf">transpose</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
        <span class="n">V</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nc">W_v</span><span class="p">(</span><span class="n">x</span><span class="p">).</span><span class="nf">view</span><span class="p">(</span><span class="n">B</span><span class="p">,</span> <span class="n">T</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">num_kv_heads</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">d_k</span><span class="p">).</span><span class="nf">transpose</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>

        <span class="k">if</span> <span class="n">kv_cache</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span>
            <span class="k">if</span> <span class="sh">"</span><span class="s">K</span><span class="sh">"</span> <span class="ow">in</span> <span class="n">kv_cache</span><span class="p">:</span>
                <span class="n">K</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">cat</span><span class="p">([</span><span class="n">kv_cache</span><span class="p">[</span><span class="sh">"</span><span class="s">K</span><span class="sh">"</span><span class="p">],</span> <span class="n">K</span><span class="p">],</span> <span class="n">dim</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
                <span class="n">V</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">cat</span><span class="p">([</span><span class="n">kv_cache</span><span class="p">[</span><span class="sh">"</span><span class="s">V</span><span class="sh">"</span><span class="p">],</span> <span class="n">V</span><span class="p">],</span> <span class="n">dim</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
            <span class="n">kv_cache</span><span class="p">[</span><span class="sh">"</span><span class="s">K</span><span class="sh">"</span><span class="p">]</span> <span class="o">=</span> <span class="n">K</span>
            <span class="n">kv_cache</span><span class="p">[</span><span class="sh">"</span><span class="s">V</span><span class="sh">"</span><span class="p">]</span> <span class="o">=</span> <span class="n">V</span>

        <span class="c1"># Expand each KV group to cover its assigned query heads
</span>        <span class="c1"># (B, 8 groups, seq, d_k) → (B, 64 heads, seq, d_k)
</span>        <span class="n">K</span> <span class="o">=</span> <span class="n">K</span><span class="p">.</span><span class="nf">repeat_interleave</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">num_queries_per_kv</span><span class="p">,</span> <span class="n">dim</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
        <span class="n">V</span> <span class="o">=</span> <span class="n">V</span><span class="p">.</span><span class="nf">repeat_interleave</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">num_queries_per_kv</span><span class="p">,</span> <span class="n">dim</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>

        <span class="n">scores</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">matmul</span><span class="p">(</span><span class="n">Q</span><span class="p">,</span> <span class="n">K</span><span class="p">.</span><span class="nf">transpose</span><span class="p">(</span><span class="o">-</span><span class="mi">2</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">))</span> <span class="o">/</span> <span class="n">math</span><span class="p">.</span><span class="nf">sqrt</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">d_k</span><span class="p">)</span>
        <span class="n">out</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">matmul</span><span class="p">(</span><span class="n">F</span><span class="p">.</span><span class="nf">softmax</span><span class="p">(</span><span class="n">scores</span><span class="p">,</span> <span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">),</span> <span class="n">V</span><span class="p">)</span>
        <span class="n">out</span> <span class="o">=</span> <span class="n">out</span><span class="p">.</span><span class="nf">transpose</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">).</span><span class="nf">contiguous</span><span class="p">().</span><span class="nf">view</span><span class="p">(</span><span class="n">B</span><span class="p">,</span> <span class="n">T</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="nc">W_o</span><span class="p">(</span><span class="n">out</span><span class="p">),</span> <span class="n">kv_cache</span>
</code></pre></div></div> <p>The critical line is <code class="language-plaintext highlighter-rouge">repeat_interleave</code> — it takes group 1’s K/V and assigns it to heads 1–8, group 2’s to heads 9–16, and so on. Only 8 K/V pairs get stored in the cache, not 64.</p> <p><strong>Real-world impact (Llama 3 70B, 128K context):</strong></p> <table> <thead> <tr> <th>Config</th> <th>KV Heads</th> <th>Cache Size</th> </tr> </thead> <tbody> <tr> <td>Standard MHA</td> <td>64</td> <td>320 GB</td> </tr> <tr> <td>GQA (8 KV heads)</td> <td>8</td> <td><strong>40 GB</strong></td> </tr> <tr> <td>MQA</td> <td>1</td> <td>5 GB</td> </tr> </tbody> </table> <p>40GB is actually manageable on a pair of A100s. Quality versus MHA: barely distinguishable on standard benchmarks. This is why <strong>GQA is now the default</strong> in every modern open-source model — Llama 2/3, Mistral, Gemma, Falcon 180B, Qwen, Phi. If you’re building a transformer today, GQA is the starting point, not MHA.</p> <hr/> <h2 id="technique-3-kv-cache-quantization--shrink-each-number">Technique 3: KV Cache Quantization — Shrink Each Number</h2> <p>GQA reduces the <strong>number</strong> of vectors stored. Quantization reduces the <strong>size of each number</strong> in those vectors.</p> <p>By default, floating point numbers in a transformer are stored in fp16 — 16 bits, or 2 bytes each. The insight behind quantization is that you don’t need full 16-bit precision for every cached value. Most of the information is preserved with 8 bits (int8) or even 4 bits (int4). The numbers get slightly wrong, but not wrong enough to matter for generation quality in most cases.</p> <p>The process works like this: find the largest absolute value in a vector (the scale), divide everything by that scale to fit it into the smaller integer range, store the integer and the scale separately, then multiply back (dequantize) when you need the actual values. It’s lossy compression — like JPEG for your attention vectors.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">QuantizedKVCache</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">bits</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">8</span><span class="p">):</span>
        <span class="n">self</span><span class="p">.</span><span class="n">bits</span> <span class="o">=</span> <span class="n">bits</span>
        <span class="n">self</span><span class="p">.</span><span class="n">cache_k</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="n">self</span><span class="p">.</span><span class="n">cache_v</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="n">self</span><span class="p">.</span><span class="n">scales_k</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="n">self</span><span class="p">.</span><span class="n">scales_v</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span>

    <span class="k">def</span> <span class="nf">_quantize</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">x</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">):</span>
        <span class="c1"># Find the max absolute value per token vector — this becomes our scale
</span>        <span class="n">max_val</span> <span class="o">=</span> <span class="n">x</span><span class="p">.</span><span class="nf">abs</span><span class="p">().</span><span class="nf">amax</span><span class="p">(</span><span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">,</span> <span class="n">keepdim</span><span class="o">=</span><span class="bp">True</span><span class="p">).</span><span class="nf">clamp</span><span class="p">(</span><span class="nb">min</span><span class="o">=</span><span class="mf">1e-8</span><span class="p">)</span>
        <span class="n">scale</span> <span class="o">=</span> <span class="n">max_val</span> <span class="o">/</span> <span class="p">(</span><span class="mi">2</span> <span class="o">**</span> <span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">bits</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span>

        <span class="k">if</span> <span class="n">self</span><span class="p">.</span><span class="n">bits</span> <span class="o">==</span> <span class="mi">8</span><span class="p">:</span>
            <span class="c1"># Compress float range into -128..127
</span>            <span class="n">x_q</span> <span class="o">=</span> <span class="p">(</span><span class="n">x</span> <span class="o">/</span> <span class="n">scale</span><span class="p">).</span><span class="nf">round</span><span class="p">().</span><span class="nf">clamp</span><span class="p">(</span><span class="o">-</span><span class="mi">128</span><span class="p">,</span> <span class="mi">127</span><span class="p">).</span><span class="nf">to</span><span class="p">(</span><span class="n">torch</span><span class="p">.</span><span class="n">int8</span><span class="p">)</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="c1"># 4-bit: compress into -8..7
</span>            <span class="n">x_q</span> <span class="o">=</span> <span class="p">(</span><span class="n">x</span> <span class="o">/</span> <span class="n">scale</span><span class="p">).</span><span class="nf">round</span><span class="p">().</span><span class="nf">clamp</span><span class="p">(</span><span class="o">-</span><span class="mi">8</span><span class="p">,</span> <span class="mi">7</span><span class="p">).</span><span class="nf">to</span><span class="p">(</span><span class="n">torch</span><span class="p">.</span><span class="n">int8</span><span class="p">)</span>

        <span class="k">return</span> <span class="n">x_q</span><span class="p">,</span> <span class="n">scale</span>  <span class="c1"># store both the compressed value and how to undo it
</span>
    <span class="k">def</span> <span class="nf">_dequantize</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">x_q</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">,</span> <span class="n">scale</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">):</span>
        <span class="k">return</span> <span class="n">x_q</span><span class="p">.</span><span class="nf">float</span><span class="p">()</span> <span class="o">*</span> <span class="n">scale</span>  <span class="c1"># undo compression
</span>
    <span class="k">def</span> <span class="nf">append</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">K</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">,</span> <span class="n">V</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">):</span>
        <span class="n">K_q</span><span class="p">,</span> <span class="n">scale_k</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">_quantize</span><span class="p">(</span><span class="n">K</span><span class="p">)</span>
        <span class="n">V_q</span><span class="p">,</span> <span class="n">scale_v</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">_quantize</span><span class="p">(</span><span class="n">V</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">cache_k</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">K_q</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">cache_v</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">V_q</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">scales_k</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">scale_k</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">scales_v</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">scale_v</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="c1"># Dequantize everything before attention computation
</span>        <span class="n">K</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">cat</span><span class="p">([</span><span class="n">self</span><span class="p">.</span><span class="nf">_dequantize</span><span class="p">(</span><span class="n">k</span><span class="p">,</span> <span class="n">s</span><span class="p">)</span> <span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">s</span> <span class="ow">in</span> <span class="nf">zip</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">cache_k</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">scales_k</span><span class="p">)],</span> <span class="n">dim</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
        <span class="n">V</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">cat</span><span class="p">([</span><span class="n">self</span><span class="p">.</span><span class="nf">_dequantize</span><span class="p">(</span><span class="n">v</span><span class="p">,</span> <span class="n">s</span><span class="p">)</span> <span class="k">for</span> <span class="n">v</span><span class="p">,</span> <span class="n">s</span> <span class="ow">in</span> <span class="nf">zip</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">cache_v</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">scales_v</span><span class="p">)],</span> <span class="n">dim</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">K</span><span class="p">,</span> <span class="n">V</span>
</code></pre></div></div> <p>Every token gets quantized as it enters the cache and dequantized just before the attention computation. The GPU stores the compressed integers; the scale factor is tiny (one number per token vector). Net effect: half the memory for int8, quarter for int4.</p> <p><strong>Memory savings vs fp16:</strong></p> <table> <thead> <tr> <th>Precision</th> <th>Bytes/value</th> <th>Reduction</th> </tr> </thead> <tbody> <tr> <td>fp16 (baseline)</td> <td>2</td> <td>1×</td> </tr> <tr> <td>int8</td> <td>1</td> <td>2×</td> </tr> <tr> <td>int4</td> <td>0.5</td> <td>4×</td> </tr> <tr> <td>int2</td> <td>0.25</td> <td>8×</td> </tr> </tbody> </table> <p>The 2024 <strong>KVQuant</strong> paper pushed this further with non-uniform quantization — instead of a uniform grid of integer values, it calibrates the grid shape per-channel to match the actual distribution of key/value vectors. Result: fp16-level perplexity at 2-bit precision. That’s a 8× memory reduction with essentially zero quality loss.</p> <p>Used by: <strong>vLLM</strong> (int8 KV), <strong>llama.cpp</strong>, <strong>TGI</strong>, every major serving framework.</p> <hr/> <h2 id="technique-4-token-eviction--h2o">Technique 4: Token Eviction — H2O</h2> <p>The previous techniques compress how we store tokens. This one asks a different question: <strong>do we need to store all tokens at all?</strong></p> <p>The answer is no — and the reason is one of the most striking empirical findings in LLM research. When you visualize attention weights across a long generation, you find that attention is wildly <strong>non-uniform</strong>. A tiny fraction of tokens — around 5% — receive roughly 80% of all attention mass. The rest barely get noticed.</p> <p>These frequently attended tokens are called <strong>heavy hitters</strong>. They tend to be things like: key facts introduced early in the prompt, entities that the model keeps referencing, structural tokens like paragraph breaks or list markers. The model keeps coming back to them.</p> <p>The remaining 95% of tokens get attended to occasionally or never. Evicting them would barely change the model’s behavior — but it would dramatically shrink the cache.</p> <p><strong>H2O</strong> (Heavy-Hitter Oracle) implements this idea: track cumulative attention scores for every cached token. When the cache exceeds a budget, evict the tokens with the lowest cumulative scores. Always keep the most recent token regardless (since it’s needed for immediate context).</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">H2OKVCache</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">max_cache_size</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">num_heads</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">d_k</span><span class="p">:</span> <span class="nb">int</span><span class="p">):</span>
        <span class="n">self</span><span class="p">.</span><span class="n">max_cache_size</span> <span class="o">=</span> <span class="n">max_cache_size</span>
        <span class="n">self</span><span class="p">.</span><span class="n">num_heads</span> <span class="o">=</span> <span class="n">num_heads</span>
        <span class="n">self</span><span class="p">.</span><span class="n">d_k</span> <span class="o">=</span> <span class="n">d_k</span>
        <span class="n">self</span><span class="p">.</span><span class="n">K</span> <span class="o">=</span> <span class="bp">None</span>
        <span class="n">self</span><span class="p">.</span><span class="n">V</span> <span class="o">=</span> <span class="bp">None</span>
        <span class="c1"># Running tally: how much total attention has each token received?
</span>        <span class="n">self</span><span class="p">.</span><span class="n">attention_scores</span> <span class="o">=</span> <span class="bp">None</span>

    <span class="k">def</span> <span class="nf">update</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">K_new</span><span class="p">,</span> <span class="n">V_new</span><span class="p">,</span> <span class="n">attn_weights</span><span class="p">):</span>
        <span class="sh">"""</span><span class="s">
        Called each generation step.
        attn_weights: how much the new token attended to each cached token.
        We use this to update each cached token</span><span class="sh">'</span><span class="s">s importance score.
        </span><span class="sh">"""</span>
        <span class="k">if</span> <span class="n">self</span><span class="p">.</span><span class="n">K</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
            <span class="n">self</span><span class="p">.</span><span class="n">K</span> <span class="o">=</span> <span class="n">K_new</span>
            <span class="n">self</span><span class="p">.</span><span class="n">V</span> <span class="o">=</span> <span class="n">V_new</span>
            <span class="n">self</span><span class="p">.</span><span class="n">attention_scores</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">zeros</span><span class="p">(</span><span class="n">K_new</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">self</span><span class="p">.</span><span class="n">num_heads</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">device</span><span class="o">=</span><span class="n">K_new</span><span class="p">.</span><span class="n">device</span><span class="p">)</span>
            <span class="k">return</span>

        <span class="c1"># Add this step's attention to the running total for each token
</span>        <span class="n">self</span><span class="p">.</span><span class="n">attention_scores</span> <span class="o">+=</span> <span class="n">attn_weights</span><span class="p">.</span><span class="nf">sum</span><span class="p">(</span><span class="n">dim</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">keepdim</span><span class="o">=</span><span class="bp">True</span><span class="p">).</span><span class="nf">transpose</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span>

        <span class="c1"># Append new token to cache
</span>        <span class="n">self</span><span class="p">.</span><span class="n">K</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">cat</span><span class="p">([</span><span class="n">self</span><span class="p">.</span><span class="n">K</span><span class="p">,</span> <span class="n">K_new</span><span class="p">],</span> <span class="n">dim</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">V</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">cat</span><span class="p">([</span><span class="n">self</span><span class="p">.</span><span class="n">V</span><span class="p">,</span> <span class="n">V_new</span><span class="p">],</span> <span class="n">dim</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
        <span class="n">new_score</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">zeros</span><span class="p">(</span><span class="n">K_new</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">self</span><span class="p">.</span><span class="n">num_heads</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">device</span><span class="o">=</span><span class="n">K_new</span><span class="p">.</span><span class="n">device</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">attention_scores</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">cat</span><span class="p">([</span><span class="n">self</span><span class="p">.</span><span class="n">attention_scores</span><span class="p">,</span> <span class="n">new_score</span><span class="p">],</span> <span class="n">dim</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>

        <span class="c1"># Over budget? Evict the least important tokens
</span>        <span class="k">if</span> <span class="n">self</span><span class="p">.</span><span class="n">K</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">&gt;</span> <span class="n">self</span><span class="p">.</span><span class="n">max_cache_size</span><span class="p">:</span>
            <span class="n">self</span><span class="p">.</span><span class="nf">_evict</span><span class="p">()</span>

    <span class="k">def</span> <span class="nf">_evict</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="n">seq_len</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">K</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span>
        <span class="n">keep</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">max_cache_size</span>
        <span class="n">recent_idx</span> <span class="o">=</span> <span class="n">seq_len</span> <span class="o">-</span> <span class="mi">1</span>

        <span class="c1"># Average importance across all heads
</span>        <span class="n">importance</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">attention_scores</span><span class="p">.</span><span class="nf">mean</span><span class="p">(</span><span class="n">dim</span><span class="o">=</span><span class="mi">1</span><span class="p">).</span><span class="nf">squeeze</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>  <span class="c1"># (batch, seq)
</span>
        <span class="c1"># Protect the most recent token — always needed
</span>        <span class="n">scores_no_recent</span> <span class="o">=</span> <span class="n">importance</span><span class="p">.</span><span class="nf">clone</span><span class="p">()</span>
        <span class="n">scores_no_recent</span><span class="p">[:,</span> <span class="n">recent_idx</span><span class="p">]</span> <span class="o">=</span> <span class="nf">float</span><span class="p">(</span><span class="sh">'</span><span class="s">-inf</span><span class="sh">'</span><span class="p">)</span>

        <span class="c1"># Take the (keep-1) highest-importance historical tokens, plus the recent one
</span>        <span class="n">_</span><span class="p">,</span> <span class="n">top_indices</span> <span class="o">=</span> <span class="n">scores_no_recent</span><span class="p">.</span><span class="nf">topk</span><span class="p">(</span><span class="n">keep</span> <span class="o">-</span> <span class="mi">1</span><span class="p">,</span> <span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">)</span>
        <span class="n">keep_indices</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">cat</span><span class="p">([</span>
            <span class="n">top_indices</span><span class="p">,</span>
            <span class="n">torch</span><span class="p">.</span><span class="nf">tensor</span><span class="p">([[</span><span class="n">recent_idx</span><span class="p">]],</span> <span class="n">device</span><span class="o">=</span><span class="n">top_indices</span><span class="p">.</span><span class="n">device</span><span class="p">).</span><span class="nf">expand</span><span class="p">(</span><span class="n">top_indices</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span>
        <span class="p">],</span> <span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">)</span>
        <span class="n">keep_indices</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">keep_indices</span><span class="p">.</span><span class="nf">sort</span><span class="p">(</span><span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">)</span>

        <span class="c1"># Discard the evicted tokens from K, V, and score trackers
</span>        <span class="n">idx</span> <span class="o">=</span> <span class="n">keep_indices</span><span class="p">.</span><span class="nf">unsqueeze</span><span class="p">(</span><span class="mi">1</span><span class="p">).</span><span class="nf">unsqueeze</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">).</span><span class="nf">expand</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">num_heads</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">d_k</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">K</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">gather</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">K</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="n">idx</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">V</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">gather</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">V</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="n">idx</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">attention_scores</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">gather</span><span class="p">(</span>
            <span class="n">self</span><span class="p">.</span><span class="n">attention_scores</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span>
            <span class="n">keep_indices</span><span class="p">.</span><span class="nf">unsqueeze</span><span class="p">(</span><span class="mi">1</span><span class="p">).</span><span class="nf">expand</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">num_heads</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span>
        <span class="p">)</span>
</code></pre></div></div> <p>The <code class="language-plaintext highlighter-rouge">_evict</code> method is the core: sort tokens by cumulative attention received, keep the top-K, drop the rest. The recently added token is always protected from eviction because it hasn’t had time to accumulate attention scores yet.</p> <p><strong>H2O results</strong>: 20× cache compression with less than 1% accuracy degradation on most benchmarks. Set a cache budget of 5% of your sequence length and you barely notice the difference on most tasks.</p> <p>Why? Because the model was already ignoring 95% of the tokens. Evicting them just makes the neglect explicit.</p> <hr/> <h2 id="technique-5-streamingllm--infinite-context-at-fixed-memory">Technique 5: StreamingLLM — Infinite Context at Fixed Memory</h2> <p>H2O evicts the least-attended tokens. But researchers at MIT made a more fundamental observation: <strong>not all tokens are equal from the moment they’re created</strong>.</p> <p>When you visualize attention patterns across thousands of generation steps, two patterns appear consistently:</p> <p><strong>Pattern 1 — Locality</strong>: most attention concentrates on the last few hundred tokens. The model cares most about what it just said. This is intuitive — you need your immediate context to write coherently.</p> <p><strong>Pattern 2 — Attention sinks</strong>: the very first tokens in any sequence (positions 0, 1, 2, 3) receive surprisingly high attention throughout the entire generation — even when their content is completely irrelevant to the current generation step. A document about quantum physics will still have heavy attention on its first four tokens, even 50,000 tokens later.</p> <p>This second pattern is counterintuitive and strange. Why would a model keep attending to the first few words of a document it’s now thousands of tokens past?</p> <p>The answer is a training artifact. During training, the softmax over attention scores must sum to 1. When a token has nothing relevant to attend to, the probability mass has to go somewhere. The model learned to dump it onto the first few tokens as a kind of “soft null” — they absorb the leftover probability. They’re not semantically important, they’re structurally necessary as garbage collectors for the softmax.</p> <p><strong>StreamingLLM</strong> uses both observations to build a fixed-size cache for infinite-length generation: always keep the first 4 tokens (the sinks) plus a rolling window of the most recent N tokens. Everything between the sink tokens and the window gets dropped permanently.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">StreamingKVCache</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">sink_size</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">4</span><span class="p">,</span> <span class="n">window_size</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">1020</span><span class="p">):</span>
        <span class="sh">"""</span><span class="s">
        Total cache = sink_size + window_size (constant, regardless of sequence length).
        The sink tokens anchor the softmax. The window provides local context.
        Everything older than the window is evicted.
        </span><span class="sh">"""</span>
        <span class="n">self</span><span class="p">.</span><span class="n">sink_size</span> <span class="o">=</span> <span class="n">sink_size</span>
        <span class="n">self</span><span class="p">.</span><span class="n">window_size</span> <span class="o">=</span> <span class="n">window_size</span>
        <span class="n">self</span><span class="p">.</span><span class="n">sink_K</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">sink_V</span> <span class="o">=</span> <span class="bp">None</span>
        <span class="n">self</span><span class="p">.</span><span class="n">window_K</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">window_V</span> <span class="o">=</span> <span class="bp">None</span>

    <span class="k">def</span> <span class="nf">update</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">K_new</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">,</span> <span class="n">V_new</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">):</span>
        <span class="c1"># Phase 1: fill the sink buffer with the first few tokens
</span>        <span class="k">if</span> <span class="n">self</span><span class="p">.</span><span class="n">sink_K</span> <span class="ow">is</span> <span class="bp">None</span> <span class="ow">or</span> <span class="n">self</span><span class="p">.</span><span class="n">sink_K</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">&lt;</span> <span class="n">self</span><span class="p">.</span><span class="n">sink_size</span><span class="p">:</span>
            <span class="n">self</span><span class="p">.</span><span class="n">sink_K</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">cat</span><span class="p">([</span><span class="n">self</span><span class="p">.</span><span class="n">sink_K</span><span class="p">,</span> <span class="n">K_new</span><span class="p">],</span> <span class="n">dim</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span> <span class="k">if</span> <span class="n">self</span><span class="p">.</span><span class="n">sink_K</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span> <span class="k">else</span> <span class="n">K_new</span>
            <span class="n">self</span><span class="p">.</span><span class="n">sink_V</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">cat</span><span class="p">([</span><span class="n">self</span><span class="p">.</span><span class="n">sink_V</span><span class="p">,</span> <span class="n">V_new</span><span class="p">],</span> <span class="n">dim</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span> <span class="k">if</span> <span class="n">self</span><span class="p">.</span><span class="n">sink_V</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span> <span class="k">else</span> <span class="n">V_new</span>
            <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="nf">get</span><span class="p">()</span>

        <span class="c1"># Phase 2: everything after the sink goes into the rolling window
</span>        <span class="k">if</span> <span class="n">self</span><span class="p">.</span><span class="n">window_K</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
            <span class="n">self</span><span class="p">.</span><span class="n">window_K</span> <span class="o">=</span> <span class="n">K_new</span>
            <span class="n">self</span><span class="p">.</span><span class="n">window_V</span> <span class="o">=</span> <span class="n">V_new</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="n">self</span><span class="p">.</span><span class="n">window_K</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">cat</span><span class="p">([</span><span class="n">self</span><span class="p">.</span><span class="n">window_K</span><span class="p">,</span> <span class="n">K_new</span><span class="p">],</span> <span class="n">dim</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
            <span class="n">self</span><span class="p">.</span><span class="n">window_V</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">cat</span><span class="p">([</span><span class="n">self</span><span class="p">.</span><span class="n">window_V</span><span class="p">,</span> <span class="n">V_new</span><span class="p">],</span> <span class="n">dim</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>

            <span class="c1"># Window full — drop the oldest token (slide forward)
</span>            <span class="k">if</span> <span class="n">self</span><span class="p">.</span><span class="n">window_K</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">&gt;</span> <span class="n">self</span><span class="p">.</span><span class="n">window_size</span><span class="p">:</span>
                <span class="n">self</span><span class="p">.</span><span class="n">window_K</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">window_K</span><span class="p">[:,</span> <span class="p">:,</span> <span class="mi">1</span><span class="p">:,</span> <span class="p">:]</span>
                <span class="n">self</span><span class="p">.</span><span class="n">window_V</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">window_V</span><span class="p">[:,</span> <span class="p">:,</span> <span class="mi">1</span><span class="p">:,</span> <span class="p">:]</span>

        <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="nf">get</span><span class="p">()</span>

    <span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="k">if</span> <span class="n">self</span><span class="p">.</span><span class="n">window_K</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="n">sink_K</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">sink_V</span>
        <span class="nf">return </span><span class="p">(</span>
            <span class="n">torch</span><span class="p">.</span><span class="nf">cat</span><span class="p">([</span><span class="n">self</span><span class="p">.</span><span class="n">sink_K</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">window_K</span><span class="p">],</span> <span class="n">dim</span><span class="o">=</span><span class="mi">2</span><span class="p">),</span>
            <span class="n">torch</span><span class="p">.</span><span class="nf">cat</span><span class="p">([</span><span class="n">self</span><span class="p">.</span><span class="n">sink_V</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">window_V</span><span class="p">],</span> <span class="n">dim</span><span class="o">=</span><span class="mi">2</span><span class="p">),</span>
        <span class="p">)</span>
</code></pre></div></div> <p>The <code class="language-plaintext highlighter-rouge">window_K[:, :, 1:, :]</code> line is the sliding — every new token pushes the oldest window token off the edge. But the sink tokens never move.</p> <p><strong>What happens without the sinks?</strong> If you just use a sliding window with no sink tokens, the model collapses — perplexity spikes dramatically after the context fills. The softmax has nowhere to send its garbage probability mass and the distribution breaks down. The 4 sink tokens are tiny in cost but critical in function.</p> <p><strong>StreamingLLM result</strong>: Llama 2 ran on 4-million-token documents using a 1024-token cache. 22× memory reduction. Constant memory usage no matter how long the generation runs.</p> <p>Used by: <strong>Mistral</strong> (sliding window attention), <strong>Gemma</strong>, <strong>Longformer</strong>.</p> <hr/> <h2 id="technique-6-snapkv--compress-before-you-generate">Technique 6: SnapKV — Compress Before You Generate</h2> <p>All the techniques above deal with the cache during generation — either evicting tokens as you go or using a fixed window. <strong>SnapKV</strong> takes a step back and asks: can we compress the cache <em>before</em> generation even starts?</p> <p>When an LLM processes a long prompt (say, 50,000 tokens of context before generating an answer), it runs what’s called a <strong>prefill</strong> step — a full forward pass over the entire prompt in parallel. During this prefill, every token attends to every other token, producing a full attention matrix.</p> <p>The SnapKV insight: that prefill attention matrix tells you which tokens are important before generation begins. Tokens that many other tokens attend to during prefill are likely to stay important during decoding. You can use the prefill attention to pre-select which tokens to keep in the cache, and discard the rest before decoding starts.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">snapkv_compress</span><span class="p">(</span><span class="n">K</span><span class="p">,</span> <span class="n">V</span><span class="p">,</span> <span class="n">attn_weights</span><span class="p">,</span> <span class="n">compression_ratio</span><span class="o">=</span><span class="mf">0.3</span><span class="p">):</span>
    <span class="sh">"""</span><span class="s">
    Run this after prefill, before generation starts.

    We look at how much attention each token received from others during prefill.
    High-attention tokens are </span><span class="sh">"</span><span class="s">important</span><span class="sh">"</span><span class="s"> — keep them.
    Low-attention tokens are likely padding or filler — discard them.

    compression_ratio = 0.3 means keep only the top 30% of tokens.
    </span><span class="sh">"""</span>
    <span class="n">B</span><span class="p">,</span> <span class="n">H</span><span class="p">,</span> <span class="n">S</span><span class="p">,</span> <span class="n">d_k</span> <span class="o">=</span> <span class="n">K</span><span class="p">.</span><span class="n">shape</span>
    <span class="n">keep_count</span> <span class="o">=</span> <span class="nf">max</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="nf">int</span><span class="p">(</span><span class="n">S</span> <span class="o">*</span> <span class="n">compression_ratio</span><span class="p">))</span>

    <span class="c1"># Column sums of attention matrix: how much did each token receive?
</span>    <span class="c1"># Shape: (B, H, S) — one score per token per head
</span>    <span class="n">importance</span> <span class="o">=</span> <span class="n">attn_weights</span><span class="p">.</span><span class="nf">sum</span><span class="p">(</span><span class="n">dim</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>

    <span class="c1"># Average importance across all heads to get a single score per token
</span>    <span class="n">avg_importance</span> <span class="o">=</span> <span class="n">importance</span><span class="p">.</span><span class="nf">mean</span><span class="p">(</span><span class="n">dim</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>  <span class="c1"># (B, S)
</span>
    <span class="c1"># Pick the top-k most attended-to tokens
</span>    <span class="n">_</span><span class="p">,</span> <span class="n">top_indices</span> <span class="o">=</span> <span class="n">avg_importance</span><span class="p">.</span><span class="nf">topk</span><span class="p">(</span><span class="n">keep_count</span><span class="p">,</span> <span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">)</span>
    <span class="n">top_indices</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">top_indices</span><span class="p">.</span><span class="nf">sort</span><span class="p">(</span><span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">)</span>  <span class="c1"># preserve original order
</span>
    <span class="c1"># Build the compressed cache — only these tokens enter generation
</span>    <span class="n">idx</span> <span class="o">=</span> <span class="n">top_indices</span><span class="p">.</span><span class="nf">unsqueeze</span><span class="p">(</span><span class="mi">1</span><span class="p">).</span><span class="nf">unsqueeze</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">).</span><span class="nf">expand</span><span class="p">(</span><span class="n">B</span><span class="p">,</span> <span class="n">H</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="n">d_k</span><span class="p">)</span>
    <span class="n">K_compressed</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">gather</span><span class="p">(</span><span class="n">K</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="n">idx</span><span class="p">)</span>
    <span class="n">V_compressed</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">gather</span><span class="p">(</span><span class="n">V</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="n">idx</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">K_compressed</span><span class="p">,</span> <span class="n">V_compressed</span>
</code></pre></div></div> <p>The key idea is <code class="language-plaintext highlighter-rouge">attn_weights.sum(dim=2)</code> — summing across the “who is attending” dimension to get “how much is each token being attended to.” High score = many tokens find this one important = keep it. Low score = nobody’s looking at this = safe to discard.</p> <p><strong>SnapKV results</strong>: 3.6× cache reduction, 3.8× speedup on generation, less than 1% accuracy loss on LongBench. Particularly strong for RAG-style tasks where the prompt is mostly retrieved context — most chunks aren’t relevant to the answer, and SnapKV confidently discards them based on prefill attention.</p> <p>The difference from H2O: SnapKV makes one compression decision upfront (at prefill time) and never changes it. H2O continuously evicts during generation. SnapKV is simpler and faster; H2O adapts dynamically as the generation unfolds.</p> <hr/> <h2 id="the-full-picture-stacking-techniques">The Full Picture: Stacking Techniques</h2> <p>These aren’t competing approaches — they’re layers. Production systems combine them:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GQA              → reduces number of K/V vectors stored
  + INT8 quant   → halves bytes per stored value
  + H2O eviction → further reduces token count dynamically
  + Sliding window → caps total cache at constant size
</code></pre></div></div> <p>Here’s what that stacking does for <strong>Llama 3 70B at 128K context</strong>:</p> <table> <thead> <tr> <th>What’s applied</th> <th>Cache Size</th> </tr> </thead> <tbody> <tr> <td>Baseline MHA, fp16</td> <td>320 GB</td> </tr> <tr> <td>+ GQA (8 KV heads)</td> <td>40 GB</td> </tr> <tr> <td>+ INT8 quantization</td> <td>20 GB</td> </tr> <tr> <td>+ H2O (20% budget)</td> <td><strong>4 GB</strong></td> </tr> </tbody> </table> <p>From 320 GB to 4 GB. Same model. Same outputs (approximately). Now fits on a single A100 with room for the model weights too.</p> <hr/> <h2 id="why-this-also-matters-for-speed">Why This Also Matters for Speed</h2> <p>Smaller cache isn’t just about fitting in memory — it directly affects <strong>how fast the model generates tokens</strong>.</p> <p>Every time the model generates a new token, it reads the entire KV cache from GPU high-bandwidth memory (HBM). The speed of that read is bounded by HBM bandwidth — for an A100, that’s about 2 TB/s. At 128K context with GQA 70B:</p> <ul> <li>Cache size: ~40 GB</li> <li>Time to read: 40 GB ÷ 2 TB/s = <strong>20ms per token</strong> → ceiling of 50 tokens/second</li> </ul> <p>Apply H2O + quantization to bring cache to 4 GB:</p> <ul> <li>Time to read: 4 GB ÷ 2 TB/s = <strong>2ms per token</strong> → ceiling of 500 tokens/second</li> </ul> <p>A <strong>10× throughput gain</strong> from the same GPU, same model, same weights. This is why inference frameworks like vLLM, TensorRT-LLM, and SGLang have invested so heavily in KV cache engineering — it’s not just about memory, it’s about the money you spend on compute per generated token.</p> <hr/> <h2 id="key-takeaways">Key Takeaways</h2> <ol> <li> <p><strong>KV cache is the dominant memory cost at long context</strong> — at 128K tokens, the cache is bigger than the model weights. This is the problem every technique here is solving.</p> </li> <li> <p><strong>GQA is the new baseline</strong> — 8× memory reduction, negligible quality loss. Every serious model uses it now. MHA is legacy.</p> </li> <li> <p><strong>Quantization stacks cleanly on top of GQA</strong> — INT8 gives you 2× more, INT4 gives 4× more, 2-bit is viable with per-channel calibration.</p> </li> <li> <p><strong>Most tokens don’t matter</strong> — 80% of attention concentrates on 5% of tokens. Eviction strategies (H2O, SnapKV) exploit this to compress aggressively without hurting quality.</p> </li> <li> <p><strong>Attention sinks are a real phenomenon</strong> — the first few tokens in any sequence must always be kept. Removing them breaks generation even when their content is irrelevant. This is a training artifact, not a design choice.</p> </li> <li> <p><strong>Cache size = latency</strong> — HBM bandwidth is the bottleneck during decoding. Smaller cache means faster reads means more tokens per second. Memory compression and speed optimization are the same problem.</p> </li> </ol> <hr/> <h2 id="further-reading">Further Reading</h2> <h3 id="kv-cache-fundamentals">KV Cache Fundamentals</h3> <ul> <li> <p><strong><a href="https://arxiv.org/abs/2309.06180">Efficient Memory Management for Large Language Model Serving with PagedAttention</a></strong> — Kwon et al., UC Berkeley (2023) The vLLM paper. Introduces virtual memory paging for KV cache — eliminates fragmentation, enables dynamic batching across requests. The production standard for KV cache management.</p> </li> <li> <p><strong><a href="https://arxiv.org/abs/1911.02150">Fast Transformer Decoding: One Write-Head is All You Need (MQA)</a></strong> — Shazeer, Google (2019) The original MQA paper. Shows that sharing KV heads barely hurts quality while massively reducing memory and improving decode speed. The idea GQA refined.</p> </li> <li> <p><strong><a href="https://arxiv.org/abs/2305.13245">GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints</a></strong> — Ainslie et al., Google (2023) GQA paper. Introduces the grouped approach and shows how to convert existing MHA checkpoints via uptraining. The technique behind Llama 2, Mistral, and every modern open-source model.</p> </li> </ul> <h3 id="quantization">Quantization</h3> <ul> <li> <p><strong><a href="https://arxiv.org/abs/2401.18079">KVQuant: Towards 10 Million Context Length LLM Inference with KV Cache Quantization</a></strong> — Hooper et al., UC Berkeley (2024) Non-uniform quantization calibrated per-channel. Achieves fp16 perplexity at 2-bit precision. The paper that proved INT2 KV cache is viable.</p> </li> <li> <p><strong><a href="https://arxiv.org/abs/2402.02750">KIVI: A Tuning-Free Asymmetric 2-bit Quantization for KV Cache</a></strong> — Liu et al. (2024) Keys and values have different numerical distributions — KIVI quantizes them differently. Clean result: 2-bit KV cache with almost no accuracy loss, no fine-tuning required.</p> </li> </ul> <h3 id="eviction--compression">Eviction &amp; Compression</h3> <ul> <li> <p><strong><a href="https://arxiv.org/abs/2306.14048">H2O: Heavy-Hitter Oracle for Efficient Generative Inference of Large Language Models</a></strong> — Zhang et al., Rice University (2023) The paper that identified the heavy-hitter phenomenon. 20× cache compression with less than 1% degradation. Empirically shows that 80% of attention concentrates on 5% of tokens.</p> </li> <li> <p><strong><a href="https://arxiv.org/abs/2404.14469">SnapKV: LLM Knows What You are Looking for Before Generation</a></strong> — Li et al. (2024) Prefill-time compression. Uses the attention matrix from prompt processing to predict which tokens will matter during decoding. 3.6× compression, 3.8× speedup.</p> </li> <li> <p><strong><a href="https://arxiv.org/abs/2309.17453">Efficient Streaming Language Models with Attention Sinks (StreamingLLM)</a></strong> — Xiao et al., MIT (2023) Discovers and explains attention sinks. Shows that 4 sink tokens + rolling window enables infinite-length generation at constant memory. Clean, surprising, and practically useful.</p> </li> </ul> <h3 id="systems">Systems</h3> <ul> <li> <p><strong><a href="https://arxiv.org/abs/2307.08691">FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning</a></strong> — Dao (2023) IO-aware attention that tiles computation to avoid materializing the full O(n²) attention matrix. The algorithm that makes long-context attention practical from a compute perspective — complements KV cache compression from the memory side.</p> </li> <li> <p><strong><a href="https://arxiv.org/abs/2308.16369">Sarathi-Serve: Efficient LLM Inference by Piggybacking Decodes with Chunked Prefills</a></strong> — Agrawal et al. (2023) Production serving paper on pipelining prefill and decode efficiently. The operational reality of running bounded KV cache systems at scale.</p> </li> </ul> <hr/> <p><em>The context window arms race isn’t about model capability — it’s about memory engineering. Every extra token you can fit in context at the same VRAM budget is a capability gain. The teams winning on long context aren’t necessarily building smarter models. They’re building smarter caches.</em></p>]]></content><author><name></name></author><category term="deep-learning"/><category term="KV Cache"/><category term="Attention"/><category term="LLMs"/><category term="Memory Optimization"/><category term="GQA"/><category term="Quantization"/><category term="Long Context"/><category term="PyTorch"/><summary type="html"><![CDATA[Every token you generate costs memory — permanently. At 1M token contexts, the KV cache alone can consume 100GB+ of VRAM. Here's how modern LLMs compress it without losing what matters.]]></summary></entry><entry><title type="html">MCP Server Starter: Build Tools AI Agents Can Actually Use</title><link href="https://sridhar-3009.github.io/blog/2026/mcp-server-starter/" rel="alternate" type="text/html" title="MCP Server Starter: Build Tools AI Agents Can Actually Use"/><published>2026-04-21T00:00:00+00:00</published><updated>2026-04-21T00:00:00+00:00</updated><id>https://sridhar-3009.github.io/blog/2026/mcp-server-starter</id><content type="html" xml:base="https://sridhar-3009.github.io/blog/2026/mcp-server-starter/"><![CDATA[<h1 id="mcp-server-starter-build-tools-ai-agents-can-actually-use">MCP Server Starter: Build Tools AI Agents Can Actually Use</h1> <p>Every demo of an AI agent shows the same thing: the agent calls a tool, gets data back, generates an answer. What nobody shows is <strong>what that tool server actually looks like</strong> and how to build one yourself.</p> <p>The <strong>Model Context Protocol (MCP)</strong> is the emerging standard for exactly this — a clean interface between AI agents and the tools/APIs they need to interact with. Think of it as the HTTP for agent-tool communication. The agent doesn’t need to know how your weather API works internally; it just needs to know what tools exist and how to call them.</p> <p>This post walks through building an MCP-style server from scratch with Python and FastAPI — the minimal version that teaches you the pattern, plus the upgrade path to production.</p> <hr/> <h2 id="the-core-flow">The Core Flow</h2> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>AI Agent → MCP Server → Tools / External APIs
</code></pre></div></div> <p>Three actors:</p> <ul> <li><strong>AI Agent</strong> — the LLM runtime (Claude, GPT-4, your own agent loop) that decides which tool to call</li> <li><strong>MCP Server</strong> — your FastAPI app that exposes tools as HTTP endpoints</li> <li><strong>Tools</strong> — functions that do real work: fetch weather, query a database, send a Slack message</li> </ul> <p>The agent doesn’t hallucinate tool results because it gets them from your server. Your server doesn’t need to understand the agent because it just responds to HTTP requests. Clean separation.</p> <hr/> <h2 id="setup">Setup</h2> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>fastapi uvicorn requests
</code></pre></div></div> <p>That’s the full dependency list for the starter. FastAPI handles routing and serialization; Uvicorn is the ASGI server.</p> <hr/> <h2 id="project-structure">Project Structure</h2> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mcp-server/
├── main.py
└── README.md
</code></pre></div></div> <p>Start flat. Add structure when you have more than ~5 tools.</p> <hr/> <h2 id="the-server">The Server</h2> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">fastapi</span> <span class="kn">import</span> <span class="n">FastAPI</span>

<span class="n">app</span> <span class="o">=</span> <span class="nc">FastAPI</span><span class="p">()</span>

<span class="nd">@app.get</span><span class="p">(</span><span class="sh">"</span><span class="s">/tool/weather</span><span class="sh">"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">weather</span><span class="p">(</span><span class="n">city</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
    <span class="k">return</span> <span class="p">{</span>
        <span class="sh">"</span><span class="s">tool</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">weather</span><span class="sh">"</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">city</span><span class="sh">"</span><span class="p">:</span> <span class="n">city</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">temp</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">32°C</span><span class="sh">"</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">status</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">Sunny</span><span class="sh">"</span>
    <span class="p">}</span>

<span class="nd">@app.get</span><span class="p">(</span><span class="sh">"</span><span class="s">/tool/users</span><span class="sh">"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">users</span><span class="p">():</span>
    <span class="k">return</span> <span class="p">{</span>
        <span class="sh">"</span><span class="s">tool</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">users</span><span class="sh">"</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">data</span><span class="sh">"</span><span class="p">:</span> <span class="p">[</span>
            <span class="p">{</span><span class="sh">"</span><span class="s">id</span><span class="sh">"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="sh">"</span><span class="s">name</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">Rahul</span><span class="sh">"</span><span class="p">},</span>
            <span class="p">{</span><span class="sh">"</span><span class="s">id</span><span class="sh">"</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span> <span class="sh">"</span><span class="s">name</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">Priya</span><span class="sh">"</span><span class="p">}</span>
        <span class="p">]</span>
    <span class="p">}</span>

<span class="nd">@app.get</span><span class="p">(</span><span class="sh">"</span><span class="s">/tools</span><span class="sh">"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">tools</span><span class="p">():</span>
    <span class="k">return</span> <span class="p">{</span>
        <span class="sh">"</span><span class="s">tools</span><span class="sh">"</span><span class="p">:</span> <span class="p">[</span>
            <span class="p">{</span><span class="sh">"</span><span class="s">name</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">weather</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">endpoint</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">/tool/weather?city=Delhi</span><span class="sh">"</span><span class="p">},</span>
            <span class="p">{</span><span class="sh">"</span><span class="s">name</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">users</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">endpoint</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">/tool/users</span><span class="sh">"</span><span class="p">}</span>
        <span class="p">]</span>
    <span class="p">}</span>
</code></pre></div></div> <p>Run it:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uvicorn main:app <span class="nt">--reload</span>
</code></pre></div></div> <p>Server up at <code class="language-plaintext highlighter-rouge">http://127.0.0.1:8000</code>.</p> <hr/> <h2 id="the-three-endpoints">The Three Endpoints</h2> <h3 id="tool-discovery--tools">Tool Discovery — <code class="language-plaintext highlighter-rouge">/tools</code></h3> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /tools
</code></pre></div></div> <p>This is the contract between agent and server. The agent hits this endpoint first, gets back a list of available tools and their call signatures. No hardcoding required — the agent adapts to whatever tools your server advertises.</p> <h3 id="weather-tool--toolweather">Weather Tool — <code class="language-plaintext highlighter-rouge">/tool/weather</code></h3> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /tool/weather?city<span class="o">=</span>Hyderabad
</code></pre></div></div> <p>Response:</p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"tool"</span><span class="p">:</span><span class="w"> </span><span class="s2">"weather"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"city"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Hyderabad"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"temp"</span><span class="p">:</span><span class="w"> </span><span class="s2">"32°C"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Sunny"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div> <h3 id="users-tool--toolusers">Users Tool — <code class="language-plaintext highlighter-rouge">/tool/users</code></h3> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /tool/users
</code></pre></div></div> <p>Response:</p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"tool"</span><span class="p">:</span><span class="w"> </span><span class="s2">"users"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"data"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w"> </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Rahul"</span><span class="w"> </span><span class="p">},</span><span class="w">
    </span><span class="p">{</span><span class="w"> </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Priya"</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div> <hr/> <h2 id="how-an-ai-agent-uses-this">How an AI Agent Uses This</h2> <p>The agent loop looks like this:</p> <ol> <li><strong>Discover</strong> — <code class="language-plaintext highlighter-rouge">GET /tools</code> → parse available tool names and endpoints</li> <li><strong>Decide</strong> — LLM sees user query, picks the right tool</li> <li><strong>Call</strong> — agent hits the chosen endpoint with required parameters</li> <li><strong>Use</strong> — returned JSON feeds back into the LLM context to generate the final answer</li> </ol> <p>No magic. No framework required. Any agent that can make HTTP requests can use this server — Claude via tool use, GPT-4 function calling, a custom ReAct loop, anything.</p> <hr/> <h2 id="upgrade-path">Upgrade Path</h2> <p>The starter above is intentionally minimal. Here’s where to take it:</p> <p><strong>Auth layer</strong> — add API key validation via FastAPI’s <code class="language-plaintext highlighter-rouge">Depends</code>:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">fastapi</span> <span class="kn">import</span> <span class="n">Header</span><span class="p">,</span> <span class="n">HTTPException</span>

<span class="k">async</span> <span class="k">def</span> <span class="nf">verify_api_key</span><span class="p">(</span><span class="n">x_api_key</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="nc">Header</span><span class="p">(...)):</span>
    <span class="k">if</span> <span class="n">x_api_key</span> <span class="o">!=</span> <span class="sh">"</span><span class="s">your-secret-key</span><span class="sh">"</span><span class="p">:</span>
        <span class="k">raise</span> <span class="nc">HTTPException</span><span class="p">(</span><span class="n">status_code</span><span class="o">=</span><span class="mi">401</span><span class="p">,</span> <span class="n">detail</span><span class="o">=</span><span class="sh">"</span><span class="s">Invalid API key</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div></div> <p><strong>Real data</strong> — swap the hardcoded returns for actual API calls or database queries:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">httpx</span>

<span class="nd">@app.get</span><span class="p">(</span><span class="sh">"</span><span class="s">/tool/weather</span><span class="sh">"</span><span class="p">)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">weather</span><span class="p">(</span><span class="n">city</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
    <span class="k">async</span> <span class="k">with</span> <span class="n">httpx</span><span class="p">.</span><span class="nc">AsyncClient</span><span class="p">()</span> <span class="k">as</span> <span class="n">client</span><span class="p">:</span>
        <span class="n">resp</span> <span class="o">=</span> <span class="k">await</span> <span class="n">client</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span>
            <span class="sh">"</span><span class="s">https://api.openweathermap.org/data/2.5/weather</span><span class="sh">"</span><span class="p">,</span>
            <span class="n">params</span><span class="o">=</span><span class="p">{</span><span class="sh">"</span><span class="s">q</span><span class="sh">"</span><span class="p">:</span> <span class="n">city</span><span class="p">,</span> <span class="sh">"</span><span class="s">appid</span><span class="sh">"</span><span class="p">:</span> <span class="n">WEATHER_API_KEY</span><span class="p">,</span> <span class="sh">"</span><span class="s">units</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">metric</span><span class="sh">"</span><span class="p">}</span>
        <span class="p">)</span>
    <span class="n">data</span> <span class="o">=</span> <span class="n">resp</span><span class="p">.</span><span class="nf">json</span><span class="p">()</span>
    <span class="k">return</span> <span class="p">{</span>
        <span class="sh">"</span><span class="s">tool</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">weather</span><span class="sh">"</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">city</span><span class="sh">"</span><span class="p">:</span> <span class="n">city</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">temp</span><span class="sh">"</span><span class="p">:</span> <span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">data</span><span class="p">[</span><span class="sh">'</span><span class="s">main</span><span class="sh">'</span><span class="p">][</span><span class="sh">'</span><span class="s">temp</span><span class="sh">'</span><span class="p">]</span><span class="si">}</span><span class="s">°C</span><span class="sh">"</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">status</span><span class="sh">"</span><span class="p">:</span> <span class="n">data</span><span class="p">[</span><span class="sh">"</span><span class="s">weather</span><span class="sh">"</span><span class="p">][</span><span class="mi">0</span><span class="p">][</span><span class="sh">"</span><span class="s">description</span><span class="sh">"</span><span class="p">],</span>
    <span class="p">}</span>
</code></pre></div></div> <p><strong>Memory layer</strong> — add a simple in-memory store (or Redis) so agents can persist state across calls:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">collections</span> <span class="kn">import</span> <span class="n">defaultdict</span>

<span class="n">memory</span><span class="p">:</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">list</span><span class="p">]</span> <span class="o">=</span> <span class="nf">defaultdict</span><span class="p">(</span><span class="nb">list</span><span class="p">)</span>

<span class="nd">@app.post</span><span class="p">(</span><span class="sh">"</span><span class="s">/memory/{session_id}</span><span class="sh">"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">store</span><span class="p">(</span><span class="n">session_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">payload</span><span class="p">:</span> <span class="nb">dict</span><span class="p">):</span>
    <span class="n">memory</span><span class="p">[</span><span class="n">session_id</span><span class="p">].</span><span class="nf">append</span><span class="p">(</span><span class="n">payload</span><span class="p">)</span>
    <span class="k">return</span> <span class="p">{</span><span class="sh">"</span><span class="s">stored</span><span class="sh">"</span><span class="p">:</span> <span class="bp">True</span><span class="p">}</span>

<span class="nd">@app.get</span><span class="p">(</span><span class="sh">"</span><span class="s">/memory/{session_id}</span><span class="sh">"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">recall</span><span class="p">(</span><span class="n">session_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
    <span class="k">return</span> <span class="p">{</span><span class="sh">"</span><span class="s">history</span><span class="sh">"</span><span class="p">:</span> <span class="n">memory</span><span class="p">[</span><span class="n">session_id</span><span class="p">]}</span>
</code></pre></div></div> <p><strong>Logging</strong> — every tool call should log <code class="language-plaintext highlighter-rouge">(timestamp, tool_name, params, response_time)</code>. Non-negotiable in production. Agents fail silently without it.</p> <hr/> <h2 id="production-use-cases">Production Use Cases</h2> <ul> <li><strong>Customer support agents</strong> — tools that query your CRM, open tickets, look up order history</li> <li><strong>Internal company assistants</strong> — tools that pull from Confluence, Jira, Slack</li> <li><strong>Sales reporting bots</strong> — tools that hit your data warehouse and return formatted metrics</li> <li><strong>Research agents</strong> — tools that search the web, fetch papers, summarize documents</li> <li><strong>CRM automation</strong> — tools that create contacts, log calls, send follow-up emails</li> </ul> <p>Same pattern in every case: define the tool, expose it as an endpoint, register it in <code class="language-plaintext highlighter-rouge">/tools</code>. The agent handles the reasoning; your server handles the execution.</p> <hr/> <h2 id="key-takeaways">Key Takeaways</h2> <ol> <li> <p><strong>MCP is just HTTP with a discovery contract</strong> — <code class="language-plaintext highlighter-rouge">/tools</code> tells the agent what exists; individual endpoints do the work. No exotic protocol needed to start.</p> </li> <li> <p><strong>Tool discovery is the key endpoint</strong> — it’s what makes your server agent-agnostic. Any LLM that can read JSON can use your tools without custom integration.</p> </li> <li> <p><strong>Keep tools single-responsibility</strong> — one endpoint, one job. Agents get confused by tools that do too much. <code class="language-plaintext highlighter-rouge">/tool/weather</code> does weather. That’s it.</p> </li> <li> <p><strong>Auth before production</strong> — the starter has no auth. Add API key verification before exposing any real data.</p> </li> <li> <p><strong>Logging is observability</strong> — you can’t debug a multi-agent system without knowing what tool was called, with what params, and what came back.</p> </li> </ol> <hr/> <h2 id="further-reading">Further Reading</h2> <ul> <li> <p><strong><a href="https://modelcontextprotocol.io/introduction">Model Context Protocol Spec</a></strong> — Anthropic’s official MCP specification. The standard this starter is inspired by — covers the full protocol including resources, prompts, and sampling beyond just tools.</p> </li> <li> <p><strong><a href="https://fastapi.tiangolo.com/">FastAPI Docs</a></strong> — Best Python API framework for this use case. Dependency injection, auto-generated OpenAPI docs, and async support out of the box.</p> </li> <li> <p><strong><a href="https://www.anthropic.com/research/building-effective-agents">Building Effective Agents</a></strong> — Anthropic’s guide on agent patterns. Covers tool use, ReAct loops, and when to use multi-agent vs. single-agent architectures.</p> </li> <li> <p><strong><a href="https://python.langchain.com/docs/concepts/tools/">LangChain Tools</a></strong> — If you want to integrate your MCP server with an existing agent framework, LangChain’s tool abstraction maps cleanly onto this pattern.</p> </li> </ul> <hr/> <p><em>The agent is the brain. Your MCP server is the hands. Getting the interface between them right — clean endpoints, honest tool descriptions, fast responses — is what separates a demo from a system.</em></p>]]></content><author><name></name></author><category term="ai-engineering"/><category term="MCP"/><category term="FastAPI"/><category term="Python"/><category term="AI Agents"/><category term="Tools"/><category term="LLMs"/><summary type="html"><![CDATA[Most tutorials show you how to call an LLM. This one shows you how to build the server on the other side — the tool layer that AI agents call into. MCP in 30 lines of Python.]]></summary></entry><entry><title type="html">TRIBE v2 and the Rise of In-Silico Neuroscience</title><link href="https://sridhar-3009.github.io/blog/2026/tribe-v2-neuroscience/" rel="alternate" type="text/html" title="TRIBE v2 and the Rise of In-Silico Neuroscience"/><published>2026-04-19T00:00:00+00:00</published><updated>2026-04-19T00:00:00+00:00</updated><id>https://sridhar-3009.github.io/blog/2026/tribe-v2-neuroscience</id><content type="html" xml:base="https://sridhar-3009.github.io/blog/2026/tribe-v2-neuroscience/"><![CDATA[<h1 id="tribe-v2-and-the-rise-of-in-silico-neuroscience">TRIBE v2 and the Rise of In-Silico Neuroscience</h1> <p>What if you could simulate a neuroscience experiment before running it on a single human subject?</p> <p>That’s the quiet but radical promise buried inside a recent paper: <strong>“A foundation model of vision, audition, and language for in-silico neuroscience.”</strong></p> <p>The model is called <strong>TRIBE v2</strong>. It takes video, audio, and text simultaneously, predicts whole-brain fMRI responses across 720 subjects and 1,000+ hours of data — and then, in its most striking result, <strong>reproduces classic neuroscience findings without running a single new human experiment.</strong></p> <p>That last part is what makes this paper worth a deep read.</p> <hr/> <h2 id="tldr">TL;DR</h2> <ul> <li>TRIBE v2 is a <strong>tri-modal transformer</strong> that jointly models vision, hearing, and language to predict brain activity</li> <li>Trained on <strong>1,000+ hours of fMRI</strong> across <strong>720 subjects</strong> — one of the largest aggregations in brain modeling</li> <li>Beats strong linear baselines by learning <strong>nonlinear cross-modal interactions</strong></li> <li>Generalizes <strong>zero-shot to unseen subjects</strong> — predicts group-level responses before any new data collection</li> <li>Reproduces <strong>FFA, PPA, EBA, VWFA</strong> and language network localizers <em>in silico</em></li> <li>Hints at a new research workflow: <strong>simulate → refine → then scan</strong></li> </ul> <hr/> <h2 id="the-problem-with-how-neuroscience-is-done-today">The Problem With How Neuroscience Is Done Today</h2> <p>Most brain models are narrow by design.</p> <p>One model handles visual cortex. Another handles auditory cortex. A third tries to align language models with text-driven activity. Each is tuned to a specific task, dataset, and usually a handful of subjects.</p> <p>That fragmentation has produced real science — but it also creates a fundamental mismatch. The brain doesn’t process the world in academic silos. It watches, listens, reads, and integrates — simultaneously, continuously, in real time.</p> <p>Meanwhile, every new experiment still requires:</p> <ul> <li>Recruiting subjects</li> <li>Hours in the MRI scanner (at ~$700–1000/hour)</li> <li>Weeks of preprocessing and analysis</li> <li>And a lot of hope that the stimulus design actually works</li> </ul> <p>The field moves slowly because iteration is expensive. TRIBE v2 asks: what if a strong enough computational model could compress that loop?</p> <hr/> <h2 id="what-tribe-v2-actually-is">What TRIBE v2 Actually Is</h2> <p>TRIBE v2 is built on a clean architectural principle: <strong>pretrained modality encoders already learned the world. Teach them to map into brain space.</strong></p> <p>Instead of handcrafting neuroscience-specific features, the authors take embeddings from state-of-the-art pretrained models for video, audio, and language. Those embeddings are time-synchronized, concatenated into a shared multimodal representation, and passed through a trainable transformer that predicts fMRI responses across the whole brain.</p> <p>The pipeline:</p> <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Stimulus (video + audio + text)
    ↓
Pretrained modality encoders (separate per stream)
    ↓
Time-aligned multimodal embeddings
    ↓
Transformer brain encoder (the trainable part)
    ↓
Predicted whole-brain fMRI activity
</code></pre></div></div> <p>This is a major shift from voxel-wise encoding pipelines — the standard approach — where researchers train separate linear models per subject, per task, and per brain region. TRIBE v2 learns one general model that handles all of them.</p> <hr/> <h2 id="the-scale-that-makes-it-work">The Scale That Makes It Work</h2> <p>Scale is not a footnote here. It’s the foundation.</p> <table> <thead> <tr> <th>Dataset type</th> <th>Details</th> </tr> </thead> <tbody> <tr> <td>Total fMRI data</td> <td>1,000+ hours</td> </tr> <tr> <td>Subjects</td> <td>720 individuals</td> </tr> <tr> <td>Dataset types</td> <td>Deep (many sessions/subject) + Wide (many subjects, fewer sessions)</td> </tr> <tr> <td>Stimulus types</td> <td>Movies, podcasts, multimodal narratives</td> </tr> </tbody> </table> <p>Aggregating this much fragmented neuroscience data into one modeling setup is itself a contribution. No individual lab has collected anything close to this in one place.</p> <hr/> <h2 id="what-it-actually-gets-right">What It Actually Gets Right</h2> <h3 id="modality-specific-structure-is-preserved">Modality-Specific Structure Is Preserved</h3> <p>When TRIBE v2 predicts brain responses to audio-heavy stimuli, activations peak in temporal auditory regions. For video-heavy stimuli, they peak in visual cortex. For multimodal inputs, predictions recruit broad cortical networks.</p> <p>That sounds like it should just work automatically — but it doesn’t in naive multimodal fusion. A poorly designed joint model blurs modality-specific responses into meaningless soup. The fact that TRIBE v2 preserves clean structure while still benefiting from cross-modal context is a meaningful result.</p> <h3 id="it-beats-linear-baselines--for-the-right-reason">It Beats Linear Baselines — For the Right Reason</h3> <p>The authors compare against <strong>Deep FIR</strong> — an optimized linear encoding pipeline using the <em>same pretrained embeddings</em>. This is the honest comparison. It isolates whether the transformer architecture is doing anything beyond better input features.</p> <p>It is. TRIBE v2 outperforms the linear baseline significantly across datasets, meaning the nonlinear cross-modal interactions are genuinely adding value — not just free-riding on stronger representations.</p> <h3 id="zero-shot-generalization-to-unseen-subjects">Zero-Shot Generalization to Unseen Subjects</h3> <p>This is the most practically significant result.</p> <p>TRIBE v2 can predict brain responses for <strong>participants it has never seen</strong>, in a zero-shot setting. For some datasets, its group-level predictions beat what you’d get from a typical individual subject measured against the cohort average.</p> <p>Think about what that enables:</p> <ul> <li>Pilot a stimulus design without any scanner time</li> <li>Identify whether a paradigm is likely to evoke the effect you’re looking for</li> <li>Pre-filter stimulus sets before committing to expensive data collection</li> </ul> <p>The model doesn’t replace human data. But it could front-load the iteration to before you collect any.</p> <h3 id="fine-tuning-closes-the-gap-fast">Fine-Tuning Closes the Gap Fast</h3> <p>With even a small amount of subject-specific data, fine-tuning improves performance substantially. The workflow mirrors what foundation models did for NLP and vision:</p> <ol> <li>Start with a broad pretrained brain model</li> <li>Collect a modest amount of new participant data</li> <li>Fine-tune lightly → get a personalized brain encoder</li> </ol> <p>No training from scratch. No waiting for a full dataset.</p> <hr/> <h2 id="the-most-important-result-simulated-neuroscience-experiments">The Most Important Result: Simulated Neuroscience Experiments</h2> <p>Here’s where the paper earns its headline claim.</p> <p>The authors test whether TRIBE v2 can <strong>reproduce findings from classic neuroscience experiments</strong> using only synthetic model-generated predictions — no new human subjects, no scanner.</p> <h3 id="visual-localizers">Visual Localizers</h3> <p>They simulate visual functional localizer experiments using controlled categories: faces, places, bodies, written characters.</p> <p>TRIBE v2 recovers the expected brain regions:</p> <table> <thead> <tr> <th>Category</th> <th>Brain region recovered</th> </tr> </thead> <tbody> <tr> <td>Faces</td> <td>FFA — Fusiform Face Area</td> </tr> <tr> <td>Places</td> <td>PPA — Parahippocampal Place Area</td> </tr> <tr> <td>Bodies</td> <td>EBA — Extrastriate Body Area</td> </tr> <tr> <td>Words</td> <td>VWFA — Visual Word Form Area</td> </tr> </tbody> </table> <p>These are landmarks of cognitive neuroscience, established over decades of carefully controlled human experiments. A model reproducing them from simulated predictions is not just passing a benchmark — it’s demonstrating structural alignment with the field’s validated knowledge.</p> <h3 id="language-localizers">Language Localizers</h3> <p>The same approach extends to language. The model is tested on conditions designed to isolate language processing, and the predicted contrast maps align with known language network responses — even though language unfolds over time, interacts with audio, and depends on hierarchical syntactic structure.</p> <h3 id="why-this-is-the-most-important-part">Why This Is the Most Important Part</h3> <p>If in-silico experiments become reliable enough, neuroscience research gets a new design loop:</p> <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Hypothesis
    ↓
Simulate expected brain response with a foundation model
    ↓
Refine stimulus or experimental protocol
    ↓
Run the expensive human experiment (now with higher confidence)
</code></pre></div></div> <p>Every other field has a version of this. Aerospace has wind tunnels. Drug discovery has molecular simulations. Physics has lattice QCD. Neuroscience has mostly had the scanner and hope.</p> <p>TRIBE v2 is a first serious hint that computational simulation could become a standard upstream step in brain research.</p> <hr/> <h2 id="what-the-learned-representations-look-like">What the Learned Representations Look Like</h2> <p>Using independent component analysis, the authors probe what the model’s latent space actually captures.</p> <p>The components align recognizably with:</p> <ul> <li>Auditory cortex</li> <li>Language network</li> <li>Motion-sensitive visual areas (MT+)</li> <li>Default mode network</li> </ul> <p>They also find something interesting: text-derived features dominate not just classical language areas but parts of <strong>prefrontal cortex</strong> too — suggesting that linguistic abstraction may provide a useful organizational scaffold for broader cognitive structure.</p> <p>That’s a scientific finding, not just a benchmark score.</p> <hr/> <h2 id="the-limitations-worth-taking-seriously">The Limitations Worth Taking Seriously</h2> <p>The authors don’t oversell this. A few things to keep in mind:</p> <p><strong>TRIBE v2 models the brain as a passive observer.</strong> It predicts responses to stimuli. It says nothing about active decision-making, motor planning, or internally generated thought.</p> <p><strong>fMRI trades temporal resolution for spatial coverage.</strong> The model is trained on a signal that averages over 1–2 seconds of neural activity. Real brain computation is happening at milliseconds. That’s a massive compression.</p> <p><strong>Strong prediction ≠ mechanism.</strong> TRIBE v2 may be a very powerful statistical emulator of brain responses without capturing the true computational principles underneath. The map is not the territory.</p> <p><strong>Data diversity is still a gap.</strong> 720 subjects is large for neuroscience — but the population is still almost certainly skewed toward Western, educated, young, healthy adults. A “foundation model of the brain” is only as universal as the brains it was trained on.</p> <hr/> <h2 id="why-the-architecture-choice-is-smart">Why the Architecture Choice Is Smart</h2> <p>TRIBE v2 does not try to reinvent the foundation-model stack for neuroscience. It borrows what already worked:</p> <ul> <li>Use large pretrained encoders for each modality</li> <li>Align them temporally</li> <li>Fuse with a trainable transformer</li> <li>Adapt final layers to the scientific target</li> </ul> <p>That’s pragmatic. The best audio, video, and language representations are already being learned at internet scale. Neuroscience doesn’t need to redo that. It just needs a bridge between those representations and brain space.</p> <p>From an engineering standpoint, this also explains why the model scales well. Once the modality encoders are strong, the remaining problem is multimodal integration, temporal aggregation, and subject-aware decoding — and transformers are already excellent at all three.</p> <hr/> <h2 id="the-shift-this-paper-represents">The Shift This Paper Represents</h2> <p>The strongest papers don’t just improve a metric. They change the default question.</p> <p>Before work like this, the standard neuroscience framing was:</p> <blockquote> <p>“Can we build a model for this specific experiment?”</p> </blockquote> <p>After TRIBE v2, the framing becomes:</p> <blockquote> <p>“Can we build a general computational instrument that makes experiments better before we run them?”</p> </blockquote> <p>That is a more ambitious goal, and a more interesting one.</p> <p>Language models became interfaces to knowledge. Diffusion models became interfaces to visual generation. Foundation models like TRIBE v2 may become interfaces to <strong>hypothesis generation in neuroscience</strong> — a tool you run before you commit to months of experimental design.</p> <hr/> <h2 id="final-thought">Final Thought</h2> <p>TRIBE v2 is not claiming to have solved the brain. It’s more honest than that.</p> <p>What it’s claiming — and largely demonstrating — is that a general-purpose model trained on enough multimodal brain data can do something genuinely new: simulate neuroscience before you run it.</p> <p>If that workflow matures, it could do for cognitive neuroscience what molecular simulation did for drug discovery and what CFD did for aerodynamics: compress the iteration cycle and expand the space of questions researchers can actually afford to ask.</p> <p>That’s worth paying attention to.</p> <hr/> <h2 id="further-reading">Further Reading</h2> <ul> <li><a href="https://arxiv.org/abs/2411.10062">Paper: A foundation model of vision, audition, and language for in-silico neuroscience</a></li> <li><a href="http://algonauts.csail.mit.edu/">Algonauts Project — brain encoding challenges</a></li> <li><a href="https://naturalscenesdataset.org/">Natural Scenes Dataset — large-scale fMRI benchmark</a></li> </ul>]]></content><author><name></name></author><category term="ai-research"/><category term="Neuroscience"/><category term="Foundation Models"/><category term="Multimodal"/><category term="fMRI"/><category term="Transformers"/><category term="TRIBE v2"/><summary type="html"><![CDATA[What if you could simulate a neuroscience experiment before running it on a single human subject? TRIBE v2 — a tri-modal foundation model trained on 1,000+ hours of fMRI data — is making that real.]]></summary></entry><entry><title type="html">Mixture of Experts: How a 141B Model Runs Like a 39B</title><link href="https://sridhar-3009.github.io/blog/2026/mixture-of-experts/" rel="alternate" type="text/html" title="Mixture of Experts: How a 141B Model Runs Like a 39B"/><published>2026-04-16T00:00:00+00:00</published><updated>2026-04-16T00:00:00+00:00</updated><id>https://sridhar-3009.github.io/blog/2026/mixture-of-experts</id><content type="html" xml:base="https://sridhar-3009.github.io/blog/2026/mixture-of-experts/"><![CDATA[<h1 id="mixture-of-experts-how-a-141b-model-runs-like-a-39b">Mixture of Experts: How a 141B Model Runs Like a 39B</h1> <p>When Mistral released Mixtral 8x7B in December 2023, people were confused. The model card said 46.7B total parameters but only 12.9B active per token. How does a 46B model run at the speed of a 13B one?</p> <p>When DeepSeek-V3 launched in late 2024 with 671B total parameters but only 37B active, the confusion deepened. Numbers that large shouldn’t fit on any reasonable hardware — but they did.</p> <p>The answer is <strong>Mixture of Experts</strong> (MoE): a decades-old idea from the early 1990s that became the dominant architecture for frontier LLMs once engineers figured out how to make it work at scale.</p> <p>The core insight is almost offensively simple: <strong>you don’t need to run every part of the network on every input.</strong> Different tokens need different computations. A model smart enough to route each token to the relevant specialists — and ignore the rest — gets the knowledge of a huge model at the cost of a small one.</p> <p>This post goes deep on how that works: the router, the expert layers, the training tricks, and the engineering tradeoffs that make production MoE systems possible.</p> <hr/> <h2 id="the-problem-with-dense-models">The Problem With Dense Models</h2> <p>In a standard transformer, every token passes through every parameter on every forward pass. A 70B dense model activates all 70B parameters for each token — a word in a poem, a number in an equation, a variable name in code. The model applies the full machinery regardless of whether it’s relevant.</p> <p>This is wasteful. A token in a Python function doesn’t need the same computations as a token in a French poem. The model’s knowledge of French grammar is entirely irrelevant when parsing <code class="language-plaintext highlighter-rouge">def forward(self, x):</code>.</p> <p>Dense scaling has another problem: <strong>compute scales with parameters</strong>. Double the parameters, roughly double the FLOPs per token. At some point, training cost becomes the bottleneck — not capability.</p> <p>MoE breaks this link between total parameters and per-token compute.</p> <hr/> <h2 id="the-architecture-experts-replace-the-ffn">The Architecture: Experts Replace the FFN</h2> <p>In a standard transformer block, the feed-forward network (FFN) is the largest sub-component — typically 4× the model dimension, applied identically to every token.</p> <p>MoE replaces this single FFN with <code class="language-plaintext highlighter-rouge">N</code> independent FFNs — the <strong>experts</strong> — plus a lightweight <strong>router</strong> that selects which experts handle each token:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Standard Transformer Block:
  x → Self-Attention → Add &amp; Norm → FFN → Add &amp; Norm → output

MoE Transformer Block:
  x → Self-Attention → Add &amp; Norm → Router → Top-K Experts → Add &amp; Norm → output
</code></pre></div></div> <p>The self-attention layers stay dense — every token attends to every other token. Only the FFN is replaced. This is the key design choice: attention handles context and relationships; experts handle token-level transformation.</p> <hr/> <h2 id="the-router-making-the-decision">The Router: Making the Decision</h2> <p>The router is a small linear layer that maps each token’s hidden state to a probability distribution over experts:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">torch</span>
<span class="kn">import</span> <span class="n">torch.nn</span> <span class="k">as</span> <span class="n">nn</span>
<span class="kn">import</span> <span class="n">torch.nn.functional</span> <span class="k">as</span> <span class="n">F</span>

<span class="k">class</span> <span class="nc">MoERouter</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">hidden_dim</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">num_experts</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">top_k</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">2</span><span class="p">):</span>
        <span class="nf">super</span><span class="p">().</span><span class="nf">__init__</span><span class="p">()</span>
        <span class="n">self</span><span class="p">.</span><span class="n">num_experts</span> <span class="o">=</span> <span class="n">num_experts</span>
        <span class="n">self</span><span class="p">.</span><span class="n">top_k</span> <span class="o">=</span> <span class="n">top_k</span>

        <span class="c1"># A single linear layer: hidden_dim → num_experts logits
</span>        <span class="n">self</span><span class="p">.</span><span class="n">gate</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="nc">Linear</span><span class="p">(</span><span class="n">hidden_dim</span><span class="p">,</span> <span class="n">num_experts</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">x</span><span class="p">):</span>
        <span class="sh">"""</span><span class="s">
        x: (batch_size * seq_len, hidden_dim)
        Returns:
            top_k_indices:  (batch_size * seq_len, top_k)  — which experts to use
            top_k_weights:  (batch_size * seq_len, top_k)  — how much to weight each
        </span><span class="sh">"""</span>
        <span class="c1"># Compute logits over all experts
</span>        <span class="n">logits</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">gate</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>                          <span class="c1"># (tokens, num_experts)
</span>
        <span class="c1"># Softmax to get routing probabilities
</span>        <span class="n">probs</span> <span class="o">=</span> <span class="n">F</span><span class="p">.</span><span class="nf">softmax</span><span class="p">(</span><span class="n">logits</span><span class="p">,</span> <span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">)</span>              <span class="c1"># (tokens, num_experts)
</span>
        <span class="c1"># Select top-K experts per token
</span>        <span class="n">top_k_weights</span><span class="p">,</span> <span class="n">top_k_indices</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">topk</span><span class="p">(</span><span class="n">probs</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">top_k</span><span class="p">,</span> <span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">)</span>

        <span class="c1"># Renormalize so selected weights sum to 1
</span>        <span class="n">top_k_weights</span> <span class="o">=</span> <span class="n">top_k_weights</span> <span class="o">/</span> <span class="n">top_k_weights</span><span class="p">.</span><span class="nf">sum</span><span class="p">(</span><span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">,</span> <span class="n">keepdim</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

        <span class="k">return</span> <span class="n">top_k_indices</span><span class="p">,</span> <span class="n">top_k_weights</span>
</code></pre></div></div> <p>For each token, the router outputs two things: <strong>which K experts to use</strong>, and <strong>how much weight to give each one</strong>. The final output is a weighted sum of those K experts’ outputs.</p> <p>Typical values: <code class="language-plaintext highlighter-rouge">num_experts = 8</code>, <code class="language-plaintext highlighter-rouge">top_k = 2</code>. So each token uses exactly 2 of 8 experts — activating 25% of the expert parameters.</p> <hr/> <h2 id="the-expert-just-an-ffn">The Expert: Just an FFN</h2> <p>Each expert is a standard feed-forward network — nothing exotic:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Expert</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">hidden_dim</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">ffn_dim</span><span class="p">:</span> <span class="nb">int</span><span class="p">):</span>
        <span class="nf">super</span><span class="p">().</span><span class="nf">__init__</span><span class="p">()</span>
        <span class="n">self</span><span class="p">.</span><span class="n">net</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="nc">Sequential</span><span class="p">(</span>
            <span class="n">nn</span><span class="p">.</span><span class="nc">Linear</span><span class="p">(</span><span class="n">hidden_dim</span><span class="p">,</span> <span class="n">ffn_dim</span><span class="p">),</span>
            <span class="n">nn</span><span class="p">.</span><span class="nc">SiLU</span><span class="p">(),</span>               <span class="c1"># SwiGLU variant used in Mixtral
</span>            <span class="n">nn</span><span class="p">.</span><span class="nc">Linear</span><span class="p">(</span><span class="n">ffn_dim</span><span class="p">,</span> <span class="n">hidden_dim</span><span class="p">),</span>
        <span class="p">)</span>

    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">x</span><span class="p">):</span>
        <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="nf">net</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
</code></pre></div></div> <p>The size of each expert’s FFN is typically the same as the FFN in a comparable dense model. So Mixtral 8x7B has 8 experts, each the size of a 7B model’s FFN — but only 2 are active per token. The “8x7B” name reflects this: 8 experts, each ~7B FFN scale.</p> <hr/> <h2 id="putting-it-together-the-moe-layer">Putting It Together: The MoE Layer</h2> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">MoELayer</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">hidden_dim</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">ffn_dim</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">num_experts</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">top_k</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">2</span><span class="p">):</span>
        <span class="nf">super</span><span class="p">().</span><span class="nf">__init__</span><span class="p">()</span>
        <span class="n">self</span><span class="p">.</span><span class="n">num_experts</span> <span class="o">=</span> <span class="n">num_experts</span>
        <span class="n">self</span><span class="p">.</span><span class="n">top_k</span> <span class="o">=</span> <span class="n">top_k</span>

        <span class="n">self</span><span class="p">.</span><span class="n">router</span> <span class="o">=</span> <span class="nc">MoERouter</span><span class="p">(</span><span class="n">hidden_dim</span><span class="p">,</span> <span class="n">num_experts</span><span class="p">,</span> <span class="n">top_k</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">experts</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="nc">ModuleList</span><span class="p">([</span>
            <span class="nc">Expert</span><span class="p">(</span><span class="n">hidden_dim</span><span class="p">,</span> <span class="n">ffn_dim</span><span class="p">)</span> <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">num_experts</span><span class="p">)</span>
        <span class="p">])</span>

    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">x</span><span class="p">):</span>
        <span class="sh">"""</span><span class="s">
        x: (batch_size, seq_len, hidden_dim)
        </span><span class="sh">"""</span>
        <span class="n">B</span><span class="p">,</span> <span class="n">S</span><span class="p">,</span> <span class="n">H</span> <span class="o">=</span> <span class="n">x</span><span class="p">.</span><span class="n">shape</span>

        <span class="c1"># Flatten to (batch*seq, hidden) — router operates per token
</span>        <span class="n">x_flat</span> <span class="o">=</span> <span class="n">x</span><span class="p">.</span><span class="nf">view</span><span class="p">(</span><span class="n">B</span> <span class="o">*</span> <span class="n">S</span><span class="p">,</span> <span class="n">H</span><span class="p">)</span>

        <span class="c1"># Get routing decisions
</span>        <span class="n">expert_indices</span><span class="p">,</span> <span class="n">expert_weights</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">router</span><span class="p">(</span><span class="n">x_flat</span><span class="p">)</span>
        <span class="c1"># expert_indices:  (B*S, top_k)
</span>        <span class="c1"># expert_weights:  (B*S, top_k)
</span>
        <span class="c1"># Collect expert outputs
</span>        <span class="n">output</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">zeros_like</span><span class="p">(</span><span class="n">x_flat</span><span class="p">)</span>

        <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">top_k</span><span class="p">):</span>
            <span class="c1"># Which tokens go to this slot's expert?
</span>            <span class="n">indices</span> <span class="o">=</span> <span class="n">expert_indices</span><span class="p">[:,</span> <span class="n">k</span><span class="p">]</span>      <span class="c1"># (B*S,) — expert index per token
</span>            <span class="n">weights</span> <span class="o">=</span> <span class="n">expert_weights</span><span class="p">[:,</span> <span class="n">k</span><span class="p">]</span>      <span class="c1"># (B*S,) — weight per token
</span>
            <span class="c1"># Process each expert's assigned tokens
</span>            <span class="k">for</span> <span class="n">expert_id</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">num_experts</span><span class="p">):</span>
                <span class="n">token_mask</span> <span class="o">=</span> <span class="p">(</span><span class="n">indices</span> <span class="o">==</span> <span class="n">expert_id</span><span class="p">)</span>
                <span class="k">if</span> <span class="ow">not</span> <span class="n">token_mask</span><span class="p">.</span><span class="nf">any</span><span class="p">():</span>
                    <span class="k">continue</span>

                <span class="n">expert_input</span> <span class="o">=</span> <span class="n">x_flat</span><span class="p">[</span><span class="n">token_mask</span><span class="p">]</span>
                <span class="n">expert_output</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">experts</span><span class="p">[</span><span class="n">expert_id</span><span class="p">](</span><span class="n">expert_input</span><span class="p">)</span>

                <span class="c1"># Weighted accumulation
</span>                <span class="n">output</span><span class="p">[</span><span class="n">token_mask</span><span class="p">]</span> <span class="o">+=</span> <span class="n">weights</span><span class="p">[</span><span class="n">token_mask</span><span class="p">].</span><span class="nf">unsqueeze</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">)</span> <span class="o">*</span> <span class="n">expert_output</span>

        <span class="k">return</span> <span class="n">output</span><span class="p">.</span><span class="nf">view</span><span class="p">(</span><span class="n">B</span><span class="p">,</span> <span class="n">S</span><span class="p">,</span> <span class="n">H</span><span class="p">)</span>
</code></pre></div></div> <p>This is the naive implementation — readable but slow. Production systems batch all tokens destined for the same expert together (a single large matmul instead of a loop), which is why MoE requires specialized CUDA kernels in practice.</p> <hr/> <h2 id="the-load-balancing-problem">The Load Balancing Problem</h2> <p>Here’s the failure mode that makes MoE training notoriously tricky: <strong>expert collapse</strong>.</p> <p>Without any intervention, the router quickly learns to always route tokens to the same 1–2 experts. Those experts see more gradients, get better faster, and attract even more tokens. The remaining experts starve — they’re never trained because tokens never reach them. You end up with a 141B model that effectively uses 10B parameters.</p> <p>The standard fix is an <strong>auxiliary load balancing loss</strong> that penalizes unequal expert utilization:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">load_balancing_loss</span><span class="p">(</span><span class="n">router_probs</span><span class="p">,</span> <span class="n">expert_indices</span><span class="p">,</span> <span class="n">num_experts</span><span class="p">):</span>
    <span class="sh">"""</span><span class="s">
    Encourages uniform distribution of tokens across experts.

    router_probs:   (num_tokens, num_experts) — softmax output of router
    expert_indices: (num_tokens, top_k)       — selected expert indices
    </span><span class="sh">"""</span>
    <span class="n">num_tokens</span> <span class="o">=</span> <span class="n">router_probs</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>

    <span class="c1"># Fraction of tokens routed to each expert
</span>    <span class="c1"># (how often each expert is selected)
</span>    <span class="n">one_hot</span> <span class="o">=</span> <span class="n">F</span><span class="p">.</span><span class="nf">one_hot</span><span class="p">(</span><span class="n">expert_indices</span><span class="p">,</span> <span class="n">num_classes</span><span class="o">=</span><span class="n">num_experts</span><span class="p">).</span><span class="nf">float</span><span class="p">()</span>
    <span class="n">tokens_per_expert</span> <span class="o">=</span> <span class="n">one_hot</span><span class="p">.</span><span class="nf">sum</span><span class="p">(</span><span class="n">dim</span><span class="o">=</span><span class="mi">1</span><span class="p">).</span><span class="nf">mean</span><span class="p">(</span><span class="n">dim</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>  <span class="c1"># (num_experts,)
</span>    <span class="n">f</span> <span class="o">=</span> <span class="n">tokens_per_expert</span> <span class="o">/</span> <span class="n">tokens_per_expert</span><span class="p">.</span><span class="nf">sum</span><span class="p">()</span>

    <span class="c1"># Average router probability assigned to each expert
</span>    <span class="c1"># (how confident the router is about each expert)
</span>    <span class="n">p</span> <span class="o">=</span> <span class="n">router_probs</span><span class="p">.</span><span class="nf">mean</span><span class="p">(</span><span class="n">dim</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>  <span class="c1"># (num_experts,)
</span>
    <span class="c1"># Loss = num_experts * sum(f_i * p_i)
</span>    <span class="c1"># Minimized when both f and p are uniform (1/num_experts each)
</span>    <span class="n">loss</span> <span class="o">=</span> <span class="n">num_experts</span> <span class="o">*</span> <span class="p">(</span><span class="n">f</span> <span class="o">*</span> <span class="n">p</span><span class="p">).</span><span class="nf">sum</span><span class="p">()</span>
    <span class="k">return</span> <span class="n">loss</span>
</code></pre></div></div> <p>This loss is added to the main language modeling loss with a small coefficient (typically <code class="language-plaintext highlighter-rouge">α = 0.01</code>). It gently pushes the router toward distributing tokens evenly without overwhelming the task objective.</p> <hr/> <h2 id="expert-capacity-the-traffic-problem">Expert Capacity: The Traffic Problem</h2> <p>Even with load balancing, real deployments face a harder constraint: <strong>expert capacity</strong>.</p> <p>In distributed training, each expert lives on a specific device. If 1000 tokens all want expert #3 and only 100 want expert #7, expert #3’s device is overwhelmed while #7’s sits idle. You can’t dynamically resize — the devices are fixed.</p> <p>The solution is a <strong>capacity factor</strong>: set a maximum number of tokens each expert can process per forward pass. Tokens that exceed an expert’s capacity are <strong>dropped</strong> — they don’t get processed by any expert.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">route_with_capacity</span><span class="p">(</span><span class="n">router_probs</span><span class="p">,</span> <span class="n">top_k</span><span class="p">,</span> <span class="n">capacity_factor</span><span class="o">=</span><span class="mf">1.25</span><span class="p">):</span>
    <span class="sh">"""</span><span class="s">
    Routes tokens to experts with a hard capacity ceiling.
    Excess tokens are dropped (their expert output is zeroed out).
    </span><span class="sh">"""</span>
    <span class="n">num_tokens</span><span class="p">,</span> <span class="n">num_experts</span> <span class="o">=</span> <span class="n">router_probs</span><span class="p">.</span><span class="n">shape</span>
    <span class="c1"># Max tokens any single expert will process
</span>    <span class="n">capacity</span> <span class="o">=</span> <span class="nf">int</span><span class="p">(</span><span class="n">capacity_factor</span> <span class="o">*</span> <span class="n">num_tokens</span> <span class="o">*</span> <span class="n">top_k</span> <span class="o">/</span> <span class="n">num_experts</span><span class="p">)</span>

    <span class="n">top_k_weights</span><span class="p">,</span> <span class="n">top_k_indices</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">topk</span><span class="p">(</span><span class="n">router_probs</span><span class="p">,</span> <span class="n">top_k</span><span class="p">,</span> <span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">)</span>
    <span class="n">top_k_weights</span> <span class="o">=</span> <span class="n">top_k_weights</span> <span class="o">/</span> <span class="n">top_k_weights</span><span class="p">.</span><span class="nf">sum</span><span class="p">(</span><span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">,</span> <span class="n">keepdim</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

    <span class="c1"># Track how many tokens each expert has accepted
</span>    <span class="n">expert_counts</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">zeros</span><span class="p">(</span><span class="n">num_experts</span><span class="p">,</span> <span class="n">dtype</span><span class="o">=</span><span class="n">torch</span><span class="p">.</span><span class="nb">long</span><span class="p">)</span>
    <span class="n">dispatch_mask</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">zeros</span><span class="p">(</span><span class="n">num_tokens</span><span class="p">,</span> <span class="n">top_k</span><span class="p">,</span> <span class="n">dtype</span><span class="o">=</span><span class="n">torch</span><span class="p">.</span><span class="nb">bool</span><span class="p">)</span>

    <span class="k">for</span> <span class="n">token_idx</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">num_tokens</span><span class="p">):</span>
        <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">top_k</span><span class="p">):</span>
            <span class="n">expert_id</span> <span class="o">=</span> <span class="n">top_k_indices</span><span class="p">[</span><span class="n">token_idx</span><span class="p">,</span> <span class="n">k</span><span class="p">].</span><span class="nf">item</span><span class="p">()</span>
            <span class="k">if</span> <span class="n">expert_counts</span><span class="p">[</span><span class="n">expert_id</span><span class="p">]</span> <span class="o">&lt;</span> <span class="n">capacity</span><span class="p">:</span>
                <span class="n">dispatch_mask</span><span class="p">[</span><span class="n">token_idx</span><span class="p">,</span> <span class="n">k</span><span class="p">]</span> <span class="o">=</span> <span class="bp">True</span>
                <span class="n">expert_counts</span><span class="p">[</span><span class="n">expert_id</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">1</span>
            <span class="c1"># else: token is dropped for this expert slot
</span>
    <span class="k">return</span> <span class="n">top_k_indices</span><span class="p">,</span> <span class="n">top_k_weights</span><span class="p">,</span> <span class="n">dispatch_mask</span>
</code></pre></div></div> <p>Token dropping introduces a tricky training dynamic: the model must learn to be robust to occasionally missing expert outputs. In practice, capacity factor ~1.25 means ~2–5% of tokens get dropped — small enough that the model learns to tolerate it.</p> <hr/> <h2 id="how-mixtral-does-it">How Mixtral Does It</h2> <p>Mixtral 8x7B uses 8 experts per MoE layer with top-2 routing. Every other transformer layer is an MoE layer (the rest are standard dense attention + FFN). The architecture:</p> <table> <thead> <tr> <th>Property</th> <th>Value</th> </tr> </thead> <tbody> <tr> <td>Total parameters</td> <td>46.7B</td> </tr> <tr> <td>Active parameters per token</td> <td>12.9B</td> </tr> <tr> <td>Experts per MoE layer</td> <td>8</td> </tr> <tr> <td>Active experts per token</td> <td>2</td> </tr> <tr> <td>Hidden dimension</td> <td>4096</td> </tr> <tr> <td>Expert FFN dimension</td> <td>14336</td> </tr> <tr> <td>Layers</td> <td>32 (alternating dense/MoE)</td> </tr> </tbody> </table> <p>The critical insight: <strong>attention layers are dense</strong> (all 12.9B active parameters), but the expert FFNs represent the bulk of “knowledge storage” — and only 2/8 = 25% of that storage is accessed per token.</p> <p>Mixtral’s routing is <strong>token-level, not sequence-level</strong>: the same word in different contexts can be routed to completely different experts. The router makes a fresh decision every token, every layer.</p> <hr/> <h2 id="how-deepseek-pushes-it-further">How DeepSeek Pushes It Further</h2> <p>DeepSeek-V3 (671B total / 37B active) introduces two innovations beyond standard MoE:</p> <p><strong>1. Fine-Grained Expert Segmentation</strong></p> <p>Instead of 8 large experts, DeepSeek uses 256 small experts per layer with top-8 routing. Each expert is ~1/32 the size of a Mixtral expert. This gives the router much finer-grained control — you can build any “combination” of specializations rather than picking from 8 coarse buckets.</p> <p><strong>2. Shared Experts</strong></p> <p>DeepSeek designates a small number of experts as <strong>always-active shared experts</strong>. Every token passes through these, regardless of routing. The shared experts handle common, general-purpose transformations; the routed experts handle token-specific specialization.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>DeepSeek MoE Layer:
  token → [Shared Expert 1] ──────────────────────────────┐
         → [Shared Expert 2] ──────────────────────────────┤
         → Router → Top-K from 256 routed experts ─────────┴→ sum → output
</code></pre></div></div> <p>This hybrid approach lets the model separate “what every token needs” from “what this specific token needs” — a cleaner decomposition than pure routing.</p> <hr/> <h2 id="why-gpt-4-is-probably-moe">Why GPT-4 Is (Probably) MoE</h2> <p>OpenAI has never confirmed GPT-4’s architecture. But the evidence is strong:</p> <ul> <li>Sam Altman confirmed GPT-4 cost ~$100M to train — consistent with MoE (you can train more total parameters for the same FLOP budget)</li> <li>Multiple credible leaks from 2023 describe GPT-4 as a ~1.8T parameter MoE with 16 experts, top-2 routing</li> <li>GPT-4’s inference latency and throughput numbers are inconsistent with a single dense model at that capability level</li> <li>OpenAI’s research trajectory — from GPT-3 (dense) through the sparse attention work — pointed toward MoE</li> </ul> <p>The economics make it inevitable: a 1.8T MoE with 2/16 active experts costs the same to run as a ~200B dense model, but has the knowledge capacity of a 1.8T one. At frontier scale, there’s no competitive reason to use dense.</p> <hr/> <h2 id="the-real-cost-communication-overhead">The Real Cost: Communication Overhead</h2> <p>MoE sounds like a free lunch. It isn’t. The hidden cost is <strong>all-to-all communication</strong>.</p> <p>In distributed training, model parameters are split across devices (tensor parallelism) or layers (pipeline parallelism). With MoE, you add a third dimension: <strong>expert parallelism</strong> — each expert lives on a different device.</p> <p>This creates a routing problem at the hardware level: after the router decides which expert each token goes to, you have to physically ship that token’s activations to the device that holds the relevant expert. After the expert processes it, the results ship back.</p> <p>This <strong>all-to-all collective</strong> — where every device sends data to every other device — is expensive on current hardware. The NVLink bandwidth between GPUs is fast, but not free. At the scale of 256 experts across 32 nodes, this communication can dominate training time.</p> <p>DeepSeek’s engineering contribution (described in their training paper) is a custom all-to-all kernel that overlaps communication with computation — hiding the latency by starting the next layer’s attention while the current layer’s expert results are still in transit. This is where the real engineering work happens in production MoE systems.</p> <hr/> <h2 id="dense-vs-sparse-when-to-use-which">Dense vs. Sparse: When to Use Which</h2> <table> <thead> <tr> <th> </th> <th>Dense</th> <th>MoE</th> </tr> </thead> <tbody> <tr> <td>Training FLOPs per token</td> <td>High</td> <td>Low</td> </tr> <tr> <td>Inference FLOPs per token</td> <td>High</td> <td>Low</td> </tr> <tr> <td>Memory (weights)</td> <td>Low</td> <td>High</td> </tr> <tr> <td>Training stability</td> <td>High</td> <td>Moderate (needs load balancing)</td> </tr> <tr> <td>Serving complexity</td> <td>Simple</td> <td>Complex (expert parallelism)</td> </tr> <tr> <td>Good for</td> <td>Smaller models, simple serving</td> <td>Frontier scale, research</td> </tr> <tr> <td>Bad for</td> <td>Frontier scale (too expensive)</td> <td>Edge/mobile deployment</td> </tr> </tbody> </table> <p>The crossover point is roughly 30–70B parameters: below that, dense is simpler and sufficient; above that, MoE becomes the only economically viable path to higher capability.</p> <hr/> <h2 id="key-takeaways">Key Takeaways</h2> <ol> <li> <p><strong>MoE breaks the compute-parameter link</strong> — you can have 10× more parameters for the same per-token FLOPs, because only a fraction of experts activate per token.</p> </li> <li> <p><strong>The router is a learned function</strong> — it figures out which tokens need which specializations entirely from gradient descent, with no human-defined expert roles.</p> </li> <li> <p><strong>Load balancing is non-negotiable</strong> — without the auxiliary loss, expert collapse happens within the first few thousand steps.</p> </li> <li> <p><strong>Expert capacity introduces token dropping</strong> — a necessary engineering compromise for distributed training that the model learns to tolerate.</p> </li> <li> <p><strong>DeepSeek’s fine-grained + shared expert design is the current state of the art</strong> — more routing flexibility, cleaner separation of general vs. specialized computation.</p> </li> <li> <p><strong>The real cost is communication, not compute</strong> — building production MoE systems is a distributed systems problem as much as an ML problem.</p> </li> </ol> <hr/> <h2 id="further-reading">Further Reading</h2> <h3 id="foundational-papers">Foundational Papers</h3> <ul> <li> <p><strong><a href="https://arxiv.org/abs/1701.06538">Outrageously Large Neural Networks: The Sparsely-Gated Mixture-of-Experts Layer</a></strong> — Shazeer et al., Google Brain (2017) The paper that revived MoE for deep learning. Introduced the top-K gating mechanism and the load balancing auxiliary loss. Everything since builds on this.</p> </li> <li> <p><strong><a href="https://arxiv.org/abs/2101.03961">Switch Transformers: Scaling to Trillion Parameter Models with Simple and Efficient Sparsity</a></strong> — Fedus et al., Google (2021) Simplified MoE to top-1 routing, showed you could scale to 1T+ parameters. First clear demonstration that MoE could match dense models at trillion-parameter scale.</p> </li> <li> <p><strong><a href="https://arxiv.org/abs/2112.06905">GLaM: Efficient Scaling of Language Models with Mixture-of-Experts</a></strong> — Du et al., Google (2021) Trained a 1.2T MoE model using 1/3 the energy of GPT-3 training. Landmark result on MoE efficiency vs. dense models.</p> </li> </ul> <h3 id="modern-moe-architectures">Modern MoE Architectures</h3> <ul> <li> <p><strong><a href="https://arxiv.org/abs/2401.04088">Mixtral of Experts</a></strong> — Jiang et al., Mistral AI (2024) The architecture paper behind Mixtral 8x7B and 8x22B. Clean, readable description of token-level routing with top-2 selection. The open-source MoE baseline.</p> </li> <li> <p><strong><a href="https://arxiv.org/abs/2412.19437">DeepSeek-V3 Technical Report</a></strong> — DeepSeek AI (2024) 671B parameters, 37B active, trained for ~$5.5M. Describes fine-grained expert segmentation, shared experts, and the custom all-to-all communication kernel. State of the art in MoE efficiency.</p> </li> <li> <p><strong><a href="https://arxiv.org/abs/2405.04434">DeepSeek-V2: A Strong, Economical, and Efficient Mixture-of-Experts Language Model</a></strong> — DeepSeek AI (2024) Predecessor to V3. Introduces the DeepSeekMoE architecture — the fine-grained + shared expert design — and validates it at scale.</p> </li> </ul> <h3 id="training--efficiency">Training &amp; Efficiency</h3> <ul> <li> <p><strong><a href="https://arxiv.org/abs/2202.08906">ST-MoE: Designing Stable and Transferable Sparse Expert Models</a></strong> — Zoph et al., Google (2022) A detailed investigation into what makes MoE training unstable and how to fix it. Router z-loss, capacity factor tuning, and expert dropout. Essential reading before training your own MoE.</p> </li> <li> <p><strong><a href="https://arxiv.org/abs/2208.02813">Towards Understanding Mixture of Experts in Deep Learning</a></strong> — Chen et al. (2022) Theoretical analysis of why MoE works: shows that sparse routing implicitly implements a form of conditional computation that dense models can’t replicate efficiently.</p> </li> <li> <p><strong><a href="https://arxiv.org/abs/2202.09368">Expert Choice Routing: Better MoE with Expert-Selected Tokens</a></strong> — Zhou et al., Google (2022) Inverts the routing: instead of tokens choosing experts, experts choose tokens. Eliminates dropped tokens, achieves perfect load balance. An elegant alternative to the standard approach.</p> </li> </ul> <h3 id="interpretability">Interpretability</h3> <ul> <li><strong><a href="https://arxiv.org/abs/2310.01786">Do Sparse Language Models Generalize Well?</a></strong> — Research into whether different experts actually specialize for different topics, syntactic structures, or languages. The empirical answer: yes, but it’s messier than you’d hope. Some experts specialize cleanly; others are general-purpose.</li> </ul> <hr/> <p><em>MoE is a bet on a simple idea: not every token needs the same computation. The remarkable thing is how much that bet pays off. A model that thinks selectively — routing each token to relevant specialists, ignoring the rest — turns out to be both more capable and cheaper to run than one that applies maximum force to everything. Intelligence, apparently, is less about brute force and more about knowing when not to think.</em></p>]]></content><author><name></name></author><category term="deep-learning"/><category term="Mixture of Experts"/><category term="MoE"/><category term="Mixtral"/><category term="DeepSeek"/><category term="LLMs"/><category term="Architecture"/><category term="PyTorch"/><summary type="html"><![CDATA[GPT-4, Mixtral, and DeepSeek aren't the dense models most people imagine. They're sparse — activating only a fraction of their parameters per token. Here's the architecture that makes that possible, and why it changes everything about how we scale AI.]]></summary></entry><entry><title type="html">RAG Beyond Naive Chunking: HyDE, Reranking, ColBERT, and Graph RAG</title><link href="https://sridhar-3009.github.io/blog/2026/rag-beyond-naive-chunking/" rel="alternate" type="text/html" title="RAG Beyond Naive Chunking: HyDE, Reranking, ColBERT, and Graph RAG"/><published>2026-04-16T00:00:00+00:00</published><updated>2026-04-16T00:00:00+00:00</updated><id>https://sridhar-3009.github.io/blog/2026/rag-beyond-naive-chunking</id><content type="html" xml:base="https://sridhar-3009.github.io/blog/2026/rag-beyond-naive-chunking/"><![CDATA[<h1 id="rag-beyond-naive-chunking-hyde-reranking-colbert-and-graph-rag">RAG Beyond Naive Chunking: HyDE, Reranking, ColBERT, and Graph RAG</h1> <p>Every company building on LLMs eventually hits the same wall. The model hallucinates facts. It doesn’t know about events after its training cutoff. You can’t fit your entire knowledge base into a context window. The answer everyone reaches for: <strong>Retrieval-Augmented Generation</strong> (RAG).</p> <p>The naive version is simple: chunk your documents into paragraphs, embed them with a sentence transformer, store in a vector database, and at query time retrieve the top-K most similar chunks. Stuff them into the prompt. Done.</p> <p>This works. Up to a point.</p> <p>Then you discover that your retrieval misses the most relevant documents 30% of the time. That questions about relationships between entities fail completely. That the 500-token chunks you split on periods don’t align with how your model reasons. That users asking “what did the CEO say about layoffs last quarter?” get back chunks about layoffs from three years ago because the embeddings didn’t capture <em>recency</em>.</p> <p>The gap between naive RAG and production RAG is wide. This post covers the techniques that close it: <strong>HyDE</strong>, <strong>reranking</strong>, <strong>ColBERT</strong>, and <strong>Graph RAG</strong> — with implementations you can actually use.</p> <hr/> <h2 id="the-failure-modes-of-naive-rag">The Failure Modes of Naive RAG</h2> <p>Before fixing things, name what’s broken:</p> <p><strong>1. Query-document mismatch</strong> Queries are short and abstract (“what causes transformer training instability?”). Documents are long and concrete (a paragraph describing gradient norms spiking due to attention softmax saturation). Their embeddings live in different regions of the vector space — not because the content is unrelated, but because the <em>linguistic form</em> is different.</p> <p><strong>2. Single-vector bottleneck</strong> A single embedding vector must compress an entire 512-token chunk. Complex chunks that discuss multiple concepts average their meaning into one point — and retrieval can fail on any of those concepts individually.</p> <p><strong>3. Semantic similarity ≠ relevance</strong> The most semantically similar chunk isn’t always the most useful one. A question about a drug’s side effects might return chunks that are topically related but don’t actually answer the question. Relevance requires understanding the <em>intent</em> of the query, not just its words.</p> <p><strong>4. Flat document structure</strong> Documents have structure — sections, subsections, tables, references between chapters. Chunking destroys this. A chunk about “revenue” in Q3 earnings doesn’t know it’s three paragraphs after the CFO’s statement that preceded it.</p> <p>Each of these has a targeted fix.</p> <hr/> <h2 id="fix-1-hyde--query-the-way-documents-answer">Fix 1: HyDE — Query the Way Documents Answer</h2> <p><strong>Hypothetical Document Embeddings</strong> (HyDE) attacks the query-document mismatch directly. Instead of embedding the raw query, you ask an LLM to <em>generate a hypothetical document that would answer the query</em> — then embed that hypothetical document and use it for retrieval.</p> <p>The intuition: a generated answer paragraph lives in the same linguistic register as real document chunks. Embedding it retrieves documents that look like answers, not documents that look like questions.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">openai</span> <span class="kn">import</span> <span class="n">OpenAI</span>
<span class="kn">import</span> <span class="n">numpy</span> <span class="k">as</span> <span class="n">np</span>

<span class="n">client</span> <span class="o">=</span> <span class="nc">OpenAI</span><span class="p">()</span>

<span class="k">def</span> <span class="nf">hyde_retrieve</span><span class="p">(</span><span class="n">query</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">vector_store</span><span class="p">,</span> <span class="n">top_k</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">5</span><span class="p">):</span>
    <span class="sh">"""</span><span class="s">
    HyDE: generate a hypothetical answer, embed it, retrieve with that embedding.
    </span><span class="sh">"""</span>
    <span class="c1"># Step 1: Generate a hypothetical document that answers the query
</span>    <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="n">chat</span><span class="p">.</span><span class="n">completions</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span>
        <span class="n">model</span><span class="o">=</span><span class="sh">"</span><span class="s">gpt-4o-mini</span><span class="sh">"</span><span class="p">,</span>
        <span class="n">messages</span><span class="o">=</span><span class="p">[</span>
            <span class="p">{</span>
                <span class="sh">"</span><span class="s">role</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">system</span><span class="sh">"</span><span class="p">,</span>
                <span class="sh">"</span><span class="s">content</span><span class="sh">"</span><span class="p">:</span> <span class="p">(</span>
                    <span class="sh">"</span><span class="s">Write a short, factual paragraph (3-5 sentences) that directly </span><span class="sh">"</span>
                    <span class="sh">"</span><span class="s">answers the following question. Write as if this were an excerpt </span><span class="sh">"</span>
                    <span class="sh">"</span><span class="s">from a technical document. Do not mention that you are generating </span><span class="sh">"</span>
                    <span class="sh">"</span><span class="s">a hypothetical answer.</span><span class="sh">"</span>
                <span class="p">),</span>
            <span class="p">},</span>
            <span class="p">{</span><span class="sh">"</span><span class="s">role</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">user</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">content</span><span class="sh">"</span><span class="p">:</span> <span class="n">query</span><span class="p">},</span>
        <span class="p">],</span>
        <span class="n">temperature</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
    <span class="p">)</span>
    <span class="n">hypothetical_doc</span> <span class="o">=</span> <span class="n">response</span><span class="p">.</span><span class="n">choices</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">message</span><span class="p">.</span><span class="n">content</span>

    <span class="c1"># Step 2: Embed the hypothetical document (not the raw query)
</span>    <span class="n">embedding_response</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="n">embeddings</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span>
        <span class="n">model</span><span class="o">=</span><span class="sh">"</span><span class="s">text-embedding-3-small</span><span class="sh">"</span><span class="p">,</span>
        <span class="nb">input</span><span class="o">=</span><span class="n">hypothetical_doc</span><span class="p">,</span>
    <span class="p">)</span>
    <span class="n">hyde_embedding</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">array</span><span class="p">(</span><span class="n">embedding_response</span><span class="p">.</span><span class="n">data</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">embedding</span><span class="p">)</span>

    <span class="c1"># Step 3: Retrieve using the hypothetical document's embedding
</span>    <span class="n">results</span> <span class="o">=</span> <span class="n">vector_store</span><span class="p">.</span><span class="nf">similarity_search_by_vector</span><span class="p">(</span><span class="n">hyde_embedding</span><span class="p">,</span> <span class="n">k</span><span class="o">=</span><span class="n">top_k</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">results</span><span class="p">,</span> <span class="n">hypothetical_doc</span>
</code></pre></div></div> <p>HyDE consistently improves retrieval recall on knowledge-intensive tasks, especially for technical or domain-specific queries where the vocabulary gap between question and answer is large. The cost is one extra LLM call per query — worth it for most production use cases.</p> <p><strong>When HyDE helps most:</strong> scientific literature search, legal document retrieval, technical documentation QA.</p> <p><strong>When it hurts:</strong> queries where the LLM generates a confidently wrong hypothetical. If the model hallucinates the hypothetical, you retrieve documents that support the hallucination. Always log both the query and the hypothetical for debugging.</p> <hr/> <h2 id="fix-2-reranking--a-second-opinion-on-retrieval">Fix 2: Reranking — A Second Opinion on Retrieval</h2> <p>Dense retrieval (embedding similarity) is fast but coarse. It finds roughly-relevant documents. A <strong>reranker</strong> is a cross-encoder that takes <code class="language-plaintext highlighter-rouge">(query, document)</code> as a pair and produces a precise relevance score — much slower than embedding similarity, but much more accurate.</p> <p>The two-stage pipeline:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Query → Dense Retrieval (fast, recall-focused) → Top 50 candidates
                                                        ↓
                                               Reranker (slow, precision-focused)
                                                        ↓
                                               Top 5 reranked results → LLM
</code></pre></div></div> <p>The reranker sees both query and document simultaneously — it can model their <em>interaction</em>, not just their individual semantics. This catches relevance signals that embedding similarity misses entirely.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">sentence_transformers</span> <span class="kn">import</span> <span class="n">CrossEncoder</span>
<span class="kn">from</span> <span class="n">typing</span> <span class="kn">import</span> <span class="n">List</span><span class="p">,</span> <span class="n">Tuple</span>

<span class="k">class</span> <span class="nc">TwoStageRetriever</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">vector_store</span><span class="p">,</span> <span class="n">reranker_model</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="sh">"</span><span class="s">cross-encoder/ms-marco-MiniLM-L-6-v2</span><span class="sh">"</span><span class="p">):</span>
        <span class="n">self</span><span class="p">.</span><span class="n">vector_store</span> <span class="o">=</span> <span class="n">vector_store</span>
        <span class="n">self</span><span class="p">.</span><span class="n">reranker</span> <span class="o">=</span> <span class="nc">CrossEncoder</span><span class="p">(</span><span class="n">reranker_model</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">retrieve</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">query</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">initial_k</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">50</span><span class="p">,</span> <span class="n">final_k</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">5</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">List</span><span class="p">[</span><span class="nb">dict</span><span class="p">]:</span>
        <span class="c1"># Stage 1: Dense retrieval — cast a wide net
</span>        <span class="n">candidates</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">vector_store</span><span class="p">.</span><span class="nf">similarity_search</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">k</span><span class="o">=</span><span class="n">initial_k</span><span class="p">)</span>

        <span class="c1"># Stage 2: Rerank with cross-encoder
</span>        <span class="n">pairs</span> <span class="o">=</span> <span class="p">[(</span><span class="n">query</span><span class="p">,</span> <span class="n">doc</span><span class="p">.</span><span class="n">page_content</span><span class="p">)</span> <span class="k">for</span> <span class="n">doc</span> <span class="ow">in</span> <span class="n">candidates</span><span class="p">]</span>
        <span class="n">scores</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">reranker</span><span class="p">.</span><span class="nf">predict</span><span class="p">(</span><span class="n">pairs</span><span class="p">)</span>

        <span class="c1"># Sort by reranker score, take top final_k
</span>        <span class="n">ranked</span> <span class="o">=</span> <span class="nf">sorted</span><span class="p">(</span>
            <span class="nf">zip</span><span class="p">(</span><span class="n">scores</span><span class="p">,</span> <span class="n">candidates</span><span class="p">),</span>
            <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="n">x</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span>
            <span class="n">reverse</span><span class="o">=</span><span class="bp">True</span>
        <span class="p">)</span>
        <span class="k">return</span> <span class="p">[{</span><span class="sh">"</span><span class="s">document</span><span class="sh">"</span><span class="p">:</span> <span class="n">doc</span><span class="p">,</span> <span class="sh">"</span><span class="s">score</span><span class="sh">"</span><span class="p">:</span> <span class="nf">float</span><span class="p">(</span><span class="n">score</span><span class="p">)}</span> <span class="k">for</span> <span class="n">score</span><span class="p">,</span> <span class="n">doc</span> <span class="ow">in</span> <span class="n">ranked</span><span class="p">[:</span><span class="n">final_k</span><span class="p">]]</span>
</code></pre></div></div> <p><strong>Reranker model options:</strong></p> <table> <thead> <tr> <th>Model</th> <th>Size</th> <th>Speed</th> <th>Quality</th> </tr> </thead> <tbody> <tr> <td><code class="language-plaintext highlighter-rouge">cross-encoder/ms-marco-MiniLM-L-6-v2</code></td> <td>22M</td> <td>Fast</td> <td>Good baseline</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">cross-encoder/ms-marco-MiniLM-L-12-v2</code></td> <td>33M</td> <td>Moderate</td> <td>Better</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">BAAI/bge-reranker-v2-m3</code></td> <td>568M</td> <td>Slow</td> <td>Excellent</td> </tr> <tr> <td>Cohere Rerank API</td> <td>—</td> <td>API call</td> <td>Production-grade</td> </tr> </tbody> </table> <p>The initial_k / final_k ratio matters: retrieve 50, rerank to 5. If you retrieve too few initially, the reranker can’t save you — the relevant document was never in the candidate set. If you rerank too many, latency explodes. 30–100 → 3–10 is the typical production range.</p> <hr/> <h2 id="fix-3-colbert--late-interaction-for-both-speed-and-precision">Fix 3: ColBERT — Late Interaction for Both Speed and Precision</h2> <p>HyDE and reranking are two ends of a spectrum: fast-and-coarse vs. slow-and-precise. <strong>ColBERT</strong> sits in the middle with a fundamentally different approach: <strong>late interaction</strong>.</p> <p>In standard dense retrieval, you embed a query into one vector and documents into one vector, then compute dot product. The entire semantic content of the query is compressed into a single point.</p> <p>ColBERT instead produces one embedding <strong>per token</strong> — both for the query and the document. Relevance is computed as the <strong>sum of maximum similarities</strong> between query tokens and document tokens:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Score(q, d) = Σ_{i ∈ query_tokens} max_{j ∈ doc_tokens} (q_i · d_j)
</code></pre></div></div> <p>Each query token independently finds its best matching document token. A query like “transformer training instability” has its “instability” token match against “gradient explosion” in the document, and its “transformer” token match against “attention layer” — neither match alone would be sufficient, but their sum is.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">torch</span>
<span class="kn">import</span> <span class="n">torch.nn.functional</span> <span class="k">as</span> <span class="n">F</span>
<span class="kn">from</span> <span class="n">transformers</span> <span class="kn">import</span> <span class="n">AutoTokenizer</span><span class="p">,</span> <span class="n">AutoModel</span>

<span class="k">class</span> <span class="nc">ColBERTRetriever</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">model_name</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="sh">"</span><span class="s">colbert-ir/colbertv2.0</span><span class="sh">"</span><span class="p">):</span>
        <span class="n">self</span><span class="p">.</span><span class="n">tokenizer</span> <span class="o">=</span> <span class="n">AutoTokenizer</span><span class="p">.</span><span class="nf">from_pretrained</span><span class="p">(</span><span class="n">model_name</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">model</span> <span class="o">=</span> <span class="n">AutoModel</span><span class="p">.</span><span class="nf">from_pretrained</span><span class="p">(</span><span class="n">model_name</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">model</span><span class="p">.</span><span class="nf">eval</span><span class="p">()</span>

    <span class="k">def</span> <span class="nf">encode</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">texts</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">],</span> <span class="n">is_query</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="bp">False</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">:</span>
        <span class="sh">"""</span><span class="s">
        Encode texts into per-token embeddings.
        Returns: (batch, seq_len, dim)
        </span><span class="sh">"""</span>
        <span class="n">prefix</span> <span class="o">=</span> <span class="sh">"</span><span class="s">[Q] </span><span class="sh">"</span> <span class="k">if</span> <span class="n">is_query</span> <span class="k">else</span> <span class="sh">"</span><span class="s">[D] </span><span class="sh">"</span>
        <span class="n">prefixed</span> <span class="o">=</span> <span class="p">[</span><span class="n">prefix</span> <span class="o">+</span> <span class="n">t</span> <span class="k">for</span> <span class="n">t</span> <span class="ow">in</span> <span class="n">texts</span><span class="p">]</span>

        <span class="n">tokens</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">tokenizer</span><span class="p">(</span>
            <span class="n">prefixed</span><span class="p">,</span>
            <span class="n">padding</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
            <span class="n">truncation</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
            <span class="n">max_length</span><span class="o">=</span><span class="mi">512</span><span class="p">,</span>
            <span class="n">return_tensors</span><span class="o">=</span><span class="sh">"</span><span class="s">pt</span><span class="sh">"</span>
        <span class="p">)</span>

        <span class="k">with</span> <span class="n">torch</span><span class="p">.</span><span class="nf">no_grad</span><span class="p">():</span>
            <span class="n">output</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">model</span><span class="p">(</span><span class="o">**</span><span class="n">tokens</span><span class="p">)</span>

        <span class="c1"># L2-normalize each token embedding
</span>        <span class="n">embeddings</span> <span class="o">=</span> <span class="n">F</span><span class="p">.</span><span class="nf">normalize</span><span class="p">(</span><span class="n">output</span><span class="p">.</span><span class="n">last_hidden_state</span><span class="p">,</span> <span class="n">p</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">embeddings</span>

    <span class="k">def</span> <span class="nf">score</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">query_embs</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">,</span> <span class="n">doc_embs</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">:</span>
        <span class="sh">"""</span><span class="s">
        Late interaction scoring: sum of max similarities.

        query_embs: (q_len, dim)
        doc_embs:   (d_len, dim)
        Returns: scalar relevance score
        </span><span class="sh">"""</span>
        <span class="c1"># Similarity matrix: (q_len, d_len)
</span>        <span class="n">sim_matrix</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">matmul</span><span class="p">(</span><span class="n">query_embs</span><span class="p">,</span> <span class="n">doc_embs</span><span class="p">.</span><span class="n">T</span><span class="p">)</span>

        <span class="c1"># Max over document tokens for each query token
</span>        <span class="n">max_sim</span> <span class="o">=</span> <span class="n">sim_matrix</span><span class="p">.</span><span class="nf">max</span><span class="p">(</span><span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">).</span><span class="n">values</span>  <span class="c1"># (q_len,)
</span>
        <span class="c1"># Sum across query tokens
</span>        <span class="k">return</span> <span class="n">max_sim</span><span class="p">.</span><span class="nf">sum</span><span class="p">()</span>

    <span class="k">def</span> <span class="nf">retrieve</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">query</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">doc_embeddings</span><span class="p">:</span> <span class="nb">list</span><span class="p">,</span> <span class="n">top_k</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">5</span><span class="p">):</span>
        <span class="n">query_embs</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">encode</span><span class="p">([</span><span class="n">query</span><span class="p">],</span> <span class="n">is_query</span><span class="o">=</span><span class="bp">True</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>  <span class="c1"># (q_len, dim)
</span>
        <span class="n">scores</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="k">for</span> <span class="n">doc_emb</span> <span class="ow">in</span> <span class="n">doc_embeddings</span><span class="p">:</span>
            <span class="n">score</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">score</span><span class="p">(</span><span class="n">query_embs</span><span class="p">,</span> <span class="n">doc_emb</span><span class="p">)</span>
            <span class="n">scores</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">score</span><span class="p">.</span><span class="nf">item</span><span class="p">())</span>

        <span class="n">top_indices</span> <span class="o">=</span> <span class="nf">sorted</span><span class="p">(</span><span class="nf">range</span><span class="p">(</span><span class="nf">len</span><span class="p">(</span><span class="n">scores</span><span class="p">)),</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">i</span><span class="p">:</span> <span class="n">scores</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">reverse</span><span class="o">=</span><span class="bp">True</span><span class="p">)[:</span><span class="n">top_k</span><span class="p">]</span>
        <span class="k">return</span> <span class="p">[(</span><span class="n">i</span><span class="p">,</span> <span class="n">scores</span><span class="p">[</span><span class="n">i</span><span class="p">])</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">top_indices</span><span class="p">]</span>
</code></pre></div></div> <p><strong>The scalability trick:</strong> document token embeddings are precomputed and stored (ColBERT indexes can be built with the <code class="language-plaintext highlighter-rouge">RAGatouille</code> library). At query time, you only compute query embeddings on the fly. The max-similarity operation over precomputed document embeddings is fast enough for millions of documents using PLAID (the ColBERT production indexing system).</p> <p><strong>When ColBERT shines:</strong> long documents where individual passages have multiple distinct topics, queries that require matching several specific concepts simultaneously, technical retrieval where exact terminology matters.</p> <hr/> <h2 id="fix-4-graph-rag--when-structure-is-the-answer">Fix 4: Graph RAG — When Structure Is the Answer</h2> <p>All three techniques above assume documents are independent, unrelated chunks. But real knowledge has structure: entities reference each other, concepts build on each other, facts have provenance. A knowledge base isn’t a bag of paragraphs — it’s a graph.</p> <p><strong>Graph RAG</strong> (popularized by Microsoft Research in 2024) builds an explicit knowledge graph from your documents and uses graph traversal alongside or instead of vector search.</p> <p>The pipeline has two phases:</p> <h3 id="phase-1-graph-construction">Phase 1: Graph Construction</h3> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">openai</span> <span class="kn">import</span> <span class="n">OpenAI</span>
<span class="kn">import</span> <span class="n">networkx</span> <span class="k">as</span> <span class="n">nx</span>
<span class="kn">import</span> <span class="n">json</span>

<span class="n">client</span> <span class="o">=</span> <span class="nc">OpenAI</span><span class="p">()</span>

<span class="k">def</span> <span class="nf">extract_entities_and_relations</span><span class="p">(</span><span class="n">text</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
    <span class="sh">"""</span><span class="s">Use LLM to extract a structured knowledge graph from a text chunk.</span><span class="sh">"""</span>
    <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="n">chat</span><span class="p">.</span><span class="n">completions</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span>
        <span class="n">model</span><span class="o">=</span><span class="sh">"</span><span class="s">gpt-4o</span><span class="sh">"</span><span class="p">,</span>
        <span class="n">messages</span><span class="o">=</span><span class="p">[</span>
            <span class="p">{</span>
                <span class="sh">"</span><span class="s">role</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">system</span><span class="sh">"</span><span class="p">,</span>
                <span class="sh">"</span><span class="s">content</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"""</span><span class="s">Extract entities and relationships from the text.
Return JSON with this structure:
{
  </span><span class="sh">"</span><span class="s">entities</span><span class="sh">"</span><span class="s">: [{</span><span class="sh">"</span><span class="s">name</span><span class="sh">"</span><span class="s">: str, </span><span class="sh">"</span><span class="s">type</span><span class="sh">"</span><span class="s">: str, </span><span class="sh">"</span><span class="s">description</span><span class="sh">"</span><span class="s">: str}],
  </span><span class="sh">"</span><span class="s">relations</span><span class="sh">"</span><span class="s">: [{</span><span class="sh">"</span><span class="s">source</span><span class="sh">"</span><span class="s">: str, </span><span class="sh">"</span><span class="s">relation</span><span class="sh">"</span><span class="s">: str, </span><span class="sh">"</span><span class="s">target</span><span class="sh">"</span><span class="s">: str}]
}
Only include clearly stated facts. Be conservative.</span><span class="sh">"""</span>
            <span class="p">},</span>
            <span class="p">{</span><span class="sh">"</span><span class="s">role</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">user</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">content</span><span class="sh">"</span><span class="p">:</span> <span class="n">text</span><span class="p">}</span>
        <span class="p">],</span>
        <span class="n">response_format</span><span class="o">=</span><span class="p">{</span><span class="sh">"</span><span class="s">type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">json_object</span><span class="sh">"</span><span class="p">},</span>
        <span class="n">temperature</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
    <span class="p">)</span>
    <span class="k">return</span> <span class="n">json</span><span class="p">.</span><span class="nf">loads</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">choices</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">message</span><span class="p">.</span><span class="n">content</span><span class="p">)</span>


<span class="k">def</span> <span class="nf">build_knowledge_graph</span><span class="p">(</span><span class="n">chunks</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="n">nx</span><span class="p">.</span><span class="n">DiGraph</span><span class="p">:</span>
    <span class="n">G</span> <span class="o">=</span> <span class="n">nx</span><span class="p">.</span><span class="nc">DiGraph</span><span class="p">()</span>

    <span class="k">for</span> <span class="n">chunk</span> <span class="ow">in</span> <span class="n">chunks</span><span class="p">:</span>
        <span class="n">extracted</span> <span class="o">=</span> <span class="nf">extract_entities_and_relations</span><span class="p">(</span><span class="n">chunk</span><span class="p">)</span>

        <span class="k">for</span> <span class="n">entity</span> <span class="ow">in</span> <span class="n">extracted</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">entities</span><span class="sh">"</span><span class="p">,</span> <span class="p">[]):</span>
            <span class="n">G</span><span class="p">.</span><span class="nf">add_node</span><span class="p">(</span>
                <span class="n">entity</span><span class="p">[</span><span class="sh">"</span><span class="s">name</span><span class="sh">"</span><span class="p">],</span>
                <span class="nb">type</span><span class="o">=</span><span class="n">entity</span><span class="p">[</span><span class="sh">"</span><span class="s">type</span><span class="sh">"</span><span class="p">],</span>
                <span class="n">description</span><span class="o">=</span><span class="n">entity</span><span class="p">[</span><span class="sh">"</span><span class="s">description</span><span class="sh">"</span><span class="p">]</span>
            <span class="p">)</span>

        <span class="k">for</span> <span class="n">rel</span> <span class="ow">in</span> <span class="n">extracted</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">relations</span><span class="sh">"</span><span class="p">,</span> <span class="p">[]):</span>
            <span class="n">G</span><span class="p">.</span><span class="nf">add_edge</span><span class="p">(</span>
                <span class="n">rel</span><span class="p">[</span><span class="sh">"</span><span class="s">source</span><span class="sh">"</span><span class="p">],</span>
                <span class="n">rel</span><span class="p">[</span><span class="sh">"</span><span class="s">target</span><span class="sh">"</span><span class="p">],</span>
                <span class="n">relation</span><span class="o">=</span><span class="n">rel</span><span class="p">[</span><span class="sh">"</span><span class="s">relation</span><span class="sh">"</span><span class="p">]</span>
            <span class="p">)</span>

    <span class="k">return</span> <span class="n">G</span>
</code></pre></div></div> <h3 id="phase-2-graph-augmented-retrieval">Phase 2: Graph-Augmented Retrieval</h3> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">graph_rag_retrieve</span><span class="p">(</span><span class="n">query</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">graph</span><span class="p">:</span> <span class="n">nx</span><span class="p">.</span><span class="n">DiGraph</span><span class="p">,</span> <span class="n">vector_store</span><span class="p">,</span> <span class="n">top_k</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">5</span><span class="p">):</span>
    <span class="sh">"""</span><span class="s">
    Two-path retrieval:
    1. Vector search for relevant chunks
    2. Entity extraction from query → graph traversal for related context
    </span><span class="sh">"""</span>
    <span class="c1"># Path 1: Standard vector retrieval
</span>    <span class="n">vector_results</span> <span class="o">=</span> <span class="n">vector_store</span><span class="p">.</span><span class="nf">similarity_search</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">k</span><span class="o">=</span><span class="n">top_k</span><span class="p">)</span>

    <span class="c1"># Path 2: Extract entities from query, traverse the graph
</span>    <span class="n">entity_response</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="n">chat</span><span class="p">.</span><span class="n">completions</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span>
        <span class="n">model</span><span class="o">=</span><span class="sh">"</span><span class="s">gpt-4o-mini</span><span class="sh">"</span><span class="p">,</span>
        <span class="n">messages</span><span class="o">=</span><span class="p">[</span>
            <span class="p">{</span>
                <span class="sh">"</span><span class="s">role</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">system</span><span class="sh">"</span><span class="p">,</span>
                <span class="sh">"</span><span class="s">content</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">Extract entity names from this query. Return a JSON array of strings.</span><span class="sh">"</span>
            <span class="p">},</span>
            <span class="p">{</span><span class="sh">"</span><span class="s">role</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">user</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">content</span><span class="sh">"</span><span class="p">:</span> <span class="n">query</span><span class="p">}</span>
        <span class="p">],</span>
        <span class="n">response_format</span><span class="o">=</span><span class="p">{</span><span class="sh">"</span><span class="s">type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">json_object</span><span class="sh">"</span><span class="p">},</span>
        <span class="n">temperature</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
    <span class="p">)</span>
    <span class="n">query_entities</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="nf">loads</span><span class="p">(</span><span class="n">entity_response</span><span class="p">.</span><span class="n">choices</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">message</span><span class="p">.</span><span class="n">content</span><span class="p">).</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">entities</span><span class="sh">"</span><span class="p">,</span> <span class="p">[])</span>

    <span class="c1"># Gather neighborhood context for each query entity
</span>    <span class="n">graph_context</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">entity</span> <span class="ow">in</span> <span class="n">query_entities</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">entity</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">graph</span><span class="p">:</span>
            <span class="k">continue</span>

        <span class="c1"># Direct neighbors (1-hop)
</span>        <span class="n">neighbors</span> <span class="o">=</span> <span class="nf">list</span><span class="p">(</span><span class="n">graph</span><span class="p">.</span><span class="nf">neighbors</span><span class="p">(</span><span class="n">entity</span><span class="p">))</span> <span class="o">+</span> <span class="nf">list</span><span class="p">(</span><span class="n">graph</span><span class="p">.</span><span class="nf">predecessors</span><span class="p">(</span><span class="n">entity</span><span class="p">))</span>

        <span class="k">for</span> <span class="n">neighbor</span> <span class="ow">in</span> <span class="n">neighbors</span><span class="p">[:</span><span class="mi">10</span><span class="p">]:</span>  <span class="c1"># cap at 10 neighbors
</span>            <span class="n">edge_data</span> <span class="o">=</span> <span class="n">graph</span><span class="p">.</span><span class="nf">get_edge_data</span><span class="p">(</span><span class="n">entity</span><span class="p">,</span> <span class="n">neighbor</span><span class="p">)</span> <span class="ow">or</span> <span class="n">graph</span><span class="p">.</span><span class="nf">get_edge_data</span><span class="p">(</span><span class="n">neighbor</span><span class="p">,</span> <span class="n">entity</span><span class="p">)</span>
            <span class="n">relation</span> <span class="o">=</span> <span class="n">edge_data</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">relation</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">related to</span><span class="sh">"</span><span class="p">)</span> <span class="k">if</span> <span class="n">edge_data</span> <span class="k">else</span> <span class="sh">"</span><span class="s">related to</span><span class="sh">"</span>
            <span class="n">graph_context</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">entity</span><span class="si">}</span><span class="s"> </span><span class="si">{</span><span class="n">relation</span><span class="si">}</span><span class="s"> </span><span class="si">{</span><span class="n">neighbor</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

        <span class="c1"># Include entity description
</span>        <span class="n">node_data</span> <span class="o">=</span> <span class="n">graph</span><span class="p">.</span><span class="n">nodes</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="n">entity</span><span class="p">,</span> <span class="p">{})</span>
        <span class="k">if</span> <span class="n">node_data</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">description</span><span class="sh">"</span><span class="p">):</span>
            <span class="n">graph_context</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">entity</span><span class="si">}</span><span class="s">: </span><span class="si">{</span><span class="n">node_data</span><span class="p">[</span><span class="sh">'</span><span class="s">description</span><span class="sh">'</span><span class="p">]</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

    <span class="k">return</span> <span class="p">{</span>
        <span class="sh">"</span><span class="s">vector_results</span><span class="sh">"</span><span class="p">:</span> <span class="n">vector_results</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">graph_context</span><span class="sh">"</span><span class="p">:</span> <span class="n">graph_context</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">query_entities</span><span class="sh">"</span><span class="p">:</span> <span class="n">query_entities</span><span class="p">,</span>
    <span class="p">}</span>
</code></pre></div></div> <h3 id="why-graph-rag-wins-on-relationship-queries">Why Graph RAG Wins on Relationship Queries</h3> <p>Consider a corpus of earnings call transcripts. The question: <em>“How has the relationship between Amazon’s AWS margins and its advertising revenue changed over the past three years?”</em></p> <p>Vector retrieval returns chunks that mention AWS margins and chunks that mention advertising revenue — independently. It has no way to represent the <em>relationship between these two things over time</em> without explicit graph structure connecting them.</p> <p>Graph RAG extracts <code class="language-plaintext highlighter-rouge">AWS_Margins --[affects]--&gt; Advertising_Investment</code>, <code class="language-plaintext highlighter-rouge">Advertising_Revenue --[grew_by]--&gt; 27%_in_Q3_2024</code>, and so on. Traversing from the query entities gives back the entire chain of relationships — exactly what the question is asking for.</p> <p>Microsoft’s GraphRAG paper showed 3–4× improvement in “global question” tasks (questions requiring synthesis across multiple documents) compared to naive RAG. Local question performance was roughly equivalent. Graph construction is expensive (many LLM calls), but for static corpora, you pay it once.</p> <hr/> <h2 id="putting-it-all-together-the-production-pipeline">Putting It All Together: The Production Pipeline</h2> <p>These techniques compose. A production RAG system typically layers several:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ProductionRAG</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">vector_store</span><span class="p">,</span> <span class="n">graph</span><span class="p">,</span> <span class="n">reranker_model</span><span class="p">):</span>
        <span class="n">self</span><span class="p">.</span><span class="n">vector_store</span> <span class="o">=</span> <span class="n">vector_store</span>
        <span class="n">self</span><span class="p">.</span><span class="n">graph</span> <span class="o">=</span> <span class="n">graph</span>
        <span class="n">self</span><span class="p">.</span><span class="n">reranker</span> <span class="o">=</span> <span class="nc">CrossEncoder</span><span class="p">(</span><span class="n">reranker_model</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">llm</span> <span class="o">=</span> <span class="nc">OpenAI</span><span class="p">()</span>

    <span class="k">def</span> <span class="nf">retrieve</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">query</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
        <span class="c1"># 1. HyDE: generate hypothetical answer for better query embedding
</span>        <span class="n">hypothetical</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">_generate_hypothetical</span><span class="p">(</span><span class="n">query</span><span class="p">)</span>

        <span class="c1"># 2. Dual retrieval: embed both raw query and hypothetical, merge results
</span>        <span class="n">raw_results</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">vector_store</span><span class="p">.</span><span class="nf">similarity_search</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">k</span><span class="o">=</span><span class="mi">30</span><span class="p">)</span>
        <span class="n">hyde_results</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">vector_store</span><span class="p">.</span><span class="nf">similarity_search</span><span class="p">(</span><span class="n">hypothetical</span><span class="p">,</span> <span class="n">k</span><span class="o">=</span><span class="mi">30</span><span class="p">)</span>

        <span class="c1"># Deduplicate by document ID
</span>        <span class="n">seen</span> <span class="o">=</span> <span class="nf">set</span><span class="p">()</span>
        <span class="n">candidates</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="k">for</span> <span class="n">doc</span> <span class="ow">in</span> <span class="n">raw_results</span> <span class="o">+</span> <span class="n">hyde_results</span><span class="p">:</span>
            <span class="k">if</span> <span class="n">doc</span><span class="p">.</span><span class="n">metadata</span><span class="p">[</span><span class="sh">"</span><span class="s">id</span><span class="sh">"</span><span class="p">]</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">seen</span><span class="p">:</span>
                <span class="n">seen</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="n">doc</span><span class="p">.</span><span class="n">metadata</span><span class="p">[</span><span class="sh">"</span><span class="s">id</span><span class="sh">"</span><span class="p">])</span>
                <span class="n">candidates</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">doc</span><span class="p">)</span>

        <span class="c1"># 3. Graph augmentation: pull in relationship context
</span>        <span class="n">graph_context</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">_graph_retrieve</span><span class="p">(</span><span class="n">query</span><span class="p">)</span>

        <span class="c1"># 4. Rerank the merged candidate set
</span>        <span class="n">pairs</span> <span class="o">=</span> <span class="p">[(</span><span class="n">query</span><span class="p">,</span> <span class="n">doc</span><span class="p">.</span><span class="n">page_content</span><span class="p">)</span> <span class="k">for</span> <span class="n">doc</span> <span class="ow">in</span> <span class="n">candidates</span><span class="p">]</span>
        <span class="n">scores</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">reranker</span><span class="p">.</span><span class="nf">predict</span><span class="p">(</span><span class="n">pairs</span><span class="p">)</span>
        <span class="n">ranked</span> <span class="o">=</span> <span class="nf">sorted</span><span class="p">(</span><span class="nf">zip</span><span class="p">(</span><span class="n">scores</span><span class="p">,</span> <span class="n">candidates</span><span class="p">),</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="n">x</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">reverse</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
        <span class="n">top_docs</span> <span class="o">=</span> <span class="p">[</span><span class="n">doc</span> <span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">doc</span> <span class="ow">in</span> <span class="n">ranked</span><span class="p">[:</span><span class="mi">5</span><span class="p">]]</span>

        <span class="k">return</span> <span class="p">{</span>
            <span class="sh">"</span><span class="s">retrieved_docs</span><span class="sh">"</span><span class="p">:</span> <span class="n">top_docs</span><span class="p">,</span>
            <span class="sh">"</span><span class="s">graph_context</span><span class="sh">"</span><span class="p">:</span> <span class="n">graph_context</span><span class="p">,</span>
            <span class="sh">"</span><span class="s">hypothetical</span><span class="sh">"</span><span class="p">:</span> <span class="n">hypothetical</span><span class="p">,</span>
        <span class="p">}</span>

    <span class="k">def</span> <span class="nf">answer</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">query</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="n">context</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">retrieve</span><span class="p">(</span><span class="n">query</span><span class="p">)</span>

        <span class="n">doc_text</span> <span class="o">=</span> <span class="sh">"</span><span class="se">\n\n</span><span class="s">---</span><span class="se">\n\n</span><span class="sh">"</span><span class="p">.</span><span class="nf">join</span><span class="p">([</span><span class="n">d</span><span class="p">.</span><span class="n">page_content</span> <span class="k">for</span> <span class="n">d</span> <span class="ow">in</span> <span class="n">context</span><span class="p">[</span><span class="sh">"</span><span class="s">retrieved_docs</span><span class="sh">"</span><span class="p">]])</span>
        <span class="n">graph_text</span> <span class="o">=</span> <span class="sh">"</span><span class="se">\n</span><span class="sh">"</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">context</span><span class="p">[</span><span class="sh">"</span><span class="s">graph_context</span><span class="sh">"</span><span class="p">])</span>

        <span class="n">response</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">llm</span><span class="p">.</span><span class="n">chat</span><span class="p">.</span><span class="n">completions</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span>
            <span class="n">model</span><span class="o">=</span><span class="sh">"</span><span class="s">gpt-4o</span><span class="sh">"</span><span class="p">,</span>
            <span class="n">messages</span><span class="o">=</span><span class="p">[</span>
                <span class="p">{</span>
                    <span class="sh">"</span><span class="s">role</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">system</span><span class="sh">"</span><span class="p">,</span>
                    <span class="sh">"</span><span class="s">content</span><span class="sh">"</span><span class="p">:</span> <span class="p">(</span>
                        <span class="sh">"</span><span class="s">Answer the user</span><span class="sh">'</span><span class="s">s question using only the provided context. </span><span class="sh">"</span>
                        <span class="sh">"</span><span class="s">If the context doesn</span><span class="sh">'</span><span class="s">t contain the answer, say so explicitly. </span><span class="sh">"</span>
                        <span class="sh">"</span><span class="s">Cite sources by referencing document metadata where available.</span><span class="sh">"</span>
                    <span class="p">),</span>
                <span class="p">},</span>
                <span class="p">{</span>
                    <span class="sh">"</span><span class="s">role</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">user</span><span class="sh">"</span><span class="p">,</span>
                    <span class="sh">"</span><span class="s">content</span><span class="sh">"</span><span class="p">:</span> <span class="p">(</span>
                        <span class="sa">f</span><span class="sh">"</span><span class="s">Question: </span><span class="si">{</span><span class="n">query</span><span class="si">}</span><span class="se">\n\n</span><span class="sh">"</span>
                        <span class="sa">f</span><span class="sh">"</span><span class="s">Document Context:</span><span class="se">\n</span><span class="si">{</span><span class="n">doc_text</span><span class="si">}</span><span class="se">\n\n</span><span class="sh">"</span>
                        <span class="sa">f</span><span class="sh">"</span><span class="s">Relationship Context:</span><span class="se">\n</span><span class="si">{</span><span class="n">graph_text</span><span class="si">}</span><span class="sh">"</span>
                    <span class="p">),</span>
                <span class="p">},</span>
            <span class="p">],</span>
        <span class="p">)</span>
        <span class="k">return</span> <span class="n">response</span><span class="p">.</span><span class="n">choices</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">message</span><span class="p">.</span><span class="n">content</span>

    <span class="k">def</span> <span class="nf">_generate_hypothetical</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">query</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="n">response</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">llm</span><span class="p">.</span><span class="n">chat</span><span class="p">.</span><span class="n">completions</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span>
            <span class="n">model</span><span class="o">=</span><span class="sh">"</span><span class="s">gpt-4o-mini</span><span class="sh">"</span><span class="p">,</span>
            <span class="n">messages</span><span class="o">=</span><span class="p">[</span>
                <span class="p">{</span><span class="sh">"</span><span class="s">role</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">system</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">content</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">Write a 3-sentence factual answer to this question.</span><span class="sh">"</span><span class="p">},</span>
                <span class="p">{</span><span class="sh">"</span><span class="s">role</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">user</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">content</span><span class="sh">"</span><span class="p">:</span> <span class="n">query</span><span class="p">},</span>
            <span class="p">],</span>
            <span class="n">temperature</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
        <span class="p">)</span>
        <span class="k">return</span> <span class="n">response</span><span class="p">.</span><span class="n">choices</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">message</span><span class="p">.</span><span class="n">content</span>

    <span class="k">def</span> <span class="nf">_graph_retrieve</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">query</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">]:</span>
        <span class="c1"># Entity extraction + graph traversal (as shown above)
</span>        <span class="bp">...</span>
</code></pre></div></div> <hr/> <h2 id="chunking-strategy-the-foundation-still-matters">Chunking Strategy: The Foundation Still Matters</h2> <p>Advanced retrieval can’t compensate for bad chunks. A few patterns worth knowing:</p> <p><strong>Semantic chunking</strong> — instead of fixed-size splits, chunk at semantic boundaries detected by embedding distance between consecutive sentences. When the embedding distance spikes, you’ve crossed a topic boundary.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">sentence_transformers</span> <span class="kn">import</span> <span class="n">SentenceTransformer</span>
<span class="kn">import</span> <span class="n">numpy</span> <span class="k">as</span> <span class="n">np</span>

<span class="k">def</span> <span class="nf">semantic_chunk</span><span class="p">(</span><span class="n">text</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">threshold</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">0.3</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">]:</span>
    <span class="n">model</span> <span class="o">=</span> <span class="nc">SentenceTransformer</span><span class="p">(</span><span class="sh">"</span><span class="s">all-MiniLM-L6-v2</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">sentences</span> <span class="o">=</span> <span class="n">text</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="sh">"</span><span class="s">. </span><span class="sh">"</span><span class="p">)</span>
    <span class="n">embeddings</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">sentences</span><span class="p">)</span>

    <span class="c1"># Cosine distance between consecutive sentences
</span>    <span class="n">chunks</span><span class="p">,</span> <span class="n">current_chunk</span> <span class="o">=</span> <span class="p">[],</span> <span class="p">[</span><span class="n">sentences</span><span class="p">[</span><span class="mi">0</span><span class="p">]]</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="nf">len</span><span class="p">(</span><span class="n">sentences</span><span class="p">)):</span>
        <span class="n">cos_sim</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">dot</span><span class="p">(</span><span class="n">embeddings</span><span class="p">[</span><span class="n">i</span><span class="o">-</span><span class="mi">1</span><span class="p">],</span> <span class="n">embeddings</span><span class="p">[</span><span class="n">i</span><span class="p">])</span> <span class="o">/</span> <span class="p">(</span>
            <span class="n">np</span><span class="p">.</span><span class="n">linalg</span><span class="p">.</span><span class="nf">norm</span><span class="p">(</span><span class="n">embeddings</span><span class="p">[</span><span class="n">i</span><span class="o">-</span><span class="mi">1</span><span class="p">])</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">linalg</span><span class="p">.</span><span class="nf">norm</span><span class="p">(</span><span class="n">embeddings</span><span class="p">[</span><span class="n">i</span><span class="p">])</span>
        <span class="p">)</span>
        <span class="k">if</span> <span class="mi">1</span> <span class="o">-</span> <span class="n">cos_sim</span> <span class="o">&gt;</span> <span class="n">threshold</span><span class="p">:</span>  <span class="c1"># topic boundary detected
</span>            <span class="n">chunks</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="sh">"</span><span class="s">. </span><span class="sh">"</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">current_chunk</span><span class="p">))</span>
            <span class="n">current_chunk</span> <span class="o">=</span> <span class="p">[</span><span class="n">sentences</span><span class="p">[</span><span class="n">i</span><span class="p">]]</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="n">current_chunk</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">sentences</span><span class="p">[</span><span class="n">i</span><span class="p">])</span>

    <span class="n">chunks</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="sh">"</span><span class="s">. </span><span class="sh">"</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">current_chunk</span><span class="p">))</span>
    <span class="k">return</span> <span class="n">chunks</span>
</code></pre></div></div> <p><strong>Parent-child chunking</strong> — index small child chunks (128 tokens) for retrieval precision, but return the larger parent chunk (512 tokens) to the LLM for context. You get the best of both: precise matching, rich context.</p> <p><strong>Proposition indexing</strong> — extract atomic factual statements from each chunk, embed and index those instead of the raw text. Each proposition is a single retrievable claim. Much higher precision for fact-seeking queries; higher construction cost.</p> <hr/> <h2 id="key-takeaways">Key Takeaways</h2> <ol> <li> <p><strong>Query-document mismatch is the root cause of most retrieval failures</strong> — HyDE fixes it by generating a hypothetical answer that lives in the same linguistic space as your documents.</p> </li> <li> <p><strong>Dense retrieval is for recall; reranking is for precision</strong> — retrieve 50, rerank to 5. Never use a single-stage retriever in production.</p> </li> <li> <p><strong>ColBERT’s per-token embeddings capture multi-concept queries</strong> that single-vector retrieval compresses away. Worth the indexing overhead for complex domains.</p> </li> <li> <p><strong>Graph RAG isn’t about replacing vector search — it’s about adding structure</strong> that vector search is blind to. Use it when your queries ask about relationships, not just topics.</p> </li> <li> <p><strong>Chunking strategy is the foundation everything else sits on</strong> — semantic chunking and parent-child indexing are the minimum viable improvements over naive fixed-size splits.</p> </li> <li> <p><strong>These techniques compose</strong> — HyDE + two-stage reranking + graph augmentation is a coherent production stack, not competing approaches.</p> </li> </ol> <hr/> <h2 id="further-reading">Further Reading</h2> <h3 id="foundational-papers">Foundational Papers</h3> <ul> <li> <p><strong><a href="https://arxiv.org/abs/2005.11401">Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks</a></strong> — Lewis et al., Facebook AI (2020) The original RAG paper. Combines a dense retriever (DPR) with a seq2seq generator (BART). Sets the vocabulary and evaluation benchmarks the field still uses.</p> </li> <li> <p><strong><a href="https://arxiv.org/abs/2004.04906">Dense Passage Retrieval for Open-Domain Question Answering (DPR)</a></strong> — Karpukhin et al., Facebook AI (2020) The bi-encoder retrieval model that underpins most dense retrieval systems. Training with in-batch negatives is the key technique.</p> </li> </ul> <h3 id="hyde">HyDE</h3> <ul> <li><strong><a href="https://arxiv.org/abs/2212.10496">Precise Zero-Shot Dense Retrieval Without Relevance Labels (HyDE)</a></strong> — Gao et al., CMU (2022) The original HyDE paper. Shows consistent improvements across BEIR benchmark tasks, especially for low-resource domains where query-document vocabulary mismatch is severe.</li> </ul> <h3 id="reranking">Reranking</h3> <ul> <li> <p><strong><a href="https://arxiv.org/abs/1611.09268">MS MARCO: A Human Generated Machine Reading Comprehension Dataset</a></strong> — Bajaj et al., Microsoft (2016) The dataset most reranker models are trained on. Understanding its structure helps you evaluate whether a reranker will generalize to your domain.</p> </li> <li> <p><strong><a href="https://arxiv.org/abs/2310.09497">A Setwise Approach for Effective and Highly Efficient Zero-shot Ranking with Large Language Models</a></strong> — Zhuang et al. (2023) LLM-based reranking — using GPT-4 or similar as a reranker directly. Slower but often better than cross-encoders for out-of-domain queries.</p> </li> </ul> <h3 id="colbert">ColBERT</h3> <ul> <li> <p><strong><a href="https://arxiv.org/abs/2004.12832">ColBERT: Efficient and Effective Passage Search via Contextualized Late Interaction over BERT</a></strong> — Khattab &amp; Zaharia, Stanford (2020) The original ColBERT paper. Introduces late interaction and the deferred MaxSim operation that makes it scalable.</p> </li> <li> <p><strong><a href="https://arxiv.org/abs/2112.01488">ColBERTv2: Effective and Efficient Retrieval via Lightweight Late Interaction</a></strong> — Santhanam et al., Stanford (2021) Improves ColBERT with residual compression — reduces storage 6–10× while maintaining quality. The version used in production systems.</p> </li> <li> <p><strong><a href="https://github.com/bclavie/RAGatouille">RAGatouille</a></strong> — Benjamin Clavié The library that makes ColBERT accessible in 10 lines of code. Wraps PLAID indexing, supports Hugging Face datasets, integrates with LangChain/LlamaIndex.</p> </li> </ul> <h3 id="graph-rag">Graph RAG</h3> <ul> <li> <p><strong><a href="https://arxiv.org/abs/2404.16130">From Local to Global: A Graph RAG Approach to Query-Focused Summarization</a></strong> — Edge et al., Microsoft Research (2024) The GraphRAG paper. Shows 3–4× improvement on global synthesis tasks. Introduces community detection over the knowledge graph as a summarization strategy — compelling results on large corpora.</p> </li> <li> <p><strong><a href="https://arxiv.org/abs/2311.13314">KGRAG: Knowledge Graph Enhanced Retrieval-Augmented Generation</a></strong> — (2023) Alternative approach using existing knowledge graphs (Wikidata, domain KGs) rather than LLM-extracted ones. Lower construction cost, less flexible.</p> </li> </ul> <h3 id="chunking--indexing">Chunking &amp; Indexing</h3> <ul> <li> <p><strong><a href="https://arxiv.org/abs/2312.06648">Propositions as the Unit of Retrieval (Dense X Retrieval)</a></strong> — Chen et al. (2023) Atomic proposition indexing — extract and index individual factual claims rather than passages. Significant precision improvement for fact-seeking queries.</p> </li> <li> <p><strong><a href="https://arxiv.org/abs/2401.18059">RAPTOR: Recursive Abstractive Processing for Tree-Organized Retrieval</a></strong> — Sarthi et al., Stanford (2024) Builds a hierarchical tree of summaries over your document corpus. Retrieval happens at multiple levels of abstraction simultaneously — strong on multi-hop reasoning tasks.</p> </li> </ul> <hr/> <p><em>Naive RAG is a prototype. Production RAG is a system. The difference isn’t any single technique — it’s building a retrieval stack that thinks about where each failure mode comes from and applies the right fix at the right layer. Get that right, and your LLM stops hallucinating not because you made it smarter, but because you stopped giving it reasons to guess.</em></p>]]></content><author><name></name></author><category term="ai-engineering"/><category term="RAG"/><category term="Retrieval"/><category term="HyDE"/><category term="ColBERT"/><category term="Reranking"/><category term="Graph RAG"/><category term="LLMs"/><category term="Vector Search"/><category term="PyTorch"/><summary type="html"><![CDATA[Most RAG systems chunk documents, embed them, and call it a day. That's the floor, not the ceiling. Here's what production retrieval actually looks like — HyDE, late interaction, reranking pipelines, and knowledge graphs that reason over structure.]]></summary></entry><entry><title type="html">Claude Mythos: Anthropic’s Most Powerful Model — Built for Cybersecurity, Not for Everyone</title><link href="https://sridhar-3009.github.io/blog/2026/claude-mythos-cybersecurity/" rel="alternate" type="text/html" title="Claude Mythos: Anthropic’s Most Powerful Model — Built for Cybersecurity, Not for Everyone"/><published>2026-04-08T00:00:00+00:00</published><updated>2026-04-08T00:00:00+00:00</updated><id>https://sridhar-3009.github.io/blog/2026/claude-mythos-cybersecurity</id><content type="html" xml:base="https://sridhar-3009.github.io/blog/2026/claude-mythos-cybersecurity/"><![CDATA[<h1 id="claude-mythos-anthropics-most-powerful-model--built-for-cybersecurity-not-for-everyone">Claude Mythos: Anthropic’s Most Powerful Model — Built for Cybersecurity, Not for Everyone</h1> <p>On April 7, 2026, Anthropic announced something unusual: a new frontier model that they have no intention of making generally available.</p> <p><strong>Claude Mythos Preview</strong> is Anthropic’s most capable model to date — outperforming Claude Opus 4.6 on every major coding and reasoning benchmark by significant margins. It can autonomously find zero-day vulnerabilities that survived decades of human review and millions of automated security tests. It can chain multiple Linux kernel vulnerabilities for privilege escalation without any human guidance.</p> <p>And you cannot use it. At least not unless you’re one of the ~40+ organizations hand-picked to access it through <strong>Project Glasswing</strong> — a coalition of the largest technology companies in the world, convened specifically to give defenders an advantage over attackers before a model like this falls into the wrong hands.</p> <p>This post covers what Mythos is, what it can do, how it compares to other Claude models, and what Anthropic’s decision to restrict it tells us about the future of frontier AI.</p> <hr/> <h2 id="what-is-claude-mythos">What Is Claude Mythos?</h2> <p>Claude Mythos Preview is a <strong>cybersecurity-specialized frontier model</strong> — Anthropic’s most capable AI system ever released (even as a preview), designed from the ground up to assist with defensive security workflows.</p> <p>The word “preview” is doing real work here. This is not a generally available product. It is a research preview offered to a curated set of organizations under strict access controls, as part of a deliberate strategy to arm defenders before attackers gain access to equivalent capabilities.</p> <p>It is available via the Claude API, Amazon Bedrock, Google Cloud Vertex AI, and Microsoft Foundry — but only for invited organizations. There is no self-serve sign-up. Access requires application to the <strong>Cyber Verification Program</strong> or membership in Project Glasswing.</p> <p><strong>Pricing (for authorized users):</strong> $25 per million input tokens / $125 per million output tokens — roughly 5x the cost of Claude Opus 4.6, reflecting both the model’s capability level and the intentionally limited rollout.</p> <hr/> <h2 id="project-glasswing-why-this-exists">Project Glasswing: Why This Exists</h2> <p>To understand Mythos, you first need to understand why Anthropic announced it the way they did.</p> <p><strong>Project Glasswing</strong> (announced April 7, 2026) is a coalition that brings together:</p> <blockquote> <p>AWS, Anthropic, Apple, Broadcom, Cisco, CrowdStrike, Google, JPMorganChase, the Linux Foundation, Microsoft, NVIDIA, and Palo Alto Networks</p> </blockquote> <p>The explicit goal: ensure that AI’s most powerful security capabilities reach defenders before attackers. Anthropic describes it as “an urgent attempt” — the language is deliberate. They believe a model capable of what Mythos can do is coming regardless of whether Anthropic builds it. The question is who gets it first.</p> <p><strong>Commitments made at launch:</strong></p> <ul> <li><strong>$100 million</strong> in Mythos usage credits to Project Glasswing partners</li> <li><strong>$4 million</strong> in usage credits to open-source security organizations</li> <li>Active development of cybersecurity safeguards to detect and block the model’s most dangerous outputs</li> <li>A <strong>Cyber Verification Program</strong> allowing vetted security professionals to access capabilities that would otherwise be blocked</li> </ul> <p>This is not a product launch. It is a coordinated deployment strategy built around a security thesis: defenders need this more urgently than the public needs access to it.</p> <hr/> <h2 id="what-mythos-can-actually-do">What Mythos Can Actually Do</h2> <p>The headline capabilities are genuinely remarkable — and in some cases, should give you pause.</p> <h3 id="autonomous-zero-day-vulnerability-discovery">Autonomous Zero-Day Vulnerability Discovery</h3> <p>Mythos can find vulnerabilities that human reviewers and automated tools have missed — for years, sometimes decades.</p> <p><strong>Documented examples from Anthropic’s announcement:</strong></p> <ul> <li> <p><strong>A 27-year-old vulnerability in OpenBSD</strong> — a security-focused operating system that has been under continuous expert review since 1999. Mythos found a flaw that had survived nearly three decades of scrutiny.</p> </li> <li> <p><strong>A 16-year-old flaw in FFmpeg</strong> — the ubiquitous open-source multimedia framework used in virtually every video streaming platform, browser, and media application on the planet. This vulnerability survived <strong>5 million automated test runs</strong> by fuzzing tools specifically designed to find bugs like it.</p> </li> <li> <p><strong>Linux kernel privilege escalation</strong> — Mythos autonomously identified and chained multiple separate vulnerabilities in the Linux kernel to achieve privilege escalation, without any human guidance or steering at any step.</p> </li> </ul> <p>The phrase “entirely autonomously, without any human steering” appears in Anthropic’s own description. This is not a model that helps a human security researcher — it is a model that <em>is</em> the security researcher, working independently.</p> <h3 id="agentic-code-execution">Agentic Code Execution</h3> <p>Mythos operates as a full agentic system — it doesn’t just identify vulnerabilities, it can develop exploits, write and execute code, navigate file systems, and chain tool calls across long autonomous sessions. This puts it in a different capability class from models that primarily generate text for human review.</p> <h3 id="broad-reasoning-superiority">Broad Reasoning Superiority</h3> <p>Beyond cybersecurity, Mythos is simply a more capable general reasoner than anything Anthropic has previously released. The benchmark numbers confirm this.</p> <hr/> <h2 id="benchmark-performance">Benchmark Performance</h2> <table> <thead> <tr> <th>Benchmark</th> <th>Mythos Preview</th> <th>Claude Opus 4.6</th> <th>Delta</th> </tr> </thead> <tbody> <tr> <td><strong>CyberGym</strong> (Vulnerability Reproduction)</td> <td><strong>83.1%</strong></td> <td>66.6%</td> <td>+16.5 pts</td> </tr> <tr> <td><strong>SWE-bench Pro</strong> (Real-world coding)</td> <td><strong>77.8%</strong></td> <td>53.4%</td> <td>+24.4 pts</td> </tr> <tr> <td><strong>SWE-bench Verified</strong></td> <td><strong>93.9%</strong></td> <td>80.8%</td> <td>+13.1 pts</td> </tr> <tr> <td><strong>Terminal-Bench 2.0</strong></td> <td><strong>82.0%</strong></td> <td>65.4%</td> <td>+16.6 pts</td> </tr> <tr> <td><strong>GPQA Diamond</strong> (Graduate reasoning)</td> <td><strong>94.6%</strong></td> <td>91.3%</td> <td>+3.3 pts</td> </tr> </tbody> </table> <p>A few things stand out:</p> <p><strong>SWE-bench Pro is the most striking.</strong> This benchmark uses real-world GitHub issues from production codebases — not curated problems. Mythos resolves 77.8% of them. Opus 4.6 resolves 53.4%. That 24-point gap is enormous in practical terms — it means Mythos solves roughly half again as many real engineering problems as the best publicly available model.</p> <p><strong>GPQA Diamond shows the narrowest gap.</strong> Graduate-level science reasoning (physics, chemistry, biology) is the one domain where Mythos’ lead over Opus 4.6 shrinks to ~3 points. Both models are operating near the ceiling of this benchmark. The differentiation is overwhelmingly in agentic, tool-use, and code-execution tasks.</p> <p><strong>CyberGym is a new benchmark.</strong> It measures vulnerability reproduction — given a description of a known CVE, can the model reproduce the exploit? At 83.1%, Mythos is far ahead of anything previously available. This is not a benchmark most models even attempt.</p> <hr/> <h2 id="how-it-compares-to-the-full-claude-model-family">How It Compares to the Full Claude Model Family</h2> <p>Claude currently has three publicly available tiers, plus Mythos as a restricted preview:</p> <table> <thead> <tr> <th>Model</th> <th>Context</th> <th>Pricing (in/out per MTok)</th> <th>Best For</th> </tr> </thead> <tbody> <tr> <td><strong>Claude Haiku 4.5</strong></td> <td>200k tokens</td> <td>$1 / $5</td> <td>Speed, high-volume tasks</td> </tr> <tr> <td><strong>Claude Sonnet 4.6</strong></td> <td>1M tokens</td> <td>$3 / $15</td> <td>Balanced speed + intelligence</td> </tr> <tr> <td><strong>Claude Opus 4.6</strong></td> <td>1M tokens</td> <td>$5 / $25</td> <td>Complex reasoning, agentic tasks</td> </tr> <tr> <td><strong>Claude Mythos Preview</strong></td> <td>TBD</td> <td>$25 / $125</td> <td>Cybersecurity, frontier capability</td> </tr> </tbody> </table> <p>Mythos sits at 5x the price of Opus 4.6 — and based on the benchmark numbers, earns it for the right workloads.</p> <hr/> <h2 id="the-safety-calculus-why-anthropic-restricted-it">The Safety Calculus: Why Anthropic Restricted It</h2> <p>This is the part that makes Mythos genuinely interesting from an AI development perspective.</p> <p>Anthropic has built a model that is meaningfully more capable than anything publicly available. Rather than releasing it broadly — which would maximize revenue and reach — they’ve chosen a controlled deployment specifically because of what the model can do.</p> <p>Their reasoning, as stated:</p> <ol> <li> <p><strong>Offense/defense asymmetry</strong>: A model that can find zero-days is dangerous in attacker hands and invaluable in defender hands. Releasing to defenders first, under access controls, tries to tilt that asymmetry.</p> </li> <li> <p><strong>Safeguards are still in development</strong>: Anthropic explicitly states they are “developing cybersecurity (and other) safeguards that detect and block the model’s most dangerous outputs” — and that these safeguards will be incorporated into future public Claude Opus releases. Mythos Preview ships without the full safety stack because the safety stack isn’t ready yet.</p> </li> <li> <p><strong>Verification before access</strong>: The Cyber Verification Program requires applicants to demonstrate legitimate security work. This isn’t perfect — verification can fail — but it’s a meaningful friction point that general API access wouldn’t provide.</p> </li> </ol> <p>This approach is a departure from the default “release and iterate” model that most AI labs follow. It raises genuinely hard questions: who decides which organizations are legitimate defenders? What happens when a Glasswing partner is compromised? How long before equivalent capabilities are available elsewhere?</p> <p>Anthropic isn’t pretending these questions have clean answers. The framing of “urgent attempt” suggests they believe the window for this strategy is limited.</p> <hr/> <h2 id="what-this-means-for-the-industry">What This Means for the Industry</h2> <p><strong>For security teams:</strong> The gap between AI-assisted and unassisted security work is about to widen dramatically. Teams with access to Mythos-class models will find vulnerabilities faster, assess attack surfaces more completely, and respond to incidents with better intelligence than teams without. This is not a marginal advantage.</p> <p><strong>For open source:</strong> The $4M in credits to open-source security organizations is significant. Projects like OpenBSD, the Linux kernel, curl, OpenSSL — the infrastructure everything else runs on — will have access to a model that has already demonstrated it can find bugs their own maintainers missed.</p> <p><strong>For AI development norms:</strong> Project Glasswing is an experiment in whether a controlled, coalition-based deployment can work for a dual-use capability. If it does — if defenders meaningfully get ahead of attackers because of it — it will influence how future frontier models with dangerous capabilities are released. If it fails, it will inform what stronger interventions might look like.</p> <p><strong>For developers:</strong> Mythos will eventually influence what’s in public Claude models. Anthropic’s statement that the cybersecurity safeguards will be incorporated into future Opus releases suggests a pipeline: build capability at the frontier, develop safety measures, then push both into general availability. The public models you use today are yesterday’s frontier models with better guardrails.</p> <hr/> <h2 id="how-to-get-access-if-you-qualify">How to Get Access (If You Qualify)</h2> <p>There are two paths:</p> <p><strong>1. Project Glasswing Partnership</strong> If your organization works on critical infrastructure security and could benefit from Glasswing partnership, the announcement page at <a href="https://www.anthropic.com/glasswing">anthropic.com/glasswing</a> is the starting point. This is designed for large organizations — the current partners are some of the largest technology and security companies in the world.</p> <p><strong>2. Cyber Verification Program</strong> For individual security researchers and smaller organizations doing legitimate defensive security work, the Cyber Verification Program offers a path to expanded access to Mythos capabilities. This is the relevant path for penetration testers, vulnerability researchers, and security engineers.</p> <hr/> <h2 id="what-comes-next">What Comes Next</h2> <p>Anthropic has not announced a timeline for general availability — and given the framing, it seems unlikely Mythos Preview will ever be generally available in its current form. The more likely path:</p> <ul> <li>Mythos Preview continues as an invitation-only model for Glasswing and verified security work</li> <li>Cybersecurity safeguards currently in development get incorporated into future Claude Opus releases</li> <li>Future Claude models gain some of Mythos’ capability at lower price points, with the safety stack that makes them appropriate for general use</li> <li>A “Mythos” successor model — more capable still — becomes the next restricted frontier</li> </ul> <p>This mirrors how the rest of the Claude family has evolved: capabilities that required frontier models a year ago are now standard in Sonnet and Haiku. The frontier keeps moving. Mythos is today’s frontier.</p> <hr/> <h2 id="further-reading">Further Reading</h2> <h3 id="official-anthropic-sources">Official Anthropic Sources</h3> <ul> <li><strong><a href="https://www.anthropic.com/glasswing">Project Glasswing — anthropic.com/glasswing</a></strong> — Official announcement, partner list, and access information</li> <li><strong><a href="https://platform.claude.com/docs/en/docs/about-claude/models/overview">Claude Models Overview</a></strong> — Full model comparison table including Mythos Preview details</li> <li><strong><a href="https://www.anthropic.com/research">Anthropic’s Core Views on AI Safety</a></strong> — The safety philosophy behind decisions like the Mythos restricted release</li> </ul> <h3 id="background-on-ai-in-cybersecurity">Background on AI in Cybersecurity</h3> <ul> <li><strong><a href="https://www.nist.gov/cyberframework">NIST Cybersecurity Framework</a></strong> — The standards framework that defensive security work is built on</li> <li><strong><a href="https://cve.mitre.org/">Common Vulnerabilities and Exposures (CVE)</a></strong> — The database of publicly disclosed vulnerabilities. Mythos-class models change how these get found.</li> <li><strong><a href="https://www.swebench.com/">SWE-bench Leaderboard</a></strong> — Live benchmark tracking for AI on real software engineering tasks</li> <li><strong><a href="https://arxiv.org/abs/2311.12022">GPQA: A Graduate-Level Benchmark</a></strong> — The paper behind the GPQA Diamond benchmark used to evaluate Mythos</li> </ul> <h3 id="dual-use-ai-and-policy">Dual-Use AI and Policy</h3> <ul> <li><strong><a href="https://www.brookings.edu/articles/the-dual-use-dilemma/">The Dual-Use Dilemma in AI</a></strong> — Brookings analysis of AI capabilities that are simultaneously defensive and offensive</li> <li><strong><a href="https://www.anthropic.com/responsible-scaling-policy">Anthropic’s Responsible Scaling Policy</a></strong> — The policy framework that governs when and how Anthropic deploys frontier capabilities</li> <li><strong><a href="https://www.anthropic.com/research/core-views-on-ai-safety">AI Safety Levels (ASL)</a></strong> — Anthropic’s internal framework for assessing and managing model risk before deployment</li> </ul> <hr/> <p><em>Claude Mythos is the clearest signal yet that frontier AI capabilities are outpacing the safety infrastructure needed to deploy them broadly. Anthropic’s choice to restrict it rather than release it is either responsible restraint or an elaborate delay — probably both. Either way, the gap between what the most capable AI can do and what the public has access to just got measurably wider.</em></p>]]></content><author><name></name></author><category term="ai-research"/><category term="Anthropic"/><category term="Claude"/><category term="Cybersecurity"/><category term="LLM"/><category term="AI Safety"/><category term="Project Glasswing"/><category term="Frontier AI"/><summary type="html"><![CDATA[Anthropic just unveiled Claude Mythos Preview — a frontier model that autonomously finds zero-day vulnerabilities, outperforms every other Claude model on coding benchmarks, and is explicitly not available to the public. Here's everything you need to know about the most capable — and most restricted — AI model Anthropic has ever built.]]></summary></entry><entry><title type="html">Artemis II: Humanity’s Return to the Moon — Everything That Has Happened and What Comes Next</title><link href="https://sridhar-3009.github.io/blog/2026/artemis-2-mission/" rel="alternate" type="text/html" title="Artemis II: Humanity’s Return to the Moon — Everything That Has Happened and What Comes Next"/><published>2026-04-07T00:00:00+00:00</published><updated>2026-04-07T00:00:00+00:00</updated><id>https://sridhar-3009.github.io/blog/2026/artemis-2-mission</id><content type="html" xml:base="https://sridhar-3009.github.io/blog/2026/artemis-2-mission/"><![CDATA[<h1 id="artemis-ii-humanitys-return-to-the-moon">Artemis II: Humanity’s Return to the Moon</h1> <blockquote> <p><em>As of April 7, 2026, the Artemis II crew is approximately Day 6 of their mission — returning from the Moon after a historic lunar flyby. This post covers the full story from preparation to present and what happens after splashdown.</em></p> </blockquote> <p>On April 1, 2026, at 22:35 UTC, a 322-foot Space Launch System rocket lit up the night sky above Kennedy Space Center’s Launch Complex 39B. Aboard the Orion capsule: four astronauts about to become the first humans to travel beyond low Earth orbit in over 53 years.</p> <p>This is Artemis II. Not a landing — a proving flight. A 10-day journey around the Moon and back, testing every system that will one day carry humans to the lunar surface, and eventually, to Mars.</p> <hr/> <h2 id="the-crew">The Crew</h2> <p>Artemis II carries four astronauts, each making history in their own right.</p> <h3 id="reid-wiseman--commander-nasa">Reid Wiseman — Commander (NASA)</h3> <p>A former Navy test pilot and veteran of the ISS, Wiseman was selected as Artemis II commander in 2023. At 58, he becomes the oldest person to travel beyond low Earth orbit. He previously served as Chief of the Astronaut Office at NASA. His role: ultimate responsibility for crew safety, mission decisions, and vehicle command.</p> <h3 id="victor-glover--pilot-nasa">Victor Glover — Pilot (NASA)</h3> <p>A Navy Captain and fighter pilot with over 3,000 flight hours, Glover flew to the ISS on the first operational Crew Dragon mission in 2020. On Artemis II he becomes the <strong>first person of color to travel around the Moon</strong> — a milestone with enormous cultural significance 54 years after Apollo’s final mission.</p> <h3 id="christina-koch--mission-specialist-1-nasa">Christina Koch — Mission Specialist 1 (NASA)</h3> <p>Koch holds the record for the longest single spaceflight by a woman (328 days on the ISS). She is the <strong>first woman ever to travel to the Moon</strong> — a barrier that stood for the entire era of human spaceflight. Her expertise in life sciences and EVA operations makes her central to crew health monitoring during the mission.</p> <h3 id="jeremy-hansen--mission-specialist-2-canadian-space-agency">Jeremy Hansen — Mission Specialist 2 (Canadian Space Agency)</h3> <p>For Hansen, this is his first spaceflight — and what a first flight. He becomes the <strong>first non-American to travel around the Moon</strong>, reflecting Canada’s deep partnership with NASA through the Lunar Gateway program and Canadarm3. The CSA contributed critical hardware to both the SLS and Orion programs.</p> <hr/> <h2 id="why-artemis-ii-is-not-a-landing-and-why-thats-the-right-call">Why Artemis II Is Not a Landing (And Why That’s the Right Call)</h2> <p>Many people ask: why go around the Moon without landing? The answer is engineering discipline.</p> <p>Artemis I (November 2022) flew the same free-return trajectory with an uncrewed Orion. It was a success by every measure — but post-flight inspection revealed unexpected <strong>heat shield erosion</strong> during reentry. Charred material had ablated in ways the models didn’t predict, raising questions about the capsule’s safety margins for a crewed return.</p> <p>NASA convened an independent review team. After months of analysis, they concluded: the heat shield is safe, but we’ll fly a slightly steeper reentry angle to reduce the ablation issue. Artemis II became the test of that fix, with a real crew, under real conditions.</p> <p>This is how spaceflight is done right. You don’t skip steps. You don’t rush the crewed landing because the public is impatient. You send four people around the Moon first, get them back safely, and then you land.</p> <hr/> <h2 id="the-road-to-launch-preparation-timeline">The Road to Launch: Preparation Timeline</h2> <p>The path from Artemis I’s splashdown to Artemis II’s launch took over three years. Here is what happened:</p> <h3 id="2023">2023</h3> <ul> <li><strong>January</strong>: Post-Artemis I heat shield investigation begins</li> <li><strong>September</strong>: RS-25 engines installed on Artemis II core stage at Stennis Space Center</li> <li><strong>October</strong>: Crew announced officially: Wiseman, Glover, Koch, Hansen</li> </ul> <h3 id="2024">2024</h3> <ul> <li><strong>Spring</strong>: Core stage processing continues at Michoud Assembly Facility</li> <li><strong>July–August</strong>: Core stage shipped to Kennedy Space Center</li> <li><strong>November</strong>: Rocket stacking begins in the Vehicle Assembly Building — SLS segments assembled vertically for the first time with Artemis II hardware</li> </ul> <h3 id="2025">2025</h3> <ul> <li><strong>April</strong>: Engine swap completed after valve issues discovered on one RS-25 engine</li> <li><strong>October</strong>: Rocket stacking completed. Full SLS + Orion stack stands 322 feet tall in the VAB</li> <li><strong>December</strong>: Orion crew module final outfitting and crew equipment loading</li> </ul> <h3 id="2026">2026</h3> <ul> <li><strong>February 2</strong>: Wet dress rehearsal reveals liquid hydrogen leak. Mission delayed.</li> <li><strong>February 19</strong>: Second wet dress rehearsal — successful</li> <li><strong>February 21</strong>: Helium flow anomaly discovered. Rocket rolled back to VAB for investigation</li> <li><strong>March 12</strong>: Flight Readiness Review approved. Go for launch</li> <li><strong>March 20</strong>: Final rollout to Launch Complex 39B</li> <li><strong>April 1, 22:35 UTC</strong>: <strong>Launch</strong></li> </ul> <hr/> <h2 id="the-mission-day-by-day">The Mission: Day by Day</h2> <h3 id="day-1--high-earth-orbit-system-checkout">Day 1 — High Earth Orbit System Checkout</h3> <p>After reaching a 23.5-hour elliptical high Earth orbit, the crew began the most critical early phase: <strong>verifying every Orion system works in space before committing to the Moon</strong>.</p> <p>Life support, propulsion, navigation, communications, suit systems, displays — all checked methodically against pre-planned procedures. If anything had failed here, the crew could have returned to Earth within hours.</p> <p>The highlight of Day 1: <strong>proximity operations with the spent ICPS upper stage</strong>. Wiseman manually flew Orion to within close range of the inert booster over a 70-minute demonstration — rehearsing the docking skills future Artemis missions will use with the Human Landing System.</p> <h3 id="day-2--trans-lunar-injection">Day 2 — Trans-Lunar Injection</h3> <p>The ICPS fired one final time: a 5-minute, 49-second burn that accelerated Orion from ~17,000 mph to ~24,500 mph — escape velocity from Earth orbit, aimed at the Moon.</p> <p>This burn placed the crew on a <strong>free-return trajectory</strong>: a precisely calculated arc that loops around the Moon and returns to Earth using only gravity, requiring no additional propulsion for the return. If the main engine failed tomorrow, the crew would still come home. This passive safety margin was a deliberate design choice absent from Apollo missions.</p> <h3 id="days-35--the-outbound-journey">Days 3–5 — The Outbound Journey</h3> <p><em>Image credit: NASA. An Artemis II crew member gazes at Earth through Orion’s window during the outbound transit to the Moon.</em></p> <p>Three days of cruise, system monitoring, and crew activities. The crew:</p> <ul> <li>Tested manual attitude control of Orion (the first human handling of a deep-space vehicle in over 50 years)</li> <li>Completed spacesuit fit checks and certification runs — verifying all four suits are ready for emergency use</li> <li>Conducted medical monitoring including radiation dosimetry readings (radiation in deep space is significantly higher than on the ISS)</li> <li>Observed Earth shrinking to a brilliant sphere in the window — the first humans to see it that way since Gene Cernan’s crew in December 1972</li> </ul> <h3 id="day-6--the-lunar-flyby">Day 6 — The Lunar Flyby</h3> <p><em>Image credit: NASA. The Orion spacecraft with the Moon visible in the distance — captured during Artemis II’s deep space transit.</em></p> <p>The emotional and scientific peak of the mission.</p> <p><strong>Closest approach: approximately 4,067 miles (6,545 km) from the lunar far side.</strong> Not as close as a landing trajectory, but far beyond anything since Apollo — and intentionally so. The free-return trajectory naturally swings farther out than a powered lunar orbit insertion would.</p> <p><strong>Records broken at closest approach:</strong></p> <ul> <li><strong>Farthest humans from Earth ever</strong>: 252,760 miles — surpassing Apollo 13’s 248,655 miles</li> <li><strong>Farthest humans beyond the Moon</strong>: ~4,700 miles past the lunar surface</li> <li><strong>Most people in deep space simultaneously</strong>: 4 (Apollo 8 record was 3)</li> </ul> <p>For 40 minutes, Orion passed behind the Moon, cutting off all radio contact with Earth — the same blackout Apollo crews experienced, now repeated for the first time in five decades.</p> <p>The crew also witnessed a <strong>57-minute solar eclipse</strong> as the Moon blocked the Sun — something no human had seen since the Apollo era.</p> <h3 id="days-710--the-return">Days 7–10 — The Return</h3> <p>Orion now coasts home on its free-return arc. Two trajectory correction burns fine-tune the reentry corridor. The crew monitors all systems, completes science objectives, and prepares for the most demanding phase: reentry.</p> <h3 id="day-10--reentry-and-splashdown">Day 10 — Reentry and Splashdown</h3> <p>Orion will re-enter Earth’s atmosphere at approximately <strong>25,000 miles per hour</strong> — the fastest crewed reentry in history, faster even than Apollo returns because of Orion’s trajectory.</p> <p>Unlike Artemis I, which used a <strong>skip reentry</strong> (bouncing off the upper atmosphere to shed speed gradually), Artemis II uses a <strong>direct steeper-angle reentry</strong> — the modification NASA made to address the heat shield erosion concern. This is the primary engineering validation of the entire mission.</p> <p><strong>Planned splashdown: April 10–11, 2026, Pacific Ocean near San Diego.</strong> USS San Diego is positioned for recovery, alongside NASA’s recovery team and medical personnel.</p> <hr/> <h2 id="what-comes-after-the-road-to-the-moons-surface">What Comes After: The Road to the Moon’s Surface</h2> <p>Artemis II is a milestone, not a destination. Here is what the program looks like beyond splashdown:</p> <h3 id="artemis-iii--first-lunar-landing-since-1972-planned-2027">Artemis III — First Lunar Landing Since 1972 (Planned 2027)</h3> <p>The first crewed landing on the Moon since Apollo 17’s Gene Cernan in December 1972. The mission will land near the <strong>lunar south pole</strong> — a region Apollo never visited, chosen because permanently shadowed craters there likely contain <strong>water ice</strong>, a critical resource for long-term exploration and potential fuel production.</p> <p><strong>Landing system</strong>: SpaceX Starship Human Landing System (HLS) — a lunar variant of Starship that will carry 2 of the 4 Orion crew members from lunar orbit to the surface and back. The other 2 remain in Orion.</p> <p><strong>Surface activities</strong>: 2 EVAs of approximately 2 hours each. The crew will collect samples, deploy instruments, and scout the terrain that future Artemis missions will inhabit for weeks.</p> <h3 id="artemis-iv--lunar-gateway-assembly-planned-2028">Artemis IV — Lunar Gateway Assembly (Planned 2028)</h3> <p>Artemis IV introduces the <strong>Lunar Gateway</strong> — a small space station in lunar orbit that will serve as a staging point for surface operations. Artemis IV will deliver the first habitation module and begin building humanity’s permanent infrastructure beyond Earth orbit.</p> <p>The Gateway is a fundamentally different approach from Apollo: instead of point-to-point missions, each Artemis mission will build infrastructure that the next one uses.</p> <h3 id="artemis-v-and-beyond--extended-surface-operations">Artemis V and Beyond — Extended Surface Operations</h3> <p>Later missions extend surface stays from hours to weeks. Pressurized rovers. Permanent habitats. Resource extraction experiments. The south pole becomes a research base, not just a landing site.</p> <h3 id="the-bigger-picture-moon-to-mars">The Bigger Picture: Moon to Mars</h3> <p>NASA’s stated long-term goal is Mars — and the Moon is the proving ground. Every system being tested on Artemis (life support for long duration, radiation mitigation, in-situ resource utilization) is directly applicable to a multi-year Mars transit. Artemis is not nostalgia for Apollo. It is the first phase of interplanetary civilization.</p> <hr/> <h2 id="why-this-matters-beyond-the-mission">Why This Matters Beyond the Mission</h2> <p>It is easy to process Artemis II as a news event — a launch, some records, a splashdown — and move on. But the significance is larger.</p> <p><strong>Generational continuity</strong>: Everyone alive during Apollo who followed the space program closely is now in their 70s or older. For the majority of humans alive today, no one has ever left low Earth orbit in their lifetime. Artemis II is the first time in most people’s lives that they can look up and know: right now, there are people near the Moon.</p> <p><strong>Diversity</strong>: Christina Koch is the first woman to travel to the Moon. Victor Glover is the first person of color. Jeremy Hansen is the first non-American. These aren’t symbolic gestures — they are the natural result of a broader talent pool and a more inclusive astronaut corps. The faces of exploration are changing.</p> <p><strong>International partnership</strong>: Artemis is not an American program with international contributors. It is a genuinely multinational effort. Canada, ESA, JAXA, and others contribute hardware, crew, and funding. The Lunar Gateway will be operated by multiple space agencies. This is the model for sustainable deep space exploration.</p> <p><strong>Commercial participation</strong>: SpaceX builds the landing system. Blue Origin builds the Gateway’s propulsion module. The era of government-only spaceflight is over. Artemis is proof that the public-private model can execute missions as ambitious as anything Apollo attempted.</p> <hr/> <h2 id="image-credits">Image Credits</h2> <p><em>All images in this post are courtesy of <strong>NASA</strong> and the <strong>Canadian Space Agency (CSA)</strong>. NASA imagery is in the public domain. Images sourced from the <a href="https://images.nasa.gov/">NASA Image and Video Library</a>.</em></p> <p><em>To explore more Artemis imagery, visit:</em></p> <ul> <li><strong><a href="https://www.nasa.gov/humans-in-space/artemis/">NASA Image Gallery — Artemis</a></strong> — official mission photography</li> <li><strong><a href="https://www.flickr.com/photos/nasahqphoto/">NASA Flickr</a></strong> — high-resolution press photos</li> <li><strong><a href="https://images.nasa.gov/">NASA Image and Video Library</a></strong> — searchable archive of all NASA imagery</li> </ul> <hr/> <h2 id="further-reading">Further Reading</h2> <h3 id="official-nasa-sources">Official NASA Sources</h3> <ul> <li><strong><a href="https://www.nasa.gov/humans-in-space/artemis/">NASA Artemis Program</a></strong> — Mission overview, crew bios, updates</li> <li><strong><a href="https://www.nasa.gov/orion/">Orion Spacecraft</a></strong> — Technical details on the capsule</li> <li><strong><a href="https://www.nasa.gov/space-launch-system/">Space Launch System</a></strong> — SLS rocket overview and specifications</li> <li><strong><a href="https://www.nasa.gov/gateway/">Lunar Gateway</a></strong> — The planned lunar space station</li> </ul> <h3 id="deep-dives">Deep Dives</h3> <ul> <li><strong><a href="https://en.wikipedia.org/wiki/Artemis_II">Artemis II Wikipedia</a></strong> — Comprehensive mission reference with citations</li> <li><strong><a href="https://www.nasa.gov/moontomars/">NASA’s Moon to Mars Architecture</a></strong> — Long-range exploration strategy</li> <li><strong><a href="https://www.nasa.gov/artemis/artemis-i/">Heat Shield Independent Review Report</a></strong> — What NASA found after Artemis I and how they fixed it</li> </ul> <h3 id="companion-reading">Companion Reading</h3> <ul> <li><strong><a href="https://science.nasa.gov/moon/lunar-south-pole/">Why the Lunar South Pole?</a></strong> — NASA’s explanation of why all Artemis landings target the south pole</li> <li><strong><a href="https://humanresearchroadmap.nasa.gov/">Human Research at the Moon</a></strong> — The medical and physiological challenges of deep space travel</li> <li><strong><a href="https://www.spacex.com/vehicles/starship/">SpaceX Starship HLS</a></strong> — The commercial lander that will carry Artemis III to the surface</li> </ul> <hr/> <p><em>The Moon is 238,855 miles away. Right now, four people are closer to it than any human has been since 1972. Follow along at <a href="https://www.nasa.gov">nasa.gov</a>.</em></p>]]></content><author><name></name></author><category term="other"/><category term="Artemis"/><category term="NASA"/><category term="Moon"/><category term="Space"/><category term="SLS"/><category term="Orion"/><category term="Deep Space"/><category term="Aerospace"/><summary type="html"><![CDATA[Artemis II is currently in flight — the first crewed mission beyond low Earth orbit since Apollo 17 in 1972. Here is a complete account of the mission, the crew, the records broken, and what comes next on humanity's road back to the Moon.]]></summary></entry><entry><title type="html">I Rebuilt Claude Code in Go — Works Offline with Ollama, No API Key Required</title><link href="https://sridhar-3009.github.io/blog/2026/forge-go-ai-agent/" rel="alternate" type="text/html" title="I Rebuilt Claude Code in Go — Works Offline with Ollama, No API Key Required"/><published>2026-04-04T00:00:00+00:00</published><updated>2026-04-04T00:00:00+00:00</updated><id>https://sridhar-3009.github.io/blog/2026/forge-go-ai-agent</id><content type="html" xml:base="https://sridhar-3009.github.io/blog/2026/forge-go-ai-agent/"><![CDATA[<h1 id="i-rebuilt-claude-code-in-go--works-offline-with-ollama-no-api-key-required">I Rebuilt Claude Code in Go — Works Offline with Ollama, No API Key Required</h1> <p>When the Claude Code source leaked, the AI developer community got an unexpected gift: a detailed look at how a production-grade agentic coding assistant is actually built under the hood. Not the marketing pitch — the real scaffolding. The tool loop. The prompt structure. The stream-parse-execute cycle that makes it feel like a colleague rather than a chatbot.</p> <p>I read every line. Then I built my own version.</p> <p><strong><a href="https://github.com/sai-sridhar-repo-07/tarra-claw">Forge</a></strong> is an open-source AI coding agent CLI written in Go. It does what Claude Code does — code review, commit generation, interactive AI chat, file editing, bash execution — but it’s a single 16MB binary that starts in under 50ms, runs fully offline via Ollama, and costs nothing if you don’t want to pay for an API.</p> <p>This post is about what I learned building it and why Go was the right choice.</p> <hr/> <h2 id="what-the-leak-revealed">What the Leak Revealed</h2> <p>The core of Claude Code — and every serious agentic coding tool — is deceptively simple. It’s a loop:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. Send context + tools to the model
2. Stream the response
3. If the model calls a tool → execute it, append result
4. Repeat until the model stops calling tools
5. Return final response
</code></pre></div></div> <p>That’s it. The magic is entirely in:</p> <ul> <li><strong>What tools you give the model</strong> (bash, file read/write, grep, glob, web fetch…)</li> <li><strong>How you manage context</strong> (what you include, what you truncate, how you represent tool results)</li> <li><strong>The quality of your system prompt</strong></li> </ul> <p>The leak confirmed what interpretability researchers already knew: the “intelligence” in these tools is overwhelmingly in the underlying model. The scaffolding is plumbing. Good plumbing matters — but it’s learnable, replicable, and in Go, very fast.</p> <hr/> <h2 id="why-go">Why Go?</h2> <p>Claude Code is TypeScript/Node. Most AI tooling is Python. I chose Go for three reasons:</p> <p><strong>1. Single binary deployment</strong> <code class="language-plaintext highlighter-rouge">go build</code> produces one self-contained executable. No <code class="language-plaintext highlighter-rouge">node_modules</code>, no virtualenv, no runtime dependency hell. You download Forge, you run Forge. That’s the entire installation.</p> <p><strong>2. Startup time</strong> Node.js cold-start on a typical dev machine: 200–400ms. Go: under 50ms. When you’re running <code class="language-plaintext highlighter-rouge">forge review --staged</code> as a pre-commit hook dozens of times a day, that difference compounds into real friction.</p> <p><strong>3. Goroutines for streaming</strong> AI responses stream as server-sent events. Go’s concurrency model — goroutines + channels — makes reading a stream while processing tool calls genuinely elegant. No async/await callback soup.</p> <hr/> <h2 id="the-architecture">The Architecture</h2> <p>Forge has four main layers:</p> <h3 id="1-provider-interface">1. Provider Interface</h3> <div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">type</span> <span class="n">Provider</span> <span class="k">interface</span> <span class="p">{</span>
    <span class="n">Chat</span><span class="p">(</span><span class="n">ctx</span> <span class="n">context</span><span class="o">.</span><span class="n">Context</span><span class="p">,</span> <span class="n">messages</span> <span class="p">[]</span><span class="n">Message</span><span class="p">,</span> <span class="n">tools</span> <span class="p">[]</span><span class="n">Tool</span><span class="p">)</span> <span class="p">(</span><span class="o">&lt;-</span><span class="k">chan</span> <span class="n">Event</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span>
    <span class="n">Models</span><span class="p">(</span><span class="n">ctx</span> <span class="n">context</span><span class="o">.</span><span class="n">Context</span><span class="p">)</span> <span class="p">([]</span><span class="n">Model</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div> <p>Both <code class="language-plaintext highlighter-rouge">AnthropicProvider</code> and <code class="language-plaintext highlighter-rouge">OllamaProvider</code> implement this. Swap the provider, everything else stays the same. This is what makes offline mode work — Ollama speaks a compatible tool-use format, so the agentic engine doesn’t know or care which backend it’s talking to.</p> <h3 id="2-the-agentic-engine">2. The Agentic Engine</h3> <p>The send-stream-tools-repeat loop from above, implemented cleanly:</p> <div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="p">(</span><span class="n">a</span> <span class="o">*</span><span class="n">Agent</span><span class="p">)</span> <span class="n">Run</span><span class="p">(</span><span class="n">ctx</span> <span class="n">context</span><span class="o">.</span><span class="n">Context</span><span class="p">,</span> <span class="n">task</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
    <span class="n">messages</span> <span class="o">:=</span> <span class="p">[]</span><span class="n">Message</span><span class="p">{{</span><span class="n">Role</span><span class="o">:</span> <span class="s">"user"</span><span class="p">,</span> <span class="n">Content</span><span class="o">:</span> <span class="n">task</span><span class="p">}}</span>

    <span class="k">for</span> <span class="p">{</span>
        <span class="n">events</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">a</span><span class="o">.</span><span class="n">provider</span><span class="o">.</span><span class="n">Chat</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">messages</span><span class="p">,</span> <span class="n">a</span><span class="o">.</span><span class="n">tools</span><span class="p">)</span>
        <span class="c">// ... stream events</span>

        <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">toolCalls</span><span class="p">)</span> <span class="o">==</span> <span class="m">0</span> <span class="p">{</span>
            <span class="k">break</span> <span class="c">// model is done</span>
        <span class="p">}</span>

        <span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">call</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">toolCalls</span> <span class="p">{</span>
            <span class="n">result</span> <span class="o">:=</span> <span class="n">a</span><span class="o">.</span><span class="n">executeTool</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">call</span><span class="p">)</span>
            <span class="n">messages</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">messages</span><span class="p">,</span> <span class="n">toolResultMessage</span><span class="p">(</span><span class="n">call</span><span class="o">.</span><span class="n">ID</span><span class="p">,</span> <span class="n">result</span><span class="p">))</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="no">nil</span>
<span class="p">}</span>
</code></pre></div></div> <h3 id="3-the-16-built-in-tools">3. The 16 Built-in Tools</h3> <table> <thead> <tr> <th>Tool</th> <th>What it does</th> </tr> </thead> <tbody> <tr> <td><code class="language-plaintext highlighter-rouge">bash</code></td> <td>Execute shell commands</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">read_file</code></td> <td>Read file contents</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">write_file</code></td> <td>Create or overwrite files</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">edit_file</code></td> <td>Targeted string replacement (preserves context)</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">grep</code></td> <td>Regex search across files</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">glob</code></td> <td>File pattern matching</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">web_fetch</code></td> <td>Fetch and summarize web pages</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">git_diff</code></td> <td>Get staged/unstaged diffs</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">list_dir</code></td> <td>Directory listing</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">todo_write</code></td> <td>Manage task lists</td> </tr> <tr> <td>+ 6 more</td> <td>Notebook, task tracking, etc.</td> </tr> </tbody> </table> <p>This is essentially the same tool surface as Claude Code. The model uses them identically because they’re described the same way in the system prompt.</p> <h3 id="4-tui-with-bubble-tea">4. TUI with Bubble Tea</h3> <p>The interactive mode uses <a href="https://github.com/charmbracelet/bubbletea">Bubble Tea</a> — a Go framework for terminal UIs following the Elm architecture. It handles the chat interface, streaming output rendering, and keyboard shortcuts cleanly without any JavaScript-framework complexity.</p> <hr/> <h2 id="the-two-things-that-actually-matter">The Two Things That Actually Matter</h2> <p>Building this taught me what the scaffold does and doesn’t do.</p> <h3 id="context-management-is-everything">Context management is everything</h3> <p>The biggest practical challenge is context window management. As a conversation grows, you hit the model’s token limit. The naive fix — truncate old messages — breaks tool-call continuity. The right approach is selective summarization: keep tool results recent, summarize earlier conversation turns, never truncate mid-tool-call.</p> <p>Getting this wrong means the model loses track of what files it’s already edited. Getting it right means long multi-file refactors work coherently.</p> <h3 id="system-prompt-quality-determines-behavior">System prompt quality determines behavior</h3> <p>I spent more time on the system prompt than on any other single component. The model’s “personality” as a coding agent — how it plans before acting, whether it reads files before editing them, how it handles errors — is entirely encoded in that prompt. The code is just infrastructure for delivering it.</p> <hr/> <h2 id="quick-start-60-seconds">Quick Start (60 seconds)</h2> <h3 id="with-anthropic-api">With Anthropic API</h3> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Download binary (macOS Apple Silicon)</span>
curl <span class="nt">-L</span> https://github.com/sai-sridhar-repo-07/tarra-claw/releases/latest/download/forge-darwin-arm64 <span class="nt">-o</span> forge
<span class="nb">chmod</span> +x forge <span class="o">&amp;&amp;</span> <span class="nb">sudo mv </span>forge /usr/local/bin/

<span class="c"># Set your key</span>
<span class="nb">export </span><span class="nv">ANTHROPIC_API_KEY</span><span class="o">=</span>sk-ant-...

<span class="c"># Review your staged changes before committing</span>
forge review <span class="nt">--staged</span>
</code></pre></div></div> <h3 id="fully-offline-with-ollama-free">Fully Offline with Ollama (free)</h3> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Install Ollama</span>
brew <span class="nb">install </span>ollama <span class="o">&amp;&amp;</span> ollama serve &amp;

<span class="c"># Pull a coding model (4GB, worth it)</span>
ollama pull qwen2.5-coder:7b

<span class="c"># Run Forge against it — no API key needed</span>
<span class="nv">FORGE_PROVIDER</span><span class="o">=</span>ollama forge review <span class="nt">--staged</span>
</code></pre></div></div> <h3 id="model-recommendations-for-offline-use">Model Recommendations for Offline Use</h3> <table> <thead> <tr> <th>Model</th> <th>Size</th> <th>Best for</th> </tr> </thead> <tbody> <tr> <td><code class="language-plaintext highlighter-rouge">llama3.2</code></td> <td>2GB</td> <td>General chat, quick tasks</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">qwen2.5-coder:7b</code></td> <td>4GB</td> <td>Code review, editing (recommended)</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">deepseek-coder-v2</code></td> <td>8GB</td> <td>Large codebases, complex refactors</td> </tr> </tbody> </table> <hr/> <h2 id="what-forge-does-that-claude-code-doesnt">What Forge Does That Claude Code Doesn’t</h2> <p><strong><code class="language-plaintext highlighter-rouge">forge review --staged</code></strong> — Dedicated code review command. Analyzes your git diff before you commit and flags real issues: divide-by-zero bugs, unhandled exceptions, SQL injection risks, logic errors. Not a style linter. Actual bug detection.</p> <p><strong><code class="language-plaintext highlighter-rouge">forge commit</code></strong> — Generates a <a href="https://www.conventionalcommits.org/">conventional commit</a> message from your staged diff. Reads your actual changes, not a template. Saves the 30 seconds of message-writing on every commit.</p> <p><strong><code class="language-plaintext highlighter-rouge">forge review --branch main</code></strong> — Review the full diff between your branch and main. Useful before opening a PR.</p> <p><strong>Zero lock-in</strong> — Your data never leaves your machine in Ollama mode. No telemetry, no accounts, MIT licensed.</p> <hr/> <h2 id="optional-config">Optional Config</h2> <p>Drop a <code class="language-plaintext highlighter-rouge">~/.config/forge/config.yaml</code> if you want persistent settings:</p> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">provider</span><span class="pi">:</span> <span class="s">ollama</span> <span class="c1"># or "anthropic"</span>
<span class="na">ollama_model</span><span class="pi">:</span> <span class="s">qwen2.5-coder:7b</span>
<span class="na">max_tokens</span><span class="pi">:</span> <span class="m">8096</span>
<span class="na">auto_approve</span><span class="pi">:</span> <span class="kc">false</span> <span class="c1"># set true to skip tool-execution confirmations</span>
</code></pre></div></div> <p>Or use environment variables for one-off overrides:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">FORGE_PROVIDER</span><span class="o">=</span>anthropic <span class="nv">ANTHROPIC_API_KEY</span><span class="o">=</span>sk-ant-... forge run <span class="s2">"refactor the auth module to use JWT"</span>
</code></pre></div></div> <hr/> <h2 id="what-id-build-next">What I’d Build Next</h2> <p>A few things I deliberately left out of v1 that are on the roadmap:</p> <ul> <li><strong>MCP server support</strong> — The Model Context Protocol lets tools be served over a network. Forge could consume MCP servers the same way Claude Code does, giving it extensible tool sets without recompiling.</li> <li><strong>Project-level memory</strong> — Persisting a summary of the codebase between sessions so the model doesn’t start cold every time.</li> <li><strong>Parallel tool execution</strong> — Right now tools execute sequentially. Most are I/O-bound; running them in parallel goroutines would speed up multi-file tasks significantly.</li> </ul> <hr/> <h2 id="the-bigger-lesson">The Bigger Lesson</h2> <p>The leak wasn’t really about Claude Code. It was a reminder that agentic AI tools — however impressive they feel — are thin scaffolding around a powerful model. The scaffolding matters, but it’s not magic. It’s software. It can be studied, replicated, and improved.</p> <p>If you’re a developer who’s been treating these tools as black boxes, I’d encourage you to build one. Even a minimal implementation clarifies how they work in a way that no amount of using them ever will.</p> <p>Forge is MIT licensed. Read it, fork it, break it, make it better.</p> <p><strong><a href="https://github.com/sai-sridhar-repo-07/tarra-claw">github.com/sai-sridhar-repo-07/tarra-claw</a></strong></p> <hr/> <h2 id="references--further-reading">References &amp; Further Reading</h2> <h3 id="core-technologies-used">Core Technologies Used</h3> <ul> <li><strong><a href="https://github.com/charmbracelet/bubbletea">Bubble Tea</a></strong> — Elm-architecture TUI framework for Go. The cleanest way to build terminal interfaces.</li> <li><strong><a href="https://github.com/spf13/cobra">Cobra</a></strong> — The standard Go CLI framework. Powers <code class="language-plaintext highlighter-rouge">kubectl</code>, <code class="language-plaintext highlighter-rouge">hugo</code>, <code class="language-plaintext highlighter-rouge">gh</code>, and now Forge.</li> <li><strong><a href="https://github.com/anthropics/anthropic-sdk-go">Anthropic Go SDK</a></strong> — Official Go client for the Claude API with streaming support.</li> <li><strong><a href="https://ollama.com/">Ollama</a></strong> — Run LLMs locally on your machine. One command install, dozens of models.</li> </ul> <h3 id="understanding-agentic-ai-systems">Understanding Agentic AI Systems</h3> <ul> <li><strong><a href="https://www.anthropic.com/research/building-effective-agents">Building Effective Agents</a></strong> — Anthropic’s guide on when to use agents vs. simpler pipelines. Essential reading before designing any agentic system.</li> <li><strong><a href="https://arxiv.org/abs/2210.03629">ReAct: Synergizing Reasoning and Acting in Language Models</a></strong> — The paper that formalized the think-act-observe loop that underpins all modern AI agents.</li> <li><strong><a href="https://docs.anthropic.com/en/docs/build-with-claude/tool-use">Tool Use in Claude</a></strong> — Anthropic’s official documentation on how tool/function calling works at the API level.</li> </ul> <h3 id="go-for-ai-tooling">Go for AI Tooling</h3> <ul> <li><strong><a href="https://go.dev/doc/effective_go">Effective Go</a></strong> — The canonical guide to writing idiomatic Go. Read this before writing your first production Go tool.</li> <li><strong><a href="https://charm.sh/">Charm CLI Tools</a></strong> — The ecosystem behind Bubble Tea. Lipgloss for styling, Glamour for markdown rendering, Huh for forms. Everything you need for beautiful terminal UIs.</li> <li><strong><a href="https://pkg.go.dev/net/http">Server-Sent Events in Go</a></strong> — How to consume streaming HTTP responses (what all LLM APIs use) idiomatically in Go.</li> </ul> <h3 id="conventional-commits--developer-workflow">Conventional Commits &amp; Developer Workflow</h3> <ul> <li><strong><a href="https://www.conventionalcommits.org/en/v1.0.0/">Conventional Commits Specification</a></strong> — The spec Forge uses for <code class="language-plaintext highlighter-rouge">forge commit</code> output. Enables automated changelogs and semantic versioning.</li> <li><strong><a href="https://pre-commit.com/">pre-commit</a></strong> — Framework for running hooks before commits. Combine with <code class="language-plaintext highlighter-rouge">forge review --staged</code> for automated AI review on every commit.</li> </ul>]]></content><author><name></name></author><category term="systems"/><category term="Go"/><category term="CLI"/><category term="Ollama"/><category term="Claude"/><category term="AI Tools"/><category term="Open Source"/><category term="Developer Tools"/><category term="LLM"/><summary type="html"><![CDATA[After the Claude Code source leak revealed how agentic CLI tools are built, I reverse-engineered the core loop and rebuilt it from scratch in Go — with support for both Anthropic's API and fully offline Ollama models. Here's what I learned.]]></summary></entry></feed>