Pratham Patel
· 32 min read

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:

  • dmodel=512d_\text{model} = 512
  • h=8h = 8 heads
  • dk=dv=64d_k = d_v = 64 (so hdk=512=dmodelh \cdot d_k = 512 = d_\text{model})
  • L=12L = 12 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 tierCapacityBandwidthLatency
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 10×10\times faster than HBM. HBM is roughly 40×40\times faster than CPU DRAM. But SRAM is roughly 2,000×2{,}000\times to 4,000×4{,}000\times smaller than HBM.

What fits where

In our running model, the KV cache for one head, one layer, at context length t=4,096t = 4{,}096 is:

(dk+dv)t2=1284,0962=1,048,576 bytes=1 MB per head per layer(d_k + d_v) \cdot t \cdot 2 = 128 \cdot 4{,}096 \cdot 2 = 1{,}048{,}576 \text{ bytes} = 1 \text{ MB per head per layer}

Across all 8 heads and 12 layers: 8×12×18 \times 12 \times 1 MB =96= 96 MB.

This is 4.8×4.8\times 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 22 TB/s.

Numerical check. At t=512t = 512 tokens: KV cache =8×12×128×512×2=12,582,912= 8 \times 12 \times 128 \times 512 \times 2 = 12{,}582{,}912 bytes 12\approx 12 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 (QKQ K^\top, AVA V), 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 (KcacheqK_\text{cache}^\top q, VcacheaV_\text{cache}^\top a). 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 t+1t+1. All previous tokens 1,2,,t1, 2, \ldots, t 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 xt+1Rdmodelx_{t+1} \in \mathbb{R}^{d_\text{model}} is projected through three weight matrices:

qt+1=xt+1WQi,kt+1=xt+1WKi,vt+1=xt+1WViq_{t+1} = x_{t+1} W_Q^i, \quad k_{t+1} = x_{t+1} W_K^i, \quad v_{t+1} = x_{t+1} W_V^i

Each projection is a matrix-vector multiply. Let us count exactly.

FLOPs for one projection. The output qt+1Rdkq_{t+1} \in \mathbb{R}^{d_k} has dk=64d_k = 64 elements. Each element is a dot product of xt+1R512x_{t+1} \in \mathbb{R}^{512} with one column of WQiR512×64W_Q^i \in \mathbb{R}^{512 \times 64}. A dot product of two dmodeld_\text{model}-dimensional vectors requires dmodeld_\text{model} multiplications and dmodel1d_\text{model} - 1 additions. Using the standard convention of counting each multiply-add as 2 FLOPs:

FLOPs per element=2dmodel=2×512=1,024\text{FLOPs per element} = 2 \cdot d_\text{model} = 2 \times 512 = 1{,}024

There are dk=64d_k = 64 output elements, so:

FLOPs per projection=2dmodeldk=2×512×64=65,536\text{FLOPs per projection} = 2 \cdot d_\text{model} \cdot d_k = 2 \times 512 \times 64 = 65{,}536

Three projections (Q, K, V) per head, h=8h = 8 heads:

FLOPsproj=h32dmodeldk=8×3×65,536=1,572,864\text{FLOPs}_\text{proj} = h \cdot 3 \cdot 2 \cdot d_\text{model} \cdot d_k = 8 \times 3 \times 65{,}536 = 1{,}572{,}864

Bytes loaded from HBM. To compute one projection, we must load the weight matrix WQiRdmodel×dkW_Q^i \in \mathbb{R}^{d_\text{model} \times d_k} from HBM. It has dmodel×dk=512×64=32,768d_\text{model} \times d_k = 512 \times 64 = 32{,}768 elements. In fp16 (2 bytes per element):

bytes per weight matrix=32,768×2=65,536 bytes=64 KB\text{bytes per weight matrix} = 32{,}768 \times 2 = 65{,}536 \text{ bytes} = 64 \text{ KB}

We also load the input vector xt+1x_{t+1} (512 elements, 1 KB) — this is tiny and shared across all projections, so we can ignore it. Three matrices per head, h=8h = 8 heads:

bytesproj=h3dmodeldk2=8×3×65,536=1,572,864 bytes1.5 MB\text{bytes}_\text{proj} = h \cdot 3 \cdot d_\text{model} \cdot d_k \cdot 2 = 8 \times 3 \times 65{,}536 = 1{,}572{,}864 \text{ bytes} \approx 1.5 \text{ MB}

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.

arithmetic intensity=FLOPsbytes loaded from HBM\text{arithmetic intensity} = \frac{\text{FLOPs}}{\text{bytes loaded from HBM}}

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:

ridge point=peak compute (FLOP/s)peak memory bandwidth (bytes/s)\text{ridge point} = \frac{\text{peak compute (FLOP/s)}}{\text{peak memory bandwidth (bytes/s)}}

For the A100 GPU:

ridge point=312×1012 FLOP/s2×1012 bytes/s=156 FLOP/byte\text{ridge point} = \frac{312 \times 10^{12} \text{ FLOP/s}}{2 \times 10^{12} \text{ bytes/s}} = 156 \text{ FLOP/byte}

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:

arithmetic intensityproj=1,572,864 FLOPs1,572,864 bytes=1.0 FLOP/byte\text{arithmetic intensity}_\text{proj} = \frac{1{,}572{,}864 \text{ FLOPs}}{1{,}572{,}864 \text{ bytes}} = 1.0 \text{ FLOP/byte}

This is 156×156\times 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 (m×n)(n×1)(m \times n) \cdot (n \times 1) performs 2mn2mn FLOPs and loads mn2mn \cdot 2 bytes (the matrix in fp16) plus n2n \cdot 2 bytes (the vector, negligible for large mm). The arithmetic intensity is:

2mn2mn=1.0 FLOP/byte\frac{2mn}{2mn} = 1.0 \text{ FLOP/byte}

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: Q=XWQQ = X W_Q where XRN×dmodelX \in \mathbb{R}^{N \times d_\text{model}} and WQRdmodel×dkW_Q \in \mathbb{R}^{d_\text{model} \times d_k}. The FLOPs are 2Ndmodeldk2 \cdot N \cdot d_\text{model} \cdot d_k. The bytes loaded are the weight matrix (dmodeldk2d_\text{model} \cdot d_k \cdot 2 bytes) plus the input (Ndmodel2N \cdot d_\text{model} \cdot 2 bytes). For large NN, the arithmetic intensity approaches:

2Ndmodeldkdmodeldk2+Ndmodel22Ndk2N=dk=64 FLOP/byte\frac{2 \cdot N \cdot d_\text{model} \cdot d_k}{d_\text{model} \cdot d_k \cdot 2 + N \cdot d_\text{model} \cdot 2} \approx \frac{2 N d_k}{2N} = d_k = 64 \text{ FLOP/byte}

At 64 FLOP/byte, training is still below the ridge point but much closer — especially with larger batch sizes that push the effective NN higher. The key difference: training’s arithmetic intensity scales with NN (batch ×\times sequence length), while inference’s arithmetic intensity is stuck at 1.0 regardless of tt.

Phase 2: Append to the KV Cache

The new kt+1k_{t+1} and vt+1v_{t+1} vectors are appended to the cache in HBM.

Each vector has dk=64d_k = 64 elements =128= 128 bytes in fp16. Two vectors (K and V) per head, h=8h = 8 heads:

bytes written=h2dk2=8×2×64×2=2,048 bytes=2 KB\text{bytes written} = h \cdot 2 \cdot d_k \cdot 2 = 8 \times 2 \times 64 \times 2 = 2{,}048 \text{ bytes} = 2 \text{ KB}

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 qt+1q_{t+1} must attend to all t+1t+1 cached keys, produce a softmax distribution, and use it to weight all t+1t+1 cached values.

We trace each sub-step for one head in full detail.

Step 3a: Score computation.

s=Kcacheqt+1Rt+1s = K_\text{cache}^\top q_{t+1} \in \mathbb{R}^{t+1}

This is the matrix-vector product of KcacheR(t+1)×dkK_\text{cache} \in \mathbb{R}^{(t+1) \times d_k} transposed with qt+1Rdkq_{t+1} \in \mathbb{R}^{d_k}.

Equivalently, each score sj=kjqt+1s_j = k_j^\top q_{t+1} is a dot product of two dkd_k-dimensional vectors. There are t+1t+1 such dot products.

FLOPs: Each dot product costs 2dk2 d_k FLOPs. There are t+1t+1 of them:

FLOPs3a=2dk(t+1)=2×64×(t+1)=128(t+1)\text{FLOPs}_{3a} = 2 \cdot d_k \cdot (t+1) = 2 \times 64 \times (t+1) = 128(t+1)

Bytes loaded: The entire KcacheK_\text{cache} must be read from HBM — (t+1)×dk(t+1) \times d_k elements, each 2 bytes:

bytes3a=(t+1)dk2=(t+1)×64×2=128(t+1) bytes\text{bytes}_{3a} = (t+1) \cdot d_k \cdot 2 = (t+1) \times 64 \times 2 = 128(t+1) \text{ bytes}

Plus the query vector qt+1q_{t+1}: dk×2=128d_k \times 2 = 128 bytes. This is negligible for large tt.

Arithmetic intensity:

128(t+1)128(t+1)=1.0 FLOP/byte\frac{128(t+1)}{128(t+1)} = 1.0 \text{ FLOP/byte}

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 dk=64=8\sqrt{d_k} = \sqrt{64} = 8: that is t+1t+1 divisions. Then apply the softmax, which requires three passes over the t+1t+1 scores:

  1. Find the maximum: tt comparisons
  2. Compute esjsmaxe^{s_j - s_\text{max}} for each jj: t+1t+1 exponentiations
  3. Normalize (divide by sum): t+1t+1 additions to compute the sum, then t+1t+1 divisions

Total: roughly 4(t+1)4(t+1) FLOPs. The bytes involved are the score vector ss which was just computed — it likely still resides in SRAM from Step 3a. If we must read it from HBM: 2(t+1)2(t+1) 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.

ot+1=Vcacheat+1Rdvo_{t+1} = V_\text{cache}^\top \cdot a_{t+1} \in \mathbb{R}^{d_v}

where at+1=softmax(s/dk)Rt+1a_{t+1} = \text{softmax}(s / \sqrt{d_k}) \in \mathbb{R}^{t+1} is the attention weight vector.

This is another matrix-vector multiply: VcacheR(t+1)×dvV_\text{cache} \in \mathbb{R}^{(t+1) \times d_v} transposed with at+1Rt+1a_{t+1} \in \mathbb{R}^{t+1}.

Equivalently, this is a weighted sum of the t+1t+1 value vectors: ot+1=j=1t+1ajvjo_{t+1} = \sum_{j=1}^{t+1} a_j \cdot v_j. Each ajvja_j \cdot v_j costs dvd_v multiplications, and summing costs tdvt \cdot d_v additions.

FLOPs:

FLOPs3c=2dv(t+1)=128(t+1)\text{FLOPs}_{3c} = 2 \cdot d_v \cdot (t+1) = 128(t+1)

Bytes loaded: The full VcacheV_\text{cache}: (t+1)dv2=128(t+1)(t+1) \cdot d_v \cdot 2 = 128(t+1) bytes.

Arithmetic intensity: 128(t+1)/128(t+1)=1.0128(t+1) / 128(t+1) = 1.0 FLOP/byte.

Total for Phase 3, One Head

Summing Steps 3a, 3b, and 3c:

FLOPsattn=128(t+1)+4(t+1)+128(t+1)=260(t+1)\text{FLOPs}_\text{attn} = 128(t+1) + 4(t+1) + 128(t+1) = 260(t+1) bytesattn=128(t+1)+128(t+1)=256(t+1)\text{bytes}_\text{attn} = 128(t+1) + 128(t+1) = 256(t+1)

(The softmax bytes are negligible since the data is likely already in SRAM.)

arithmetic intensityattn=260(t+1)256(t+1)1.0 FLOP/byte\text{arithmetic intensity}_\text{attn} = \frac{260(t+1)}{256(t+1)} \approx 1.0 \text{ FLOP/byte}

The arithmetic intensity stays near 1.0 as tt 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 tt.

Scaling to All Heads and All Layers

Each of the h=8h = 8 heads performs the same computation with different weight matrices. Each of the L=12L = 12 layers performs the same computation with different parameters. Since each head has its own KV cache, the total bytes read are:

bytesattn, total=Lh256(t+1)=12×8×256×(t+1)=24,576(t+1)\text{bytes}_\text{attn, total} = L \cdot h \cdot 256(t+1) = 12 \times 8 \times 256 \times (t+1) = 24{,}576 \cdot (t+1)

Numerical check. At t+1=4,096t+1 = 4{,}096:

24,576×4,096=100,663,296 bytes96 MB24{,}576 \times 4{,}096 = 100{,}663{,}296 \text{ bytes} \approx 96 \text{ MB}

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:

96×1062×1012=48×106 s=48 microseconds\frac{96 \times 10^6}{2 \times 10^{12}} = 48 \times 10^{-6} \text{ s} = 48 \text{ microseconds}

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 t+1=128,000t+1 = 128{,}000:

24,576×128,000=3,145,728,000 bytes3 GB24{,}576 \times 128{,}000 = 3{,}145{,}728{,}000 \text{ bytes} \approx 3 \text{ GB}

At 2 TB/s: 3×109/(2×1012)=1.53 \times 10^9 / (2 \times 10^{12}) = 1.5 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 tt).

