<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Speculative-Decoding on lo0 — Tech Blog</title><link>https://blog.lo0.es/en/tags/speculative-decoding/</link><description>Recent content in Speculative-Decoding on lo0 — Tech Blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Tue, 09 Jun 2026 01:40:00 +0000</lastBuildDate><atom:link href="https://blog.lo0.es/en/tags/speculative-decoding/index.xml" rel="self" type="application/rss+xml"/><item><title>Self-speculative decoding: the model that gets ahead of itself</title><link>https://blog.lo0.es/en/posts/self-speculative-decoding-model-gets-ahead-of-itself/</link><pubDate>Tue, 09 Jun 2026 01:40:00 +0000</pubDate><guid>https://blog.lo0.es/en/posts/self-speculative-decoding-model-gets-ahead-of-itself/</guid><description>&lt;blockquote>
&lt;p>This post is the direct companion to &lt;a href="https://blog.lo0.es/en/posts/speculative-decoding-fundamentals-maths-state-of-play/">Speculative decoding: the secretary who types ahead&lt;/a>. There, draft and target are two different models; here they are the same model at two depths. Read that one first: we take rejection sampling, the &lt;code>1/(1-α)&lt;/code> ceiling and the speedup formula as known, and all we change here is what the draft is.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Classic speculative decoding demands a pair: a cheap &lt;em>draft&lt;/em> model proposes γ tokens and an expensive &lt;em>target&lt;/em> verifies them in a single parallel forward pass. On large models the draft can be 1 % of the target and fit comfortably. On &lt;strong>small models&lt;/strong> (SLMs, 1B–8B) that recipe breaks on two fronts: a draft that is 1/10 of a 3B is a 0.3B that barely gets anything right (α collapses), and loading a second model, however small, doubles the pieces to maintain and eats VRAM that is not spare on a 4090 or on device. &lt;em>Self-speculative decoding&lt;/em> solves both: the draft is the model itself, run shallowly. A model with &lt;code>L&lt;/code> layers produces draft tokens by exiting at an intermediate layer &lt;code>k &amp;lt; L&lt;/code> (&lt;em>early-exit&lt;/em>) or by skipping a subset of layers (&lt;em>layer-skip&lt;/em>), and then verifies those tokens with the full forward pass through the &lt;code>L&lt;/code> layers. Because draft and verify &lt;strong>share weights and share the KV cache of the common layers&lt;/strong>, the extra memory cost is &lt;strong>zero&lt;/strong>: there is no second model, no second KV cache, nothing new to load. The price is that the early-exit draft is more expensive than a tiny external draft (it walks &lt;code>k/L&lt;/code> of the model instead of 1 %), so the relative cost &lt;code>c&lt;/code> goes up. The honest trade-off: with a well-trained dedicated draft (EAGLE-3) that &lt;strong>fits&lt;/strong> in memory, its α is usually higher and it wins; self-spec wins when there is no trained draft, when it does not fit, or when you are on device.&lt;/p>
&lt;h2 id="the-analogy-the-chess-player-who-plays-by-eye-and-then-calculates">The analogy: the chess player who plays by eye and then calculates&lt;/h2>
&lt;p>A good chess player does two things with the same brain. First they look at the board and, by eye, in half a second, propose a move that &amp;ldquo;looks right&amp;rdquo;: pattern intuition, fast recognition, the shallow layers of judgement. Then, before moving, they calculate properly: three moves ahead, the opponent&amp;rsquo;s replies, the tactical lines. That deep calculation confirms the intuition or corrects it.&lt;/p>
&lt;p>The decisive part is that it is the same person acting as drafter and as reviewer. They do not hire a second, weaker player to guess the move so they can validate it; that would be classic speculative with an external draft. Here the fast draft and the slow verification come out of the same brain, traversed at two depths.&lt;/p>
&lt;p>The analogy holds point by point:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>The glance by eye is the early-exit forward pass&lt;/strong>: the model walks only the first &lt;code>k&lt;/code> layers and emits a draft token. Fast, approximate.&lt;/li>
&lt;li>&lt;strong>The deep calculation is the full forward pass through the &lt;code>L&lt;/code> layers&lt;/strong>, which verifies the draft with exact rejection sampling.&lt;/li>
&lt;li>&lt;strong>That it is the same person is the reuse of weights and KV cache&lt;/strong>: the &lt;code>k&lt;/code> shallow layers of the draft are literally the same as the first &lt;code>k&lt;/code> layers of the verify; what has already been computed is not recomputed.&lt;/li>
&lt;li>&lt;strong>That the final move is identical to the one the player would have chosen by always calculating deeply&lt;/strong> is the rejection sampling guarantee: output quality does not degrade (the proof is in the &lt;a href="https://blog.lo0.es/en/posts/speculative-decoding-fundamentals-maths-state-of-play/">speculative post&lt;/a>).&lt;/li>
&lt;/ul>
&lt;h2 id="why-the-external-draft-does-not-fit-small-models">Why the external draft does not fit small models&lt;/h2>
&lt;p>Let us revisit the cost of classic speculative with two numbers. The speedup depends on the acceptance rate α (how often the draft is right) and on the relative cost &lt;code>c = T_draft / T_target&lt;/code>. A useful draft needs high α and low c at the same time. On large models that is reachable: a 1B draft for a 70B target has &lt;code>c ≈ 0.015&lt;/code> and, if well distilled (EAGLE), α &amp;gt; 0.8. The product pays off.&lt;/p>
&lt;p>On a small model the balance breaks:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>The proportional draft is useless.&lt;/strong> If you want &lt;code>c ≈ 0.1&lt;/code> for a 3B target, your draft is around 0.3B. A generic 0.3B has a distribution so different from the 3B that α falls into the 0.3–0.5 range. And &lt;code>1/(1-α)&lt;/code> with α = 0.4 is a ceiling of 1.67 tokens/step: not even infinite γ gets you more. The prize evaporates.&lt;/li>
&lt;li>&lt;strong>Loading a second model doubles the pieces.&lt;/strong> Even if the draft is small in VRAM, it is another checkpoint to version, quantise, validate and serve, and it has its own KV cache. On an RTX 4090 (24 GB, Ada Lovelace) with a quantised 8B and a long context, the KV cache is already tight; adding a second model and its cache can force you to lower concurrency or maximum context. On &lt;strong>device&lt;/strong> (a phone, a NUC, an edge box) there is simply no room.&lt;/li>
&lt;li>&lt;strong>A trained draft does not always exist&lt;/strong> for your exotic or fine-tuned model. EAGLE needs the draft trained on-policy against that specific target (see &lt;a href="https://blog.lo0.es/en/posts/knowledge-distillation-teaching-small-model-think-like-big-one/">knowledge distillation&lt;/a>). If your SLM is your own fine-tune, there is no official published draft.&lt;/li>
&lt;/ol>
&lt;p>Self-speculative attacks all three at once with one idea: &lt;strong>do not bring a second model; use the first one at half depth.&lt;/strong>&lt;/p>
&lt;h2 id="the-mechanism-early-exit-as-draft-full-forward-as-verify">The mechanism: early-exit as draft, full forward as verify&lt;/h2>
&lt;p>A transformer with &lt;code>L&lt;/code> layers transforms the hidden state layer by layer at each position: &lt;code>h_0 → h_1 → ... → h_L&lt;/code>, and the &lt;em>LM head&lt;/em> projects &lt;code>h_L&lt;/code> to logits. The observation that enables everything: &lt;code>h_k&lt;/code> for &lt;code>k &amp;lt; L&lt;/code> is already a reasonable hidden state. If you push it through the same LM head (or through a dedicated lightweight head), you get an output distribution that is &amp;ldquo;premature&amp;rdquo; but often correct for easy tokens. That is the source of the draft.&lt;/p>
&lt;p>The self-speculative iteration has the same structure as classic speculative (draft, verify, accept/reject) but both roles are the same model:&lt;/p>
&lt;p>&lt;strong>Step 1 — Shallow draft.&lt;/strong> To produce γ draft tokens, the model walks only the first &lt;code>k&lt;/code> layers (or a subset of layers in the layer-skip case) and applies the LM head. Each draft token costs ≈ &lt;code>k/L&lt;/code> of a full forward pass. We call &lt;code>c = k/L&lt;/code> the relative cost of the draft. The γ drafts are generated autoregressively at this reduced cost.&lt;/p>
&lt;p>&lt;strong>Step 2 — Full verify.&lt;/strong> The model runs a single forward pass through the &lt;code>L&lt;/code> layers over &lt;code>prompt + x_1...x_γ&lt;/code>. Because attention is causal it obtains &lt;code>p(·|prompt, x_&amp;lt;i)&lt;/code> for each position, exactly as in classic speculative.&lt;/p>
&lt;p>&lt;strong>Step 3 — Accept/reject.&lt;/strong> Rejection sampling identical to the previous post: tokens are accepted from left to right, the first divergence is corrected by sampling from the residual &lt;code>norm(max(0, p−q))&lt;/code>, and if all γ are accepted the bonus token is appended. Output quality is &lt;strong>exactly&lt;/strong> that of the full model.&lt;/p>
&lt;h3 id="the-trick-that-makes-c-cheaper-still-reusing-the-kv-cache-of-shared-layers">The trick that makes &lt;code>c&lt;/code> cheaper still: reusing the KV cache of shared layers&lt;/h3>
&lt;p>Here is the key difference against an external draft. When the model drafts by walking layers &lt;code>0..k&lt;/code>, it computes and stores the KV cache of those &lt;code>k&lt;/code> layers for the prompt tokens and the drafts. When the full verify comes along, layers &lt;code>0..k&lt;/code> of the &lt;code>L&lt;/code>-layer forward pass are bit for bit the same operations on the same weights that the draft already performed. There is no need to recompute them: the verify reuses the KV cache the draft left for layers &lt;code>0..k&lt;/code> directly, and only genuinely computes the missing layers &lt;code>k..L&lt;/code>.&lt;/p>
&lt;p>That has two consequences:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Zero extra memory.&lt;/strong> There is no second KV cache. The KV of the common layers is a single one, shared between draft and verify. Contrast that with vanilla SD, where the draft has its own complete cache (see &lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">KV cache&lt;/a>).&lt;/li>
&lt;li>&lt;strong>Partially reused compute.&lt;/strong> The verify only pays for the &amp;ldquo;new&amp;rdquo; layers &lt;code>k..L&lt;/code> for tokens that already went through the draft. The full forward pass is not as expensive as &lt;code>L&lt;/code> suggests, because the first &lt;code>k&lt;/code> layers come from the cache.&lt;/li>
&lt;/ul>
&lt;div class="diagram" style="max-width:780px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 780 400" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Early-exit draft and full forward verify with shared KV">
&lt;text x="390" y="22" text-anchor="middle" fill="currentColor" font-size="14" font-weight="700">A single L=32 layer model, traversed at two depths&lt;/text>
&lt;!-- DRAFT column -->
&lt;text x="180" y="50" text-anchor="middle" fill="currentColor" font-size="12" font-weight="700">DRAFT · early-exit at k=8&lt;/text>
&lt;rect x="120" y="60" width="120" height="120" fill="#fff4d6" stroke="#a48000" stroke-width="1.4" rx="6"/>
&lt;text x="180" y="95" text-anchor="middle" fill="#a48000" font-size="12" font-weight="600">layers 0..8&lt;/text>
&lt;text x="180" y="115" text-anchor="middle" fill="#a48000" font-size="11">shallow traversal&lt;/text>
&lt;text x="180" y="133" text-anchor="middle" fill="#a48000" font-size="11">cost ≈ k/L = 0.25&lt;/text>
&lt;rect x="120" y="190" width="120" height="30" fill="#fff4d6" stroke="#a48000" stroke-width="1.4" rx="6"/>
&lt;text x="180" y="210" text-anchor="middle" fill="#a48000" font-size="11" font-weight="600">LM head → draft&lt;/text>
&lt;text x="180" y="245" text-anchor="middle" fill="currentColor" font-size="11">x₁ x₂ x₃ x₄ (γ=4)&lt;/text>
&lt;!-- VERIFY column -->
&lt;text x="600" y="50" text-anchor="middle" fill="currentColor" font-size="12" font-weight="700">VERIFY · full forward L=32&lt;/text>
&lt;rect x="540" y="60" width="120" height="120" fill="#d4ecff" stroke="#1f5fa8" stroke-width="1.4" rx="6"/>
&lt;text x="600" y="92" text-anchor="middle" fill="#1f5fa8" font-size="12" font-weight="600">layers 0..8&lt;/text>
&lt;text x="600" y="110" text-anchor="middle" fill="#1f5fa8" font-size="11">(reused, not&lt;/text>
&lt;text x="600" y="125" text-anchor="middle" fill="#1f5fa8" font-size="11">recomputed)&lt;/text>
&lt;line x1="540" y1="135" x2="660" y2="135" stroke="#1f5fa8" stroke-width="1" stroke-dasharray="4 2"/>
&lt;text x="600" y="158" text-anchor="middle" fill="#1f5fa8" font-size="12" font-weight="600">layers 8..32&lt;/text>
&lt;text x="600" y="174" text-anchor="middle" fill="#1f5fa8" font-size="11">new compute&lt;/text>
&lt;rect x="540" y="190" width="120" height="30" fill="#d4ecff" stroke="#1f5fa8" stroke-width="1.4" rx="6"/>
&lt;text x="600" y="210" text-anchor="middle" fill="#1f5fa8" font-size="11" font-weight="600">LM head → p(·)&lt;/text>
&lt;!-- shared KV cache box -->
&lt;rect x="300" y="80" width="180" height="80" fill="#cdebd0" stroke="#2a7a40" stroke-width="1.6" rx="8"/>
&lt;text x="390" y="110" text-anchor="middle" fill="#2a7a40" font-size="12" font-weight="700">SHARED KV cache&lt;/text>
&lt;text x="390" y="128" text-anchor="middle" fill="#2a7a40" font-size="11">layers 0..8 · a single cache&lt;/text>
&lt;text x="390" y="145" text-anchor="middle" fill="#2a7a40" font-size="11">extra memory = 0&lt;/text>
&lt;!-- arrows draft->KV and KV->verify -->
&lt;defs>&lt;marker id="ssd1" 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="#2a7a40"/>&lt;/marker>&lt;/defs>
&lt;path d="M240,120 L300,120" fill="none" stroke="#2a7a40" stroke-width="1.6" marker-end="url(#ssd1)"/>
&lt;text x="270" y="112" text-anchor="middle" fill="#2a7a40" font-size="10">writes KV 0..8&lt;/text>
&lt;path d="M480,120 L540,120" fill="none" stroke="#2a7a40" stroke-width="1.6" marker-end="url(#ssd1)"/>
&lt;text x="510" y="112" text-anchor="middle" fill="#2a7a40" font-size="10">reads KV 0..8&lt;/text>
&lt;!-- accept/reject row -->
&lt;text x="390" y="280" text-anchor="middle" fill="currentColor" font-size="12" font-weight="700">Rejection sampling (identical to classic speculative)&lt;/text>
&lt;rect x="180" y="295" width="80" height="34" fill="#cdebd0" stroke="#2a7a40" stroke-width="1.4" rx="6"/>&lt;text x="220" y="316" text-anchor="middle" fill="#2a7a40" font-size="12" font-weight="600">x₁ ✓&lt;/text>
&lt;rect x="270" y="295" width="80" height="34" fill="#cdebd0" stroke="#2a7a40" stroke-width="1.4" rx="6"/>&lt;text x="310" y="316" text-anchor="middle" fill="#2a7a40" font-size="12" font-weight="600">x₂ ✓&lt;/text>
&lt;rect x="360" y="295" width="80" height="34" fill="#cdebd0" stroke="#2a7a40" stroke-width="1.4" rx="6"/>&lt;text x="400" y="316" text-anchor="middle" fill="#2a7a40" font-size="12" font-weight="600">x₃ ✓&lt;/text>
&lt;rect x="450" y="295" width="80" height="34" fill="#f6caca" stroke="#a52a2a" stroke-width="1.4" rx="6"/>&lt;text x="490" y="316" text-anchor="middle" fill="#a52a2a" font-size="12" font-weight="600">x₄ ✗&lt;/text>
&lt;text x="390" y="360" text-anchor="middle" fill="currentColor" font-size="11">Output = exactly that of the full model · 0 extra models · 0 extra KV&lt;/text>
&lt;text x="390" y="385" text-anchor="middle" fill="currentColor" font-size="11">Draft and verify are the same model; layers 0..8 are computed only once.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="the-families-state-of-play-2026">The families (state of play 2026)&lt;/h2>
&lt;p>There is no single way of doing self-speculative. They differ in which layers are skipped and in whether training is needed.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Family&lt;/th>
&lt;th>Year / venue&lt;/th>
&lt;th>How it picks what to skip&lt;/th>
&lt;th>Training?&lt;/th>
&lt;th>Extra KV&lt;/th>
&lt;th>Distinctive idea&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>LayerSkip&lt;/strong> (Elhoushi et al.)&lt;/td>
&lt;td>2024, arXiv:2404.16710&lt;/td>
&lt;td>Early-exit at a fixed layer &lt;code>k&lt;/code>; a single LM head serves every exit&lt;/td>
&lt;td>Yes — &lt;em>layer dropout&lt;/em> + &lt;em>early-exit loss&lt;/em> in train/fine-tune&lt;/td>
&lt;td>0&lt;/td>
&lt;td>A single model trained to do both draft and verify; reuses partial compute&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>SWIFT&lt;/strong>&lt;/td>
&lt;td>ICLR 2025 (OpenReview EKJhH5D5wA)&lt;/td>
&lt;td>Selects which layers to skip &lt;strong>on the fly&lt;/strong>, without touching weights&lt;/td>
&lt;td>&lt;strong>No&lt;/strong> — plug-and-play on the given model&lt;/td>
&lt;td>0&lt;/td>
&lt;td>&lt;em>Training-free&lt;/em> self-spec: optimises the set of skipped layers at runtime&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>CLaSp&lt;/strong>&lt;/td>
&lt;td>2025, arXiv:2505.24196&lt;/td>
&lt;td>Dynamic &lt;em>in-context&lt;/em> layer skip: the pattern of skipped layers adapts to the context&lt;/td>
&lt;td>No (dynamic at inference)&lt;/td>
&lt;td>0&lt;/td>
&lt;td>The skip is not fixed; it changes with what is being generated&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>ConfLayers&lt;/strong>&lt;/td>
&lt;td>2026, arXiv:2604.14612&lt;/td>
&lt;td>Skips layers according to the &lt;strong>confidence&lt;/strong> of the intermediate state (adaptive per token)&lt;/td>
&lt;td>No (confidence criterion)&lt;/td>
&lt;td>0&lt;/td>
&lt;td>Variable depth: easy tokens exit earlier, hard ones go deeper&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Saguaro&lt;/strong>&lt;/td>
&lt;td>2025–26&lt;/td>
&lt;td>&lt;strong>Asynchronous&lt;/strong> formulation: the draft keeps speculating in parallel while verification runs&lt;/td>
&lt;td>Depends on the variant&lt;/td>
&lt;td>0&lt;/td>
&lt;td>Overlaps draft and verify in time instead of alternating them&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>SSD for on-device MoE&lt;/strong>&lt;/td>
&lt;td>ACM Web Conf. 2026, doi 10.1145/3774904.3792218&lt;/td>
&lt;td>Self-spec exploiting MoE &lt;em>sparsity&lt;/em> (few active experts per token)&lt;/td>
&lt;td>MoE-specific variant&lt;/td>
&lt;td>0&lt;/td>
&lt;td>The shallow draft activates even fewer experts; fits MoE on device&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Three operational readings of the table:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>The axis that matters most is training yes/no.&lt;/strong> LayerSkip gives the best α because the model learns to be a good shallow draft (with early-exit loss the intermediate layers are explicitly trained to predict well). But it demands a fine-tune. SWIFT, CLaSp and ConfLayers are &lt;strong>training-free&lt;/strong>: worse α, but they apply to any already-trained model without touching anything. For an SLM you do not control, training-free is the realistic option.&lt;/li>
&lt;li>&lt;strong>Adaptive skip (CLaSp, ConfLayers) raises α&lt;/strong> because it fits the draft&amp;rsquo;s depth to the token: it spends little on the easy ones and more on the hard ones, instead of a fixed &lt;code>k&lt;/code>. In exchange, the effective &lt;code>c&lt;/code> stops being constant.&lt;/li>
&lt;li>&lt;strong>Saguaro attacks something else:&lt;/strong> it does not raise α, it overlaps draft and verify time. It is orthogonal to the rest and can be combined.&lt;/li>
&lt;/ol>
&lt;h2 id="the-maths-same-framework-different-c">The maths: same framework, different &lt;code>c&lt;/code>&lt;/h2>
&lt;p>We reuse the apparatus of the &lt;a href="https://blog.lo0.es/en/posts/speculative-decoding-fundamentals-maths-state-of-play/">speculative post&lt;/a> without changing a letter. With α the acceptance rate and γ the number of drafts:&lt;/p>
$$E[\text{tokens per step}] = \frac{1 - \alpha^{\gamma+1}}{1 - \alpha}, \qquad \text{Speedup} = \frac{1 - \alpha^{\gamma+1}}{(1 - \alpha)(\gamma c + 1)}$$
&lt;p>And the algorithmic ceiling is the same: &lt;code>lim_{γ→∞} = 1/(1-α)&lt;/code>. The only thing that changes in self-speculative is the value of &lt;code>c&lt;/code>: it is no longer the size ratio of two models, but &lt;code>c = k/L&lt;/code>, the fraction of layers the early-exit draft walks.&lt;/p>
&lt;h3 id="numerical-example-self-spec-with-l32-exit-at-k8">Numerical example: self-spec with L=32, exit at k=8&lt;/h3>
&lt;p>Take an SLM with &lt;code>L = 32&lt;/code> layers that exits at &lt;code>k = 8&lt;/code> for the draft: &lt;code>c = k/L = 8/32 = 0.25&lt;/code>. Suppose α = 0.7 (reasonable for early-exit on conversational tokens) and γ = 4.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Expected tokens per step:&lt;/strong> &lt;code>(1 − 0.7⁵) / (1 − 0.7) = (1 − 0.168) / 0.3 = 0.832 / 0.3 = 2.77&lt;/code>&lt;/li>
&lt;li>&lt;strong>Speedup:&lt;/strong> &lt;code>2.77 / (4 × 0.25 + 1) = 2.77 / 2.0 = 1.39×&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>The denominator factor is &lt;code>γc + 1 = 4·0.25 + 1 = 2.0&lt;/code>: the early-exit draft, costing a quarter of the model per token, eats part of the benefit. Exiting higher up helps: with &lt;code>k = 4&lt;/code> (&lt;code>c = 0.125&lt;/code>), the denominator is &lt;code>1.5&lt;/code> and the speedup &lt;code>2.77/1.5 = 1.85×&lt;/code>. But exiting higher up normally lowers α, so there is real tension between a small &lt;code>k&lt;/code> (cheap) and a high α (accurate).&lt;/p>
&lt;h3 id="an-honest-comparison-with-an-external-draft">An honest comparison with an external draft&lt;/h3>
&lt;p>Put a tiny, well-distilled external draft alongside: &lt;code>c = 0.1&lt;/code> and α = 0.78 (what an EAGLE-style draft can deliver), same γ = 4.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Tokens/step:&lt;/strong> &lt;code>(1 − 0.78⁵)/(1 − 0.78) = (1 − 0.289)/0.22 = 0.711/0.22 = 3.23&lt;/code>&lt;/li>
&lt;li>&lt;strong>Speedup:&lt;/strong> &lt;code>3.23 / (4 × 0.1 + 1) = 3.23 / 1.4 = 2.31×&lt;/code>&lt;/li>
&lt;/ul>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Configuration&lt;/th>
&lt;th>c&lt;/th>
&lt;th>α&lt;/th>
&lt;th>tokens/step&lt;/th>
&lt;th>speedup&lt;/th>
&lt;th>extra VRAM&lt;/th>
&lt;th>pieces to maintain&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Self-spec early-exit (k=8)&lt;/td>
&lt;td>0.25&lt;/td>
&lt;td>0.70&lt;/td>
&lt;td>2.77&lt;/td>
&lt;td>&lt;strong>1.39×&lt;/strong>&lt;/td>
&lt;td>&lt;strong>0&lt;/strong>&lt;/td>
&lt;td>&lt;strong>0&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Self-spec early-exit (k=4)&lt;/td>
&lt;td>0.125&lt;/td>
&lt;td>0.65&lt;/td>
&lt;td>2.50&lt;/td>
&lt;td>&lt;strong>1.67×&lt;/strong>&lt;/td>
&lt;td>&lt;strong>0&lt;/strong>&lt;/td>
&lt;td>&lt;strong>0&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Distilled external draft&lt;/td>
&lt;td>0.10&lt;/td>
&lt;td>0.78&lt;/td>
&lt;td>3.23&lt;/td>
&lt;td>&lt;strong>2.31×&lt;/strong>&lt;/td>
&lt;td>yes (+model +KV)&lt;/td>
&lt;td>1 extra model&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The reading is exactly what you would expect, and it is worth not dressing it up: if you have a dedicated draft, trained against your target, and it &lt;strong>fits&lt;/strong> in memory, its higher α and lower &lt;code>c&lt;/code> give it more speedup. EAGLE-3 with a well-trained draft usually wins on raw speedup. &lt;strong>Self-spec does not compete on raw speedup; it competes on total cost.&lt;/strong> Its winning columns are the two on the right: zero extra VRAM and zero pieces to maintain. Self-spec wins when:&lt;/p>
&lt;ul>
&lt;li>there is no trained draft for your model (your own SLM, an unusual fine-tune),&lt;/li>
&lt;li>the draft does not fit (a 4090 already full, a long context that needs the KV),&lt;/li>
&lt;li>you are on device (phone, NUC, edge), where a second model and its KV simply do not go in.&lt;/li>
&lt;/ul>
&lt;p>It is the same pattern as with MTP in the previous post: sometimes the best draft is the one you do not have to load.&lt;/p>
&lt;h2 id="why-it-fits-small-models-and-device-precisely">Why it fits small models and device precisely&lt;/h2>
&lt;p>The regime where self-spec shines is low concurrency, memory-bandwidth-bound, with a tight memory budget, exactly that of an SLM on a single GPU or on device (the reason for the regime is in &lt;a href="https://blog.lo0.es/en/posts/roofline-flips-optimising-small-models/">the inverted roofline&lt;/a>). Three reasons:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Zero extra memory is decisive where there is none to spare.&lt;/strong> On an RTX 4090 (24 GB, Ada Lovelace) serving a quantised 7B–8B with a long context, every GB counts. Self-spec asks for none: it reuses weights and KV. An external draft, however small, forces you to trim context or concurrency. On device the difference is binary: with self-spec you accelerate; with an external draft there is no room, full stop.&lt;/li>
&lt;li>&lt;strong>There is no second checkpoint to version.&lt;/strong> Operationally, an SLM at the edge deployed across hundreds of boxes becomes unsustainable if each one needs two synchronised models. A single binary that does draft and verify is far simpler to maintain.&lt;/li>
&lt;li>&lt;strong>It fits MoE on device.&lt;/strong> In a fine-grained MoE for device (see &lt;a href="https://blog.lo0.es/en/posts/device-native-architectures-fine-grained-moe-pre-attention-router/">native architectures for device&lt;/a>), the shallow draft activates even fewer experts, and the memory-bound regime persists even at medium batch, precisely what the work on SSD for on-device MoE (ACM WWW 2026) exploits.&lt;/li>
&lt;/ol>
&lt;p>The counterpoint, repeated so it does not get forgotten: on a generic 4×H100 SXM cluster (320 GB, NVLink, native FP8), where memory is not the bottleneck, a dedicated EAGLE-3 draft does fit and its higher α gives it more speedup. There, self-spec is plan B: you use it if the model is exotic and there is no trained draft, not because memory is tight.&lt;/p>
&lt;h2 id="pitfalls">Pitfalls&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>α depends enormously on &lt;code>k&lt;/code>.&lt;/strong> Exiting too high up (small &lt;code>k&lt;/code>) makes the draft cheap but sinks α; exiting too low down (&lt;code>k&lt;/code> close to &lt;code>L&lt;/code>) raises α but the draft costs almost a full forward pass and &lt;code>c → 1&lt;/code>, killing the speedup. The optimum is empirical and model-specific. Distrust any speedup number that does not say at which &lt;code>k&lt;/code> it was measured.&lt;/li>
&lt;li>&lt;strong>Training-free is not free in draft quality.&lt;/strong> SWIFT and CLaSp give lower α than LayerSkip precisely because the model&amp;rsquo;s intermediate layers were not trained to be good premature exits. The number that matters is α measured on &lt;em>your&lt;/em> distribution, not the paper&amp;rsquo;s.&lt;/li>
&lt;li>&lt;strong>Sampling temperature and creative outputs&lt;/strong> lower α just as in classic speculative. At high T, the self-spec speedup erodes faster still because it starts from a lower α.&lt;/li>
&lt;li>&lt;strong>Large batch neutralises it just as it does classic speculative.&lt;/strong> As soon as decode turns compute-bound, the drafts stop being &amp;ldquo;almost free&amp;rdquo;. Self-spec is for low concurrency.&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/speculative-decoding-fundamentals-maths-state-of-play/">Speculative decoding: the secretary who types ahead&lt;/a> — the direct companion and prerequisite: draft + verify + rejection sampling, the &lt;code>1/(1-α)&lt;/code> ceiling and the speedup formula we reuse here unchanged.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/roofline-flips-optimising-small-models/">The inverted roofline in small models&lt;/a> — why the SLM lives in a memory-bound regime, which is exactly what enables any form of speculative.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/device-native-architectures-fine-grained-moe-pre-attention-router/">Native architectures for device: fine-grained MoE&lt;/a> — where self-spec lands on MoE on device, taking advantage of the router&amp;rsquo;s sparsity.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">KV cache: the working memory&lt;/a> — reusing the KV of the layers shared between draft and verify is what makes the extra memory zero; the cache mechanism is there.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/knowledge-distillation-teaching-small-model-think-like-big-one/">Knowledge distillation&lt;/a> — LayerSkip&amp;rsquo;s early-exit loss is a relative of distillation: it teaches the intermediate layers to predict like the full model.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-pruning-cutting-without-amputating/">Pruning LLM models&lt;/a> — skipping layers is a form of structured pruning &lt;em>at inference&lt;/em>; layer-skip and layer-dropping share a conceptual root.&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> — the persistent memory-bound regime of MoE means self-spec over MoE wins even at medium batch.&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> — where speculative methods are configured in practice in production.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/mixed-nvidia-intel-environments-llm-inference/">Mixed NVIDIA + Intel environments&lt;/a> — the device/edge case where &amp;ldquo;zero extra model&amp;rdquo; stops being a convenience and becomes the only viable option.&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>Elhoushi, M., et al. &lt;em>LayerSkip: Enabling Early Exit Inference and Self-Speculative Decoding&lt;/em>. 2024. &lt;a href="https://arxiv.org/abs/2404.16710">https://arxiv.org/abs/2404.16710&lt;/a>&lt;/li>
&lt;li>&lt;em>SWIFT: On-the-Fly Self-Speculative Decoding for LLM Inference Acceleration&lt;/em>. ICLR 2025. &lt;a href="https://openreview.net/forum?id=EKJhH5D5wA">https://openreview.net/forum?id=EKJhH5D5wA&lt;/a>&lt;/li>
&lt;li>&lt;em>CLaSp: In-Context Layer Skip for Self-Speculative Decoding&lt;/em>. 2025. &lt;a href="https://arxiv.org/abs/2505.24196">https://arxiv.org/abs/2505.24196&lt;/a>&lt;/li>
&lt;li>&lt;em>ConfLayers: Confidence-Adaptive Layer Skipping for Self-Speculative Decoding&lt;/em>. 2026. &lt;a href="https://arxiv.org/abs/2604.14612">https://arxiv.org/abs/2604.14612&lt;/a>&lt;/li>
&lt;li>&lt;em>Self-Speculative Decoding for MoE on Device&lt;/em>. ACM Web Conference 2026. &lt;a href="https://doi.org/10.1145/3774904.3792218">https://doi.org/10.1145/3774904.3792218&lt;/a>&lt;/li>
&lt;li>Hugging Face blog. &lt;em>Faster Text Generation with Self-Speculative Decoding&lt;/em>. &lt;a href="https://huggingface.co/blog/layerskip">https://huggingface.co/blog/layerskip&lt;/a>&lt;/li>
&lt;li>Leviathan, Y., Kalman, M., Matias, Y. &lt;em>Fast Inference from Transformers via Speculative Decoding&lt;/em>. ICML 2023. &lt;a href="https://arxiv.org/abs/2211.17192">https://arxiv.org/abs/2211.17192&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Optimising decode in vLLM: squeezing every token out of small hardware</title><link>https://blog.lo0.es/en/posts/optimising-decode-vllm-squeezing-every-token-small-hardware/</link><pubDate>Fri, 05 Jun 2026 04:00:00 +0000</pubDate><guid>https://blog.lo0.es/en/posts/optimising-decode-vllm-squeezing-every-token-small-hardware/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Decode is the phase in which vLLM generates output tokens one at a time. It is &lt;strong>memory-bound&lt;/strong>, not compute-bound: the GPU spends more time waiting for the weights to arrive from VRAM than doing arithmetic. On small hardware, an RTX 4090 (24 GB) or an L40 (48 GB), a badly configured decode wastes half the card&amp;rsquo;s capacity. Five vLLM parameters change the equation: &lt;code>gpu-memory-utilization&lt;/code>, &lt;code>max-num-seqs&lt;/code>, speculative decoding, FP8 KV cache and a &lt;code>swap-space&lt;/code> correctly set to zero. Well calibrated, the difference is real: from 15 tokens/s to 35–50 tokens/s on the same hardware.&lt;/p>
&lt;hr>
&lt;h2 id="the-analogy">The analogy&lt;/h2>
&lt;p>An assembly-line worker putting cars together. Every car needs exactly the same process: walk to the store for the part, come back, bolt it on, repeat. The travel time to the store, the VRAM latency, is fixed and cannot be eliminated. But there are ways to make it less painful:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Have several cars in parallel on the line&lt;/strong> (more concurrency, the same travel time amortised).&lt;/li>
&lt;li>&lt;strong>Have an assistant prefabricate common parts&lt;/strong> (speculative decoding: the draft model proposes, the verifier confirms).&lt;/li>
&lt;li>&lt;strong>Keep only the most-used parts in the workshop&lt;/strong> (quantised KV cache: more contexts fit in the same space).&lt;/li>
&lt;/ol>
&lt;p>Those three strategies are exactly the three axes of decode optimisation in vLLM.&lt;/p>
&lt;hr>
&lt;h2 id="why-decode-is-memory-bound">Why decode is memory-bound&lt;/h2>
&lt;p>During prefill, the GPU processes N tokens in parallel: the attention operation is a large matmul and the compute units are busy. During decode, it processes &lt;strong>1 token per step&lt;/strong>: the matmul becomes a vector-matrix product, an operation that underuses the tensor cores.&lt;/p>
&lt;p>The typical compute utilisation ratio during decode on an RTX 4090:&lt;/p>
$$\text{MFU}_{decode} \approx 5–15\% \quad \text{(vs 40–60\% in prefill)}$$
&lt;p>The bottleneck is not compute power, it is bandwidth. To generate each token, the model has to read its complete weights from VRAM:&lt;/p>
$$\text{time per token} \approx \frac{\text{weight size in bytes}}{\text{VRAM bandwidth}}$$
&lt;p>For Qwen2.5-7B in BF16 (14 GB of weights) on an RTX 4090 (1,008 GB/s):&lt;/p>
$$t \approx \frac{14 \times 10^9}{1.008 \times 10^{12}} \approx 13.9 \text{ ms/token} \approx 72 \text{ tokens/s theoretical maximum}$$
&lt;p>The real value is lower (~30–50 tok/s) because of scheduler overhead, attention over the growing KV cache and other latencies. But the theoretical limit sets the ceiling.&lt;/p>
&lt;p>With Q4_K_M (weights ~4 GB):&lt;/p>
$$t \approx \frac{4 \times 10^9}{1.008 \times 10^{12}} \approx 3.97 \text{ ms/token} \approx 252 \text{ tokens/s theoretical}$$
&lt;p>Quantising the model is the most direct way to improve decode throughput on memory-bound hardware. Everything else optimises on top of that ceiling.&lt;/p>
&lt;hr>
&lt;h2 id="the-five-levers">The five levers&lt;/h2>
&lt;h3 id="1-give-vllm-all-the-vram-you-can----gpu-memory-utilization">1. Give vLLM all the VRAM you can — &lt;code>--gpu-memory-utilization&lt;/code>&lt;/h3>
&lt;p>&lt;code>--gpu-memory-utilization&lt;/code> (short form &lt;code>--gpu-mem-util&lt;/code>) defines the fraction of available VRAM that vLLM may use for the &lt;strong>KV cache&lt;/strong>, once the model weights are loaded. The rest is reserved for activations during the forward pass and for the CUDA context.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">vllm serve my-model &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --gpu-memory-utilization 0.92
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The default value is &lt;code>0.90&lt;/code>. On bare metal where no other process uses the GPU, &lt;code>0.92–0.95&lt;/code> is safe. Do not go above &lt;code>0.95&lt;/code>: vLLM needs headroom for activations during batch peaks, and running out of VRAM in the middle of an inference results in a process crash, not a clean error.&lt;/p>
&lt;p>&lt;strong>Why it matters:&lt;/strong> more KV cache available = more simultaneous requests in flight = better GPU utilisation during decode. PagedAttention allocates the KV cache in fixed-size blocks (16 tokens per block by default), and vLLM manages them like virtual memory pages. The more blocks available, the more requests it can serve without any of them waiting for space.&lt;/p>
&lt;pre tabindex="0">&lt;code>RTX 4090, Qwen2.5-7B-BF16 (14 GB weights):
Total VRAM: 24 GB
Weights: 14 GB
Available for KV cache: 10 GB
gpu-memory-utilization 0.90 → 0.90 × 10 GB = 9 GB for KV cache
gpu-memory-utilization 0.94 → 0.94 × 10 GB = 9.4 GB → ~4% more tokens in flight
&lt;/code>&lt;/pre>&lt;p>The impact is modest with models that fit comfortably, but it is amplified with models that push VRAM to its limit.&lt;/p>
&lt;hr>
&lt;h3 id="2-real-concurrency----max-num-seqs">2. Real concurrency — &lt;code>--max-num-seqs&lt;/code>&lt;/h3>
&lt;p>&lt;code>--max-num-seqs&lt;/code> is the maximum number of requests vLLM can have in process simultaneously (counting prefill and decode together). It is the parameter that controls the system&amp;rsquo;s effective concurrency.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">vllm serve my-model &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-num-seqs &lt;span class="m">128&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The effect is direct: more requests in simultaneous decode = better amortisation of the fixed cost of reading the weights. When the decode batch grows from 1 to 8, the time to generate 8 tokens is almost the same as generating 1 (the weights are read once for all of them). Aggregate throughput scales almost linearly until the KV cache or the activation VRAM becomes the bottleneck.&lt;/p>
$$\text{aggregate throughput}(B) \approx B \times \text{throughput}(1) \quad \text{for } B \ll B_{max}$$
&lt;p>&lt;strong>Common mistake:&lt;/strong> raising &lt;code>--max-num-seqs&lt;/code> without making sure there is enough KV cache in VRAM for all the requests. If vLLM cannot hold the KV caches of 128 simultaneous requests, it does preemption (pauses some request and frees its KV cache) at a latency cost. Monitor &lt;code>vllm:num_preemptions_total&lt;/code>.&lt;/p>
&lt;p>&lt;strong>Interaction with &lt;code>--max-num-batched-tokens&lt;/code>:&lt;/strong> the vLLM scheduler processes up to &lt;code>max-num-batched-tokens&lt;/code> tokens per step. If you have 128 requests in decode generating 1 token each, that is 128 decode tokens. The decode budget consumes 128 tokens of the total budget; the rest goes to prefill in chunks. Tune both values together.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># For an RTX 4090 serving ~50 concurrent users with responses of up to 512 tokens&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">vllm serve my-model &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-num-seqs &lt;span class="m">64&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-num-batched-tokens &lt;span class="m">8192&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 64 decode tokens per step + up to 8128 chunked prefill tokens&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h3 id="3-speculative-decoding----speculative-model----num-speculative-tokens">3. Speculative decoding — &lt;code>--speculative-model&lt;/code> + &lt;code>--num-speculative-tokens&lt;/code>&lt;/h3>
&lt;p>Speculative decoding is the highest-impact change for decode on small hardware. The idea is simple: a small draft model proposes several tokens at once, and the verifier model validates or rejects them in a single forward pass.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">vllm serve Qwen/Qwen2.5-7B-Instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --speculative-model Qwen/Qwen2.5-0.5B-Instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --num-speculative-tokens &lt;span class="m">5&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --speculative-draft-tensor-parallel-size &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Why it works:&lt;/strong> the 7B verifier has to read 14 GB of weights per step. With 5 proposed tokens, if the acceptance rate is 80%, an average of 4–5 tokens are generated per verifier step instead of 1. Effective throughput goes up without the GPU working any harder.&lt;/p>
&lt;p>The acceptance rate (α) depends on how well the draft predicts the verifier&amp;rsquo;s distribution. Within the same domain, models from the same family usually have α &amp;gt; 0.75:&lt;/p>
$$\text{speedup} \approx \frac{1 + \alpha \cdot k}{1 + \alpha \cdot k / \text{cost ratio}}$$
&lt;p>Where $k$ is the number of proposed tokens and cost_ratio is the draft/verifier cost ratio. For a 0.5B draft and a 7B verifier (ratio ~14×):&lt;/p>
$$\text{speedup} \approx 1 + 0.8 \times 5 \approx 5 \text{ (theoretical maximum, not reachable)}$$
&lt;p>In practice, with α = 0.75 and k = 5 on hardware without NVLink: &lt;strong>1.8–2.5× more tokens/s&lt;/strong> compared with decode alone.&lt;/p>
&lt;p>&lt;strong>EAGLE-3 in 2026:&lt;/strong> the best drafters today are not small versions of the same model, but networks specialised in predicting the verifier&amp;rsquo;s distribution. EAGLE-3 reports a 3–6.5× speedup over vanilla decode in public benchmarks. In production with mixed batches the real speedup is more conservative (1.5–3×). vLLM supports EAGLE/EAGLE-2 via &lt;code>--speculative-model&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># With an EAGLE drafter (requires a drafter trained specifically for the base model)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">vllm serve meta-llama/Llama-3.1-8B-Instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --speculative-model yuhuili/EAGLE3-LLaMA3.1-Instruct-8B &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --num-speculative-tokens &lt;span class="m">6&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>When speculative decoding does NOT help:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Very large batches (&amp;gt;32 requests): the acceptance rate varies between requests and the batch spends more time re-drafting than accepting.&lt;/li>
&lt;li>High-entropy tasks (brainstorming, very creative code): the draft predicts worse, α falls below 0.5 and the draft overhead outweighs the gain.&lt;/li>
&lt;li>If the draft model does not fit in the available VRAM alongside the verifier.&lt;/li>
&lt;/ul>
&lt;p>On an RTX 4090 with a 7B verifier and a 0.5B draft (BF16): 14 + 1 GB = 15 GB. That leaves 9 GB for the KV cache. It works.&lt;/p>
&lt;hr>
&lt;h3 id="4-quantised-kv-cache----kv-cache-dtype-fp8">4. Quantised KV cache — &lt;code>--kv-cache-dtype fp8&lt;/code>&lt;/h3>
&lt;p>This was already covered in the prefill article for its effect on context capacity. From the decode point of view, the benefit is different: more tokens fit in the KV cache → more simultaneous requests without preemption → better aggregate throughput.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">vllm serve my-model &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --kv-cache-dtype fp8 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --calculate-kv-scales
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Precision warning in decode:&lt;/strong> the KV cache is read on every attention step of the decode. Quantisation introduces noise into the attention activations. For long texts (&amp;gt;4K tokens of context) it can accumulate. In quality benchmarks (MMLU, HellaSwag) the degradation with FP8 KV and &lt;code>--calculate-kv-scales&lt;/code> is &amp;lt;0.5% on modern models. Without &lt;code>--calculate-kv-scales&lt;/code>, the degradation can be larger because the scales are fixed statically.&lt;/p>
&lt;p>&lt;strong>Optimal combination for RTX 4090:&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">vllm serve Qwen/Qwen2.5-7B-Instruct-AWQ &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --quantization awq &lt;span class="se">\ &lt;/span> &lt;span class="c1"># weights in INT4: 4 GB model&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --kv-cache-dtype fp8 &lt;span class="se">\ &lt;/span> &lt;span class="c1"># KV cache at half the size&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --calculate-kv-scales &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --gpu-memory-utilization 0.94
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Available VRAM: 24 - 4 = 20 GB for KV cache&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># With FP8: ~40 KB/token (vs 80 KB BF16) → 20 GB / 40 KB = 500,000 tokens of total context&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># With max-num-seqs 64 and a 4K ctx: 64 × 4096 × 40KB = 10 GB → fits with room to spare&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h3 id="5-remove-the-swap----swap-space-0">5. Remove the swap — &lt;code>--swap-space 0&lt;/code>&lt;/h3>
&lt;p>&lt;code>--swap-space&lt;/code> defines how much system RAM (not VRAM) vLLM may use to preempt KV caches. When vLLM has more active requests than fit in VRAM, it can &amp;ldquo;pause&amp;rdquo; some by moving their KV cache to RAM and reactivating them later.&lt;/p>
&lt;p>The problem: moving a 4K-token KV cache from VRAM to RAM and back has a latency of tens of milliseconds over PCIe. For a system where you want predictable latency, the swap introduces unacceptable jitter.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">vllm serve my-model &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --swap-space &lt;span class="m">0&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>With &lt;code>--swap-space 0&lt;/code>, when vLLM cannot hold any more requests in VRAM, it simply queues them instead of doing preemption. The queue adds waiting latency, but it is predictable and it does not interrupt the requests already in flight.&lt;/p>
&lt;p>&lt;strong>When should you have swap?&lt;/strong> If your workload has short demand peaks and you can tolerate occasional jitter in exchange for not rejecting requests, a swap of 4–8 GB can be useful. In ENS deployments where latency is a contractual SLA, &lt;code>--swap-space 0&lt;/code> is the right option.&lt;/p>
&lt;hr>
&lt;h2 id="the-reference-configuration-per-hardware">The reference configuration per hardware&lt;/h2>
&lt;h3 id="rtx-4090-24-gb--7b-model-internal-use">RTX 4090 (24 GB) — 7B model, internal use&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">vllm serve Qwen/Qwen2.5-7B-Instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --gpu-memory-utilization 0.92 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-model-len &lt;span class="m">8192&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-num-seqs &lt;span class="m">64&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-num-batched-tokens &lt;span class="m">8192&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --enable-chunked-prefill &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --enable-prefix-caching &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --kv-cache-dtype fp8 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --calculate-kv-scales &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --swap-space &lt;span class="m">0&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --speculative-model Qwen/Qwen2.5-0.5B-Instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --num-speculative-tokens &lt;span class="m">5&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --speculative-draft-tensor-parallel-size &lt;span class="m">1&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --dtype bfloat16
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Expected throughput: &lt;strong>35–55 tokens/s per user&lt;/strong>, up to 64 simultaneous, TTFT &amp;lt;500ms for prompts &amp;lt;1K tokens.&lt;/p>
&lt;h3 id="l40-48-gb--14b-model-multi-user">L40 (48 GB) — 14B model, multi-user&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">vllm serve Qwen/Qwen2.5-14B-Instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --gpu-memory-utilization 0.90 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-model-len &lt;span class="m">16384&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-num-seqs &lt;span class="m">128&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-num-batched-tokens &lt;span class="m">16384&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --enable-chunked-prefill &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --enable-prefix-caching &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --kv-cache-dtype fp8 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --calculate-kv-scales &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --swap-space &lt;span class="m">0&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --speculative-model Qwen/Qwen2.5-1.5B-Instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --num-speculative-tokens &lt;span class="m">5&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --dtype bfloat16
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Expected throughput: &lt;strong>25–40 tokens/s per user&lt;/strong>, up to 128 simultaneous with speculative decoding active, TTFT &amp;lt;800ms for prompts &amp;lt;2K tokens.&lt;/p>
&lt;hr>
&lt;h2 id="how-to-measure-that-decode-is-optimised">How to measure that decode is optimised&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Key metrics at vllm:8000/metrics&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">vllm:generation_tokens_total &lt;span class="c1"># total tokens generated → trend&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">vllm:e2e_request_latency_seconds_* &lt;span class="c1"># end-to-end latency per percentile&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">vllm:time_per_output_token_seconds_* &lt;span class="c1"># ITL (inter-token latency)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">vllm:num_preemptions_total &lt;span class="c1"># if it rises, the KV cache is filling up&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">vllm:spec_decode_draft_acceptance_rate &lt;span class="c1"># speculative decoding hit rate&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If &lt;code>spec_decode_draft_acceptance_rate&lt;/code> &amp;lt; 0.6, the drafter is not helping: turn speculative decoding off or find a drafter better trained for your model or domain.&lt;/p>
&lt;p>If &lt;code>num_preemptions_total&lt;/code> grows, you have too many simultaneous requests for the available KV cache. Options: lower &lt;code>max-num-seqs&lt;/code>, enable FP8 KV cache, lower &lt;code>max-model-len&lt;/code>, or quantise the model further.&lt;/p>
&lt;hr>
&lt;h2 id="implications-for-on-premise-inference">Implications for on-premise inference&lt;/h2>
&lt;p>In a sovereign deployment with fixed hardware, you cannot buy more GPUs at will. Every well-calibrated tenth of &lt;code>gpu-memory-utilization&lt;/code>, every point of speculative decoding acceptance rate and every MB of KV cache freed by FP8 is real capacity you do not have to provision with another node.&lt;/p>
&lt;p>The combination of quantised weights (AWQ/GPTQ), FP8 KV cache and speculative decoding lets a 14B serve on one L40 what without optimisations would need two L40s in tensor parallel. That is the economic argument for investing time in these parameters.&lt;/p>
&lt;p>Decode cannot be accelerated indefinitely on memory-bound hardware: the theoretical limit is set by VRAM bandwidth. But the difference between the minimum and the maximum achievable on that hardware can be 3–4× with the right levers.&lt;/p>
&lt;hr>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/optimising-prefill-vllm-knobs-ttft-will-not-forgive/">the prefill-phase optimisations: chunked prefill, prefix caching and FP8 KV&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/speculative-decoding-fundamentals-maths-state-of-play/">how speculative decoding works inside&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">the structure decode consults on every token&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/quantization-llm-inference-fp8-int4-gguf/">why quantising weights changes the speed ceiling of decode&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/continuous-batching-orca-vllm-chunked-prefill-goodput/">the foundation of concurrency management in vLLM&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/instrumenting-vllm-otel-measuring-optimisations/">how to measure with Prometheus whether speculative decoding, gpu-memory-utilization and max-num-seqs are working: &lt;code>spec_decode_draft_acceptance_rate&lt;/code>, &lt;code>num_preemptions_total&lt;/code> and the diagnostic matrix&lt;/a>&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://arxiv.org/abs/2309.06180">Efficient Memory Management for Large Language Model Serving with PagedAttention&lt;/a> — Kwon et al., 2023&lt;/li>
&lt;li>&lt;a href="https://arxiv.org/abs/2406.16858">EAGLE-2: Faster Inference of Language Models with Dynamic Draft Trees&lt;/a> — Li et al., 2024&lt;/li>
&lt;li>&lt;a href="https://arxiv.org/abs/2211.17192">Fast Inference from Transformers via Speculative Decoding&lt;/a> — Leviathan et al., 2022&lt;/li>
&lt;li>&lt;a href="https://docs.vllm.ai/en/stable/configuration/optimization/">vLLM Optimization and Tuning — documentación oficial&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://docs.vllm.ai/en/stable/features/spec_decode.html">vLLM Speculative Decoding&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://docs.vllm.ai/en/latest/features/quantization/quantized_kvcache/">FP8 KV Cache en vLLM&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Speculative decoding: the secretary who types ahead of the boss — fundamentals, maths and state of play in May 2026</title><link>https://blog.lo0.es/en/posts/speculative-decoding-fundamentals-maths-state-of-play/</link><pubDate>Fri, 29 May 2026 11:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/speculative-decoding-fundamentals-maths-state-of-play/</guid><description>&lt;blockquote>
&lt;p>This post complements &lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">KV cache: the working memory&lt;/a> and &lt;a href="https://blog.lo0.es/en/posts/quantization-llm-inference-fp8-int4-gguf/">Quantisation for LLM inference&lt;/a>. Both are acceleration levers &lt;strong>inside a single forward pass&lt;/strong>. Speculative decoding is the complementary lever: instead of making each forward pass cheaper, it tries to &lt;strong>produce more tokens per forward pass&lt;/strong>. It is orthogonal to quantisation and to the KV cache, and it stacks multiplicatively with both.&lt;/p>
&lt;/blockquote>
&lt;h2 id="you-are-here-deploy">You are here: DEPLOY&lt;/h2>
&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:#7ad88f;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(#spm)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#spm)}&lt;/style>
&lt;defs>&lt;marker id="spm" 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 · more tokens per forward pass without touching quality&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="tldr">TL;DR&lt;/h2>
&lt;p>Autoregressive LLM inference has a structural problem: every new token needs a complete forward pass of the model, and decode forward passes are memory-bandwidth-bound, not compute-bound (the detail is in &lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">KV cache&lt;/a>). The GPU spends most of its time waiting for the next weight to arrive from HBM. &lt;strong>Speculative decoding&lt;/strong> uses that dead time by doing two things in parallel: a small, cheap model (&lt;em>draft&lt;/em>) generates γ tokens autoregressively, fast but imprecise, and the large model (&lt;em>target&lt;/em>) verifies those γ tokens &lt;strong>in a single parallel forward pass&lt;/strong>, at almost the same cost as a single-token forward pass. An acceptance rule based on rejection sampling decides how many draft tokens are accepted, and the maths proves, it does not approximate, it proves, that the output distribution matches sampling directly from the target exactly. The technique has a ceiling: you never generate more than &lt;code>1/(1-α)&lt;/code> tokens per step, with α the acceptance rate. The five dominant families in May 2026 are &lt;strong>vanilla SD&lt;/strong> (Leviathan 2023), &lt;strong>Medusa&lt;/strong> (extra parallel heads), &lt;strong>EAGLE-1/2/3&lt;/strong> (a draft that operates at the level of hidden states), &lt;strong>MTP&lt;/strong> (multi-token prediction native to DeepSeek-V3) and &lt;strong>P-EAGLE&lt;/strong> (a draft that produces the γ tokens in a single forward pass, integrated in vLLM 0.16+). On low-concurrency workloads with short prompts and long outputs, the typical case of the on-premise conversational assistant, the real speedup sits between 2× and 4× on modern hardware.&lt;/p>
&lt;h2 id="the-analogy-the-secretary-who-types-ahead-and-the-boss-who-validates">The analogy: the secretary who types ahead and the boss who validates&lt;/h2>
&lt;p>Picture a crowded press conference with a real-time transcription setup. Two people are working:&lt;/p>
&lt;p>The &lt;strong>secretary&lt;/strong> sits at the keyboard. They know the subject, they have read the briefings and they know the boss&amp;rsquo;s verbal tics. As soon as the boss opens their mouth, the secretary starts typing what they think is coming. They are fast, three or four words ahead of the boss, but they sometimes get it wrong, especially when the boss takes an unexpected turn.&lt;/p>
&lt;p>The &lt;strong>boss&lt;/strong> periodically reads the secretary&amp;rsquo;s screen. They read a whole block at once, three or four words, and mentally compare what is written with what they would have said. If it matches their intent, they leave the text and carry on. If the secretary drifted at some point, they correct right there, and whatever the secretary typed after the divergence is discarded (it could be wrong because of the earlier error). If the whole block was fine, the boss takes the chance to add one more word while correcting.&lt;/p>
&lt;p>The result: the final text is identical to what the boss would have dictated alone, but it comes out faster because the secretary works ahead.&lt;/p>
&lt;p>The analogy holds on four details:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>The secretary is the draft model&lt;/strong>. Small, cheap, fast, approximate.&lt;/li>
&lt;li>&lt;strong>The boss is the target model&lt;/strong>. Slow, expensive, but the only authority on output quality.&lt;/li>
&lt;li>&lt;strong>The block the boss reads at once is the target&amp;rsquo;s parallel forward pass&lt;/strong> over &lt;code>prompt + γ draft tokens&lt;/code>. Verifying γ tokens costs almost the same as generating one, because the bottleneck is loading the weights from HBM, not the operations.&lt;/li>
&lt;li>&lt;strong>The &amp;ldquo;correct right where it diverged&amp;rdquo; rule is the rejection sampling&lt;/strong> that preserves the target distribution.&lt;/li>
&lt;/ul>
&lt;p>From here we go into the mechanism and into why quality is preserved exactly, not approximately.&lt;/p>
&lt;h2 id="the-bare-mechanism">The bare mechanism&lt;/h2>
&lt;p>Call &lt;code>p&lt;/code> the target distribution (what it would produce if it were the only one speaking) and &lt;code>q&lt;/code> the draft&amp;rsquo;s. The speculative decoding iteration has three steps:&lt;/p>
&lt;p>&lt;strong>Step 1 — Draft.&lt;/strong> The draft generates γ tokens autoregressively: &lt;code>x_1, x_2, ..., x_γ&lt;/code>. Each one through its own forward pass. Since the draft is small, the γ steps cost little. Typically γ ∈ [4, 8].&lt;/p>
&lt;p>&lt;strong>Step 2 — Verify.&lt;/strong> The target runs a single forward pass over the complete sequence &lt;code>prompt + x_1 ... x_γ&lt;/code>. Because attention is causal, that forward pass simultaneously produces the distributions &lt;code>p(·|prompt, x_&amp;lt;i)&lt;/code> for each position &lt;em>i&lt;/em>. In other words, in a single step it obtains the verification of the γ tokens and, on top of that, an extra distribution &lt;code>p(·|prompt, x_1...x_γ)&lt;/code> for the next token.&lt;/p>
&lt;p>&lt;strong>Step 3 — Accept/reject token by token.&lt;/strong> For each &lt;code>x_i&lt;/code> from left to right it applies:&lt;/p>
&lt;ul>
&lt;li>If &lt;code>p(x_i) ≥ q(x_i)&lt;/code>: always accept.&lt;/li>
&lt;li>If &lt;code>p(x_i) &amp;lt; q(x_i)&lt;/code>: accept with probability &lt;code>p(x_i)/q(x_i)&lt;/code>.&lt;/li>
&lt;li>If it rejects: stop, sample a replacement token from the normalised residual distribution &lt;code>norm(max(0, p − q))&lt;/code>, and discard the rest.&lt;/li>
&lt;li>If it reaches the end having accepted the γ tokens: append a &lt;strong>bonus token&lt;/strong> sampled directly from &lt;code>p(·|prompt, x_1...x_γ)&lt;/code>, which is already in the logits of the target&amp;rsquo;s forward pass.&lt;/li>
&lt;/ul>
&lt;p>Result per iteration: between 1 and γ+1 new tokens. In the best case (all accepted + bonus) γ+1 tokens are generated at the cost of &lt;strong>a single target forward pass plus γ draft forward passes&lt;/strong>, against γ+1 target forward passes in the version without speculative.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 780 340" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Speculative decoding mechanism">
&lt;style>
.dbox{fill:#fff4d6;stroke:#a48000;stroke-width:1.4;rx:6}
.tbox{fill:#d4ecff;stroke:#1f5fa8;stroke-width:1.4;rx:6}
.acc{fill:#cdebd0;stroke:#2a7a40;stroke-width:1.4;rx:6}
.rej{fill:#f6caca;stroke:#a52a2a;stroke-width:1.4;rx:6}
.bon{fill:#e6d0ff;stroke:#5a2db0;stroke-width:1.4;rx:6}
.lbl{font:600 12px sans-serif;fill:#222}
.sub{font:400 11px sans-serif;fill:#555}
.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#spec1)}
&lt;/style>
&lt;defs>&lt;marker id="spec1" 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="20" y="25" class="lbl">1. Draft model (q) generates γ=4 tokens autoregressively&lt;/text>
&lt;rect x="20" y="35" width="80" height="35" class="dbox"/>&lt;text x="60" y="58" text-anchor="middle" class="lbl">x₁&lt;/text>
&lt;rect x="120" y="35" width="80" height="35" class="dbox"/>&lt;text x="160" y="58" text-anchor="middle" class="lbl">x₂&lt;/text>
&lt;rect x="220" y="35" width="80" height="35" class="dbox"/>&lt;text x="260" y="58" text-anchor="middle" class="lbl">x₃&lt;/text>
&lt;rect x="320" y="35" width="80" height="35" class="dbox"/>&lt;text x="360" y="58" text-anchor="middle" class="lbl">x₄&lt;/text>
&lt;text x="420" y="58" class="sub">4 cheap forward passes&lt;/text>
&lt;text x="20" y="105" class="lbl">2. Target model (p) verifies the 4 tokens in ONE parallel forward pass&lt;/text>
&lt;rect x="20" y="115" width="380" height="40" class="tbox"/>&lt;text x="210" y="140" text-anchor="middle" class="lbl">single forward pass over [prompt, x₁, x₂, x₃, x₄]&lt;/text>
&lt;text x="420" y="140" class="sub">obtains p(·|prompt, x&amp;lt;i) for i=1..5&lt;/text>
&lt;text x="20" y="180" class="lbl">3. Left-to-right rejection sampling&lt;/text>
&lt;rect x="20" y="190" width="80" height="35" class="acc"/>&lt;text x="60" y="208" text-anchor="middle" class="lbl">x₁ ✓&lt;/text>&lt;text x="60" y="221" text-anchor="middle" class="sub">p≥q&lt;/text>
&lt;rect x="120" y="190" width="80" height="35" class="acc"/>&lt;text x="160" y="208" text-anchor="middle" class="lbl">x₂ ✓&lt;/text>&lt;text x="160" y="221" text-anchor="middle" class="sub">accepted&lt;/text>
&lt;rect x="220" y="190" width="80" height="35" class="acc"/>&lt;text x="260" y="208" text-anchor="middle" class="lbl">x₃ ✓&lt;/text>&lt;text x="260" y="221" text-anchor="middle" class="sub">accepted&lt;/text>
&lt;rect x="320" y="190" width="80" height="35" class="rej"/>&lt;text x="360" y="208" text-anchor="middle" class="lbl">x₄ ✗&lt;/text>&lt;text x="360" y="221" text-anchor="middle" class="sub">rejected&lt;/text>
&lt;text x="20" y="255" class="lbl">4. Result of this iteration&lt;/text>
&lt;rect x="20" y="265" width="80" height="35" class="acc"/>&lt;text x="60" y="288" text-anchor="middle" class="lbl">x₁&lt;/text>
&lt;rect x="120" y="265" width="80" height="35" class="acc"/>&lt;text x="160" y="288" text-anchor="middle" class="lbl">x₂&lt;/text>
&lt;rect x="220" y="265" width="80" height="35" class="acc"/>&lt;text x="260" y="288" text-anchor="middle" class="lbl">x₃&lt;/text>
&lt;rect x="320" y="265" width="80" height="35" class="rej"/>&lt;text x="360" y="288" text-anchor="middle" class="lbl">x'₄&lt;/text>
&lt;text x="420" y="280" class="sub">x'₄ = sample from norm(max(0, p−q)) at pos 4&lt;/text>
&lt;text x="420" y="295" class="sub">bonus dropped (only if all accepted)&lt;/text>
&lt;text x="20" y="325" class="sub">→ 4 new tokens in a single target iteration. Without speculative it would be 4 iterations.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="why-quality-does-not-degrade-the-proof">Why quality does not degrade (the proof)&lt;/h2>
&lt;p>This is the part of the technique that confuses people most the first time. It looks like magic: how can sampling from an arbitrary draft and then &amp;ldquo;validating&amp;rdquo; produce exactly the same distribution as sampling from the original target?&lt;/p>
&lt;p>The proof fits in two lines. For any token &lt;code>x&lt;/code>, the final probability of emitting it is the sum of two disjoint events: the draft proposes it and it is accepted, or the draft proposes something else, it is rejected, and sampling from the residual yields &lt;code>x&lt;/code>. Formally:&lt;/p>
$$P(\text{emit } x) = q(x) \cdot \min\!\left(1, \frac{p(x)}{q(x)}\right) + P(\text{reject}) \cdot \frac{\max(0, p(x)-q(x))}{\sum_y \max(0, p(y)-q(y))}$$
&lt;p>The first term is &lt;code>min(p(x), q(x))&lt;/code>. The second is &lt;code>max(0, p(x) − q(x))&lt;/code> (the total rejected mass &lt;code>Σ max(0, p−q)&lt;/code> cancels the denominator exactly). Adding them: &lt;code>min(p(x), q(x)) + max(0, p(x) − q(x)) = p(x)&lt;/code>. The output is statistically indistinguishable from sampling directly from the target, down to floating-point numerical differences.&lt;/p>
&lt;p>This matters operationally: it means speculative decoding is not perceptual compression, it is not a quality-speed trade-off, it does not require extra validation with evals. If the target would have passed a given eval, the system with speculative passes it too.&lt;/p>
&lt;h2 id="the-maths-that-matters-ceiling-and-speedup">The maths that matters: ceiling and speedup&lt;/h2>
&lt;p>Call α the &lt;strong>acceptance rate&lt;/strong>: the expected probability that an individual draft token is accepted. Assuming that acceptances of consecutive tokens are independent (a reasonable approximation in practice), the expected number of tokens generated per iteration is:&lt;/p>
$$E[\text{tokens per step}] = \sum_{k=0}^{\gamma} \alpha^k + \alpha^{\gamma+1} = \frac{1 - \alpha^{\gamma+1}}{1 - \alpha}$$
&lt;p>And the &lt;strong>theoretical speedup&lt;/strong> relative to generating one token per target iteration, with &lt;code>c = T_draft / T_target&lt;/code> the relative cost of the draft, is:&lt;/p>
$$\text{Speedup} = \frac{1 - \alpha^{\gamma+1}}{(1 - \alpha)(\gamma c + 1)}$$
&lt;p>There is a ceiling that many implementations try to beat and cannot: as &lt;code>γ → ∞&lt;/code>, the speedup converges to:&lt;/p>
$$\lim_{\gamma \to \infty} E[\text{tokens per step}] = \frac{1}{1 - \alpha}$$
&lt;p>It is an algorithmic ceiling, not a hardware one. With α = 0.7 you never generate more than 3.33 tokens per iteration no matter how much the hardware improves. With α = 0.8 → 5. With α = 0.9 → 10. That is why EAGLE-3, which targets α &amp;gt; 0.8 on many benchmarks, is not an incremental improvement over vanilla SD (α ≈ 0.5-0.6): it is a change of regime, because it raises the ceiling instead of merely getting closer to it.&lt;/p>
&lt;p>A concrete numerical example, Llama 3 70B as target with a draft giving α = 0.75, γ = 5, c = 0.1:&lt;/p>
&lt;ul>
&lt;li>Expected tokens per step: &lt;code>(1 − 0.75⁶) / (1 − 0.75) = (1 − 0.178) / 0.25 = 3.29&lt;/code>&lt;/li>
&lt;li>Speedup: &lt;code>3.29 / (5 × 0.1 + 1) = 3.29 / 1.5 = 2.19×&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>If we raise α to 0.85 with the same configuration: tokens per step = &lt;code>(1 − 0.85⁶) / 0.15 = (1 − 0.377) / 0.15 = 4.16&lt;/code>, speedup = &lt;code>4.16 / 1.5 = 2.77×&lt;/code>. A change of α = 0.75 → 0.85 (10 absolute points) raises the speedup by 27 %. That is why the research of the last two years focuses on pushing α: that is where the prize is.&lt;/p>
&lt;h2 id="the-five-modern-families-may-2026">The five modern families (May 2026)&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Family&lt;/th>
&lt;th>Year&lt;/th>
&lt;th>Core idea&lt;/th>
&lt;th>Draft size&lt;/th>
&lt;th>Typical α / τ&lt;/th>
&lt;th>Speedup&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Vanilla SD&lt;/strong>&lt;/td>
&lt;td>2023&lt;/td>
&lt;td>Same-family target + draft pair, rejection sampling&lt;/td>
&lt;td>1/10 – 1/100 of target&lt;/td>
&lt;td>0.5 – 0.8&lt;/td>
&lt;td>2-3×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Medusa&lt;/strong>&lt;/td>
&lt;td>2024&lt;/td>
&lt;td>N extra heads in parallel predict t+1, t+2, &amp;hellip;; tree attention verifies several candidates&lt;/td>
&lt;td>no draft, +1-2 % params&lt;/td>
&lt;td>top-5 &amp;gt; 0.8&lt;/td>
&lt;td>2.18-2.83×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>EAGLE-1/2/3&lt;/strong>&lt;/td>
&lt;td>2024-25&lt;/td>
&lt;td>Autoregressive draft at the level of &lt;strong>features (hidden states)&lt;/strong>, not tokens. Reuses the target&amp;rsquo;s embedding&lt;/td>
&lt;td>1 transformer block (~0.5-2 % params)&lt;/td>
&lt;td>EAGLE-3: α &amp;gt; 0.8, τ up to 7.5&lt;/td>
&lt;td>up to 6.5× peak&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>MTP&lt;/strong> (DeepSeek-V3)&lt;/td>
&lt;td>2024&lt;/td>
&lt;td>Multi-token heads trained from scratch as part of the model; at inference they act as a &amp;ldquo;free&amp;rdquo; draft&lt;/td>
&lt;td>14B params in V3 671B&lt;/td>
&lt;td>α &amp;gt; 0.8 (MTP1)&lt;/td>
&lt;td>1.5-1.8×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>P-EAGLE&lt;/strong>&lt;/td>
&lt;td>2026&lt;/td>
&lt;td>EAGLE but producing the γ drafts in &lt;strong>a single forward pass&lt;/strong> (parallel, not autoregressive)&lt;/td>
&lt;td>same as EAGLE&lt;/td>
&lt;td>+30 % over EAGLE-3&lt;/td>
&lt;td>4-5× vs AR&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Three operational observations:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>EAGLE dominates in production&lt;/strong> because its overhead is minimal (one transformer block, ~1 % of the target in parameters) and its α is high. The draft does not need its own &amp;ldquo;complete&amp;rdquo; KV cache because it shares features with the target.&lt;/li>
&lt;li>&lt;strong>Medusa mattered historically&lt;/strong>, it showed that speculative was possible without a separate draft, but EAGLE beat it on every published benchmark through 2024-2025.&lt;/li>
&lt;li>&lt;strong>MTP is special&lt;/strong>: it is not something you add to an existing model. It is something the model trained natively. If you buy DeepSeek-V3, MTP comes free and gives around 1.8× without touching anything. If you buy Llama 3, there is no MTP to be had; use EAGLE-3.&lt;/li>
&lt;/ol>
&lt;p>There are also two related techniques that deserve a brief mention, both without a draft model: &lt;strong>Lookahead decoding&lt;/strong> (Fu et al. 2024), which formulates decoding as Jacobi iteration and extracts n-grams from the trajectory; and &lt;strong>REST&lt;/strong> (He et al. 2024), which keeps an n-gram datastore and proposes drafts by longest-prefix match against the last generated tokens. Both give 1.5-2× with no extra VRAM. Useful when you have no trained draft model and do not want to maintain one.&lt;/p>
&lt;h2 id="real-implementations-in-may-2026">Real implementations in May 2026&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>vLLM v0.16+&lt;/strong>: unified support for EAGLE-1/2/3, P-EAGLE, Medusa, native MTP (DeepSeek-V3 and variants), n-gram/suffix decoding without a draft, an arbitrary draft model and MLP speculators. The canonical flag is &lt;code>--speculative-config '{&amp;quot;method&amp;quot;:&amp;quot;eagle3&amp;quot;, &amp;quot;model&amp;quot;:&amp;quot;...&amp;quot;, &amp;quot;num_speculative_tokens&amp;quot;: 5}'&lt;/code>.&lt;/li>
&lt;li>&lt;strong>SGLang&lt;/strong>: native EAGLE-3 support with &lt;code>--speculative-algorithm EAGLE3&lt;/code>. For DeepSeek-V3 it uses MTP through an EAGLE adapter. It has its own draft training framework (SpecForge).&lt;/li>
&lt;li>&lt;strong>TensorRT-LLM&lt;/strong>: Medusa, EAGLE (a simplified variant without a tree), ReDrafter, Lookahead. They report around 2.2× with EAGLE.&lt;/li>
&lt;li>&lt;strong>llama.cpp&lt;/strong>: basic speculative only with a draft model (&lt;code>--model-draft&lt;/code>). No native EAGLE/Medusa/MTP as far as I have verified. Typical speedup 1.5-2.5× single-user.&lt;/li>
&lt;/ul>
&lt;h2 id="when-speculative-does-not-help">When speculative does NOT help&lt;/h2>
&lt;p>The technique has three important blind spots:&lt;/p>
&lt;p>&lt;strong>Large batch.&lt;/strong> The GPU moves from memory-bound (decode at low concurrency) to compute-bound (decode at high concurrency). In the compute-bound regime the &amp;ldquo;almost free&amp;rdquo; target forward passes stop being free: γ tokens start costing γ times more instead of nearly 1. The typical crossover is at &lt;strong>batch 16-32 for dense models&lt;/strong>; for MoE with few active parameters per token the crossover happens later. At load peaks, speculative can worsen aggregate throughput per GPU.&lt;/p>
&lt;p>&lt;strong>Prefill / TTFT.&lt;/strong> Speculative decoding does not touch prefill. The phase that processes the whole prompt stays identical. TTFT does not improve and can get marginally worse because of the draft setup. If the SLA is TTFT-bound (assistants with short outputs, search, RAG with brief answers), this is not the right tool.&lt;/p>
&lt;p>&lt;strong>Short outputs.&lt;/strong> If the model generates 10-20 tokens and the answer is over, the fixed setup overhead is not amortised. Speculative shines with long outputs (300+ tokens): generative assistants, extended code completion, drafting.&lt;/p>
&lt;h2 id="operational-pitfalls">Operational pitfalls&lt;/h2>
&lt;p>&lt;strong>Extra VRAM for the draft.&lt;/strong> In vanilla SD, loading a complete draft plus its KV cache is expensive. Llama 3 8B in BF16 as the draft for a 70B is about 16 GB of weights plus KV cache. On an H100 80 GB with the target already nearly filling it, that can force you to shrink the target&amp;rsquo;s KV cache and lower maximum concurrency. EAGLE solves this: the draft is one transformer block (~1 GB for a 70B) and reuses the target&amp;rsquo;s features.&lt;/p>
&lt;p>&lt;strong>Draft quantisation.&lt;/strong> Quantising the draft to INT4 with GPTQ degrades α substantially (errors in the logits accumulate in the comparison against &lt;code>p&lt;/code>). AWQ holds up better but also lowers α. &lt;strong>Common practice in 2026: target in FP8 or INT4, draft in FP16/BF16&lt;/strong>. The draft is small enough that the VRAM saved by quantising it does not compensate for the drop in α and therefore in speedup. The detail of each format is in &lt;a href="https://blog.lo0.es/en/posts/quantization-llm-inference-fp8-int4-gguf/">Quantisation&lt;/a>.&lt;/p>
&lt;p>&lt;strong>Interaction with continuous batching.&lt;/strong> It does not break it, but it creates &lt;em>nested raggedness&lt;/em>: each request in the batch can accept a different number of tokens on each iteration. vLLM&amp;rsquo;s PagedAttention absorbs it, but the scheduler loses efficiency. At low QPS (conversational assistant, low simultaneous concurrency) the combination is excellent. At high QPS there is real tension, and work such as &lt;em>Goodput-optimized speculative decoding&lt;/em> (Liu et al., 2024) optimises γ dynamically according to the state of the batch.&lt;/p>
&lt;p>&lt;strong>Sampling temperature.&lt;/strong> α falls with high temperatures. At T = 1.0 with creative outputs (free-form writing), α can drop 10-15 points relative to T = 0 with the same model-draft pair. The speedup scales accordingly.&lt;/p>
&lt;h2 id="implications-on-on-premise-hardware">Implications on on-premise hardware&lt;/h2>
&lt;h3 id="on-an-rtx-4090-24-gb-ada-lovelace">On an RTX 4090 (24 GB, Ada Lovelace)&lt;/h3>
&lt;p>The classic use is &lt;strong>vanilla SD with two quantised models&lt;/strong>: for instance, Llama 3 70B AWQ-INT4 as target (~35 GB → needs TP=2 across two 4090s) and Llama 3 8B AWQ-INT4 as draft. In practice, the most realistic single-card case is &lt;strong>Llama 3 8B target + a 1B model as draft&lt;/strong> or &lt;strong>Qwen 3 14B target + Qwen 3 0.5B as draft&lt;/strong>: they fit comfortably and give 1.8-2.5× on conversational tasks. For EAGLE on consumer cards, the official drafts for popular families (Llama 3, Qwen 3) are published on Hugging Face and take up 0.5-2 GB extra.&lt;/p>
&lt;h3 id="on-a-generic-4h100-sxm-cluster-320-gb-nvlink-native-fp8">On a generic 4×H100 SXM cluster (320 GB, NVLink, native FP8)&lt;/h3>
&lt;p>Here EAGLE-3 shines:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Llama 3 70B FP8 + EAGLE-3 draft in FP16&lt;/strong>: the draft takes about 0.7 GB; the target about 70 GB with TP=2 (35 GB per GPU). The speedup observed in reproducible benchmarks is between 2.5× and 4× at batch 1-4, falling to almost break-even at batch 32.&lt;/li>
&lt;li>&lt;strong>DeepSeek-V3 671B FP8 + native MTP&lt;/strong>: the model ships with MTP trained in; there is nothing to add. The speedup is 1.5-1.8× with zero extra VRAM. It is the most efficient option operationally: zero additional pieces.&lt;/li>
&lt;li>&lt;strong>Combining with disaggregated serving&lt;/strong>: as &lt;a href="https://blog.lo0.es/en/posts/disaggregated-serving-prefill-decode-specialised-pods/">Disaggregated serving&lt;/a> explains, prefill and decode can live in separate pods. Speculative applies only in the decode pods, which fits the separation perfectly (prefill is compute-bound and would not benefit).&lt;/li>
&lt;/ul>
&lt;p>The rule of thumb on an H100 cluster in May 2026: &lt;strong>if the model is DeepSeek-V3 / V4 → native MTP, nothing else; if it is Llama 3 / Qwen 3 → EAGLE-3 with the official draft; if it is exotic → vanilla SD with a draft from the same family&lt;/strong>.&lt;/p>
&lt;h2 id="what-we-have-not-covered">What we have not covered&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Training custom EAGLE drafts&lt;/strong> with SpecForge: how to collect target trajectories and train the draft on-policy.&lt;/li>
&lt;li>&lt;strong>Speculative Prefill&lt;/strong> (arXiv:2502.02789): a variant for accelerating TTFT, a different mechanism from the decode one described here.&lt;/li>
&lt;li>&lt;strong>Tree attention in detail&lt;/strong>: how Medusa and EAGLE-2 verify several candidates at once with specific attention masks.&lt;/li>
&lt;li>&lt;strong>MoE + speculative&lt;/strong>: the combination has non-trivial interactions with the expert router. Low activated params keep the memory-bound regime in place even at high batch, which changes the rules.&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/kv-cache-working-memory-llm-inference/">KV cache: the working memory that holds up LLM inference&lt;/a> — speculative would not exist without the memory-bound nature of decode, which is a direct consequence of the KV cache; that post gives the framework.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/pagedattention-vllm-block-manager/">PagedAttention deep dive&lt;/a> — vLLM&amp;rsquo;s scheduler has to manage the nested raggedness of the γ tokens accepted per request; PagedAttention is what makes that possible without reserving fixed blocks per session.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/quantization-llm-inference-fp8-int4-gguf/">Quantisation for LLM inference&lt;/a> — a lever orthogonal to and multiplicative with speculative; that post explains why the draft is usually left in BF16 even when the target is in FP8/INT4.&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> — speculative applies only in decode; disaggregation makes that specialisation easy without touching prefill.&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> — the persistent memory-bound regime of MoE means speculative gains more on MoE than on dense at medium batch. MTP in DeepSeek-V3 is speculative decoding native to the model (no external draft) with acceptance around 85-90 % on the second token.&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 scheduler where speculative lives. Speculative breaks the symmetry of the batch (each request accepts between 1 and γ+1 tokens per iteration); at high QPS it can reduce goodput if the draft consumes slots from the decode pool.&lt;/li>
&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 where Deploy is stage 4.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/mixed-nvidia-intel-environments-llm-inference/">Mixed NVIDIA + Intel environments&lt;/a> — the &amp;ldquo;drafter near edge on an Intel NUC + target on a central H100&amp;rdquo; pattern as the canonical case of speculative decoding deployed heterogeneously.&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> — the concrete vLLM parameters for turning on speculative decoding in production (&lt;code>--speculative-model&lt;/code>, &lt;code>--num-speculative-tokens&lt;/code>) with reference configs for RTX 4090 and L40.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/knowledge-distillation-teaching-small-model-think-like-big-one/">Knowledge distillation&lt;/a> — the best drafters are not small versions of the base model: they are students distilled specifically to predict the verifier&amp;rsquo;s distribution; distillation explains why EAGLE beats a plain generic 0.5B.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-pruning-cutting-without-amputating/">Pruning LLM models&lt;/a> — an alternative to the distilled drafter: a pruned draft model (layer dropping from the base) as a cheap approximation of the verifier; it works worse than EAGLE but needs no extra training.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/self-speculative-decoding-model-gets-ahead-of-itself/">Self-speculative decoding: the model that gets ahead of itself&lt;/a> — the variant without a separate draft: the model itself run in early-exit acts as the draft and is verified with the full forward pass, zero extra VRAM. It is the form of speculative that fits small models and on-device deployment.&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>Leviathan, Y., Kalman, M., Matias, Y. &lt;em>Fast Inference from Transformers via Speculative Decoding&lt;/em>. ICML 2023. &lt;a href="https://arxiv.org/abs/2211.17192">https://arxiv.org/abs/2211.17192&lt;/a>&lt;/li>
&lt;li>Chen, C., Borgeaud, S., Irving, G., Lespiau, J.B., Sifre, L., Jumper, J. &lt;em>Accelerating Large Language Model Decoding with Speculative Sampling&lt;/em>. DeepMind 2023. &lt;a href="https://arxiv.org/abs/2302.01318">https://arxiv.org/abs/2302.01318&lt;/a>&lt;/li>
&lt;li>Cai, T., Li, Y., Geng, Z., Peng, H., Lee, J.D., Chen, D., Dao, T. &lt;em>Medusa: Simple LLM Inference Acceleration with Multiple Decoding Heads&lt;/em>. ICML 2024. &lt;a href="https://arxiv.org/abs/2401.10774">https://arxiv.org/abs/2401.10774&lt;/a>&lt;/li>
&lt;li>Li, Y., Wei, F., Zhang, C., Zhang, H. &lt;em>EAGLE: Speculative Sampling Requires Rethinking Feature Uncertainty&lt;/em>. ICML 2024. &lt;a href="https://arxiv.org/abs/2401.15077">https://arxiv.org/abs/2401.15077&lt;/a>&lt;/li>
&lt;li>Li, Y., Wei, F., Zhang, C., Zhang, H. &lt;em>EAGLE-2: Faster Inference of Language Models with Dynamic Draft Trees&lt;/em>. EMNLP 2024. &lt;a href="https://arxiv.org/abs/2406.16858">https://arxiv.org/abs/2406.16858&lt;/a>&lt;/li>
&lt;li>Li, Y., Wei, F., Zhang, C., Zhang, H. &lt;em>EAGLE-3: Scaling up Inference Acceleration of Large Language Models via Training-Time Test&lt;/em>. NeurIPS 2025. &lt;a href="https://arxiv.org/abs/2503.01840">https://arxiv.org/abs/2503.01840&lt;/a>&lt;/li>
&lt;li>DeepSeek-AI. &lt;em>DeepSeek-V3 Technical Report&lt;/em> — Multi-Token Prediction. &lt;a href="https://arxiv.org/abs/2412.19437">https://arxiv.org/abs/2412.19437&lt;/a>&lt;/li>
&lt;li>Fu, Y., Bailis, P., Stoica, I., Zhang, H. &lt;em>Break the Sequential Dependency of LLM Inference Using Lookahead Decoding&lt;/em>. ICML 2024. &lt;a href="https://arxiv.org/abs/2402.02057">https://arxiv.org/abs/2402.02057&lt;/a>&lt;/li>
&lt;li>He, Z., Zhong, Z., Cai, T., Lee, J.D., He, D. &lt;em>REST: Retrieval-Based Speculative Decoding&lt;/em>. NAACL 2024. &lt;a href="https://arxiv.org/abs/2311.08252">https://arxiv.org/abs/2311.08252&lt;/a>&lt;/li>
&lt;li>vLLM speculative decoding docs: &lt;a href="https://docs.vllm.ai/en/latest/features/speculative_decoding/">https://docs.vllm.ai/en/latest/features/speculative_decoding/&lt;/a>&lt;/li>
&lt;li>SGLang speculative decoding docs: &lt;a href="https://docs.sglang.ai/advanced_features/speculative_decoding.html">https://docs.sglang.ai/advanced_features/speculative_decoding.html&lt;/a>&lt;/li>
&lt;li>TensorRT-LLM speculative sampling: &lt;a href="https://nvidia.github.io/TensorRT-LLM/advanced/speculative-decoding.html">https://nvidia.github.io/TensorRT-LLM/advanced/speculative-decoding.html&lt;/a>&lt;/li>
&lt;li>vLLM blog &lt;em>Speculative Decoding in vLLM&lt;/em> (oct 2024): &lt;a href="https://blog.vllm.ai/2024/10/17/spec-decode.html">https://blog.vllm.ai/2024/10/17/spec-decode.html&lt;/a>&lt;/li>
&lt;li>vLLM blog &lt;em>P-EAGLE: Parallel Speculative Decoding in vLLM&lt;/em> (mar 2026): &lt;a href="https://vllm.ai/blog/2026-03-13-p-eagle">https://vllm.ai/blog/2026-03-13-p-eagle&lt;/a>&lt;/li>
&lt;li>Official EAGLE repo: &lt;a href="https://github.com/SafeAILab/EAGLE">https://github.com/SafeAILab/EAGLE&lt;/a>&lt;/li>
&lt;li>Official Medusa repo: &lt;a href="https://github.com/FasterDecoding/Medusa">https://github.com/FasterDecoding/Medusa&lt;/a>&lt;/li>
&lt;/ul></description></item></channel></rss>