TAI BUI
Back to curriculum
BuildPython

KV Cache, Flash Attention & Inference Optimization

Training is parallel and bound by FLOPs. Inference is serial and bound by memory. Different bottleneck, different tricks.

TIME: ~75 minutesPREREQUISITES: Phase 7 · 02 (Self-Attention), Phase 7 · 05 (Full Transformer), Phase 7 · 07 (GPT)

Learning Objectives

  • Calculate the KV cache size of a decoder from layers, KV heads, head dimension, dtype, and context length, and explain why GQA shrinks it.
  • Explain why a KV cache removes redundant recomputation during autoregressive decoding, and what it costs in memory.
  • Describe how Flash Attention tiles attention with a running softmax so the N×N matrix never reaches HBM, and why its output is exact.
  • Choose among continuous batching, PagedAttention, prefix caching, and speculative decoding for a given serving bottleneck.

The Problem

A KV cache stores the key and value vectors of every prefix token so a decoder can reuse them instead of recomputing them. Without one, a naive autoregressive decoder does O(N²) work to generate N tokens, because at each step it recomputes attention over the full prefix. For a 4K-token response that is 16M attention operations, most of them redundant. A prefix token's hidden state is deterministic once computed, so the new token needs to run its query against the cached keys and values of everything before it.

Attention also moves a lot of data. Standard attention materializes an N×N score matrix, an N×d softmax output, and an N×d final output, which means many reads and writes to HBM. For N≥2K, attention becomes memory-bound before it becomes FLOP-bound. Classic attention kernels underuse modern GPUs by 4–10×.

Two optimizations, both from Dao et al., took frontier inference from slow to fast:

  1. KV cache. Store the K and V vectors of each prefix token. A new token's attention is one query against the cached keys, which takes inference from O(N²) to O(N) per generation step.
  2. Flash Attention. Tile the attention computation so the full N×N matrix does not have to be written to HBM. The softmax and the matmuls run in SRAM. That gives a 2–4× wall-clock speedup on A100 and 5–10× on H100 with FP8.

By 2026 both are standard. vLLM, TensorRT-LLM, SGLang, and llama.cpp all assume them, and frontier models ship with Flash Attention enabled.

Warm-up0/3
1. A function is slow and is called many times with identical arguments. Which general technique avoids the repeated work?Warm-up
Six calls to the same slow function with the same argument 7, each one running the function body, and an unknown technique box.
Illustrative calls, not from a real program.
2. How many bytes does one fp16 number occupy?Warm-up
Four fp16 numbers 0.5, -1.25, 3.0 and 0.1 in memory cells, each labelled with an unknown number of bytes.
Typed illustration; the four values are made up.
3. A GPT-style language model generates text one step at a time. What happens after each new token is produced?Warm-up
Step 1: the tokens The, cat, sat go into a GPT-style language model and the token 'on' comes out. Step 2 has an unknown input.
Illustrative words, not a real model run.
Check: The Problem0/4
1. What is a KV cache?Define
Code that builds a KVCache and appends four entries, then a table of the first three numbers of each stored K and V vector per position.
Values from running the lesson's main.py with seed 42; first 3 of 8 dimensions.
2. Why does a naive autoregressive decoder do O(N²) work to generate N tokens?Explain
Line chart of the naive decoder's total attention ops against tokens generated, from 1 at N=1 up to 55 at N=10, curving upward.
Counts from main.py's decode_naive for N = 1 to 10.
3. In a GPT-style decoder, which earlier information does the newest token's attention read?Explain
Five earlier tokens t1 to t5 and a newest token t6, with a dashed arc from t6 back over the earlier tokens to an unknown box.
Illustrative token names.
4. With a KV cache, what does the new token's attention consist of at each step?Explain
Six cached token entries t1 to t6, each marked with K and V, a new token t7 beside them, and an unknown box for t7's attention.
Illustrative with 6 cached tokens.

The Concept

KV cache growth and Flash Attention tiling

