Track A · Model internals

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.

  1. 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).
  2. 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.

One request on the GPU timeline time → queue PREFILL all n prompt tokens compute-bound DECODE: 1 token / step, bandwidth-bound TTFT (queue + prefill + 1st step) ITL / TPOT end-to-end latency = TTFT + (N_out − 1) · TPOT
Prefill is one large, wide pass. Decode is many thin, sequential passes. The user sees the first token after TTFT, then a stream at roughly one token per TPOT.
Interview angle

"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:

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.

PrefillDecode
Tokens per sequence per passAll \(n\) prompt tokens1 (or \(k{+}1\) with speculative decoding)
BottleneckTensor-core FLOPsHBM bandwidth (weights + KV cache)
Arithmetic intensity (linear layers)≈ prompt tokens in batch (hundreds to thousands)≈ batch size (1 to a few hundred)
Drives which metricTTFTTPOT / ITL, and total throughput
What helpsFP8/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 lengthLinear in \(n\) for MLPs, quadratic in \(n\) for attentionPer step linear in context length (KV read)
Intuition

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.

Common mistake

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.

Go deeper

3. Latency metrics, SLOs and the latency–throughput trade-off

MetricDefinitionDominated byTypical target (chat, rough)
TTFT (time to first token)Request arrival → first output token streamedQueueing + 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 firstDecode step time, which grows with batch size and context; stalls from prefills sharing the GPU20–50 ms (20–50 tok/s per user); faster than reading speed
E2E latencyTTFT + (Nout − 1)·TPOTOutput length × TPOT for long answersDepends on task; reasoning models make this huge
ThroughputTokens/s (input, output or total) or requests/s across the whole serverBatch size, KV capacity, kernel efficiencyMaximize, subject to SLOs
GoodputRequests/s that meet all SLOs (e.g. P90 TTFT < X and P90 TPOT < Y)The scheduler's ability to keep both phases inside budgetThe 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.

throughput TPOT (per-user latency) (tok/s) ________ compute/KV bound (ms) / | / | / | / ← knee: pick the batch here | ___/ | / | flat: weights / | / linear: weight read is amortized | dominate ____/ | / |____________/ +------------------------- batch size +------------------------- batch size

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.

Interview angle

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.

Go deeper

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 / precisionWeight bytesFits onCeiling (tok/s, batch 1)Realistic (×~0.7)
Llama-3-8B, BF16~16 GB1 GPU3350/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 GB2 GPUs (TP=2)6700/140 ≈ 48~30–38
Llama-3-70B, FP8~70 GB1 GPU (tight: ~8 GB left for KV)3350/70 ≈ 48~30–35
Llama-3-70B, FP8, TP=4~70 GB over 44 GPUs13400/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:

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.

Interview angle

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 sequence32 seqs × 8k
Llama-3-8B: L=32, nkv=8 (GQA, 32 query heads), d=128 Llama 3 20242·32·8·128·2 = 131,072 B = 128 KiB1.07 GB34 GB
Llama-3-70B: L=80, nkv=8 (64 query heads), d=1282·80·8·128·2 = 327,680 B = 320 KiB2.7 GB86 GB (more than the FP8 weights)
Hypothetical 70B with full MHA (nkv=64)2.5 MiB21 GB687 GB
Llama-3-70B with FP8 KV160 KiB1.3 GB43 GB
DeepSeek-V3 with MLA: 61 layers, cache a 512-dim latent + 64-dim RoPE key per token per layer DeepSeek-V2 202461·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

TechniqueMechanismReductionCost
MQA Shazeer 2019All query heads share one K/V head\(n_{heads}\times\)Some quality loss, training instability reported
GQA Ainslie+ 2023Query 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 latentLarge: tens of times smaller than equivalent MHAMore complex kernels (e.g. FlashMLA); decoupled RoPE needed; TP by heads no longer shards the cache
KV quantizationStore 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 attentionSome or all layers attend only to the last \(w\) tokens (e.g. local/global interleaving)Cache bounded by \(w\) on those layersArchitectural; 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+ 2023Bounded cacheLossy; fine for streaming chat, risky for retrieval over long docs
OffloadMove cold KV to CPU DRAM or SSD; bring it back on reuseCapacity, not sizeTransfer latency; worth it when recompute would cost more
Common mistake

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.

Interview angle

"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.

Go deeper

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.

SchemeHow it worksProblem
Static batchingCollect \(b\) requests, run them together until all finish, then take the next batchShort 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 completionFixes 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.
Static batching (b=4), '#' = useful decode step, '.' = idle slot seq A ########## seq B ####...... ← finished at step 4, slot wasted for 6 steps seq C ######.... seq D ########## new requests wait until step 10 Continuous batching: slots refill as soon as a sequence finishes slot 1 A A A A A A A A A A slot 2 B B B B E E E E E E ← E joins at step 5 (its prefill folded into that step) slot 3 C C C C C C F F F F slot 4 D D D D D D D D D D

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

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

