Modules
12/30
PagedAttention

Contents

PagedAttention

How a language model server makes room for requests as their answers grow.

A language model server often answers many people at the same time. Some send a short question, while others provide several pages of text. As each answer grows, the model keeps intermediate results from the text it has already processed so that it can reuse them. These results take up GPU memory, and the server does not know in advance exactly how much space each answer will need.

Reserving a large amount of memory for every request leaves less room for everyone else. PagedAttention addresses this problem by letting the server store those intermediate results in small, reusable blocks and assign more blocks as a request grows. We will first look at what is being stored, then follow how the blocks are allocated, read, shared, and released.

1. Why Each Request Needs Its Own Memory

A model processes text as tokens, which can be words, parts of words, or punctuation. Inside each attention layer, a token produces lists of numbers called its query, key, and value. The query is compared with keys to decide how strongly to use the corresponding values. This is how attention brings information from earlier positions into the current calculation.

For ordinary causal generation, the keys and values of an earlier position can be reused when later tokens are processed. The model stores them in the key-value cache, usually shortened to KV cache. Keeping these results avoids recomputing them at every step. The cache contains numerical vectors from attention layers, rather than a copy of the text or the model's weights. Hugging Face's cache explanation describes this reuse in more detail.

The initial prompt-processing stage is called prefill. It builds the prompt's cache and produces the prediction for the first output token. During decoding, that output token is fed back through the model, its keys and values are added, and the model predicts the next token. The cache grows as more tokens are processed.

Requests use the same model weights, but generally need separate caches because their contexts differ. A single long conversation can therefore consume substantial cache memory, and a busy server must make room for many such conversations at once.

2. The Problem with Reserving Space Up Front

A straightforward design gives each request one continuous region of memory, meaning its cache entries sit next to one another. To avoid moving the cache as it grows, the server could reserve enough room for the request's maximum allowed length. However, a limit of 4,096 tokens does not mean that every request will actually use 4,096 tokens.

Consider a request with 600 cached tokens and space reserved for 4,096. The remaining 3,496 slots are unavailable to other requests even though they contain no useful cache entries yet. Some may be filled later, while others may never be used. This is the cost of reserving memory for an uncertain future.

Variable-size allocations can also leave gaps between occupied regions. There may be enough free memory in total, but no single gap large enough for the next allocation. This is external fragmentation. Unused space inside an allocated region is internal fragmentation. The original PagedAttention paper identifies these allocation problems as obstacles to serving larger batches.

3. Dividing the Cache into Small Blocks

PagedAttention was introduced with vLLM, an open-source engine for serving language models. It lets a sequence's cache occupy several separate blocks. Each block has room for the keys and values of a fixed number of tokens. These blocks do not need to be adjacent in GPU memory, so the server can use whichever suitable blocks are free.

We will use four tokens per block in the examples. Six cached tokens need two blocks: one holds tokens 1 through 4, and the other holds tokens 5 and 6 with two slots left over. Tokens 7 and 8 can fill those slots. Only when token 9 is processed does the request need a third block.

The word paged comes from a related memory-management idea in operating systems. A program can see an ordered address space even when its data occupies separate physical pages. Here, the serving engine manages KV-cache blocks and the attention implementation reads them through a mapping. This does not require moving the cache to CPU memory or disk. vLLM's introduction explains this connection to paging.

In practice, a server can reserve a GPU cache pool in advance and then assign blocks from that pool to requests on demand. Allocating a block to a request does not necessarily mean asking the GPU driver for a new allocation at every step.

4. Finding Tokens with a Block Table

Separating the blocks creates a new question: how does attention find the right cache entries? Each sequence has a block table, a small list that maps blocks in sequence order to their locations in the physical cache pool.

A logical block describes a range of positions in the sequence. A physical block is the storage assigned to that range. For example, logical block 0 might live in GPU block 3, while logical block 1 lives in GPU block 0. The IDs start at zero; they are labels, not token positions. The sequence still reads tokens 1, 2, 3, and so on in their original order.

A growing cache, four tokens at a time

Follow request A while request B stays in memory. Select a block-table entry to find its physical block below.

Request A's block table

6 cached tokens

Physical blocks in the GPU cache pool

