LLM Inference | For You
Lecture 1 of 9

Principles, Metrics, and the Roofline

The main question of this lecture: why is generating text slow? And who do we blame: the compute or the memory?

You know the feeling. You type "write me a haiku about GPUs" into a chatbot, and the answer starts appearing... one... word... at... a... time. But notice: the first word took a noticeable pause, and everything after it pours out at a steady drip. That pause and that drip come from two different machines hiding inside one model. By the end of this lecture you will be able to put numbers on both of them, on real hardware, with real models.

Main idea: Everything in this course (paged KV storage, sparse attention, continuous batching, disaggregation, parallelism, kernels, speculative decoding) is an intervention on one ratio: the arithmetic intensity of decode, \[ I \approx \frac{2P\,B}{2P + B \cdot kv}. \] Keep this ratio in your pocket. We will pull it out in every lecture.

A Request's Journey: Prefill and Decode

When your request arrives at a serving engine, it passes through four stages:

1. Tokenize string → L token ids 2. Prefill all L tokens at once (a real GEMM!) 3. Decode loop 1 token per pass, one after another (a poor little GEMV) repeat until EOS / max_tokens 4. Detokenize ids → text you read
How to: read left to right. The dashed arrow is why your answer drips out word by word.

The single most consequential structural fact in all of LLM serving is the asymmetry between stages 2 and 3:

You can probably guess that one of these two is a much weaker use of a GPU than the other. Hold that thought. In a moment we will make it precise, and the number is going to surprise you. This one asymmetry returns as the batching problem (Lecture 4), the disaggregation problem (Lecture 5), and the kernel-split problem (Lecture 7).

The KV Cache: Legal, Minimal, and Expensive to Lose

Here is a puzzle. During decode, step \(t\) needs attention over tokens \(1..t\). Surely we don't re-run the whole prefix every step... or do we? What are we even allowed to reuse?

? Appending a new token at the end: does it change the hidden states of earlier tokens? Think about what causal masking means before you open this.

Possible answer

No. And this is provable, not folklore. In a causal decoder, attention at position \(s\) reads only positions \(1..s\). So once token \(s\) has been processed, none of its intermediate activations can ever change again. A tidy induction over layers makes it rigorous:

Setup. Let \(h_t^{(l)}\) be the hidden state at position \(t\) after layer \(l\). A causal layer computes \(h_t^{(l)} = \mathrm{MLP}^{(l)}\!\left(\mathrm{Attn}^{(l)}(q_t, \{K_s, V_s\}_{s\le t}) + h_t^{(l-1)}\right) + \dots\) The crucial detail is the index set \(\{s \le t\}\).

Base (\(l=0\)): \(h_t^{(0)} = \mathrm{Embed}(x_t) + \mathrm{pos}(t)\) depends only on \(x_{1..t}\). ✓
Step: if \(h_s^{(l-1)}\) depends only on \(x_{1..s}\) for all \(s\), then \(K_s^{(l)}, V_s^{(l)}\) depend only on \(x_{1..s} \subseteq x_{1..t}\), the query \(q_t\) depends on \(x_{1..t}\), and the MLP is position-wise. So \(h_t^{(l)}\) depends only on \(x_{1..t}\). ∎

Corollary (cache legality). Appending tokens cannot change \(h_s, K_s, V_s\) for any \(s \le t\). Storing prefix K/V and reusing them is exact, not approximate. The KV cache is licensed by causality. It is a structural property of the decoder, not an engineering trick.

Contrast: a bidirectional encoder replaces \(\{s\le t\}\) with all positions. There, one appended token changes every attention output in every layer, and any cache is instantly invalid. BERT could never have this for free.

Note: the KV cache is exactly the recurrent state of the transformer, unrolled as a recurrence over tokens. An RNN keeps its state in a fixed-size vector; a transformer keeps it as an ever-growing pile of K/V pairs. Same job, different appetite.

And what happens if we lose a KV entry? Its replacement cost is one full prefix re-prefill: deep-layer K/V are deep nonlinear functions of the whole prefix, so there is no shortcut from token ids back to \(K_s^{(l)}\). In other words, discarding is recomputing. This identity is the basis of the recompute-vs-swap trade-off we will keep meeting (Lectures 2, 4, 5).

