hello·ai

Prefill & decode

AssumesParameters & layersAttention

In one line

One request is two different workloads with two different bottlenecks, and every latency number you care about belongs to one or the other.

Why it exists

"The model is slow" is not an actionable statement, because a request has two phases whose costs scale with different inputs and which respond to entirely different fixes.

processes the prompt. produces the output, one token per full pass through the model. Prefill is compute-bound and scales with prompt length. Decode is memory-bandwidth-bound and scales with output length. Optimising the wrong one is the most common wasted week in this area.

PREFILL — one parallel pass over the whole prompt← 2,000 prompt tokens, all at once →compute-bound · sets time to first tokenDECODE — one token per full pass through the modelt1t2t3t4t5… 400 more, one at a timememory-bandwidth-bound · sets everything after the first tokenlatency = TTFT(prompt length) + output length × inter-token latencyTwo different bottlenecks. Shortening a prompt and shortening an answer fix different numbers.
Two phases, two bottlenecks. Shortening the prompt and shortening the answer fix different numbers.

Prefill: one parallel pass#

The whole prompt is available at once, so every position can be processed in parallel. One pass through the model computes the representations for all 2,000 of your prompt tokens simultaneously, filling the as it goes.

Because it is a large matrix multiplication with plenty of parallel work, prefill saturates the GPU's arithmetic units. It is genuinely -bound, and the standard estimate applies: roughly two operations per parameter per token.

Prefill is what sets . Doubling the prompt roughly doubles the wait before anything appears, which is the latency a user in a streaming interface actually perceives as "slow to start".

The important property of prefill is that it is cacheable. The computation for a given prefix is deterministic, so if the next request begins with the same 2,000 tokens of system prompt and examples, its KV entries can be reused wholesale. exploits exactly this, which is why stable content belongs at the start of a prompt and volatile content at the end — the same ordering instinct as a Dockerfile.

Decode: one token at a time#

Once prefill has finished, generation is : produce a token, append it, produce the next. Each step is a full pass through every layer to produce a single token.

Here is the part that surprises people. That pass reads all 16GB of an 8B model's weights from memory in order to compute one token's worth of output. The arithmetic is trivial by GPU standards; the memory traffic is not. Decode is bound by , and a GPU running a decode step at batch size one is mostly idle, waiting on memory.

This single fact explains most of serving:

Output length dominates latency. A 2,000-token prompt and a 50-token answer is fast. A 200-token prompt and a 2,000-token answer is slow, on the same model.

is roughly constant per model and load, so total time is predictable: time to first token, plus output tokens times the per-token gap.

Batching is close to free on the decode side. Since the bottleneck is reading weights, and one read can serve every request in the batch, doubling the batch barely changes per-token latency while doubling . This is the entire basis of and the reason serving economics work at all.

Where the randomness comes in#

Every decode step ends the same way: the model emits — one raw score per vocabulary entry — and something has to pick one token from them. That choice is the only place randomness enters the whole pipeline, and it is entirely under your control.

turns the scores into a probability distribution. is a divisor applied before that step: below one it sharpens the distribution toward the likeliest token, above one it flattens it. then trims the tail, sampling only from the smallest set of tokens whose probabilities add up to p. is the degenerate case — always take the top token — and it is what temperature zero effectively means.

Two consequences matter in practice. For extraction, classification and anything you will compare in an , you want greedy: the same input then produces the same output, run to run, which is what makes a regression measurable. For prose, greedy produces flat, repetitive text, and a moderate temperature with top-p is the usual setting.

And note what temperature is not: a correctness dial. Turning it down makes the model more consistent, not more truthful. A confidently wrong answer at temperature zero is still wrong, just reproducibly so.

Where the levers are#

Because the two phases respond to different inputs, the fixes are different, and it is worth being explicit about which lever moves which number.

To reduce time to first token: shorten the prompt, cache the stable prefix, or reduce queueing. Nothing about the answer affects it.

