Skip to main content

The KV Cache

Introduction

In the last lesson (L207) we said decode is memory-bandwidth-bound — each token must stream the model's weights and "the KV cache" out of memory. This lesson is about that cache. It's not a minor optimization: the KV cache is the single thing that makes generation fast enough to be usable at all, and it's simultaneously the biggest memory consumer in LLM serving — the thing that decides how long a context and how large a batch you can afford.

Generation is autoregressive: the model writes one token at a time, each one attending back over every token before it (recall L7). The naive way to do that re-reads the entire story every time it adds a word. The KV cache is the fix — and understanding it explains most of why inference costs what it costs.

In this lesson:

  • The problem — why naive decoding is O(n²) and gets slower with every token
  • The fix — cache the keys and values of past tokens so each step processes one token, not the whole prefix (O(n))
  • Why this is the root of the memory-bound behavior from L207
  • The cost — the KV-cache memory formula, real GB numbers, and why it caps batch size and context length
  • Shrinking itMQA, GQA, MLA, and quantization — what every modern model does

Scope: this is the cache itself. How it powers continuous batching for throughput is L209; PagedAttention shows up here as memory management but its batching payoff is L209. Speculative decoding (L210) and vLLM/TGI (L211) build on this. And the KV cache (inside one request) is not the same as prompt caching (reusing a prefix across requests) — that's a cost feature in L214. We'll flag the difference, not cover it here.

Infographic titled 'The KV Cache'. The big idea: the KV cache is what makes autoregressive decoding fast enough to exist — it trades GPU memory for compute. CENTER, a two-row comparison of generating the 8th token. ROW 1, WITHOUT THE CACHE: to produce token 8 the model must re-derive the keys and values for ALL 8 tokens, shown as eight cells all marked with a recompute symbol in red; the work this step is 8 token-passes, and because every step reprocesses the whole growing prefix it is O(n-squared) — it gets slower with every token. ROW 2, WITH THE CACHE: the keys and values of the 7 previous tokens are read straight from a stored KV CACHE (seven green cells marked done) and only the new token, #8, is computed (one amber cell); the work this step is just 1 token-pass plus a cache read, so it is O(n) — constant work per step. The key reason it works: a past token's K and V never change, so they can be stored once (during prefill) and reused every step. RIGHT, THE COST panel: the cache grows linearly with sequence length and batch, by the formula 2 (for K and V) times layers times kv-heads times head-dim times sequence-length times batch times bytes-per-element. For a Llama-70B-class model with grouped-query attention that is about 0.32 MB per token: 4K tokens is about 1.3 GB, 32K is about 11 GB, and 128K is about 43 GB — the KV cache can exceed the model weights, and it is what caps how long a context and how large a batch you can serve. THREE CARDS. Card 1, the trick: cache K/V, so each decode step processes one token instead of the whole prefix — O(n-squared) becomes O(n); this is also why decode is memory-bandwidth-bound, since every step reads the whole cache from memory. Card 2, shrink it: MHA (64 KV heads, biggest) to GQA (8 KV heads, about 8x smaller, quality close to MHA, the modern default in Llama 3, Mistral, Qwen) to MQA (1 KV head, about 64x smaller, small quality hit) to MLA (DeepSeek latent compression, about 3-5x smaller than GQA); plus KV-cache quantization to fp8/int8. Card 3, manage it: PagedAttention stores the cache in fixed pages like OS virtual memory to stop fragmentation and pack more sequences (the batching payoff is the next lesson); and note that the KV cache (within one request) is NOT the same as prompt caching (reusing a prefix across requests). Section roadmap strip: metrics, KV cache, batching, speculative decoding, serving with vLLM/TGI, streaming. Takeaway banner: the KV cache trades memory for compute — store past K/V once so each step is one token, not the whole prefix; the price is memory that grows with context and batch, which GQA, MQA, MLA and quantization exist to control.

The Problem — Re-Reading the Whole Prefix, Every Token

Start from how attention works. To produce the next token, the model computes a query (Q) for the current position and compares it against a key (K) for every previous token; the match scores weight a sum of the values (V). Because generation is causal, token t attends over the K and V of all t tokens so far.

