<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: mayankpallai</title>
    <description>The latest articles on DEV Community by mayankpallai (@cyprus09).</description>
    <link>https://dev.to/cyprus09</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4029231%2F57f4fc44-366b-4f42-b2ec-a487edc16481.png</url>
      <title>DEV Community: mayankpallai</title>
      <link>https://dev.to/cyprus09</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cyprus09"/>
    <language>en</language>
    <item>
      <title>Deep Dive into Mixture of Experts: From 1991 to DeepSeek-V3</title>
      <dc:creator>mayankpallai</dc:creator>
      <pubDate>Mon, 14 Sep 2026 19:20:46 +0000</pubDate>
      <link>https://dev.to/cyprus09/deep-dive-into-mixture-of-experts-from-1991-to-deepseek-v3-2pgh</link>
      <guid>https://dev.to/cyprus09/deep-dive-into-mixture-of-experts-from-1991-to-deepseek-v3-2pgh</guid>
      <description>&lt;p&gt;Every major LLM lab is in a conundrum today, deliberating between scale vs cost. Making a dense model bigger makes it smarter, yes, but also makes every token more expensive to generate. In a dense model, every parameter activates on every token, and the compute cost of a forward pass scales ~linearly with parameter count.&lt;/p&gt;

&lt;p&gt;Mixture of Experts (MoE) challenges the status quo with its architecture. Instead of one giant feed-forward network per layer, an MoE layer holds many smaller so-called "expert" networks, and a learned gate activates only a handful of them per token per layer. The result is a model that can carry hundreds of billions of parameters in vRAM, while activating only a small fraction of them for each run. To give an idea of scale, the DeepSeek-V3 model holds 671 billion params, while MoE enables it to use only around 37 billion of them each time.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. 1991: Where It Started - Adaptive Mixtures of Local Experts
&lt;/h2&gt;

&lt;p&gt;The idea of "mixture of experts" predates deep learning as we know it. It was first proposed in a 1991 paper by Jacobs, Jordan, Nowlan, and Hinton, &lt;em&gt;&lt;a href="https://ieeexplore.ieee.org/document/6797059" rel="noopener noreferrer"&gt;Adaptive Mixtures of Local Experts&lt;/a&gt;&lt;/em&gt;. The core idea: instead of training one large network to solve a hard task, train several smaller "expert" networks and a separate &lt;strong&gt;gating network&lt;/strong&gt; that learns how to weight and combine their outputs, all trained jointly.&lt;/p&gt;

&lt;p&gt;The paper's demonstration was a vowel discrimination task. Given the same speech data, a system of competing experts plus a gating network was compared against a single monolithic network. The learning procedure was shown to decompose the hard problem into sub-problems that each expert could handle individually.&lt;/p&gt;

&lt;p&gt;There's an important architectural detail here that's easy to miss, and it matters for everything that follows: this original formulation is what we'd now call a &lt;strong&gt;dense&lt;/strong&gt;-MoE. The gate produces a weight for &lt;em&gt;every&lt;/em&gt; expert, and &lt;em&gt;every&lt;/em&gt; expert computes an output for every input; they're just combined with different weights.&lt;/p&gt;

&lt;p&gt;That single detail is why the 1991 paper sits as a conceptual ancestor rather than a direct blueprint for modern LLM MoE. It establishes gating as a &lt;em&gt;learned, differentiable&lt;/em&gt; routing function, which is an idea that survives for the next three decades. However, it doesn't buy any compute savings, because all experts still run on every input.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvy6gz7u2q9bw3r1wqm3m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvy6gz7u2q9bw3r1wqm3m.png" alt="Arch" width="550" height="572"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Model Architecture of the original 1991 expert/gate paper&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  2. 2017: The Sparse-Gating Leap
&lt;/h2&gt;

&lt;p&gt;The paper that actually bridges 1991 and modern LLMs is Shazeer et al.'s 2017 paper from Google Brain, &lt;em&gt;&lt;a href="https://arxiv.org/pdf/1701.06538" rel="noopener noreferrer"&gt;Outrageously Large Neural Networks: The Sparsely-Gated Mixture-of-Experts Layer&lt;/a&gt;&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The key change here: &lt;strong&gt;sparse&lt;/strong&gt; gating. Instead of every expert computing on every input, the gate selects only the &lt;strong&gt;top-k&lt;/strong&gt; highest-scoring experts (4/4096 in this scenario), and only those experts actually run. This is the shift that actually saves compute, and it's the mechanism every modern LLM MoE is based on.&lt;/p&gt;

&lt;p&gt;A minimal version of the routing logic looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Simplified top-k sparse gating
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;moe_layer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;experts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gate_weights&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;logits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;gate_weights&lt;/span&gt;          &lt;span class="c1"&gt;# [num_experts] raw scores
&lt;/span&gt;    &lt;span class="n"&gt;top_k_vals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k_idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;topk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;top_k_weights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;softmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;top_k_vals&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# renormalized over just the top-k
&lt;/span&gt;
    &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;top_k_weights&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k_idx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;experts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# only these experts run
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Need for a Load-Balancing Loss
&lt;/h3&gt;

&lt;p&gt;Sparse gating introduces a new problem. If you train the gate network purely on the downstream task loss, it tends to &lt;strong&gt;collapse&lt;/strong&gt;. Here's the failure mode: a small random edge in initialisation means one expert responds marginally better to some input than another. The gate notices, routes slightly more traffic there. That expert now gets more gradient updates than its neighbours, so it improves further, so the gate favours it even more. &lt;/p&gt;

&lt;p&gt;Left unchecked, this rich-get-richer loop concentrates almost all traffic onto a handful of experts, while the rest stay undertrained and never develop anything useful.&lt;/p&gt;

&lt;p&gt;Shazeer et al.'s fix is an &lt;strong&gt;auxiliary loss&lt;/strong&gt;, a term added on top of the main task loss that penalises uneven expert usage across a batch, nudging the gate toward spreading tokens more evenly across the whole expert pool.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Simplified load-balancing auxiliary loss
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_balance_loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gate_probs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expert_assignments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_experts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# fraction of tokens routed to each expert
&lt;/span&gt;    &lt;span class="n"&gt;frac_routed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;count_per_expert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expert_assignments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expert_assignments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# average gate probability assigned to each expert
&lt;/span&gt;    &lt;span class="n"&gt;avg_gate_prob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gate_probs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;num_experts&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frac_routed&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;avg_gate_prob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fauufavv6li98rpmx83ix.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fauufavv6li98rpmx83ix.png" alt="load loss" width="800" height="370"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  3. 2020–2021: Google's MoE-for-Transformers Era
&lt;/h2&gt;

&lt;p&gt;The 2017 paper proved sparse MoE worked in an LSTM. The next step was bringing it into the Transformer, inside the feed-forward (FFN) sub-layer of each transformer block, rather than replacing the whole layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GShard (2020)&lt;/strong&gt; was the first major distributed-training MoE story at Transformer scale, using &lt;strong&gt;top-2 gating&lt;/strong&gt;. Each token routed to its top 2 experts, and GShard combined this with techniques for sharding enormous expert pools across many devices.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnp5d4p9f6gkgfl3gtymt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnp5d4p9f6gkgfl3gtymt.png" alt="Gshard" width="800" height="509"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Switch Transformer (2021)&lt;/strong&gt; simplified this further, to &lt;strong&gt;top-1 gating&lt;/strong&gt;, just one expert per token, per layer. The authors argued this was not just cheaper but &lt;em&gt;more stable&lt;/em&gt; to train than top-2, while still scaling to trillion-parameter total capacity.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm38wjsks9knlwve1r1hr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm38wjsks9knlwve1r1hr.png" alt="switch" width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;GShard (2020)&lt;/th&gt;
&lt;th&gt;Switch Transformer (2021)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gating&lt;/td&gt;
&lt;td&gt;Top-2&lt;/td&gt;
&lt;td&gt;Top-1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Main claim&lt;/td&gt;
&lt;td&gt;Distributed sparse training at scale&lt;/td&gt;
&lt;td&gt;Simpler routing = more stable + cheaper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where it lives&lt;/td&gt;
&lt;td&gt;Transformer FFN sub-layer&lt;/td&gt;
&lt;td&gt;Transformer FFN sub-layer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  4. 2023: Mixtral and the Specialisation Question Gets Interesting
&lt;/h2&gt;

&lt;p&gt;Mistral's &lt;strong&gt;Mixtral 8x7B&lt;/strong&gt; (2023) is one of the first widely-used open-weight LLMs built on MoE, 8 experts per layer, top-2 gating, with routing decisions made independently at every layer for every token.&lt;/p&gt;

&lt;p&gt;The Mixtral paper includes its own analysis of what the experts specialise in, and the finding is striking: at this scale, on general language modelling, they found &lt;strong&gt;no clean topical or domain specialisation&lt;/strong&gt;. No expert cleanly owns "math," or "code," or "French." What structure they did find looked more syntactic or positional than semantic, plus some correlation between consecutive tokens routing to the same expert, which reads more like local context redundancy than deliberate task-based division of labor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9izyn6jxfsqgmb4sr0ma.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9izyn6jxfsqgmb4sr0ma.png" alt="experts" width="800" height="534"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Proportion of Expert Distributions&lt;/em&gt;&lt;/p&gt;


&lt;h3&gt;
  
  
  What the Experts Actually Learned
&lt;/h3&gt;

&lt;p&gt;The 2017 paper also did something that turns out to matter a lot for the rest of this story: it looked inside the trained model to see what each expert had specialised in. Table 9 of the paper, from a WMT'14 English→French translation model, shows real, identifiable clustering, but not along the lines you'd guess.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One expert fires heavily on phrasing like "a leading/critical/central role"&lt;/li&gt;
&lt;li&gt;Another clusters around rapid/quick/swift-type intensifiers&lt;/li&gt;
&lt;li&gt;Another around innovation- and research-adjacent nouns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fihe1i8bwiyzhltpgwugs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fihe1i8bwiyzhltpgwugs.png" alt="Experts" width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is real specialisation, but it's at &lt;strong&gt;pattern-level&lt;/strong&gt;, not domain-level. There's no "the translation expert" or "the grammar expert."&lt;/p&gt;

&lt;p&gt;Mixtral 8x7B (2023), at LLM scale on general-purpose language modelling, found no such clean structure. That's the tension this section picks up: &lt;strong&gt;the specialisation debate isn't new&lt;/strong&gt;, and it doesn't have a single settled answer.&lt;/p&gt;

&lt;p&gt;There are at least three plausible explanations for why expert specialisation is still debatable.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Scale changes what specialises.&lt;/strong&gt; At LSTM-with-thousands-of-experts scale on a single task, fine-grained syntactic clusters are easy to detect. At LLM with hundreds of experts per layer scale on a much broader data distribution, that same structure might exist but be diluted across dramatically more traffic per expert.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Task changes what specialises.&lt;/strong&gt; Translation is a much narrower objective than general next-token prediction across a huge, heterogeneous corpus. Narrower tasks might simply produce cleaner, more legible specialisation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Methodology changes what you find.&lt;/strong&gt; "Specialisation" isn't a single measurable quantity, how you probe for it (which tokens you test, which layer you look at, what counts as a "pattern") shapes what you're able to see.&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  5. DeepSeek-V3 and the Current State of the Art
&lt;/h2&gt;

&lt;p&gt;DeepSeek-V3 represents the current high-water mark for this architecture, and it makes two specific bets that build directly on everything above.&lt;/p&gt;

&lt;p&gt;Fine-grained experts + shared experts. Instead of a handful of large experts, DeepSeek-V3 uses many smaller experts per layer, alongside a small number of always-on "shared" experts that every token passes through regardless of what the gate decides. &lt;/p&gt;

&lt;p&gt;Each MoE layer holds 256 routed experts plus 1 shared expert, and the gate selects the top 8 routed experts per token, so every token is processed by 9 experts total out of 257 available in that layer. The idea is to let the shared expert absorb general, broadly-useful knowledge, freeing the 256 routed experts to specialise on narrower, more distinctive patterns than they could if they also had to carry generic load.&lt;/p&gt;

&lt;p&gt;This design choice isn't just architectural, DeepSeek ran empirical patterns to check if it actually does what it's supposed to. Turning off the shared expert causes a large capability drop, consistent with it having absorbed genuinely general-purpose knowledge. Turning off even a small slice of the top routed experts (as little as 1/16 of them) also meaningfully hurts performance; a real, measurable signal that the routed experts are holding specialised, non-redundant information. That's a useful data point to bring back into the specialisation debate from earlier: whatever these experts are specialising in, removing them provably costs the model something, even if we can't cleanly label what that something is.&lt;/p&gt;

