Inference
Percy takes a break from scaling laws to ask what happens after training. Prefill parallelizes like training, but generation emits one token at a time, so attention's arithmetic intensity is stuck below 1 and inference is memory-bound. Everything else follows from that one fact — the latency-throughput tradeoff, the war on KV-cache size, quantization and pruning, speculative sampling, and vLLM-style paging.
Inference is where the compute now goes
Training is a one-time cost; inference is a repeated one, incurred every single day. Percy’s calibration point: OpenAI is estimated to produce about 8.6 trillion tokens a day, while DeepSeek v4 was trained on 32 trillion — so in under four days of serving, the token count of a frontier training run goes by. And the agentic shift raises the stakes. A chatbot’s tokens are for a human to read, and humans read slowly; an agent’s tokens are mostly internal — thinking, tool calls, introspection — with only the final output meant for eyes. “You should really think about the number of tokens generated as really the compute spend,” and for an ambitious enough problem there is no ceiling on how much value more inference buys. Inference also sits inside the pipeline itself: generation-based evaluation and RL rollouts both need it. Hence a whole serving ecosystem — vLLM (the go-to), SGLang (strong for agentic workloads), TensorRT-LLM (very fast, narrower), llama.cpp (CPU inference).
But what does “fast” mean? Three metrics, for different settings:
- Time-to-first-token (TTFT) — how long a user waits before anything appears.
- Latency (seconds/token) — how fast tokens stream for one query.
- Throughput (tokens/second) — how fast tokens appear across many queries, the number a batch-processing job cares about.
The one high-level bit to remember: in training you see all the tokens at once — the sequence is just another dimension of a big tensor that gets multiplied. Autoregressive generation forbids that. Tokens come out sequentially, one at a time, so you cannot parallelize over the sequence dimension, and keeping the accelerator busy gets hard.
The KV cache, and the arithmetic intensity of generation
Naive inference refeeds the whole growing history through the transformer for every new token: each forward pass is , so generating tokens costs . But the transformer is causal — appending a token changes nothing about earlier positions — so the keys and values of the prefix can be computed once and reused.
Notation, borrowed from Google’s scaling book (much of this lecture follows it): batch, model dimension, the MLP dimension, query heads, key/value heads, head dimension with , and two sequence lengths — tokens conditioned on, tokens being generated. Training has ; generation has .
First a lecture-2 reprise. Multiplying () by () in bf16 reads bytes, does FLOPs, writes bytes; when is much smaller than and the intensity simplifies to just . The H100’s accelerator intensity is , so this matmul is compute-bound iff exceeds 295. The extreme case is a matrix–vector product with intensity 1 — you read a whole matrix to do only FLOPs — and “this is basically what happens with inference.”
Now do the same accounting for a transformer block, matmuls only (everything else is few FLOPs and fusable). The MLP’s three projections (up, gate, down) cost FLOPs against bytes; assuming is much smaller than and , the intensity is . Attention is a different story: reading , , and writing the output moves bytes for FLOPs, giving intensity
The batch dimension has vanished. Specialize both formulas to the two stages:
Latency, throughput, and the batch-size dial
Memory-boundedness simplifies everything: assuming compute and communication overlap perfectly, the time per step is just the memory that must move through HBM.
Shrinking the KV cache without losing the model
Memory is the bottleneck and the KV cache can outweigh the parameters at large batch size, so the recurring game is: reduce the KV cache, carefully, without giving up accuracy.
Grouped-query attention (GQA) keeps query heads but only key/value heads ( is standard multi-head attention; is multi-query attention, “which no one uses because it’s really bad”), cutting the cache by . Rerunning the Llama numbers with (a 1:5 ratio): at memory drops from 79.7 GB to 33.4 GB, latency improves to 10.0 ms/token, and throughput jumps to ~6,400 tok/s — reducing memory improves both metrics, since it’s only the batch dimension that puts them in tension. And the freed memory lets you push to (65.6 GB, ~13,000 tok/s). The GQA paper’s evals show little accuracy loss — though DeepSeek’s later tables show MHA beating GQA, so “take everything that’s not just math with a grain of salt.”
Multi-head latent attention (MLA), from DeepSeek, compresses instead of sharing: project activations down to a -dimensional latent (DeepSeek v2: from to ), cache only that, and materialize keys and values on demand. One wrinkle: MLA is incompatible with RoPE, so 64 extra dimensions are added for it (576 total). Their tables show MLA matching or slightly beating full MHA. Cross-layer attention (CLA) shares KVs across layers, the way GQA shares them across heads, improving the accuracy-versus-cache-size Pareto frontier. Sliding-window attention attends only to the last tokens, making the cache independent of sequence length (effective context still grows with depth, since information propagates across layers) — but it hurts accuracy, so practical models interleave local layers with full-attention layers. DeepSeek v4’s 1M-context stack piles on compressed sparse attention: compress every tokens into one, cheaply score and select a top- subset, compress further. And beyond the transformer sit linear attention and state-space models (Mamba, gated DeltaNet), which replace the cache with a fixed-size state — more powerful than a sliding window, but a lossy summary of the past — plus non-autoregressive diffusion models.
Quantize, prune, distill
Two more lossy levers, more systems- than architecture-flavored. Quantization reduces the precision of the numbers themselves: bf16 is the inference default, and the menu runs down through fp8, int8, and int4. You can train with quantization in the loop (quantization-aware training — simulate the round-trip during the forward pass; effective but expensive) or quantize after the fact (post-training quantization — cheap). Naive per-tensor scale-and-zero-point works poorly; GPTQ uses Hessian information to push each layer’s quantization error into the not-yet-quantized weights; activation-aware quantization (AWQ) observes that some activation channels are large, so the weights they touch matter more — keep 0.1 to 1 percent of weights in high precision and crunch the rest, getting fp16-to-int3’s 4x memory reduction with a 3.2x speedup.
Model pruning is cruder: rip parts out of a big model, then fix it up. NVIDIA’s recipe identifies important layers, heads, and hidden dimensions on a small calibration set (~1,024 samples, judging by activation magnitudes), removes the unimportant ones, and distills the original model into the pruned one to heal it — taking a 15B model to 8B with little accuracy loss and far less training than starting over. That’s the general pattern: either define a faster architecture and train it from scratch, or initialize the fast architecture from a good model and repair it with distillation.
Speculative sampling: a lossless shortcut
Everything so far trades accuracy for speed. Speculative sampling (also called speculative decoding) is the elegant exception. The asymmetry it exploits: prefill processes tokens in parallel and is compute-bound, generation is one-at-a-time and memory-bound — so checking a proposed sequence is much faster than generating it.
Too few draft tokens wastes the target model’s parallel checking; too many means frequent rejections; the sweet spot in the original paper sits around three or four. Draft models are typically an order of magnitude smaller (70B target with an 8B draft, 8B with 1B) and ideally distilled toward the target — so all the compression tricks above reappear here: if the shrunken model is good enough, serve it; if not, it makes a fine draft model. Extensions like Medusa (draft multiple tokens in parallel) and EAGLE (draft from the target’s high-level features) keep improving the drafter.
Serving live traffic: continuous batching and PagedAttention
A live server is nothing like training’s dense token blocks: requests arrive at different times, share prefixes (system prompts, multiple samples per prompt), and have different lengths. Two systems ideas handle the mess. Continuous batching (introduced by Orca) schedules at the iteration level: decode one step for every sequence in the batch, eject sequences that finish, and slot newly arrived requests in immediately rather than waiting for the batch to drain. Ragged lengths are handled by selective batching — attention has to process each sequence separately anyway (each has its own cache), but the MLP layers, which carry most of the FLOPs, can concatenate everything into one mega-sequence, e.g. lengths 3, 9, and 5 becoming a single tensor.
PagedAttention (the core idea of the vLLM paper) fixes how the KV cache is stored. Reserving a contiguous max-length buffer per request wastes memory exactly the way files fragment a hard drive: internal fragmentation (you reserved 1,024 tokens and generated 50) and external fragmentation (unusable gaps between allocations). The operating-systems people recognized their own problem and applied their own solution — paging. Chop each sequence’s cache into fixed-size blocks that can live anywhere in memory, keep an index, and share blocks across requests: a common system prompt is cached once for everyone, multiple samples from one prompt share its prefix, and copy-on-write splits a block only when two continuations diverge.
Check yourself
Napkin math in the lecture’s style — the formulas above and a calculator are all you need.
A model is generating (so ) with tokens of context on an H100 (accelerator intensity 295). What is the attention arithmetic intensity? What is the MLP intensity, and how many concurrent requests would make the MLP compute-bound? Why can’t the same batching trick save attention?
Show solution
Attention: — hopelessly below 295, and note that does not appear, so it stays there at any batch size. MLP: intensity is , so you need concurrent requests to cross the threshold. Batching helps the MLP because all sequences share the same weight matrices — read them once, use them times. In attention, each sequence has its own KV cache: is a batching dimension that appears in both operands and the output, so doubling doubles the bytes moved along with the FLOPs. It’s independent matrix–vector products, and more of them doesn’t raise the intensity of any one.
An 8B-parameter model served in bf16 (16 GB of weights) uses GQA with KV heads, , layers, and serves sequences of tokens. How big is the KV cache per sequence, and how many concurrent sequences fit on one 80 GB H100?
Show solution
Per sequence: MB. Budget left after weights: GB, so sequences. Note the cache term dominates the weights well before that: past , the server is spending more HBM on cache than on the model itself — which is why every KV-cache-reduction trick translates directly into batch size and throughput.
For the model of 10.2 on an H100 (bandwidth bytes/s), compute the theoretical latency and throughput at and at . Which setting would you pick for a chatbot, and which for overnight document processing?
Show solution
: memory per step GB, latency ms/token, throughput tok/s. : memory GB, latency ms/token, throughput tok/s. Doubling the batch made latency 1.5x worse but throughput only 1.3x better — the parameter reads are already amortized and the growing cache dominates. The chatbot wants (or less): users feel latency and TTFT. The overnight job wants (or as large as memory allows): nobody is waiting on any single sequence, only on the whole pile.
A target model over two tokens A and B has , ; the draft model has , . Walk one step of speculative sampling and verify the output distribution is exactly .
Show solution
The draft proposes A with probability 0.8; A is accepted with probability . It proposes B with probability 0.2; B is accepted with probability . On rejecting A (probability ), sample from the residual normalized: A’s residual is , B’s is , so the residual puts all mass on B. Totals: and . Exact — the draft model only determines speed (how often you get tokens accepted in cheap bursts), never the distribution.