Without caching, generating \(N\) tokens costs \[ \sum_{t=1}^{N} t = \frac{N(N+1)}{2} = O(N^2) \quad \text{token-forwards,} \] versus exactly \(N\) with a cache. For \(N = 2048\) that is 2,098,176 versus 2,048, about a \(1024\times\) difference. The cache is not an optimization; it is the difference between feasible and absurd. The price we pay is memory, and the state grows linearly with context length. How different architectures pay that price defines a whole spectrum:

SchemeState sizeFidelity
RNN / linear attentionfixed, \(O(1)\) in \(L\)lossy compression of the prefix
MLA (DeepSeek-V3)low-rank latent, ~576 elem/token/layernearly lossless
GQA / MHAfull (grouped) K and Vlossless
DSA (V3.2)state not reduced; a subset is read per steplossless state, sparse reads

Four generations of attack on the same term. Remember this table in Lecture 2!

Counting Bytes: How Big Is One Token of Memory?

Per token, per request, the cache stores K and V at every layer: \[ kv = \underbrace{2}_{K \text{ and } V} \times n_{layers} \times n_{kv} \times d_h \times b_{dtype} \quad \text{bytes/token.} \] Note that \(n_{kv}\) is the number of KV heads (after GQA/MQA grouping), not query heads.

Worked example (do it with me): Llama-3.3-70B (GQA): \(n_{layers}=80\), \(n_{kv}=8\), \(d_h=128\), BF16: \[ kv = 2 \times 80 \times 8 \times 128 \times 2\,\text{B} = 327{,}680\ \text{B} = 320\ \text{KB/token.} \] A single 128K-token request: \(327{,}680 \times 131{,}072 \approx 40\) GiB. That is half of an 80 GB H100, for one request's state. And GQA is already \(64/8 = 8\times\) compression over plain MHA!

DeepSeek-V3 (MLA): cache a 512-dim latent + a 64-dim decoupled-RoPE key = 576 elements per layer in FP8: \(576 \times 61 \approx 34\) KB/token, about 9.3× smaller.

TA says: the 2026 update split this formula in two. Kimi K3 is a hybrid of 69 KDA layers (a gated delta-rule linear attention: fixed recurrent state, \(S_t \in \mathbb{R}^{128\times 128}\) per head, never grows with \(L\), ≈ 0.20 GiB per request total) and 24 Gated-MLA layers (the usual linear-in-\(L\) cache, 27 KiB/token). At \(L = 1{,}048{,}576\), K3's state is ≈ 27.2 GiB vs ≈ 320 GiB for a GQA-70B-style model. That is about 12×, and the gap widens with \(L\), because part of K3's state simply never grows. The "RNN row" of our table became production reality. Two consequences for later: you can't prefix-cache a state that's overwritten in place (engines checkpoint it at block boundaries instead), and rolling it back for speculative decoding is its own adventure (Lecture 8).

Counting FLOPs: The 2P Rule

Now for the numerator of our ratio. How much compute does one token cost? Let's just count.

Main claim: one token (prefill or decode) costs approximately \(2P\) FLOPs, where \(P\) is the (activated) parameter count. One multiply–add per parameter per token. That's the whole rule.
? Why 2P and not, say, 7P? Try deriving it yourself for one linear layer \(Y = XW\), \(X \in \mathbb{R}^{M \times d_{in}}\), \(W \in \mathbb{R}^{d_{in} \times d_{out}}\). Count the multiply–accumulates.

Derivation

Each output entry \(Y_{ij}\) is a dot product of length \(d_{in}\): \(d_{in}\) multiplications and \(d_{in}-1\) additions ≈ \(2d_{in}\) FLOPs. With \(M \cdot d_{out}\) entries: \[ \mathrm{FLOPs}(Y = XW) = 2 M d_{in} d_{out}. \] The layer's parameter count is \(d_{in} d_{out}\), so per token (\(M=1\)) the cost is \(2 \times\) its parameters. Summing over every linear layer (the Q/K/V/O projections and the MLP) gives \(2\sum_i p_i = 2P\), since transformer parameters live almost entirely in these matrices. ∎