A transformer layer has:

  • Attention projections: WQ,WK,WVRdmodel×dmodelW_Q, W_K, W_V \in \mathbb{R}^{d_\text{model} \times d_\text{model}} (each is hh head matrices concatenated) and WORdmodel×dmodelW_O \in \mathbb{R}^{d_\text{model} \times d_\text{model}}. Total: 4dmodel24 \cdot d_\text{model}^2 parameters.
  • FFN: two matrices, W1Rdmodel×4dmodelW_1 \in \mathbb{R}^{d_\text{model} \times 4d_\text{model}} and W2R4dmodel×dmodelW_2 \in \mathbb{R}^{4d_\text{model} \times d_\text{model}}. Total: 2×4×dmodel2=8dmodel22 \times 4 \times d_\text{model}^2 = 8 \cdot d_\text{model}^2 parameters.
  • LayerNorm parameters: negligible (2×dmodel2 \times d_\text{model} per norm, two norms per layer).

Total per layer: (4+8)dmodel2=12dmodel2(4 + 8) \cdot d_\text{model}^2 = 12 \cdot d_\text{model}^2 parameters. In fp16:

weight bytes per layer=12dmodel22=24dmodel2\text{weight bytes per layer} = 12 \cdot d_\text{model}^2 \cdot 2 = 24 \cdot d_\text{model}^2

Numerical check: 24×5122=24×262,144=6,291,45624 \times 512^2 = 24 \times 262{,}144 = 6{,}291{,}456 bytes 6\approx 6 MB per layer.

Across L=12L = 12 layers:

total weight bytes=L24dmodel2=12×6,291,456=75,497,47272 MB\text{total weight bytes} = L \cdot 24 \cdot d_\text{model}^2 = 12 \times 6{,}291{,}456 = 75{,}497{,}472 \approx 72 \text{ MB}

(The slight discrepancy from 12×6=7212 \times 6 = 72 is because we are rounding. The exact value is 12×24×5122=75,497,47212 \times 24 \times 512^2 = 75{,}497{,}472 bytes =72= 72 MB.)

Category 2: KV cache bytes (grows linearly with tt):

KV bytes=Lh(dk+dv)2(t+1)=24,576(t+1)\text{KV bytes} = L \cdot h \cdot (d_k + d_v) \cdot 2 \cdot (t+1) = 24{,}576 \cdot (t+1)

The total and the crossover

Bstep(t)=24Ldmodel2weights (fixed)+Lh(dk+dv)2tKV cache (grows with t)\boxed{B_\text{step}(t) = \underbrace{24 L \cdot d_\text{model}^2}_{\text{weights (fixed)}} + \underbrace{L \cdot h \cdot (d_k + d_v) \cdot 2 \cdot t}_{\text{KV cache (grows with } t)}}

Per-step time at HBM bandwidth WW:

Tstep(t)=Bstep(t)WT_\text{step}(t) = \frac{B_\text{step}(t)}{W}

The crossover. KV cache reads exceed model weight reads when:

Lh(dk+dv)2t>24Ldmodel2L \cdot h \cdot (d_k + d_v) \cdot 2 \cdot t > 24 L \cdot d_\text{model}^2

The LL cancels from both sides, leaving:

h(dk+dv)2t>24dmodel2h \cdot (d_k + d_v) \cdot 2 \cdot t > 24 \cdot d_\text{model}^2

Since dk=dvd_k = d_v and hdk=dmodelh \cdot d_k = d_\text{model}:

h2dk2t>24dmodel2h \cdot 2 d_k \cdot 2 \cdot t > 24 \cdot d_\text{model}^2 4dmodelt>24dmodel24 \cdot d_\text{model} \cdot t > 24 \cdot d_\text{model}^2 t>6dmodelt > 6 \cdot d_\text{model}

Numerical check: 6×512=3,0726 \times 512 = 3{,}072. So t3,072t^* \approx 3{,}072 tokens.

Let us verify: 24,576×3,072=75,497,47224{,}576 \times 3{,}072 = 75{,}497{,}472 bytes =72= 72 MB \approx weight bytes. Correct.

This is a remarkably clean result: the crossover occurs at approximately t=6dmodelt^* = 6 \cdot d_\text{model}, independent of hh, dkd_k, or LL.

Interpretation. For our small model (dmodel=512d_\text{model} = 512), the crossover is at ~3K tokens. For LLaMA-7B (dmodel=4,096d_\text{model} = 4{,}096), the crossover is at 6×4,096=24,5766 \times 4{,}096 = 24{,}576 tokens. For GPT-3 (dmodel=12,288d_\text{model} = 12{,}288), 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 ttWeight bytesKV cache bytesDominant load
51272 MB12 MBWeights
1,02472 MB24 MBWeights
2,04872 MB48 MBWeights
3,07272 MB72 MBTie
4,09672 MB96 MBKV cache
8,19272 MB192 MBKV cache
16,38472 MB384 MBKV 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 W=2W = 2 TB/s:

Tstep(t)=Bstep(t)W=72×106+24,576t2×1012T_\text{step}(t) = \frac{B_\text{step}(t)}{W} = \frac{72 \times 10^6 + 24{,}576 \cdot t}{2 \times 10^{12}}

Numerical check at t=4,096t = 4{,}096:

Tstep=72×106+100,663,2962×1012=172,663,2962×1012=86.3 microsecondsT_\text{step} = \frac{72 \times 10^6 + 100{,}663{,}296}{2 \times 10^{12}} = \frac{172{,}663{,}296}{2 \times 10^{12}} = 86.3 \text{ microseconds}

At t=128,000t = 128{,}000:

Tstep=72×106+3,145,728,0002×1012=3,217,728,0002×1012=1.61 msT_\text{step} = \frac{72 \times 10^6 + 3{,}145{,}728{,}000}{2 \times 10^{12}} = \frac{3{,}217{,}728{,}000}{2 \times 10^{12}} = 1.61 \text{ ms}

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 BB, Phase 1 stays the same: the weight matrices are loaded once and applied to BB input vectors. This is now a matrix-matrix multiply — WQXbatchW_Q \cdot X_\text{batch}^\top where XbatchRB×dmodelX_\text{batch} \in \mathbb{R}^{B \times d_\text{model}}. The FLOPs scale by BB, the weight bytes stay constant, so the arithmetic intensity becomes BB FLOP/byte. At B=156B = 156, 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 KcacheK_\text{cache} for request 1 is different from the KcacheK_\text{cache} for request 2 (they are answering different prompts). So the bytes loaded in Phase 3 are:

bytes3,batched=BLh(dk+dv)2t=B24,576t\text{bytes}_{3, \text{batched}} = B \cdot L \cdot h \cdot (d_k + d_v) \cdot 2 \cdot t = B \cdot 24{,}576 \cdot t

The FLOPs also scale by BB. Each request’s query attends to its own cache — there is no sharing. So the arithmetic intensity of Phase 3 remains:

B260(t+1)B256(t+1)1.0 FLOP/byte\frac{B \cdot 260(t+1)}{B \cdot 256(t+1)} \approx 1.0 \text{ FLOP/byte}

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 BB.

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 t=4,096t = 4{,}096, each request’s KV cache is 96 MB. With B=156B = 156:

156×96 MB=14,976 MB15 GB156 \times 96 \text{ MB} = 14{,}976 \text{ MB} \approx 15 \text{ GB}

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 60,000/9662560{,}000 / 96 \approx 625 concurrent requests at 4K context.

Now scale to t=128,000t = 128{,}000: each cache is 3 GB. Maximum concurrent requests: 60,000/3,0002060{,}000 / 3{,}000 \approx 20.

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:

Batching cures the compute problem but amplifies the memory capacity problem.\boxed{\text{Batching cures the compute problem but amplifies the memory capacity problem.}}

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 \ell, head ii, for one token at position tt, the KV cache stores:

  • One key vector: kt(,i)Rdkk_t^{(\ell, i)} \in \mathbb{R}^{d_k}
  • One value vector: vt(,i)Rdvv_t^{(\ell, i)} \in \mathbb{R}^{d_v}

In fp16, each element is 2 bytes. So the cache for one token, one head, one layer is:

bytes1,1,1=(dk+dv)2\text{bytes}_{1,1,1} = (d_k + d_v) \cdot 2

With dk=dv=64d_k = d_v = 64: (64+64)×2=256(64 + 64) \times 2 = 256 bytes.

Numerical check: 256 bytes stores two 64-element fp16 vectors. 64×2=12864 \times 2 = 128 bytes per vector, two vectors: 128+128=256128 + 128 = 256. Correct.

Summing over heads

There are hh heads per layer, each with its own K and V:

bytes1,all heads,1=h(dk+dv)2\text{bytes}_{1,\text{all heads},1} = h \cdot (d_k + d_v) \cdot 2

With h=8h = 8: 8×128×2=2,0488 \times 128 \times 2 = 2{,}048 bytes =2= 2 KB per token per layer.

Summing over layers

There are LL layers, each with its own attention:

bytes per token=Lh(dk+dv)2\text{bytes per token} = L \cdot h \cdot (d_k + d_v) \cdot 2

With L=12L = 12: 12×2,048=24,57612 \times 2{,}048 = 24{,}576 bytes =24= 24 KB per token.

Simplification using hdk=dmodelh \cdot d_k = d_\text{model}

In all standard architectures, the head dimension dkd_k is chosen so that hdk=dmodelh \cdot d_k = d_\text{model}. This is not a constraint — it is a design convention that keeps the total width constant. Using this identity:

h(dk+dv)=h2dk=2hdk=2dmodelh \cdot (d_k + d_v) = h \cdot 2 d_k = 2 \cdot h \cdot d_k = 2 \cdot d_\text{model}

Substituting:

bytes per token=L2dmodel2=4Ldmodel\text{bytes per token} = L \cdot 2 \cdot d_\text{model} \cdot 2 = 4 \cdot L \cdot d_\text{model} KV cache bytes per token=4Ldmodel\boxed{\text{KV cache bytes per token} = 4 \cdot L \cdot d_\text{model}}

Numerical check: 4×12×512=24,5764 \times 12 \times 512 = 24{,}576 bytes =24= 24 KB. Matches.

This formula reveals that the KV cache cost per token depends on exactly two hyperparameters: model depth LL and model width dmodeld_\text{model}. It does not depend on hh or dkd_k individually — only on their product dmodeld_\text{model}. Doubling the number of heads while halving dkd_k (keeping dmodeld_\text{model} constant) does not change the KV cache size at all.

Scaling to real models

The total KV cache for a sequence of length tt is:

total KV bytes=4Ldmodelt\text{total KV bytes} = 4 \cdot L \cdot d_\text{model} \cdot t

Let us compute this for production models at t=128,000t = 128{,}000 tokens:

GPT-2 Large (dmodel=1,280d_\text{model} = 1{,}280, L=36L = 36):

4×36×1,280×128,000=23,592,960,00022 GB4 \times 36 \times 1{,}280 \times 128{,}000 = 23{,}592{,}960{,}000 \approx 22 \text{ GB}

Numerical check of per-token cost: 4×36×1,280=184,3204 \times 36 \times 1{,}280 = 184{,}320 bytes 180\approx 180 KB/token. 180×128,000=23,040,000180 \times 128{,}000 = 23{,}040{,}000 KB 22\approx 22 GB. Consistent.

LLaMA-7B (dmodel=4,096d_\text{model} = 4{,}096, L=32L = 32):

4×32×4,096×128,000=67,108,864,00064 GB4 \times 32 \times 4{,}096 \times 128{,}000 = 67{,}108{,}864{,}000 \approx 64 \text{ GB}

Per-token: 4×32×4,096=524,2884 \times 32 \times 4{,}096 = 524{,}288 bytes =512= 512 KB/token.

LLaMA-65B (dmodel=8,192d_\text{model} = 8{,}192, L=80L = 80):

4×80×8,192×128,000=335,544,320,000320 GB4 \times 80 \times 8{,}192 \times 128{,}000 = 335{,}544{,}320{,}000 \approx 320 \text{ GB}

Per-token: 4×80×8,192=2,621,4404 \times 80 \times 8{,}192 = 2{,}621{,}440 bytes 2.5\approx 2.5 MB/token.

GPT-3 175B (dmodel=12,288d_\text{model} = 12{,}288, L=96L = 96):

4×96×12,288×128,000=603,979,776,000576 GB4 \times 96 \times 12{,}288 \times 128{,}000 = 603{,}979{,}776{,}000 \approx 576 \text{ GB}

Per-token: 4×96×12,288=4,718,5924 \times 96 \times 12{,}288 = 4{,}718{,}592 bytes 4.5\approx 4.5 MB/token.

Modeldmodeld_\text{model}LLbytes/tokenKV cache at 128K
Our running model5121224 KB3 GB
GPT-2 Large1,28036180 KB22 GB
LLaMA-7B4,09632512 KB64 GB
LLaMA-65B8,192802.5 MB320 GB
GPT-3 175B12,288964.5 MB576 GB

The model weights of LLaMA-65B are ~130 GB in fp16 (65 billion parameters ×\times 2 bytes). The KV cache at 128K tokens is 320/1302.5×320 / 130 \approx 2.5\times 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):

weight bytes=24Ldmodel2\text{weight bytes} = 24 \cdot L \cdot d_\text{model}^2

(This is 12dmodel212 \cdot d_\text{model}^2 parameters per layer ×\times LL layers ×\times 2 bytes.)

KV cache bytes at context length tt:

KV bytes=4Ldmodelt\text{KV bytes} = 4 \cdot L \cdot d_\text{model} \cdot t

The ratio:

KV bytesweight bytes=4Ldmodelt24Ldmodel2\frac{\text{KV bytes}}{\text{weight bytes}} = \frac{4 \cdot L \cdot d_\text{model} \cdot t}{24 \cdot L \cdot d_\text{model}^2}

The LL cancels. One power of dmodeld_\text{model} cancels. What remains is:

KV bytesweight bytes=t6dmodel\boxed{\frac{\text{KV bytes}}{\text{weight bytes}} = \frac{t}{6 \cdot d_\text{model}}}

