1. 为什么“最快推理”是个伪命题,但值得追
先把话说在前头:不存在一套放之四海皆准的“最快推理代码”。你在网上看到的那些“一行代码让大模型推理提速10倍”的标题,点进去十有八九是在特定硬件、特定模型、特定batch size下测出来的极限值,换个场景直接打回原形。我自己就踩过这个坑——照着某篇热文把torch.compile加上,结果在小显存卡上编译阶段直接OOM,折腾半天还不如原始版本稳。
但这不代表追求极致推理速度没有意义。恰恰相反,推理速度直接决定了单位算力能服务多少用户、单次请求的成本是多少、用户体验是“秒回”还是“转圈等到怀疑人生”。一个70亿参数的模型,优化前后吞吐量差3到5倍是常有的事,换算成真金白银就是服务器账单差好几倍。
这篇内容适合三类人看:一是手里有模型、想把它跑得更快的开发者;二是正在做推理服务、被延迟和成本两头夹击的工程同学;三是对底层机制好奇、想知道“为什么同样一个模型别人跑得比我快”的技术爱好者。我会从瓶颈到底在哪、有哪些真正有效的优化手段、怎么组合、怎么测这几个角度,把这件事讲透。全程不吹牛,只讲我实际验证过的东西。
需要先建立一个认知:推理优化是一个系统工程,不是某个单点技巧。它涉及计算、显存、内存带宽、调度、批处理、量化、编译等多个层面。你只优化其中一个,往往会被另一个瓶颈卡死。所以下面我会分层拆解,最后再讲怎么把它们串起来。
2. 先搞清楚瓶颈:你的推理到底慢在哪
2.1 大模型推理的两个阶段:Prefill和Decode
很多人一上来就问“怎么加速”,但连自己慢在哪个阶段都不知道。大模型的推理分成两个性质完全不同的阶段:
- Prefill(预填充)阶段:处理你输入的整段prompt,把所有token一次性喂进去,计算量大、并行度高,属于计算密集型。这个阶段GPU的算力利用率通常比较高。
- Decode(解码)阶段:一个token一个token地往外吐,每次只算一个新token,但要反复读取整个模型的权重和KV Cache,属于内存带宽密集型。这个阶段GPU算力大量闲置,瓶颈在显存带宽上。
这两个阶段的优化思路完全不同。Prefill阶段拼的是算力,适合用更大的batch、更激进的并行;Decode阶段拼的是带宽,适合用量化、KV Cache优化、投机解码等手段。如果你连自己卡在哪个阶段都没测,那所有优化都是瞎猜。
我一般会先跑一个简单的profiling,把两个阶段的耗时分别打出来。如果Prefill占比高,说明输入太长或者batch太大;如果Decode占比高(大多数在线服务都是这种情况),那重点就该放在带宽和KV Cache上。
2.2 显存带宽才是Decode阶段的真凶
举个直观的例子。一个70亿参数的模型,用FP16存储,权重大约14GB。Decode阶段每生成一个token,理论上要把这14GB权重完整读一遍(实际上有缓存,但量级差不多)。假设你的卡带宽是2TB/s,那读一遍权重就要7毫秒,一秒最多生成140个token左右。这就是为什么单请求的decode速度很难突破某个上限——它被显存带宽死死锁住了。
理解了这一点,很多优化手段就顺理成章了:量化把权重从FP16压到INT8甚至INT4,等于把要读的数据量砍半甚至砍到四分之一,速度自然上去;投机解码用小模型一次猜多个token,让大模型一次验证,等于摊薄了每个token的权重读取成本;KV Cache优化则是减少decode阶段额外要读的缓存数据。
2.3 一个容易被忽略的瓶颈:KV Cache的显存占用
长上下文场景下,KV Cache的显存占用会变得非常恐怖。它的计算公式大致是:
KV Cache大小 = 2 × 层数 × 注意力头数 × head_dim × 序列长度 × batch_size × 数据类型字节数以一个常见的70亿模型为例,32层、32个头、head_dim为128,序列长度拉到8192,batch为16,FP16存储,算下来KV Cache就要占用好几GB,甚至超过模型权重本身。这时候你会发现,明明权重没变,但能塞进去的batch越来越小,吞吐量反而下降。所以长上下文场景下,KV Cache的管理和压缩,优先级甚至高于权重优化。
3. 量化:性价比最高的一刀
3.1 为什么量化几乎总是第一个该做的
如果只能选一个优化手段,我会毫不犹豫选量化。原因很简单:它同时降低了显存占用和内存带宽压力,而这两者正是decode阶段的核心瓶颈。权重从FP16降到INT8,理论上带宽需求减半,decode速度接近翻倍;降到INT4,理论上能到3倍左右(实际因为反量化开销会打折扣,但提升依然显著)。
更重要的是,量化对部署门槛的降低是立竿见影的。原本需要两张卡才能塞下的模型,量化后一张卡就够了,这对成本敏感的场景意义巨大。
3.2 主流量化方案的取舍
市面上量化方案很多,我按实际使用体验给个对比:
| 方案 | 精度损失 | 速度提升 | 部署难度 | 适用场景 |
|---|---|---|---|---|
| FP16 | 无 | 基准 | 最低 | 精度要求极高、显存充足 |
| INT8(如bitsandbytes) | 很小 | 约1.5-2倍 | 低 | 通用场景,快速上手 |
| GPTQ | 小 | 约2-3倍 | 中 | 单卡部署大模型 |
| AWQ | 小 | 约2-3倍 | 中 | 对精度更敏感的场景 |
| GGUF(llama.cpp系) | 可调 | 视配置 | 低 | CPU/混合推理、边缘设备 |
我的经验是:先上INT8,如果显存还是不够或者速度还不满意,再考虑GPTQ或AWQ的4bit。不要一上来就冲4bit,因为4bit在部分任务上(尤其是需要精确推理、代码生成、数学计算)会有可感知的精度下降,得根据你的业务容忍度来定。
3.3 量化的坑:不是所有层都适合量化
这里有个很多人不知道的细节:模型的某些层对量化特别敏感,比如第一层和最后一层、以及注意力输出投影层。一些高级量化工具支持“混合精度量化”,把这些敏感层保留在FP16,其余层量化。实测下来,这种做法的精度损失比全量量化小很多,而速度损失几乎可以忽略。
另外,量化后的模型首次加载会慢一些,因为要做反量化或者权重重排。如果你做的是冷启动频繁的服务,这一点要提前考虑,最好把量化后的权重缓存下来。
4. 批处理与调度:把GPU喂饱的艺术
4.1 连续批处理(Continuous Batching)为什么是游戏规则改变者
传统的静态批处理是这样的:攒够一批请求,一起送进去,等这一批全部生成完,再处理下一批。问题在于,同一批里有的请求生成10个token就结束了,有的要生成500个,短请求结束后它占的显存和算力就白白浪费了,得等长请求跑完。
连续批处理(也叫迭代级调度)改变了这个逻辑:每个decode step都重新组批,已经结束的请求立刻踢出去,新来的请求立刻加进来。这样GPU几乎永远处于满载状态,吞吐量能提升好几倍。现在主流的推理框架(vLLM、TGI、TensorRT-LLM等)都默认支持这个机制。
我实测过一个场景,同样的硬件和模型,静态批处理吞吐是每秒300 token,换成连续批处理后直接飙到每秒900 token以上。这是我认为仅次于量化的第二大优化点。
4.2 PagedAttention:解决KV Cache碎片化
连续批处理带来一个新问题:KV Cache的显存管理变得复杂。不同请求的序列长度动态变化,如果按最大长度预分配,浪费巨大;如果动态分配,又容易产生碎片。
PagedAttention的思路借鉴了操作系统的虚拟内存分页:把KV Cache切成固定大小的块(block),按需分配,用一张表来映射逻辑位置和物理块。这样显存利用率能到90%以上,而且支持不同请求共享相同的块(比如相同的system prompt)。这个机制是vLLM的核心创新之一,也是它吞吐量高的关键原因。
4.3 调度策略的细节:优先级与抢占
在线服务里,请求的优先级往往不一样。有的用户愿意等,有的必须秒回。这时候就需要带优先级的调度,甚至支持抢占——高优先级请求来了,可以暂停低优先级请求,把资源让出来。
但抢占是有代价的:被抢占的请求,它的KV Cache要么换出到内存(慢),要么直接丢弃重算(更慢)。所以抢占策略要谨慎设计,不能频繁触发。我的做法是给不同优先级的请求设置不同的等待阈值,只有超过阈值才触发抢占,避免系统在临界点反复横跳。
5. 投机解码:用“猜”来换速度
5.1 投机解码的基本原理
投机解码(Speculative Decoding)的核心思想很巧妙:用一个小的“草稿模型”快速生成一串候选token,然后让大模型一次性验证这串token对不对。因为大模型验证一串token的计算量,和生成一个token差不多(都是读一遍权重),所以如果草稿模型猜得准,就能用一次大模型前向换来多个token的输出。
这里的关键是:验证过程是并行的,而且能保证输出分布和大模型单独生成完全一致(数学上可以证明)。也就是说,投机解码不牺牲精度,纯粹是拿小模型的计算换大模型的带宽。
5.2 草稿模型怎么选
草稿模型的选择直接决定加速效果。理想情况下,草稿模型要满足两个条件:一是足够快,不能拖后腿;二是和大模型的输出分布足够接近,猜得准。
常见的做法有几种:用同系列的小模型(比如大模型是70B,草稿用7B);用专门训练的草稿头;或者用n-gram、检索等非神经网络的方法做草稿。我实测下来,同系列小模型的效果最稳,接受率(草稿token被大模型认可的比例)通常能到70%以上,加速比在1.5到2.5倍之间。
5.3 投机解码的适用边界
投机解码不是万能的。它的加速效果高度依赖接受率,而接受率又和任务类型强相关。对于高度确定性的任务(比如代码补全、格式化输出、翻译),接受率很高,加速明显;对于开放性创作(比如写故事、头脑风暴),接受率会掉下来,加速效果打折。
另外,投机解码会增加显存占用(要同时装两个模型),在显存紧张的场景要权衡。我的建议是:先测接受率,再决定要不要上。如果接受率低于50%,那这套方案在你的场景里可能不划算。
6. 编译与算子优化:榨干最后一点性能
6.1 torch.compile到底值不值得用
torch.compile是PyTorch官方推出的编译加速方案,原理是把动态图编译成静态图,然后做算子融合、内存规划等优化。理论上能带来20%到50%的提升,但实际体验比较看场景。
我踩过的坑:首次编译时间可能很长,尤其是大模型,编译几分钟甚至十几分钟都正常;编译缓存的管理也是个问题,模型一变、输入shape一变,可能就要重新编译。所以它更适合输入shape相对固定、服务长期运行的场景,不适合频繁变动的实验环境。
如果决定用,记得把编译缓存持久化到磁盘,避免每次重启都重新编译。另外,mode="max-autotune"虽然能榨出更多性能,但编译时间会成倍增加,生产环境要谨慎。
6.2 FlashAttention:注意力机制的必选项
FlashAttention解决的是注意力计算中的显存读写问题。标准注意力实现要把中间结果(注意力矩阵)写回显存再读出来,IO开销巨大。FlashAttention通过分块计算,把中间结果留在片上高速缓存里,大幅减少了显存访问。
现在FlashAttention已经迭代到第二、第三代,对长序列的加速尤其明显。只要你的模型支持,几乎没有任何理由不开它。它不改变计算结果,纯粹是工程优化,属于“白捡的加速”。
6.3 算子融合与自定义kernel
更进阶的优化是算子融合:把多个连续的小算子合并成一个大的kernel,减少kernel启动开销和中间结果的显存读写。比如把LayerNorm、残差连接、激活函数融合在一起。这类优化通常由推理框架或专门的kernel库(如TensorRT-LLM、FasterTransformer)提供,自己手写门槛较高。
我的建议是:除非你是专门做推理引擎的团队,否则优先用成熟框架,不要自己造轮子。这些框架已经把常见的融合模式都做完了,自己搞容易踩坑还未必更快。
7. 怎么测才算数:基准测试的正确姿势
7.1 别被“峰值速度”骗了
很多宣传里的速度是峰值速度——在最优条件下测出来的极限值。但真实服务里,请求是波动的,有高峰有低谷,有长有短。真正该看的是P99延迟和稳定吞吐量,而不是某个瞬间的峰值。
我一般会关注这几个指标:
- TTFT(Time To First Token):首token延迟,直接影响用户感知的“响应快不快”。
- TPOT(Time Per Output Token):每个输出token的耗时,决定“吐字”是否流畅。
- 吞吐量(tokens/s):单位时间能处理多少token,决定成本。
- P99延迟:最慢那1%请求的延迟,决定用户体验的下限。
7.2 测试要贴近真实负载
测试时最容易犯的错是用固定长度的输入输出。真实场景里,输入长度是变化的,输出长度也是变化的,而且往往服从某种长尾分布。用固定长度测出来的结果,和真实情况可能差很远。
我的做法是:构造一个符合真实分布的请求集,输入长度和输出长度都按实际业务的分布来采样,然后跑足够长的时间,观察各项指标的稳定性。这样才能测出真正有参考价值的数据。
7.3 一个可复现的测试流程
给你一套我常用的测试流程,可以直接抄:
- 固定硬件和软件环境,记录GPU型号、驱动版本、框架版本、CUDA版本。
- 准备测试数据集,按真实分布采样输入输出长度,至少准备几百条。
- 预热,先跑几十条请求让编译缓存、显存分配稳定下来。
- 正式测试,用并发压测工具(如locust、wrk的变体)按不同并发级别跑,记录TTFT、TPOT、吞吐、P99。
- 对比不同配置,每次只改一个变量(比如只改量化方案),保证对比有效。
- 重复多次,取稳定后的中位数,避免偶然波动。
注意:测试时一定要监控GPU利用率和显存占用,如果GPU利用率长期低于50%,说明瓶颈不在计算,而在调度或IO,这时候加算力是浪费。
8. 组合拳:一套可落地的优化顺序
讲了这么多单点技术,最后说说怎么组合。我的经验是按性价比排序,逐步叠加:
第一步,量化。先上INT8,看显存和速度是否满足需求。不够再上4bit。这一步投入小、见效快。
第二步,换用支持连续批处理和PagedAttention的推理框架。比如vLLM或TensorRT-LLM。这一步是吞吐量的大头,通常能带来数倍提升。
第三步,开启FlashAttention。基本是白捡的加速,没有理由不开。
第四步,根据场景决定是否上投机解码。先测接受率,接受率高的场景再上。
第五步,考虑torch.compile或框架自带的图优化。这一步收益相对小,且有一定复杂度,放在最后。
第六步,精细化调优。包括调度策略、KV Cache管理、batch size调参等,属于锦上添花。
这套顺序的逻辑是:先做收益大、风险低的,再做收益小、复杂度高的。每一步都要测,确认有效再进入下一步,不要一次性全上,否则出了问题都不知道是哪个环节导致的。
9. 几个我踩过的坑和私房经验
最后分享几个文档里不会写、但实际会遇到的坑。
坑一:量化后精度下降,但不知道降在哪。解决办法是准备一个精度评估集,覆盖你的核心业务场景,每次量化后都跑一遍,对比量化前后的输出差异。不要只看困惑度这种宏观指标,要看具体任务的准确率。
坑二:batch size调大后速度反而下降。这通常是因为显存不够,触发了换页或者重算。batch size不是越大越好,要找到那个“甜点”——刚好把显存吃满、又不触发换页的点。这个点因模型、因硬件而异,必须实测。
坑三:多卡推理的通信开销被低估。张量并行能让你跑更大的模型,但卡之间的通信是有成本的。如果模型本身能塞进单卡,优先单卡,多卡并行的加速比往往达不到线性,甚至可能因为通信拖累而变慢。
坑四:忽略了请求的排队时间。你优化了半天单请求速度,结果请求在队列里排了半天,用户感知还是慢。端到端延迟 = 排队时间 + 推理时间,两者都要优化。提高并发处理能力、优化调度策略,有时候比抠单请求速度更有效。
坑五:版本兼容性。推理优化这块生态更新极快,框架版本、CUDA版本、驱动版本之间经常有兼容性问题。锁定一套验证过的版本组合,不要轻易升级,除非新版本有你需要的关键特性。
我个人在实际操作中的体会是:推理优化没有银弹,只有不断测量、不断迭代。每换一个模型、换一套硬件、换一个业务场景,最优配置可能都要重新调。把这套方法论掌握好,比记住某个具体参数值重要得多。