尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
LLM 推理中的 PageAttention 实现原理:虚拟内存管理在 KV Cache 中的创新应用
LLM 推理中的 PageAttention 实现原理虚拟内存管理在 KV Cache 中的创新应用一、KV Cache 的内碎片问题固定分配如何浪费 40% 显存传统 LLM 推理为每个请求预分配一块连续的 KV Cache 空间大小 max_seq_len × num_layers × 2 × head_dim × num_heads × dtype_size。以 Llama-2-70B 为例max_seq_len4096 时的单请求 KV Cache 为 2.5GB。当实际生成长度仅 128 tokens 时预分配的 96.9% 空间闲置。更严重的是连续分配导致严重的外碎片。多个请求的 KV Cache 在显存中交错分配短请求完成后释放的空间被长请求的活跃 KV Cache 块隔开无法合并成大块。最终显存利用率仅 60%但 GPU OOM 频率上升——不是因为总量不够而是无法分配连续空间。此外连续分配策略在动态批处理Dynamic Batching场景下表现极差。当 Batch 中某个请求提前结束时其释放的 KV Cache 空间若小于其他请求所需就会成为无法利用的空洞。PageAttention 的非连续分配天然解决了这个问题——释放的 Block 可以立即分配给任何其他请求无论其大小。PageAttentionvLLM 提出的 PagedAttention将虚拟内存的分页思想引入 KV Cache 管理KV Cache 被分割为固定大小的 Block类似 4KB 内存页每个 Block 存储固定数量 token 的 Key 和 Value。请求的 KV Cache 由 Block 链表组成不需要物理连续。释放时逐 Block 回收无外碎片。二、PageAttention 的虚拟内存管理架构Attention kernel 计算时通过block_table[request_id]查找每个 token 对应的物理 Block 地址然后使用 GPU 的gather指令从非连续的 Block 中收集 Key/Value。这个收集操作的额外开销约 3~5%——远小于显存利用率的提升从 60% 到 96%。gather操作的性能瓶颈在于显存带宽利用率。当 Block 分散在 VRAM 的不同区域时GPU 的 Memory Controller 需要发起多次显存事务Transaction降低了有效带宽。但 Attention 计算本身是 Bandwidth-Bound 的计算量小数据搬运量大额外的gather开销被掩盖在计算延迟中。实测显示在 A100 80GB 上PageAttention 的 Attention Kernel 吞吐量仅比连续分配低 2-3%。Block 大小是一个关键超参数。Block 越大映射表的条目越少减少查找开销但内碎片越大。vLLM 默认 Block Size 16这是基于实验的折中——A100 的 L2 Cache line 128 字节16 个 fp16 token 的 KV 约为 16 × 2 × 128 × 32 × 2 / 8 32KB适配 L1 Cache 大小。选择 Block Size 16 的另一个原因是与 GPU Warp Size32 threads对齐。每个 Block 的 Attention 计算可以由一个 Warp 独立完成避免 Warp 内分支发散。如果 Block Size 不是 Warp Size 的倍数会导致部分线程空闲降低 GPU 利用率。三、PageAttention 在 Rust 推理引擎中的实现use std::collections::{HashMap, VecDeque}; use std::sync::Arc; use tokio::sync::Mutex; /// Block 配置 const BLOCK_SIZE: usize 16; // 每个 Block 存储 16 个 token /// 物理 Block 在 GPU 显存中的表示 #[derive(Debug, Clone, Copy)] struct PhysicalBlock { /// Block 在 Block 池中的索引 block_id: usize, /// GPU 显存中的虚拟地址 gpu_addr: u64, /// 引用计数多个请求可能共享前缀 Block ref_count: u32, } /// Block 池管理器GPU 显存管理器 struct BlockPool { /// 总 Block 数量 total_blocks: usize, /// 空闲 Block 队列 free_blocks: VecDequePhysicalBlock, /// 所有 Block索引 → Block all_blocks: VecPhysicalBlock, /// 当前已分配的 Block 数量 allocated_blocks: usize, } impl BlockPool { fn new(num_gpu_blocks: usize, base_addr: u64, block_size_bytes: u64) - Self { let mut all_blocks Vec::with_capacity(num_gpu_blocks); let mut free_blocks VecDeque::with_capacity(num_gpu_blocks); for i in 0..num_gpu_blocks { let block PhysicalBlock { block_id: i, gpu_addr: base_addr (i as u64) * block_size_bytes, ref_count: 0, }; all_blocks.push(block); free_blocks.push_back(block); } BlockPool { total_blocks: num_gpu_blocks, free_blocks, all_blocks, allocated_blocks: 0, } } /// 分配一个空闲 Block /// 设计原因使用 VecDeque::pop_front 实现 O(1) 分配 /// 返回的 Block 引用计数设为 1首次分配 fn allocate(mut self) - OptionPhysicalBlock { self.free_blocks.pop_front().map(|mut block| { block.ref_count 1; self.allocated_blocks 1; block }) } /// 释放一个 Block引用计数减 1归零时回收 /// 设计原因使用引用计数支持前缀共享 /// 多个请求可以共享同一个 Block如系统 prompt 的 KV Cache fn free(mut self, block_id: usize) { let block mut self.all_blocks[block_id]; block.ref_count block.ref_count.saturating_sub(1); if block.ref_count 0 { self.free_blocks.push_back(*block); self.allocated_blocks - 1; } } /// 增加引用计数共享 Block 时使用 /// 设计原因前缀共享时多个请求需要访问同一个 Block /// 只有当所有持有者都释放后Block 才真正回收 fn add_ref(mut self, block_id: usize) { self.all_blocks[block_id].ref_count 1; } fn usage_ratio(self) - f64 { self.allocated_blocks as f64 / self.total_blocks as f64 } } /// 逻辑 Block 映射一个请求的 Block 表 #[derive(Debug, Clone)] struct BlockTable { /// token_index → physical_block_id 的映射 /// key: 逻辑 Block 序号0, 1, 2, ... /// value: 物理 Block ID blocks: Vecusize, } impl BlockTable { fn new() - Self { BlockTable { blocks: Vec::new() } } /// 获取指定 token 索引对应的物理 Block 和块内偏移 fn get_physical_location(self, token_idx: usize) - Option(usize, usize) { let logical_block token_idx / BLOCK_SIZE; let offset token_idx % BLOCK_SIZE; self.blocks.get(logical_block).map(|phys_id| (phys_id, offset)) } /// 需要多少个逻辑 Block 来存储给定数量 token fn num_required_blocks(num_tokens: usize) - usize { (num_tokens BLOCK_SIZE - 1) / BLOCK_SIZE } } /// PageAttention 管理器协调 KV Cache 的分配与释放 struct PageAttentionManager { /// GPU 显存 Block 池 block_pool: ArcMutexBlockPool, /// 每个活跃请求的 Block 表 request_blocks: ArcMutexHashMapu64, BlockTable, } impl PageAttentionManager { fn new(num_gpu_blocks: usize) - Self { let block_pool BlockPool::new( num_gpu_blocks, 0x1000_0000, // GPU 显存基地址 2 * BLOCK_SIZE as u64 * 4096 * 32 * 2, // 每 Block 字节数 ); PageAttentionManager { block_pool: Arc::new(Mutex::new(block_pool)), request_blocks: Arc::new(Mutex::new(HashMap::new())), } } /// 为新请求分配 KV Cache Blocks /// 返回分配的 Block 数量 /// 设计原因分配可能部分失败在分配过程中 Block 池耗尽 /// 必须回滚已分配的 Block避免资源泄漏 async fn allocate_for_request( self, request_id: u64, prompt_len: usize, ) - Resultusize, String { let num_blocks BlockTable::num_required_blocks(prompt_len); let mut pool self.block_pool.lock().await; // 检查是否有足够的空闲 Block // 设计原因先检查再分配避免部分分配后失败 // 但此检查与后续分配之间存在 TOCTOU 竞态 // 生产实现中应使用 try_allocate 原子操作 if pool.free_blocks.len() num_blocks { return Err(format!( Insufficient blocks: need {}, available {}, num_blocks, pool.free_blocks.len() )); } let mut table BlockTable::new(); for _ in 0..num_blocks { match pool.allocate() { Some(block) table.blocks.push(block.block_id), None { // 回滚释放已分配的 Block // 设计原因必须保证原子性全部分配或完全不分配 for block_id in table.blocks { pool.free(block_id); } return Err(Allocation failed mid-way.to_string()); } } } let mut requests self.request_blocks.lock().await; requests.insert(request_id, table); Ok(num_blocks) } /// 为生成阶段追加新的 KV Cache Block /// 每次自回归解码生成一个新 token可能触发新 Block 分配 async fn append_slot( self, request_id: u64, current_len: usize, ) - ResultOptionusize, String { // 检查当前是否正好是 Block 边界 if current_len % BLOCK_SIZE ! 0 { return Ok(None); // 无需新 Block } let mut pool self.block_pool.lock().await; let mut requests self.request_blocks.lock().await; let table requests.get_mut(request_id) .ok_or(Request not found)?; match pool.allocate() { Some(block) { table.blocks.push(block.block_id); Ok(Some(block.block_id)) } None { // 无空闲 Block → 触发抢占Preemption // 策略选择 oldest request 驱逐其 Blocks Err(Preemption required: no free blocks.to_string()) } } } /// 释放请求的所有 Blocks async fn free_request(self, request_id: u64) { let mut requests self.request_blocks.lock().await; let mut pool self.block_pool.lock().await; if let Some(table) requests.remove(request_id) { for block_id in table.blocks { pool.free(block_id); } } } /// 前缀共享新请求复用已有请求的 prompt Block /// 例如系统 prompt 被多个请求共享无需重复计算 KV Cache async fn share_prefix_blocks( self, source_request_id: u64, target_request_id: u64, shared_len: usize, ) - Result(), String { let shared_blocks BlockTable::num_required_blocks(shared_len); let requests self.request_blocks.lock().await; let mut pool self.block_pool.lock().await; let source_table requests.get(source_request_id) .ok_or(Source request not found)?; if source_table.blocks.len() shared_blocks { return Err(Source has fewer blocks than requested share length.to_string()); } // 为共享的 Blocks 增加引用计数 for i in 0..shared_blocks { pool.add_ref(source_table.blocks[i]); } let mut target_table BlockTable::new(); target_table.blocks source_table.blocks[..shared_blocks].to_vec(); drop(requests); let mut requests self.request_blocks.lock().await; requests.insert(target_request_id, target_table); Ok(()) } } #[tokio::main] async fn main() { let manager PageAttentionManager::new(1024); // 1024 个物理 Blocks // 分配请求 A 的 KV Cacheprompt50 tokens → 需要 4 个 Blocks let blocks_a manager.allocate_for_request(1, 50).await.unwrap(); println!(Request A allocated {} blocks, blocks_a); // 自回归生成第 17 个 token触发新 Block 分配 let new_block manager.append_slot(1, 17).await.unwrap(); println!(Request A new block: {:?}, new_block); // 请求 B 共享 A 的前 2 个 Block系统 prompt manager.share_prefix_blocks(1, 2, 32).await.unwrap(); println!(Request B shared prefix blocks from request A); // 释放请求 manager.free_request(1).await; manager.free_request(2).await; println!(All requests freed); }前缀共享是 PageAttention 额外带来的重要优化。在 RAG 场景中多个请求共享相同的检索文档 prompt前缀 KV Cache 的复用可以减少 50~80% 的 prefill 计算量。引用计数机制保证共享 Block 在所有使用者释放后才回收。Block 分配失败时的抢占策略需要根据请求优先级选择性驱逐。低优先级请求的 KV Cache 被卸载到 CPU 内存Swap Out待 GPU 有空闲 Block 时再换入。这需要在 GPU 和 CPU 之间做高效的数据传输。四、PageAttention 的性能边界与退化场景额外开销gather操作从 Block 表中查找物理地址每次 Attention 增加约 3% 计算开销Block 管理的元数据内存每 Block 约 64 字节1000 个 Block 仅 64KB退化场景短序列 32 tokensBlock 内碎片占比大连续分配可能更优极高并发 100 requestsBlock 表查找的 TLB 压力增大需增大 Block SizeBeam Search每个 Beam 需要独立的 Block 表fork 操作需要复制 Block 引用显存效率数据连续分配利用率 60%总 KV Cache 浪费约 40%PageAttention利用率 96%Block 内碎片占比约 4%Block Size16PageAttention 的退化场景与应对短序列 32 tokensBlock 内碎片占比大可能 50%连续分配可能更优。应对策略动态选择 Block Size——短提示用 Block Size4长生成用 Block Size32。极高并发 100 requestsBlock 表查找的 TLB 压力增大每次 Attention 需要多次显存访问。应对策略增大 Block Size 到 32 或 64减少 Block 表大小。Beam Search每个 Beam 需要独立的 Block 表fork 操作需要复制 Block 引用引用计数1。当 Beam Width5 时KV Cache 占用放大 5 倍。应对策略使用共享前缀 独立后缀仅 fork 生成部分的 Block 表。Multi-Modal 模型图像文本图像 token 的 KV Cache 远大于文本固定 Block Size 导致图像 Block 浪费显存。应对策略使用可变大小 BlockvLLM 的 v2 引擎支持。与 DeepSpeed Inference 的 Tensor Parallelism 结合当使用 TPTensor Parallelism时每个 GPU 只持有部分注意力头。PageAttention 需要跨 GPU 同步 Block 表——这会导致额外的通信开销。优化方案每个 GPU 独立管理 Block 池仅共享逻辑 Block 表的元数据通过 NCCL AllReduce。五、总结PageAttention 将虚拟内存的分页管理引入 KV Cache消除物理连续分配的外碎片将显存利用率从 60% 提升至 96%。KV Cache 被分割为固定大小的 Block默认 16 tokens通过 Block 表完成逻辑→物理地址映射Attention kernel 使用 gather 操作访问非连续 Block。gather 操作的额外开销约 3%Block 内碎片约 4%净收益显存利用率提升 36%远超额外开销。前缀共享利用引用计数实现多请求复用公共 prompt 的 KV CacheRAG 场景下减少 50~80% prefill 计算。Block 耗尽时的抢占策略将低优先级请求的 KV Cache 交换到 CPU 内存GPU 空闲后换回。
RELATED

