Skip to content
AILinkDeepTech
Go back
Reinforcement Learning Medium

PagedAttention: Virtual-Memory KV Cache Management, Continuous Batching, and PyTorch Architecture

Abstract

Master PagedAttention: virtual memory KV cache paging, block tables, prefix caching (CoW), continuous batching, memory fragmentation math, and PyTorch kernels.

KV Cache Memory Bottlenecks in Autoregressive Serving

During autoregressive generation in decoder-only Transformers, generating each subsequent token requires computing self-attention over all previous key and value tensor representations. Storing these intermediate states in the Key-Value (KV) Cache eliminates redundant quadratic recomputations () during sequential decoding:

For a standard parameter model (, FP16 precision), each token consumes . A sequence of tokens consumes of High Bandwidth Memory (HBM) per concurrent request.

Under traditional contiguous memory allocation, servers pre-allocate static contiguous buffers sized to the maximum possible sequence length . This paradigm introduces three critical memory pathologies:

  1. Reserved Over-Allocation: Memory is locked for tokens even if requests terminate early (), leaving of allocated HBM idle.
  2. Internal & External Fragmentation: Varied sequence lengths fragment GPU memory, preventing new requests from finding contiguous buffers even when aggregate free memory is sufficient.
  3. Redundant Duplicate Allocations: Shared system prompts (e.g., in multi-turn chat or agentic workflows) duplicate identical KV cache pages for every active stream.

PagedAttention (Kwon et al., SOSP 2023) resolves these bottlenecks by translating operating system paged virtual memory principles to GPU KV cache management.


KV Cache Allocation Paradigms Comparison

Allocation ParadigmMemory ContiguityInternal FragmentationExternal FragmentationPrefix Sharing & CoWGPU Memory Utilization ()Serving Throughput
Static Pre-Allocation (Naive)Fully Contiguous ()Severe (Static Slabs)Impossible (Baseline)
Dynamic Ring BufferSegment ContiguousModerateModerate (Re-alloc Spikes)Complex / Limited
Chunked Slab AllocatorPower-of-Two SlabsUp to per SlabModerateNo
PagedAttention (vLLM)Non-Contiguous (Block-Paged) Tokens () (Fixed Block Size)Native Zero-Cost (CoW)
RadixAttention (SGLang)Radix-Tree Paged Blocks Tokens ()Tree-Structured (Radix)
MLA Paging (DeepSeek-V3)Compressed Latent Block-Paged TokensNative Latent CoW

Mathematical Foundations

flowchart TD subgraph LOGICAL_SPACE["Logical Sequence Space (Request R)"] L0["Logical Block 0 (Tokens 0..15)"] L1["Logical Block 1 (Tokens 16..31)"] L2["Logical Block 2 (Tokens 32..47)"] end subgraph BLOCK_TABLE["Block Table (Page Table)"] BT0["Logical 0 -> Physical Block #7"] BT1["Logical 1 -> Physical Block #23"] BT2["Logical 2 -> Physical Block #4"] end subgraph PHYSICAL_HBM["Physical GPU HBM (Fixed Block Size B=16)"] PB4["Physical Block #4 (Tokens 32..47)"] PB7["Physical Block #7 (Tokens 0..15)"] PB23["Physical Block #23 (Tokens 16..31)"] PBFREE["Free Blocks Pool [ #1, #2, #3, #5, ... ]"] end L0 --> BT0 --> PB7 L1 --> BT1 --> PB23 L2 --> BT2 --> PB4

Figure 1: PagedAttention virtual memory mapping translating contiguous logical token positions to non-contiguous physical GPU HBM blocks.

1. Paged Virtual Memory Mapping

Let denote the fixed block size (number of tokens per physical memory block).

A request sequence of length is partitioned into logical blocks:

Each request maintains a dynamic Block Table (page table) mapping logical block indices to physical block identifiers :

where .


2. Memory Utilization Bound and Fragmentation Analysis

Under naive contiguous allocation, memory efficiency for a request with actual length under maximum allocation evaluates to:

Under PagedAttention, internal fragmentation occurs strictly within the final allocated block:

The effective memory utilization is analytically bounded by:

For block size and sequence length :

Eliminating memory over-reservation increases concurrent batch capacity by , directly multiplying decoding throughput.


3. Non-Contiguous Online Softmax Attention Kernel

Standard self-attention computes over contiguous tensors. PagedAttention factors the reduction across non-contiguous physical blocks via tiled online softmax (FlashAttention-style):

Let represent the running maximum logit, represent the running normalization factor, and represent the unnormalized output accumulator. For each physical block :

Final normalized output evaluates to: .

