Designing an LLM serving system
Prefill against decode, the KV cache, continuous batching, paged attention, tensor and pipeline parallelism, scheduling, prefix caching, speculative decoding and autoscaling.
On this page
Serving a large language model breaks the assumptions every other system in this series rests on. A request does not have a response time, it has a stream. The model is too large for one chip. The cost of a request depends on how long its answer turns out to be, which nobody knows when it arrives. And the resource that limits throughput is not compute or network but the memory holding a per-request cache that grows with every token generated. This article is about the handful of mechanisms that make that tractable.
The map
Read this first when short on time. Every branch is a section below.
What it must do, and at what size
It must accept a prompt, stream back generated tokens, serve many users concurrently on expensive hardware, and hold a latency contract. It must not, in this article, cover training, fine-tuning, or the product layer around the model — the next article builds that on top of this one.
Model 70B parameters at 2 bytes each = 140 GB of weights
one accelerator has ~80 GB -> at least 2 chips, usually 4
KV cache 2 (keys and values) x layers x kv_heads x head_dim x 2 bytes
= 2 x 80 x 8 x 128 x 2 ~= 320 KB per token
an 8,000-token conversation ~= 2.5 GB for ONE request
Memory 4 chips x 80 GB = 320 GB
minus 140 GB of weights and overhead -> ~150 GB for cache
-> roughly 60 concurrent 8k-token requests, not thousands
Latency time to first token: ~200-500 ms is a good target
time per output token: ~20-50 ms, must beat reading speed
Unit cost tokens per second per accelerator is the number that mattersRead that memory line again, because it is the whole article in one number. Concurrency is capped in the tens, not the thousands, and the cap comes from a cache whose size grows with every token generated. Every technique below is either a way to make that cache smaller, to use it more efficiently, or to get more useful work out of each pass over the weights.
Prefill and decode: one request, two workloads
Generation happens in two phases with entirely different performance characteristics, and failing to separate them is the most common mistake in this area.
Prefill processes the whole prompt at once. Every token can be computed in parallel, so it is a large matrix multiplication: the accelerator is genuinely busy, arithmetic units saturated, and the time scales with the prompt length. It is compute bound.
Decode produces one token at a time, each depending on the last, so there is no parallelism across tokens within a request. Generating a single token requires reading every parameter in the model — 140 gigabytes of weights — to do a small amount of arithmetic. It is memory bandwidth bound, and it is where nearly all the wall-clock time goes.
The consequence is the central insight of LLM serving. If one request is decoded alone, the accelerator spends its time waiting on memory and its arithmetic units idle — an enormously expensive machine running at a few percent of capability. But the weights that were just read can serve any number of sequences in the same step. Batching thirty-two sequences reads the weights once and produces thirty-two tokens instead of one, at almost the same wall-clock cost.
Batching is therefore not an optimisation here; it is the design. Throughput scales nearly linearly with batch size until memory runs out, and per-token latency barely moves. Everything else in this article exists to make larger batches possible.
The KV cache
Attention needs every previous token's keys and values to compute the next token. Recomputing them each step would make generation quadratic, so they are cached — and that cache is the resource that limits everything.
Its size is fixed by the model's shape and grows linearly with sequence length: for each token, keys and values, for every layer, for every attention head. For a large model that is around 320 kilobytes per token, so a single conversation with eight thousand tokens of context holds about two and a half gigabytes — for one user. A hundred concurrent users with long contexts would need more memory than the weights themselves.
Three properties follow. Memory caps concurrency, not compute: the question "how many users can this machine serve" is answered by dividing free memory by cache-per-request. The cache grows during the request, so a sequence admitted safely can exhaust memory later, which makes admission a prediction rather than a check. And architectural choices target it directly — grouped-query attention, which lets several query heads share one set of key and value heads, exists largely to cut this cache by a factor of several, at a small quality cost.
- Prefill
- Processing the prompt, computing its keys and values in one parallel pass. Compute bound, scales with prompt length.
- Decode
- Generating one token at a time, reading all weights per step. Memory bandwidth bound, scales with output length.
- KV cache
- Stored keys and values for every token so far, per sequence. The resource that determines how many requests fit.
Continuous batching
Given that batching is everything, the question is how to form batches when requests arrive continuously and finish at unpredictable times.
Static batching — collect N requests, run them together until all are done — fails badly for two reasons. The batch cannot start until it is full, so early arrivals wait. And it cannot finish until the longest generation finishes, so a request producing a thousand tokens holds thirty-one slots idle while it completes alone. With output lengths varying by an order of magnitude, most of the batch's capacity is wasted.
flowchart TD
S["scheduler runs before every token step"] --> F{Any sequence finished?}
F -- yes --> R["release its cache blocks"]
R --> A
F -- no --> A{Room for a waiting request?}
A -- yes --> AD["admit it, run its prefill"]
A -- no --> D["run one decode step for the batch"]
AD --> D
D --> S
Continuous batching — also called in-flight batching — makes the batch a living set rather than a fixed group. Before each token step the scheduler removes sequences that have finished, admits waiting requests into the freed space, and runs one decode step for whatever is currently resident. A request that finishes early releases its memory immediately; a request that arrives mid-flight joins within one step rather than waiting for a batch boundary.
The effect on utilisation is large: reported throughput improvements of several times over static batching are typical, without changing the model or the hardware at all. It is the single highest-leverage technique in this article.
Paged attention
Continuous batching exposes a memory management problem. How much cache should be reserved for a request whose output length is unknown?
The naive answer — reserve enough for the maximum possible output — wastes most of the memory, because the maximum is rarely reached: a request that could generate two thousand tokens and stops after eighty has held twenty-five times what it needed. Contiguous allocation adds external fragmentation on top, leaving gaps too small to use. Together these can waste the majority of available cache memory, which translates directly into a smaller batch and lower throughput.
The fix is borrowed from operating systems: allocate the cache in fixed-size blocks with a page table per sequence. Logical positions map to physical blocks through an indirection, blocks are allocated as the sequence grows, and they need not be contiguous. Waste falls to a fraction of one block per sequence — a few percent instead of a majority.
The indirection buys a second, equally valuable property: blocks can be shared. Many requests begin with the same long system prompt, and with a page table they can point at the same physical blocks for that prefix instead of each storing a copy. When sequences diverge, the shared block is copied on write. For a product where every request carries the same few thousand tokens of instructions, this is an enormous saving in both memory and prefill compute.
- vLLM introduced paged attention and continuous batching together, and the combination is now standard across serving stacks.
- TensorRT-LLM and TGI implement the same ideas with different names, which is a good sign they are structural rather than incidental.
- Prefix caching across requests is how providers offer discounted pricing for repeated prompt prefixes: the saving is real and mechanical.
Splitting the model across chips
A hundred and forty gigabytes of weights does not fit on an eighty-gigabyte accelerator, so the model must be divided. There are three ways, with different communication costs.
| Strategy | How it splits | Communication | Use when |
|---|---|---|---|
| Tensor parallel | Each layer's matrices are split across chips, every chip holds a slice of every layer | A collective operation on every layer, every step — very high | Within one machine, over a fast interconnect |
| Pipeline parallel | Layers are divided into consecutive stages, one stage per chip | Activations passed between stages only — low | Across machines, where the link is ordinary networking |
| Expert parallel | A mixture-of-experts model places different experts on different chips | Routing tokens to their experts and back | Sparse models, where only some experts run per token |
flowchart TB
subgraph T["Tensor parallel"]
direction LR
T1["chip 1: a slice of every layer"] --> T2["collective every layer"]
T2 --> T3["chip 2: the other slice"]
end
subgraph P["Pipeline parallel"]
direction LR
P1["node A: layers 1-40"] --> P2["activations only"]
P2 --> P3["node B: layers 41-80"]
end
T ~~~ P
The practical arrangement is a hierarchy that matches the hardware: tensor parallelism inside a node, where chips are connected by a high-bandwidth interconnect that can absorb a collective per layer, and pipeline parallelism across nodes, where the link is ordinary networking and communication must be rare. Getting this backwards — tensor parallelism across a network — produces a system that spends most of its time in communication.
Pipeline parallelism introduces bubbles: while stage one works on a batch, stages two and three have nothing to do. Splitting the batch into microbatches so that several are in flight at different stages fills most of the gap, which is the same technique as instruction pipelining in a processor and has the same residual inefficiency at the start and end.
Scheduling: when prefill meets decode
Now the two phases collide. A newly admitted request needs a prefill pass, which is a large compute burst, while resident sequences want a steady rhythm of decode steps. Running the prefill blocks the decodes, and every user in the batch sees their stream pause.
This is visible to users as a stutter: tokens arriving smoothly, then a gap, then smoothly again. The longer the incoming prompt, the longer the gap, so one user pasting a large document degrades everyone else's experience simultaneously.
Chunked prefill is the standard answer: split a long prompt into pieces and interleave those pieces with decode steps, so the batch keeps generating while the prompt is absorbed over several steps. Time to first token for the new request rises slightly; the stutter for everyone else disappears. It is a direct trade of one user's latency against the smoothness of all the others, and stating which side you are protecting is part of the design.
Preemption handles the harder case. When memory runs out mid-generation — because admitted sequences grew longer than predicted — some sequence must yield. Its cache blocks are either swapped out or simply discarded and recomputed when it resumes, which is cheaper than it sounds because prefill is fast and compute-bound. Choosing the victim is a scheduling policy: most recently admitted, lowest priority, or the one whose recomputation is cheapest.
Admission cannot be a simple memory check, because a request's cache grows as it generates and nobody knows how long its output will be. Admit on an estimate, monitor cache occupancy as the real signal, and have a preemption path ready — a serving system without one will eventually fail requests mid-stream instead of degrading.
Caching and shortcuts
Three techniques buy back significant cost, in descending order of how easily they are obtained.
Prefix caching is the largest easy win. In most products every request begins with the same system prompt and often the same conversation history, so computing that prefix once and reusing its cached keys and values across requests removes both repeated prefill compute and duplicated memory. With paged attention this falls out of block sharing almost for free.
Speculative decoding attacks the memory-bandwidth bound directly. A small, fast draft model proposes several tokens ahead; the large model then verifies all of them in a single forward pass — which costs about the same as generating one token, because that pass is bandwidth bound and the extra tokens ride along. Accepted drafts are effectively free, rejected ones cost only the draft. When the draft model is well matched, two to three times the tokens per second at identical output quality is achievable, because verification guarantees the result matches what the large model would have produced.
Semantic caching — answering a new question from a stored answer to a similar one — is the most tempting and the most dangerous. It changes the output rather than accelerating it, similarity is not equivalence, and a cached answer may be stale or subtly wrong for the new phrasing. It belongs to narrow, well-understood domains rather than to a general assistant.
Autoscaling
Every scaling assumption from the rest of this series fails here, and it is worth being explicit about why.
Cold start is minutes, not seconds. A new replica must load a hundred and forty gigabytes of weights from storage into accelerator memory, which takes minutes even with fast storage and warm caches. Reactive autoscaling against a traffic spike arrives long after the spike is over.
There is no scale to zero, and capacity is bought in whole accelerators — a large, indivisible unit. Utilisation cannot be smoothed the way it can with stateless containers.
Utilisation is the wrong signal. Accelerator utilisation looks high during decode even when the machine is doing very little useful work, because it is waiting on memory. The signals that mean something are queue depth, cache occupancy — how full the KV memory is, which is the true measure of how close to capacity the replica is — and the latency targets themselves.
So the shape is: keep warm replicas sized for expected load plus headroom, scale on leading indicators with generous lead time, queue with a deadline rather than accepting unbounded work, and shed politely at the front door when full — a clear rejection with a retry hint is far better than admitting a request that will time out after occupying memory for a minute.
Cost, quantisation and routing
The unit of cost is tokens per second per accelerator, and there are three levers on it.
Quantisation stores weights in fewer bits — eight or even four instead of sixteen. Because decode is bandwidth bound, halving the bytes read per step nearly halves the time per step, so the speedup is roughly proportional. It also shrinks the weights, leaving more memory for KV cache and therefore a larger batch. The cost is some quality loss, which varies by model and task and must be measured on the actual workload rather than on public benchmarks.
Shorter contexts attack the cache directly. Trimming conversation history, summarising older turns and retrieving only relevant passages reduce cache per request linearly, which raises concurrency by the same factor.
Routing is the largest lever of all: send the easy majority of requests to a small model and reserve the large one for requests that need it. Most traffic in most products does not need the biggest available model, and the cost difference between model sizes is an order of magnitude — far larger than any serving optimisation can deliver.
What breaks first
- KV cache memory. Always first. It caps batch size, which caps throughput, and it grows with context length and concurrency simultaneously. Relieved by paging, prefix sharing, shorter contexts and quantised weights that leave more room.
- The interconnect. Under tensor parallelism, a collective operation runs on every layer of every step, so a slow link between chips turns into per-token latency directly.
- Prefill interference. Long prompts stalling everyone else's decode, visible as stutter. Chunked prefill is the fix and it costs the new request a little latency.
- Cold start on a spike. Minutes to load weights means capacity cannot be summoned reactively, so headroom must be bought in advance and queues must have deadlines.
The judgement underneath all of it: throughput and latency trade directly here in a way they do not elsewhere. A larger batch means more tokens per second per accelerator and a slightly slower stream for each user. There is no configuration that optimises both, so the design must say which one it protects — and for an interactive product the answer is usually latency, bought with more hardware.
Recap
- Generation has two phases: prefill processes the prompt in parallel and is compute bound; decode produces one token at a time and is memory bandwidth bound.
- Decoding a single token reads every parameter in the model, so serving one request alone uses a few percent of the hardware.
- Batching is therefore the design, not an optimisation: the weights read in a step can serve every sequence in the batch, so throughput scales nearly linearly with batch size.
- The KV cache stores keys and values for every token so far, around 320 KB per token for a large model, so one long conversation holds gigabytes.
- Memory, not compute, caps concurrency — in the tens, not thousands — and the cache grows during a request, so admission is a prediction rather than a check.
- Static batching wastes most of its capacity because the batch waits to fill and cannot finish until its longest generation does.
- Continuous batching re-forms the batch before every token step, releasing finished sequences and admitting waiting ones, for several times the throughput on identical hardware.
- Reserving cache for the maximum possible output wastes most of memory; fixed-size blocks with a page table per sequence cut waste to a few percent.
- Block indirection also lets requests share a common prompt prefix, saving both memory and prefill compute, with copy-on-write when they diverge.
- Tensor parallelism splits every layer and needs a fast interconnect, so it belongs inside a node; pipeline parallelism splits layers, communicates rarely, and suits crossing machines — with microbatches to fill its bubbles.
- A prefill pass blocks every decode in the batch, which users perceive as a stutter; chunked prefill interleaves the two and trades one request's latency for everyone else's smoothness.
- Preemption is required, because admitted sequences can outgrow the memory that was available when they started.
- Prefix caching is the largest easy win; speculative decoding buys two to three times the tokens per second by verifying several drafted tokens in one bandwidth-bound pass.
- Autoscaling is slow and coarse: minutes of cold start, no scale to zero, and accelerator utilisation is a misleading signal — queue depth and cache occupancy are the real ones.
- Quantisation speeds decode nearly in proportion to the bytes saved, and routing easy requests to a smaller model is a larger lever than any serving optimisation.
- Throughput and latency trade directly through batch size, so the design must state which one it protects.
Questions
Try answering each one out loud before opening it. Lead with the one-line answer, then the reasoning, then one detail from a real system.
Why are prefill and decode treated as separate workloads?
Because they have opposite bottlenecks: prefill processes every prompt token in parallel and saturates the arithmetic units, while decode produces one token at a time and is limited by how fast weights can be read from memory.
- Prefill time scales with prompt length; decode time scales with output length.
- Decode leaves the arithmetic units mostly idle, which is why a single request uses a small fraction of the machine.
- They also interfere: running a prefill pauses the decode steps of every other sequence in the batch.
Almost every serving technique is best understood as addressing one phase or the conflict between them.
Why is batching the central mechanism rather than an optimisation?
Because decode reads all 140 GB of weights to produce one token, and those same weights can produce a token for every sequence in the batch at nearly the same cost — so batch size multiplies throughput almost linearly.
- Serving one request at a time wastes most of a very expensive machine.
- Per-token latency barely increases with batch size until memory or compute limits are reached.
- Everything else in the design — paging, prefix sharing, quantisation — exists to allow larger batches.
This is the opposite of most systems, where batching trades latency for efficiency at the margin.
What is the KV cache and why does it dominate capacity planning?
It holds the keys and values of every previous token so attention does not recompute them, and it grows linearly with sequence length — roughly 320 KB per token for a large model, so an 8,000-token context is about 2.5 GB for a single request.
- Concurrency is free memory divided by cache per request, which lands in the tens rather than the thousands.
- The cache grows during the request, so a safely admitted sequence can exhaust memory later.
- Grouped-query attention exists largely to shrink it, by sharing key and value heads across query heads.
Without it, generation would be quadratic in sequence length, so the memory cost buys a very large compute saving.
What is wrong with static batching?
The batch cannot start until it is full, so early arrivals wait, and it cannot finish until its longest generation finishes, so most slots sit idle while one request continues alone.
- Output lengths vary by an order of magnitude, which makes the waste severe rather than marginal.
- A request arriving just after a batch forms waits for the entire batch to complete.
- Continuous batching fixes both by re-forming the batch before every token step.
Moving from static to continuous batching often multiplies throughput several times with no hardware change at all.
Explain continuous batching.
The scheduler runs before every token step: it removes sequences that have finished, frees their cache, admits waiting requests into the space, and then executes one decode step for whatever is currently resident.
- A finished request releases its memory immediately rather than at a batch boundary.
- A newly arrived request joins within one step, so queueing delay is bounded by a token step rather than by a whole generation.
- It requires the cache allocator to handle sequences entering and leaving constantly, which is what paging provides.
It is the single highest-leverage technique in LLM serving and is now standard across serving stacks.
Why apply paging to the KV cache?
Because output length is unknown, so reserving the maximum possible wastes most of the memory, and contiguous allocation fragments what remains — together these can waste the majority of cache memory, which directly shrinks the batch.
- Fixed-size blocks with a page table per sequence allocate as the sequence grows and need not be contiguous.
- Waste falls to a fraction of one block per sequence, a few percent instead of a majority.
- The indirection also enables sharing blocks between sequences.
It is a direct transplant of virtual memory into a machine learning runtime, and it works for the same reasons.
How does prefix sharing save money?
Most requests in a product begin with the same system prompt and often the same conversation history, so with a page table they can point at the same physical cache blocks for that prefix instead of each computing and storing their own.
- It removes repeated prefill compute as well as duplicated memory.
- Divergence is handled by copying the block on write.
- Providers offering discounted pricing for cached prompt prefixes are passing on exactly this saving.
For a product with a few thousand tokens of shared instructions on every call, it is often the largest single cost reduction available.
Tensor or pipeline parallelism?
Tensor parallelism inside a machine and pipeline parallelism across machines, because tensor parallelism performs a collective operation on every layer of every step and needs a high-bandwidth interconnect, while pipeline parallelism communicates only between stages.
- Tensor parallelism across ordinary networking spends most of its time communicating.
- Pipeline parallelism introduces bubbles, since later stages idle while the first works; microbatches fill most of the gap.
- Mixture-of-experts models add expert parallelism, where routing imbalance becomes the risk.
Matching the parallelism strategy to the hardware topology is what keeps communication off the critical path.
Why do users see a stutter in their token stream?
Because a newly admitted request's prefill is a large compute burst that blocks the decode steps of every sequence in the batch, so all of their streams pause while one long prompt is processed.
- The longer the incoming prompt, the longer the gap everyone else experiences.
- Chunked prefill splits the prompt into pieces interleaved with decode steps, removing the stall.
- The cost is slightly higher time to first token for the request being admitted.
It is an explicit trade between one user's start latency and every other user's smoothness, and the design should say which it protects.
Why does a serving system need preemption?
Because the KV cache grows as sequences generate, so a batch admitted safely can run out of memory later — and without preemption the only remaining option is failing requests mid-stream.
- The victim's blocks can be swapped out or discarded and recomputed on resume, which is cheap because prefill is fast.
- Choosing the victim is a policy: lowest priority, most recently admitted, or cheapest to recompute.
- Admission has to be based on an estimate of output length, which is inherently uncertain.
A system without a preemption path degrades by erroring rather than by slowing down, which is the wrong failure mode.
How does speculative decoding produce speed without changing output?
A small draft model proposes several tokens, and the large model verifies all of them in a single forward pass — which costs about as much as generating one token because that pass is bandwidth bound — accepting the prefix that matches what it would have produced anyway.
- Accepted tokens are effectively free; rejected ones cost only the draft model's time.
- Output is identical in distribution to decoding normally, because verification is exact.
- A well-matched draft model can deliver two to three times the tokens per second.
It works precisely because decode wastes most of its memory bandwidth on a single token, leaving room to check several.
Why is autoscaling harder here than for ordinary services?
A new replica must load a hundred and forty gigabytes of weights before it can serve anything, which takes minutes; capacity is bought in whole accelerators; and there is no scaling to zero.
- Reactive scaling arrives long after the spike that triggered it.
- Headroom must therefore be bought in advance and paid for while idle.
- Queues need deadlines, and the system should reject politely at the front door rather than admit work it cannot finish.
This inverts the usual cloud assumption that capacity is elastic and nearly instantaneous.
Why is accelerator utilisation a misleading signal?
Because during decode the device looks busy while it is mostly waiting on memory, so high utilisation can coexist with very little useful work being done.
- Queue depth measures whether requests are waiting, which is what users feel.
- KV cache occupancy measures how close the replica is to its real capacity limit.
- The latency targets themselves — time to first token and time per output token — are the ultimate signals.
Scaling on utilisation tends to add capacity late and remove it too early, which is the worst combination.
How does quantisation improve serving, and what does it cost?
Storing weights in fewer bits reduces the bytes read per decode step, and since decode is bandwidth bound the speedup is roughly proportional; it also frees memory for a larger KV cache and therefore a larger batch.
- Halving the bits roughly halves the time per token and doubles the room for cache.
- The cost is some quality loss, which varies by model and by task.
- It must be measured on the actual workload, since public benchmarks often miss task-specific degradation.
It is a rare change that improves latency, throughput and concurrency simultaneously, which is why it is usually tried first.
What is the largest cost lever available?
Routing: sending the easy majority of requests to a smaller model and reserving the largest model for the ones that need it, because the cost difference between model sizes is an order of magnitude — larger than any serving optimisation can deliver.
- Most traffic in most products does not require the biggest available model.
- Shorter contexts are the next lever, since cache per request falls linearly with context length.
- Serving optimisations compound with these rather than replacing them.
The engineering work is in classifying requests reliably, which is itself a model problem with its own failure modes.
How do throughput and latency trade off, and how do you choose?
Directly, through batch size: a larger batch produces more tokens per second per accelerator and a slightly slower stream for each user, and no configuration optimises both.
- An interactive product protects latency and buys throughput with more hardware.
- A batch workload — summarising a corpus overnight — protects throughput and accepts long queues.
- Serving both from one pool without separating them means each degrades the other.
Saying explicitly which side is protected is the design decision that everything else follows from.