6.3 PagedAttention 分页显存原理:vLLM 的革命性突破 操作系统用分页虚拟内存解决了物理内存碎片——把连续的逻辑地址映射到离散的物理页。vLLM 把这个思想搬到了 GPU 显存:把 KV Cache 当成虚拟内存,按块(Block)分配,用 Block Table 映射。这一招让 LLM 推理吞吐飙升 10-20 倍。 6.3.1 灵感来源:操作系统的虚拟内存 回顾 6.2 节的诊断:传统 KV Cache 管理的核心问题是「为每个请求预分配连续的、最大长度的显存」,导致严重的内部碎片与外部碎片。 这个问题在操作系统发展史上早就遇到过——早期操作系统也为每个进程预分配连续内存,碎片严重。
操作系统用分页虚拟内存解决了物理内存碎片——把连续的逻辑地址映射到离散的物理页。vLLM 把这个思想搬到了 GPU 显存:把 KV Cache 当成虚拟内存,按块(Block)分配,用 Block Table 映射。这一招让 LLM 推理吞吐飙升 10-20 倍。
回顾 6.2 节的诊断:传统 KV Cache 管理的核心问题是「为每个请求预分配连续的、最大长度的显存」,导致严重的内部碎片与外部碎片。
这个问题在操作系统发展史上早就遇到过——早期操作系统也为每个进程预分配连续内存,碎片严重。解决之道是 虚拟内存(Virtual Memory)+ 分页(Paging):
这套机制让进程「以为自己有连续大内存」,实际物理内存可以是离散分布的,碎片问题消失。
vLLM 的核心创新——PagedAttention——就是把这套思想搬到 KV Cache:
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 的几个关键设计:
block_size=16),固定大小便于管理。对比传统预分配,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 倍的核心原因之一。
PagedAttention 还消灭了外部碎片。传统管理中,不同请求的 KV Cache 块大小不一,请求结束后留下大小不一的空洞,新请求可能装不进。PagedAttention 中所有物理块都是同样大小,请求结束后归还到 free pool(空闲池),新请求随时可取——像停车场管理标准化车位,永远不会「装不进」。
PagedAttention 解决了「显存碎片」,但要解决「Static Batching 后半程 GPU 空闲」的问题,还需要 Continuous Batching(连续批处理,也叫 Iteration-Level Batching 或 Inflight Batching)。
回顾 6.2 节的 Static Batching:必须等所有请求完成才返回,导致已完成的请求陪最慢的请求空等。
Continuous Batching 的核心思想是「每个时间步(iteration)都可以动态调整 batch 成员」:
时间轴示意:
每个时间步 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 的高吞吐。
理解 PagedAttention 在 GPU kernel 层面如何工作:传统注意力计算假设 K/V 在连续显存,可以一次性读取。PagedAttention 中 K/V 分散在物理块里,注意力 kernel 要:
vLLM 在 CUDA kernel 中实现了这套逻辑,性能接近连续内存的注意力(开销 <5%)。这是 PagedAttention 能实用的工程基础——如果 kernel 太慢,分页的收益就被开销吃掉了。
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 对以下场景价值巨大:
vLLM 默认开启 Prefix Caching,是它在这些场景下吞吐进一步领先的原因。
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 数倍增加,吞吐随之数倍提升。这是「系统级优化」的力量,而非「算法级优化」。
PagedAttention 不是免费的,它有几个代价:
但相比它带来的 10-20 倍吞吐提升,这些代价微不足道。这也是为什么 PagedAttention 已成为 LLM 推理的事实标准——不仅 vLLM,TGI、TensorRT-LLM、SGLang 等都采用了类似机制。
PagedAttention 的成功证明了一件事:经典系统设计思想(虚拟内存、分页)在新硬件(GPU)场景仍有强大生命力。它启发了一系列后续工作:
PagedAttention 不只是一个推理优化技术,它是「LLM 推理系统化」的奠基性工作。它让业界意识到,LLM 推理的瓶颈不在算法而在系统,从此 LLM 推理优化成为一个独立且活跃的研究方向。
下一节《6.4 vLLM 与 TGI 对比与工程取舍》将辨析两大主流 LLM 推理引擎的差异。