3.1 PagedAttention的核心原理


3.1 PagedAttention的核心原理

非连续内存分配的革命性突破

3.1.1 从连续到非连续的范式转变

PagedAttention是2023年由UC Berkeley团队在vLLM项目中提出的革命性注意力计算机制,其最大创新在于将操作系统的虚拟内存分页技术引入GPU显存管理,彻底改变了KV Cache的传统内存管理模式。这种转变不仅是技术上的改进,更是推理系统设计思维模式的重要革新。在PagedAttention出现之前,几乎所有主流推理框架(包括HuggingFace Transformers、FasterTransformer等)都采用连续内存预分配的方式来管理KV Cache,这种方式在面对变长序列和动态批处理时暴露出严重的效率问题。

传统与PagedAttention的对比

传统连续分配的局限性:

  • 固定大小限制:系统必须为每个序列预分配最大可能长度(如2048个token)的连续KV Cache空间,即使实际生成长度远小于此值
  • 内存浪费严重:以一个7B参数模型为例,每个token的KV Cache约占1MB,预分配2048个token就需要2GB显存,而平均生成长度可能只有512个token,浪费率高达75%
  • 碎片化问题:在动态批处理场景下,不同请求到达和结束的时间不同,频繁的分配和释放产生严重的外部碎片,导致大量显存虽然空闲但无法被利用
  • 扩展性差:无法灵活处理不同长度的序列,也无法在运行时根据实际需求动态调整内存分配
  • 批处理效率低:由于每个请求需要固定大小的预留空间,GPU上能同时处理的请求数量远低于理论最大值

PagedAttention的优势:

  • 按需分配:只分配实际生成过程中需要的内存页,token逐个生成时逐页分配,不浪费任何显存
  • 非连续存储:支持将KV Cache分散存储在GPU显存的任意位置,通过页表映射实现逻辑连续性
  • 高内存利用率:通过共享相同前缀的KV Cache(如system prompt),几乎消除了内存浪费,实际测试中显存利用率可达95%以上
  • 灵活扩展:理论上可以处理任意长度的序列,只要GPU显存中还有空闲页面即可继续分配
  • 高吞吐量:在不增加任何额外计算开销的前提下,将GPU的批处理吞吐量提升2-4倍

技术哲学的转变

从"静态规划"到"动态适应":

  • 传统思维:预先规划所有资源,追求确定性和可预测性,以最坏情况为基准进行资源预留
  • PagedAttention思维:动态适应需求变化,追求最优效率,以实际使用量为基准进行按需分配
  • 核心价值:在不确定性和变化中寻找最优解,将操作系统领域经过数十年验证的分页技术成功迁移到GPU推理场景
graph TB subgraph 传统连续分配 A1[请求1: 预分配2048 token] --> A2[实际使用512 token] A3[请求2: 预分配2048 token] --> A4[实际使用1024 token] A5[大量显存浪费: 75%+ 闲置] end subgraph PagedAttention分页分配 B1[请求1: 按需分配8页] --> B2[请求1: 扩展到16页] B3[请求2: 按需分配16页] --> B4[请求3: 共享请求1前8页] B5[显存利用率: 95%+ 几乎无浪费] end A5 -.->|技术演进| B5

3.1.2 页面级内存管理架构

PagedAttention的核心是借鉴操作系统的分页内存管理思想,将KV Cache划分为固定大小的"页面"(Page),每个页面包含固定数量token的Key和Value向量。这个架构包含多个关键组件,共同构成了一个高效的显存管理系统。

页面结构设计

在vLLM的实现中,一个典型的页面大小为16个token。以LLaMA-7B模型为例,其隐藏层维度为4096,注意力头数为32,每个头的维度为128。每个token的KV Cache占用量为:

  • Key Cache: 32头 × 128维 × 2字节(FP16)= 8192字节 = 8KB
  • Value Cache: 同样为8KB
  • 每个token的KV Cache总计: 16KB

