Skip to content
AI360Xpert
Gen AI

Paged Attention

Instead of storing a sequence's KV cache in one massive, contiguous chunk of memory that wastes space, break it into fixed-size blocks mapped through a table—just like an operating system manages virtual memory.

Paged attention splits the KV cache into fixed-size blocks mapped through a block table, eliminating contiguous memory fragmentation
Paged attention splits the KV cache into fixed-size blocks mapped through a block table, eliminating contiguous memory fragmentation

Why Does This Exist?

The KV cache saves enormous amounts of compute during generation, but it trades that compute for memory. As a model generates text, it appends new key and value vectors to the cache for every single token.

Historically, AI frameworks (like PyTorch) managed memory by allocating a single, large, contiguous block of GPU RAM for a sequence's cache. The problem? The framework doesn't know in advance how many tokens the model is going to generate. If it pre-allocates a maximum-size block (e.g., 8,000 tokens) and the model only generates 20 tokens, the remaining 7,980 tokens' worth of memory sits entirely empty and wasted. If it tries to grow the block dynamically, it fragments the GPU memory, eventually failing to find a contiguous space large enough for a new request. This massive waste meant early LLM servers could only handle a tiny fraction of the requests their GPUs were computationally capable of serving.

Paged attention solved this by bringing a classic operating system concept—virtual memory paging—to the GPU.

Think of It Like This

Seating a restaurant with unpredictable party sizes

Imagine a restaurant with long, continuous benches. A party of two walks in, but they say "we might have up to 20 friends joining us later." To be safe, the host reserves an entire 22-person bench for them. Only two friends show up. The rest of the bench sits empty for the entire night, while new customers are turned away at the door. That is contiguous memory allocation.

Paged attention changes the restaurant to use standard 4-person tables. When the party of two arrives, they get one table. When three more friends arrive, the host gives them a second 4-person table anywhere in the room, and simply hands the waiter a map linking the two tables together. The restaurant operates at maximum capacity, with zero wasted benches.

How It Actually Works

Blocks and Block Tables

Paged attention divides the KV cache into fixed-size "blocks" (typically holding the keys and values for 16 or 32 tokens).

When a user sends a prompt, the system breaks the prompt into blocks and allocates physical memory for them wherever space is available on the GPU—they do not need to be next to each other. To keep track of this scattered data, the system maintains a Block Table for each request. The block table maps the logical sequence of the tokens to their physical block addresses in the GPU memory.

Zero waste during generation

As the autoregressive generation loop runs, the model produces new tokens one by one. The system adds these new tokens to the currently active physical block. Once that block fills up (e.g., hits 16 tokens), the system simply asks the GPU for one new, empty physical block, adds it to the request's block table, and continues.

Because memory is requested exactly when it is needed, and only in small 16-token increments, internal fragmentation (wasted pre-allocated space) drops from >50% in naive systems to <4%.

The foundation for sharing

Because the memory is decoupled through a block table, different requests can easily share identical blocks of memory. If two users send identical system prompts, the server only computes and stores the KV cache for that prompt once. It simply points both users' block tables to the exact same physical blocks for those initial tokens. This is the underlying mechanism that makes prefix caching and parallel sampling (like beam search) highly efficient.

Show Me the Code

While the actual CUDA kernels for paged attention (developed by the vLLM team) are highly complex, the logical routing through a block table is straightforward to model in Python.

class PagedKVCache:    def __init__(self, block_size: int = 4):        self.block_size = block_size        # The physical memory pool (list of available blocks)        self.physical_blocks: dict[int, list[str]] = {0: [], 1: [], 2: [], 3: []}        self.free_blocks = [0, 1, 2, 3]            def allocate_block(self) -> int:        return self.free_blocks.pop(0)
class RequestTracker:    def __init__(self, cache: PagedKVCache):        self.cache = cache        self.block_table: list[int] = [] # Maps logical block to physical block index        self.logical_length = 0            def append_token(self, token: str):        # Do we need a new block?        if self.logical_length % self.cache.block_size == 0:            new_block_idx = self.cache.allocate_block()            self.block_table.append(new_block_idx)                    # Find the physical block for the current logical end        current_logical_block = self.logical_length // self.cache.block_size        physical_idx = self.block_table[current_logical_block]                # Store the token in the physical block        self.cache.physical_blocks[physical_idx].append(token)        self.logical_length += 1        print(f"Token '{token}' -> Physical Block {physical_idx}")
# Initialize systemmemory = PagedKVCache(block_size=2)req = RequestTracker(memory)
# Simulate generationreq.append_token("The")  # -> Physical Block 0req.append_token("cat")  # -> Physical Block 0req.append_token("sat")  # -> Physical Block 1 (New block allocated!)req.append_token("on")   # -> Physical Block 1

Notice how the RequestTracker seamlessly flows across physical blocks, allocating new ones exactly when the previous one fills up, wasting zero space.

Watch Out For

Assuming paged attention speeds up a single request

Paged attention does not make the math of the attention mechanism faster; in fact, looking up addresses in a block table adds a tiny amount of overhead. Its sole purpose is memory efficiency. By freeing up massive amounts of wasted memory, it allows the server to batch many more concurrent requests together, massively increasing the total throughput of the system, even if the latency of one single request remains exactly the same.

Block size tuning

The size of the block matters. If a block is too small (e.g., 1 token), the overhead of the block table becomes massive and memory access becomes incredibly inefficient. If the block is too large (e.g., 512 tokens), you start experiencing memory fragmentation again when a request only generates 10 tokens into that large block. 16 or 32 tokens are the industry standard sweet spots.

The Quick Version

  • Naive KV cache management wastes massive amounts of GPU memory by pre-allocating large, contiguous chunks that often go unused.
  • Paged attention solves this by breaking the KV cache into small, fixed-size blocks (e.g., 16 tokens).
  • A Block Table maps a sequence's logical tokens to physical blocks scattered anywhere in GPU memory.
  • New blocks are allocated dynamically only when needed, dropping memory waste from >50% to <4%.
  • This architecture enables safe memory sharing across concurrent requests, drastically increasing total server throughput.
  • KV Cache explains the mathematical necessity of saving these vectors in the first place.
  • Prefix Caching explores how paged attention's block tables allow multiple users to safely share the exact same physical memory.
  • Beam Search (upcoming) heavily leverages paged attention, as parallel beams can share a single block table for their shared prefixes.

Related concepts