<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
<channel><title>Sachin Rajput: Inference, explained simply. Measured for real.</title><link>https://blog.sachinrajput.dev/</link>
<atom:link href="https://blog.sachinrajput.dev/rss.xml" rel="self" type="application/rss+xml"/>
<description>How AI models actually answer you: LLM inference explained simply, with real measurements and code you can run. By Sachin Rajput.</description><language>en</language><lastBuildDate>Thu, 22 Oct 2026 11:59:09 +0000</lastBuildDate>
<item><title>One Request, End to End: Tokenize, Prefill, Decode, Stream</title><link>https://blog.sachinrajput.dev/posts/one-request-end-to-end/</link>
<guid isPermaLink="true">https://blog.sachinrajput.dev/posts/one-request-end-to-end/</guid><pubDate>Thu, 22 Oct 2026 09:00:00 +0000</pubDate>
<description>What really happens in the second after you ask a chatbot a question? A trip through the Word Factory, explained simply, with a tiny engine you can run and time yourself.</description>
<content:encoded><![CDATA[<p><img src="https://blog.sachinrajput.dev/img/posts/f1-cover.jpg" alt=""></p><p>You type <strong>"Why is the sky blue?"</strong> and press Enter.</p>
<p>There's a short pause. Then words start appearing, one after another, like someone is typing to you very fast.</p>
<p>What happened during that pause? And why do the words come out one at a time instead of all at once?</p>
<p>Let's take a walk through the <strong>Word Factory</strong>.</p>
<h2 id="welcome-to-the-word-factory">Welcome to the Word Factory</h2>
<p>Picture a small factory with a conveyor belt. Your question goes in at one end, and an answer comes out on your phone at the other. Along the way there are four machines:</p>
<ol>
<li><strong>The Chopper</strong> cuts your question into small pieces.</li>
<li><strong>The Reader</strong> has lots of eyes and reads every piece <em>at the same time</em>.</li>
<li><strong>The Typewriter</strong> writes the answer, but only <strong>one piece at a time</strong>.</li>
<li><strong>The Delivery Tape</strong> carries each piece to your phone the moment it's typed.</li>
</ol>
<p>There's also a <strong>notebook</strong> hanging above the Reader. It's the factory's secret weapon, and it matters a lot later.</p>
<p>Engineers have proper names for these four steps: <strong>tokenize</strong>, <strong>prefill</strong>, <strong>decode</strong> and <strong>stream</strong>. That's all an "inference engine" is: the factory that runs a language model and turns your question into an answer.</p>
<p>Let's visit each machine.</p>
<h2 id="machine-1-the-chopper-tokenize">Machine 1: The Chopper (tokenize)</h2>
<p>A language model can't read letters the way you do. It only understands <strong>numbers</strong>. So the first job is to cut your text into small pieces called <strong>tokens</strong> and give each piece a number.</p>
<p>A token is usually a word or part of a word. Think of LEGO bricks: common words are one brick, and long or rare words get built from a few smaller bricks.</p>
<div class="codewrap"><div class="hl"><pre><span></span><code>"Why is the sky blue?"
   ↓ chop
[ "Why" ][ " is" ][ " the" ][ " sky" ][ " blue" ][ "?" ]
   ↓ give each a number
[  512,     87,     33,    2041,     978,     11  ]
</code></pre></div><button class="copy" type="button" aria-label="Copy code">COPY</button></div>

<p><em>(These numbers are made up for the example. Each model has its own dictionary that decides which number goes with which piece.)</em></p>
<p><strong>How long does it take?</strong> Almost no time at all: a tiny fraction of a millisecond. The Chopper is never the slow machine.</p>
<h2 id="machine-2-the-reader-prefill">Machine 2: The Reader (prefill)</h2>
<p>Now the Reader looks at your whole question.</p>
<p>Here's the clever part: <strong>the Reader has lots of eyes, and it reads every token at the same time.</strong> It doesn't go word by word. Whether your question is 6 tokens or 600, it reads them all in one big gulp.</p>
<p>While it reads, it writes notes in the <strong>notebook</strong>: for every token, a short note about what that token means and how it connects to the others. Engineers call this notebook the <strong>KV cache</strong>. (KV stands for "keys and values", which is just what the notes are called inside the model.)</p>
<p>When the Reader finishes, it has a guess for the very first word of the answer.</p>
<p>This step is called <strong>prefill</strong> because it <em>fills</em> the notebook with notes about your whole question before any answer is written.</p>
<p><strong>The important bit:</strong> a longer question means more to read. So the <strong>longer your question, the longer you wait for the first word.</strong></p>
<h2 id="machine-3-the-typewriter-decode">Machine 3: The Typewriter (decode)</h2>
<p>Now the answer gets written, and this is where things slow down.</p>
<p>The Typewriter can only type <strong>one token per step</strong>. Why? Because each new word depends on the words before it. You can't decide the 5th word of a sentence until you know the first 4. Try finishing <em>"The sky is blue because..."</em> without knowing the word "because" was there.</p>
<p>So for every single token, the factory does a full trip:</p>
<ol>
<li>Look at the notebook (all the notes so far)</li>
<li>Run through the <em>whole</em> model to pick the next token</li>
<li>Add one new line to the notebook</li>
<li>Type the token</li>
<li>Go back to step 1 for the next token</li>
</ol>
<p>That's the pink "again &amp; again" arrow in the picture. Engineers call this the <strong>decode loop</strong>.</p>
<p><strong>Why the notebook is so useful:</strong> without it, the Typewriter would have to re-read your entire question <em>and</em> everything it has typed so far, every single step. That would be like re-reading a whole book from page 1 every time you want to write the next word. With the notebook, it just glances at its notes.</p>
<h2 id="machine-4-the-delivery-tape-stream">Machine 4: The Delivery Tape (stream)</h2>
<p>The moment a token is typed, it's turned back into text and sent straight to your screen. The factory doesn't wait to finish the whole answer first.</p>
<p>That's why you see words appear one by one. It's not a fancy animation. <strong>You're watching the Typewriter work in real time.</strong></p>
<p>This is called <strong>streaming</strong>, and it makes chatbots <em>feel</em> much faster. You start reading while the rest is still being written.</p>
<h2 id="three-stopwatches">Three stopwatches</h2>
<p>So how do we say if a factory is "fast"? There are three different stopwatches, and they measure different things.</p>
<p><strong>⏱ Stopwatch 1: Time to First Token (TTFT)</strong>
How long from pressing Enter until the <strong>first word</strong> shows up. This is the "awkward pause." It includes chopping, the whole reading step, and picking the first token.</p>
<div class="codewrap"><div class="hl"><pre><span></span><code>TTFT = time first token arrives − time you pressed Enter
</code></pre></div><button class="copy" type="button" aria-label="Copy code">COPY</button></div>

<p><strong>⏱ Stopwatch 2: Inter-Token Latency (ITL)</strong>
The <strong>gap between one word and the next</strong>. If it's small and steady, the text flows smoothly. If one gap is suddenly huge, it feels like the chatbot froze, even if the average is fine.</p>
<div class="codewrap"><div class="hl"><pre><span></span><code>ITL = time token 2 arrives − time token 1 arrived   (and so on, for every gap)
</code></pre></div><button class="copy" type="button" aria-label="Copy code">COPY</button></div>

<p><strong>⏱ Stopwatch 3: Throughput (tokens per second)</strong>
How many tokens come out per second. Careful, there are two kinds:</p>
<ul>
<li><strong>For one person:</strong> how fast <em>your</em> answer is typed.</li>
<li><strong>For the whole factory:</strong> how many tokens it produces for <em>everyone</em> at once.</li>
</ul>
<p>A factory serving 100 people at 5,000 tokens per second in total gives each person about 50 tokens per second. Both numbers are true. They just answer different questions.</p>
<p>And the total wait for your full answer is simply:</p>
<div class="codewrap"><div class="hl"><pre><span></span><code>total time ≈ TTFT + (number of tokens − 1) × ITL
</code></pre></div><button class="copy" type="button" aria-label="Copy code">COPY</button></div>

<h2 id="lets-build-a-tiny-factory-and-time-it">Let's build a tiny factory and time it</h2>
<p>Talking is fine, but let's <em>measure</em>. Below is a complete Word Factory in about 120 lines of Python, using only NumPy.</p>
<p>It has the same shape as a small real model (GPT-2 small: 12 layers, 85 million numbers inside). <strong>One honest warning:</strong> its "brain" is filled with random numbers, so the words it types are gibberish. But it does <strong>exactly the same amount of work</strong> as a trained model of that size, so the stopwatch readings are real.</p>
<h3 id="the-chopper">The Chopper</h3>
<p>To keep it simple, our Chopper cuts text into single bytes (roughly, one letter = one token).</p>
<div class="codewrap"><div class="hl"><pre><span></span><code><span class="kn">import</span><span class="w"> </span><span class="nn">codecs</span>
<span class="kn">import</span><span class="w"> </span><span class="nn">numpy</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="nn">np</span>

<span class="k">class</span><span class="w"> </span><span class="nc">ByteTokenizer</span><span class="p">:</span>
    <span class="n">vocab_size</span> <span class="o">=</span> <span class="mi">256</span>

    <span class="k">def</span><span class="w"> </span><span class="nf">encode</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">text</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">]:</span>
        <span class="c1"># Chop: every byte of the text becomes one token number.</span>
        <span class="k">return</span> <span class="nb">list</span><span class="p">(</span><span class="n">text</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">))</span>

    <span class="k">def</span><span class="w"> </span><span class="nf">stream_decoder</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="c1"># Turn token numbers back into text, one at a time.</span>
        <span class="c1"># (It politely waits if a letter like "é" is split across two tokens.)</span>
        <span class="n">dec</span> <span class="o">=</span> <span class="n">codecs</span><span class="o">.</span><span class="n">getincrementaldecoder</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">)(</span><span class="n">errors</span><span class="o">=</span><span class="s2">"replace"</span><span class="p">)</span>
        <span class="k">return</span> <span class="k">lambda</span> <span class="n">tok</span><span class="p">:</span> <span class="n">dec</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="nb">bytes</span><span class="p">([</span><span class="n">tok</span><span class="p">]))</span>
</code></pre></div><button class="copy" type="button" aria-label="Copy code">COPY</button></div>

<h3 id="the-reader-and-the-typewriter-are-the-same-machine">The Reader and the Typewriter are the same machine</h3>
<p>Here's a surprise: <strong>reading and typing use the same machine.</strong> The only difference is how many tokens you feed it. Reading (prefill) feeds it the whole question at once. Typing (decode) feeds it one token at a time.</p>
<p>You don't need to follow every line. Look for the two comments marked 📓: that's where the notebook gets written and read.</p>
<div class="codewrap"><div class="hl"><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">TinyDecoder</span><span class="p">:</span>
    <span class="k">def</span><span class="w"> </span><span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">n_layers</span><span class="o">=</span><span class="mi">12</span><span class="p">,</span> <span class="n">d</span><span class="o">=</span><span class="mi">768</span><span class="p">,</span> <span class="n">n_heads</span><span class="o">=</span><span class="mi">12</span><span class="p">,</span> <span class="n">d_ff</span><span class="o">=</span><span class="mi">3072</span><span class="p">,</span>
                 <span class="n">vocab</span><span class="o">=</span><span class="mi">256</span><span class="p">,</span> <span class="n">max_seq</span><span class="o">=</span><span class="mi">4096</span><span class="p">,</span> <span class="n">seed</span><span class="o">=</span><span class="mi">0</span><span class="p">):</span>
        <span class="n">rng</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">random</span><span class="o">.</span><span class="n">default_rng</span><span class="p">(</span><span class="n">seed</span><span class="p">)</span>
        <span class="n">w</span> <span class="o">=</span> <span class="k">lambda</span> <span class="o">*</span><span class="n">s</span><span class="p">:</span> <span class="p">(</span><span class="n">rng</span><span class="o">.</span><span class="n">standard_normal</span><span class="p">(</span><span class="n">s</span><span class="p">)</span> <span class="o">*</span> <span class="mf">0.02</span><span class="p">)</span><span class="o">.</span><span class="n">astype</span><span class="p">(</span><span class="n">np</span><span class="o">.</span><span class="n">float32</span><span class="p">)</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">L</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">d</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">H</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">hd</span> <span class="o">=</span> <span class="n">n_layers</span><span class="p">,</span> <span class="n">d</span><span class="p">,</span> <span class="n">n_heads</span><span class="p">,</span> <span class="n">d</span> <span class="o">//</span> <span class="n">n_heads</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">tok_emb</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">pos_emb</span> <span class="o">=</span> <span class="n">w</span><span class="p">(</span><span class="n">vocab</span><span class="p">,</span> <span class="n">d</span><span class="p">),</span> <span class="n">w</span><span class="p">(</span><span class="n">max_seq</span><span class="p">,</span> <span class="n">d</span><span class="p">)</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">layers</span> <span class="o">=</span> <span class="p">[</span><span class="nb">dict</span><span class="p">(</span><span class="n">qkv</span><span class="o">=</span><span class="n">w</span><span class="p">(</span><span class="n">d</span><span class="p">,</span> <span class="mi">3</span> <span class="o">*</span> <span class="n">d</span><span class="p">),</span> <span class="n">o</span><span class="o">=</span><span class="n">w</span><span class="p">(</span><span class="n">d</span><span class="p">,</span> <span class="n">d</span><span class="p">),</span>
                            <span class="n">up</span><span class="o">=</span><span class="n">w</span><span class="p">(</span><span class="n">d</span><span class="p">,</span> <span class="n">d_ff</span><span class="p">),</span> <span class="n">down</span><span class="o">=</span><span class="n">w</span><span class="p">(</span><span class="n">d_ff</span><span class="p">,</span> <span class="n">d</span><span class="p">))</span>
                       <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">n_layers</span><span class="p">)]</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">lm_head</span> <span class="o">=</span> <span class="n">w</span><span class="p">(</span><span class="n">d</span><span class="p">,</span> <span class="n">vocab</span><span class="p">)</span>
        <span class="c1"># The notebook (KV cache): room for notes on up to 4096 tokens.</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">k</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">zeros</span><span class="p">((</span><span class="n">n_layers</span><span class="p">,</span> <span class="n">n_heads</span><span class="p">,</span> <span class="n">max_seq</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">hd</span><span class="p">),</span> <span class="n">np</span><span class="o">.</span><span class="n">float32</span><span class="p">)</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">v</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">zeros_like</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">k</span><span class="p">)</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">n_cached</span> <span class="o">=</span> <span class="mi">0</span>

    <span class="nd">@staticmethod</span>
    <span class="k">def</span><span class="w"> </span><span class="nf">_norm</span><span class="p">(</span><span class="n">x</span><span class="p">):</span>
        <span class="k">return</span> <span class="p">(</span><span class="n">x</span> <span class="o">-</span> <span class="n">x</span><span class="o">.</span><span class="n">mean</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="n">keepdims</span><span class="o">=</span><span class="kc">True</span><span class="p">))</span> <span class="o">/</span> <span class="n">np</span><span class="o">.</span><span class="n">sqrt</span><span class="p">(</span><span class="n">x</span><span class="o">.</span><span class="n">var</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="n">keepdims</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span> <span class="o">+</span> <span class="mf">1e-5</span><span class="p">)</span>

    <span class="k">def</span><span class="w"> </span><span class="nf">forward</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">ids</span><span class="p">:</span> <span class="n">np</span><span class="o">.</span><span class="n">ndarray</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">np</span><span class="o">.</span><span class="n">ndarray</span><span class="p">:</span>
