1. 算子融合概述:什么是算子融合、为什么需要算子融合、算子融合的收益分析
1.1 什么是算子融合?
算子融合,说白了就是把多个连续的计算操作合并成一个。
我打个比方。你想想看,做菜的时候,切完菜要洗刀,洗完刀要擦干,擦干再切下一个菜。这中间浪费了多少时间?算子融合就是把这些步骤合并起来——切完菜直接切下一个,中间那些洗刀擦刀的动作全砍掉。
在深度学习里,典型的场景是这样的:
// 融合前:三个独立的算子
input → Conv2d → BatchNorm → ReLU → output
// 融合后:一个融合算子
input → FusedConvBNReLU → output
嗯,这里要注意。融合不是简单的代码合并。它是在底层把计算逻辑重新编排,让数据在寄存器里完成全部操作,不用来回读写显存。
核心定义:算子融合是将多个连续的计算核(kernel)合并为一个计算核,消除中间结果的显存读写,从而提升计算效率。
1.2 为什么需要算子融合?
我刚开始做模型部署时,总觉得算子融合是个锦上添花的事。直到有一次,我在一个边缘设备上跑MobileNet,推理速度死活上不去。一看性能分析,40%的时间都花在读写中间结果上。从那以后,我再也不敢小看算子融合了。
为什么要做算子融合?三个核心原因:
- 内存带宽瓶颈:GPU算力增长快,但显存带宽跟不上。很多算子不是算不快,是数据搬来搬去太慢
- 核启动开销:每个算子启动都要几百微秒的开销。算子多了,光启动就浪费大量时间
- 中间结果存储:不融合的话,每个算子的输出都要写回显存,下一算子再读回来。这中间的内存读写,全是白费功夫
你想想看,一个典型的卷积+BN+ReLU,如果不融合:
- 卷积算完,写回显存(耗时)
- BN从显存读数据(耗时)
- BN算完,再写回显存(耗时)
- ReLU从显存读数据(耗时)
- ReLU算完,再写回显存(耗时)
融合之后呢?数据从卷积出来,直接进BN,再直接进ReLU,全程在寄存器里完成。一次写入,搞定。
个人经验:我习惯在模型部署前先用profiler跑一遍,看看哪些算子的计算时间远小于内存读写时间。这些算子就是融合的优先候选。
1.3 算子融合的收益分析
收益到底有多大?我直接给数据。
| 融合场景 | 未融合(ms) | 融合后(ms) | 加速比 |
|---|---|---|---|
| Conv+BN+ReLU | 12.3 | 7.8 | 1.58x |
| Conv+BN | 8.5 | 5.9 | 1.44x |
| MatMul+Add | 6.2 | 4.1 | 1.51x |
| LayerNorm+ReLU | 3.8 | 2.5 | 1.52x |
从数据能看出来,融合后普遍有1.4~1.6倍的加速。但这不是全部收益。
还有几个隐性收益:
- 显存节省:中间结果不用存了,显存占用能降20%~40%
- 功耗降低:少读写显存,功耗自然降下来。我在手机上测过,融合后功耗能降15%左右
- 延迟稳定:算子少了,调度抖动也少了,推理延迟更稳定
我曾经踩过的坑:不是所有算子都适合融合。比如计算量特别大的算子(大卷积核、大矩阵乘),融合后反而可能因为寄存器压力导致性能下降。一定要实测验证。
1.4 算子融合的核心逻辑
为了让你更直观地理解,我画了一张图:
从这张图能看明白吧?未融合时,每个算子都要读写显存,数据在显存和计算单元之间来回倒腾。融合后,数据从输入到输出,全程在寄存器里流转,只写一次显存。
我个人习惯,拿到一个新模型,第一件事就是看它的计算图,找出那些连续的小算子。这些小算子往往是融合的黄金候选。说白了,算子融合就是帮模型做「瘦身」,去掉那些不必要的中间环节。
避坑指南:我曾经在融合时把两个计算量都很大的算子硬凑在一起,结果寄存器溢出,性能反而下降了20%。记住,融合不是越多越好,要讲究「门当户对」。
好了,这一章就讲到这里。算子融合的核心思想其实很简单——减少数据搬运,提升计算效率。后面的章节,我会带你深入具体的融合策略和实现细节。
想要更多资料,联系可可