Continuous batching
Static batches waste GPU on short requests; continuous batching admits new sequences as others finish — scheduler knobs, fairness, and prefill/decode mixing.
Static batching wastes the GPU
If you wait to fill a batch of size (B), short requests stall behind the slowest filler. If you run batch=1, the GPU underutilizes during decode while other users wait in a queue.
Continuous batching (iteration-level scheduling) inserts new sequences into the running batch as others complete — core to vLLM-class engines and peers (TGI, TensorRT-LLM in-flight batching, SGLang).
gantt
title Continuous batching intuition
dateFormat X
axisFormat %s
section GPU
ReqA decode :a1, 0, 8
ReqB decode :a2, 2, 6
ReqC decode :a3, 5, 9
Interview cue: Continuous batching is iteration-level scheduling, not “bigger static batches.” Sequences enter and leave every decode step.
The problem in product terms
| Without continuous batching | What users experience |
|---|---|
| Wait for full static batch | Random TTFT under load |
| Early finishers leave idle slots | Lower tokens/sec, higher cost |
| One long request holds the batch | Short chats stuck |
Serving is a queueing + packing problem. The GPU is happiest when every step has useful work; users are happiest when short jobs finish early.
Static vs continuous — side by side
| Static batching | Continuous / in-flight | |
|---|---|---|
| Admission | Wait for (B) prompts | Admit when KV/slots free |
| Batch composition | Fixed until all done | Mixed lengths each step |
| Short request UX | Often poor under load | Can finish early |
| GPU during decode | Idle slots if early finish | Refill with new work |
| Implementation | Simple | Needs KV paging + scheduler |
flowchart LR
subgraph Static
W[Wait for B] --> R[Run until all done]
R --> N[Next batch]
end
subgraph Continuous
Q[Queue] --> S[Scheduler each step]
S --> Mix[Mixed decode batch]
Mix --> Free[Free finished KV]
Free --> S
end
Dynamic batching vs continuous batching (naming trap)
Vendors overload “dynamic batching.” Sometimes it still means “wait up to N ms to fill a static batch.” Continuous / in-flight / iteration-level means admit mid-flight. Ask: can a new request join while others are still decoding?
Why it couples to KV management
You cannot freely add sequences unless you can allocate and free KV without fragmenting the GPU heap. That is why PagedAttention-style block allocators and continuous batching ship together:
- Request arrives → reserve KV blocks for max planned length (or grow)
- Each decode step → advance tokens; append KV blocks as needed
- Request finishes / cancels → free blocks immediately for newcomers
Without paging, “continuous” admission still dies on fragmentation. See KV-cache, prefill, and decode.
flowchart TD
Arrive[Request arrives] --> Reserve[Reserve KV blocks]
Reserve --> Step[Decode / prefill step]
Step --> Grow{Need more blocks?}
Grow -->|yes| Alloc[Allocate blocks]
Grow -->|no| Step
Alloc --> Step
Step --> Done{Finished?}
Done -->|yes| Free[Free blocks]
Done -->|no| Step
Free --> Next[Admit waiting request]
Prefill vs decode inside the scheduler
Real engines mix prefill chunks and decode steps. Policies differ:
| Concern | Typical approach |
|---|---|
| Huge prefill | Chunked prefill so interactive decode is not starved |
| Many short chats | Prefer decode-heavy packing for TPOT |
| Fairness | Per-user limits; prevent one tenant from filling max_num_seqs |
| Priorities | Interactive > batch; or paid tier > free |
| Preemption | Pause low-priority when KV tight (engine-dependent) |
Ship rule: continuous batching raises aggregate tokens/sec. It does not automatically fix p95 TTFT if prefills are unbounded — still isolate heavy jobs.
Token budget per step
max_num_batched_tokens (name varies) caps how many tokens the engine processes in one scheduler iteration — across prefill chunks and decode tokens. Too low → underutilization. Too high → one fat prefill monopolizes the step.
| Setting feel | Symptom |
|---|---|
| Too conservative | Low GPU util, high queue |
| Too aggressive | TTFT spikes when long prompts arrive |
| Balanced | Stable p95 with healthy util |
Tune with a realistic mix, not identical 50-token prompts.
Knobs product engineers actually set
| Knob | Meaning |
|---|---|
max_num_seqs / max batch |
Concurrent sequences cap (VRAM) |
max_num_batched_tokens |
Tokens processed per step (prefill+decode budget) |
| Max model length | KV ceiling per sequence |
| Queue timeout | Fail fast vs wait forever |
| Priority / QoS | Who enters the running set first |
| Prefill chunk size | Fairness under long prompts |
Load-test with a realistic mix: 80% short chat, 15% RAG, 5% long dump — synthetic identical prompts hide scheduler pathologies.
# Shape — synthetic load mix for a bake-off
mix = [
("short", 0.80, 64, 128), # name, weight, prompt_tok, out_tok
("rag", 0.15, 4096, 256),
("dump", 0.05, 32000, 512),
]Fairness and multi-tenancy
Continuous batching without limits → noisy-neighbor hell:
- One agent loop with huge tool dumps fills KV
- Free-tier scrapers raise everyone’s queue time
- Offline eval jobs sneak onto the interactive pool
Patterns:
- Separate pools (interactive GPU vs batch GPU)
- Admission control (max concurrent per tenant)
- Token budgets at the gateway (cost/latency routing)
- Preemption (pause low-priority when KV tight) — engine-dependent
flowchart TD
GW[Gateway quotas] --> IQ[Interactive queue]
GW --> BQ[Batch queue]
IQ --> EngA[Engine pool A]
BQ --> EngB[Engine pool B]
EngA --> KV[Paged KV]
EngB --> KV2[Paged KV]
Per-tenant caps (minimal policy)
| Control | Example |
|---|---|
| Max in-flight per API key | 4 interactive |
| Max prompt tokens (tier) | Free 2k / Pro 16k |
| Queue class | Interactive vs batch |
| Kill / reject | Timeout with clear error |
Engines schedule tokens; gateways schedule business rules.
Timeline sketch (use this for the micro-project)
Five requests, rough wall clock:
| t | Static (B=4) | Continuous |
|---|---|---|
| 0 | A,B,C wait for D | A starts |
| 1 | still waiting | B joins |
| 2 | D arrives; batch runs | C joins; A may finish |
| 5 | all finish together | D joins on free slots |
| 8 | next batch starts | steady refill |
Draw your own Gantt with different arrival times — the continuous column should show early finishers freeing capacity.
gantt
title Five-request contrast intuition
dateFormat X
axisFormat %s
section Static B4
Wait ABD for C :s0, 0, 3
Run ABCD :s1, 3, 9
section Continuous
A :c1, 0, 5
B :c2, 1, 6
C :c3, 2, 4
D :c4, 4, 8
What “good” looks like in metrics
| Metric | Static under mixed load | Continuous (healthy) |
|---|---|---|
| Short-request E2E | High variance | Completes early |
| GPU util during decode | Gaps | Higher steady util |
| Aggregate tok/s | Lower | Higher |
| p95 TTFT | Can explode with wait-for-B | Dominated by prefill/queue, not batch fill |
Failure modes
- Calling “dynamic batching” when you still wait for a full static batch
- Raising
max_num_seqsuntil OOM / thrash without measuring p95 - Ignoring prefill chunking → TTFT spikes under load
- No per-tenant caps → one customer owns the batch
- Benchmarking only throughput on identical short prompts
- One pool for interactive + overnight summarization
- Assuming continuous batching fixes a too-large
max-model-len
Micro-project
Contrast static vs continuous batching with a 5-request timeline sketch (Gantt or table). Annotate when each request’s first and last token occur. Optional: add a 6th long-prefill request and show how chunking / separate queues change the sketch.