<span class="w">        </span><span class="sd">"""Feed in NEW tokens (the whole question, or just one).</span>
<span class="sd">        Returns the scores for what the next token should be."""</span>
        <span class="n">T</span><span class="p">,</span> <span class="n">start</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">ids</span><span class="p">),</span> <span class="bp">self</span><span class="o">.</span><span class="n">n_cached</span>
        <span class="n">x</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">tok_emb</span><span class="p">[</span><span class="n">ids</span><span class="p">]</span> <span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">pos_emb</span><span class="p">[</span><span class="n">start</span><span class="p">:</span><span class="n">start</span> <span class="o">+</span> <span class="n">T</span><span class="p">]</span>
        <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">p</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">layers</span><span class="p">):</span>
            <span class="n">q</span><span class="p">,</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_norm</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="o">@</span> <span class="n">p</span><span class="p">[</span><span class="s2">"qkv"</span><span class="p">],</span> <span class="mi">3</span><span class="p">,</span> <span class="n">axis</span><span class="o">=-</span><span class="mi">1</span><span class="p">)</span>
            <span class="n">q</span><span class="p">,</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span> <span class="o">=</span> <span class="p">(</span><span class="n">t</span><span class="o">.</span><span class="n">reshape</span><span class="p">(</span><span class="n">T</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">H</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">hd</span><span class="p">)</span><span class="o">.</span><span class="n">transpose</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span> <span class="k">for</span> <span class="n">t</span> <span class="ow">in</span> <span class="p">(</span><span class="n">q</span><span class="p">,</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span><span class="p">))</span>
            <span class="c1"># 📓 write notes for the new tokens into the notebook</span>
            <span class="bp">self</span><span class="o">.</span><span class="n">k</span><span class="p">[</span><span class="n">i</span><span class="p">,</span> <span class="p">:,</span> <span class="n">start</span><span class="p">:</span><span class="n">start</span> <span class="o">+</span> <span class="n">T</span><span class="p">],</span> <span class="bp">self</span><span class="o">.</span><span class="n">v</span><span class="p">[</span><span class="n">i</span><span class="p">,</span> <span class="p">:,</span> <span class="n">start</span><span class="p">:</span><span class="n">start</span> <span class="o">+</span> <span class="n">T</span><span class="p">]</span> <span class="o">=</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span>
            <span class="c1"># 📓 read ALL notes so far (old + new)</span>
            <span class="n">K</span><span class="p">,</span> <span class="n">V</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">k</span><span class="p">[</span><span class="n">i</span><span class="p">,</span> <span class="p">:,</span> <span class="p">:</span><span class="n">start</span> <span class="o">+</span> <span class="n">T</span><span class="p">],</span> <span class="bp">self</span><span class="o">.</span><span class="n">v</span><span class="p">[</span><span class="n">i</span><span class="p">,</span> <span class="p">:,</span> <span class="p">:</span><span class="n">start</span> <span class="o">+</span> <span class="n">T</span><span class="p">]</span>
            <span class="n">att</span> <span class="o">=</span> <span class="n">q</span> <span class="o">@</span> <span class="n">K</span><span class="o">.</span><span class="n">transpose</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="o">/</span> <span class="n">np</span><span class="o">.</span><span class="n">sqrt</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">hd</span><span class="p">)</span>
            <span class="n">mask</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">triu</span><span class="p">(</span><span class="n">np</span><span class="o">.</span><span class="n">ones</span><span class="p">((</span><span class="n">T</span><span class="p">,</span> <span class="n">start</span> <span class="o">+</span> <span class="n">T</span><span class="p">),</span> <span class="nb">bool</span><span class="p">),</span> <span class="n">k</span><span class="o">=</span><span class="n">start</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span>  <span class="c1"># no peeking ahead</span>
            <span class="n">att</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="n">mask</span><span class="p">,</span> <span class="o">-</span><span class="mf">1e9</span><span class="p">,</span> <span class="n">att</span><span class="p">)</span>
            <span class="n">att</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">exp</span><span class="p">(</span><span class="n">att</span> <span class="o">-</span> <span class="n">att</span><span class="o">.</span><span class="n">max</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="n">keepdims</span><span class="o">=</span><span class="kc">True</span><span class="p">))</span>
            <span class="n">att</span> <span class="o">/=</span> <span class="n">att</span><span class="o">.</span><span class="n">sum</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="n">keepdims</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
            <span class="n">y</span> <span class="o">=</span> <span class="p">(</span><span class="n">att</span> <span class="o">@</span> <span class="n">V</span><span class="p">)</span><span class="o">.</span><span class="n">transpose</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span><span class="o">.</span><span class="n">reshape</span><span class="p">(</span><span class="n">T</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">d</span><span class="p">)</span>
            <span class="n">x</span> <span class="o">=</span> <span class="n">x</span> <span class="o">+</span> <span class="n">y</span> <span class="o">@</span> <span class="n">p</span><span class="p">[</span><span class="s2">"o"</span><span class="p">]</span>
            <span class="n">h</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_norm</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="o">@</span> <span class="n">p</span><span class="p">[</span><span class="s2">"up"</span><span class="p">]</span>
            <span class="n">x</span> <span class="o">=</span> <span class="n">x</span> <span class="o">+</span> <span class="p">(</span><span class="mf">0.5</span> <span class="o">*</span> <span class="n">h</span> <span class="o">*</span> <span class="p">(</span><span class="mi">1</span> <span class="o">+</span> <span class="n">np</span><span class="o">.</span><span class="n">tanh</span><span class="p">(</span><span class="mf">0.79788456</span> <span class="o">*</span> <span class="p">(</span><span class="n">h</span> <span class="o">+</span> <span class="mf">0.044715</span> <span class="o">*</span> <span class="n">h</span><span class="o">**</span><span class="mi">3</span><span class="p">))))</span> <span class="o">@</span> <span class="n">p</span><span class="p">[</span><span class="s2">"down"</span><span class="p">]</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">n_cached</span> <span class="o">+=</span> <span class="n">T</span>
        <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_norm</span><span class="p">(</span><span class="n">x</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">])</span> <span class="o">@</span> <span class="bp">self</span><span class="o">.</span><span class="n">lm_head</span>
</code></pre></div><button class="copy" type="button" aria-label="Copy code">COPY</button></div>

<h3 id="the-whole-factory-start-to-finish">The whole factory, start to finish</h3>
<p>This little function <em>is</em> the conveyor belt. Read it top to bottom and you'll spot all four machines:</p>
<div class="codewrap"><div class="hl"><pre><span></span><code><span class="kn">import</span><span class="w"> </span><span class="nn">time</span>

<span class="k">def</span><span class="w"> </span><span class="nf">generate</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">tok</span><span class="p">,</span> <span class="n">prompt</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">max_new_tokens</span><span class="p">:</span> <span class="nb">int</span><span class="p">):</span>
<span class="w">    </span><span class="sd">"""Yield (event, text, timestamp) for one request, end to end."""</span>
    <span class="n">model</span><span class="o">.</span><span class="n">n_cached</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="n">ids</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">(</span><span class="n">tok</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span><span class="n">prompt</span><span class="p">))</span>                     <span class="c1"># 1. CHOP</span>
    <span class="k">yield</span> <span class="s2">"tokenized"</span><span class="p">,</span> <span class="s2">""</span><span class="p">,</span> <span class="n">time</span><span class="o">.</span><span class="n">perf_counter</span><span class="p">()</span>
    <span class="n">logits</span> <span class="o">=</span> <span class="n">model</span><span class="o">.</span><span class="n">forward</span><span class="p">(</span><span class="n">ids</span><span class="p">)</span>                            <span class="c1"># 2. READ it all at once</span>
    <span class="k">yield</span> <span class="s2">"prefilled"</span><span class="p">,</span> <span class="s2">""</span><span class="p">,</span> <span class="n">time</span><span class="o">.</span><span class="n">perf_counter</span><span class="p">()</span>
    <span class="n">detok</span> <span class="o">=</span> <span class="n">tok</span><span class="o">.</span><span class="n">stream_decoder</span><span class="p">()</span>
    <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">max_new_tokens</span><span class="p">):</span>                        <span class="c1"># 3. TYPE, one at a time...</span>
        <span class="n">nxt</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">np</span><span class="o">.</span><span class="n">argmax</span><span class="p">(</span><span class="n">logits</span><span class="p">))</span>                       <span class="c1">#    pick the best next token</span>
        <span class="k">yield</span> <span class="s2">"token"</span><span class="p">,</span> <span class="n">detok</span><span class="p">(</span><span class="n">nxt</span><span class="p">),</span> <span class="n">time</span><span class="o">.</span><span class="n">perf_counter</span><span class="p">()</span>     <span class="c1"># 4. SEND it right away</span>
        <span class="n">logits</span> <span class="o">=</span> <span class="n">model</span><span class="o">.</span><span class="n">forward</span><span class="p">(</span><span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([</span><span class="n">nxt</span><span class="p">]))</span>            <span class="c1">#    ...and again</span>
</code></pre></div><button class="copy" type="button" aria-label="Copy code">COPY</button></div>

<p>The full file, including a small <code>measure()</code> helper that reads the stopwatches, is <a href="/code/f1/tiny_engine.py"><code>tiny_engine.py</code></a> (download it and run <code>python tiny_engine.py</code>).</p>
<h2 id="the-results">The results</h2>
<p>I ran the factory with three question sizes (64, 256 and 1,024 tokens), asked for 64 tokens of answer each time, and did one warm-up run first. Plain CPU, 2 cores, no GPU:</p>
<div class="codewrap"><div class="hl"><pre><span></span><code> question  chop ms  read ms   TTFT ms  gap p50  gap p99  total s  tokens/s
     64      0.01     452.2    452.2    101.8    120.9     6.83      9.9
    256      0.03    1656.2   1656.4    106.9    126.4     8.33      9.4
   1024      0.04    9444.8   9445.0    134.4    175.0    18.00      7.4
</code></pre></div><button class="copy" type="button" aria-label="Copy code">COPY</button></div>

<p><em>("gap p50" is the typical gap between words. "gap p99" is the gap that only 1 in 100 steps is slower than: the "worst normal" gap.)</em></p>
<p>Here's the 256-token run drawn on a timeline:</p>
<div class="codewrap"><div class="hl"><pre><span></span><code> press Enter                                                               done
 │                                                                          │
 ├─ chop: 0.03 ms (too small to see)
 │
 ├────────── READ the whole question: 1,656 ms ──────────┤
 │                                                        │
 │                                  first word appears ──▶ █ █ █ █ █ █ █ ... █
 │                                                         └─┘
 │                                                       gap ≈ 107 ms, 63 times
 │◀──────────────── TTFT = 1.66 s ───────────────────────▶│◀── typing: 6.7 s ──▶│
</code></pre></div><button class="copy" type="button" aria-label="Copy code">COPY</button></div>

<h2 id="four-things-we-just-discovered">Four things we just discovered</h2>
<h3 id="1-reading-is-about-16-times-cheaper-per-token-than-typing">1. Reading is about 16 times cheaper per token than typing</h3>
<p>For the 256-token question, reading took 1,656 ms. That's about <strong>6.5 ms per token</strong>.</p>
<p>Typing took <strong>107 ms per token</strong>.</p>
<p>Same machine, same computer. Why the huge difference?</p>
<p>Every trip through the model means hauling all <strong>85 million numbers</strong> (about 340 MB) through the processor. When the Reader does that trip, it handles 256 tokens at once, so the cost is shared 256 ways. When the Typewriter does it, the whole trip is for <strong>one</strong> token.</p>
<p>It's like a school bus. Driving the bus across town costs about the same whether it carries 1 kid or 50. The Reader fills the bus. The Typewriter drives it across town with one passenger, over and over again.</p>
<p>This is the single most important idea in making chatbots fast, and it's why real servers try to type for many people at once (filling the bus). Later posts in this series dig into it.</p>
<h3 id="2-the-awkward-pause-is-almost-all-reading">2. The awkward pause is almost all reading</h3>
<p>Chopping took 0.01 to 0.04 ms: basically nothing. TTFT was almost exactly the reading time.</p>
<p>And reading time grew with the question: <strong>0.45 s for 64 tokens, 9.4 s for 1,024 tokens.</strong> A question 16 times longer made the wait about 21 times longer. (It grows a bit faster than the question, because every token has to "look at" every other token.)</p>
<p><strong>Lesson:</strong> if the first word takes too long, the first suspect is a long question.</p>
<h3 id="3-the-notebook-gets-fatter-so-typing-slowly-gets-slower">3. The notebook gets fatter, so typing slowly gets slower</h3>
<p>The typical gap between words grew from <strong>102 ms</strong> (short question) to <strong>134 ms</strong> (long question).</p>
<p>That's the notebook. Every step, the Typewriter reads <em>all</em> the notes. Every token adds a new line. For our little model, each token adds about <strong>72 KB</strong> of notes:</p>
<div class="codewrap"><div class="hl"><pre><span></span><code>notes per token = 2 (K and V) × 12 layers × 768 numbers × 4 bytes = 73,728 bytes ≈ 72 KB
</code></pre></div><button class="copy" type="button" aria-label="Copy code">COPY</button></div>

<p>After about 1,100 tokens, that's roughly <strong>80 MB of notes</strong>, read again on every single step. Bigger models have much bigger notebooks, and that's what decides how many people one GPU can serve at a time.</p>
<h3 id="4-most-of-the-total-time-is-typing">4. Most of the total time is typing</h3>
<p>For 256 tokens in and 64 tokens out: 1.66 s of reading + 63 gaps × 0.107 s of typing ≈ <strong>8.4 s</strong>. The real measurement was 8.33 s.</p>
<p>Typing was 80% of the total. So <strong>the length of the answer</strong> matters more than anything else for total waiting time. That's why chatbots that "think" for thousands of tokens before answering are so expensive to run.</p>
<h2 id="bonus-time-a-real-chatbot-server">Bonus: time a real chatbot server</h2>
<p>Measuring inside the factory flatters it. It doesn't count time spent waiting in line or travelling over the network. The fair way is to measure from <strong>your side</strong>, like a real user.</p>
<p>This script (<a href="/code/f1/measure_client.py"><code>measure_client.py</code></a>) works with any server that speaks the OpenAI API format, including popular engines like <strong>vLLM</strong>, <strong>SGLang</strong> and <strong>TensorRT-LLM</strong>:</p>
<div class="codewrap"><div class="hl"><pre><span></span><code><span class="kn">import</span><span class="w"> </span><span class="nn">argparse</span><span class="o">,</span><span class="w"> </span><span class="nn">statistics</span><span class="o">,</span><span class="w"> </span><span class="nn">time</span>
<span class="kn">from</span><span class="w"> </span><span class="nn">openai</span><span class="w"> </span><span class="kn">import</span> <span class="n">OpenAI</span>

<span class="n">ap</span> <span class="o">=</span> <span class="n">argparse</span><span class="o">.</span><span class="n">ArgumentParser</span><span class="p">()</span>
<span class="n">ap</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s2">"--base-url"</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="s2">"http://localhost:8000/v1"</span><span class="p">)</span>
<span class="n">ap</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s2">"--model"</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="s2">"tiny"</span><span class="p">)</span>
<span class="n">ap</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s2">"--max-tokens"</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">int</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mi">64</span><span class="p">)</span>
<span class="n">ap</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s2">"--prompt"</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="s2">"Explain how a large language model answers "</span>
                                    <span class="s2">"a question, step by step. "</span> <span class="o">*</span> <span class="mi">4</span><span class="p">)</span>
