0%
Sizing inference

Sizing LLM inference without a GPU run

Every team putting an LLM into production faces the same expensive question: how many GPUs, configured how, for the traffic pattern they expect to serve?

← Home

Today, teams benchmark a handful of configurations and extrapolate from a space of tens of thousands. We take a different approach: we simulate the serving stack, run through every permutation and combination of the configuration in under ten minutes, and find the optimal deployment configuration before running a single GPU experiment.

This post explains how you get to the optimal GPU deployment configuration fast, and how close the predictions land to real hardware.

01How many GPUs?

Say you’re about to put a 27-billion-parameter model into production. You need it to serve, say, 5,000 tokens a second to your users, and your traffic looks like a coding assistant: ISL ~8,000 tokens, OSL ~300. Then we have to answer: how many H100s do we rent, and how do we run them?

This is not division. Dividing required throughput by single-GPU throughput assumes per-GPU throughput is a constant, and it is not: it depends on how you split the model and on the traffic you serve. The same eight H100s deliver 1,255 or 2,325 tok/s depending only on how the model is split across them, and adding GPUs returns less than linear throughput.

On-demand, an 8×H100 node runs between $55.04 and $98.32 an hour depending on the provider. Every cost in this post is priced at $50/hr - a round figure below the cheapest of those, so the numbers here understate what you would actually pay. Even at that rate one node is $438,000 a year, used or idle.

Buy too many and the waste is obvious - provision 64 GPUs where 24 would have carried the load and you’re paying for 40 idle cards, month after month. Under-provisioning costs more than the GPUs you skipped: you miss your latency target, requests queue, and users see a laggy product. And the fix is slow - adding GPUs means new hardware, re-testing the configuration, and redeploying, so a shortfall you find in production can take weeks to close.

The third failure mode is the least obvious: you can buy the right number of GPUs and still throw away half your performance by arranging them wrong. Eight GPUs can run as one replica split across all eight, as eight independent single-GPU replicas, or several ways in between. In our tests two configurations of the same eight cards delivered 1,255 versus 2,325 tok/s - nearly double, from identical hardware, at essentially the same per-token latency. And no configuration wins for every workload, so the answer you buy today can be wrong in six months.

Figure 1 recasts those numbers as unit cost - the hourly rate of the nodes a configuration occupies, over the output tokens it actually produces in that hour:

\[ \text{cost per Mtok} \;=\; \frac{\text{node rate }(\$/\text{hr})}{\text{tok/s}\;\times\;3600 \;/\; 10^{6}} \]

At $50/hr per 8-GPU node, 1,255 tok/s gives \(\$50 / 4.518 = \$11.07\) per million output tokens. Every point is measured.

Measured cost per million output tokens against GPU count for Qwen3.6-27B FP8 on H100-SXM-80, priced at $50 per hour for an eight-GPU node. On 8 GPUs tp=8/pp=1 sustains 1,255 tok/s at $11.07 per million output tokens; on the same 8 GPUs tp=4/pp=2 sustains 2,325 tok/s at $5.97, the cheapest point. 16 GPUs at dp=2 give 2,386 tok/s at $11.64; 32 at dp=4 give 4,446 tok/s at $12.50; 64 at dp=8 give 7,186 tok/s at $15.46. The curve rises to the right because replica scaling is sublinear. A footer notes real on-demand rates run $55.04 to $98.32 per hour, that $50 is used throughout as a round figure below the cheapest so every cost shown is conservative, that the top of the range multiplies every value by about two, and that both percentage changes are unaffected by the rate because they are ratios.
Figure 1. Unit cost, measured, at $50/hr per 8-GPU node. Two results fall out. Scaling out raises the price of a token: dp=1 to dp=8 buys 5.7× the throughput for 8× the GPUs, so cost per million output tokens climbs 40%, from $11.07 to $15.46. And on a fixed 8 GPUs, changing only the parallelism to tp=4/pp=2 cuts unit cost 46% to $5.97, cheaper than every scaled-out option here. Real on-demand rates for this node run $55.04-$98.32/hr, so at the top of that range multiply every figure by about two - the percentages do not move, because they are ratios. The sizing question is therefore not only how many GPUs, but which configuration you run on them.