What did we ignore, and when does it bite?

The Roofline: Two Speed Limits and One Ridge

Every GPU kernel obeys two speed limits: peak compute \(\pi\) (FLOP/s) and peak memory bandwidth \(\beta\) (bytes/s). If your computation has arithmetic intensity \(I\) (FLOPs per byte of memory traffic), then \[ T \ge \max\!\left(\frac{\text{FLOPs}}{\pi},\, \frac{\text{bytes}}{\beta}\right) \quad\Longrightarrow\quad \text{attainable FLOP/s} = \min(\pi,\; \beta \cdot I). \]

The two limits meet at the ridge point \(I_{ridge} = \pi/\beta\). For the canonical GPU of this course, the H100 SXM (\(\pi \approx 990\) TFLOPS BF16, \(\beta \approx 3.35\) TB/s HBM3, 80 GB): \(I_{ridge} \approx 295\) FLOP/byte.

arithmetic intensity I (FLOP/byte) → log scale attainable FLOP/s → log scale 10⁰ 10¹ 10² π/β ≈ 295 10³ 10⁰ 10¹ 10² 10³ ≈3.35 TFLOPS memory-bound: β · I compute-bound: π ≈ 990 TFLOPS decode, B = 1 I ≈ 1 → 0.3% of π ! decode, B = 32 decode, B = 512 prefill (I ≈ L)
How to: find your kernel's intensity \(I\) on the x-axis and walk up to the roofline. The height there is the best performance physics will allow. Left of the ridge, HBM is the bottleneck; right of it, the tensor cores are.

Now the punchline. Decode at \(B=1\), FP16: each token costs \(2P\) FLOPs; each step must read all \(P\) parameters (\(2P\) bytes) plus a (then small) KV cache: \[ I_{decode}(B=1) \approx \frac{2P\ \text{FLOPs}}{2P\ \text{bytes}} = 1\ \text{FLOP/byte}. \] One FLOP per byte sits a factor of 295 to the left of the ridge, deep in memory-bound territory. Attainable compute: \(\beta \cdot I = 3.35\) TFLOPS. That is about 0.3% of what the tensor cores can do.

TA says: this is my favorite sentence in the whole course: during decode, the GPU is an extremely fast memory system with an incidental compute unit attached. When someone proposes to "optimize your decode kernels", your first question should be: which bytes do they eliminate? A kernel that makes a bandwidth-saturated GEMV 2× faster at math does exactly nothing.

Every decode step must read all the weights once, and no kernel, however clever, moves bytes faster than the memory hands them over. So nothing can beat \[ T_{step} \ge \frac{\text{weight bytes}}{\beta} = \frac{2P}{\beta}. \] For 70B FP16: \(T_{step} \ge 140\ \text{GB} / 3.35\ \text{TB/s} \approx 41.8\) ms, a single-request ceiling of \(1/T_{step} \lesssim 24\) tok/s. Only three levers move this bound: (i) shrink the bytes (FP8 halves it, INT4 quarters it); (ii) produce several tokens per weight read (speculative decoding, Lecture 8); (iii) buy more bandwidth.

? Kimi K3 is a 2.8T-parameter MoE, yet its batch-1 ceiling is higher than a 70B dense model's. Before opening: which term in \(2P/\beta\) actually applies to a MoE, and in which precision?

Possible answer

The bound reads weight bytes, not \(2P\). K3 activates only 104B of 2.8T parameters (16 of 896 routed experts + 2 shared), and stores routed experts in MXFP4, about 17.5 MB per expert. Routed-only traffic: \(17.5 \text{ MB} \times 16 \times 92 \approx 26\) GB per step, so \(T_{step} \ge 26\text{ GB}/3.35\text{ TB/s} \approx 7.8\) ms, i.e. ≲ 129 tok/s. (The full always-on BF16 parts push real traffic to ≈ 55–60 GB.) Observed on GB300: 111–118 tok/s. That is bandwidth theory predicting production reality within about 15%. Two morals: the bound transfers across hardware only through \(\beta\); and at ~90% MBU there is little kernel headroom left, so further gains must come from the three levers.