<span class="n">args</span> <span class="o">=</span> <span class="n">ap</span><span class="o">.</span><span class="n">parse_args</span><span class="p">()</span>
<span class="n">client</span> <span class="o">=</span> <span class="n">OpenAI</span><span class="p">(</span><span class="n">base_url</span><span class="o">=</span><span class="n">args</span><span class="o">.</span><span class="n">base_url</span><span class="p">,</span> <span class="n">api_key</span><span class="o">=</span><span class="s2">"unused"</span><span class="p">)</span>

<span class="n">t_send</span> <span class="o">=</span> <span class="n">time</span><span class="o">.</span><span class="n">perf_counter</span><span class="p">()</span>                       <span class="c1"># press Enter</span>
<span class="n">stream</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">completions</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="n">model</span><span class="o">=</span><span class="n">args</span><span class="o">.</span><span class="n">model</span><span class="p">,</span> <span class="n">prompt</span><span class="o">=</span><span class="n">args</span><span class="o">.</span><span class="n">prompt</span><span class="p">,</span>
                                   <span class="n">max_tokens</span><span class="o">=</span><span class="n">args</span><span class="o">.</span><span class="n">max_tokens</span><span class="p">,</span> <span class="n">temperature</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span>
                                   <span class="n">stream</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
                                   <span class="n">stream_options</span><span class="o">=</span><span class="p">{</span><span class="s2">"include_usage"</span><span class="p">:</span> <span class="kc">True</span><span class="p">})</span>
