Skip to content
learn
  1. Learn
  2. Artificial Intelligence
  3. LLM Inference and Serving

AI AI-11.03

Batching and PagedAttention

Why static batching wastes GPU time, how continuous batching swaps requests in and out every step, and how paged KV blocks end cache fragmentation.

Advanced 14 min Last reviewed

The big idea

A server batches 8 requests. One asks for a 10-token answer, another for 500. With the simplest kind of batching, the short request finishes after 10 steps and its slot then sits empty for 490 more, because the batch only ends when its longest member does. New requests wait in the queue the whole time, while the GPU does a step's worth of work for a slot nobody is using.

Think of a tour bus versus a city bus. The tour bus leaves with a full load and returns only when the last passenger is back, so seats stay empty while a queue forms at the depot. A city bus lets people off and on at every stop, so empty seats fill almost immediately. LLM servers switched from tour buses to city buses, and that alone multiplied their throughput.

The second half of the story is memory. Letting requests come and go at every step only helps if their KV caches can come and go too, without leaving holes. PagedAttention borrows the operating system's answer to that problem: split memory into small fixed-size pages and keep a table of where each request's pages live.

How it works

Static versus continuous batching Two timelines of four batch slots, with decode steps running left to right. Top, static batching: requests r1 to r4 start together; r1, r2 and r3 finish early and their slots sit idle until r4, the longest, finishes; only then do the waiting requests r5 and r6 start, so the last one ends late. Bottom, continuous batching: as soon as r1 finishes, r5 takes its slot at the next step, and r6 takes r2's slot, so all six requests are done much sooner. r1 idle r5 waited r2 idle r6 waited r3 idle r4 (longest) r1 r5 joins r2 r6 joins r3 r4 static batching last one ends here continuous batching last one ends here slot 1 slot 2 slot 3 slot 4 slot 1 slot 2 slot 3 slot 4 Figure 1. Static batching holds idle slots until the longest request ends. Continuous batching refills each slot the step after it frees up. Step through it.
  1. Static batch r1 to r4 start together. r1, r2 and r3 finish early, but their slots stay idle until r4 is done. r5 and r6 wait for the next batch.
  2. Continuous batch The scheduler runs one step at a time. When r1 finishes, r5 joins in its slot at the next step; r6 takes r2's slot.
  3. Compare Same six requests, same four slots. Continuous batching finishes far sooner, and r1 and r2 return to their users the moment they are done.

Static batching

Static batchingGrouping requests into a batch that runs until every member has finished, then starting the next batch. Simple, but slots of finished requests sit idle. is what you get from a plain "collect N requests, run them, return the results" loop. It works well when every request does the same amount of work, as in image classification. Text generation breaks that assumption: output lengths vary from a few tokens to thousands. Three things go wrong at once:

  • Idle slots: a finished request's slot does nothing until the longest request ends.
  • Late returns: a request that finished early cannot return its answer until the batch is done (unless the server streams).
  • Queueing: new arrivals wait for a whole batch to drain, which hurts time to first token.

Continuous batching

Continuous batchingScheduling at the granularity of single decode steps: after every step, finished requests leave the batch and waiting requests join it. Also called iteration-level scheduling or in-flight batching. moves the scheduling decision from "per batch" to "per step". After every decode step the scheduler removes finished sequences, returns them immediately, and admits waiting requests into the freed slots, as long as their KV cache fits.

A newly admitted request first needs its prefill, while the others are mid-decode. One step can mix both: the non-attention layers (the big matrix multiplications) process all tokens from all requests stacked together, while attention runs per request because each has its own cache and length. The Orca paper called this selective batching. Very long prompts are often split into chunks across several steps (chunked prefill), so one big prefill does not stall everyone else's next token.

KV cache fragmentation

Continuous batching keeps the slots busy, but the number of slots is set by memory ( GPU memory math ). Early servers stored each request's KV cache in one contiguous chunk sized for the maximum length it might reach, because they could not know in advance when it would stop. That wastes memory three ways:

  • Reserved: space for tokens not generated yet. It will be used eventually, but it blocks other requests for the whole lifetime of this one.
  • Internal fragmentation: space reserved for a maximum the request never reaches.
  • External fragmentation: gaps between chunks of different sizes, too small for a new request.

In the PagedAttention paper's measurements, only 20.4 to 38.2 percent of the KV cache memory in such systems held actual token states. The rest was waste, and waste means a smaller batch.

PagedAttention