Batching: The Decode Intensity Formula

Fine, one request is memory-bound. But we don't serve one request. We serve many at once. Let \(B\) requests decode together, each with mean resident context \(L\). Count the two quantities for one batched decode step:

Numerator (FLOPs): each of the \(B\) tokens needs \(2P\) weight FLOPs, so \[ \text{FLOPs} \approx 2P\,B. \] Denominator (bytes): three contributions:

  1. Weights: \(2P\) bytes, read once per step, serving all \(B\) tokens. This amortization is the entire point of batching.
  2. KV cache: each request reads its own whole cache, \(L \cdot kv\) bytes. KV is private; it does not amortize. Total: \(B \cdot L \cdot kv\).
  3. Activations: \(O(B \cdot d_{model})\) per layer: negligible.

Therefore \[ I \approx \frac{2P\,B}{2P + B\, \cdot kv} \] (the short form absorbs the mean context \(L\) into the per-request KV footprint read each step). Divide top and bottom by \(B\) to see the two regimes: \[ I = \frac{2P}{\underbrace{2P/B}_{\text{weights} \to 0} + \underbrace{L \cdot kv}_{\text{KV: invariant}}} \qquad\Longrightarrow\qquad I \xrightarrow[B \to \infty]{} I_{sat} = \frac{2P}{L \cdot kv}. \] Batching can lift you off the floor. But there is a ceiling, set by KV, and it sinks as context grows. KV reads start to dominate once \(B \cdot L \cdot kv = 2P\), i.e. at \(B_{cross} = 2P/(L\,kv)\).

Worked example (reading a table like an engineer): Llama-3.3-70B FP16 on H100 (\(2P = 1.4\times 10^{11}\), \(kv = 327{,}680\) B, ridge 295):
\(L\)\(I\) @ B=1\(I\) @ B=32\(I\) @ B=256\(I_{sat}\)
5121.030.8195.9834.5
2,0481.027.7115.0208.6
8,1921.019.843.352.2
32,7680.99.312.413.0
131,0720.83.03.23.3
Read it column by column and the whole course falls out. (1) Throughput needs a large \(B\); that is where continuous batching comes from (Lecture 4). (2) \(B\) is capped by VRAM, since every added request carries \(L \cdot kv\) bytes of private state; that is where paged KV memory comes from (Lecture 2). (3) GQA/MLA/DSA shrink \(kv\), raising \(I_{sat}\) proportionally (Lectures 2–3). (4) Long context kills intensity: the KV term grows linearly in \(L\) no matter what (Lecture 3). And here is the sobering one: beyond \(L \approx 2P/(295 \cdot kv) \approx 1{,}446\) tokens, no batch size can ever make 70B decode compute-bound on an H100. The saturation intensity itself lies below the ridge.

Two Different Machines

Put the two phases side by side:

PrefillDecode
Linear-layer shapeGEMM, \(M = L\)GEMV / small GEMM, \(M = B\)
Attention\(L \times L\) lower-triangular1 query vs all history
Intensity\(\approx L\) (hundreds–thousands)\(\approx \frac{2PB}{2P + B \cdot kv}\) (1–50)
Bound bytensor-core FLOPS (\(\pi\))HBM bandwidth (\(\beta\)): weights + KV
Right metricMFUMBU

Prefill needs \(I > 295\), i.e. \(L\) of a few hundred tokens. That is a short paragraph of text. So nearly all real prompts are compute-bound in prefill; prefilling 4,096 tokens of 70B costs \(2PL \approx 573\) TFLOP ≈ 1.2 s of pure compute at a realistic 50% MFU. Different bottlenecks mean a mixed batch forces one kernel to serve two regimes at once. This is the direct motivation for prefill/decode disaggregation (Lecture 5) and chunked prefill (Lecture 4).

The Language of Metrics: MFU, MBU, TTFT, TPOT, ITL, Goodput

