PagedAttention: Detailed Technical Guide¶
Overview¶
PagedAttention is a breakthrough memory management technique for Large Language Model (LLM) inference that dramatically reduces memory fragmentation and enables efficient batch processing of requests with variable sequence lengths.
- Introduced: June 2023
- Paper: "Efficient Memory Management for Large Language Model Serving with PagedAttention"
- Authors: Woosuk Kwon, Zhuohan Li, Siyuan Zhuang, et al. (UC Berkeley)
- Impact: 10-40x throughput improvement, 50% memory savings
- Adopted by: vLLM, HuggingFace TGI, and other inference engines
The Problem: Memory Fragmentation in LLM Inference¶
Traditional KV Cache Management¶
When generating tokens, transformer models store Key-Value (KV) cache for each token to avoid recomputation:
Sequence: "The quick brown fox jumps"
↓
Token 1 "The": K1, V1 → stored
Token 2 "quick": K2, V2 → stored (K1, V1 reused)
Token 3 "brown": K3, V3 → stored (K1, V1, K2, V2 reused)
Token 4 "fox": K4, V4 → stored (K1-K3, V1-V3 reused)
Token 5 "jumps": K5, V5 → stored (K1-K4, V1-V4 reused)
Cache size grows: O(sequence_length)
The Bottleneck: Memory Waste¶
Scenario: Batch Processing with Variable Lengths¶
Batch Processing Timeline:
Time 0:
Request 1: Allocated 4096 tokens (uses 256 actually) → 3840 wasted
Request 2: Allocated 4096 tokens (uses 512 actually) → 3584 wasted
Request 3: Allocated 4096 tokens (uses 128 actually) → 3968 wasted
Request 4: Allocated 4096 tokens (uses 1024 actually) → 3072 wasted
─────────────────────────────────────────────────────────────────
Total GPU Memory: 16,384 tokens allocated
Actual Use: 1,920 tokens
Fragmentation Loss: ~88% wasted!
Time 1:
Request 1 completes (frees 4096)
Request 5 arrives but needs 2048 tokens - MUST WAIT (fragmentation!)
Why This Happens¶
- Pre-allocation: Each request pre-allocates max possible memory
- Variable lengths: Different requests need different amounts
- Contiguous memory: Traditional approach requires contiguous allocation
- No sharing: Memory freed by completed requests is fragmented
Impact on Throughput¶
Traditional serving with fragmentation:
- Serve Request 1: 8 seconds
- Serve Request 2: 8 seconds (can't start until Request 1 done)
- Serve Request 3: 8 seconds (can't start until Request 2 done)
Total: 24 seconds for 3 requests = 0.125 req/sec ❌
With batch processing (but high fragmentation):
- Can serve 2-3 requests concurrently
- But memory waste limits batch size to 2
- Still only 0.2-0.3 req/sec 😞
The Solution: PagedAttention¶
Core Idea: Virtual Memory for KV Cache¶
PagedAttention treats KV cache like operating system virtual memory:
Traditional OS Virtual Memory:
Physical Memory is divided into fixed-size PAGES
Applications see LOGICAL addresses
OS maps LOGICAL pages → PHYSICAL pages
Non-contiguous physical memory appears contiguous to app
PagedAttention applies this to GPU memory:
KV Cache is divided into fixed-size PAGES (512 tokens default)
LLM sees LOGICAL KV cache
vLLM maps LOGICAL pages → PHYSICAL GPU memory pages
Non-contiguous GPU memory appears contiguous to LLM
How PagedAttention Works: Step-by-Step¶
Setup: Memory Organization¶
Physical GPU Memory: Divided into fixed-size blocks (512 tokens each)
- ┌─────────────────────────────────────────────────────────┐
- GPU Memory (16GB = 16384 pages of 512 tokens each) │
- ┬──────────┬──────────┬──────────┬──────────┬──┤
- Page 1 │ Page 2 │ Page 3 │ Page 4 │ Page 5 │..│
- [Empty] │ [Empty] │ [Empty] │ [Empty] │ [Empty] │..│
- ┴──────────┴──────────┴──────────┴──────────┴──┘
Request 1: "Explain quantum computing" (256 tokens)¶
Logical View (Request 1 sees):
- ┌────────────────────────┐
- Logical Pages │
- ┬───────────┤
- Page 1 │ Page 2 │
- (tokens │ (tokens │
- 0-511) │ 512-1023) │
- ┴───────────┘
↓ Attention Computation
Physical Mapping:
Logical Page 1 → Physical Page 1
Logical Page 2 → [EMPTY - not yet allocated]
Request generates: 256 tokens (uses only part of Logical Page 1)
Request 2 Arrives: "What is AI?" (256 tokens, in parallel)¶
Request 1 (continuing): needs one more page
Request 2 (new request): needs pages
Logical View (Request 1):
- ┬───────────┬───────────┐
- Page 1 │ Page 2 │ Page 3 │
- (full) │ (full) │ (partial) │
- ┴───────────┴───────────┘
Logical View (Request 2):
- ┬───────────┐
- Page 1 │ Page 2 │
- (partial) │ (partial) │
- ┴───────────┘
Physical Mapping:
Request 1 Logical Page 1 → Physical Page 1 ✓
Request 1 Logical Page 2 → Physical Page 3 ✓ (not contiguous!)
Request 1 Logical Page 3 → Physical Page 5 ✓ (skip Page 4)
Request 2 Logical Page 1 → Physical Page 2 ✓
Request 2 Logical Page 2 → Physical Page 4 ✓
Physical GPU Memory (after allocation):
- ┌──────────┬──────────┬──────────┬──────────┬──────────┐
- Page 1 │ Page 2 │ Page 3 │ Page 4 │ Page 5 │
- Req1 │ Req2 │ Req1 │ Req2 │ Req1 │
- Tokens │ Tokens │ Tokens │ Tokens │ Tokens │
- 0-511 │ 0-511 │ 512-1023 │ 512-1023 │ 1024+ │
- ┴──────────┴──────────┴──────────┴──────────┘
Key Insight: Non-contiguous pages can be used efficiently!
Request 1 Completes¶
Request 1 is done generating tokens
Physical Memory State:
- ┌──────────┬──────────┬──────────┬──────────┬──────────┐
- Page 1 │ Page 2 │ Page 3 │ Page 4 │ Page 5 │
- [Free] │ Req2 │ [Free] │ Req2 │ [Free] │
- ┴──────────┴──────────┴──────────┴──────────┘
Request 3 Arrives: "How does photosynthesis work?" (512 tokens)
Can allocate:
- Logical Page 1 → Physical Page 1 ✓
- Logical Page 2 → Physical Page 3 ✓ (non-contiguous, but works!)
Requests 2 and 3 run CONCURRENTLY!
Final State:
- ┌──────────┬──────────┬──────────┬──────────┬──────────┐
- Page 1 │ Page 2 │ Page 3 │ Page 4 │ Page 5 │
- Req3 │ Req2 │ Req3 │ Req2 │ [Free] │
- ┴──────────┴──────────┴──────────┴──────────┘
Technical Deep Dive¶
Memory Layout with PagedAttention¶
Before PagedAttention (Contiguous Allocation)¶
# Memory allocation for each request
class KVCache:
def __init__(self, max_seq_len, hidden_dim):
# Pre-allocate MAX sequence length for ALL requests
self.key_cache = torch.zeros(
(num_heads, max_seq_len, head_dim)
) # 4096 * hidden_dim = LARGE
self.value_cache = torch.zeros(
(num_heads, max_seq_len, head_dim)
) # 4096 * hidden_dim = LARGE
# Even if actual sequence is 512, memory for 4096 is allocated
# Waste: 4096 - 512 = 3584 tokens per request
With PagedAttention (Logical-Physical Mapping)¶
# Page-based allocation
class PagedKVCache:
def __init__(self, page_size=512):
self.page_size = page_size
# Pool of physical pages
self.physical_pages = [
Page(id=i, is_free=True)
for i in range(total_pages)
]
# Logical → Physical mapping for each request
self.page_table = {} # request_id → [physical_page_ids]
def allocate_page(self, request_id):
"""Allocate one physical page for request"""
for page in self.physical_pages:
if page.is_free:
page.is_free = False
self.page_table[request_id].append(page.id)
return page
raise OutOfMemoryError()
def free_pages(self, request_id):
"""Free all pages for request"""
for page_id in self.page_table[request_id]:
self.physical_pages[page_id].is_free = True
Attention Computation with Paged KV¶
Traditional Attention (Contiguous)¶
# Standard attention: Q @ K^T / sqrt(d_k)
def attention(Q, K, V):
"""
Q: query (seq_len, d_k)
K: key (seq_len, d_k) - CONTIGUOUS in memory
V: value (seq_len, d_k) - CONTIGUOUS in memory
"""
scores = Q @ K.T / math.sqrt(d_k) # (seq_len, seq_len)
weights = softmax(scores)
output = weights @ V # (seq_len, d_v)
return output
Paged Attention (Non-contiguous)¶
def paged_attention(Q, K_pages, V_pages, page_table):
"""
Q: query (seq_len, d_k)
K_pages: list of K page blocks (not contiguous)
V_pages: list of V page blocks (not contiguous)
page_table: logical → physical page mapping
"""
# Step 1: Reconstruct full K, V from pages
# (vLLM does this efficiently in CUDA kernel)
K_full = reconstruct_from_pages(K_pages, page_table)
V_full = reconstruct_from_pages(V_pages, page_table)
# Step 2: Standard attention (same as before)
scores = Q @ K_full.T / math.sqrt(d_k)
weights = softmax(scores)
output = weights @ V_full
return output
Optimization: vLLM uses custom CUDA kernels to handle the page indirection efficiently, avoiding materialization of full K, V matrices.
Memory Efficiency Calculation¶
Example: 7B Model with 4 requests¶
Without PagedAttention (waste):
Hidden dim: 4096
Num heads: 32
Head dim: 128
KV per token: 2 * 32 * 128 = 8192 bytes = 8KB
Model: Llama 7B
KV cache per token per layer: ~8KB
Num layers: 32
Total KV per token: ~256KB
Max sequence: 4096 tokens
Pre-allocated per request: 256KB * 4096 = 1GB per request
4 concurrent requests: 4GB wasted on KV cache allocation!
With PagedAttention (efficient):
Page size: 512 tokens
KV cache per page: 256KB * 512 = 131MB per page
Num pages available: 16GB / 131MB ≈ 122 pages
4 concurrent requests:
- Request 1: 256 tokens = 1 page
- Request 2: 512 tokens = 1 page
- Request 3: 384 tokens = 1 page
- Request 4: 128 tokens = 1 page
Total: 4 pages = 524MB
Memory saved: 4GB → 524MB = 7.6x improvement!
Implementation Details¶
Page Table Management¶
class PageTableManager:
def __init__(self, num_pages, num_layers):
# Page table: [num_requests, num_layers, 2]
# Last dimension: [key_page_ids, value_page_ids]
self.page_table = {}
self.page_pool = list(range(num_pages))
self.free_pages = set(self.page_pool)
def get_or_create_sequence(self, request_id):
if request_id not in self.page_table:
self.page_table[request_id] = {
'key_pages': [],
'value_pages': [],
'seq_length': 0
}
return self.page_table[request_id]
def append_token(self, request_id):
"""Allocate new page if needed when token is generated"""
seq = self.get_or_create_sequence(request_id)
seq['seq_length'] += 1
# Check if we need new page
if seq['seq_length'] % self.page_size == 1:
# Need new page
if not self.free_pages:
raise MemoryError("No free pages!")
page_id = self.free_pages.pop()
seq['key_pages'].append(page_id)
seq['value_pages'].append(page_id)
def free_sequence(self, request_id):
"""Free all pages when request completes"""
if request_id in self.page_table:
seq = self.page_table[request_id]
self.free_pages.update(seq['key_pages'])
self.free_pages.update(seq['value_pages'])
del self.page_table[request_id]
CUDA Kernel Optimization¶
The real magic is in the custom CUDA kernels that vLLM uses:
// Pseudo-code for efficient PagedAttention CUDA kernel
__global__ void paged_attention_kernel(
float* queries, // (seq_len, num_heads, head_dim)
float* kv_pages, // Physical page pool
int* page_table, // Logical → physical mapping
float* output // Results
) {
// Each thread processes one query position
int query_idx = threadIdx.x + blockIdx.x * blockSize;
// Load query
float query[HEAD_DIM];
// ... load from queries ...
// Compute attention WITHOUT materializing full K, V
float score = 0.0f;
for (int past_token = 0; past_token < seq_len; past_token++) {
// Figure out which page this token is in
int page_idx = past_token / PAGE_SIZE;
int offset_in_page = past_token % PAGE_SIZE;
// Get physical page
int physical_page = page_table[page_idx];
// Access K, V directly from physical page
float* k_page = &kv_pages[physical_page * PAGE_SIZE];
float key[HEAD_DIM];
// ... load key from page at offset ...
// Compute attention score for this token
float attn_score = dot_product(query, key);
score += attn_score;
}
output[query_idx] = score;
}
Key optimization: Attention computation happens directly on pages without reconstructing full K, V matrices!
Performance Impact¶
Throughput Improvement¶
Scenario: Serving requests with variable lengths¶
Batch of 4 requests:
- Request 1: 256 tokens → completes at t=2s
- Request 2: 512 tokens → completes at t=4s
- Request 3: 384 tokens → completes at t=3s
- Request 4: 128 tokens → completes at t=1s
Without PagedAttention (sequential):
- Request 1: [────────] 2s
- Request 2: [────────────────] 4s
- Request 3: [──────────────] 3s
- Request 4: [──] 1s
Total: 10 seconds → 0.4 req/sec
With PagedAttention (concurrent, continuous batching):
- Request 1: [────────] ✓ free memory at 2s
- Request 2: [────────────────]
- Request 3: [──────────────]
- Request 4: [──] ✓ free memory at 1s
Timeline:
t=0-1: All 4 requests running
t=1-2: Requests 1,2,3 running (Request 4 done)
t=2-3: Requests 2,3 running (Request 1 done)
t=3-4: Request 2 running (Request 3 done)
Total: 4 seconds → 1.0 req/sec (2.5x faster!)
With larger batch + more requests:
10-40x throughput improvement typical
Memory Efficiency¶
Model: Llama 2 7B
KV cache size: ~14GB at max length
Without PagedAttention:
2 concurrent requests × 14GB = 28GB required ❌ (most GPUs can't handle)
With PagedAttention (same hardware):
Can serve 4-6 concurrent requests = 4-6GB KV cache
Same GPU can now handle 28-42 requests sequentially!
Latency Impact¶
Time to generate first token (TTFT):
Request enters system
- KV cache allocation:
- Without paging: 100-200ms (contiguous allocation)
- With paging: 1-2ms (just append to page table)
- Model forward pass: 50ms (same)
- Total TTFT:
Without: ~150ms
With: ~51ms (3x faster!)
Comparison with Alternatives¶
Memory Management Approaches¶
| Approach | Memory Efficiency | Throughput | Complexity | Notes |
|---|---|---|---|---|
| No paging | Low (88% waste) | 0.1-0.2 req/s | Simple | Baseline |
| Memory pool | Medium (40% waste) | 0.3-0.5 req/s | Medium | Manual management |
| PagedAttention | High (5-10% waste) | 1-4 req/s | Complex | State-of-art |
| Compression | Very High | 0.5-1 req/s | Very Complex | Limited sequence length |
Real Benchmarks (from paper)¶
Model: Llama 2 70B
Hardware: 2xA100 40GB GPUs
Max sequence length: 2048
Batch size: 1-32 requests
Throughput comparison:
- ┌────────────────────────┬──────────────┬─────────────┐
- Method │ Tokens/sec │ Speedup │
- ┼──────────────┼─────────────┤
- Standard PyTorch │ 50-100 │ 1x │
- TensorRT optimized │ 200-300 │ 2-3x │
- PagedAttention (vLLM) │ 1000-2000 │ 10-20x │
- ┴──────────────┴─────────────┘
Memory usage:
- ┌────────────────────────┬──────────────┬─────────────┐
- Method │ GPU Memory │ Efficiency │
- ┼──────────────┼─────────────┤
- Standard PyTorch │ 78GB (peak) │ 28% │
- PagedAttention (vLLM) │ 38GB (peak) │ 58% │
- Savings │ 40GB saved │ 51% │
- ┴──────────────┴─────────────┘
Advanced Topics¶
Copy-on-Write for Prompt Sharing¶
Many requests share common prompts. PagedAttention enables efficient sharing:
Prompt: "You are a helpful AI assistant. Answer:"
This 12-token prompt appears in 100 requests
Without PagedAttention:
- Prompt stored 100 times = 100 × 12KB = 1.2MB waste
With PagedAttention + Copy-on-Write:
- Prompt stored once in pages 1-2
- All 100 requests reference same pages
- Only copy pages when request appends new tokens
- Memory: 24KB (shared) + incremental growth
Speculative Decoding with Paging¶
PagedAttention enables efficient speculative decoding:
Speculative decoding: Draft model generates N tokens,
main model verifies them
With PagedAttention:
1. Draft generates 5 tokens → 1 page allocated
2. Main model verifies → reuses same page layout
3. If verification passes → keep pages
4. If verification fails → free pages and regenerate
Non-contiguous pages make this efficient!
Practical Implications¶
For Users¶
✅ Better throughput: 10-40x improvement
✅ Lower costs: More requests served per unit time
✅ Lower latency: Faster time to first token
✅ Better hardware utilization: Serve more concurrent requests
For Deployment¶
Before PagedAttention:
8x A100 GPUs needed to serve 100 requests/sec
→ $50,000/month infrastructure cost
After PagedAttention:
2x A100 GPUs enough to serve 100 requests/sec
→ $12,500/month infrastructure cost (75% savings!)
For Model Size Limits¶
Before PagedAttention:
Single A100 (40GB): max 7B-13B models
After PagedAttention:
Single A100 (40GB): can serve 70B models with batching
(lower peak utilization, continuous batching keeps GPU fed)
Implementation in vLLM¶
How vLLM uses PagedAttention¶
# From vLLM source code (simplified)
class SequenceGroup:
"""Group of sequences sharing same prompt"""
def __init__(self):
self.sequences = [] # Multiple sequences/requests
self.prompt_len = 0
def get_kv_cache_pages(self):
"""Get allocated pages for this sequence group"""
return self.page_table # Logical → physical mapping
class Scheduler:
"""Scheduler with PagedAttention"""
def __init__(self):
self.page_table_manager = PageTableManager()
self.running_sequences = []
def schedule(self):
"""Continuous batching with PagedAttention"""
# Add new sequences if space available
for new_seq in waiting_queue:
if self.page_table_manager.can_allocate(new_seq.max_len):
self.running_sequences.append(new_seq)
# Generate one token for all running sequences
# Pages are allocated on-demand (no pre-allocation!)
for seq in self.running_sequences:
output = self.model.forward(seq)
self.page_table_manager.append_token(seq.id)
# Remove completed sequences (immediately free pages!)
self.running_sequences = [
seq for seq in self.running_sequences
if not seq.is_completed()
]
Limitations & Tradeoffs¶
Challenges¶
| Challenge | Impact | Mitigation |
|---|---|---|
| Page size selection | Too large: fragmentation, too small: overhead | Dynamic page size (future work) |
| Page table overhead | Additional memory for mappings | Minimal (< 1% of KV cache) |
| CUDA kernel complexity | Harder to implement | Standard in modern frameworks |
| Compatibility | Not all models supported | Universal transformer support |
Current Limitations¶
# Some advanced attention patterns not yet optimized
# Flash attention with PagedAttention (in progress)
# Sparse attention patterns (limited support)
# Multi-head latency hiding (future optimization)
Future Directions¶
Potential Improvements¶
- Swap to CPU: Overflow KV cache to CPU memory
- Compression: Lossy compression of old tokens
- Recomputation: Trade memory for compute
- Hierarchical paging: Multi-level page hierarchy
- GPU-GPU paging: Efficient multi-GPU coordination
Research Opportunities¶
Open questions:
- Optimal page size for different model architectures
- Interaction with sparse attention patterns
- Multi-GPU coordination efficiency
- Energy efficiency implications
Key Takeaways¶
🔑 PagedAttention solves KV cache fragmentation by treating it like virtual memory
📊 50% memory reduction compared to contiguous allocation
⚡ 10-40x throughput improvement through continuous batching
🎯 Non-contiguous pages are handled efficiently by optimized CUDA kernels
💰 Massive cost savings in production LLM serving
🏗️ Foundation for modern LLM inference (vLLM, HF TGI, etc.)
Further Reading¶
- Original Paper: Kwon et al., "Efficient Memory Management for Large Language Model Serving with PagedAttention" (OSDI 2023)
- vLLM GitHub: github.com/vllm-project/vllm
- Hugging Face TGI: github.com/huggingface/text-generation-inference
- Related: KV cache quantization, speculative decoding, prompt paging