计算图与算子:计算图基础、算子的定义与分类、计算图中的数据流

好,咱们正式开始聊算子融合。在动手优化之前,有一件事必须搞清楚——计算图和算子到底是什么。

我见过不少同学,一上来就盯着融合策略猛看,结果连计算图怎么构建的都没弄明白。嗯,这就像你连地图都没看懂,就急着规划最短路径。所以,咱们先把基础打牢。

计算图基础

说白了,计算图就是一个有向无环图(DAG)。它把整个神经网络的计算过程,拆解成一个个节点和边。

  • 节点(Node):代表一个算子,比如卷积、ReLU、加法。
  • 边(Edge):代表数据流动,也就是张量(Tensor)。

我个人习惯把计算图想象成「工厂流水线」。每个节点是一个工位,边就是传送带上的零件。数据从输入流进来,经过一个个工位加工,最后变成输出。

核心要点:计算图是静态的,它描述了「计算逻辑」,而不是「执行过程」。你定义好图,框架再决定怎么跑。

我在项目中遇到过一个问题:有人把计算图定义得特别深,一个分支连了十几个小算子。结果推理时显存爆了。为什么?因为框架为了保持图结构,不得不把中间结果都存下来。这就是典型的「图设计不合理」带来的性能损失。

算子的定义与分类

算子,就是计算图里的节点。每个算子完成一个特定的数学操作。

你想想看,一个神经网络有成百上千个算子。如果每个都单独执行,调度开销、内存读写开销都很大。这就是我们要做融合的原因。

算子通常分为几类:

类别 典型算子 特点
元素级算子 ReLU、Sigmoid、Add、Mul 每个元素独立计算,不依赖邻居
规约算子 Sum、Mean、Softmax 将一组数据合并成一个或少量值
访存密集型算子 Reshape、Transpose、Concat 计算量小,但数据搬运量大
计算密集型算子 Conv、MatMul、GEMM 计算量大,访存相对较少

这里有个避坑指南:我曾经把Reshape和Transpose这类算子当成「免费」的,觉得它们不耗算力。后来做性能分析才发现,它们占用了大量内存带宽。尤其是Transpose,数据重排会导致缓存命中率暴跌。所以,融合时千万别忽略它们。

计算图中的数据流

数据流,就是张量在计算图里的传递路径。每个算子的输出,会成为下一个算子的输入。

数据流有几个关键属性:

  • 形状(Shape):张量的维度信息,比如 [B, C, H, W]。
  • 数据类型(Dtype):float32、float16、int8 等。
  • 内存布局(Layout):NCHW 还是 NHWC,这直接影响访存效率。

我建议你重点关注数据流的局部性。什么意思?就是看数据在算子之间传递时,能不能尽量留在缓存里。如果能,融合后的性能提升会非常明显。

举个例子:一个 Conv 后面接一个 ReLU。如果分开执行,Conv 的输出要写回显存,ReLU 再读进来。但如果融合成一个算子,ReLU 可以在 Conv 的计算过程中直接完成,省掉一次读写。这就是融合的核心收益之一。

小技巧:分析数据流时,可以用 profiling 工具看每个算子的「输入输出大小」。如果两个算子之间传递的数据量很大,而且它们都是访存密集型的,那融合的收益通常很高。

下面这张图,是我自己画的一个简化版计算图,展示了数据流和算子的关系:

输入张量 Conv (计算密集型) ReLU (元素级) 输出张量 融合区域 Conv + ReLU 合并为1个算子 减少1次显存读写

你看,数据流就是一条线。但这条线上,每个节点都有优化的空间。融合的本质,就是把多条短线合并成一条长线,减少中间的「交接」成本。

注意:不是所有算子都适合融合。比如,如果两个算子之间依赖的是全局数据(像 Softmax 需要所有元素参与),强行融合反而可能破坏并行性。一定要具体分析。

好了,计算图和算子的基础就聊到这儿。记住一句话:理解数据流,是算子融合的第一步。 后面我们会基于这个基础,一步步深入具体的融合策略。

想要更多资料,联系可可