相关推荐

5大硬件加速技巧:深度解析Thorium浏览器性能优化架构

5大硬件加速技巧:深度解析Thorium浏览器性能优化架构

5大硬件加速技巧:深度解析Thorium浏览器性能优化架构 【免费下载链接】thorium Chromium fork named after radioactive element No. 90. Source code and Linux releases. Windows/MacOS/ARM builds served in different repos, links are towards the top of the …

📅 2026/11/10 10:56:03
如何用开源AI标书工具10倍提升投标效率?易标投标工具箱全攻略

如何用开源AI标书工具10倍提升投标效率?易标投标工具箱全攻略

如何用开源AI标书工具10倍提升投标效率?易标投标工具箱全攻略 【免费下载链接】OpenBidKit_Yibiao 开箱即用的AI标书编写工具,标书AI生成工具,投标工具箱、知识库、标书查重、废标项检查,完全开源免费,欢迎使用 项目…

📅 2026/11/10 10:56:03
【前端埋点生死线】:Cursor统计代码未做这4类上下文隔离,90%团队正在 silently 丢失核心行为数据

【前端埋点生死线】:Cursor统计代码未做这4类上下文隔离,90%团队正在 silently 丢失核心行为数据

更多请点击: https://codechina.net 第一章:Cursor埋点统计代码的生死线本质 Cursor 埋点统计代码并非普通业务逻辑,而是前端可观测性的神经末梢——它既不能阻塞主线程,又必须在页面生命周期关键节点精准触发;稍有偏…

