6.1.13 · D3Scaling & Efficient Architectures

Worked examples — KV-cache optimization

3,382 words15 min readBack to topic

Before we start, one tiny reminder of the symbols we will lean on, so nobody is left behind:


The scenario matrix

Every KV-cache question is really one of these cells. The examples that follow each declare which cell(s) they cover.

# Case class Concrete trigger Covered by
A Empty / first token () prefill of the very first token, cache starts empty Ex 1
B Growing cache, one step append token , attend over positions Ex 2
C Prefill vs decode asymmetry a whole prompt goes in at once, then decode one-by-one Ex 3
D Zero / degenerate input , or single-head (), or tiny Ex 4
E Limiting behaviour () when does cache memory overtake model weights? Ex 5
F Head-layout variants standard vs MQA vs GQA cache size Ex 6
G Real-world word problem "will a 32 GB GPU serve this?" — deployment sizing Ex 7
H Exam twist sliding window: cache stops growing, find the break-even token Ex 8

We attack them in order. Cells A–C are the mechanics, D–E the edge and limit, F–H the engineering reality.


Example 1 — Cell A: the empty cache, first token

Forecast: Guess now — before we compute anything, how many rows does the cache have? And after one token?

Steps:

  1. Before generation, the cache is empty. Its shape is — literally zero rows. Why this step? Caching is incremental; there is nothing to remember before the first token exists. This is the base case every loop starts from.

  2. Compute the first token's projections: and , each of shape . Why this step? is the embedding of token 1 (a length- vector). Multiplying by the fixed matrix produces this token's key across all 4 heads. Same for value. See the projection step.

  3. Append. Cache goes from to . Why this step? Appending is the whole trick: we grow by exactly one row per token.

  4. Count stored numbers. Two matrices (K and V), each numbers: Why this step? This is the per-token memory formula from the parent note, evaluated at .

Verify: The attention for token 1 must attend only to position 1 — a score (one score per head, one position). With a cache of exactly 1 row that is exactly what we get. Units check: . ✓


Example 2 — Cell B: one growth step, the 3rd token

Forecast: How wide (how many columns) is the score row for token 3?

Steps:

  1. Cache before: (tokens 1,2). Why this step? We only re-read; we never recompute . That is the point of the cache.

  2. Compute the new token's key and value: and , each of shape , where is token 3's embedding. Why this step? Only token 3 is genuinely new, so only its K and V need computing — the reusable "what previous tokens offer" pieces that we will append to the cache.

  3. Compute the new query separately: , also — and we do not cache it. Why this step? encodes "what token 3 is looking for". Every future token brings its own fresh query, and old queries are never re-attended, so caching would waste memory for nothing (this is Mistake 1 in the parent). Keys and values are remembered; queries are always thrown away after one use.

  4. Append K and V: cache becomes . ( is not appended.)

  5. Score shape. For each head, (a vector) dots against 3 cached keys (): Why this step? One row per head (4), one column per attended position (3). The scaling keeps the dot products from blowing up (from scaled dot-product attention).

Figure — KV-cache optimization
Figure 1 — cache append and score fan-out. On the left, the two lavender rows are the frozen cache () that we never recompute; the coral row (, marked NEW) is the single row we just appended, making the cache . On the right, the mint box is the fresh query — trace the three coral arrows: reaches back to each of the 3 cached keys, and the vertical slate arrow (labelled "/ 8") divides by before the butter box collects the result: a score, one row per head, three columns for the three attended positions. Notice has no arrow into the cache — it is used once and discarded, exactly as step 3 argued.

Verify: Look at the figure — the new row (coral) is height-1, and the score arrow fans out to exactly 3 cached columns. Column count = current token index = 3. ✓ And , a clean integer. ✓


Example 3 — Cell C: prefill vs decode

Forecast: Will the cached run be roughly times cheaper, or something in between because of the big prefill chunk?

Steps:

  1. Fix the unit. Projecting one token means multiplying its -vector by each of (each ), which costs multiply-adds. We call that one block. Projecting tokens is independent such products, so it costs exactly blocks — whether done one-at-a-time or fused into a single batched matmul. Why this step? A batched matmul does not do less arithmetic than separate ones — it packs the same token-products into one call for hardware efficiency. So "10 tokens in one matmul" is still 10 blocks of work, not 1. This is why prefill of tokens costs blocks, not a constant.

  2. Naive projection cost. At each token position the naive model recomputes K,V for all positions: blocks. Why this step? Without a cache, generating token treats the sequence as fresh — projections each time.

  3. Cached projection cost. Prefill projects all prompt tokens once (10 blocks, by step 1), then each of the decode steps projects exactly 1 new token (5 blocks): blocks. Why this step? The cache means we never redo the prompt's K,V, and each decode step adds only its single new token.

  4. Speed ratio. . Why this step? This is the concrete cash value of the theoretical "" speed-up — but it lands at 8, not 15, because the naive sum is , not , and the cached cost equals .

Verify: General check: naive , cached , ratio . For : . ✓ Matches step 4. The parent's "" is the big-O scaling; the exact ratio is .


Example 4 — Cell D: degenerate inputs

Forecast: Which of these gives zero bytes, and which gives the smallest non-zero cache?

