Beyond a Forward Pass · Part 1 of 5 · LLM inference · July 2026
Part 1 of 5. Why variable-length generation breaks classic batching — and how iteration-level scheduling became the foundation of modern LLM serving.
A vision model takes an image, runs one forward pass, and says “cat.” An LLM generates one token at a time, with variable-length inputs and outputs, multi-turn context, and memory that grows as the conversation grows. That is why specialized engines — vLLM, SGLang, TensorRT-LLM — exist.
This series takes each of the five differences he walked through and goes to foundation depth: what breaks without the technique, how it is implemented, and what you should measure when you run or buy inference.
Autoregressive decoding is a loop. Given prompt tokens \(x_1,\ldots,x_n\), the model
produces \(y_1\), then \(y_2\) conditioned on \(x\) and \(y_1\), and so on until EOS or
max_tokens. You cannot know \(T_{\text{out}}\) at request start. That single
fact destroys classic batching assumptions.
Batching still matters: GPU kernels amortize weight loads across sequences. Loading 14 GB of BF16 weights once to process 32 tokens is far better than loading them 32 times. But the batch composition must change while generation is in flight.
Static batching collects up to B requests, runs them until every sequence in the batch hits EOS or max length, then starts the next batch. While short requests sit idle waiting for the long tail of the batch, their KV slots and GPU slots are still occupied. White space after early EOS is pure underutilization.
How bad is it? It depends on variance of output lengths. Chat and agent workloads have heavy-tailed generation lengths. Anyscale’s 2023 continuous-batching benchmarks (OPT-13B, A100-40GB) showed naive static batching collapsing toward ~80 tok/s as variance rose, while continuous batching + memory optimizations stayed an order of magnitude higher.
The fix was formalized in Orca (OSDI ’22): schedule at the iteration (token step), not the request. At every decode step:
This is also called in-flight batching or dynamic batching (careful: “dynamic” sometimes means request-level batch size selection, which is weaker). Industry engines implement the Orca idea with refinements:
| Engine | What you get | Notes |
|---|---|---|
| vLLM | Continuous batching + PagedAttention | De facto open serving default; huge batch headroom from paging |
| Hugging Face TGI | Continuous batching router | Early production continuous batcher; waiting_served_ratio knobs |
| TensorRT-LLM | In-flight batching + fused kernels | NVIDIA stack; strong for TRT-optimized graphs |
| SGLang | Continuous batching + RadixAttention | Strong on structured decoding and prefix reuse |
Production schedulers maintain (at least) a waiting queue and a running set. A simplified control loop looks like:
while True:
# 1) Free finished sequences; reclaim KV blocks
for seq in running:
if seq.finished: free_kv(seq); running.remove(seq)
# 2) Admit new work if memory + policy allow
while waiting and can_allocate(waiting[0]):
seq = waiting.pop(0)
allocate_kv(seq)
if needs_prefill(seq):
prefill_queue.append(seq)
else:
running.append(seq)
# 3) Build this iteration's batch
batch = select_batch(prefill_queue, running) # tokens, not just sequences
# 4) One model forward (possibly chunked prefill + decode mixed carefully)
outputs = model.forward(batch)
append_tokens(outputs)
Prefill of a long prompt is a large, compute-heavy GEMM-heavy step. Decode of many sequences is memory-bandwidth heavy (see Part 2). Naively mixing a huge prefill with latency-sensitive decode can spike TTFT for everyone else. Engines therefore use policies such as:
Continuous batching only helps if you can fit more concurrent sequences. That is why PagedAttention (Part 3) is not a side topic: without non-contiguous, just-in-time KV allocation, iteration-level admission still fails on fragmentation and over-reservation. Anyscale’s results: continuous batching alone ~8× vs naive static; continuous batching + vLLM memory management up to ~23× on high-variance workloads (OPT-13B, A100-40GB — the peak multiple is not “batching alone”).
Orca’s original design reserves KV capacity up to each request’s
max_tokens on admit so a sequence cannot get stuck halfway through generation
with no free KV left. Finished requests free those slots. At matched latency they reported
up to 36.9× throughput vs FasterTransformer on GPT-3 175B-class serving —
a different experiment than Anyscale’s 23×, same family of ideas.
TensorRT-LLM’s name for the same loop is in-flight batching (IFB). Useful
knobs in that stack: max_batch_size, max_num_tokens (packed token
budget per step; default often 8192), optional chunked prefill, and metrics that split
waiting vs scheduled/active requests. Thinking in “tokens per step,” not
only “sequences per batch,” is how packed continuous batchers stay safe under mixed
prefill+decode.
| Metric | What it measures | Continuous batching effect |
|---|---|---|
| Throughput (tok/s) | Aggregate generation rate | Primary win — GPU stays full |
| TTFT | Time to first token | Can improve (immediate admit) or hurt (prefill contention) |
| TPOT / ITL | Time per output token / inter-token latency | Stable if batch size is controlled; degrades when oversubscribed |
| Goodput | Throughput under SLA (e.g. TPOT < 50ms) | The metric that matters in prod |
Next: the two compute stages inside every request — prefill vs decode — and why modern clusters sometimes put them on different machines.