Paged KV cache with block tables Step 1: request A's KV cache is split into logical blocks of 4 tokens each: block 0 holds 'Four score and seven', block 1 holds 'years ago our' plus one empty slot. A block table maps logical block 0 to physical block 7 and logical block 1 to physical block 1. Physical blocks are scattered in GPU memory. Step 2: when A generates a new token it fills the empty slot in its last block; when that block is full, the manager allocates any free physical block and adds a row to the table. Step 3: request B, which shares the same prompt, points its block table at the same physical blocks, with a reference count of 2; a block is copied only when one of them needs to write to it. when full copy on write request A logical blocks 0, 1 block table A 0 -> 7, 1 -> 1 phys 1 years ago our _ phys 3 free, then A's phys 7 Four score and seven request B same prompt block table B 0 -> 7, 1 -> 1, refs 2 Figure 2. Each request sees contiguous logical blocks; a block table maps them to any free physical blocks. Only the last block can have empty slots. Step through it.
  1. Blocks and a table A's cache lives in fixed-size blocks. Its block table says logical block 0 is physical block 7, logical block 1 is physical block 1.
  2. Grow on demand New tokens fill the last block. Only when it is full does the manager hand out another free block, from anywhere in memory.
  3. Share and copy on write B shares A's prompt, so its table points to the same physical blocks. A block is copied only when one of them must write to it.

PagedAttentionAn attention algorithm that stores each request's KV cache in fixed-size blocks anywhere in GPU memory, found through a per-request block table, like pages in an operating system's virtual memory., introduced with the vLLM serving engine, applies operating-system paging to the KV cache. Think of blocks as pages, tokens as bytes and requests as processes:

  1. GPU memory for the cache is divided into equal physical blocks, each holding the keys and values of a fixed number of tokens (16 tokens by default in the original vLLM).
  2. Each request sees a sequence of logical blocks, filled left to right. A block table maps each logical block to whichever physical block holds it.
  3. Blocks are allocated on demand, one at a time as tokens arrive. Nothing is reserved for a maximum length.
  4. The attention kernel follows the block table to gather keys and values from scattered blocks.

The waste is now at most one partly filled block per request, and since all blocks have the same size, there is no external fragmentation at all. The vLLM team reported a waste of under 4 percent.

Sharing blocks

Because the table is an indirection, two sequences can point at the same physical block. When a request asks for several samples of the same prompt, or for beam search, the prompt's blocks are stored once with a reference count, and a block is copied only when one sequence needs to write to it (copy-on-writeSharing one copy of data between several users until one of them needs to modify it, at which point that user gets a private copy., the same trick operating systems use when forking a process). Requests that share a system prompt can share its blocks too, which is the basis of prefix caching.

When blocks run out

With on-demand allocation, a batch can outgrow memory mid-generation. vLLM serves requests first come, first served, and when it must preempt, it evicts the most recently arrived requests first. A preempted request's blocks are either swapped out to CPU memory or freed and recomputed later from the tokens so far. All blocks of a sequence are evicted together, since attention needs all of them at every step.

Try it

This interactive needs JavaScript. The diagrams and text above cover the same idea.

  1. With the defaults (4 slots, mixed lengths), compare the two timelines. Where do the static gaps come from, and how much sooner does the average request finish with continuous batching?
  2. Switch to similar lengths. Why does continuous batching barely help now?
  3. Press "New requests" a few times. The continuous timeline also has gaps at the very end. What causes them, and why would a busy production server rarely see them?

How much memory does on-demand allocation save? A toy KV cache with room for 32,768 tokens, a maximum request length of 4,096 and mostly short requests:

paged.pyruns with python3 (standard library)
# Contiguous KV reservation vs paged blocks. Memory is counted in token slots.
import random

random.seed(7)
CAPACITY = 32_768    # token slots of KV cache memory on the GPU
MAX_LEN = 4_096      # longest request the server allows
BLOCK = 16           # tokens per KV block

# final lengths (prompt + output) of a stream of requests, mostly short
lengths = [min(MAX_LEN, int(random.expovariate(1 / 600)) + 50) for _ in range(200)]

def admit(cost):
    """Admit requests in order until the next one does not fit."""
    used, n = 0, 0
    for length in lengths:
        if used + cost(length) > CAPACITY:
            break
        used, n = used + cost(length), n + 1
    real = sum(lengths[:n])
    return n, used, real

