计算图与算子:基础概念与实战理解
好,咱们今天聊聊计算图和算子。说实话,这两个概念是深度学习推理加速的基石。你如果不理解它们,后面讲算子融合、内存优化什么的,基本就是听天书。
我个人习惯把计算图想象成一张「菜谱」。算子就是里面的每个步骤——切菜、焯水、爆炒。而依赖关系呢,就是「你必须先切好菜,才能下锅炒」。就这么简单。
计算图是什么?
计算图,说白了就是一个有向无环图(DAG)。每个节点代表一个算子,每条边代表数据流动的方向。
举个例子,一个简单的计算图:
输入 → Conv → ReLU → Pool → 输出
这个图里,Conv 的输出是 ReLU 的输入。ReLU 必须等 Conv 算完才能开始。这就是依赖关系。
我在项目中遇到过一种情况:有人把计算图画得特别复杂,几十个节点串在一起。结果推理速度慢得离谱。后来一分析,发现很多节点其实可以并行执行,但图结构没设计好,白白浪费了 GPU 的并行能力。
核心要点:计算图不仅是代码的抽象,更是优化的蓝图。你如何组织这张图,直接决定了推理引擎能做什么优化。
嗯,这里要注意:计算图有两种表示方式——静态图和动态图。静态图(如 TensorFlow 1.x)先定义再执行,适合部署优化。动态图(如 PyTorch)边定义边执行,灵活但优化空间小。我个人更倾向于用静态图做推理加速,因为编译器能看到全局信息。
常见算子类型
算子就是计算图中的基本操作单元。咱们挑几个最常用的聊聊。
卷积(Conv)
卷积是 CNN 的绝对核心。它做的事情很简单:用一个滑动窗口在输入上提取特征。
你想想看,一张 224x224 的图片,经过卷积后变成 112x112 的特征图。这中间的计算量有多大?我算过,一个标准的 3x3 卷积,输入输出都是 64 通道,一次前向传播就要做几百万次乘加运算。
所以卷积算子往往是推理加速的重点优化对象。我曾经在一个项目中,把卷积的 im2col + GEMM 实现换成了 Winograd 算法,速度直接快了 2 倍。
ReLU 激活函数
ReLU 太简单了,就是 max(0, x)。但它有个特点:计算量极小,几乎不占时间。
那为什么还要专门提它?因为 ReLU 经常和 Conv、BN 等算子「粘」在一起。你想想看,Conv 的输出直接进 ReLU,中间没有其他操作。那为什么不把 ReLU 融合到 Conv 里面去?这就是算子融合的雏形。
实战技巧:ReLU 和 Conv 融合后,不仅减少了 kernel launch 的开销,还能减少一次内存读写。对于带宽受限的场景,这个优化效果非常明显。
批归一化(BN)
BN 在训练时很重要,能加速收敛、防止梯度消失。但在推理时,BN 其实可以「折叠」到前面的 Conv 或全连接层中。
为什么?因为推理时 BN 的均值和方差是固定的。你可以把 BN 的缩放和平移操作,直接吸收到 Conv 的权重里。这样推理时就少了一个算子。
我曾经踩过一个坑:BN 折叠后精度掉了 0.1%。排查了半天,发现是 BN 的 epsilon 参数在折叠时没处理好。嗯,这种细节问题,你不亲自调一遍真的很难发现。
加法(Add / Element-wise Add)
Add 算子通常出现在残差连接(ResNet)中。它就是把两个张量逐元素相加。
Add 本身计算量不大,但它会引入一个依赖关系:Add 必须等两个输入都准备好才能执行。这在并行调度时是个瓶颈。
我建议你在设计计算图时,尽量把 Add 放在合适的位置。有时候调整一下算子的顺序,就能让两个输入更早地准备好,从而减少等待时间。
算子间的依赖关系
依赖关系决定了算子的执行顺序。理解依赖关系,是进行算子融合和并行调度的前提。
依赖关系分两种:
- 数据依赖:一个算子的输出是另一个算子的输入。这是最常见的依赖。
- 控制依赖:一个算子的执行条件依赖于另一个算子的结果。比如 if-else 分支。
在推理加速中,我们主要关注数据依赖。因为控制依赖在推理时通常已经被展开或消除了。
举个例子,看下面这个计算图:
输入 → Conv1 → ReLU1 → Conv2 → ReLU2 → Add → ReLU3 → 输出
↘——————————↗
这里 Conv1 的输出分了两路:一路直接进 ReLU1,另一路跳过 ReLU1 直接进 Add。所以 Conv1 和 Add 之间有直接的数据依赖。而 Conv2 必须等 ReLU1 算完才能开始。
你想想看,这个图里哪些算子可以并行?Conv1 和 Conv2 不能并行,因为 Conv2 依赖 Conv1 的输出。但 ReLU1 和 Conv2 其实可以部分重叠执行——只要 Conv1 算出一部分数据,ReLU1 就可以开始处理,同时 Conv2 也可以开始计算。
注意:依赖关系分析是编译器优化的核心。如果分析得不够细,就会错过并行机会。如果分析得太细,又会增加编译开销。这个度需要根据实际场景来把握。
知识体系总览
下面这张图展示了本章的核心逻辑:
总结一下
计算图是推理加速的「地图」,算子是地图上的「地标」,依赖关系是连接地标的「道路」。你只有把这三者都搞清楚了,才能知道哪些算子可以融合、哪些可以并行、哪些需要优化。
我个人建议你动手画一画自己模型的计算图。不用多复杂,就画主干网络的那几个关键算子。画完之后,你自然就能看出哪些地方可以优化了。
想要更多资料,联系可可