Chapter 2. Mixed Precision Training

在NVIDIA DeepLearning SDK的cuda8与Pascal架构中已经在训练时引入了低精度的能力。

混合精度在计算方法中结合了不同数据精度。

半精度(也被称为FP16)对比高精度的FP32与FP64降低了神经网络的显存占用,使得我们可以训练部署更大的网络,并且FP16在数据转换时比FP32或者FP64更节省时间。
单精度(也被称为32-bit)是通用的浮点数格式(在C扩展语言中表示为float),64-bit被称为双精度(double)。

2.1 Half Precision Format

IEEE 754标准定义了如下16bit半精度浮点数格式:1个符号(sign)bit,5个指数(exponent)bit,10个小数(fractional)bit。

指数通过15编码作为bias,指数范围在[-14,15]间(两个指数值,从0到31,保留用于特殊情况)。一个隐藏的1bit作为正则化值,正如IEEE浮点数格式定义的那样。

半精度格式有如下动态范围与精度:

正则化值(Normalizedvalues)

2-14到215,11bits有效位。

非正则化值(Denormalvalues)

2-14到2-15,随着指数值变小,有效位数减少。指数在[-24,-15]范围内k的有效精度范围是(25-k)bit。

一些例子量级:

最大归一化值(Maximumnormalized) 65504

最小归一化值(Minimumnormalized) 2-14=~6.10e-5

最小非归一化值(Minimundenormal) 2-24=~5.96e-8

半精度包括非归一化动态范围,为2的40次幂。对比来看,单精度包括非归一化动态范围为2的264次幂。

2.2 Volta Tensor Core Math

Volta架构系列GPU引入了Tensor Cores,比单精度math流水线提供了8x吞吐量。每个Tensor Cores进行D=AxB+C运算,A/B/C/D均指矩阵。A与B是半精度4x4矩阵,D与C可以是半精度或者单精度4x4矩阵。也是就说,Tensor Core math可以计算半精度乘法,并且产生半精度或者单精度的输出。

实践中,当A与B的维度时8的倍数的时候,可以取得好的表现。Tensor Core操作会调用cuDNN v7与cuBLAS9中的一些函数,为了得到更好的性能,输入与输出需要是8的倍数。更多详细信息请见cuDNN Developer Guide。

半精度之所以如此吸引人是因为V100GPU中有640个Tensor Cores,因此他们可以同时进行4*4的乘法。理论上V100的Tensor Cores运算峰值约为120TFOPS。这比双精度的fp64几乎快了一个量级(10x),比单精度fp32快了4倍。

矩阵乘法CNN运算的核心。CNN在深度学习的许多网络中都十分常见。从CUDA9与cnDNN7开始,在硬件允许的情况下,卷积操作通过Tensor Cores的操作实现。这种方法可以提升包含卷积运算的CNN与模型的训练与推理运算的速度。

2.3 Considering When Training With Mixed Precision

通过支持Tensor Coresmath的深度学习框架可以简单的通过选择框架中Tensor Core路径的方法进行训练加速(卷积与全连接选择FP16格式的tensors),更多特征请见Frameworks。在训练步骤中以FP32格式保存所有超参数。

然而,一些网络结构需要将他们的梯度值转为FP16表达范围,满足FP32训练步骤精度。下图展示了一个例子。

gpu单精度和半精度哪个更常用 半精度 单精度 双精度_gpu单精度和半精度哪个更常用

图1 FP32训练SSD网络过程中,激活梯度幅值直方图。X轴由于0的存在是对数值。例如。66.5%的值是0,4%的幅值在(2-32,2-30)间。

然而,并不是所有情况都是这样。有可能在运用FP16训练时还要进行一些scaling与normalization操作。

gpu单精度和半精度哪个更常用 半精度 单精度 双精度_deeplearning_02

图2 FP32训练SSD网络的激活梯度幅值直方图。X与y轴都取对数。

考虑到图1与图2分别表示的,在用FP32训练VGG-D为基础网络的SSD检测机,y轴线性与取对数的激活梯度直方图,当转化为FP16的时候,31%的值变为0,只有5.3%非0值在训练时导致了区分度。

注:绝大部分FP16表达范围的值没有被梯度值使用。因此,如果我们将梯度之进行平移来覆盖更大的范围,则可以避免更多的值为0。