schemes = {
    "contiguous, reserve max": lambda L: MAX_LEN,
    "paged, 16-token blocks": lambda L: -(-L // BLOCK) * BLOCK,  # round up
}
for name, cost in schemes.items():
    n, used, real = admit(cost)
    print(f"{name:<24} {n:>3} requests at once, "
          f"{real / used:6.1%} of reserved slots hold real tokens")

wasted = sum(-(-L // BLOCK) * BLOCK - L for L in lengths)
print(f"\npaged waste is only the unfilled tail of each last block: "
      f"{wasted / len(lengths):.1f} slots per request on average")

Output

contiguous, reserve max    8 requests at once,   7.9% of reserved slots hold real tokens
paged, 16-token blocks    60 requests at once,  98.6% of reserved slots hold real tokens

paged waste is only the unfilled tail of each last block: 7.7 slots per request on average

Reserving the maximum per request admits 8 requests, and under 8 percent of the reserved memory holds real tokens. Paging admits 60, because a request only ever holds the blocks it has filled. The exact numbers depend on the length distribution; the PagedAttention paper measured 20.4 to 38.2 percent of cache memory in real use for earlier systems.

Going deeper

Choosing the block size

Bigger blocks let the attention kernel read more keys and values per block in parallel, which uses the hardware better. Smaller blocks waste less in the last, partly filled block. The PagedAttention authors found that 16 tokens was large enough to use the GPU efficiently and small enough to avoid significant internal fragmentation in most workloads, and made it vLLM's default.

The indirection has a cost

Following a block table, branching and handling variable lengths makes the attention kernel itself slower. The paper measured 20 to 26 percent higher attention kernel latency than a highly optimized contiguous implementation. That cost touches only the attention operator, not the large matrix multiplications, and the much bigger batches more than pay for it.

Scheduling is a policy, not just a mechanism

Continuous batching decides when a request can join; the scheduler still decides which one. First come, first served is fair and prevents starvation. Serving engines add knobs on top, such as a cap on the tokens processed per step, which bounds how much prefill work any one step can absorb and so keeps time per output token steady for requests already decoding.

Trade-offs

OptionWins whenLoses when
Static batching Simple; fine when every request does the same work Idle slots and late returns when output lengths vary
Continuous batching Slots refill every step; higher throughput and lower queueing More complex scheduler; mixing prefill and decode needs care
Contiguous KV reservation Simplest, fastest attention kernel Most of the reserved memory can sit unused; smaller batches
Paged KV blocks Near-zero waste; sharing for sampling, beams and prefixes Block-table indirection slows the attention kernel somewhat
Preempt by recompute No CPU memory or transfer needed Repeats prefill work for the evicted request

Common mistakes

  • Benchmarking a server with identical request lengths, which hides exactly the problem continuous batching solves.
  • Assuming that continuous batching alone raises capacity, when the KV cache memory still caps how many requests can join.
  • Sizing a deployment with the maximum context per request when actual lengths are much shorter, or the reverse: ignoring that a few long requests can trigger preemption for everyone.
  • Treating a rise in latency under load as a model problem, when it is preemption or queueing.

In the wild

vLLM

vLLM: Easy, fast and cheap LLM serving with PagedAttention (opens in a new tab)

The launch post explains that the KV cache is large (up to 1.7 GB for a single sequence in a 13B LLaMA model) and dynamic, and that the existing systems they studied wasted 60 to 80 percent of its memory to fragmentation and over-reservation. With PagedAttention, waste occurs only in a sequence's last block, under 4 percent in practice. They report up to 24 times the throughput of Hugging Face Transformers and up to 3.5 times that of Text Generation Inference, memory sharing that cuts the memory of parallel sampling and beam search by up to 55 percent, and that serving the Vicuna demo and Chatbot Arena with vLLM let LMSYS cut the GPUs used for that traffic by 50 percent.

Yu et al., OSDI 2022 (paper)

Orca: A Distributed Serving System for Transformer-Based Generative Models (opens in a new tab)

Orca identified the problem with request-level scheduling: finished requests cannot return and new ones must wait until the whole batch completes. It introduced iteration-level scheduling, which runs one model iteration at a time and changes the batch between iterations, plus selective batching to batch only the operations that allow it. On a 175-billion-parameter model it reported a 36.9 times throughput improvement over NVIDIA FasterTransformer at the same latency.

Check yourself

  1. What is the core difference between static and continuous batching?

    Show answer

    B. Continuous batching reschedules after every decode step, so finished requests leave and waiting ones join immediately Iteration-level scheduling moves the decision from per batch to per step, which keeps slots full when output lengths vary.

  2. A server pre-allocates a 4,096-token KV region for every request, but most requests end near 500 tokens. Which waste dominates?

    Show answer

    B. Internal fragmentation and reserved space: most of each region never holds a token Space reserved for a maximum the request never reaches is internal fragmentation, and space for tokens not yet generated blocks other requests meanwhile.

  3. In PagedAttention, where can a request's wasted cache memory be?

    Show answer

    B. Only in its last, partly filled block Blocks are allocated on demand and filled in order, so only the last block can have empty slots. Fixed-size blocks also remove external fragmentation.

  4. A request asks for 4 samples of the same 2,000-token prompt. How does a paged KV cache help?

    Show answer

    B. All four block tables point to the prompt's physical blocks, which are copied only when a sample needs to write to one Reference-counted blocks with copy-on-write store the shared prompt once, freeing memory for more requests.

  5. You switch from static to continuous batching but throughput barely improves. Which is the most likely reason?

    Show answer

    A. Your requests all have very similar output lengths, so static batches had few idle slots Continuous batching recovers slots left idle by short requests. With uniform lengths there was little to recover; memory limits or prefill may be the real bottleneck.

Recap

  • Static batching holds every slot until the longest request finishes, wasting GPU steps and delaying both returns and new arrivals.
  • Continuous batching reschedules every decode step: finished requests leave at once and queued ones take their slots.
  • Contiguous KV reservation wastes memory through reserved space and internal and external fragmentation; earlier systems used as little as 20 to 38 percent for real tokens.
  • PagedAttention stores the cache in fixed-size blocks found through a block table, so waste is limited to one partly filled block per request.
  • Block tables also enable sharing with copy-on-write for parallel samples, beam search and shared prefixes, and preemption by swapping or recompute when memory runs out.

Get new lessons by email

Get new posts on backend and AI internals. No spam, unsubscribe anytime.

Your email is only used to send new posts. Privacy