Numerical check with our running model at t=4,096t = 4{,}096:

4,0966×512=4,0963,072=1.33\frac{4{,}096}{6 \times 512} = \frac{4{,}096}{3{,}072} = 1.33

So the KV cache is 1.33×1.33\times the model weights. Verifying: 96 MB/72 MB=1.3396 \text{ MB} / 72 \text{ MB} = 1.33. Correct.

At t=128,000t = 128{,}000 for LLaMA-65B (dmodel=8,192d_\text{model} = 8{,}192):

128,0006×8,192=128,00049,152=2.60\frac{128{,}000}{6 \times 8{,}192} = \frac{128{,}000}{49{,}152} = 2.60

The KV cache is 2.6×2.6\times the model weights. We computed 320 GB cache vs. ~130 GB weights earlier — 320/1302.46320/130 \approx 2.46. The small discrepancy is because the actual model includes embedding layers and LM head not counted in our 12dmodel212 d_\text{model}^2 per-layer estimate, but the formula captures the correct order of magnitude.

Interpretation. The ratio t/(6dmodel)t / (6 \cdot d_\text{model}) tells us:

  • At the crossover (t=6dmodelt = 6 \cdot d_\text{model}), the ratio is exactly 1. KV cache = weights.
  • The ratio grows linearly with tt. Every additional token adds a fixed cost.
  • The ratio shrinks inversely with dmodeld_\text{model}. 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 bytes/token=Lh(dk+dv)2\text{bytes/token} = L \cdot h \cdot (d_k + d_v) \cdot 2 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 hh unique KV heads, we use g<hg < h KV heads (sharing each across h/gh/g query heads), the per-token cache becomes:

bytes/token=Lg(dk+dv)2\text{bytes/token} = L \cdot g \cdot (d_k + d_v) \cdot 2

The reduction factor — by dividing the original by the new — is:

Lh(dk+dv)2Lg(dk+dv)2=hg\frac{L \cdot h \cdot (d_k + d_v) \cdot 2}{L \cdot g \cdot (d_k + d_v) \cdot 2} = \frac{h}{g}

The LL, (dk+dv)(d_k + d_v), and the 2 all cancel. The reduction is exactly h/gh/g.

With g=1g = 1 (all queries share one KV head): factor =h=8×= h = 8\times. This is Multi-Query Attention (MQA, Shazeer 2019).

With g=2g = 2: factor =4×= 4\times. This is Grouped-Query Attention (GQA, Ainslie et al. 2023).

Numerical check. MQA on our running model:

12×1×128×2=3,072 bytes/token=3 KB12 \times 1 \times 128 \times 2 = 3{,}072 \text{ bytes/token} = 3 \text{ KB}

Reduction: 24,576/3,072=8=h24{,}576 / 3{,}072 = 8 = h. Correct.

GQA with g=2g = 2:

12×2×128×2=6,144 bytes/token=6 KB12 \times 2 \times 128 \times 2 = 6{,}144 \text{ bytes/token} = 6 \text{ KB}

Reduction: 24,576/6,144=4=h/g=8/224{,}576 / 6{,}144 = 4 = h/g = 8/2. 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 ktRdkk_t \in \mathbb{R}^{d_k} and vtRdvv_t \in \mathbb{R}^{d_v} per head, project the input to a shared low-dimensional latent ctRdcc_t \in \mathbb{R}^{d_c} where dch(dk+dv)d_c \ll h \cdot (d_k + d_v). Cache only ctc_t; reconstruct per-head K and V from ctc_t using learned up-projection matrices during attention.

Per-token cache: Ldc2L \cdot d_c \cdot 2 bytes.

With dc=512d_c = 512 (matching dmodeld_\text{model}), the cache per token per layer is 512×2=1,024512 \times 2 = 1{,}024 bytes. Compare to MHA: h(dk+dv)2=8×128×2=2,048h \cdot (d_k + d_v) \cdot 2 = 8 \times 128 \times 2 = 2{,}048 bytes. Reduction: 2×2\times.

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 ctc_t 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 tt tokens, cache only the most recent ww tokens. The total cache is bounded:

cache bytes=Lh(dk+dv)2w\text{cache bytes} = L \cdot h \cdot (d_k + d_v) \cdot 2 \cdot w

This is constant regardless of tt.

With w=512w = 512 on our running model:

12×8×128×2×512=12,582,912 bytes=12 MB12 \times 8 \times 128 \times 2 \times 512 = 12{,}582{,}912 \text{ bytes} = 12 \text{ MB}

Compare to full cache at t=128,000t = 128{,}000: 33 GB. A 250×250\times reduction.

