KV Cache
Once your model has computed a past token's keys and values, it files them away instead of redoing them. Each new token then costs one fresh computation plus a lookup, not a full replay.
Why Does This Exist?
You generate text one token at a time. To pick token 1,000, the model must weigh it against the 999 tokens before it, which means self-attention needs every one of those tokens' key and value vectors close at hand. If those terms are new, how LLMs work sets up next-token prediction first.
The naive way rebuilds all of them. At step 1 you project 1 token, at step 2 you re-project 2, at step 3 you re-project 3, and so on. Nothing about token 500's key changes when token 999 arrives, yet the naive loop recomputes it hundreds of times. Across a full generation of tokens that is projections, which grows like squared. For 3 tokens that is 6 projections. For 1,000 it is 500,500.
The KV cache deletes that waste. Compute each token's keys and values once, store them, and append only the newcomer's pair at each step. Projection work across the whole generation drops from about squared to .
This page covers the per-sequence cache itself. Its neighbours answer the follow-ups: paged attention is how servers store the cache without wasting memory, prefix caching shares one cache across requests, and grouped-query attention shrinks it.
Think of It Like This
A court reporter's transcript
You are a court reporter and the trial is long. Each spoken line goes into the transcript once. When the judge says "read back what the witness said three statements ago," you read the transcript. You do not ask the whole courtroom to repeat everything from the morning.
New testimony still gets typed live and appended to the back. The transcript only grows, so a month-long trial fills boxes of paper. That is the memory trade-off waiting below. And the transcript belongs to this trial. Start a new case and the old pages buy you nothing, so you open a fresh pad. Same with the cache: change the prefix and every stored row goes stale.
Where it stops: you read back one line, but attention re-weighs every stored line at each step. The cache saves the writing, not the re-reading. Scoring the new token against all stored keys still costs work proportional to the sequence length.
How It Actually Works
Where keys and values come from
At each attention layer, token carries an embedding , a list of numbers describing it in context. Two fixed weight matrices, and , project it:
In plain words: is the key vector, what token offers up for matching. is the value vector, what token contributes to the output if it gets picked. Fixed means frozen after training; neither matrix changes while you generate.
To extend the sequence, the newest token forms a query , what it is looking for. It scores against every key, normalises the scores into weights, and blends the values:
Here is the score saying how much the new token cares about position , is the head dimension (one number shared by the whole head), and exponentiates each score and divides by the total, so the weights are positive and sum to 1. The output is a weighted blend of stored values. Dividing by keeps the scores in a range where softmax can still tell them apart.
Why old rows never change
Two facts make caching exact rather than approximate. First, causality: in a decoder, token may only look at positions up to (causal masking enforces this), so later tokens never retroactively alter an earlier token's embedding. Second, the matrices and are frozen. Same input through the same matrices gives the same output, so the you computed at step 1 is identical to the you would recompute at step 50. Storing it loses nothing.
That rests on one assumption: causal, decoder-side generation with a fixed prefix. Encoders that read the whole sequence at once, and any edit to an earlier token, break it, which is what the first pitfall below is about.
Worked example
One head, head dimension , two tokens cached:
The new query is . The scale is .
Scores, showing every step:
The scores tie, so softmax splits evenly: . Quick check: twice, total , and per share. Then:
The new token now computes its own pair, call them and , and the cache grows from 2 rows to 3. Next step scores its query against all three stored rows. Count the projections: step 3 did exactly 1, the newcomer's. Across 3 steps the cache did 3 total, while the naive loop did . Same answers, half the projection work, and the gap widens every step.
What it costs in memory
The cache trades memory for speed. The number of stored values is:
where is the layer count, the key-value heads per layer, the numbers per head, and the tokens stored so far. The factor of 2 counts keys and values separately. Multiply by , the bytes per stored number, for bytes.
Toy check against the fixture above: 1 layer, 1 head, , 3 tokens, 2 bytes each. That is numbers, or 24 bytes. Count them directly: 3 rows times a key pair plus a value pair is . It agrees.
One honesty note before you celebrate: the cache removes re-projection, not re-scoring. Each step still scores the new query against stored keys, so total scoring across a generation still grows like squared. That half is what flash attention attacks. And when there is nothing to reuse, skip the cache entirely: scoring a full prompt in one parallel pass or generating a single label gains nothing from it. At long contexts the cache itself turns into the bottleneck, which is why sliding-window attention caps how many rows you keep.
Code
This snippet replays the worked example: two cached rows, one new query, and the append that grows the cache. No ML library needed.
import math
def softmax(xs: list[float]) -> list[float]: exps = [math.exp(x) for x in xs] total = sum(exps) return [e / total for e in exps]
def dot(a: list[float], b: list[float]) -> float: return sum(x * y for x, y in zip(a, b))
# Rows stored at steps 1-2; step 3 only reads them.keys: list[list[float]] = [[1.0, 0.0], [0.0, 1.0]]values: list[list[float]] = [[4.0, 0.0], [0.0, 2.0]]
query: list[float] = [1.0, 1.0]scale: float = math.sqrt(2.0) # 1 / sqrt(d) with head dim d = 2scores = [dot(query, k) / scale for k in keys]weights = softmax(scores)out = [sum(w * v[i] for w, v in zip(weights, values)) for i in range(2)]
print([round(s, 2) for s in scores]) # -> [0.71, 0.71]print([round(w, 2) for w in weights]) # -> [0.5, 0.5]print([round(x, 2) for x in out]) # -> [2.0, 1.0]
# Step 3 appends its own pair; the old rows are never touched again.keys.append([1.0, 1.0])values.append([1.0, -1.0])print(len(keys)) # -> 3The prints match the hand computation: tied scores split 50/50 and blend the stored values into . The append is the whole mechanism. One projection per step, forever.
Watch Out For
Reusing a cache built from a different prefix
The cache is only valid for the exact token sequence it was built from. Swap the system prompt or edit an earlier message and every stored row describes tokens that no longer exist in those positions. The symptom is silently wrong continuations: shapes still match, nothing errors, but attention scores blend values from a ghost prefix.
Fix it by keying the cache on the exact token ids and, on any edit, dropping every row from the first changed position onward and recomputing forward. Shared-prefix systems do exactly this matching, which is what prefix caching is careful about.
Sizing a server by weights alone
For one short request the cache is small next to the model weights, so people leave it out of capacity math. Then long contexts or many concurrent requests arrive and the server runs out of memory with the weights fitting comfortably. The symptom is out-of-memory failures that scale with context length and concurrency, not with model size.
Fix it with the formula above, multiplied by concurrent sequences, before you blame compute. If the number scares you, that is the signal to reach for paged attention, fewer key-value heads, or a sliding window, not a bigger GPU.
The Quick Version
- A KV cache stores each past token's key and value vectors because causal generation never changes them once written.
- Each decoding step then computes one new key-value pair and scores one new query against the stored rows, with weights that sum to 1.
- Across a full generation this cuts projection work from about squared to , while per-step scoring against stored keys still costs proportional to .
- Its memory is 2 times layers times key-value heads times head dim times tokens times bytes per value, and it grows with every token: the toy above holds 12 numbers in 24 bytes.
- Never reuse a cache after its prefix changes, and budget cache memory per concurrent request before blaming compute.