The Interview Edge Blog
← Back to all guides
Inference · LLM internals

KV Cache Explained: How LLM Inference Caches Keys and Values

Why autoregressive generation would be painfully wasteful without cached keys and values: the memory math, the trade-offs, and where the cache breaks.

Listen to this guideNarrated audio · ~14 min

Explain it like I’m five

Imagine telling a long story one word at a time. Without notes, you would reread the whole story and rewrite what you learned after every new word. That would be very slow.

A KV cache is the model’s stack of useful notes about all the words it has already read. For each new word, the model looks at those notes, makes only one new set, and adds it to the stack. Generation gets faster, but the growing stack takes memory.

Intuition: keep yesterday’s work

Your LLM serves the first token fast, then each new token dribbles out one at a time while your expensive GPUs sit at 10% utilization. The problem isn’t compute — every decode step re-reads the entire model and every past key-value pair from memory. The KV cache is the fix: save what you already computed and stream only what’s new. In interviews, nobody asks ‘what is a KV cache?’ They ask ‘why is decoding memory-bandwidth-bound, and what is paged attention solving?’ By the end of this guide you’ll size a cache, explain GQA vs MQA trade-offs, and design an admission controller that won’t fall over.

To produce token 101, a decoder attends to tokens 1–100. To produce token 102, it attends to 1–101. Without caching, it recomputes key and value projections for the same first 100 tokens again and again.

The KV cache stores each past token’s key and value tensors at every transformer layer. On the next step, the model computes Q, K, and V only for the new token, appends its K and V, and lets its query attend over the accumulated cache.

KV cache reuses past key and value projections during autoregressive decodingDECODE STEP 4: GENERATE “MAT”The · K,Vcat · K,Vsat · K,Von · newAttention for “on”query × cached K → weights × Vnext token: “mat”Append only the new token’s K,V. Never recompute the first three.

The cache stores intermediate keys and values per layer—not token IDs and not final hidden states.

What is not cached

The new query still changes every step, so it must be computed. The cache saves historical K and V projections; it does not eliminate attention over past positions.

Why it changes inference

Prefill versus decode

During prefill, the entire prompt runs in parallel and fills the cache. During decode, each step processes one new token and reads the existing cache. Prefill is compute-heavy; decode is often memory-bandwidth-bound.

Memory grows with context

For batch size B, layers L, cached sequence length S, KV heads H, head dimension D, and bytes per element P, cache size is approximately 2 × B × L × S × H × D × P. The 2 is for keys and values.

Serving trade-offs

Continuous batching improves accelerator use but mixes requests with different lengths. Paged attention avoids reserving one giant contiguous region per request. Multi-query and grouped-query attention reduce the number of distinct KV heads. Quantization shrinks the cache at a possible quality cost.

vLLM: fitting more requests into serving memory

The vLLM serving system made KV-cache management a first-class systems problem. Its PagedAttention design borrows virtual-memory ideas to reduce fragmentation and share cached blocks safely.

Each live generation request owns a cache that grows token by token. Traditional contiguous allocation can reserve too much space, strand unusable gaps, or duplicate a shared prompt. Those losses reduce batch size even when the accelerator still has raw memory available.

01 · Page

PagedAttention divides the KV cache into fixed-size blocks. A request’s logical sequence can map to non-contiguous physical blocks, so growth does not require one large reserved region.

02 · Share

Requests with a common prefix can reference shared KV blocks rather than storing duplicate copies. Copy-on-write preserves isolation when their generated continuations diverge.

03 · Batch

Lower waste leaves room for more simultaneous sequences. The scheduler can form larger batches, improving accelerator utilization while requests continue to decode.

The vLLM paper reports near-zero KV-cache memory waste and 2–4× higher throughput at comparable latency than the FasterTransformer and Orca systems tested. This is a research paper plus an open-source serving system, not one company’s product case study.

Source: “Efficient Memory Management for Large Language Model Serving with PagedAttention,” Kwon et al. ↗
The user-facing effect

Efficient cache allocation does not change the words directly. It changes how many conversations the serving system can admit, how quickly tokens stream, and how much hardware that service needs.

A memory calculation

Take a model with 32 layers, sequence length 4,096, 32 KV heads, head dimension 128, batch size 1, and FP16 values (2 bytes).

Keys plus values.
1.07B2 × 1 × 32 × 4096 × 32 × 128 = 1,073,741,824 stored elements.
2 GiBMultiply by 2 bytes: 2,147,483,648 bytes, or exactly 2 GiB for one request in this illustrative configuration.

If the architecture uses 8 KV heads via grouped-query attention, the cache falls to 512 MiB. This is why head layout is a serving decision, not just a modeling detail.

