Interactive Explainer
The KV-Cache, Made Concrete
Every token an LLM generates re-reads every key and value from all previous tokens. Caching them is mandatory — and the cache quickly dwarfs the weights. Slide the model, the context, and the batch below and watch the moment the KV-cache overtakes the parameters. This is why “LLM inference is memory-bound, not compute-bound.”
Why the cache has to exist
To predict token $t{+}1$, self-attention compares the current query against the key of every earlier token and mixes in their values:
The keys $k_i$ and values $v_i$ of past tokens never change. Recomputing them at each step would make generation cost $\mathcal{O}(n^2)$ in time for no reason. So we cache them: after a token is processed, its $k$ and $v$ (in every layer, every head) are stored and re-used forever. That trade — spend memory to save compute — is what makes autoregressive decoding fast, and what makes it memory-bound. Every new token reads the entire cache back out of HBM; the arithmetic is trivial next to the bytes moved.
The one formula that governs everything
For a single token, in a single layer, one attention head stores one key vector and one value vector, each of dimension $d_h$. Multiply out over the whole model and you get the size of the cache per token:
- $L$ — number of transformer layers (the cache is stored once per layer).
- $n_{kv}$ — number of key/value heads. Under multi-head attention this equals the query heads; grouped-query and multi-query attention shrink it.
- $d_h$ — dimension of each head.
- $p$ — bytes per number (FP16 = 2, INT8 = 1).
- The factor $2$ is because we store both a key and a value.
Multiply by the context length and the batch size (concurrent sequences) and you have the total footprint: $\;\text{bytes}_{\text{total}} = \text{bytes}_{\text{token}} \cdot \text{context} \cdot \text{batch}.$ Nothing here depends on the size of the MLP or the vocabulary — the cache is a pure function of the attention shape. Time to make it move.
The memory-cliff calculator
Set a model with the sliders, or load a real one with a preset. The chart plots two things against context length: the flat model weights and the rising KV-cache. Where the cache line overtakes the weights is the memory cliff — past it, your GPU is spending more on remembering the conversation than on the model itself.
Model shape
Attention type → sets KV heads $n_{kv}$
KV-cache dtype → bytes per number $p$
Effective KV heads: 8 — .
PagedAttention: stop reserving what you don't use
There is a second, subtler waste. A naive server does not know how long a reply will be, so it reserves a contiguous buffer for the maximum context of every sequence up front. Most replies are short, so most of that buffer sits empty — internal fragmentation. vLLM's PagedAttention borrows the operating-system trick of paging: memory is a pool of small fixed blocks, and a sequence grabs blocks only as it grows. Toggle between the two and watch the wasted (hatched) memory collapse.
Allocation strategy
The three escape hatches
Every term in $2 \cdot L \cdot n_{kv} \cdot d_h \cdot p$ is a lever, and the field has pulled each of them:
Grouped-query attention
Let several query heads share one key/value head. GQA(8) cuts $n_{kv}$ from 64 to 8 — an $8\times$ smaller cache with negligible quality loss. MQA takes it to the extreme with a single KV head.
INT8 / FP8 KV-cache
Store keys and values in 8 bits instead of 16. Halves the bytes, often for free; occasionally a small accuracy nudge. Toggle it in the Lab and the cliff moves twice as far right.
PagedAttention
Allocate KV in fixed blocks instead of one giant contiguous buffer per sequence. No fragmentation, no over-reservation — the trick at the heart of vLLM's throughput.
Where each model lands
| Model | Attention | KV heads | KV / token (FP16) |
|---|---|---|---|
| Llama-2 70B | MHA | 64 | ~2.5 MB |
| Llama-3 70B | GQA | 8 | ~320 KB |
| Mistral 7B | GQA | 8 | ~128 KB |
| Multi-query (MQA) | MQA | 1 | ~40 KB |