6.3 PagedAttention 分页显存原理:vLLM 的革命性突破


文档摘要

6.3 PagedAttention 分页显存原理:vLLM 的革命性突破 操作系统用分页虚拟内存解决了物理内存碎片——把连续的逻辑地址映射到离散的物理页。vLLM 把这个思想搬到了 GPU 显存:把 KV Cache 当成虚拟内存,按块(Block)分配,用 Block Table 映射。这一招让 LLM 推理吞吐飙升 10-20 倍。 6.3.1 灵感来源:操作系统的虚拟内存 回顾 6.2 节的诊断:传统 KV Cache 管理的核心问题是「为每个请求预分配连续的、最大长度的显存」,导致严重的内部碎片与外部碎片。 这个问题在操作系统发展史上早就遇到过——早期操作系统也为每个进程预分配连续内存,碎片严重。

6.3 PagedAttention 分页显存原理:vLLM 的革命性突破

操作系统用分页虚拟内存解决了物理内存碎片——把连续的逻辑地址映射到离散的物理页。vLLM 把这个思想搬到了 GPU 显存:把 KV Cache 当成虚拟内存,按块(Block)分配,用 Block Table 映射。这一招让 LLM 推理吞吐飙升 10-20 倍。

6.3.1 灵感来源:操作系统的虚拟内存

回顾 6.2 节的诊断:传统 KV Cache 管理的核心问题是「为每个请求预分配连续的、最大长度的显存」,导致严重的内部碎片与外部碎片。

这个问题在操作系统发展史上早就遇到过——早期操作系统也为每个进程预分配连续内存,碎片严重。解决之道是 虚拟内存(Virtual Memory)+ 分页(Paging)

  • 把进程的逻辑地址空间切成固定大小的页(Page)
  • 物理内存也切成同样大小的帧(Frame)
  • 页表(Page Table) 把逻辑页映射到物理帧。
  • 按需分配:用到哪页才分配哪页,不必预先全部分配。

这套机制让进程「以为自己有连续大内存」,实际物理内存可以是离散分布的,碎片问题消失。

vLLM 的核心创新——PagedAttention——就是把这套思想搬到 KV Cache:

  • 把每个请求的逻辑 KV Cache 切成固定大小的逻辑块(Logical Block)
  • 物理显存切成同样大小的物理块(Physical Block)
  • Block Table(块表) 把逻辑块映射到物理块。
  • 按需分配:每生成一个 token,按需申请物理块,绝不预分配最大长度。

6.3.2 Block Table:逻辑到物理的映射

