DEV Community

Cover image for Why the KV cache, not the weights, decides how many users an LLM server can hold
Andika Rachman
Andika Rachman

Posted on

Why the KV cache, not the weights, decides how many users an LLM server can hold

A 7B model in fp16 takes 13.5 GB of GPU memory for its weights. On an 80 GB card that leaves about 66 GB free, which sounds like plenty. It runs out at around 30 users with 4,096-token contexts, and the weights aren't what fills it. The KV cache is.

This post works through the arithmetic. All the numbers are for a Llama-2-7B-shaped model: 32 layers, 32 attention heads, head dimension 128, fp16.

What the cache stores

A decoder-only model generates one token per forward pass. The new token's query has to attend to the keys and values of every earlier token. Without a cache, each pass recomputes those keys and values for the whole prefix.

The causal mask is what makes reuse safe. Earlier tokens can't see later ones, so appending a token never changes the keys and values already computed. The model can compute them once, store them, and on each step add one new key row and one new value row per layer.

The saving is large. With a 5-token prompt and 64 generated tokens, recomputing everything costs 2,336 key/value projections per layer. With a cache it costs 68, about 34 times less. Attention still reads every cached key at each step, so the cache removes the recomputed projections, not the attention cost itself.

What it costs

The cache for one token is:

2 (K and V) × layers × kv_heads × head_dim × bytes
= 2 × 32 × 32 × 128 × 2
= 524,288 bytes ≈ 0.5 MiB
Enter fullscreen mode Exit fullscreen mode

Half a megabyte per token adds up quickly:

Context Batch KV cache
4,096 1 2.1 GB
32,768 1 17.2 GB
4,096 32 68.7 GB

A single 32K-token conversation needs more memory for its cache than the model needs for its weights. And the weights are paid once, while the cache is paid per sequence. So the number of users a server can batch is roughly the free memory divided by the cache for one sequence: 66.5 GB / 2.1 GB ≈ 30 sequences of 4,096 tokens, before activations and runtime buffers.

Why batching matters so much

Generation has two phases. Prefill runs the whole prompt through the model at once, so each byte of weights read from memory does work for every prompt token. Decode produces one token per step, and each byte of weights read does work for just that one token. Decode is limited by memory bandwidth, not by compute.

Batching is the fix: one read of the weights serves every sequence in the batch. But every extra sequence brings its own KV cache, so cache size is what caps the batch, and with it the throughput.

How models shrink it

Each technique cuts one term of the formula:

  • Fewer KV heads. Multi-query attention (Shazeer, 2019) shares a single key/value head across all query heads. Grouped-query attention (Ainslie et al., 2023) uses a few groups. With 8 KV heads instead of 32, the same 7B shape would fit about 123 sequences of 4,096 tokens instead of 30. Llama 2 70B uses 8 KV heads; the 7B keeps all 32.
  • Fewer bytes. Storing the cache in int8 halves it again, to about 247 sequences with 8 KV heads, usually at a small accuracy cost.
  • Fewer tokens. A sliding window keeps only the most recent tokens. Mistral 7B uses a 4,096-token window.
  • Less waste. PagedAttention (Kwon et al., 2023), used in vLLM, stores the cache in fixed-size blocks instead of reserving the maximum length for every sequence, so less memory sits empty.

The first and third are architecture choices made before or during training. Cache quantization and paging can be applied at serving time.

Try it

I built an interactive version of this as part of Lensa ML, a free set of ML explainers. The KV cache explainer lets you step through generation token by token, drag the context length and batch size to watch the cache outgrow the weights, and toggle KV heads, cache precision and a sliding window to see each saving.

References

Top comments (4)

Collapse
 
max_quimby profile image
Max Quimby •

This is the arithmetic everyone skips when they size a GPU box off model weights alone and then wonder why they OOM at 30 users. The line worth tattooing on the wall: weights are paid once, cache is paid per sequence.

Two things from running this in production that sharpen the picture. First, your table assumes every sequence hits full context — in practice the cache is paid per token actually present, so with PagedAttention (which you mention) real concurrency is set by the distribution of live context lengths, not the max. A fleet of mostly-short chats fits far more than 30; a handful of 32K-token sessions can starve the box. Capacity is a function of the workload mix, not a single number.

Second, the knob you don't list: prefix sharing. With a long shared system prompt across users, vLLM can dedupe those KV blocks, and that changed our effective batch size more than int8 did on an agent workload where every request carried the same 4K-token preamble. Worth a line in the "how models shrink it" section — it's a serving-layer win, not a model-architecture one, which is probably why it's easy to miss.

Collapse
 
qisuancloud profile image
Alex Chen •

The 0.5 MiB per token figure makes the capacity cliff much clearer than the usual 'weights are small' framing. I wonder how you would weigh int8 KV quantization against grouped-query attention when the workload is already long-context: does quantization preserve quality well enough to prefer it first, or is reducing KV heads the safer first move? In practice, PagedAttention seems to change the question too, since it turns the constraint from raw capacity into fragmentation and scheduling.

Collapse
 
andika_rachman_aa62526318 profile image
Andika Rachman •

GQA is baked in at training time. You can't switch a trained model to fewer KV heads without uptraining (the GQA paper used about 5% of the original pretraining compute). So for a model you already serve, int8 or FP8 KV is the lever you have, and 8-bit usually costs very little quality. I'd still test it on long-context evals, where small errors add up. If you're choosing a model, one that already uses GQA saves more (4x vs 2x), and the two stack.

Agreed on PagedAttention. It removes the waste (vLLM reported 60–80% before paging), but not the 0.5 MiB per token, so after that the limit is capacity again, and the hard part is scheduling.

Collapse
 
paul_irolla_6b5ae261d0224 profile image
Paul Irolla •

The number of users moves with the context you allow each one. Does your memory budget assume an average context per user?