NVL72.dev

Part III · Chapter 06

Serving as a Fleet

The previous chapter takes one request apart: prefill is compute-bound, decode is memory-bound, and separating them is the payoff for owning a coherent 72-GPU domain. This chapter puts the requests back together - thousands of them, arriving unannounced, in every shape - and asks what breaks first.

Requests do not arrive in batches

A benchmark is a coach party: everyone arrives together, wants the same thing, and leaves together. Real traffic is a walk-in restaurant. People turn up when they turn up, one orders a coffee and the next a seven-course meal, and the kitchen that was perfectly efficient for the coach party now has a queue out of the door.

The original way to serve a model was to collect requests into a batch, run the batch to completion, and start the next one. The failure is straightforward and was named precisely by the Orca paper: under request-level batching, "requests that have finished earlier than other requests in a batch cannot return to the client, while newly arrived requests have to wait until the current batch completely finishes".Yu et al., OSDI ’22

Since output lengths vary by more than an order of magnitude, a batch runs at the pace of its longest member. A request that needed twenty tokens waits for one that needed two thousand.

Orca's fix is iteration-level scheduling: the scheduler "invokes the execution engine to run only a single iteration of the model on the batch",Yu et al., OSDI ’22 so membership is reconsidered between every decode step. A finished request leaves immediately and a new one joins on the next token. The industry renamed this continuous or in-flight batching, and it is the single most important thing an inference server does.

It came with a second idea that is less discussed and equally necessary. Batching an attention operation across requests at different sequence positions is not a well-defined matrix operation, so Orca applies selective batching - "batching only to a selected set of operations",Yu et al., OSDI ’22 keeping the dense layers batched and handling attention per request.

Throughput holds, latency does not

Serving is judged on two clocks that pull in opposite directions. Time to first token is set by prefill and by however long the request queued. Time per output token is set by decode. The lever that improves throughput - a bigger batch - makes both worse, because decode is bandwidth-bound and a larger batch shares the same 8 TB/sMemory bandwidth per GPU NVIDIA between more sequences.

So a fleet has an SLO, typically a first-token target and a per-token target, and the number to watch is attainment: the fraction of requests that met it. The figure below runs a standard queueing model over a fixed pool. Throughput rises and then flattens, politely. Attainment falls off a cliff well before that.

The interesting region is the one nobody plans for: the load at which the fleet is still delivering almost every token it is capable of delivering, and a quarter of users are already outside the latency target.

Load against a fixed pool

Raise the arrival rate · switch the batching
Offered load
-
Pool utilisation
-
Throughput
-
Median TTFT
-
SLO target
-
Attainment
-

This figure is interactive and needs JavaScript. The prose around it states every number it shows.

Standard M/M/1 queueing over one pool: utilisation is arrival rate over service rate, and waiting time is exponentially distributed, so attainment against a first-token target t is 1 − ρ·exp(−μ(1−ρ)(t − prefill)). Service rate is 12 requests per second for the rack - about 4,800 tokens/s at a 400-token mean output - which is a plausible rate for a large model rather than a measured one. Request-level batching is modelled as continuous batching at 45% of the service rate, the mean-over-maximum penalty for waiting on the longest member of a batch when output lengths are spread. Poisson arrivals are kinder than real traffic, which is bursty, so treat the cliff's position as optimistic and its existence as the point.

Two pools, and the ratio between them

Disaggregation splits prefill and decode onto separate GPUs, which is the right decision for the reason the previous chapter gives - the two phases want different hardware characteristics. It also creates a new operational problem that only appears at fleet scale: the correct ratio between the pools depends on the traffic mix, and the traffic mix changes during the day.

A shift towards long documents and short answers loads prefill. A shift towards short prompts and long generations loads decode. Neither pool can help the other, so a fleet sized for the morning mix is mis-sized by the afternoon, and rebalancing means moving model weights between GPUs.

This is where KV cache management stops being an implementation detail. Every in-flight request holds a cache proportional to its context, so the batch size a pool can sustain is set by memory, not by arithmetic - and admitting one more request can evict another's cache, forcing a recomputation that looks exactly like a latency spike with no cause. Prefix caching cuts the other way, making repeated system prompts nearly free, which is why serving the same application at scale is cheaper per token than the benchmark suggests.

The honest summary is that a served fleet has more state than a training job does. A training run is one long computation with a checkpoint; a fleet is thousands of short ones sharing a memory budget, and almost every performance surprise in serving traces back to that budget rather than to the model.

Why the cliff is a queueing result rather than a GPU one

Nothing about the shape of that attainment curve is specific to accelerators. Any single-queue system with random arrivals has a mean wait proportional to ρ/(1−ρ), which is finite and unremarkable up to about 80% utilisation and then goes to infinity. Serving inherits this from queueing theory, not from HBM.

What is specific to this workload is how much it hurts. A request occupies its slot for hundreds of sequential decode steps, so service times are long and the queue that forms in front of them is correspondingly expensive - and unlike a web server, you cannot shed the tail by timing out cheaply, because the work already done on a half-generated response is lost with it.