One decode step

PyTorch · one cached attention step
import math
import torch

def decode_step(x_new, W_q, W_k, W_v, cache=None):
    q = x_new @ W_q                 # only newest token
    k_new, v_new = x_new @ W_k, x_new @ W_v

    if cache is None:
        K, V = k_new, v_new
    else:
        K = torch.cat([cache["K"], k_new], dim=-2)
        V = torch.cat([cache["V"], v_new], dim=-2)

    weights = torch.softmax(q @ K.transpose(-2, -1) / math.sqrt(q.size(-1)), dim=-1)
    return weights @ V, {"K": K, "V": V}

A real server maintains caches per layer and request, handles rotary position encodings, maps logical pages to physical memory, and frees blocks as sequences complete.

What interviewers actually ask

Expect the conversation to move quickly from the tensor idea to fleet economics.

OpenAI
Why is decoding often memory-bandwidth-bound?

What to say (≈90 sec): “I’d frame it around arithmetic intensity — FLOPs per byte read. In a single-token decode step, each token’s matrix-vector multiply reuses each weight exactly once, so at batch size one the intensity is roughly one FLOP per byte — the roofline model says you’re memory-bound. Every step streams two things: the full weight matrix — a 70B model in fp16 is about 140GB crossing HBM on every token — and the KV cache, which grows linearly with context length and batch size. Batching raises intensity because one weight stream serves many tokens, but the KV read scales with the batch, so at long contexts even batched decoding is cache-bandwidth-bound. That’s why GQA and paged attention show up here: they shrink the KV bytes per token, which is exactly the bottleneck.”

Likely follow-up: “So why doesn’t batching fully fix it?” → Because weights are shared across the batch but each request’s KV cache is its own bytes. As the batch grows, the KV read — which scales with batch × context — overtakes the weight read, and the bottleneck just moves from the model stream to the cache stream.

The answer that sinks you: “Because decoding does so much compute.” Why it fails: decoding is tiny compute — one token’s matvec per step. The GPU mostly idles waiting on memory, which is why utilization sits near 10%.

Anthropic
How would prompt caching interact with privacy and tenant boundaries?

What to say (≈90 sec): “I’d design it as four guardrails. First, the cache key must bind the prefix hash to the authorization context — the tenant ID and the permission set used to fetch any retrieved documents. Same bytes, different authorization, different key — that kills cross-tenant leakage at the key level. Second, scope: partition caches by tenant namespace, and for RAG-heavy prefixes expire entries aggressively or encrypt them with a tenant-scoped key, because a cached prefix can leak the documents behind it. Third, reuse accounting: measure hit rates on hashed keys only and never log the raw prefix text, so the metric itself can’t leak content. Fourth, treat cached prefixes as retained data under the tenant’s retention policy — expire them when the tenant’s data expires and invalidate on permission changes. The core principle: a cache is a copy of someone else’s data, and it must inherit every boundary the original had.”

Likely follow-up: “What breaks when a user’s permissions change mid-conversation?” → Their cached prefix may reference documents they can no longer see. Tie cache keys to a permission-set version and bump it on any revocation, so stale entries miss instead of serving unauthorized content.

The answer that sinks you: “Just share the cache across tenants for efficiency — hits only depend on the prompt text.” Why it fails: prompt caching is exact-prefix matching, so a shared cache lets tenant B observe a hit and learn tenant A’s prefix existed — that’s a cross-tenant information channel and a real privacy breach.

Meta
How do MHA, GQA, and MQA change cache size?

What to say (≈90 sec): “Cache bytes scale linearly with the number of KV heads. Standard multi-head attention keeps one key-value pair per query head, per token. Multi-query attention keeps one KV pair for all query heads — a factor-H shrink in cache size and in the bytes streamed per decode step — but squeezing every query head through a single KV bottleneck can cost quality on long-range reasoning. Grouped-query attention is the middle ground: G groups of query heads share one KV pair each, cutting cache by H/G — Llama 2 70B uses 8 KV heads for 64 query heads, an 8x shrink — with quality close to MHA. The knob is KV heads: fewer heads means proportionally less memory and bandwidth per token, traded against representational capacity in the keys and values. As a rule of thumb I’d quote: KV cache per token is 2 × layers × KV heads × head dim × dtype bytes.”

Likely follow-up: “When would you still ship MHA?” → For short contexts and small batches, where cache bytes are small next to the weights — the quality margin of MHA costs nothing practical there. MQA and GQA only pay off when KV bytes dominate, which is long contexts and large batches.