To talk about any of this honestly we need scorecards. Two utilization metrics; match each to its regime: \[ \mathrm{MFU} = \frac{\text{useful FLOPs/s achieved}}{\pi}, \qquad \mathrm{MBU} = \frac{\text{weight+KV bytes/s actually moved}}{\beta}. \] MFU is for compute-bound work (prefill, training); MBU is for decode, where good engines hold 60–80%. Reporting decode MFU (0.3%!) measures the wrong resource; reporting prefill MBU is equally meaningless.

Four latency metrics. Take a request issued at \(t_0\), first token at \(t_1\), tokens at \(t_1 < t_2 < \dots < t_{N_{out}}\), finish at \(t_e\): \[ \mathrm{TTFT} = t_1 - t_0, \qquad \mathrm{TPOT} = \frac{\mathrm{E2E} - \mathrm{TTFT}}{N_{out}-1}, \qquad \mathrm{ITL}_i = t_{i+1} - t_i, \qquad \mathrm{E2E} = t_e - t_0 = \mathrm{TTFT} + \sum_{i=1}^{N_{out}-1} \mathrm{ITL}_i. \]

Important: TPOT is an average; ITL is a distribution. Under chunked prefill or speculative decoding, TPOT can stay flat while ITL spikes periodically (a stall per prefill chunk; a burst per accepted speculative round). User-perceived stutter lives in ITL P99, not in TPOT. Report P50/P90/P99: a single 10 s queue stall moves a mean while P50 doesn't blink. And report input vs output tok/s separately: agent workloads routinely run input:output above 10:1, and a blended number hides which side drives cost.

Throughput determines cost: serving cost per token ≈ GPU-hours ÷ tokens produced, so doubling throughput halves per-token cost. (An H100 at $2/hour sustaining 3,000 output tok/s costs \(\frac{\$2}{3{,}000 \times 3{,}600} \times 10^6 \approx \$0.19\) per 1M output tokens; the intensity formula is literally a cost model.) But batching raises per-step time and queuing delay, so a throughput number without a latency constraint is meaningless. The honest form is goodput: \[ \text{goodput} = \frac{\text{requests (or tokens) completed per unit time that meet the SLO}}{\text{unit time}}. \] Timed-out requests don't count. A system can show high throughput while violating P99 TTFT for half its requests. Then goodput < throughput, and you're over usable capacity.

Prefix Caching: The (1 − h) Discount

Multi-turn and agent workloads resend the entire conversation history with each turn; the genuinely new content is a small suffix. With prefix caching, a hit means prefill computes only the uncached suffix. With hit rate \(h\) (fraction of input tokens whose KV is already resident): \[ \text{effective prefill FLOPs} \approx (1 - h) \cdot 2P \cdot L. \] DeepSeek reported a production input hit rate ≈ 56%. Agentic coding is higher still: Moonshot reports >90% on coding workloads whose typical prefix is ≈ 400K tokens and grows by ≈ 4K per turn.

? As \(h \to 1\), prefill compute vanishes. Does TTFT go to zero too? What becomes the bottleneck? Think about where the cached KV physically lives.

Possible answer

No. TTFT stops being a FLOPs question and becomes a placement/scheduling question: which replica holds the matching prefix, and can the request be routed to it? This motivates cache-aware routing and multi-level KV storage (Lecture 2): KV turns from per-request scratch space into a long-lived, cross-request asset. Route by cache affinity, not round-robin. A 10-point drop of \(h\) at \(L\) = 32K on a 70B model adds ≈ 460 TFLOP ≈ 0.9 s of prefill per request. Kimi K3's API even prices it directly: $0.30/MTok cache-hit vs $3.00/MTok cache-miss, which is exactly the 1/10 rule of thumb.

How to Benchmark Without Fooling Yourself

A benchmark has three defining elements. Get any one of them wrong and no number is comparable:

  1. Length distribution. ShareGPT-style chat, long documents, and agent/coding traces are different workloads. The 2026 extreme: 1M-context agents, three orders of magnitude from ShareGPT.
  2. Arrival process. Closed-loop (fixed concurrency) is self-back-pressured: queues never grow and capacity is systematically overestimated. Open-loop (Poisson or trace replay) is the honest mode: sweep the rate and find the goodput knee.
  3. Sampling and termination control. Uncontrolled output length makes engines incomparable; a lucky short run just looks faster. Fix it with ignore_eos + fixed max_tokens.