The KV cache is the stored K and V of each prefix token, and this section covers what it costs in memory, how Flash Attention keeps attention on-chip, and three serving techniques that cut latency or raise throughput.

KV cache math

Per decoder layer, per token, the cache holds one K vector and one V vector for every KV head:

bytes_per_token_per_layer = 2 * n_kv_heads * d_head * dtype_size
                            ^
                            K and V

One head alone costs 2 * 128 * 2 = 512 bytes per token per layer (d_head=128, fp16). The head count multiplies that. For a 7B-class model with 32 layers and 32 KV heads (full multi-head attention):

per token per layer = 2 * 32 * 128 * 2 = 16,384 bytes (16 KB)
per token (32 layers) = 512 KB
per 32K context = 16 GB

For Llama 3 70B (80 layers, d_head=128, GQA with 8 KV heads):

per token per layer = 2 * 8 * 128 * 2 = 4096 bytes (4 KB)
per token (80 layers) = 320 KB
per 32K context = 10.7 GB

At 128K context that is about 43 GB, more than an entire 40 GB A100, for KV cache alone at batch size 1 and before a single weight is loaded.

GQA is where the KV-cache savings come from. The cache scales with KV heads, not query heads. Llama 3 70B has 64 query heads, so MHA with 64 KV heads would be 8× larger, about 86 GB at 32K. MLA compresses further still.

Drag the dimensions and watch the cache size move. Push the sequence length or batch size up and see how quickly it outgrows a single GPU:

Flash Attention: the tiling trick

Flash Attention computes attention in tiles so the N×N score matrix is not materialized in HBM. For contrast, here is standard attention:

S = Q @ K^T          (HBM read, N×N, HBM write)
P = softmax(S)       (HBM read, HBM write)
O = P @ V            (HBM read, HBM write)

That is three HBM round trips. On H100, HBM bandwidth is 3 TB/s and SRAM is 30 TB/s, so an HBM trip costs roughly 10× what staying on-chip does.

Flash Attention:

for each block of Q (tile size ~128 × 128):
    load Q_tile into SRAM
    for each block of K, V:
        load K_tile, V_tile into SRAM
        compute S_tile = Q_tile @ K_tile^T     (SRAM)
        running softmax aggregation             (SRAM)
        accumulate into O_tile                  (SRAM)
    write O_tile to HBM

Each tile takes one HBM trip, and the total memory footprint drops from O(N²) to O(N). The backward pass recomputes some values from the forward pass instead of storing them, which saves more memory.

Numerical trick. The running softmax keeps (max, sum) across tiles, so the final normalization is exact. Flash Attention is not an approximation. Its output matches standard attention up to fp16 non-associativity.

Version evolution:

VersionYearKey changeSpeedup on reference hardware
Flash 12022Tiled SRAM kernel2× on A100
Flash 22023Better parallelism, causal-first ordering3× on A100
Flash 32024Hopper asynchrony, FP81.5–2× on H100 (~740 TFLOPs FP16)
Flash 42026Blackwell 5-stage pipeline, software exp2Inference-first (forward only initially)

Flash 4 is forward-pass only at launch. Training still uses Flash 3. GQA and varlen support for Flash 4 is pending (mid-2026).

Speculative decoding: the other latency win

Speculative decoding has a cheap model propose N tokens and a big model verify all N in parallel. If verification accepts k tokens, you paid one big-model forward pass for k generations. Typical k is 3–5 on code and prose.

2026 defaults:

  • EAGLE 2 / Medusa. Draft heads built into the model that share the verifier's hidden states. 2–3× speedup with no quality loss.
  • Speculative decoding with a draft model. 2–4× speedup on consumer hardware.
  • Lookahead decoding. Uses Jacobi iteration, so no draft model is needed. Niche but free.

Continuous batching

Continuous batching swaps new requests into the batch as soon as old ones finish. It first shipped in Orca and now lives in vLLM, TensorRT-LLM, and SGLang. Typical chat workloads see a 5–10× throughput gain.

