聊到大模型推理优化,很多人第一反应是"上vLLM就完事了""4bit量化拉满,速度肯定快"。但真正跑到生产环境里,你会发现事情没那么简单:显存莫名其妙涨、并发一高速度就崩、量化之后效果漂移严重。过去一年我把大部分精力都耗在了LLM推理侧的优化上,从最开始加载一个70B模型要等半天,到后来把百路并发压到稳定服务,中间的坑踩了不少,今天这篇就当作一份技术原理解析总览,把我自己验证过的东西一次性梳理清楚。
这篇内容适合做模型部署的工程师、正在研究推理框架的技术人,也适合想搞明白"大模型跑起来为什么这么贵"的算法同学。读完之后,你至少能弄清楚:推理优化的瓶颈到底在哪、KV Cache为什么是基石、Continuous Batching和PagedAttention解决了什么问题、量化该不该用、以及我实测下来从基线到30倍吞吐提升到底做了哪几步。
1. 推理瓶颈拆解:延迟与吞吐,先解决哪一个
1.1 推理优化到底在优化什么
先说一个最常见的误区。很多人以为推理优化就是把"首token速度"追上去,让模型回复得更快。实际上,工程上真正在意的是两个指标:单请求延迟和系统吞吐。延迟是用户体验,吞吐是成本,两者在一定条件下是矛盾的。
- 首token延迟(TTFT):从发送请求到看到第一个token的时间,影响交互感。
- 单token延迟(TPOT):生成每个token的耗时,决定"打字机"的速度。
- 吞吐量(Tokens/s):整机每秒能生成的token数,直接挂钩部署成本。
- 并发能力:单卡或单集群能同时服务的请求数。
你可以把推理服务想象成一家餐厅:出第一道菜的速度是TTFT,上菜节奏是TPOT,一天能接待多少桌客人是吞吐,能同时开几桌是并发。一家盈利能力强的餐厅,看的不是每道菜多快,而是翻台率和每桌客人都满意。推理服务也一样,单条请求再快,如果整机只能服务三个人,那成本会高到不可接受。
我的经验是,项目初期先定清楚核心诉求。如果做内部工具、Copilot页面,那延迟优先;如果做开放的API服务、SaaS产品,那吞吐优先,延迟只要控制在可接受范围就行。别一上来就两个都要,那只会让你在优化方向上反复横跳。
1.2 两个阶段,瓶颈完全不同
大模型推理不是一个均匀的过程,它天然分成两个阶段:预填充(Prefill)和解码(Decode)。
预填充阶段,你抛给它一句完整的prompt,模型把所有输入token并行处理,生成第一个输出token。这个阶段是计算密集型的,重点在矩阵乘法,GPU的算力在这里是主要消耗品。
解码阶段,模型逐token生成输出。每一步都依赖前一步的结果,没法并行,每一步其实都在做一次完整的前向传播,但只产生一个token。这个阶段是访存密集型的,它的瓶颈不在计算,而在把模型权重和KV Cache从显存搬到缓存里这个过程。
拿一个7B模型举例,FP16精度下权重就有约14GB。而每生成一个token只需要取其中很少的参数参与计算,大部分时间都花在搬运数据上。所以decode阶段的优化核心是减少访存量,而不是堆算力。我在项目里见过有人为了让单token生成更快,把模型从8卡张量并行改成16卡,结果速度几乎没变化——因为通信开销反而拖了后腿,方向就反了。
1.3 推理优化技术栈全景
如果把推理优化画成一个金字塔,从顶层到底层大概是这个结构:
- 框架层:vLLM、TensorRT-LLM、SGLang、LMDeploy、TGI等,决定了调度策略和显存管理方式。
- 算子层:FlashAttention、cuDNN、CUTLASS、Triton等,决定单步计算效率和显存占用。
- 模型层:量化、量化感知训练、GQA/MQA结构改动、投机解码、MoE等,决定模型本身的计算与访存特性。
- 硬件层:GPU型号、多卡互联拓扑、显存大小、PCIe/NVLink带宽、CPU offload等。
我之前有一段时期认为框架选好了就不用再管底层了,后来被现实打脸。同一个模型,在vLLM和TensorRT-LLM上的性能差距能到20%以上,具体哪个更好还跟你的batch大小、序列长度分布强相关。所以这篇总览会从存储器、调度、算子、模型等多个维度串起来讲,而不是只推荐某一种工具。
2. 底层机制解密:KV Cache、PagedAttention与投机解码
2.1 KV Cache的显存占用到底有多夸张
KV Cache是推理优化绕不开的基石。它的作用一句话就能说清:把已经算过的历史token对应的Key和Value缓存下来,避免每生成一个新token就把历史全部重新计算一遍。
问题在于,这个缓存非常吃显存。计算公式是:
总显存占用 = 2(K和V两个矩阵) × 层数 × 注意力头数 × 每个头的维度 × 序列长度 × 并发数 × 字节数
当模型结构确定后,前几个因子是固定的,真正随运行状态变化的是序列长度和并发数。以7B模型、40层、32个注意力头、每个头128维来算,每个token的KV Cache在所有层加起来大约是 2×40×32×128 = 327,680个数,FP16下就是655KB。看起来不多,但一个4096长度的请求就要约2.6GB,10个并发请求就是26GB,比模型权重本身还大。
这还不是最夸张的。换到70B级别模型,KV Cache的膨胀会更吓人。所以很多团队的显存规划里,KV Cache才是大头,模型权重反而只占了一部分。做容量规划时,必须把这个算清楚,不然上线第一天就OOM。
2.2 PagedAttention和Continuous Batching是怎么把吞吐拉起来的
要说清PagedAttention,得先回顾旧时代的做法。早期的推理框架(包括我最早用的几个)采用的是静态Batching:一批请求一次性加载,整批做完才能释放,再换下一批。这样做的缺陷非常明显:
- 批内请求按最长的那个padding,短序列被浪费了。
- 一个慢请求卡住,整批都在等它。
- 显存按最大序列长度预留,实际用不到那么多,造成浪费。
后来出现了Continuous Batching,打破了"整进整出"的模式。它在迭代级别做调度,每个step它都会根据当前请求的执行状态决定谁能加入batch、谁的batch slot该腾出来。就像食堂打饭,不再是凑齐一桌才开饭,而是来一个人就补一个位置,翻台率自然就上来了。
PagedAttention则借鉴了操作系统的虚拟内存分页思想。把连续的KV Cache逻辑上看作一个整体,但物理上可以存在不连续的页里,按需分配,不再预分配整块显存。这样既能避免显存碎片,又能让KV Cache的空间利用率大幅提升。vLLM是这套方案的代表实现,后来很多框架也做了类似的机制。
我在一个内部项目里做过对比,同样的8卡A800、同样的72B模型,开启Continuous Batching后,吞吐量比原来的静态批处理提升了约3倍,显存占用还降了30%左右。这属于最值得优先做的优化,几乎没有成本,换框架就能白拿。
2.3 投机解码:用一个"快嘴"替大模型"打草稿"
自回归生成是串行过程,每一步都要等上一步结束,GPU的算力在decode阶段其实是闲着大半的。投机解码(Speculative Decoding)的想法很巧妙:先让一个小模型一口气草稿生成一批token,再让大模型一次校验。校验是并行的,如果草稿中大部分token都被采纳,那么理想情况下能把解码速度提升到接近批量推理的倍数。
这里有个关键参数叫接受率,即草稿token被目标模型认可的比例。如果接受率太低,草稿被频繁拒绝,反而浪费了算力。需要说明的是,投机解码对实现细节非常敏感,草稿模型和主模型的分布越接近,收益越高;反之则可能没有任何效果甚至负优化。
我踩过的坑是,试图用同一个模型的低量化版本当草稿模型,结果因为分布漂移太大,接受率不到0.5,速度没提升反而降了。后来换成专门蒸馏出来的小模型,配合ngram采样,才把有效速度拉起来约1.8倍。这里面的调参空间不小,建议先走默认参数,再根据实际命中率调整草稿长度。
2.4 前缀缓存与结构化场景的额外红利
除了上面三大件,还有一个简单但容易被忽略的优化:前缀缓存(Prefix Caching)。它的本质是,如果两个请求的开头若干token完全一样,这些token对应的KV Cache可以直接复用。
这个在真实场景里命中率非常高。做AI助手或者Copilot时,每个请求都带一长段system prompt,这些是完全相同的前缀;多轮对话时,历史消息也会被反复计算,其实完全可以复用。我的做法是把前缀缓存显存预留开到一个合适的比例,实测大概能省掉30%-50%的prefill计算量,尤其在长提示词场景下收益明显。
如果你的场景里存在大量重复上下文,建议打开框架的前缀缓存开关,这属于性价比极高的一行配置。
3. 实操调优实录:从Baseline到30倍吞吐提升
3.1 部署环境与基线数据
理论讲再多,最终还是得跑起来说话。我以一次真实的调优经历为例,把过程和数据都贴出来。这次用的是目前比较典型的软硬件组合:
- 硬件:8卡A100 80G(NVLink互联)
- 框架:vLLM 0.5.4
- 模型:Qwen2.5-72B-Instruct
- 量化:AWQ 4bit
- 推理精度:FP16权重,FP16计算
- 测试工具:内部压测脚本模拟多路请求
没有对比就没有伤害,先记录基线。关闭所有额外优化,batch size固定为1,连续发64个请求,统计平均指标:
| 指标 | 基线值 |
|---|---|
| 单请求TTFT | 约1.8s |
| 单请求TPOT | 约112ms/token |
| 平均吞吐 | 约9 tokens/s |
| 显存占用 | 73GB(接近打满) |
这个数据放到生产环境是没法看的。单请求还好,一旦并发上来,显存直接不够用,更别提服务稳定性。接下来我按顺序做了四轮优化,每一轮都单独验证,确保知道是哪个改动带来的收益。
3.2 优化一:打开连续批处理,吞吐直接翻倍
第一件事是把vLLM里的连续批处理能力用起来,同时开启greedy decoding下的预分配优化。严格来说这不是什么奇招,它只是把框架的默认优势发挥出来。
改动后的数据非常直接:同样64个请求、同样的显存,吞吐量从9 tokens/s涨到28 tokens/s左右,提升了2倍不止。TTFT因为批处理排队,稍微涨到2.1s,但TPOT没有明显恶化,整体可接受。
这一步的教训是:在你费劲搞量化、改算子之前,先把框架自身的能力榨干。很多人插上vLLM就以为自动吃到Continuous Batching红利,其实框架的默认配置往往偏保守,显存预分配策略、max_num_seqs、max_num_batched_tokens这些参数都会显著影响实际收益,需要根据场景调。
3.3 优化二:KV Cache量化,把显存抠出来换并发
基线里显存已经73GB打满了,想提升并发,就得先省显存。KV Cache精度从FP16降到FP8,是我认为最稳的一步。因为KV Cache在推理过程中是重复写入、重复读取的,对精度不如权重敏感,FP8的量化误差在多数任务里可以忽略。
实际操作中,我在vLLM里开了kv_cache_dtype=fp8_e4m3,并调整了KV Cache的预留比例。显存占用立刻从73GB降到62GB左右,释放出的空间足够把max_num_seqs调大。这一轮单看速度没有提升,但把并发能力从6路提升到了12路,整机吞吐量从28涨到51 tokens/s。
你说"显存不着急,我显存才用了一半"?我劝你还是早点把KV Cache量化打开。KV Cache省下来的空间不仅可以提高并发,还能降低max_model_len不够用的风险。很多OOM事故都是长序列请求进来后把预留的Cache空间挤爆引起的。
3.4 优化三:开前缀缓存,打掉重复计算的浪费
前面说过,生成场景里大量重复前缀是极常见的。这次压测的请求里,每个人都带着一段2K长度的system prompt。这个重复的上下文没开缓存之前,每个新请求进来都要完整跑一遍prefill。
打开前缀缓存后,请求在prefill阶段的耗时明显下降,TTFT从2.1s降到0.9s左右。这部分的收益和prompt复用率强相关,如果你们的场景全是独立问题、几乎无共享前缀,这一步效果就会打折扣。但原则上我建议始终开启,因为它几乎没有副作用。
这里有个细节:前缀缓存命中依赖token完全一致,所以system prompt末尾如果带有时间戳或随机ID等动态内容,缓存就会失效。我见过好几个团队因为这个原因报"缓存没用",排查之后发现是动态内容把前缀切断了。
3.5 优化四:张量并行与批大小联合调优
最后一步是把所有参数联合调整。基线是8卡张量并行,但之前max_num_seqs只设了6,远没有发挥出多卡的威力。随着显存释放,我把max_num_seqs逐步提高到16、24、32,并同步观察TPOT变化。注意,TPOT不会因为batch变大而无限制恶化,因为只要显存不爆,GPU计算单元大多数时候仍在等待访存,batch大了反而能把带宽占满。
最终在max_num_seqs=24、启用FP8 KV Cache、前缀缓存开启、连续批处理开启的情况下,收敛到比较满意的状态:
| 指标 | 优化后 | 对比基线 |
|---|---|---|
| TTFT | 0.9s | 降幅50% |
| TPOT | 78ms/token | 降幅30% |
| 平均吞吐 | 276 tokens/s | 提升约30倍 |
| 显存占用 | 68GB | 基本打满但稳定 |
从9 tokens/s到276 tokens/s,30倍就是这么来的。每一轮单独来看都不是爆炸性的,但组合起来就是量级的差距。
3.6 这一轮的调优心得
- 一次只改一个变量。我在这轮里严格遵循了"先开框架能力、再省显存、再降冗余计算、最后批量调参"的顺序,任何一个步骤出了问题都能立刻定位。
- 每一步必须重测完整指标,别只看单个TTFT或TPOT。有些参数优化了延迟却压低了吞吐,权衡要靠全局数据。
- 把优化清单化和数据化。我习惯用表格记录每一轮的参数和结果,一个月后回看,比任何文档都有说服力。
4. 避坑指南与排查思路:那些文档里不写的经验
4.1 显存OOM排查思路
OOM是推理服务最常见的事故。我的排查顺序是这样的:
- 先看权重占多少、KV Cache预留占多少。用nvidia-smi和框架的metrics对不上时,优先信框架的统计接口,nvidia-smi显示的是整体显存,包括CUDA context、激活值、临时缓冲区等,容易误导。
- 查max_model_len和并发请求的序列长度分布。最大长度*并发数往往就是KV Cache的上限,一旦某个请求很极限,就会立刻触发OOM。
- 查框架的显存预留比例。vLLM里的gpu_memory_utilization默认0.9,如果又开了大量额外buffer,OOM概率会很高。
解决方式也直接:降低max_model_len、收紧并发上限、开启KV Cache量化、或者换更大的GPU。
4.2 偶发的TPOT飙高问题
有一次压测时发现平均TPOT正常,但每过几个请求就出现一次300ms以上的尖峰。排查发现,是有长序列请求进入时,KV Cache需要重新分配页,触发显存整理和内存拷贝。PagedAttention虽然减轻了碎片化,但极端情况下仍会有分配开销。
最终解决方法是调整框架的调度参数,限制了超长请求的批内比例,并对请求按长度分组调度。尖峰频率大幅下降。基本思路是:不要让长短悬殊的请求坐到同一班车里,会互相拖累。
4.3 量化后效果不如预期
很多团队为了省显存直接上INT4量化,结果发现任务效果下降明显。这个问题的根因通常是校准集和真实数据分布不一致。AWQ、GPTQ这类方法需要一小批有代表性的数据做校准,校准集选不好,量化后的模型权重就"偏"了。
我的做法是至少准备一份和线上业务同分布的prompt集合,长度、风格、任务类型都要覆盖。量化的取舍要结合显存、吞吐、精度三个维度来做,不要在没评估的情况下为了省显存直接压到INT4。
4.4 常见问题速查表
我整理了一份实战中高频问题与排查办法,方便直接作为参考手册:
| 症状 | 常见原因 | 排查方向 | 常用解法 |
|---|---|---|---|
| 显存OOM | KV Cache预留不足或并发过高 | 检查显存分布与max_num_seqs | 调低并发、量化KV Cache、限制最大长度 |
| 吞吐低 | 未开启连续批处理 | 查看框架调度配置 | 开启Continuous Batching、增大batch上限 |
| TTFT过长 | 重复前缀没走缓存 | 检查前缀缓存命中率 | 开启Prefix Caching、移除前缀尾部动态内容 |
| TPOT偶发飙高 | 长短序列混批 | 观察尖峰对应请求长度 | 按长度分组、限制超长请求比例 |
| 量化后质量下降 | 校准集偏差 | 对比量化前后效果指标 | 重做校准、换量化位宽、改用KV量化 |
| 多卡吞吐不增长 | 通信开销超过算力 | 观察GPU利用率与通信耗时 | 尝试单卡或更小并行度、改用更长batch |
这六条几乎覆盖了我大部分线上问题的排查路径,抄作业应该能解决掉80%的坑。
做LLM推理优化这段时间,我最大的体会是:这个领域的优化空间永远比想象中大,但前提是你得先把原理吃透。很多人从网上复制一段配置就跑到线上,出问题了连日志都看不懂,那是最难受的。如果你刚开始接触这个领域,我建议别着急上各种高大上的框架组合,先从KV Cache的显存计算开始,先理解清楚你模型的显存分布、访问特性和两个阶段的瓶颈差异,再谈优化。
最后分享一个我养成的习惯:为每一次调优建立一份checklist,记录环境、参数、改动点、前后指标对比。看起来麻烦,但当你遇到生产环境性能回退时,这份清单就是最快帮你缩小范围的工具。推理优化没有一劳永逸,换成新模型、新硬件甚至新的平均请求长度后,原来最优的参数都可能需要重新调,这个心理准备得有。