Skip to content
AI360XpertBeta

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.

A KV cache reuses stored keys and values so each decoding step scores one new query and blends stored values with weights that sum to one
A KV cache reuses stored keys and values so each decoding step scores one new query and blends stored values with weights that sum to one

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 nn tokens that is 1+2+…+n1 + 2 + \ldots + n projections, which grows like nn 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 nn squared to nn.

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 ii carries an embedding xix_i, a list of numbers describing it in context. Two fixed weight matrices, WKW_K and WVW_V, project it:

ki=xiWK,vi=xiWVk_i = x_i W_K, \qquad v_i = x_i W_V

In plain words: kik_i is the key vector, what token ii offers up for matching. viv_i is the value vector, what token ii 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 qq, what it is looking for. It scores qq against every key, normalises the scores into weights, and blends the values:

si=q⋅Kid,w=softmax(s),out=∑iwiVis_i = \frac{q \cdot K_i}{\sqrt{d}}, \qquad w = \mathrm{softmax}(s), \qquad \mathrm{out} = \sum_i w_i V_i

Here sis_i is the score saying how much the new token cares about position ii, dd is the head dimension (one number shared by the whole head), and softmax\mathrm{softmax} exponentiates each score and divides by the total, so the weights wiw_i are positive and sum to 1. The output is a weighted blend of stored values. Dividing by d\sqrt{d} 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 ii may only look at positions up to ii (causal masking enforces this), so later tokens never retroactively alter an earlier token's embedding. Second, the matrices WKW_K and WVW_V are frozen. Same input through the same matrices gives the same output, so the k1k_1 you computed at step 1 is identical to the k1k_1 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 d=2d = 2, two tokens cached:

K1=[1,0], V1=[4,0],K2=[0,1], V2=[0,2]K_1 = [1, 0],\ V_1 = [4, 0], \qquad K_2 = [0, 1],\ V_2 = [0, 2]

The new query is q=[1,1]q = [1, 1]. The scale is 2≈1.41\sqrt{2} \approx 1.41.

Scores, showing every step:

s1=1×1+1×01.41=11.41≈0.71,s2=1×0+1×11.41≈0.71s_1 = \frac{1 \times 1 + 1 \times 0}{1.41} = \frac{1}{1.41} \approx 0.71, \qquad s_2 = \frac{1 \times 0 + 1 \times 1}{1.41} \approx 0.71

The scores tie, so softmax splits evenly: w=[0.5,0.5]w = [0.5, 0.5]. Quick check: e0.71≈2.03e^{0.71} \approx 2.03 twice, total ≈4.06\approx 4.06, and 2.03/4.06=0.52.03 / 4.06 = 0.5 per share. Then:

out=0.5×[4,0]+0.5×[0,2]=[2.0,1.0]\mathrm{out} = 0.5 \times [4, 0] + 0.5 \times [0, 2] = [2.0, 1.0]

The new token now computes its own pair, call them K3K_3 and V3V_3, 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 1+2+3=61 + 2 + 3 = 6. 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:

values=2×L×H×d×n\text{values} = 2 \times L \times H \times d \times n

where LL is the layer count, HH the key-value heads per layer, dd the numbers per head, and nn the tokens stored so far. The factor of 2 counts keys and values separately. Multiply by bb, the bytes per stored number, for bytes.

Toy check against the fixture above: 1 layer, 1 head, d=2d = 2, 3 tokens, 2 bytes each. That is 2×1×1×2×3=122 \times 1 \times 1 \times 2 \times 3 = 12 numbers, or 24 bytes. Count them directly: 3 rows times a key pair plus a value pair is 3×4=123 \times 4 = 12. It agrees.

One honesty note before you celebrate: the cache removes re-projection, not re-scoring. Each step still scores the new query against nn stored keys, so total scoring across a generation still grows like nn 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))                        # -> 3

The prints match the hand computation: tied scores split 50/50 and blend the stored values into [2.0,1.0][2.0, 1.0]. 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 nn squared to nn, while per-step scoring against stored keys still costs proportional to nn.
  • 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.