&lt;p&gt;Auxiliary-loss-free load balancing. Rather than the balancing loss described earlier, DeepSeek-V3 uses dynamically adjusted per-expert bias terms added to the gate's scores — nudging routing toward underused experts on the fly, without the accuracy cost that a traditional balancing loss can impose on the main training objective.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Simplified DeepSeek-style routing sketch
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;deepseek_moe_layer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;routed_experts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shared_experts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gate_weights&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bias&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;logits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;gate_weights&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;bias&lt;/span&gt;        &lt;span class="c1"&gt;# bias dynamically adjusted for balance
&lt;/span&gt;    &lt;span class="n"&gt;top_k_vals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k_idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;topk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;top_k_weights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;softmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;top_k_vals&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;s&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;shared_experts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# always-on
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;top_k_weights&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k_idx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;routed_experts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;         &lt;span class="c1"&gt;# sparse, routed
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The efficiency numbers. DeepSeek-V3 carries roughly 671 billion total parameters, but activates only around 37 billion per token. That gap is the whole MoE thesis made concrete: enormous total capacity, a fraction of it touched per token — and at 256 experts per layer, "a fraction" means well under 4% of any single layer's routed experts fire for a given token.&lt;/p&gt;

&lt;p&gt;The engineering this demands. A pool this large only works with serious infrastructure underneath it: DeepSeek-V3 uses wide expert parallelism during training, spreading the 256 routed experts across many GPUs so each one handles a large enough batch of tokens to stay efficient, with token routing communicated over high-bandwidth interconnects between nodes. This is a direct, practical instance of the compute-vs-memory tension discussed later in this post, you can't just put 256 experts on one device and expect either training or serving to be fast.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz33r1gotsldnm2r04q28.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz33r1gotsldnm2r04q28.png" alt="Params" width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Compute vs. Memory: MoE Solves One Bottleneck and Sharpens the Other
&lt;/h2&gt;

&lt;p&gt;The "active params" compute gain is real, but it doesn't tell the whole story of how fast MoE models actually feel to use, because being compute or memory bound are two different bottlenecks, and MoE is genuinely lopsided in how it treats them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where MoE wins: compute-bound work.&lt;/strong&gt; For any single token, only a handful of experts run, so the FLOPs cost per token scales with active parameters, not total parameters. This shows up most clearly during &lt;strong&gt;pre-fill&lt;/strong&gt;, where the model processes the input prompt, typically with large batch sizes and math-heavy workloads. Pre-fill is compute-bound by nature, and cutting the required FLOPs is a direct, unambiguous win. It's also the main driver behind why training a 671B-parameter-class model can cost closer to what a much smaller dense model would.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where MoE struggles: memory-bound work.&lt;/strong&gt; The catch shows up on the other side of the ledger:&lt;/p&gt;

&lt;p&gt;1) &lt;strong&gt;The full model still has to live in memory.&lt;/strong&gt; Because &lt;em&gt;any&lt;/em&gt; token can route to &lt;em&gt;any&lt;/em&gt; expert, every expert's weights need to be resident in vRAM/RAM at all times.&lt;br&gt;
2) &lt;strong&gt;Decode is naturally memory-bandwidth-bound, and MoE doesn't help.&lt;/strong&gt; During &lt;strong&gt;decode&lt;/strong&gt;, generating tokens one at a time, typically at small batch sizes, the bottleneck is moving weights from memory to the compute units, not doing arithmetic.&lt;br&gt;
3) &lt;strong&gt;Dynamic, irregular routing adds its own overhead.&lt;/strong&gt; Because each token can route to a different, unpredictable set of experts, fetching the relevant weights is less regular than in a dense model's fixed access pattern, which adds its own memory-traffic cost.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6iiordmnu2ib0k6dyw7a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6iiordmnu2ib0k6dyw7a.png" alt=" " width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Who Actually Benefits — and Why Adoption Isn't a Clean Story
&lt;/h3&gt;

&lt;p&gt;This split has a real consequence that doesn't get talked about enough: &lt;strong&gt;MoE's advantages and disadvantages land very differently depending on who's running the model.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;1) &lt;strong&gt;Independent users and local/open-source deployments&lt;/strong&gt; are usually running at small batch sizes, often a single user, one conversation at a time. That's exactly the decode-heavy, memory-bandwidth-bound regime where MoE's compute savings barely matter. A dense model of comparable &lt;em&gt;active&lt;/em&gt; size can be a more practical fit here, since there's no giant idle parameter pool to store.&lt;br&gt;
2) &lt;strong&gt;Large inference providers running frontier-scale models (1T+ total parameters)&lt;/strong&gt; are the setting where MoE's tradeoffs actually pay off. At that scale, requests are batched heavily across many concurrent users, which pushes the workload back toward compute-bound territory and lets the FLOPs savings materialise as real throughput. Combined with expert parallelism across many GPUs, this is how labs serve enormous total-parameter models at low per-token cost and competitive latency.&lt;/p&gt;

&lt;p&gt;So "is MoE worth it" doesn't have one answer. It depends heavily on batch size and deployment scale. It's a strong architecture for compute-bound, high-concurrency serving, and a comparatively weak fit for the memory-bound, low-batch regime that a lot of independent and local usage actually falls into.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Potential Research Areas
&lt;/h2&gt;

&lt;p&gt;A few directions worth watching, stated with appropriate hedging since none of this is settled:&lt;/p&gt;

&lt;p&gt;1) &lt;strong&gt;Deliberately steering specialisation&lt;/strong&gt; - Biasing initialisation or auxiliary losses toward more domain-aware clustering, essentially a semi-supervised approach to routing, rather than leaving it fully emergent.&lt;br&gt;
2) &lt;strong&gt;Routing stability across layers&lt;/strong&gt; — does a token's full path through the network (its sequence of expert choices, layer by layer) carry any exploitable structure, or is each layer's decision genuinely independent of the others in any way that matters?&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The through-line across three decades of this idea is more consistent than it might seem: a gating network learns to route inputs. Sparsity, introduced in 2017, turned MoE from a clever decomposition trick into a genuine solution to the compute-vs-capacity tradeoff, and that's the part of the story that's now settled and load-bearing in every frontier model.&lt;/p&gt;

&lt;p&gt;What isn't settled is what the experts &lt;em&gt;are&lt;/em&gt;. As expert counts keep climbing, understanding what's actually happening inside the routing may end up mattering as much as the routing mechanism itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Citations &amp;amp; Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;R. A. Jacobs, M. I. Jordan, S. J. Nowlan and G. E. Hinton, "Adaptive Mixtures of Local Experts," in Neural Computation, vol. 3, no. 1, pp. 79-87, March 1991, doi: 10.1162/neco.1991.3.1.79.&lt;/li&gt;
&lt;li&gt;Shazeer, N., Mirhoseini, A., Maziarz, K., Davis, A., Le, Q., Hinton, G., &amp;amp; Dean, J. (2017). 
. ArXiv. &lt;a href="https://arxiv.org/abs/1701.06538" rel="noopener noreferrer"&gt;https://arxiv.org/abs/1701.06538&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Lepikhin, D., Lee, H., Xu, Y., Chen, D., Firat, O., Huang, Y., Krikun, M., Shazeer, N., &amp;amp; Chen, Z. (2020). GShard: Scaling Giant Models with Conditional Computation and Automatic Sharding. ArXiv. &lt;a href="https://arxiv.org/abs/2006.16668" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2006.16668&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Fedus, W., Zoph, B., &amp;amp; Shazeer, N. (2021). Switch Transformers: Scaling to Trillion Parameter Models with Simple and Efficient Sparsity. ArXiv. &lt;a href="https://arxiv.org/abs/2101.03961" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2101.03961&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Jiang, A. Q., Sablayrolles, A., Roux, A., Mensch, A., Savary, B., Bamford, C., Chaplot, D. S., Casas, D. D., Hanna, E. B., Bressand, F., Lengyel, G., Bour, G., Lample, G., Lavaud, L. R., Saulnier, L., Lachaux, M. A., Stock, P., Subramanian, S., Yang, S., . . .  Sayed, W. E. (2024). Mixtral of Experts. ArXiv. &lt;a href="https://arxiv.org/abs/2401.04088" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2401.04088&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Dai, D., Deng, C., Zhao, C., Xu, R. X., Gao, H., Chen, D., Li, J., Zeng, W., Yu, X., Wu, Y., Xie, Z., Li, Y. K., Huang, P., Luo, F., Ruan, C., Sui, Z., &amp;amp; Liang, W. (2024). DeepSeekMoE: Towards Ultimate Expert Specialisation in Mixture-of-Experts Language Models. ArXiv. &lt;a href="https://arxiv.org/abs/2401.06066" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2401.06066&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>deepseek</category>
      <category>google</category>
    </item>
    <item>
      <title>How LLMs Learned to Reason: SFT --&gt; RLHF --&gt; RLVR</title>
      <dc:creator>mayankpallai</dc:creator>
      <pubDate>Wed, 09 Sep 2026 00:09:45 +0000</pubDate>
      <link>https://dev.to/cyprus09/how-llms-learned-to-reason-sft-rlhf-rlvr-1ldh</link>
      <guid>https://dev.to/cyprus09/how-llms-learned-to-reason-sft-rlhf-rlvr-1ldh</guid>
      <description>&lt;h2&gt;
  
  
  1. The Starting Line: The Last Non-Reasoning Flagships
&lt;/h2&gt;

&lt;p&gt;GPT-4.5, DeepSeek-V3, and Claude 3.5 Sonnet share something that has nothing to do with benchmark scores: they were the last major models built entirely on the "pretrain, then instruct-tune" recipe. All internal computation was done in one forward pass per token, with no backtracking, verification or revision mechanisms in place.&lt;/p&gt;

&lt;p&gt;By late 2024, these labs had run into the same big wall: scaling pretraining data and compute had reached a point of saturation. The naive recipe of more tokens, bigger model, more GPU-compute no longer bought equivalent capability gains, particularly on multi-step reasoning tasks. Good output quality became the bottleneck before parameter count did.&lt;/p&gt;

&lt;p&gt;What came after wasn't a bigger version of the same thing. It was a different training paradigm applied &lt;em&gt;on top of&lt;/em&gt; the existing ones. The field pivoted to training models to think before they answer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7yk0hwke0zupbmxe487d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7yk0hwke0zupbmxe487d.png" alt="Timeline" width="800" height="227"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  2. RL Terminologies Recap
&lt;/h2&gt;

&lt;p&gt;Think of a Roomba cleaning a house. The &lt;strong&gt;environment&lt;/strong&gt; is the house; the &lt;strong&gt;state&lt;/strong&gt; is what the Roomba currently senses (position, dirt map, obstacles); an &lt;strong&gt;action&lt;/strong&gt; is a movement decision (turn, advance, suck); a &lt;strong&gt;trajectory&lt;/strong&gt; is one full cleaning run; and the &lt;strong&gt;reward&lt;/strong&gt; is some measure of how much dirt got picked up, dispensed along the way or tallied at the end.&lt;/p&gt;

&lt;p&gt;Map this onto an LLM generating text: the &lt;strong&gt;state&lt;/strong&gt; is the prompt plus every token generated so far; an &lt;strong&gt;action&lt;/strong&gt; is the next token (or, at a coarser grain, the next reasoning step); a &lt;strong&gt;trajectory&lt;/strong&gt; is the full generated sequence; and the &lt;strong&gt;reward&lt;/strong&gt; is a scalar signal applied to that sequence, either at the very end or, in some setups, at intermediate points.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj78i7ikwmarjwxsg65g6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj78i7ikwmarjwxsg65g6.png" alt="Flowchart for Terminologies" width="798" height="189"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The Pipeline, Oversimplified
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pretraining → SFT → RLHF / RLVR&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Pretraining gives the model raw capability and world knowledge from next-token prediction over a huge corpus. SFT and the RL stages that follow are about &lt;em&gt;shaping&lt;/em&gt; that capability toward useful, correct, well-formed behaviour. The rest of this post is a section-by-section zoom into each stage after pretraining.&lt;/p&gt;




&lt;h2&gt;
  
  
  3.1. SFT: Imitation Learning, and Why It Plateaus
&lt;/h2&gt;

