Skip to main content
PagedAttention is the core memory management innovation introduced by vLLM. miniVLLM implements it end-to-end: from the BlockManager that allocates physical pages, to the Triton kernel that reads through the block table during decode.

The problem: fragmentation

In a naive implementation, each sequence pre-allocates a contiguous slab of GPU memory large enough for its maximum KV cache. This causes two kinds of waste:
  • Internal fragmentation — a sequence that generates 10 tokens wastes the memory reserved for the remaining 990.
  • External fragmentation — after some sequences finish, the freed slabs are scattered across memory and cannot be combined to serve a new long sequence.
Because sequence lengths are unknown at request time, a serving system cannot reliably pre-size allocations without either wasting large amounts of memory or frequently running out.

The solution: fixed-size pages

PagedAttention borrows the virtual memory idea from operating systems. KV cache is divided into fixed-size blocks (pages), each holding block_size tokens. A sequence’s KV cache is spread across as many blocks as it needs — they do not need to be contiguous in physical memory. Each sequence maintains a block table: a list that maps a logical block index to a physical block ID.
During decode, the Triton kernel consults block_tables to translate each logical token position into a physical cache slot:

The BlockManager class

BlockManager (engine/block_manager.py) owns all physical blocks and exposes the methods the scheduler calls at each step.
Checks whether enough free blocks exist to hold all of a new sequence’s tokens before it is moved from the waiting queue to running.
Called once when a sequence is first scheduled (prefill). Iterates over each logical block, looks for a prefix-cache hit, and allocates a fresh physical block on miss.
Called every decode step to confirm there is room to write one more token. A new physical block is only needed when num_tokens % block_size == 0.
Called after a token has been appended to the sequence object but before the model run writes its KV values. Allocates a new physical block when the last block is full, and records the content hash once a block becomes complete.
Decrements the reference count of every block in a sequence’s block table. A block is returned to the free pool only when its reference count reaches zero, which means no other sequence is sharing it (prefix caching can cause sharing).

Block lifecycle

1

New request arrives

The scheduler calls can_allocate(seq). If sufficient free blocks exist, the sequence moves from the waiting queue to running.
2

Prefill allocation

allocate(seq) walks the logical blocks. Full blocks that match a cached hash are reused immediately; partial or uncached blocks receive a fresh physical block from free_block_ids.
3

Decode steps

Each step the scheduler calls can_append(seq). If a new physical block is needed (num_tokens % block_size == 0), append(seq) allocates one. When the block is later completed, its hash is recorded for future prefix reuse.
4

Sequence finishes

deallocate(seq) decrements all reference counts. Blocks that reach zero are pushed back onto free_block_ids and can be reused immediately.

Prefix caching

Many requests share a common prefix — a system prompt, few-shot examples, or a document. PagedAttention can skip recomputing the KV values for those tokens entirely.

Content-based hashing

A block’s hash is computed from its token IDs and the hash of the preceding block, making it context-sensitive:
This means two blocks containing identical token IDs but following different prefixes get different hashes, preventing false sharing.
Hashes are only computed for full blocks. The partial (last) block of a sequence always has hash = -1 and is never shared.

Cache hit detection

During allocate, a hash lookup alone is not sufficient — hash collisions are possible. miniVLLM validates the stored token IDs too:
On a confirmed hit, seq.num_cached_tokens is incremented by block_size, and the model runner will skip writing those tokens to the cache (their values already exist in the physical block).

Reference counting

When two sequences share a prefix block, both point to the same physical block ID and the block’s ref_count is incremented. The block is only freed when every sharing sequence has been deallocated.
Prefix caching is most effective for workloads with a long, stable system prompt. The first request pays the full prefill cost; every subsequent request with the same prompt hits the cache and only processes its unique tokens.