The answer that sinks you: “MQA is always better because it uses less memory.” Why it fails: it optimizes one axis and ignores quality — on short-context workloads the MQA quality hit is real while the savings are irrelevant, so the trade-off flips.

Google
What is paged attention solving?

What to say (≈90 sec): “I’d start from the allocation problem. In naive serving you reserve a contiguous block for each request’s maximum possible context — prompt plus max tokens. Variable-length requests mean most of that reservation is empty, and when the gaps don’t line up you get external fragmentation: memory is free but no contiguous chunk fits the next request, so you OOM with capacity sitting idle. Paged attention slices the KV cache into fixed-size blocks — vLLM’s PagedAttention uses 16-token blocks — and a block table maps each request’s logical positions to physical blocks, so one request’s cache can be scattered non-contiguously. Two wins: first, memory is allocated block by block as tokens arrive, so waste drops to at most one partial block per request; second, blocks are shareable — parallel samples from one prompt share the prompt’s blocks by reference, so beam search or n-sample decoding costs one copy of the prefix instead of n.”

Likely follow-up: “What’s the cost of that indirection?” → Every attention lookup goes through the block table, and a partially filled last block wastes up to block-size-minus-one slots per request. It pays for itself the moment utilization is the bottleneck — which at fleet scale it always is.

The answer that sinks you: “It makes attention faster.” Why it fails: it doesn’t change the attention math at all — it’s a memory-utilization trick, and the throughput gain is indirect, through bigger batches and fewer OOMs.

Amazon
Design an admission controller for an LLM endpoint.

What to say (≈90 sec): “I’d walk through it in five steps. First, estimate worst-case memory before admitting: KV bytes per token — 2 × layers × KV heads × head dim × dtype — times prompt length plus max new tokens, plus model and scratch overhead. Second, reserve in block units, not raw bytes — since the cache is paged, admit only if free blocks cover the worst case, otherwise queue. Third, cap each request’s share so one 100k-context request can’t lock the pool and starve fifty small ones — that’s the noisy-neighbor guard. Fourth, schedule by SLO: interactive traffic with tight time-to-first-token targets gets reserved headroom over batch and offline jobs. Fifth, make rejection a first-class path: return a clean 429 with a Retry-After estimate instead of admitting into an OOM, and log the dropped request’s estimate so capacity planning sees the real demand signal. And I’d close with validation: load tests with adversarial mixes — all-long-context bursts — plus a dashboard on free blocks and queue depth, because the controller is only as good as its estimates.”

Likely follow-up: “What if requests exceed their estimated max tokens?” → Cap with max tokens as a hard preemption point and never admit against 100% of free blocks — keep headroom. If a request exhausts its estimate mid-flight, preempt and recompute or re-queue rather than let one overrun crash the whole batch.

The answer that sinks you: “Admit everything and let the scheduler sort it out.” Why it fails: an OOM mid-decode kills every request sharing the GPU, not just the offender — admission control exists precisely because recovery isn’t free.

Apple
What cache policy fits an on-device assistant?

What to say (≈90 sec): “The constraint set is different on device: a fixed RAM budget shared with the OS, thermal throttling under sustained load, and private data that must never leave. So I’d layer four policies. First, bound the context with a rolling window and a hard cap — when it fills, evict the oldest tokens or, better, compress them into a rolling summary so long conversations degrade gracefully instead of forgetting abruptly. Second, shrink bytes per token: quantize the KV cache — int8 or int4 K and V is standard — and run a GQA model so there are fewer KV heads to store. Third, respect the device: watch memory pressure and temperature, and shed load — shorter max tokens, smaller batches — before the OS kills the process. Fourth, the privacy one: every cache entry is derived from user data, so it’s encrypted at rest on device, never uploaded, and wiped on logout. The metric I’d watch is p99 time-per-token under thermal throttle, because a policy that looks fine on a cool phone falls apart on a hot one.”

Likely follow-up: “Why not just spill the cache to flash?” → Flash is orders of magnitude slower than the bandwidth decode needs, the read amplification would drain the battery, and the privacy surface grows. If the cache doesn’t fit in RAM, the right answer is to shrink it, not page it.

The answer that sinks you: “Keep the full context and let the OS handle memory pressure.” Why it fails: the OS handles pressure by killing your process — there’s no graceful degradation, and the whole session state goes with it.

Key takeaways

  1. KV caching avoids recomputing historical key and value projections.
  2. Prefill and decode have different performance profiles.
  3. Cache memory grows linearly with sequence length, layers, KV heads, and batch.
  4. GQA, MQA, quantization, and paging are major serving levers.
  5. At scale, cache allocation determines throughput and admission capacity.
Read nextEvaluating LLMs →