&lt;p&gt;Supervised fine-tuning takes a pretrained base model and fine-tunes it on curated (prompt, response) pairs. This is typically written or heavily edited by human annotators to model the response style and quality. It's imitation learning: the model isn't exploring or being scored on outcomes, it's just learning to match a distribution of demonstrated behaviour.&lt;/p&gt;

&lt;p&gt;This works well for instruction-following, tone, and formatting. &lt;/p&gt;

&lt;p&gt;It eventually plateaus for two structural reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bounded by demonstrator quality.&lt;/strong&gt; The model can't exceed the skill ceiling of whoever annotated the data. If no annotator in the labelling pool can reliably solve a hard math problem, the model can't either.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No exploration.&lt;/strong&gt; The model never sees the consequence of a different action than the one demonstrated. It has no signal about &lt;em&gt;why&lt;/em&gt; one continuation was better than another it might have generated on its own.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the core limitation RL-based methods are built to address: give the model a way to try multiple candidates and get some sort of a signal on which one was actually better.&lt;/p&gt;




&lt;h2&gt;
  
  
  3.2 Reinforcment Learning from Human Feedback (RLHF)
&lt;/h2&gt;

&lt;p&gt;The classic RLHF pipeline has 3 stages: an SFT model, a reward model trained on human preference comparisons, and a gradient step that optimises the SFT model against that reward model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt;: &lt;/p&gt;

&lt;p&gt;1) Humans are shown several candidate responses to the same prompt and rank them by preference. &lt;br&gt;
2) That ranking data trains a separate reward model to predict, for any given response, how a human would score it. &lt;br&gt;
3) Once the reward model is trained, it stands in for the human and the policy (LLM) is optimised directly, using &lt;a href="https://arxiv.org/pdf/1707.06347" rel="noopener noreferrer"&gt;PPO (Proximal Policy Optimization)&lt;/a&gt;&lt;br&gt;
4) The labelling process effectively automates itself after the initial ranking data is collected.&lt;/p&gt;

&lt;p&gt;I shall digress here a bit to mention PPO is &lt;em&gt;no longer&lt;/em&gt; the default anymore. DeepSeek's papers (DeepSeekMath, DeepSeek-V3, DeepSeek-R1) use &lt;a href="https://arxiv.org/pdf/2402.03300" rel="noopener noreferrer"&gt;&lt;strong&gt;GRPO&lt;/strong&gt; (Group Relative Policy Optimization)&lt;/a&gt; instead.&lt;/p&gt;

&lt;p&gt;Worth a separate discussion on its own, so we'll save the full internals for a separate blog and just sum it up here below. (Note: Policy &amp;lt;--&amp;gt; LLMs in this context and topic)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PPO&lt;/strong&gt;: The standard policy-gradient method. Generate a response, score it, and update the policy in the direction that increases reward while a clipping term keeps each update close to the old policy to not destabilise training. &lt;br&gt;
Doing this well requires a separate critic network (~same size as the policy) to estimate expected reward at each step, which is expensive since LLM reward is typically only assigned at the end of a sequence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GRPO&lt;/strong&gt;: Same clipped policy-gradient, but drops the critic network. For each prompt, it samples a group of candidate outputs from the current policy, scores each, and uses the group's own mean/variance as the advantage baseline directly which required no learned value estimates. A response better than the group average gets reinforced, one worse than average gets suppressed. Cheaper and simpler.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdeddli3l6rtmjnych5g0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdeddli3l6rtmjnych5g0.png" alt="FlowChart for RLHF" width="798" height="214"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Regardless of PPO vs. GRPO, the more important issue is &lt;em&gt;what the reward model is trained to predict in this step&lt;/em&gt;: human preference, not correctness. This acts as a good proxy for tone, helpfulness, and safety, but it's a poor proxy for reasoning tasks specifically. A human rater judges plausibility, confidence and structure, often not the actual correctness of the output. &lt;/p&gt;

&lt;p&gt;This leads us to the limitation where a confidently wrong answer, well formatted and persuasively argued, can out-score a correct but messier one.&lt;/p&gt;




&lt;h2&gt;
  
  
  3.3. Reinforcement Learning with Verifiable Rewards (RLVR)
&lt;/h2&gt;

&lt;p&gt;RLVR comes after/along with the learned reward model with something more fitting for reasoning tasks: a programmatic and deterministic checker for correctness, with no learned approximation in the loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it Works&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;1) Code: The model's output is compiled to run against unit tests similar to a LeetCode compiler. Pass/Fail quantifies the reward.&lt;br&gt;
2) Math: The final answer is checked against a known ground truth (calculated output), with normalisation (&lt;code&gt;1/2&lt;/code&gt; == &lt;code&gt;0.5&lt;/code&gt;).&lt;br&gt;
3 Structured/Rule-governed tasks (grammar correction, formatting compliance): Rule-based checker validates the output against the fixed specifications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this unlocks reasoning gains that RLHF alone didn't reliably produce&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;1) No reward hacking via sycophancy. A learned reward model can be gamed by outputs that are &lt;em&gt;seemingly&lt;/em&gt; good to the reward model's learned heuristics (confident tone, agreeable phrasing). A compiler has no aesthetic preferences. It either passes or fails.&lt;br&gt;
2) The reward signal scales with problem difficulty. A model gets no partial credit from a unit-test suite for writing confidently and the result is rather deterministic, it either produces working code or it doesn't, regardless of how hard the underlying problem was.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Case Study: DeepSeek's R1 Series of Models
&lt;/h2&gt;

&lt;p&gt;DeepSeek released three closely related artifacts in January 2025 that, taken together, answer three separate questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What happens if you apply RLVR to a base model with &lt;em&gt;no&lt;/em&gt; SFT step at all? → &lt;strong&gt;R1-Zero&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;What breaks in that setup, and how do you fix it? → &lt;strong&gt;R1&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Can the resulting reasoning capability be transferred cheaply into much smaller models? → &lt;strong&gt;R1-Distill&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  4.1. DeepSeek-R1-Zero: Pure RLVR, No SFT Warm Start
&lt;/h2&gt;

&lt;p&gt;R1-Zero was trained directly from a DeepSeek-V3-Base checkpoint using large-scale RLVR (via GRPO). This had no supervised fine-tuning step beforehand.&lt;/p&gt;

&lt;p&gt;The outcome being reasoning benchmark accuracies climbed substantially, and the model spontaneously developed longer chains of thought and self-verification-like behaviour. None of which were part of the reward function.&lt;/p&gt;

&lt;p&gt;But the model also developed serious presentation problems: reasoning chains would mix languages mid-thought (English and Chinese within the same reasoning chains), general readability was poor with the raw output being difficult to follow even when the final answer was correct. &lt;/p&gt;

&lt;p&gt;This is the direct consequence of optimising purely for a verifiable outcome reward with no constraint pushing the intermediate reasoning. The reward function never mentioned the &lt;strong&gt;chain&lt;/strong&gt; being legible, only the final answer mattered.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxtp0vw9nxpgre8w527t5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxtp0vw9nxpgre8w527t5.png" alt="A-ha moment" width="783" height="508"&gt;&lt;/a&gt;&lt;em&gt;The &lt;code&gt;a-ha&lt;/code&gt; moment as highlighted for R1-zero as in their famous paper. Source: &lt;a href="https://github.com/deepseek-ai/DeepSeek-R1/blob/main/DeepSeek_R1.pdf" rel="noopener noreferrer"&gt;DeepSeek Paper&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  4.2. DeepSeek-R1: The Cold-Start SFT Fix
&lt;/h2&gt;

&lt;p&gt;The fix to the above section was to reintroduce a small amount of supervision before the RL stage ("cold start SFT"): a modest set of curated, high-quality chain-of-thought examples used to fine-tune the base model via SFT &lt;em&gt;before&lt;/em&gt; running RLVR. &lt;/p&gt;

&lt;p&gt;This cold-start data was specifically curated for readability and consistent language use, giving the model a coherent starting distribution to explore from, rather than leaving it to discover legible reasoning formats on its own. &lt;/p&gt;

&lt;p&gt;Subsequently, the pipeline proceeded through further RL stages (including a language-consistency reward), followed by rejection sampling and additional SFT on the resulting high-quality outputs, before a final RL pass.&lt;/p&gt;

&lt;p&gt;The result, per DeepSeek's reported benchmarks, is a model that performs on par with OpenAI's o1 on math, code, and STEM reasoning benchmarks, while producing coherent, single-language, readable reasoning traces. A direct nudge away from R1-zero.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh6vt6cjanvmlwrr80biy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh6vt6cjanvmlwrr80biy.png" alt="Deepseek-R1-Thoughtology" width="800" height="317"&gt;&lt;/a&gt;&lt;em&gt;Source: &lt;a href="https://arxiv.org/abs/2504.07128" rel="noopener noreferrer"&gt;Cohere's Thoughtology Paper&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  4.3. R1-Distill: Distillation via SFT Alone
&lt;/h2&gt;

&lt;p&gt;The third piece to this case study answers the question: does the reasoning capability in a 671B-parameter model require RL to reproduce reasoning in a smaller model, or can it just be copied?&lt;/p&gt;

&lt;p&gt;DeepSeek generated a large set of reasoning traces from the full R1 model and used them purely as SFT data to fine-tune smaller dense models (Qwen2.5 and Llama3 based) ranging from 1.5B to 70B parameters. No RL stage was run on the smaller models at all. Just imitation. (Note: We are not talking about Logits-based distillation here, this was purely used as SFT)&lt;/p&gt;

&lt;p&gt;The result is genuinely counterintuitive given everything above in R1-zero covered SFT's limitations: distillation from R1 outperformed running RL from scratch directly on the smaller base models. &lt;/p&gt;

&lt;p&gt;DeepSeek compared distilling into Qwen2.5-32B against applying the same large-scale RLVR recipe directly to a 32B base model, and the distilled version won. &lt;/p&gt;

&lt;p&gt;A few concrete distilled-model numbers from DeepSeek's released benchmarks (AIME 2024 pass@1):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F720k7e0lhhyk067b9lhx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F720k7e0lhhyk067b9lhx.png" alt="Benchmarks" width="800" height="416"&gt;&lt;/a&gt;&lt;em&gt;Source: &lt;a href="https://github.com/deepseek-ai/DeepSeek-R1/blob/main/DeepSeek_R1.pdf" rel="noopener noreferrer"&gt;DeepSeek’s release paper&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The exploration concurred, good reasoning strategies via RLVR seem to require the extra capacity of a large base model. Once those strategies exist as demonstrated behaviour, a much smaller model can absorb them through plain imitation.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Conclusion
&lt;/h2&gt;

&lt;p&gt;We went from single-pass, non-reasoning flagships to models trained with verifiable rewards to actually reason: SFT gave a plateauing imitation baseline, RLHF added preference optimisation but mismatched on correctness, and RLVR fixed that mismatch with checkable outcomes. DeepSeek's R1 lineage showed what this looks like in practice. R1-Zero's raw, undirected RL reasoning; R1's cold-start fix; and R1-Distill's counterintuitive proof that those reasoning patterns transfer via simple imitation, no RL required downstream. &lt;/p&gt;

&lt;p&gt;One side effect along the way: as RL training progressed, chain-of-thought length grew on its own and tracked with accuracy on harder problems, reasoning longer turned out to be a strategy the model discovered, not one it was told to use.&lt;/p&gt;




