Memory Management for LLM Inference — The KV Cache Problem

The single largest consumer of GPU memory during inference often isn't the model — it's the KV cache, the growing store of attention state that lets a model generate text without recomputing the past. Managing it well is the difference between serving a handful of users and serving thousands on the same hardware. This post explains what the KV cache is, why it's such a brutal memory problem, and how PagedAttention borrowed a 1960s operating-systems idea to solve it.

Posts 2 and 5 were about the weights — the fixed cost of the model in HBM. This post is about the other half of inference memory: the per-request state that grows with every token. It’s where capacity (post 2) becomes the binding constraint on how many users you can serve, and where the cleverest fix in modern serving comes from an old, familiar place: virtual memory.

What the KV cache is and why it exists

When a transformer generates token by token, each new token attends to all previous tokens. Naively, generating token 1000 would require recomputing attention over tokens 1–999 from scratch — quadratic wasted work. The fix is the KV cache: for every token processed, store its attention keys and values in HBM, so each new token only computes its own K/V and reuses the cached ones. This turns per-token work from “reprocess the whole history” into “process one token,” and it’s why generation is fast enough to be practical.

The cost is memory. The KV cache stores K and V vectors for every token, in every layer, for every attention head, for every active request. It grows linearly with sequence length and with the number of concurrent requests, and for long contexts and many users it routinely exceeds the size of the model weights themselves. So inference memory has two tenants in HBM: the weights (fixed, shared across all requests) and the KV cache (per-request, growing) — and the KV cache is usually the one that runs you out of room.

Why the KV cache wrecks naive memory management

The hard part isn’t the total size — it’s that each request’s cache grows unpredictably, one token at a time, to an unknown final length. Classic serving systems handled this by pre-allocating a contiguous block of HBM per request, sized for the maximum possible sequence length. That’s catastrophic on two counts:

The result was that GPUs ran at a fraction of their potential concurrency — not because they lacked memory, but because they managed it badly. The KV cache was the bottleneck on throughput, and it was a pure memory-management problem, not a compute one.

PagedAttention: virtual memory for the KV cache

The breakthrough (from the vLLM project) was to recognize this as a problem operating systems solved decades ago. PagedAttention applies paging — the virtual-memory idea — to the KV cache:

This near-eliminates both fragmentation problems: no waste from over-reservation (you grow block by block) and no external fragmentation (fixed-size blocks pack perfectly). The payoff is far more concurrent requests on the same GPU — often several times the throughput — because memory that used to sit reserved-but-empty is now available. It even enables sharing: requests with a common prefix (the same system prompt, say) can point at the same physical blocks, storing one copy instead of one per request.

The engineering lesson is worth more than the specific technique: a defining inference bottleneck turned out to be a memory-management problem, and the solution was not new math but the disciplined reuse of a classic systems idea. This is the texture of inference engineering — the wins come from treating the GPU as the memory-constrained parallel computer it is, and borrowing hard-won lessons from the rest of systems programming.

The takeaway: the KV cache stores each token’s attention keys and values so generation processes one token at a time instead of reprocessing the whole history — essential for speed, but it grows linearly with sequence length and concurrent requests and often exceeds the model weights in HBM. Naive per-request contiguous, max-length allocation wrecks this with internal fragmentation (reserved-but-unused) and external fragmentation (unusable gaps), capping concurrency far below the hardware’s potential. PagedAttention fixes it by applying operating-system paging: fixed-size KV blocks, mapped non-contiguously through a lookup table, allocated on demand — eliminating fragmentation, multiplying throughput, and even letting shared prefixes share physical blocks. The lesson: a core inference bottleneck was a memory-management problem solved by borrowing a classic systems idea.

Key takeaways

Further reading

Sources & References