大语言模型推理优化实战:从 KV Cache、连续批处理到投机解码
为什么推理优化是大模型落地的主战场
训练一次大模型的成本很高,但真正决定业务盈亏的,往往是推理。一个在线问答服务每天可能要处理数百万次请求,单次响应的延迟、每千 token 的成本、GPU 的显存占用,都会直接反映到账单和用户体验上。模型结构在短期内很难改变,而推理侧还有大量工程优化空间:同样的硬件,经过合理优化后吞吐提升数倍、首 token 延迟下降一半,是完全可复现的结果。
本文按照"先定位瓶颈、再选择手段"的顺序,梳理大语言模型推理优化的关键技术,并给出在实际项目中落地时的优先级建议。
一、先看清瓶颈:Prefill 与 Decode 是两种完全不同的负载
自回归生成可以分为两个阶段,二者对硬件的诉求截然不同:
- Prefill(预填充):一次性处理整段输入 prompt,所有 token 可以并行计算。这个阶段计算量大、矩阵乘密集,通常是 compute-bound,决定首 token 延迟(TTFT)。
- Decode(解码):逐 token 生成,每一步只处理一个新 token,但需要读取全部历史 KV Cache 和模型权重。这个阶段算术强度很低,通常是 memory-bound,决定每 token 的输出间隔(TPOT)和整体吞吐。
这个区分非常重要:很多优化只对其中一个阶段有效。例如量化主要降低 Decode 阶段的显存带宽压力,而 FlashAttention 在 Prefill 阶段收益最明显。混淆两者,往往会导致优化方向选错。
可以用一个粗略的 roofline 视角来判断:当 batch size 较小时,Decode 的瓶颈几乎完全在显存带宽上,此时提升吞吐的关键是"每次搬运权重能服务更多 token",也就是增大有效 batch;当 batch 较大时,瓶颈会逐渐转移到算力上,需要重新评估量化与算子效率。
二、KV Cache:显存与带宽的核心矛盾
KV Cache 是推理优化的第一优先级。它的显存占用随序列长度和并发数线性增长,而 Decode 每一步都要完整读取它,因此它同时吃显存和带宽。
1. PagedAttention 与分页管理
传统实现为每个请求预分配连续的最大长度 KV 空间,导致严重的内部碎片。PagedAttention 借鉴操作系统虚拟内存的分页思想,把 KV Cache 切成固定大小的 block,按需分配、用页表映射。这样不同请求可以共享物理块,显存利用率能从 20%~40% 提升到 80% 以上,进而支撑更大的 batch。
2. KV Cache 量化
把 KV 从 FP16 量化到 FP8 甚至 INT8/INT4,可以直接把 KV 显存和读取带宽减半甚至更多。工程上通常只量化 KV 而不量化权重,或对 K/V 采用不同的粒度(per-token 或 per-channel),以控制精度损失。实践表明,FP8 KV Cache 在多数对话场景下几乎无损,而更激进的 4bit 量化需要针对任务做评估。
3. 前缀缓存(Prefix Caching)
在线服务中大量请求共享相同的前缀,例如系统提示词、few-shot 示例、长文档。前缀缓存会把相同前缀的 KV 计算结果复用起来,跳过重复的 Prefill。对于带固定 system prompt 的 Agent 应用,这一项经常能带来 30% 以上的 TTFT 改善和可观的算力节省。
需要注意的是,前缀缓存命中依赖精确的 token 序列匹配,因此提示词模板应尽量保持稳定,把变化的内容放到末尾。
三、批处理与调度:把 GPU 喂饱
Decode 阶段 memory-bound 的结论意味着:只要还能塞进更多请求,单步计算的时间几乎不变,吞吐就会线性上升。因此调度策略是推理服务的关键。
1. 连续批处理(Continuous Batching)
静态批处理要求一个 batch 内所有请求同时开始、同时结束,短请求必须等待长请求,GPU 大量时间空转。连续批处理在每一步 decode 时动态决定本步参与计算的请求集合:新请求可以随时加入,完成的请求立即退出。这是当前主流推理框架的标配,通常能带来数倍的吞吐提升。
2. Chunked Prefill
长 prompt 的 Prefill 会占用大量算力并阻塞正在 decode 的请求,造成 TPOT 抖动。Chunked Prefill 把长 prompt 切成多个小块,与 decode 请求混合调度,从而在吞吐和延迟之间取得平衡。对长上下文场景,这一步几乎是必选项。
3. Prefill / Decode 分离部署
既然两个阶段特性不同,就可以把它们放到不同的实例上:Prefill 实例使用算力更强的卡,Decode 实例使用显存带宽更优、显存更大的卡,中间通过高速网络传递 KV Cache。这种 P/D 分离(disaggregated serving)能显著提升集群整体利用率,代价是系统复杂度上升,需要处理 KV 传输和调度。
四、模型压缩与近似计算
1. 权重量化
GPTQ、AWQ 等训练后量化方法可以把权重压到 INT4,配合 group-wise 反量化,在消费级显卡上也能跑起 7B~14B 模型。量化带来的收益不仅是显存下降,更重要的是 Decode 阶段每次读取的字节数减少,直接提升 memory-bound 场景的吞吐。工程上需要关注 kernel 是否针对具体硬件做过优化,否则反量化开销可能抵消收益。
2. 投机解码(Speculative Decoding)
用一个小的 draft 模型先快速生成若干候选 token,再由大模型并行验证,接受一致的片段。由于验证是并行的,一次前向可以产出多个 token,从而在不改变输出分布的前提下降低平均延迟。变体包括 Medusa、EAGLE 等基于额外预测头的方案,以及在长上下文场景下效果更稳定的树状投机。投机解码的收益高度依赖任务的可预测性,代码补全、格式化输出等场景收益通常更明显。
3. 结构化剪枝与稀疏化
稀疏激活的 MoE 模型通过路由只激活部分专家,在总参数量很大的情况下控制单 token 计算量。但 MoE 对显存容量要求高,且负载均衡、专家并行会引入额外的通信开销,需要结合具体部署形态评估。
五、算子与并行:把硬件压榨到极限
- FlashAttention 系列:通过分块与在线 softmax 减少 HBM 读写,把注意力计算保持在 SRAM 中完成,显著降低长序列的显存占用和延迟。
- 算子融合:把 RMSNorm、残差、激活函数等小算子融合进大 kernel,减少 kernel launch 与显存往返。Decode 阶段小算子占比高,融合收益尤其明显。
- 张量并行与流水并行:单卡放不下或单卡算力不足时,用 TP 切分权重、用 PP 切分层数。TP 通信频繁,适合节点内 NVLink/高速互联;跨节点优先考虑 PP 或专家并行。
- CUDA Graph:Decode 每步的计算图结构固定,用 CUDA Graph 捕获后可消除大量启动开销,对低延迟场景提升明显。
六、工程落地的优先级建议
优化手段很多,但收益和成本并不对等。建议按下面的顺序推进:
- 先建立可观测性:明确 TTFT、TPOT、吞吐(tokens/s)、显存占用、batch 利用率等指标,并压测出基线。没有基线,任何优化都无法验证。
- 启用连续批处理:如果使用的框架还没开启,这通常是投入产出比最高的一步。
- 管理 KV Cache:开启 PagedAttention 与前缀缓存,按场景评估 FP8 KV 量化。
- 做权重量化:在精度可接受的范围内选择 INT4/FP8,并确认所用 kernel 与硬件匹配。
- 针对长上下文与延迟敏感场景:引入 Chunked Prefill、投机解码、CUDA Graph。
- 规模再往上:考虑 P/D 分离、多机并行与更精细的调度策略。
一个可参考的 Decode 调度伪代码如下,核心思想是每步都重新评估哪些请求可以进入本轮计算:
while running or waiting:
# 每步动态组 batch:新请求可加入,已完成的请求退出
for req in waiting:
if kv_cache.can_allocate(req):
running.append(req)
batch = [r for r in running if not r.finished]
outputs = model.decode_step(batch)
for req, out in zip(batch, outputs):
if out.is_eos or req.reached_max_len:
running.remove(req)
# 显存不足时抢占(preempt)低优先级请求,回收其 KV 页
总结
大语言模型推理优化的本质,是在"显存、带宽、算力、延迟"四者之间做权衡。先分清 Prefill 与 Decode 的不同瓶颈,再围绕 KV Cache、批处理调度、量化与投机解码、算子与并行逐层优化,通常能在不改模型的前提下获得数倍的吞吐提升。
更重要的是建立量化指标与压测基线,让每一次优化都有数据支撑。优化不是把所有手段堆上去,而是找到当前系统真正的瓶颈,用最低的复杂度解决它。
- 点赞
- 收藏
- 关注作者
评论(0)