2. 深度学习基础与计算图概念:张量、算子、前向/反向传播、静态图与动态图、计算图的基本表示方法

AI编译器本质上是对深度学习模型的计算过程进行解析、优化和代码生成。要理解AI编译器的工作机制,首先需要掌握深度学习模型的基本数学对象与执行范式。本章将围绕张量(Tensor)、算子(Operator)、前向/反向传播(Forward/Backward Propagation)、静态图与动态图(Static/Dynamic Graph)以及计算图(Computation Graph)的基本表示方法展开,为后续学习图优化、算子调度和代码生成奠定基础。

计算图核心概念关系图 张量 (Tensor) 多维数组 + 数据类型 + 形状 算子 (Operator) Conv/MatMul/ReLU/Add 计算图 (Graph) 节点=算子,边=张量 输入/输出 拓扑组合 前向传播 (Forward) 输入 → 逐层计算 → 输出/损失 反向传播 (Backward) 链式法则 → 梯度计算 → 参数更新 梯度反向流动 静态图 (Static) 先定义后执行 全局优化 / 部署友好 动态图 (Dynamic) 边执行边构建 灵活调试 / 开发效率高 两种执行范式

2.1 张量与算子:计算图的基本组成单元

核心原理

张量是深度学习中最基础的数据结构,可以理解为多维数组的泛化。一个张量由三个核心属性定义:形状(Shape)、数据类型(Dtype)和存储布局(Layout)。形状描述各维度的大小,例如一个形状为[4, 3, 224, 224]的张量表示4张3通道224×224像素的图像;数据类型决定了每个元素占用的字节数,常见的有FP32、FP16、INT8、BF16等;存储布局则描述了数据在内存中的排列方式,如NCHW(通道在前)或NHWC(通道在后),这直接影响算子的访存效率。

算子是作用在张量上的基本计算单元,例如卷积(Conv2D)、矩阵乘法(MatMul)、激活函数(ReLU)、池化(Pooling)等。每个算子都定义了输入张量到输出张量的映射关系,并且通常带有可学习的参数(如卷积核权重)。在计算图中,算子被抽象为节点,张量被抽象为边,从而形成一张有向无环图(DAG)。

属性张量算子
本质数据容器计算逻辑
关键信息形状、Dtype、Layout类型、参数、输入/输出数量
在图中角色边(Edge)节点(Node)
编译期关注点内存分配、数据布局转换调度策略、内核选择、融合机会

判断方法或操作流程

在实际工程中,判断一个张量或算子是否适合优化,通常遵循以下流程:

  1. 形状分析:检查张量的形状是否在编译期完全已知。静态形状有助于编译器提前分配内存和进行循环展开。
  2. 算子分类:将算子分为访存密集型(如Elementwise Add)和计算密集型(如MatMul)。前者受内存带宽限制,后者受算力限制。
  3. 布局检查:确认张量的内存布局是否与目标硬件(如GPU的Tensor Core)匹配,不匹配时需插入布局转换算子。

案例拆解或实战场景

以PyTorch中一个简单的张量加法为例:c = a + b。在底层,PyTorch会创建一个Add算子节点,输入为张量a和b,输出为张量c。AI编译器在优化时,会检查a和b的形状是否一致、Dtype是否相同,并决定是否可以将该Add算子与相邻的ReLU算子融合为一个融合算子,从而减少一次内核启动开销和中间张量的内存读写。

常见误区或总结复盘

风险提示:初学者常忽略张量布局对性能的影响。例如,在GPU上使用NHWC布局进行卷积通常比NCHW更快,因为NHWC更利于利用Tensor Core的通道对齐特性。但若后续接的是按NCHW实现的算子,则需插入转置操作,反而增加开销。编译器需要全局权衡布局转换的代价。

2.2 前向传播与反向传播:梯度计算的两阶段范式

核心原理

深度学习模型的训练过程由前向传播和反向传播交替组成。前向传播将输入数据依次经过计算图中的每个算子,最终得到损失值(Loss)。反向传播则利用链式法则,从损失值出发,逐层计算每个参数相对于损失的梯度。AI编译器对反向传播的优化至关重要,因为反向传播的计算量通常是前向的2~3倍。

在计算图表示中,反向传播可以看作是在原图基础上自动构建一个“反向图”。每个前向算子都对应一个反向算子(如Conv2D对应Conv2DBackward),反向算子的输入是前向算子的输出梯度以及前向算子的输入/权重,输出是输入梯度和权重梯度。

判断方法或操作流程

编译器在生成反向图时,需要遵循以下步骤:

  • 遍历前向图,记录每个算子的输出张量(因为反向计算需要用到)。
  • 从损失节点开始,按拓扑序逆序为每个算子生成对应的反向算子。
  • 将反向算子连接成反向图,并确保梯度张量的形状与对应前向张量一致。
  • 对反向图进行独立的内存优化,因为反向图通常需要保存更多中间结果。

案例拆解或实战场景

考虑一个简单的线性模型 y = Wx + b,损失函数为均方误差 L = (y - t)^2。前向图包含MatMul、Add、Sub、Square和Mean五个算子。反向传播时,编译器会生成对应的反向算子:MeanBackward(将梯度均匀分配到每个元素)、SquareBackward(乘以2倍的原始差值)、SubBackward(将梯度分别传给y和t,t的梯度通常被忽略)、AddBackward(将梯度分别传给Wx和b)、MatMulBackward(计算W的梯度和x的梯度)。AI编译器会分析这些反向算子的数据依赖,并尝试将某些连续的反向算子融合,例如将AddBackward和MatMulBackward融合,减少中间梯度的内存写入。