&lt;h2&gt;
  
  
  Citations &amp;amp; Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;DeepSeek-AI et al., &lt;em&gt;DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning&lt;/em&gt; (2025) — &lt;a href="https://github.com/deepseek-ai/DeepSeek-R1" rel="noopener noreferrer"&gt;github.com/deepseek-ai/DeepSeek-R1&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;DeepSeek-AI et al., &lt;em&gt;DeepSeek-V3 Technical Report&lt;/em&gt; (2024) — &lt;a href="https://arxiv.org/abs/2412.19437" rel="noopener noreferrer"&gt;arxiv.org/abs/2412.19437&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Shao et al., &lt;em&gt;DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models&lt;/em&gt; (2024) — introduces GRPO — &lt;a href="https://arxiv.org/abs/2402.03300" rel="noopener noreferrer"&gt;arxiv.org/abs/2402.03300&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Ouyang et al., &lt;em&gt;Training language models to follow instructions with human feedback&lt;/em&gt; (InstructGPT, 2022) — canonical RLHF pipeline — &lt;a href="https://arxiv.org/abs/2203.02155" rel="noopener noreferrer"&gt;arxiv.org/abs/2203.02155&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Schulman et al., &lt;em&gt;Proximal Policy Optimization Algorithms&lt;/em&gt; (2017) — &lt;a href="https://arxiv.org/abs/1707.06347" rel="noopener noreferrer"&gt;arxiv.org/abs/1707.06347&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Marjanović, S. V., Patel, A., Adlakha, V., Aghajohari, M., BehnamGhader, P., Bhatia, M., Khandelwal, A., Kraft, A., Krojer, B., Lù, X. H., Meade, N., Shin, D., Kazemnejad, A., Kamath, G., Mosbach, M., Stańczak, K., &amp;amp; Reddy, S. (2025). DeepSeek-R1 Thoughtology: Let's think about LLM Reasoning. ArXiv. &lt;a href="https://arxiv.org/abs/2504.07128" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2504.07128&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>ai</category>
      <category>llm</category>
      <category>learning</category>
      <category>deepseek</category>
    </item>
    <item>
      <title>Is Speculative Decoding's Speedup a Hardware Problem or a Model Problem?</title>
      <dc:creator>mayankpallai</dc:creator>
      <pubDate>Sat, 25 Jul 2026 22:15:25 +0000</pubDate>
      <link>https://dev.to/cyprus09/is-speculative-decodings-speedup-a-hardware-problem-or-a-model-problem-7k6</link>
      <guid>https://dev.to/cyprus09/is-speculative-decodings-speedup-a-hardware-problem-or-a-model-problem-7k6</guid>
      <description>&lt;p&gt;&lt;em&gt;A follow-up/sub-part to &lt;a href="https://dev.to/cyprus09/building-a-terminal-based-llm-inference-internals-explorer-part-3-5593"&gt;Part 3 of the LLM inference internals series&lt;/a&gt;. Part 3 built sampling-mode speculative decoding with KV caching on both the draft and verifier sides, and it worked correctly, but the speedup it delivered didn't match expectations set by the paper. This post is the record of chasing that gap: every hypothesis tested, which ones were wrong, and what the real answer turned out to be.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Starting Point: It's Slower, Not Faster
&lt;/h2&gt;

&lt;p&gt;The first full gamma sweep, on an open-ended, opinion-style prompt, came back like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Gamma&lt;/th&gt;
&lt;th&gt;Tokens&lt;/th&gt;
&lt;th&gt;Tok/s&lt;/th&gt;
&lt;th&gt;Acceptance&lt;/th&gt;
&lt;th&gt;Measured Speedup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Verifier-only&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;250&lt;/td&gt;
&lt;td&gt;14.4&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;1.00x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speculative&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;250&lt;/td&gt;
&lt;td&gt;12.3&lt;/td&gt;
&lt;td&gt;39.2%&lt;/td&gt;
&lt;td&gt;0.86x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speculative&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;250&lt;/td&gt;
&lt;td&gt;9.5&lt;/td&gt;
&lt;td&gt;33.6%&lt;/td&gt;
&lt;td&gt;0.66x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Slower across the board. The accept/reject math had already been verified correct, with stable, repeatable acceptance rates and no correctness bugs, so this wasn't a bug. It was a real result that needed an explanation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hypothesis 1: It's MPS Dispatch Overhead
&lt;/h2&gt;

&lt;p&gt;The first instinct was to blame the hardware. Apple Silicon's MPS backend pays a large, roughly fixed dispatch cost per model call that doesn't shrink proportionally with model size. That was confirmed via isolated microbenchmarks: the 6x smaller draft model was only about 2.8x faster per single-token call (21 to 28ms versus 60 to 90ms). Two sequential model calls per round, each paying that fixed tax, looked like the obvious explanation.&lt;/p&gt;

&lt;p&gt;To test this without conflating hardware cost with algorithm cost, I computed a FLOPs-based theoretical speedup using &lt;a href="https://arxiv.org/abs/2211.17192" rel="noopener noreferrer"&gt;Leviathan et al.'s&lt;/a&gt; formula, &lt;code&gt;E[speedup] = (1 - α^(γ+1)) / ((1 - α)(γ·c + 1))&lt;/code&gt;, with a cost ratio &lt;code&gt;c&lt;/code&gt; derived from each model's architecture (layers, hidden size, GQA heads, FLOPs per token via the standard &lt;code&gt;2 × params&lt;/code&gt; approximation) rather than measured milliseconds. That makes the number hardware-agnostic by construction.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Verifier (Qwen2.5-3B-Instruct):  6,171,394,048 FLOPs/token
Draft   (Qwen2.5-0.5B-Instruct):   987,922,432 FLOPs/token
c = 0.1601
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plugging in the real measured acceptance rates:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;γ&lt;/th&gt;
&lt;th&gt;Acceptance&lt;/th&gt;
&lt;th&gt;Theoretical (FLOPs-only)&lt;/th&gt;
&lt;th&gt;Measured (MPS)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;39.2%&lt;/td&gt;
&lt;td&gt;0.99x&lt;/td&gt;
&lt;td&gt;0.86x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;33.6%&lt;/td&gt;
&lt;td&gt;0.71x&lt;/td&gt;
&lt;td&gt;0.66x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;19.7%&lt;/td&gt;
&lt;td&gt;0.55x&lt;/td&gt;
&lt;td&gt;0.55x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This was the first wrong turn. Even with zero hardware overhead, the theoretical ceiling never cleared 1.0x. If MPS overhead were the whole story, the theoretical column should have shown a clear, comfortable win being eaten by dispatch cost. Instead it showed that the algorithm itself, on paper, wasn't going to win at this acceptance rate. Hypothesis 1 was only a partial explanation at best.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hypothesis 2: A Bigger Draft Model Would Fix It
&lt;/h2&gt;

&lt;p&gt;If acceptance rate was the real ceiling, the obvious next move was a closer-sized draft: Qwen2.5-1.5B instead of 0.5B, on the theory that a closer size means a closer distribution match to the verifier.&lt;/p&gt;

&lt;p&gt;Checking this on paper first, via &lt;code&gt;AutoConfig&lt;/code&gt; with no weights downloaded, gave a worse cost ratio: &lt;code&gt;c = 0.5002&lt;/code&gt;, about 2x cheaper than the verifier instead of 6x. Solving the formula for breakeven acceptance rate at each gamma showed the 1.5B candidate would need 69 to 84 percent acceptance just to reach 1.0x, far above anything measured so far. Second wrong turn, ruled out before spending compute on it. A bigger draft looked like trading one problem, low acceptance, for a worse version of the same problem, an unfavorable cost ratio.&lt;/p&gt;

&lt;p&gt;Then I actually ran it. Real numbers, not just theory:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;th&gt;γ&lt;/th&gt;
&lt;th&gt;Acceptance&lt;/th&gt;
&lt;th&gt;Theoretical&lt;/th&gt;
&lt;th&gt;Measured&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;86.4%&lt;/td&gt;
&lt;td&gt;1.18x&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.37x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;19.8%&lt;/td&gt;
&lt;td&gt;0.31x&lt;/td&gt;
&lt;td&gt;0.48x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;39.5%&lt;/td&gt;
&lt;td&gt;0.41x&lt;/td&gt;
&lt;td&gt;0.72x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;30.1%&lt;/td&gt;
&lt;td&gt;0.36x&lt;/td&gt;
&lt;td&gt;0.59x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One run cleared 1.0x by a wide margin, the first of the whole investigation. But three others landed back in the 20 to 40 percent range the FLOPs math had already predicted was a losing zone. This wasn't the bigger-draft theory being confirmed. It was acceptance rate itself being far noisier and more content-dependent than a single-number cost-ratio model could capture. The 1.5B draft wasn't reliably better, it was occasionally much better, which pointed at something other than model size driving the variance.&lt;/p&gt;

&lt;p&gt;There was also a second anomaly in this data. In several rows, measured speedup exceeded the theoretical ceiling, which is supposedly impossible since theoretical was meant to be a zero-overhead upper bound. That turned out to be a real finding, not a bug. FLOPs-based &lt;code&gt;c&lt;/code&gt; assumes cost scales with parameter count, but on MPS, dispatch overhead compresses the real cost gap between a 1.5B and 3B model far more than FLOPs alone would predict. The FLOPs-based ceiling is a valid algorithmic upper bound, but it is not a valid hardware upper bound on MPS, a subtlety worth naming rather than glossing over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hypothesis 3: It's the Prompt
&lt;/h2&gt;

&lt;p&gt;The high-variance 1.5B result raised the real question directly: what made that one run hit 86 percent acceptance? The recurring guess was entropy. A prompt with structured, predictable, lower-branching continuations should let a small draft model track a large verifier much more closely than an open-ended, creative one.&lt;/p&gt;

&lt;p&gt;To test this cleanly, I swept temperature (0.01, 0.3, 0.7) and top_p on the original opinion-style prompt, and acceptance barely moved, staying in the 20 to 45 percent band regardless of sampling settings. Then I swapped the prompt itself: same models, same code, same hyperparameter ranges, but asked for a detailed summary of a structured, factual passage (photosynthesis) instead of an open-ended question, at 500 tokens:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Gamma&lt;/th&gt;
&lt;th&gt;Tok/s&lt;/th&gt;
&lt;th&gt;Acceptance&lt;/th&gt;
&lt;th&gt;Measured Speedup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Verifier-only&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;13.1-14.3&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;1.00x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speculative&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;12.8-15.5&lt;/td&gt;
&lt;td&gt;43.6-54.0%&lt;/td&gt;
&lt;td&gt;0.95x-1.16x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speculative&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;13.7-15.3&lt;/td&gt;
&lt;td&gt;45.0-52.6%&lt;/td&gt;
&lt;td&gt;0.99x-1.08x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's the first time changing a single variable moved acceptance meaningfully, and it wasn't temperature or top_p, it was the content itself. Structured, factual text is where this draft and verifier pair actually performs. Open-ended, creative text is where it doesn't. This became the real answer to what causes the acceptance ceiling here: not the models being poorly matched in general, but poorly matched specifically on high-branching-entropy content, a hypothesis Phase 4's entropy correlation study can now test directly instead of guessing at.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hypothesis 4: It's Still Just MPS, Even at High Acceptance
&lt;/h2&gt;

&lt;p&gt;With acceptance now reliably in the 45 to 54 percent range and several runs already crossing 1.0x on PyTorch and MPS, the last open question was whether the shortfall from the 1.5 to 2x range reported in the literature was still a hardware story. To check, I ran the same models (GGUF, Q4_K_M quantization) through &lt;a href="https://github.com/ggml-org/llama.cpp" rel="noopener noreferrer"&gt;llama.cpp&lt;/a&gt; on the same photosynthesis-style prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;n_draft   = 4
n_predict = 504
n_accept  = 391 / 448 drafted  -&amp;gt;  87.277% acceptance
decoded 504 tokens in 10.581s -&amp;gt; 47.6 tok/s (speculative)
verifier-only baseline (same prompt, same runtime): 50.0 tok/s
Speedup: 47.6 / 50.0 ≈ 0.95x
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;llama.cpp's Metal backend is genuinely faster in absolute terms, roughly 3x the raw tok/s of the PyTorch and MPS setup on both the baseline and the speculative run, confirming the earlier microbenchmark finding that MPS pays real dispatch overhead PyTorch doesn't fully hide. But at 87.3 percent acceptance, well above anything achieved on PyTorch, llama.cpp's own speedup was still around 0.95x, not the 1.5 to 2x hoped for. Fourth wrong turn. If MPS and PyTorch overhead were the primary blocker, a near-zero-overhead runtime at excellent acceptance should have shown a clear win. It didn't.&lt;/p&gt;

&lt;p&gt;llama.cpp printed something that pointed at the real remaining factor: 35.7 percent of total wall-clock was unaccounted time, not verifier compute, not draft compute, not sampling, but round-trip and orchestration cost between the draft and verify phases. That's consistent with both implementations, this repo's and llama.cpp's CLI example, running draft and verify strictly sequentially, round after round, rather than pipelining verification of round N with drafting of round N+1.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Explains the Gap
&lt;/h2&gt;

