本文从"为什么 LLM 推理慢"这个问题出发,逐步推导 KV Cache、PagedAttention、Continuous Batching 三大推理优化技术,并介绍 vLLM 工程框架。
前置知识:了解 Transformer 的 Attention 机制(Q、K、V 的含义和计算过程)。
简述
LLM 推理(生成文本)的三大瓶颈及解决方案:
| 瓶颈 | 原因 | 解决方案 |
|---|---|---|
| 重复计算 | 自回归生成每轮重算旧 token 的 K、V | KV Cache(用显存换时间) |
| 显存浪费 | KV Cache 预分配连续显存,利用率低 | PagedAttention(分块管理,消除碎片) |
| GPU 空闲 | 短请求完成后 GPU 资源闲置 | Continuous Batching(动态调度) |
vLLM 是将这三项技术整合为一个高性能推理服务框架的开源项目。
1. 自回归生成的计算瓶颈
1.1 自回归生成回顾
LLM 生成文本是逐词进行的——每轮将已有序列送入模型,预测下一个词,再将其追加到序列末尾,重复此过程。
以生成"今天天气真好"为例:
第 1 轮:输入 ["今天"] → 预测 "天气"
第 2 轮:输入 ["今天", "天气"] → 预测 "真"
第 3 轮:输入 ["今天", "天气", "真"] → 预测 "好"
每轮都需要一次完整的前向传播。生成 $n$ 个词 = $n$ 次前向传播。
1.2 每轮前向传播中的重复计算
以 Attention 层为例,每轮需要计算 Q、K、V:
$$Q = W_q X, \quad K = W_k X, \quad V = W_v X$$
问题:第 3 轮中,"今天"和"天气"的 K 和 V 在第 1、2 轮时已经算过了,但第 3 轮又重新算了一遍。
逐轮统计 K 和 V 的计算量:
| 轮次 | 输入 token 数 | K/V 计算次数 | 其中新计算 | 重复计算 |
|---|---|---|---|---|
| 第 1 轮 | 1 | 2 | 2 | 0 |
| 第 2 轮 | 2 | 4 | 2 | 2 |
| 第 3 轮 | 3 | 6 | 2 | 4 |
| 第 $n$ 轮 | $n$ | $2n$ | 2 | $2(n-1)$ |
生成 $n$ 个词的 K/V 总计算量:
$$\sum_{i=1}^{n} 2i = n(n+1)$$
其中真正"新"的计算只有 $2n$ 次,重复计算占比 $\frac{n-1}{n+1}$。当 $n = 1000$ 时,99.8% 的计算都是重复的。
2. KV Cache:用显存换时间
2.1 核心思想
既然旧 token 的 K 和 V 不会变化(权重已冻结,输入也不变),那就缓存起来,每轮只计算新 token 的 K 和 V。
2.2 工作流程
第 1 轮:
新算 k_今天, v_今天
写入缓存:cache = [(k_今天, v_今天)]
计算:q_今天 @ cache → 输出 "天气"
第 2 轮:
新算 k_天气, v_天气(只算 1 个新 token)
追加缓存:cache = [(k_今天, v_今天), (k_天气, v_天气)]
计算:q_天气 @ cache → 输出 "真"
第 3 轮:
新算 k_真, v_真(只算 1 个新 token)
追加缓存:cache = [(k_今天, v_今天), (k_天气, v_天气), (k_真, v_真)]
计算:q_真 @ cache → 输出 "好"
2.3 为什么只缓存 K 和 V,不缓存 Q?
Q 表示"当前 token 在查询什么",每轮只有新 token 生成一个 Q,用完即弃——下一轮的新 token 有自己的 Q,旧 token 的 Q 不会再被使用。而 K 和 V 表示"每个旧 token 有什么"和"每个旧 token 的内容",在后续每一轮的 Attention 计算中都要被"查询"和"读取",因此需要缓存。
2.4 加速效果
| 序列长度 $n$ | 无 KV Cache | 有 KV Cache | 加速比 |
|---|---|---|---|
| 10 | 110 次 | 20 次 | 5.5× |
| 100 | 10,100 次 | 200 次 | 50× |
| 1,000 | 1,001,000 次 | 2,000 次 | 500× |
生成 $n$ 个词的 K/V 计算量从 $O(n^2)$ 降至 $O(n)$。
2.5 KV Cache 的显存代价
以 Qwen3-8B 为例(36 层,每层 8 个 KV 头,每头 128 维,使用 GQA 架构):
$$\text{每个 token 的 KV Cache 大小} = 2 \times n_{\text{layers}} \times n_{\text{kv\_heads}} \times d_{\text{head}} \times \text{bytes\_per\_element}$$
$$= 2 \times 36 \times 8 \times 128 \times 2 \text{ (FP16)} = 147{,}456 \text{ bytes} \approx 0.14 \text{ MB}$$
| 序列长度 | 单请求 KV Cache | 10 并发 |
|---|---|---|
| 2,048 | ~288 MB | ~2.8 GB |
| 8,192 | ~1.1 GB | ~11 GB |
| 32,768 | ~4.5 GB | ~45 GB |
KV Cache 的显存占用随序列长度和并发数线性增长,成为推理服务的主要瓶颈。
3. PagedAttention:消除显存碎片化
3.1 问题:传统 KV Cache 管理的浪费
传统方式为每个请求预分配最大长度的连续显存空间:
假设 max_seq_len = 4096,每 token 0.14MB:
每个请求预分配 4096 × 0.14MB ≈ 576MB
请求 A(实际 10 token):预分配 576MB,实际用 1.4MB → 浪费 99.8%
请求 B(实际 1000 token):预分配 576MB,实际用 140MB → 浪费 97.6%
请求 C(实际 50 token):预分配 576MB,实际用 7MB → 浪费 98.8%
三个请求总计预分配 ~1.7GB,实际使用 ~148MB → 利用率 < 9%
这就像为每辆车预留一个标准停车位——即使停的是摩托车也占一整个车位。
3.2 核心思想:借鉴操作系统的虚拟内存分页
操作系统通过分页(Paging) 解决内存碎片化:将物理内存切成固定大小的"页"(如 4KB),进程按需分配页,不要求连续。用"页表"记录逻辑页到物理页的映射。
PagedAttention 将同样的思路用在 KV Cache 管理上:
将 KV Cache 显存切成固定大小的 block(如每个 block 存 16 个 token 的 KV)
请求 A(10 token)→ 分配 1 个 block(容量 16 token)
请求 B(1000 token)→ 分配 64 个 block(容量 1024 token)
block 不要求连续分配!用 block table 记录映射关系。
3.3 Block Table 机制
物理 block: [0] [1] [2] [3] [4] [5] [6] [7]
序列 A(45 token,3 个 block):
Block Table: A_0 → 物理 2, A_1 → 物理 5, A_2 → 物理 0
序列 B(20 token,2 个 block):
Block Table: B_0 → 物理 7, B_1 → 物理 3
物理显存布局:
[A_2] [空] [A_0] [B_1] [空] [A_1] [空] [B_0]
空闲 block: 1, 4, 6(可分配给新请求)
3.4 显存利用率对比
| 管理方式 | 浪费来源 | 显存利用率 |
|---|---|---|
| 传统预分配 | 预分配未使用 + 外部碎片 | < 30% |
| PagedAttention | 仅最后一个 block 未填满 | > 96% |
同样的显存,PagedAttention 可以同时服务的请求数增加 2~4 倍。
3.5 Copy-on-Write 优化
PagedAttention 还支持 Copy-on-Write,用于 Beam Search 等需要分支的场景:
Beam Search 中,一个序列可能分裂成多个候选:
原始序列:[block_0, block_1, block_2]
分裂为候选 1 和候选 2
传统方式:复制整个 KV Cache → 3 个 block × 2 = 6 个 block
PagedAttention:两个候选共享 block_0 和 block_1
候选 1: [block_0, block_1, block_2]
候选 2: [block_0, block_1, block_3] ← 只新建 block_3
节省 2/3 的显存拷贝开销。
4. Continuous Batching:最大化 GPU 利用率
4.1 问题:Static Batching 的浪费
传统 Static Batching 需要凑齐一个 batch,等所有请求都完成后才处理下一批:
batch = [请求 A (10 词), 请求 B (100 词), 请求 C (5 词)]
请求 C 第 5 轮完成 → 但必须等 B 的 100 轮结束
请求 A 第 10 轮完成 → 也要等 B
[A ████████░░░░░░░░░░░░] ← 完成后 GPU 资源空闲
[B ████████████████████████████████████████]
[C ████░░░░░░░░░░░░░░░░] ← 完成后 GPU 资源空闲
短请求完成后,其 GPU 资源(计算和显存)被浪费。
4.2 Continuous Batching 的做法
请求完成一个,立刻从队列中插入一个新请求:
t=1: [A, B, C] ← 3 个请求并行
t=5: [A, B, D] ← C 完成,D 插入
t=10: [E, B, D] ← A 完成,E 插入
t=15: [E, F, D] ← B 完成,F 插入
[A ████████][E █████████████]
[B ████████████████████████████]
[C ████][D ███████████████████████]
[ ][F ██████████████]
GPU 始终满载,无空闲。
4.3 技术挑战
Continuous Batching 需要配合 PagedAttention 才能实现:
每个 step 中,batch 内的请求数量可能变化
→ KV Cache 的大小动态变化
→ 传统连续分配无法支持动态增减
→ PagedAttention 的 block 级管理天然支持动态分配和回收
5. vLLM:整合三大优化
5.1 vLLM 是什么
vLLM 是 2023 年由 UC Berkeley 开发的高性能 LLM 推理服务框架,将 KV Cache、PagedAttention、Continuous Batching 整合为一个开箱即用的推理引擎。
定位对比:
| 框架 | 侧重点 | 适用场景 |
|---|---|---|
| HuggingFace Transformers | 训练(加载模型、微调、分布式训练) | 研究和开发 |
| vLLM | 推理(高吞吐、低延迟服务部署) | 生产环境部署 |
5.2 整体架构
用户请求 → API Server(兼容 OpenAI API)
↓
Scheduler(调度器)
├── 管理请求队列
├── 实现 Continuous Batching
└── 管理 Block Table(KV Cache 分配/回收)
↓
GPU Workers(执行前向传播)
└── 每个 Worker 管理一部分 KV Cache blocks
5.3 性能表现
相比 HuggingFace Transformers 的默认推理(无优化):
| 指标 | Transformers | vLLM | 提升 |
|---|---|---|---|
| 吞吐量 | 基准 | 2~4× | 显著 |
| 首 token 延迟 | 基准 | ~相同 | — |
| 逐 token 延迟 | 基准 | 更低 | 中等 |
| 显存利用率 | < 30% | > 96% | 显著 |
5.4 快速上手
# 安装:pip install vllm
from vllm import LLM, SamplingParams
# 加载模型(自动配置 PagedAttention)
llm = LLM(
model="Qwen/Qwen3-8B",
tensor_parallel_size=1, # GPU 数量
max_model_len=4096, # 最大序列长度
gpu_memory_utilization=0.9, # 使用 90% GPU 显存
)
# 设置采样参数
params = SamplingParams(
temperature=0.7,
max_tokens=256,
)
# 批量推理
outputs = llm.generate([
"今天天气真好,",
"深度学习的核心是",
], params)
for output in outputs:
print(output.outputs[0].text)
5.5 OpenAI 兼容的 API 服务
# 启动兼容 OpenAI API 的服务
vllm serve Qwen/Qwen3-8B --port 8000
# 使用 OpenAI SDK 调用
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="empty")
response = client.chat.completions.create(
model="Qwen/Qwen3-8B",
messages=[{"role": "user", "content": "介绍一下厦门大学"}],
temperature=0.7,
)
print(response.choices[0].message.content)
6. 训练 vs 推理:为什么优化方向不同?
| 维度 | 训练 | 推理 |
|---|---|---|
| 输入方式 | 完整序列一次性输入 | 逐 token 自回归生成 |
| 计算模式 | 所有位置并行计算 | 每轮只算 1 个新 token |
| 是否需要 KV Cache | 不需要(无重复计算) | 需要(避免 $O(n^2)$ 重复) |
| 正确答案 | 直接给出(Teacher Forcing) | 未知,需逐词生成 |
| 权重更新 | 反向传播更新权重 | 权重冻结 |
| 优化重点 | 显存(模型+梯度+优化器状态) | 显存(KV Cache)+ 延迟 |
Teacher Forcing:训练时,正确答案直接作为输入(而非模型自己的预测),所有位置的 loss 在一次前向传播中并行计算。这避免了误差累积,也使得训练不需要 KV Cache。
总结
LLM 推理优化的三大核心技术及其解决的问题:
问题 1:自回归生成每轮重复计算旧 token 的 K 和 V → 慢
↓ KV Cache(缓存旧 K/V,用显存换时间)
问题 2:KV Cache 预分配连续显存,利用率低 → 并发受限
↓ PagedAttention(分块管理,消除碎片,类似 OS 虚拟内存)
问题 3:短请求完成后 GPU 资源闲置 → 吞吐低
↓ Continuous Batching(动态调度,GPU 始终满载)
vLLM 将这三项技术整合为一个高性能推理框架,是目前部署 LLM 推理服务的主流选择。
参考文献
- Kwon, W., et al. (2023). "Efficient Memory Management for Large Language Model Serving with PagedAttention." SOSP 2023.
- Yu, G., et al. (2022). "Orca: A Distributed Serving System for Transformer-Based Generative Models." OSDI 2022.
- Pope, R., et al. (2022). "Efficiently Scaling Transformer Inference." arXiv preprint.
- vLLM GitHub: https://github.com/vllm-project/vllm