Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

~11 min readSOSP 2023

PagedAttention,
explained.

Here is a serving secret that sounds like a bug: most of an LLM server's precious GPU memory sits empty. Each request reserves room for the longest answer it might ever produce, then usually stops early — leaving the rest reserved but unused. PagedAttention borrowed a fifty-year-old operating-systems trick, virtual memory paging, and applied it to the KV cache. The wasted memory came back, batch sizes grew, and throughput multiplied. Here's the fragmentation problem, and the paging that fixes it.

The delta

What changes against a faster kernel: the cache stops being pre-allocated

Rung 2 makes the forward pass efficient. This rung looks at what limits how many users a server can hold at once, and finds it is not speed but memory capacity — specifically the key-value cache, which grows with every token of every active sequence and, at realistic concurrency, claims a large share of the accelerator beside the weights (about 30 percent of an A100 in the paper's OPT-13B example, against about 65 percent for the weights).

The diagnosis is that the memory was mostly being wasted. Serving systems allocated each sequence one contiguous buffer sized to the maximum length it might reach, because attention kernels wanted contiguity. A request that generates two hundred tokens against a two-thousand-token budget leaves nine tenths of its allocation untouched, and because the blocks are large and contiguous, the leftovers are also fragmented into gaps too small to reuse. Most of the expensive memory held nothing.

The change is to apply the operating system's answer. Store the cache in fixed-size blocks that need not be adjacent, with a per-sequence table mapping logical positions to physical blocks, and allocate a block only when it is needed. Waste per sequence is then bounded by one partially-filled block instead of the entire unused budget. The second gain follows for free from the indirection: two sequences sharing a prefix — a common system prompt, or several samples drawn from one prompt — can point at the same physical blocks and only copy when one of them diverges. Stated in units, fitting several times as many concurrent sequences multiplies throughput at the same latency, which is the number that decides what serving costs.

01

The KV cache problem

When an LLM generates text, it caches the key and value vectors of every token it has seen so it doesn't recompute them — the KV cache. For long sequences and big models this cache dominates GPU memory, and how much of it fits directly sets how many requests you can batch, which sets throughput. Memory is the bottleneck of serving.

The old way to manage it was crude: give each request one contiguous buffer sized for its maximum possible length. But you don't know how long an answer will be, and most are short. So the server reserves room for 2,000 tokens and the request stops at 100 — the other 1,900 slots are reserved, untouchable by anyone else, and wasted. This is internal fragmentation, and measurements showed it could consume the large majority of the KV cache. Add the gaps left between requests as they come and go (external fragmentation) and a server could be sitting on mostly-empty memory while turning new requests away.

The one-sentence version

Reserving each request's maximum length up front wastes most of the KV cache; paging allocates it in small blocks on demand, so almost none is wasted.

02

Paging the cache

Operating systems solved exactly this problem for RAM decades ago, and PagedAttention imports the solution wholesale. Instead of one contiguous buffer per request, the KV cache is cut into fixed-size blocks (say 16 tokens each). A request is given blocks as it grows, and they can sit anywhere in memory — a block table maps the request's logical token positions to their scattered physical blocks, just like an OS page table.

Contiguous reservation vs paged blocks
ContiguousReserve max_len up front, one block of memory. Short requests waste the rest.
PagedAllocate fixed-size blocks on demand; grow one block at a time as tokens arrive.
Block tableMaps logical positions → physical blocks, so blocks can live anywhere — no external fragmentation.

The attention kernel is rewritten to gather keys and values through this table rather than assuming they are laid out contiguously — that is the "attention" half of PagedAttention. The result is that memory is handed out in small increments and reclaimed the instant a request finishes, keeping the cache densely packed.

03

The waste, bounded

The improvement is easy to quantify. With contiguous reservation, the wasted memory per request is everything you reserved but didn't use — max_len − seq_len, which for a short answer is nearly the whole buffer. With paging, a request uses ⌈seq_len / block_size⌉ blocks, so the only waste is the unfilled tail of its last block:

The paged waste is capped at less than one block regardless of sequence length — a handful of tokens, not thousands. Concretely, a 100-token request with a 2,048 max wastes 1,948 slots contiguously (95%) but only 12 with 16-token blocks. Reclaim that and the same GPU holds many times more requests: in the runnable below, roughly 18× more. Since decoding throughput is set by how many requests you can batch, more requests packed in means proportionally more tokens per second.

04

Copy-on-write sharing

Blocks addressed through a table unlock a second win for free: sharing. Because several sequences can point their block tables at the same physical blocks, a common prefix need only be stored once. This is everywhere in practice — parallel sampling (many completions of one prompt), beam search (many beams from one context), a shared system prompt across users. All of them share a prefix.

PagedAttention keeps the shared blocks read-only and applies copy-on-write: the moment one sequence needs to write into a shared block (it diverges), that block alone is copied so the write doesn't corrupt the others. Until then, the prefix costs memory once no matter how many sequences read it. For beam search over a long prompt, that can cut KV memory dramatically — the runnable shows two beams sharing a prefix using 6 blocks instead of 10.

How it works

How vLLM manages the KV cache, step by step

Start with the number that motivates the design. For OPT-13B, one token's keys and values take 2 (key and value) × 5,120 (hidden size) × 40 (layers) × 2 bytes = 800 KB, so a single 2,048-token sequence can need 1.6 GB. On a 40 GB A100 the 13B weights take about 26 GB, leaving roughly 30 percent of the GPU for the KV cache, and how that space is managed decides the batch size. vLLM's procedure:

sequence A block tables (logical → physical) physical KV blocks logical 0 logical 1 A appends a token sequence B logical 0 logical 1 #7 prompt · ref 2 #3 copy for A · ref 1 #1 B writes here · ref 1 copy-on-write ref count above 1 → copy before write table entry = block id + slots filled
Block tables let two sequences point at the same physical blocks; a block is copied only when a sequence writes into one whose reference count is above 1.
  1. Carve the cache into blocks. The GPU memory left for the KV cache is split into fixed-size physical blocks, each holding keys and values for B tokens (16 by default). A free list tracks unused blocks.
  2. Give each sequence a block table. A sequence's tokens live in logical blocks 0, 1, 2 and so on. Its block table maps each logical block to a physical block and records how many of its B slots are filled.
  3. Prefill. For a new request, the scheduler allocates ⌈prompt length / B⌉ blocks and the prompt's keys and values are written into them.
  4. Decode. Each generated token's key and value go into the next free slot of the sequence's last block. Only when that block is full is a new physical block taken from the free list, so a sequence never holds more than one partly empty block.
  5. Attend through the table. The PagedAttention kernel computes attention for a query by fetching key and value blocks wherever the table says they are and processing them block by block, so physical blocks need not be adjacent.
  6. Share with reference counts. Every physical block counts the logical blocks that point to it. Parallel samples of one prompt point at the same prompt blocks. When a sequence must write into a block whose count is above one, vLLM first copies it to a new block (copy-on-write) and decrements the old count; beam search shares blocks among beams the same way and frees a block when its count reaches zero.
  7. Preempt when memory runs out. Scheduling is first-come-first-served at the granularity of one decoding iteration. When no free block is left for the next tokens, vLLM preempts the most recently arrived requests and evicts all of a sequence's blocks at once, since every block of a sequence is read at every step. Sequences that share blocks, such as the beams of one request, are scheduled and evicted together.
  8. Recover by swapping or recomputing. Evicted blocks are either copied to CPU memory and copied back later, or dropped and rebuilt when the request resumes: its prompt and the tokens generated so far are run as one new prefill, which is much faster than the original token-by-token decoding.

Across GPUs

For models split with Megatron-style tensor parallelism, a single KV cache manager sits in the central scheduler. Every GPU worker uses the same block table and the same physical block numbers but stores only the keys and values for its own attention heads, so the mapping is decided once and sent out with each step's inputs.

The authors built vLLM with a FastAPI front end and a GPU engine in about 8,500 lines of Python and 2,000 of C++ and CUDA, including fused kernels for writing new keys and values into blocks and for the block copies that copy-on-write needs.

The math

KV cache bytes per token, and the paging waste bound

KV bytes per token = 2 · nlayers · dmodel · bytes per element
OPT-13B: 2 · 40 · 5,120 · 2 B = 819,200 B ≈ 800 KB
blocks(n) = ⌈n / B⌉, waste(n) = B · ⌈n / B⌉ − n ≤ B − 1
batch ≈ KV memory / (KV bytes per token × tokens reserved per sequence)
  • 2 — one key vector and one value vector per token per layer.
  • nlayers · dmodel — every layer caches a hidden-size vector for each. For models whose query heads share key and value heads (multi-query or grouped-query attention), dmodel becomes the number of KV heads times the head size.
  • B — block size in tokens; n — the sequence's current length.
  • tokens reserved per sequence — the maximum length under contiguous allocation; the current length rounded up to a whole block under paging.

At the extremes: B = 1 wastes nothing but makes every token a separate lookup, which keeps the kernel from reading memory in efficient chunks, while B equal to the maximum length is contiguous reservation again. Because the waste bound does not grow with n, long sequences gain the most.

The evidence

What the vLLM paper measured

The evaluation served OPT-13B on one A100, OPT-66B on four and OPT-175B on eight, plus LLaMA-13B, replaying ShareGPT and Alpaca request traces with Poisson arrivals at rising rates. The metric was normalized latency (mean end-to-end latency divided by output length); a system's capacity is the request rate it sustains before that latency climbs. Baselines were FasterTransformer and three Orca variants the authors reimplemented: Oracle, which knows each output length in advance; Pow2, which over-reserves by up to 2×; and Max, which reserves the maximum length.

MeasurementResult
KV memory holding real token states, earlier systems20.4–38.2%
KV memory wasted in vLLMunder 4%
Throughput at equal latency vs FasterTransformer and Orca2–4× higher
Sustained request rate vs Orca (Oracle), ShareGPT1.7–2.7×
Sustained request rate vs Orca (Max), ShareGPT2.7–8×
Sustained request rate vs FasterTransformerup to 22×
Memory saved by sharing, parallel sampling (Alpaca)6.1–9.8%
Memory saved by sharing, beam search (Alpaca)37.6–55.2%
Shared few-shot prefix, LLaMA-13B, vs Orca (Oracle)1.67× (1-shot), 3.58× (5-shot)

The ablations show the costs. The PagedAttention kernel itself was 20–26 percent slower than FasterTransformer's attention kernel, because it reads through the block table and handles variable lengths; the end-to-end gain comes from batch size, not kernel speed. Block sizes from 16 to 128 worked best on ShareGPT, while on Alpaca's shorter sequences large blocks hurt, which is why 16 is the default. For preemption, swapping was expensive with small blocks, since that means many small transfers over PCIe, while recomputation cost the same at any block size; between 16 and 64 the two were comparable. And on Alpaca with OPT-175B, where eight GPUs leave plenty of room for short sequences, the advantage over the Oracle and Pow2 baselines was much smaller: paging pays off where memory is the bottleneck.

Since then

What the authors admit, and what happened next

The trade-offs are the familiar ones from paging, and the paper treats them as such. Block size is a tuning decision — smaller blocks waste less and add more indirection — and the non-contiguous layout means the attention kernel has to gather through a block table, so this rung depends on writing a kernel that can, which is why it arrived after rung 2 rather than before.

Prefix sharing is also only as good as the sharing that actually occurs: it is transformative for workloads with a long common prompt and does nothing for unrelated requests, so reported gains depend on a workload shape as much as on the mechanism.

How it held up: it became the default architecture for language-model serving within about a year, and the framing is now simply assumed — the cache is a managed, paged resource rather than a buffer. Read this rung alongside latent compression of the cache on the sparse ladder: one makes each cached entry smaller and the other stops wasting space around it, they compose, and both exist because the cache, which grows with load while the weights stay fixed, is what a serving operator runs out of.

SOLVE IT YOURSELF

Count the wasted memory

The whole argument is arithmetic on blocks. This models both schemes: a 100-token request with a 2,048 max wastes 95% of its buffer contiguously, but paging into 16-token blocks caps the waste below one block (12 slots). Reclaim it and the same 16,384-slot pool holds ~18× more requests. It also prices copy-on-write prefix sharing — two beams sharing a 64-token prefix cost 6 blocks instead of 10. Change the block size, max length, or sequence length and watch the waste and capacity move.

YOUR TASK

Write the body of no_share_blocks. Everything else in the file is already written; this is the function the paper is about. Press ▶ Run to check it against the 5 assertions below — they start red.

WHAT IT HAS TO SATISFY — 5 ASSERTIONS
  1. Blocks needed is ceil(seq_len / block_size)
  2. Paged internal waste is bounded by block_size
  3. Naive pre-allocation wastes far more than paging
  4. Paging fits many more requests in the same memory
  5. Copy-on-write shares a prefix, storing it once
CPython · WebAssembly
05

The throughput win

Reclaimed memory doesn't just sit there — it becomes batch size. Decoding is memory-bandwidth-bound, so the way to more tokens per second is to process more requests together in each pass. PagedAttention's dense packing is what makes those bigger batches fit, and vLLM pairs it with continuous batching (swapping finished requests out and new ones in every step, rather than waiting for a whole batch to complete).

EffectCause
2–4× throughputReclaimed fragmentation → more requests batched at the same latency.
Bigger wins on long sequencesThe longer the max length, the more contiguous reservation wasted — and the more paging saves.
Extra wins on shared prefixesParallel sampling and beam search share prompt blocks via copy-on-write.
Small overheadBlock-table lookups and a paged attention kernel — cheap next to the memory reclaimed.

The trade is a little indirection: attention now gathers through a block table instead of reading contiguous memory. That cost is tiny compared to the memory it frees, which is why the net effect is a large, consistent throughput gain.

06

Why it spread

vLLM, built on PagedAttention, became one of the most widely deployed open-source LLM serving engines almost immediately, and paged KV caching is now standard across serving stacks. The reason is that it attacked the exact bottleneck of the moment — GPU memory during inference — with a fix that was principled, general, and required no change to the model. Cheaper serving is what let more teams run large models at all.

Its deeper lesson is a favorite of systems work: the right abstraction from one field often solves a fresh problem in another untouched. LLM serving had reinvented contiguous memory allocation and inherited its decades-old fragmentation pains; PagedAttention simply pointed out that operating systems had already solved this with paging, and ported the idea. Sometimes the breakthrough is recognizing which old, well-understood mechanism your new problem secretly is.

Worth knowing

Block size is a tuning knob: smaller blocks waste less on the tail but add more block-table overhead; larger blocks are cheaper to manage but waste a bit more per request. vLLM's default of 16 tokens is a practical balance.

Questions

Questions people ask about PagedAttention

How do you calculate the KV cache size per token?

Multiply 2 (one key and one value) by the number of layers, by the width of the cached vectors, and by the bytes per number. For OPT-13B, the vLLM paper's example, that is 2 × 40 layers × 5,120 × 2 bytes in fp16, about 800 KB per token, so a 2,048-token sequence can need 1.6 GB. For models that use multi-query or grouped-query attention, replace the hidden size with the number of key-value heads times the head dimension, which is why those models cache far less per token. Multiply by the number of tokens across all active sequences to get the memory the cache needs.

What does vLLM do when GPU memory runs out: swap or recompute?

It preempts. When no free block is left for the next tokens, the first-come-first-served scheduler evicts the most recently arrived requests, and it evicts all of a sequence's blocks at once because every block is read at every step. Recovery uses one of two mechanisms. Swapping copies the evicted blocks to CPU memory and back when the request resumes. Recomputation drops them and, on resumption, runs the prompt plus the tokens generated so far as a single prefill. The paper measured that swapping is costly with small blocks because of many small PCIe transfers, recomputation costs the same at any block size, and for blocks of 16 to 64 tokens the two perform comparably.

Why does vLLM use a block size of 16?

Block size trades two costs. Small blocks waste less memory, since a sequence only ever leaves its last block partly empty, but they give the attention kernel less contiguous data to read at a time and cost more block-table lookups. Large blocks read efficiently but waste more of the final block and make prefix sharing coarser. In the paper's sweep, block sizes from 16 to 128 performed best on ShareGPT traces, while on the shorter Alpaca requests larger blocks hurt, because many sequences are shorter than a block. Sixteen is large enough to use the GPU well and small enough to keep internal fragmentation low on both workloads.

How does copy-on-write work in PagedAttention?

Each physical block keeps a reference count of how many sequences' block tables point to it. When several samples are drawn from one prompt, all of them map their prompt's logical blocks to the same physical blocks, and the counts go up. Nothing is copied while the samples only read. When a sample needs to write a new token into a block whose count is above one, typically the partly filled last block of the prompt, vLLM allocates a fresh block, copies the contents, points that sample's table at the copy and decrements the original's count. The paper measured 6.1 to 9.8 percent memory saved for parallel sampling and 37.6 to 55.2 percent for beam search.

How much faster is vLLM than Orca and FasterTransformer?

The paper reports 2 to 4 times the throughput of FasterTransformer and Orca at the same latency. Broken down on ShareGPT traces, vLLM sustained 1.7 to 2.7 times the request rate of an Orca variant that knows every output length in advance, 2.7 to 8 times that of Orca reserving the maximum length, and up to 22 times that of FasterTransformer. The gain shrinks when memory is not the bottleneck: with short Alpaca requests on OPT-175B spread over eight GPUs, the advantage over the best Orca variants was much smaller. Its own attention kernel was 20 to 26 percent slower, so the whole win comes from fitting larger batches.

How does PagedAttention work with tensor parallelism across GPUs?

vLLM supports Megatron-style tensor parallelism, where each GPU holds a slice of every layer's attention heads. A single KV cache manager in the central scheduler owns the block tables. All workers use the same logical-to-physical mapping and the same physical block numbers, but each GPU stores in those blocks only the keys and values for its own heads. At every step the scheduler sends the input tokens together with the block tables, and the workers run the model and synchronize through the usual all-reduce operations without any memory bookkeeping of their own. Memory management is therefore decided once, centrally, however many GPUs share the model.

Check yourself

4 questions to test what you just read

Work each one out before you open it — a number, a prediction or a reason, not a definition. Producing the answer is what makes it stick. Your progress is kept on this device.

Using the per-token formula, how much KV cache does one 2,048-token OPT-13B sequence need, and about how many such sequences fit in the roughly 12 GB a 40 GB A100 has left after 26 GB of weights and some activation memory?

Show answer

800 KB × 2,048 ≈ 1.6 GB per sequence, so about 12 / 1.6 ≈ 7 sequences if each reserves its maximum length. With paging, each sequence holds only its actual length rounded up to a block, so a workload of mostly short replies fits many times more.

A request with a 512-token prompt has generated 30 tokens. With 16-token blocks, how many blocks does it hold, and how many slots are empty?

Show answer

542 tokens need ⌈542 / 16⌉ = 34 blocks, which is 544 slots, so 2 are empty. Under contiguous allocation with a 2,048-token maximum, 2,048 − 542 = 1,506 slots would sit reserved and unused.

Four parallel samples share a 160-token prompt with 16-token blocks. How many prompt blocks are stored with sharing versus without, and when does the first copy-on-write happen?

Show answer

160 / 16 = 10 blocks stored once, versus 40 without sharing. And no copy is ever made: the prompt fills its last block exactly, so each sample's first new token goes into a fresh block of its own. Copy-on-write only triggers when the shared last block is partly empty.

Why does vLLM evict all of a sequence's blocks when it preempts, rather than just a few?

Show answer

Attention at every decoding step reads the keys and values of the whole context, so every block of a running sequence is needed every step. Evicting only some would stall the sequence until they returned while freeing nothing useful. All-or-nothing eviction, plus gang-scheduling of sequences that share blocks, keeps preemption simple and the freed memory genuinely free.

Where this sits

Serving it cheaply

Five papers about the bill. Each one buys throughput back from a different part of the stack: the weights, the memory hierarchy, the KV cache, the decode loop.

  1. 1Knowledge Distillation2014
  2. 2FlashAttention2022
  3. 3PagedAttention2023
  4. 4Speculative Decoding2023
  5. 5Medusa2024

All 110 papers, in 19 ladders →

Efficient Memory Management for Large Language Model Serving with PagedAttention · Kwon, Li, Zhuang, Sheng, Zheng, Yu, Gonzalez, Zhang, Stoica · SOSP 2023 · read the original paper on arXiv → · Vibe Engines · 2026
Finished this one? 0 / 110 Paper Breakdowns done
NextSpeculative Decoding Next on the “Serving it cheaply” ladder · rung 4 of 5

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More Paper Breakdowns