6.1.13Scaling & Efficient Architectures

KV-cache optimization

2,881 words13 min readdifficulty · medium3 backlinks

Why We Need It: Derivation from First Principles

The Naive Attention Cost

In multi-head self-attention, for sequence length LL, we compute:

Attention(Q,K,V)=softmax(QKTdk)V\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V

Step-by-step cost analysis:

  1. During training (teacher forcing): We have all LL tokens at once.

    • Q,K,VRL×dQ, K, V \in \mathbb{R}^{L \times d} are computed once
    • Attention matrix: O(L2d)O(L^2 d) operations
    • Total per layer: O(L2d+Ld2)O(L^2 d + Ld^2)
  2. During autoregressive generation (the problem):

    • Generate token at position t=1t=1: compute K1,V1K_1, V_1
    • Generate token at position t=2t=2: compute Q2Q_2 (new), but recompute K1,V1,K2,V2K_1, V_1, K_2, V_2
    • Generate token at position t=Lt=L: recompute K1,,KLK_1, \ldots, K_L and V1,,VLV_1, \ldots, V_L

Why this step? Each generation step only adds one new token, but without caching, the model treats it as a fresh sequence and recomputes projections for all previous tokens.

Total naive cost for generating LL tokens:

t=1LO(td2)=O(L2d2)\sum_{t=1}^{L} O(t \cdot d^2) = O(L^2 d^2)

This is quadratic in sequence length just for the projection overhead, before even doing attention!

The KV-Cache Solution

Derivation of savings:

With cache: t=1LO(d2)=O(Ld2)Without cache: t=1LO(td2)=O(L2d2)Speedup: L2d2Ld2=L\begin{align} \text{With cache: } & \sum_{t=1}^{L} O(d^2) = O(Ld^2) \\ \text{Without cache: } & \sum_{t=1}^{L} O(td^2) = O(L^2 d^2) \\ \text{Speedup: } & \frac{L^2 d^2}{Ld^2} = L \end{align}

Why this works: The causality mask in attention ensures token tt only attends to positions t\leq t. Since Ki,ViK_i, V_i for i<ti < t are fixed (the model is frozen during generation), their values never change—perfect for caching.

Full-model cache (across all layers) = 2×nlayers×L×h×dk×precision2 \times n_{\text{layers}} \times L \times h \times d_k \times \text{precision}.

Example: GPT-3 style model (nlayers=96n_{\text{layers}}=96, h=96h=96 heads, dk=128d_k=128, fp16):

  • Per token, per layer: 2×96×128×2=49,152 bytes48 KB2 \times 96 \times 128 \times 2 = 49{,}152 \text{ bytes} \approx 48\text{ KB}
  • Per token, all 96 layers: 96×48 KB=2×96×96×128×2 bytes4.7 MB96 \times 48\text{ KB} = 2 \times 96 \times 96 \times 128 \times 2 \text{ bytes} \approx 4.7\text{ MB}
  • For 2048 tokens (all layers): 2048×4.7 MB9.6 GB2048 \times 4.7\text{ MB} \approx 9.6\text{ GB}

Why this step? We need 2 matrices (K and V), each of size (num_tokens × num_heads × head_dim), stored per layer. The full-model cost multiplies by the number of layers, which is why the per-token figure jumps from ~48 KB (one layer) to ~4.7 MB (all 96 layers).

Worked Examples

Step 1: Previous cache state (after generating tokens 1-2):

CacheK=[K1K2]R2×4×64,CacheV=[V1V2]R2×4×64\text{Cache}_K = \begin{bmatrix} K_1 \\ K_2 \end{bmatrix} \in \mathbb{R}^{2 \times 4 \times 64}, \quad \text{Cache}_V = \begin{bmatrix} V_1 \\ V_2 \end{bmatrix} \in \mathbb{R}^{2 \times 4 \times 64}

Why this step? Each KiK_i has shape (num_heads, head_dim) = (4, 64). We've generated 2 tokens so far.

Step 2: Compute projections for new token:

K3=WKx3R4×64,V3=WVx3R4×64,Q3=WQx3R4×64K_3 = W_K x_3 \in \mathbb{R}^{4 \times 64}, \quad V_3 = W_V x_3 \in \mathbb{R}^{4 \times 64}, \quad Q_3 = W_Q x_3 \in \mathbb{R}^{4 \times 64}

Step 3: Update cache (concatenation):

