第1章:计算图与算子——从零理解AI模型的骨架

做AI系统优化这些年,我越来越觉得一个道理:不懂计算图,就别谈算子融合。就像修车你得先知道发动机长什么样,对吧?

今天咱们就从最基础的东西聊起。别急,我保证不枯燥。

1.1 计算图:AI模型的“施工图纸”

什么叫计算图?说白了,就是一张有向无环图(DAG)。

每个节点代表一个操作,每条边代表数据流动的方向。你想想看,一个神经网络前向传播的过程,其实就是数据沿着这张图一路“流”下去。

核心定义:计算图 = 节点(算子)+ 边(数据依赖)

我个人习惯把计算图想象成工厂的流水线。原材料(输入数据)从入口进来,经过一道道工序(算子),最终变成成品(输出结果)。

举个例子,一个简单的计算图:

输入 x → [MatMul] → [Add] → [ReLU] → 输出 y
           ↑          ↑
         权重 W      偏置 b

这里每个方括号就是一个算子,箭头就是数据流向。嗯,就是这么简单。

1.2 算子(Operator):计算图的最小执行单元

算子,也叫操作符、OP。在AI框架里,算子就是一个具体的数学运算。

常见的算子有:

  • 矩阵乘法(MatMul):全连接层的核心
  • 卷积(Conv2D):CNN的看家本领
  • 激活函数(ReLU、Sigmoid):引入非线性
  • 池化(MaxPool):降采样用的
  • 归一化(BatchNorm):稳定训练过程

每个算子都有三个基本属性:

属性 说明 举例
输入张量 算子要处理的数据 形状 [B, C, H, W] 的特征图
输出张量 算子计算后的结果 形状 [B, C_out, H_out, W_out]
参数/属性 控制算子行为的配置 卷积核大小、步长、padding

我的经验:在项目中,我经常看到新人把算子和层(Layer)搞混。其实层是算子的高级封装,一个层可能包含多个算子。比如一个卷积层,内部可能包含 Conv2D + BatchNorm + ReLU 三个算子。

1.3 数据流依赖:算子的“上下游关系”

计算图里,算子之间不是孤立的。一个算子的输出,可能是另一个算子的输入。这种关系就叫数据流依赖。

依赖关系分两种:

  • 直接依赖:A的输出直接给B用,B必须等A算完
  • 间接依赖:A和B没有直接连接,但共享同一个上游数据

为什么会这样?因为计算图是拓扑排序的。你想想看,如果B依赖A,那A没算完,B就不能开始。这就是并行计算的瓶颈所在。

避坑指南:我曾经在一个分布式训练项目里,因为没搞清楚算子间的依赖关系,把两个有依赖的算子强行分到不同设备上。结果通信开销比计算本身还大,训练速度反而慢了3倍。嗯,从那以后我每次画计算图都会先标清楚依赖边。

1.4 计算图的可视化:一张图看懂全局

光说理论太干,咱们画张图看看。

计算图示例:一个简单的两层神经网络 输入 x 权重 W1 MatMul 偏置 b1 Add ReLU 输出 h1 算子节点 参数节点 数据流依赖

这张图展示了一个简单的全连接层:输入 x 和权重 W1 做矩阵乘法,加上偏置 b1,再经过 ReLU 激活。每个箭头都代表一个数据依赖关系。

1.5 为什么理解这些对算子融合很重要?

好,现在咱们回到正题。算子融合要解决的核心问题是什么?减少通信开销。

你想想看,如果每个算子都单独执行,那中间结果就得写回显存,下一个算子再读进来。这一写一读,就是两次显存访问。在分布式场景下,甚至可能是跨设备的网络通信,那开销就更大了。

算子融合的思路很简单:把多个有依赖关系的算子合并成一个,中间结果直接留在寄存器或共享内存里,不用来回倒腾。

举个例子: Conv2D + BatchNorm + ReLU 三个算子,如果不融合,需要3次显存读写。融合成一个算子后,只需要1次读写。在 ResNet-50 这样的模型里,这种融合能减少30%-50%的显存带宽消耗。

但融合不是随便合的。你得搞清楚:

  • 哪些算子之间有数据依赖?
  • 依赖关系是线性的还是分叉的?
  • 融合后会不会破坏计算图的拓扑结构?

这些问题的答案,都藏在计算图和算子的基础知识里。

我的建议:刚开始学算子融合时,别急着看复杂的融合策略。先把计算图画清楚,把每个算子的输入输出标明白。我每次做性能优化,第一件事就是打印出模型的计算图,用笔在上面标出热点路径。这个方法虽然土,但特别管用。

好了,这一章的内容就到这儿。计算图是骨架,算子是零件,数据依赖是连接线。这三样东西搞明白了,后面讲具体的融合策略,你就能跟上节奏了。


想要更多资料,联系可可