LLM inference serving: KV cache, batching, paging and speculative decoding
Before this lesson: the roofline, the ridge point and the matmul threshold (lesson 2); merging partial attention results and split-KV decoding (lesson 4); parameter counts (lesson 6); weight-only (W4A16) and W8A8 quantization (lesson 11). Text generation with a KV cache and grouped-query attention (deep learning 11, deep learning 9). For the proof in section 3: discrete distributions and expectation (probability 2). The model view of the KV cache and of decoding will be in the LLMs track (in preparation). Time: about 85 minutes, plus the notebook.
Why this matters #
For a widely used model, the total cost of serving can be larger than the cost of training. Each generated token reads every weight and the whole KV cache of its sequence from HBM, so the cost of serving is set by bytes, and by how many sequences share one read of the weights. Paged KV memory raised the throughput of LLM servers by 2–4× at the same latency, compared with FasterTransformer and Orca (Kwon et al., 2023). Speculative decoding gave 2–3× at small batch, with the same output distribution (Leviathan et al., 2023).
The open-source engines vLLM, SGLang and TensorRT-LLM combine all the methods of this lesson. The same numbers decide how many GPUs a chat service needs, how fast RL rollouts (many samples per prompt) run, and which quantization format to choose.
1. Predict first #
2. The picture #
Two phases. Generation starts with a prefill: one forward pass over the whole prompt. It writes the keys and values (K and V) of every prompt token into the KV cache and gives the first output token. Then decode runs one step per output token. Each step feeds one new token per sequence, appends its K and V to the cache, and attends over everything cached so far.
Simplest case: one sequence. One decode step multiplies one token vector by every weight matrix. Each weight is read from HBM once and used for one multiply-add: 2 FLOPs per 2 bytes in bf16, an intensity of 1 FLOP/byte. The ridge point of an H100 is about 295 FLOP/byte (lesson 2), so the tensor cores are idle more than 99% of the time. The step time is the time to read the weights: ms for an 8B model.
Batching shares the read. With sequences in one step, each weight that is read is used times: the same bytes, times the useful work. But each sequence brings its own cache. Its K and V serve only that sequence, so their bytes grow with the batch, and they need memory. The batch is limited by the free memory divided by the cache per sequence.
Move the batch from 1 to 64, then to 186. The top bars are the roofline times of one step: orange is the time to read the weights (dark) and the KV cache (light), blue is the compute time. The step takes the longer of the two. From to the work grows 64×, but the step time only doubles. Then the light part (the caches) takes over. The chart below shows the trade-off over all batch sizes: more tokens per second, but a longer time per output token. Try a long context, fp8 for the cache, or the 7B model without GQA, and watch and the memory bar.
A server is a memory manager plus a scheduler. The scheduler (continuous batching) decides at every step which sequences run. The memory manager (PagedAttention) gives KV memory to sequences as they grow. Speculative decoding attacks a different limit: the number of sequential steps per token.
Llama-3-8B in bf16 on one H100 (16.1 GB of weights, 128 KiB of KV cache per token). 32 sequences decode together, each with 4,096 cached tokens. About how long is one step?
The weights are read once: 16.1 GB. The caches are not shared: B GB. In total about 33.2 GB, and ms. The compute time is only about 0.6 ms, so the step is memory-bound. "5 ms" forgets the caches; "155 ms" forgets that the weight read is shared.
3. Definitions and main results #
Notation (as in the whole track). layers; query heads; key/value heads ( for multi-head attention, MHA; fewer for grouped-query attention, GQA; for multi-query attention, MQA); query heads per KV head; head dimension; model width; bytes per stored number (2 for bf16); parameters; bytes of weights; sequences in a decode step; cached tokens per sequence (the context); peak FLOP/s; BW the HBM bandwidth in bytes/s; the ridge point. H100 SXM: TFLOP/s (dense bf16), BW TB/s, FLOP/byte, 80 GB of HBM.
KV-cache memory and the batch that fits #
Definition. The KV cache stores, for every past token, one key vector and one value vector of length per KV head and per layer. So one token needs
The 2 counts K and V. Let be the memory left for the cache: the GPU memory minus the weights, the activations and the runtime buffers. Then tokens fit, and at context the batch is at most
GQA divides by (compared with MHA of the same shape); an fp8 cache halves .
Example. Llama-3-8B has , , . In bf16, B KiB per token. Llama-2-7B is about the same size but uses MHA with 32 KV heads: 512 KiB per token, 4× more.
A model has layers, query heads, KV heads and (the shape of Llama-3-70B). How many bytes of KV cache does one token need in bf16?
B KiB. Using the 64 query heads instead of the 8 KV heads gives 2.5 MiB, 8× too much. Forgetting the factor 2 for K and V gives 160 KiB. 4 KiB is one layer only: every layer has its own K and V.
Proposition (decode step: bytes, FLOPs, intensity and time) #
A decode step with sequences of context , in bf16 ( for the weights and the cache), reads at least bytes and does about FLOPs. Its intensity is a byte-weighted average of and :
Since in common models (–8 with GQA, with MQA), the step is memory-bound unless and the weights are most of the bytes.
Why this is true, in one sentence. The weights are shared by the whole batch, so each weight byte does units of work; the cache is private, so each cache byte does only units, whatever the batch is.
Proof.
- Weights. Every weight matrix is read once per step, whatever is: bytes. Each weight takes part in one multiply and one add per sequence: FLOPs. In bf16, , so this is .
- Cache. Each sequence reads its own K and V, bytes, so bytes in total. Nothing is shared across the batch.
- Attention FLOPs. Per sequence, layer and query head, the scores cost FLOPs and the weighted sum of the values another . In total per sequence. The cache of that sequence has bytes (with ). The ratio is : attention does FLOPs per cache byte.
- Intensity. Divide the FLOPs of steps 1 and 3 by the bytes of steps 1 and 2. The weights contribute intensity and the cache contributes , each weighted by its share of the bytes.
- Bound. The roofline gives , and the step is memory-bound exactly when . Since , the average reaches only if and is most of the bytes.
Left out: the query and output vectors, norms and activations ( bytes, small) and the KV writes of the new tokens. With other formats, the weight term becomes FLOP per weight byte and the cache term : int4 weights () make weight bytes 4× fewer and their intensity 4× higher.
Decode at a long context, where the KV caches are almost all of the bytes. You double the batch (the memory allows it). What happens to the intensity of the step?
is a byte-weighted average of (weight bytes) and (cache bytes). When the cache share is near 1, whatever is: every cache byte serves only its own sequence. Batching helps the weight matmuls, not decode attention. GQA (a larger ) is what raises the attention intensity.
When are the weight matmuls compute-bound? By the matmul result of lesson 2, a matmul is compute-bound from , about 300 rows for large . For Llama-3-8B: for the projections and for the MLP matrices. A batch of 326–345 sequences fits in memory only with short contexts (section 4).
Prefill is the opposite. A prompt of tokens goes through each weight matrix as one matmul, so its intensity is about (for ). A prompt of more than a few hundred tokens is compute-bound. Its time is about plus the attention FLOPs, about with a causal mask.
Latency and throughput: TTFT, TPOT, goodput #
- TTFT (time to first token) = the time in the queue + the prefill. It grows with the prompt length.
- TPOT (time per output token; also called inter-token latency) = the time between two later tokens of one sequence, about one decode step.
- Throughput = output tokens per second over all sequences, . It sets the cost per token.
- Goodput = the number of requests per second that finish within their latency targets (service-level objectives, SLOs), for example TTFT ≤ 1 s and TPOT ≤ 50 ms for 99% of requests.
A larger raises throughput, but each step reads more cache, so TPOT grows too: the curve in the figure above. A server chooses the largest batch whose TPOT still meets the SLO.
Continuous batching #
Static (request-level) batching starts a batch together and runs it until its longest request ends. Finished sequences keep their slot and idle. New requests wait for the whole batch (head-of-line blocking), so TTFT suffers.
Continuous batching (iteration-level scheduling; Orca, Yu et al., OSDI 2022) lets the scheduler decide again before every step. A finished sequence leaves at once; a waiting request joins at the next step, and its prefill runs in that step. The non-attention layers treat the batch as one flat list of tokens, so prefill and decode tokens of different sequences share one matmul (Orca's "selective batching"). Attention runs per sequence, so its kernel must accept ragged lengths, and the KV memory of any sequence must be able to grow at any step. Paging gives that.
Compare the two schedules of the same requests. Each row is a batch slot; each request starts with a prefill step and continues with decode steps. Violet cells are slots that do no work. In static batching, a short request holds its slot until the longest request of its batch ends. Press New random requests a few times, and make the output lengths more different (longest output): static batching gets worse, continuous batching stays near full use. One step costs about the same for any batch that fits, so fewer steps means more tokens per second.
PagedAttention #
Earlier servers stored the cache of each request as one contiguous tensor, sized for its maximum length. The vLLM paper (Kwon et al., 2023, §3) names three kinds of waste:
- reserved slots for future tokens, held for the whole life of the request;
- internal fragmentation: slots up to the maximum length that the request never uses;
- external fragmentation: gaps between buffers of different sizes, left by the allocator.
In their measurements, only 20.4–38.2% of the KV memory of existing systems held actual token states; vLLM reached 96.3%.
PagedAttention (§4.1–4.3) applies the idea of virtual memory:
- The KV memory is a pool of fixed-size physical blocks of tokens each (vLLM's default is ).
- Each sequence has a block table: logical block (tokens to ) maps to a physical block id anywhere in the pool.
- A sequence gets a new block only when its last block is full.
- The attention kernel follows the block table and reads K and V one block at a time. The partial results of the blocks are merged with the online-softmax update of lesson 4, so no contiguous copy is needed.
Waste bound. A sequence of tokens uses blocks, so it wastes at most slots, all in its last block. All blocks have the same size, so there is no external fragmentation.
Sharing (§4.4). A physical block can belong to several sequences; a reference count says how many. Parallel sampling (several answers to one prompt) and beam search map their prompt blocks to the same physical blocks.
- Copy-on-write: before a sequence writes into a block whose reference count is above 1, it gets a private copy, and the count of the shared block drops by 1. The last sharer finds a count of 1 and writes in place.
- Prefix caching: a request whose prompt starts with the same tokens as cached blocks (a system prompt, earlier chat turns) maps its first logical blocks to them and skips their prefill. The paper reserves blocks for known shared prefixes; later vLLM versions find a cached full block by a hash of its tokens and of all tokens before it.
Preemption (§4.5). When no block is free, some sequence must stop. vLLM serves requests first-come-first-served, so it preempts the newest request first: the oldest requests keep their progress. It evicts all blocks of a sequence or none, because a sequence needs all its blocks at every step. To restore the sequence later, it either swaps the blocks back from CPU memory or recomputes them with one prefill over the prompt plus the tokens generated so far.
Press Step a few times in the contiguous mode, then switch to paged and do the same. Each square is one token slot; a group of 4 is one block. Orange slots hold tokens; violet slots are reserved or allocated but empty (waste); a short vertical line marks the start of each reserved range. Notice the used share: low with reservation, close to 100% with paging. In paged mode, press Fork the oldest request: the new sequence shares all blocks (magenta frame, ×2). At the next step, watch copy-on-write in the message. Add requests until the pool is full to see preemption.
What paging costs. Reading through a block table made the attention kernel 20–26% slower (vLLM §7.1). The larger batch more than pays for it.
Theorem (speculative sampling) #
Setup. A cheap draft model gives next-token distributions ; the target model gives . In one round, the draft proposes tokens , one after the other, each sampled from . The target scores all positions in one forward pass, which gives at each position. For : accept with probability . At the first rejection, sample a replacement token from the residual distribution
and end the round. If all tokens are accepted, sample one bonus token from the target's distribution at position .
(a) Every emitted token has exactly the distribution , given the same prefix.
(b) If each draft token is accepted with probability , independently, the expected number of tokens per target pass is
In this lesson, is the acceptance rate and the draft length, as in the papers. This is not the message latency of lesson 7.
Small example first. Take two tokens, target and draft .
- The draft draws with probability 0.9 and keeps it with probability . So is drawn and kept with probability .
- The draft draws with probability 0.1 and always keeps it (): probability 0.1.
- A rejection happens with probability . The residual is , normalised to : after a rejection the output is always .
- In total: and . This is exactly .
The draft over-proposes ; the rule throws away exactly the surplus and gives it to the token the draft under-proposes.
One position, two views of the same tokens. The outlined bar is the draft ; the stacked bar is what the rule emits: the kept part and the rejected-then-resampled part. The dashed line is the target . The stacked bar always ends at the dashed line. Move the draft quality : the acceptance rate grows and the magenta part shrinks, but the total stays . Press Sample to check with 2,000 random draws (dots). Switch to 4 tokens for a less simple case.
Proof of (a) (one position; and are the distributions at that position).
- Token can be emitted in two ways: the draft draws and it is accepted, or a draft token is rejected and is drawn from .
- Drawn and accepted:
- The probability of a rejection is one minus the probability of a kept draft. Because ,
- This is exactly the normaliser of , so it cancels:
- Add the two ways:
(If , the sum is ; if , it is .)
A rejection ends the round, and the later draft tokens are discarded, because their prefix has changed. Each position is handled given the accepted prefix, so the whole sequence has the target's joint distribution. (If the residual is all zero, then and no rejection can happen.)
Proof of (b).
- One round gives (number of draft tokens accepted before the first rejection) + 1: the replacement token, or the bonus token if all are accepted.
- At least draft tokens are accepted exactly when the first all pass: for .
- For a count , the tail-sum formula for expectations gives
(Each outcome is counted once in each of the terms .) 4. Add the one extra token and use the geometric sum:
By step 3 of (a), the acceptance probability at one position is , where is the total-variation distance. In the example: . The rate is the average of this over positions; the independence in (b) is a simplifying assumption (Leviathan et al. make the same one).
Why it is faster: the roofline. Decode is memory-bound, so a target pass over positions costs about the same as a pass over one: the weights and the cache are read once, and the extra FLOPs stay below the ridge point. Let be the time of one draft step divided by the time of one target step. A round costs target steps and gives tokens, so the speed-up is
(Leviathan et al., Theorem 3.8). It needs a high and a cheap draft. It shrinks at large batch: the verify pass processes tokens per weight read; above the ridge point it is compute-bound, and the rejected tokens then cost real compute.
Press Simulate 12 rounds. Each row is one round: accepted draft tokens, then either the replacement token at the first rejection (later draft tokens are discarded) or, if all are accepted, a bonus token. The number at the right is the tokens of that round. The bars below are ; their sum is the expected number of tokens. Raise : the new bars get small fast, while the draft cost grows linearly, so the speed-up has a best .
Speculative decoding with acceptance rate and draft length . How many tokens does one target pass give on average?
. "" forgets the replacement or bonus token and ignores that a rejection ends the round; 5 is the maximum, reached only if all 4 drafts pass.
Prefill and decode interference #
A long prompt that enters the running batch makes that iteration long, and every decoding sequence waits: a TPOT spike. If the scheduler delays prefills instead, TTFT grows.
- Chunked prefill (Sarathi-Serve, Agrawal et al., OSDI 2024): split a long prompt into chunks of a few hundred tokens and put one chunk into each iteration, next to the decode tokens. A decode step is memory-bound, so its unused compute absorbs a chunk almost for free (section 4), and TPOT stays smooth.
- Prefill/decode disaggregation (DistServe, Zhong et al., OSDI 2024; Splitwise, Patel et al., ISCA 2024): separate GPU pools for prefill and decode; after the prefill, the KV cache is sent to a decode GPU. Each pool gets the batch size and parallelism for its own target (TTFT or TPOT), without interference. The cost: the KV transfer, two pools to size and balance, a more complex system. It pays off at large scale with strict latency targets and a fast interconnect.
A 32,768-token prompt arrives (magenta line) while 64 sequences decode. Each bar is one step; its height is the TPOT that all 64 sequences see (log scale). With one piece, one blue step lasts more than a second. Switch to chunked prefill: the prompt is spread over many short steps, and TPOT stays near the 50 ms target; the new prompt's TTFT grows a little. Switch to disaggregated: the decode GPU is not disturbed at all; the cost moves to a second GPU and a KV transfer.
Your server meets its TTFT target, but users see TPOT spikes of more than a second whenever a long document is pasted as a prompt. Which change fixes this most directly on the same GPUs?
The spike is one long iteration that contains the whole prefill; all decoding sequences wait for it. Chunked prefill puts at most a few hundred prompt tokens into each step, so every step stays short. A larger batch makes steps longer; speculative decoding and an fp8 cache shorten normal decode steps but do not split the long prefill.
Answers to section 1. (1) About 6 ms, not 150. The 16.1 GB of weights are read once for all 32 sequences (4.8 ms). The 32 caches add KiB GB, another 1.3 ms. The step does 32 times the work in 1.27 times the time. (2) No: speculative sampling keeps the target distribution exactly (part (a)). On average you get tokens per target pass.
Smaller caches: GQA, MQA, MLA and quantized caches
The KV cache per token is . Each factor has its own fix. GQA and MQA lower (Ainslie et al., 2023, show that MHA checkpoints can be converted to GQA with a little extra training). Multi-head latent attention (MLA, DeepSeek-V2, 2024) stores one compressed latent vector per token and layer (512 numbers, plus 64 for the rotary part of the key) instead of full K and V per head, and recovers K and V from it with matrices that are merged into the attention computation. An fp8 or int8 cache halves ; this doubles and halves the cache read time, at a small cost in accuracy (lesson 11). Sliding-window attention bounds for some layers. All of them raise either the batch that fits or the intensity of decode attention.
4. Worked example #
Serve Llama-3-8B (, , , , so ; , , MLP width 14,336) in bf16 on one H100 SXM (80 GB).
Step 1: memory. Weights GB. Assume GB remain for the cache after activations, CUDA graphs and runtime buffers. With KiB: tokens, that is 23,841 blocks of 16 tokens.
Step 2: the batch that fits. At : . Contiguous allocation for a maximum length of 8,192 tokens fits only sequences, whatever their real length. Paging counts only the live tokens, plus less than one block per sequence.
Step 3: step time at , from the proposition:
| bytes | FLOPs | (FLOP/byte) | bytes/BW | FLOPs/ | tokens/s | |
|---|---|---|---|---|---|---|
| 1 | 16.3 GB | 0.017 T | 1.0 | 4.87 ms | 0.02 ms | 205 |
| 8 | 18.2 GB | 0.14 T | 7.5 | 5.44 ms | 0.14 ms | 1,472 |
| 64 | 33.2 GB | 1.10 T | 33.0 | 9.92 ms | 1.11 ms | 6,450 |
| 186 | 66.0 GB | 3.19 T | 48.3 | 19.70 ms | 3.22 ms | 9,442 |
Every row is memory-bound. From to the throughput grows 31×; from 64 to 186 only 1.5×, because the cache reads now dominate the bytes. TPOT grows from 4.9 to 19.7 ms.
Step 4: when is decode compute-bound? The MLP matmuls need , the projections . At only 186 sequences fit, so never. With very short contexts, and , the cache is only 8.6 GB, , and the compute time (8.35 ms) exceeds the memory time (7.36 ms). Compute-bound decode needs large batches of short sequences.
Step 5: prefill and chunking. A 2,048-token prompt costs FLOPs (3% of it causal attention): 34 ms at peak, about 69 ms at 50% of peak. Take 64 decoding sequences with : a step needs 7.36 ms for its bytes but only 1.07 ms of compute. If the whole prompt joins one step at 50% of peak, that step takes about 71 ms: a 10× TPOT spike for all 64 sequences.
A chunk of prompt tokens adds about FLOPs and almost no bytes (the weights are read anyway). So it is free while the compute time stays below the memory time:
With chunks of 256 tokens, the prompt enters over 8 steps of 7.4 ms (at peak) or about 11 ms (at 50% of peak), instead of one step of 71 ms.
Step 6: disaggregation cost. The cache of a 2,048-token prompt is B MB: 5.4 ms over a 50 GB/s network link, 0.6 ms over NVLink (450 GB/s). This is small next to a 34–69 ms prefill.
Step 7: speculative decoding at batch 1. Take a draft of about 1.2B parameters with the same tokenizer (for example Llama-3.2-1B). Decode time follows the weight bytes, so . With and :
| 2 | 3 | 4 | 5 | 6 | 8 | |
|---|---|---|---|---|---|---|
| tokens per target pass | 2.44 | 2.95 | 3.36 | 3.69 | 3.95 | 4.33 |
| speed-up | 1.88 | 2.04 | 2.10 | 2.11 | 2.08 | 1.97 |
The best is 4–5. The verify pass scores tokens per weight read, an intensity of about 5, far below 295: it costs one memory-bound step, as the formula assumes. At the same pass has tokens per weight read, near the compute-bound thresholds of the matmuls (326–345), and the gain shrinks.
5. In code #
NumPy checks the numbers of section 4 and the theorem. The notebook has these cells and the exercises.
import numpy as np
# Llama-3-8B shape, bf16 weights and cache (e = 2 bytes), one H100 SXM
L, a, n_kv, d_h, e = 32, 32, 8, 128, 2
Psi = 8.03e9 # parameters
P, BW = 989e12, 3.35e12 # dense bf16 FLOP/s, HBM bytes/s
W = e * Psi # weight bytes
g = a // n_kv # query heads per KV head
k = 2 * L * n_kv * d_h * e # KV bytes per token (K and V, every layer)
print(k, k // 1024, "KiB") # 131072 128 KiB
M_kv = 50e9 # bytes left for the KV cache
print(int(M_kv // k), int(M_kv // (k * 2048))) # 381469 186
# Decode step at context C = 2048: bytes, FLOPs, intensity, roofline times
C = 2048
B = np.array([1, 8, 64, 186])
kv_bytes = B * C * k
n_bytes = W + kv_bytes
flops = 2 * Psi * B + 4 * B * C * L * a * d_h # weight matmuls + attention (q.K and p.V)
t_mem, t_comp = n_bytes / BW, flops / P
print((n_bytes / 1e9).round(1)) # [16.3 18.2 33.2 66. ]
print((flops / n_bytes).round(1)) # [ 1. 7.5 33. 48.3] far below 295
print((t_mem * 1e3).round(2), (t_comp * 1e3).round(2))
# [ 4.87 5.44 9.92 19.7 ] [0.02 0.14 1.11 3.22]
print((B / np.maximum(t_mem, t_comp)).round()) # [ 205. 1472. 6450. 9442.] tokens/s
# The intensity is a byte-weighted average of B (weights) and g (cache)
print(np.allclose(flops / n_bytes, (B * W + g * kv_bytes) / n_bytes)) # True
The speculative sampling theorem, checked step by step:
import numpy as np
rng = np.random.default_rng(0)
# 1. Part (a), step by step, on random target/draft pairs over 50 tokens
for _ in range(1000):
p = rng.dirichlet(np.full(50, 0.3)) # target distribution
q = rng.dirichlet(np.full(50, 0.3)) # draft distribution
kept = q * np.minimum(1.0, p / q) # step 2: equals min(p, q)
residual = np.maximum(p - q, 0.0) # unnormalised residual
p_reject = 1.0 - kept.sum()
assert np.isclose(p_reject, residual.sum()) # step 3
emitted = kept + p_reject * residual / residual.sum()
assert np.allclose(emitted, p) # step 5: exactly the target
print("exact on 1000 random pairs")
# 2. Part (b): Monte Carlo of one round with alpha = 0.8, gamma = 4
alpha, gamma = 0.8, 4
acc = rng.random((200_000, gamma)) < alpha
n_acc = np.where(acc.all(axis=1), gamma, acc.argmin(axis=1)) # accepted before the first rejection
print(round((n_acc + 1).mean(), 2), round((1 - alpha**(gamma + 1)) / (1 - alpha), 4)) # 3.36 3.3616
And the memory that paging saves, on 200 requests with live lengths between 100 and 3,000 tokens:
import numpy as np
# 200 running requests: live lengths (prompt + tokens so far), maximum length 8192
rng = np.random.default_rng(1)
live = rng.integers(100, 3000, size=200)
block = 16
reserved = 8192 * len(live) # contiguous: maximum length per request
paged = block * np.ceil(live / block).sum() # paged: whole blocks, on demand
print(live.sum(), reserved, int(paged)) # 322055 1638400 323568
print(f"used share: reserved {live.sum() / reserved:.1%}, paged {live.sum() / paged:.1%}")
# used share: reserved 19.7%, paged 99.5%
print("waste per request < one block:", bool((block * np.ceil(live / block) - live).max() < block))
# waste per request < one block: True
With reservation, about 20% of the cache memory holds tokens, close to the low end of the vLLM measurements; with paging, 99.5%.
6. Where it shows up #
- LLM engines (vLLM, SGLang, TensorRT-LLM) combine everything here: paged KV memory, continuous batching, prefix caching, chunked prefill, speculative decoding, and CUDA graphs for the decode step (lesson 13).
- Long contexts use split-KV decoding (lesson 4): at batch 1 the cache read must still be spread over all SMs.
- RL rollouts (as in GRPO) need throughput only, with many samples per prompt that share the prompt blocks. Agents and chat reuse long prefixes, so prefix caching saves most of their prefill.
- Quantization (lesson 11): weight-only int4 shrinks (shorter steps at small batch); an fp8 KV cache halves (twice the sequences, shorter long-context steps).
- ML for science. An autoregressive emulator (weather, Earth system) processes the whole state at every step: thousands of tokens per weight read, like a prefill. It is compute-bound even at batch 1 when its tokens per step exceed , which needs wide enough weights (lesson 2: ). It needs no KV cache unless it attends over past steps. A latent diffusion or rectified-flow sampler (for example for seismic ground motion) also runs the whole latent at every step; its lever is fewer steps, not a cache.
7. Common mistakes #
- Using instead of , or forgetting the factor 2 (K and V), in the KV size. For Llama-3-70B this is the difference between 320 KiB and 2.5 MiB per token.
- "Decode is slow because it needs much compute." It needs little compute. It is limited by reading the weights and the caches, so a GPU with more FLOP/s and the same bandwidth does not help.
- Expecting batching to make decode compute-bound. Batching helps the weight matmuls only. Each sequence reads its own cache (intensity ), so at long contexts the step stays memory-bound at any batch.
- Calling speculative decoding approximate. It is exact in distribution. (Outputs can still differ bit by bit, because different kernel shapes give different floating-point sums.) Its gain disappears at large batch, where the verify pass is compute-bound.
- Reporting only mean throughput. A server with high throughput and TPOT spikes fails its SLO. Report TTFT and TPOT percentiles, and goodput.
- Sizing the batch from the prompt length only. The cache grows with every generated token. A batch that fits at the start can run out of blocks later; this is why a server needs preemption.
8. Check yourself #
- Llama-2-7B has 32 layers, 32 KV heads and . Give its KV bytes per token in bf16, and
the number of 4,096-token sequences that fit in 50 GB. Compare with Llama-3-8B.
Answer
B KiB. One sequence needs B GB, so sequences. Llama-3-8B () stores 4× less per token: 93 sequences. - Llama-3-8B on one H100 with and : step time, tokens/s and compute time. Which
limit binds?
Answer
Bytes: 16.1 GB of weights + B GB of cache, 50.4 GB in total. So the step takes at least ms: about 4,250 tokens/s. Compute: ms plus 0.14 ms of attention. Memory-bound by more than 10×. - Parallel sampling: 3 answers to one 100-token prompt, blocks of 16 tokens. How many blocks without
and with sharing? What happens when each sample writes its first new token?
Answer
blocks (6 full, the last with 4 tokens). Without sharing ; with sharing 7, the shared ones with reference count 3. The first new token goes into the shared, partly filled last block: the first two writers trigger copy-on-write (each gets a private copy, and the count drops to 2, then 1), and the third writes in place. Total 9 blocks. - Speculative decoding with , and : expected tokens per target
pass and speed-up. Does the output distribution change?
Answer
tokens; . No: by the theorem, every token has the target distribution. - Why does vLLM preempt the newest request first, and evict all of its blocks? For a 1,500-token
Llama-3-8B sequence, compare the cost of swapping and of recomputation.
Answer
First-come-first-served: the oldest requests keep their progress. All blocks, because a sequence needs all of them at every step. Swap: KiB MB each way, about 4 ms at 50 GB/s over PCIe, plus CPU memory. Recompute: one prefill of FLOPs, about 24 ms at peak, with no CPU memory and no transfer. Here swapping takes less time; recomputation is simpler. - A 32,768-token prompt arrives while 64 sequences decode on the Llama-3-8B server (step about
7.4 ms). What happens to TPOT without chunked prefill, and what does chunked prefill change?
Answer
The prefill costs FLOPs: 0.8 s at peak, about 1.6 s at 50%. In one piece, all 64 sequences wait that long for one token: TPOT jumps more than 100×. With 64 chunks of 512 tokens, each step carries 1/64 of the prompt. The steps take 9.5 to 18 ms at peak (19 to 36 ms at 50%); the later chunks are slower because they attend over more earlier tokens. So TPOT rises for 64 steps instead of one long stall. The prompt's TTFT grows a little (0.89 s instead of 0.82 s at peak).
Write the KV-cache size per token.
bytes: K and V, every layer, every KV head, numbers of bytes. Llama-3-8B in bf16: 128 KiB.
Compute the KV cache per token of a model with , , in bf16.
B KiB.
Why is a decode step memory-bound?
Each weight byte is used for only multiply-adds and each cache byte for only about , both far below the ridge point (about 295 FLOP/byte on an H100). So the time is bytes / bandwidth.
In the decode intensity, which part grows with the batch and which does not?
The weight part has intensity (one read shared by all sequences). The cache part has intensity at any batch (each sequence reads its own cache).
What is the difference between TTFT and TPOT?
TTFT is the time to the first token: queueing plus prefill. TPOT is the time between later tokens: about one decode step.
What is the idea of continuous batching?
Re-decide the batch before every step (iteration-level scheduling): a finished sequence leaves at once and a waiting request joins at the next step.
What does a PagedAttention block table store, and what is the waste per sequence?
For each logical block of a sequence, the id of its physical block in the pool. The waste is below one block, all in the last block.
When does copy-on-write happen in PagedAttention?
When a sequence writes into a block whose reference count is above 1. It gets a private copy and the shared block's count drops by 1.
Give the acceptance rule of speculative sampling.
Keep draft token with probability . At the first rejection, sample from normalised, and end the round.
Expected tokens per target pass with acceptance rate and draft length ?
; for , : 3.36.
9. Interview angle #
- "Why is decode memory-bound, and where does batching stop helping?" The weights are shared (intensity about ); each cache is not (intensity ). KV memory caps , and TPOT grows with .
- "Estimate the step time of model X at batch B and context C." Bytes over the bandwidth; check that FLOPs/ is smaller. Then tokens/s .
- "How does vLLM work?" Paged KV memory (block tables, blocks on demand, reference counts, copy-on-write, prefix caching), continuous batching, and preemption of the newest request with recompute or swap.
- "Is speculative decoding exact? When does it not help?" Exact by the accept/resample rule. It does not help with a low , a slow draft, or a large batch.
- "TPOT spikes when long prompts arrive." Chunked prefill with a token budget per step, or separate prefill and decode pools.
- "Swap or recompute after preemption?" It depends. Recompute: one compute-bound prefill, no CPU memory. Swap: PCIe copies both ways. The vLLM paper found recomputation faster for small blocks and swapping faster for large blocks (§7.3).
10. Practice #
The notebook for this lesson runs in Colab, with NumPy only. It has five exercises, each with a check cell and a folded solution:
kv_bytes_per_token(cfg)andmax_batch(m_kv, C, cfg), checked on Llama-3-8B, Llama-2-7B and Llama-3-70B.decode_step(B, C, cfg, gpu): the roofline step time, tokens/s and the bound; find the largest batch whose TPOT meets a target.- A
BlockAllocatorwith block tables, reference counts and copy-on-write; compare its memory use with contiguous reservation on a synthetic request trace. speculative_round(p, q, rng): show by Monte Carlo that the outputs follow , and that the mean tokens per round match .- A small simulator of static and continuous batching: steps, slot use and mean latency.
11. Watch and read #
- Kwon et al. (2023), Efficient Memory Management for Large Language Model Serving with PagedAttention (vLLM, SOSP 2023): §3 (the three kinds of waste), §4.1–4.5 (paging, sharing, preemption), §7.1–7.3 (kernel overhead, block size, recompute vs swap).
- Leviathan, Kalman, Matias (2023), Fast Inference from Transformers via Speculative Decoding: §2.3 (the rule), §3.1 (the expected number of tokens), Theorem 3.8 (the speed-up), Appendix A.1 (the proof).
- Chen et al. (2023), Accelerating Large Language Model Decoding with Speculative Sampling: the same rule, found independently, on a 70B model.
- Yu et al. (2022), Orca: A Distributed Serving System for Transformer-Based Generative Models (OSDI 2022): iteration-level scheduling and selective batching.
- Agrawal et al. (2024), Taming Throughput-Latency Tradeoff in LLM Inference with Sarathi-Serve: chunked prefill and stall-free scheduling.
- Zhong et al. (2024), DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving; Patel et al. (2024), Splitwise.
- Stanford CS336, Language Modeling from Scratch (Spring 2025), lecture 10 "Inference": arithmetic intensity of prefill and decode, KV-cache size, batching, speculative sampling and paging (the lecture video is on YouTube, Stanford Online).
- Austin et al., How to Scale Your Model, All About Transformer Inference: latency and throughput estimates, the design of an inference engine, and speculative sampling.
- nano-vllm: a readable vLLM in about 1,200 lines of Python (the scheduler and the block manager with reference counts and prefix hashing).
Runs in your browser with nothing to install. The notebook repeats the key demos and has exercises with checks; solutions are inside, folded.
No Google account needed: Python runs inside your browser. The first start downloads about 40 MB; your changes are saved in this browser only.