Part III - Production inference
Benchmarking and Runtime Tuning
Lesson 07 spread a model across several GPUs and showed how interconnect topology decides whether that split helps or hurts. You now have a serving deployment that runs — but someone files a ticket that says "the model feels slow," and you have no idea whether that means the queue is backed up, the prompts got longer, the batch is too big, or a quantization change last week quietly broke the task. This lesson turns that vague complaint into a controlled experiment. The core discipline is treating a benchmark as a contract: a number like "1,800 tokens/s" means nothing until you also state the prompt length, output length, concurrency, sampling settings, and hardware it was measured on. We will pin down that contract, walk the coupled runtime knobs that move the numbers, and prove with arithmetic why a tidy synthetic benchmark can lie about production by a factor of sixty.
New capability: A repeatable method for measuring serving performance under a stated workload, and for tuning the runtime knobs as a coupled set while watching latency, throughput, and task quality together — so you optimize the thing the user feels, not a benchmark artifact.
1 · A benchmark is a workload contract
Imagine two teams both report "we get 2,000 tokens/second on an H100." One measured it sending 64-token prompts and asking for 8 tokens out, 256 requests at once. The other measured 4,000-token prompts asking for 1,000 tokens out, 4 requests at once. These are not the same model behaving inconsistently — they are two entirely different workloads that happen to share a hardware label. Comparing their tokens/s tells you nothing. The number is meaningless until the workload is pinned down.
So the first artifact of any tuning effort is not a graph — it is a workload contract: the full set of parameters that, once fixed, make a measurement reproducible and comparable. Change any one of them and you are measuring a different thing.
The mental model: a benchmark result is a function of all of these, written down together, or it is an anecdote. A throughput figure presented without its contract is the serving equivalent of a price with no currency.
2 · Two clocks: TTFT vs TPOT, and why the workload shape matters
Earlier lessons introduced the two latencies that matter to a user. We need them sharp here because they are governed by different physical resources, which is the whole reason a single benchmark cannot characterize a model.
Prefill is compute-bound; decode is memory-bandwidth-bound. That single sentence is the roofline intuition behind everything in this lesson. Prefill processes all prompt tokens in parallel through dense matrix multiplies, so it saturates the GPU's arithmetic units — more FLOPs, more tensor cores help. Decode generates one token at a time, so each step does very little arithmetic but must stream the full weight matrices and the KV cache out of HBM (high-bandwidth memory — the GPU's on-package RAM). Decode is gated by how fast you can move bytes, not multiply them. (The fix for that idleness — batching many decode steps together so one weight read serves many requests — is exactly why concurrency raises throughput.)
The consequence for benchmarking is direct: a prefill-heavy workload (long prompts, short answers — RAG, summarization, classification) and a decode-heavy workload (short prompts, long answers — chat, code generation) stress different ceilings. A knob that helps one can be irrelevant or harmful to the other. So you benchmark them separately, with their own contracts, and you never average them into one headline number.
3 · The 60x lie — when a synthetic benchmark mispredicts production
Here is the worked number that justifies the whole "contract" discipline. Prefill compute scales roughly linearly with prompt length (each token's attention also grows, but the dominant term for these sizes is the per-token matmul work). Suppose your engineering team benchmarks with neat 64-token prompts, while production traffic — a RAG assistant stuffing retrieved documents into context — averages 4,000-token prompts.
There is a redeeming twist that the synthetic benchmark also misses, in the other direction. Production RAG prompts often share a long, identical prefix — the same system prompt, the same retrieved boilerplate. With prefix caching (the engine keeps the KV-cache blocks for a prefix it has already computed and reuses them across requests), the expensive 4,000-token prefill is paid once and skipped on every subsequent hit. A short-prompt benchmark has no shared prefix, so it can neither see the 60x penalty nor the cache that rescues it. Only a benchmark whose contract mirrors the real prompt structure — length and reuse — predicts production at all.
4 · The runtime knobs are one coupled system
vLLM-style engines expose a handful of knobs that look independent and are not. Move one and the others' best settings shift. Define them once, then see why they couple.
| Knob | What it does | Improves | Can hurt |
|---|---|---|---|
max-num-seqs | Cap on concurrent sequences in a batch | Throughput, GPU utilization | p99 TTFT; KV pressure → preemption |
max-num-batched-tokens | Token budget per scheduler step (prefill + decode) | Prefill throughput, fairness | Big prefills monopolize a step, stalling decode |
| KV-cache dtype (fp16 → fp8) | Bytes per cached key/value | ~2x more concurrency in same HBM | Small quality loss on some tasks |
| Weight quantization (AWQ/GPTQ/int4) | Bytes per weight | HBM footprint, sometimes decode speed | Task quality, runtime/kernel compatibility |
| Prefix caching | Reuse KV blocks for shared prefixes | Repeated prefill cost (huge for RAG) | Wasted memory when reuse is low |
| Chunked prefill | Slice long prefills, interleave with decode | Mixed-workload utilization, decode fairness | More complex latency tuning |
Quantization means storing weights (or the KV cache) in fewer bits than the trained fp16 — int8, int4, fp8. It shrinks HBM use and can speed memory-bound decode, but it is lossy: the model's answers can shift. Pruning is the sibling technique — removing weights (or whole structures) deemed redundant — same trade: smaller and faster, at some quality cost. Both are bets that the task you care about tolerates the approximation, and that bet must be measured, never assumed.
Why they couple: raise max-num-seqs and each extra sequence wants KV-cache space, so you hit HBM limits sooner — unless you switched KV dtype to fp8 (frees room) or enabled prefix caching (frees room when prefixes repeat) or quantized the weights (frees room for cache). Push max-num-batched-tokens high for prefill throughput and a single 4,000-token prefill can eat an entire scheduler step, stalling every other request's decode and spiking TPOT — which is exactly what chunked prefill exists to prevent. You cannot tune these one at a time; you fix the workload contract, sweep one knob, measure the full vector, and only then move the next.
max-num-seqs to maximize tokens/s. Throughput climbs because bigger batches amortize the memory-bound weight reads of decode — but every request now waits behind a fuller queue and a heavier prefill step, so p99 TTFT explodes. A config that wins the throughput headline can violate your latency SLO on the very tail of traffic that users complain about. The widget below makes this curve draggable.5 · Pair every speed number with a quality number
The single rule that turns runtime tuning from folklore into engineering: a performance experiment that does not also measure quality is incomplete. Quantization, pruning, and aggressive batching can all silently move the model's answers. The platform must be able to make a sentence like: "this fp8 KV-cache config is 20% cheaper and raises concurrency 1.9x, and it changes our task-eval score from 0.84 to 0.83 on the regression suite." Without the second clause you have not made a decision — you have made a guess that looks like a decision.
So run a fixed task evaluation (the held-out set that represents what users actually need — correctness, format adherence, safety, refusal behavior) before and after every compression or knob change, and report the delta next to the speed delta. The dangerous quality losses are rarely uniform: int4 might be fine on chat but wreck a math or code task, fp8 KV cache might be invisible on short answers but drift on long ones. The eval set is the only instrument that catches it; tokens/s never will.
6 · Startup time is part of the workload too
One number the synthetic benchmark forgets entirely: how long a replica takes to become ready. A serving pod must pull a multi-gigabyte image, download or mount tens-of-GB of weights, allocate the KV cache, and warm the engine before it serves a single token — often 60-300 seconds. That latency is invisible at steady state but decisive the moment you autoscale (next lesson): if cold start is 4 minutes and a traffic spike arrives in 1, the new replicas are useless for the spike. So startup time belongs in the tuning budget — bake weights into the image or use a fast model-streaming mount, pre-allocate the cache, and measure cold-start as a first-class metric, not an afterthought.
7 · Where it breaks and what to check
Failure modes
- Synthetic short-prompt benchmark predicts production badly. Real prompts are long and often share a cacheable prefix; a 64-token benchmark misses both the 60x prefill penalty and the prefix-cache rescue. Signal: p99 TTFT in production is many times the benchmark's, and it tracks prompt length, not concurrency.
- Quantization improves cost but silently degrades the task. int4/fp8 shrinks HBM and may speed decode, but task accuracy slips on exactly the workload you care about. Signal: dashboards are all green while user-reported answer quality or eval scores fall — because nothing was watching quality.
- Knobs tuned for peak throughput blow up p99. A huge
max-num-seqswins the tokens/s headline while violating the latency SLO on the tail. Signal: mean latency looks fine, p99 TTFT is 5-10x p50, and complaints cluster at peak traffic. - Knobs tuned in isolation. Raising concurrency without freeing KV space (fp8/prefix cache/quant) triggers cache overflow and preemptions. Signal: throughput drops as concurrency rises, with preemption/recompute counters climbing.
- Early stops inflate tokens/s. Generation that hits a stop token at 50 tokens instead of the contracted 500 makes throughput look great while measuring a different workload. Signal: measured output length far below the contract.
Implementation checklist
- Is there a written workload contract — input/output length distributions, concurrency, sampling, hardware, model + runtime version — attached to every result?
- Do you benchmark prefill-heavy and decode-heavy workloads separately, never averaged into one number?
- Does every run record the full vector together: TTFT (p50/p99), TPOT, tokens/s, GPU util, HBM occupancy, queue time, and a quality score?
- Are
max-num-batched-tokens,max-num-seqs, KV-cache dtype, quantization, and prefix caching tuned as a coupled set, re-baselining after each change? - Is there a p99 SLO that any throughput-maximizing config must respect, and do you pick the largest safe concurrency rather than the peak of the throughput curve?
- Do you run a fixed task eval before and after every compression and report the quality delta beside the speed delta?
- Is cold-start time measured as a first-class metric, since autoscaling depends on it?
Checkpoint exercise
max-num-seqs? KV dtype?). Then state the p99 SLO and the one quality metric you would gate the change on. Justify why a single combined benchmark would have hidden the right answer for at least one of them.Where this points next
You can now characterize a serving deployment under a stated load and tune it without fooling yourself. But all of that assumes a fixed number of replicas. Real traffic breathes — and the moment you add or remove replicas, two things you measured here become load-bearing: the cold-start time that decides whether new capacity arrives before the spike passes, and the latency/throughput curve that tells the autoscaler what "saturated" even means for a GPU (CPU and request-count signals lie for LLM serving). Lesson 09 — Autoscaling and AI-Aware Routing — builds the machinery that scales replicas on GPU-aware signals like queue depth and KV-cache pressure, and routes requests to exploit the prefix caching and warm replicas you tuned here.
max-num-batched-tokens, max-num-seqs, KV-cache dtype, weight quantization, prefix caching, chunked prefill — are one coupled system: tune them together, sweep one at a time, keep p99 inside the SLO instead of chasing the peak of the throughput curve, and pair every speed change with a before/after task-eval delta. Quantization and pruning trade quality for cost; that trade is only honest when it is measured.Interview prompts
- Why is "2,000 tokens/s on an H100" a meaningless number on its own? (§1 — throughput is a function of the workload contract; without input/output length, concurrency, sampling, and hardware+version it describes two different workloads sharing a label.)
- Why must prefill-heavy and decode-heavy workloads be benchmarked separately? (§2 — prefill is compute-bound and decode is memory-bandwidth-bound; they stress different ceilings, so a knob that helps one is irrelevant or harmful to the other and an averaged number hides both.)
- How can a synthetic benchmark mispredict production by 60x? (§3 — prefill cost scales with prompt length; 4,000/64 ≈ 62x, turning a ~5 ms prefill into ~310 ms that lands in TTFT, flipping which latency dominates. A short-prompt run also misses the shared-prefix cache that rescues it.)
- Why can't you tune the vLLM knobs one at a time and stop? (§4 — they couple: more concurrency needs KV space freed by fp8/quant/prefix caching; a big batched-token budget lets one prefill stall decode unless chunked prefill intervenes. Sweep one, re-baseline, repeat.)
- Why is maximizing tokens/s the wrong objective? (§4 — bigger batches raise throughput but blow up p99 TTFT; the right target is the largest concurrency that stays under the latency SLO, not the peak of the throughput curve.)
- What makes a quantization or pruning decision honest? (§5 — a before/after task-eval delta reported beside the speed/cost delta; the loss is non-uniform across tasks, so only the eval set catches it — tokens/s never will.)
- Why does startup time belong in the tuning budget? (§6 — cold start (image pull, weight load, cache alloc) is 60-300 s and invisible at steady state, but decisive for autoscaling: if cold start outlasts the spike, new replicas are useless. Measure it as a first-class metric.)