A GPU serving one LLM request is mostly idle. Not slightly idle. Most of the silicon does nothing for most of the wall clock, because every decode step drags the entire model out of HBM to produce a single token. Part 9 showed why decode is memory-bound and why batch size is the only lever that moves it. Continuous batching and PagedAttention are the two engineering ideas that let you actually pull that lever.
They fix different things. Continuous batching fixes scheduling: who is in the batch, and when they may join or leave. PagedAttention fixes memory: how the KV cache is allocated, and how much of it you waste. Iteration-level scheduling came from Orca in 2022, PagedAttention from vLLM in 2023. Together they are why a naive Hugging Face generate() loop leaves most of a rented H100 doing nothing.
PagedAttention only makes sense as a fix for a problem that continuous batching creates, so the order here is scheduling first and memory second. By the end you will be able to work out how many concurrent sequences your KV cache holds, explain why PagedAttention has nothing to do with attention, and read the five vLLM flags that control both.
- Part 1. How an LLM Answers a Question: The Complete Inference Path
- Part 2. Byte-Pair Encoding Explained: How LLMs Turn Text Into Tokens
- Part 3. Inside One Transformer Block: The Residual Stream and Its Seven Matrices
- Part 4. Multi-Head Attention Explained: Why 64 Heads Instead of One
- Part 5. The Output Projection: How 64 Attention Heads Become One Thought
- Part 6. The Feed-Forward Network: Where a Transformer Keeps What It Knows
- Part 7. Mixture of Experts Explained: Conditional Computation From Zero
- Part 8. Attention Is All You Need, Dissected: The 2017 Figure, Box by Box
- Part 9. The Roofline Model: Why LLM Decode Is Memory-Bound
- Part 10. Continuous Batching and PagedAttention: How vLLM Keeps a GPU Busy (you are here)
- Part 11. Benchmark Your Own LLM Serving Stack: Two Measurements, One Afternooncoming 19 Aug
Where the idle GPU time goes
One request, alone on the machine
A single request has two phases: one wide parallel pass over the prompt, called prefill, then a long serial run of one-token steps, called decode. Prefill is short in wall-clock time and dense in work. Decode ticks are individually cheap and there are hundreds of them.
The ticks are the problem. Each one reads all 140 GB of a 70B-class model’s weights to emit one token. The arithmetic units barely warm up, and the slice of the card you are using does not get bigger by trying harder.
Static batching gets the roofline win, then throws half of it away
Definition
Static batching
Grouping a fixed set of requests when the batch forms and running them together until the last one finishes. Membership is decided once, so four slots stay reserved from step 0 to step 26 even when two of them stop producing tokens at step 7 and step 10.
Origin: batching to amortise weight reads is how neural networks have been served since long before LLMs, in a setting where every input in a batch takes the same fixed number of passes. Carried unchanged into generation, where each request runs for an unpredictable number of steps, it is the arrangement Orca (Yu and colleagues, OSDI 2022) called request-level scheduling when it proposed the alternative.
Why it matters: an idle slot still holds its KV cache, so it counts against capacity while producing nothing.
Group four requests, run them as one batch, and the weights are read once per step and shared by all four. That is nearly free throughput, and it is exactly the win from Part 9: arithmetic intensity rises with batch size, so you slide rightward off the memory slope.
Then the output lengths ruin it. R4 stops generating at step 7. R2 stops at step 10. But the batch formed as a unit and it dissolves as a unit, so nothing is released until R1 finishes at step 26. Those slots sit dead for the whole interval, still holding cache, still counted against capacity, producing nothing.
The precise version of the cause tells you the fix. The batch is the unit of scheduling. Membership is decided once, at admission, at the exact moment when the one fact that matters is unknowable: how many tokens each sequence will generate. Nobody chose it and nobody can predict it. The model decides when it emits a stop token, several hundred forward passes into the future.
Analogy
A minibus that will not pick anyone up until every passenger from the original group has been dropped off. Four people board, one gets out early, and that seat stays empty for the rest of the route while a queue waits at the stop.
Where it breaks: a driver knows every passenger’s destination the moment they board. Nobody knows how far a sequence is going, including the model, until it emits a stop token several hundred forward passes later. An empty minibus seat also costs nothing, while an idle slot is still holding its KV cache.
A second failure sits on top of the first. Requests arriving at step 3 can see empty slots from step 7 onward and cannot touch them. They wait for the batch to drain. This is textbook head-of-line blocking, and it is unpleasant to explain to a customer: R1’s generation length sets the time to first token for everyone queued behind it.
Definition
Head-of-line blocking
A queue in which the item at the front holds up everything behind it, even when the resources those later items need are already free. Here the requests that arrived at step 3 wait for R1 to finish at step 26 while four slots stand empty from step 7.
Origin: the term comes from queueing and packet switching, where a packet at the head of an input queue stalls every packet behind it because its own output port is busy, even when their ports are idle.
Why it matters: it makes one user’s time to first token a function of another user’s generation length, which is the hardest serving behaviour there is to explain to whoever is paying.
Section takeaways
- Every decode tick reads all 140 GB of weights from HBM to emit one token, so at batch 1 the arithmetic units are close to idle.
- Batching amortises that read across the batch, which is the only thing that raises arithmetic intensity during decode.
- Static batching fixes membership at admission, the one moment when generation length is unknowable, since the model decides it hundreds of forward passes later.
- In the four-request batch R4 stops at step 7 and R2 at step 10, yet both slots stay reserved until step 26, wasting roughly half the slot-steps.
- Requests arriving at step 3 can see free slots from step 7 and cannot use them, so one sequence’s length sets everybody else’s time to first token.
Continuous batching: make the step the unit of scheduling
Definition
Continuous batching (iteration-level scheduling)
Recomputing batch membership before every forward pass instead of once per request. A sequence that emits a stop token at step 7 leaves at step 7, its blocks are freed at step 7, and a queued request takes the vacancy at that same boundary.
Origin: introduced as iteration-level scheduling by Orca (Yu and colleagues, OSDI 2022), built to remove exactly the dead slots and head-of-line blocking that fixed-membership batching creates once requests generate for different numbers of steps.
Why it matters: it is legal only because nothing except each sequence’s KV cache survives a step boundary. Get the freeing wrong and you either leak blocks or attend over somebody else’s keys.
A decode step needs one thing from each sequence: a KV cache to attend over. It does not care what the previous step’s membership was, and nothing else survives a step boundary. The batch dimension carries no state, so it is free to change between steps.
So recompute membership every iteration. Finished sequences are evicted the moment they emit a stop token and their cache is freed at once. Waiting requests are admitted into the vacancy at that same boundary. In the diagram the rows stop being requests and become slots, which is the entire change. R4 ends at step 7 and R5 starts at step 7. R2 ends at 10 and R6 starts at 10. The slots never go grey, and the scheduler now runs once per token rather than once per request, which is why the second name for it, iteration-level scheduling, is the more accurate one.
The scheduler loop, written out
Stripped of accounting, the loop that produces that diagram is about twenty lines, and all of it is bookkeeping wrapped around one forward pass.
while True:
# 1. retire anything that hit EOS or its length limit last step
for seq in list(running):
if seq.finished:
running.remove(seq)
free_blocks(seq)
# 2. fill the vacancy from the waiting queue, memory permitting
while waiting and can_allocate(waiting[0]):
seq = waiting.popleft()
allocate_blocks(seq)
running.append(seq)
# 3. if a running sequence cannot grow, something has to give
while not can_grow(running):
victim = running.pop() # newest first, usually
free_blocks(victim) # its tokens get recomputed later
waiting.appendleft(victim)
# 4. one forward pass, one token for every sequence in running
logits = model.step(running)
for seq, token in zip(running, sample(logits)):
seq.append(token)
Steps 1 and 4 are bookkeeping. Steps 2 and 3 are both memory questions: can this new sequence’s cache fit, and if a running sequence needs another block and there is none, who gets thrown out. Every interesting decision in continuous batching is an allocation decision.
Definition
Preemption, recomputation and swapping
Evicting a sequence that is already running because the scheduler has no free block to grow it into. The victim goes back to the waiting queue, and its cache has to be rebuilt when it is readmitted, either by recomputing its prefill or by copying blocks back from CPU memory they were swapped out to.
Origin: the vLLM paper (2023) sets out both recovery paths and evicts all of a sequence’s blocks together rather than page by page, because every block of a sequence is needed on the step it runs.
Why it matters: a preempted sequence pays for its prefill twice. Constant preemption is the signature of an oversubscribed cache, and vLLM logs it by name.
The honest complication: admitting someone costs the incumbents
Admitting a request means running its prefill, and prefill is a different shape of work from decode. A 2,000-token prompt does not slot cleanly into a decode step. Run it as its own step and every active stream freezes for the duration. Users see a stutter, and your metrics show a spike in time per output token. If you are streaming responses, that stutter is the most visible thing your service does.
Definition
Chunked prefill
Splitting a long prompt’s prefill into slices of a few hundred tokens and mixing one slice into each decode step, so a 2,000-token prompt is absorbed over several steps instead of stalling one.
Origin: introduced in 2023 work on piggybacking prefill chunks onto decode batches, adopted specifically to remove the stall that one arriving prompt inflicts on every stream already running. vLLM exposes it as a scheduler option.
Why it matters: the prefill chunk uses the arithmetic units that the decode part of the step leaves idle, so the smoothing costs almost nothing in throughput.
The roofline reading of that is neat: the decode portion of the step leaves the arithmetic units idle, and the prefill chunk fills exactly that gap. Nobody stalls.
The tradeoff is real. Existing users get a smoother time per output token. The arriving user gets a slightly worse time to first token, since their prompt is now spread over several steps. That is usually the trade you want, because one person waiting an extra 80 milliseconds beats forty people seeing a visible hitch.
In practice
If your p99 time per output token spikes at the moments long prompts arrive, that is prefill stalling the batch, and chunked prefill is the lever. Check your vLLM version before you touch the flag, because recent releases turn it on by default and the flag may already be doing nothing. Measure time to first token on both sides of the change, since the arriving request is the one paying for everyone else’s smooth pacing.
Section takeaways
- A decode step needs only a KV cache from each sequence, so the batch dimension carries no state across a step boundary and membership is free to change.
- Rebuilding membership every iteration closes each gap at the step where it opened: R4 out at 7 and R5 in at 7, R2 out at 10 and R6 in at 10.
- The loop is four operations per token: retire finished sequences, admit from the queue, preempt if a running sequence cannot grow, then one forward pass.
- Steps 2 and 3 are both allocation questions, so every hard decision in continuous batching is a memory decision.
- Running a 2,000-token prefill as its own step freezes every active stream, and chunked prefill trades roughly 80 milliseconds of the newcomer’s time to first token for smooth pacing for everyone already running.
Memory is the ceiling, and contiguous allocation wastes most of it
Roofline says batch until you approach the ridge. In practice you stop well short, and continuous batching is what makes the limit bite. Weights amortize across the batch, which is the whole point. KV cache does not. Each sequence holds a private cache, and that cache grows with every token it generates.
So the HBM budget has two very different tenants. Weights are a fixed toll, paid once, shared by everyone. Everything left over is divided among concurrent sequences. The gap between them is your batch size, and at long context it closes early. A batch that fit at step 0 can stop fitting at step 200, which is when the scheduler must preempt a sequence that is already running rather than simply decline an arriving one.
The bind: reserve before you can know
Before vLLM, a sequence’s KV cache was one contiguous slab. A contiguous allocation cannot grow, because the bytes after it belong to someone else. So at admission you reserve for the largest the sequence could ever become. Reserve for the expected size instead, and when the model generates past it there is nowhere to put the overflow.
Combine that with the scheduling half: the length is unknowable at admission. The reservation is therefore always worst case, and worst case is almost never what happens. A request declares max_tokens of 2,048 and generates 300. That is the normal outcome, not the pathological one. The other 1,748 slots are held for the entire life of the request and unavailable to anyone else.
Then churn turns the cache into Swiss cheese
Continuous batching means constant arrivals and departures. Sequences of different sizes free their slabs at different moments, leaving gaps between live neighbours. After a couple of minutes of real traffic the cache reads: live sequence, 600-slot gap, live sequence, 550-slot gap, and so on.
Now a request arrives needing 1,200 contiguous slots. There are 1,700 free in total, and it cannot be served, because no single gap is big enough. This is external fragmentation, and anyone who has read about page allocation inside a storage engine will recognise it. It also worsens the longer the server runs, so throughput degrades quietly over a shift and recovers when someone restarts the process.
Definition
Internal and external fragmentation
Internal fragmentation is memory wasted inside an allocation its owner never uses, such as the 1,748 reserved slots of a request that declared max_tokens 2,048 and generated 300. External fragmentation is memory wasted between allocations, such as 1,700 free slots split into 600-slot and 550-slot gaps when the arriving request needs 1,200 in a row.
Origin: the pair of terms comes from operating system memory allocation, where allocators have been judged on both since the 1960s. The vLLM paper carried the same accounting over to the KV cache in 2023 and found 60 to 80 percent of it lost to the two together.
Why it matters: both are pure bookkeeping loss. Every wasted slot is a sequence you could have batched, and batch size is your position on the roofline.
Analogy
A car park where every vehicle needs a run of adjacent spaces. Cars and coaches leave at different times, so the free spaces end up scattered in twos and threes. Forty spaces free, and a coach needing twelve in a row is turned away at the gate.
Where it breaks: an attendant can ask the cars to shuffle up and compact the gaps. KV blocks cannot be moved while the kernel is reading them without rewriting every address the sequence holds, so compaction is not available to a running serving engine.
Cache capacity caps batch size, and batch size is your position on the memory slope. Two thirds of the cache lost to over-reservation and fragmentation pins you far to the left of the ridge with the arithmetic units idle, for no reason except allocation policy. That is a bookkeeping limit wearing a hardware limit’s clothes.
Section takeaways
- Weights are a fixed toll paid once and shared by the whole batch, while KV cache is private per sequence and grows with every token generated.
- A batch that fits at step 0 can stop fitting at step 200, forcing the scheduler to preempt a running sequence rather than decline an arriving one.
- Contiguous allocation must reserve for the worst case, so a request declaring
max_tokens2,048 and generating 300 holds 1,748 slots for its whole life. - Churn leaves gaps between live sequences, so 1,700 free slots split into 600s and 550s cannot serve a request needing 1,200 in a row.
- The vLLM paper measured 60 to 80 percent of KV cache lost to those two effects together, which caps batch size for reasons that have nothing to do with the hardware.
PagedAttention: a memory allocator borrowed from virtual memory
The name misleads nearly everyone. PagedAttention is a memory allocator for the KV cache, lifted almost unchanged from operating system virtual memory. It changes nothing about how attention scores are computed and does not alter the math from Part 4 on multi-head attention. It needed a custom kernel only because the kernel must now look up where the data lives before it reads it.
Definition
PagedAttention and vLLM
PagedAttention stores each sequence’s KV cache in fixed-size blocks, 16 tokens each by default, and finds them through a small per-sequence table instead of requiring one contiguous slab. vLLM is the serving engine built around that allocator.
Origin: introduced in “Efficient Memory Management for Large Language Model Serving with PagedAttention” (Kwon and colleagues, 2023), which explicitly borrowed virtual memory and paging from operating systems to fix KV cache fragmentation and worst-case reservation, measured in that paper at 60 to 80 percent of the cache wasted.
Why it matters: it does not touch a single line of the attention math. Treat it as an attention optimisation and you will look for speedups per token that are not there.
Fixed-size blocks make every free block usable
The attention kernel needs to find the K and V for position 7. It does not need position 7 to sit physically beside position 6. If something else can say where position 7 lives, the physical layout is free. So chop the cache into fixed-size blocks, 16 tokens each by default in vLLM, and let them land anywhere. Every block is identical, so any free block satisfies any request, and external fragmentation stops existing by construction.
The block table is a page table
Each sequence keeps a small array mapping logical block indices to the physical blocks holding them. The sequence believes it has contiguous memory. Physically its blocks are scattered across HBM in whatever order they were handed out.
Definition
Block table
The per-sequence array that maps logical block index to physical block number. A sequence holding three blocks believes it owns 48 consecutive token positions; physically it owns blocks 7, 2 and 9, in that order and nowhere near each other.
Origin: it is a page table under another name. Paged virtual memory arrived with the Manchester Atlas in the early 1960s so a program could address more memory than the machine physically had, and vLLM carried the structure over to the KV cache in 2023.
Why it matters: it costs three pointers against several megabytes of actual cache, and it is the only reason blocks can be scattered, shared between sequences, or reclaimed one at a time.
The correspondence with virtual memory is exact and deliberate. Block is page, block table is page table, token positions are the virtual address space, HBM is physical memory. The price is paid in the kernel, which must gather through the table instead of striding, and that costs a few percent.
Analogy
It is operating system virtual memory applied to one array. A process believes it holds a contiguous run of addresses while the pages sit wherever the allocator put them, and a table in the middle keeps the illusion honest.
Where it breaks: an OS translates in hardware, in an MMU with a TLB, and it can overcommit because a page fault is always serviceable from disk. vLLM translates in software inside the attention kernel, which is where the few percent goes, and it cannot overcommit: a running sequence that needs a block and cannot get one is preempted rather than paused. A page is also 4 KiB of arbitrary bytes usable by any process, while a block is 16 tokens of one sequence’s K and V, useful only to a sequence that owns or shares that exact prefix.
Growth on demand, so the unknowable length stops mattering
A sequence gets the blocks its prompt needs, then one more only when the current one fills. There is no reservation, so there is no guess to get wrong. The problem from the scheduling half, that you must commit to a length before you can know it, simply stops existing.
What remains is internal fragmentation, bounded at 15 unused slots in the final partial block, once per sequence. For the 300-token request above that is 4 wasted slots out of 304 allocated, about 1.3 percent, against 85 percent under contiguous allocation.
Sharing, the part that goes beyond fixing what broke
Because every access runs through a table, two sequences can point at the same physical block. Requests sharing a system prompt have bit-identical K and V for that prefix, so store it once and let both tables reference it, with a reference count tracking how many. When one diverges and needs to write, it gets a private copy first.
Definition
Copy-on-write and prefix sharing
Two sequences whose prompts start identically produce bit-identical K and V for that prefix, so both block tables point at the same physical blocks and a reference count says how many owners there are. The first sequence that needs to write into a shared block gets a private copy of that block first.
Origin: copy-on-write is standard operating system practice for sharing pages between a parent process and a forked child until one of them writes. The vLLM paper applied it to KV blocks in 2023, and it is the mechanism underneath both prefix caching and parallel sampling.
Why it matters: a 1,200-token system prompt shared by thousands of requests is stored once instead of thousands of times, and four samples from one prompt share that prompt’s blocks instead of copying them.
This is the mechanism underneath prefix caching, and it is a real cost lever. A 1,200-token system prompt shared across thousands of requests is stored once and, with prefix caching on, prefilled once. Anyone who has measured what prompt caching does to a bill on a hosted API is seeing the same idea from outside. The same indirection makes parallel sampling cheap, since four completions from one prompt share the prompt’s blocks.
Section takeaways
- PagedAttention leaves the attention arithmetic untouched. The custom kernel exists because K and V must now be gathered through a table instead of strided over.
- Blocks are a fixed 16 tokens by default, so any free block satisfies any request and external fragmentation stops existing by construction.
- The block table costs about three pointers per sequence against several megabytes of cache, and the gather costs a few percent on the attention kernel.
- Blocks are handed out one at a time as the current one fills, so there is no reservation and no length guess to get wrong.
- The only waste left is the tail of the last block, at most 15 slots: 4 of 304 for a 300-token request, about 1.3 percent against 85 percent under contiguous allocation.
- Shared prefixes are stored once behind a reference count, and copy-on-write makes a private block only at the moment a sequence writes.
The honest scoreboard
PagedAttention makes nothing faster per token. The gather costs a few percent on the attention kernel. What it does is let three to four times as many sequences fit in the same cache, which raises batch size, which slides you rightward on the roofline slope.
The vLLM paper reports that serving systems before it wasted 60 to 80 percent of KV cache memory to fragmentation and over-reservation, and that vLLM holds waste under 4 percent. It reports 2 to 4 times higher throughput at comparable latency against FasterTransformer and Orca, with the gain largest for long sequences, large models and complex decoding schemes. Those are the authors’ measurements on their hardware and workloads, so treat them as the right order of magnitude rather than a number to expect exactly.
The sentence worth keeping: PagedAttention does not make attention faster, it makes the server bigger.
Section takeaways
- Per token PagedAttention is slightly slower, because the block table gather costs a few percent on the attention kernel.
- It lets three to four times as many sequences fit in the same cache, and that batch size is what moves you along the roofline.
- The vLLM paper reports 60 to 80 percent KV cache waste in the systems before it, against under 4 percent for vLLM.
- It reports 2 to 4 times higher throughput at comparable latency against FasterTransformer and Orca, largest for long sequences and large models.
- Those numbers are the authors’ own measurements, so use them as an order of magnitude and measure your own stack.
Sizing the KV cache for the running 70B model
The running example is a Llama-3-70B class model: 80 layers, 8 KV heads, head_dim 128, bf16 throughout. Per token, per layer, you store one K vector and one V vector for each KV head. That is 2 tensors times 8 heads times 128 dimensions, which is 2,048 values. At 2 bytes each in bf16 that is 4,096 bytes, or 4 KiB per token per layer. Multiply by 80 layers for 327,680 bytes per token, which is 320 KiB. A 4,096-token sequence therefore holds 1.25 GiB of KV cache, per user, on top of the 140 GB of weights.
Now the payoff for grouped-query attention from Part 4. Plain multi-head attention gives this model 64 KV heads instead of 8, an eight-fold increase in every line.
| Quantity | GQA, 8 KV heads (actual) | MHA, 64 KV heads (hypothetical) |
|---|---|---|
| K and V values per token per layer | 2 x 8 x 128 = 2,048 | 2 x 64 x 128 = 16,384 |
| Bytes per token per layer, bf16 | 4,096 (4 KiB) | 32,768 (32 KiB) |
| Bytes per token, all 80 layers | 327,680 (320 KiB) | 2,621,440 (2.5 MiB) |
| Cache for one 4,096-token sequence | 1.25 GiB | 10 GiB |
| Sequences that fit in 140 GiB of spare HBM | 112 | 14 |
That 140 GiB is roughly what four 80 GB cards leave after 140 GB of bf16 weights, workspace and activations. So the deployment holds 112 concurrent full-context sequences. Recall from Part 9 that decode’s arithmetic intensity is roughly the batch size, since a bf16 weight byte earns about one FLOP per sequence in the batch. An A100 offers about 312 TFLOP/s against 2.0 TB/s, so its ridge sits near batch 150. At 112 you are close. At 14 you are nowhere near it, and the cards spend their lives on the memory slope. That is what eight KV heads bought.
In practice
Do this arithmetic before you tune anything. Per token the cache costs 2 * layers * kv_heads * head_dim * dtype_bytes, which is 320 KiB here, and your concurrency ceiling is spare HBM divided by that times your typical sequence length. If the ceiling lands well below the ridge for your card, more cards or more tensor parallelism is the only real fix and no flag will rescue you.
Section takeaways
- One token of KV costs 2 tensors x 8 KV heads x 128 dimensions x 2 bytes, which is 4 KiB per layer and 320 KiB across all 80 layers.
- A 4,096-token sequence therefore holds 1.25 GiB of cache, per user, on top of 140 GB of shared weights.
- 140 GiB of spare HBM holds about 112 concurrent full-context sequences on this model.
- With 64 KV heads instead of 8 the same sequence would cost 10 GiB and only 14 would fit, an eight-fold difference from one design choice.
- Decode’s arithmetic intensity is roughly the batch size and an A100’s ridge sits near 150, so 112 is close to it and 14 leaves the card stranded on the memory slope.
The vLLM flags that actually matter here
Five flags control everything above. The defaults are sensible, and the failure modes are specific enough to diagnose from a log.
| Flag | What it does | What breaks if you get it wrong |
|---|---|---|
--gpu-memory-utilization |
Fraction of each card’s memory vLLM may claim for weights, activations and KV cache. Default 0.9. | Too low and the cache is tiny, so few sequences are admitted and preemption is constant. Too high and you hit an out-of-memory error at peak, or collide with another process on the card. |
--max-model-len |
Caps prompt plus generation for any single request. | Because allocation is paged, a large value does not pre-reserve memory. But vLLM refuses to start if the cache cannot hold one sequence of this length, and longer requests are rejected outright. |
--max-num-seqs |
Hard cap on how many sequences the scheduler runs in a single step. | Too low and you sit on the memory slope with cache to spare. Too high and you thrash: sequences are admitted, run out of blocks, get preempted, and their tokens are recomputed from scratch. |
--enable-prefix-caching |
Keeps completed blocks in a hash-addressed pool so a later request with the same prefix reuses them instead of prefilling again. | Large win for shared system prompts, none at all when prompts differ from token 0. Retained blocks are cache active sequences could have used, so on diverse traffic it is a small net loss. |
--enable-chunked-prefill |
Splits long prefills across steps and mixes them into decode batches. | Smooths time per output token at the cost of slightly worse time to first token for the arriving request. Newer vLLM versions turn this on by default, so check your version before assuming the flag changes anything. |
In practice
One operational tell is worth more than all five flags. When vLLM logs preemption warnings, it is telling you it ran out of KV cache blocks and evicted a running sequence, so you are paying for the same prefill twice. The periodic log line reporting GPU KV cache usage is the leading indicator: if it sits near 100 percent, you are one long prompt away from preemption. The fixes are more cards, more tensor parallelism, a higher memory utilisation setting, or a lower --max-num-seqs, in that order of preference.
What your stack actually does under this load is a measurement, not an argument, which is where Part 11 on benchmarking your own serving stackcoming 19 Aug picks up.
Section takeaways
--gpu-memory-utilizationdefaults to 0.9 and decides how much of the card vLLM claims; too low starves the cache, too high hits an out-of-memory error at peak.--max-model-lenpre-reserves nothing under paged allocation, but vLLM refuses to start if the cache cannot hold one sequence of that length.--max-num-seqsset too high causes thrashing: sequences are admitted, run out of blocks, get preempted and have their tokens recomputed.- Prefix caching is a large win on shared system prompts and a small net loss on traffic that differs from token 0, because retained blocks are cache live sequences could have used.
- A preemption warning means work you already paid for is being recomputed, and the reported KV cache usage line sitting near 100 percent is the warning before the warning.
Key takeaways
- Static batching wastes slots because the batch is the unit of scheduling, and membership is fixed at the one moment when generation length is unknowable.
- Continuous batching rebuilds membership every forward pass, so finished sequences leave and queued ones join at the same step boundary. Nothing survives a step except each sequence’s KV cache, which is what makes it legal.
- Admitting a request runs its prefill and stalls every active stream. Chunked prefill trades a little time to first token for the newcomer against smooth pacing for everyone already running.
- PagedAttention is a memory allocator, not an attention technique. Block is page, block table is page table, and copy-on-write comes along unchanged.
- Contiguous allocation forces worst-case reservation and then fragments, which the vLLM paper measured at 60 to 80 percent waste. Paging cuts that to a few percent.
- The running 70B model costs 320 KiB of cache per token, or 1.25 GiB for a full 4,096-token sequence. Grouped-query attention is why that is 1.25 GiB instead of 10 GiB.
- Preemption warnings mean you ran out of KV cache and are recomputing work you already paid for. Watch reported cache usage before it gets there.
Frequently asked questions
What is the difference between continuous batching and static batching?
Static batching fixes the set of requests when the batch forms and runs it to completion, so a slot freed by a short request stays idle until the longest request in the batch finishes. Continuous batching recomputes membership at every forward pass, so finished sequences are evicted and queued ones admitted at the same step boundary. The change is that the scheduling unit becomes the iteration rather than the batch.
Is PagedAttention an attention algorithm?
No. It is a memory allocator for the KV cache, borrowed from operating system virtual memory, and it does not change how attention scores are computed. It needed a custom kernel only because the kernel must now gather K and V through a block table instead of striding through contiguous memory.
How much KV cache does a 70B model need per token?
For a Llama-3-70B class model with 80 layers, 8 KV heads and head_dim 128 in bf16, one token costs 2 x 8 x 128 x 2 bytes per layer, which is 4 KiB, and 320 KiB across all 80 layers. A full 4,096-token sequence therefore holds 1.25 GiB of cache, so 140 GiB of spare HBM holds about 112 concurrent sequences.
Why does vLLM log preemption warnings?
Preemption means the scheduler ran out of free KV cache blocks and had to evict a sequence that was already running, so its tokens get recomputed later. It is the clearest symptom of an undersized cache, and the usual responses are more GPU memory, more tensor parallelism, a higher gpu_memory_utilization value, or a lower max_num_seqs.
Should I enable chunked prefill?
Enable it when long prompts are causing visible stutters in active token streams, which is most interactive workloads. It costs the arriving request a little time to first token because its prompt is spread across several steps. Recent vLLM versions enable it by default, so confirm your version’s behaviour before changing the flag.
Does PagedAttention make inference faster?
Not per token. The block table gather costs a few percent on the attention kernel. It raises throughput by letting far more sequences fit in the same cache, which increases batch size and moves decode rightward on the roofline toward the compute ceiling.
Sources and further reading
- Efficient Memory Management for Large Language Model Serving with PagedAttention, Kwon et al. The vLLM paper, source of the waste measurement and the throughput comparisons.
- Orca: A Distributed Serving System for Transformer-Based Generative Models, Yu et al., OSDI 2022. The origin of iteration-level scheduling.
- vLLM documentation, for current flag names, defaults and engine behaviour, which move between releases.
- GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints, Ainslie et al. Why the model has 8 KV heads instead of 64.
- Roofline: An Insightful Visual Performance Model for Multicore Architectures, Williams, Waterman and Patterson.
Check your understanding
Take the 8 question quiz on this article
Eight questions are drawn from a pool of 30 and spread across the ideas this article covers, so no two attempts are quite the same. It runs here on the page and keeps your place, so you can go and read a section and come back without losing anything. You get a full report at the end: your score, the correct answer to anything you missed, why it is correct, and a link straight back to the section it came from.
- 8 questions
- drawn from 30
- 7 knowledge areas
- hint on every question
- timed, no limit