Block Table 是 PagedAttention 的核心数据结构。每个请求有一个 Block Table,记录它的逻辑块对应的物理块编号。

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 880 380" font-family="sans-serif" font-size="12"> <text x="440" y="22" text-anchor="middle" font-size="15" font-weight="bold">PagedAttention Block Table:逻辑块映射物理块</text> <!-- 请求 A 逻辑视图 --> <text x="60" y="60" font-weight="bold" fill="#1e40af">请求 A 逻辑 KV Cache(生成 5 token,逻辑上连续)</text> <rect x="60" y="70" width="50" height="30" fill="#dbeafe" stroke="#2563eb"/> <text x="85" y="90" text-anchor="middle">t0</text> <rect x="110" y="70" width="50" height="30" fill="#dbeafe" stroke="#2563eb"/> <text x="135" y="90" text-anchor="middle">t1</text> <rect x="160" y="70" width="50" height="30" fill="#dbeafe" stroke="#2563eb"/> <text x="185" y="90" text-anchor="middle">t2</text> <rect x="210" y="70" width="50" height="30" fill="#dbeafe" stroke="#2563eb"/> <text x="235" y="90" text-anchor="middle">t3</text> <rect x="260" y="70" width="50" height="30" fill="#dbeafe" stroke="#2563eb"/> <text x="285" y="90" text-anchor="middle">t4</text> <!-- 块大小示意 --> <text x="320" y="90" font-size="10" fill="#64748b">每块 4 token</text> <!-- Block Table A --> <text x="60" y="130" font-weight="bold" fill="#1e40af">请求 A Block Table</text> <rect x="60" y="140" width="100" height="30" fill="#fef9c3" stroke="#ca8a04"/> <text x="80" y="160" font-size="10">逻辑块 0</text> <text x="135" y="160" text-anchor="middle" font-weight="bold">→ 物理 7</text> <rect x="60" y="170" width="100" height="30" fill="#fef9c3" stroke="#ca8a04"/> <text x="80" y="190" font-size="10">逻辑块 1</text> <text x="135" y="190" text-anchor="middle" font-weight="bold">→ 物理 3</text> <!-- 物理显存 --> <text x="500" y="60" font-weight="bold" fill="#9d174d">物理显存(离散分布,可复用)</text> <g font-size="10"> <rect x="500" y="70" width="50" height="30" fill="#fce7f3" stroke="#db2777"/> <text x="525" y="90" text-anchor="middle">物理 0</text> <rect x="550" y="70" width="50" height="30" fill="#fce7f3" stroke="#db2777"/> <text x="575" y="90" text-anchor="middle">物理 1</text> <rect x="600" y="70" width="50" height="30" fill="#fce7f3" stroke="#db2777"/> <text x="625" y="90" text-anchor="middle">物理 2</text> <rect x="650" y="70" width="50" height="30" fill="#dcfce7" stroke="#16a34a"/> <text x="675" y="90" text-anchor="middle">物理 3 ✓A</text> <rect x="700" y="70" width="50" height="30" fill="#fce7f3" stroke="#db2777"/> <text x="725" y="90" text-anchor="middle">物理 4</text> <rect x="750" y="70" width="50" height="30" fill="#fce7f3" stroke="#db2777"/> <text x="775" y="90" text-anchor="middle">物理 5</text> <rect x="800" y="70" width="50" height="30" fill="#fce7f3" stroke="#db2777"/> <text x="825" y="90" text-anchor="middle">物理 6</text> <rect x="500" y="105" width="50" height="30" fill="#dcfce7" stroke="#16a34a"/> <text x="525" y="125" text-anchor="middle">物理 7 ✓A</text> <rect x="550" y="105" width="50" height="30" fill="#fce7f3" stroke="#db2777"/> <text x="575" y="125" text-anchor="middle">物理 8</text> <rect x="600" y="105" width="50" height="30" fill="#fce7f3" stroke="#db2777"/> <text x="625" y="125" text-anchor="middle">物理 9</text> </g> <!-- 映射箭头 --> <line x1="160" y1="160" x2="525" y2="105" stroke="#16a34a" stroke-width="2" stroke-dasharray="3,2"/> <line x1="160" y1="190" x2="675" y2="70" stroke="#16a34a" stroke-width="2" stroke-dasharray="3,2"/> <!-- 说明 --> <rect x="60" y="250" width="760" height="110" rx="6" fill="#fef9c3" stroke="#ca8a04"/> <text x="80" y="275" font-weight="bold" fill="#854d0e">关键洞察:</text> <text x="80" y="298" font-size="12" fill="#713f12">· 请求 A 逻辑上「连续」的 KV Cache,物理上分散在物理块 7 和 3</text> <text x="80" y="320" font-size="12" fill="#713f12">· 物理块按需分配,请求只生成 5 token → 只占 2 个物理块,无内部碎片</text> <text x="80" y="342" font-size="12" fill="#713f12">· 请求结束时物理块归还到 free pool,可被其他请求复用,无外部碎片</text> </svg>

Block Table 的几个关键设计:

  • 块大小固定:通常 16 个 token 一个块(vLLM 默认 block_size=16),固定大小便于管理。
  • 逻辑块连续:请求以为自己有连续的 KV Cache,无需感知物理分布。
  • 物理块离散:物理块可在显存任意位置,由 Block Table 记录映射。
  • 按需分配:每生成一个 token,若当前块满了就申请新物理块;不预分配最大长度。

