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.

Builds onModule 8·
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.

Open in Colab
3 Hours·Core·Intermediate·0/3

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:

  1. Tokenize the prompt.
  2. Run the model on the tokens so far and take the logits at the last position: the scores for the next token.
  3. Choose one token from them, with one of the decoding rules of section 12.3.
  4. Append it to the sequence.
  5. 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.
python
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 tt reruns the model over all tt 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 tt costs on the order of t2t^2 operations, since attention compares every token with every other. Over TT steps:

Time⁡naive(T)=∑t=1TO(t2)=O(T3) operations!\operatorname{Time}_{\text{naive}}(T) = \sum_{t=1}^T \mathcal{O}(t^2) = \mathcal{O}(T^3) \text{ operations!}

O(⋅)\mathcal{O}(\cdot) ("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 1…t−11 \dots t-1 are the same at every step. Computed once and stored, they form the KV cache.

Architecture Flowchart
Diagram: Generation step t; Newest token x_t; query q_t = x_t W^Q; key k_t = x_t W^K; value v_t = x_t W^V; Append k_t to K_cache; Append v_t to V_cache; Attend: q_t against all of K_cache, weights applied to V_cache; Output y_t, used to choose token x_{t+1}

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 tt, compute a query for the newest token only:

qt=xtWQ∈R1×dk\mathbf{q}_t = \mathbf{x}_t W^Q \in \mathbb{R}^{1 \times d_k}

Append its key and value as new rows of the caches (the semicolon stacks the new row under the old):

Kcache(t)=[Kcache(t−1); xtWK]∈Rt×dkK_{\text{cache}}^{(t)} = \left[ K_{\text{cache}}^{(t-1)}; \, \mathbf{x}_t W^K \right] \in \mathbb{R}^{t \times d_k}
Vcache(t)=[Vcache(t−1); xtWV]∈Rt×dvV_{\text{cache}}^{(t)} = \left[ V_{\text{cache}}^{(t-1)}; \, \mathbf{x}_t W^V \right] \in \mathbb{R}^{t \times d_v}

The superscript (t)(t) labels the step, not a power. The output is ordinary attention, one query row against the whole cache:

yt=Softmax⁡(qt(Kcache(t))⊤dk)Vcache(t)∈R1×dv\mathbf{y}_t = \operatorname{Softmax}\left(\frac{\mathbf{q}_t (K_{\text{cache}}^{(t)})^\top}{\sqrt{d_k}}\right) V_{\text{cache}}^{(t)} \in \mathbb{R}^{1 \times d_v}

This is exactly the yt\mathbf{y}_t of full recomputation, with no approximation. Each step projects one token instead of tt and computes one row of scores instead of t×tt \times t, so TT tokens cost on the order of T2T^2 instead of T3T^3.

Figure 12.2
What the cache saves at each step

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.

Each square is one query–key score. Counts are per layer and per head; the model repeats them in every one.
Predict, then try

Without a cache, how many attention scores are computed in total to generate 12 tokens, if step tt computes all t×tt \times t 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 t−1t-1 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 7.15×10−77.15 \times 10^{-7}, and the top token agrees at every position (24/2424/24). Over a 24-token decode, K/V projections fall from 468468 to 3131, a 15.1×15.1\times reduction. Wall-clock time improves by only 1.56×1.56\times 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 z\mathbf{z}. Most also apply a temperature (section 4.3) first: low sharpens toward the top token, high flattens.

  1. 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.

  2. Top-kk sampling. Keep the kk highest-scoring tokens (say k=50k=50), set the other logits to , softmax and sample. This cuts the long tail of unlikely tokens, where most nonsense comes from.

  3. Top-pp (nucleus) sampling (Holtzman et al., 2020). Keep the smallest set V(p)V^{(p)} of top tokens whose probabilities reach pp, renormalize and sample:

    ∑i∈V(p)P(wi)≥p(typically p=0.90)\sum_{i \in V^{(p)}} P(w_i) \ge p \quad (\text{typically } p = 0.90)

    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 kk cannot do.

  4. 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):

    zi⟵{zi/θ,if zi>0zi⋅θ,if zi<0(θ>1.0,e.g. θ=1.15)z_i \longleftarrow \begin{cases} z_i / \theta, & \text{if } z_i > 0 \\ z_i \cdot \theta, & \text{if } z_i < 0 \end{cases} \quad (\theta > 1.0, \text{e.g. } \theta = 1.15)

    Positive logits shrink toward zero and negative ones grow more negative: either way the token becomes less likely, discouraging loops.

  5. 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.

Figure 12.3
Which tokens each decoding rule keeps

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.

Illustrative logits, sorted from most to least likely; a real vocabulary has tens of thousands more tokens in the tail. Kept probabilities are renormalized to sum to 1 before sampling.
Predict, then try

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 in src/infra/generation.py. The kept set is the smallest whose total probability reaches pp. The kept probabilities sum to 1 within 10−610^{-6}. The top token is always kept, even as p→0p \to 0, so torch.multinomial never receives a row of all zeros. The tests also confirm that sampling becomes greedy decoding as the temperature goes to 0+0^{+}, and that raising the temperature increases the number of distinct 8-token completions produced from 40 seeds.

Exercises

Check

Why can the prompt be processed in one parallel pass, but the generated tokens cannot?

Apply

Without a cache, how many key/value projections does it take to generate 24 tokens after a prompt of 1 token, if step tt projects all tt tokens? (Count one projection per token per step.)

Apply

A next-token distribution is [0.5,0.3,0.1,0.05,0.05][0.5, 0.3, 0.1, 0.05, 0.05] (already sorted). Which tokens does top-p keep at p=0.9p = 0.9, and what is the renormalized probability of the first one?

Apply

With a repetition penalty of θ=1.25\theta = 1.25, what does a logit of 2.02.0 become, and what does a logit of −2.0-2.0 become, for a token that has already appeared?

Extend

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

per sequence; dropping the bytes factor gives 1.07 GB, off by 2x

the same, 32 sequences at once
64 GiB
LLaMA-3 8B with GQA, 8,192 positions
1 GiB

twice the context in half the memory

largest output difference, with vs without cache
max |Δ| = 0.000000715256

checked at every position of the same input

key/value computations while generating
468 → 31

15.1x fewer with the cache

speedup in seconds
1.53x

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

bash
cd tiny-transformers && .venv/bin/python experiments/kv_cache.py

The code being tested