计算图与算子:理解训练效率的基石

各位同学,今天我们来聊聊算子融合的基础——计算图和算子。说实话,这两个概念我刚开始接触时也觉得挺抽象的,直到有一次在优化一个BERT模型时,发现计算图里密密麻麻的节点,才真正意识到它的重要性。

计算图的基本概念

计算图,说白了就是深度学习框架用来描述运算流程的一种数据结构。它把复杂的神经网络拆解成一个个基本操作,然后用有向图的方式组织起来。

我个人习惯把计算图想象成一张「菜谱流程图」:

  • 节点(Node):代表一个具体的运算,比如矩阵乘法、激活函数
  • 边(Edge):代表数据流动的方向,也就是张量的传递路径

举个例子,一个简单的线性层 y = Wx + b,在计算图中会被拆成两个节点:矩阵乘法节点和加法节点。数据从输入 x 出发,先经过 MatMul 节点,再流向 Add 节点,最后输出 y。

关键点:计算图是静态的,但数据是动态流动的。框架先构建图结构,再喂入数据执行。

我在项目中遇到过一个问题:刚开始用 PyTorch 时,总觉得动态图和静态图差不多。直到有一次部署模型到移动端,发现静态图可以提前做很多优化,而动态图却不行。嗯,这里要注意,不同的计算图模式决定了后续优化的空间。

算子的定义与分类

算子(Operator)就是计算图中的节点。每个算子完成一个特定的数学运算。你想想看,一个神经网络动辄几百层,其实就是这些算子的排列组合。

我通常把算子分成三类:

类别 典型算子 特点
计算密集型 Conv2D, MatMul, BatchNorm 计算量大,访存少
访存密集型 Reshape, Transpose, Concat 数据搬运多,计算少
元素级算子 ReLU, Sigmoid, Add 每个元素独立运算

为什么要这样分类?因为不同类型的算子,融合策略完全不同。我曾经踩过一个坑:把两个访存密集型算子强行融合,结果性能反而下降了。后来才明白,融合的核心是减少访存开销,而不是盲目合并。

小技巧:在分析模型性能时,先看算子的类型分布。如果访存密集型算子占比过高,优先考虑算子融合;如果计算密集型算子占主导,则考虑量化或剪枝。

计算图中的数据流

数据流描述了张量在计算图中的传递过程。每个算子接收输入张量,经过计算后产生输出张量,再传递给下一个算子。

这里有个重要的概念——数据依赖。如果算子 B 需要算子 A 的输出,那么 A 必须在 B 之前执行。这种依赖关系决定了计算图的执行顺序。

我画了一张图来展示这个过程:

输入 x MatMul Add 输出 y 权重 W 偏置 b 图:线性层 y = Wx + b 的计算图与数据流

从图中可以看到,数据流是单向的,从输入流向输出。权重 W 和偏置 b 作为常量输入,不参与数据流的主路径。

为什么数据流这么重要?因为算子融合的本质,就是缩短数据流的路径。你想想看,如果两个相邻的算子可以合并成一个,中间那个临时张量就不用写回显存了,直接留在寄存器里继续算。这一来一回,能省下不少时间。

避坑指南:我曾经在优化一个 Transformer 模型时,试图把 LayerNorm 和后面的 Add 融合。结果发现 LayerNorm 的均值和方差计算需要全局信息,融合后反而破坏了数值精度。所以,融合不是万能的,一定要验证数值正确性。

计算图的执行模式

目前主流框架有两种执行模式:

  • 静态图(Graph Mode):先构建完整的计算图,再执行。TensorFlow 1.x 和 MindSpore 采用这种方式。优点是便于全局优化,缺点是调试不方便。
  • 动态图(Eager Mode):边构建边执行。PyTorch 是典型代表。优点是灵活易调试,缺点是优化空间有限。

我个人更倾向于动态图做开发,静态图做部署。PyTorch 2.0 的 torch.compile 其实就是试图结合两者的优点——先用动态图写代码,再通过 JIT 编译成静态图做优化。

嗯,这里要注意,算子融合在静态图上更容易实现,因为框架能看到完整的计算图结构。动态图模式下,框架不知道下一步会执行什么算子,融合的难度就大很多。

小结

今天我们聊了计算图、算子和数据流这三个基础概念。说白了,它们就是深度学习框架的「语法」和「词汇」。理解这些,你才能明白算子融合到底在优化什么——减少数据搬运、合并计算步骤、缩短执行路径。

下一节我们会深入具体的融合策略,比如 Horizontal Fusion 和 Vertical Fusion。到时候我会拿实际项目中的案例来讲解,保证让你听得过瘾。

核心要点回顾:

  • 计算图是框架描述运算流程的数据结构
  • 算子分为计算密集型、访存密集型和元素级三类
  • 数据流决定了算子的执行顺序和依赖关系
  • 算子融合的本质是缩短数据流路径,减少访存开销

想要更多资料,联系可可