The KV Bottleneck Explained: Why Inference Is Memory-Bound
Building from exact byte counts to the fundamental insight: autoregressive inference is bottlenecked not by arithmetic but by memory bandwidth from loading keys and values. Every KV cache optimization in the literature is a response to this single bottleneck — derived step by step with concrete numbers.
The previous blogs established three bottlenecks of vanilla attention: quadratic compute, quadratic memory, and linear KV cache growth. We gave the numbers. Now we go deeper into the third one — the KV cache — because it is the bottleneck that dominates modern inference and the one that the next several blogs will systematically attack.
This blog is not about solutions. It is about understanding the problem with enough precision that every solution in the series becomes obvious.
The Running Model
We continue with the same model from the previous blogs:
- heads
- (so )
- layers
- fp16 throughout (2 bytes per element)
We established in the previous blog that this model costs 24 KB per token in KV cache. We will now trace exactly where those bytes come from, why they matter more than FLOPs, and what happens when you scale up.
The GPU Memory Hierarchy
Before we can understand why inference is memory-bound, we need to understand where data lives on a GPU. Modern GPUs have a multi-level memory hierarchy, and the speed difference between levels is enormous.
Three tiers
A GPU has three main tiers of memory. We will use the NVIDIA A100 as our concrete reference:
| Memory tier | Capacity | Bandwidth | Latency |
|---|---|---|---|
| Registers + SRAM (on-chip) | ~20 MB | ~19 TB/s | ~1 ns |
| HBM (high-bandwidth memory, off-chip) | 40–80 GB | ~2 TB/s | ~100 ns |
| CPU DRAM (main memory) | >1 TB | ~50 GB/s | ~500 ns |
The key numbers: SRAM is roughly faster than HBM. HBM is roughly faster than CPU DRAM. But SRAM is roughly to smaller than HBM.
What fits where
In our running model, the KV cache for one head, one layer, at context length is:
Across all 8 heads and 12 layers: MB MB.
This is larger than the A100’s entire SRAM. The KV cache cannot live in SRAM. It must live in HBM. Every time the model needs to read the cached keys and values for an attention step, those bytes must travel from HBM to SRAM at HBM bandwidth — roughly TB/s.
Numerical check. At tokens: KV cache bytes MB. Still smaller than SRAM’s 20 MB — in principle, the KV cache for a very short context could fit. But the model’s weight matrices also need SRAM space during computation, so in practice even 12 MB is too much. For any meaningful context length, the KV cache lives in HBM.
Why this hierarchy matters for inference
During training, the input is a full batch of sequences processed in parallel. The attention computation involves large matrix-matrix multiplies (, ), which have much higher arithmetic intensity and are often compute-bound in practice. The GPU’s arithmetic units matter much more here than in autoregressive decoding.
During autoregressive inference, we generate one token at a time. The attention computation involves matrix-vector multiplies (, ). The arithmetic intensity of a matrix-vector multiply is fundamentally different from a matrix-matrix multiply. We will derive this precisely in the next section.
What Happens During One Autoregressive Step
We are generating token . All previous tokens have already been processed. Their keys and values sit in the KV cache in HBM.
The computation for one step has three phases. We trace each one for a single head, then scale to all heads and layers.
Phase 1: Compute the New Query, Key, and Value
The embedding of the new token is projected through three weight matrices:
Each projection is a matrix-vector multiply. Let us count exactly.
FLOPs for one projection. The output has elements. Each element is a dot product of with one column of . A dot product of two -dimensional vectors requires multiplications and additions. Using the standard convention of counting each multiply-add as 2 FLOPs:
There are output elements, so:
Three projections (Q, K, V) per head, heads:
Bytes loaded from HBM. To compute one projection, we must load the weight matrix from HBM. It has elements. In fp16 (2 bytes per element):
We also load the input vector (512 elements, 1 KB) — this is tiny and shared across all projections, so we can ignore it. Three matrices per head, heads:
The Roofline Model: Compute-bound vs. Memory-bound
We now have both the FLOPs and the bytes for Phase 1. The ratio of these two quantities has a name.
Arithmetic intensity is the number of floating-point operations performed per byte of data transferred from memory. It is measured in FLOP/byte.
This concept comes from the Roofline model (Williams, Waterman, and Patterson, 2009). The Roofline model says: every hardware platform has a ridge point — the arithmetic intensity at which the platform transitions from being memory-bandwidth-limited to compute-limited. The ridge point is:
For the A100 GPU:
If an operation’s arithmetic intensity is below 156 FLOP/byte, it is memory-bandwidth-bound: the GPU finishes its arithmetic before the next chunk of data arrives from HBM. It sits idle, waiting for data.
If an operation’s arithmetic intensity is above 156 FLOP/byte, it is compute-bound: data arrives faster than the GPU can process it. The arithmetic units are the bottleneck.
Phase 1’s arithmetic intensity:
This is below the ridge point. The projection phase is catastrophically memory-bandwidth-bound. For every FLOP the GPU performs, it must load one byte from HBM. But the GPU could perform 156 FLOPs per byte loaded if the data were available. The arithmetic units are idle more than 99% of the time.
Why is the intensity exactly 1.0? This is not a coincidence. A matrix-vector multiply of shape performs FLOPs and loads bytes (the matrix in fp16) plus bytes (the vector, negligible for large ). The arithmetic intensity is:
Under this simplified fp16 accounting, the arithmetic intensity is approximately 1.0 FLOP/byte for large matrix-vector multiplies regardless of the matrix dimensions. That is why autoregressive inference — where nearly every operation is a matrix-vector multiply — tends to be memory-bandwidth-bound.
Contrast with training. During training, the projection is a matrix-matrix multiply: where and . The FLOPs are . The bytes loaded are the weight matrix ( bytes) plus the input ( bytes). For large , the arithmetic intensity approaches:
At 64 FLOP/byte, training is still below the ridge point but much closer — especially with larger batch sizes that push the effective higher. The key difference: training’s arithmetic intensity scales with (batch sequence length), while inference’s arithmetic intensity is stuck at 1.0 regardless of .
Phase 2: Append to the KV Cache
The new and vectors are appended to the cache in HBM.
Each vector has elements bytes in fp16. Two vectors (K and V) per head, heads:
This is negligible compared to the reads we are about to do in Phase 3. The write is a one-time cost per step that does not scale with context length. We include it for completeness but will not track it further.
Phase 3: Compute Attention Over the Full Context
This is where the KV cache bottleneck lives. The new query must attend to all cached keys, produce a softmax distribution, and use it to weight all cached values.
We trace each sub-step for one head in full detail.
Step 3a: Score computation.
This is the matrix-vector product of transposed with .
Equivalently, each score is a dot product of two -dimensional vectors. There are such dot products.
FLOPs: Each dot product costs FLOPs. There are of them:
Bytes loaded: The entire must be read from HBM — elements, each 2 bytes:
Plus the query vector : bytes. This is negligible for large .
Arithmetic intensity:
Again, exactly 1.0. This is a matrix-vector multiply, and as we showed, all matrix-vector multiplies in fp16 have arithmetic intensity 1.0.
Step 3b: Scaling and softmax.
First, divide each score by : that is divisions. Then apply the softmax, which requires three passes over the scores:
- Find the maximum: comparisons
- Compute for each : exponentiations
- Normalize (divide by sum): additions to compute the sum, then divisions
Total: roughly FLOPs. The bytes involved are the score vector which was just computed — it likely still resides in SRAM from Step 3a. If we must read it from HBM: bytes.
This step’s cost is dominated by Steps 3a and 3c. We include the count but it does not change the analysis.
Step 3c: Weighted sum of values.
where is the attention weight vector.
This is another matrix-vector multiply: transposed with .
Equivalently, this is a weighted sum of the value vectors: . Each costs multiplications, and summing costs additions.
FLOPs:
Bytes loaded: The full : bytes.
Arithmetic intensity: FLOP/byte.
Total for Phase 3, One Head
Summing Steps 3a, 3b, and 3c:
(The softmax bytes are negligible since the data is likely already in SRAM.)
The arithmetic intensity stays near 1.0 as grows. It does not improve with longer context. In this roofline analysis, the attention computation during inference remains memory-bandwidth-bound across sequence lengths rather than suddenly becoming compute-bound at larger .
Scaling to All Heads and All Layers
Each of the heads performs the same computation with different weight matrices. Each of the layers performs the same computation with different parameters. Since each head has its own KV cache, the total bytes read are:
Numerical check. At :
This is 96 MB of HBM reads per generation step — just for loading the KV cache. At A100 HBM bandwidth of 2 TB/s, this takes:
Each generation step spends at least 48 microseconds just loading KV cache data from HBM, regardless of how fast the arithmetic is.
Numerical check at :
At 2 TB/s: ms per step. That is 1.5 ms per token — for the KV cache reads alone.
The Fundamental Equation of Inference Throughput
We can now write down the time for one autoregressive step. Since the computation is memory-bandwidth-bound, time is determined by bytes loaded, not FLOPs.
Deriving the total bytes per step
The total bytes loaded per step consist of two categories:
Category 1: Model weight bytes (loaded once per step, independent of ).
A transformer layer has:
- Attention projections: (each is head matrices concatenated) and . Total: parameters.
- FFN: two matrices, and . Total: parameters.
- LayerNorm parameters: negligible ( per norm, two norms per layer).
Total per layer: parameters. In fp16:
Numerical check: bytes MB per layer.
Across layers:
(The slight discrepancy from is because we are rounding. The exact value is bytes MB.)
Category 2: KV cache bytes (grows linearly with ):
The total and the crossover
Per-step time at HBM bandwidth :
The crossover. KV cache reads exceed model weight reads when:
The cancels from both sides, leaving:
Since and :
Numerical check: . So tokens.
Let us verify: bytes MB weight bytes. Correct.
This is a remarkably clean result: the crossover occurs at approximately , independent of , , or .
Interpretation. For our small model (), the crossover is at ~3K tokens. For LLaMA-7B (), the crossover is at tokens. For GPT-3 (), it is at ~74K tokens. Wider models have a later crossover because their weight matrices are proportionally larger.
But every model eventually crosses — and modern models routinely operate at 100K+ tokens.
| Context length | Weight bytes | KV cache bytes | Dominant load |
|---|---|---|---|
| 512 | 72 MB | 12 MB | Weights |
| 1,024 | 72 MB | 24 MB | Weights |
| 2,048 | 72 MB | 48 MB | Weights |
| 3,072 | 72 MB | 72 MB | Tie |
| 4,096 | 72 MB | 96 MB | KV cache |
| 8,192 | 72 MB | 192 MB | KV cache |
| 16,384 | 72 MB | 384 MB | KV cache |
Beyond the crossover, every doubling of context length doubles the per-step latency (since the dominant cost — loading the KV cache — doubles). The model weights are a fixed cost that does not grow with context.
Deriving per-step latency
At A100 bandwidth TB/s:
Numerical check at :
At :
At 128K context, a single token takes 1.6 ms. To generate 100 tokens, the model spends 160 ms just on memory transfers. And this is for our tiny 12-layer model.
Why Batch Size Cannot Save You
A natural reaction: if each step is memory-bound because the arithmetic intensity is 1.0, batch more requests together to amortize the weight loading. This is correct — but it hits a wall.
How batching helps
With batch size , Phase 1 stays the same: the weight matrices are loaded once and applied to input vectors. This is now a matrix-matrix multiply — where . The FLOPs scale by , the weight bytes stay constant, so the arithmetic intensity becomes FLOP/byte. At , we reach the A100’s ridge point and Phase 1 becomes compute-bound.
Phase 3 is different. Each request in the batch has its own context — its own KV cache. The for request 1 is different from the for request 2 (they are answering different prompts). So the bytes loaded in Phase 3 are:
The FLOPs also scale by . Each request’s query attends to its own cache — there is no sharing. So the arithmetic intensity of Phase 3 remains:
Batching does not help Phase 3 at all. The KV cache for each request must be loaded separately, and the FLOPs and bytes both scale linearly with .
The memory capacity wall
Even if batching could help compute, it hits a hard wall: the KV caches must all fit in HBM simultaneously.
At , each request’s KV cache is 96 MB. With :
Add the model weights (72 MB for our small model — negligible here, but for a 7B model the weights are ~14 GB in fp16), activations, optimizer states (if finetuning), and framework overhead. On an A100 with 80 GB HBM, we might have ~60 GB available for KV caches, allowing concurrent requests at 4K context.
Now scale to : each cache is 3 GB. Maximum concurrent requests: .
The situation for larger models is much worse. For LLaMA-7B at 128K context, each cache is 64 GB — it does not even fit on a single GPU. Batch size must be 1, and multi-GPU parallelism is required just to hold one request.
This is the fundamental tension:
The KV cache is the binding constraint on both throughput (bandwidth) and concurrency (capacity). It is the single bottleneck that limits how many tokens per second a serving system can produce and how many users it can serve simultaneously.
Deriving the Per-Token KV Cache Formula
Let us derive a clean, closed-form expression for the KV cache cost per token.
Starting from first principles
At layer , head , for one token at position , the KV cache stores:
- One key vector:
- One value vector:
In fp16, each element is 2 bytes. So the cache for one token, one head, one layer is:
With : bytes.
Numerical check: 256 bytes stores two 64-element fp16 vectors. bytes per vector, two vectors: . Correct.
Summing over heads
There are heads per layer, each with its own K and V:
With : bytes KB per token per layer.
Summing over layers
There are layers, each with its own attention:
With : bytes KB per token.
Simplification using
In all standard architectures, the head dimension is chosen so that . This is not a constraint — it is a design convention that keeps the total width constant. Using this identity:
Substituting:
Numerical check: bytes KB. Matches.
This formula reveals that the KV cache cost per token depends on exactly two hyperparameters: model depth and model width . It does not depend on or individually — only on their product . Doubling the number of heads while halving (keeping constant) does not change the KV cache size at all.
Scaling to real models
The total KV cache for a sequence of length is:
Let us compute this for production models at tokens:
GPT-2 Large (, ):
Numerical check of per-token cost: bytes KB/token. KB GB. Consistent.
LLaMA-7B (, ):
Per-token: bytes KB/token.
LLaMA-65B (, ):
Per-token: bytes MB/token.
GPT-3 175B (, ):
Per-token: bytes MB/token.
| Model | bytes/token | KV cache at 128K | ||
|---|---|---|---|---|
| Our running model | 512 | 12 | 24 KB | 3 GB |
| GPT-2 Large | 1,280 | 36 | 180 KB | 22 GB |
| LLaMA-7B | 4,096 | 32 | 512 KB | 64 GB |
| LLaMA-65B | 8,192 | 80 | 2.5 MB | 320 GB |
| GPT-3 175B | 12,288 | 96 | 4.5 MB | 576 GB |
The model weights of LLaMA-65B are ~130 GB in fp16 (65 billion parameters 2 bytes). The KV cache at 128K tokens is larger than the model itself. The cache has become the dominant consumer of GPU memory.
The KV Cache vs. Model Weight Ratio
This is worth formalizing. The ratio of KV cache bytes to model weight bytes tells us how much of the GPU’s memory and bandwidth is consumed by the cache versus the model.
Deriving the ratio
Model weight bytes (as we derived):
(This is parameters per layer layers 2 bytes.)
KV cache bytes at context length :
The ratio:
The cancels. One power of cancels. What remains is:
Numerical check with our running model at :
So the KV cache is the model weights. Verifying: . Correct.
At for LLaMA-65B ():
The KV cache is the model weights. We computed 320 GB cache vs. ~130 GB weights earlier — . The small discrepancy is because the actual model includes embedding layers and LM head not counted in our per-layer estimate, but the formula captures the correct order of magnitude.
Interpretation. The ratio tells us:
- At the crossover (), the ratio is exactly 1. KV cache = weights.
- The ratio grows linearly with . Every additional token adds a fixed cost.
- The ratio shrinks inversely with . Wider models have proportionally more weights, so the cache takes longer to overtake them. But it always does eventually.
The Three Levers for Reducing KV Cache
The formula tells us where to push. There are three major levers we will focus on in this series, and many KV cache optimizations pull one or more of them.
Lever 1: Reduce the number of KV heads
If instead of unique KV heads, we use KV heads (sharing each across query heads), the per-token cache becomes:
The reduction factor — by dividing the original by the new — is:
The , , and the 2 all cancel. The reduction is exactly .
With (all queries share one KV head): factor . This is Multi-Query Attention (MQA, Shazeer 2019).
With : factor . This is Grouped-Query Attention (GQA, Ainslie et al. 2023).
Numerical check. MQA on our running model:
Reduction: . Correct.
GQA with :
Reduction: . Correct.
Tradeoff. Fewer KV heads means all query heads in a group must use the same key-value representation. This limits the model’s ability to attend to different features in different heads. The quality cost depends on how redundant the original heads were.
Lever 2: Compress the KV representation
Instead of caching and per head, project the input to a shared low-dimensional latent where . Cache only ; reconstruct per-head K and V from using learned up-projection matrices during attention.
Per-token cache: bytes.
With (matching ), the cache per token per layer is bytes. Compare to MHA: bytes. Reduction: .
For larger models with more heads, the compression ratio improves proportionally. This is Multi-head Latent Attention (MLA, DeepSeek-V2).
Tradeoff. The up-projection matrices (from to per-head K and V) must be applied during every attention step, adding compute. This trades memory for FLOPs — the opposite direction from what FlashAttention does.
Lever 3: Cache fewer tokens
Instead of caching all tokens, cache only the most recent tokens. The total cache is bounded:
This is constant regardless of .
With on our running model:
Compare to full cache at : GB. A reduction.
Tradeoff. Positions more than tokens ago are invisible to the current layer’s attention. Information can propagate further than through multi-layer composition — if token at position attends to token at position , and token attends to token at position , then indirectly accesses through two layers. But direct access is limited to the window.
Composing the levers
These three levers compose cleanly in the simple accounting used here. GQA () + sliding window ():
Compare to vanilla MHA at : GB. Combined reduction: .
Numerical check: the two individual reductions are (from GQA) and (from sliding window). Product: . And . Correct — the reductions compose multiplicatively as claimed.
Why Memory Bandwidth Is the Right Metric
We have used “bytes loaded from HBM” as the primary cost metric throughout this blog. Let us justify this choice with a direct comparison.
Compute time vs. memory time
Consider generating one token at context length with our running model.
Total FLOPs (across all heads and layers, Phase 3 only):
Time if compute-bound (limited by arithmetic throughput, A100 at 312 TFLOPS fp16):
Time if memory-bound (limited by HBM bandwidth, A100 at 2 TB/s):
The ratio:
The memory transfer takes longer than the computation. This is almost exactly the ridge-point ratio of 156 — not a coincidence, since our arithmetic intensity is and the ridge point is 156.
The GPU sits idle 99.3% of the time, waiting for KV cache data to arrive from HBM. The actual per-step time is microseconds (the Roofline model takes the maximum, since the bottleneck determines the runtime).
Does the ratio improve with context?
As increases, both FLOPs and bytes grow proportionally:
The ratio stays constant at . The operation never becomes compute-bound, no matter how long the context.
This is the mathematical inevitability of matrix-vector multiplies: the arithmetic intensity is always 1.0 FLOP/byte in fp16, the hardware’s ridge point is always 156 FLOP/byte, and the gap never closes.
The implication: in the KV-bandwidth-dominant regime, reducing KV cache size can translate into roughly proportional inference speedups. Halve the KV bytes loaded and you can often cut a large part of the per-step latency, though fixed costs such as weight loading still remain.
Connecting to the Papers
Two papers in our collection directly address the bottlenecks we have derived.
FlashAttention (Dao et al., 2022)
FlashAttention solves the memory materialization problem during training. Recall from the “Why Vanilla Attention Breaks” blog that standard attention materializes the score matrix and the attention weight matrix to HBM. These matrices have elements, requiring HBM reads and writes.
FlashAttention avoids this by tiling , , into blocks that fit in SRAM and using the online softmax algorithm — a technique for computing softmax incrementally, block by block, without needing all scores simultaneously. The key insight: you can compute one block of at a time, keeping only a running maximum and running sum in SRAM. No matrix is ever written to HBM.
The IO complexity drops from (standard) to a lower tiled bound that depends on SRAM size (FlashAttention), substantially reducing HBM traffic. The exact constant-factor gain depends on , , , and the implementation, so it is better not to compress it to a single universal number.
What FlashAttention does not do. FlashAttention does not reduce the KV cache during autoregressive inference. During generation, the bottleneck is not materializing and (which are vectors for a single query, not matrices). The bottleneck is loading the KV cache itself. FlashAttention solves a training bottleneck (Axis 4: storage), not an inference bottleneck (Axis 2: KV representation).
GQA (Ainslie et al., 2023)
GQA directly attacks the inference KV bottleneck by pulling Lever 1: reducing the number of KV heads from to . The per-token cache drops by , and inference speed improves proportionally at long contexts.
The paper’s central finding: with KV groups on T5-XXL (which has query heads), quality remains within 0.1 points of full MHA while inference is faster. The quality cost of reducing KV heads is far smaller than the bandwidth saving — exactly because many heads are redundant.
The next blog derives GQA in full detail.
Complementarity
These two papers are complementary:
| FlashAttention | GQA | |
|---|---|---|
| Bottleneck addressed | Training memory | Inference bandwidth |
| Axis modified | Axis 4 (storage) | Axis 2 (KV representation) |
| What changes | How attention is computed | What K/V tensors exist |
| Cache reduction | None | factor |
| Speed improvement | Training 1.5–3× | Inference up to |
They compose cleanly: use FlashAttention during training for memory efficiency, and GQA during inference for bandwidth efficiency. Modern systems (LLaMA 2, Mistral) use both.
Summary
Autoregressive inference is memory-bandwidth-bound, not compute-bound. We derived this from first principles using the Roofline model: in our simplified accounting, every attention operation during token generation behaves like a matrix-vector multiply with arithmetic intensity near 1.0 FLOP/byte, which is far below the A100’s ridge point. In this regime, the GPU spends most of its time waiting for data from HBM rather than doing arithmetic.
The dominant memory load is the KV cache. Its cost per token is bytes in fp16 — a formula that depends only on model width and depth. The KV cache overtakes the model weight load at context length , and beyond this point, per-step latency grows linearly with context. At production scale (LLaMA-65B, 128K tokens), the KV cache reaches 320 GB — 2.5× the model’s own weight memory.
Batching helps compute efficiency but amplifies the memory capacity problem, creating a fundamental tension. Three major levers for reducing the KV cache are: reduce KV heads (MQA/GQA, Lever 1), compress the KV representation (MLA, Lever 2), or cache fewer tokens (sliding window, Lever 3). These levers compose cleanly in the simple accounting used here, and many KV cache papers pull one or more of them.
Previous: What Can We Actually Modify in Attention? Next: Grouped-Query Attention: Fewer KV Heads, Same Quality
Enjoyed this post?
Subscribe to get notified when I publish new posts. No spam, unsubscribe anytime.