生产工程 · 2025年5月19日 · 6 分钟

LLM 推理原理与优化:KV Cache、Speculative Decoding 与 Continuous Batching

Prefill/Decode 区别、KV Cache 显存管理、PagedAttention、Continuous Batching、Speculative Decoding 等推理优化技术详解。

用同一个模型,为什么有的服务每秒只能处理几个请求,有的能处理几百个?为什么有些场景等第一个 token 就要好几秒,有些却能"秒出"?

不是因为 GPU 不同,而是推理优化技术的差距。这篇文章拆解 LLM 推理的底层原理和主流优化手段。

自回归推理的本质

LLM 的推理过程是逐个 token 生成的——每生成一个 token,都要把整个序列重新计算一遍。

输入:"北京的春天"
生成过程:
  "北京的春天" → 预测下一个 token"很"
  "北京的春天很" → 预测下一个 token"美"
  "北京的春天很美" → 预测下一个 token"丽"
  "北京的春天很美丽" → 预测下一个 token"。"
  "北京的春天很美丽。" → 预测下一个 token → 结束

每一步都要跑一次完整的前向传播。输出的 token 数越多,需要跑的步数就越多。

这里有一个关键区分:

Prefill 阶段(预填充):
  一次性处理所有输入的 token
  计算密集(矩阵 × 矩阵)
  可以充分利用 GPU 的并行计算能力

Decode 阶段(解码):
  逐个生成 output token
  访存密集(矩阵 × 向量)
  GPU 利用率很低

对于长输出场景(比如生成一篇 2000 字的文章),Decode 阶段占据了 90%+ 的推理时间。

KV Cache:最基础的优化

每次生成新 token 时,都需要重新计算注意力。但仔细看:

输入:"北京的春天很美丽。"
                                  ─────────
第一次计算注意力时:位置 1-10 完全算了一次
第二次计算注意力时:位置 1-11 又算了一次
第三次计算注意力时:位置 1-12 又算了一次

位置 1-10 的 Key 和 Value 在每一步都在重复计算。完全是浪费。

KV Cache 的思路:把之前层算好的 Key 和 Value 缓存起来,新 token 只需要计算自己的 K 和 V,然后拼到缓存后面。

KV Cache:
  第 N 步:重新计算所有位置的 QKV → 输出 token N
  计算量:O(N × L) 每步

有 KV Cache:
  第 1 步(Prefill):计算所有位置的 KV,存入缓存
  第 N 步:只计算新位置的 QKV
    → 从缓存读之前的所有 KV
    → 注意力计算 = O(L) 而不是 O(N × L)

KV Cache 可以节省 80-95% 的计算量,是现在所有推理引擎的标配。

但 KV Cache 不是免费的——它会消耗大量显存。假设 7B 模型、4K 上下文、FP16:

单层 KV Cache 大小 = 注意力头数 × head_dim × 2 (K + V) × seq_len
7B 模型 ≈ 32 层,每层 ≈ 0.5MB/token
4K tokens 的 KV Cache ≈ 32 × 0.5 × 4096 ≈ 64MB 每请求
100 个并发请求 = 6.4GB

这就是为什么上下文越长、并发越高,显存消耗越恐怖。KV Cache 往往是推理时最大的显存消耗者,甚至超过模型参数本身。

PagedAttention:vLLM 的核心创新

KV Cache 是分给每个请求的固定大小的连续显存块。问题是——显存碎片化

传统 KV Cache 分配:
  ┌─────────┬─────────┬─────────┬─────────┐
  │ Req 1Req 2Req 1Free      │
  │ (预留4K)(预留4K) │ 实际只用2K │          │
  └─────────┴─────────┴─────────┴─────────┘
                 ↑ 浪费了 2K 的显存

vLLM 的 PagedAttention 把 KV Cache 分页管理——按需分配,不用提前预留完整空间。

PagedAttention:
  ┌────┬────┬────┬────┬────┬────┬────┬────┐
  │R1P1│R2P1│R1P2│R3P1│R1P3│R2P2│R3P2│R1P4│
  └────┴────┴────┴────┴────┴────┴────┴────┘
  每个请求的 KV Cache 分布在多个页中,按需分配
  没有预留浪费,没有外部碎片

效果:同样的显存可以处理 2-4 倍的并发请求。

Continuous Batching:提高 GPU 利用率

传统推理是"分批处理"——攒够一批请求,一起处理,等全部处理完再接收下一批。

传统 Batching:
  ┌─────────┐  ┌─────────┐  ┌─────────┐
  │ Batch 1 │  │ Batch 2 │  │ Batch 3 │
  │ A B C D  │→│ E F G H  │→│ I J K L  │
  │ (全部等   │  │ (全部等   │  │ (全部等   │
  │ 最慢的)  │  │ 最慢的)  │  │ 最慢的)  │
  └─────────┘  └─────────┘  └─────────┘
                  时间→

问题:如果 batch 里大部分请求已经生成完毕,只剩一个还在跑,GPU 的利用率就很低。

Continuous Batching 的思路:请求不用等 batch,随时来随时处理。生成的请求完成就退出,新请求插入。