02Why the answer depends on your traffic

Every request runs two phases with different bottlenecks. Prefill processes the whole prompt in one forward pass and saturates the GPU’s arithmetic units, so it is compute-bound. Decode emits one token per iteration, and every iteration re-reads the KV cache of every request in the batch. Arithmetic per token is negligible, so decode is bound by memory bandwidth: the rate the GPU streams the KV cache out of HBM.

Every quantity used in this post, defined once:

TermDefinitionUnit
TTFTTime to first token, measured from request submission, so it includes queueingms
TPOTTime per output token, averaged over a request’s decode tokensms
Decode throughputOutput tokens per second summed over all in-flight requeststok/s
ConcurrencyRequests in flight at once, admitted and not yet finishedrequests
max_num_batched_tokens
the “token budget”
Most tokens the scheduler may place in one forward pass, across the whole batch. Not a per-request sequence limittokens
KV cache capacityHBM bytes available for the paged KV cache after weights and marginbytes
Memory bandwidthRate the GPU streams KV cache and weights out of HBMbytes/s
ISL / OSLInput and output sequence length per requesttokens

Which limit binds first depends on traffic shape:

Three panels, one per workload shape, each showing which resource runs out and how full it already is, with bars drawn to scale. Coding assistant, prefill-dominated: a stacked bar of token work shows prefill at 8,000 tokens filling 96 percent and decode at 300 tokens a thin sliver; below it the same 8,000-token prompt is drawn twice, split by the per-iteration token budget into 2 chunks at a 6,144 budget and 12 chunks at a 682 budget, each chunk being one iteration where prefill competes with decode. Interactive chat, latency-bound: a bar of time per iteration against the 50 millisecond target is almost full at 48 milliseconds with 2 milliseconds left, so one more request would slow every iteration; below it a mostly empty bar shows about 125 megabytes per request against 72 gigabytes that would hold roughly 590 of them, so the wall is time rather than bytes. Reasoning or agent, KV-cache-bound: two mirrored bar pairs. KV cache per request grows six times from 250 megabytes to 1.5 gigabytes while the request runs, and the number of requests that fit in the same 72 gigabytes falls six times from 295 to 49. Same budget, six times the bytes each, and decode re-reads all of it.
Figure 2. Every request runs prefill once, then decode in a loop. Each panel shows what those phases do to one resource, drawn to scale. Coding fills the token budget: prefill is 96% of the work, and a prompt too large for one pass is split into chunks that then compete with decode. Chat fills the latency budget: 48 ms of a 50 ms target, so the batch stops growing while HBM sits largely unused. Reasoning fills HBM, and the two mirrored bars are the mechanism - cache per request grows 6× as the request runs, so requests that fit fall 6×, from 295 to 49, while they are still running.

A benchmark published for one of these traffic shapes carries no information about the others.

The knobs that decide it

Each knob trades one outcome against another. The direction, the mechanism, and where the direction stops holding:

The knobs are coupled, which is why they cannot be tuned one at a time: tensor parallelism changes how many replicas fit, batch size moves you between bandwidth-bound and compute-bound, and precision moves the capacity and bandwidth limits together while changing output quality.

A measured sweep of five serving configurations for one workload: Qwen3.6-27B FP8 on H100-SXM-80, 5,000 active sessions, input sequence length about 8,000 tokens with p90 near 20,000, output about 300 with p90 near 1,200, roughly 70 percent cache hit. Rows ordered by GPU count with a bar for decode throughput and a column for average time-to-first-token. Eight GPUs at tp=8/pp=1/dp=1 give 1,255 tok/s at about 956 seconds. Eight GPUs at tp=4/pp=2/dp=1 give 2,325 tok/s at about 491 seconds. Sixteen at dp=2 give 2,386 tok/s at about 477 seconds. Thirty-two at dp=4 give 4,446 tok/s at about 237 seconds. Sixty-four at dp=8 give 7,186 tok/s at about 117 seconds. Annotations note that dp 1 to 8 multiplies throughput by 5.7 rather than 8, and that changing only the parallelism on the same eight GPUs gives 1.85 times.
Figure 3. The same five configurations measured on one workload, so the levers read as numbers rather than directions. Scaling replicas dp=1 to dp=8 - 8 GPUs to 64 - multiplies decode throughput by 5.7, not 8. On a fixed 8 GPUs, changing only the parallelism to tp=4/pp=2 gives 1.85×. TTFT here is dominated by queueing at 5,000 arrivals, so read the fall from ~956 s to ~117 s as backlog draining, not as model latency.