Now watch what happens if you implement decoding naively — just call the model on the sequence so far, repeatedly:

StepSequence fed inK/V computedWork
Generate token 16 prompt tokensfor 6 tokens6
Generate token 27 tokensfor 7 tokens7
Generate token 38 tokensfor 8 tokens8
Generate token n~n tokensfor ~n tokensn

Every step reprocesses the entire growing prefix from scratch. Generating n tokens costs 6 + 7 + 8 + … ≈ O(n²) total work, and — worse for UX — each token is slower than the last (TPOT keeps rising; recall L207). For a 2,000-token answer that's millions of redundant token-passes. It's like re-reading the whole book from page one every time you write a single new word.

The waste has a specific shape: you are recomputing the K and V of tokens you already processed a moment ago.

The Fix — Cache the Keys & Values

Here's the insight that fixes it: a past token's K and V never change. They're computed from that token (and its position) alone — they don't depend on anything that comes after. So recomputing them every step is pure waste. Compute them once, store them, and reuse them. That store is the KV cache.

With the cache, decoding splits cleanly into the two phases from L207:

  • Prefill: run the whole prompt through once, in parallel, and save every token's K and V into the cache. (This is the work behind TTFT.)
  • Decode: for each new token, compute K/V/Q for just that one token, append its K and V to the cache, and attend its Q over the cached K/V. Then repeat.

Each decode step is now constant work — one token — instead of reprocessing the prefix. Generating n tokens drops from O(n²) to O(n). In pseudocode the difference is stark:

# WITHOUT a KV cache — reprocess the whole prefix every step  ->  O(n^2)
tokens = prompt[:]
for _ in range(max_new_tokens):
    logits = model(tokens)            # recomputes K,V for ALL tokens, every step
    next_tok = sample(logits[-1])
    tokens.append(next_tok)

# WITH a KV cache — compute only the new token each step  ->  O(n)
cache = None
logits, cache = model(prompt, cache=None)     # PREFILL: fills the cache with the prompt's K,V
next_tok = sample(logits[-1])
for _ in range(max_new_tokens - 1):
    # DECODE: feed ONE token; model appends its K,V to the cache and reads the rest from it
    logits, cache = model([next_tok], cache=cache)
    next_tok = sample(logits[-1])

This is exactly what use_cache=True does in libraries like Hugging Face Transformers, and what every serving engine (vLLM, TGI, SGLang) does under the hood — you rarely write the loop yourself, but this is the mechanism you're paying for. The cache turns an impossible quadratic into a linear stream of single-token steps.

Why This Makes Decode Memory-Bound

The KV cache also explains the deepest fact from L207 — why decode is memory-bandwidth-bound. Look at what a single decode step actually does: it computes one token's worth of math (tiny), but to do it, it must read the entire model's weights and the entire KV cache out of memory. The compute is small; the bytes moved are huge. That's low arithmetic intensity — the GPU's math units sit mostly idle (~20-40% utilization) waiting on memory.

Prefill is compute-bound (it builds the cache by crunching the whole prompt in parallel). Decode is memory-bound (it reads the cache, one token at a time). The KV cache is the object being streamed — so its size directly drives your TPOT. A bigger cache (longer context) means more bytes to move per token, means a slower decode. Memory footprint and decode speed are the same problem.

This is why the rest of the lesson is about cache size — it's not just a capacity question, it's a speed question too.

The Cost — A Memory Monster

How big does the cache get? It's deterministic. You store a K and a V vector for every token, in every layer, for every KV head:

KV bytes = 2 × layers × kv_heads × head_dim × seq_len × batch × bytes_per_element

(The leading 2 is because you store both K and V.) The thing to notice: it grows linearly with sequence length and with batch size — both of which you want to make large. Plug in a Llama-3.1-70B-class model (80 layers, grouped-query attention with 8 KV heads, head_dim 128, BF16 = 2 bytes) and run it:

def kv_cache_gb(layers, kv_heads, head_dim, seq_len, batch=1, bytes_per=2):
    """KV cache size in GB.  2 = one Key + one Value tensor."""
    total_bytes = 2 * layers * kv_heads * head_dim * seq_len * batch * bytes_per
    return total_bytes / 1e9

