老李经营着一家城市图书馆,来往读者络绎不绝。图书馆有条特殊规定:每位读者看书时必须写读书笔记,把翻过的书页、要点记下来放在手边,方便随时回看。于是书桌上堆满了笔记,谁也不肯收起来。大模型推理也是如此——每生成一个字,模型都要“回想”此前所有内容,用来回想的“笔记”就是 KV 缓存。没有它,模型就得把前面的内容重算一遍,计算量直接爆炸。 问题出在老李的法子上:每位读者固定一张大书桌,先到先得。有人只借一本小册子,却占着能摊开十本百科全书的桌面;有人想加写笔记时,桌子却早已满了。整间图书馆坐满了人,真正被利用的桌面却不到一半——这就是显存碎片与浪费的真相。后来老李想通了,把笔记按统一规格装订成标准册页,任何一面墙、一面隔板都能贴,再在墙边挂张“索引贴纸”,记下每册笔记贴在哪儿。于是缝隙被填平,图书馆能同时接待的读者翻了几倍。这套“书页索引”法,就是 vLLM 的核心:PagedAttention。
PagedAttention 是理解主流大模型推理引擎的“心脏”。无论是评估部署成本、优化并发瓶颈,还是设计长上下文服务,掌握它都是一线 AI 工程师的硬通货。
目录
可视化演示
步进式展示vLLM PagedAttention核心机制:传统KV缓存的显存浪费问题、Block Table逻辑到物理块映射、非连续块注意力计算、连续批处理多序列共享块,点击步骤切换查看分页机制各阶段。
本节配有交互动效(进入视口后自动加载,约 3KB)。
传统KV Cache显存浪费 每个序列预分配最大长度的连续显存,实际使用远小于分配量 显存碎片化和内部浪费导致利用率仅30-40%,限制并发序列数 Waste = 1 - used / allocated 传统方式:预分配最大长度连续显存
KV缓存之谜:AI 的读书笔记究竟有多占地方
老李的图书馆里,读者们的笔记越堆越厚,桌上的空间越来越小。大模型推理时也一样:模型每生成一个 token,都要“回头翻看”之前所有 token 的 Key 和 Value 信息,这份“回想用笔记”就叫 KV 缓存(Key-Value Cache)。如果没有它,每个新字都得把前面所有内容重新算一遍,计算量随序列长度呈平方级爆炸。所以 KV 缓存是典型的“空间换时间”——用显存扛住反复阅读的成本。
但这份笔记远比想象中占地方。以 Llama-7B 为例:32 层 Transformer、每层 32 个注意力头、每头 128 维向量。每一层都要独立记笔记(32 份),每一份笔记又分为 Key 和 Value 两栏(乘以 2),每栏的“行数”等于序列长度——聊到第 2048 个 token,就是 2048 行。一行里的一个数字用半精度 float16 存储占 2 字节。把所有乘起来,才是单条请求的缓存体积。
代入 Llama-7B 算一笔账:2×32×32×128×2048×1×2 字节,约 1GB。对一块 24GB 的 3090 显卡,仅一条长请求的 KV 缓存就占去近二十分之一——笔记一摊开,能同时看书的读者自然就少了。这也是为什么传统推理框架明明显存不小,却总觉得“装不下几个人”。
- 2(K 与 V 双份)注意力需要同时保存 Key 键查找表和 Value 内容表两份缓存,就像读书笔记既有“这段出现在哪页”,又有“这段讲了什么”两栏,缺一不可。
- L 层数Transformer 每一层都有独立的注意力子层,每一层都要写一份自己的笔记,层数 L 直接乘进总量。
- H × d 头维度积多头注意力里每个头有 d 维向量,H 个头拼起来才是完整笔记的“行宽”。
- S × b 序列与批序列长度 S 是笔记的行数,批大小 b 是有多少份同样规模、同时摊开的笔记。
- P 精度字节每个数字以 float16 存储占 2 字节,P 就是单个元素的字节数。
连续分配之殇:固定大桌子的浪费与死区
老李过去给每位读者分配一张固定的大书桌,不管对方借一本小册子还是十本百科全书,桌子大小一律按“最胖读者”的标准预埋。结果短请求占着大半张空桌子,长请求反而在门外排队。传统推理框架(如 Hugging Face Transformers)的做法如出一辙:为每个请求按“最大可能序列长度”预留一整段连续显存,实际序列往往远短于预设——预留了却用不上的空间,就是内部碎片。
碎片还有第二种形态。显存分配要求连续,请求 A、B、C 依次排队,中间的间隙往往无法合并再分配给任何新请求,这类零碎空隙叫外部碎片。变长的请求序列让两种碎片雪上加霜:线上请求长度从一句话到一整篇文章波动极大,系统只能按上限定,碎片率居高不下。于是明明显存总和够用,新请求却因找不到“足够长的一段连续空间”而被拒之门外。
老李看着满屋空荡荡的桌面直叹气:明明是“空间不够”,其实是“空间没用好”。如果能让笔记散落到任何缝隙,何必人人霸占一张大桌?这正是下一节的破局之道。
- S_i(实际使用量)第 i 个请求真正写下的笔记页数,即实际序列长度。
- S_i^max(预留上限)系统怕超纲而提前留出的最大页数,通常取允许的最大序列长度。
- Σ 求和把批处理内所有请求的使用量加总,得到总体账面数字。
- 1 − 比值减去“实际用了多少”,剩余比例就是被锁死、别人碰不得的浪费空间。
分页存储设计:标准册页与索引贴纸
老李终于想出了新法子:把笔记按统一规格装订成“标准册页”,每册固定装 16 页;读者不用再占一整张桌子,拿到哪面墙就把册页贴在哪儿——第一册贴墙 A,第二册贴墙 D,中间隔着多远都无所谓。墙边挂一张“索引贴纸”,记下“第 1 册在 A 墙、第 2 册在 D 墙”。想读时按贴纸查位置即可。这就是 PagedAttention 的核心思想:KV 缓存不再需要连续显存。
技术实现上,PagedAttention 借鉴了操作系统虚拟内存的分页机制。KV 缓存被切成固定大小的块(Block),每块通常容纳 16 个 token 的 K/V 数据;逻辑上连续的一段 KV 缓存,在物理显存中可能分散在任意位置。每个请求维护一张块表(Block Table),记录“逻辑第几块 → 物理第几块”的映射,GPU 计算时按表寻址拼接即可。分块还带来一个额外好处:最后一个块往往用不满,剩余空间可以留给其他请求共享,块级粒度让显存分配收放自如。
散落的册页让整个书库像拼图一样严丝合缝,碎片被彻底填平。老李还惊喜地发现:纸张的规格统一之后,读者之间甚至可以互相借用空余的页面——块级的精细管理,为后续吞吐跃升埋下了伏笔。
- N_i 所需块数请求 i 需要多少张标准册页。
- ⌈ ⌉ 向上取整19 页笔记按每册 16 页装订,也要开 2 册,不能只开 1.19 册。
- B 块容量每个物理块能装多少个 token 的 KV 值,vLLM 默认块大小通常设为 16。
- BlockTable_i[j] 块表请求 i 的第 j 个逻辑块实际贴在哪个物理块,查表即得,完全无需连续。
显存利用跃升:同一间书库,两倍的读者
墙壁利用率从 40% 一路升到 90% 以上,老李没有扩建一寸书库,能同时接待的读者却翻了一倍多。PagedAttention 带来的第一个直接收益就是显存有效容量骤然变大:过去被内部碎片锁死的空间全部释放,同样 16GB 显存能装下更多请求的 KV 缓存。物理块可以散落分配,外部碎片也基本消失,显存分配不再要求“一整段连续区”。
显存宽裕直接转化为吞吐。vLLM 官方与 Hugging Face Transformers 在相同硬件上的对比显示,PagedAttention 能将推理吞吐量提升 2 到 4 倍。原因并不神秘:吞吐 ≈ 并发批大小 × 单请求速度,分页让有效批处理规模扩大,单位时间内完成的请求数随之跃升。显存越紧张(长序列、高并发),提升越明显——因为传统方案被碎片拖垮的部分恰好全部被捡了回来。
老李算了一笔账:省下的不是钱,是“墙”——显存的每一 MB 都是真金白银。同样的 GPU 预算下,能服务的用户直接翻倍,这对任何一家做 AI 服务的公司都是质变。
- Throughput_paged(新方案吞吐)单位时间内完成并返回的请求总数,由批处理规模和单请求速度共同决定。
- Throughput_naive(老方案吞吐)传统连续分配下的吞吐,受碎片限制,批处理上不去。
- Speedup(加速比)两个吞吐相除,得到分页带来的净收益倍数。
- 2 ~ 4 倍实验观测区间——碎片越严重、负载越长,加速越接近上限 4 倍。
连续批处理:随到随走的新秩序
空间问题解决后,老李还顺手改了一条规矩:图书馆不再等第一批读者全部看完才放第二批进来。写完笔记的读者起身就走,空出来的墙面立刻挂上新读者的册页——这就是连续批处理(Continuous Batching)。传统批处理是“整进整出”:一个批次同时开始、同时结束,有人提前写完也得干等,阻塞了后来者。连续批处理则让请求随时加入、随时离开。
连续批处理能流畅运转,正是因为有 PagedAttention 的块表兜底。一个请求完成了,把它的整组物理块归还给空闲池,调度器立刻把新请求的逻辑块插入;如果显存吃紧需要抢占,可以把某个请求的块整体“换出”(拷回 CPU 内存),等资源宽裕再“换入”。块表让“打包、搬运、恢复”变得像搬几本标准册页一样轻量。批处理规模不再是一成不变的固定值,而是随时间动态起伏的水位线。
如今的图书馆更像一条流水线餐厅而不是“整进整出”的包间。配合 PagedAttention 的块管理,每个请求都按自己的节奏完成,系统整体效率再上一个台阶——空间与时间的双重优化叠加,才构成 vLLM 完整的加速故事。
- |Batch(t)|(瞬时批大小)t 时刻正在并行处理的请求数,连续批处理中它随时间变化。
- Σ/T(时间平均)对所有时刻的批大小求和再除以总时间,得到平均水位。
- N_total / T_wall(总吞吐)总完成请求数除以墙钟时间,这是业务最关心的宏观指标。
- 传统对比旧方案批大小呈阶梯状突变,中途无人进出;新方案随时补位,平均并发更高。
新旧方案对比:一页账本看穿优劣
把传统方案与 PagedAttention 的两本账摊开对照,优劣一目了然。传统方案像“一人一桌”的规矩:分配简单、实现直观,但内部碎片高达 50% 以上,外部碎片无法合并,批处理只能整进整出,显存利用率常年低迷。PagedAttention 像“册页上墙”的新馆:块表多花一点索引开销,换来的却是碎片率降到 10% 以下、请求动态进出、并发规模翻倍。
适用场景的边界同样清晰。PagedAttention 最擅长高并发、长序列、请求长短参差的推理负载——比如云端大模型 API 服务、长上下文智能体;而对于一次只跑一个极短请求、显存本就绰绰有余的离线场景,分页带来的收益会被索引与调度的开销抵消,传统方案反而更简单。理解边界比背诵结论更重要,这也是工程判断力的体现。
PagedAttention 并不是什么魔法,它只是把“空间管理”这堂操作系统的基础课,补到了大模型推理身上。KV 缓存一个字都没有压缩,模型计算一丝也没有改变,变的只是“摆放方式”——而正是这个摆放方式,撑起了 vLLM 数倍于传统框架的推理吞吐。
- S_used^total(实际数据量)所有请求真正写入缓存的有效 KV 数据大小。
- S_alloc^total(占用容量)系统为这些请求分配的显存总量,包含浪费部分。
- U_eff(有效利用率)两者相除,越接近 1 说明显存越物尽其用。
- 2~3 倍差距经验值——同一块 GPU 上,分页让有效显存容量放大数倍。
AI 应用场景
云端大模型 API 服务:并发翻倍,成本减半
阶段:推理部署与性能优化
解决的问题:多租户共享 GPU 时,传统连续分配让每个用户的请求都预占大块显存,短请求大量浪费,并发上不去,GPU 利用率低。PagedAttention 让所有请求的 KV 块共享物理显存,短请求的空块可以被其他请求复用。
具体案例:某 AI 公司部署 Llama-3-8B 的对话 API,日请求量百万级且单条请求普遍很短。改用 vLLM 后,单卡同时服务的请求数从 8 路提升到 24 路,吞吐提升约 3 倍,GPU 采购成本下降 60%。
长文档阅读助手:撑住十万 token 的“超长笔记”
阶段:长上下文推理
解决的问题:RAG 与智能体场景中,单条请求可能携带数万 token 的上下文,KV 缓存动辄数 GB。传统方案极易 OOM(显存溢出),而 PagedAttention 把超长 KV 缓存切块散放,碎片空间全部用上。
具体案例:某智能客服系统需要读取整本企业手册(约 8 万 token)后回答用户问题。部署 vLLM 后,长上下文请求不再因显存不足被拒,且可同时服务多个长会话,平均响应延迟保持稳定。
本地单卡部署:个人开发者的小型推理服务
阶段:边缘部署与成本控制
解决的问题:普通开发者只有一块 4090(24GB),既要跑模型权重又要跑 KV 缓存,批大小长期上不去。PagedAttention 把显存利用率从不足 50% 提升到 90% 以上,让单卡也能支撑多路并发。
具体案例:开发者用 vLLM 在本地搭建私人知识库问答服务,同时服务自己和三个同事的聊天窗口,四个会话并行推理不卡顿,显存占用不到 60%。
Python 演示
"""模拟 PagedAttention 的 KV 缓存块管理流程。"""import math class PagedAttentionSimulator: """以块为单位管理 KV 缓存的显存分配器。""" def __init__(self, num_blocks=16, block_size=4): self.num_blocks = num_blocks self.block_size = block_size self.free_blocks = list(range(num_blocks)) self.block_tables = {} # 请求 id -> 物理块号列表 self.kv_used = {} # 物理块号 -> 已写 token 行数 def need_blocks(self, seq_len): """根据序列长度计算所需块数。""" return math.ceil(seq_len / self.block_size) def allocate(self, req_id, seq_len): """为请求划分物理块并登记块表。""" n = self.need_blocks(seq_len) if n > len(self.free_blocks): return False blocks = self.free_blocks[:n] del self.free_blocks[:n] self.block_tables[req_id] = blocks for b in blocks: self.kv_used[b] = 0 return True def append_kv(self, req_id): """生成一个新 token 后,向最后一块写入一行 KV。""" blocks = self.block_tables[req_id] last = blocks[-1] if self.kv_used[last] >= self.block_size: if not self.free_blocks: return False new_block = self.free_blocks.pop(0) blocks.append(new_block) self.kv_used[new_block] = 0 last = new_block self.kv_used[last] += 1 return True def free(self, req_id): """请求结束,归还全部物理块。""" for b in self.block_tables.get(req_id, []): self.free_blocks.append(b) del self.kv_used[b] self.block_tables.pop(req_id, None) def utilization(self, req_id): """某请求的块利用率:已写行数 / 总容量。""" blocks = self.block_tables.get(req_id, []) used = sum(self.kv_used[b] for b in blocks) return used / max(len(blocks) * self.block_size, 1) def simulate(): sim = PagedAttentionSimulator(num_blocks=16, block_size=4) for rid, need in zip(["A", "B", "C"], [5, 12, 3]): assert sim.allocate(rid, need), rid print(f"{rid} 占用块: {sim.block_tables[rid]}") print("3 个请求分配完,剩余空闲块:", sim.free_blocks) sim.free("A") sim.free("C") print("A/C 完成后归还,空闲块:", len(sim.free_blocks)) assert sim.allocate("D", 6) print("新请求 D 占用块:", sim.block_tables["D"]) print("D 的块利用率:", sim.utilization("D")) if __name__ == "__main__": simulate()
self.free_blocks = list(range(num_blocks))所有物理块先进入空闲池,相当于图书馆里所有空白墙面。return math.ceil(seq_len / self.block_size)一个请求需要几块,向上取整保证装得下。blocks = self.free_blocks[:n]从空闲池取出前 n 块,分配给请求。self.block_tables[req_id] = blocks登记块表:该请求的逻辑块依次对应哪些物理块。if self.kv_used[last] >= self.block_size:最后一块写满了,需要再申请一个新的物理块追加到块表尾部。self.kv_used[last] += 1向最后一块写入一行 KV,模拟 decode 阶段每生成一个 token 追加一次。sim.free("A")请求完成即把块归还空闲池,新请求 D 立刻能复用这些“碎片”。
常见误区
误区PagedAttention 名字里带“页”,听起来像一种压缩算法,以为它像量化或剪枝一样把 KV 缓存变小了,所以才能省显存。
正解它没有压缩任何数据。KV 缓存的内容一个字节都没变,只是把“存放方式”从连续大块改成固定大小的散块。好比笔记内容一个字没少,只是从一整张大纸改成标准册页贴到墙上。省下的显存来自消除内部碎片,而非数据瘦身。
为何常见:“页”让人联想到“分页压缩”,加上 PagedAttention 确实省显存,直觉上容易归功于数据变小。
误区块大可以减少块表条目数量,块表开销更小,那是不是把块设成 1024 甚至更大更好?
正解块越大,内部碎片越严重——一个只需 17 个 token 的请求会占据一整块 1024 容量的物理块,浪费率反而回到传统方案的老路。块越小碎片越少,但块表条目数激增,索引和寻址开销变大。vLLM 默认 16 是权衡后的结果:碎片可控、块表不过大。好比册页太薄则索引贴纸贴满墙,太厚又浪费空页。
为何常见:从“减少块表开销”的角度推理容易得出大块更优,忽略了碎片率会同步反弹,缺少对立统一思维。
误区在 vLLM 的文档里经常一起看到,以为 Continuous Batching 就是 PagedAttention 的另一个名字。
正解两者解决的问题完全不同。PagedAttention 管“空间怎么放”(显存分配机制),连续批处理管“时间怎么排”(请求调度策略)。前者让 KV 块可以散落存放、随借随还,后者让请求可以随时加入和退出批次。两者是配合关系,不是同一件事——好比书页索引负责找墙,排班表决定谁先来看书。
为何常见:两者在 vLLM 中协同实现高性能,经常并列出现,容易让初学者误认为二名一体。
误区短请求的 KV 缓存很小,占不了多少显存,PagedAttention 对短序列没什么用。
正解恰恰相反,短请求多时浪费比例反而最大。传统方案按最大长度预留,一条 50 token 的短请求也要预留 2048 的空间,利用率不到 3%。大量短请求并发时,PagedAttention 让它们的块紧密贴合、互相借位,批处理规模大幅扩大,吞吐提升甚至比长请求更明显。
为何常见:直觉上“小数据不值得优化”,但忽略了量变引起质变——海量短请求累计浪费的总量十分惊人。
误区PagedAttention 把吞吐提升了 2~4 倍,那单个请求的响应时间应该也快了这么多。
正解吞吐和延迟是两回事。PagedAttention 提升的是单位时间能完成的总请求数,主要来自批处理规模扩大;单请求的 decode 延迟反而可能因为 GPU 算力被更多请求分摊而略有增加。好比图书馆墙利用率高了,单个读者看书的速度不变,但能同时接待更多人。追求吞吐还是单请求低延迟,是部署时的取舍问题。
为何常见:把吞吐(Throughput)和延迟(Latency)混为一谈,是性能优化里最常见的概念混淆之一。
面试题
Q1. PagedAttention 的核心思想借鉴了哪个经典系统机制?
- A.操作系统虚拟内存分页← 正确答案
- B.数据库索引优化技术
- C.网络数据包分组传输
- D.硬盘磁道扇区缓存
答案:A。PagedAttention 将 KV 缓存切成固定大小的块并用块表记录逻辑到物理的映射,与操作系统把虚拟内存分页映射到物理内存如出一辙。数据库索引、网络分组、磁盘缓存在机制上并不负责“逻辑块到物理块的非连续映射”这一核心任务。
Q2. KV 缓存中 K 和 V 分别代表什么?
- A.Key 键矩阵与 Value 值矩阵← 正确答案
- B.Kernel 核矩阵与 Vector 向量矩阵
- C.Knowledge 知识矩阵与 View 视图矩阵
- D.Variable 变量矩阵与 Vision 视觉矩阵
答案:A。注意力机制中,输入通过线性变换得到 Query(查询)、Key(键)、Value(值)三个矩阵。已生成的 token 的 Key 和 Value 被缓存起来,供后续 Query 做注意力加权。Kernel/Vector/Knowledge/Vision 在 Transformer 的 KV 缓存中都没有对应定义。
Q3. 下列哪种浪费属于 PagedAttention 主要解决的“内部碎片”?
- A.预留 2000 token 却只用 100 token← 正确答案
- B.两个请求之间无法合并的小空隙
- C.显存带宽不足导致的计算等待
- D.模型权重重复加载消耗的显存
答案:A。内部碎片指“分配了但由于预留过大而用不上”的空间,预留 2000 只用 100 就是典型,PagedAttention 用块粒度分配将其大幅削减。B 是外部碎片,C 是带宽瓶颈,D 是权重管理问题,都不属于内部碎片范畴。
Q4. 块表(Block Table)的核心作用是什么?
- A.记录逻辑块号到物理块号的映射← 正确答案
- B.存储注意力分数的计算中间结果
- C.记录每个 token 的生成概率排序
- D.管理 CPU 内存与显存的数据拷贝
答案:A。块表是 PagedAttention 的“索引贴纸”,每个请求维护一张表,记录其第 j 个逻辑 KV 块实际存放在哪个物理块。注意力计算时按表寻址把分散的 KV 块取回。它不存注意力分数、不做概率排序,也不负责内核数据拷贝。
Q5. PagedAttention 相比传统连续分配能带来哪些改进?
- A.显著减少 KV 缓存的内部碎片← 正确答案
- B.允许 KV 缓存非连续存放← 正确答案
- C.减少模型参数量以节省显存
- D.支持更大的动态批处理规模← 正确答案
答案:ABD。PagedAttention 把 KV 切成块,逻辑连续物理可散落,消除大量内部碎片,并因显存宽松使批处理规模变大。但模型参数一个没少,它不负责裁剪或量化权重,C 是混淆项。
Q6. 为什么 PagedAttention 能带来 2~4 倍的吞吐提升?
- A.显存利用率提高使批处理更大← 正确答案
- B.模型权重的显存占用被消除了
- C.注意力计算复杂度变成了常量
- D.用 CPU 内存替代了全部显存缓存
答案:A。吞吐 = 并发批大小 × 单请求速率。PagedAttention 没有改变注意力算法复杂度,也没有减少权重显存,更没有把 KV 挪到 CPU,它只是通过消除碎片让更多请求同时驻留显存,批处理规模扩大,单位时间完成的请求数因此翻倍。
Q7. 关于连续批处理(Continuous Batching),哪些说法正确?
- A.请求完成即可释放块并插入新请求← 正确答案
- B.所有请求必须同时开始同时结束
- C.抢占时可将请求的块整体换出← 正确答案
- D.需要对所有请求固定序列长度
答案:AC。连续批处理的核心就是动态进出:请求完成归还资源,新请求立即补位;显存不足时通过块表整体换出某请求的块。B 是传统整进整出批处理的特征,D 与连续批处理相反,它的价值恰恰在于支持变长请求。
Q8. 传统 Attention 推理时显存浪费的最主要原因是什么?
- A.按最大序列长度预留且不可共享← 正确答案
- B.模型权重文件在磁盘上占用太大
- C.注意力头数量过多导致计算高
- D.训练数据集规模超出显存容量
答案:A。传统框架为每个请求预留最大可能长度的连续显存,预留量与实际使用量差距巨大,且预留空间不能共享。权重在磁盘上是存储问题,注意力头多是计算开销问题,训练数据属于训练阶段范畴,均非推理显存浪费主因。
Q9. PagedAttention 的“块”(Block)通常容纳多少 token 的 KV 数据?
- A.固定大小的 token 数(如 16 个)← 正确答案
- B.与请求序列长度完全相同的 token 数
- C.所有请求共享的一个大页 token 数
- D.每层每头各不相同的随机 token 数
答案:A。块的大小是系统预设的固定值,vLLM 默认 16 个 token 的 KV 数据。它不随请求变化,也不全局共享一页,更不会在层和头之间随机变化。固定块大小正是碎片可控、块表开销适中的关键。
Q10. 以下哪些场景更适合使用 PagedAttention?
- A.高并发多请求的大模型推理服务← 正确答案
- B.长上下文多轮对话的智能体应用← 正确答案
- C.计算量小但显存占用大的批处理← 正确答案
- D.只跑单个极短请求的离线推理
答案:ABC。高并发、长上下文、批处理占显存大的场景,碎片问题突出,PagedAttention 收益显著。D 场景显存充裕、请求极短,分页带来的索引与调度开销反而可能超过收益,传统简化流程更合适。