Problem A: static batching idles the GPU
A GPU running one request at a time barely uses its compute. The obvious fix is to batch requests. But the obvious way to batch them leaves the GPU mostly empty — and the reason is the loop you already know from earlier lessons.
What static batching does
Static batching groups N requests, runs them together, waits for all of them to finish, then starts the next batch. Simple to build, easy to reason about — and quietly wasteful.
Why it idles
Requests finish at wildly different lengths: one wants 10 tokens, another wants 800. The whole batch stalls on the slowest one while the finished slots just sit there. Measured across a variable workload, roughly 60% of the GPU sits idle.
Keywords — tap to unfold the plain meaning
Static batching waits for the whole batch to drain before refilling, so it stalls on the slowest request and idles ~60% of the GPU.
Fix A: continuous batching
If the problem is waiting for the whole batch to finish, the fix is to stop thinking in batches and start thinking in single steps. Decide who's in the batch fresh, every single step.
Schedule per step, not per batch
Continuous batching schedules one decode step at a time. After every step it evicts finished requests and admits waiting ones, so a slot freed by a short request is instantly reused — the batch never drains.
The name and the origin
This is iteration-level scheduling — the scheduling unit is one iteration (one decode step), not one whole request. It was introduced by Orca (Yu et al., OSDI'22).
What it's worth
On variable-length workloads — the realistic case — continuous batching is worth roughly 4–8× the throughput of static batching, just by never letting finished slots sit idle.
Keywords — tap to unfold the plain meaning
Continuous batching (iteration-level scheduling, from Orca) refills the batch every decode step instead of waiting for it to drain — worth 4–8×.
Problem B: contiguous KV wastes memory
Continuous batching fixes the idle compute. But there's a second, quieter waste — in memory, not compute — and it limits how many sequences you can fit at once no matter how clever your scheduler is.
Reserve-for-the-worst-case
Early servers gave each request one contiguous KV region, sized to the maximum possible length the request might generate. The KV cache must live somewhere, so they grabbed the whole worst-case slab up front.
Most of it never gets used
Most requests never reach that maximum. So 60–80% of reserved memory sits empty, and the box fits far fewer sequences than the raw KV math promised — a memory problem, not a compute one.
Keywords — tap to unfold the plain meaning
Contiguous KV pre-reserves the worst-case length per request, so 60–80% of reserved memory sits empty and you fit far fewer sequences.
Fix B: PagedAttention
The trick is borrowed straight from operating systems. You don't hand a program one giant contiguous slab of RAM — you hand it pages on demand. Do the same to the KV cache and the empty-shelf waste disappears.
Blocks on demand
PagedAttention splits the KV cache into small fixed-size blocks, handed out on demand as each sequence actually grows. The blocks are non-contiguous — they can sit anywhere in memory — so nothing is reserved before it's needed and waste drops to nearly zero.
Where the idea comes from
It borrows virtual memory / OS paging: just as an operating system maps scattered physical pages into one tidy address space, PagedAttention maps scattered KV blocks into one logical sequence.
The combined payoff
Together with continuous batching, PagedAttention gave vLLM up to 24× the throughput of naïve HuggingFace serving. It was introduced by Kwon et al., SOSP'23 (arXiv 2309.06180).
Keywords — tap to unfold the plain meaning
PagedAttention is OS paging for the KV cache: small fixed-size non-contiguous blocks on demand, waste ≈ 0. Plus continuous batching, vLLM hit up to 24× naïve HF.
Both fixes, one batch
The two fixes attack different bills — compute and memory — and they compose. Walk one batch with two requests through both, and you can see exactly which cost is shared and which is paid per sequence.
Shared compute, separate memory
Both requests prefill, then enter the same decode batch. The big win: one weight-read serves both requests (shared compute), but each maintains its own KV cache (a memory cost paid per sequence). That split is the whole reason batching helps.
The two fixes do their jobs
Across the timeline, continuous batching swaps finished requests out and waiting ones in each step, while PagedAttention hands out KV blocks as each sequence grows — paging across iterations so neither request over-reserves.
Chunked prefill
Chunked prefill splits a long prompt into smaller pieces so prefill interleaves with other requests' decode in the same batch — instead of one long prompt freezing everyone else's token generation.
Keywords — tap to unfold the plain meaning
One weight-read serves the whole batch (shared); each request keeps its own KV (per-sequence). Continuous batching swaps, PagedAttention pages, chunked prefill interleaves.
On your cluster
All of this ships in vLLM as plain flags. You set the batch width, turn on prefix and chunked prefill, watch two numbers — and you can see the same KV-paging idea show up as a price on the hosted APIs.
The flags that turn it on
--max-num-seqs sets the continuous-batch width (e.g. 8 or 64). --enable-prefix-caching shares KV blocks across requests with the same prefix. --enable-chunked-prefill keeps long prefills from blocking decodes.
The live gauge
Watch num_requests_running versus num_requests_waiting. Running well below your --max-num-seqs while many wait means the batch isn't full — you're leaving throughput on the table.
vLLM flags on the 4×H100 box
vllm serve Qwen3.6-27B-FP8 \ --max-num-seqs 64 \ --enable-prefix-caching \ --enable-chunked-prefill # live gauge — is the batch full? num_requests_running # active decode slots, vs --max-num-seqs num_requests_waiting # queued, not yet admitted
The same idea, billed
Hosted-API prompt caching stores the prefill KV for a stable prefix so the next request skips re-prefilling it — the same KV-reuse from --enable-prefix-caching, now a line on your bill.
60% of the GPU idle on a variable-length workload. Continuous batching is worth 4–8× there; PagedAttention cut KV waste from 60–80% toward zero; together they put vLLM at up to 24× naïve HuggingFace throughput.
Keywords — tap to unfold the plain meaning
Check yourself
- Why does static batching idle the GPU?
- What is continuous batching?
- What does PagedAttention borrow?
Set --max-num-seqs, enable prefix and chunked prefill, and watch running vs waiting. The same KV-paging trick is what makes prompt caching ~90% cheaper.