CUDA最简实现计算图和自动微分

计算节点(ComputeNode) + 计算图(ComputeGraph)

一个基于计算图的自动微分流程,其核心由以下部分组成:

  1. 计算节点(ComputeNode): 包含操作类型、前向传播值、梯度值和输入节点

  2. 计算图(ComputeGraph): 管理所有计算节点,执行前向和反向传播

  3. 操作类型(OpType): 包括输入、加法、减法、乘法、除法、矩阵乘法和各种激活函数

1、梯度矩阵化的CUDA过程

1.1 基本原理:链式法则的矩阵化表示

代码中的反向传播遵循链式法则,对于复合函数 f(g(x)),其导数 df/dx = df/dg * dg/dx。在深度学习中,这被扩展到矩阵形式。

1.2 各操作的梯度计算实现

加法操作 (OP_ADD)

对于 C = A + B,梯度反向传播为:

这里体现了加法的导数特性:∂C/∂A = ∂C/∂B = 1,所以输出梯度直接传递给输入梯度。

减法操作 (OP_SUB)

对于 C = A - B,梯度反向传播为:

这里体现了减法的导数特性:∂C/∂A = 1∂C/∂B = -1,所以A的梯度等于输出梯度,B的梯度等于输出梯度的负值。

元素级乘法 (OP_MUL)

对于 C = A * B(元素级乘法),梯度计算为:

Loss对A求导,要先针对C求导,再对A求导,Loss对B求导同理:

(1)A:L/A=L/(AB)(AB)/A=L/(AB)BB:L/B=L/(AB)(AB)/B=L/(AB)A

链式法则:A的梯度是输出梯度乘以B,B的梯度是输出梯度乘以A。

矩阵乘法 (OP_MATMUL)

对于矩阵乘法 C = A * B,梯度计算更为复杂:

假设:

ARm×n

BRn×p

C=ABCRm×p

损失函数 L 依赖于 C,则 LCRm×p

计算LA

根据矩阵求导规则:

LA=LCCA

C=ABA 求导:

CA=BT

所以:

LA=LCBT

维度匹配:

LCRm×p

BTRp×n

LA

LA=(m×p)×(p×n)=m×n

这正好是 A 的维度。

BT​ 的引入是为了确保矩阵乘法合法,同时维度匹配正确。

所以,具体CUDA核函数实现为:

(2)A:dL/dA=dL/dCdC/dA=dL/dCBT

代码实现:

 

激活函数 (OP_RELU, OP_SIGMOID)

对于ReLU函数:

对于Sigmoid函数:

这些实现体现了激活函数的导数特点:

2、梯度的层层计算过程

2.1 反向传播的初始化与执行流程

反向传播从输出节点开始,按照计算图的逆向顺序进行:

关键步骤:

  1. 初始化所有梯度为0

  2. 设置输出节点梯度为1(假设损失函数是输出的和)

    在反向传播中,输出节点(即损失函数的输出)对自身的梯度被初始化为 1.0,因为 L/L = 1,表示损失函数相对于自身的变化率为 1。

  3. 按计算图的逆序遍历节点,执行反向传播

2.2 具体示例:以main函数中的网络为例

main函数构建了一个简单的前向网络:

这构建了一个计算图:input -> matmul(with weight) -> (optional)relu -> output

反向传播过程:

  1. 输出层初始化:

    • 如果使用ReLU,则ReLU输出节点的梯度设为1

      • 因为如果 use_relu = true,则计算图变成:

        (3)inputMatMulReLUoutput

        输出变成ReLU。

    • 否则,MatMul输出节点的梯度设为1

      • (4)inputMatMuloutput

         

  2. ReLU层梯度传播

    (如果使用):

    • 计算fc1节点的梯度:fc1_grad = relu_grad * (input > 0 ? 1 : 0)

      即根据ReLU的导数规则,只保留输入为正值处的梯度

  3. MatMul层梯度传播:

    • 计算weight的梯度:weight_grad = input^T * fc1_grad

    • 计算input的梯度:input_grad = fc1_grad * weight^T

最后输出计算过程:

完整代码:

https://github.com/AllenZYJ/Edge-Computing-Engine/blob/master/cuda_mat/mat_grad.cu