&lt;p&gt;Putting all four hypotheses together, in order of how much each one turned out to matter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Per-round orchestration overhead, not per-call dispatch cost, is the main remaining bottleneck&lt;/strong&gt; at good acceptance rates, confirmed by llama.cpp hitting the same 0.95x ceiling at 87 percent acceptance despite near-zero dispatch overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content and entropy drive acceptance rate far more than temperature, top_p, or draft model size&lt;/strong&gt;, the single biggest, most reproducible lever found in this whole investigation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MPS dispatch overhead is real but secondary.&lt;/strong&gt; It explains part of the PyTorch versus llama.cpp absolute speed gap, but not why even the fast runtime falls short of the literature's numbers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A bigger draft model is not a reliable fix.&lt;/strong&gt; It's occasionally much better, but with high enough variance that it isn't a dependable lever on its own.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why the Paper's Numbers Are Higher
&lt;/h2&gt;

&lt;p&gt;Leviathan et al.'s headline results didn't come from one trick. They stack several advantages this setup doesn't have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A much larger size gap&lt;/strong&gt; (T5-XXL to T5-small, roughly 180x parameters) versus this pairing's roughly 6x. A bigger absolute verifier cost makes batching gamma plus 1 tokens into one pass save proportionally more wall-clock time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drafts trained to align with the verifier&lt;/strong&gt;, not independently instruction-tuned same-family siblings. This is very likely the single largest acceptance-rate lever available, and this repo's Qwen2.5-0.5B and 3B pair has none of it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pipelined, overlapped execution&lt;/strong&gt; between draft and verify stages, rather than strict sequential round-trips, directly addressing the orchestration overhead this investigation found via llama.cpp's unaccounted time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TPU and datacenter-GPU dispatch overhead&lt;/strong&gt;, lower even than llama.cpp's Metal backend, shrinking the fixed per-round tax further.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these is a bug in this implementation. They're conditions the paper's results depended on that this project, by design (local, from scratch, consumer hardware, off-the-shelf same-family models), doesn't have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Draft and verifier size ratio and training.&lt;/strong&gt; A 6x parameter gap versus the paper's roughly 180x, and no distillation. The draft was never trained to mimic the verifier, which is very likely the dominant reason acceptance tops out where it does even on favorable content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sequential execution only.&lt;/strong&gt; Both this implementation and the llama.cpp comparison run draft and verify strictly in sequence. No round-pipelining was attempted, so the roughly 35.7 percent orchestration overhead measured here isn't necessarily a hard floor, just what this simple execution model costs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hardware and runtime.&lt;/strong&gt; Results span PyTorch and MPS, and llama.cpp and Metal, on Apple Silicon only. No datacenter GPU or TPU data point exists to confirm how much of the remaining gap is Apple Silicon specific versus fundamental to unpipelined two-model decoding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sample size.&lt;/strong&gt; Acceptance-rate variance was large enough (19.8 to 86.4 percent on the same 1.5B and 3B pair, same gamma) that single-prompt, few-run comparisons should be read as suggestive, not conclusive. The entropy and content hypothesis needs Phase 4's larger prompt set to confirm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FLOPs approximation.&lt;/strong&gt; The &lt;code&gt;2 × params&lt;/code&gt; estimate doesn't model attention's quadratic term in sequence length, and the theoretical formula assumes no fixed per-round overhead. Both simplifications that this investigation's own data (measured beating theoretical, llama.cpp's unaccounted time) shows don't fully hold in practice.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The speedup gap turned out to be &lt;strong&gt;mostly an acceptance-rate and orchestration problem, not a hardware problem&lt;/strong&gt;. MPS dispatch overhead is real, but even llama.cpp's near-zero-overhead Metal backend topped out around 0.95x at 87% acceptance, which rules out hardware as the primary bottleneck. The &lt;strong&gt;biggest lever by far was content entropy&lt;/strong&gt;: structured, factual prompts pushed acceptance from the 20–40% range up to 45–54%, while temperature and top_p barely moved the needle. A bigger draft model helped occasionally but was too high-variance to trust on its own. The paper's 1.5–2x numbers depend on conditions this setup doesn't have: a far larger size gap, a draft model &lt;strong&gt;distilled to match the verifier&lt;/strong&gt;, and pipelined rather than sequential execution. None of that makes this implementation wrong, it just means the ceiling here is lower than the literature's headline number, for well-understood reasons.&lt;/p&gt;




&lt;h2&gt;
  
  
  Citations &amp;amp; Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Leviathan, Y., Kalman, M., &amp;amp; Matias, Y. (2023). &lt;a href="https://arxiv.org/abs/2211.17192" rel="noopener noreferrer"&gt;Fast Inference from Transformers via Speculative Decoding&lt;/a&gt;. &lt;em&gt;ICML 2023&lt;/em&gt;. The original paper this investigation's formula and baseline expectations come from.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://huggingface.co/Qwen/Qwen2.5-3B-Instruct" rel="noopener noreferrer"&gt;Qwen2.5-3B-Instruct&lt;/a&gt; and &lt;a href="https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct" rel="noopener noreferrer"&gt;Qwen2.5-0.5B-Instruct&lt;/a&gt; model cards — architecture and parameter details used for the FLOPs-based cost ratio.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/ggml-org/llama.cpp" rel="noopener noreferrer"&gt;llama.cpp&lt;/a&gt; — the Metal-backend runtime used for the near-zero-dispatch-overhead comparison in Hypothesis 4.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Series:&lt;/strong&gt; Part 3, Speculative Decoding: Sampling-Mode Accept/Reject, leads into this post, which leads into Part 4, The Empirical Study &lt;em&gt;(coming)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>machinelearning</category>
      <category>performance</category>
    </item>
    <item>
      <title>Building a Terminal Based LLM Inference Internals Explorer - Part 3: Speculative Decoding</title>
      <dc:creator>mayankpallai</dc:creator>
      <pubDate>Fri, 24 Jul 2026 16:02:54 +0000</pubDate>
      <link>https://dev.to/cyprus09/building-a-terminal-based-llm-inference-internals-explorer-part-3-5593</link>
      <guid>https://dev.to/cyprus09/building-a-terminal-based-llm-inference-internals-explorer-part-3-5593</guid>
      <description>&lt;h2&gt;
  
  
  Part 3: Speculative Decoding via Sampling-Mode Accept/Reject
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Part 3 of a 4-part series on system-level LLM inference internals. Part 1 tracked entropy during decode; Part 2 measured attention sinks during prefill. This one implements sampling-mode speculative decoding: a small draft model proposes tokens, a large verifier checks them in one batched pass, and a probability-ratio test decides what survives.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Core Idea
&lt;/h2&gt;

&lt;p&gt;Text generation is sequential: each token's distribution depends on all previous tokens, so a large model generates one token per full forward pass. Speculative decoding breaks that: a small, fast draft model proposes multiple tokens at once, and a large verifier model checks all of them in a single batched forward pass. The trick is making this mathematically sound, so the output distribution still matches sampling from the verifier alone, not some hybrid of draft and verifier.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;What We Build&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Per-token entropy tracker, visualized in real time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Attention sink detector, context health scoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3 — this post&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Speculative decoding: sampling-mode accept/reject&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Empirical study: correlation plots across 50 prompts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The Accept/Reject Mechanism
&lt;/h2&gt;

&lt;p&gt;The hard part isn't drafting tokens — it's accepting or rejecting them in a way that provably preserves the verifier's distribution. For each draft token, the core test is a probability ratio:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;accept_prob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p_verify&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;p_draft&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p_draft&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;accept_prob&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;continue&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;If &lt;code&gt;p_verify &amp;gt; p_draft&lt;/code&gt;: the verifier is &lt;em&gt;more confident&lt;/em&gt; than the draft in this token, so &lt;code&gt;accept_prob&lt;/code&gt; clamps to 1 — always accept.&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;p_verify &amp;lt; p_draft&lt;/code&gt;: the verifier is &lt;em&gt;less confident&lt;/em&gt;, so accept only with probability &lt;code&gt;p_verify / p_draft&lt;/code&gt;. This is the rate that exactly cancels out the "extra" mass the draft added.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On rejection, you don't just take the verifier's argmax — you resample from what's left over:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;residual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;verify_probs&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;draft_dist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;residual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;residual&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;residual&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;correction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;multinomial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;residual&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_samples&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;residual&lt;/code&gt; is &lt;code&gt;max(0, p_verify - p_draft)&lt;/code&gt; — the verifier's probability mass that isn't already accounted for by the draft's guess. Sampling from it, rather than the verifier's raw distribution, is what makes the whole thing add up: &lt;strong&gt;accept + resample composes back to exactly &lt;code&gt;p_verify&lt;/code&gt;&lt;/strong&gt;, so the output is provably indistinguishable from sampling the verifier alone, even though the draft moved first.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;draft_dist&lt;/code&gt; is the draft's full filtered distribution over the whole vocabulary, not just the probability of the token it sampled — the residual subtraction needs to know what the draft thought about every token, not only the one that got drawn.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Worked Example
&lt;/h3&gt;

&lt;p&gt;Say the draft samples "cat" at some position, with &lt;code&gt;p_draft(cat) = 0.6&lt;/code&gt;. The verifier doesn't sample its own token here — it just reads what probability &lt;em&gt;it&lt;/em&gt; would have assigned to "cat" off its own distribution. Say &lt;code&gt;p_verify(cat) = 0.3&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;accept_prob = min(1, 0.3 / 0.6) = 0.5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The verifier is &lt;em&gt;less&lt;/em&gt; confident in "cat" than the draft was, so "cat" only survives a coin flip at 50%. Say it fails, so "cat" is rejected.&lt;/p&gt;

&lt;p&gt;Now build the residual. Suppose the (simplified) vocabulary is just &lt;code&gt;{cat, dog, fox}&lt;/code&gt;, the draft's own distribution at this position was &lt;code&gt;cat=0.6, dog=0.3, fox=0.1&lt;/code&gt;, and the verifier's distribution is &lt;code&gt;cat=0.3, dog=0.5, fox=0.2&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;draft_dist&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dog&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;residual&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="n"&gt;dog&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;# max(0, verify - draft)
&lt;/span&gt;&lt;span class="n"&gt;normalized&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="n"&gt;dog&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.67&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.33&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"cat" gets zeroed out of the residual entirely — it already had its shot during the accept/reject coin flip, so it can't be picked again as its own replacement. That would double-count its mass. Note the residual uses the draft's full distribution, not just &lt;code&gt;p_draft(cat)&lt;/code&gt; — "dog" and "fox" both had real draft mass too, and that mass has to be subtracted out just like "cat"'s did, or the residual overstates how much the draft actually left on the table for them. The correction token is sampled from &lt;code&gt;{dog: 0.67, fox: 0.33}&lt;/code&gt; — say "dog" comes out.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhgx17daw1wpka4t93yh7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhgx17daw1wpka4t93yh7.png" alt="cats-dogs" width="799" height="583"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One more detail worth being explicit about: rejection &lt;strong&gt;stops the round right there&lt;/strong&gt;. If this was draft token 3 of 4, token 4 is discarded unchecked — it was never verified, and it was conditioned on the now-rejected "cat," so it doesn't causally follow the corrected sequence anymore. The next round's draft phase starts fresh from right after "dog," not from the discarded token 4.&lt;/p&gt;

&lt;p&gt;If every draft token in the round survives, there's one more free token to collect — the verifier already computed logits one position past the last draft token, so sampling from those is essentially free:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;bonus_logits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;prefix_len&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;gamma&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;bonus_probs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;top_p_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;softmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bonus_logits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;top_p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;bonus_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;multinomial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bonus_probs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_samples&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the payoff case: &lt;code&gt;gamma + 1&lt;/code&gt; tokens for the cost of one verifier forward pass.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sampling vs Greedy
&lt;/h2&gt;

&lt;p&gt;Two variants exist in the literature: &lt;strong&gt;greedy&lt;/strong&gt; (draft argmax, accept iff verifier argmax matches — deterministic, higher acceptance rates) and &lt;strong&gt;sampling&lt;/strong&gt; (stochastic, uses the probability-ratio test above).&lt;/p&gt;

&lt;p&gt;Parts 1–2 use stochastic sampling at &lt;code&gt;temperature=0.7, top_p=0.9&lt;/code&gt;, so this implementation uses sampling mode to keep acceptance rates comparable across the series. The formula &lt;code&gt;min(1, p_verify(x)/p_draft(x))&lt;/code&gt; is what the literature proves correct — it guarantees the output distribution equals the verifier's, token for token, regardless of what the draft proposed.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Flow
&lt;/h2&gt;

&lt;p&gt;Each round has three phases. The draft phase proposes &lt;code&gt;gamma&lt;/code&gt; tokens one at a time from the small model, reusing a KV cache so each new token costs one incremental forward step rather than a full replay of the prefix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;draft_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;use_cache&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;past_key_values&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gamma&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;model_input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input_ids&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="n"&gt;draft_ids_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]]],&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;position_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="n"&gt;prefix_len&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;draft_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;past_key_values&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;position_ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;position_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;use_cache&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;past_key_values&lt;/span&gt;
    &lt;span class="n"&gt;logits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;token_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sample_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;draft_ids_list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;draft_probs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each draft token conditions on the ones before it, and both the token ID and its probability under the draft's filtered distribution get carried forward — &lt;code&gt;p_draft&lt;/code&gt; is needed later for the accept/reject ratio. The explicit &lt;code&gt;position_ids&lt;/code&gt; matters here: HuggingFace models don't infer a token's absolute position from &lt;code&gt;past_key_values&lt;/code&gt; alone, so an incremental call with cache but no position IDs would silently assume position 0 and corrupt every rotary embedding downstream. Passing &lt;code&gt;prefix_len + i&lt;/code&gt; keeps attention and position encoding correct even though only one new token enters the forward pass.&lt;/p&gt;

