从操作系统到LLM:PagedAttention如何用虚拟内存思维提升10倍推理效率?
如果你最近在部署大语言模型,尤其是处理高并发、长序列的推理请求,大概率已经听过vLLM这个名字。它几乎成了高效推理的代名词,而支撑其性能飞跃的核心,正是PagedAttention。这个听起来有些“复古”的技术,将计算机操作系统里沉淀了几十年的虚拟内存管理智慧,巧妙地移植到了大模型推理的内存管理难题上。
我最初接触PagedAttention时,第一反应是“这想法太妙了”。我们团队在部署一个内部对话系统时,曾饱受显存碎片化的折磨——明明显存总量还有不少,但新的长对话请求就是分配不到连续空间,导致服务频繁报错。尝试了各种内存池优化收效甚微,直到我们把引擎切换到vLLM,吞吐量直接翻了近三倍,而且服务稳定性大幅提升。这背后的功臣,就是PagedAttention那套源自操作系统的分页管理哲学。
这篇文章,我将带你深入PagedAttention的设计内核。我们不会停留在概念复述,而是会从系统架构师的视角,剖析它如何将虚拟内存的“页表”、“物理块”、“按需分配”等经典思想,转化为解决KV Cache管理困境的工程方案。更重要的是,我会结合NUMA架构、GPU显存特性等硬件知识,和你一起探讨页面大小、块表设计等关键参数对实际吞吐量的影响,并给出面向不同硬件环境的配置优化建议。无论你是算法工程师希望理解推理优化的底层逻辑,还是系统架构师正在为生产环境选型,这些实战经验都能帮你避开我们踩过的坑。
1. 内存管理的困局:为什么传统KV Cache成了推理瓶颈?
要理解PagedAttention的革命性,首先得看清它要解决什么问题。在大语言模型的自回归生成过程中,KV Cache(Key-Value缓存) 是提升推理效率的关键技术。模型每生成一个token,都会计算其对应的Key和Value向量,并将它们缓存起来,供后续生成步骤使用。这样,在计算注意力时,就无需为历史token重复计算K和V,节省了大量计算开销。
然而,这套看似完美的机制,在工程落地时却遇到了严峻的内存管理挑战。传统的KV Cache管理方式,可以概括为“连续预分配”模式:当一个推理请求到来时,系统会根据模型支持的最大序列长度(比如2048),预先分配一块连续的GPU显存,用于存放该请求整个生命周期内可能产生的所有KV对。
这种模式带来了三个致命问题,我称之为“内存三宗罪”。
第一宗罪:内部碎片(Internal Fragmentation)严重。 绝大多数请求的实际生成长度远小于最大长度。比如一个简单的问答,可能只需要生成几十个token。但系统已经为它预留了2048个token的空间,剩余的大量内存就被白白闲置,无法被其他请求使用。在我们早期的监控数据里,这种浪费平均占到了预分配空间的60%以上。
第二宗罪:外部碎片(External Fragmentation)难以消除。 当大量并发请求进入系统,每个请求占用一块连续内存,释放的时间又各不相同,显存中就会形成一堆“内存孤岛”。如下图所示,虽然总的空闲显存可能还有很多,但它们被切割成了许多不连续的小块。当一个需要较长连续内存的新请求(比如一个长文档总结任务)到来时,即使空闲总量足够,也可能找不到一块足够大的连续空间来满足它。
传统连续分配示意图:
显存布局:[请求A: 2048块] [空闲: 512块] [请求B: 1024块] [空闲: 256块] [请求C: 512块]
新请求D需要1024块连续空间 -> 失败!尽管总空闲(512+256=768)小于需求,但物理上不连续。
第三宗罪:内存共享几乎不可能。 在并行采样(Parallel Sampling)、束搜索(Beam Search)等高级解码策略中,多个输出序列共享相同的输入前缀(prompt)。在连续分配模式下,每个序列都必须独立保存一份完整的KV Cache副本,包括完全相同的共享前缀部分。这造成了巨大的内存冗余。例如,用同一个prompt并行生成8个候选回答,内存占用就直接变成8倍。
这些问题叠加的后果,就是GPU显存的有效利用率极低。根据vLLM论文中的实测数据,在一些传统推理系统中,真正用于存储有效KV Cache的显存占比最低只有**20%**左右,其余80%都浪费在了碎片和冗余上。这直接限制了系统的批处理大小(Batch Size)和并发处理能力,成为提升推理吞吐量的核心瓶颈。
注意:这里说的“传统系统”包括早期基于Hugging Face Transformers和FasterTransformer的一些实现。它们并非没有优化,但在内存管理的核心思想上,仍然受限于连续内存分配的范式。
2. 灵感的跨界:虚拟内存分页如何照亮LLM推理?
面对内存碎片化的顽疾,计算机领域早有成熟的解决方案——操作系统的虚拟内存(Virtual Memory) 和分页(Paging) 机制。PagedAttention的核心创新,正是将这套历经数十年考验的思想,创造性地应用到了KV Cache的管理中。
让我们先快速回顾一下操作系统的分页机制。在早期,程序需要直接操作物理内存地址,这带来了内存保护和碎片化等问题。虚拟内存的引入,为每个进程提供了一个独立的、连续的虚拟地址空间。操作系统通过一个叫做页表(Page Table) 的数据结构,将虚拟页面映射到物理内存帧(Frame)上。关键点在于:
- 页面大小固定:虚拟和物理空间都被划分为固定大小的块(如4KB)。
- 非连续映射:一个进程的连续虚拟页面,可以映射到物理内存中任意不连续的帧上。
- 按需分配:并非一开始就分配所有物理帧,而是当进程真正访问某个虚拟页面时,再分配物理帧并建立映射(按需调页)。
PagedAttention几乎完整地借鉴了这套范式:
- KV Cache Block 对应物理内存帧。它是GPU显存中固定大小的存储单元,用于存放若干个token的Key和Value向量。
- 逻辑Block序列 对应虚拟地址空间。每个推理请求(序列)的KV Cache在逻辑上被视作一系列连续的Block。
- Block Table(块表) 对应页表。它为每个序列维护一个映射表,记录每个逻辑Block对应哪个物理Block。
- Block Manager(块管理器) 对应操作系统的内存管理器。它负责物理Block的分配、回收和全局管理。
这种映射关系带来了根本性的优势:
- 消除外部碎片:物理Block是固定大小的标准单元,分配和回收都以Block为单位。由于所有Block大小相同,就不会产生外部碎片——任何空闲Block都可以满足新请求的需求。
- 高效支持动态增长:序列的KV Cache可以按需增长。当序列需要存储新的token时,只需向Block Manager申请一个新的物理Block,并在自己的Block Table中建立映射即可。无需预先分配大块连续内存。
- 天然支持内存共享:多个序列的Block Table可以指向同一个物理Block。这对于共享前缀的场景至关重要,只需增加该物理Block的引用计数即可,实现了真正的零拷贝共享。
下面的表格清晰地对比了传统方式与PagedAttention在内存管理上的核心差异:
| 特性维度 | 传统连续KV Cache管理 | PagedAttention分页管理 |
|---|---|---|
| 分配单元 | 整个序列的连续大内存块 | 固定大小的Block(如16个token) |
| 物理布局 | 必须连续 | 可以非连续,分散在显存中 |
| 内存碎片 |

1822

被折叠的 条评论
为什么被折叠?



