2. PagedAttention核心思想:从操作系统的虚拟内存分页机制借鉴灵感,解决显存碎片化问题
好,咱们进入正题。PagedAttention 这个名字,你一听就知道跟“分页”有关。没错,它的灵感直接来自操作系统的虚拟内存管理。说白了,就是操作系统怎么管物理内存,vLLM 就怎么管显存。
我刚开始接触大模型推理时,最头疼的就是显存碎片化。明明显存还有不少空闲,但就是分配不出一块连续的大空间来放新的 KV Cache。这感觉,就像你手机存储空间还剩 10G,但每个文件都是零散的小碎片,连个 2G 的电影都存不进去。嗯,PagedAttention 就是来解决这个问题的。
2.1 为什么需要分页?显存碎片化的根源
先想想传统方案是怎么做的。在标准的 Transformer 推理中,每个请求的 KV Cache 是一整块连续显存。比如一个请求生成了 1024 个 token,那它的 KV Cache 就是一块 1024 行的大矩阵。
问题来了:
- 预分配浪费:你没法提前知道一个请求最终会生成多少个 token。为了保险,你只能按最大长度(比如 2048)预分配。结果很多请求只生成了几十个 token,剩下的显存全浪费了。
- 内存碎片:多个请求同时跑,各自占着一大块连续显存。有的请求结束了,释放出一块空间。但新来的请求需要一块更大的连续空间,这些释放出来的小块拼不到一起,就形成了碎片。
- 利用率低:我见过一个线上服务,显存利用率只有 40% 左右。剩下的 60% 全是碎片和预分配浪费。你说心疼不心疼?
核心矛盾:KV Cache 需要动态增长,但显存分配要求连续。这两个需求天然冲突。
2.2 虚拟内存分页的启示
操作系统怎么解决物理内存碎片?虚拟内存分页啊!
它把物理内存切成固定大小的“页”(Page),每个进程看到的是连续的“虚拟地址空间”,但实际上这些虚拟页可以映射到任意物理页上。物理页可以不连续,但虚拟地址看起来是连续的。
PagedAttention 借鉴的就是这个思路:
- 把 KV Cache 切成块:每个块叫一个“Page”,大小固定(比如 16 个 token 的 KV 值)。
- 逻辑上连续,物理上离散:一个请求的 KV Cache 在逻辑上是一串连续的 token 序列,但在显存中,这些 Page 可以分散存放在任意位置。
- 通过 Block Table 映射:每个请求维护一个 Block Table,记录逻辑 Page 到物理 Page 的映射关系。
我举个例子你就明白了。假设一个请求有 3 个逻辑 Page(Page 0、Page 1、Page 2),它们在 Block Table 中记录为:
逻辑 Page 0 → 物理 Page 7
逻辑 Page 1 → 物理 Page 3
逻辑 Page 2 → 物理 Page 12
你看,物理上完全不连续,但 Attention 计算时,通过 Block Table 找到对应的物理 Page,就能像操作连续内存一样工作。
个人经验:我最初看到这个设计时,第一反应是“这不就是操作系统的页表吗?” 没错,vLLM 的 Block Table 本质上就是页表。只不过操作系统的页表管理的是物理内存,vLLM 的 Block Table 管理的是显存。
2.3 PagedAttention 的核心数据结构
要理解 PagedAttention,你得先搞懂三个核心概念:
| 概念 | 说明 | 类比操作系统 |
|---|---|---|
| Logical KV Blocks | 每个请求的逻辑 KV Cache,按 Page 切分 | 进程的虚拟地址空间 |
| Physical KV Blocks | 显存中实际存放 KV 数据的位置 | 物理内存页框 |
| Block Table | 记录逻辑块到物理块的映射 | 页表 |
每个 Block 的大小是固定的。vLLM 默认每个 Block 包含 16 个 token 的 KV 值。为什么是 16?太小了映射开销大,太大了又不够灵活。16 是个不错的平衡点。
这里有个细节:每个 Block 里存的是 一个 token 在所有注意力头、所有维度上的 Key 和 Value。所以一个 Block 的大小 = 16 × 头数 × 每头维度 × 2(Key 和 Value)× 数据类型大小。
2.4 分页带来的三大好处
搞清楚了数据结构,咱们来看看分页到底解决了什么问题。
2.4.1 零碎片化
因为物理块是固定大小的,分配和释放都是按块来。一个请求结束后,它占用的物理块被释放,新来的请求可以任意组合这些块。不存在“需要一块连续大空间”的问题。碎片化?不存在的。
2.4.2 按需分配
传统方案要预分配最大长度的空间。PagedAttention 只需要分配当前已生成 token 所需的块数。生成一个新 token,就分配一个新块(如果当前块满了)。
我算过一笔账:一个服务平均请求长度只有 128 个 token,但最大长度设到了 2048。传统方案浪费了 93.75% 的显存。用 PagedAttention,几乎零浪费。
2.4.3 高效共享
这是 PagedAttention 的杀手锏。在并行采样或 Beam Search 中,多个请求可能共享相同的前缀。比如两个请求都是“今天天气真”,只是后续生成不同。
传统方案下,每个请求都要独立存储“今天天气真”的 KV Cache,完全重复。PagedAttention 可以让多个请求的 Block Table 指向相同的物理块。只有不同的部分才分配新块。
避坑指南:我曾经在实现共享时踩过一个坑。共享的物理块是只读的,如果某个请求要修改它(比如在 Prefix Caching 场景下),必须先做 Copy-on-Write。vLLM 内部通过引用计数来管理,当引用计数大于 1 时,写操作会触发块复制。这个机制一定要处理好,否则会出现数据竞争。
2.5 分页下的 Attention 计算
你可能要问:物理块不连续,Attention 怎么算?
嗯,这里有个巧妙的设计。PagedAttention 在计算时,不是一次性加载整个 KV Cache,而是按块加载。每个块内部的数据是连续的,所以可以高效利用 GPU 的并行计算能力。
具体流程是这样的:
- 根据 Block Table,找到当前需要访问的物理块 ID。
- 从显存中加载这个物理块的数据到 GPU 寄存器或共享内存。
- 计算当前 Query 与该块内所有 Key 的注意力分数。
- 加载对应的 Value 块,计算加权和。
- 重复步骤 1-4,直到处理完所有块。
你看,虽然物理上不连续,但计算时是按块顺序进行的。每个块内部连续,块之间通过 Block Table 跳转。这就像你读一本分章节的书,虽然章节在书里不连续(比如第一章在第 3 页,第二章在第 10 页),但你按章节顺序读就行了。
注意:分页会引入额外的访存开销。每次切换块时,都需要查 Block Table 并加载新块。如果块太小,切换太频繁,性能会下降。vLLM 的 16 token/block 是在灵活性和性能之间做的权衡。我在调优时试过 8 和 32,16 确实是最优解。
2.6 一张图看懂 PagedAttention
说了这么多,咱们用一张图来总结 PagedAttention 的核心逻辑。
这张图展示了 PagedAttention 最核心的思想:逻辑连续、物理离散。左边是请求看到的连续逻辑块,中间是 Block Table 做映射,右边是显存中实际存放的物理块。你看物理块 3、5、7、12 完全不挨着,但通过 Block Table,它们被组织成了一个连续的序列。
2.7 小结
PagedAttention 的核心就一句话:把 KV Cache 切成固定大小的块,通过映射表管理,让逻辑连续而物理离散。
这个设计直接解决了三个痛点:
- 显存碎片化 → 固定大小块,零碎片
- 预分配浪费 → 按需分配,用多少分多少
- 重复存储 → 块级别共享,节省显存
我个人觉得,PagedAttention 最妙的地方在于它把操作系统的智慧用到了 GPU 显存管理上。有时候,换个角度看问题,答案就在那里。
一句话总结:PagedAttention = 虚拟内存思想 + KV Cache 管理。它让显存利用率从 40% 提升到了 95% 以上。这就是为什么 vLLM 能成为大模型推理的事实标准。
公众号:蓝海资料掘金营,微信deep3321