Logical blocks → block table → physical KV blocks (block size = 4 tokens) Seq A (sample 1) L0 prompt L1 prompt L2 gen Seq B (sample 2) L0 prompt L1 prompt L2 gen Table A L0 → P7L1 → P1L2 → P4 Table B L0 → P7L1 → P1L2 → P2 Physical KV pool (GPU HBM) P0 free P1 rc=2 P2 B P3 free P4 A P5 otherP6 free P7 rc=2 • Shared prompt blocks P7, P1 have reference count 2 (parallel sampling / shared prefix). • Writing into a shared block triggers copy-on-write: allocate a new block, copy, decrement rc. • Only the last block of each sequence can be partially empty → near-zero fragmentation.
Paged KV cache. Logical blocks are contiguous per sequence; physical blocks are scattered. Sharing is just two table entries pointing at the same block.

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.

Intuition

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.

Interview angle

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.

Go deeper

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.

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

Common mistake

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.

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.

┌──────────────── router (KV-aware, SLO-aware) ─────────────────┐ request ──►│ │ ▼ │ ┌──────────────────┐ KV cache (GBs) over NVLink / RDMA ┌──────────────────┐ │ PREFILL POOL │ ───────────────────────────────────► │ DECODE POOL │──► tokens │ compute-bound │ (NIXL, Mooncake transfer engine) │ bandwidth-bound │ streamed │ small batches of │ │ huge batches, │ │ long prompts, │ │ CUDA graphs, │ │ TP for TTFT │ │ wide EP for MoE │ └──────────────────┘ └──────────────────┘ ▲ │ └──────── optional shared KV store: CPU DRAM / SSD (prefix cache) ◄──┘

Why split?

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

May be out of date

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.

Interview angle

"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.

Go deeper

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