Classic batched inference waits for the slowest sequence to finish before starting a new batch, which wastes GPU time when short responses finish early.

PagedAttention: KV cache as virtual memory

PagedAttention is vLLM's headline feature. The KV cache is allocated in 16-token blocks, and a page table maps logical positions to physical blocks. That lets you share KV across parallel samples (beam search, parallel sampling), hot-swap prefixes for prompt caching, and defragment memory. Throughput improves 4× over naive contiguous allocation.

Check: The Concept0/23
1. What is HBM on a GPU?Define
Schematic of a GPU die containing six SM compute blocks, with two HBM blocks wired to each side of the die.
Schematic, not to scale.
2. What is SRAM in the context of GPU attention kernels?Define
Schematic: an HBM block outside, wired to a streaming multiprocessor that contains compute units and an SRAM block.
Schematic, not to scale.
3. What is Flash Attention?Define
An eight by eight grid of tiles labelled Q0 to Q7 down the side and K0 to K7 across the top, with one tile highlighted and marked as roughly 128 by 128.
Schematic; the 8 x 8 count is only for drawing.
4. What does continuous batching do when one sequence in the running batch finishes?Define
Snapshot of a batch with four slots: sequences A, C and D still generating, sequence B just finished, and requests E and F waiting in a queue.
Illustrative, not measured.
5. In PagedAttention, what does the page table map?Define
A 48-token sequence drawn as three blocks of 16 tokens, a dashed box labelled page table, and a row of eight numbered memory blocks.
Schematic; no mapping is drawn.
6. How does speculative decoding produce tokens?Define
A small cheap model box and a larger big model box above a row of six empty output token slots.
Schematic, not a measured setup.
7. What does a KV cache cost in exchange for removing the recomputation?Explain
Left panel lists four recomputation rows for K and V of prefix tokens, all struck out; right panel is an unknown box labelled paid instead.
Illustrative; four prefix tokens drawn.
8. Llama 3 70B uses 8 KV heads instead of 64. Why does that shrink its KV cache so much?Explain
A grid of 64 query-head dots in eight rows of eight, dotted lines to eight KV-head boxes, and an unknown box for KV cache size.
Schematic of 64 query heads and 8 KV heads.
9. What makes standard (non-tiled) attention memory-bound rather than FLOP-bound for N≥2K?Explain
Dataflow of standard attention with N = 2048 and d = 128: Q, K and V of 2048 by 128 feed S, then P, then O, whose shapes are marked with question marks.
Shapes of S, P, O left unknown; d = 128 is illustrative.
10. Does Flash Attention approximate the standard attention computation?Explain
Bar chart of ten attention scores for keys k0 to k9, split by dashed lines into three groups: k0 to k3, k4 to k7 and k8 to k9.
Real scores from the lesson's main.py, seed 42.
11. Why does the running softmax keep a (max, sum) pair across tiles?Explain
A table of three tiles giving each tile's own max score, the running max m and the running sum s after that tile.
Values from stepping through the lesson's tiled_softmax_dot, seed 42.
12. Why does the Flash Attention backward pass recompute some forward values instead of storing them?Explain
Forward pass boxes for tiles of S and P, output and loss, with a backward box that needs the S and P tile values again, and an unknown box asking where they come from.
Schematic.
13. What do EAGLE 2 and Medusa do in speculative decoding?Explain
A stack of three layers topped by a top-layer output feeding an LM head and a next token, with a dashed unknown box labelled EAGLE 2 / Medusa.
Schematic of a decoder.
14. What did Flash Attention 3 change to suit newer GPUs?Explain
A timeline of Flash 1 (2022), Flash 2 (2023), Flash 3 (2024) and Flash 4 (2026), each with an unknown change box under it.
Years from the lesson's version table.
15. For a 7B-class model (32 layers, 32 KV heads, d_head=128, fp16), how big is the KV cache at 32K context?Apply
A settings table for a 7B-class model: 32 layers, 32 KV heads, d_head 128, fp16, 32K context, and an unknown box for the KV cache size.
Settings copied from the question.
16. You plan to serve Llama 3 70B (80 layers, 8 KV heads, d_head=128) at 128K context, batch size 1, fp16, on one 40 GB A100. What does the KV cache math predict?Apply
A 40 GB GPU memory bar, an unknown KV cache box, and a settings table: 80 layers, 8 KV heads, d_head 128, fp16, 128K context.
Settings copied from the question.
17. Llama 3 70B (80 layers, 8 KV heads, d_head=128) is served with an fp8 KV cache at 1 byte per value, as in the vLLM command. How many KB does the cache take per token?Apply
The vLLM serve command with --kv-cache-dtype fp8, a settings table with 80 layers, 8 KV heads, d_head 128 and 1 byte per value, and an unknown box for KB per token.
Command copied from the lesson's Use It section.
18. A team tries to train a model with Flash Attention 4 at its 2026 launch and finds there is no backward kernel. What explains this?Analyze
A training step: a forward kernel box, a loss box, and a dashed backward kernel box marked as missing, plus an unknown box asking why.
Schematic of the question's situation.
19. A team cannot fit the KV cache of a GQA model (Llama 3 70B, 8 KV heads) at 128K context on a 40 GB GPU at batch size 1. They plan to turn on Flash Attention to fix it. Why will that not solve the problem?Analyze
A 40 GB GPU memory bar, a box saying the KV cache does not fit, a plan to turn Flash Attention on, and an unknown box asking if that fixes it.
Situation from the question.
20. An engineer sizes the Llama 3 70B cache at 32K using its 64 query heads and gets about 86 GB, but the real figure is 10.7 GB. What did they get wrong?Analyze
Two bars, an engineer's estimate of about 85.9 GB and a real figure of about 10.7 GB for Llama 3 70B at 32K, and a model table with 80 layers, 64 query heads, d_head 128 and fp16.
Figures copied from the question.
21. A chat server shows idle GPU time. Each batch holds until its slowest response ends, though most responses finished much earlier. Which change targets this?Analyze
Four slots with response bars of different lengths in one batch; dashed idle gaps follow the three shorter responses until the slowest one ends.
Illustrative lengths, not measured.
22. You generate eight parallel samples from one long prompt, and memory fills with eight copies of the prompt's KV. Which technique lets the samples share it?Design
Eight rows for eight samples, each holding its own full copy of the prompt's KV block followed by a short block of its own tokens.
Illustrative layout.
23. You serve a large model to one interactive user. Decoding is latency-bound at one token per big-model pass, and you can add extra heads to the model. Which approach fits, with no quality loss?Design
Five big-model pass boxes in a row, each producing a single token, with time running left to right.
Illustrative.

