vLLM 原理浅读:PagedAttention 为什么能大幅提升大模型吞吐
1. 引言:大模型推理的隐形瓶颈
2023 年之后,大语言模型(LLM)的参数量从几十亿一路飙到几千亿。大家的目光大多集中在「模型有多大、效果有多好」,但真正把模型部署上线时,一个更现实的问题摆在面前:同一个 GPU,为什么别人能扛住每秒几百个请求,而你的服务几十个并发就开始排队?
答案往往不在模型本身,而在推理服务的内存管理。vLLM 团队在 2023 年提出的 PagedAttention 机制,正是针对这个隐形瓶颈的一次关键突破。本文不深入内核代码,只用通俗的类比,讲清楚三件事:
- 为什么传统的 KV 缓存管理会浪费大量显存、拖慢并发?
- PagedAttention 如何借鉴操作系统的「分页」思想来管理 KV 缓存?
- 为什么这种设计让 vLLM 特别适合高并发推理场景?
读完你会理解:vLLM 的高吞吐,不是靠魔法,而是靠着把显存当成操作系统管理虚拟内存一样去精细管理。
2. 先理解:KV 缓存到底存了什么
在讲 PagedAttention 之前,我们先花两分钟弄清楚 KV 缓存是什么。这部分不需要任何内核知识,只要理解「模型是逐 token 生成」这件事。
2.1 自回归生成:一次只吐一个字
大模型生成文本时,是一个 token 一个 token 依次进行的。比如你让它输出「今天天气很好」,它不会一次性蹦出整句话,而是:
- 先看用户输入,预测出「今」;
- 把「今」拼到输入后面,再预测出「天」;
- 把「今天」拼到后面,再预测出「天」;
- 以此类推,直到输出终止符。
这个过程叫自回归生成。看似效率不高,却是目前主流大模型的通用范式。
2.2 每次预测都要重复计算?需要缓存
关键问题来了:每一步预测,模型都要对「之前所有的 token」做一次注意力计算。第一步处理 1 个 token,第二步处理 2 个,第三步处理 3 个……如果每次都从头算一遍,计算量会随着输出长度急剧膨胀。
KV 缓存(Key-Value Cache)就是为此而生的。 它的思路是:每次计算注意力时,把中间产生的 Key 和 Value 张量保存下来。下一步预测时,旧 token 的 KV 直接复用,只需要为新 token 计算一份新的 KV 拼接上去即可。
用一个生活类比:KV 缓存就像你做一道复杂菜时,把「备好的菜、调好的酱汁」放进冰箱。下一顿做同款菜时,不用从洗菜开始,直接从冰箱拿出来回锅就行。
由于 LLM 推理往往同时服务几十上百个请求,而每个请求都有自己独立的 KV 缓存,KV 缓存很快成为显存占用的大头——有时甚至比模型权重本身还大。如何高效管理这块显存,就成了吞吐量的命门。
3. 原生 KV 缓存的三宗罪
在 vLLM 出现之前,主流推理框架(如 Hugging Face Transformers、早期的 PyTorch 推理脚本)对 KV 缓存的管理方式相当「粗放」。我们可以总结为三大痛点。
3.1 为最大长度预分配:大量显存被浪费
传统做法是:在请求到来时,按照模型允许的最大序列长度,为这个请求一次性预分配一整块连续的 KV 缓存空间。
举个例子:假设模型最大支持 2048 个 token,每个 token 的 KV 缓存占 2MB 显存,那么每个请求一来就预留 2048 × 2MB = 4GB。
但实际场景中,绝大多数请求的输出可能只有一两百个 token。这就意味着:预分配的 4GB 里,真正用到的可能只有 200MB,剩下 3.8GB 全在闲置。 更憋屈的是,这些闲置显存还不能给别人用——因为它是按请求独占的一块连续空间。
这就是典型的内部碎片问题:分配的空间大于实际需求。
3.2 连续空间限制:小空位用不上
传统分配要求 KV 缓存必须占据一段连续的显存空间。随着不同请求不断创建和销毁,显存中会出现很多零零散散的小空隙。
比如显存中有三段 100MB、200MB、150MB 的空闲块,但一个新请求需要 400MB 连续空间。理论上空闲空间加起来有 450MB,足够用;但因为没有一段连续的 400MB,请求无法分配,只能干等。
这就好比停车:停车场里有很多分散的小空位,但一辆大巴需要连续五个车位,就是停不进去。这叫外部碎片问题。
3.3 无法跨请求复用:同样的前缀各存一份
大模型推理中有一个极常见的场景:多个请求共享同一个长前缀。比如:
- 提示词模板(系统提示词)个个请求都一样;
- 批量请求里,同一个文档作为上下文发给多个不同问题;
- 多轮对话里,前面的历史消息在多轮中重复出现。
这些前缀对应的 KV 缓存,按理说算一次就够了,后面的请求可以直接复用。但原生实现里,每个请求各存各的 KV,同样的前缀被重复计算、重复存储,既浪费算力又浪费显存。
4. PagedAttention 的核心思想:像操作系统一样管理显存
vLLM 团队提出的 PagedAttention,用一句话概括就是:不要为每个请求预留一整块连续显存,而是把 KV 缓存切成固定大小的「小页」,按需分配、动态映射。
如果你学过操作系统,这个思路会非常亲切——它就是虚拟内存分页机制在 GPU 显存上的翻版。
4.1 固定大小的 KV 块:显存里的「页」
PagedAttention 把 KV 缓存拆分成若干个固定大小的 KV 块(KV Block)。每个块能容纳固定数量 token 的 KV 数据,比如一个块存 16 个 token。
注意这里的「固定大小」很关键。因为所有块大小一致,显存中任何空闲块都可以被任何请求使用,天然消除了外部碎片——就像停车场把车位全部画成标准大小,大巴不再要求连续车位,而是可以拆成几段停到不同区域。
4.2 逻辑到物理的映射:请求看到的连续,实际可以分散
每个请求维护一个块表(Block Table),记录「我的第 1 个逻辑 KV 块,实际存在物理显存的哪一块;第 2 个逻辑块,又在物理显存的哪一块……」。
请求在计算注意力时,看到的是逻辑上连续的序列;而底层真实存储可以是分散在显存各处的物理块。计算内核通过块表做映射,按块粒度读取 KV 数据。
这就是操作系统里「逻辑地址」与「物理地址」的对应关系:程序以为自己拥有一段连续内存,实际上数据可能分散在物理内存的各个页框里,靠页表把它们串起来。
4.3 按需增长:用多少,分配多少
有了按块分配的能力,一个请求不再需要一开始就预分配最大长度的空间。它一开始只分到少量块,随着输出 token 逐渐增多,一边生成、一边申请新的块。
如果请求只输出了 100 个 token,那它就只占用能容纳 100 个 token 的块。所有未使用的显存都还留在公共池子里,可以服务其他请求。内部碎片问题也迎刃而解——只有在最后一个块没被填满时,才产生一点点浪费(平均半个块大小,可以忽略不计)。
5. 传统 KV 缓存与 PagedAttention 对比示意
下面这张图直观对比了两种内存管理方式。传统方式为每个请求预留一整段连续显存(哪怕用不完),且不同请求的前缀各自重复存储;PagedAttention 则把显存切成标准块,按需分配,并让多个请求共享相同前缀的块。
再用一个表格把关键差异并排放在一起:
| 对比维度 | 传统 KV 缓存 | PagedAttention |
|---|---|---|
| 分配单位 | 每个请求一段连续显存 | 固定大小的 KV 块 |
| 分配时机 | 请求到来时按最大长度预分配 | 按实际需求动态增长 |
| 内部碎片 | 严重(预留远大于实际使用) | 几乎消除(最多浪费半块) |
| 外部碎片 | 存在(连续空间难凑齐) | 消除(任意空闲块都可用) |
| 跨请求复用 | 不支持(各存各的) | 支持(相同前缀共享物理块) |
| 显存利用率 | 通常只有 20%–40% | 可以接近 4 倍提升 |
上表中的「4 倍提升」不是夸张:vLLM 论文里的实验表明,在相同显存条件下,PagedAttention 能将可同时服务的请求数量提升 2–4 倍,吞吐相应大幅增加。
6. 复用机制:Copy-on-Write 的妙用
分页解决了「空间浪费」,但 PagedAttention 还有一个更聪明的设计:相同前缀的 KV 块可以在多个请求之间共享。
6.1 场景:同一前缀,不同问题
设想一个典型的企业级应用:系统提示词长约 500 token,所有用户请求都带上它。传统方式下,100 个并发请求要把这 500 token 的 KV 各存 100 份。而 PagedAttention 的做法是:这 500 token 只需要计算一次、存储一份,100 个请求的块表里都指向同一批物理块。
6.2 写入时才复制:Copy-on-Write(写时复制)
细心的读者会问:不同请求的后续输出不同,会不会把共享的块改坏?
答案是不会,因为这里用到了 Copy-on-Write(写时复制) 机制:
- 当多个请求共享同一批物理 KV 块时,这些块是只读的;
- 如果一个请求需要对某个共享块写入新数据(比如新增 token 恰好落在该块的边界内),系统会先把这个块复制一份,让该请求在新副本上修改,原块保持不动,继续服务其他请求;
- 只有「被写入」的那一个块发生了复制,而不是整条序列。
这个机制和操作系统进程中「父子进程共享内存页、直到某个进程写数据才复制该页」的逻辑完全一致。通过 COW,共享前缀的安全性和复用效率同时得到了保证。
这也解释了为什么 vLLM 在处理大批量同前缀请求(如批量问题回答、带长系统提示词的服务)时优势格外明显:前缀越长、重复请求越多,省下的计算和显存就越可观。
7. 部署示例:快速上手 vLLM
理论讲完,来看看实际部署有多简单。vLLM 安装后,一条命令就能启动一个兼容 OpenAI 接口的高性能推理服务。
7.1 安装
推荐在 Python 3.9+ 环境使用 pip 安装(示例版本会随上游更新,安装时以官方文档为准):
pip install vllm
如果使用 CUDA,请确保驱动与 vLLM 要求的 CUDA 版本匹配;生产环境建议使用官方 Docker 镜像,避免依赖冲突。
7.2 启动一个 OpenAI 兼容服务
下面以 Meta 的 Llama 3 8B 模型为例,启动一个推理服务:
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Meta-Llama-3-8B-Instruct \
--max-model-len 4096 \
--gpu-memory-utilization 0.9
参数说明:
--model:模型名称或本地路径,支持 Hugging Face 上的模型;--max-model-len:模型最大序列长度,影响块表和显存规划;--gpu-memory-utilization:允许 vLLM 使用的显存比例,剩下的留给 CUDA 上下文等开销。
启动后,服务默认监听在 http://localhost:8000,提供 /v1/completions 和 /v1/chat/completions 两个标准接口。
7.3 用 curl 发一个对话请求
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "meta-llama/Meta-Llama-3-8B-Instruct",
"messages": [
{"role": "system", "content": "你是一个 Linux 命令行专家,回答要简洁。"},
{"role": "user", "content": "如何查看磁盘剩余空间?"}
],
"temperature": 0.7,
"max_tokens": 256
}'
服务端会返回标准的 OpenAI 格式响应。由于接口完全兼容,你也可以用任何 OpenAI SDK(如 openai Python 包)把 base_url 指向本地服务即可无缝接入:
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="EMPTY")
resp = client.chat.completions.create(
model="meta-llama/Meta-Llama-3-8B-Instruct",
messages=[
{"role": "system", "content": "你是一个 Linux 命令行专家。"},
{"role": "user", "content": "如何查看磁盘剩余空间?"},
],
)
print(resp.choices[0].message.content)
7.4 开启前缀缓存的收益
上述示例中,每次请求都会带上同样的 system prompt。vLLM 默认启用前缀缓存(prefix caching)后,系统提示词部分的 KV 在第一个请求后就被复用,后续请求直接跳过这部分计算。请求越多、系统提示词越长,节省越明显。这也是 PagedAttention 分块复用能力在实际部署中最直接的体现。
8. 为什么 vLLM 适合高并发推理
结合前面的分析,PagedAttention 对高并发场景的适配可以归纳为三个层面:
8.1 显存利用率高,同卡能塞更多请求
分页管理让显存几乎「应算尽算」:没有预分配浪费、没有碎片死角。同样一块 80GB 的 A100/H100,vLLM 能同时容纳的并发请求数,往往比传统框架多 2–4 倍。对于面向大量用户的在线服务,这意味着硬件成本直接下降。
8.2 连续批处理让吞吐最大化
vLLM 默认使用连续批处理(Continuous Batching)。传统静态批处理必须等一批里最长的请求结束才能释放整批资源;而 vLLM 每时每刻都在动态调度:哪个请求吐完 token 终止了,立刻把它的 KV 块还给公共池子,腾出的空间马上让给新请求。结合分页释放的便利性,GPU 几乎不会因为「等某个慢请求」而空转。
8.3 同前缀请求越多,优势越明显
在线服务最常见的形态——同一套系统提示词、同一个长文档做 RAG(检索增强生成)、同一段历史对话反复追问——恰恰是 PagedAttention 前缀复用发力的主场。并发越高、前缀重复度越高,共享 KV 块带来的算力与显存双重节省就越突出。
9. 总结
PagedAttention 看似是一套 GPU 内存管理方案,但它解决的是大模型推理中的本质矛盾:显存是有限的,而并发请求对显存的需求是无限的。 它的答案也很朴素——像操作系统管理内存那样管理 KV 缓存:
- 分页:把连续的显存大块切成标准小块,消除外部碎片;
- 按需分配:用多少分多少,消除内部碎片;
- 块表映射:逻辑连续、物理分散,保证正确性的同时获得灵活性;
- 写时复制:相同前缀只存一份,安全地跨请求复用。
这些设计叠加连续批处理,造就了 vLLM 远超传统框架的吞吐能力,也让它成为当前高并发大模型推理服务的首选方案之一。
如果你正在为线上大模型服务的显存紧张、并发不足而头疼,不妨试试 vLLM——很可能不需要换更好的卡,只需要换一种更聪明的显存管理方式。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)