6.3.3 按需分配:消灭内部碎片

对比传统预分配,PagedAttention 的按需分配如何消灭碎片:

维度 传统预分配 PagedAttention
分配时机 请求开始时分配最大长度 每步按需分配
内部碎片 严重(实际长度 << 最大长度) 极小(最多一个块的碎片)
外部碎片 严重(不同请求块大小不一) 不存在(所有块固定大小)
利用率 60%-80% 接近 100%
显存可装请求数 多(吞吐提升)

PagedAttention 的内部碎片最多是一个块——例如块大小 16 token,请求生成了 100 token(6.25 块),那它占 7 个块,第 7 个块只用了 4 token,浪费 12/16 = 75%。但这是「最后一个块的局部浪费」,相比传统「整个请求的浪费」,可忽略不计。

💡 判读:PagedAttention 的显存利用率从 60%-80% 提升到接近 100%,意味着同一张 GPU 能装的请求数(batch 大小)提升 20%-60%,这直接转化为吞吐的同等提升。这是 vLLM 比 HuggingFace Transformers 快 10-20 倍的核心原因之一。

6.3.4 物理块复用:消灭外部碎片

PagedAttention 还消灭了外部碎片。传统管理中,不同请求的 KV Cache 块大小不一,请求结束后留下大小不一的空洞,新请求可能装不进。PagedAttention 中所有物理块都是同样大小,请求结束后归还到 free pool(空闲池),新请求随时可取——像停车场管理标准化车位,永远不会「装不进」。

6.3.5 Continuous Batching:消灭 Static Batching 的浪费

PagedAttention 解决了「显存碎片」,但要解决「Static Batching 后半程 GPU 空闲」的问题,还需要 Continuous Batching(连续批处理,也叫 Iteration-Level Batching 或 Inflight Batching)

回顾 6.2 节的 Static Batching:必须等所有请求完成才返回,导致已完成的请求陪最慢的请求空等。

Continuous Batching 的核心思想是「每个时间步(iteration)都可以动态调整 batch 成员」:

  • 某个请求生成完毕 → 立即从 batch 中移出,归还 KV Cache 块。
  • 新请求到来 → 立即加入 batch,分配 KV Cache 块。
  • 不必等整批完成,每步独立调度。

时间轴示意:

  • 第 0 步:A、B 在 batch。
  • 第 2 步:C 加入(A、B、C 三请求并发)。
  • 第 5 步:A 完成、移出;D 加入(B、C、D)。
  • 第 8 步:C 完成、移出;E 加入(B、D、E)。
  • ……

每个时间步 batch 成员都在变化,GPU 始终满载,没有「陪最慢请求空等」的浪费。

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 880 280" font-family="sans-serif" font-size="12"> <text x="440" y="22" text-anchor="middle" font-size="15" font-weight="bold">Static vs Continuous Batching 的 GPU 利用率</text> <!-- Static --> <text x="40" y="60" font-weight="bold" fill="#dc2626">Static Batching:</text> <rect x="40" y="70" width="800" height="40" fill="#fecaca" stroke="#dc2626"/> <rect x="40" y="75" width="200" height="30" fill="#dc2626"/> <text x="140" y="95" text-anchor="middle" font-size="10" fill="#fff">请求 A 占用(前 50 步)</text> <rect x="40" y="75" width="800" height="30" fill="#dc2626" opacity="0.6"/> <text x="440" y="95" text-anchor="middle" font-size="10" fill="#fff">请求 B 占用(满 200 步)</text> <text x="240" y="130" font-size="10" fill="#dc2626">A 在第 50 步完成,但显存占着等 B,GPU 后 75% 时间低载</text> <!-- Continuous --> <text x="40" y="170" font-weight="bold" fill="#16a34a">Continuous Batching:</text> <rect x="40" y="180" width="800" height="40" fill="#dcfce7" stroke="#16a34a"/> <rect x="40" y="185" width="200" height="30" fill="#16a34a"/> <rect x="240" y="185" width="150" height="30" fill="#16a34a" opacity="0.8"/> <rect x="390" y="185" width="180" height="30" fill="#16a34a" opacity="0.6"/> <rect x="570" y="185" width="140" height="30" fill="#16a34a" opacity="0.9"/> <rect x="710" y="185" width="130" height="30" fill="#16a34a" opacity="0.7"/> <text x="440" y="240" font-size="10" fill="#16a34a">A 完成立即移出,C/D/E 依次填入,GPU 始终满载</text> </svg>