flowchart TD Q["Query Vector q in R^(1 x d)"] --> LOOP["Iterate over Physical Blocks b in BlockTable"] subgraph TILE_KERNEL["Fused Paged Attention CUDA Kernel"] GATHER["Fetch K_B, V_B from Physical Block ID\n(Non-Contiguous HBM Access)"] SCORE["Block Scores: S_b = (q * K_B^T) / sqrt(d)"] ONLINE_MAX["Update Running Max: m_new = max(m_prev, max(S_b))"] EXP["Scale & Exponentiate: P_b = exp(S_b - m_new)"] UPDATE_L["Update Running Normalizer: l_new = l_prev * exp(m_prev - m_new) + Sum(P_b)"] UPDATE_O["Update Accumulator: o_new = o_prev * exp(m_prev - m_new) + P_b * V_B"] end LOOP --> GATHER --> SCORE --> ONLINE_MAX --> EXP --> UPDATE_L --> UPDATE_O UPDATE_O --> LOOP LOOP --> NORM["Final Division: Output = o_final / l_final"]

Figure 2: Online softmax reduction iterating across non-contiguous physical blocks without materializing intermediate global attention matrices.


4. Automatic Prefix Caching (APC) and Copy-on-Write (CoW)

When multiple requests share identical prompt prefixes, PagedAttention shares physical memory blocks using reference counting:

flowchart LR REQ1["Request 1 Block Table"] --> SHARED_PB["Physical Block #42\n(System Prompt Tokens 0..15)\nref_count = 2"] REQ2["Request 2 Block Table"] --> SHARED_PB REQ1 --> PB_GEN1["Physical Block #88\n(Req 1 Generated Tokens)\nref_count = 1"] REQ2 --> PB_GEN2["Physical Block #93\n(Req 2 Generated Tokens)\nref_count = 1"]

When an active stream modifies or appends to a shared block (), the allocator triggers Copy-on-Write (CoW):

  1. Allocate a fresh physical block from the free list.
  2. Copy the tokens from the shared block to the new block.
  3. Decrement the shared block’s reference count ().
  4. Update the requesting stream’s block table pointer to the newly allocated block.

Production PyTorch Implementation

Below is a complete, modular PyTorch implementation of the PagedBlockAllocator (with reference counting and Automatic Prefix Caching) and a vectorized PagedAttention decoding kernel with online softmax.

Step 1: Paged Block Allocator & Page Table Manager

from __future__ import annotations

import hashlib
import math
from collections import deque
import torch


class PagedBlockAllocator:
    """Manages physical GPU memory blocks and per-request block tables."""

    def __init__(self, num_blocks: int, block_size: int = 16) -> None:
        self.num_blocks = num_blocks
        self.block_size = block_size
        self.free_blocks: deque[int] = deque(range(num_blocks))
        
        # Mapping: request_id -> list of physical block IDs
        self.block_tables: dict[str, list[int]] = {}
        # Reference counting for prefix sharing / CoW: block_id -> count
        self.ref_counts: dict[int, int] = {i: 0 for i in range(num_blocks)}
        # Prefix cache: hash(tokens) -> physical_block_id
        self.prefix_cache: dict[str, int] = {}

    def _hash_tokens(self, tokens: list[int]) -> str:
        return hashlib.sha256(str(tokens).encode("utf-8")).hexdigest()

    def allocate_prompt(self, request_id: str, prompt_tokens: list[int]) -> list[int]:
        num_blocks_needed = math.ceil(len(prompt_tokens) / self.block_size)
        table: list[int] = []

        for i in range(num_blocks_needed):
            chunk = prompt_tokens[i * self.block_size : (i + 1) * self.block_size]
            chunk_hash = self._hash_tokens(chunk) if len(chunk) == self.block_size else None

            # Check Prefix Cache Hit
            if chunk_hash and chunk_hash in self.prefix_cache:
                phys_id = self.prefix_cache[chunk_hash]
                self.ref_counts[phys_id] += 1
            else:
                if not self.free_blocks:
                    raise MemoryError("GPU HBM Exhausted: No free blocks available for allocation.")
                phys_id = self.free_blocks.popleft()
                self.ref_counts[phys_id] = 1
                if chunk_hash:
                    self.prefix_cache[chunk_hash] = phys_id

            table.append(phys_id)

        self.block_tables[request_id] = table
        return table

    def append_token_slot(self, request_id: str, current_seq_len: int) -> int:
        """Allocates a new block if the token crosses the block boundary."""
        table = self.block_tables[request_id]
        if current_seq_len % self.block_size == 0:
            if not self.free_blocks:
                raise MemoryError("GPU HBM Exhausted during autoregressive decode step.")
            new_block = self.free_blocks.popleft()
            self.ref_counts[new_block] = 1
            table.append(new_block)
        return table[-1]

    def free_request(self, request_id: str) -> None:
        if request_id not in self.block_tables:
            return
        table = self.block_tables.pop(request_id)
        for phys_id in table:
            self.ref_counts[phys_id] -= 1
            if self.ref_counts[phys_id] == 0:
                self.free_blocks.append(phys_id)

