Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when serving or optimizing LLM inference in production — diagnosing or improving TTFT/TPOT/throughput, choosing batching strategy, sizing GPUs, picking vLLM/TensorRT-LLM, or debugging low GPU utilization, TTFT spikes, and OOM. Covers prefill vs decode, the roofline, continuous batching, PagedAttention, chunked prefill, disaggregation, and FlashAttention.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-21 | ✗→✓ | ▲ Improved | 390% | 0% |
| case-01 | ✓→✗ | ▼ Worse | 68% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 239% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 295% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 209% | 0% |
Serving an autoregressive transformer is two workloads wearing one trench coat. Prefill processes the whole prompt at once; decode emits one token at a time. They have opposite bottlenecks, so a config tuned for one starves the other. Master this split and the rest (roofline, batching, paging) follows.
| | Prefill | Decode | |---|---|---| | Work | Process all P prompt tokens in parallel | Generate token t from tokens 0..t-1 | | Matmul shape | [P, d] × [d, d] — tall, fat GEMM | [1, d] × [d, d] — GEMV per request | | Bottleneck | Compute (FLOPS) | Memory bandwidth (HBM) | | Passes per token | 1 (amortized over P tokens) | 1 full weight read per token | | KV cache | Writes P tokens of K,V | Reads all prior K,V, appends 1 | | Drives | TTFT (time to first token) | TPOT/ITL (time per output token) | | Parallelism | Across the sequence (free) | Across the batch (must aggregate) | | Scales with | Prompt length P (∝ FLOPs, ∝ P² attn) | Output length O (∝ steps) |
The asymmetry is the whole game. Prefill does ~P tokens of work in one weight-load, so it is compute-bound the moment P exceeds a few dozen. Decode loads all model weights to produce one token, so at batch 1 it is brutally memory-bound — the GPU's ALUs sit idle waiting on HBM. You fix them with different levers: prefill wants more FLOPS and work-splitting; decode wants more bandwidth and a bigger batch.
1/TPOT = per-stream tokens/s. The "reads smoothly" metric.TTFT + (O − 1) × TPOT for O output tokens. Long generations are TPOT-dominated; short ones are TTFT-dominated.tokens/s (decode) and req/s. The cost metric: $/token ∝ 1/throughput.> Rule: TTFT is a prefill problem, TPOT is a decode problem, throughput is a batching problem. Never debug them with one knob.
The shape of a request — prompt length P and output length O — determines which metric dominates and which phase you pay for:
| Lever | TTFT | TPOT | E2E | Throughput | KV memory | |---|---|---|---|---|---| | ↑ prompt P | ↑↑ (∝ P, attn ∝ P²) | ~flat | ↑ | ↓ (prefill steals compute) | ↑ | | ↑ output O | none | ~flat per token | ↑↑ (∝ O) | ↓ (long tail holds slot) | ↑ | | ↑ batch B | ↑ (queueing) | ↑ (contention) | ↑ | ↑↑ | ↑ |
Two profiles need opposite tuning:
P huge, O small): TTFT-dominated, prefill-bound. Fix with chunked prefill, prefix caching (RadixAttention), FP8 prefill, prefill-pool disaggregation. Batching barely helps — prefill is already compute-bound.P modest, O huge): TPOT- and E2E-dominated, decode-bound. Fix with bigger batch, KV quant, speculative decoding, higher-bandwidth GPU. A 2000-token generation at 42 ms/token = 84 s of pure decode — TPOT is the product.Arithmetic intensity (AI) = FLOPs performed ÷ bytes moved from HBM. Compare to the ridge point = peak compute ÷ peak bandwidth. Below the ridge → memory-bound; above → compute-bound.
| GPU | Dense BF16 | HBM BW | Ridge point (FLOPs/byte) | |---|---|---|---| | A100 80GB | 312 TFLOPS | 2.0 TB/s | ~156 | | H100 SXM | ~989 TFLOPS | 3.35 TB/s | ~295 | | H200 | ~989 TFLOPS | 4.8 TB/s | ~206 |
(Marketing "624/1979 TFLOPS" figures include 2:1 structured sparsity; use dense for serving math.)
For a model with N parameters, decode at batch B:
2 · N · B (one multiply-add per weight per sequence)2 · N (weights read once, shared across the batch; BF16 = 2 B/param)So at batch 1, AI ≈ 1 — three orders of magnitude under the ridge. Decode stays memory-bound until B ≈ 150–300 (the ridge), which is exactly why decode demands batching and a high-bandwidth GPU. Prefill, by contrast, has AI ≈ P (every weight reused across P tokens), so it crosses the ridge at small prompt lengths and is compute-bound — which is why prefill scales with FLOPS and benefits from FP8/sparsity, while decode does not until batched.
Worked example — same 70B model on one H100, both phases:
2N / HBM_BW = 140 GB / 3.35 TB/s ≈ 42 ms/token → ~24 tok/s. Bound by bandwidth (the 140 GB weight read), GPU compute idle.2·N·P / FLOPS = 2·70e9·4096 / 989e12 ≈ 0.58 s. Bound by compute (5.7e14 FLOPs), bandwidth slack.One model, one GPU, two limits: prefill is compute-bound (AI ≈ P = 4096 ≫ ridge 295) and decode is bandwidth-bound (AI ≈ 1 ≪ 295). That single contrast is why you never tune them with one knob. Batching amortizes decode's weight read across B streams, so aggregate throughput climbs near-linearly in B until you hit the ridge or the KV-cache wall — but prefill, already compute-bound, gets no throughput lift from batching, only from FLOPS (FP8, sparsity, faster GPU).
If decode SM occupancy is low but HBM bandwidth is pegged, that is correct — you are bandwidth-bound; add batch, don't chase FLOPS.
Static batching groups N requests, runs them lockstep, releases all when the slowest finishes. Catastrophic for LLMs: output lengths vary 10–100×, so a batch of 8 where one request emits 2000 tokens and seven emit 50 keeps those seven slots idle for the whole tail. GPU util craters.
Continuous batching schedules at the iteration (token) level: every decode step the scheduler can evict finished sequences and admit waiting ones into freed slots. No request waits for the batch; new arrivals join mid-flight.
| | Static | Continuous | |---|---|---| | Scheduling unit | Whole request | One token step | | Slot on completion | Idle until batch ends | Reused immediately | | New request | Waits for next batch | Joins next iteration | | GPU utilization | Low, sawtooth | High, sustained |
Effect: ~2–4× throughput over static batching at equal latency in the common case. vLLM's headline "up to 23×" is vs. naive HuggingFace generate() — a real but generous baseline; quote 2–4× as the defensible number. This is the single highest-leverage serving change; it is the default in vLLM, TensorRT-LLM, and TGI. Originated as iteration-level scheduling in Orca (OSDI '22).
Continuous batching needs many concurrent KV caches; naive contiguous KV allocation reserves max-sequence-length per slot and wastes 60–80% of KV memory (internal fragmentation + reservation). PagedAttention (vLLM) stores KV in fixed-size blocks (e.g. 16 tokens) with a block table per sequence — like OS virtual memory paging. Waste drops to < 4%, blocks are allocated on demand, and identical prefixes (system prompts, few-shot) are shared copy-on-write across requests.
More usable KV memory → larger batch → (see roofline) more throughput. PagedAttention is what makes continuous batching fit in memory. KV-cache sizing, block management, eviction, and KV quantization live in the KV-cache skill — cross-ref it; the OOM sizing formula is reproduced below for convenience.
> Related skills: KV-cache (the memory wall this skill keeps hitting), speculative-decoding (decode acceleration), quantization (bandwidth + memory relief). This skill is the systems view that ties them to TTFT/TPOT/throughput; each cross-ref skill is the deep dive.
Two distinct techniques for the prefill-blocks-decode problem:
Use chunked prefill first (one flag); reach for disaggregation when prefill/decode interference still violates SLOs at scale.
Standard attention materializes the [N, N] score matrix in HBM → O(N²) memory and HBM traffic; at long context this dominates and thrashes bandwidth. FlashAttention tiles Q/K/V into SRAM, fuses softmax, and never writes the full matrix to HBM (online softmax + recomputation in the backward pass).
O(N²) → O(N). HBM traffic: O(N²) → O(N²/M) for SRAM size M — a large constant-factor cut.Net: longer context at lower memory and bandwidth cost — directly relieves the decode bottleneck and enables larger batches. Default-on in every serious engine.
k tokens, the target verifies them in one forward pass; accepted tokens are free. ~2–3× decode speedup, output-distribution-lossless via rejection sampling. Win is gated by the draft acceptance rate (and draft cost). Exploits decode's idle compute (memory-bound → spare FLOPs). Cross-ref the speculative-decoding skill.Don't hand-roll the scheduler — pick an engine that already does continuous batching + paged KV, then tune it. Differentiators (qualitative; cross-engine speed numbers are version- and workload-dependent — benchmark on your traffic, don't trust a blog's multiplier):
| Engine | Core technique | Strength | Friction | |---|---|---|---| | vLLM | PagedAttention | Easiest path, broadest model + HW support, fast-moving | Python overhead at extreme low-latency | | TensorRT-LLM | Compiled engines, fused kernels | Top NVIDIA-only perf, FP8/INT4, in-flight batching | Build/compile step, per-model engine artifacts, setup friction | | SGLang | RadixAttention | Automatic radix-tree prefix caching — wins on shared system prompts, few-shot, agentic/multi-turn | Newer, smaller ecosystem | | TGI (HF) | Continuous batching | HF-native, production-hardened server, easy deploy | Fewer cutting-edge knobs | | LMDeploy | TurboMind | Strong throughput, good quantization support | Smaller community |
RadixAttention (SGLang) deserves a name: it auto-detects and reuses any shared prefix across requests via a radix tree of KV blocks — strictly more general than static prefix caching, and the right default when many requests share a long system prompt or conversation history.
Concrete knobs for a single-GPU latency-SLO chat deployment (vLLM-style names; map to your engine):
max_num_batched_tokens — cap to bound worst-case TTFT (e.g. 8192). This is your prefill-burst limiter.max_num_seqs — set from the KV formula, not guesswork: largest batch whose batch × max_context KV fits in GPU_mem − weights. Start below the OOM ceiling, leave headroom.gpu_memory_utilization ≈ 0.90 — give KV cache room without OOM on activation spikes.TP ≤ 8 and intra-node (NVLink).max_num_seqs until TTFT/TPOT SLO breaks; back off one step.Batch size is the master knob and it is a direct tradeoff:
| Batch ↑ | Throughput | TPOT (per-stream latency) | $/token | KV memory | |---|---|---|---|---| | effect | ↑ (until ridge/OOM) | ↑ (worse) | ↓ | ↑ |
You cannot maximize throughput and minimize latency at once — pick the operating point your SLO allows, then push batch to the largest value that still meets TTFT/TPOT (i.e. maximizes goodput). Cost follows directly from throughput:
$ / 1M tokens = GPU_$/hr ÷ (throughput_tok/s × 3600 / 1e6)At ~$2/hr for an H100: 2,200 tok/s → ~$0.25/M tokens; doubling throughput to 5,000 tok/s halves it to ~$0.11/M. This is why batching is an economic lever, not just a latency knob — every extra concurrent stream that fits before the KV wall divides fixed GPU cost across more tokens. A latency-SLO that forces batch down doesn't just slow you; it raises unit cost proportionally. That tension — cheaper-but-slower (big batch) vs faster-but-pricier (small batch) — is the frontier you're actually choosing a point on. Practice:
KV bytes = batch × seq_len × 2(K,V) × n_layers × n_kv_heads × head_dim × dtype_bytesn_kv_heads is where GQA/MQA shrink the cache (fewer KV heads than query heads). This product is your hard batch×context ceiling: total KV must fit in GPU_mem − weights − activations. Example — Llama-3-8B (32 layers, 8 KV heads, head_dim 128, BF16): per token = 2·32·8·128·2 ≈ 131 KB; 8k context = ~1.07 GB per sequence. On an 80 GB GPU after ~16 GB weights, you fit ~50–60 such sequences before OOM — that is your real max batch, usually before the roofline ridge.
Because decode loads weights once per step regardless of batch, aggregate decode throughput rises near-linearly with batch size — until one of two walls:
| Batch region | What limits you | Behavior | |---|---|---| | B small (1–8) | Bandwidth (weight load) | Per-stream TPOT ~flat; aggregate tok/s ≈ linear in B | | B mid | Still bandwidth, KV growing | Throughput climbs; KV memory creeps up | | B near ridge (~150–300) | Compute | Throughput flattens — you've crossed into compute-bound | | B past KV wall | HBM capacity | OOM, or scheduler throttles new requests |
For most real models the KV wall hits first: the Llama-3-8B example above caps at ~50–60 concurrent 8k sequences — well below the A100 ridge of ~156. So your max batch is almost always set by GPU_mem, not by the roofline. Levers to push the wall out (and the batch up): PagedAttention (recover 60–80% wasted KV), GQA/MQA (fewer KV heads), KV-cache quantization (FP8/INT8 KV halves bytes), and shorter max_context. Each one directly raises the achievable batch → throughput.
| Symptom | Cause | Fix | |---|---|---| | Low GPU util, sawtooth throughput | Static batching — slots idle for the slow tail | Continuous batching (vLLM/TRT-LLM/TGI) | | TTFT spikes under load | Long prompt's prefill blocks all decode (head-of-line) | Chunked prefill; cap prompt len; prefill/decode disaggregation | | OOM after a while at high concurrency | batch × context KV exceeds HBM | PagedAttention; cap max-batched-tokens; GQA; KV quant; shorter max-context | | Decode "slow" but SMs idle, BW pegged | Memory-bound by design at low batch | Increase batch; quantize weights; higher-BW GPU — not more FLOPS | | Throughput great, p99 latency awful | Batch too large for the latency SLO | Optimize goodput; lower max batch; split latency vs throughput pools | | TP scaling stalls past 8 GPUs | All-reduce comm dominates | Keep TP intra-node (NVLink); add PP across nodes | | Prefix-heavy workload, redundant compute | No prefix reuse | Prefix/KV caching (PagedAttention CoW shared blocks) |
The scheduler decides, each iteration, which waiting requests to admit and which running ones to continue or preempt. This is where TTFT and fairness are won or lost:
max_num_batched_tokens so a burst of long prompts can't starve decode.gpu_memory_utilization headroom traded for stability.You cannot tune what you measure wrong. Rules for a credible benchmark:
benchmark_serving, GenAI-Perf / NVIDIA Triton perf tools, or llmperf. Log per-request TTFT, TPOT, and SLO-attainment to derive goodput directly.Prefill is a sprint (compute), decode is a marathon (bandwidth). Batching amortizes decode's weight-load across runners; PagedAttention is the track that fits more runners; continuous batching keeps every lane full; chunked prefill stops the sprinter from tripping the marathoners. Tune for goodput at your SLO, and let the roofline tell you which resource you are actually short on before you spend on the wrong one.
Other measured skills in the registry, with their headline benchmark lift.