📅 2026/9/13 10:52:45
MORE NEWS

更多资讯

📰

不说再见电工证怎么考?真实经历告诉你怎么避坑

不说再见电工证怎么考?真实经历告诉你怎么避坑 证书过期了不知道怎么复审延期?这事儿我见过太多人踩坑,尤其是像你这样在驻马店从事公路工程的电工朋友,很多人因为不了解流程,导致证过期了才知道补救已经来不及。今天我就结合真实经历,把【不说再见电工…

📰

苏州哪有考电工证报名入口与办理流程良心建议

苏州哪有考电工证报名入口与办理流程良心建议 你急需特种作业证才能上岗找工作,但不知道从哪开始,更怕走弯路?别急,这是很多刚入行的电工朋友的真实写照。今天我就以一个过来人的身份,跟你说说苏州哪有考电工证、报名流程、考试技巧,还有避坑指南。这些…

📰

沐川免费考电工证保姆级教程:外省证怎么转?看完这篇全搞懂

沐川免费考电工证保姆级教程:外省证怎么转?看完这篇全搞懂 之前考的证在外省能不能转过来?这几乎是每个想在本地发展、又手握外地证书的电工最关心的问题。尤其在沐川地区,不少从事水利工程、电力运维、施工现场管理等工作的同行,手中握着的电工证可能来…

📰

办电工证石家庄千万别踩坑,新手报名避坑指南

办电工证石家庄千万别踩坑,新手报名避坑指南 你是不是正愁不知道去哪儿报名办电工证石家庄,怕被中介坑得血本无归?别急,今天这篇就是为你量身打造的避坑指南,让你不花冤枉钱,稳稳拿到电工证,还能避开那些套路满满的中介陷阱。…

📰

娄底双峰县电工证在哪考不迷路,别让培训费打水漂

娄底双峰县电工证在哪考不迷路,别让培训费打水漂 你是不是也担心考电工证费了钱却考不过?怕白交培训费?别急,这篇文章就是为你准备的,教你 在哪考 娄底双峰县电工证,避开培训机构的坑,稳稳拿下证书。…

📰

天津电工证免考怎么操作?全国通用免考政策解读来了

天津电工证免考怎么操作?全国通用免考政策解读来了 怕考不过白交培训费?现在天津电工证免考政策落地,全国通用的电工证不用再死磕考试了。特别是郑州的市政公用工程从业者,新规下报考流程更简化,学历和工作年限要求也更灵活。今天就来聊聊天津电工证免考…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