4、推理加速关键技术:模型量化(INT8/FP16)、算子融合(Fuse)、内存复用、计算图优化
好,咱们今天聊点硬核的。推理加速这件事,说白了就是跟计算和存储较劲。我做了这么多年芯片,见过太多模型在理论算力上跑得飞起,一上板子就卡成PPT。为什么?因为光有算力不够,你得把数据喂得进去、存得下来、算得高效。
我个人习惯把推理加速技术分成四个维度:精度压缩、计算合并、存储复用、结构重组。这四板斧砍下去,大部分模型的推理延迟都能降一个数量级。咱们一个一个说。
4.1 模型量化:从FP32到INT8/FP16
量化,说白了就是用更少的比特去表示一个数。你想想看,FP32有32位,INT8只有8位,数据量直接缩到四分之一。带宽压力小了,计算单元也能塞更多。
但问题来了——精度会不会掉?会,但可控。
核心思路:用更少的比特,保留足够的信息。不是所有层对精度都敏感,找到那些“粗枝大叶”的层,大胆量化。
4.1.1 INT8量化:对称 vs 非对称
INT8量化有两种主流方式:对称量化和非对称量化。我简单解释一下。
- 对称量化:把浮点数的范围对称映射到[-127, 127]。适合权重分布比较对称的情况,比如经过BatchNorm后的激活值。
- 非对称量化:用零点偏移来适配任意分布。适合ReLU这种只有正数的激活函数。
我在项目中遇到过一个问题:某个模型用对称量化后,精度掉了2个点。排查了半天,发现是激活值分布严重偏斜。换成非对称量化,精度立马回来了。嗯,这里要注意——量化方式不是通用的,得看你的数据分布。
我的建议:先跑一遍校准集,统计每层的激活值分布。分布对称的用对称量化,不对称的用非对称。别偷懒,这一步值得做。
4.1.2 FP16量化:半精度浮点
FP16比INT8精度高,但比FP32省一半带宽。很多GPU原生支持FP16计算,比如NVIDIA的Tensor Core。我个人习惯在对精度敏感的层用FP16,比如分类网络的最后几层。
为什么?因为FP16的动态范围比INT8大,不容易溢出或下溢。但代价是——计算单元比INT8少。所以你得权衡:是精度重要,还是吞吐重要?
| 量化类型 | 位宽 | 带宽节省 | 精度损失 | 适用场景 |
|---|---|---|---|---|
| FP32 | 32 | 1x | 无 | 训练/高精度推理 |
| FP16 | 16 | 2x | 极小 | 敏感层/混合精度 |
| INT8 | 8 | 4x | 可控 | 大部分推理场景 |
注意:INT8量化后一定要做校准。我曾经跳过这一步,结果模型在边缘设备上直接输出乱码。校准集最好覆盖真实场景的数据分布,别用训练集凑合。
4.2 算子融合:把多个小算子合并成一个
算子融合,说白了就是减少计算和访存的开销。你想想看,一个Conv后面跟一个BatchNorm,再跟一个ReLU。如果分开算,每次都要把中间结果写回显存,再读出来。这中间浪费了多少带宽?
我建议的做法是:把Conv+BN+ReLU融合成一个算子。这样中间结果直接留在寄存器里,不用来回倒腾。在GPU上,这能省掉30%-50%的访存开销。
4.2.1 常见的融合模式
- Conv + BN + ReLU:最经典的融合,几乎每个推理框架都支持。
- Conv + Add + ReLU:ResNet中的残差连接,融合后减少一次写回。
- MatMul + Add:全连接层中的偏置加法,直接合并到矩阵乘法里。
我记得有一次优化一个检测模型,光是把Conv+BN+ReLU融合,推理速度就快了40%。你想想看,这还只是最基础的融合。如果再加上后续的量化,效果更明显。
避坑指南:不是所有算子都能融合。比如有些自定义算子,框架不支持融合。这时候你得手动写一个融合后的kernel。我曾经为了一个特殊的激活函数,硬是写了一个融合算子,效果还不错。
4.3 内存复用:让显存利用率翻倍
内存复用,说白了就是让同一块显存被多个张量共享。你想想看,推理过程中,很多中间张量用完就扔了。如果每个张量都单独分配显存,那显存很快就爆了。
我个人的做法是:用内存池来管理显存。提前分配一大块显存,然后按需分配和回收。这样能避免频繁的cudaMalloc和cudaFree,既省时间又省空间。
4.3.1 内存复用的策略
- 生命周期分析:分析每个张量的生命周期,把生命周期不重叠的张量放到同一块显存。
- 内存池化:预分配固定大小的内存块,按需分配,避免碎片。
- 原地操作:有些算子支持原地计算,比如ReLU可以直接覆盖输入。
我在项目中遇到过一个问题:一个模型在FP32下显存刚好够用,量化到INT8后反而爆显存了。排查后发现是内存碎片导致的。换成内存池后,问题解决了。嗯,这里要注意——内存复用不是简单的“省空间”,还得考虑碎片问题。
核心原则:能复用的绝不新分配,能原地计算的绝不拷贝。显存是稀缺资源,每一字节都得精打细算。
4.4 计算图优化:让计算更聪明
计算图优化,说白了就是重新组织计算顺序。你想想看,一个计算图里有很多节点,有些节点可以并行,有些节点可以合并,有些节点可以删除。优化计算图,就是找到最优的执行顺序。
4.4.1 常见的图优化技术
- 常量折叠:把编译时就能算出来的常量提前算好,避免运行时重复计算。
- 死代码消除:删除那些输出不会被用到的节点。
- 算子重排:把计算密集的算子放到一起,减少上下文切换。
- 并行调度:把没有依赖关系的节点放到不同的流上并行执行。
我记得有一次优化一个Transformer模型,发现计算图里有大量冗余的reshape和transpose操作。把这些操作合并后,推理速度提升了15%。你想想看,这还只是图优化,还没动算子本身。
我的建议:别手动优化计算图,用现成的框架。比如TensorRT、ONNX Runtime都自带图优化引擎。你只需要把模型导出成ONNX格式,剩下的交给框架。
4.5 总结:四板斧怎么用?
好,咱们把这四板斧串起来。我个人的经验是:先量化,再融合,然后优化内存,最后优化计算图。为什么是这个顺序?
- 量化:降低数据精度,减少带宽和计算量。这是最立竿见影的。
- 融合:合并小算子,减少访存开销。量化后的数据更小,融合效果更好。
- 内存复用:优化显存使用,避免碎片。量化+融合后,显存压力已经小了,但还得精打细算。
- 计算图优化:最后一步,优化执行顺序。前面的优化已经让计算图变简单了,图优化更容易生效。
当然,这不是绝对的。有些场景下,你可能需要先做图优化再做量化。比如有些量化工具对计算图结构有要求,得先优化才能量化。嗯,灵活一点,别死板。
最后说一句:推理加速不是一锤子买卖。你得反复调优,不断迭代。我做过一个项目,前前后后优化了五轮,才把延迟从100ms降到10ms。别怕麻烦,每一毫秒的提升都值得。
好,这一章就到这里。下一章咱们聊聊硬件加速器的设计思路,包括脉动阵列、数据流架构这些硬核内容。到时候见。