03Why you can’t just test every option

The obvious answer is to measure. Set up each option, send it realistic traffic, write down how fast it runs, and pick the best. Testing like this works, and it should always be the final check before you commit. The problem is that there are far too many combinations to test them all.

A meaningful benchmark run - enough traffic to reach steady state and read a stable number - takes roughly 30 minutes on the GPUs the configuration needs. At $50/hr for an 8×H100 node, a single 8-GPU test costs about $25. Test 50 configurations and you’re at about $1,250 and a full day of machine time; do it properly - a few traffic levels each, repeated for noise - and it’s closer to $6,000 and a week.

Now count the configurations. Each of these is a real choice you have to make, and they multiply:

ParameterTypical optionsCount
GPU typeA40, A100, H100, H2004
Tensor parallelism1, 2, 4, 84
Pipeline parallelism1, 2, 43
Replicas1 up to the node count4
PrecisionFP8, BF16, INT8…3
KV-cache formatFP16, FP82
max_num_batched_tokensa handful of settings5
Serving modeco-located, disaggregated2
Multiplied together11,520

And that is a conservative slice - it leaves out finer scheduler and runtime knobs. Figure 4 takes this exact grid through the pipeline. At $25 a test, running even this reduced grid on real hardware would cost around $290,000 and take months. In practice teams test a handful of configurations, pick the least-bad one, and leave most of the space unexplored.

As your product grows, prompts get longer, traffic rises, and usage patterns shift - and the configuration that won at launch may not win six months later. You can’t spend $6,000 and a week every single time your traffic changes.

So the goal isn’t to test faster. It’s to predict how a configuration would perform without running it - for traffic you describe rather than record - accurately enough to rank the options before renting a single GPU.

04How we search the whole space

The whole search runs on CPUs. No GPU is touched while we explore the 11,520 configurations - we simulate each one. The most detailed simulator replays the serving engine’s scheduler step by step, and even at a few seconds per configuration that would still take hours across the full grid. So we don’t run the detailed simulator on everything.

Instead we filter in stages, cheapest first. The early stages are nearly free and run on every configuration, dropping the ones that can’t work. Only the 20 selected candidates reach the detailed simulation. The early stages are set algebra over model and device facts, so they are orders of magnitude cheaper per candidate than a scheduler replay; we report the candidate counts each stage admits and rejects, not per-stage timings.

The seven sieves as tapering horizontal bars, cheapest stage first. A solid bar means the set shrank at that stage; a dashed outline means the same set with more work done. Stage 1, rule out the invalid, takes 11,520 down to 7,920, removing 3,600 that do not divide evenly, exceed a node, or exceed the GPU budget; free integer arithmetic running on all 11,520. Stage 2, fits in memory, leaves 7,840, removing 80 whose weight shard alone was too large for a 45 GB card in BF16 or INT8, at microseconds each. Stage 3, size the batch, keeps 7,840 and drops none, sizing each configuration's largest batch within KV cache and the latency target; the first stage to use the SLOs. Stage 4, coarse rank and shortlist, cuts to 20 using a cheap analytical estimate of throughput per GPU under SLA read from a profiled kernel database rather than by simulating. Stage 5, detailed simulation, replays the scheduler iteration by iteration on those 20 across five traffic levels, 100 replays at about 3 seconds each, minutes of CPU time. Stage 6 ranks against the throughput and time-to-first-token targets using the conservative end of each estimate, leaving one or two. Stage 7 confirms with one deployment on real GPUs. A callout notes arithmetic runs on all 11,520, the analytical estimate on 7,840, only the simulation costs seconds and sees 20, and that benchmarking the grid on real GPUs would instead be 5,760 GPU-hours, about $290,000 and eight months on one node.
Figure 4. The seven sieves on the 11,520-configuration grid from the table above. A solid bar means the set shrank at that stage; a dashed outline means the same set with more work done, which is why stages 3 and 5 repeat a count. The cost hierarchy is the point: stages 1 and 2 are arithmetic and run on all 11,520; stage 4’s analytical estimate is cheap enough to run on all 7,840 survivors; only stage 5 costs seconds, and it sees 20; only stage 7 touches a GPU, and it sees one. The decisive cut is the shortlist at stage 4, and those 7,820 are set aside rather than rejected - they would all run. The comparison that matters is against hardware: benchmarking this grid on real GPUs is 5,760 GPU-hours, roughly $290,000 and eight months on one node.