To reduce total response time: cap output tokens, ask for a more compact format, or so the user sees progress while the rest arrives. A smaller model helps here more than anywhere else, since per-token cost falls with weight size.

To reduce cost per token: raise batch size, which is a serving-side concern, and shorten prompts, which is yours. On a per-request basis, prompt tokens usually dominate the bill — a 2,000-token prompt with a 200-token answer is ten to one before any pricing asymmetry.

To reduce : cap maximum output tokens. The tail here is unusual in that it is partly a property of what the model decided to say, and an unbounded generation is an unbounded latency.

deserves a mention because it attacks the decode bottleneck directly: a small draft model proposes several tokens, the large model verifies them in one parallel pass, and accepted drafts come for free. Output is identical to normal decoding, so it is a pure latency win where it applies.

The math

Total latency splits cleanly into the two phases:

Ttotal=Tqueue+2NPRcomputeprefill  +  O×ttokendecodeT_{\text{total}} = \underbrace{T_{\text{queue}} + \frac{2 N P}{R_{\text{compute}}}}_{\text{prefill}} \;+\; \underbrace{O \times t_{\text{token}}}_{\text{decode}}

where NN is parameters, PP prompt tokens, OO output tokens, RR the effective compute rate, and ttokent_{\text{token}} the inter-token latency.

For an 8B model, a 2,000-token prompt, a 400-token answer, an effective 100 TFLOP/s and a measured 12ms per output token:

Tprefill=2×8×109×20001×1014=0.32 sT_{\text{prefill}} = \frac{2 \times 8{\times}10^9 \times 2000}{1{\times}10^{14}} = 0.32\text{ s} Tdecode=400×0.012=4.8 sT_{\text{decode}} = 400 \times 0.012 = 4.8\text{ s}

5.12 seconds, and 94% of it is decode. Halving the prompt saves 160ms. Halving the answer saves 2.4 seconds. That ratio is the whole point of separating the phases.

Worked example

A support summariser takes 30 seconds per ticket and the team wants it under 10. Measured breakdown:

queue                              0.4 s
prefill  (6,000-token transcript)  1.1 s
decode   (1,200-token summary)    28.5 s   <- 95%

The instinct is usually to trim the prompt, since 6,000 tokens looks like the big number. It would save about half a second.

Instead, three changes to the output side:

1. Ask for structured bullets, not prose      1,200 -> 320 tokens
2. Cap max_tokens at 400                      hard ceiling on the tail
3. Stream to the UI                           perceived wait = TTFT, ~1.5 s
new decode   320 x 0.012                      3.8 s
new total                                     5.3 s

Under target, with no model change, no hardware change and no serving work. The diagnosis came entirely from knowing which phase owned the time.

Gotchas

  • Optimising the prompt when decode owns the latency. It is the more visible number and usually the smaller one. Measure the split before touching anything: time to first token versus total time tells you immediately which phase to work on.

  • Leaving max_tokens unbounded. The model decides how long to be, so your p99 latency and your worst-case bill are both set by its verbosity rather than by you. A cap is the cheapest tail-latency control available.

  • Benchmarking at batch size one. Decode throughput improves several-fold under batching, so a single-request benchmark tells you about latency and nothing useful about capacity. Load-test at your intended concurrency.

  • Putting volatile content at the front of the prompt. A timestamp or a user id before the system prompt invalidates the whole cached prefix on every request. Stable first, volatile last, exactly as you would order layers in a container build.

Mental model

Prefill is a bulk load and decode is a cursor. The bulk load is wide, parallel and cacheable; the cursor walks row by row, and no amount of hardware makes a single cursor walk faster — you can only run more of them at once. Most serving work is either making the bulk load reusable or making the cursors share a scan.

In practice

Problem statements that lean on this topic. Rated by the phase that makes them land.

Done reading?

Nothing marks itself complete. Say so only when you could explain this to someone else.

Next: KV cache

This topic has 4 subtopics.