1. 引言:大模型推理的隐形瓶颈

2023 年之后,大语言模型(LLM)的参数量从几十亿一路飙到几千亿。大家的目光大多集中在「模型有多大、效果有多好」,但真正把模型部署上线时,一个更现实的问题摆在面前:同一个 GPU,为什么别人能扛住每秒几百个请求,而你的服务几十个并发就开始排队?

答案往往不在模型本身,而在推理服务的内存管理。vLLM 团队在 2023 年提出的 PagedAttention 机制,正是针对这个隐形瓶颈的一次关键突破。本文不深入内核代码,只用通俗的类比,讲清楚三件事:

  • 为什么传统的 KV 缓存管理会浪费大量显存、拖慢并发?
  • PagedAttention 如何借鉴操作系统的「分页」思想来管理 KV 缓存?
  • 为什么这种设计让 vLLM 特别适合高并发推理场景?

读完你会理解:vLLM 的高吞吐,不是靠魔法,而是靠着把显存当成操作系统管理虚拟内存一样去精细管理。

2. 先理解:KV 缓存到底存了什么

在讲 PagedAttention 之前,我们先花两分钟弄清楚 KV 缓存是什么。这部分不需要任何内核知识,只要理解「模型是逐 token 生成」这件事。

2.1 自回归生成:一次只吐一个字

大模型生成文本时,是一个 token 一个 token 依次进行的。比如你让它输出「今天天气很好」,它不会一次性蹦出整句话,而是:

  1. 先看用户输入,预测出「今」;
  2. 把「今」拼到输入后面,再预测出「天」;
  3. 把「今天」拼到后面,再预测出「天」;
  4. 以此类推,直到输出终止符。

这个过程叫自回归生成。看似效率不高,却是目前主流大模型的通用范式。

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 则把显存切成标准块,按需分配,并让多个请求共享相同前缀的块。

改进

PagedAttention:分页 + 复用

显存切成固定大小 KV 块
(如每块 16 token)

请求按需申请块
用多少占多少,无预分配浪费

块可分散存放
块表维护逻辑顺序

相同前缀只存一份
多请求共享同一批物理块

传统 KV 缓存:连续预分配

请求 A 预留 4GB 连续空间
实际只用了 200MB

请求 B 预留 4GB 连续空间
实际只用了 1GB

空闲碎片散落,凑不出
下一个大请求的连续空间

请求 A、B 的相同前缀
各存一份,重复占用

再用一个表格把关键差异并排放在一起:

对比维度传统 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——很可能不需要换更好的卡,只需要换一种更聪明的显存管理方式。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