# Llama-3.1-70B: 80 layers, GQA (8 KV heads), head_dim 128, BF16
per_token_mb = kv_cache_gb(80, 8, 128, seq_len=1) * 1000
print(f"per token: {per_token_mb:.2f} MB")
for ctx in (4096, 32768, 131072):
    print(f"{ctx:>7,} tokens -> {kv_cache_gb(80, 8, 128, ctx):6.1f} GB")

# What if this model used full multi-head attention (64 KV heads) instead of GQA's 8?
print("128K @ MHA (64 heads) ->", round(kv_cache_gb(80, 64, 128, 131072), 0), "GB")

Output:

per token:   0.33 MB
  4,096 tokens ->    1.3 GB
 32,768 tokens ->   10.7 GB
131,072 tokens ->   42.9 GB
128K @ MHA (64 heads) -> 344.0 GB

Sit with those numbers. At 128K context the KV cache is ~43 GB — that can be larger than a quantized copy of the model's own weights, and it's per concurrent request (multiply by batch size). On an 80 GB GPU, the weights plus a few long-context requests' caches is all your memory — gone.

This is the real reason you can't just "turn up the context length" or "batch more users": the KV cache is the budget. It sets the hard ceiling that batching strategies (L209) spend their cleverness trying to raise. And notice the last line: full multi-head attention would need ~344 GB for the same context — which brings us to how modern models make the cache fit.

Making the Cache Fit — Shrink It, Then Page It

Since the cache is the budget, modern models are designed to make each token's K/V smaller. The big lever is how many KV heads you store (the kv_heads term in the formula):

SchemeKV headsvs MHAQualityUsed by
MHA (multi-head)one per query head (e.g. 64)1× (baseline)bestolder models (GPT-3-era)
MQA (multi-query)1, shared by all query heads~64× smallersmall but measurable dropPaLM, Falcon
GQA (grouped-query)a few groups (e.g. 8)~8× smaller≈ MHALlama 2/3, Mistral, Qwen (the default)
MLA (latent)compresses K/V to a small latent vector~3-5× smaller than GQAstrongDeepSeek V2/V3

GQA is the modern default because it sits in the sweet spot: query heads share K/V in small groups, giving ~8× smaller cache at quality indistinguishable from full MHA (and you can uptrain an MHA model into GQA with ~5% of pretraining compute). MQA pushes to a single KV head for maximum savings at a small quality cost. MLA (DeepSeek) takes a different route — instead of sharing heads, it compresses what's stored into a low-rank latent, beating even GQA. On top of any of these, KV-cache quantization (store the cache in FP8/INT8 instead of BF16) gives another 2-4×.

There's a second, orthogonal idea: don't shrink the cache, manage its memory better. Naively you'd reserve a contiguous block for each request's maximum length — wasting most of it. PagedAttention (the technique behind vLLM) instead stores the cache in fixed-size pages, allocated on demand like an operating system's virtual memory — no fragmentation, no over-reservation, so you fit far more sequences in the same VRAM. That is what unlocks high-throughput batching — which is the whole of L209. Here, just hold the idea: shrink the cache (GQA/MQA/MLA/quant), then pack it efficiently (paging).

See It — The KV Cache Lab

Make it concrete. Below, step through generation and flip the cache OFF vs ON, then switch the attention scheme. Watch the work per step and the memory cost react:

Step through the generation loop and flip the cache OFF/ON. With it OFF, every step re-derives the K/V for the whole growing prefix (all cells turn red) — O(n-squared), and the 'decode work so far' counter explodes. With it ON, the past tokens are read from the cache (green) and only the new token is computed (amber) — O(n), one token-pass per step. Then switch MHA / GQA / MQA / MLA and watch the per-token cost and the '@128K context' GB shrink — the reason long context fits in GPU memory at all. Numbers are illustrative for a ~70B-class model.