Build It

code/main.py implements:

  1. A naive incremental decoder that recomputes attention over the full prefix at every step.
  2. A KV-cached decoder that appends one K and V per step and queries against the cache.
  3. A tiled softmax that simulates Flash Attention's running-max algorithm.
  4. kv_cache_bytes, which turns the KV cache math above into a size table.

Step 1: KV cache

python
class KVCache:
    def __init__(self):
        self.K = []
        self.V = []
 
    def append(self, k, v):
        self.K.append(k)
        self.V.append(v)
 
    def __len__(self):
        return len(self.K)

The cache appends one K and one V vector per generated token. This demo models a single head, so it is two flat lists. A real cache keeps one such pair per layer and per KV head, which is exactly the multiplier kv_cache_bytes applies.

Step 2: tiled softmax

python
def tiled_softmax_dot(q, Ks, Vs, tile=4):
    """Flash-attention-style incremental softmax(qK^T)V with tile size `tile`."""
    d_head = len(Vs[0])
    scale = 1.0 / math.sqrt(len(q))
    m = float("-inf")
    s = 0.0
    out = [0.0] * d_head
    for start in range(0, len(Ks), tile):
        k_block = Ks[start:start + tile]
        v_block = Vs[start:start + tile]
        scores = [dot(q, k) * scale for k in k_block]
        new_m = max(m, *scores)
        if m == float("-inf"):
            exp_old = 0.0
        else:
            exp_old = math.exp(m - new_m)
        exp_new = [math.exp(sc - new_m) for sc in scores]
        s = s * exp_old + sum(exp_new)
        for j in range(d_head):
            out[j] = out[j] * exp_old + sum(e * v[j] for e, v in zip(exp_new, v_block))
        m = new_m
    return [o / s for o in out]