Tradeoff. Positions more than ww tokens ago are invisible to the current layer’s attention. Information can propagate further than ww through multi-layer composition — if token AA at position ii attends to token BB at position iw/2i - w/2, and token BB attends to token CC at position iwi - w, then AA indirectly accesses CC 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 (g=2g = 2) + sliding window (w=512w = 512):

12×2×128×2×512=3,145,728 bytes=3 MB (fixed)12 \times 2 \times 128 \times 2 \times 512 = 3{,}145{,}728 \text{ bytes} = 3 \text{ MB (fixed)}

Compare to vanilla MHA at t=128,000t = 128{,}000: 3\approx 3 GB. Combined reduction: 1,000×1{,}000\times.

Numerical check: the two individual reductions are h/g=4×h/g = 4\times (from GQA) and 128,000/512=250×128{,}000 / 512 = 250\times (from sliding window). Product: 4×250=1,0004 \times 250 = 1{,}000. And 3,000/3=1,0003{,}000 / 3 = 1{,}000. 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 t=4,096t = 4{,}096 with our running model.

Total FLOPs (across all heads and layers, Phase 3 only):

FLOPstotal=Lh260(t+1)=12×8×260×4,096=102,236,160100 million\text{FLOPs}_\text{total} = L \cdot h \cdot 260(t+1) = 12 \times 8 \times 260 \times 4{,}096 = 102{,}236{,}160 \approx 100 \text{ million}

Time if compute-bound (limited by arithmetic throughput, A100 at 312 TFLOPS fp16):

Tcompute=100×106312×1012=0.32 microsecondsT_\text{compute} = \frac{100 \times 10^6}{312 \times 10^{12}} = 0.32 \text{ microseconds}

Time if memory-bound (limited by HBM bandwidth, A100 at 2 TB/s):

Tmemory=96×1062×1012=48 microsecondsT_\text{memory} = \frac{96 \times 10^6}{2 \times 10^{12}} = 48 \text{ microseconds}

The ratio:

TmemoryTcompute=480.32=150\frac{T_\text{memory}}{T_\text{compute}} = \frac{48}{0.32} = 150

The memory transfer takes 150×150\times longer than the computation. This is almost exactly the ridge-point ratio of 156 — not a coincidence, since our arithmetic intensity is 1.0\approx 1.0 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 Tstep=max(Tcompute,Tmemory)=48T_\text{step} = \max(T_\text{compute}, T_\text{memory}) = 48 microseconds (the Roofline model takes the maximum, since the bottleneck determines the runtime).

Does the ratio improve with context?

As tt increases, both FLOPs and bytes grow proportionally:

FLOPst,bytest\text{FLOPs} \propto t, \quad \text{bytes} \propto t

The ratio Tmemory/TcomputeT_\text{memory} / T_\text{compute} stays constant at 150\approx 150. 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 O(N2)O(N^2) memory materialization problem during training. Recall from the “Why Vanilla Attention Breaks” blog that standard attention materializes the N×NN \times N score matrix S=QKS = Q K^\top and the attention weight matrix P=softmax(S)P = \text{softmax}(S) to HBM. These matrices have O(N2)O(N^2) elements, requiring O(N2)O(N^2) HBM reads and writes.

FlashAttention avoids this by tiling QQ, KK, VV 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 softmax(QK)V\text{softmax}(Q K^\top) V one block of K,VK, V at a time, keeping only a running maximum and running sum in SRAM. No N×NN \times N matrix is ever written to HBM.

The IO complexity drops from Θ(Nd+N2)\Theta(Nd + N^2) (standard) to a lower tiled bound that depends on SRAM size MM (FlashAttention), substantially reducing HBM traffic. The exact constant-factor gain depends on NN, dd, MM, 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 SS and PP (which are 1×t1 \times t vectors for a single query, not N×NN \times N 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 hh to gg. The per-token cache drops by h/gh/g, and inference speed improves proportionally at long contexts.

The paper’s central finding: with g=8g = 8 KV groups on T5-XXL (which has h=64h = 64 query heads), quality remains within 0.1 points of full MHA while inference is 5.4×5.4\times 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:

FlashAttentionGQA
Bottleneck addressedTraining memoryInference bandwidth
Axis modifiedAxis 4 (storage)Axis 2 (KV representation)
What changesHow attention is computedWhat K/V tensors exist
Cache reductionNoneh/gh/g factor
Speed improvementTraining 1.5–3×Inference up to h/gh/g

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 4dmodelL4 \cdot d_\text{model} \cdot L bytes in fp16 — a formula that depends only on model width and depth. The KV cache overtakes the model weight load at context length t=6dmodelt^* = 6 \cdot d_\text{model}, 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.