因此,一个页面(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位置到物理页面位置的映射关系。块表的设计直接借鉴了操作系统的页表机制:

  • 逻辑页面号:由token位置除以页面大小得到,表示token在逻辑序列中的页面编号
  • 物理页面号:表示该逻辑页面实际存储在GPU显存中的哪个物理页面上
  • 有效位:标记该页表项是否有效(页面是否已分配)
  • 引用计数:记录有多少个请求共享该物理页面(用于Copy-on-Write机制)
# 模拟块表管理 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)}")

3.1.3 页面映射与地址转换

页面映射是PagedAttention的核心机制,负责将逻辑token位置转换为物理显存地址。这一过程在GPU内核中高效执行,是实现非连续KV Cache访问的关键。

逻辑地址到物理地址的转换

在实际的Attention计算中,当需要访问某个序列第t个token的Key向量时,地址转换过程如下:

  1. 计算逻辑页面号:logical_page = t // page_size
  2. 计算页内偏移:page_offset = t % page_size
  3. 查找块表得到物理页面号:physical_page = block_table[seq_id][logical_page]
  4. 计算物理地址:physical_addr = physical_page * page_size * kv_dim + page_offset * kv_dim

这个过程在GPU的CUDA内核中通过一个简单的查表操作完成,开销极小。vLLM的实现中使用了一个扁平化的block_table数组,通过索引直接访问,避免了多级指针解引用的开销。

页面访问优化

为了最小化非连续访问带来的性能损失,PagedAttention在多个层面进行了优化:

  • 合并kernel启动:将多个序列的注意力计算合并到一个CUDA kernel中执行,减少kernel启动开销
  • 分块注意力计算:按照物理页面的连续性重新组织注意力计算,最大化利用GPU的合并内存访问能力
  • Warp级别的页面预取:在CUDA warp执行前,通过预取指令提前加载即将访问的页面数据到L1/L2缓存中
  • KV Cache重排序:在注意力计算时,根据物理页面的实际布局重新排列计算顺序,提高内存访问的局部性
sequenceDiagram participant A as Attention Kernel participant BT as Block Table participant PM as Physical Memory A->>A: 计算逻辑页面号 logical_page = token_idx // 16 A->>BT: 查询 block_table[seq_id][logical_page] BT-->>A: 返回物理页面号 physical_page = 42 A->>A: 计算页内偏移 offset = token_idx % 16 A->>PM: 读取 physical_page * 16 * kv_dim + offset * kv_dim PM-->>A: 返回Key/Value向量 A->>A: 执行注意力权重计算

3.1.4 页面生命周期管理

页面的生命周期管理是PagedAttention的重要功能,涵盖页面从创建到销毁的完整过程。理解页面生命周期对于系统调优和故障排查至关重要。

页面状态管理

在vLLM的实现中,物理页面经历以下几种状态:

  • 空闲(Free):页面未被分配,属于GPU的空闲页面池,可随时分配给任何请求
  • 活跃(Active):页面正在被某个或多个请求使用,存储着有效的KV Cache数据
  • 共享(Shared):页面被多个请求通过Copy-on-Write机制共享,引用计数大于1
  • 待回收(Pending Free):请求已完成,页面即将被释放,等待引用计数归零
# 模拟页面状态机 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丢失导致的重新计算开销。

3.1.5 Copy-on-Write与KV Cache共享

PagedAttention最具实用价值的创新之一就是通过Copy-on-Write(写时复制)机制实现KV Cache的共享。在实际的推理服务中,大量请求往往共享相同的system prompt或对话前缀,这些重复的KV Cache在传统架构中会被重复存储多次,造成巨大的显存浪费。

CoW机制的实现

PagedAttention的CoW机制工作流程如下:

  1. 当一个新请求到来时,系统检查其前缀是否与已有请求匹配
  2. 如果匹配,新请求的块表直接指向已有的物理页面,引用计数加1
  3. 当新请求需要修改某个共享页面时(例如生成新token导致需要新的KV Cache),系统首先复制该页面内容到一个新的物理页面,然后更新块表指向新页面
  4. 原始页面的引用计数减1,新页面归当前请求独占

这种机制在多轮对话场景中尤为有效。例如,一个包含2000 token system prompt的对话服务,如果有100个并发请求共享同一个system prompt,传统架构需要2000 × 100 = 200,000个token的KV Cache空间,而PagedAttention只需要2000个token加上各请求差异部分的额外空间。

graph LR subgraph 共享前缀 P1[System Prompt Page 0] --> P2[Page 1] P2 --> P3[Page 2] end subgraph 请求A A1[引用 P1] --> A2[引用 P2] --> A3[引用 P3] --> A4[私有 Page A] end subgraph 请求B B1[引用 P1] --> B2[引用 P2] --> B3[引用 P3] --> B4[私有 Page B] end subgraph 请求C C1[引用 P1] --> C2[引用 P2] --> C3[引用 P3] --> C4[私有 Page C] end P1 -.->|ref_count=3| A1 P1 -.->|ref_count=3| B1 P1 -.->|ref_count=3| C1

3.1.6 性能分析

PagedAttention自发布以来,在学术界和工业界都得到了广泛验证。以下是基于实际测试数据的性能对比分析。

内存使用效率

序列长度 连续分配利用率 固定分块利用率 PagedAttention利用率 提升幅度
100 80% 85% 95% +12%
1000 60% 75% 92% +17%
5000 40% 65% 90% +25%
10000 30% 55% 88% +33%

处理速度

  • 连续分配:200ms(长序列时因内存碎片导致延迟波动大,峰值可达1000ms+)
  • 固定分块:150ms(相对稳定,但仍受分块大小影响)
  • PagedAttention:80ms(保持稳定,几乎不受序列长度影响)

并发性能

  • 连续分配:最多支持约10个并发请求(受限于预分配的固定空间)
  • 固定分块:最多支持约20个并发请求
  • PagedAttention:最多支持约50个并发请求,吞吐量提升2-4倍

关键性能洞察

PagedAttention的性能优势在以下场景中尤为显著:

  1. 高并发短文本场景:如聊天机器人服务,大量短请求共享system prompt,CoW机制使得显存利用率接近100%
  2. 变长序列场景:如代码补全、文档摘要等任务,输入和输出长度差异极大,按需分配避免了固定预留的浪费
  3. 多轮对话场景:对话历史作为前缀被多个请求共享,显著减少重复计算和存储开销

通过本节的学习,我们深入理解了PagedAttention的核心原理和关键技术。从非连续内存分配的范式转变,到页面级管理架构的详细设计,从地址转换机制到Copy-on-Write共享策略,我们建立了对PagedAttention的完整认知。这些核心技术为现代大模型推理系统的显存管理奠定了重要基础,使得GPU资源利用效率实现了质的飞跃。在接下来的小节中,我们将进一步探讨动态页表设计和页面级显存管理的具体实现细节。


作者与出处
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 内存溢出警告的小龙虾 转发
评论区 (0)
U