PagedAttention: virtual memory for the KV cache
Module 2 established the waste: a naive allocator reserves max_seq_len contiguously per sequence, and the vLLM authors measured that only 20.4% to 38.2% of allocated KV memory actually held tokens. Between 60% and 80% was fragmentation.
Operating systems solved exactly this problem in the 1960s, and the solution transfers almost without modification.
The mechanism. Divide the KV cache into fixed-size blocks — typically 16 tokens' worth. Each sequence gets a block table mapping its logical positions to physical block numbers. Blocks are allocated on demand as the sequence grows, from a shared pool, and returned when it finishes. Physical blocks belonging to one sequence need not be adjacent.
sequence A, 35 tokens, block size 16:
block table: [ 7, 2, 19 ] 3 blocks = 48 slots, 35 used
physical pool:
block 0 [ free ]
block 1 [ seq B ]
block 2 [ seq A ] <- logical positions 16..31
block 3 [ seq C ]
...
block 7 [ seq A ] <- logical positions 0..15
...
block 19 [ seq A ] <- logical positions 32..34, 13 slots spare
Waste drops to at most one partial block per sequence — on average half a block, so 8 tokens. Against 128 KiB/token on Llama-3-8B that is about 1 MiB per sequence, versus gigabytes under reservation. vLLM reports waste below 4%.
The attention kernel has to change to match: instead of reading a contiguous [seq_len, head_dim] tensor, it gathers through the block table. That indirection costs a little, and the gain vastly outweighs it.
Copy-on-write falls out for free, and this is the elegant part. Two sequences sharing a prefix can point their block tables at the same physical blocks. Nothing is copied. When one of them writes to a shared block — which only happens at the boundary block where they diverge — that block is copied and the writer's table updated. Parallel sampling with n=4 from one prompt therefore costs one copy of the prompt's cache, not four. Beam search gets the same treatment.
Choosing the block size is a real trade-off. Small blocks (8) minimize internal fragmentation but mean more block-table entries and more indirection per attention call. Large blocks (32+) reduce overhead but waste more on partial blocks and make prefix sharing coarser — two sequences must share a whole block to share anything. 16 is the common default and is a reasonable compromise rather than a derived optimum.
NAIVE PAGED
reserve max_seq_len contiguously 16-token blocks from a shared pool
A [####·······················] A -> [7][2][19]
B [#########··················] B -> [1][4][11][3]
C [##·························] C -> [0]
^ used ^ reserved, unlendable
free pool: [5][6][8][9][10][12]...
utilization 20-38% utilization > 96%
and A,B can SHARE a block if they
share a prefix -- no copy
REMEMBERAllocate the cache in small fixed-size blocks with a per-sequence block table, so physical memory need not be contiguous and nothing is reserved for tokens that may never exist.