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 的并行计算能力。

具体流程是这样的:

  1. 根据 Block Table,找到当前需要访问的物理块 ID。
  2. 从显存中加载这个物理块的数据到 GPU 寄存器或共享内存。
  3. 计算当前 Query 与该块内所有 Key 的注意力分数。
  4. 加载对应的 Value 块,计算加权和。
  5. 重复步骤 1-4,直到处理完所有块。

你看,虽然物理上不连续,但计算时是按块顺序进行的。每个块内部连续,块之间通过 Block Table 跳转。这就像你读一本分章节的书,虽然章节在书里不连续(比如第一章在第 3 页,第二章在第 10 页),但你按章节顺序读就行了。

注意:分页会引入额外的访存开销。每次切换块时,都需要查 Block Table 并加载新块。如果块太小,切换太频繁,性能会下降。vLLM 的 16 token/block 是在灵活性和性能之间做的权衡。我在调优时试过 8 和 32,16 确实是最优解。

2.6 一张图看懂 PagedAttention

说了这么多,咱们用一张图来总结 PagedAttention 的核心逻辑。

PagedAttention 核心架构图 逻辑视图(请求视角) Logical Block 0 Logical Block 1 Logical Block 2 Logical Block 3 Block Table → Phys 7 → Phys 3 → Phys 12 → Phys 5 物理视图(显存视角) Physical Block 3 Physical Block 5 Physical Block 7 Physical Block 12 逻辑块连续 → Block Table 映射 → 物理块离散存放 解决显存碎片化,实现按需分配与高效共享

这张图展示了 PagedAttention 最核心的思想:逻辑连续、物理离散。左边是请求看到的连续逻辑块,中间是 Block Table 做映射,右边是显存中实际存放的物理块。你看物理块 3、5、7、12 完全不挨着,但通过 Block Table,它们被组织成了一个连续的序列。

2.7 小结

PagedAttention 的核心就一句话:把 KV Cache 切成固定大小的块,通过映射表管理,让逻辑连续而物理离散

这个设计直接解决了三个痛点:

  • 显存碎片化 → 固定大小块,零碎片
  • 预分配浪费 → 按需分配,用多少分多少
  • 重复存储 → 块级别共享,节省显存

我个人觉得,PagedAttention 最妙的地方在于它把操作系统的智慧用到了 GPU 显存管理上。有时候,换个角度看问题,答案就在那里。

一句话总结:PagedAttention = 虚拟内存思想 + KV Cache 管理。它让显存利用率从 40% 提升到了 95% 以上。这就是为什么 vLLM 能成为大模型推理的事实标准。


公众号:蓝海资料掘金营,微信deep3321