The search is a sieve of seven stages, cheapest first. Each stage runs on everything the previous one passed, and each costs more per configuration than the last while seeing far fewer of them. The counts below are for the 11,520-configuration grid in the table above.

  1. Rule out the invalid. Tensor parallelism has to divide both the attention head count and the embedding width, pipeline depth has to divide the layer count, the configuration has to fit inside one node, and GPUs per replica times replica count has to stay within the GPU budget you are willing to provision. Integer arithmetic on the model shape, so effectively free. Removes 3,600 of 11,520, leaving 7,920. One consequence surfaces later: when tensor parallelism exceeds the number of key/value heads, those heads are replicated rather than split, so past that point the KV cache stops shrinking as you add GPUs.
  2. Check it fits in memory. Per GPU, take 90% of the card as the budget, subtract the weight shard, and convert what remains into fixed 16-token KV cache pages. Reject anything whose weights alone overflow the budget, or that cannot hold one page plus a 1% reserve. Microseconds. Removes 80, leaving 7,840 - all of them a 45 GB card trying to hold a 27B model in BF16 or INT8. On 80 GB cards nothing fails here, which is itself worth knowing.
  3. Size the batch. For each surviving configuration, sweep batch size and parallelism to find the largest batch it can actually run before it either runs out of KV cache or misses your latency target. This is the first stage that consults your SLOs rather than just the hardware, and it is why a configuration that looks feasible on memory alone can still turn out useless.
  4. Coarse rank, and shortlist. A cheap analytical estimate - throughput per GPU under your SLA, read from a profiled kernel database with interpolation rather than by simulating anything - orders what is left and keeps the top 20, with a limit of three sharing the same GPU model and parallelism shape so one family cannot take every slot. This is the decisive cut: 7,840 down to 20. It is fast enough to run on everything, which is the whole reason it comes before the simulator.
  5. Detailed simulation. Now, and only now, the event simulator runs on the survivors. It replays the serving engine’s scheduler iteration by iteration - admission, the per-iteration token budget, chunked prefill - reading each operation’s time from the profiled table. About 3 seconds per configuration on CPU. Twenty configurations across five traffic levels is 100 replays, minutes of CPU time.
  6. Rank against your SLOs. Configurations are scored on how well they fit the targets you actually gave - throughput and time-to-first-token - using the conservative end of each simulated estimate rather than the midpoint. A configuration whose conservative bound clears your target is reported differently from one that only clears it on the point estimate, and when two cannot be separated we say so instead of inventing a winner.
  7. Confirm on real hardware. The one or two finalists get a real deployment on real GPUs to verify the number before you commit the spend. That run also feeds back into the profiling and calibration data, so the next search starts from better tables.

The shape of the cost is the point. Stages 1 and 2 are arithmetic and run on all 11,520. Stages 3 and 4 are analytical estimates, cheap enough to run on everything that survived. Only stage 5 costs seconds, and it sees 20. Only stage 7 touches a GPU, and it sees one. Set against the alternative: benchmarking the full grid on real GPUs is 5,760 GPU-hours, about $290,000 and eight months on a single node. This path is a CPU search measured in minutes, plus one confirmation run on GPUs - $25 of hardware.

Why being within 8% is good enough

Assumption: candidate decode throughputs are separated by more than the prediction error. Ranking does not need exact speeds, only correct order. An 8% band preserves the order whenever candidates differ by more than that; when two fall inside it we report a tie and name the single benchmark that settles it. Benchmarking is not removed, it is reduced to the one or two configurations where it changes the decision.

05The prediction holds on real hardware

