LLM + RoboticsModule 09 · Problems 3–5

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.

Problems covered3 (size the KV cache) · 4 (PagedAttention) · 5 (why FlashAttention is faster)
Interview levelMid to senior. Problem 3 is a standard screen; 4 and 5 test whether you understand memory, not just names.
Key ideasKV bytes per token, GQA, KV precision, fragmentation, block tables, HBM vs SRAM traffic, tiling, online softmax
Time to prep3–4 h: 2 h reading, 1–2 h working the numbers and the exercise

Source: Vishal Singh's post of 25 inference-engineering interview problems · saved in sources/09-vishal-inference-25.md

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

  1. For each word in a conversation, the model saves a small "key" and "value" note in every layer. That's the KV cache.
  2. Notes grow with the length of the conversation and with the number of people chatting at once.
  3. 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.
  4. 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

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 × 32KTotalH100 SXM at 72 GB usableH200 at 126.9 GB usable
BF161,073.7 GB1,143.7 GB15.9 → 16 GPUs9.0 → 10 GPUs
FP8536.9 GB606.9 GB8.4 → 9 GPUs4.8 → 5 GPUs
4-bit268.4 GB338.4 GB4.7 → 5 GPUs2.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

LeverMemory savedCost
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 cache2× vs BF16Usually small quality loss; check long-context retrieval
4-bit or 2-bit KV4–8×Needs careful schemes (per-channel keys); more risk on long contexts
Shorter max context or fewer users per replicaLinearProduct limits, or more replicas
Offload cold KV to CPU memoryFrees HBMPCIe 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

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

KV per token
–
KV, all users at max
–
GPUs needed (90% usable)
–
Requests that fit: contiguous
–
Requests that fit: paged
–

Try this: the defaults are problem 3 with FP8 weights and FP8 KV on 8 H100 SXMs. KV is 160 KiB per token and 500 GiB for 100 users at 32K, and 9 GPUs are needed, so the bar crosses the dashed line. Switch GPU to H200: 5 are needed. Set KV heads to 64 (no GQA): 4,000 GiB of cache and 61 H100s. Back at the defaults, the "requests that fit" counts use the KV memory left on your GPUs after weights: contiguous allocation fits 94 requests, paged fits about 1,500, because real requests average 2,048 tokens, not 32K. GPU memory figures are from NVIDIA's product pages; lengths come from a seeded random draw with the average you set; the 90% usable share is an assumption.

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:

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:

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

ChoiceUpsideDownside
Small blocks (e.g. 8–16 tokens)Less waste per sequence, finer sharingBigger block tables, more scattered reads, less efficient kernels
Large blocks (e.g. 64–256)Contiguous reads, simpler kernelsMore waste in the last block; coarser prefix sharing
Preempt by recomputeNo CPU memory or PCIe trafficRedo the prefill later
Preempt by swapNo recomputePCIe 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

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:

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

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

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

Practise it

  1. 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 fp8 and confirm the token capacity roughly doubles.
  2. 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.
  3. Feel FlashAttention on Colab. On a free T4 GPU, time a naive PyTorch attention (explicit softmax(Q @ K.T) @ V) against torch.nn.functional.scaled_dot_product_attention for N = 512 to 8,192, recording time and peak memory with torch.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.
  4. 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

Why it matters for robotics

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