⚠️ 关键挑战:Continuous Batching 的实现难点在于「变长 batch 的并行计算」——每步 batch 内请求数与序列长度都在变化,注意力计算要适配这种动态形状。PagedAttention 的 Block Table 设计正好解决这个问题:通过 Block Table,注意力 kernel 可以高效地访问每个请求离散分布的 KV Cache,而无需关心它们是否在连续显存。PagedAttention 与 Continuous Batching 是天生一对——前者让显存可离散管理,后者让 batch 可动态调整,二者结合才实现了 vLLM 的高吞吐。

6.3.6 PagedAttention 的注意力计算

理解 PagedAttention 在 GPU kernel 层面如何工作:传统注意力计算假设 K/V 在连续显存,可以一次性读取。PagedAttention 中 K/V 分散在物理块里,注意力 kernel 要:

  1. 读 Block Table,找到当前请求的物理块列表。
  2. 按物理块顺序读取 K/V(每个块 16 token)。
  3. 计算注意力得分(Q × K)。
  4. 按 Block Table 写回新 token 的 K/V(追加到当前块,或申请新块)。

vLLM 在 CUDA kernel 中实现了这套逻辑,性能接近连续内存的注意力(开销 <5%)。这是 PagedAttention 能实用的工程基础——如果 kernel 太慢,分页的收益就被开销吃掉了。

6.3.7 Prefix Caching:Block Table 的意外红利

PagedAttention 的 Block Table 还带来了一个意外红利——Prefix Caching(前缀缓存)/ Shared Prefix

考虑 RAG 应用:所有请求都以「同一个 system prompt + 知识库文档」开头,只有最后的 user query 不同。传统方式下,每个请求都要为这个共同前缀独立计算与存储 KV Cache,重复浪费。

PagedAttention 让多个请求共享同一组物理块——只要它们的逻辑块 0..N 都映射到同一组物理块即可。

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 240" font-family="sans-serif" font-size="12"> <text x="360" y="22" text-anchor="middle" font-size="15" font-weight="bold">Prefix Caching:多请求共享前缀块</text> <!-- 共享前缀 --> <rect x="40" y="60" width="500" height="40" rx="4" fill="#fef9c3" stroke="#ca8a04"/> <text x="290" y="85" text-anchor="middle">共同前缀(system prompt + 文档)→ KV 存于物理块 7, 3, 1</text> <!-- 请求 A --> <rect x="40" y="120" width="600" height="40" rx="4" fill="#dbeafe" stroke="#2563eb"/> <text x="50" y="145" font-size="11">请求 A:[共享前缀 → 物理 7,3,1] + [独立 query A → 物理 8]</text> <!-- 请求 B --> <rect x="40" y="170" width="600" height="40" rx="4" fill="#dcfce7" stroke="#16a34a"/> <text x="50" y="195" font-size="11">请求 B:[共享前缀 → 物理 7,3,1] + [独立 query B → 物理 9]</text> <text x="40" y="230" font-size="11" fill="#16a34a" font-style="italic">前缀 KV 只算一次、只存一份,多请求复用 → 节省计算 + 显存</text> </svg>

Prefix Caching 对以下场景价值巨大:

  • RAG:共同知识库前缀。
  • Few-shot:共同的 few-shot 示例。
  • 系统 prompt:所有请求共享的 system message。
  • 多轮对话:历史轮次的 KV Cache 复用。

vLLM 默认开启 Prefix Caching,是它在这些场景下吞吐进一步领先的原因。

