LLM inference, part 2 of 3
KV cache size, not quadratic attention, is what fills your GPU at 32k context
KV cache size is what fills a GPU at long context. For Mistral NeMo 12B, Pexon measured 640 MiB of cache at 4,096 tokens and 5,120 MiB at 32,768 in llama.cpp: eight times the memory for eight times the context. Added to 7 GiB of four-bit weights, that is more than a 12 GB card holds.
One model, two context lengths
What llama.cpp allocated for Mistral NeMo 12B at 4,096 and 32,768 tokens
| Line item | At 4,096 tokens | At 32,768 tokens | The caveat in the same row |
|---|---|---|---|
| Weights, Q4_K_M | 6.96 GiB | 6.96 GiB | Paid once at load time; does not grow with context |
| KV cache, f16 (the default) | 640 MiB | 5,120 MiB | Exactly eight times for eight times the tokens: linear, not quadratic |
| KV cache, q8_0 | 340 MiB | 2,720 MiB | 53% of f16, not 50%, because q8_0 stores a scale for every 32 values |
| Attention scratch, flash attention off | 328 MiB | 2,176 MiB | Grows with context times the 512-token processing chunk, not context squared |
| Attention scratch, flash attention on | 300 MiB | 328 MiB | Nearly flat; this is the setting that should be on |
Read from llama.cpp's allocation log on the Metal backend. CUDA reserves its scratch buffers slightly differently, so treat the two scratch rows as an order of magnitude; the weight and cache rows are the same on any backend.
The cache is the line item that grows, and it grows in a straight line
Generating a token means comparing it with every token before it. For that comparison each earlier token contributes two vectors per layer: a key, which is what the new token is matched against, and a value, which is what it contributes once matched. Those vectors do not change once computed, so every engine stores them instead of recomputing them for each new token. That store is the KV cache.
One more token in the conversation means one more key and one more value in every layer, and nothing else. The cache therefore grows linearly with context and with the number of conversations in flight, and llama.cpp allocates it up front for the full context you configure, before the first prompt arrives.
That is why the error appears when you raise the context setting, not when someone sends a long prompt.
Mistral NeMo 12B at 32,768 tokens and f16, from the model's own config.json. The result matches the 5,120 MiB llama.cpp logged.
Key and value, layers, KV heads, head dimension
2 · 40 · 8 · 128
81,920 values stored per token
Bytes per value at f16
× 2
160 KiB per token
Context configured
× 32,768
allocated before the first prompt
KV cache for one sequence
5,120 MiB
5.0 GiB, on top of the weights
The formula and the allocator agree to the mebibyte. If your estimate is off, the inputs are wrong, not the arithmetic.
Where the quadratic story comes from, and why it is not your out-of-memory error
Attention compute is quadratic: every token is scored against every other, so doubling the context quadruples the scoring work. Memory is a different matter. If the full score matrix were stored, one layer of Mistral NeMo at 32,768 tokens would need 32,768 × 32,768 entries for each of 32 heads, about 64 GiB at two bytes each, and nobody would get past 8k context on any card.
Nobody stores it. FlashAttention computes the scores in tiles that fit in on-chip memory and never writes the n × n matrix out; its authors state the memory cost as linear in sequence length while the operation count stays quadratic. Without flash attention, llama.cpp still processes the prompt in 512-token chunks, so its scratch buffer scales with context times 512, which is the 2,176 MiB in the table. A 512-token chunk scored against 32,768 positions for 32 heads at four bytes is 2,048 MiB, most of that buffer.
Grouped-query attention is the other reason the numbers are survivable. Mistral NeMo has 32 query heads but only 8 key-value heads, so it stores a quarter of what full multi-head attention would. At 32k tokens that is 5 GiB instead of 20.
Our view, stated plainly: an out-of-memory error at long context is a KV cache budgeting problem in every engine we would deploy. Debugging it as an attention-algorithm problem wastes a day.
Prefill vs decode: why token generation ignores TFLOPs: where the quadratic compute cost actually shows up, as prefill time
One conversation, getting longer
The f16 KV cache for Mistral NeMo 12B as the context grows
- 4,096 tokens 640 MiB
- 16,384 tokens 2.5 GiB
- 32,768 tokens 5 GiB, with 6.96 GiB of weights: past a 12 GB card
- 131,072 tokens, the full window 20 GiB for one user, before any weights
Computed at 160 KiB per token, the figure llama.cpp allocated; one sequence, f16 cache. Every additional concurrent user adds the same again, which is why the 128k window the model advertises is not a setting a single consumer card can honour.
Five levers, in the order of what they cost you
Check the formula against the memory left after the weights are loaded, not against the card's total. On a 12 GB card running the four-bit weights above, that is roughly 4 GiB for cache and scratch together.
- Turn flash attention on: it costs nothing in quality and took the scratch buffer from 2,176 to 328 MiB at 32k. In current llama.cpp, -fa defaults to auto.
- Quantise the cache to q8_0: 5,120 MiB became 2,720 at 32k. Cheap, but evaluate it on your tasks; we have not measured its accuracy cost on this model.
- Configure the context you actually use: the cache is allocated for the full setting up front. A 32k setting for 4k conversations pays for 28k tokens nobody sends.
- Count concurrent sequences: every slot carries its own cache. Thirty-two users at 32k tokens each is 160 GiB at f16 for this model.
- Prefer models with fewer KV heads: grouped-query attention is a property of the model, not a runtime flag. It is decided when you choose the model, so decide it deliberately.
Measured by Pexon on 16 October 2026: llama.cpp build 11429 (d81235049), Metal backend, Apple M4 Pro with 24 GB unified memory, Mistral-Nemo-Instruct-2407 Q4_K_M. Each configuration was loaded once with -v and the allocation lines read from the log. The 12 GB card arithmetic applies the same allocations to a 12 GB GPU; we did not run this on one.
Reproduce it
Two commands: one prints what llama.cpp allocates for your model and context, the other computes it before you download anything.
# 1. What llama.cpp allocates (verbose log, one generated token)
llama-completion -v -m model.gguf -c 32768 -ctk q8_0 -ctv q8_0 -fa on \
-n 1 -p "Hi" -no-cnv 2>&1 | grep -a -E "llama_kv_cache: size|compute buffer"
# llama_kv_cache: size = 2720.00 MiB ( 32768 cells, 40 layers, 1/1 seqs) ...
# 2. The same number from config.json, before downloading weights
def kv_cache_mib(layers, kv_heads, head_dim, tokens, bytes_per_value=2.0, seqs=1):
# q8_0 = 34/32 bytes per value, q4_0 = 18/32
return 2 * layers * kv_heads * head_dim * tokens * bytes_per_value * seqs / 2**20
kv_cache_mib(40, 8, 128, 32768) # 5120.0 (f16)
kv_cache_mib(40, 8, 128, 32768, 34/32) # 2720.0 (q8_0)If the two numbers disagree, check num_key_value_heads in config.json. The widely copied formula in NVIDIA's 2023 inference-optimisation post uses hidden_size instead, which ignores grouped-query attention and gives 25 GiB for this model at 32k instead of 5.
When shrinking the cache is the wrong fix
If the weights alone fill the card, no cache setting will save the deployment: Mistral NeMo 12B at bfloat16 is about 24 GB of weights before a single token of cache. And if the workload is many concurrent users rather than one long conversation, the honest answer is usually a larger card or a second one, sized against measured concurrency. That is the arithmetic behind GPU sizing for LLM inference.
Where to go next
- GPU sizing for LLM inference: buy the right number of cards — the same memory model, fed with your measured context lengths and users
- Prefill vs decode: why token generation ignores TFLOPs — part 1, why every byte of cache also costs decode speed
- LLM batching: 6.8x the throughput at 4.7x the latency — part 3, where the cache decides how many users fit at once
- LLM quantisation accuracy: what four bits actually costs — shrinking the weights to leave room for the cache
Next in the series · Part 3LLM Batching: 6.8x the Throughput at 4.7x the Latency
What people ask after the out-of-memory error
How do I calculate the KV cache size for a model?
Multiply two, layers, KV heads, head dimension, tokens and bytes per value. Take the layer and head counts from the model's config.json, and use num_key_value_heads, not num_attention_heads. For Mistral NeMo 12B that is 2 × 40 × 8 × 128 × 2 bytes, or 160 KiB per token at f16, and llama.cpp allocated exactly that.
Does attention really scale quadratically with context length?
Its compute does; its memory does not, in any current engine. FlashAttention computes the scores in tiles and never stores the full n × n matrix, so memory beyond the KV cache stays linear in sequence length. The quadratic part shows up as prefill time on very long prompts, not as an out-of-memory error.
Is quantising the KV cache to q8_0 safe?
It is the cheapest memory lever, and it still needs an evaluation. In llama.cpp it cuts the cache to 53% of f16, 2,720 against 5,120 MiB at 32k tokens. We have not measured its accuracy effect on this model, so treat it as a candidate to test on your own tasks, not a free setting. A quantised V cache also requires flash attention, which the auto setting enables.
When does a smaller KV cache not help?
When the weights alone already exceed the card. Mistral NeMo 12B at bfloat16 is about 24 GB of weights, twice a 12 GB card, before a single token of cache. No cache setting fixes that; only quantised weights, a smaller model or a larger card do.
What does it cost to plan memory for a real deployment?
Our fixed-price Readiness Blueprint is €4,900 over two weeks. It replaces assumed context lengths and user counts with measured ones, then computes weights, KV cache and headroom for three candidate configurations. The formula on this page is free; the numbers you feed it are what usually turn out to be wrong.
Next step
Find out how much context your hardware really holds
Two weeks, fixed price: measured context lengths and concurrency from your own traffic, then weights, KV cache and headroom computed for three candidate configurations, with every assumption written down.
