Inference & Serving
Training a model is a one-off cost. Serving it is a cost you pay on every token, for every user, forever. This page covers how an LLM actually generates text on a GPU, why the two phases of generation have opposite bottlenecks, and the stack of techniques (KV caching, continuous batching, paging, prefix reuse, disaggregation, speculative decoding, quantization, parallelism) that modern engines use to turn a fixed GPU budget into as many tokens as possible within a latency target. Interviewers ask about this because it is where model knowledge meets systems engineering: a strong candidate can do the back-of-envelope arithmetic on a whiteboard and explain which knob moves which metric.
TL;DR: the things to be able to say out loud
- Generation has two phases. Prefill processes the whole prompt in parallel and is compute-bound. Decode produces one token per step per sequence and is memory-bandwidth-bound, because every step re-reads all the weights (and the KV cache) to do very little math.
- Arithmetic intensity of a decode matmul is roughly the batch size in FLOPs per byte. An H100 needs a few hundred FLOPs per byte to be compute-bound, so batch-1 decode uses well under 1% of the chip's FLOPs.
- Batch-1 decode speed ceiling ≈ memory bandwidth ÷ bytes of weights. 8B model in BF16 on an H100: about 3.35 TB/s ÷ 16 GB ≈ 200 tokens/s, before overheads.
- The KV cache stores keys and values of past tokens so decode is \(O(n)\) per step instead of recomputing everything. Size per token = \(2 \cdot L \cdot n_{kv} \cdot d_{head} \cdot \text{bytes}\). For Llama-3-70B that's about 320 KiB per token, so it quickly rivals the weights. GQA, MLA and FP8/INT4 KV shrink it.
- Metrics: TTFT (time to first token, driven by prefill and queueing), TPOT/ITL (time per output token, driven by decode), throughput, and goodput (throughput that meets the SLOs). Bigger batches raise throughput and worsen per-user latency.
- Continuous batching (Orca) schedules at the iteration level, so finished sequences leave and new ones join every step. PagedAttention (vLLM) stores the KV cache in fixed-size blocks with a block table, killing fragmentation and enabling sharing.
- Prefix caching (RadixAttention in SGLang) reuses KV for shared prompt prefixes. Chunked prefill splits long prompts so they don't stall decodes. Prefill–decode disaggregation runs the phases on separate GPU pools and ships the KV cache between them.
- Speculative decoding: a cheap drafter proposes \(k\) tokens, the target model verifies them in one forward pass, and a rejection-sampling rule makes the output distribution exactly the target's. Speedup depends on the acceptance rate. Variants: draft model, Medusa, EAGLE-1/2/3, lookahead, n-gram, MTP heads.
- Weight-only quantization (GPTQ, AWQ, INT4/INT8) speeds up decode because decode is bandwidth-bound. Weight+activation quantization (SmoothQuant, FP8, FP4) speeds up compute-bound work like prefill and large-batch decode.
- Tensor parallelism cuts latency (weights are read in parallel), pipeline parallelism adds capacity, expert parallelism is how big MoE models are served, often with data-parallel attention.
- Cost per million tokens = GPU $/hour ÷ (tokens/s × 3600) × 10⁶. Output tokens cost more than input tokens because decode is the inefficient phase.
1. The autoregressive loop
A decoder-only transformer defines \(p(x_t \mid x_{<t})\). To generate, you run the network on the prompt, get a distribution over the vocabulary for the next position, pick a token (the sampling step), append it, and repeat until an end-of-sequence token or a length limit. In pseudocode:
tokens = tokenize(prompt)
kv = model.prefill(tokens) # one big parallel pass over the prompt
logits = kv.last_logits
while not done:
t = sample(logits, temperature, top_p, ...) # pick next token
stream_to_user(t)
logits, kv = model.decode_step(t, kv) # one token in, one distribution out
Two facts about this loop drive everything on this page.
- The output is sequential. Token \(t+1\) cannot be computed until token \(t\) is chosen, because it is an input. Within a single sequence you cannot parallelize across output positions (speculative decoding is a clever way around this).
- Each step only needs one new row of computation, provided you saved the intermediate state (keys and values) of all earlier tokens. That saved state is the KV cache. Without it each step would recompute attention over the entire prefix: \(O(n^2)\) work per step, \(O(n^3)\) per sequence in the attention part. With it, each step is \(O(n)\) for attention plus a constant amount of weight math.
So a request has two very different phases. The prefill (also called the prompt or context phase) processes all \(n\) prompt tokens at once, fills the KV cache and produces the first output token. The decode (generation) phase then runs one step per output token, each step processing a single new token per sequence against the cached state.
"Why can't you just parallelize generation?" A strong answer: tokens within one sequence depend on each other, so decode is inherently sequential per sequence. You get parallelism by batching across sequences (continuous batching), by verifying several guessed tokens at once (speculative decoding), or by splitting each step's math across GPUs (tensor parallelism). Then mention the KV cache as what makes each step cheap.
2. Prefill vs decode: the arithmetic-intensity argument
The key concept is arithmetic intensity: FLOPs performed per byte moved from memory (HBM). A GPU has a peak compute rate \(F\) (FLOP/s) and a peak memory bandwidth \(B\) (bytes/s). A kernel with intensity \(I\) runs at \(\min(F,\ I \cdot B)\). The crossover \(I^* = F/B\) is the "ridge point" of the roofline. Below it you are memory-bound and FLOPs sit idle. Above it you are compute-bound. Austin+ 2025 (Scaling Book)
For an H100 SXM, NVIDIA lists 3.35 TB/s of HBM3 bandwidth and ~989 dense BF16 TFLOPS (the spec sheet's 1,979 figure is with 2:4 sparsity). NVIDIA H100 So $$ I^*_{\text{H100, BF16}} \approx \frac{989\times 10^{12}}{3.35\times 10^{12}} \approx 295\ \text{FLOPs/byte}. $$ With FP8 compute the ridge doubles to roughly 590. Any kernel doing less than a few hundred FLOPs per byte loaded leaves the tensor cores waiting.
Why a linear layer's intensity ≈ the number of tokens in the batch
Take one linear layer \(Y = XW\) with \(W \in \mathbb{R}^{d \times d}\) in BF16 (2 bytes per value), and \(X\) holding \(T\) tokens (rows). FLOPs: \(2Td^2\). Bytes: the weights \(2d^2\), plus activations \(2Td + 2Td\). When \(T \ll d\) the weights dominate: $$ I \approx \frac{2Td^2}{2d^2 + 4Td} \approx T \quad (\text{for } T \ll d). $$ In plain terms: each weight byte you load gets used once per token in the batch. That gives the whole story:
- Prefill of a 2,000-token prompt has \(T = 2000\), well above 295, so it is compute-bound. Doubling prompt length roughly doubles prefill time (plus a quadratic attention term that matters at long context).
- Decode at batch size \(b\) has \(T = b\) (one new token per sequence). At \(b=1\), \(I \approx 1\): the GPU runs at about 1/300 of its peak FLOPs. Adding more sequences to the batch is almost free until \(b\) approaches the ridge point, because you're paying for the weight read anyway.
Attention during decode is worse: batching doesn't help it
The weight matmuls are shared across the batch, so batching amortizes them. Decode attention is different: each sequence reads its own KV cache (\(n\) cached tokens) to do about \(2 \cdot n \cdot d\) FLOPs per head group. The intensity stays around 1 FLOP/byte for MHA no matter how big the batch (GQA raises it by the group factor, since several query heads share one KV head; MLA raises it further). So at long contexts and large batches, decode time becomes dominated by streaming KV caches, not weights. This is why KV-cache size is a first-class serving concern.
| Prefill | Decode | |
|---|---|---|
| Tokens per sequence per pass | All \(n\) prompt tokens | 1 (or \(k{+}1\) with speculative decoding) |
| Bottleneck | Tensor-core FLOPs | HBM bandwidth (weights + KV cache) |
| Arithmetic intensity (linear layers) | ≈ prompt tokens in batch (hundreds to thousands) | ≈ batch size (1 to a few hundred) |
| Drives which metric | TTFT | TPOT / ITL, and total throughput |
| What helps | FP8/FP4 compute, FlashAttention, prefix caching (skip work), more FLOPs (TP) | Bigger batches, smaller weights (weight-only quant), smaller KV (GQA/MLA/KV quant), speculative decoding, more bandwidth (TP across GPUs) |
| Scaling with length | Linear in \(n\) for MLPs, quadratic in \(n\) for attention | Per step linear in context length (KV read) |
Think of the GPU as a factory with a huge assembly floor (tensor cores) fed by a narrow loading dock (HBM). Decode at batch 1 is like trucking in the whole factory's tooling to build one widget, then trucking it back out. Batching means building hundreds of widgets per tooling delivery. Prefill already has hundreds of widgets queued, so the floor is the limit, not the dock.
Saying "decode is slow because the model is big, so we need more FLOPs." More FLOPs do nothing for batch-1 decode. What helps is fewer bytes per token (quantization, smaller KV), more bandwidth (HBM3e, splitting weights across more GPUs with TP), or more useful tokens per weight read (batching, speculative decoding). Also don't say MFU is the right metric for decode; memory-bandwidth utilization (MBU) is.
- How to Scale Your Model: Inference chapter: rooflines for transformer inference, worked numbers for TPUs and GPUs.
- Transformer Inference Arithmetic (kipply): the classic back-of-envelope post on memory vs compute bounds and KV cache.
- Efficiently Scaling Transformer Inference (Pope+ 2022): partitioning strategies and the latency/throughput Pareto frontier.
3. Latency metrics, SLOs and the latency–throughput trade-off
| Metric | Definition | Dominated by | Typical target (chat, rough) |
|---|---|---|---|
| TTFT (time to first token) | Request arrival → first output token streamed | Queueing + prefill (+ KV transfer if disaggregated) | a few hundred ms to ~1–2 s |
| TPOT (time per output token) / ITL (inter-token latency) | Average (TPOT) or per-gap (ITL) time between streamed tokens after the first | Decode step time, which grows with batch size and context; stalls from prefills sharing the GPU | 20–50 ms (20–50 tok/s per user); faster than reading speed |
| E2E latency | TTFT + (Nout − 1)·TPOT | Output length × TPOT for long answers | Depends on task; reasoning models make this huge |
| Throughput | Tokens/s (input, output or total) or requests/s across the whole server | Batch size, KV capacity, kernel efficiency | Maximize, subject to SLOs |
| Goodput | Requests/s that meet all SLOs (e.g. P90 TTFT < X and P90 TPOT < Y) | The scheduler's ability to keep both phases inside budget | The number you actually optimize |
Goodput was popularized by DistServe, which defines it as the maximum request rate served within both TTFT and TPOT constraints. Zhong+ 2024 The point is that raw throughput can be gamed: you can push tokens/s up with a giant batch while every user waits 200 ms per token. Always quote throughput at a latency target, ideally a tail percentile (P90/P99), since averages hide stalls.
The latency–throughput curve
Model one decode step as reading the weights \(W\) plus all active KV caches, at bandwidth \(B\), with compute \(2Pb\) FLOPs for \(P\) parameters and batch \(b\): $$ t_{\text{step}}(b) \approx \max\!\left(\frac{W + b \cdot \text{KV}_{\text{avg}}}{B},\ \frac{2Pb}{F}\right), \qquad \text{throughput} = \frac{b}{t_{\text{step}}},\qquad \text{TPOT} = t_{\text{step}}. $$ At small \(b\) the weight read dominates, so \(t_{\text{step}}\) barely moves while throughput grows linearly with \(b\): free throughput. As \(b\) grows, the KV term (and eventually compute) takes over, TPOT rises, and throughput flattens. Beyond that point you're only adding latency. The operator's job is to sit at the knee of this curve, at the largest batch that still meets the TPOT SLO. A memory ceiling also exists: KV capacity caps \(b \times\) context.
Little's law ties it to load: concurrent requests in the system \(= \lambda \times\) mean time in system. If you serve 20 requests/s each lasting 10 s end to end, you hold about 200 sequences in flight, which sets your KV memory need.
Expect "your P99 TTFT spiked but GPU utilization is fine, what's happening?" Good answers: queueing because KV memory is full (requests wait for blocks, or get preempted and recomputed); long prompts monopolizing prefill (fix with chunked prefill or disaggregation); cache misses after a routing change (prefix cache hit rate dropped); a burst of very long contexts. Show you'd look at queue depth, KV utilization, preemption count, prefix-hit rate, and batch composition, not just GPU util.
- DistServe (Zhong+ 2024): the goodput framing and why mixing phases hurts it.
- DistServe blog (Hao AI Lab): an accessible walk-through of TTFT/TPOT trade-offs.
- Anyscale: continuous batching: measured throughput vs latency across batching schemes.
4. Back-of-envelope: how fast can decode be?
At batch 1 each decode step must read every weight once (dense model). So the ceiling is $$ \text{tokens/s}_{\,b=1} \;\lesssim\; \frac{\text{memory bandwidth}}{\text{bytes of weights}} . $$ Real systems hit roughly 60–85% of peak bandwidth, and also read the KV cache, so treat this as an upper bound.
Worked example on an H100 SXM (≈3.35 TB/s, 80 GB)
| Model / precision | Weight bytes | Fits on | Ceiling (tok/s, batch 1) | Realistic (×~0.7) |
|---|---|---|---|---|
| Llama-3-8B, BF16 | ~16 GB | 1 GPU | 3350/16 ≈ 210 | ~140–170 |
| Llama-3-8B, INT4 weights | ~4.5 GB (incl. scales) | 1 GPU | ≈ 740 | ~400–500 (kernel/overhead limits bite) |
| Llama-3-70B, BF16 | ~140 GB | 2 GPUs (TP=2) | 6700/140 ≈ 48 | ~30–38 |
| Llama-3-70B, FP8 | ~70 GB | 1 GPU (tight: ~8 GB left for KV) | 3350/70 ≈ 48 | ~30–35 |
| Llama-3-70B, FP8, TP=4 | ~70 GB over 4 | 4 GPUs | 13400/70 ≈ 190 | ~100–130 (all-reduce costs grow) |
Note the TP rows: splitting weights across \(g\) GPUs multiplies aggregate bandwidth by \(g\), which is why tensor parallelism is the standard latency lever. It isn't perfectly linear because each layer adds two all-reduces whose latency doesn't shrink.
Adding batch: the aggregate throughput estimate
Llama-3-8B BF16 on one H100, batch 64, average context 2,048 tokens. KV per token for this model is 128 KiB (computed in the next section), so KV for the batch is \(64 \times 2048 \times 128\ \text{KiB} \approx 17\ \text{GB}\). Each step reads \(16 + 17 = 33\) GB, so \(t_\text{step} \approx 33/3350\ \text{s} \approx 10\) ms. Compute per step is \(2 \times 8\times10^9 \times 64 \approx 1\) TFLOP, about 1 ms at 989 TFLOPS. Still bandwidth-bound. So:
- TPOT ≈ 10 ms (100 tok/s per user), and aggregate ≈ 64/0.010 ≈ 6,400 output tok/s as a ceiling, perhaps 3,000–4,500 in practice.
- Compare batch 1 at ~200 tok/s: batching bought ~30× throughput for a 2× hit on per-user speed. That is the economics of serving in one line.
- Memory check: 16 GB weights + 17 GB KV + activations and CUDA graphs fit in 80 GB, so you could push the batch further until KV fills HBM or TPOT breaks the SLO.
Prefill estimate
Prefill FLOPs ≈ \(2P\) per token (plus attention). For 8B params, that's 16 GFLOP/token. At 989 TFLOPS and a realistic 40–60% MFU, that's roughly 25k–37k prompt tokens/s per GPU. A 4,000-token prompt takes on the order of 110–160 ms of prefill. For a 70B model on one GPU in FP8 (≈2× BF16 FLOPs), scale by 70/8 and divide by about 2: around 400–600 ms for the same prompt. Those are the TTFT floors before queueing.
The canonical whiteboard question is "estimate tokens/s for model X on GPU Y." Score points by (1) separating prefill (FLOPs ÷ peak × MFU) and decode (bytes ÷ bandwidth), (2) remembering the KV term grows with batch × context, (3) checking memory capacity, (4) stating the efficiency fudge factor out loud, and (5) noting what changes with MoE (decode reads only active experts at small batch, but large batches touch nearly all of them).
5. The KV cache
What is cached and why
In each attention layer, every token produces a query \(q\), key \(k\) and value \(v\). The output for position \(t\) is \(\text{softmax}(q_t K_{\le t}^\top/\sqrt{d})V_{\le t}\). With causal masking, the keys and values of earlier tokens never change once computed, because they depend only on earlier tokens. So you compute them once, store them per layer, and at each decode step compute only the new token's \(q,k,v\), append \(k,v\) to the cache, and attend over the cache. Queries are not cached since each is used only at its own step.
Size formula
$$ \text{KV bytes} = 2 \times L \times n_{kv} \times d_{head} \times \text{bytes/elem} \times \text{seq\_len} \times \text{batch} $$The 2 is for K and V. \(L\) is layers, \(n_{kv}\) is the number of KV heads (equal to query heads for MHA, fewer for GQA, 1 for MQA), \(d_{head}\) is head dimension.
Worked examples
| Model (config) | Per-token KV (BF16) | One 8k-token sequence | 32 seqs × 8k |
|---|---|---|---|
| Llama-3-8B: L=32, nkv=8 (GQA, 32 query heads), d=128 Llama 3 2024 | 2·32·8·128·2 = 131,072 B = 128 KiB | 1.07 GB | 34 GB |
| Llama-3-70B: L=80, nkv=8 (64 query heads), d=128 | 2·80·8·128·2 = 327,680 B = 320 KiB | 2.7 GB | 86 GB (more than the FP8 weights) |
| Hypothetical 70B with full MHA (nkv=64) | 2.5 MiB | 21 GB | 687 GB |
| Llama-3-70B with FP8 KV | 160 KiB | 1.3 GB | 43 GB |
| DeepSeek-V3 with MLA: 61 layers, cache a 512-dim latent + 64-dim RoPE key per token per layer DeepSeek-V2 2024 | 61·576·2 ≈ 69 KB | ~0.56 GB | ~18 GB |
The 70B row is the headline: at modest batch × context, KV memory exceeds weight memory, so KV capacity, not compute, decides how many users a GPU can hold. The MHA counterfactual shows why nearly every modern model uses GQA or something stronger.
How to shrink it
| Technique | Mechanism | Reduction | Cost |
|---|---|---|---|
| MQA Shazeer 2019 | All query heads share one K/V head | \(n_{heads}\times\) | Some quality loss, training instability reported |
| GQA Ainslie+ 2023 | Query heads in groups share a K/V head (e.g. 64 query, 8 KV) | \(n_{heads}/n_{kv}\) (8× for Llama-3-70B) | Near-MHA quality; can be "uptrained" from MHA checkpoints |
| MLA (DeepSeek-V2/V3) | Cache a low-rank latent \(c_t\) per token; K and V for all heads are up-projected from it. At inference the up-projections can be absorbed into the query and output projections, so attention runs directly against the latent | Large: tens of times smaller than equivalent MHA | More complex kernels (e.g. FlashMLA); decoupled RoPE needed; TP by heads no longer shards the cache |
| KV quantization | Store K/V in FP8 (common, near-lossless), INT4 or 2-bit (KIVI: keys per-channel, values per-token) Liu+ 2024 (KIVI) | 2× (FP8) to ~8× (2-bit) | FP8 usually fine; lower bits need care, especially for keys with outlier channels |
| Sliding-window / local attention | Some or all layers attend only to the last \(w\) tokens (e.g. local/global interleaving) | Cache bounded by \(w\) on those layers | Architectural; must be trained in |
| Eviction / sparsity (attention sinks, H2O-style heavy hitters) | Keep only initial "sink" tokens plus a recent window, or tokens with high attention mass Xiao+ 2023 | Bounded cache | Lossy; fine for streaming chat, risky for retrieval over long docs |
| Offload | Move cold KV to CPU DRAM or SSD; bring it back on reuse | Capacity, not size | Transfer latency; worth it when recompute would cost more |
Forgetting the factor of 2 (K and V), using query heads instead of KV heads for a GQA model, or forgetting that the cache is per layer. Another: claiming MLA "reduces compute." It mainly reduces cache size and bandwidth; the latent-attention math can actually be more FLOP-heavy, which is a good trade in the bandwidth-bound decode phase.
"How many concurrent 32k-context users can one 8×H100 node serve with Llama-3-70B in FP8?" Weights ~70 GB across the node; 640 GB total HBM; reserve ~10% for activations, leaving ~500 GB for KV. KV per user at 32k = 320 KiB × 32k ≈ 10.7 GB in BF16 KV, ~5.4 GB in FP8 KV. So ~45 users with BF16 KV, ~90 with FP8 KV, before considering prefix sharing. Then say the real number depends on actual (not max) context lengths, which is exactly what paging exploits.
- GQA (Ainslie+ 2023): grouped KV heads and uptraining from MHA.
- DeepSeek-V2 (2024): Multi-head Latent Attention and the absorbed-projection trick.
- FlashMLA: production decode kernels for MLA.
- KIVI (2024): why keys and values want different quantization axes.
6. Batching: static, dynamic, continuous
Batching is the main way to raise arithmetic intensity in decode. The question is when sequences enter and leave the batch.
| Scheme | How it works | Problem |
|---|---|---|
| Static batching | Collect \(b\) requests, run them together until all finish, then take the next batch | Short responses finish early and their slots sit idle while the longest one finishes. New arrivals wait for the whole batch. Padding wastes compute. |
| Dynamic batching (classic model servers) | Form a batch when it's full or a timeout expires, then run it to completion | Fixes the arrival side for fixed-cost models (e.g. classifiers), but generation length still varies, so the same idle-slot problem applies. |
| Continuous / in-flight batching (Orca's iteration-level scheduling) Yu+ 2022 (Orca) | The scheduler decides batch membership every iteration. A finished sequence leaves immediately; a waiting request joins at the next step (its prefill runs in that iteration) | Requires handling ragged batches (different lengths, some in prefill, some in decode). Orca's "selective batching" batches the linear layers across all tokens but runs attention per sequence. |
Continuous batching is now universal in serious engines (vLLM, SGLang, TensorRT-LLM's "in-flight batching", TGI). Anyscale measured large throughput gains over static batching on real workloads, on the order of up to ~20×+ in their benchmark, with most of it coming from not wasting slots and from fitting more sequences via better memory management. Anyscale 2023
Continuous batching exposed the next bottleneck: memory. If each sequence pre-reserves a contiguous KV region sized for the maximum length, you can only fit a handful of sequences and most of the reserved memory is empty. That's what PagedAttention solves.
Scheduling policies worth knowing
- FCFS with admission control: admit a waiting request only if enough free KV blocks exist for its prompt (plus a margin).
- Preemption: when the cache runs out mid-generation, evict a sequence and either swap its blocks to CPU or recompute its KV later (recompute is often cheaper because prefill is efficient). vLLM supports both.
- Prefill-priority vs decode-priority: running new prefills ASAP minimizes TTFT but stalls ongoing decodes (ITL spikes); deferring them protects ITL. Chunked prefill and disaggregation reduce this conflict.
- Cache-aware / priority scheduling: SGLang sorts waiting requests to maximize prefix-cache hits; production systems add priorities and fairness per tenant.
7. PagedAttention (vLLM)
The fragmentation problem
Before vLLM, engines allocated a contiguous KV buffer per sequence, sized to the maximum possible length, because the final length is unknown. That causes three kinds of waste: reservation (slots for tokens not yet generated), internal fragmentation (slots that will never be used because the sequence ends early), and external fragmentation (free gaps between buffers that are too small for a new request). The vLLM authors reported that existing systems wasted 60–80% of KV memory this way, while paging brings waste to under 4%. vLLM blog 2023
The mechanism: virtual memory for the KV cache
PagedAttention borrows the OS idea of paging. Kwon+ 2023
- GPU KV memory is carved into fixed-size physical blocks, each holding the K and V for a fixed number of tokens (16 is a common default) for all layers/heads (implementations differ in layout).
- Each sequence has a block table mapping its logical blocks (token positions 0–15, 16–31, …) to physical block IDs, which need not be contiguous.
- Blocks are allocated on demand as the sequence grows. Waste is limited to the unfilled tail of the last block.
- The attention kernel takes the block table as input and gathers K/V from scattered blocks. This indirection costs a little kernel efficiency, which is more than repaid by the larger batches it allows.
Sharing and copy-on-write
Because blocks are referenced through tables, sequences can share physical blocks. With parallel sampling (n completions of one prompt) the prompt's blocks are stored once with a reference count. With beam search beams share long common histories and fork often. When a sequence needs to write into a block whose reference count is above 1 (typically the partially filled last block), the engine performs copy-on-write: allocate a new block, copy the contents, point this sequence at the copy, decrement the original's count. The vLLM team reported memory savings of up to ~55% for these sampling methods. vLLM blog 2023
The paper's headline was 2–4× throughput over FasterTransformer and Orca at the same latency, mostly from fitting larger batches. Kwon+ 2023 Paged KV is now standard: TensorRT-LLM, SGLang, TGI and others all use block-based KV.
It's malloc with fixed-size pages instead of one big contiguous buffer per process. Block size is a trade-off: small blocks mean less tail waste and finer sharing; large blocks mean fewer table lookups and better memory coalescing in the kernel.
Interviewers often ask you to design the KV allocator. Cover: a free list of blocks, per-sequence block tables, reference counts, copy-on-write on append to a shared block, admission control (don't admit if you can't fit the prompt), preemption via swap or recompute, and a hash of block contents for prefix reuse. Bonus: mention that the kernel must handle the indirection, and that newer GPUs' virtual memory APIs allow alternative designs that keep the KV virtually contiguous.
- PagedAttention paper (Kwon+ 2023): the design, the kernel, and measurements.
- Orca (OSDI 2022): iteration-level scheduling and selective batching.
- vLLM V1 announcement: how the scheduler was re-architected (unified prefill/decode token budget, prefix caching on by default).
8. Prefix caching and RadixAttention
Many requests share a prefix: the same system prompt, few-shot examples, a long document asked several questions, previous turns in a multi-turn chat, an agent's growing transcript. If the KV for that prefix is still in memory, a new request can skip prefilling it entirely. For agentic and chat workloads this is often the largest single TTFT and cost saving.
- Automatic prefix caching (vLLM): each full KV block is identified by a hash of its tokens and the hash of the prefix before it, so identical prefixes map to the same block. Freed blocks stay in an LRU pool until reused or evicted. vLLM docs
- RadixAttention (SGLang): keeps a radix tree (compressed trie) over token sequences whose edges point at KV memory. A new request walks the tree to find its longest cached prefix; eviction is LRU over leaves; the scheduler orders requests to maximize hits. Zheng+ 2023 (SGLang)
Both approaches reuse at block or token granularity, and only for exact prefixes: any difference early in the prompt (say a timestamp at the top of the system prompt) invalidates everything after it. That's why API providers tell you to put static content first and variable content last, and why they price cached input tokens at a steep discount. DeepSeek reported that on one day in early 2025, 56.3% of input tokens hit their on-disk KV cache. DeepSeek 2025
Cache-aware routing
With many replicas, a prefix cache only helps if the request lands on the replica that holds it. So load balancers become KV-aware: route by longest-prefix match weighed against load. NVIDIA Dynamo and llm-d both implement this (llm-d reads vLLM's KV events to know which replica holds which prefix). NVIDIA Dynamo llm-d Hierarchical caches extend capacity by spilling KV to CPU DRAM, SSD or a distributed store (LMCache, Mooncake Store, Dynamo's KV block manager). LMCache
Assuming prefix caching changes outputs. It doesn't (up to floating-point nondeterminism): the reused KV is exactly what prefill would have computed. It is a pure performance optimization. Also, it does nothing for TPOT; it reduces TTFT and prefill compute.
9. Chunked prefill
With continuous batching, a new request's prefill runs in the same iteration as everyone else's decode step. A 30k-token prompt makes that iteration take hundreds of milliseconds, so every other user sees a stall (an ITL spike). Running prefill and decode in separate iterations has a similar problem: the decodes just wait.
Chunked prefill splits the prompt into chunks (e.g. 512–2,048 tokens) and processes one chunk per iteration alongside the batch's decode tokens, under a per-iteration token budget. SARATHI introduced the idea of "piggybacking" decodes onto prefill chunks: the prefill chunk makes the iteration compute-bound, and the decodes ride along nearly free because their weight reads are already happening. Agrawal+ 2023 Sarathi-Serve made it a "stall-free" scheduler with a token budget derived from the TPOT SLO. Agrawal+ 2024 vLLM V1 and SGLang use chunked prefill by default.
- Pros: bounded iteration time, so ITL stays smooth; higher utilization from mixing compute-bound and memory-bound work.
- Cons: TTFT for the long prompt goes up somewhat (it's spread over several iterations, and each chunk re-reads the KV of earlier chunks); smaller chunks lose some prefill efficiency.
10. Prefill–decode disaggregation
Chunking shares a GPU between phases. Disaggregation goes the other way: run prefill on one pool of GPUs, decode on another, and transfer the KV cache in between.
Why split?
- No interference: decode ITL never stalls behind a long prefill, and prefill TTFT doesn't wait for decode batches.
- Independent tuning: different parallelism, batch sizes, even GPU types per phase. Splitwise argued that prefill wants compute-heavy GPUs while decode can use cheaper, bandwidth-rich or older ones. Patel+ 2023 (Splitwise)
- Independent scaling: the prefill-to-decode GPU ratio follows the workload's input/output length mix. DistServe optimizes this placement for goodput and reported serving 7.4× more requests (or 12.6× tighter SLOs) than colocated baselines in its experiments. Zhong+ 2024
The cost: moving the KV cache
For a 70B GQA model a 4k-token prompt has about 4096 × 320 KiB ≈ 1.3 GB of KV. Over a 400 Gb/s (50 GB/s) RDMA NIC that's ~26 ms; over NVLink much less. That's acceptable next to a prefill of hundreds of ms, especially if transfer is pipelined layer by layer as prefill proceeds. For MLA models the KV is far smaller, which makes disaggregation even more attractive. Over slower networks, or for short prompts, the transfer overhead can outweigh the gains, so colocated with chunked prefill remains a good default for small deployments.
Production examples
- Mooncake (Moonshot AI's Kimi): a KV-cache-centric architecture with a disaggregated KV pool using the cluster's spare CPU DRAM and SSD, plus prediction-based early rejection under overload. Reported handling 75% more requests on real workloads. Qin+ 2024 (Mooncake)
- DeepSeek-V3/R1 serving: prefill and decode on separate deployments, with different expert-parallel degrees (EP32 for prefill, EP144 for decode) and data-parallel attention. DeepSeek 2025
- Open source: SGLang and vLLM both support PD disaggregation; NVIDIA Dynamo and llm-d orchestrate it across engines, with NIXL as a transfer library. LMSYS 2025
Disaggregation went from research (2024) to the default architecture for large-scale MoE serving (2025–2026). Further splits are being explored, such as separating attention and FFN/expert computation onto different hardware. Check the current Dynamo, llm-d, vLLM and SGLang docs for what's production-ready.
"When would you not disaggregate?" Good answer: small deployments (one or two nodes) where you can't keep two pools busy; short prompts where the transfer and scheduling overhead dominates; no fast interconnect; workloads with very high prefix-cache hit rates where prefill is already cheap. Then contrast with chunked prefill, the colocated alternative.
- Sarathi-Serve (2024): chunked prefill and stall-free scheduling.
- Splitwise (2023): phase splitting with heterogeneous hardware.
- Mooncake (2024): a production KV-centric disaggregated design.
- SGLang (2023): RadixAttention and cache-aware scheduling.
11. Speculative decoding
Decode is memory-bound, so a forward pass over \(k+1\) tokens costs about the same as a pass over one token: the weights are read once either way. Speculative decoding exploits this. A cheap drafter guesses the next \(k\) tokens; the target model scores all of them in a single forward pass; you keep the longest prefix the target agrees with, plus one extra token from the target. If guesses are often right, you emit several tokens per expensive pass. Introduced concurrently by Leviathan et al. and Chen et al. Leviathan+ 2022 Chen+ 2023
Why it's lossless: the rejection-sampling rule
Let \(q(x)\) be the drafter's distribution and \(p(x)\) the target's, at some position. The drafter sampled \(x \sim q\). Accept it with probability $$ \Pr[\text{accept } x] = \min\!\left(1, \frac{p(x)}{q(x)}\right). $$ If rejected, sample a replacement from the normalized residual $$ p'(x) = \frac{\max(0,\ p(x) - q(x))}{\sum_{x'} \max(0,\ p(x') - q(x'))}. $$ The probability of ending up with token \(x\) is \(q(x)\min(1, p(x)/q(x)) + \Pr[\text{reject}]\,p'(x) = \min(q(x), p(x)) + \max(0, p(x) - q(x)) = p(x)\). So each emitted token is distributed exactly as the target would have produced. It is not an approximation, and the quality is identical. Under greedy decoding this reduces to "accept while the draft token equals the target's argmax."
Acceptance-rate math
The per-token acceptance probability is \(\alpha = \sum_x \min(p(x), q(x)) = 1 - \text{TV}(p, q)\): one minus the total-variation distance between drafter and target. Assuming each token is accepted independently with probability \(\alpha\), and \(k\) tokens are drafted, the expected number of tokens emitted per target pass is $$ \mathbb{E}[\text{tokens/pass}] = \frac{1 - \alpha^{k+1}}{1 - \alpha}. $$ With \(\alpha = 0.8\), \(k = 4\): \((1 - 0.8^5)/0.2 \approx 3.4\) tokens per pass. With \(\alpha = 0.6\): ≈ 2.3. If one draft step costs a fraction \(c\) of a target step, the wall-clock speedup is roughly \(\frac{1-\alpha^{k+1}}{(1-\alpha)(ck + 1)}\). Leviathan+ 2022 The diminishing returns in \(k\) are visible: at \(\alpha = 0.8\), going from \(k=4\) to \(k=8\) only raises the expectation from 3.4 to 4.3 while doubling draft cost.
| α | k=2 | k=4 | k=8 | limit k→∞ |
|---|---|---|---|---|
| 0.5 | 1.75 | 1.94 | 2.00 | 2 |
| 0.7 | 2.19 | 2.77 | 3.20 | 3.33 |
| 0.8 | 2.44 | 3.36 | 4.33 | 5 |
| 0.9 | 2.71 | 4.10 | 6.13 | 10 |
(Expected tokens per target pass, ignoring draft cost.) Acceptance is high on predictable text such as code, boilerplate, JSON, and quoting the input; it's lower on creative, high-temperature sampling.
When it helps and when it hurts
- Helps most at small batch / latency-sensitive serving, where the GPU has spare FLOPs. Verifying \(k+1\) tokens is nearly free in compute.
- Helps less, or hurts, at large batch: the batch already makes decode closer to compute-bound, so verifying \(k\) extra tokens per sequence costs real FLOPs, and rejected tokens are wasted work. Engines often adapt \(k\) to load or switch speculation off at high batch.
- It also costs memory (the drafter's weights and KV) and engineering complexity (rolling back KV, tree attention masks).
Variants
| Method | Drafter | Key idea | Reported speedup (paper's setting) |
|---|---|---|---|
| Draft model Leviathan+ 2022 | Small model from the same family (same tokenizer), e.g. 1B drafting for 70B | Simplest; needs a well-matched small model | ~2–3× |
| SpecInfer Miao+ 2023 | One or more small models | Draft a tree of candidates, verify all branches in one pass with a tree attention mask | — |
| Medusa Cai+ 2024 | Extra decoding heads on the target's last hidden state; head \(i\) predicts token \(t+i\) | No separate model; candidates form a tree verified with tree attention. Original paper used a "typical acceptance" option that is not strictly lossless | 2.2× (Medusa-1), 2.3–3.6× (Medusa-2) |
| EAGLE Li+ 2024 | A single lightweight transformer layer that autoregresses over the target's features (second-to-top hidden states), fed the sampled token too | Feature-level drafting is easier than token-level; drafter sees target's internal state | ~3× |
| EAGLE-2 Li+ 2024 | Same drafter | Dynamic draft tree: expand branches where the drafter's confidence (a good proxy for acceptance) is high | ~20–40% over EAGLE |
| EAGLE-3 Li+ 2025 | Drafter fuses low/mid/high-level target features and predicts tokens directly | "Training-time test" simulates multi-step drafting during training so the drafter scales with more data | up to 6.5×; ~1.4× over EAGLE-2 |
| Lookahead decoding Fu+ 2024 | None: the target itself runs Jacobi-style parallel iterations to generate n-grams | Collect n-grams in a pool, verify promising ones; no training needed | ~1.5–2.3× |
| N-gram / prompt lookup Saxena 2023 | String matching: find the last few generated tokens earlier in the prompt and propose what followed | Free, great for summarization, RAG, code editing where output copies input | Large on copy-heavy tasks, ~none otherwise |
| MTP heads (DeepSeek-V3) DeepSeek-V3 2024 | Multi-token-prediction modules trained with the model as an auxiliary objective | At inference, reuse the MTP module as a built-in drafter. The report cites ~85–90% acceptance for the second token and ~1.8× decode TPS | ~1.8× |
Related training-side work: Gloeckle et al. showed multi-token prediction as a pretraining objective can both improve quality and enable self-speculation. Gloeckle+ 2024 In production engines, vLLM's current docs list EAGLE (including EAGLE-3), MTP, draft models, n-gram and suffix decoding among supported methods. vLLM docs
Speculative decoding is moving fast (2025–2026): EAGLE-3 drafters ship for many open models, models increasingly ship with MTP heads, and engines add dynamic speculation that adapts \(k\) to batch size. Reported speedups are highly setting-dependent (batch size, temperature, domain). Check current engine docs and benchmarks before quoting numbers.
Expect to (1) explain why verification of \(k\) tokens costs about one decode step (memory-bound), (2) state the accept rule \(\min(1, p/q)\) and the residual resampling and show why the result equals \(p\), (3) compute expected tokens per step from \(\alpha\) and \(k\), and (4) explain why speedups shrink at high batch. A senior answer adds: drafter must share the tokenizer (or use vocabulary mapping), KV rollback on rejection, tree attention for multi-candidate drafts, and that greedy-only implementations aren't valid for sampling.
- Leviathan+ 2022: the algorithm with the speedup analysis.
- Chen+ 2023 (DeepMind): the same idea, framed as speculative sampling, with Chinchilla results.
- EAGLE repo: EAGLE-1/2/3 code and drafter checkpoints.
- vLLM speculative decoding docs: what's production-supported today.
12. Quantization for inference
Quantization stores numbers in fewer bits. For inference it buys three things: less memory (fit a bigger model, or more KV, on a GPU), less bandwidth per token (faster memory-bound decode), and, if the matmul itself runs in low precision, more FLOPs (faster compute-bound prefill). Which of these you get depends on what you quantize.
| Scheme | What's low-precision | Speeds up | Typical methods / formats |
|---|---|---|---|
| Weight-only (W8A16, W4A16) | Weights stored in INT8/INT4 with per-group scales; dequantized to BF16/FP16 inside the matmul kernel | Decode, especially small batch (fewer bytes to read). Not prefill: the math is still in 16-bit | GPTQ, AWQ, GGUF k-quants; Marlin-style mixed-precision kernels |
| Weight + activation (W8A8 INT8, FP8) | Both operands of the matmul in 8 bits; tensor cores run at 2× the BF16 rate | Prefill and large-batch decode (compute) plus the bandwidth win | SmoothQuant (INT8), FP8 E4M3 with per-tensor/per-channel/per-block scales |
| 4-bit floating point (W4A4) | Weights and activations in FP4 with fine-grained block scales | Compute again 2× over FP8 on Blackwell-class hardware | NVFP4, MXFP4 |
| KV cache | Stored K/V tensors | Long-context decode (KV bandwidth), capacity (more users) | FP8 KV (common), INT4/2-bit (KIVI-style) |
Why weight-only quantization helps decode so much
At small batch the decode step time is about weight bytes ÷ bandwidth. Going BF16 → INT4 cuts weight bytes by about 3.5–4× (scales add overhead), so the ceiling rises by a similar factor even though the multiply still happens in BF16 after on-the-fly dequantization. The dequant costs ALU work, which is idle anyway in a memory-bound kernel. As batch grows and decode becomes compute-bound, the advantage shrinks and dequant overhead can make W4A16 slower than FP8 W8A8. Rule of thumb: weight-only INT4 for latency at low concurrency (local, edge, small-batch); FP8 (or FP4) W+A for high-throughput serving.
The main methods
- Round-to-nearest (RTN) per channel or per group (e.g. groups of 128 weights share a scale) is the baseline. At 8 bits it's nearly lossless; at 4 bits it degrades noticeably on some models, and at 3 bits badly.
- GPTQ quantizes one layer at a time, column by column, using second-order information: it minimizes the layer's output error \(\|WX - \hat W X\|^2\) on calibration data, and after quantizing each column it updates the not-yet-quantized columns to compensate for the error, using the inverse Hessian \(H = 2XX^\top\). It quantizes 175B-class models in a few GPU hours at 3–4 bits. Frantar+ 2022
- AWQ observes that a small fraction of weight channels (~1%) matter most, identified by large activation magnitudes. Rather than keep them in high precision (bad for kernels), it scales those channels up before quantization (and folds the inverse scale into the previous op), which reduces their relative rounding error. No backprop or Hessian needed, and it generalizes well. Lin+ 2023
- LLM.int8() found that LLM activations have a few outlier feature dimensions with huge magnitude, which wreck naive INT8 activation quantization. It handled them with mixed precision (outlier columns in FP16). Dettmers+ 2022
- SmoothQuant makes W8A8 practical by moving the difficulty from activations to weights with a per-channel scale \(s\): \(Y = (X\,\mathrm{diag}(s)^{-1})(\mathrm{diag}(s)\,W)\), with \(s_j = \max|X_j|^{\alpha} / \max|W_j|^{1-\alpha}\) and \(\alpha \approx 0.5\). The transform is mathematically exact and can be folded into the previous LayerNorm, so it's free at runtime. Xiao+ 2022
- FP8: E4M3 (more precision, used for weights/activations in inference) and E5M2 (more range). Micikevicius+ 2022 Hopper and later run FP8 matmuls at 2× BF16 throughput. FP8 W8A8 with per-channel or block-wise scales is close to lossless for most large models and has become the default production precision for many deployments; DeepSeek-V3 was even trained in FP8.
- FP4: MXFP4 and NVFP4. FP4 (E2M1) has only 16 values, so everything depends on scaling. MXFP4 (the OCP Microscaling spec) uses blocks of 32 elements sharing a power-of-two (E8M0) scale. Rouhani+ 2023 NVIDIA's NVFP4 uses blocks of 16 with an FP8 (E4M3) scale, plus a per-tensor FP32 scale; the finer, non-power-of-two scale fits values better. NVIDIA reports ≤1% accuracy drop going from FP8 to NVFP4 on DeepSeek-R1 benchmarks. NVIDIA 2025
- System co-design like QServe's W4A8KV4 shows the right mix depends on kernels as much as accuracy: INT4 weights with INT8 activations can beat both pure W4A16 and W8A8 at serving batch sizes. Lin+ 2024 (QServe)
Quality trade-offs
| Precision | Typical quality impact (large models) | Notes |
|---|---|---|
| FP8 W8A8, FP8 KV | Usually within noise of BF16 | Default for production on Hopper/Blackwell |
| INT8 weight-only | Near-lossless | Easy win on older GPUs |
| INT4 weight-only (GPTQ/AWQ, group 128) | Small drop, often ~1 point on broad benchmarks; larger on math/code/long-context for some models | Smaller models suffer more than large ones |
| NVFP4/MXFP4 W4A4 | Small drop with good calibration or quantization-aware training | Recent; some models (e.g. gpt-oss) shipped with MXFP4 MoE weights natively |
| ≤3-bit | Noticeable degradation without QAT | Mostly for edge or extreme memory limits |
Judging a quantized model only by perplexity or MMLU. Degradation tends to show up first in long-form reasoning, math, code, multilingual and long-context retrieval. Evaluate on your own task distribution, and compare at matched serving cost (e.g. a 70B INT4 vs a 34B FP8 at the same memory).
FP4 inference (NVFP4/MXFP4) only became mainstream with Blackwell in 2025, and quantization-aware training for FP4 is an active area. Check current accuracy reports for your specific model before deploying.
- GPTQ and AWQ: the two most common weight-only methods.
- SmoothQuant: the outlier-migration trick behind W8A8.
- NVIDIA: Introducing NVFP4: block-scaled FP4 explained.
13. Parallelism for inference
Training parallelism (see the distributed training page) splits the work to fit and to speed up a giant batch. Inference parallelism is driven by two different goals: fit the model and its KV cache, and hit a latency target.
| Strategy | What's split | Communication | Effect on latency / throughput | Where it's used |
|---|---|---|---|---|
| Tensor parallel (TP) | Each weight matrix (column/row split), attention heads across GPUs | 2 all-reduces per layer of a \(b \times d\) activation; latency-sensitive, needs NVLink | Lowers per-token latency: \(g\) GPUs read \(1/g\) of the weights each in parallel. Efficiency falls as \(g\) grows (fixed comms latency, smaller matmuls) | Within a node (TP=2–8). KV heads must divide across ranks or get replicated |
| Pipeline parallel (PP) | Layers into stages on different GPUs/nodes | Point-to-point activations between stages; cheap | Doesn't reduce single-request latency (stages run in sequence) but adds capacity; keeps all stages busy only with many concurrent requests (micro-batches) | Across nodes when the model doesn't fit in one node |
| Data parallel (DP) / replicas | Nothing: full copies | None (a router in front) | Linear throughput scaling; no latency change | Default way to scale out |
| Expert parallel (EP) | MoE experts across GPUs | All-to-all dispatch and combine per MoE layer | Lets huge MoE models fit; with wide EP, each GPU holds few experts and receives large per-expert batches | DeepSeek-V3/R1, Kimi, Qwen MoE at scale |
| DP attention | Attention replicated, each rank handles different requests; MoE layers use EP | All-to-all at the MoE boundary | Avoids duplicating KV when TP can't shard it (MLA has one latent shared by all heads, so TP would replicate it on every rank) | DeepSeek-style MLA + MoE serving (SGLang, vLLM) |
| Context / sequence parallel | The sequence dimension (e.g. ring attention) | Passing KV blocks around a ring | Cuts TTFT for very long prompts | Million-token prefill Liu+ 2023 |
TP is "more bandwidth on the same token," PP is "more memory at the cost of a longer pipe," DP is "more copies." For latency, use the smallest TP that fits and meets the TPOT target; then scale throughput with replicas. Use PP only when you must span nodes and don't have a fast enough interconnect for TP.
14. Serving Mixture-of-Experts models
An MoE layer replaces one FFN with \(E\) expert FFNs and a router that sends each token to the top-\(k\). DeepSeek-V3 has 671B total parameters with 37B active per token (256 routed experts, 8 active, plus a shared expert). DeepSeek-V3 2024 This creates a split personality at inference time:
- Memory scales with total params: ~671 GB at FP8, more than one 8×H100 node (640 GB), so you need H200/B200-class nodes or multiple nodes.
- Compute per token scales with active params (37B), which makes prefill cheap relative to a dense model of similar quality.
- Decode bandwidth depends on batch. At batch 1 you read only the ~37B active params. But across a batch, different tokens pick different experts. The expected fraction of experts touched per layer is \(1 - (1 - k/E)^b\). With \(k/E = 8/256\) and \(b = 32\), that's \(1 - 0.969^{32} \approx 64\%\); at \(b = 128\), ~98%. So at serving batch sizes you read nearly all expert weights each step, but each expert only sees about \(bk/E\) tokens: at \(b=128\), just 4 tokens per expert. That's terrible arithmetic intensity.
The fix is to make the global batch enormous and spread experts thin: wide expert parallelism. DeepSeek's decode deployment used EP across 18 nodes (144 GPUs), so each GPU holds only a couple of experts and receives tokens from the whole cluster, reporting roughly 14.8k output tokens/s per H800 node for decode and 73.7k input tokens/s per node for prefill (including cache hits). DeepSeek 2025 SGLang reproduced this style on 96 H100s with PD disaggregation and reported about 22k decode tokens/s per node. LMSYS 2025
MoE-specific serving problems
- All-to-all communication: each MoE layer dispatches tokens to expert owners and gathers results. Libraries like DeepEP provide a high-throughput mode for prefill and a low-latency mode (CUDA-graph compatible) for decode. DeepEP
- Overlap: split the batch into two micro-batches so one computes while the other communicates ("two-batch overlap"); SGLang reported 27–35% throughput gains from it. LMSYS 2025
- Load imbalance: popular experts become stragglers. An expert-parallel load balancer (EPLB) replicates hot experts and re-places them based on observed traffic.
- Capacity and dropping: training sometimes uses capacity factors that drop overflow tokens; inference engines generally avoid dropping, so worst-case expert load matters for memory planning.
- Small deployments: on one node, MoE gives you fast batch-1 decode (few active params) but poor batched efficiency. MoE economics shine at large scale with lots of traffic.
"Is an MoE with 37B active params as cheap to serve as a 37B dense model?" No. It's similar in FLOPs per token, but it needs memory for all 671B params, and at realistic batch sizes it reads nearly all of them each decode step while giving each expert few tokens. It's cheap only when you can aggregate a very large batch across many GPUs with expert parallelism, which needs fast all-to-all networking and careful load balancing.
- DeepSeek-V3/R1 inference system overview: real production numbers, including cost and margin.
- SGLang large-scale EP blog: an open reproduction with PD disaggregation, DeepEP, TBO, EPLB.
- Pope+ 2022: partitioning analysis that still frames TP/EP choices.
15. Sampling
The model outputs logits \(z\) over the vocabulary. Sampling turns them into one token. The common pipeline applies penalties, then temperature, then truncation, then samples. Order varies by engine, and it matters.
| Knob | Definition | Effect / when to use |
|---|---|---|
| Greedy | \(\arg\max_x z_x\) | Deterministic in principle (batching and floating-point order can still cause variation). Good for extraction and short factual answers; can loop on long generations |
| Temperature \(T\) | \(p_x \propto \exp(z_x / T)\) | \(T<1\) sharpens, \(T>1\) flattens; \(T \to 0\) is greedy |
| Top-k | Keep the \(k\) most likely tokens, renormalize | Fixed cut regardless of shape; too loose when the distribution is peaked, too tight when it's flat |
| Top-p (nucleus) Holtzman+ 2019 | Keep the smallest set whose cumulative probability ≥ \(p\) | Adapts to shape; the default for chat (e.g. \(p = 0.9\)–\(0.95\)) |
| Min-p Nguyen+ 2024 | Keep tokens with \(p_x \ge p_{\min} \cdot p_{\max}\) | Scales the cut with model confidence; argued to stay coherent at higher temperatures |
| Repetition penalty | Divide positive logits (multiply negative ones) by \(\theta > 1\) for tokens already present | Fights loops; too strong hurts code and data where repetition is correct |
| Frequency / presence penalties | Subtract \(\lambda_f \cdot \text{count}(x) + \lambda_p \cdot \mathbb{1}[\text{count}(x) > 0]\) from logits | Additive variants (OpenAI-style API parameters) |
| Beam search | Keep the \(B\) highest-probability partial sequences each step | Approximates the highest-likelihood sequence |
Why beam search is rare for chat
- Quality: maximizing likelihood produces bland, generic and repetitive open-ended text. Holtzman et al. showed human text is not the highest-probability text. Holtzman+ 2019 Beam search still makes sense for constrained tasks such as translation or ASR.
- Cost: \(B\) beams multiply decode compute and KV (paging with copy-on-write softens the memory part), and beams fork and die unpredictably, which complicates batching.
- Streaming: you don't know which beam wins until the end, so you can't stream tokens to the user.
- Modern alternatives for "better answers" use sampling: best-of-N with a reward model, self-consistency, or reasoning models' long chains of thought.
Assuming temperature 0 gives bit-identical outputs across runs. In batched serving the batch composition changes kernel reduction orders, so logits differ in the last bits and ties can flip. Exact reproducibility needs batch-invariant kernels, which some engines now offer as an option at a performance cost.
16. Structured and constrained decoding
Applications often need output that parses: JSON matching a schema, a regex, SQL, a function call. Prompting alone fails some fraction of the time. Constrained decoding guarantees the format by masking the logits at each step so only tokens that keep the output a valid prefix of the grammar can be sampled.
FSM approach (regex, simple JSON schemas)
Outlines compiles a regex (JSON schemas are converted to regexes) into a finite-state machine over characters, then precomputes an index mapping each FSM state to the set of vocabulary tokens that are valid from that state (a token may span several characters and FSM transitions). At runtime each step is a lookup plus a mask: \(O(1)\) amortized per token after the one-time compile. Willard & Louf 2023
CFG approach (nested JSON, code, arbitrary grammars)
Recursive structures (arbitrarily nested JSON) need a context-free grammar and a pushdown automaton, whose state includes a stack, so you can't precompute masks for every state. XGrammar splits the vocabulary into context-independent tokens (validity determined by the current grammar position alone, precomputed and cached) and context-dependent tokens (checked at runtime against the stack), uses a persistent stack for fast branching and rollback, and overlaps mask computation on CPU with the GPU's forward pass. It reported up to ~100× speedups over prior approaches and near-zero overhead in end-to-end serving. Dong+ 2024 (XGrammar) It's the default structured-output backend in several engines.
Practical gotchas
- Token boundaries: grammar is over characters, the model emits multi-character tokens. Masks must account for tokens that complete one symbol and start another. Forcing unnatural tokenizations can hurt quality.
- Quality under constraints: forcing a format the model isn't used to can degrade content. Put a reasoning field before the answer field, use field names the model understands, and keep schemas simple.
- Jump-forward: when the grammar allows only one continuation (e.g. a fixed key name), the engine can append those tokens directly without sampling them one at a time (SGLang does this). LMSYS 2024
- Interaction with speculative decoding: draft tokens must also be checked against the grammar; engines handle this unevenly.
- Efficient Guided Generation (Outlines): the FSM indexing trick.
- XGrammar: CFG-constrained decoding at near-zero overhead.
- Min-p sampling: a recent, popular truncation rule and its evaluation.
17. Long-context serving
Long contexts (128k to 1M+ tokens) stress every part of the stack.
- Prefill becomes quadratic. Per layer, attention costs about \(4n^2d\) FLOPs (scores plus weighted sum, before causal halving), against about \(2Pn\) for the linear layers over the whole model. For a 70B model (\(d = 8192\), \(L = 80\)) the two are equal around \(n \approx 2P/(4dL) \approx\) 50k tokens (roughly 100k with causal masking). Past that, attention dominates and TTFT grows quadratically. FlashAttention keeps it IO-efficient but doesn't change the FLOP count. Dao+ 2022
- KV capacity: one 128k-token Llama-3-70B sequence needs ~42 GB of BF16 KV. A single user can consume most of a GPU, so batch sizes collapse and cost per token rises. Providers often price long context higher for this reason.
- Decode reads the whole KV per step, so TPOT grows linearly with context. At small batch and long context, standard attention kernels underuse the GPU because they parallelize over batch and heads only. Flash-Decoding also splits the KV sequence across thread blocks and combines partial softmaxes, recovering parallelism. Dao+ 2023
- Prefill parallelism: context/sequence parallelism (ring attention) spreads a single long prefill over many GPUs to bound TTFT. Liu+ 2023
- Prefix caching matters even more: re-asking questions about the same 200k-token document should never re-prefill it. Tiered KV storage (GPU → CPU → SSD → remote) makes this feasible.
- Architecture-level fixes: GQA/MLA, sliding-window or local–global layers, attention sinks for streaming Xiao+ 2023, and trained sparse attention.
Sparse and hybrid attention for long context (trained sparse attention in frontier open models, hybrid linear-attention/state-space layers mixed with full attention) moved quickly in 2025–2026. These change the KV and prefill math above. Check the specific model's architecture before applying dense-attention formulas.
18. Serving engines
| Engine | Origin / niche | Signature features | Status (as of late 2026, verify) |
|---|---|---|---|
| vLLM GitHub | UC Berkeley; the de facto open-source default | PagedAttention, continuous batching, automatic prefix caching, chunked prefill, speculative decoding, PD disaggregation, broad hardware support (NVIDIA, AMD, TPU and others via plugins) | Very active; V1 engine re-architecture from 2025 vLLM 2025 |
| SGLang GitHub | LMSYS / Berkeley+Stanford; high performance, programmable | RadixAttention, cache-aware scheduling, fast structured outputs, strong DeepSeek/MoE support with large-scale EP and PD disaggregation | Very active; widely used for large MoE serving |
| TensorRT-LLM GitHub | NVIDIA; peak performance on NVIDIA GPUs | Optimized kernels, in-flight batching, paged KV, FP8/NVFP4, speculative decoding | Active; has moved toward a PyTorch-based workflow (verify the current default) |
| TGI (Hugging Face) GitHub | Early production server for HF models | Continuous batching, paged attention, multi-backend | Maintenance mode; the repo shows as archived (March 2026). Don't pick it for new deployments |
| llama.cpp GitHub | Local / edge, C/C++ | GGUF format, k-quants (2–8 bit), CPU, Apple Metal, CUDA, Vulkan; underlies many desktop tools | Very active |
| MLC LLM GitHub | Compiler-based (TVM) universal deployment | Compile once, run on CUDA, Metal, Vulkan, WebGPU, iOS/Android | Active; also home of XGrammar |
| NVIDIA Dynamo GitHub | Orchestration layer above engines (not an engine) | Disaggregated serving, KV-aware routing, KV block manager (multi-tier offload), SLA-based planner; backends: vLLM, SGLang, TensorRT-LLM | Open-sourced 2025; active |
| llm-d GitHub | Kubernetes-native distributed inference (Red Hat, Google, IBM, NVIDIA, CoreWeave and others) | Prefix/KV-aware routing via the Gateway API inference extension, PD disaggregation over vLLM | Active, newer |
Others you may hear about: LMDeploy, DeepSpeed-FastGen (introduced "Dynamic SplitFuse", a chunked-prefill relative) Holmes+ 2024, Ollama (desktop wrapper building on llama.cpp-style runtimes), MLX (Apple silicon) MLX, and managed inference platforms that wrap these engines.
Engine rankings flip every few months and benchmarks are often vendor-run with favorable settings. TGI's status changed in 2025–2026; TensorRT-LLM's architecture changed in 2025. Check release notes and run your own benchmark on your model, hardware and traffic shape.
"Which engine would you pick?" Avoid brand loyalty. A good answer is conditional: vLLM for breadth of models and hardware and a big community; SGLang for heavy prefix reuse, structured outputs, or large MoE with EP; TensorRT-LLM when you're all-in on NVIDIA and need the last 10–30%; llama.cpp/MLX for local and edge; Dynamo or llm-d when the problem becomes fleet-level routing and disaggregation. Then say you'd benchmark on your actual traffic at your SLO.
19. Hardware notes
| Accelerator | Memory | Bandwidth | Dense compute (approx) | Inference relevance |
|---|---|---|---|---|
| NVIDIA H100 SXM NVIDIA | 80 GB HBM3 | 3.35 TB/s | ~989 TF BF16, ~1,979 TF FP8 (spec sheet doubles these with sparsity) | The workhorse; FP8 support |
| NVIDIA H200 NVIDIA | 141 GB HBM3e | 4.8 TB/s | Same as H100 | Same compute, ~1.4× bandwidth, ~1.8× memory: faster decode, more KV. A great decode GPU |
| NVIDIA B200 NVIDIA DGX B200 | ~180 GB HBM3e (1,440 GB per 8-GPU system) | ~8 TB/s (64 TB/s per system) | Native FP4 (NVFP4); 144 PF FP4 per 8-GPU system | FP4 inference; NVL72 rack-scale variants put 72 GPUs in one NVLink domain, ideal for wide EP |
| Google TPU v7 "Ironwood" Google | 192 GB HBM | ~7.2–7.4 TB/s | ~4.6 PF FP8 | Google's first inference-focused TPU; large pods with high-bandwidth ICI |
| Groq LPU Wikipedia | On-chip SRAM only (a few hundred MB per chip) | Very high on-chip | — | Deterministic, compiler-scheduled; very low latency per user, but needs hundreds of chips for a 70B model. |
| Cerebras WSE-3 Cerebras | Wafer-scale, ~44 GB on-chip SRAM | ~PB/s on-chip | — | Extremely high single-stream tokens/s; weights spread across wafers for big models |
The pattern to remember: decode speed per user tracks memory bandwidth per byte of model; decode throughput tracks HBM capacity (KV space) and bandwidth; prefill tracks FLOPs. SRAM-based designs (Groq, Cerebras) win on single-stream latency because on-chip bandwidth is orders of magnitude above HBM, but they pay in chip count and capacity. AMD's MI300-series GPUs compete with large HBM capacity (192 GB+) and are supported by vLLM and SGLang.
Hardware specs and availability change quickly (Blackwell Ultra, Rubin, newer TPUs and AMD parts in 2026). The figures above come from vendor pages at the time of writing; vendors often quote sparse or low-precision peaks. Check the current datasheet and confirm whether a number is dense or sparse.
20. Cost math
$$ \frac{\$}{\text{1M tokens}} = \frac{\text{GPU cost per hour (all GPUs in the replica)}}{\text{tokens/s} \times 3600} \times 10^6 $$Worked example 1: 8B model on one H100
Assume an H100 rents for $2.50/hour (cloud prices vary widely by provider and commitment; verify current rates).
- Output tokens: from section 4, ~3,500 output tok/s realistic at batch 64. That's 12.6M tokens/hour → $2.50 / 12.6 ≈ $0.20 per 1M output tokens.
- Input tokens: prefill ~30k tok/s → 108M tokens/hour → ≈ $0.023 per 1M input tokens, about 9× cheaper. This asymmetry is why API providers price output tokens several times higher than input, and cached input lower still.
Worked example 2: 70B FP8 on an 8×H100 node
- Node at $20/hour. Run 4 replicas with TP=2 (aggregate 6.7 TB/s per replica).
- Per replica at batch 64, 2k average context, FP8 KV (160 KiB/token): KV = 64 × 2048 × 160 KiB ≈ 21 GB. Step bytes ≈ 70 + 21 = 91 GB → \(t_{\text{step}}\) ≈ 13.6 ms → ceiling ≈ 4,700 tok/s. Compute check: \(2 \times 70\text{B} \times 64 \approx 9\) TFLOP per step across ~4 PF of FP8: ~2–3 ms, so still bandwidth-bound.
- Assume ~55% of ceiling: ~2,500 tok/s per replica, 10,000 tok/s per node → 36M tokens/hour → ≈ $0.56 per 1M output tokens at full load.
- Real traffic is bursty. At 40% average utilization the effective cost is ≈ $1.40/M. Utilization is often the biggest cost lever, bigger than kernel tweaks.
For calibration, DeepSeek published that its V3/R1 service averaged ~227 H800 nodes over one day, costing about $87k/day at an assumed $2/GPU-hour, and claimed a theoretical cost-profit margin of 545% at R1 list prices (actual revenue was much lower). DeepSeek 2025
When asked to cost a product feature, walk through: tokens per request (input/output, and the cached fraction) × requests/day → tokens/day; tokens/s per replica at your SLO → replicas needed for peak (not average) load; × GPU price. Then name the levers in order of impact: utilization and autoscaling, prefix caching, quantization, model size (distill or route easy queries to a small model), batch size versus latency SLO, speculative decoding for latency-bound tiers. Compare against API pricing as a build-vs-buy check.
21. Edge and on-device inference
The same roofline applies with much smaller numbers. A phone's memory bandwidth is roughly 50–100 GB/s; Apple's high-end desktop chips reach ~800 GB/s. A 3B model at 4-bit (~1.7 GB) on a phone has a decode ceiling around 30–60 tok/s, and an 8B 4-bit model maybe half that. Batch is almost always 1, so decode is purely bandwidth-bound, which is why aggressive weight-only quantization (4-bit and below) is standard on device.
- Constraints: RAM shared with the OS and apps, thermal throttling during long generations, battery, and app size. Prefill on long prompts is often the slowest part on device, since mobile NPUs and GPUs have modest FLOPs.
- Runtimes: llama.cpp (GGUF), MLX on Apple silicon, MLC LLM (including WebGPU in the browser), Google's LiteRT LiteRT, PyTorch ExecuTorch, and OS-level APIs such as Apple's Foundation Models framework for its on-device model Apple.
- Patterns: small distilled models (1–4B) for on-device tasks, hybrid routing (device first, cloud for hard queries), LoRA adapters swapped per task on a shared base, speculative decoding with a tiny on-device drafter.
- llama.cpp: the reference for local inference and GGUF quantization types.
- MLC LLM: compiler-based deployment across phones, browsers and GPUs.
- NVIDIA Dynamo: what a datacenter-scale serving layer looks like.
- llm-d: the Kubernetes-native take on the same problems.
Interview question bank
Why is prefill compute-bound and decode memory-bound?
For a linear layer the arithmetic intensity is about the number of tokens processed per weight load, because each weight byte is reused once per token. Prefill processes hundreds or thousands of prompt tokens in one pass, so intensity is far above the GPU's ridge point (~300 FLOPs/byte for BF16 on H100) and tensor cores are the limit. Decode processes one token per sequence per step, so intensity equals the batch size, often 1–64, far below the ridge. The GPU spends its time streaming weights and KV cache from HBM. Decode attention is even worse because each sequence reads its own KV and batching doesn't amortize it. Consequences: optimize prefill with FLOPs (FP8, FlashAttention) and decode with bytes (quantization, smaller KV, batching, speculative decoding).
Estimate batch-1 decode speed for a 13B model in FP16 on a GPU with 2 TB/s bandwidth.
Weights are 13B × 2 bytes = 26 GB. Every decode step reads them once, so the ceiling is 2000/26 ≈ 77 tokens/s. Real kernels reach maybe 70–80% of peak bandwidth and also read KV, so expect ~55–60 tok/s at short context. INT4 weights (~7 GB with scales) would raise the ceiling to ~280 tok/s, realistically perhaps 150–200 due to dequant and kernel overheads. Compute isn't the issue: 2 × 13B = 26 GFLOP per token is trivial for a modern GPU.
Compute the KV cache size for Llama-3-70B at 32k context, batch 16. What would you do if it doesn't fit?
Per token: 2 (K,V) × 80 layers × 8 KV heads × 128 dims × 2 bytes = 327,680 bytes ≈ 320 KiB. Per sequence at 32k: ~10.7 GB. Batch 16: ~172 GB in BF16, more than double the FP8 weights (~70 GB). Options: FP8 KV (halves it to ~86 GB), spread across more GPUs with TP (KV heads shard across ranks, up to 8 ways here), rely on paging since real sequences are rarely all at max length, prefix sharing if prompts overlap, offload cold KV to CPU, cap concurrency through admission control, or choose a model with MLA or sliding-window layers. I'd also question whether every request truly needs 32k.
Explain continuous batching and why it beats static batching.
Static batching runs a fixed set of sequences until the longest one finishes. Slots of early finishers go idle and new requests wait for the whole batch. Continuous (iteration-level) batching, from Orca, makes batch membership a per-step decision: finished sequences leave immediately and waiting requests join at the next iteration, with their prefill folded in. GPU slots stay full, so throughput rises and queueing delay falls. It requires handling ragged batches (mixed lengths, mixed prefill/decode), which is why Orca batched linear layers across all tokens but handled attention per sequence. Its success made memory the next bottleneck, which PagedAttention solved.
What problem does PagedAttention solve and how?
Contiguous per-sequence KV allocation forces reserving space for the maximum length, causing reservation waste, internal fragmentation and external fragmentation; vLLM measured 60–80% of KV memory wasted in prior systems. PagedAttention splits KV memory into fixed-size blocks (e.g. 16 tokens), gives each sequence a block table mapping logical to physical blocks, and allocates on demand, so waste is limited to the last partial block (under 4%). The attention kernel follows the block table. Blocks can be shared via reference counts for parallel sampling, beam search and shared prefixes, with copy-on-write when a shared block must be modified. More sequences fit, so batch size and throughput rise (2–4× in the paper).
How does prefix caching work, and how should an application be designed to benefit?
The engine keeps KV blocks of previous requests and indexes them by token content: hash chains over blocks (vLLM) or a radix tree over token sequences (SGLang). A new request whose prompt starts with a cached sequence reuses those blocks and only prefills the remainder, cutting TTFT and compute. Reuse requires an exact prefix match, so applications should put stable content (system prompt, tool definitions, documents, few-shot examples) first and variable content last; avoid timestamps or per-user IDs at the top. At fleet scale, route requests to the replica that holds their prefix (KV-aware routing). It's lossless, and it doesn't speed up decode.
Users complain of stutters (ITL spikes) during streaming. Diagnose and fix.
The likely cause is long prefills sharing iterations with decodes: when a 20k-token prompt is admitted, that iteration takes hundreds of ms and every streaming user stalls. Other causes are preemption under KV memory pressure (sequences evicted and recomputed), CPU-side scheduling or detokenization overhead, and garbage collection. Fixes: enable chunked prefill with a token budget per iteration sized from the ITL SLO; cap concurrent prefill tokens; disaggregate prefill and decode at larger scale; give KV more headroom (FP8 KV, lower max concurrency) to avoid preemption; check CUDA graphs and async scheduling are on. Measure P99 ITL, not mean TPOT, to confirm.
What is prefill–decode disaggregation, and what are its costs?
Prefill and decode run on separate GPU pools; after prefill, the KV cache is transferred to a decode worker over NVLink or RDMA. Benefits: no interference (decode ITL never waits behind a prefill), phase-specific parallelism and batch sizes, independent scaling of the two pools to match the input/output mix, and the option of different hardware per phase. Costs: KV transfer bandwidth and latency (≈1.3 GB for a 4k-token prompt on a 70B GQA model), needing a fast interconnect; more complex scheduling, routing and failure handling; and pool-ratio tuning as traffic changes. DistServe, Splitwise and Mooncake showed large goodput gains; it's now standard for large MoE deployments. For small deployments, colocated serving with chunked prefill is often simpler and just as good.
Prove that speculative decoding preserves the target distribution.
The drafter samples \(x \sim q\); we accept with probability \(\min(1, p(x)/q(x))\); on rejection we sample from the residual \(\max(0, p - q)\) normalized. The probability of outputting \(x\) via acceptance is \(q(x)\min(1, p(x)/q(x)) = \min(p(x), q(x))\). The rejection probability is \(1 - \sum_x \min(p, q) = \sum_x \max(0, p - q)\), so the probability via resampling is exactly \(\max(0, p(x) - q(x))\). Summing gives \(\min(p, q) + \max(0, p - q) = p(x)\). Apply this sequentially per position, stopping at the first rejection; if all are accepted, sample a bonus token from the target's next-position distribution. Output is distributed exactly as target-only sampling.
Acceptance rate is 0.75 and you draft 5 tokens with a drafter costing 5% of a target pass. What speedup do you expect?
Expected tokens per target pass = \((1 - 0.75^6)/(1 - 0.75) = (1 - 0.178)/0.25 \approx 3.29\). Cost per iteration = 1 target pass + 5 × 0.05 = 1.25 target-pass equivalents. Speedup ≈ 3.29/1.25 ≈ 2.6× under the i.i.d.-acceptance assumption and assuming verifying 6 tokens costs the same as 1 (true at low batch, since decode is memory-bound). At high batch, verification costs real compute and the gain shrinks. Acceptance is also not i.i.d. in practice: it varies by content, which is why dynamic draft trees (EAGLE-2) help.
Compare draft-model, Medusa, EAGLE and n-gram speculation.
A draft model is a separate small LM with the same tokenizer; simple, but it needs a well-aligned small model and its own KV cache. Medusa adds extra heads on the target's final hidden state to predict tokens t+1..t+k, with tree-structured candidates verified by tree attention; cheap to train but acceptance drops for far positions since heads don't condition on each other. EAGLE trains a single lightweight layer that autoregresses on the target's hidden features, getting higher acceptance; EAGLE-2 adds confidence-driven dynamic trees; EAGLE-3 fuses multi-layer features and uses training-time test to scale with data. N-gram/prompt lookup needs no model at all and shines when output copies input (RAG, editing, summarization). MTP heads (DeepSeek-V3) are trained with the model and reused as a built-in drafter.
Why does weight-only INT4 speed up decode but not prefill?
Decode is bandwidth-bound: step time ≈ bytes read ÷ bandwidth, and weights dominate the bytes at small batch, so 4× fewer weight bytes gives up to ~3–4× faster steps. The dequantization to BF16 happens in registers, using ALUs that are idle anyway. Prefill is compute-bound, and in W4A16 the matmul still runs in 16-bit, so FLOPs are unchanged; dequant adds slight overhead. To speed up prefill you need low-precision compute: W8A8 INT8 (SmoothQuant), FP8, or FP4. At large decode batches the kernel also becomes compute-bound, so FP8 W8A8 can beat W4A16 in high-throughput serving.
What problem does SmoothQuant solve, and how?
LLM activations have a few channels with extreme outliers (often 100× typical values). Per-tensor INT8 quantization of activations then wastes nearly all levels on the outliers and crushes everything else. Weights are smooth and easy to quantize. SmoothQuant divides each activation channel by a factor \(s_j\) and multiplies the corresponding weight row by \(s_j\), an exact transformation of \(XW\). Choosing \(s_j = \max|X_j|^\alpha / \max|W_j|^{1-\alpha}\) with \(\alpha \approx 0.5\) balances the difficulty between the two. The scales fold into the preceding LayerNorm offline, so there is no runtime cost, and both operands become INT8-friendly, enabling W8A8 tensor-core matmuls.
How would you serve a 671B-parameter MoE (37B active) model? What's hard about it?
Memory: ~671 GB at FP8 doesn't fit one 8×H100 node, so use H200/B200-class nodes or multi-node. Parallelism: expert parallelism for MoE layers plus data-parallel attention (MLA's shared latent KV would be replicated by TP), with prefill and decode disaggregated. The hard part is decode efficiency: across a batch, nearly all experts get activated, yet each sees only about \(b k / E\) tokens, so per-expert arithmetic intensity is poor. The fix is wide EP across many GPUs so the global batch is huge and each GPU holds few experts (DeepSeek used EP144 for decode). That brings all-to-all communication (DeepEP, overlapping compute and comms with two micro-batches), expert load imbalance (EPLB with replicated hot experts), and failure blast radius. Add MTP-based speculative decoding and FP8 throughout.
When would you use tensor parallelism versus pipeline parallelism for inference?
TP splits every layer across GPUs so each reads a fraction of the weights per step, cutting per-token latency roughly in proportion until all-reduce latency dominates. It needs a fast interconnect (NVLink), so it's used within a node. PP splits layers into stages; a single request passes through stages sequentially, so latency doesn't improve (it gets slightly worse) but memory capacity increases and throughput is fine with enough concurrent requests. Use PP across nodes when the model doesn't fit in one node's TP group. In practice: smallest TP that fits and meets the TPOT SLO, PP only to span nodes, data-parallel replicas to scale throughput, EP for MoE.
Explain top-p and min-p, and why beam search is rarely used for chat.
Top-p keeps the smallest set of tokens whose cumulative probability reaches p, then renormalizes, so the candidate set adapts to how peaked the distribution is. Min-p keeps tokens whose probability is at least \(p_{\min}\) times the top token's, so the cutoff scales with model confidence; it's argued to stay coherent at higher temperatures. Beam search maximizes sequence likelihood, which for open-ended text produces bland, repetitive output (high-probability text isn't human-like). It also multiplies compute and KV by the beam width, complicates batching, and can't stream because the winning beam is unknown until the end. It remains useful for constrained tasks such as translation or ASR.
How does grammar-constrained decoding work, and what are its costs?
At each step the engine computes a mask of tokens that keep the output a valid prefix of the grammar and sets other logits to −∞. For regular languages (regex, flat JSON schemas), Outlines compiles an FSM and precomputes, for each state, the allowed vocabulary tokens, so runtime is a lookup. For context-free grammars (nested JSON, code) a pushdown automaton is needed; XGrammar precomputes masks for context-independent tokens and checks only context-dependent ones at runtime, overlapping that CPU work with GPU inference for near-zero overhead. Costs: grammar compile time (cacheable), possible quality loss if the format forces unnatural tokenization or prevents the model from "thinking" before answering, and interactions with speculative decoding. Jump-forward decoding can speed up deterministic stretches.
Back-of-envelope: what does it cost per million output tokens to serve a 70B model on H100s?
Take an 8×H100 node at about $20/hour, FP8 weights, 4 replicas at TP=2. Each step reads ~70 GB of weights plus KV (~21 GB for batch 64 at 2k context with FP8 KV) over 6.7 TB/s, so ~14 ms per step, ~4,700 tok/s ceiling per replica, maybe ~2,500 realistic. Four replicas give ~10k tok/s, 36M tokens/hour, so ≈ $0.55 per million output tokens at full utilization. At realistic 40% utilization it's ≈ $1.40/M. Input tokens are roughly an order of magnitude cheaper because prefill is compute-efficient. The big levers are utilization, prefix caching, quantization (FP4 on Blackwell), and model choice.
Why does long context hurt serving so much, and what mitigates it?
Three effects. Prefill attention FLOPs grow quadratically and overtake the linear layers beyond roughly 50–100k tokens for a 70B model, so TTFT grows fast. KV memory grows linearly per sequence (~42 GB for 128k tokens on Llama-3-70B in BF16), collapsing the batch size and raising cost per token. Decode must read the whole KV each step, so TPOT grows with context. Mitigations: GQA/MLA and FP8 KV, sliding-window or sparse attention in the architecture, context parallelism (ring attention) for prefill, Flash-Decoding for small-batch long-context decode, aggressive prefix caching with tiered KV storage, and pricing or routing long-context requests to dedicated pools.
Design an LLM serving system for a chat product at 5,000 requests/minute with P90 TTFT under 1 s and P90 TPOT under 50 ms.
Start with the workload: assume ~2k input tokens (half cached) and ~400 output tokens per request, giving ~83 req/s, ~33k output tok/s and ~170k input tok/s, with about 83 × (1 + 400 × 0.04) ≈ 1,400 concurrent sequences (Little's law). Pick the model and precision (e.g. 70B FP8 on H100/H200), and benchmark one replica to find the largest batch meeting the 50 ms TPOT at your context distribution; this gives tokens/s per replica and the replica count for peak plus headroom. Architecture: an API gateway with auth and rate limits, a KV-aware router for prefix hits, engine replicas (vLLM/SGLang) with continuous batching, paged KV, chunked prefill and FP8 KV; consider PD disaggregation if long prompts threaten TTFT. Autoscale on queue depth and KV utilization, not CPU. Add streaming, cancellation (free KV when users disconnect), admission control with graceful 429s, priority classes, and observability for TTFT/ITL percentiles, prefix hit rate, preemptions and cost per token. Mention speculative decoding for a low-latency tier and a small-model router for easy queries.
What does goodput measure, and why is it better than throughput?
Goodput is the request rate a system can sustain while meeting all latency SLOs (for example P90 TTFT and P90 TPOT under targets). Raw throughput can be inflated with huge batches that give terrible per-user latency, or by measuring at an unrealistic load. Goodput captures the trade-off directly: it's the throughput on the right side of the SLO line. It's the metric DistServe optimized, and it's how you should compare configurations (batch sizes, TP degrees, colocated vs disaggregated): fix SLOs, then maximize sustainable rate per dollar.
Why might two identical requests at temperature 0 return different outputs from the same server?
Floating-point arithmetic isn't associative, and the reduction order inside kernels depends on batch size and composition, which changes from moment to moment under continuous batching. Different chunking of prefill, different TP splits, or speculative decoding paths can also change rounding. Tiny logit differences can flip an argmax when two tokens are nearly tied, and the outputs then diverge. Fixes are batch-invariant kernels (some engines offer a deterministic mode at a throughput cost), fixed batch configurations, or accepting non-determinism and designing evals around it.
How does the KV cache interact with GQA and tensor parallelism?
With GQA, a model like Llama-3-70B has 64 query heads but only 8 KV heads. Under TP, heads are split across ranks, so with TP=8 each rank holds one KV head and the cache shards cleanly; with TP=16 there are fewer KV heads than ranks, so KV heads must be replicated, wasting memory. With MLA (DeepSeek), all heads share one compressed latent per token, so TP can't shard the cache by head at all; every rank would hold the full latent. That's why DeepSeek-style serving uses data-parallel attention (each rank owns different requests' caches) with expert parallelism for the MoE layers.