6.3.8 性能数据:vLLM 的实际收益

vLLM 论文与社区实测数据:

场景 HuggingFace Transformers vLLM 提升
LLaMA-7B 吞吐 基线 14-24x 14-24 倍
LLaMA-13B 吞吐 基线 10-20x 10-20 倍
显存利用率 60%-80% ~96% +20%-30%
同硬件 batch 上限 基线 数倍 显著

💡 判读:vLLM 的 10-20 倍提升并非来自「单次推理更快」,而是来自「同硬件能并发处理更多请求」——通过 PagedAttention 让显存利用率从 60%-80% 提升到 96%,可装的 batch 数倍增加,吞吐随之数倍提升。这是「系统级优化」的力量,而非「算法级优化」。

6.3.9 PagedAttention 的代价

PagedAttention 不是免费的,它有几个代价:

  • kernel 复杂度:分页注意力 kernel 比连续注意力复杂,实现与优化难度大。
  • 小块开销:块大小越小越灵活,但 Block Table 越大、kernel 跳转越多;块大小越大越省 Table,但灵活性下降。vLLM 默认 16 是权衡值。
  • Block Table 内存:Block Table 本身要占少量显存(每块一个映射条目)。
  • 共享前缀的回收复杂:当多个请求共享前缀块时,要引用计数,最后一个引用释放才能回收。

但相比它带来的 10-20 倍吞吐提升,这些代价微不足道。这也是为什么 PagedAttention 已成为 LLM 推理的事实标准——不仅 vLLM,TGI、TensorRT-LLM、SGLang 等都采用了类似机制。

6.3.10 PagedAttention 的更广意义

PagedAttention 的成功证明了一件事:经典系统设计思想(虚拟内存、分页)在新硬件(GPU)场景仍有强大生命力。它启发了一系列后续工作:

  • PagedAttention for 长上下文:用分页管理超长上下文的 KV Cache。
  • ** disaggregated memory**:把 KV Cache 卸载到 CPU 内存或 NVMe(vLLM 的 prefix cache 跨节点扩展)。
  • KV Cache 量化:分页后更易做 KV Cache 的 INT8/FP8 量化(7.2 节)。
  • PD 分离:把 Prefill 与 Decode 的 KV Cache 分离到不同节点(7.4 节)。

PagedAttention 不只是一个推理优化技术,它是「LLM 推理系统化」的奠基性工作。它让业界意识到,LLM 推理的瓶颈不在算法而在系统,从此 LLM 推理优化成为一个独立且活跃的研究方向。

本节小结

  • PagedAttention 的灵感来自操作系统虚拟内存:把 KV Cache 切成固定大小的块,用 Block Table 把逻辑块映射到物理块。
  • Block Table 是核心数据结构:每个请求一份,记录逻辑块 → 物理块的映射。
  • 按需分配消灭内部碎片(最多一个块的浪费),物理块复用消灭外部碎片,显存利用率从 60%-80% 提升到 ~96%。
  • Continuous Batching 与 PagedAttention 是天生一对:前者让 batch 动态调整,后者让显存可离散管理,二者结合实现 vLLM 高吞吐。
  • 注意力 kernel 按 Block Table 读取分散的 K/V,开销 <5%,性能接近连续内存。
  • Block Table 带来 Prefix Caching 红利:多请求共享前缀块,RAG/Few-shot/系统 prompt 场景吞吐进一步领先。
  • vLLM 实测吞吐比 HuggingFace Transformers 高 10-20 倍,主要来自「同硬件并发更多请求」。
  • 代价是 kernel 复杂度、块大小权衡、Block Table 开销、共享前缀的引用计数,但相对收益可忽略。
  • PagedAttention 启发了 KV Cache 量化、PD 分离、长上下文优化等后续工作,是 LLM 推理系统化的奠基。

下一节《6.4 vLLM 与 TGI 对比与工程取舍》将辨析两大主流 LLM 推理引擎的差异。


发布者: 作者: 灏天文库 转发
评论区 (0)
U