nvmath-python (Beta) 是一个开源 Python 库,为 Python 程序员提供对 NVIDIA CUDA-X 数学库的高性能数学运算访问。nvmath-python 既提供底层库的低级绑定,也提供更高级别的 Python 抽象。它可与 PyTorch 和 CuPy 等现有 Python 软件包进行互操作。
在本文中,我将展示如何在 nvmath-python 中将 结语 与矩阵乘法结合使用。结语是可以与正在执行的数学运算(如 FFT 或矩阵乘法)融合的运算。可用的结语涵盖了大多数常见的深度学习计算。我通过实施简单神经网络的常见正向和反向传递运算来演示其用法。
要安装 nvmath-python,请 按照安装说明 操作。
使用 RELU_BIAS 后记优化正向传递
在本节中,我将演示如何使用 epilogs 实现简单线性层的前向传递。此层首先将输入向量乘以权重矩阵,然后向生成矩阵的每个元素添加偏差,最后应用 ReLU 激活函数。
ReLU 是修正线性单元的简称,是一种常用的激活函数,可以在保持正值不变的同时将负值替换为 0。
在矩阵运算方面,该层可以表示为:
在方程中,以下定义成立:
- 是一批形状为的输入向量 :
- 是层的输入数量。
- 是批量大小。
- 是形状的权重矩阵 :
- 是层的输出数量。
- 是其输入的数量。
- 是长度为 的偏置向量,将其添加到生成矩阵的每一列中。
假设您的输入、权重和偏差为 CuPy 数组:
num_inputs, num_outputs = 784, 100
batch_size = 256
weights = cupy.random.rand(num_outputs, num_inputs)
bias = cupy.random.rand(num_outputs)
x = cupy.zeros((num_inputs, batch_size))
在最基本的版本中,您可以通过使用 nvmath-python 计算 ,然后手动处理偏差和 ReLU 来实现此线性层,如下代码示例所示。
在本示例中,我使用 nvmath.linalg.advanced.Matmul -class.html” rel=”follow noopener” target=”_blank”>有状态 API ,其中您可以将初始化和规划与乘法的实际执行分开。当您必须执行多个类似的乘法运算时,我推荐这种方法,因为它可以让您分期偿还规划的初始成本。有关 Matmul
的更多信息,请参阅 nvmath.linalg.advanced.Matmul。
mm = Matmul(weights, x)
mm.plan()
def forward():
y = mm.execute()
y += bias[:,cupy.newaxis]
y[y < 0] = 0
return y
要提高代码的性能,请利用 RELU_BIAS epilog 在单个融合的 cuBLAS 操作中执行所有三个操作。这个结语首先将偏差添加到乘法结果中,然后应用 ReLU 函数。
您可以使用 Matmul.plan
方法的 epilog
参数指定结语。一些结语(包括 RELU_BIAS
)会接收额外的输入,可在 epilog_inputs
字典中指定。有关结语的更多信息,请参阅 nvmath.linalg.advanced.Matmul 。
from nvmath.linalg.advanced import MatmulEpilog
mm = Matmul(weights, x)
mm.plan(epilog=MatmulEpilog.RELU_BIAS, epilog_inputs={"bias": bias})
def forward():
y = mm.execute()
return y
正如我稍后解释的那样,要通过 ReLU 函数进行反向传播,您必须知道向 ReLU 的哪些输入为正、哪些为负。此辅助信息称为 ReLU 掩码 ,可通过 RELU_AUX_BIAS
后记获得。
当使用带有辅助输出的结语时,Matmul.execute
将返回一个包含实际结果和辅助输出字典的元组。在 RELU_AUX_BIAS
的情况下,辅助输出字典只有一个键 relu_aux
,其中包含 ReLu 掩码。该掩码是位编码的,可能难以读取,但在向后传递期间,有专门的结语可以为您执行此操作。
from nvmath.linalg.advanced import MatmulEpilog
mm = Matmul(weights, x)
mm.plan(epilog=MatmulEpilog.RELU_AUX_BIAS, epilog_inputs={"bias": bias})
relu_mask = None
def forward():
global relu_mask
y, aux_outputs = mm.execute()
relu_aux = aux_outputs["relu_aux"]
return y
使用 RELU_AUX_BIAS
epilog 的实现速度比其朴素的实现要快,从而显著提升性能。
图 2 显示了对大小为(65536,16384)(16384,8192)的 float16 矩阵执行矩阵乘法运算,然后执行偏加和 ReLU 运算。性能在 NVIDIA H200 GPU 上进行测量。
使用 DRELU_BGRAD 后记优化反向传播
在神经网络的反向传播过程中,损失函数相对于输出的梯度会反向传播到网络层,以计算每个参数的梯度。
直观地说,对于每个操作,当其输出对损失的影响已知时,就有可能确定其输入和参数(例如权重矩阵中的值)如何影响损失。有关更多信息,请参阅 反向传播 。
在这一部分,我假设有多个线性层堆叠在一起。我对通常被认为属于不同层的操作序列实施反向传播:添加偏差、应用 ReLU 以及乘以权重。
让 作为前面显示的网络部分的输入,并分别通过 、 和 显示中间结果:
在反向传播中,当您知道 loss function 是如何受 (即 )影响时,即可计算相对于其他参数的梯度。如需详细了解用于计算梯度的公式的推导,请参阅 Automatic Differentiation 和 Neural Networks 。
- ,其中 为负值,,其中 为非负值(ReLU 掩码包含此信息)
- 是 ,按批量维度求和
计算 和 所需的运算可以通过使用仅用于矩阵乘法的 Matmul
,然后手动处理掩码和批量和来简单实现。
mm = Matmul(weights.T, grad)
mm.plan()
def backward():
grad_t1 = mm.execute()
grad_t1[mask] = 0 # assuming that `mask = (t1 < 0)`
grad_bias = cupy.sum(grad_t1, axis=1)
return grad_t1, grad_bias
要优化您的向后传递,请使用 DRELU_BGRAD
后记。假设梯度 在 CuPy 数组 grad
中可用。DRELU_BGRAD
的 epilog 需要一个输入 relu_aux
,其中包含从 RELU_AUX_BIAS
的 epilog 返回的掩码。它将此遮罩应用于乘法结果。它还会返回一个辅助输出,其中包含结果的逐列总和,恰好是 。
mm = Matmul(weights.T, grad)
mm.plan(epilog=MatmulEpilog.DRELU_BGRAD, epilog_inputs={"relu_aux":relu_mask})
def backward():
grad_t1, aux_outputs = mm.execute()
grad_bias = aux_outputs["drelu_bgrad"]
return grad_t1, grad_bias
图 5 显示了对大小为(65536,16384)(16384,8192)的 float16 矩阵执行矩阵乘法运算,然后应用 ReLU 掩码和偏差梯度计算。该性能在 NVIDIA H200 GPU 上进行了测量。
结束语
借助 nvmath-python 的后记,您可以在 Python 代码中融合常见的深度学习计算,从而大幅提高性能。有关更多信息,请参阅 nvmath-python:在 Python 文档中充分发挥 NVIDIA Math Libraries 的功能 。
我们是一个开源库,请随时访问 /NVIDIA/nvmath-python GitHub 仓库并与我们联系。