Steps:

  1. (a) . Total cache . Why this step? Empty sequence → nothing to store. Confirms the formula degrades gracefully; no divide-by-zero, no negative memory.

  2. (b) . Per token bytes. Why this step? A single-head model collapses to just . This is also the MQA per-token figure (one shared K/V) — a preview of Ex 6.

  3. (c) , . Per token bytes. Why this step? Shrinking the head dimension to 1 makes each key/value a single scalar. Still 8 heads, so 8 scalars for K + 8 for V = 16 numbers = 32 bytes.

Verify: (a) . ✓ (b) bytes, non-zero and smallest of the "real" cases per number of heads. (c) bytes is actually the smallest total here. Ordering: . ✓ Every formula stays finite and non-negative under degenerate inputs.


Example 5 — Cell E: the crossover limit ()

Forecast: Thousands of tokens? Millions? Guess the order of magnitude.

Steps:

  1. Per-token full-model cache. bytes KB. Why this step? This is the parent's formula summed over layers, per token. Every new token adds this fixed slab.

  2. Set cache = weights. Solve : Why this step? Cache grows linearly in while weights are constant — so there is always a crossover. Past it, memory is dominated by cache, not the model.

  3. Interpret the limit. As the cache dwarfs everything: . This is why MQA/GQA and sliding windows exist (Ex 6, 8). Why this step? The whole "advanced optimizations" section of the parent is a response to this unbounded growth.

Verify: MB. ✓ Linear-in- growth means the ratio has no finite bound. ✓ (Here MB = bytes, decimal — see Ex 7 for a units clarification.)


Example 6 — Cell F: standard vs MQA vs GQA

Forecast: MQA should be some big divisor smaller. By exactly what factor, and where does GQA land?

Steps:

  1. (a) Standard. bytes KB. Why this step? Every query head keeps its own K and V. This is the baseline the parent's formula gives.

  2. (b) MQA. One shared K/V head: bytes. Why this step? MQA replaces K/V heads with a single one. Reduction .

  3. (c) GQA, . bytes KB. Why this step? GQA keeps one K/V per group. With 8 groups the reduction is — the parent's stated Llama-2 figure.

Figure — KV-cache optimization
Figure 2 — cache size staircase. Three bars, tallest to shortest: the lavender bar is standard multi-head at 32,768 bytes/token/layer (all 64 K/V heads kept); the mint bar is GQA with 8 groups at 4,096 bytes (labelled "8x smaller"); the coral bar is MQA at 512 bytes (labelled "64x smaller"). Read left-to-right and the staircase drops each time we share K/V heads across more queries — the height literally is the memory bill, so a shorter bar is a cheaper cache. GQA sits deliberately between the two extremes: much of MQA's saving, little of its quality loss.

Verify: Standard B; MQA B → ratio . ✓ GQA B → ratio . ✓ GQA sits between MQA and standard, exactly as the figure's staircase shows. ✓


Example 7 — Cell G: real-world deployment sizing

Forecast: With plenty of GPU headroom, is the cache even a problem here — yes or no?

Steps:

  1. Per-user cache (standard). From Ex 5, per-token full-model cache bytes. For 2048 tokens: bytes. In binary MiB (): MiB exactly. Why this step? One user's cache = per-token slab context length. The bytes divide cleanly by , so 72 MiB is exact, not rounded.

  2. 8 users + weights. MiB GiB. Why this step? Concurrent requests each need their own cache; weights are shared once. Total must fit the 32 GiB budget.

  3. Fit check. yes, fits easily. After reserving weights, budget for caches MiB, so Why this step? The cache, not the weights, is the scaling bottleneck for concurrency — this is the core inference-optimization insight.

  4. MQA switch. MQA shrinks K/V by the head factor , so per-user cache MiB. Max users . Why this step? Cutting cache multiplies serveable concurrency by roughly (448 → ~5378). This is why production LLMs use MQA/GQA — see deployment.

Verify: Standard: MiB, and MiB. ✓ Max standard users ; MQA ; ratio . ✓ Units: MiB / (MiB/user) = users, all binary throughout. ✓


Example 8 — Cell H: exam twist, sliding window break-even

Forecast: Does the window cache save memory from token 1, or only after some threshold?

Steps:

  1. Growth laws. Full cache rows (grows forever). Window cache rows . Why this step? Sliding window drops the oldest token once you exceed , so its size plateaus at 512 — constant memory, from the parent's "Sliding Window" section.

  2. First difference. For every both caches hold rows (nothing dropped yet), so they are identical. At the full cache has 513 rows but the window has already evicted token 1 and holds only . So they first differ at . Why this step? Below the window size there is no saving at all; the saving begins exactly one token past . That threshold is the "break-even" the question asks for.

  3. Ratio at . Full rows; window rows. Why this step? At long context the window is smaller here — the tradeoff is losing dependencies older than 512 tokens (relevant to long-horizon decoding where distant context can matter).

  4. Limit behaviour. Window memory is constant while full is linear, so as the ratio : the saving grows without bound. Why this step? This is the whole point of sliding-window attention for 100K+ token contexts — memory stops growing entirely.

Verify: and → first divergence at . ✓ . ✓ Ratio is unbounded as . ✓


Recall Self-check

With-cache projection cost vs naive, exact ratio for length ? ::: (not — that's the big-O). When does KV-cache first differ from a length- window cache? ::: At token . MQA cache reduction factor vs standard multi-head with heads? ::: (one shared K/V head). Why is never cached? ::: Each new token needs a fresh query ("what it's looking for"); old queries are never re-attended. Per-token full-model cache formula? ::: .

Related: 6.3.1-model-quantization shrinks the bytes-per-number factor in every formula above — orthogonal to head-count tricks, and stacks with them.