Three things to take away from playing with it:

  • Cache OFF turns every cell red — each step re-derives the whole prefix, and the decode-work-so-far counter grows like . Cache ON keeps the past tokens green (read for free) and computes only the amber new token — O(n), flat work per step. That gap is the lesson.
  • The price is the cache itself: it grows every token, and the @128K readout shows it in GB.
  • Flip MHA → GQA → MQA → MLA and the @128K cache collapses from ~344 GB to a few GB — the difference between "impossible on one GPU" and "fits comfortably." That's why these schemes exist.

🧪 Try It Yourself

Reason through these, then use the lab (and the calculator above) to confirm:

  1. Predict: with the cache ON, why doesn't "decode work this step" grow as you generate more tokens?
  2. You double your context length from 64K to 128K. Roughly what happens to (a) the KV-cache memory and (b) the per-token decode time, and why are those the same answer?
  3. A model switches from MHA to GQA with 8 KV heads (down from 64). About how much smaller is the KV cache, and what roughly happens to quality?
  4. Your serving box OOMs (out of memory) when a few users send very long prompts, even though the model weights fit fine. What's eating the memory, and name two levers to fix it.
  5. A teammate says "let's enable the KV cache to make our prompt caching work across requests." What's the confusion?

(1) Because past tokens' K/V are already stored — each step computes only the one new token and reads the rest from the cache. Work per step is constant (O(n)). (2) Both double: the cache is linear in sequence length, and since decode is memory-bound, the per-token time tracks how many cache bytes you must read — memory size and decode speed are the same problem. (3) ~8× smaller cache (you store 8 KV heads instead of 64), with quality ≈ MHA — that's why GQA is the default. (4) The KV cache (it grows with context × batch and can exceed the weights). Levers: a smaller-cache attention scheme (GQA/MQA/MLA), KV quantization (FP8/INT8), PagedAttention to pack memory, shorter context, or fewer concurrent long requests. (5) They're different things: the KV cache is the within-request store that accelerates decode (always on); prompt caching reuses a prefix's KV across requests to save cost (L214). Turning on one doesn't give you the other.

Mental-Model Corrections

  • "The KV cache stores the model's answers/outputs." No — it stores the keys and values of past tokens within a single generation, so they aren't recomputed each step.
  • "KV cache = prompt caching." Different. KV cache = within-request decode accelerator (always on). Prompt caching = reusing a prefix's KV across requests for cost (L214).
  • "Caching saves memory." The opposite — it spends memory to save compute. The cache is one of the largest memory consumers in serving and can exceed the model weights.
  • "Longer context is mainly a compute cost." It's mainly a memory cost: the KV cache grows linearly with context and caps your batch size — and, since decode is memory-bound, it slows every token too.
  • "GQA/MQA are just speed tricks." They're KV-cache-size reductions (fewer KV heads). The speed and the long-context feasibility both come from a smaller cache; GQA keeps quality ≈ MHA.
  • "The KV cache speeds up prefill." No — prefill builds the cache. The cache speeds up decode, by avoiding reprocessing the prefix on every token.
  • "Just raise the context window / batch size." Both are gated by KV-cache memory. You raise them by shrinking the cache (GQA/MQA/MLA/quant) or paging it (L209), not by wishing.

Key Takeaways

  • The KV cache is why decode is feasible. Past tokens' K/V never change, so store them once and reuse them — each decode step processes one token instead of the whole prefix. O(n²) → O(n).
  • Prefill builds the cache; decode reads and appends to it. That read-the-whole-cache-every-token pattern is why decode is memory-bandwidth-bound (L207) — so cache size drives TPOT.
  • The cache is a memory monster: 2 × layers × kv_heads × head_dim × seq_len × batch × bytes. For a 70B GQA model that's ~0.33 MB/token → ~43 GB at 128K context, often bigger than the weights, and per request.
  • It caps context length and batch size — it's the budget every serving optimization fights over.
  • Shrink it: MHA → GQA (8×, quality ≈ MHA, the default) → MQA (64×, small quality hit) → MLA (DeepSeek latent, ~3-5× over GQA); plus KV quantization (2-4×). Then page it (PagedAttention) to pack memory.
  • Don't confuse it with prompt caching (cross-request prefix reuse, L214).
  • Next — L209: Batching & Continuous Batching — now that you know the cache is the memory budget, see how continuous batching and PagedAttention pack many requests onto one GPU to turn that budget into throughput.