Systems

Continuous batching

The same twelve requests scheduled twice: static batches strand GPU slots behind their slowest member, continuous admission refills them next step. Watch the score diverge live.

Run
Speed 1x
Time step 0
STATIC BATCHESslot 0slot 1slot 2slot 3CONTINUOUS BATCHINGslot 0slot 1slot 2slot 3
0%
Static utilisation
0%
Continuous utilisation
0 vs 0of 12
Completed
- vs -steps, completed so far
Mean turnaround

Twelve requests, four GPU slots, lengths from 4 to 26 decode steps. Press play. The two schedulers receive exactly the same stream.

Generation lengths are wildly unequal: one user asks for a word, another for an essay, and nothing about the prompt reliably tells you which. That variance is the central scheduling problem of serving.

Classic static batching handles it badly in an instructive way. Grab a batch of requests, run them together, and the batch is only finished when its LONGEST member is. Every request that finishes early leaves its slot idle, hatched red in the upper panel, until the straggler completes. With one 26-step request batched beside a 4-step one, most of that window is a GPU multiplying zeros.

Continuous batching, sometimes called in-flight batching, is the fix every serious serving stack converged on: admission happens per step, not per batch. The moment a slot frees, the next queued request takes it. Nothing about the arithmetic gets faster, and the lower panel does exactly the same work; it simply refuses to let a finished request hold capacity hostage. Watch the completed counter: the gap opens the first time a short request ends inside a long batch, and does not close until the static schedule finally drains, long after the continuous panel has finished.

Two things make this practical for transformers specifically. Each decode step is a batch-wide forward pass anyway, so swapping one sequence for another between steps is cheap. And paged KV caches remove the memory obstacle: a newly admitted request can take non-contiguous cache blocks, so admission does not wait for a tidy hole. Continuous batching came first, from Orca, and paged attention arrived in vLLM a year later, but they belong together because each one removes the other's obstacle.

The simplification to be honest about: this model charges every request one slot for its whole length, ignoring the prefill step being heavier than decode steps and ignoring per-token batching effects on kernel efficiency. Those change the constants, not the shape: idle slots are pure loss under any accounting.

The maths

What a static batch costs
Tbatch  =  maxibatchi,idle  =  Smaxjj    iiT_{batch} \;=\; \max_{i \in \text{batch}} \ell_i, \qquad \text{idle} \;=\; S \cdot \max_j \ell_j \;-\; \sum_{i} \ell_i

The batch takes as long as its longest member, and the idle work is everything the S slots could have done in that window minus what they actually did, which also charges any slots the batch left unfilled. High variance in lengths makes it enormous, and length variance is exactly what generation has.

Utilisation
U(t)  =  busy slot-steps up to tSmin(t,Tmakespan)U(t) \;=\; \frac{\text{busy slot-steps up to } t}{S \cdot \min(t, T_{\text{makespan}})}

S is the number of GPU slots, four here. The two live utilisation percentages below the panels: the fraction of GPU slot-time spent on an actual request. The clamp at the makespan means a schedule that has finished keeps reporting utilisation over its own busy window instead of decaying while the slower panel catches up. Continuous admission pushes this toward the ceiling set by arrivals, static batching caps it by construction.

Related terms

More visualisations

Building with language models?

These explainers come out of the work. If you want the same thinking applied to your own system, that is what I do.

See how I can help