对于这个网络,平移3个指数值(乘以8)可以通过负载相关值得损失为0来达到FP32训练过程中的准确率。平移15个指数值(乘以32K)可以在转换成FP16时还原99.9%的参数,避免了数据溢出。也就是说,FP16的范围对于训练来说是足够的,但是梯度值可能需要进行scaled操作来使其避免变为0。

2.3.1 Loss Scaling To Preserve Small Gradient Magnitudes

从前面的章节可知,成功训练FP16网络需要对梯度值进行scaling操作来避免值为0。这个可以通过在进行反向传播前,简单的对前向运算结束的loss值乘以一个scaling值来实现。根据链式法则,反向传播保证所有梯度值被同一个值进行scaling。这样在反向传播过程中不需要其他额外操作,并且保证相对梯度值不会变为0,也不会失去梯度信息。

权值梯度必须在梯度更新前进行unscale,来保证梯度更新与FP32训练时一致。最简单的方式是在反向传播后,在梯度剪裁或者其他梯度相关计算前进行descaling操作。这样保证了没有超参数(灰度裁剪阈值,权重衰减值等)需要调整。

许多网络使用FP16tensors的结果可以比得上FP32训练结果,但是有些需要更新一个FP32类型的权值。此外,被大量减少的值需要被存在FP32中。例如一些统计数据(均值或者方差)BN或者SoftMax。

BN可以利用FP16作为输入输出,节约FP32一半的带宽,只有统计与值得调整需要在FP32下进行。这导致以下高层训练流程:

1、  保存一个FP32参数的主备份。

2、  每次迭代:

a.       创建一个FP16格式的权重副本。

b.       前向传播(FP16权重与激活)。

c.       将结果loss与尺度系数S相乘。

d.       反向传播(FP16权重,激活值以及梯度值)。

e.       权重梯度乘以1/S。

f.       完成权重更新,包括权重剪裁。

2.3.2 Choosing A Scaling Factor

前一章描述的流程要求用户选择一个scaling值来调整梯度幅值。当反向传播不出现溢出的时候,选择一个大的scaling值没有一点问题。反向传播的溢出会导致权值梯度包含无限值或者NaNs,在权值更新是会导致不可逆转的损失。这些权值溢出可以简单有效的通过检查计算出来的梯度权值来检测出来,例如,将前一章提到的权值梯度每一步乘以1/S。

一个方式是当权值溢出检测到后跳过权值共享步骤,直接进行下一次迭代。

有几种方式选择loss值的scaling系数。最简单的方式是选一个常量作为scaling系数。我们训练了一系列前馈网络与循环网络,使用Tensor Core math与范围由8到32K的scaling系数(许多网络不需要scaling系数),取得了匹配FP32训练的精确度。如果使用梯度统计可以更方便的统计出常量scaling系数。选择一个值,使其与最大绝对灰度值得乘积小于65504即可(FP16中能表示的最大值)。

更鲁棒的选择scaling权值方式是动态选取。基本思路是初始选取一个较大的scaling值,然后在之后训练迭代中逐渐重新修改。如果在设置的N次迭代中都没有出现溢出的情况,增大scaling系数。如果出现了溢出,跳过权值更新,降低scaling权值。我们发现由于溢出很少发生,所以不需要改动训练方式就可以达到FP32的训练精度。注意N实际上限制了我们可能溢出或者跳过更新的频率。Scaling系数变化率可以通过跟选择N一样选择一个增长或者下降的乘数来实现。我们设置N=2000,增长系数为2,下降系数为0.5,成功的训练了网络,其他许多参数组合可以成功训练。动态Loss-scaling方式可以概括成下面高层训练流程:

1.  保存一个FP32参数的主备份。

2.  初始化S为一个较大的值。

3.  每次迭代:

a.       创建一个FP16格式的权重副本。

b.       前向传播(FP16权重与激活)。

c.       将结果loss与尺度系数S相乘。

d.       反向传播(FP16权重,激活值以及梯度值)。

e.       如果权重梯度出现Inf或者NaN:

a.       修改S值。

b.       跳过权重更新,进行下一次迭代。

f.       权重梯度乘以1/S。

g.       完成权重更新,包括权重剪裁。

h.       如果最近的N才迭代中没有出现Inf或者NaN,增大S。