Beyond a Forward Pass · Part 1 of 5 · LLM inference · July 2026

Part 1: Continuous Batching

Part 1 of 5. Why variable-length generation breaks classic batching — and how iteration-level scheduling became the foundation of modern LLM serving.

Series · Beyond a Forward PassPart 1 of 5Continuous batchingJul 28, 2026

Why this series exists

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.

“There’s a reason you have specialized inference engines like vLLM, SGLang, TensorRT-LLM… LLMs do variable-length computation… that’s very different from a traditional CNN where you have a fixed-size image input and a fixed amount of computation.” — Robert Nishihara, co-founder of Anyscale (walk with Linda Vivah, NYC)

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.

The core problem: variable-length work

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.

Classic ML batching

  • Fixed input shape (e.g. 224×224 image)
  • One forward pass per sample
  • Batch of N finishes together
  • Memory footprint ≈ model + activations

LLM “batching”

  • Prompts of different lengths
  • N forward passes per request (decode)
  • Requests finish at different times
  • Memory = weights + growing KV cache

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 (request-level) batching — and why it wastes the GPU

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.

Mental model: static batching optimizes for “batch of jobs finish together.” LLM serving optimizes for “GPU never has empty seats while a queue exists.” Those are different objective functions.

Continuous batching = iteration-level scheduling

The fix was formalized in Orca (OSDI ’22): schedule at the iteration (token step), not the request. At every decode step:

  1. Run one forward pass for all active sequences in the current batch.
  2. Any sequence that just emitted EOS is evicted; its resources free.
  3. If the waiting queue has work and memory allows, admit new sequences.
  4. Repeat.

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:

EngineWhat you getNotes
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

How it is implemented in practice

The scheduler loop

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)

Why prefill complicates the batch

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:

Memory is the real admission control

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”).

Admission control that avoids mid-generation deadlock

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.

Metrics you actually care about

MetricWhat it measuresContinuous 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
Practice: do not celebrate tok/s alone. A giant batch that makes every chat feel laggy can raise throughput while destroying product quality. Cap concurrent tokens or concurrent sequences, and plot latency CDFs at target QPS.

What to do with this knowledge

Next: the two compute stages inside every request — prefill vs decode — and why modern clusters sometimes put them on different machines.

Sources & further reading

Part 2: Prefill, Decode & Disaggregation →