Block 0A

2 of 4 slots used

Block 1B

4 of 4 slots used

Block 2Free

Available to assign

Block 3A

4 of 4 slots used

Block 4B

1 of 4 slots used

Block 5Free

Available to assign

Block 6Free

Available to assign

Block 7Free

Available to assign

Request A has six cached tokens in two blocks. Request B occupies two other blocks.

4 of 8 blocks free. A has 2 unused slots inside its assigned blocks.

Each numbered slot represents a token's cached keys and values, not the text itself. This example stops at 13 tokens and uses a chosen allocation order to make non-contiguous placement visible. Released blocks are not retained for prefix caching here.

Add tokens until the cache reaches nine entries. A third mapping appears, but the earlier entries stay in place. Finishing A returns its blocks to the pool without disturbing B. The block table lets storage change in these small units while preserving each request's view of its own sequence.

The address lookup in numbers

Number token positions from zero for the calculation. With block size B and position t, integer division gives the logical block; the remainder gives the offset inside it.

b=t/B,o=tmodBb = \lfloor t / B \rfloor, \qquad o = t \bmod B

The physical block is the entry at index b in the request's block table. In the example, the seventh token has t = 6. With B = 4, it belongs to logical block 1, offset 2. The table maps that block to physical block 0, so the token occupies the third slot there.

The actual byte address also depends on the layer, KV head, data type, and tensor layout. This lookup shows the token-to-block mapping, not a complete GPU memory-address formula.

5. Reading the Cache During Attention

Now follow one decoding step at one layer. The incoming token produces a new query, key, and value. The cache manager has arranged space for the new entry, and the key and value are written to its assigned block and offset. The query then attends to the valid keys and values in that sequence, including the current position.

A paged attention kernel, the GPU implementation of the operation, follows the block table to read those entries. If the table is [3, 0, 6], the logical sequence draws its cache from those physical blocks. There is no need to copy them into one continuous sequence-sized buffer before attention. The sequence length tells the kernel which slots in the final block are valid.

The attention calculation still compares the query with the allowed keys, applies softmax, and combines the values. Softmax must normalize over all valid positions for that query. Normalizing each page independently and simply adding its output would give the wrong result. Physical placement also does not change a token's logical position or the causal mask.

vLLM's historical kernel walkthrough shows these reads and reductions. It documents the original implementation, rather than every attention backend used by current vLLM releases.

6. How Much Memory Is Saved?

Paging reduces unused allocation space; it does not make an individual key or value smaller. In our four-token example, nine cached tokens occupy three blocks with a total capacity of twelve tokens. Three slots remain unused. At eight tokens, two blocks are exactly full and there are no unused slots.

Nine cached tokens, four slots per block

3

Blocks assigned

12

Slots reserved

3

Slots unused

For an unshared sequence that grows by appending tokens, with no extra blocks reserved ahead of time, only its final block can be partly empty. With B slots per block, that leaves at most B minus 1 unused slots per sequence. Equal-size blocks also remove the need to find one long continuous gap within the cache pool.

Smaller blocks can reduce unused tail space, but require more block-table entries and may be less efficient for a particular kernel. Larger blocks make the allocation coarser. Four is only a teaching choice here; supported sizes and suitable choices depend on the attention backend and hardware.

Putting the token slots into bytes

For a conventional Transformer with the same KV dimensions at every layer, a token's cache contains two sets of vectors, keys and values, for each layer and KV head. If L is the number of layers, H is the number of KV heads, D is their vector width, and s is the bytes per stored number:

KV bytes per token=2×L×H×D×s\text{KV bytes per token} = 2 \times L \times H \times D \times s

An illustrative model with 32 layers, 8 KV heads, width 128, and two bytes per number needs 131,072 bytes, or 128 KiB, per token. Its 4,096-token cache contains 512 MiB of KV data. These are totals across the model's layers before any distribution across devices, excluding metadata, padding, and other GPU memory.

Use the number of KV heads, which can be smaller than the query-head count in grouped-query attention. Paging changes how storage is assigned. Reducing the number of KV heads or quantizing the cache changes how many bytes each token needs.

7. Sharing a Prefix Without Overwriting It

