<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Transformers on lo0 — Tech Blog</title><link>https://blog.lo0.es/en/tags/transformers/</link><description>Recent content in Transformers on lo0 — Tech Blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Mon, 18 May 2026 10:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/en/tags/transformers/index.xml" rel="self" type="application/rss+xml"/><item><title>KV cache: the working memory that holds up LLM inference</title><link>https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/</link><pubDate>Mon, 18 May 2026 10:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>The KV cache is the &lt;strong>working memory&lt;/strong> a language model keeps during a conversation. Without it, every new token would force a recomputation of the whole conversation from the start, at a cost that is &lt;strong>quadratic&lt;/strong> in the length of the text. With it, the cost is linear, but in exchange the cache &lt;strong>lives in VRAM and grows with every token&lt;/strong>. In practice, it is not the model that limits how much context you can serve: it is the KV cache. On an RTX 4090 running Llama 3 8B, the model fits in 16 GB and barely leaves room for ~64 K tokens of cache in total, adding up all the simultaneous sessions. Understanding that number is the difference between promising a client &amp;ldquo;128 K of context&amp;rdquo; and actually delivering it.&lt;/p>
&lt;h2 id="you-are-here-deploy">You are here: Deploy&lt;/h2>
&lt;p>This post opens the series on LLM inference fundamentals. Within the &lt;a href="https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/">six-stage LLMOps pipeline&lt;/a> that articulates the whole system, the KV cache lives in the &lt;strong>Deploy&lt;/strong> stage: it is the piece that dictates how much traffic fits in your inference engine and, therefore, how much platform you can offer on top of it.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="you are here: Deploy">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#ff8a4c;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#kvm)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#kvm)}&lt;/style>
&lt;defs>&lt;marker id="kvm" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">You are here: DEPLOY · KV cache as the VRAM bottleneck&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box idle"/>&lt;text x="85" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" class="box idle"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" class="box idle"/>&lt;text x="335" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" class="box active"/>&lt;text x="460" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" class="box idle"/>&lt;text x="585" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" class="box idle"/>&lt;text x="710" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;path class="arr" d="M140,52 L155,52"/>&lt;path class="arr" d="M265,52 L280,52"/>&lt;path class="arr" d="M390,52 L405,52"/>&lt;path class="arr" d="M515,52 L530,52"/>&lt;path class="arr" d="M640,52 L655,52"/>
&lt;path class="cyc" d="M710,72 L710,82 L85,82 L85,72"/>
&lt;/svg>
&lt;/div>
&lt;h2 id="the-analogy-the-speaker-with-amnesia">The analogy: the speaker with amnesia&lt;/h2>
&lt;p>Imagine you are at a two-hour technical conference. Every time the speaker is about to say a new sentence, he &lt;strong>mentally rewinds the whole talk from the beginning&lt;/strong>, reassembles the thread, and only then carries on. His next sentence requires recalling the previous one; the one after that, the two before it; an hour in, every new word costs him an hour of recap. A conference like that would be materially impossible.&lt;/p>
&lt;p>Now imagine the same speaker with a notebook in which he jots down, as he goes, the two or three key ideas of each sentence: subject, object, link to what came before. Before each new sentence he glances at the notebook and continues. His next word only costs a glance at the notebook, not a rewind of the entire talk.&lt;/p>
&lt;p>That notebook, in a transformer, is called the &lt;strong>KV cache&lt;/strong>. Without it, conversational language models would be unworkable. With it, they are commercial products. But the notebook &lt;strong>weighs something&lt;/strong>, and understanding how much, where and why is what separates an inference infrastructure that works from one that falls over at the third concurrent client.&lt;/p>
&lt;h2 id="the-mechanism-itself-in-plain-terms">The mechanism itself (in plain terms)&lt;/h2>
&lt;p>A transformer generates text &lt;strong>one token at a time&lt;/strong>. To decide the next token, the model applies a mechanism called &lt;strong>attention&lt;/strong> over all previous tokens: it asks &amp;ldquo;which parts of the earlier context are relevant for predicting what comes next?&amp;rdquo;.&lt;/p>
&lt;p>Internally, each input token is projected into three vectors:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Q&lt;/strong> (Query): &amp;ldquo;what I am looking for&amp;rdquo;&lt;/li>
&lt;li>&lt;strong>K&lt;/strong> (Key): &amp;ldquo;what this token offers&amp;rdquo;&lt;/li>
&lt;li>&lt;strong>V&lt;/strong> (Value): &amp;ldquo;what information this token carries&amp;rdquo;&lt;/li>
&lt;/ul>
&lt;p>The attention of the current token against the context is computed by multiplying its &lt;strong>Q&lt;/strong> against the &lt;strong>K&lt;/strong> of all previous tokens, normalising with softmax, and weighting the corresponding &lt;strong>V&lt;/strong>. The result is a contextualised representation of the current token.&lt;/p>
&lt;div class="diagram" style="max-width: 720px; margin: 1.5rem auto;">
&lt;svg viewBox="0 0 720 260" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Diagram of the attention computation with Q, K, V">
&lt;style>
.box { fill: #f4f4f4; stroke: #333; stroke-width: 1.5; }
.box-q { fill: #ffe9d6; }
.box-k { fill: #d6eaff; }
.box-v { fill: #d9f5d6; }
.lbl { font: 600 13px sans-serif; fill: #222; }
.lbl-sm { font: 11px sans-serif; fill: #444; }
.arr { stroke: #444; stroke-width: 1.4; fill: none; marker-end: url(#ah); }
&lt;/style>
&lt;defs>
&lt;marker id="ah" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
&lt;path d="M0,0 L10,5 L0,10 z" fill="#444"/>
&lt;/marker>
&lt;/defs>
&lt;text x="360" y="22" text-anchor="middle" class="lbl">Attention computation for token N&lt;/text>
&lt;rect x="40" y="60" width="120" height="40" rx="6" class="box box-q"/>
&lt;text x="100" y="85" text-anchor="middle" class="lbl">Q (token N)&lt;/text>
&lt;text x="100" y="115" text-anchor="middle" class="lbl-sm">"what I seek"&lt;/text>
&lt;rect x="280" y="60" width="160" height="40" rx="6" class="box box-k"/>
&lt;text x="360" y="85" text-anchor="middle" class="lbl">K (tokens 1..N)&lt;/text>
&lt;text x="360" y="115" text-anchor="middle" class="lbl-sm">from the cache&lt;/text>
&lt;rect x="560" y="60" width="120" height="40" rx="6" class="box box-v"/>
&lt;text x="620" y="85" text-anchor="middle" class="lbl">V (tokens 1..N)&lt;/text>
&lt;text x="620" y="115" text-anchor="middle" class="lbl-sm">from the cache&lt;/text>
&lt;path class="arr" d="M160,80 L280,80"/>
&lt;path class="arr" d="M440,80 L560,80"/>
&lt;p>&lt;text x="220" y="74" text-anchor="middle" class="lbl-sm">Q·Kᵀ → softmax&lt;/text>
&lt;text x="500" y="74" text-anchor="middle" class="lbl-sm">× V&lt;/text>&lt;/p>
&lt;rect x="240" y="170" width="240" height="44" rx="6" class="box"/>
&lt;text x="360" y="197" text-anchor="middle" class="lbl">representation of token N&lt;/text>
&lt;path class="arr" d="M620,100 C620,150 480,150 480,170"/>
&lt;path class="arr" d="M100,100 C100,150 240,150 240,170"/>
&lt;/svg>
&lt;/div>
&lt;p>Here is the key point: to predict token N, I only need the &lt;strong>new Q&lt;/strong> (that of token N) and the &lt;strong>K, V of all previous tokens&lt;/strong>. The K and V of tokens 1..N-1 have not changed since the previous iteration. Recomputing them would be throwing work away.&lt;/p>
&lt;p>&lt;strong>The KV cache is exactly that: the memory that holds the K and V of every token already processed, in every layer of the model, so they never have to be recomputed.&lt;/strong>&lt;/p>
&lt;h2 id="why-it-exists-the-quadratic-cost-without-it">Why it exists: the quadratic cost without it&lt;/h2>
&lt;p>Generating a text of N tokens involves N steps. At step &lt;code>i&lt;/code>, attention is computed over &lt;code>i&lt;/code> previous tokens. Without a cache, at every step you recompute the K, V of the &lt;code>i-1&lt;/code> previous tokens &lt;strong>plus&lt;/strong> those of the new one. The total count of attention computations grows as:&lt;/p>
$$\sum_{i=1}^{N} i = \frac{N(N+1)}{2} \approx \frac{N^2}{2}$$
&lt;p>With a KV cache, you only process the new token at each step: cost &lt;strong>linear in N&lt;/strong>.&lt;/p>
&lt;div class="diagram" style="max-width: 720px; margin: 1.5rem auto;">
&lt;svg viewBox="0 0 720 320" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Comparison of linear vs quadratic cost">
&lt;style>
.kv-ax { stroke: currentColor; stroke-width: 1.5; opacity: 0.4; fill: none; }
.kv-grid { stroke: currentColor; stroke-width: 1; stroke-dasharray: 4,4; opacity: 0.15; fill: none; }
.kv-lin { stroke: #2a9d8f; stroke-width: 3; fill: none; }
.kv-quad { stroke: #e76f51; stroke-width: 3; fill: none; }
.kv-lbl { font: 600 13px sans-serif; fill: currentColor; }
.kv-sm { font: 11px sans-serif; fill: currentColor; opacity: 0.6; }
.kv-note { font: italic 10px sans-serif; fill: currentColor; opacity: 0.45; }
.kv-tlin { fill: #2a9d8f; font: 700 12px sans-serif; }
.kv-tqud { fill: #e76f51; font: 700 12px sans-serif; }
&lt;/style>
&lt;!-- Title -->
&lt;p>&lt;text x="380" y="22" text-anchor="middle" class="kv-lbl">Cumulative compute to generate N tokens&lt;/text>
&lt;text x="380" y="38" text-anchor="middle" class="kv-note">(schematic scale — exact data is in the table)&lt;/text>&lt;/p>
&lt;!-- Axes -->
&lt;line class="kv-ax" x1="80" y1="260" x2="680" y2="260"/>
&lt;line class="kv-ax" x1="80" y1="55" x2="80" y2="260"/>
&lt;!-- Axis labels -->
&lt;p>&lt;text x="380" y="295" text-anchor="middle" class="kv-sm">tokens generated (N)&lt;/text>
&lt;text x="25" y="158" text-anchor="middle" class="kv-sm" transform="rotate(-90 25 158)">attention operations&lt;/text>&lt;/p>
&lt;!-- X ticks -->
&lt;p>&lt;text x="80" y="277" text-anchor="middle" class="kv-sm">0&lt;/text>
&lt;text x="230" y="277" text-anchor="middle" class="kv-sm">1K&lt;/text>
&lt;text x="380" y="277" text-anchor="middle" class="kv-sm">2K&lt;/text>
&lt;text x="530" y="277" text-anchor="middle" class="kv-sm">3K&lt;/text>
&lt;text x="680" y="277" text-anchor="middle" class="kv-sm">4K&lt;/text>&lt;/p>
&lt;!-- Grid -->
&lt;line class="kv-grid" x1="80" y1="210" x2="680" y2="210"/>
&lt;line class="kv-grid" x1="80" y1="160" x2="680" y2="160"/>
&lt;line class="kv-grid" x1="80" y1="110" x2="680" y2="110"/>
&lt;line class="kv-grid" x1="80" y1="60" x2="680" y2="60"/>
&lt;!-- Linear curve (with KV cache): O(N) — gentle, visible slope -->
&lt;path class="kv-lin" d="M80,260 L680,215"/>
&lt;!-- Quadratic curve (without KV cache): O(N²) — starts flat, rises fast -->
&lt;path class="kv-quad" d="M80,260 C250,258 420,200 680,58"/>
&lt;!-- Curve labels -->
&lt;p>&lt;text x="570" y="208" class="kv-tlin">with KV cache&lt;/text>
&lt;text x="570" y="222" class="kv-tlin">O(N) — linear&lt;/text>&lt;/p>
&lt;p>&lt;text x="440" y="95" class="kv-tqud">without KV cache&lt;/text>
&lt;text x="440" y="109" class="kv-tqud">O(N²) — quadratic&lt;/text>
&lt;/svg>&lt;/p>
&lt;/div>
&lt;p>The concrete numbers are devastating:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:right">Tokens generated&lt;/th>
&lt;th style="text-align:right">Without KV cache (operations)&lt;/th>
&lt;th style="text-align:right">With KV cache&lt;/th>
&lt;th style="text-align:right">Ratio&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">8,256&lt;/td>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">64×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">1,024&lt;/td>
&lt;td style="text-align:right">524,800&lt;/td>
&lt;td style="text-align:right">1,024&lt;/td>
&lt;td style="text-align:right">512×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">4,096&lt;/td>
&lt;td style="text-align:right">8,390,656&lt;/td>
&lt;td style="text-align:right">4,096&lt;/td>
&lt;td style="text-align:right">2,048×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">32,768&lt;/td>
&lt;td style="text-align:right">536,887,296&lt;/td>
&lt;td style="text-align:right">32,768&lt;/td>
&lt;td style="text-align:right">16,384×&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>At 32 K tokens, &lt;strong>the cache saves you four orders of magnitude&lt;/strong> of compute. This is not an optimisation: it is what makes conversational inference possible.&lt;/p>
&lt;h2 id="the-price-how-heavy-the-backpack-is">The price: how heavy the backpack is&lt;/h2>
&lt;p>The KV cache is paid for in VRAM. The formula, per &lt;strong>sequence&lt;/strong>, is:&lt;/p>
&lt;pre tabindex="0">&lt;code>KV_size = 2 · n_layers · n_kv_heads · head_dim · context_len · bytes_per_param
↑
K and V
&lt;/code>&lt;/pre>&lt;p>Per &lt;strong>token&lt;/strong> (dropping &lt;code>context_len&lt;/code>), it is a constant of the model itself. Let us look at real numbers:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Model&lt;/th>
&lt;th style="text-align:right">n_layers&lt;/th>
&lt;th style="text-align:right">n_kv_heads&lt;/th>
&lt;th style="text-align:right">head_dim&lt;/th>
&lt;th style="text-align:right">Bytes/token (BF16)&lt;/th>
&lt;th style="text-align:right">GB at 8 K ctx&lt;/th>
&lt;th style="text-align:right">GB at 32 K&lt;/th>
&lt;th style="text-align:right">GB at 128 K&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Llama 3 8B (hypothetical MHA)&lt;/td>
&lt;td style="text-align:right">32&lt;/td>
&lt;td style="text-align:right">32&lt;/td>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">524,288&lt;/td>
&lt;td style="text-align:right">4.00&lt;/td>
&lt;td style="text-align:right">16.00&lt;/td>
&lt;td style="text-align:right">64.00&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Llama 3 8B (real GQA)&lt;/strong>&lt;/td>
&lt;td style="text-align:right">32&lt;/td>
&lt;td style="text-align:right">8&lt;/td>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">131,072&lt;/td>
&lt;td style="text-align:right">&lt;strong>1.00&lt;/strong>&lt;/td>
&lt;td style="text-align:right">&lt;strong>4.00&lt;/strong>&lt;/td>
&lt;td style="text-align:right">&lt;strong>16.00&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Llama 3 70B (GQA)&lt;/td>
&lt;td style="text-align:right">80&lt;/td>
&lt;td style="text-align:right">8&lt;/td>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">327,680&lt;/td>
&lt;td style="text-align:right">2.50&lt;/td>
&lt;td style="text-align:right">10.00&lt;/td>
&lt;td style="text-align:right">40.00&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Qwen3 8B (GQA)&lt;/td>
&lt;td style="text-align:right">36&lt;/td>
&lt;td style="text-align:right">8&lt;/td>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">147,456&lt;/td>
&lt;td style="text-align:right">1.12&lt;/td>
&lt;td style="text-align:right">4.50&lt;/td>
&lt;td style="text-align:right">18.00&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Mistral 7B (GQA)&lt;/td>
&lt;td style="text-align:right">32&lt;/td>
&lt;td style="text-align:right">8&lt;/td>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">131,072&lt;/td>
&lt;td style="text-align:right">1.00&lt;/td>
&lt;td style="text-align:right">4.00&lt;/td>
&lt;td style="text-align:right">16.00&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Two immediate readings:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Without GQA there is no 128 K worth talking about.&lt;/strong> A Llama 3 8B with classic multi-head attention would need 64 GB of KV cache alone for a single sequence with 128 K tokens. In other words, &lt;strong>it does not fit on any consumer GPU&lt;/strong>. That is why Meta, Mistral and the rest adopted Grouped Query Attention.&lt;/li>
&lt;li>&lt;strong>The KV cache can be larger than the model.&lt;/strong> Llama 3 8B in BF16 takes ~16 GB. With 128 K of context, its cache is another 16 GB. A single session ties with the model in VRAM.&lt;/li>
&lt;/ol>
&lt;div class="diagram" style="max-width: 720px; margin: 1.5rem auto;">
&lt;svg viewBox="0 0 720 280" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Growth of the KV cache with context length">
&lt;style>
.ax { stroke: #333; stroke-width: 1.5; }
.grid { stroke: #ddd; stroke-width: 1; stroke-dasharray: 3,3; }
.l8b { stroke: #2a9d8f; stroke-width: 2.5; fill: none; }
.l70b { stroke: #e76f51; stroke-width: 2.5; fill: none; }
.lq8 { stroke: #6a4c93; stroke-width: 2.5; fill: none; stroke-dasharray: 5,3; }
.lim { stroke: #c1121f; stroke-width: 1.5; stroke-dasharray: 4,4; }
.lbl { font: 600 13px sans-serif; fill: #222; }
.lbl-sm { font: 11px sans-serif; fill: #555; }
.tag { font: 600 11px sans-serif; }
&lt;/style>
&lt;text x="360" y="20" text-anchor="middle" class="lbl">KV cache (GB) vs context length (1 sequence, BF16)&lt;/text>
&lt;line class="ax" x1="80" y1="240" x2="680" y2="240"/>
&lt;line class="ax" x1="80" y1="40" x2="80" y2="240"/>
&lt;line class="grid" x1="80" y1="190" x2="680" y2="190"/>
&lt;line class="grid" x1="80" y1="140" x2="680" y2="140"/>
&lt;line class="grid" x1="80" y1="90" x2="680" y2="90"/>
&lt;p>&lt;text x="75" y="244" text-anchor="end" class="lbl-sm">0&lt;/text>
&lt;text x="75" y="194" text-anchor="end" class="lbl-sm">10&lt;/text>
&lt;text x="75" y="144" text-anchor="end" class="lbl-sm">20&lt;/text>
&lt;text x="75" y="94" text-anchor="end" class="lbl-sm">30&lt;/text>
&lt;text x="75" y="44" text-anchor="end" class="lbl-sm">40 GB&lt;/text>&lt;/p>
&lt;p>&lt;text x="80" y="258" text-anchor="middle" class="lbl-sm">0&lt;/text>
&lt;text x="180" y="258" text-anchor="middle" class="lbl-sm">8K&lt;/text>
&lt;text x="305" y="258" text-anchor="middle" class="lbl-sm">32K&lt;/text>
&lt;text x="430" y="258" text-anchor="middle" class="lbl-sm">64K&lt;/text>
&lt;text x="680" y="258" text-anchor="middle" class="lbl-sm">128K&lt;/text>&lt;/p>
&lt;!-- Available VRAM limit on an RTX 4090 (~8 GB free after the model) -->
&lt;line class="lim" x1="80" y1="200" x2="680" y2="200"/>
&lt;text x="680" y="196" text-anchor="end" class="tag" fill="#c1121f">≈ VRAM free after loading an 8B on a 4090&lt;/text>
&lt;!-- Llama 3 8B GQA: linear, 1 GB @8K, 16 GB @128K -->
&lt;path class="l8b" d="M80,240 L180,235 L305,220 L430,200 L680,160"/>
&lt;!-- Qwen3 8B GQA -->
&lt;path class="lq8" d="M80,240 L180,234 L305,217 L430,194 L680,150"/>
&lt;!-- Llama 3 70B GQA -->
&lt;path class="l70b" d="M80,240 L180,228 L305,190 L430,140 L680,40"/>
&lt;p>&lt;text x="690" y="160" class="tag" fill="#2a9d8f">Llama 3 8B&lt;/text>
&lt;text x="690" y="148" class="tag" fill="#6a4c93">Qwen3 8B&lt;/text>
&lt;text x="690" y="42" class="tag" fill="#e76f51">Llama 3 70B&lt;/text>
&lt;/svg>&lt;/p>
&lt;/div>
&lt;p>The dashed red line marks the realistic VRAM available on an RTX 4090 after loading the model. &lt;strong>Any model whose curve crosses that line will not be able to serve that context&lt;/strong> without additional strategies: cache quantisation, offload, partitioning.&lt;/p>
&lt;h2 id="inference-is-memory-bound-not-compute-bound">Inference is memory-bound, not compute-bound&lt;/h2>
&lt;p>There is a common misconception: thinking that &amp;ldquo;fast GPU = fast inference&amp;rdquo;. In the regime where inference services with a KV cache actually operate, &lt;strong>what gets measured is memory bandwidth&lt;/strong>. Every new token requires reading the K and V of all previous tokens from HBM. The compute is modest; the data movement is massive.&lt;/p>
&lt;p>That is why an H100 SXM (3.35 TB/s of HBM3) can be 2-3× faster than an A100 (1.55-2 TB/s) &lt;strong>without the clock frequency or the core count fully explaining the difference&lt;/strong>. Bandwidth explains it.&lt;/p>
&lt;p>And that is also why offers of &amp;ldquo;cheap GPUs with plenty of VRAM but slow HBM&amp;rdquo; (some variants with GDDR6 or LPDDR5) disappoint in inference with long contexts: they have room to store the cache but take forever to re-read it.&lt;/p>
&lt;h2 id="tricks-to-make-the-notebook-thinner">Tricks to make the notebook thinner&lt;/h2>
&lt;p>Three techniques, in chronological order, have progressively flattened the size of the KV cache:&lt;/p>
&lt;p>&lt;strong>Multi-Head Attention (MHA).&lt;/strong> The original transformer design (Vaswani et al., 2017). Every attention head has its own K and V. Expensive in cache but theoretically maximal in expressiveness. It is what models had until around 2023.&lt;/p>
&lt;p>&lt;strong>Multi-Query Attention (MQA).&lt;/strong> A single K and V shared by all heads. It reduces the cache by a factor of &lt;code>n_heads&lt;/code>. It works reasonably well but degrades generation quality on some benchmarks.&lt;/p>
&lt;p>&lt;strong>Grouped Query Attention (GQA).&lt;/strong> The middle ground that won. Heads are grouped: in Llama 3 8B, 32 query heads share K, V in groups of 4, giving 8 KV groups. It cuts the cache 4× relative to MHA with almost identical quality. It has been the de facto standard since 2024.&lt;/p>
&lt;p>&lt;strong>Multi-Head Latent Attention (MLA).&lt;/strong> DeepSeek-V2/V3&amp;rsquo;s innovation: instead of storing K, V per head, it compresses the state into a smaller latent vector and projects out to K, V on the fly. The cache can come down to 70 bytes/token, two orders of magnitude less than GQA. It is the main reason DeepSeek-V3 (671 B parameters, 37 B active) is servable on affordable infrastructure.&lt;/p>
&lt;div class="diagram" style="max-width: 640px; margin: 1.5rem auto;">
&lt;svg viewBox="0 0 640 200" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="KV cache reduction by technique">
&lt;style>
.bar { stroke: #333; stroke-width: 1; }
.b-mha { fill: #e76f51; }
.b-gqa { fill: #f4a261; }
.b-mqa { fill: #e9c46a; }
.b-mla { fill: #2a9d8f; }
.lbl { font: 600 13px sans-serif; fill: #222; }
.lbl-sm{ font: 11px sans-serif; fill: #444; }
&lt;/style>
&lt;text x="320" y="20" text-anchor="middle" class="lbl">KB of cache per token (Llama 3 8B equivalent, BF16)&lt;/text>
&lt;rect x="200" y="40" width="380" height="22" class="bar b-mha"/>
&lt;text x="170" y="56" text-anchor="end" class="lbl-sm">MHA (32 KV heads)&lt;/text>
&lt;text x="595" y="56" class="lbl-sm">512 KB&lt;/text>
&lt;rect x="200" y="76" width="95" height="22" class="bar b-gqa"/>
&lt;text x="170" y="92" text-anchor="end" class="lbl-sm">GQA (8 KV heads)&lt;/text>
&lt;text x="310" y="92" class="lbl-sm">128 KB&lt;/text>
&lt;rect x="200" y="112" width="12" height="22" class="bar b-mqa"/>
&lt;text x="170" y="128" text-anchor="end" class="lbl-sm">MQA (1 KV head)&lt;/text>
&lt;text x="225" y="128" class="lbl-sm">16 KB&lt;/text>
&lt;rect x="200" y="148" width="3" height="22" class="bar b-mla"/>
&lt;text x="170" y="164" text-anchor="end" class="lbl-sm">MLA (DeepSeek-V3)&lt;/text>
&lt;text x="215" y="164" class="lbl-sm">~0.5 KB (real V3)&lt;/text>
&lt;/svg>
&lt;/div>
&lt;blockquote>
&lt;p>&lt;strong>Note:&lt;/strong> the MLA bar is illustrative, using typical values published by DeepSeek; the exact implementation depends on the latent size. What matters is the order of magnitude.&lt;/p>
&lt;/blockquote>
&lt;p>On top of this comes a fourth, orthogonal technique: &lt;strong>quantising the cache&lt;/strong> to FP8, INT8 or even INT4. vLLM and TensorRT-LLM already support it in production. Going from BF16 (2 bytes) to FP8 (1 byte) &lt;strong>halves the cache&lt;/strong> at a small cost in quality. Going to INT4 divides it by four, at a somewhat higher cost.&lt;/p>
&lt;h2 id="the-next-dragon-fragmentation">The next dragon: fragmentation&lt;/h2>
&lt;p>So far we have talked about the cache as if it were a contiguous block. In practice, an inference server handles &lt;strong>dozens of simultaneous sessions&lt;/strong>, each with its own cache growing at a different rate. Naive allocation, reserving the maximum possible per session, &lt;strong>wastes between 60 % and 80 % of the VRAM&lt;/strong> according to the original PagedAttention paper.&lt;/p>
&lt;div class="diagram" style="max-width: 720px; margin: 1.5rem auto;">
&lt;svg viewBox="0 0 720 240" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="KV cache fragmentation: naive vs PagedAttention">
&lt;style>
.used { fill: #2a9d8f; stroke: #1a6e63; stroke-width: 1; }
.free { fill: #f0e7d8; stroke: #aaa; stroke-width: 1; }
.blk { stroke: #555; stroke-width: 0.5; }
.lbl { font: 600 13px sans-serif; fill: #222; }
.lbl-sm { font: 11px sans-serif; fill: #444; }
&lt;/style>
&lt;text x="180" y="22" text-anchor="middle" class="lbl">Naive allocation (contiguous)&lt;/text>
&lt;text x="540" y="22" text-anchor="middle" class="lbl">PagedAttention (blocks)&lt;/text>
&lt;!-- Naive: 4 sessions reserve the maximum, use little -->
&lt;p>&lt;text x="30" y="60" class="lbl-sm">session A&lt;/text>
&lt;rect x="90" y="48" width="50" height="18" class="used"/>
&lt;rect x="140" y="48" width="180" height="18" class="free"/>&lt;/p>
&lt;p>&lt;text x="30" y="92" class="lbl-sm">session B&lt;/text>
&lt;rect x="90" y="80" width="25" height="18" class="used"/>
&lt;rect x="115" y="80" width="205" height="18" class="free"/>&lt;/p>
&lt;p>&lt;text x="30" y="124" class="lbl-sm">session C&lt;/text>
&lt;rect x="90" y="112" width="100" height="18" class="used"/>
&lt;rect x="190" y="112" width="130" height="18" class="free"/>&lt;/p>
&lt;p>&lt;text x="30" y="156" class="lbl-sm">session D&lt;/text>
&lt;rect x="90" y="144" width="35" height="18" class="used"/>
&lt;rect x="125" y="144" width="195" height="18" class="free"/>&lt;/p>
&lt;p>&lt;text x="180" y="190" text-anchor="middle" class="lbl-sm">→ ~70 % of VRAM reserved and empty&lt;/text>&lt;/p>
&lt;!-- PagedAttention: small blocks, dense occupancy -->
&lt;g transform="translate(400,40)">
&lt;!-- 8 blocks x 5 rows -->
&lt;g>
&lt;!-- row 1 -->
&lt;rect x="0" y="0" width="30" height="20" class="used blk"/>
&lt;rect x="30" y="0" width="30" height="20" class="used blk"/>
&lt;rect x="60" y="0" width="30" height="20" class="used blk"/>
&lt;rect x="90" y="0" width="30" height="20" class="used blk"/>
&lt;rect x="120" y="0" width="30" height="20" class="used blk"/>
&lt;rect x="150" y="0" width="30" height="20" class="used blk"/>
&lt;rect x="180" y="0" width="30" height="20" class="used blk"/>
&lt;rect x="210" y="0" width="30" height="20" class="used blk"/>
&lt;pre>&lt;code> &amp;lt;rect x=&amp;quot;0&amp;quot; y=&amp;quot;22&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;used blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;30&amp;quot; y=&amp;quot;22&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;used blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;60&amp;quot; y=&amp;quot;22&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;used blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;90&amp;quot; y=&amp;quot;22&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;used blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;120&amp;quot; y=&amp;quot;22&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;used blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;150&amp;quot; y=&amp;quot;22&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;used blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;180&amp;quot; y=&amp;quot;22&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;free blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;210&amp;quot; y=&amp;quot;22&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;free blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;0&amp;quot; y=&amp;quot;44&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;used blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;30&amp;quot; y=&amp;quot;44&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;used blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;60&amp;quot; y=&amp;quot;44&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;used blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;90&amp;quot; y=&amp;quot;44&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;free blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;120&amp;quot; y=&amp;quot;44&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;free blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;150&amp;quot; y=&amp;quot;44&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;free blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;180&amp;quot; y=&amp;quot;44&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;free blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;210&amp;quot; y=&amp;quot;44&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;free blk&amp;quot;/&amp;gt;
&amp;lt;/g&amp;gt;
&lt;/code>&lt;/pre>
&lt;/g>
&lt;text x="540" y="190" text-anchor="middle" class="lbl-sm">→ &amp;lt; 4 % waste (vLLM paper)&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>&lt;strong>PagedAttention&lt;/strong>, the idea from Kwon et al. (2023) that gave rise to vLLM, solves this by borrowing a technique from operating systems: divide the VRAM into small &lt;strong>blocks&lt;/strong> (typically 16 tokens) and keep a &lt;strong>page table&lt;/strong> mapping logical to physical blocks per session. A session no longer reserves one huge contiguous block: it grows one block at a time, and the blocks can be scattered across the VRAM. The result is an effective occupancy of 90 % instead of 30 %, and therefore &lt;strong>2-4× more aggregate throughput&lt;/strong> on the same hardware.&lt;/p>
&lt;p>PagedAttention deserves an article of its own. I am noting it down for the next one.&lt;/p>
&lt;h2 id="applied-to-generic-on-premise-hardware">Applied to generic on-premise hardware&lt;/h2>
&lt;p>Let us come down to concrete cases.&lt;/p>
&lt;h3 id="case-1--rtx-4090-24-gb-ada-lovelace">Case 1 — RTX 4090 (24 GB, Ada Lovelace)&lt;/h3>
&lt;p>A typical configuration with Qwen3-8B BF16:&lt;/p>
&lt;pre tabindex="0">&lt;code>Model in BF16: ~16 GB
Activations + overhead: ~2 GB
VRAM available for KV cache: ~6 GB (with margin)
&lt;/code>&lt;/pre>&lt;p>At 144 KB/token (Qwen3-8B GQA), that comes to &lt;strong>~43 K total tokens of cache&lt;/strong> distributed across all simultaneous sessions. In practice:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:right">Concurrency&lt;/th>
&lt;th style="text-align:right">Maximum context per session&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:right">1&lt;/td>
&lt;td style="text-align:right">32,768&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">4&lt;/td>
&lt;td style="text-align:right">8,192&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">8&lt;/td>
&lt;td style="text-align:right">4,096&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">16&lt;/td>
&lt;td style="text-align:right">2,048&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>If you need to advertise &amp;ldquo;we support 32 K of context&amp;rdquo; with concurrency of 4 or more, you have to &lt;strong>quantise the cache&lt;/strong> (FP8 brings it down to 72 KB/token, doubling capacity) or &lt;strong>move up the model range&lt;/strong> (a 4B with GQA and a quantised cache would have room to spare).&lt;/p>
&lt;h3 id="case-2--cluster-of-4h100-320-gb-total-nvlink">Case 2 — Cluster of 4×H100 (320 GB total, NVLink)&lt;/h3>
&lt;p>With tensor parallel = 4 and Llama 3 70B BF16:&lt;/p>
&lt;pre tabindex="0">&lt;code>Model in BF16: ~140 GB (35 GB/GPU)
vLLM overhead per GPU: ~2 GB
VRAM free for KV per GPU: ~43 GB → ~172 GB aggregate
&lt;/code>&lt;/pre>&lt;p>At 320 KB/token (Llama 3 70B GQA), that comes to &lt;strong>~537 K total tokens of cache&lt;/strong>. Ample margin for long contexts with high concurrency:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:right">Concurrency&lt;/th>
&lt;th style="text-align:right">Maximum context per session&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:right">4&lt;/td>
&lt;td style="text-align:right">134,000&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">16&lt;/td>
&lt;td style="text-align:right">33,500&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">64&lt;/td>
&lt;td style="text-align:right">8,375&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>For DeepSeek-V3 671 B with MLA the economics change radically, because the cache is ~100× thinner. What limits you is no longer the cache but the VRAM of the model itself (quantised to FP8 it is ~335 GB, so it fits on 4×H100 with room for the KV cache).&lt;/p>
&lt;h3 id="operational-implications">Operational implications&lt;/h3>
&lt;p>Three observations we repeat in every consulting engagement:&lt;/p>
&lt;p>First, &lt;strong>the maximum context a model advertises is not the one you can serve on your hardware&lt;/strong>. Llama 3 8B &amp;ldquo;supports&amp;rdquo; 128 K, but on a 4090 with 4 simultaneous sessions your effective context is ~8 K. It is trivial to check before promising it to a client.&lt;/p>
&lt;p>Second, &lt;strong>quantising the KV cache is one of the optimisations with the best cost/benefit ratio in the ENS context&lt;/strong>. It does not touch the weights, it does not affect audit reproducibility, and it doubles capacity. vLLM supports it via &lt;code>--kv-cache-dtype fp8&lt;/code>.&lt;/p>
&lt;p>Third, &lt;strong>if the SLAs dictate long contexts with many concurrent users, GQA is necessary but not sufficient&lt;/strong>. In the medium term you need to look at models with MLA or attention variants with compression.&lt;/p>
&lt;h2 id="what-we-have-not-covered-upcoming-articles">What we have not covered (upcoming articles)&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>PagedAttention&lt;/strong> and its implementation in vLLM: blocks, page table, eviction.&lt;/li>
&lt;li>&lt;strong>Prefix caching&lt;/strong>: when several requests share the system prompt, there is no need to recompute the K, V of the common part.&lt;/li>
&lt;li>&lt;strong>Speculative decoding&lt;/strong> and its interaction with the cache.&lt;/li>
&lt;li>&lt;strong>Cache offloading&lt;/strong>: moving cold blocks to RAM or NVMe, a key technique for contexts above 1 M.&lt;/li>
&lt;/ul>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/">The six-stage LLMOps pipeline&lt;/a> — the master map of the system in production, in which the Deploy stage is one box out of six. This post goes into one of the critical decisions inside Deploy.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/pagedattention-vllm-block-manager/">Inside PagedAttention: blocks, page table, eviction and the state of the art of the KV cache in 2026&lt;/a> — a theoretical deep dive at block level plus a survey of derived optimisations (vAttention, EvicPress, RadixAttention, speculative decoding). It continues this post from the academic theory.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/continuous-fine-tuning-production-real-traffic-deployed-adapter/">Continuous fine-tuning in production: from real traffic to the deployed adapter&lt;/a> — how the loop between inference and incremental training closes on the same stack (vLLM + Postgres), with VRAM budgets that explicitly include the KV cache during eval.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/disaggregated-serving-prefill-decode-specialised-pods/">Disaggregated serving: prefill and decode in specialised pods&lt;/a> — the KV cache stops being a private GPU buffer and becomes the artefact transferred between pods. Here the cache size formula determines the economics of the transfer.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/gpu-cluster-as-multi-tenant-platform/">The GPU cluster as a multi-tenant platform&lt;/a> — how the cluster becomes a service with tenants, gateway, quotas and isolation. That is where the KV cache stops being only a performance resource and becomes a platform matter.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/vllm-kubernetes-llm-inference-that-scales/">vLLM on Kubernetes: the LLM inference piece that does scale&lt;/a> — the engine that materialises everything discussed here, deployed on K8s with tensor parallel and autoscaling.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/quantization-llm-inference-fp8-int4-gguf/">Quantization for LLM inference: FP8, INT4 and GGUF&lt;/a> — KV cache quantisation (&lt;code>--kv-cache-dtype=fp8/int4&lt;/code>), mentioned here as the fourth orthogonal technique, is taken apart there with the maths, the formats and the measurable loss.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/speculative-decoding-fundamentals-maths-state-of-play/">Speculative decoding: the secretary who anticipates what the boss will say&lt;/a> — the memory-bound decode regime caused by the KV cache is exactly what speculative decoding exploits: a forward pass with γ tokens costs almost the same as one with a single token.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/flashattention-librarian-io-awareness-async-blackwell-asymmetry/">FlashAttention v1/v2/v3/v4: the librarian who never clears the desk&lt;/a> — the kernel that sweeps the KV cache against Q on every iteration without materialising the N×N matrix. The compute layer underneath the cache.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/moe-inference-fundamentals-expert-parallel-deepseek-economics/">MoE inference: the call centre with 256 specialists&lt;/a> — attention is still dense in every MoE of 2026 (Mixtral, DeepSeek, Qwen3, Llama 4, Kimi K2), so the KV cache keeps its shape; DeepSeek&amp;rsquo;s MLA is the orthogonal optimisation that compresses it ~10× to make long context viable on modest clusters.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/continuous-batching-orca-vllm-chunked-prefill-goodput/">Continuous batching&lt;/a> — the iterative scheduler manages the dynamic allocation of the KV cache across requests; without PagedAttention, continuous batching would fragment the HBM, and without continuous batching the KV cache would be underused because of padding.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/capacity-planning-on-premise-llm-inference/">Capacity planning for on-premise LLM inference&lt;/a> — the KV cache is the dominant component of the VRAM budget when sizing a cluster from an SLO; the spreadsheet is built there step by step.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/optimising-prefill-vllm-knobs-ttft-will-not-forgive/">Optimising prefill in vLLM&lt;/a> — the four concrete knobs (chunked prefill, prefix caching, FP8 KV, max-model-len) that turn KV cache theory into production parameters for the RTX 4090 and the L40.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/optimising-decode-vllm-squeezing-every-token-small-hardware/">Optimising decode in vLLM&lt;/a> — how &lt;code>--gpu-memory-utilization&lt;/code>, speculative decoding and an FP8 KV cache combine to squeeze small hardware during the generation phase.&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>Vaswani et al., &lt;em>Attention Is All You Need&lt;/em> (NeurIPS 2017) — foundational transformer paper.&lt;/li>
&lt;li>Ainslie et al., &lt;em>GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints&lt;/em> (EMNLP 2023).&lt;/li>
&lt;li>Kwon et al., &lt;em>Efficient Memory Management for Large Language Model Serving with PagedAttention&lt;/em> (SOSP 2023) — original vLLM paper.&lt;/li>
&lt;li>DeepSeek-AI, &lt;em>DeepSeek-V2 Technical Report&lt;/em> (2024) — introduction of Multi-Head Latent Attention.&lt;/li>
&lt;li>Official vLLM documentation: &lt;a href="https://docs.vllm.ai/">https://docs.vllm.ai/&lt;/a>.&lt;/li>
&lt;li>Llama 3 model card (Meta): GQA specs, n_layers, n_kv_heads.&lt;/li>
&lt;/ul></description></item></channel></rss>