&lt;p&gt;The verify phase is a single batched call over the whole thing at once. On the very first round there's no cache yet, so the verifier prefills on the full prompt plus all &lt;code&gt;gamma&lt;/code&gt; draft tokens; every round after that, it reuses the cache and only forwards the newest accepted token plus the new draft tokens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;new_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input_ids&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;input_ids&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
&lt;span class="n"&gt;new_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;new_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;draft_ids&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;verifier_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;past_key_values&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;use_cache&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;logits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gamma&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):]&lt;/span&gt;  &lt;span class="c1"&gt;# last gamma+1 positions: draft scores + bonus
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frsu6wkdut3orzos8ozjx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frsu6wkdut3orzos8ozjx.png" alt="Flow" width="799" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the actual source of the speedup: instead of &lt;code&gt;gamma&lt;/code&gt; sequential verifier calls, one forward pass produces logits for every draft position simultaneously. The accept/reject phase then walks through those &lt;code&gt;gamma&lt;/code&gt; positions (shown above), and the main loop just appends whatever survives and starts the next round.&lt;/p&gt;

&lt;p&gt;One easy-to-miss requirement: both models must sample with the &lt;em&gt;same&lt;/em&gt; temperature and top_p. The ratio &lt;code&gt;p_verify(x) / p_draft(x)&lt;/code&gt; is only meaningful if both probabilities were computed under identical filtering, &lt;code&gt;sample_token&lt;/code&gt; and the verifier's inline softmax + &lt;code&gt;top_p_filter&lt;/code&gt; call share the same &lt;code&gt;TEMPERATURE&lt;/code&gt;/&lt;code&gt;TOP_P&lt;/code&gt; constants for exactly this reason.&lt;/p&gt;

&lt;p&gt;This is the actual source of the speedup: instead of &lt;code&gt;gamma&lt;/code&gt; sequential verifier calls, one forward pass produces logits for every draft position simultaneously. The accept/reject phase then walks through those &lt;code&gt;gamma&lt;/code&gt; positions (shown above), and the main loop just appends whatever survives and starts the next round.&lt;/p&gt;

&lt;p&gt;One easy-to-miss requirement: both models must sample with the &lt;em&gt;same&lt;/em&gt; temperature and top_p. The ratio &lt;code&gt;p_verify(x) / p_draft(x)&lt;/code&gt; is only meaningful if both probabilities were computed under identical filtering, &lt;code&gt;sample_token&lt;/code&gt; and the verifier's inline softmax + &lt;code&gt;top_p_filter&lt;/code&gt; call share the same &lt;code&gt;TEMPERATURE&lt;/code&gt;/&lt;code&gt;TOP_P&lt;/code&gt; constants for exactly this reason.&lt;/p&gt;




&lt;h2&gt;
  
  
  KV Caching on Both Sides
&lt;/h2&gt;

&lt;p&gt;Recomputing a full forward pass over the entire sequence on every single-token step is the obvious thing to avoid, the model has already seen every prior token, so there's no reason to make it re-derive their key/value projections each round. Both the draft phase and the verify phase are built around a KV cache: each side keeps its own &lt;code&gt;past_key_values&lt;/code&gt;, and every subsequent call forwards only the newest token(s) rather than the whole sequence so far.&lt;/p&gt;

&lt;p&gt;Without caching, each draft token costs a full forward pass over everything generated so far:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# no cache: full recompute every iteration
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gamma&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;logits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;draft_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generated&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;logits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;token_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sample_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;generated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;generated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="n"&gt;token_id&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With caching, the model only sees the newest token each step — everything before it is already encoded in &lt;code&gt;past_key_values&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;draft_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;use_cache&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;past_key_values&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gamma&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;model_input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input_ids&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="n"&gt;draft_ids_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]]],&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;position_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="n"&gt;prefix_len&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;draft_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;past_key_values&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;position_ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;position_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;use_cache&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;past_key_values&lt;/span&gt;
    &lt;span class="n"&gt;logits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;token_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sample_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;draft_ids_list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;draft_probs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The verifier's cache works the same way across rounds: instead of re-forwarding the entire generated sequence every round, it only forwards the newest accepted token plus the newly proposed draft tokens on top of an already-cached prefix. The one difference from the draft side is the very first round, where there's no cache yet — that call has to prefill on the full prompt, not just its last token, otherwise the verifier would be scoring draft tokens with no context on what came before them.&lt;/p&gt;

&lt;p&gt;Cost per round now scales with the number of &lt;em&gt;new&lt;/em&gt; tokens (&lt;code&gt;O(1)&lt;/code&gt; for the draft's incremental steps), not with how much has already been generated (&lt;code&gt;O(prefix)&lt;/code&gt; for a full re-forward). Measuring both versions head to head on the same prompt and gamma:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No cache:  100 tokens in 14.9s (6.7 tok/s), 26 rounds
Cached:    100 tokens in 7.1s  (14.2 tok/s), 36 rounds
Speedup from caching: 2.12x
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Caching alone more than doubles throughput on this setup, and the gap only grows with sequence length, since the no-cache cost is quadratic in tokens generated while the cached cost is linear.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fab4u2lh2jf4ht2lk0scp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fab4u2lh2jf4ht2lk0scp.png" alt="KV-Cache" width="800" height="401"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Entropy-Guided Stopping
&lt;/h2&gt;

&lt;p&gt;The draft phase above always proposes a fixed &lt;code&gt;gamma&lt;/code&gt; tokens per round. But the draft model knows, at each step, how confident it is in its own guess, that's exactly what Part 1's entropy tracker measures. If the draft's normalized entropy at a position crosses a threshold, it's a signal the draft itself is unsure, and a token it's unsure about is a token likely to get rejected anyway. So instead of always proposing the full &lt;code&gt;gamma&lt;/code&gt;, the draft phase checks its own entropy before sampling each token and stops proposing early once it crosses that threshold:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;normalized_entropy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compute_entropy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;normalized_entropy&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;ENTROPY_STOP_THRESHOLD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;break&lt;/span&gt;

&lt;span class="n"&gt;token_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sample_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The round always proposes at least one token, so a round can never come back empty. Everything downstream — accept/reject, acceptance-rate bookkeeping, the bonus-token logic — is unchanged: a round that stopped early just proposed fewer tokens, exactly as if &lt;code&gt;gamma&lt;/code&gt; had been smaller for that one round.&lt;/p&gt;

&lt;p&gt;This isn't primarily a speedup feature — cutting a proposal short saves a small draft forward pass, but the verifier's per-round cost is roughly the same regardless. The real value is data: &lt;code&gt;entropy_trace&lt;/code&gt; records the draft's own uncertainty at every proposed position, giving Phase 4 a direct per-token signal to line up against acceptance, rather than inferring the entropy/acceptance link from acceptance rate alone.&lt;/p&gt;




&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;With caching on both sides, here's a gamma sweep against the verifier-only baseline, asking for a detailed summary of a factual, structured passage at 500 tokens, across 5 repeated runs per gamma:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Gamma&lt;/th&gt;
&lt;th&gt;Tok/s&lt;/th&gt;
&lt;th&gt;Acceptance (range, mean)&lt;/th&gt;
&lt;th&gt;Speedup (range, mean)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Verifier-only&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;13.9–14.1&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;1.00x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speculative&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;13.8–14.7&lt;/td&gt;
&lt;td&gt;47.8–50.6%, mean 49.2%&lt;/td&gt;
&lt;td&gt;0.99x–1.04x, mean 1.02x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speculative&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;13.7–15.3&lt;/td&gt;
&lt;td&gt;45.0–52.6%, mean 48.8%&lt;/td&gt;
&lt;td&gt;0.99x–1.08x, mean 1.04x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Acceptance held in the low-to-mid 40s for both gamma values, and measured speedup tracked close from the acceptance rate — gamma=6 almost exactly, gamma=4 losing a bit more to overhead. That's consistent with a single rejection discarding every draft token after it in that round: higher gamma means more wasted draft work per rejection, not just a bigger payoff when everything's accepted. The accept/reject math held up throughout — stable, repeatable acceptance rates round to round.&lt;/p&gt;

&lt;p&gt;That acceptance rate is notably higher than an early sweep on an open-ended, opinion-style prompt, which landed in the 20–40% range regardless of temperature/top_p. Structured, factual content gives the draft model an easier job predicting what the verifier would say next than open-ended, creative text does — a first, real signal ahead of Phase 4's dedicated study.&lt;/p&gt;

&lt;p&gt;The natural next question is &lt;em&gt;why&lt;/em&gt; even the best runs here land near 1.0x rather than the 1.5–2x speedups reported in the literature. That's its own investigation, including a cross-check against llama.cpp's Metal backend to separate hardware/runtime effects from the algorithm itself: &lt;em&gt;&lt;a href="https://dev.to/cyprus09/is-speculative-decodings-speedup-a-hardware-problem-or-a-model-problem-7k6"&gt;Is Speculative Decoding's Speedup a Hardware Problem or a Model Problem?&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;th&gt;Link&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GitHub repo&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/cyprus09/llm-inference-lab" rel="noopener noreferrer"&gt;https://github.com/cyprus09/llm-inference-lab&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speculative Decoding (Leviathan et al., 2023)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://arxiv.org/abs/2211.17192" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2211.17192&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speculative Decoding with Large Language Models (Chen et al., 2023)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://arxiv.org/abs/2302.01318" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2302.01318&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;StreamingLLM (Xiao et al., 2023)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://arxiv.org/abs/2309.17453" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2309.17453&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;&lt;strong&gt;Series:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: Entropy Tracker&lt;/li&gt;
&lt;li&gt;Part 2: Attention Sink Detector&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 3: Speculative Decoding, Sampling-Mode Accept/Reject&lt;/strong&gt; &lt;em&gt;(you are here)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Part 4: The Empirical Study &lt;em&gt;(coming)&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>pytorch</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building a Terminal Based LLM Inference Internals Explorer - Part 2: Attention Sinks</title>
      <dc:creator>mayankpallai</dc:creator>
      <pubDate>Wed, 15 Jul 2026 19:55:50 +0000</pubDate>
      <link>https://dev.to/cyprus09/building-a-terminal-based-llm-inference-internals-explorer-part-2-p3f</link>
      <guid>https://dev.to/cyprus09/building-a-terminal-based-llm-inference-internals-explorer-part-2-p3f</guid>
      <description>&lt;h2&gt;
  
  
  Part 2: The Attention Sink Detector
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Part 2 of a 4-part series on system-level LLM inference internals. Part 1 built the entropy tracker; this one looks one step earlier in the pipeline — at prefill, before a single token is generated.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Where This Fits
&lt;/h2&gt;

&lt;p&gt;Part 1 tracked entropy during decode: how confident the model is, one generated token at a time. This post goes further upstream. Before decode even starts, the model runs a single forward pass over the entire prompt, and that pass is where attention gets distributed across every token in the context.&lt;/p&gt;

&lt;p&gt;The unifying thesis from Part 1 said context quality shapes attention distribution during prefill, and attention distribution shapes generation confidence during decode. This post is where that first link gets measured directly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcw03z6bxj7c70zucfzh5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcw03z6bxj7c70zucfzh5.png" alt="Main Pipeline" width="800" height="120"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;What We Build&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Per-token entropy tracker, visualized in real time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2 — this post&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Attention sink detector, context health scoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Entropy-guided adaptive speculative decoding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Empirical study: correlation plots proving the causal chain&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The Phenomenon
&lt;/h2&gt;

&lt;p&gt;Across a wide range of transformer LLMs, a small number of tokens, very often just the first token in the sequence — receive a hugely disproportionate share of attention from almost every later query, in almost every head, almost regardless of semantic relevance. StreamingLLM (Xiao et al., 2023) is the paper that named this: &lt;strong&gt;attention sinks&lt;/strong&gt;, and the tokens absorbing the attention are &lt;strong&gt;sink tokens&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The mechanism comes straight out of the softmax constraint. Attention weights over keys are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;a_i = exp(q · k_i) / Σ_j exp(q · k_j)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This always sums to 1. A query can't express "nothing here is relevant" by producing low weight everywhere — the weights must still add up to 1 across all keys. So the model needs a release valve: somewhere to dump residual probability mass when nothing in the context is a strong match. Position 0 is the natural candidate, since it's visible to every query in a causal model and it's the one position guaranteed to exist as a valid attention target in every training sequence, at every length, regardless of anything else.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuesctznnvqtjo279lliz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuesctznnvqtjo279lliz.png" alt="Softmax" width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why this matters practically, per the StreamingLLM paper: if you're managing a KV cache for long-context or streaming generation and evict the sink token to save memory, perplexity spikes — even though position 0 seems completely irrelevant by the time you're thousands of tokens deep. Their fix is to pin the first few tokens' KV pairs permanently, alongside a sliding window of recent tokens.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 0: Getting the Numbers to Exist at All
&lt;/h2&gt;

&lt;p&gt;The first obstacle isn't conceptual, it's mechanical. &lt;code&gt;SDPA&lt;/code&gt; and FlashAttention kernels — the fast, default attention implementations — never materialize the full &lt;code&gt;seq_len × seq_len&lt;/code&gt; attention matrix. That's the entire point of them: they compute attention output through tiled, online-softmax kernels, discarding each block's partial scores as soon as it's consumed. Great for speed and memory at long context. Useless if you want to look at the numbers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AutoModelForCausalLM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Qwen/Qwen2.5-3B-Instruct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;attn_implementation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eager&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# non-negotiable for this phase
&lt;/span&gt;    &lt;span class="n"&gt;torch_dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mps&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;attn_implementation="eager"&lt;/code&gt; is the only path where &lt;code&gt;output_attentions=True&lt;/code&gt; returns real tensors instead of &lt;code&gt;None&lt;/code&gt;. It's slower and considerably more memory-hungry — for a &lt;code&gt;seq_len × seq_len&lt;/code&gt; matrix per head per layer, that adds up fast — but for a single prefill pass on a 3B model on Apple Silicon, it's a completely reasonable trade.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 1: The Prefill Analyzer
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Aggregating Attention
&lt;/h3&gt;

&lt;p&gt;A single forward pass with &lt;code&gt;output_attentions=True&lt;/code&gt; returns one tensor per layer, each shaped &lt;code&gt;(batch, num_heads, seq_len, seq_len)&lt;/code&gt;. Averaging across layers and heads gives one matrix: &lt;code&gt;attn[i, j]&lt;/code&gt; = how much query token &lt;code&gt;i&lt;/code&gt; attends to key token &lt;code&gt;j&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;aggregate_attention&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attentions&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Mean attention matrix across layers and heads.
    Returns (seq_len, seq_len); attn[i, j] = query i&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s attention to key j.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;stacked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;layer_attn&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;layer_attn&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;attentions&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;stacked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Causal-Normalized Attention Received
&lt;/h3&gt;

&lt;p&gt;A raw column sum over this matrix favors early tokens for a boring reason: token &lt;code&gt;j&lt;/code&gt; is only visible to queries &lt;code&gt;i &amp;gt;= j&lt;/code&gt;, so early positions have simply had more chances to be attended to. Dividing by the number of queries that could actually see each position gives a fair, comparable-across-positions score:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mean_attention_received&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attn_matrix&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;seq_len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attn_matrix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;col_sums&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attn_matrix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;valid_queries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seq_len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;col_sums&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;valid_queries&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdfnmfr59clru7rxh5at0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdfnmfr59clru7rxh5at0.png" alt="Causal" width="800" height="866"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Structural Tokens Aren't Content
&lt;/h3&gt;

&lt;p&gt;Every ChatML-templated prompt carries fixed scaffolding — &lt;code&gt;&amp;lt;|im_start|&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;|im_end|&amp;gt;&lt;/code&gt;, role-name tokens, and the newlines the template inserts around them. These tokens are content-independent: their identity is fixed by the template, not by what was actually said. They need to be excluded from the &lt;em&gt;baseline&lt;/em&gt; used to judge what "normal" attention looks like, or they distort the comparison for every real content token in the prompt.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_structural_positions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Positions fixed by the ChatML template rather than by content.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input_ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;special_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;all_special_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;im_start_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;convert_tokens_to_ids&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;|im_start|&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;im_end_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;convert_tokens_to_ids&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;|im_end|&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;structural&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tid&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tid&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;special_ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;structural&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;im_start_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;im_end_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;structural&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# role name (after im_start) or newline (after im_end)
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;im_start_id&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;structural&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# newline after the role name specifically
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;structural&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Phase 2: Getting the Statistics Right (Two Times)
&lt;/h2&gt;

&lt;p&gt;The first version of the sink-scoring logic looked reasonable and produced numbers that were, on inspection, badly wrong. Getting this right took two separate corrections.&lt;/p&gt;

&lt;h3&gt;
  
  
  Correction 1 — z-score, not skew-blind mean/std
&lt;/h3&gt;

&lt;p&gt;Attention weight is strictly non-negative and heavily right-skewed: most tokens sit near zero, a handful sit higher. A plain mean/std z-score assumes something closer to a normal distribution, and against a skewed population that assumption quietly inflates what counts as "anomalous." A first pass at a threshold of 2.0 reliably flagged over a dozen tokens per prompt as sinks — and on inspection they were just ordinary salient content words ("regarding," "potential," "biases"), not sinks in any mechanistic sense.&lt;/p&gt;

&lt;p&gt;The fix: log-transform attention values before computing spread, and use median and median absolute deviation (MAD) instead of mean/std — MAD doesn't get dragged upward by the same skew it's measuring.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;detect_sinks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mean_attn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;6.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exclude_positions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,)):&lt;/span&gt;
    &lt;span class="n"&gt;mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ones_like&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mean_attn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;exclude_positions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="n"&gt;population&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mean_attn&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;log_population&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;population&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clamp_min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1e-8&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;log_all&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mean_attn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clamp_min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1e-8&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="n"&gt;median&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;log_population&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;mad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;log_population&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;median&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;scaled_mad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mad&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.4826&lt;/span&gt;  &lt;span class="c1"&gt;# comparable scale to std under normality
