rumblr Work in progressWIP

● The AI Primer · Lesson 16 · Part 1: how the model works inside

Inference

what happens, and what it costs, when a model generates

You'll be able to explain Prefill vs. decode, the KV cache, sampling, speculative decoding, memory math

Members · open during launch 34 min10 figures and diagrams9 interactive
Guide is what to use and when. How it works builds it from scratch. Math & code adds the formulas and the Python.

The lesson in one minute

What you'll be able to explain

  1. Prefill reads the prompt in parallel (compute-bound, sets time to first token); decode writes one token at a time (memory-bound, sets tokens per second).
  2. The KV cache stores every earlier token's keys and values so each step computes one token; it trades GPU memory for speed.
  3. Memory math: weights = parameters × bytes; KV per token = 2 × layers × KV heads × head dim × bytes. 70B at 16-bit is 140 GB; 32k tokens of Llama-3-8B cache is about 4 GB.
  4. Temperature reshapes the distribution; top-k and top-p cut the tail. Temperature 0 reduces but does not guarantee determinism.
  5. Speculative decoding: a small model drafts, the big one verifies in one pass; output is identical in distribution.
  6. Quantization, continuous batching and prompt caching are the other big serving levers.

Level 1

The practitioner's guide

In one sentence

Inference is everything that happens when a trained model answers a request, and its cost and speed are governed by one fact: each generated token reads every weight from memory, so the levers that matter (caching, batching, quantization, speculation, sampling settings) are all ways to read less or to reuse each read.

When you need it

You need this lesson the day a model leaves the notebook: when you set temperature and top_p on a request, when someone asks why the first token takes two seconds, when a GPU bill arrives, or when you decide whether to serve a model yourself. The tell is a latency or cost question you can only answer by guessing. The numbers here replace the guess. This lesson's model of an 8-billion-parameter network at 16 bits on an H100-class GPU (3.35 TB/s of memory bandwidth, about 10¹⁵ operations per second, the constants in this module) reads a 1,000-token prompt in about 16 ms but then produces at most about 209 tokens per second for a single user, because every token needs all 16 GB of weights read again (4.78 ms each). A lone request uses well under 1% of the GPU's arithmetic. You do not need this lesson for a prototype at ten requests a day; you need it before the first load test.

Your options

The levers a practitioner can pull, roughly from the cheapest to the most involved:

Lever What it does What it buys you What it costs Where it lives
Sampling settings Temperature reshapes the token probabilities; top-k and top-p cut the unlikely tail Control over variety: T = 0 for extraction and tool calls, higher for ideas Nothing in compute; wrong settings cost quality The request
Prompt caching Reuses the prefill of a shared prefix (system prompt, tools, documents) across calls This lesson's 100 calls with a 10,000-token prefix: $0.48 instead of $3.15, an 85% saving, and a faster first token A cache write at about 1.25× the input price on the first call (Anthropic's prompt caching docs) The provider, or your server's prefix cache
Quantization Stores weights at 8 or 4 bits instead of 16 A 70B model in 35 GB instead of 140 GB, and faster memory-bound decode 8-bit is nearly lossless; naive 4-bit loses small weights (the lesson's 0.02 rounds to 0) unless a smarter method such as GPTQ or AWQ is used The model files and the server
A model with grouped-query attention Fewer key/value heads means a smaller KV cache per token 8 KV heads instead of 32 fit four times as many long conversations per GPU A model choice made at training time; you can only pick a model that has it The model architecture
Continuous batching Seats a new request the moment any slot frees, instead of waiting for the whole batch In this lesson's 32-request simulation, 82% slot utilisation instead of 60%, in fewer steps Nothing beyond a server that does it (they all do now) The inference server
Speculative decoding A small model drafts several tokens; the big one verifies them in one pass With an 80% acceptance rate and 4 drafts, 3.36 tokens per big-model pass instead of 1, with the same output distribution A draft model to run, and gains that shrink when the draft guesses badly or the server is already batch-saturated The inference server
A hosted API Someone else runs all of the above No GPUs to size, caching and batching done for you A per-token price, and less control over settings and residency The provider

How to choose

Start from the symptom.

  • Slow first token: the prompt is long. Trim it, or put its stable part first and let prompt caching skip its prefill.
  • Slow streaming: decode is memory-bound. Quantize, batch more requests together, or add speculative decoding.
  • Running out of GPU memory as traffic grows: it is the KV cache, not the weights. Do the arithmetic (weights plus cache per token times context times concurrent requests) before renting a bigger card; prefer a model with grouped-query attention and cap the context you allow.
  • Answers that vary when you want them stable: temperature 0 and a validator, not a hope. Answers that all sound the same when you want range: raise the temperature and let top-p keep the tail sane.
  • Deciding whether to self-host: only when volume, privacy or a model the APIs do not offer justifies owning the batching and memory problems above.
  • Whatever you pick, measure time to first token and tokens per second separately. They are set by different phases and fixed by different levers.

What it costs

Money follows tokens, and tokens follow decode. Prefill of 1,000 tokens costs 16 ms of a GPU's full compute; each output token costs a full read of the weights, so output tokens are the expensive ones, and providers price them that way. Memory sets capacity: on this lesson's Llama-3-8B-shaped model each token of context holds about 128 KB of keys and values, a 32,000-token conversation holds 4.2 GB, and an 80 GB GPU with 16 GB of weights fits 15 such conversations at once. Batching is what makes serving economical: one read of the weights serves every request in the batch, which is why the roofline figure in Level 2 shows a batch of 64 reaching 21% of the GPU's arithmetic where a single user reaches 0.3%. Prompt caching costs a little on the first call and saves most of the input bill after it; Anthropic prices a five-minute cache write at 1.25× and a read at 0.1× the input price, with a one-hour write at 2× (its prompt caching docs). Quantization costs a little quality for a large memory saving. Speculative decoding costs a second model and a more complex server.

What breaks

  • Temperature 0 still varies. Floating-point addition depends on order, and GPU kernels change their order with the batch they land in. One published measurement found 80 distinct completions in 1,000 runs at temperature 0, identical for the first 102 tokens and then diverging (Thinking Machines, Defeating Nondeterminism in LLM Inference). Treat determinism as reduced, not guaranteed, and validate outputs.
  • A timestamp at the top of the prompt silently disables prompt caching, because a cache matches only up to the first differing token. Stable content first, volatile content last; on Anthropic's API the order is tools, then system, then messages.
  • Short prompts are not cached. Providers set a minimum cacheable length (Anthropic's is between 512 and 4,096 tokens depending on the model) and return no error below it; the bill just does not fall.
  • A long-context feature exhausts memory. Doubling the context you allow doubles the cache per request and halves the requests that fit.
  • Naive 4-bit quantization erases small weights. Use a method that compensates (GPTQ, AWQ) and check quality on your own evaluation, not on the model card.
  • Top-k with a fixed k cuts too much when the model is unsure and too little when it is confident; top-p adapts, which is why it is the usual default.
  • Speculation that guesses badly costs more than it saves: the big model's pass still runs, and every rejected draft is wasted work.

In the wild

vLLM's documentation lists continuous batching, chunked prefill, prefix caching, PagedAttention for KV memory, speculative decoding (n-gram and EAGLE drafts among others) and quantization from FP8 to INT4, GPTQ and AWQ, behind an OpenAI-compatible API. SGLang offers the same set with RadixAttention for prefix caching; NVIDIA's TensorRT-LLM does it with custom kernels and FP8 and FP4 formats on NVIDIA GPUs; llama.cpp runs quantized models from 1.5-bit to 8-bit on CPUs and Apple silicon. Hugging Face's Transformers exposes the sampling knobs (greedy by default, sampling with do_sample, beam search with num_beams). Hosted APIs expose prompt caching explicitly, with Anthropic's linked in Further reading. The ideas come from the papers at the end of this lesson: speculative decoding (Leviathan, Kalman and Matias), PagedAttention (Kwon et al.), FlashAttention (Dao et al.), LLM.int8() and GPTQ for quantization, and nucleus sampling (Holtzman et al.).

Go deeper

Level 2 builds each lever from nothing: the roofline that explains why decode is memory-bound, a tiny decoder with and without a KV cache whose outputs match to ten decimal places, the temperature and top-p arithmetic, the accept-or-reject rule that makes speculative decoding exact, a quantizer in five lines, and simulations of both batching policies. If you only needed to size a deployment or set a request, you are done.

Level 2

How it works, from scratch

Training happens once; inference happens every time anyone uses the model, so this is where the money goes. Generating text has two very different phases, one memory trick that makes it affordable (the KV cache), a few knobs that decide which token comes out (sampling), and a toolbox of speedups: quantization, speculative decoding, continuous batching and prompt caching. Every one of them follows from one fact:

Generating one token requires reading every weight of the model from memory, and memory is much slower than arithmetic.

Chapter 1

Two phases: prefill and decode

Everyday picture You are handed a letter and asked to reply. Reading the letter is fast: your eyes take in whole lines at once. Writing the reply is slow: one word at a time, and before each word you must walk to a filing cabinet and flip through an entire reference binder. The walk, not the thinking, is what takes the time.

The model is the same. Prefill reads the whole prompt in one parallel pass. Decode then produces the answer one token at a time, and every single token requires streaming all the weights from GPU memory.

Tiny worked example An 8-billion-parameter model stored at 16 bits is 16 GB of weights. On a GPU that moves 3.35 TB/s from memory and does about 10¹⁵ 16-bit operations per second:

  • Prefill of a 1,000-token prompt: 2 × 8×10⁹ × 1,000 = 1.6×10¹³ operations, about 16 ms.
  • Decode: read 16 GB for every token, 16×10⁹ / 3.35×10¹² s = 4.78 ms per token, at most about 209 tokens per second for a single user.

Figure 2 · Diagram

Reading it: time runs downward. The first arrow is prefill: one big parallel pass over the whole prompt, which also fills the KV cache (section 2). It decides the time to first token. Everything inside the loop is decode: one small pass per token, each reading the cache and adding one entry to it. It decides tokens per second. Long prompts slow the first token; long answers slow the total.

The reason the phases behave so differently is arithmetic intensity: how many operations you do for each byte you fetch from memory.

Level 3: the formula and its symbols

Symbols

Symbol Meaning here Shape / range
arithmetic intensity: operations per byte of weights read FLOPs/byte
tokens processed in one pass (1 when decoding, the prompt length when prefilling) ≥ 1
one multiply plus one add per weight per token
bytes per weight (2 at 16-bit, 0.5 at 4-bit)
number of parameters (weights) e.g. 8×10⁹
BW memory bandwidth: bytes the GPU can read per second e.g. 3.35×10¹²
FLOPS arithmetic throughput: operations per second e.g. 10¹⁵
"roughly": these are lower bounds that ignore overheads

In words: decode does two operations per two-byte weight it reads, so its speed is set by memory bandwidth; prefill reuses each weight for every prompt token, so its speed is set by arithmetic.

On the worked example: decode I = 2 × 1 / 2 = 1 FLOP per byte; prefill of 1,000 tokens I = 1,000. The GPU breaks even at 10¹⁵ / 3.35×10¹² ≈ 299 FLOPs per byte, so decode sits far below it (memory-bound) and prefill far above (compute-bound).

Level 3: in Python
# 8 billion weights at 2 bytes each (16-bit)
P, b = 8e9, 2
# bytes read per second, operations per second
BW, FLOPS = 3.35e12, 1e15
def I(n):
    # operations per byte of weights read
    return 2 * n / b
# decode, then a 1,000-token prefill
I(1), I(1000)  # → (1.0, 1000.0)
# the break-even intensity
round(FLOPS / BW)  # → 299
# t_decode, in milliseconds per token
round(P * b / BW * 1000, 2)  # → 4.78
# t_prefill for 1,000 tokens, in milliseconds
round(2 * P * 1000 / FLOPS * 1000, 1)  # → 16.0

Figure 1 · Drawn from the lesson's code

1 0 0 1 0 1 1 0 2 1 0 3 1 0 4 arithmetic intensity (FLOPs per byte of weights read) 1 0 1 2 1 0 1 3 1 0 1 4 1 0 1 5 attainable FLOP/s Decode is memory-bound; prefill is compute-bound break-even ≈ 299 FLOPs/byte decode, 1 user (I = 1) decode, batch of 64 (I = 64) prefill, 1,000 tokens (I = 1000)

Decode for 1 user uses 0.3% of the GPU's compute and a batch of 64 reaches 21%, while a 1,000-token prefill passes the 299 break-even to run at full speed

Reading it: the x-axis is arithmetic intensity (log scale); the y-axis is the speed the GPU can actually reach. The sloped part of the roof is the memory limit (bandwidth × intensity); the flat part is the arithmetic limit. The corner is the break-even point, ~299. Decode at batch size 1 sits at intensity 1, deep in the memory-bound region, using well under 1% of the GPU's arithmetic. Batching many users together moves decode to the right, because one read of the weights then serves every user in the batch. That is the economic reason inference servers batch aggressively.

In code: arithmetic_intensity computes I, ridge_point finds the break-even, and bottleneck says which side a pass falls on; decode_seconds_per_token and prefill_seconds give the two time bounds.

Why it matters in practice. When a system feels slow, ask which phase dominates. Slow first token: the prompt is long (trim it, or cache it). Slow streaming: decode is memory-bound (quantize, batch, speculate).

The roofline above, with the number of tokens per pass in your hand.

Chapter 2

The KV cache: take notes instead of rereading

Count the work yourself first; the cache will read as the obvious move.

Everyday picture Reading a mystery novel, you don't reread the whole book before each new sentence; you keep notes on every character and clue. For each new sentence you glance at your notes and add one line.

Attention needs every earlier token's key and value (see primer.ml.attention). Because of causal masking, an earlier token's keys and values never change when later tokens arrive, so they can be computed once and kept.

Tiny worked example An 8-token prompt, then 16 generated tokens.

  • Without a cache, step k re-processes the whole sequence so far: 8 + 9 + … + 23 = 248 token positions.
  • With a cache: prefill the 8 prompt tokens once, then 1 new position for each of the next 15 tokens = 23 token positions.

On TinyDecoder that is about 11× fewer operations for this short run, and the gap grows with length: without a cache the total work grows with the square of the length.

Figure 4 · Diagram

Reading it: the top row recomputes a sequence that grows by one each step. The bottom row computes each token exactly once: prefill stores keys and values for the prompt, and each decode step computes only the newest token's query, key and value, reading everything older from the cache. The outputs are identical (TinyDecoder checks this to 10 decimal places); only the cost differs.

Figure 3 · Drawn from the lesson's code

0 5 10 15 20 25 30 tokens generated after an 8-token prompt 0.0 0.2 0.4 0.6 0.8 1.0 1.2 total FLOPs so far (TinyDecoder) 1e7 The KV cache turns quadratic work into linear work no cache: recompute the whole sequence KV cache: compute only the new token

Without a cache, total work curves upward to about 20 times the cached total after 32 tokens; with the cache it grows in a straight line

Reading it: the x-axis is how many tokens have been generated after an 8-token prompt; the y-axis is total operations spent so far, counted inside TinyDecoder. Without a cache the curve bends upward (each step costs more than the last); with a cache it is a straight line (each step costs about the same). The gap between them is pure waste the cache removes.

In code: TinyDecoder.forward_full processes a whole sequence (and fills a cache during prefill), TinyDecoder.forward_step processes one new token against the cache from TinyDecoder.new_cache, and TinyDecoder.generate runs either way, returning a Generation that holds the tokens and the work counted.

Why it matters in practice. The cache trades memory for speed, and that memory is what limits how many users one GPU can serve. Section 3 does the math.

Chapter 3

Memory math: will it fit?

Everyday picture Packing for a trip. The suitcase is GPU memory. The model's weights are the big fixed items that always go in. Every active conversation adds a bag of notes (its KV cache) whose size grows with the conversation's length. Once the suitcase is full, the next customer waits.

Tiny worked example

  • Weights: 70×10⁹ parameters × 2 bytes (16-bit) = 140 GB: more than one 80 GB GPU. At 4 bits (half a byte) it is 35 GB and fits on one.
  • KV cache per token for a Llama-3-8B-shaped model (32 layers, 8 KV heads, 128 dimensions per head, 16-bit): 2 × 32 × 8 × 128 × 2 = 131,072 bytes, about 128 KB.
  • A 32,000-token conversation: 32,000 × 131,072 ≈ 4.2 GB of cache.
  • An 80 GB GPU holding 16 GB of weights has 64 GB left: room for 15 such conversations at once.
Level 3: the formula and its symbols

Symbols

Symbol Meaning here Shape / range
number of parameters e.g. 7×10¹⁰
bits / 8 bytes per parameter (16 bits = 2 bytes)
one key and one value per token
number of transformer layers, each with its own cache e.g. 32
number of key/value heads (fewer than query heads with grouped-query attention) e.g. 8
dimensions per head e.g. 128
bytes per stored number 2 at 16-bit

In words: weights cost parameters times bytes each; the cache costs, for every token, one key and one value per layer per KV head.

On the worked example: 7×10¹⁰ × 16/8 = 1.4×10¹¹ bytes = 140 GB; and 2 × 32 × 8 × 128 × 2 = 131,072 bytes per token.

Level 3: in Python
P = 7e10
# weight GB at 16 bits, then at 4 bits
P * 16 / 8 / 1e9, P * 4 / 8 / 1e9  # → (140.0, 35.0)
L, H_kv, d_h, b = 32, 8, 128, 2
# KV bytes per token: a key and a value, per layer, per KV head
2 * L * H_kv * d_h * b  # → 131072
# GB of cache for a 32,000-token conversation
round(32_000 * 131_072 / 1e9, 1)  # → 4.2

Figure 5 · Drawn from the lesson's code

0 20 40 60 80 100 120 context length per request (thousands of tokens) 0 10 20 30 40 50 60 70 KV cache for one request (GB) Long contexts eat GPU memory; fewer KV heads help 32 KV heads (multi-head attention) 8 KV heads (grouped-query, Llama-3-8B) free memory: 80 GB GPU − 16 GB weights

At 128k tokens, 32 KV heads need 67 GB, more than the 64 GB free, while 8 KV heads need 17 GB, so three such requests fit

Reading it: the x-axis is context length per request; the y-axis is KV cache memory for one request. The steep line is a model with 32 KV heads (classic multi-head attention); the shallow one has 8 (grouped-query attention, like Llama 3). The dashed line is the 64 GB left on an 80 GB GPU after 16 GB of weights. With 32 KV heads a single 128k-token request would not fit; with 8 it fits three times over. Doing this arithmetic out loud is the fastest way to size a deployment.

Figure 6 · Interactive · computed from the lesson's code

KV cache memory calculator

Try it: pick a model shape, then drag the context length and the number of requests. The bar is one GPU's memory: the grey part is the weights, the coloured part the KV cache. Watch how quickly a long context pushes the cache past the weights, and how much further 8 KV heads go than 32.

In code: weight_bytes and kv_cache_bytes_per_token are the two formulas, kv_cache_bytes scales the cache to a context and batch, and max_concurrent_requests counts how many conversations fit beside the weights.

The calculator above answers whether it fits; this shows the arithmetic that decides it, one factor at a time.

Chapter 4

Sampling: from scores to one token

Turn the three knobs before reading what each one is called.

Everyday picture Choosing where to eat. Greedy always picks the top-rated place. Sampling holds a lottery weighted by rating. Temperature is how adventurous you feel: low means you nearly always pick the favourite, high means long shots get a real chance. Top-k says "only consider the top 3". Top-p says "consider just enough places to cover 90% of my enthusiasm": one place if you have a clear favourite, several if you're torn.

Tiny worked example Scores (logits) 2, 1, 0 for three tokens:

temperature probabilities
0 (greedy) 1, 0, 0
0.5 0.867, 0.117, 0.016
1 0.665, 0.245, 0.090
2 0.506, 0.307, 0.186

With probabilities 0.5, 0.3, 0.15, 0.05: top-k = 2 keeps 0.5 and 0.3, renormalised to 0.625 and 0.375. Top-p = 0.9 keeps 0.5, 0.3 and 0.15 (the first set whose total reaches 0.9) and cuts the 0.05 tail.

Figure 8 · Diagram

Reading it: the model only ever produces the scores on the left; everything after that is a choice you make at request time. Temperature reshapes the whole distribution; top-k and top-p then cut off the unreliable tail before the draw, so a rare nonsense token can't be picked by bad luck.
Level 3: the formula and its symbols

Symbols

Symbol Meaning here Shape / range
the model's score (logit) for vocabulary token i any real
temperature > 0; T → 0 approaches greedy
the exponential function; makes every score positive and stretches gaps
sum over every token j in the vocabulary, so the add to 1
probability of drawing token i 0 … 1

In words: divide every score by the temperature, exponentiate, and divide by the total so the results sum to one.

On the worked example: T = 0.5 turns (2, 1, 0) into (4, 2, 0); e⁴ = 54.6, e² = 7.39, e⁰ = 1, total 63.0; probabilities 0.867, 0.117, 0.016.

Level 3: in Python
import math
z, T = [2.0, 1.0, 0.0], 0.5
# e^(z_i / T)
exps = [math.exp(z_i / T) for z_i in z]
# Σ_j e^(z_j / T)
round(sum(exps), 1)  # → 63.0
# p_i
[round(e / sum(exps), 3) for e in exps]  # → [0.867, 0.117, 0.016]

Figure 7 · Drawn from the lesson's code

0 1 2 3 4 candidate token 0.0 0.2 0.4 0.6 0.8 probability T = 0.5 0 1 2 3 4 candidate token T = 1.0 0 1 2 3 4 candidate token T = 2.0 Temperature reshapes the distribution; top-p (hatched = cut) trims the tail

Five tokens at three temperatures: at T = 0.5 the favourite takes 79% and top-p 0.9 cuts three tokens; at T = 2 it falls to 38% and only one is cut

Reading it: five candidate tokens with fixed scores, shown at three temperatures. At T = 0.5 (left) the favourite takes 79%; at T = 2 (right) it falls to 38% and the rest spread out, down to 7% for the least likely. The hatched bars are the tokens top-p = 0.9 would cut: three at T = 0.5, two at T = 1, one at T = 2. A confident model reaches 90% with fewer tokens, so top-p cuts more of them; an unsure one needs more tokens to reach 90%, so it cuts fewer. That's why top-p adapts where a fixed top-k can't.

Temperature 0 is not a determinism guarantee. Floating-point addition isn't associative: in 32-bit floats, (10⁸ + 1) − 10⁸ = 0 but (10⁸ − 10⁸) + 1 = 1. On a GPU the order of additions can depend on the kernel chosen and on what else is in the batch, so two nearly tied tokens can swap places between otherwise identical requests.

In code: temperature_probs applies the formula, top_k_filter and top_p_filter cut the tail, and sample_next chains them into one draw. float32_sum and greedy_pick_with_summation_order show a near tie flipping with the order of additions.

Why it matters in practice. Use low temperature for extraction, classification and tool calls; higher for brainstorming and creative text.

Chapter 5

Speculative decoding: a junior drafts, a senior checks

Everyday picture A junior writer drafts the next few sentences quickly. A senior editor reads the whole draft at once, keeps every sentence they would have written themselves, rewrites the first one they wouldn't, and throws away the rest. The senior's reading is fast; their writing is slow. So the team moves at the junior's speed but produces the senior's text.

This works because decode is memory-bound: checking 5 draft tokens in one pass of the big model costs about the same as generating 1.

Tiny worked example The big model's next-token probabilities are (0.5, 0.3, 0.2); the small model's are (0.3, 0.3, 0.4). A draft token survives with probability Σ min = 0.3 + 0.3 + 0.2 = 0.8. Drafting 4 tokens per round yields on average (1 − 0.8⁵) / (1 − 0.8) = 3.36 tokens per pass of the big model instead of 1.

Figure 9 · Diagram

Reading it: the loop in the middle walks the drafts left to right. A draft the big model likes at least as much as the small one did (p ≥ q) is always kept; one it likes less is kept only with probability p/q. The first rejection is replaced by a token drawn from exactly the probability the small model under-proposed, which is what makes the final output statistically identical to sampling from the big model alone. The test suite checks this empirically over 20,000 rounds.
Level 3: the formula and its symbols

Symbols

Symbol Meaning here Shape / range
the big (target) model's probability for token x 0 … 1
the small (draft) model's probability for token x 0 … 1
the smaller of a and b
alpha, the acceptance rate: how often a draft survives 0 … 1
gamma, how many tokens the small model drafts per round e.g. 4
expected value: the long-run average

In words: keep a draft with probability "how much the big model likes it compared with the small one, capped at 1"; the average number of tokens per big-model pass is a geometric series in the acceptance rate.

On the worked example: α = 0.8, γ = 4: (1 − 0.8⁵)/(1 − 0.8) = (1 − 0.328)/0.2 = 3.36.

Level 3: in Python
# big model
p = [0.5, 0.3, 0.2]
# small model
q = [0.3, 0.3, 0.4]
# P(keep x) for each token
[round(min(1.0, p_x / q_x), 2) for p_x, q_x in zip(p, q)]  # → [1.0, 1.0, 0.5]
alpha = sum(min(p_x, q_x) for p_x, q_x in zip(p, q))
round(alpha, 2)  # → 0.8
gamma = 4
# expected tokens per big-model pass
round((1 - alpha ** (gamma + 1)) / (1 - alpha), 2)  # → 3.36

In code: acceptance_rate computes α and expected_tokens_per_round the geometric series; speculative_round runs the draft, verify and replace loop once, and speculative_generate repeats it until enough tokens exist.

Chapter 6

Quantization: fewer bits per weight

Snap the lesson's three weights to a ruler first; the formula below is that ruler.

Everyday picture Writing prices to the nearest dollar instead of the nearest cent: shorter to store, slightly less exact. Using one ruler per row of a spreadsheet, instead of one for the whole sheet, keeps a single huge number in one row from making every other row coarse.

Tiny worked example A row of weights (0.5, −1.27, 0.02).

  • int8: the largest magnitude, 1.27, maps to 127, so the step size is 0.01. Codes: 50, −127, 2. Decoding gives back exactly (0.5, −1.27, 0.02).
  • int4: only 15 levels (−7 … 7), step size 1.27/7 = 0.181. Codes: 3, −7, 0. Decoding gives (0.544, −1.27, 0): the small weight vanished.
Level 3: the formula and its symbols

Symbols

Symbol Meaning here Shape / range
the j-th original weight in the row real
the largest absolute value in the row ≥ 0
the largest code: 127 for 8 bits, 7 for 4 bits
the scale (step size), one per row > 0
the stored integer code −127…127 or −7…7
the weight as reconstructed at inference time ("w-hat") real

In words: pick a step size so the largest weight lands on the largest code, store each weight as the nearest whole number of steps, and multiply back at run time.

On the worked example: int4: s = 1.27/7 = 0.181; 0.5/0.181 = 2.76 → 3; 3 × 0.181 = 0.544.

Level 3: in Python
w = [0.5, -1.27, 0.02]
def quantize(w, bits):
    # the step size
    s = max(abs(w_j) for w_j in w) / (2 ** (bits - 1) - 1)
    # c_j: whole steps
    return s, [round(w_j / s) for w_j in w]
s, c = quantize(w, bits=8)
round(s, 3), c  # → (0.01, [50, -127, 2])
s, c = quantize(w, bits=4)
round(s, 3), c  # → (0.181, [3, -7, 0])
# ŵ_j = s · c_j: the 0.02 is gone
[round(s * c_j, 3) for c_j in c]  # → [0.544, -1.27, 0.0]

In code: quantize returns the integer codes and one scale per row, dequantize multiplies them back, and quantization_error measures how far the round trip lands from the original weights.

Why it matters in practice. 8-bit weights are nearly lossless; 4-bit methods with smarter rounding (GPTQ, AWQ) keep most quality at a quarter of the memory. Fewer bytes per weight also means faster memory-bound decode.

Chapter 7

Continuous batching: seat the next party as soon as a table frees

Everyday picture A restaurant with two tables. Static batching seats two parties and won't seat anyone new until both have left, so a table sits empty while one slow diner lingers. Continuous batching seats the next party the moment any table frees.

Tiny worked example Four requests needing 4, 1, 1 and 1 decode steps, two slots. Static: {4, 1} runs 4 steps with one slot idle for 3, then {1, 1} runs 1: 5 steps, 70% of slot-steps busy. Continuous: the short requests slide into the freed slot while the long one runs: 4 steps, 87.5% busy.

Figure 10 · Drawn from the lesson's code

0 2 4 6 slot static batching: 433 steps, 60% of slot-steps busy 0 50 100 150 200 250 300 decode step 0 2 4 6 slot continuous batching: 318 steps, 82% of slot-steps busy

With 32 requests on 8 slots, static batching leaves idle gaps and needs 433 steps at 60% busy; continuous batching needs 318 steps at 82%

Reading it: each row is a GPU batch slot and each column is one decode step; colour identifies the request occupying the slot, and white is an idle slot. On a realistic mix of 32 requests of varied length over 8 slots, static batching (top) leaves white holes wherever a short request finished early; continuous batching (bottom) keeps nearly every cell busy and finishes the same work in fewer steps (about 82% vs. 60% utilisation).
Level 3: the formula and its symbols

Symbols

Symbol Meaning here Shape / range
utilisation: the share of slot-steps doing real work 0 … 1
useful slot-steps total decode steps all requests need integer
steps × slots the capacity the GPU offered while serving them integer

In words: utilisation is the work the requests needed divided by the work capacity the server spent serving them.

On the worked example: 7 useful slot-steps; static 7/(5×2) = 0.70, continuous 7/(4×2) = 0.875.

Level 3: in Python
# decode steps the four requests need
useful = 4 + 1 + 1 + 1
slots = 2
# static takes 5 steps, continuous 4
useful / (5 * slots), useful / (4 * slots)  # → (0.7, 0.875)

In code: simulate_static_batching and simulate_continuous_batching play out the two policies step by step, each returning a ServingRun that holds the slot timeline and its utilisation U.

Why it matters in practice. Continuous batching (together with paged KV-cache memory, as in vLLM) is a large part of why modern inference servers reach high throughput.

Chapter 8

Prompt caching: reuse the prefill of a shared prefix

Everyday picture A kitchen that pre-chops the ingredients every order uses, instead of chopping them again for each plate.

The KV cache normally lives for one request. Prompt caching keeps it across requests: if many calls start with the same long system prompt, tool definitions or reference document, the provider stores that prefix's keys and values and skips its prefill next time. It is only valid up to the first differing token, because every token's keys depend on everything before it, so put stable content first and volatile content last.

Tiny worked example 100 requests, each a 10,000-token shared prefix plus a 500-token question, at $3 per million input tokens, with cache writes at 1.25× and cache reads at 0.1× (typical of providers; check current pricing):

  • Without caching: 100 × 10,500 × $3/10⁶ = $3.15.
  • With caching: the first call writes the cache ($0.039); each of the other 99 costs $0.0045. Total $0.48, an 85% saving, and each cached call also skips 10,000 tokens of prefill, so its first token arrives sooner.

Figure 11 · Diagram

Reading it: the prompt is read left to right, and the cache can cover only an unbroken run from the very start. Stable parts go first so that every request shares the longest possible prefix; the part that changes on every call goes last. A timestamp or request ID placed at the top would break the match on the first token and silently disable caching.

In code: reusable_prefix_tokens counts how many leading tokens two prompts share, and prompt_cache_cost prices a run of requests with and without the cache.

Test yourself

9 questions

Answer each one out loud or on paper before you open it. If you can explain it, you know it.

Question 1Q: Why is decoding slow even on a huge GPU?Think it through, then reveal

A: Each token needs every weight read from memory but does only about one operation per byte read, far below the GPU's break-even (~300 FLOPs/byte). The arithmetic units mostly wait on memory.

Question 2Q: What is the KV cache, and why does it matter for serving cost?Think it through, then reveal

A: Stored keys and values of all earlier tokens, so each new token is computed once instead of recomputing the whole sequence. It grows with context length and batch size, and that memory caps how many requests a GPU serves at once.

Question 3Q: How much memory does a 70B model need at 16-bit and at 4-bit?Think it through, then reveal

A: 140 GB and 35 GB for weights alone, plus KV cache and activations.

Question 4Q: Estimate the KV cache for one 32k-token request on a Llama-3-8B-shaped model.Think it through, then reveal

A: 2 × 32 × 8 × 128 × 2 = 131,072 bytes per token; × 32,000 ≈ 4.2 GB.

Question 5Q: Why does grouped-query attention make serving cheaper?Think it through, then reveal

A: It shares each key/value head among several query heads, shrinking the KV cache (4× for 32 → 8 KV heads), so more requests fit per GPU.

Question 6Q: Why isn't temperature 0 perfectly deterministic?Think it through, then reveal

A: Floating-point addition isn't associative, and GPU reduction order can change with kernels and batch composition, so nearly tied logits can flip.

Question 7Q: How can speculative decoding be faster yet produce the same distribution?Think it through, then reveal

A: Verifying several draft tokens costs one memory-bound pass of the big model, about the same as generating one. The min(1, p/q) accept rule plus resampling from max(0, p − q) on rejection makes the output exactly the big model's distribution.

Question 8Q: What does continuous batching fix?Think it through, then reveal

A: Idle slots: static batches wait for their longest request, while continuous batching refills any freed slot immediately, raising utilisation.

Question 9Q: How do you structure a prompt to benefit from prompt caching?Think it through, then reveal

A: Put stable content (system prompt, tool definitions, reference documents) first and anything that varies (the question, timestamps, IDs) last, because a cache is valid only up to the first differing token.

Primary sources

The papers behind this lesson

Leviathan, Kalman & Matias, Fast Inference from Transformers via Speculative Decoding (2022): Introduced the draft-then-verify scheme with an accept/reject rule that provably preserves the target model's output distribution.

Read the annotated companion →The paper ↗

Kwon et al., Efficient Memory Management for Large Language Model Serving with PagedAttention (2023): Stored the KV cache in fixed-size pages like an operating system's virtual memory, eliminating fragmentation and enabling vLLM's high-throughput continuous batching.

Read the annotated companion →The paper ↗

Dao et al., FlashAttention (2022): Computed exact attention in GPU-memory-sized tiles, cutting the slow memory traffic that dominates long-context inference.

Read the annotated companion →The paper ↗

Dettmers et al., LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale (2022): Showed that a few outlier features break naive 8-bit quantization and handled them separately.

The paper ↗

Frantar et al., GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers (2022): Made 3-4-bit weight quantization practical with error-compensating rounding.

The paper ↗

Holtzman et al., The Curious Case of Neural Text Degeneration (2019): Introduced nucleus (top-p) sampling.

The paper ↗

Researcher's shelf

Further reading

  • vLLM documentation: https://docs.vllm.ai/
  • Hugging Face, Text generation strategies: https://huggingface.co/docs/transformers/generation_strategies
  • Anthropic, Prompt caching: https://docs.claude.com/en/docs/build-with-claude/prompt-caching
  • Thinking Machines Lab, Defeating Nondeterminism in LLM Inference: https://thinkingmachines.ai/blog/defeating-nondeterminism-in-llm-inference/
  • Grattafiori et al., The Llama 3 Herd of Models (2024): https://arxiv.org/abs/2407.21783

About this lesson. This is the illustrated edition of a lesson from the open-source AI Primer. Its text, figures and numbers are generated from the Primer's source at commit 048aeaa, so the two always agree: the explanation, the code that builds it and the tests that prove it.