We served Qwen 3.6 27B FP8 with vLLM on H100s at TP=2, drove it with SWE-bench coding traces, and compared predicted against measured decode throughput in every two-minute window, with nothing trimmed and no outliers removed.

max_num_batched_tokens is the per-iteration token budget: the most tokens the scheduler may place in one forward pass across every request in the batch. It is not a per-request sequence limit. Dividing it by concurrency gives the tokens available per in-flight request, which is the quantity the error tracks.

Concurrencymax_num_batched_tokensTokens per requestMeasuredPredictedError
849,1526,144107.6 tok/s107.7 tok/s+0.10%
1432,7682,341123.6 tok/s113.5 tok/s−8.20%
128,192683114.2 tok/s104.3 tok/s−8.70%
Three row groups showing measured versus predicted throughput bars on one shared scale. The first pair is visually identical at +0.10% error. The lower panel plots error against scheduling pressure, descending from near zero into a band of 8 to 9 percent.
Figure 5. Rows run from most tokens per request to fewest, and the error grows as that number shrinks: 6,144 tokens per request is off by 0.10%, 683 by about 9%. This is aggregate decode throughput - the number you size a cluster on. Per-request latency is a harder problem, and we’re upfront below about where it holds and where it doesn’t.

Across every configuration tested, predicted decode throughput lands within 8.7% of measured. Two things about how that error behaves matter more than the headline figure: what it tracks, and which direction it points.

The error tracks the per-request token budget, not concurrency. With 6,144 tokens available per request the prediction lands within 0.10%; at 2,340 and 682 tokens per request it under-predicts by about 8%.

Prompt chunking explains it. An 8,000-token prompt cannot enter a single forward pass unless the per-request budget covers all 8,000, so the scheduler splits it into chunks and feeds one chunk per iteration. At 6,144 tokens per request the prompt is split into 2 chunks and so occupies 2 iterations. At 682 tokens per request the same prompt is split into 12 chunks and occupies 12 iterations. In every one of those iterations the prefill chunk competes with decode work for the same token budget, and that contention is the part the prediction models least well - which is why the tighter budgets carry the 8% error.

Concurrency does not explain the ordering: the middle row runs at higher concurrency than the last.

The direction is consistent: all but one comparison under-predicts, and the exception over-predicts by 0.10%. For sizing that is the safe direction - if we say a configuration sustains 5,000 tok/s and we under-predict, the real hardware has slightly more headroom than promised, not less.

The safe direction holds for throughput, not for percentile latency. Aggregate decode throughput we under-predict, so real hardware has more headroom than promised. Predicted p95 time-per-output-token goes the other way - it comes in below measured, which is optimistic, and contention under heavy load is the cause. So treat predicted p95 TPOT as a lower bound: size on throughput, and if you hold a strict p95 latency SLO, make that the number you confirm on hardware before committing.

Windowed tracking, not just the mean

A matching mean can come from cancelling errors, so the stronger test is whether the prediction tracks throughput as traffic rises and falls. Below is the same four-hour run in two-minute windows.

Decode throughput over time on SWE-bench coding traces, comparing the real serving engine against the prediction across 120 two-minute windows spanning about four hours. Both traces oscillate strongly between roughly 45 and 195 tokens per second and rise and fall together: every major peak and trough in the real trace has a matching peak and trough in the predicted trace at the same point in time. The predicted trace sits slightly below the real one through most of the run, with the gap shaded, giving means of 105.0 against 115.3 tokens per second, a ratio of 0.91.
Figure 6. SWE-bench traffic is bursty, so decode throughput swings between ~45 and 195 tok/s as long prompts consume the per-iteration token budget and briefly starve decode. What matters is that the prediction turns where the real system turns, ~120 times over the run, without ever seeing the real trace. The only gap is a steady ~9% under-prediction - we track the shape, we’re just slightly conservative on the level. (Values are read off the run chart, so treat them as good to a token or two per second.)

We ran the same test on a mixture-of-experts model, Qwen 3.6 35B-A3B, where per-token compute depends on which experts each token activates - harder to predict. The match was tighter still.