&lt;/span&gt;
    &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zeros_like&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mean_attn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;scaled_mad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;1e-8&lt;/span&gt; &lt;span class="nf"&gt;else &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;log_all&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;median&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;scaled_mad&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft1zao0tg51c72kl8chh8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft1zao0tg51c72kl8chh8.png" alt="log-mad" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Correction 2 — structural doesn't mean sink
&lt;/h3&gt;

&lt;p&gt;An earlier version force-set every structural position to "is a sink," on the reasoning that BOS-as-sink is well-established enough to just assume. That reasoning breaks the moment it's generalized to every scaffolding token, most of the non-BOS structural positions turned out to have z-scores near zero or negative once actually measured. The fix was to stop overwriting measured evidence: a position is only a sink if its z-score says so. "Structural" and "sink" are now two independent flags, not one collapsing into the other.&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Looks Like End to End
&lt;/h2&gt;

&lt;p&gt;Here's an actual run, on a prompt about training-data provenance and self-supervised bias:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;╭─ Context health ─╮
│ 6.7/10           │
╰──────────────────╯

    Structural positions (template-fixed, excluded from baseline)
┌──────────┬────────────────┬────────────┬─────────┬───────┐
│ Position │ Token          │ Mean attn. │ Z-score │ Sink? │
├──────────┼────────────────┼────────────┼─────────┼───────┤
│ 0        │ '&amp;lt;|im_start|&amp;gt;' │ 0.3716     │ 7.23    │ yes   │
│ 1        │ 'system'       │ 0.0023     │ -1.06   │ no    │
│ 2        │ '\n'           │ 0.0359     │ 3.42    │ yes   │
│ 19       │ '&amp;lt;|im_end|&amp;gt;'   │ 0.0027     │ -0.82   │ no    │
│ 20       │ '\n'           │ 0.0038     │ -0.26   │ no    │
│ 21       │ '&amp;lt;|im_start|&amp;gt;' │ 0.0018     │ -1.44   │ no    │
│ 22       │ 'user'         │ 0.0018     │ -1.50   │ no    │
│ 23       │ '\n'           │ 0.0062     │ 0.55    │ no    │
│ 274      │ '&amp;lt;|im_end|&amp;gt;'   │ 0.0648     │ 4.38    │ yes   │
│ 275      │ '\n'           │ 0.0684     │ 4.47    │ yes   │
│ 276      │ '&amp;lt;|im_start|&amp;gt;' │ 0.0726     │ 4.57    │ yes   │
│ 277      │ 'assistant'    │ 0.0734     │ 4.59    │ yes   │
│ 278      │ '\n'           │ 0.1230     │ 5.43    │ yes   │
└──────────┴────────────────┴────────────┴─────────┴───────┘

              Anomalous sink tokens