CacheK[K1K2K3]R3×4×64\text{Cache}_K \leftarrow \begin{bmatrix} K_1 \\ K_2 \\ K_3 \end{bmatrix} \in \mathbb{R}^{3 \times 4 \times 64}

Step 4: Compute attention using only Q3Q_3 with all cached keys:

scores=Q3(K1,K2,K3)T64R4×3\text{scores} = \frac{Q_3 (K_1, K_2, K_3)^T}{\sqrt{64}} \in \mathbb{R}^{4 \times 3}

Why this step? Q3Q_3 (shape 4×64) multiplies with cached K (shape 3×4×64 → transpose to 4×64×3). Each head gets a 1×3 score vector (attending to 3 positions).

Cost: O(d2)O(d^2) for one token's projections + O(td)O(t \cdot d) for attention scores. No recomputation of K1,K2K_1, K_2.

Generating a 100-token sequence:

Without cache:

  • Per layer, per token tt: Compute tt key-value pairs → O(t×7682)O(t \times768^2)
  • Total: 12×t=1100t×76823.5×101012 \times \sum_{t=1}^{100} t \times 768^2 \approx 3.5 \times 10^{10} FLOPs

With cache:

  • Per layer, per token: Compute 1 new K, V → O(7682)O(768^2)
  • Total: 12×100×76827.1×10812 \times 100 \times 768^2 \approx 7.1 \times 10^8 FLOPs
  • Speedup: 50× (approximately matches the LL factor for L=100L=100)

Memory overhead:

  • Cache size: 12 layers×2 (K+V)×100 tokens×12 heads×64 dim×2 bytes3.7 MB12 \text{ layers} \times 2 \text{ (K+V)} \times 100 \text{ tokens} \times 12 \text{ heads} \times 64 \text{ dim} \times 2 \text{ bytes} \approx 3.7\text{ MB}

Why this matters: 3.7 MB is negligible compared to the 500 MB model weights, but saves 98% of projection compute.

Advanced Optimizations

1. Multi-Query Attention (MQA)

Cache reduction:

MQA cacheStandard cache=2Ldk2Lhdk=1h\frac{\text{MQA cache}}{\text{Standard cache}} = \frac{2Ld_k}{2Lhd_k} = \frac{1}{h}

Why this step? With h=32h=32 heads, MQA uses 32× less KV-cache memory. Quality drop is typically 1-2% perplexity.

2. Grouped-Query Attention (GQA)

Middle ground: Group hh query heads into gg groups (g<hg < h), each group shares one K, V head.

GQA cache=2Lgdk,where g=h/k for some k>1\text{GQA cache} = 2Lgd_k, \quad \text{where } g = h/k \text{ for some } k > 1

Example: Llama-2 70B uses h=64h=64, g=8g=8 (8× cache reduction vs. standard).

3. Sliding Window / Local Attention

For very long sequences, only cache the last WW tokens:

CacheK(t)=[KtW+1,,Kt]\text{Cache}_K^{(t)} = [K_{t-W+1}, \ldots, K_t]

Tradeoff: Constant O(W)O(W) memory, but loses long-range dependencies beyond WW tokens.

Use case: Summarization with 100K+ token contexts where recent context matters most.

4. FlashAttention Integration

FlashAttention computes attention without materializing the full O(L2)O(L^2) score matrix by fusing operations and using SRAM tiling. KV-cache + FlashAttention synergize:

  • Cache eliminates redundant K, V projections
  • FlashAttention eliminates memory overhead of attention scores

Combined speedup: 10-20× for long sequences (both reduce different bottlenecks).

Common Mistakes

Why it feels right: QQ also requires a matrix multiply, so caching seems logical.

Steel-man: In a typical forward pass, caching QQ would help. But in autoregressive generation, each new token generates a fresh query that attends to all previous positions. We need a new QtQ_t for token tt because it encodes "what the new token is looking for."

The fix: Only K and V are reusable because they represent "what previous tokens offer." The query represents "what the current token needs"—it must be recomputed.

Why it feels right: The cache has the data, why not use it?

Steel-man: During training with full sequences, we use a causal mask. It's easy to forget this is still needed during generation.

The fix: Even with caching, apply causal mask:

mask[i,j]={0if jiif j>i\text{mask}[i, j] = \begin{cases} 0 & \text{if } j \leq i \\ -\infty & \text{if } j > i \end{cases}

This ensures token tt only sees positions 1,,t1, \ldots, t (even though physically Kt+1,K_{t+1}, \ldots don't exist yet in autoregressive mode, this matters for batched generation).

Why it feels right: The notation says O(L)O(L) memory.

Steel-man: The notation hides the constants. For a 32-layer, 32-head model with dk=128d_k=128 in fp16:

Cache=2×32×32×128×L×2 bytes=1,048,576L bytes1 MB×L\text{Cache} = 2 \times 32 \times 32 \times 128 \times L \times 2 \text{ bytes} = 1{,}048{,}576 \, L \text{ bytes} \approx 1\text{ MB} \times L

For L=100,000L=100{,}000 tokens: ~105 GB just for the cache (exceeds most GPU memory).

The fix: Use hybrid strategies like sliding window cache, offloading old cache to CPU, or sparse attention for ultra-long contexts.

Implementation Details

Typical PyTorch pattern:

class CachedAttention:
    def __init__(self):
        self.kv_cache = None  # Will hold (batch, num_heads, seq_len, head_dim)
    def forward(self, x, use_cache=True):
        B, T, C = x.shape
        Q = self.W_q(x)  # Always compute fresh
        K_new = self.W_k(x)
        V_new = self.W_v(x)
        if use_cache and self.kv_cache is not None:
            K_cached, V_cached = self.kv_cache
            K = torch.cat([K_cached, K_new], dim=2)  # Concat along seq_len
            V = torch.cat([V_cached, V_new], dim=2)
        else:
            K, V = K_new, V_new
        # Standard attention computation
        scores = (Q @ K.transpose(-2, -1)) / math.sqrt(self.head_dim)
        atn = F.softmax(scores, dim=-1)
        out = atn @ V
        
        if use_cache:
            self.kv_cache = (K.detach(), V.detach())  # Detach to avoid gradient retention
        
        return out

Why detach? Prevents backprop through the entire cached history (only needed during training; during inference, no gradients anyway).

When to Use What

Context Length Strategy Example Use Case
< 2K tokens Standard KV-cache Chatbot turns, code completion
2K - 8K KV-cache + MQA/GQA Document Q&A
8K - 32K Sliding window cache Long-form writing
32K+ Sparse/local + offload Book summarization, legal docs
Recall Explain to a 12-Year-Old

Imagine you're writing a story one word at a time. Every time you write a new word, you need to remember what all the previous words were about so your new word makes sense.

The slow way: Every time you write a new word, you re-read the entire story from the beginning and take notes about each word. Writing 100 words means reading the story 100 times!

The KV-cache way: The first time you read each word, you write down notes on sticky notes (that's the "cache"). When you write word #50, you just glance at your 49 sticky notes instead of re-reading the whole story. You only add one new sticky note for word #50.

The tradeoff: You need space on your desk for all the sticky notes (memory), but you save tons of time not re-reading. If your desk gets too crowded (very long story), you might only keep the last 20 sticky notes and throw away older ones (sliding window).


Connections

  • 6.1.1-transformer-architecture - The self-attention mechanism that KV-cache optimizes
  • 6.1.4-multi-head-attention - How multiple heads multiply cache requirements
  • 6.2.3-inference-optimization - Broader category of deployment speedups
  • 6.3.1-model-quantization - Reducing cache memory via lower precision (int8, fp16)
  • 5.4.2-beam-search - Generation algorithm that becomes cache-intensive with multiple beams
  • 6.1.11-sparse-attention - Alternative approach to reduce O(L2)O(L^2) attention cost
  • 7.2.1-llm-deployment - Real-world systems where cache management is critical

#flashcards/ai-ml

What is the primary problem KV-cache solves? :: During autoregressive generation, preventing redundant recomputation of key and value projections for previously generated tokens. Without caching, we recompute K and V for all prior tokens at every step, leading to O(L2d2)O(L^2 d^2) cost instead of O(Ld2)O(Ld^2).

Derive the KV-cache memory formula per layer :: Cache size = 2×L×h×dk×bytes2 \times L \times h \times d_k \times \text{bytes}. The 2 accounts for separate K and V matrices, LL is sequence length, hh is number of attention heads, dkd_k is head dimension, and bytes is precision (2 for fp16, 4 for fp32). Multiply by nlayersn_{\text{layers}} for the full model.

Why don't we cache the Query (Q) projections?
The query represents "what the current token is looking for" and must be recomputed for each new token. Keys and values represent "what previous tokens offer," which are fixed for past tokens, making them safe to cache. Caching Q would prevent the new token from forming its own context-dependent query.
What is Multi-Query Attention (MQA) and its cache benefit?
MQA uses a single shared K and V projection across all query heads instead of per-head K, V pairs. This reduces cache size by a factor of hh (number of heads), since cache becomes 2Ldk2Ld_k instead of 2hLdk2hLd_k. Trade-off: ~1-2% quality drop for massive memory savings.

Calculate KV-cache size for GPT-2 Small generating 512 tokens :: GPT-2 Small: 12 layers, 12 heads, dk=64d_k=64, fp16. Cache = 12×2×512×12×64×212 \times 2 \times 512 \times 12 \times 64 \times 2 bytes = 18.9 MB. This is per-sample; batch size multiplies this.

Why does KV-cache still require causal masking?
Even though we only have past tokens in cache during generation, masking is crucial for (1) batched generation where sequences have different lengths, (2) consistency with training behavior, (3) preventing off-by-one errors in attention score indexing. The mask ensures position ii only attends to positions i\leq i.
What is Grouped-Query Attention (GQA)?
A middle ground between standard multi-head attention and MQA. Query heads are divided into gg groups (where g<hg < h), and each group shares one K, V head. Provides partial cache reduction (h/gh/g factor) with less quality degradation than full MQA. Example: Llama-2 70B uses h=64,g=8h=64, g=8.
When does KV-cache become a bottleneck?
(1) Very long sequences (100K+ tokens) where O(L)O(L) memory still exceeds GPU capacity, (2) Large batch sizes in production, (3) Models with many layers and heads (GPT-3 scale), (4) Multi-beam search multiplying cache by beam width. Solutions: sliding window, offloading, or sparse attention.

Concept Map

recomputes

costs

motivates

stores

reused via

reduces to

yields

makes past KV fixed

trades

scales with

Autoregressive generation

Redundant KV projections

Naive cost O L squared d squared

KV-cache

Cached K and V matrices

Compute once and append

Cached cost O L d squared

Speedup factor L

Causal mask

Extra memory per layer

2 x layers x L x h x d_k x precision

Hinglish (regional understanding)

Intuition Hinglish mein samjho

Dekho, transformer jab ek-ek token generate karta hai (jaise GPT bolte-bolte agla shabd nikalta hai), toh har step pe woh saare purane tokens ka attention dobara se compute karta hai. Ab socho, token 1 ka key aur value toh badalta hi nahi jab aap token 100 generate kar rahe ho — phir bhi model har baar unhe naye sire se calculate karta hai. Yeh bilkul aisa hai jaise agla word likhne ke liye aap poori kitaab dobara padho. Bahut waste ho raha hai! KV-cache ka core idea simple hai: jo K aur V ek baar compute ho gaye, unhe memory mein store kar lo aur reuse karo. Bas naye token ka K aur V calculate karke cache mein append kar do, purane wale chhedo mat.

Iska maths dekhoge toh samajh aayega kitna bada farak padta hai. Bina cache ke, L tokens generate karne ka cost O(L²d²) aata hai — quadratic, matlab sequence lambi hote hi cost tezi se badhta hai. Cache ke saath yeh O(Ld²) ban jaata hai, yaani L guna speedup! Yeh kaam isliye karta hai kyunki attention mein causal mask hota hai — token t sirf apne se pehle wale tokens ko dekhta hai, aur woh purane K, V generation ke dauraan fixed rehte hain (model freeze hai). Fixed cheezein cache karne ke liye perfect hoti hain.

Lekin ek catch hai: yeh speed memory ke badle milti hai. Har token ke liye, har layer ke liye 2 matrices (K aur V) store karni padti hai. GPT-3 jaise bade model mein 2048 tokens ka cache lagbhag 9.6 GB tak pahunch sakta hai! Isiliye yeh topic important hai — real deployment mein aapko yeh tradeoff samajhna zaroori hai. Fast inference chahiye toh cache lagao, par phir memory ka intezaam bhi karna padega. Isi wajah se aage aap padhoge ki log KV-cache ko aur optimize karte hain (jaise multi-query attention, quantization) taaki memory bhi bache aur speed bhi mile.

Go deeper — visual, from zero

Test yourself — Scaling & Efficient Architectures

Connections