常见误区或总结复盘

经验提醒:反向传播的内存占用往往比前向更高,因为需要保存前向的中间激活值。编译器常采用“重计算”(Recomputation)技术,即不保存某些中间结果,而在反向传播时重新计算,以内存换算力。这是AI编译器优化的重要方向之一。

2.3 静态图与动态图:两种执行范式的权衡

核心原理

静态图(Graph Mode)采用“先定义后执行”的策略:用户先构建完整的计算图,然后将其交给编译器进行整体优化和代码生成,最后在运行时一次性执行。TensorFlow 1.x、Caffe和TVM都采用静态图模式。动态图(Eager Mode)则采用“边执行边构建”的策略:每执行一行代码,就立即完成一次计算,同时动态构建计算图。PyTorch的默认模式就是动态图。

静态图的优势在于编译器可以看到完整的计算流程,从而进行全局优化,如算子融合、内存复用、死代码消除等。动态图的优势在于灵活性和可调试性,用户可以像编写普通Python程序一样使用if-else、for循环等控制流,且可以随时打印中间结果。

对比维度静态图动态图
构建时机运行前运行时
控制流支持需特殊语法(如tf.cond)原生Python控制流
优化空间全局优化,性能上限高局部优化,性能受限
调试难度较难,需图模式调试容易,可逐行打印
典型代表TensorFlow 1.x, TVMPyTorch, TensorFlow 2.x Eager

判断方法或操作流程

在实际工程中,选择静态图还是动态图,取决于应用场景:

  1. 研究探索阶段:优先选择动态图,便于快速迭代和调试。
  2. 生产部署阶段:优先选择静态图或基于动态图追踪(Tracing)导出的静态图,以获得更好的推理性能和硬件适配性。
  3. 控制流复杂模型:如递归神经网络(RNN)或树形结构模型,动态图更自然;若必须使用静态图,需借助控制流算子。

案例拆解或实战场景

以PyTorch为例,用户可以使用torch.jit.trace或torch.jit.script将动态图转换为静态图。Trace方法通过实际运行一次模型并记录算子执行顺序来构建静态图,但无法处理数据依赖的控制流。Script方法则通过解析Python代码直接生成静态图,支持if-else和for循环,但对代码写法有约束。AI编译器(如TorchScript编译器)会对生成的静态图进行优化,例如将多个小算子融合为一个大算子,减少内核启动次数。

常见误区或总结复盘

风险提示:动态图虽然灵活,但在大规模分布式训练中,每次迭代都需要重新构建图,导致额外的通信和调度开销。许多框架(如PyTorch)引入了“图模式”训练(如torch.compile),在保持一定灵活性的同时,通过编译优化逼近静态图的性能。理解两者的本质差异,是选择正确工具的前提。

2.4 计算图的基本表示方法:从IR到执行

核心原理

计算图在AI编译器中通常以中间表示(IR,Intermediate Representation)的形式存在。IR是编译器内部用于表示程序结构的数据结构,它比源代码更接近机器码,但比最终二进制码更抽象。常见的计算图IR包括:数据流图(Dataflow Graph)、控制流图(Control Flow Graph)和SSA(Static Single Assignment)形式。

数据流图强调数据依赖关系,每个节点代表一个算子,每条边代表一个张量。控制流图则额外包含分支、循环等控制逻辑。SSA形式要求每个变量只能被赋值一次,这简化了数据流分析和优化过程。现代AI编译器(如XLA、TVM)通常采用基于SSA的IR,以便进行更高效的优化。

判断方法或操作流程

计算图IR的设计需要满足以下要求:

  • 完备性:能够表达所有深度学习算子及其组合。
  • 可优化性:IR结构应便于进行图变换(如算子融合、常量折叠)。
  • 可移植性:IR应尽量与具体硬件无关,以便在不同后端(CPU、GPU、NPU)间复用。

一个典型的IR节点包含以下字段:算子类型、输入张量列表、输出张量列表、算子属性(如卷积的stride、padding)、设备放置信息等。

案例拆解或实战场景

以TVM的Relay IR为例,一个卷积算子的表示如下:

def @main(%data: Tensor[(1, 3, 224, 224), float32], %weight: Tensor[(64, 3, 7, 7), float32]) {
  %1 = nn.conv2d(%data, %weight, strides=[2, 2], padding=[3, 3]) 
  %2 = nn.relu(%1)
  %3 = nn.max_pool2d(%2, pool_size=[2, 2], strides=[2, 2])
  %4 = nn.global_avg_pool2d(%3)
  %5 = reshape(%4, newshape=[1, 64])
  %6 = nn.dense(%5, %weight2)
  %7 = nn.softmax(%6)
  %7
}

这段Relay IR清晰地展示了数据流:输入图像经过卷积、ReLU、池化、全连接和Softmax,最终输出分类概率。编译器可以分析该IR,发现Conv2D和ReLU可以融合为一个算子,因为ReLU是逐元素操作且不改变数据形状。

常见误区或总结复盘

总结:计算图的表示方法直接决定了编译器的优化上限。一个好的IR应该具备三个特征:一是显式表达数据依赖,便于并行分析;二是支持高阶操作(如融合算子),减少优化复杂度;三是具备可扩展性,能够容纳新出现的算子类型。理解IR的设计哲学,是深入AI编译器内部机制的关键一步。