Continuous Batching:
  时间轴 →
  A[##########]  →  A 完成,退出
  B[################]  →  B 还在跑
  C[####]  →  C 完成,退出
  D[###########]  →  D 还在跑
                     E 新加入 → E[#####]

每次迭代时,引擎检查哪些请求已经完成、哪些新请求进来,动态调整 batch。这样就可以一直保持 GPU 满负荷运转。

实际效果:在相同硬件上,Continuous Batching 可以把吞吐量提升 3-5 倍。

Speculative Decoding:更快的生成

KV Cache 和 PagedAttention 提高的是吞吐量,但单个请求的首 token 延迟和生成速度还是受限。

Speculative Decoding 的思路很巧妙:用小模型帮大模型"打草稿"

正常的生成:每一步都用大模型
步骤  : 1  2  3  4  5  6  7  8
大模型: A  B  C  D  E  F  G  H(每步一次大模型调用)

Speculative Decoding:
步骤  : 1           2           3
小模型: A→B→C→D     E→F→G→H    ...
大模型: ✓✓✓✗        ✓✓✓✗
        拒绝 D,重新生成     拒绝 H,重新生成

大模型调用次数:8 步 → 2 

具体流程:

  1. 小模型(drafter)快速生成 K 个候选 token
  2. 把 K 个 token 一起喂给大模型做一次验证
  3. 大模型逐个 token 接受或拒绝
  4. 被拒绝的位置,大模型用自己的分布重新采样

在理想情况下(小模型预测准确率高),Speculative Decoding 可以把生成速度提升 1.5-2.5 倍。

但要注意:加速效果高度依赖小模型和大模型的一致性。如果小模型猜的 token 经常被大模型拒绝,效率反而更低。一般用同一个模型系列的较小版本作为 drafter(比如 7B 给 70B 打草稿)。

量化推理

之前那篇量化的文章主要讲模型加载时的量化。推理优化也用到量化——在推理阶段动态量化

静态量化(模型加载时):
  FP16 → INT4 转换一次
  后续全用 INT4 计算
  精度损失确定

动态量化(推理运行时):
  权重用 INT4 存,计算时"反量化"到 FP16
  激活值用 FP16,精度更高
  但每次计算多一次反量化操作

当前的主流做法是权重量化 + 激活值不量化(W4A16)——权重存为 INT4 节省显存,计算时转回 FP16 保持精度。这是 llama.cpp Q4_K_M 量化的默认方式。

推理引擎对比

引擎 KV Cache Batching 量化 特点
llama.cpp W4A16/W8A16 最省显存,CPU 友好
Ollama 内置 简易 同上 最易用,封装好的 llama.cpp
vLLM PagedAttention+ Continuous AWQ/GPTQ 吞吐量之王
TensorRT-LLM PagedAttention+ Continuous FP8/INT4/INT8 NVIDIA 生态最佳
TGI Continuous GPTQ/AWQ HuggingFace 全家桶

如何选择:

个人使用:Ollama(一键启动,不用操心配置)
开发调试:llama.cpp(灵活,精细控制)
生产环境:vLLM(高并发吞吐量)
NVIDIA 全栈:TensorRT-LLM(极致性能)
HuggingFace 生态:TGI(无缝集成)

推理延迟的构成

一个典型的 LLM 推理请求延迟 = 三个部分的加和:

总延迟 = Prefill 时间 + Decode 时间 + 传输时间

Prefill 时间:
  取决于输入 token 数
  7B 模型:约 0.5ms/token(A100)
  70B 模型:约 3ms/token(A100)

Decode 时间(每 token):
  7B 模型:约 8-15ms(A100)
  70B 模型:约 30-50ms(A100)

传输时间:
  服务器之间、客户端到服务器的网络延迟
  通常 10-50ms

对于一个输入 500 tokens、输出 500 tokens 的请求:

Prefill: 500 × 0.5ms = 250ms
Decode: 500 × 10ms = 5000ms
传输: ~50ms
总延迟: ~5.3s

首 token 延迟: 250ms + 50ms = ~300ms
(用户最先看到第一个字的时间)

后续 token 速度: 100 tokens/s
(每 token 10ms)

这也是为什么流式输出(SSE/WebSocket)如此重要——用户可以 300ms 就开始看到回复,而不是等 5.3 秒全文生成完。

推理成本

70B 模型在 A100 上运行:
  GPU 成本:约 $2/小时
  单次推理(输入500+输出500):约 5 秒
  每小时可处理:720 次
  单次推理 GPU 成本:约 $0.0028

7B 模型在 A100 上运行:
  GPU 成本:约 $2/小时
  单次推理:约 1.5 秒
  每小时可处理:2400 次
  单次推理 GPU 成本:约 $0.0008

使用 API(如 OpenAI):
  GPT-4o 单次推理:约 $0.01-$0.05
  GPT-4o mini:约 $0.001-$0.005

本地部署省钱的前提是有足够的利用量。如果一天只跑几千次请求,用 API 比租 GPU 划算。

未来方向

推理优化是现在大模型工程中最活跃的领域。几个值得关注的方向:

1. Flash Attention:通过 tiling 技术让注意力计算在 SRAM 中完成,减少 HBM 访问。已经是 vLLM 等引擎的标配优化之一。

2. 推测解码的升级:多候选推测、树形推测、self-speculation——从单条候选路径扩展到多路径,增加小模型命中的概率。

3. 硬件协同设计:NVIDIA 的 Hopper/Blackwell 架构引入 FP8 Transformer Engine、NVLink 更高带宽——这些硬件特性让推理引擎可以更激进地优化,比如 FP8 KV Cache 让显存占用直接减半。

4. 稀疏推理:MoE(Mixture of Experts)模型在推理时只激活部分参数,理论上可以显著减少计算量。但 MoE 的推理引擎优化还在早期,Mixtral 8x7B 有时比 7B Dense 模型还慢,就是因为稀疏计算的调度开销没有抵消掉它节约的计算量。

理解推理优化有什么意义?如果你的应用面向最终用户,推理优化的影响是最直接、最可感知的——首 token 延迟从 3 秒降到 300ms,用户体验不是一个量级的。这是工程投入回报最高的方向之一。

继续阅读

评论