
摘要大模型推理的瓶颈常常不是算力而是装不下更多请求的 KV 缓存。vLLM 的 PagedAttention 借用操作系统的虚拟内存思路把 KV 缓存切成固定大小的块按需分配、用块表映射几乎消除了显存碎片还能在多个请求之间共享块。原论文SOSP 2023的实验里相同延迟下吞吐比 FasterTransformer 和 Orca 高 2 到 4 倍。我还写了一个小模拟器把这套机制跑了一遍。背景与问题自回归生成时每个已经处理过的 token 都要保存它在每一层的 Key 和 Value这就是 KV 缓存。它很大论文里 13B 的 OPT 模型单个 token 就要 800KB2 个向量 × 5120 隐层维度 × 40 层 × 2 字节一个 2048 token 的请求最多占 1.6GB。在 40GB 的 A100 上约 65% 的显存给了权重接近 30% 用来存 KV 缓存其余是激活等能同时服务多少请求几乎由这 30% 决定。早期系统把一个请求的 KV 缓存放在一段连续显存里并且按最大长度比如 2048提前预留。论文把浪费分成三类预留浪费为将来要生成的 token 先占着位置但这些位置在整个请求期间别人用不了内部碎片预留的最大长度远大于实际长度用不上的部分白白浪费外部碎片不同请求预留的大小不同分配器留下一堆零散缝隙论文在实验里测到现有系统里只有 20.4% 到 38.2% 的 KV 缓存显存真正存着 token 状态。另外连续存放还让共享变得很难同一个请求的多个采样结果、多个请求共用的系统提示词它们的 KV 本来可以共用却各存一份。核心思路与优势像操作系统一样分页PagedAttention 的类比很直接块是页token 是字节请求是进程。把 KV 缓存切成固定大小的块vLLM 论文默认每块 16 个 token每个请求有一张块表把它的逻辑块按顺序排的映射到物理块显存里的实际位置物理块不必连续用到哪里分配到哪里新 token 写满一个块才再要一个新块这样三类浪费就都被压住了不再提前预留最大长度浪费只可能出现在每个请求最后一个没写满的块里所有块一样大也就没有外部碎片。论文的说法是 KV 缓存显存接近零浪费。代价是注意力内核要先查块表再取数据。论文测得注意力内核延迟比高度优化的 FasterTransformer 高 20% 到 26%但这只影响注意力算子不影响线性层端到端仍然大幅领先论文 2023 年的实验里相同延迟下吞吐比 FasterTransformer 和 Orca 高 2 到 4 倍原因是显存省下来之后一次能塞进更多请求。块级共享和写时复制块表还带来第二个好处共享。物理块上有引用计数多个序列的块表可以指向同一个物理块。并行采样同一个提示词生成多个候选提示词部分的块全部共用束搜索不同候选的前缀大部分相同共享比例更高共享的块如果某个序列要往里写新内容就用写时复制先复制一份给它再改其他序列不受影响论文给出的显存节省并行采样 6.1% 到 9.8%束搜索 37.6% 到 55.2%Alpaca 数据换成对话更长的 ShareGPT 数据分别是 16.2% 到 30.5% 和 44.3% 到 66.3%。显存不够时的调度块是按需增长的所以显存可能中途耗尽。vLLM 的做法是先来先服务显存不够就抢占而且整个序列的块要么全部驱逐要么都不驱逐同一个请求里的多个序列比如束搜索的候选一起抢占、一起恢复。被驱逐的块有两种恢复方式交换搬到 CPU 内存需要时再搬回来重算丢掉之后重新计算这些 token 的 KV论文发现块太小时交换要做大量零碎的 CPU-GPU 小传输开销很大重算不读 KV 块开销与块大小无关。所以小块时重算更划算大块时交换更划算但即便在交换占优的情况下重算也最多比交换慢 20% 左右块大小在 16 到 64 之间时两者的端到端性能相当。块大小怎么选块太小GPU 读 KV 缓存的效率下降块太大内部碎片变多共享的机会也变少。论文在 ShareGPT 数据上测到 16 到 128 都不错在较短序列的 Alpaca 数据上 16 和 32 好、更大的块性能明显变差最终默认 16。后来的演进自动前缀缓存分块之后还能做一件事把已经算好的块缓存下来新请求只要前缀相同就直接复用。vLLM 当前的实现是基于哈希的自动前缀缓存每个块的哈希由前一个块的哈希、本块的 token以及 LoRA 编号、多模态输入哈希、缓存盐值等附加信息共同决定所以哈希能唯一标识这个块 它前面的全部内容。几个要点只缓存写满的块从 v0.11 起默认哈希算法是 sha256降低碰撞风险也可以通过--prefix-caching-hash-algo换成可跨环境复现的 sha256_cbor或更快但非加密、碰撞风险理论上更高的 xxhash / xxhash_cbor多租户场景可以给请求加cache_salt只有盐值相同的请求才能复用缓存避免通过延迟差异推测别人缓存了什么内容另外要注意vLLM 官方仓库里paged_attention.md那篇讲内核实现的设计文档开头就标明是基于原论文的历史文档已不再描述今天 vLLM 的代码。想读当前实现看注意力后端和 KV 缓存管理器的文档更靠谱。面向人群想弄清 vLLM 为什么比朴素推理吞吐高的工程师需要给线上服务估算显存、调max-model-len之类参数的人学过操作系统想看虚拟内存思想怎么用到大模型上的人面试或做技术分享需要讲清 PagedAttention 的人实践步骤第一步先算清楚 KV 缓存多大每个 token 的 KV 大小 2K 和 V× 层数 × KV 头数 × 头维度 × 每个数的字节数。论文里 OPT-13B 用的是传统多头注意力KV 头数等于注意力头数所以大。现在的新模型多用分组查询注意力GQAKV 头数少得多。我用本机的 Qwen2.5-1.5B-Instruct 配置算了一下28 层、2 个 KV 头、头维度 1281536 隐层 ÷ 12 个注意力头、BF16 每个数 2 字节每个 token 只要 28KiB2048 个 token 才 56MiB比 OPT-13B 单个 token 800KB 小了约 28 倍。所以你的模型到底有多吃 KV 缓存要看它的配置不能套用论文里的数字。第二步用一个小模拟器看清分页的效果下面的数字是我写的一个玩具模拟器算出来的不是 vLLM 的实测目的是把机制跑一遍。设定沿用论文的 OPT-13B 数字每 token 800KB给 KV 缓存 12GB约 15000 个 token 位置最大长度 2048块大小 165000 个合成请求提示词和输出长度服从对数正态分布平均总长约 480 个 token每个请求取生命周期中随机一刻的快照。方案真正存着 token 的显存占比同时能容纳的请求数连续预留 2048 个位置17.1%7分页块大小 1697.9%40分页方案里平均每个请求浪费 7.4 个位置就是最后一块没写满的部分。要说明的是这里能容纳 40 个是按当前长度算的快照没有模拟后续增长、抢占和调度真实系统不可能一直满载。但量级的差异能说明问题连续预留把绝大部分显存都耗在了用不上的位置上。核心的块管理只需要引用计数和写时复制BLOCK16classBlockManager:def__init__(self):self.ref{}# 物理块编号 - 引用计数self.next_id0defnew_block(self):bself.next_id self.next_id1self.ref[b]1returnbdeffork(self,table):# 复制块表只增加引用计数forbintable:self.ref[b]1returnlist(table)defappend_token(self,table,n_tokens):# 给序列再放一个 tokenifn_tokens%BLOCK0:# 最后一块写满了 - 新开一块table.append(self.new_block())elifself.ref[table[-1]]1:# 要写的块被共享 - 写时复制self.ref[table[-1]]-1table[-1]self.new_block()用它模拟提示词 300 个 token每个采样再生成 100 个 token的并行采样采样数不共享块共享块节省2503236.0%41004654.0%61506060.0%采样数越多省得越多提示词在总长里占比越高同理也越省趋势和论文一致具体数字和论文不同因为这只是固定长度的玩具设定工作负载完全不同。第三步在 vLLM 里和分页相关的参数下面的参数和默认值取自 2026 年 10 月初 vLLM 主分支的缓存配置你装的版本可能不同以vllm serve --help为准--gpu-memory-utilization这个 vLLM 实例能用的显存比例默认 0.92。启动时在这个额度内扣掉权重、剖析得到的峰值激活等开销剩下的大体划给 KV 缓存的块池。和别的进程共卡时要调低--max-model-len单个请求提示词加输出的最大长度不指定就取模型配置里的上下文长度。块是按需分配的它并不改变块池大小它限制的是单个请求最多占多少块启动时 vLLM 还会检查块池至少装得下一个满长度的请求装不下就报错并给出估算的可用长度设成 -1 或 auto 会自动选一个装得下的长度--block-size块大小不指定时默认 16个别平台或注意力后端会自行调整一般不需要改自动前缀缓存当前版本默认开启系统提示词很长、多轮对话多的场景收益最大--prefix-caching-hash-algo前缀缓存的哈希算法默认 sha256启动时 vLLM 会先剖析模型的显存占用再算出能分出多少个 KV 块日志里会打印类似GPU KV cache size: N tokens, Maximum concurrency for L tokens per request: X x的一行。后半句是按每个请求都用满最大长度算的比较保守用 N 除以你的平均请求长度可以得到一个粗略的并发上限。我的看法PagedAttention 的价值不在于某个精巧的算法而在于换了一个视角KV 缓存的问题本质上是内存管理问题而内存管理问题操作系统几十年前就解决过。把页、页表、引用计数、写时复制、换入换出这一整套搬过来论文在 2023 年的实验里就换到了 2 到 4 倍的吞吐。几点提醒论文的 2 到 4 倍是在 2023 年的模型、基线和负载上测的今天的推理引擎也在吸收分页的思想别把它当成对任何场景都成立的加速比我的模拟器是玩具只证明机制不证明 vLLM 的实际收益新的混合注意力模型滑动窗口、Mamba 等对 KV 缓存管理提出了新要求vLLM 官方已有专门的混合 KV 缓存管理器但文档标注这个功能还处于早期阶段。