Key-Value Cache
A Key-Value Cache (KV Cache) is a memory optimization technique in Transformer-based models that stores the intermediate key and value tensors computed during attention, allowing previously processed tokens to be reused across autoregressive generation steps without recomputation. It is foundational to practical LLM inference, reducing the per-step computational complexity from O(n²) to O(n) and making generation of long sequences tractable on real hardware. The counterintuitive fact: the KV cache can consume more GPU memory than the model weights themselves for long-context generation — a 7B-parameter model may need 14 GB for weights but over 30 GB of KV cache at context lengths above 100K tokens.
During autoregressive generation, the new token attends to every previous token — but those previous tokens' keys and values are identical to what was computed on the prior step. The aha moment is recognizing that attention is stateless across the sequence dimension: storing K and V matrices from past steps is lossless, and recomputing them is pure waste. The KV cache turns inference from a full forward-pass problem into an incremental one: only the newest token's query, key, and value need to be freshly computed, while the cache supplies all historical context.
How It Works
Attention Recap
In a standard multi-head self-attention layer, given an input sequence of tokens with representation matrix , we project into query, key, and value spaces:
Explanation: Three learned linear projections transform the same input sequence into the role-specific matrices Q, K, and V used by the attention mechanism. Here are learned weight matrices, is the model dimension, and is the per-head dimension for heads. The attention output is then:
Explanation: Each query row scores against all key rows to produce a weighted sum of value rows, scaled to prevent vanishing gradients in the softmax.
The Autoregressive Setting
During generation, the model produces one token at a time. At step , the input is only the new token , giving a single query vector . However, it must attend to all previous tokens . Without caching, we would recompute the full at every step, costing FLOPs just in projection — repeated times to generate tokens, yielding total. With the KV cache, we maintain:
Explanation: The cache simply concatenates the new key and value row vectors onto the stored matrices at each step, growing the cache by one row per generated token. where and are computed once when token is first processed, then stored. At step we only compute fresh, then:
Explanation: The single new query vector scores against the full cached key matrix, producing a weighted sum of cached values — no historical recomputation needed.
Memory Cost
The memory required to store the KV cache for a model with layers, heads, head dimension , context length , batch size , and float16 precision is:
Explanation: The factor of 2 accounts for both K and V tensors, and 2 bytes per float16 element; every additional token or batch element linearly increases this cost. For LLaMA-2-70B (, , , ) at and : approximately 40 GB — exceeding the 70B weight size in float16 (~140 GB total, but the cache alone is significant).
PyTorch Implementation Sketch
```python import torch import torch.nn.functional as F class CachedAttention(torch.nn.Module): def __init__(self, d_model: int, n_heads: int): super().__init__() self.n_heads = n_heads self.d_k = d_model // n_heads self.W_q = torch.nn.Linear(d_model, d_model, bias=False) self.W_k = torch.nn.Linear(d_model, d_model, bias=False) self.W_v = torch.nn.Linear(d_model, d_model, bias=False) self.W_o = torch.nn.Linear(d_model, d_model, bias=False) def forward( self, x: torch.Tensor, # (B, T, D) — T=1 during generation kv_cache: dict | None = None, # {"k": (B,H,S,dk), "v": (B,H,S,dk)} use_cache: bool = True, ): B, T, D = x.shape H, dk = self.n_heads, self.d_k def split_heads(t): # (B, T, D) -> (B, H, T, dk) return t.view(B, T, H, dk).transpose(1, 2) q = split_heads(self.W_q(x)) # (B, H, T, dk) k = split_heads(self.W_k(x)) # (B, H, T, dk) v = split_heads(self.W_v(x)) # (B, H, T, dk)
# ── Append to cache ────────────────────────────────────────── if kv_cache is not None: k = torch.cat([kv_cache["k"], k], dim=2) # grow sequence dim v = torch.cat([kv_cache["v"], v], dim=2) new_cache = {"k": k, "v": v} if use_cache else None
# ── Scaled dot-product attention ───────────────────────────── scale = dk ** -0.5 scores = torch.einsum("bhqd,bhkd->bhqk", q, k) * scale
# Causal mask only needed for prefill (T > 1) if T > 1: mask = torch.triu(torch.ones(T, k.size(2), device=x.device), diagonal=1).bool() scores = scores.masked_fill(mask[-T:, :], float("-inf")) attn = F.softmax(scores, dim=-1) out = torch.einsum("bhqk,bhkd->bhqd", attn, v) # (B, H, T, dk)
# ── Merge heads & project ──────────────────────────────────── out = out.transpose(1, 2).contiguous().view(B, T, D) return self.W_o(out), new_cache ``` The `kv_cache` dict is passed layer-by-layer and updated at each generation step — in practice, frameworks like HuggingFace `transformers` manage a list of per-layer caches automatically via `past_key_values`.
Ashish Vaswani led the team at Google Brain that published "Attention Is All You Need" (2017), the foundational paper that introduced the Transformer and the self-attention mechanism from which KV caching directly emerges. While the original paper focused on encoder-decoder translation and did not explicitly discuss inference-time KV caching, the architectural separation of Q, K, and V projections is precisely what makes stateless incremental caching possible. Vaswani later co-founded Essential AI; the 2017 paper has accumulated over 100,000 citations, making it one of the most cited ML papers ever.
Intuition
Imagine you're a research assistant reading a long document to answer questions from a professor. Every time the professor asks a new question, you must re-read the entire document to know what's relevant — unless you've made margin notes. The KV cache is your margin notes: when you first read each page, you annotate what's important (the keys) and what each passage means (the values). From then on, a new question (query) just scans your notes — you never re-read the source pages. The query must always be freshly formulated because the question changes each step; but the document content is fixed, so its K and V representations are immortal once computed. The cost you pay is carrying a growing notebook (memory), and the benefit is never re-reading old text (computation).
Historical Development
Origins · 1 minhow this was discovered
Why It Matters
Prerequisites
What This Leads To
See Also
Flash Attention · Rotary Position Embedding · Causal Masking · Mixture of Experts · Quantization in Deep Learning