Continuous batching and scheduler internals¶
Scope: how a modern serving scheduler (vLLM, SGLang) iterates via token-level continuous (in-flight) batching that admits and retires requests every step, the waiting/running queues, chunked-prefill interleaving of prefill with decode, and preemption/recompute under KV pressure. The per-step engine loop underneath inference serving and serving open-weight models.
What it is¶
Continuous batching (also called in-flight batching or iteration-level scheduling) refills the batch on every token-generation iteration instead of waiting for whole sequences to finish. It evicts completed requests and pulls in new ones based on GPU readiness, so a new request can join an ongoing batch mid-generation without blocking on the longest sequence.1 This is the mechanism Orca's "iteration-level scheduling" introduced and the single largest reason vLLM delivers multiples of naive-batching throughput.2
The scheduler keeps two queues. vLLM's V1 scheduler holds a waiting queue for new requests and a running queue for active ones; each engine step admits requests from waiting into running subject to available KV blocks and a per-step token budget, and returns KV blocks to the free pool on completion.3 SGLang mirrors this with a waiting_queue and a running_batch, rebuilding the batch each iteration via get_next_batch_to_run(), removing finished requests and admitting new ones when space is free.6
Three batching strategies, contrasted:
| Aspect | Static | Dynamic | Continuous |
|---|---|---|---|
| Trigger | fixed batch size / seq length | batch-size target or timeout | token-generation completion event |
| Latency bound | unbounded (wait for full batch) | bounded by max_batch_delay_ms |
minimal; evicts and refills mid-batch |
| Padding | pad to longest sequence | pad within each batch | slots refilled without padding wait |
| GPU idle | poor under variable loads | better, but a timer firing on a small batch idles the GPU | keeps GPU saturated every iteration |
| Implementation | simple | timers + queue mgmt | per-token coordination and state tracking |
Source: Fregly, Ch. 16, Table 16-4.1
flowchart LR
N["New request"] --> W["waiting queue (FCFS)"]
W -->|"admit if KV blocks + token budget"| R["running batch"]
R -->|"one token / step"| R
R -->|"stop condition met"| D["retire: free KV blocks"]
R -->|"KV pressure"| P["preempt: recompute, re-queue"]
P --> W
Why it matters¶
Decode generates one token at a time; a single sequence leaves the GPU memory-bandwidth-bound and underutilized.1 Waiting for an entire static batch to finish wastes slots because output lengths vary wildly; the batch stalls on its longest member. Continuous batching removes that idle time by replacing each finished sequence immediately, raising throughput while holding per-request added latency to a few milliseconds.1 Batching 4–8 requests can double or triple throughput over 1–2 request batches on large models because the math units and memory pipelines are better fed.1
The harder problem is head-of-line blocking: a long prefill monopolizes the step and spikes inter-token latency (ITL) for every decode in flight. Chunked prefill is the fix: it bounds prefill work per step so decodes keep progressing. Without it, p95 ITL degrades under any traffic mix that interleaves long prompts with active generations.
When it is needed (and when not)¶
- Default on. Continuous batching is the baseline in vLLM and SGLang for any concurrent serving: chat assistants, multi-tenant APIs, agent backends. There is no production reason to disable it.
- Chunked prefill: on for mixed/long-prompt traffic. In vLLM V1 chunked prefill is always enabled by default.4 Enable explicitly on engines where it is optional when prompts are long or prompt/decode interleaving causes ITL spikes.
- Tune, do not disable, under tight ITL SLOs. Lower the per-step token budget for better ITL; raise it for better TTFT (see How). A single global knob trades the two.
- Marginal at very low RPS / single-stream. With one request and no concurrency there is nothing to interleave; the machinery adds bookkeeping for no batching gain. It still costs nothing meaningful to leave on.
- Not a substitute for disaggregation. When prefill and decode contend hard for the same GPUs at scale, split the pools (disaggregated inference) rather than relying on chunked prefill alone to hide the contention.
How: implement, integrate, maintain¶
The per-step loop¶
Each step the scheduler (1) admits/refills from waiting, (2) runs one forward pass over the running batch producing one token per sequence, (3) retires sequences hitting a stop condition and frees their KV blocks, (4) preempts if KV is exhausted. vLLM V1 prioritizes decode requests already in running: it computes their next token and calls allocate_slots before scheduling any prefill from waiting, then spends the remaining token budget on prefill.3
Token budget and chunked prefill¶
The per-step ceiling is max_num_batched_tokens. The scheduler batches all pending decodes first, then fills the leftover budget with prefill tokens, chunking a long prefill across multiple steps so it never starves decodes.4 This is "stall-free scheduling": a 20K-token prompt split into 5K chunks bounds per-iteration work and smooths per-iteration latency.1 Chunking does not reduce total attention cost: prefill self-attention is N(N+1)/2 QK dot products per layer per head, quadratic in N; chunking changes only when work becomes available to the decoder, i.e. overlap and latency, not the total. Reducing the total requires a smaller effective context or local/sparse attention (O(NW) for a fixed window W).1
The budget is the ITL/TTFT lever: smaller values (e.g. 2048) give better ITL because fewer prefill tokens slow decodes; higher values give better TTFT because more prefill lands per step.4
# vLLM: chunked prefill is always on in V1. Tune the per-step token budget.
# Lower -> better inter-token latency; higher -> better TTFT / throughput.
vllm serve deepseek-ai/DeepSeek-V3 \
--tensor-parallel-size 8 --trust-remote-code \
--max-num-batched-tokens 2048 \
--max-num-seqs 256
# SGLang: chunk size is the prefill budget per step (tokens).
# --enable-mixed-chunk lets prefill and decode share one batch.
python -m sglang.launch_server \
--model-path deepseek-ai/DeepSeek-V3 --tp 8 \
--chunked-prefill-size 2048 \
--enable-mixed-chunk
vLLM's scheduler config defaults max_num_batched_tokens to 2048, max_num_seqs to 128, enable_chunked_prefill to True, and policy to fcfs (set --scheduling-policy priority for priority-aware admission, the hook for inference QoS / admission control).5 Defaults shift across releases: the max_num_batched_tokens default has moved over versions (512 in early chunked-prefill builds).4 Pin and confirm against the installed version rather than assuming.
Preemption and recompute under KV pressure¶
When KV blocks run out, the scheduler cannot keep every running sequence resident. It preempts: a victim sequence is evicted from the batch, its KV is dropped, and the request is re-queued; when blocks free up it is recomputed from its tokens (prefill re-run) rather than resumed.4 vLLM V1's default preemption mode is RECOMPUTE (not SWAP to host), because recomputation has lower overhead in the V1 architecture.4 The book shows the exact V1-era log line. A recompute preemption costs extra GPU compute and raises latency, so a steady stream of these warnings means the KV pool is undersized:
WARNING ... scheduler.py:1057 Sequence group 0 is preempted by
PreemptionMode.RECOMPUTE because not enough KV cache space.
total_cumulative_preemption_cnt=1
Source: Fregly, Ch. 16, sample vLLM log.1 Recommended actions when these appear: raise the GPU memory-utilization threshold, reduce max_num_batched_tokens, or rely on PagedAttention's block allocation to pack KV tighter.1 SGLang exposes priority preemption (--enable-priority-scheduling with --priority-scheduling-preemption-threshold) so high-priority requests evict low-priority ones, saving state and re-queuing the victim,6 and --schedule-conservativeness to bias the admission estimate away from OOM.6
What makes it sustain ~100% utilization¶
vLLM's PagedAttention slices the KV cache into fixed-size pages and groups page-level compute across active sequences, interleaving large prefill GEMMs with small decode kernels so the scheduler can pack work without padding.1 SGLang's RadixAttention uses tree-based KV grouping with lazy eviction for shared-prefix reuse, reaching similar utilization.1 Both are open source; read the scheduler implementations directly. The block-level bookkeeping is what lets the scheduler track each request's state separately and refill slots every iteration.1
Maintain¶
- Watch preemption counters: vLLM increments
total_cumulative_preemption_cnt; a rising count means KV starvation, not a transient. Pair with prefix-cache hit metrics (vllm:gpu_prefix_cache_queries,vllm:gpu_prefix_cache_hits).1 - Plot p50/p95/p99 latency against RPS; with batching p50 stays flat or drops as throughput rises up to an inflection point. Stay under it (the SLO/SLI catalog).1
- Treat
max_num_batched_tokens/--chunked-prefill-sizeas SLO controls, retuned per traffic shape, not set-and-forget. - KV swap/recompute thrash shows as a copy-engine spike with low SM utilization; confirm with profiling before changing paging params (CUDA streams and concurrency).1
References¶
- Chris Fregly, "AI Systems Performance Engineering" (O'Reilly), Chapter 16 — Profiling, Debugging, and Tuning Inference at Scale (Continuous Batching, Continuous Scheduling, Stall-Free/Chunked Prefill, Table 16-4)
- vLLM, "Inside vLLM: Anatomy of a High-Throughput LLM Inference System": https://vllm.ai/blog/2025-09-05-anatomy-of-vllm
- vLLM, "Optimization and Tuning" (chunked prefill,
max_num_batched_tokens, preemption mode): https://docs.vllm.ai/en/latest/performance/optimization.html - vLLM engine/scheduler arguments: https://docs.vllm.ai/en/latest/serving/engine_args.html
- vLLM
SchedulerConfigdefaults (max_num_batched_tokens,max_num_seqs,enable_chunked_prefill,policy): https://docs.vllm.ai/en/latest/api/vllm/config/scheduler/ - SGLang, "Server Arguments" (
--chunked-prefill-size,--enable-mixed-chunk,--schedule-conservativeness,--enable-priority-scheduling): https://docs.sglang.io/advanced_features/server_arguments.html - Sarathi-Serve (chunked-prefill / decode coalescing, iteration-level scheduling): https://arxiv.org/abs/2403.02310
Related: Inference Serving · Serving OSS Models · Disaggregated Inference · KV Cache Management · Speculative Decoding · Inference QoS / Admission Control · SLO/SLI Catalog · CUDA Streams & Concurrency · Glossary
-
Chris Fregly, "AI Systems Performance Engineering" (O'Reilly), Ch. 16 — "Continuous Batching," "Continuous Scheduling," "Stall-Free Scheduling (Chunked Prefill)," Table 16-4, and the sample vLLM scheduler preemption log. Continuous batching "maintains high GPU utilization by refilling batches on every token-generation iteration," chunked prefill "schedules prompt prefill processing and token generation in a tightly interleaved way," and prefill self-attention is
N(N+1)/2QK dot products per layer per head. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Orca introduced iteration-level scheduling; vLLM and SGLang adopt it as the basis of continuous batching. vLLM, "Inside vLLM: Anatomy of a High-Throughput LLM Inference System," https://vllm.ai/blog/2025-09-05-anatomy-of-vllm. ↩
-
vLLM V1 scheduler maintains a
waitingand arunningqueue; each step admits fromwaitingsubject to KV availability and atoken_budget(max_num_batched_tokens), prioritizes decode requests already inrunning(computing their next token viaallocate_slots) before scheduling prefill, and frees KV blocks on completion. vLLM, "Inside vLLM: Anatomy of a High-Throughput LLM Inference System," https://vllm.ai/blog/2025-09-05-anatomy-of-vllm. ↩↩ -
"In vLLM V1, chunked prefill is always enabled by default." Smaller
max_num_batched_tokens(e.g. 2048) yields better ITL; higher yields better TTFT. Default preemption mode in V1 isRECOMPUTErather thanSWAP. vLLM, "Optimization and Tuning," https://docs.vllm.ai/en/v0.9.1/configuration/optimization.html (also https://docs.vllm.ai/en/latest/performance/optimization.html). ↩↩↩↩↩↩ -
SchedulerConfigdefaults:max_num_batched_tokens=2048,max_num_seqs=128,enable_chunked_prefill=True("prefill requests can be chunked based on the remainingmax_num_batched_tokens"),policy="fcfs"(or"priority"). vLLM, "config.scheduler" API reference, https://docs.vllm.ai/en/latest/api/vllm/config/scheduler/. ↩ -
SGLang maintains a
waiting_queueandrunning_batch, rebuilds the batch each iteration viaget_next_batch_to_run(), and chunks prefill via--chunked-prefill-size(tokens) with--enable-mixed-chunkto share prefill and decode in one batch; priority preemption via--enable-priority-scheduling/--priority-scheduling-preemption-threshold, admission bias via--schedule-conservativeness. SGLang docs, "Server Arguments," https://docs.sglang.io/advanced_features/server_arguments.html (source: https://github.com/sgl-project/sglang/blob/main/docs/advanced_features/server_arguments.md). ↩↩↩