Decode throughput over time for a mixture-of-experts model, comparing the real serving engine against the prediction across roughly 5,250 seconds. The two traces are nearly indistinguishable: they oscillate together between about 45 and 280 tokens per second and every peak and trough coincides. Reported means are 170.424 tokens per second for the real engine and 170.415 for the prediction, a difference of 0.005 percent.
Figure 7. Over 87 minutes, the real average was 170.424 tokens a second and we predicted 170.415 - 0.005% apart. Agreement to three decimal places on the mean is coincidence; the load-bearing result is that the two series track each other across the whole run, which requires the per-iteration behaviour to be right.

Both sides ran the same work

The comparison only means anything if the simulator and the real engine were handed identical work. They were - not because we compared two graphs afterwards and found them similar, but because there is only ever one trace.

AIPerf generates it once, from a fixed seed, using its agentic code-generation dataset. That is why the traffic looks like a coding assistant. AIPerf then validates its own output. That single file is the only source of truth: the CSV the simulator reads is a column-by-column copy of it, and every row still carries the original record it came from, so any individual request can be checked against the source rather than trusted because a histogram matched.

Arrival times are deterministic: each request sits at a fixed point in the schedule rather than being drawn at random, so the same spec always produces the same timings. Five fields are pinned per request - input length, requested output length, arrival time, which session it belongs to, and the prefix blocks that decide cache reuse. Output length is exact on the simulator side because there is no sampler in it at all: no logits, no end-of-sequence token, nothing that could stop early. It counts, and a request finishes once it has produced its input tokens plus the output tokens the trace asked for. One precondition belongs in the run config rather than the data: replay can scale token counts and can trim prompts longer than the configured maximum, so this holds only when those scale factors are 1.0 and the maximum exceeds the longest request in the trace.

A provenance chain: a workload spec plus an integer seed feed one seeded AIPerf run emitting a single dataset file, which is the single source of truth. On the left it is projected column by column into the replay CSV the simulator consumes, each row embedding its original record. On the right the same file drives the real engine. A centre box lists the fields pinned per request: input token count, output token count, scheduled arrival time, session grouping, prefix-block hash ids. A note records that arrivals carry no randomness. An amber box explains that output length is exact rather than a sampled outcome, because the simulator has no logits, no end-of-sequence token and no stop condition, completing a request once it has emitted the input tokens plus the requested output tokens.
Figure 8. Both sides are driven from one file rather than reconciled afterwards. A single seeded AIPerf run emits the trace; the CSV the simulator reads is a column-by-column copy of it, and every row still carries the original record it came from - so any individual request can be checked against the source. Arrival times are computed, not sampled, so repeating the run reproduces the timings exactly. Output length is exact because the simulator has no sampler: it finishes a request when it has produced the input tokens plus the output tokens the trace specified. The one thing to verify is in the run config, not the data - that replay is not scaling token counts or trimming prompts.

The system predicts throughput accurately enough to get the full ranking right. It tells you which configurations are clearly better, flags the handful that are genuinely too close to call, and sends only those few to real hardware to settle. That is what sizing needs: approximate SLO numbers for every configuration - throughput and latency estimated closely enough to order the candidates and to say which ones are worth measuring - rather than an exact speed for any single one of them.

06Where the accuracy comes from

Decode throughput is output tokens over wall-clock time. We decompose that into three segments, each modelled and validated on its own:

Compose them - iteration cost over the batch the scheduler assembled, driven by the arrival process - and you have throughput. Validating each segment separately is what makes a miss diagnosable instead of mysterious.

