本文从"为什么 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 推理服务的主流选择。


参考文献

  1. Kwon, W., et al. (2023). "Efficient Memory Management for Large Language Model Serving with PagedAttention." SOSP 2023.
  2. Yu, G., et al. (2022). "Orca: A Distributed Serving System for Transformer-Based Generative Models." OSDI 2022.
  3. Pope, R., et al. (2022). "Efficiently Scaling Transformer Inference." arXiv preprint.
  4. vLLM GitHub: https://github.com/vllm-project/vllm