KV cache, PagedAttention and FlashAttention
The KV cache decides how many users fit on a GPU. PagedAttention decides how much of that memory is actually used. FlashAttention decides how fast attention runs once the data is there. This page sizes the cache with real numbers, then explains the two ideas that made modern serving possible.
Start here (no maths needed)
A chatbot keeps notes on every word of every conversation it's having. Those notes fill up the computer's memory. Clever bookkeeping lets the same memory hold many more conversations, and clever reading makes the notes faster to use.
Think of it like this
Picture a school library with one long shelf. The old way: every student who walks in gets a whole metre of shelf "just in case" they write a lot, even if most write two pages. The shelf fills up after a few students, mostly with empty space. The new way: give out small boxes, one at a time, only when a student needs another. Keep a list saying which boxes belong to whom, in any order. Now the same shelf holds many more students' notes. That list of boxes is PagedAttention.
FlashAttention is about reading, not storing. Instead of carrying every book to a far-away table and back for each question, you bring a few books to your desk, finish all the work on them, and only then fetch the next few. Same answers, far fewer trips.
How it works
- For each word in a conversation, the model saves a small "key" and "value" note in every layer. That's the KV cache.
- Notes grow with the length of the conversation and with the number of people chatting at once.
- PagedAttention stores the notes in small fixed-size blocks and keeps a table of where each conversation's blocks are, so no space is reserved and wasted.
- FlashAttention does the attention maths in small tiles inside the chip's fast memory, so it rarely goes back to the slow, big memory.
Why robots care
A robot's computer has a fixed amount of memory that must also hold the camera pipeline and the model itself. If the model's notes on the last minute of camera frames and instructions don't fit, the robot forgets context or slows down. Each control step must still finish on time, so fast attention matters too.
Try it
- Give five friends a sheet of paper each to write their name. Then give five other friends one sheet cut into strips, handing out a strip only when someone asks. Count how much paper is left over each way.
- In the demo, change KV precision from 16 to 8 to 4 bits and watch GPUs needed fall. Change KV heads from 8 to 64 to see what the model would cost without grouped-query attention. Then lower Average actual length and compare Requests that fit: contiguous with paged.
Words to know
- KV cache
- Saved keys and values for every earlier token, so they aren't recomputed for each new token.
- HBM
- The GPU's main memory: big, but slow compared with the chip's on-chip memory.
- SRAM
- Tiny, very fast memory on the chip itself.
- Fragmentation
- Memory that's reserved or split up so badly that nobody can use it.
- Block table
- A list mapping a conversation's token positions to the memory blocks that hold them.
Going deeper: everything below is the full version, with some maths. On a first read, skim the headings and the "Why it matters for robotics" box, and come back for the rest later.
Problem 3 · Size the KV cache for a Llama-style 70B at 32K context and 100 users, with GQA and precision
What the interviewer is testing: can you write the formula from memory, plug in the right shape (KV heads, not query heads), convert units without slipping, and then turn the answer into a hardware decision with headroom?
The concepts
Every layer stores one key vector and one value vector per KV head per token. Query heads don't count: under grouped-query attention (GQA) several query heads share one KV head. Module 08 derives this; here's the formula to use:
KV bytes = 2 × L × n_kv × d_head × bytes_per_value × T × users
(K and V) (layers) (KV heads) (head dim) (precision) (tokens) (sequences)
per-token cost = 2 × L × n_kv × d_head × bytes_per_value
The rest of GPU memory goes to the weights (parameters × bytes per weight), activations and temporary buffers, the CUDA context, and the engine's own overhead. Engines like vLLM reserve a fraction of memory up front and carve the KV pool out of what's left, so plan with headroom. This page assumes 90% of HBM is usable for weights plus KV; that's an assumption to tune.
A worked answer with real numbers
Model shape. The Llama-2/3 70B published configs: 80 layers, 64 query heads, 8 KV heads, head dim 128 (hidden size 8,192 = 64 × 128). About 70 billion parameters.
GPUs (NVIDIA product pages): H100 SXM 80 GB at 3.35 TB/s; H200 141 GB at 4.8 TB/s. Note GB here is 109 bytes while the cache maths below lands in GiB (230); mixing them is a classic 7% error.
Values per token = 2 × 80 × 8 × 128 = 163,840
Per token: BF16 (2 B) = 327,680 B = 320 KiB
FP8 (1 B) = 160 KiB
4-bit = 80 KiB (+ a little for scales)
Per user at 32K (32,768 tokens):
BF16 = 320 KiB × 32,768 = 10 GiB
FP8 = 5 GiB
4-bit = 2.5 GiB
100 users, all at full 32K:
BF16 = 1,000 GiB = 1,073.7 GB
FP8 = 500 GiB = 536.9 GB
4-bit = 250 GiB = 268.4 GB
Without GQA (64 KV heads, BF16): 2.5 MiB/token → 8,000 GiB ≈ 8,590 GB. GQA saves 8×.
Now add the weights and divide by usable memory per GPU. Weights at FP8 are about 70 GB (at BF16, about 140 GB).
| KV precision (weights FP8, 70 GB) | KV for 100 × 32K | Total | H100 SXM at 72 GB usable | H200 at 126.9 GB usable |
|---|---|---|---|---|
| BF16 | 1,073.7 GB | 1,143.7 GB | 15.9 → 16 GPUs | 9.0 → 10 GPUs |
| FP8 | 536.9 GB | 606.9 GB | 8.4 → 9 GPUs | 4.8 → 5 GPUs |
| 4-bit | 268.4 GB | 338.4 GB | 4.7 → 5 GPUs | 2.7 → 3 GPUs |
Turning that into a deployment. Tensor-parallel groups usually come in powers of two, and 8 GPUs is one node. FP8 weights plus FP8 KV need 9 H100s at 90% usable: just over one node. Options: one 8×H200 node (lots of room), two H100 nodes, or trim the requirement. With TP = 8, each GPU holds exactly one of the 8 KV heads, so per-GPU KV cost is 20 KiB per token at FP8. Beyond TP = 8, KV heads must be replicated, and the per-GPU saving stops scaling.
The question behind the question. "100 users at 32K" is the worst case. Real conversations are mostly short. If the average live context is 8K, the FP8 cache for 100 users is 125 GiB, and the whole thing fits on one 8×H100 node with room to spare. Whether you can bank on the average instead of the maximum depends on how the cache is allocated, which is problem 4.
Trade-offs
| Lever | Memory saved | Cost |
|---|---|---|
| GQA (decided at training time) | n_q / n_kv, here 8× | Small quality cost; can't add to a finished model without uptraining |
| FP8 KV cache | 2× vs BF16 | Usually small quality loss; check long-context retrieval |
| 4-bit or 2-bit KV | 4–8× | Needs careful schemes (per-channel keys); more risk on long contexts |
| Shorter max context or fewer users per replica | Linear | Product limits, or more replicas |
| Offload cold KV to CPU memory | Frees HBM | PCIe transfer time when the conversation resumes |
How to say it in the interview
"KV bytes per token are 2 × layers × KV heads × head dim × bytes. For a Llama-style 70B that's 2 × 80 × 8 × 128 = 163,840 values, so 320 KiB per token in BF16 and 160 KiB in FP8. At 32K tokens that's 10 GiB per user in BF16, so 100 users at full context is 1,000 GiB, or 500 GiB in FP8. Add about 70 GB of FP8 weights and 10% headroom: around nine H100 SXMs or five H200s. GQA is already saving 8× here; without it the BF16 cache would be 8,000 GiB. And 100 × 32K is the worst case; with paged allocation you pay for actual tokens, so I'd size on the measured length distribution."
Common mistakes
- Using query heads (64) instead of KV heads (8): an 8× overestimate.
- Forgetting the factor 2 for keys and values.
- Mixing GiB and GB, or forgetting the weights and headroom.
- Assuming tensor parallelism always divides the cache: with more GPUs than KV heads, heads are replicated.
Interactive: KV-cache sizer and fragmentation
Top: memory if every user reaches full context. Grey is weights, blue is KV cache; the red line is total GPU memory and the dashed line is the 90% usable share. Bottom: the first 40 simulated requests stored two ways, on the same scale. Contiguous reserves the full context per request (amber is reserved but empty). Paged hands out 16-token blocks as needed (red slivers are the half-empty last blocks).
Problem 4 · Explain PagedAttention and the problem it solves
What the interviewer is testing: whether you know the problem is memory waste limiting batch size, not attention speed. Bonus points for block tables, copy-on-write, and what it costs.
The concepts
The problem. A sequence's KV cache grows by one token per step, and you don't know in advance how long the answer will be. Early serving systems stored each sequence's cache in one contiguous tensor sized for the maximum length. That wastes memory three ways:
- Reservation: space for tokens not yet generated sits idle for the whole request.
- Internal fragmentation: most requests finish far short of the maximum, so most of the slab is never used.
- External fragmentation: slabs of different sizes leave gaps between them that are too small to reuse.
Wasted KV memory means fewer sequences in the batch, and in the bandwidth-bound decode phase a bigger batch is almost free throughput. So waste directly caps throughput.
The idea (Kwon et al., 2023), borrowed from operating-system virtual memory:
- Split the KV pool into fixed-size blocks, each holding the keys and values for a fixed number of tokens (commonly 16) across all layers' heads for that block.
- Each sequence has a block table mapping its logical block 0, 1, 2… to physical blocks anywhere in the pool. Blocks are allocated only when the previous one fills.
- The attention kernel reads keys and values through the block table, gathering non-contiguous blocks.
- Because blocks are indirect, sequences can share them. Parallel samples or beam-search branches point to the same prompt blocks, with a reference count. On write to a shared block, copy it first (copy-on-write). The same mechanism enables prefix caching across requests (page 04).
- When memory runs out, the scheduler can preempt a sequence: free its blocks and later recompute its prefix, or swap the blocks to CPU memory.
logical token position p → block = p // B, offset = p % B
physical address = block_table[seq][block] × block_bytes + offset × token_bytes
waste per sequence ≤ B − 1 tokens (only the last block is partly empty)
average waste ≈ B / 2 tokens
The paper reports near-zero KV waste and 2–4× higher throughput than the systems it compared against (FasterTransformer, Orca) at the same latency, with larger gains for longer sequences, bigger models and complex decoding.
A worked answer with real numbers
Assumptions. The 70B shape from problem 3, FP8 weights (70 GB) and FP8 KV (160 KiB per token), one node of 8 H100 SXMs, 90% of HBM usable, max context 32K, real requests averaging 2,048 tokens (an assumed traffic mix).
KV budget = 8 × 80 GB × 0.9 − 70 GB = 506 GB ≈ 471 GiB
Contiguous (reserve 32K per request):
per request = 32,768 × 160 KiB = 5 GiB
fits = 471 / 5 ≈ 94 requests
utilisation = 2,048 / 32,768 ≈ 6% of reserved KV memory holds real tokens
Paged (16-token blocks):
per request ≈ (2,048 + 8 wasted) × 160 KiB ≈ 321 MiB
fits ≈ 471 × 1,024 / 321 ≈ 1,500 requests
utilisation ≈ 99.6%
One block = 16 tokens × 160 KiB = 2.5 MiB (the whole model; 320 KiB per GPU at TP = 8)
About 16× more concurrent sequences from the same memory. In practice you won't run 1,500 at once on one node, because TPOT climbs with batch size and you hit compute limits or latency targets first. The point is that memory stops being the binding constraint, and the scheduler can choose the batch size that meets latency targets.
Trade-offs
| Choice | Upside | Downside |
|---|---|---|
| Small blocks (e.g. 8–16 tokens) | Less waste per sequence, finer sharing | Bigger block tables, more scattered reads, less efficient kernels |
| Large blocks (e.g. 64–256) | Contiguous reads, simpler kernels | More waste in the last block; coarser prefix sharing |
| Preempt by recompute | No CPU memory or PCIe traffic | Redo the prefill later |
| Preempt by swap | No recompute | PCIe bandwidth and CPU memory |
How to say it in the interview
"PagedAttention solves KV-cache memory waste, not attention speed. Contiguous caches reserve the maximum length per request, so most of the memory sits empty: reserving 32K for requests that average 2K uses about 6% of what's reserved. PagedAttention splits the cache into fixed blocks of, say, 16 tokens, and keeps a per-sequence block table so blocks can live anywhere and are allocated on demand. Waste drops to under one block per sequence, and shared prefixes can point to the same blocks with copy-on-write. On a 70B FP8 model on 8 H100s that's roughly 94 versus 1,500 sequences that fit, which lets the scheduler run much bigger batches; the paper reports 2–4× throughput. The cost is an indirection in the attention kernel and an allocator to manage."
Common mistakes
- Describing PagedAttention as a faster attention algorithm. Its gain is batch size via memory efficiency.
- Confusing it with offloading to disk or CPU. Paging here is within GPU memory; swapping is an optional extra.
- Forgetting sharing and copy-on-write, which make parallel sampling and prefix caching cheap.
Problem 5 · Explain why FlashAttention is faster when the maths is the same
What the interviewer is testing: whether you think in memory traffic, not FLOPs. The key sentence: standard attention is limited by reading and writing the N×N matrix to HBM; FlashAttention never writes it.
The concepts
Standard attention for one head, sequence length N, head dim d:
S = Q Kᵀ / √d N×N written to HBM
P = softmax(S) N×N read from HBM, written back
O = P V N×d P read again from HBM
FLOPs ≈ 4 N² d (two matmuls)
HBM traffic ≈ Θ(N d + N²): the N² term dominates
A GPU has a large, relatively slow HBM and a tiny, very fast on-chip SRAM. Softmax and masking do few FLOPs per byte, so the kernels that read and write S and P are memory-bound. The matmuls are fast; the round trips through HBM are what cost time.
FlashAttention (Dao et al., 2022) changes the order of work, not the result:
- Tiling. Load a block of Q rows and a block of K and V rows into SRAM. Compute that tile of S, use it, discard it. S and P never go to HBM.
- Online softmax. Softmax needs a row's max and sum over all columns, which you don't have when working tile by tile. Keep a running max m and running sum ℓ per row, and rescale the partial output when the max changes:
for each K,V tile j:
S_j = Q_i K_jᵀ / √d
m_new = max(m, rowmax(S_j))
ℓ = e^(m − m_new) · ℓ + rowsum(e^(S_j − m_new))
O = e^(m − m_new) · O + e^(S_j − m_new) · V_j
m = m_new
O = O / ℓ exact same result as softmax(QKᵀ/√d) V
- Kernel fusion. Matmul, scale, mask, softmax and the second matmul run in one kernel, so there's one launch and no intermediate tensors.
- Recompute in the backward pass. For training, it stores only m and ℓ and recomputes S tile by tile during backprop. More FLOPs, much less memory traffic, still faster.
The paper shows HBM accesses fall to Θ(N² d² / M), where M is the SRAM size, and that this is optimal for a range of SRAM sizes. Memory use goes from quadratic to linear in N, which is what made long contexts practical.
Versions. FlashAttention-2 (Dao, 2023) reorganised the work: fewer non-matmul operations, parallelism across the sequence dimension, and better splitting of work between warps. FlashAttention-3 (Shah et al., 2024) targets Hopper GPUs: it overlaps data movement and computation using the Tensor Cores' and TMA's asynchrony, and adds FP8. Its abstract reports 1.5–2.0× over FlashAttention-2 on H100, up to 740 TFLOPS in FP16 (75% utilisation) and close to 1.2 PFLOPS in FP8.
Serving note. Decode is different: one query token against a long cache. There's no N×N matrix to avoid, and the cost is reading K and V once. Decode kernels split the long KV sequence across thread blocks so the GPU stays busy at batch 1, and they read through PagedAttention's block table. Modern engines combine both ideas.
A worked answer with real numbers
Assumptions. One sequence of N = 8,192 tokens, head dim 128, FP16, 32 query heads per layer (an 8B-class shape), one H100 SXM: 3.35 TB/s HBM, about 989 TFLOPS dense FP16 (half the page's 1,979 "with sparsity" figure). Count only the big N×N traffic for standard attention: write S, read S, write P, read P.
Standard attention, per head:
S size = 8,192² × 2 B = 128 MiB
HBM traffic ≈ 4 × 128 MiB = 512 MiB ≈ 0.54 GB
time at 3.35 TB/s ≈ 160 µs
× 32 heads ≈ 5.1 ms per layer (memory-bound)
Compute, per layer:
FLOPs = 32 × 4 × 8,192² × 128 ≈ 1.1e12
time at 989 TFLOPS ≈ 1.1 ms
FlashAttention, per head:
HBM traffic ≈ read Q, K, V + write O = 4 × 8,192 × 128 × 2 B = 8 MiB
time ≈ 2.5 µs → now compute-bound (~1.1 ms per layer)
Memory for S if materialised: 128 MiB × 32 heads = 4 GiB per layer, per sequence
Same 1.1e12 FLOPs either way. Standard attention spends roughly 5 ms per layer just moving S and P; FlashAttention moves about 64× less data per head and becomes limited by the matmuls. These are idealised floors, but they show the shape of the win. Real speedups depend on N (bigger N, bigger win) and the kernel: the original paper reports 3× on GPT-2 training at 1K tokens.
Trade-offs
- Exact vs approximate. FlashAttention is exact (up to floating-point ordering). Approximate attention methods cut FLOPs but often didn't speed up wall-clock time, a point the paper makes.
- Hardware-specific code. Peak performance needs kernels tuned per GPU generation (FA3 is Hopper-specific). Portable fallbacks are slower.
- Short sequences gain less. At small N the N² traffic is small and other overheads dominate.
- Recompute costs FLOPs in training's backward pass; it pays off because memory, not compute, was the limit.
How to say it in the interview
"The FLOPs are the same; the memory traffic isn't. Standard attention writes the N×N score matrix to HBM, reads it for softmax, writes the probabilities and reads them again, and those steps are memory-bound. At 8K tokens that matrix is 128 MiB per head in FP16, so on an H100 just moving it costs about 5 ms per layer, versus about 1 ms of actual matmul. FlashAttention tiles Q, K and V into on-chip SRAM, uses an online softmax with a running max and sum so it never needs the full row, and fuses everything into one kernel, so S never touches HBM. That turns attention from memory-bound to compute-bound and makes memory linear in sequence length."
Common mistakes
- Saying it "reduces FLOPs" or "approximates attention". It does neither.
- Not mentioning online softmax, the trick that makes tiling possible.
- Assuming the same speedup at decode time. Decode has a different bottleneck: reading the KV cache.
Practise it
- Check the formula against an engine. On a GPU with 16–24 GB, start vLLM with a small GQA model (for example a 1B–3B Llama-family instruct model). At startup it logs how many KV-cache blocks or tokens it allocated. Compute the expected number from the formula, the model's config.json (layers, KV heads, head dim) and the memory left after weights. They should agree to within the engine's overheads. Repeat with
--kv-cache-dtype fp8and confirm the token capacity roughly doubles. - Simulate fragmentation. In 30 lines of Python, draw 10,000 request lengths from a log-normal distribution, then compute utilisation for contiguous max-length allocation and for block sizes 1, 16, 64 and 256. Plot waste against block size.
- Feel FlashAttention on Colab. On a free T4 GPU, time a naive PyTorch attention (explicit
softmax(Q @ K.T) @ V) againsttorch.nn.functional.scaled_dot_product_attentionfor N = 512 to 8,192, recording time and peak memory withtorch.cuda.max_memory_allocated. Watch the naive version's memory grow with N² and eventually run out. Note which backend PyTorch picks on your GPU; FlashAttention kernels need recent GPUs, so a T4 may use the memory-efficient backend instead. - Write online softmax. Implement the tiled loop above in NumPy for one head, check it matches the naive result to 1e-5, and count bytes "moved" with a tile size you choose.
In this course
- Module 08 · Inference-aware design: MHA vs GQA vs MQA, KV quantisation (KIVI, KVQuant), and a KV-memory demo. Read it before this page if the formula is new.
- Module 05 · Serving: two cache types in one paged pool, prefix caching with checkpoints, and a production-scale serving stack.
- Module 08 · Tensor maths and autograd: Triton and FlashAttention-style tiling from the kernel side.
- Module 08 · Distributed training: tensor parallelism, and why KV heads get replicated beyond TP = n_kv.
- Page 01 · Request lifecycle: where prefill and decode sit in a request.
Why it matters for robotics
- On-robot memory is shared and small. An embedded GPU module splits one memory pool between the OS, perception, weights and the KV cache. The per-token formula tells you how many seconds of camera and instruction history a VLA can hold before something has to give.
- Vision tokens are the big multiplier. Multi-camera inputs produce thousands of tokens per step (Module 08, Stage 07). At the 8B shape in BF16 (128 KiB per token), 4,000 tokens is about 500 MiB of cache per step; GQA and FP8 KV are often what make it fit.
- Paging matters less at batch 1, but sharing still helps. One robot runs one sequence, so fragmentation across users is a server problem. Reusing the cached prefix (instruction plus current images) across action steps is the robot version of block sharing.
- Fused attention kernels set the control rate. Long-context attention without a fused kernel can blow the control-step budget and memory. Check which attention backend your edge runtime actually uses.
- Fleets of robots are a cloud problem. When many robots share a server model, all of problem 3 and 4 applies exactly as for chat users.
Self-check
KV cache per token for 80 layers, 8 KV heads, head dim 128, in FP8?
2 × 80 × 8 × 128 × 1 B = 163,840 B = 160 KiB.
Why use KV heads, not query heads, in the formula?
Under GQA several query heads share one stored key and value head. Only KV heads are cached. Using query heads for a 64-query, 8-KV model overestimates by 8×.
What does PagedAttention improve directly, and what improves as a result?
Directly: KV memory utilisation, by removing reservation and fragmentation waste. As a result: more sequences fit, so batches are bigger and throughput rises.
What is copy-on-write used for in PagedAttention?
Several sequences (parallel samples, beam branches, shared prefixes) point to the same physical blocks. A block is copied only when one of them needs to write to it.
FlashAttention does the same FLOPs. Where does the speedup come from?
From not writing and reading the N×N score and probability matrices to HBM. Tiling in SRAM with an online softmax and one fused kernel cuts memory traffic, turning a memory-bound computation into a compute-bound one.
Why does the online softmax need a running max?
Softmax is computed with the row max subtracted for numerical stability. When a later tile has a bigger max, the earlier partial sum and output must be rescaled by e^(m_old − m_new) so the final result is exact.
Resources
- Efficient Memory Management for Large Language Model Serving with PagedAttention (Kwon et al., 2023)
- vLLM docs · Paged attention kernel design and vLLM documentation
- SGLang documentation and TensorRT-LLM documentation
- FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness (Dao et al., 2022)
- FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning (Dao, 2023)
- FlashAttention-3: Fast and Accurate Attention with Asynchrony and Low-precision (Shah et al., 2024)
- flash-attention repository (kernels and supported GPUs)
- Llama 2: Open Foundation and Fine-Tuned Chat Models (Touvron et al., 2023): the 70B model with GQA
- NVIDIA Nsight Compute: measure a kernel's memory traffic yourself
- NVIDIA product pages: H100 and H200 (memory and bandwidth used above)