Three columns setting out what the prediction models, one per unknown. Iteration cost, the wall-clock time of one forward pass, models bytes streamed over weights and KV cache, per-operator kernel latency read from a profiled table and interpolated across shapes, collective cost profiled per node topology, every attention family separately including multi-head latent, grouped-query, multi-query and sliding-window, precision byte widths, the language-model head and vocabulary projection, and a calibrated non-KV memory overhead; it is built to err slow rather than fast. Batch composition, which requests occupy that pass, replays the vLLM V1 scheduler iteration by iteration rather than averaging it, covering two-phase scheduling, per-iteration token budget accounting, chunked prefill, preemption with budget rollback, prefix-cache admission and reuse, paged KV block allocation, sliding-window reservation caps, decode CUDA-graph capture sizes and speculative decoding, reproducing the same admission and batching decisions the engine would make. Arrivals models fitted input and output length distributions sampled once into a materialised trace, arrival times placed deterministically rather than sampled, prefix-block identities so cache reuse is part of the trace, and session grouping; it is exact by construction because both sides are driven from one artifact. A footer reports how close the composition lands: across three configurations aggregate decode throughput is within 0.1 to 8.7 percent of measured and always on the conservative side; it tracks the shape rather than just the mean, turning where the real series turns window by window on bursty traffic; and on a mixture-of-experts model the two means agree to 0.005 percent.
Figure 9. Throughput has three unknowns, and each gets its own model rather than a fudge factor. The middle column is the one that matters most: the scheduler is replayed iteration by iteration, not approximated by an average batch size - token budget accounting, chunked prefill, preemption and rollback, prefix-cache admission, paged block allocation. It reproduces the same admission and batching decisions the engine would make, step for step, which is what lets the prediction follow the peaks and troughs in Figures 6 and 7 rather than only the mean. Iteration cost is built to err slow rather than fast, so throughput and capacity land on the conservative side. Arrivals contribute no error at all, because both sides are driven from the same artifact. Composed, the three land within 0.1-8.7% of measured decode throughput across three configurations.

Two of these carry most of the risk. Iteration cost: We combine hardware limits with measured kernel behaviour, and the model is deliberately biased slow rather than fast: it will not claim a step is quicker than the hardware delivers. That is why the throughput number stays conservative. And the scheduler: which requests share a batch, when prompts are admitted, how prefill competes with decode. That cannot be reduced to an average batch size, so we replay the real vLLM scheduler instead.

Time-to-first-token cumulative distributions for two configurations of the same model, each comparing the real serving engine against the prediction. In both panels the predicted curve sits consistently to the left of the measured curve, meaning the prediction expects first tokens slightly sooner than the hardware delivers them. For the concurrency 12 configuration the p95 ratio is 0.92 times; for the concurrency 8 configuration it is 0.83 times. The offset is present across the whole distribution rather than only in the tail.
Figure 10. Time-to-first-token is where the scheduler replay still slips. The predicted distribution sits the same small distance off the measured one at p50, p80 and p95, in both configurations. That’s a steady bias, not random error - the shape is right, just shifted - which means we can correct it.

07Three workloads, three different answers

We ran three traffic patterns through the search on the same model and GPU fleet. Each returned a different best configuration, because each binds on a different limit.

Three columns compare one 27B dense model on the same GPU fleet under three traffic profiles. Each hits a different wall first: agentic coding binds on prefill budget, interactive chat on the latency ceiling, and RL rollout generation on KV capacity. The winning configuration differs in each case.
Figure 11. Same model, same GPUs, three traffic patterns - each runs into a different limit first (the diagonal). Coding hits the arithmetic/prefill wall, chat hits the latency wall, reasoning hits the KV cache capacity wall.

The best configuration can change with load alone: one that wins at one level of concurrent in-flight requests can fall behind a different one when that load rises only slightly, because the extra requests tip it past a KV cache capacity or latency limit that the other configuration absorbs. Eight replicas give about 5.7× the throughput of one, not 8×, and even that only shows up once there’s enough traffic to keep all eight busy - more replicas raise the throughput ceiling, they do not create traffic. And two configurations of the same 8 GPUs can be nearly 2× apart in throughput at the same latency, purely from how they’re wired. None of this appears on a spec sheet. It emerges only when real traffic competes for the batch, the KV cache, and the GPU, which is what the scheduler replay reproduces.

08Sizing this for your own workload

If you’re staring at the same question - how many GPUs, arranged how, for the traffic you serve today and the traffic you’re growing into - you don’t have to guess your way to an answer or burn weeks of GPU time benchmarking a handful of configurations. Describe your model and the traffic you expect, and we’ll search the space for you and come back with the configuration that fits, the GPU count behind it, and the evidence for why.

Facing this problem? If you’re sizing an LLM deployment and want the right answer before you commit the budget, get in touch - tell us your model and workload, and we’ll help you find the optimal deployment configuration.