Step 2: Vectorized PagedAttention Kernel (Online Softmax)

from __future__ import annotations

import math
import torch


def paged_attention_decode_step(
    q: torch.Tensor,                # [B, H_q, D] (Single decode query token)
    k_cache: torch.Tensor,          # [Total_Blocks, Block_Size, H_kv, D]
    v_cache: torch.Tensor,          # [Total_Blocks, Block_Size, H_kv, D]
    block_tables: torch.Tensor,     # [B, Max_Blocks_Per_Seq] (Physical block IDs)
    seq_lens: torch.Tensor,         # [B] (Current sequence length per request)
    block_size: int = 16,
) -> torch.Tensor:
    """Executes non-contiguous paged attention across requests via online softmax."""
    batch_size, num_heads_q, head_dim = q.shape
    num_heads_kv = k_cache.shape[2]
    num_queries_per_kv = num_heads_q // num_heads_kv # Grouped-Query Attention ratio
    scale = 1.0 / math.sqrt(head_dim)

    outputs = torch.zeros_like(q)

    for b in range(batch_size):
        cur_len = seq_lens[b].item()
        num_blocks = math.ceil(cur_len / block_size)
        q_b = q[b] # [H_q, D]

        # Initialize online softmax accumulators per head
        m_i = torch.full((num_heads_q, 1), float("-inf"), device=q.device, dtype=torch.float32)
        l_i = torch.zeros((num_heads_q, 1), device=q.device, dtype=torch.float32)
        acc_o = torch.zeros((num_heads_q, head_dim), device=q.device, dtype=torch.float32)

        for block_idx in range(num_blocks):
            phys_block_id = block_tables[b, block_idx].item()
            k_block = k_cache[phys_block_id] # [Block_Size, H_kv, D]
            v_block = v_cache[phys_block_id] # [Block_Size, H_kv, D]

            # Expand KV heads for GQA if necessary
            if num_queries_per_kv > 1:
                k_block = k_block.repeat_interleave(num_queries_per_kv, dim=1)
                v_block = v_block.repeat_interleave(num_queries_per_kv, dim=1)

            # Compute valid tokens in block
            valid_tokens = min(block_size, cur_len - block_idx * block_size)
            k_block = k_block[:valid_tokens] # [Valid, H_q, D]
            v_block = v_block[:valid_tokens] # [Valid, H_q, D]

            # Block attention scores: [H_q, Valid]
            scores = torch.einsum("hd,vhd->hv", q_b, k_block) * scale

            # Online Softmax Update
            m_block = torch.max(scores, dim=-1, keepdim=True).values
            m_new = torch.maximum(m_i, m_block)
            
            p_block = torch.exp(scores - m_new)
            alpha = torch.exp(m_i - m_new)

            l_i = l_i * alpha + torch.sum(p_block, dim=-1, keepdim=True)
            acc_o = acc_o * alpha + torch.einsum("hv,vhd->hd", p_block, v_block)
            m_i = m_new

        outputs[b] = (acc_o / (l_i + 1e-8)).to(q.dtype)

    return outputs

Step 3: Production Serving Pipeline Simulation

from __future__ import annotations

import torch
from paged_allocator import PagedBlockAllocator
from paged_attention_kernel import paged_attention_decode_step


