用同一个模型,为什么有的服务每秒只能处理几个请求,有的能处理几百个?为什么有些场景等第一个 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 步:重新计算所有位置的 Q、K、V → 输出 token N
计算量:O(N × L) 每步
有 KV Cache:
第 1 步(Prefill):计算所有位置的 K、V,存入缓存
第 N 步:只计算新位置的 Q、K、V
→ 从缓存读之前的所有 K、V
→ 注意力计算 = 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 1 │ Req 2 │ Req 1 │ Free │
│ (预留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 次
具体流程:
- 小模型(drafter)快速生成 K 个候选 token
- 把 K 个 token 一起喂给大模型做一次验证
- 大模型逐个 token 接受或拒绝
- 被拒绝的位置,大模型用自己的分布重新采样
在理想情况下(小模型预测准确率高),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,用户体验不是一个量级的。这是工程投入回报最高的方向之一。
评论