1、算子融合概述:什么是算子融合、为什么需要算子融合、算子融合的收益分析
1.1 什么是算子融合?
算子融合,说白了就是把多个连续的计算操作合并成一个。你想想看,在深度学习模型里,我们经常看到 Conv + BN + ReLU 这种组合。传统做法是三个算子挨个执行,每个都要读写一次数据。融合之后,这三个操作变成一个大的 kernel,数据从显存读一次,算完所有结果再写回去。
我习惯把算子融合比作「流水线上的工序合并」。假设你要给零件喷漆、烘干、打磨,分开做的话,每个工序之间都要搬运零件。合并成一个工位后,零件不用挪窝,一气呵成。硬件加速的本质逻辑,就是这个道理。
核心定义:算子融合是将多个计算密集且数据依赖连续的算子,合并为一个复合算子,在单次 kernel 启动中完成全部计算。
1.2 为什么需要算子融合?
这个问题我经常被问到。很多刚入行的同学觉得,分开算也没什么不好,反正 GPU 算得快。但实际部署过就知道,瓶颈往往不在计算本身,而在数据搬运。
我曾经在一个边缘端项目里踩过坑。模型跑在 Jetson 上,Conv + BN + ReLU 分开实现,帧率死活上不去。用 profiler 一查,计算单元利用率不到 30%,大部分时间花在读写显存上。后来做了融合,帧率直接翻倍。嗯,从那以后我再也不敢轻视算子融合了。
具体来说,需要算子融合的原因有三点:
- 访存带宽瓶颈:现代 GPU 算力增长远超显存带宽增长。分开执行时,中间结果反复读写,带宽被白白浪费。
- kernel launch 开销:每次启动一个 kernel,CPU 都要下发指令、同步。算子多了,启动开销累积起来很可观。
- 计算密度不足:小算子往往计算量小、访存量大,无法充分利用硬件计算单元。
个人经验:我建议你在做部署优化时,先用 profiler 看看算子的「计算-访存比」。如果这个比值小于 10,基本就是融合的候选对象。
1.3 算子融合的收益分析
收益分析,我们得从两个维度来看:访存优化和计算密度提升。这两个是硬件加速的命门。
1.3.1 访存优化
访存优化是算子融合最直接的收益。我举个例子:
假设一个 Conv 输出 4MB 数据,BN 读这 4MB 再写 4MB,ReLU 再读再写。分开算的话,总访存量是 4 + 4 + 4 + 4 = 16MB(读两次、写两次)。融合后,Conv 算完直接进 BN 和 ReLU,只读一次输入、写一次输出,访存量降到 4 + 4 = 8MB。省了一半。
| 执行方式 | 读操作 | 写操作 | 总访存量 |
|---|---|---|---|
| 分开执行 | Conv读输入 + BN读Conv输出 + ReLU读BN输出 | Conv写输出 + BN写输出 + ReLU写输出 | 16MB |
| 融合执行 | 融合算子读输入 | 融合算子写输出 | 8MB |
你想想看,在带宽受限的硬件上,访存量减半意味着什么?意味着同样的带宽可以喂给计算单元更多数据,吞吐量自然就上去了。
1.3.2 计算密度提升
计算密度,指的是每个访存操作对应的计算量。融合后,中间数据不用写回显存,直接在寄存器或共享内存里流转,计算单元可以持续工作。
我记得有一次优化一个 MobileNet 模型,里面有很多 Depthwise Conv + Pointwise Conv 的组合。分开算的时候,计算密度只有 2-3 FLOPs/Byte,融合后提升到 15+ FLOPs/Byte。硬件利用率从 20% 飙到 70%。
关键指标:计算密度 = 总计算量 (FLOPs) / 总访存量 (Bytes)。融合的目标就是把这个比值尽可能做大。
1.3.3 其他隐性收益
- 减少 kernel launch 次数:融合后一次启动搞定,CPU 调度开销降低。
- 减少中间显存占用:不需要为每个中间结果分配独立 buffer,显存压力减小。
- 利于算子库优化:融合后的复合算子可以针对特定硬件手写汇编优化,比通用库更高效。
注意:算子融合不是万能的。如果融合后的算子计算量过大,导致寄存器溢出或共享内存不足,反而会降低性能。我曾经在融合一个超大 Conv 时踩过这个坑,性能不升反降。所以融合前一定要评估硬件资源上限。
1.4 算子融合的知识体系
下面这张图是我梳理的算子融合核心知识框架,涵盖了融合类型、收益维度、硬件影响和实现方法。你可以把它当作本章的思维导图。
1.5 一个简单的融合示例
光说不练假把式。我们看一个 Conv + ReLU 融合的伪代码,感受一下差异。
// 分开执行
__global__ void conv_kernel(...) { /* 计算卷积,写结果到global memory */ }
__global__ void relu_kernel(float* data) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
data[idx] = fmaxf(0.0f, data[idx]);
}
// 启动两次kernel
conv_kernel<<<grid, block>>>(input, output);
relu_kernel<<<grid, block>>>(output);
// 融合执行
__global__ void fused_conv_relu_kernel(...) {
// 计算卷积,结果直接进ReLU,不写回global memory
float conv_out = compute_conv(...);
float relu_out = fmaxf(0.0f, conv_out);
// 只写一次最终结果
output[idx] = relu_out;
}
// 只启动一次kernel
fused_conv_relu_kernel<<<grid, block>>>(input, output);
你看,融合后的代码更紧凑,数据在寄存器里流转,不用经过显存。这就是性能提升的根源。
我的建议:刚开始做算子融合时,先从 Conv + BN + ReLU 这种经典组合入手。这类融合在 TensorRT、TVM 等框架里已经高度优化,你可以先跑通再自己手写,对比性能差异。
好了,算子融合的基本概念和收益就讲到这里。记住三个关键词:访存优化、计算密度、kernel 启动开销。后面的章节我们会深入具体的融合策略和硬件适配技巧。