hello·ai

Serving & batching

AssumesPrefill & decodeKV cache

In one line

Decoding is bandwidth-bound, so one read of the weights can serve many requests at once — and that single fact is what makes the economics work.

Why it exists

A GPU running one for one request is mostly idle. It reads 16GB of weights from memory to compute a single token, and while that read happens the arithmetic units have almost nothing to do.

Serving many requests together fixes this, because the same weight read serves every request in the batch. rises several-fold, per-token latency barely moves, and cost per token falls proportionally. Nearly everything about how models are served in production follows from this one asymmetry.

STATIC BATCH — everyone waits for the longest requestdashed = GPU idle, slot held by a finished requestCONTINUOUS BATCH — a finished slot is refilled at the next stepshaded = a new request joined mid-flightDecoding is bandwidth-bound, so one read of the weights serves everyrequest in the batch. Throughput rises several-fold.
A finished slot is refilled at the next decode step rather than at the end of the batch. That is the difference between an idle GPU and a busy one.

Why batching is nearly free on the decode side#

The bottleneck during generation is , not compute. Reading the weights is the expensive part; multiplying them by one token's activations is not.

So when you batch eight requests, you read the weights once and do eight times the arithmetic — and since the arithmetic was never the constraint, the step takes only slightly longer. Eight times the output for perhaps 1.2 times the time.

That relationship holds until the arithmetic does become the constraint, which happens somewhere in the tens of concurrent requests depending on hardware. Past that point, adding requests raises per-token latency roughly linearly and throughput flattens. The curve has a knee, and finding it on your hardware is the main tuning exercise.

behaves differently. It is already compute-saturated by a single long prompt, so batching prefill gives much less. This is why serving systems treat the two phases separately, and why some disaggregate them onto different hardware pools entirely.

Continuous batching#

Static batching collects N requests, runs them together, and returns when the longest finishes. The flaw is visible as soon as you draw it: a request needing 80 output tokens sits in a batch with one needing 800, and its slot is held — producing nothing — for 720 steps.

evaluates the batch at every decode step instead. A finished request leaves immediately and a waiting request joins in its place at the next step. The GPU is never holding an idle slot.

The improvement over static batching is typically two to four times on realistic traffic, and it is larger the more variable your output lengths are. Every modern serving stack does this — vLLM, TGI, TensorRT-LLM — and if you are running your own inference and not using one of them, this is the first thing to fix.

Paired with it is paged attention, which stores the in fixed-size blocks rather than one contiguous per-request allocation. Without it, each slot must reserve memory for the longest output it might produce, which wastes most of the cache. With it, memory is allocated as tokens are actually generated. The two features together are what made high-concurrency serving practical.

What actually limits concurrency#

Not compute. Almost always memory.

GPU memory        80 GB
weights (8B, fp16)     16 GB
overhead                4 GB
available for cache    60 GB
per request at 4k ctx   0.54 GB
max concurrent          ~111

A serving system refusing new requests while GPU utilisation reads 45% is not broken. It is out of cache. This is the single most common confusion when operating an inference server, and the metric to watch is cache occupancy rather than utilisation.

Which means the levers on capacity are mostly memory levers:

Shorter contexts — cache is linear in sequence length, so halving context doubles concurrency.

Cache — 8-bit keys and values halve the per-request footprint for a small quality cost, and for long-context workloads this is usually the highest-leverage change available.

Weight — frees memory for cache and speeds decoding, because there is less to read per token.

Grouped-query attention — an architectural property of the model rather than a knob, but it is why a model advertised as long-context-efficient actually is.

The tension you cannot avoid#

Throughput and per-request latency move in opposite directions under load, and no configuration serves both.

Large batches mean excellent throughput, low cost per token, and worse because new requests queue behind a full batch. Small batches mean the reverse.

The resolution is not a clever setting; it is separate pools. An interactive endpoint runs small batches and aggressive admission control. A batch pipeline runs large batches and accepts queueing. Trying to serve both from one deployment produces a configuration that is wrong for both, and this is a capacity-planning decision rather than a tuning one.

Two controls belong on the interactive path specifically. Cap maximum output tokens, because here is partly a property of what the model decided to say. And use so shared prefixes skip prefill entirely, which removes queueing pressure at its source.

is worth noting as the one technique that improves latency without hurting throughput much: a small draft model proposes tokens, the large model verifies several at once. Output is identical, so it is a pure win where the draft model's acceptance rate is high.

Worked example

An 8B model on one A100 80GB, interactive chat, 4k contexts, 300-token answers. Baseline is a naive loop with static batch size 8:

concurrency                          8
throughput                      310 tokens/s
p50 time to first token         1.4 s
GPU utilisation                  31%
cost per 1M output tokens       $2.60

Switch to a continuous-batching server with paged attention:

concurrency                        111   (cache-limited)
throughput                    2,850 tokens/s   (9.2x)
p50 time to first token         0.9 s
GPU utilisation                  78%
cost per 1M output tokens       $0.28   (-89%)

Same model, same hardware, same quality. Then add 8-bit cache quantization:

per-request cache      0.54 GB -> 0.27 GB
concurrency                111 -> 222
throughput             2,850 -> 3,900 tokens/s
p99 latency            rises from 4.1 s to 6.8 s   <- the knee

The last line is the one to notice. Throughput kept rising and the tail got meaningfully worse, because concurrency has passed the compute knee. For a batch pipeline that is a good trade. For an interactive product it is not, and the right setting is somewhere below the maximum the memory allows.

Gotchas

  • Reading GPU utilisation as capacity. A cache-limited server rejects requests at moderate utilisation. Monitor KV cache occupancy, and alert on it rather than on compute.

  • Benchmarking at batch size one. It measures latency and tells you nothing about capacity, which scales completely differently. Load-test at your intended concurrency or the numbers are fiction.

  • Serving interactive and batch traffic from one pool. is a direct trade between throughput and latency, so one setting cannot serve both. Separate deployments, separate settings.

  • Leaving output length uncapped. One request generating 4,000 tokens holds a slot for the whole run, degrading the tail for everyone else on the box.

  • Tuning the server before shortening the prompt. Prompt tokens usually dominate cost, and prefix caching is often a larger win than any batching change. Look at the request before looking at the fleet.

Mental model

It is a connection pool where the expensive resource is memory bandwidth rather than sockets, and continuous batching is what turns a fixed-size pool into one that releases a connection the instant its work finishes. Everything else — admission control, queue discipline, separate pools for interactive and batch — is the capacity planning you have already done for a database, applied to a different bottleneck.

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.

This topic has 4 subtopics.