The output matches softmax(qK^T / sqrt(d)) V computed in one shot (attention_full in main.py), but the working set at any moment is a tile × d_head block instead of the full N × d_head.

Step 3: compare naive vs cached decoding on a 10-token generation

The demo runs N=10 and counts one attention op per key attended to. Both decoders report 55 (N(N+1)/2), because each step still attends to the whole prefix. The cache's saving is not recomputing the earlier tokens' K and V (hidden states) at every step, and this pure-Python demo does not count that work. What it does verify is that the two decoders produce the same outputs (max abs diff 0).

Step 4: print the KV cache size table

kv_cache_bytes(N, n_layers, n_heads_kv, d_head, dtype) is the formula from the KV cache math section. main.py prints it for six configs at 2K, 32K, and 128K context: Llama-3.2-3B, Llama-3-8B, Llama-3-70B, Llama-3.1-405B, Qwen2.5-72B, and DeepSeek-V3. The DeepSeek-V3 row is a rough stand-in for MLA, modeled as one head with a 512-dim latent. Llama-3-70B comes out at 10.74 GB for 32K and 42.95 GB for 128K, matching the math above.

Check: Build It0/5
1. In the Build It demo (N=10), the naive and cached decoders both report 55 attention ops. What does the demo actually show?Apply
Terminal showing the first lines of the Build It demo: both the naive and cached decoders report 55 attention ops for N=10, then the output is cut off.
Real output of the lesson's main.py, truncated.
2. A config with 32 layers, 32 KV heads, d_head=128 and fp16 is passed to kv_cache_bytes with N=1000 tokens. About how big is the result?Apply
The signature and docstring of kv_cache_bytes, then a call with N=1000, 32 layers, 32 KV heads, d_head 128 and dtype 2, with the result shown as a question mark.
Signature copied from main.py; the call is typed.
3. tiled_softmax_dot is called with tile=4 on 10 keys. How many tiles does its loop process?Apply
Ten key vectors K0 to K9 in a row, the call tiled_softmax_dot with tile=4, and an unknown box for the number of tiles processed.
Ten keys as in the question.
4. On the first tile of tiled_softmax_dot, m is -inf. What does the code set exp_old to, and why?Apply
Excerpt of tiled_softmax_dot with m starting at minus infinity and the exp_old value in the m equals minus infinity branch replaced by a question mark.
Lines copied from main.py; one value hidden.
5. Your tiled attention kernel differs from standard attention by about 1e-2, far above the floating-point noise the reference demo shows. Where do you look first?Analyze
Log-scale chart: the reference demo's differences for tile sizes 1, 2, 4, 8 and 32 sit near 1e-16, while the question's own kernel sits near 1e-2.
Reference points from a real run of the lesson's main.py, seed 42; the 1e-2 point is from the question.

Use It

python
# HuggingFace transformers auto-enables KV cache on decoder-only generate().
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-3.2-3B",
    attn_implementation="flash_attention_2",  # use FA3 if Hopper
    torch_dtype="bfloat16",
)
# generate() uses KV cache automatically

vLLM production:

bash
pip install vllm
vllm serve meta-llama/Llama-3.1-70B-Instruct \
    --tensor-parallel-size 4 \
    --max-model-len 32768 \
    --enable-prefix-caching \
    --kv-cache-dtype fp8

