第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 做矩阵乘法,加上偏置 b1,再经过 ReLU 激活。每个箭头都代表一个数据依赖关系。
1.5 为什么理解这些对算子融合很重要?
好,现在咱们回到正题。算子融合要解决的核心问题是什么?减少通信开销。
你想想看,如果每个算子都单独执行,那中间结果就得写回显存,下一个算子再读进来。这一写一读,就是两次显存访问。在分布式场景下,甚至可能是跨设备的网络通信,那开销就更大了。
算子融合的思路很简单:把多个有依赖关系的算子合并成一个,中间结果直接留在寄存器或共享内存里,不用来回倒腾。
举个例子: Conv2D + BatchNorm + ReLU 三个算子,如果不融合,需要3次显存读写。融合成一个算子后,只需要1次读写。在 ResNet-50 这样的模型里,这种融合能减少30%-50%的显存带宽消耗。
但融合不是随便合的。你得搞清楚:
- 哪些算子之间有数据依赖?
- 依赖关系是线性的还是分叉的?
- 融合后会不会破坏计算图的拓扑结构?
这些问题的答案,都藏在计算图和算子的基础知识里。
我的建议:刚开始学算子融合时,别急着看复杂的融合策略。先把计算图画清楚,把每个算子的输入输出标明白。我每次做性能优化,第一件事就是打印出模型的计算图,用笔在上面标出热点路径。这个方法虽然土,但特别管用。
好了,这一章的内容就到这儿。计算图是骨架,算子是零件,数据依赖是连接线。这三样东西搞明白了,后面讲具体的融合策略,你就能跟上节奏了。
想要更多资料,联系可可