def run_continuous_batching_simulation() -> None:
    # 1. Hardware & Memory Geometry
    num_physical_blocks = 128
    block_size = 16
    num_layers = 32
    num_heads_q = 32
    num_heads_kv = 8 # GQA
    head_dim = 128

    allocator = PagedBlockAllocator(num_blocks=num_physical_blocks, block_size=block_size)

    # Physical HBM KV Cache Pool
    k_cache = torch.randn(num_physical_blocks, block_size, num_heads_kv, head_dim, device="cuda", dtype=torch.bfloat16)
    v_cache = torch.randn(num_physical_blocks, block_size, num_heads_kv, head_dim, device="cuda", dtype=torch.bfloat16)

    # 2. Admit Prompt with Shared System Prefix (Automatic Prefix Caching)
    system_prefix = [101, 2054, 2003, 1037] * 4 # 16 tokens (1 Block)
    req1_prompt = system_prefix + [1001, 1002, 1003]
    req2_prompt = system_prefix + [2001, 2002, 2003]

    table1 = allocator.allocate_prompt("req_1", req1_prompt)
    table2 = allocator.allocate_prompt("req_2", req2_prompt)

    print(f"Req 1 Physical Block Allocation: {table1}")
    print(f"Req 2 Physical Block Allocation: {table2}")
    print(f"Shared Prefix Block Ref Count: {allocator.ref_counts[table1[0]]} (Expected: 2)")

    # 3. Simulate Iteration-Level Continuous Batching Step
    max_blocks = max(len(table1), len(table2))
    block_table_tensor = torch.zeros(2, max_blocks, dtype=torch.long, device="cuda")
    block_table_tensor[0, :len(table1)] = torch.tensor(table1)
    block_table_tensor[1, :len(table2)] = torch.tensor(table2)

    seq_lens = torch.tensor([len(req1_prompt), len(req2_prompt)], device="cuda")
    q = torch.randn(2, num_heads_q, head_dim, device="cuda", dtype=torch.bfloat16)

    out = paged_attention_decode_step(q, k_cache, v_cache, block_table_tensor, seq_lens, block_size)
    print(f"Decoded Output Tensor Shape: {out.shape} (Success)")


if __name__ == "__main__":
    if torch.cuda.is_available():
        run_continuous_batching_simulation()

Empirical Benchmark Evaluation

Serving throughput, memory efficiency, and time-to-first-token (TTFT) across high-concurrency benchmarks:

Model ArchitectureServing EngineMemory Utilization ()Max Concurrency (A100-80GB)Shared Prefix Throughput ()P50 Inter-Token Latency
LLaMA-2 (7B)HuggingFace (Static)
vLLM (PagedAttention) ()
LLaMA-2 (70B, TP=4)HuggingFace (Static)
vLLM (PagedAttention) ()
SGLang (RadixAttention) ()
DeepSeek-V3 (671B, TP=8)TensorRT-LLM (Static)
vLLM V1 (Paged + MLA) ()

Troubleshooting Common PagedAttention Faults

1. CUDA Out-of-Memory on Engine Warmup

  • Symptom: Server crashes during initialization before receiving traffic: torch.cuda.OutOfMemoryError.
  • Root Cause: gpu_memory_utilization set too high (), leaving insufficient headroom for PyTorch CUDA context, model weights, activations, and NCCL communication buffers.
  • Remedy: Set gpu_memory_utilization = 0.90 (or 0.85 for multi-GPU Tensor Parallelism).

2. Zero Prefix Cache Hit Rate in Production

  • Symptom: vllm:prefix_cache_hit_rate metric reports , and system prompt prefill latencies spike.
  • Root Cause: Non-deterministic prompt string formatting (e.g., dynamic timestamps, random whitespace, or varying BOS/EOS token additions) altering hash signatures.
  • Remedy: Ensure system prompt strings and token IDs are strictly identical across requests; place dynamic variables at the end of the prompt sequence.

3. CPU Swap Thrashing & High Inter-Token Latency Spikes

  • Symptom: Inter-token latency suddenly degrades from to under heavy concurrency.
  • Root Cause: Active working set exceeds physical GPU HBM, triggering synchronous PCIe block swaps between GPU and host CPU RAM.
  • Remedy: Increase max_num_seqs throttling limits, configure --swap-space 8 (GB), or deploy horizontal model replicas.

References

  1. Kwon, W., Li, Z., Zhuang, S., Sheng, Y., Zheng, L., Yu, C. H., Gonzalez, J. E., Zhang, H., & Stoica, I. (2023). Efficient Memory Management for Large Language Model Serving with PagedAttention. Proceedings of the 29th ACM Symposium on Operating Systems Principles (SOSP 2023).
  2. Dao, T. (2023). FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning. International Conference on Learning Representations (ICLR 2024).
  3. Zheng, L., et al. (2024). SGLang: Efficient Execution of Structured Language Model Programs. arXiv:2312.07104.
  4. DeepSeek-AI. (2024). DeepSeek-V3 Technical Report. arXiv:2412.19437.


Cite this Explanation

@article{ailinkdeeptech2025pagedattention,
  title={PagedAttention: Virtual-Memory KV Cache Management, Continuous Batching, and PyTorch Architecture},
  author={AILinkDeepTech},
  journal={AILinkDeepTech Algorithm Explanations},
  year={2025},
  url={https://ailinkdeeptech.com/research/pagedattention}
}

Related Explanations