<span class="n">arrivals</span><span class="p">,</span> <span class="n">usage</span> <span class="o">=</span> <span class="p">[],</span> <span class="kc">None</span>
<span class="k">for</span> <span class="n">chunk</span> <span class="ow">in</span> <span class="n">stream</span><span class="p">:</span>
    <span class="k">if</span> <span class="n">chunk</span><span class="o">.</span><span class="n">choices</span><span class="p">:</span>                              <span class="c1"># a token arrived!</span>
        <span class="n">arrivals</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">time</span><span class="o">.</span><span class="n">perf_counter</span><span class="p">())</span>
    <span class="k">if</span> <span class="n">chunk</span><span class="o">.</span><span class="n">usage</span><span class="p">:</span>
        <span class="n">usage</span> <span class="o">=</span> <span class="n">chunk</span><span class="o">.</span><span class="n">usage</span>
<span class="n">t_done</span> <span class="o">=</span> <span class="n">time</span><span class="o">.</span><span class="n">perf_counter</span><span class="p">()</span>

<span class="n">ttft</span> <span class="o">=</span> <span class="n">arrivals</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">-</span> <span class="n">t_send</span>
<span class="n">gaps</span> <span class="o">=</span> <span class="p">[</span><span class="n">b</span> <span class="o">-</span> <span class="n">a</span> <span class="k">for</span> <span class="n">a</span><span class="p">,</span> <span class="n">b</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">arrivals</span><span class="p">,</span> <span class="n">arrivals</span><span class="p">[</span><span class="mi">1</span><span class="p">:])]</span>
<span class="n">out_tokens</span> <span class="o">=</span> <span class="n">usage</span><span class="o">.</span><span class="n">completion_tokens</span> <span class="k">if</span> <span class="n">usage</span> <span class="k">else</span> <span class="nb">len</span><span class="p">(</span><span class="n">arrivals</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">"prompt tokens      : </span><span class="si">{</span><span class="n">usage</span><span class="o">.</span><span class="n">prompt_tokens</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="n">usage</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="s1">'?'</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">"output tokens      : </span><span class="si">{</span><span class="n">out_tokens</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">"TTFT               : </span><span class="si">{</span><span class="n">ttft</span><span class="o">*</span><span class="mf">1e3</span><span class="si">:</span><span class="s2">8.1f</span><span class="si">}</span><span class="s2"> ms"</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">"ITL p50 / p99      : </span><span class="si">{</span><span class="n">statistics</span><span class="o">.</span><span class="n">median</span><span class="p">(</span><span class="n">gaps</span><span class="p">)</span><span class="o">*</span><span class="mf">1e3</span><span class="si">:</span><span class="s2">8.1f</span><span class="si">}</span><span class="s2"> / "</span>
      <span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="nb">sorted</span><span class="p">(</span><span class="n">gaps</span><span class="p">)[</span><span class="nb">int</span><span class="p">(</span><span class="mf">0.99</span><span class="o">*</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">gaps</span><span class="p">)</span><span class="o">-</span><span class="mi">1</span><span class="p">))]</span><span class="o">*</span><span class="mf">1e3</span><span class="si">:</span><span class="s2">.1f</span><span class="si">}</span><span class="s2"> ms"</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">"end-to-end latency : </span><span class="si">{</span><span class="n">t_done</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">t_send</span><span class="si">:</span><span class="s2">8.2f</span><span class="si">}</span><span class="s2"> s"</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">"decode tokens/s    : </span><span class="si">{</span><span class="p">(</span><span class="n">out_tokens</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="mi">1</span><span class="p">)</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="p">(</span><span class="n">arrivals</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">arrivals</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span><span class="si">:</span><span class="s2">8.1f</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
</code></pre></div><button class="copy" type="button" aria-label="Copy code">COPY</button></div>

<p>To test it without a GPU, I wrapped our tiny factory in a small web server (<a href="/code/f1/tiny_server.py"><code>tiny_server.py</code></a>, 42 lines) and pointed the script at it:</p>
<div class="codewrap"><div class="hl"><pre><span></span><code>prompt tokens      : 276
output tokens      : 64
TTFT               :   1713.4 ms
ITL p50 / p99      :    107.5 / 129.0 ms
end-to-end latency :     8.53 s
decode tokens/s    :      9.4
</code></pre></div><button class="copy" type="button" aria-label="Copy code">COPY</button></div>

<p>It matches the factory's own numbers, plus a little extra for the trip over the network. Point it at a real GPU server and the numbers get much smaller, but the <em>pattern</em> stays the same: the first word waits for reading, and then the words arrive at a steady beat.</p>
<h2 id="five-easy-mistakes-when-timing-a-chatbot">Five easy mistakes when timing a chatbot</h2>
<ol>
<li><strong>Starting the stopwatch at the wrong place.</strong> Start it when the user presses Enter, not when the server starts working. Waiting in line is part of the wait.</li>
<li><strong>Counting chunks instead of tokens.</strong> Some servers bundle a few tokens into one message. Check how many messages you got against how many tokens the server says it made.</li>
<li><strong>Only looking at the average gap.</strong> One 2-second freeze in a long answer barely changes the average, but everybody notices it. Always look at the slow end too (p99).</li>
<li><strong>Forgetting to warm up.</strong> The very first request is slow because the engine is still setting up. Don't count it.</li>
<li><strong>Mixing up "fast for me" and "fast for everyone."</strong> Per-person speed and whole-factory speed are different numbers. Keep them in separate columns.</li>
</ol>
<h2 id="cheat-sheet">Cheat sheet</h2>
<div class="tablewrap"><table>
<thead>
<tr>
<th>Machine</th>
<th>Real name</th>
<th>What it does</th>
<th>Speed</th>
</tr>
</thead>
<tbody>
<tr>
<td>Chopper</td>
<td>Tokenize</td>
<td>Text → token numbers</td>
<td>Practically instant</td>
</tr>
<tr>
<td>Reader</td>
<td>Prefill</td>
<td>Reads the whole question at once, fills the notebook</td>
<td>Cheap per token; total grows with question length</td>
</tr>
<tr>
<td>Notebook</td>
<td>KV cache</td>
<td>Notes on every token so far</td>
<td>Grows by one line per token</td>
</tr>
<tr>
<td>Typewriter</td>
<td>Decode</td>
<td>One token per full trip through the model</td>
<td>Expensive per token; the slow part</td>
</tr>
<tr>
<td>Delivery Tape</td>
<td>Stream</td>
<td>Sends each token to you immediately</td>
<td>Makes it <em>feel</em> fast</td>
</tr>
</tbody>
</table></div>
<div class="tablewrap"><table>
<thead>
<tr>
<th>Stopwatch</th>
<th>Measures</th>
<th>Mostly decided by</th>
</tr>
</thead>
<tbody>
<tr>
<td>TTFT</td>
<td>Wait for the first word</td>
<td>Length of your question</td>
</tr>
<tr>
<td>ITL</td>
<td>Gap between words</td>
<td>Model size + size of the notebook</td>
</tr>
<tr>
<td>Throughput</td>
<td>Tokens per second</td>
<td>Per person, or for everyone at once</td>
</tr>
</tbody>
</table></div>
<h2 id="whats-next">What's next</h2>
<p>Next time, we open up the Reader-Typewriter machine and look inside. <strong>F2: A Forward Pass for One Token: Where the Work Goes.</strong> We'll follow one token through a single layer and count the work done by each part, and find out why almost all of it is just multiplying big tables of numbers.</p>
<hr />
<p><em>Every code block in this post was actually run, and every number above comes from those runs. The tiny factory used random weights on a 2-core CPU, so it shows the pattern, not the speed of any real GPU.</em></p>]]></content:encoded></item>
</channel></rss>