The KV cache and fast generation
Here is the generation loop from §6.4, stripped to its core. Run the model on everything so far, take the last position's logits, pick a token, append it, repeat. It's correct, and it is wildly wasteful. To produce token 1,000 it runs the model on 999 tokens, and to produce token 1,001 it runs it on 1,000, recomputing 999 rows it computed one step earlier.
Here's the puzzle. Of everything the model computes for the 1,000-token prefix, all but one row is identical to what it computed for the 999-token prefix. Which row is new, and what is the least we need to remember from earlier steps to compute it? The answer is the KV cache, the single most important optimization in LLM inference. Every serving system you'll ever use is organized around it, and its size is what limits how many users one GPU can serve at once.
Why naive generation is quadratic
Let be the number of parameters that every token is multiplied through (the blocks' matrices plus the output head, but not the embedding lookup, which is free), the number of layers, and the width. A forward pass over tokens costs about floating-point operations for the matrix multiplies (one multiply and one add per parameter per token, as in §5.6), plus attention, where each of the queries is compared with up to keys and mixes up to values. With a causal mask that's about more.
The naive loop runs that forward pass at every step. Generating tokens from a short prompt therefore costs
In plain words, doubling the length of the output quadruples the matmul work and multiplies the attention work by eight. Generating a 1,000-token answer redoes, in total, about five hundred times the work of reading a 1,000-token prompt once.
What changes when you append a token
Here's the observation that fixes it. In a causal model, the hidden state at position depends only on tokens . That's true at the input, where . And if it's true at layer , it stays true at layer . The MLP and LayerNorm act on each position separately, and the attention at position reads only keys and values from positions , because the mask forbids anything later. By induction over the layers, appending token changes nothing at positions , at any layer.
So at step the only new row is position 's, and to compute it, each layer needs exactly three things.
- the new token's own vector, which comes from the layer below,
- its query , computed from that vector,
- the keys and values of all positions at that layer.
Old queries are never used again, since past positions are finished. Old MLP outputs are never used again either. Only the keys and values of past positions are read by future tokens. Store them, one and one per layer, and each step needs only the new token's computation,
where the subscript selects the columns that belong to head , one of the heads. No mask is needed in decoding, since every cached position is in the past. In plain words, each layer appends one row to its key table and one to its value table, and the new query reads both tables in full.
The cost per generated token drops to about (every weight used once, plus one query against keys and values in every layer), so
The matmul work is now linear in the length. Attention is still quadratic in total, because the -th token really does have to look at earlier tokens, but its per-token cost is linear and small. Let's put numbers on it for GPT-2 small.
The ratio grows roughly like , about 500 times less work at the full 1,024-token context.
Building it
We need the §6.2 model again, written for one sequence at a time (no batch axis) so that the shapes stay readable. gpt_forward is the reference with no cache. forward_cached runs any number of new tokens. It looks up their positions starting where the cache ends, appends their keys and values, and lets each new token attend to the whole cache plus the new tokens up to itself.
The mask line deserves a second look. With tokens cached and new ones, the score matrix is , not square. New token sits at absolute position , so it may see columns to . That's a lower triangle shifted right by , which is np.tril(..., k=t0). With the mask is all True, and with it's the ordinary causal mask.
Generation now has two phases. Prefill runs the whole prompt through forward_cached in one pass, filling the cache and producing the first new token's logits. Decode then feeds one token at a time. Let's check that the cached generator produces exactly the same tokens as the naive one, and time both.
The tokens are identical, and the logits agree to rounding error, about . (They aren't always bit-identical, because a matrix-vector product and a matrix-matrix product may add the same numbers in a different order.) Even for this tiny model the cache is many times faster, and the gap grows with length, as the widget below shows by timing the real tiny GPT in your browser.
A generate loop with a KV cache that matches the uncached output exactly
The setup provides P (a small GPT in the §6.2 format), n_head, the reference gpt_forward(P, idx, h) returning (T, V) logits for a 1-D idx, and the helpers layer_norm, gelu, and softmax. Implement two functions.
decode_step(P, token, cache, h) processes exactly one token. cache is a dict {"k": [...], "v": [...]} with one entry per layer, each None at first and afterwards an array of shape (t, d) holding that layer's keys (or values) for all heads side by side. The function uses position t (the cache length), appends the token's key and value rows to every layer, and returns the (V,) logits for the next token.
generate_cached(P, prompt, n_new, h) feeds the prompt through decode_step one token at a time, then generates n_new tokens greedily (np.argmax), and returns the full list of ids. It must equal the naive result exactly.
Prefill a chunk in one pass
Feeding a prompt one token at a time wastes the GPU, which is fast precisely at processing many tokens together. Write prefill_chunk(P, ids, cache, h) that processes a whole chunk of new tokens in one batched pass, continuing an existing cache (which may be empty). It must use positions t0, ..., t0 + n - 1 where t0 is the current cache length, append all n key and value rows to every layer, and return the (n, V) logits. This is how servers do chunked prefill, splitting a long prompt into pieces so decoding requests aren't starved. The setup is the same as in 6.5a.
How big is the cache?
The cache holds one key vector and one value vector per token, per layer, per key-value head. With key-value heads of size (for GPT-2, and ), a context of tokens, a batch of sequences, and bytes per number,
In plain words, two tables (keys and values), times layers, times the width of a key, times tokens, times sequences, times bytes per number. The leading 2 and the use of rather than are the two places people slip.
Work three examples in 16-bit precision ().
- GPT-2 small has and , so each token costs bytes, 36 KiB, and a full 1,024-token sequence 36 MiB.
- LLaMA-2 7B has , , , so each token costs bytes, exactly 0.5 MiB. Its 4,096-token context is 2 GiB per sequence. A batch of 16 such sequences needs 32 GiB of cache, more than twice the 13.5 GB of weights.
- LLaMA-2 70B uses grouped-query attention with only key-value heads shared by its 64 query heads (Ainslie et al., 2023). With , a token costs bytes, 320 KiB, less than the 7B model despite ten times the parameters. Without GQA it would be 2.5 MiB.
When does the cache outgrow the weights? Take multi-head attention (), with weights and cache stored at the same precision. The weights are about numbers (§5.6), and the cache for sequences of tokens is , so
In plain words, the cache matches the weights once the batch holds about tokens in total, whatever the depth. For LLaMA-2 7B that is tokens, six sequences at its 4,096-token context, which agrees with the 2 GiB per sequence against 13.5 GB of weights above. Grouped-query attention multiplies the ratio by , so a model with 8 key-value heads for 64 query heads holds eight times as many tokens before its cache catches up with its weights.
So at long context the cache, not the weights, is what limits how many users a GPU can serve. That single fact drives a whole family of techniques. Sharing keys and values across heads (multi-query and grouped-query attention) and compressing them into a latent vector (multi-head latent attention) are derived in §12.5. Storing the cache in 8 or 4 bits and splitting it into fixed-size pages that are allocated on demand, so that sequences of different lengths don't waste memory, are the domain of serving systems built around paged attention and quantized KV caches.
LLaMA-2 70B's weights alone exceed one 80 GB GPU, so it shows a batch of zero. It has to be split across GPUs (§10.4), and the cache is split with it. Explore other configurations below.
KV cache memory, and the batch that fits
Write kv_cache_bytes(n_layers, n_kv_heads, head_dim, seq_len, batch, bytes_per_elem=2) returning the cache size in bytes, and max_batch(gpu_bytes, weight_bytes, n_layers, n_kv_heads, head_dim, seq_len, bytes_per_elem=2) returning the largest batch of full-length sequences whose cache fits in the memory left after the weights (0 if none). Use Python integers, so that large models don't overflow.
Prefill versus decode
The two phases of generation stress the hardware in opposite ways, and understanding why is the starting point for everything in inference engineering.
Prefill processes all prompt tokens at once. Every weight matrix is multiplied by a block of activations, so each weight read from memory is used times. The work is FLOPs for about bytes of weights in 16-bit, an arithmetic intensity of roughly FLOPs per byte. For a prompt of hundreds of tokens that is far above what the GPU needs to stay busy (§11.2), so prefill is compute-bound. Its duration sets the time to first token.
Decode processes one token per sequence. Every weight is read from memory to be used once per sequence in the batch, so at batch size 1 the intensity is about 1 FLOP per byte, and the step also reads the entire KV cache. Decode is memory-bound. The time per token is about the bytes moved divided by the memory bandwidth, whatever the GPU's FLOPS.
Put numbers on a 7B model in bfloat16, about 13.5 GB of weights, on a GPU with roughly 2 TB/s of memory bandwidth and 312 TFLOPS of bfloat16 compute (an A100 80GB). One decode step at batch 1 must read the weights at least once, ms, so at most about 150 tokens per second, while the arithmetic would take ms. Prefilling a 1,000-token prompt needs FLOPs, about 43 ms at full utilization, the price of six or seven decode steps for a thousand tokens of work.
The consequence is that decode is almost free to batch. Serving 32 sequences at once reads the same weights once per step and does 32 times the arithmetic, so throughput rises nearly 32-fold until the cache reads or the compute catch up. With the cache counted, one decode step takes about divided by the memory bandwidth. For the 7B model at batch 16 with full 4,096-token contexts that is 13.5 GB of weights plus 34 GB of cache, about 24 ms per step, so at large batch and long context the cache, not the weights, sets the speed, a second reason to shrink it. Inference servers therefore batch decode steps from many users together, and treat the prefill of a long prompt as a separate, compute-heavy job, which they often split into chunks (exactly Exercise 6.5b) so it doesn't stall everyone else's next token. Serving systems track latency as two numbers, time to first token (set by prefill) and time per output token (set by decode), and schedule the two phases separately to keep both low, which is the core of fast inference engineering.
In PyTorch
Production caches are preallocated, one tensor of shape for keys and one for values, written in place. Concatenating as our NumPy code does copies the whole cache at every step, which turns linear memory traffic quadratic and fragments GPU memory. A static shape also lets the decode step be captured once as a CUDA graph (§11.7).
One line in that block hides a famous bug. For the cached path we build the mask by hand instead of passing is_causal=True. When the query length differs from the key length, PyTorch's is_causal aligns the triangle to the top-left corner, so a single decode query would be allowed to see only the first key. The correct mask for a cache is aligned to the bottom-right, which is what tril(diagonal=t0) builds.
Batching adds one more wrinkle. Prompts in a batch have different lengths, but a preallocated cache wants every row to write at the same index. The simple fix is left padding. Pad the short prompts at the start, mask the pad positions out of every attention row, and give each real token its position counted from its own first real token, so that all sequences then decode in lockstep. Serving systems avoid the waste altogether with paged caches, where each sequence owns its own list of fixed-size blocks and the attention kernel gathers them (Kwon et al., 2023).
| naive loop | KV cache | |
|---|---|---|
| work to generate tokens | ||
| extra memory | none | bytes |
| per-step cost | grows with the prefix, all layers recomputed | one token's matmuls plus a read of the cache |
| sliding past (learned positions) | crop and recompute | crop and recompute the cache too |
| used in practice | never, except as a test reference | always |
Read more
- Pope et al. (2022). Efficiently Scaling Transformer Inference. Prefill versus decode, memory bandwidth, and KV cache costs at scale.
- Shazeer (2019). Fast Transformer Decoding: One Write-Head is All You Need. Multi-query attention, motivated by the cache.
- Ainslie et al. (2023). GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. Grouped-query attention, the middle ground used by LLaMA-2 70B and most models since.
- Kwon et al. (2023). Efficient Memory Management for Large Language Model Serving with PagedAttention. The vLLM paper, paging the cache like virtual memory.
- Touvron et al. (2023). Llama 2: Open Foundation and Fine-Tuned Chat Models. The configurations used for the cache sizes above.
- Karpathy. nanoGPT model.py. Its
generateis the naive loop, a good exercise to retrofit with this section's cache.