Suppose we want two different continuations of the same prompt. They begin with identical context, so their already-computed prefix can share physical cache blocks. Each continuation has its own block table, but the prefix entries point to the same storage. A reference count records how many active sequences use each block.

Sharing needs care when a block is only partly filled. If B appends a new entry to a block that A also uses, it must not modify A's storage. Copy-on-write handles this by giving B a private copy when a write becomes necessary. Full prefix blocks that will not be modified can remain shared.

Two continuations, one shared prefix

A and B begin with the same six cached tokens. See what happens when B needs to write its seventh token into a shared, partly filled block.

Continuation A

Logical block 0GPU block 0
Logical block 1GPU block 1

Continuation B

Logical block 0GPU block 0
Logical block 1GPU block 1
Block 0Shared

2 references: A and B

Block 1Shared

2 references: A and B

Both block tables point to blocks 0 and 1. There are two references to each block, but only one physical copy of the six-token prefix.

An illustration of block-level copy-on-write for forked sequences. The labels stand for KV entries. This is separate from vLLM's full-block automatic prefix-cache lookup across requests.

In this example, the first four entries are never copied. Only the partly filled second block needs a private copy for B. When A finishes, a block still referenced by B cannot be reclaimed. The original design uses this combination of block tables, reference counts, and copy-on-write for forked generation, including parallel sampling. vLLM's introduction illustrates the same sharing mechanism.

8. Reusing Work Across Separate Requests

Sharing can also help when a later request starts with a prefix that the server has already processed. For example, several questions might begin with the same system instructions and document. Automatic prefix caching identifies reusable KV blocks so the server can skip the corresponding prefill work. It reuses model computation, not a previously written answer. vLLM's feature guide describes this use case.

Matching a few words anywhere in the text is insufficient. A cached entry depends on the context before it, so reuse requires a matching prefix and compatible model state. vLLM's hash-based prefix-cache design identifies blocks using their tokens, preceding-prefix information, and relevant identifiers such as the LoRA adapter. Its documented block-cache scheme stores full blocks, which is distinct from the partial-block branching example above.

A prefix cache may retain a finished request's blocks as reusable entries. Once no active request references a block, the manager can evict it when that space is needed. Paged allocation makes block-level reuse convenient, while prefix matching and eviction decide which results to keep. Even with a prefix-cache hit, decoding still has to compute the new answer.

9. Making Room for More Requests

Better cache utilization can let a server keep more requests active within the same memory budget. A scheduler decides which of them run in each iteration. With continuous batching, finished requests leave the batch and waiting requests can join as capacity becomes available, instead of waiting for every request in the original batch to finish.

The two responsibilities work together: the scheduler chooses the work, while the cache manager provides and tracks its storage. In our first example, finishing A makes several blocks available. A waiting request can use those blocks even though they are separated by B's cache. It does not need B to finish or move elsewhere.

The pool still has a finite capacity. If every usable block is occupied, a request that needs another block must wait or the engine must make space. For example, vLLM can preempt a request, release its cache, and recompute the needed state later. That has a latency cost, as its preemption documentation explains.

The main opportunity is higher serving throughput, meaning more useful work completed over time. A larger active batch does not guarantee that one person's answer finishes sooner. The result also depends on request lengths, available compute, the attention implementation, and scheduling decisions.

10. How PagedAttention Fits with FlashAttention

FlashAttention focuses on how the attention calculation moves data through GPU memory. Tiling and online softmax let it avoid storing the full table of attention weights. PagedAttention focuses on allowing the persistent KV cache to occupy separate, manageable blocks as requests grow and share context.

Paged KV storage

Where are this request's cached keys and values, and which blocks can grow, be shared, or be reused?

Efficient attention computation

How can the GPU read those keys and values and calculate the output with less intermediate storage and data movement?

These ideas can be combined when a kernel supports a paged cache layout. The FlashAttention implementation, for example, includes support for paged KV caches. Neither paging nor tiling by itself removes the need for full attention to consider the allowed context positions.

The useful separation is between the sequence a model sees and the storage a server manages. Token order and attention semantics remain intact, while block tables let the underlying cache grow and be reclaimed in small pieces. That flexibility is what makes PagedAttention valuable when many conversations compete for the same GPU memory.