Module 12 · Part III · Building an LLM
Generation
The Autoregressive Loop, the KV Cache, and Decoding Strategies From Greedy to Nucleus Sampling
By the end you can implement a decoding loop with a KV cache and choose a decoding strategy for a task.
Check yourself ▸
Before you start, can you already…
- ☐choose the right family for a task and explain why decoder-only won for generation. (Module 8)
If any is shaky, the linked module teaches it.
A trained decoder-only model answers one question: given these tokens, how likely is each next token? Generation turns that into text, one token at a time. This module covers the loop, the cache that stops it redoing work, and the rules for choosing each token. Module 23 returns to the loop when serving many users at once.
12.1 The Autoregressive LoopRun this section
Generation is a loop around the model:
- Tokenize the prompt.
- Run the model on the tokens so far and take the logits at the last position: the scores for the next token.
- Choose one token from them, with one of the decoding rules of section 12.3.
- Append it to the sequence.
- Stop if the token is the special end-of-sequence token, or a length limit is reached, or a stop string has appeared. Otherwise go back to step 2.
tokens = tokenizer.encode(prompt)
for _ in range(max_new_tokens):
logits = model(tokens)[-1] # scores for the next token only
next_token = choose(logits) # greedy, top-k, top-p ... (section 12.3)
tokens.append(next_token)
if next_token == tokenizer.eos_id:
break
text = tokenizer.decode(tokens)Two features drive the rest of the module. Each step depends on the previous token, so steps run strictly in order. And as written, step reruns the model over all tokens though only the last is new; section 12.2 removes that waste.
The first pass is different: the prompt's tokens are all known, so they are processed in one parallel pass, as in training. This is ; the one-token steps after it are decode. Section 23.2 shows why their costs differ so much.
12.2 The KV CacheRun this section
Rerunning the whole sequence at step costs on the order of operations, since attention compares every token with every other. Over steps:
("big O") describes how cost grows, ignoring constant factors. Doubling the output length makes generation about eight times slower.
Most of that work is repeated. Under the causal mask, earlier tokens never see later ones, so the keys and values of tokens are the same at every step. Computed once and stored, they form the KV cache.
In words. At step t, only the newest token becomes a query, key and value. The key and value are appended to the cache, and the one new query attends over all of it.
Mathematical KV Cache Derivation
At step , compute a query for the newest token only:
Append its key and value as new rows of the caches (the semicolon stacks the new row under the old):
The superscript labels the step, not a power. The output is ordinary attention, one query row against the whole cache:
This is exactly the of full recomputation, with no approximation. Each step projects one token instead of and computes one row of scores instead of , so tokens cost on the order of instead of .
Generating one token at a time. Without a cache, every step recomputes the whole triangle of attention scores and every key and value. With the cache, a step adds one row of scores and one new key and value.
Without a cache, how many attention scores are computed in total to generate 12 tokens, if step computes all scores of the causal triangle (counting only the allowed ones)? With a cache, each step computes just one new row.
In words. Twelve generation steps, shading the scores each computes. Without the cache, every step rebuilds the whole triangle and re-projects every key and value; with it, each step adds one row and one key and value. That is 78 scores instead of 364.
The new token must also get its real position (index counting from 0), not position 0. The reference positional_embedding.py numbers its input from 0 on every call, so with a cache the caller must pass the offset; RoPE (section 15.1) needs the same. A cache that forgets the offset gives every new token the first token's position, and output quietly degrades.
Measured (lab 3a). A cache built on this repository's own
MultiHeadAttention, reusing its weights and changing no projection, reproduces the uncached logits position by position. The largest absolute difference is , and the top token agrees at every position (). Over a 24-token decode, K/V projections fall from to , a reduction. Wall-clock time improves by only at 112k parameters, because in a model this small the fixed cost of each call outweighs the arithmetic. The projection count is the measure that carries over to larger models.
Caveat. Do not test a cache by comparing generated text. The two paths can differ in the last bit of a float, and when two tokens nearly tie, that picks a different token and the outputs diverge: a tie-break, not a bug. Instead feed both paths the same tokens and compare logits at each position (teacher forcing). A difference there is real, usually because the cached path does not mirror the block exactly, such as normalizing in a different place (section 8.5).
The cache trades arithmetic for memory: one key and value per token, layer and head. For long contexts and many users, that memory is the main cost of serving (section 23.1).
12.3 Decoding: From Logits to TextRun this section
A decoding strategy chooses the next token from the logits . Most also apply a temperature (section 4.3) first: low sharpens toward the top token, high flattens.
-
Greedy decoding. Always pick . Deterministic, so often used for code and maths, where there is usually one right answer, but it tends to loop and its prose is flat.
-
Top- sampling. Keep the highest-scoring tokens (say ), set the other logits to , softmax and sample. This cuts the long tail of unlikely tokens, where most nonsense comes from.
-
Top- (nucleus) sampling (Holtzman et al., 2020). Keep the smallest set of top tokens whose probabilities reach , renormalize and sample:
The set adapts: it grows when the model is unsure and shrinks to one or two tokens when the next word is obvious, which a fixed cannot do.
-
Repetition penalty (Keskar et al., 2019). Every token already in the output gets a less favorable logit, with a strength slightly above 1 (a chosen number, not the model parameters):
introduces 2 notationsPositive logits shrink toward zero and negative ones grow more negative: either way the token becomes less likely, discouraging loops.
-
Beam search. Keep the several most probable partial outputs (a beam of 4 or 5), extend each by every token, and keep the best by total log-probability. It suits tasks with one right answer, such as translation, but open-ended text comes out bland, because the most probable text is rarely the most natural, so chat models sample instead. Summed log-probabilities favor short outputs, so scores are divided by a power of the length.
The model’s top ten next tokens for two prompts. Greedy keeps one, top-k keeps a fixed number, and top-p keeps however many it takes to reach probability p. Compare the confident prompt with the open one.
Set top-p to 0.9 and compare the two prompts. Before you look, predict which prompt keeps more candidate tokens, and what that says about why top-p was invented.
In words. The ten most likely next tokens for two prompts. With one right answer, top-p keeps just that token; for an open-ended prompt the same p keeps many. Top-k keeps the same number in both, greedy always one, and a higher temperature makes every rule keep more.
Measured (lab 2b and
tests/test_generation.py). The tests check three properties of the nucleus sampler insrc/infra/generation.py. The kept set is the smallest whose total probability reaches . The kept probabilities sum to 1 within . The top token is always kept, even as , sotorch.multinomialnever receives a row of all zeros. The tests also confirm that sampling becomes greedy decoding as the temperature goes to , and that raising the temperature increases the number of distinct 8-token completions produced from 40 seeds.
Exercises
Why can the prompt be processed in one parallel pass, but the generated tokens cannot?
Without a cache, how many key/value projections does it take to generate 24 tokens after a prompt of 1 token, if step projects all tokens? (Count one projection per token per step.)
A next-token distribution is (already sorted). Which tokens does top-p keep at , and what is the renormalized probability of the first one?
With a repetition penalty of , what does a logit of become, and what does a logit of become, for a token that has already appeared?
Explain why comparing the generated text of a cached and an uncached model is a poor test of a KV cache, and describe a better one.
Core Academic References
- Holtzman et al. (2020): The Curious Case of Neural Text Degeneration (nucleus sampling). ICLR 2020. arXiv:1904.09751
- Keskar et al. (2019): CTRL: A Conditional Transformer Language Model for Controllable Generation (repetition penalty). arXiv:1909.05858
- Pope et al. (2023): Efficiently Scaling Transformer Inference on TPU v4 (KV caching). MLSys 2023. arXiv:2211.05102
- Freitag & Al-Onaizan (2017): Beam Search Strategies for Neural Machine Translation. arXiv:1702.01806
Measured labs
These labs test what this module says by running real code. Each card states the idea, what the experiment found, and what it does not prove. The numbers come from experiments/results.json, the output of running experiments/run_all.sh on the tiny-transformers reference code at version 137f7ef. The project checks fail if a measured number in the module text stops matching that file.
Lab 3a — how much memory the KV cache needs, and what it saves
What the module says
Section 12.2 computes the KV cache for LLaMA-2 7B: the saved keys and values for every layer, head and position. The formula is 2 (keys and values) × 32 layers × 32 heads × 128 numbers per head × 4,096 positions × 2 bytes per fp16 number. It argues that at batch 32 this cache alone is bigger than a 24 GB graphics card.
What the experiment found
The product is 2,147,483,648 bytes: 2.0 GiB per sequence and 64 GiB at batch 32, so the module's point holds. A common slip is to drop the final ×2 bytes, which gives 1,073,741,824 (about 1.07 GB), half the true size. That smaller number happens to be the correct cache for LLaMA-3 8B at 8,192 positions, which shares keys and values across heads (GQA). The experiment also checks that the cache changes nothing: outputs with and without it agree to within 7.2e-07.
- LLaMA-2 7B, 4,096 positions, fp16
- 2 GiB
- the same, 32 sequences at once
- 64 GiB
- LLaMA-3 8B with GQA, 8,192 positions
- 1 GiB
- largest output difference, with vs without cache
- max |Δ| = 0.000000715256
- key/value computations while generating
- 468 → 31
- speedup in seconds
- 1.53x
per sequence; dropping the bytes factor gives 1.07 GB, off by 2x
twice the context in half the memory
checked at every position of the same input
15.1x fewer with the cache
small, because on a ~125k-parameter model fixed per-call overhead dominates
What this does not prove
The correctness test compares the model's output scores, not the generated text. Tiny rounding differences between the two paths can flip a near-tie between two tokens, and the texts then diverge even though the cache is correct. The cache also needs position lookups that know how far into the sequence a new token is. The repository's learned position table does not provide that, which is one real cost of choosing it (see section 7.1).
Run it yourself
cd tiny-transformers && .venv/bin/python experiments/kv_cache.pyThe code being tested