? A blog claims "engine X is 2.1× faster than Y". Setup: closed-loop client with 64 fixed connections; ShareGPT duplicated 1,000×; prefix caching ON for X, OFF for Y; measurement starts at process launch and runs 5 minutes; outputs uncontrolled; report shows mean TPOT and blended tok/s. Find at least five flaws before opening.

Possible answers (any five)

Tooling: vllm bench serve and SGLang's bench_serving drive an OpenAI-compatible endpoint end-to-end. Benchmark the full online path, not engine internals.

Research Thinking

How to: read the starting point; read the question and think, for a minute, a day, a week... and only then open the answers. You are not supposed to reinvent several months of someone's research; it's the habit of thinking that counts.

? Card 1, "invent" grouped-query attention. Starting point: KV is private per request and does not amortize: \(kv = 2\, n_{layers} n_{kv} d_h b_{dtype}\), and 70B-GQA already costs 320 KB/token. Question: which factor of \(kv\) can you attack without retraining the model's quality away? And what does the intensity formula promise you in return?

Some existing attempts

Attack \(n_{kv}\): query heads can share KV heads. MQA (all share one) → GQA (8 groups for 64 query heads: 8× compression, minimal quality loss) → MLA (don't store K/V at all: store a low-rank 576-element latent and re-derive K/V, about 9.3× under Llama-70B-GQA). Each step raises \(I_{sat} = 2P/(L \cdot kv)\) proportionally and pushes the ridge-crossing context out by the same factor. The DSA generation then keeps the state but stops reading all of it. Lecture 2 takes this thread and runs with it.

? Card 2, beating the 2P/β wall. Starting point: the single-request latency floor \(T_{step} \ge 2P/\beta\) exists because one weight read buys exactly one token. Question: what would it take to get several accepted tokens from one read of the weights? And why is verifying a proposed token almost free compared to generating it? (Hint: what does one batched/prefill-style pass over k tokens cost versus k sequential decode steps?)

Where this leads (Lecture 8)

Let a cheap "draft" propose several tokens, then check them all in one parallel forward pass: \(k\) tokens at GEMM cost \(2Pk\), which at small \(k\) still reads the weights once. Accept the longest agreeing prefix, roll the KV back, repeat. That's speculative decoding: exact (lossless), and it multiplies tokens per weight-read. K3's DSpark reaches 331–370 tok/s at batch 1, well over the 129 tok/s "impossible" ceiling we derived. The ceiling is only impossible for sequential one-token-per-read decode; keep the three levers honest.

? Card 3, who should live together? Starting point: prefill wants compute (\(I \approx L\)), decode wants bandwidth (\(I \approx 1\)–50): "two different machines". A mixed batch must serve both regimes in one kernel launch. Question: sketch two different system designs that resolve the tension. What does each one cost you?

Existing designs (Lectures 4–5)

(i) Chunked prefill (Sarathi): slice the prefill into chunks small enough to piggyback on decode iterations without blowing TPOT: one pool, carefully interleaved. (ii) PD disaggregation (Splitwise, Mooncake): run prefill and decode on different GPU pools sized to their own SLOs and ship the KV across the network. You pay a transfer cost and buy independent scaling. Which one wins depends on the workload; how much, and when, is Lecture 5's obsession.

Have Fun! The Intensity Playground

Enough reading. Now you drive the formula. Set a model, a batch size, and a context, and watch where you land on the roofline. Can you find a setting where decode is compute-bound? (And if not... why not?)



Playground physics: H100 roofline, ridge at π/β ≈ 295 FLOP/byte.

Seminar & Homework

Take Lab 1: Roofline and Metrics from the course's hands-on pack: run a real decode sweep with vllm bench serve, compute \(I\) at several \((B, L)\) points, compare against the theoretical table above, and report TTFT/TPOT/ITL percentiles plus a goodput curve under an SLO you choose. Bonus: find the goodput knee by sweeping the open-loop arrival rate.

Summary

← Course map Lecture 2: Systems Centered on the KV Cache →