Prefix caching across requests is a big 2026 win. The same system prompt, few-shot examples, or long context document reuses its KV across calls. For agent workloads with repeated tool prompts, prefix caching often gives a 5× throughput gain.

Check: Use It0/3
1. What does prefix caching reuse across requests?Define
Three requests drawn as strips, each starting with the same system prompt block and ending with a different short user message.
Illustrative layout, not real traffic.
2. What does the `--enable-prefix-caching` flag do when you serve a model with vLLM?Apply
The lesson's vLLM serve command with --enable-prefix-caching highlighted, and an unknown box asking what the flag does.
Command copied from the lesson's Use It section.
3. Your agent sends many requests that all begin with the same long system prompt and tool definitions, and only a short new message differs. Which technique cuts cost most directly?Design
Four agent calls drawn as strips; each has the same long system prompt and tool definitions block and a short different new message.
Illustrative proportions, not real traffic.

Ship It

outputs/skill-inference-optimizer.md holds the skill. It picks the attention implementation, KV cache strategy, quantization, and speculative decoding for a new inference deployment.

Check: Ship It0/1
1. What does this lesson's Ship It skill, inference-optimizer, do?Explain
A file tree for this lesson: code/main.py and outputs/skill-inference-optimizer.md, with the markdown file highlighted.
File layout only.
Author's Note

Rerun the arithmetic in the KV cache math section with your own model's numbers. kv_cache_bytes in main.py does the multiplication and prints sizes for six configs at 2K, 32K, and 128K context. Its takeaway line says 70B-class dense models use 10+ GB for KV at 128K, and that GQA and MLA are why long-context inference stays affordable.

The check I'd run first is the tiled-softmax comparison. main.py compares tiled_softmax_dot against standard attention at tile sizes 1, 2, 4, 8, and 32 and prints the max abs diff for each. Those diffs are floating-point noise. If a tiled kernel you write or port shows a diff far above that noise, check the running max and sum rescaling first.

I'd read the op counts in the demo with care. main.py runs 10 tokens, not 100, and counts one op per key attended to, so the naive and cached decoders come out with the same count. Its printed note says the saving comes from not recomputing earlier hidden states, which this demo does not count. What the demo does show is the two decoders producing the same outputs, and that is the part to rely on.

Exercises

  1. Easy. Run code/main.py. Confirm the naive and cached decoders produce the same output; note that the op counts are equal and why (the saving is the K/V recomputation the demo does not count).
  2. Medium. Implement prefix caching: given a prompt P and several completions, run one forward pass over P to fill the KV cache, then branch per-completion. Measure speedup vs re-encoding P for each.
  3. Hard. Implement a toy PagedAttention: KV cache in fixed 16-token blocks with a free-list. When a sequence finishes, return its blocks to the pool. Simulate 1,000 chat completions with varying lengths. Compare memory fragmentation vs contiguous allocation.

Key Terms

TermWhat people sayWhat it actually means
KV cache"The trick that makes decoding fast"Stored K and V from each prefix token; new queries attend to them instead of recomputing.
HBM"GPU main memory"High Bandwidth Memory; 80 GB on H100, 192 GB on B200. ~3 TB/s bandwidth.
SRAM"On-chip memory"Per-SM fast memory, ~256 KB per SM on H100. ~30 TB/s bandwidth.
Flash Attention"Tiled attention kernel"Computes attention without materializing N×N in HBM.
Continuous batching"No-wait batching"Swap finished sequences out, new ones in, without draining the batch.
PagedAttention"vLLM's headline"KV cache allocated in fixed blocks with a page table; eliminates fragmentation.
Prefix caching"Reuse long prompts"Cache KV for a shared prefix across requests; major cost cut for agents.
Speculative decoding"Draft + verify"Cheap draft model proposes tokens; big model verifies k in one pass.

Further Reading

Code

Ships With