PagedAttention是2023年由UC Berkeley团队在vLLM项目中提出的革命性注意力计算机制,其最大创新在于将操作系统的虚拟内存分页技术引入GPU显存管理,彻底改变了KV Cache的传统内存管理模式。这种转变不仅是技术上的改进,更是推理系统设计思维模式的重要革新。在PagedAttention出现之前,几乎所有主流推理框架(包括HuggingFace Transformers、FasterTransformer等)都采用连续内存预分配的方式来管理KV Cache,这种方式在面对变长序列和动态批处理时暴露出严重的效率问题。
传统连续分配的局限性:
PagedAttention的优势:
从"静态规划"到"动态适应":
PagedAttention的核心是借鉴操作系统的分页内存管理思想,将KV Cache划分为固定大小的"页面"(Page),每个页面包含固定数量token的Key和Value向量。这个架构包含多个关键组件,共同构成了一个高效的显存管理系统。
在vLLM的实现中,一个典型的页面大小为16个token。以LLaMA-7B模型为例,其隐藏层维度为4096,注意力头数为32,每个头的维度为128。每个token的KV Cache占用量为:
因此,一个页面(16个token)占用的显存为:16 × 16KB = 256KB。这种固定大小的页面设计使得内存管理变得简单而高效,类似于操作系统中4KB的内存页。
# 模拟页面结构设计 class Page: """KV Cache页面结构""" def __init__(self, page_size: int = 16, num_heads: int = 32, head_dim: int = 128): self.page_size = page_size # 每个页面包含的token数量 self.num_heads = num_heads # 注意力头数 self.head_dim = head_dim # 每个头的维度 self.dtype = 'float16' # 数据类型 # 计算页面大小 bytes_per_element = 2 # FP16 self.kv_size_per_token = num_heads * head_dim * bytes_per_element # Key或Value self.page_bytes = page_size * 2 * self.kv_size_per_token # Key + Value print(f"页面配置:") print(f" Token数量/页: {page_size}") print(f" 每Token KV大小: {2 * self.kv_size_per_token / 1024:.1f} KB") print(f" 每页显存占用: {self.page_bytes / 1024:.0f} KB") def calculate_gpu_capacity(self, gpu_memory_gb: int = 24): """计算GPU能容纳的最大页面数""" total_bytes = gpu_memory_gb * 1024 ** 3 # 假设KV Cache可用显存占总显存的80% kv_budget = total_bytes * 0.8 max_pages = int(kv_budget / self.page_bytes) max_tokens = max_pages * self.page_size print(f" GPU {gpu_memory_gb}GB 可用KV Cache: {kv_budget / 1024**3:.1f} GB") print(f" 最大页面数: {max_pages}") print(f" 最大Token数: {max_tokens}") return max_pages, max_tokens # 以LLaMA-7B为例 page = Page(page_size=16, num_heads=32, head_dim=128) page.calculate_gpu_capacity(gpu_memory_gb=24)
页表(Block Table)是PagedAttention架构中连接逻辑序列和物理页面的桥梁。每个请求都维护一个块表,记录其逻辑token位置到物理页面位置的映射关系。块表的设计直接借鉴了操作系统的页表机制:
# 模拟块表管理 class BlockTable: """PagedAttention块表管理器""" def __init__(self, total_physical_pages: int, page_size: int = 16): self.page_size = page_size self.total_pages = total_physical_pages # 物理页面位图:记录哪些页面已被分配 self.free_pages = set(range(total_physical_pages)) # 逻辑到物理的映射表:{seq_id: [physical_page_num, ...]} self.block_tables = {} # 物理到逻辑的引用计数:{physical_page_num: ref_count} self.ref_counts = {} def allocate_page(self, seq_id: int) -> int: """为指定序列分配一个物理页面""" if not self.free_pages: raise MemoryError("GPU显存已满,无可用页面") physical_page = self.free_pages.pop() if seq_id not in self.block_tables: self.block_tables[seq_id] = [] self.block_tables[seq_id].append(physical_page) self.ref_counts[physical_page] = 1 return physical_page def get_physical_page(self, seq_id: int, logical_page: int) -> int: """通过块表查找逻辑页面对应的物理页面""" if seq_id not in self.block_tables: raise KeyError(f"序列 {seq_id} 不存在") if logical_page >= len(self.block_tables[seq_id]): raise IndexError(f"逻辑页面 {logical_page} 超出范围") return self.block_tables[seq_id][logical_page] def free_pages_for_seq(self, seq_id: int): """释放指定序列的所有页面""" if seq_id not in self.block_tables: return for physical_page in self.block_tables[seq_id]: self.ref_counts[physical_page] -= 1 if self.ref_counts[physical_page] <= 0: self.free_pages.add(physical_page) del self.ref_counts[physical_page] del self.block_tables[seq_id] def share_pages(self, src_seq: int, dst_seq: int, num_shared_pages: int): """共享页面(Copy-on-Write机制的基础)""" if dst_seq not in self.block_tables: self.block_tables[dst_seq] = [] for i in range(num_shared_pages): phys_page = self.block_tables[src_seq][i] self.block_tables[dst_seq].append(phys_page) self.ref_counts[phys_page] += 1 # 演示 bt = BlockTable(total_physical_pages=1000, page_size=16) seq1 = bt.allocate_page(1) # 序列1分配第1页 bt.allocate_page(1) # 序列1分配第2页 bt.share_pages(1, 2, 1) # 序列2共享序列1的第1页 print(f"序列1块表: {bt.block_tables[1]}") print(f"序列2块表: {bt.block_tables[2]}") print(f"引用计数: {bt.ref_counts}") print(f"剩余空闲页面: {len(bt.free_pages)}")
页面映射是PagedAttention的核心机制,负责将逻辑token位置转换为物理显存地址。这一过程在GPU内核中高效执行,是实现非连续KV Cache访问的关键。
在实际的Attention计算中,当需要访问某个序列第t个token的Key向量时,地址转换过程如下:
这个过程在GPU的CUDA内核中通过一个简单的查表操作完成,开销极小。vLLM的实现中使用了一个扁平化的block_table数组,通过索引直接访问,避免了多级指针解引用的开销。
为了最小化非连续访问带来的性能损失,PagedAttention在多个层面进行了优化:
页面的生命周期管理是PagedAttention的重要功能,涵盖页面从创建到销毁的完整过程。理解页面生命周期对于系统调优和故障排查至关重要。
在vLLM的实现中,物理页面经历以下几种状态:
# 模拟页面状态机 class PageState: FREE = "free" ACTIVE = "active" SHARED = "shared" PENDING = "pending_free" class PageLifecycleManager: """页面生命周期管理器""" def __init__(self, total_pages: int): self.total_pages = total_pages self.page_states = {i: PageState.FREE for i in range(total_pages)} self.page_owners = {} # {page_id: set of seq_ids} def allocate(self, seq_id: int) -> int: """分配一个空闲页面""" for pid, state in self.page_states.items(): if state == PageState.FREE: self.page_states[pid] = PageState.ACTIVE self.page_owners[pid] = {seq_id} return pid raise MemoryError("所有页面已分配") def share(self, page_id: int, new_seq_id: int): """共享页面(Copy-on-Write)""" assert self.page_states[page_id] in (PageState.ACTIVE, PageState.SHARED) self.page_owners[page_id].add(new_seq_id) if len(self.page_owners[page_id]) > 1: self.page_states[page_id] = PageState.SHARED def release(self, seq_id: int, page_id: int): """释放序列对页面的引用""" self.page_owners[page_id].discard(seq_id) if not self.page_owners[page_id]: self.page_states[page_id] = PageState.FREE del self.page_owners[page_id] elif len(self.page_owners[page_id]) == 1: self.page_states[page_id] = PageState.ACTIVE def get_stats(self): """获取页面统计信息""" from collections import Counter counts = Counter(self.page_states.values()) return dict(counts) plm = PageLifecycleManager(100) plm.allocate(1) # 分配页面给序列1 plm.share(0, 2) # 序列2共享页面0 print(f"页面状态统计: {plm.get_stats()}")
当所有物理页面都被占用时,PagedAttention需要决定哪些页面可以被回收。vLLM采用了基于引用计数的策略:只有引用计数为1的页面才可以被回收,而被多个请求共享的页面(引用计数 > 1)必须保留。这种策略保证了正在使用的KV Cache不会被意外覆盖。
在实际的推理服务中,页面替换通常与请求调度紧密配合。当新的请求到达但显存不足时,系统会优先等待当前正在处理的请求完成并释放页面,而不是强制替换活跃页面的内容。这种"等待而非替换"的策略虽然可能增加新请求的延迟,但避免了KV Cache丢失导致的重新计算开销。
PagedAttention最具实用价值的创新之一就是通过Copy-on-Write(写时复制)机制实现KV Cache的共享。在实际的推理服务中,大量请求往往共享相同的system prompt或对话前缀,这些重复的KV Cache在传统架构中会被重复存储多次,造成巨大的显存浪费。
PagedAttention的CoW机制工作流程如下:
这种机制在多轮对话场景中尤为有效。例如,一个包含2000 token system prompt的对话服务,如果有100个并发请求共享同一个system prompt,传统架构需要2000 × 100 = 200,000个token的KV Cache空间,而PagedAttention只需要2000个token加上各请求差异部分的额外空间。
PagedAttention自发布以来,在学术界和工业界都得到了广泛验证。以下是基于实际测试数据的性能对比分析。
| 序列长度 | 连续分配利用率 | 固定分块利用率 | PagedAttention利用率 | 提升幅度 |
|---|---|---|---|---|
| 100 | 80% | 85% | 95% | +12% |
| 1000 | 60% | 75% | 92% | +17% |
| 5000 | 40% | 65% | 90% | +25% |
| 10000 | 30% | 55% | 88% | +33% |
PagedAttention的性能优势在以下场景中尤为显著:
通过本节的学习,我们深入理解了PagedAttention的核心原理和关键技术。从非连续内存分配的范式转变,到页面级管理架构的详细设计,从地址转换机制到Copy-on-Write共享策略,我们建立了对PagedAttention的完整认知。这些核心技术为现代大模型推理系统的显存管理奠定了重要基础,使得GPU资源利用效率实现了质的飞跃。在接下来的小节中,我们将进一步探讨动态页表设计和页面级显存管理的具体实现细节。