┌──────────┬──────────────┬────────────┬─────────┐
│ Position │ Token        │ Mean attn. │ Z-score │
├──────────┼──────────────┼────────────┼─────────┤
│ 73       │ ' variety'   │ 0.0208     │ 2.53    │
│ 246      │ ' ###'       │ 0.0187     │ 2.36    │
│ 247      │ ' Self'      │ 0.0173     │ 2.23    │
│ 253      │ ' Bias'      │ 0.0217     │ 2.59    │
│ 254      │ '.'          │ 0.0182     │ 2.31    │
│ 255      │ ' When'      │ 0.0213     │ 2.57    │
│ 256      │ ' an'        │ 0.0152     │ 2.02    │
│ 259      │ ' trains'    │ 0.0217     │ 2.60    │
│ 260      │ ' on'        │ 0.0186     │ 2.35    │
│ 264      │ ','          │ 0.0304     │ 3.15    │
│ 265      │ ' several'   │ 0.0249     │ 2.83    │
│ 266      │ ' factors'   │ 0.0288     │ 3.06    │
│ 267      │ ' come'      │ 0.0165     │ 2.15    │
│ 269      │ ' play'      │ 0.0209     │ 2.53    │
│ 270      │ ' regarding' │ 0.0247     │ 2.81    │
│ 271      │ ' potential' │ 0.0202     │ 2.48    │
│ 272      │ ' biases'    │ 0.0215     │ 2.58    │
│ 273      │ ':'          │ 0.0319     │ 3.23    │
└──────────┴──────────────┴────────────┴─────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reading this correctly, in three parts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Position 0 is a real, strong sink.&lt;/strong&gt; z = 7.23 on a prompt where the log-MAD baseline is built entirely from the remaining ~270 positions. This is the textbook case — BOS absorbing residual softmax mass regardless of content, exactly as StreamingLLM describes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The five-token run right at the generation boundary (&lt;code&gt;&amp;lt;|im_end|&amp;gt; \n &amp;lt;|im_start|&amp;gt; assistant \n&lt;/code&gt;, positions 274–278) is a second, milder sink cluster&lt;/strong&gt; — z climbing from 4.38 to 5.43 as you approach the point where generation actually starts. This wasn't something I expected going in; the working theory is that these tokens function as a second "nothing decided yet" boundary, structurally similar to BOS in that every later query can see them and their identity is fixed by the template rather than by the conversation. Whether this is a general pattern or specific to this template/model is exactly the kind of question the 50-prompt run should answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "anomalous" table is still showing false positives, and that's on me, not the model.&lt;/strong&gt; Every row here — "regarding," "biases," "trains," even bare punctuation like &lt;code&gt;,&lt;/code&gt; and &lt;code&gt;:&lt;/code&gt; — is an ordinary content word from a real sentence about training data and bias, not a token with no semantic relationship to its context. The log-MAD correction from Phase 2 fixed &lt;em&gt;how&lt;/em&gt; the spread is measured, but this run still uses &lt;code&gt;threshold=2.0&lt;/code&gt;, which is a leftover from when the metric was mean/std-based and needs recalibrating against the new scale. With BOS at z=7.23 and the boundary run at z=4.4–5.4, a threshold somewhere in the 5–6 range would separate real structural/positional sink behavior from what's currently just the natural right tail of content-word attention. I'm leaving this table in rather than cropping it out, because the gap between "the statistic is now sound" and "the threshold is correctly tuned" is itself worth showing — they're different bugs, and fixing the first doesn't automatically fix the second.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1xo2vj4tnnflnin4iotm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1xo2vj4tnnflnin4iotm.png" alt="distribution" width="800" height="270"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  How This Connects to Parts 1 and 3
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Back to entropy (Part 1):&lt;/strong&gt; The thesis was that degraded attention during prefill should show up as elevated generation entropy during decode. Now that sink detection is trustworthy rather than noisy, this phase produces a single per-prompt "context health" number that Part 4 can actually correlate against Part 1's mean-entropy metric, instead of correlating against a score that was partly measuring content-word variance by accident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forward to speculative decoding (Part 3):&lt;/strong&gt; KV cache eviction under speculative decoding runs into exactly the sink-eviction problem StreamingLLM identified — evict the wrong position and acceptance rates degrade. Knowing precisely which positions in a given prompt are genuine sinks, rather than assuming "it's probably just BOS," is a direct input into cache retention policy once Part 3 starts trading off draft length against verification cost.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;th&gt;Link&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GitHub repo&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/cyprus09/llm-inference-lab" rel="noopener noreferrer"&gt;https://github.com/cyprus09/llm-inference-lab&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;StreamingLLM&lt;/td&gt;
&lt;td&gt;&lt;a href="https://arxiv.org/abs/2309.17453" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2309.17453&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speculative Decoding&lt;/td&gt;
&lt;td&gt;&lt;a href="https://arxiv.org/abs/2211.17192" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2211.17192&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lost in the Middle&lt;/td&gt;
&lt;td&gt;&lt;a href="https://arxiv.org/abs/2307.03172" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2307.03172&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The Illustrated Transformer&lt;/td&gt;
&lt;td&gt;&lt;a href="https://jalammar.github.io/illustrated-transformer/" rel="noopener noreferrer"&gt;https://jalammar.github.io/illustrated-transformer/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;&lt;strong&gt;Series:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1 — Entropy Tracker&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 2 — Attention Sink Detector&lt;/strong&gt; &lt;em&gt;(you are here)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Part 3 — Speculative Decoding with Entropy-Guided Draft Length &lt;em&gt;(coming)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Part 4 — The Empirical Study &lt;em&gt;(coming)&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>python</category>
      <category>llm</category>
    </item>
    <item>
      <title>Building a Terminal Based LLM Inference Internals Explorer - Part 1: Entropy Tracker</title>
      <dc:creator>mayankpallai</dc:creator>
      <pubDate>Wed, 15 Jul 2026 08:21:39 +0000</pubDate>
      <link>https://dev.to/cyprus09/building-a-terminal-based-llm-inference-internals-explorer-1b</link>
      <guid>https://dev.to/cyprus09/building-a-terminal-based-llm-inference-internals-explorer-1b</guid>
      <description>&lt;h2&gt;
  
  
  Part 1: The Entropy Tracker
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Part 1 of a 4-part series on system-level LLM inference internals.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Series Builds
&lt;/h2&gt;

&lt;p&gt;Most LLM tooling treats inference as a black box. Hosted APIs make this worse; they strip away logits, attention weights, and intermediate activations entirely. What's left is just surface behavior.&lt;/p&gt;

&lt;p&gt;This project goes the other direction. Running a 3B model locally on Apple Silicon means getting everything: raw logit distributions at every decode step, full attention weight tensors during prefill, and direct control over the generation loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The unifying thesis:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Context quality shapes attention distribution during prefill. Attention distribution shapes generation confidence during decode. Generation confidence determines how efficiently speculative decoding can run. These three things are causally linked — and this series builds the tools to try and prove it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;📊 Diagram:&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcopy6ja8sxcq39rill5x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcopy6ja8sxcq39rill5x.png" alt="Overall Architecture" width="799" height="142"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;What We Build&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1 — this post&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Per-token entropy tracker, visualized in real time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Attention sink detector, context health scoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Entropy-guided adaptive speculative decoding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Empirical study: correlation plots proving the causal chain&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Hardware &amp;amp; Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;MacBook Pro M1 Pro, 16GB unified memory&lt;/li&gt;
&lt;li&gt;Model: Qwen2.5-3B-Instruct, fp16, MPS backend (~17 tok/s warm)&lt;/li&gt;
&lt;li&gt;Python · PyTorch · HuggingFace Transformers · Rich&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This project &lt;strong&gt;requires local inference&lt;/strong&gt;. Hosted APIs (Anthropic, OpenAI) don't expose raw logits or attention weights. To see inside the model, you have to run it yourself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 0: Environment Setup
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;pyproject.toml&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[project]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"llm-inference-lab"&lt;/span&gt;
&lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.1.0"&lt;/span&gt;
&lt;span class="py"&gt;requires-python&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;3.10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mf"&gt;3.13&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="py"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="py"&gt;"torch&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.3&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"transformers&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;4.42&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"accelerate&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.31&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"sentencepiece&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"protobuf&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;4.25&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"rich&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;13.7&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"numpy&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.26&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"matplotlib&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;3.8&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"pandas&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.2&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;MPS sanity check&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;backends&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_available&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;   &lt;span class="c1"&gt;# True on M1/M2/M3
&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mps&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                       &lt;span class="c1"&gt;# confirms GPU matmul works
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Benchmarks on M1 Pro, 16GB&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;th&gt;Tokens&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Tok/s&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cold (MPS kernel compilation)&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;5.59s&lt;/td&gt;
&lt;td&gt;8.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warm&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;2.89s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;17.3&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warm, longer&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;5.95s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;16.8&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The cold-start penalty is a one-time cost per process. All subsequent calls run at ~17 tok/s. All params confirmed on &lt;code&gt;mps:0&lt;/code&gt; in fp16 — no silent CPU fallback.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 1: The Entropy Tracker
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Math
&lt;/h3&gt;

&lt;p&gt;At each decode step, the model produces a logit vector over ~32,000 vocabulary tokens. After softmax this becomes a probability distribution. Shannon entropy measures how uncertain the model is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;H(t) = -Σ p(x) · log2(p(x))    over all vocab tokens x

Low H  → peaked distribution → confident
High H → flat distribution   → uncertain
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Hook: &lt;code&gt;LogitsProcessor&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LogitsProcessor&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch.nn.functional&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EntropyCapture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LogitsProcessor&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entropies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top_tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__call__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;probs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;softmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;log_probs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;probs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log2&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mf"&gt;1e9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;H&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;probs&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;log_probs&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entropies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;H&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

        &lt;span class="n"&gt;top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;topk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;probs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top_tokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;values&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;indices&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;  &lt;span class="c1"&gt;# unchanged -- observing, not modifying
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Wiring it in:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;processor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EntropyCapture&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_new_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;do_sample&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;logits_processor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# processor.entropies now has one float per generated token
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Rich Terminal Renderer
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;rich.text&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;rich.console&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Console&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;entropy_color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;H&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;H&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;green&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;H&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yellow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;red&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;console&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;token_str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;H&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generated_token_strings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entropies&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;entropy_color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;H&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnlkj6e2sx2iketf51c8q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnlkj6e2sx2iketf51c8q.png" alt="Local Terminal Output" width="799" height="332"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Finding: Subword Commitment Points
&lt;/h3&gt;

&lt;p&gt;Qwen2.5 uses BPE tokenization — words split into subword units. "civilization" might tokenize as &lt;code&gt;civil&lt;/code&gt; + &lt;code&gt;ization&lt;/code&gt;. What happens at the entropy level?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;civil&lt;/code&gt;&lt;/strong&gt; → &lt;strong&gt;high&lt;/strong&gt; entropy (the model commits to this word here)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ization&lt;/code&gt;&lt;/strong&gt; → &lt;strong&gt;low&lt;/strong&gt; entropy (the continuation is already determined)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;📊 Diagram:&lt;/strong&gt; &lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9o2oxt8d0illa2lgrz1t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9o2oxt8d0illa2lgrz1t.png" alt="Token Level Entropy Diagram" width="799" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Entropy spikes mark &lt;em&gt;commitment points&lt;/em&gt; — where the model decides among multiple valid continuations. Once the first subword of a new word is chosen, the rest is nearly deterministic. The real decision happens at the leading edge.&lt;/p&gt;

&lt;h3&gt;
  
  
  Entropy Profiles by Prompt Type
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prompt Type&lt;/th&gt;
&lt;th&gt;Mean Entropy&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Factual ("capital of France")&lt;/td&gt;
&lt;td&gt;~0.8&lt;/td&gt;
&lt;td&gt;Mostly confident, few spikes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Creative writing("name 10 new colors")&lt;/td&gt;
&lt;td&gt;~2.6&lt;/td&gt;
&lt;td&gt;Frequent uncertainty&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code generation&lt;/td&gt;
&lt;td&gt;~1.1&lt;/td&gt;
&lt;td&gt;Surprisingly confident — syntax constrains&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Math reasoning&lt;/td&gt;
&lt;td&gt;~1.4&lt;/td&gt;
&lt;td&gt;Spikes at numeric choices&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ambiguous questions("what makes most sense?")&lt;/td&gt;
&lt;td&gt;~3.1&lt;/td&gt;
&lt;td&gt;Sustained high entropy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Code being "greener" than prose is counterintuitive but makes sense: the model has strong priors about what syntactically valid Python looks like. In prose, almost any word could plausibly follow.&lt;/p&gt;




&lt;h2&gt;
  
  
  How This Connects to Parts 2 and 3
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;To attention sinks (Part 2):&lt;/strong&gt; If attention during prefill pools into irrelevant sink tokens, the model enters decode with a degraded state — observable as higher mean generation entropy on poisoned contexts. Part 2 measures both sides and plots the correlation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;To speculative decoding (Part 3):&lt;/strong&gt; The draft model acceptance criterion is &lt;code&gt;min(1, p_verifier / p_draft)&lt;/code&gt;. The draft gets accepted most when it's &lt;em&gt;confident&lt;/em&gt; — low entropy, peaked distribution. High draft entropy signals a likely upcoming rejection. So instead of always drafting a fixed &lt;em&gt;k&lt;/em&gt; tokens, we stop early when entropy spikes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📊 Diagram:&lt;/strong&gt; &lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8k7k3dfjy62ysbmdjh46.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8k7k3dfjy62ysbmdjh46.png" alt="Future Growth" width="800" height="688"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;— decision flow: low draft entropy → keep drafting (likely accepted); high draft entropy → call verifier (rejection incoming).&lt;/p&gt;

&lt;p&gt;This is entropy-guided adaptive speculative decoding — the thread connecting all three phases.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;th&gt;Link&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GitHub repo&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/cyprus09/llm-inference-lab" rel="noopener noreferrer"&gt;https://github.com/cyprus09/llm-inference-lab&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;StreamingLLM&lt;/td&gt;
&lt;td&gt;&lt;a href="https://arxiv.org/abs/2309.17453" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2309.17453&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speculative Decoding&lt;/td&gt;
&lt;td&gt;&lt;a href="https://arxiv.org/abs/2211.17192" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2211.17192&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lost in the Middle&lt;/td&gt;
&lt;td&gt;&lt;a href="https://arxiv.org/abs/2307.03172" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2307.03172&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The Illustrated Transformer&lt;/td&gt;
&lt;td&gt;&lt;a href="https://jalammar.github.io/illustrated-transformer/" rel="noopener noreferrer"&gt;https://jalammar.github.io/illustrated-transformer/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Making Deep Learning Go Brrrr&lt;/td&gt;
&lt;td&gt;&lt;a href="https://horace.io/brrr_intro.html" rel="noopener noreferrer"&gt;https://horace.io/brrr_intro.html&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nanoGPT&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/karpathy/nanoGPT" rel="noopener noreferrer"&gt;https://github.com/karpathy/nanoGPT&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;&lt;strong&gt;Series:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Part 1 — Entropy Tracker&lt;/strong&gt; &lt;em&gt;(you are here)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Part 2 — Attention Sink Detector &lt;em&gt;(coming)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Part 3 — Speculative Decoding with Entropy-Guided Draft Length &lt;em&gt;(coming)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Part 4 — The Empirical Study &lt;em&gt;(coming)&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>opensource</category>
      <category>llm</category>
      <category>pytorch</category>
    </item>
  </channel>
</rss>