One speculative step (k = 4) 1. Draft (cheap, sequential) "the cat" satonamat 2. Target: ONE forward pass over all k+1 positions computes p(·|prefix), p(·|…sat), p(·|…on), p(·|…a), p(·|…mat) cost ≈ one normal decode step (memory-bound) 3. Accept / reject left→right sat ✓ on ✓ a ✗ mat – → emit "sat on" + resample from (p − q)⁺ at the rejected slot: "the" → 3 tokens for one target pass (if all 4 accepted: 5 tokens, the extra "bonus" token comes free from the last position's p)
Draft, verify in parallel, accept the agreeing prefix, correct at the first disagreement. The KV entries for rejected tokens are discarded.

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=2k=4k=8limit k→∞
0.51.751.942.002
0.72.192.773.203.33
0.82.443.364.335
0.92.714.106.1310

(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

Variants

MethodDrafterKey ideaReported speedup (paper's setting)
Draft model Leviathan+ 2022Small model from the same family (same tokenizer), e.g. 1B drafting for 70BSimplest; needs a well-matched small model~2–3×
SpecInfer Miao+ 2023One or more small modelsDraft a tree of candidates, verify all branches in one pass with a tree attention mask—
Medusa Cai+ 2024Extra 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 lossless2.2× (Medusa-1), 2.3–3.6× (Medusa-2)
EAGLE Li+ 2024A single lightweight transformer layer that autoregresses over the target's features (second-to-top hidden states), fed the sampled token tooFeature-level drafting is easier than token-level; drafter sees target's internal state~3×
EAGLE-2 Li+ 2024Same drafterDynamic draft tree: expand branches where the drafter's confidence (a good proxy for acceptance) is high~20–40% over EAGLE
EAGLE-3 Li+ 2025Drafter 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 dataup to 6.5×; ~1.4× over EAGLE-2
Lookahead decoding Fu+ 2024None: the target itself runs Jacobi-style parallel iterations to generate n-gramsCollect n-grams in a pool, verify promising ones; no training needed~1.5–2.3×
N-gram / prompt lookup Saxena 2023String matching: find the last few generated tokens earlier in the prompt and propose what followedFree, great for summarization, RAG, code editing where output copies inputLarge on copy-heavy tasks, ~none otherwise
MTP heads (DeepSeek-V3) DeepSeek-V3 2024Multi-token-prediction modules trained with the model as an auxiliary objectiveAt 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

May be out of date

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.

Interview angle

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.

Go deeper

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.

SchemeWhat's low-precisionSpeeds upTypical methods / formats
Weight-only (W8A16, W4A16)Weights stored in INT8/INT4 with per-group scales; dequantized to BF16/FP16 inside the matmul kernelDecode, especially small batch (fewer bytes to read). Not prefill: the math is still in 16-bitGPTQ, 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 ratePrefill and large-batch decode (compute) plus the bandwidth winSmoothQuant (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 scalesCompute again 2× over FP8 on Blackwell-class hardwareNVFP4, MXFP4
KV cacheStored K/V tensorsLong-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

Quality trade-offs

PrecisionTypical quality impact (large models)Notes
FP8 W8A8, FP8 KVUsually within noise of BF16Default for production on Hopper/Blackwell
INT8 weight-onlyNear-losslessEasy 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 modelsSmaller models suffer more than large ones
NVFP4/MXFP4 W4A4Small drop with good calibration or quantization-aware trainingRecent; some models (e.g. gpt-oss) shipped with MXFP4 MoE weights natively
≤3-bitNoticeable degradation without QATMostly for edge or extreme memory limits
Common mistake

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).

May be out of date

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.

Go deeper

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.

StrategyWhat's splitCommunicationEffect on latency / throughputWhere it's used
Tensor parallel (TP)Each weight matrix (column/row split), attention heads across GPUs2 all-reduces per layer of a \(b \times d\) activation; latency-sensitive, needs NVLinkLowers 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/nodesPoint-to-point activations between stages; cheapDoesn'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) / replicasNothing: full copiesNone (a router in front)Linear throughput scaling; no latency changeDefault way to scale out
Expert parallel (EP)MoE experts across GPUsAll-to-all dispatch and combine per MoE layerLets huge MoE models fit; with wide EP, each GPU holds few experts and receives large per-expert batchesDeepSeek-V3/R1, Kimi, Qwen MoE at scale
DP attentionAttention replicated, each rank handles different requests; MoE layers use EPAll-to-all at the MoE boundaryAvoids 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 parallelThe sequence dimension (e.g. ring attention)Passing KV blocks around a ringCuts TTFT for very long promptsMillion-token prefill Liu+ 2023
Intuition

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:

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

Interview angle

"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.

Go deeper

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.

KnobDefinitionEffect / 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-kKeep the \(k\) most likely tokens, renormalizeFixed cut regardless of shape; too loose when the distribution is peaked, too tight when it's flat
Top-p (nucleus) Holtzman+ 2019Keep the smallest set whose cumulative probability ≥ \(p\)Adapts to shape; the default for chat (e.g. \(p = 0.9\)–\(0.95\))
Min-p Nguyen+ 2024Keep tokens with \(p_x \ge p_{\min} \cdot p_{\max}\)Scales the cut with model confidence; argued to stay coherent at higher temperatures
Repetition penaltyDivide positive logits (multiply negative ones) by \(\theta > 1\) for tokens already presentFights loops; too strong hurts code and data where repetition is correct
Frequency / presence penaltiesSubtract \(\lambda_f \cdot \text{count}(x) + \lambda_p \cdot \mathbb{1}[\text{count}(x) > 0]\) from logitsAdditive variants (OpenAI-style API parameters)
Beam searchKeep the \(B\) highest-probability partial sequences each stepApproximates the highest-likelihood sequence

Why beam search is rare for chat

Common mistake

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

Go deeper

17. Long-context serving

Long contexts (128k to 1M+ tokens) stress every part of the stack.

May be out of date

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

EngineOrigin / nicheSignature featuresStatus (as of late 2026, verify)
vLLM GitHubUC Berkeley; the de facto open-source defaultPagedAttention, 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 GitHubLMSYS / Berkeley+Stanford; high performance, programmableRadixAttention, cache-aware scheduling, fast structured outputs, strong DeepSeek/MoE support with large-scale EP and PD disaggregationVery active; widely used for large MoE serving
TensorRT-LLM GitHubNVIDIA; peak performance on NVIDIA GPUsOptimized kernels, in-flight batching, paged KV, FP8/NVFP4, speculative decodingActive; has moved toward a PyTorch-based workflow (verify the current default)
TGI (Hugging Face) GitHubEarly production server for HF modelsContinuous batching, paged attention, multi-backendMaintenance mode; the repo shows as archived (March 2026). Don't pick it for new deployments
llama.cpp GitHubLocal / edge, C/C++GGUF format, k-quants (2–8 bit), CPU, Apple Metal, CUDA, Vulkan; underlies many desktop toolsVery active
MLC LLM GitHubCompiler-based (TVM) universal deploymentCompile once, run on CUDA, Metal, Vulkan, WebGPU, iOS/AndroidActive; also home of XGrammar
NVIDIA Dynamo GitHubOrchestration layer above engines (not an engine)Disaggregated serving, KV-aware routing, KV block manager (multi-tier offload), SLA-based planner; backends: vLLM, SGLang, TensorRT-LLMOpen-sourced 2025; active
llm-d GitHubKubernetes-native distributed inference (Red Hat, Google, IBM, NVIDIA, CoreWeave and others)Prefix/KV-aware routing via the Gateway API inference extension, PD disaggregation over vLLMActive, 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.

May be out of date

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.

Interview angle

"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

AcceleratorMemoryBandwidthDense compute (approx)Inference relevance
NVIDIA H100 SXM NVIDIA80 GB HBM33.35 TB/s~989 TF BF16, ~1,979 TF FP8 (spec sheet doubles these with sparsity)The workhorse; FP8 support
NVIDIA H200 NVIDIA141 GB HBM3e4.8 TB/sSame as H100Same 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 systemFP4 inference; NVL72 rack-scale variants put 72 GPUs in one NVLink domain, ideal for wide EP
Google TPU v7 "Ironwood" Google192 GB HBM~7.2–7.4 TB/s~4.6 PF FP8Google's first inference-focused TPU; large pods with high-bandwidth ICI
Groq LPU WikipediaOn-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 CerebrasWafer-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.

May be out of date

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).

Worked example 2: 70B FP8 on an 8×H100 node

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

Interview angle

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.

Go deeper
  • 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.