由浅入深的混合精度训练教程

2022 年 6 月 22 日 PaperWeekly


©作者 | serendipity
单位 | 同济大学
研究方向 | 行人搜索



2022 年的当下,混合精度 (Automatically Mixed Precision, AMP) 训练已经成为了炼丹师的标配工具,仅仅只需几行代码,就能让显存占用减半,训练速度加倍。 

AMP 技术是由百度和 NIVDIA 团队在 2017 年提出的 (Mixed Precision Training [1]),该成果发表在 ICLR 上。PyTorch 1.6之前,大家都是用 NVIDIA 的 apex [2] 库来实现 AMP 训练。1.6 版本之后,PyTorch 出厂自带 AMP。

这篇文章由浅入深地讲解了: 如何在 PyTorch 中使用 AMP、AMP 的原理、AMP 的代码实现。




如何在 PyTorch 中使用 AMP

如果你是新手,只是想简单地试用一下 AMP,只需要将相关训练代码

output = net(input)
loss = loss_fn(output, target)
loss.backward()
optimizer.step()
optimizer.zero_grad()


修改如下即可。


with torch.cuda.amp.autocast():
    output = net(input)
    loss = loss_fn(output, target)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
optimizer.zero_grad()


如果 GPU 支持 Tensor Core (Volta、Turing、Ampere架构),AMP 将大幅减少显存消耗,加快训练速度。对于其它类型的 GPU,仍可以降低显存,但训练速度可能会变慢。



AMP的原理


2.1 什么是FP16


半精度浮点数 (FP16)  是一种计算机使用的二进制浮点数数据类型,使用 2 字节 (16 位) 存储,表示范围为 。而 PyTorch 默认使用 单精度浮点数 (FP32) 来进行网络模型的计算和权重存储。FP32 在内存中用 4 字节 (32 位) 存储,表示范围为 。可以看到 FP32 能够表示的范围要比 FP16 大的多得多。

此外浮点数还有一个神奇的特点:当两个数字相差太大时,相加是无效的,又称舍入误差 [3]。 

用一段代码来展示:


>>> # FP32相加不会有问题。
>>> torch.tensor(2**-3) + torch.tensor(2**-14)
tensor(0.1251)
>>> # FP16相加,较小的数字会被忽略掉。因为在[2**-3, 2**-2]内,FP16表示的固定间隔为2**-13。
>>> # 也就是说比2**-3大的下一个数字为2**-3 + 2**-13,因此2**-14加了跟没加一样。
>>> # half()的作用是将FP32转化为FP16。
>>> torch.tensor(2**-3).half() + torch.tensor(2**-14).half()
tensor(0.1250, dtype=torch.float16)
>>> # 将2**-14换成2**-13就可以了。
>>> torch.tensor(2**-3).half() + torch.tensor(2**-13).half()
tensor(0.1251, dtype=torch.float16)


2.2 为什么要用 FP16


如果我们在训练过程中将 FP32 替代为 FP16,有以下两个好处:

1. 减少显存占用: FP16 的显存占用只有 FP32 的一半,这使得我们可以用更大的 batch size;

2. 加速训练:使用 FP16,模型的训练速度几乎可以提升 1 倍。

2.3 为什么只用 FP16 会有问题


如果我们简单地把模型权重和输入从 FP32 转化成 FP16,虽然速度可以翻倍,但是模型的精度会被严重影响。原因如下:

上/下溢出: FP16 的表示范围不大,超过 的数字会上溢出变成 inf,小于 的数字会下溢出变成 0。下溢出更加常见,因为在网络训练的后期,模型的梯度往往很小,甚至会小于 FP16 的下限 ,此时梯度值就会变成 0,模型参数无法更新。下图为 SSD 网络在训练过程中的梯度统计,有 67% 的值下溢出变成 0。


舍入误差: 就算梯度不会上/下溢出,如果梯度值和模型的参数值相差太远,也会发生舍入误差的问题。假设模型参数 weight ,学习率 ,梯度 gradient ,weight weight gradient

2.4 解决方案

损失缩放 (Loss Scaling)


为了解决下溢出的问题,论文中对计算出来的 loss 值进行缩放 (scale),由于链式法则的存在,对 loss 的缩放会作用在每个梯度上。缩放后的梯度,就会平移到 FP16 的有效范围内。这样就可以用 FP16 存储梯度而又不会溢出了。此外,在进行更新之前,需要 先将缩放后的梯度转化为 FP32,再将梯度反缩放 (unscale) 回去。 

注意这里一定要先转成 FP32,不然 unscale 的时候还是会下溢出。


缩放因子 (loss_scale) 一般都是框架自动确定的,只要没有发生 inf 或者 nan,loss_scale 越大越好。因为随着训练的进行,网络的梯度会越来越小,更大的 loss_scale 可以更加充分地利用 FP16 的表示范围。

FP32 权重备份


为了实现 FP16 的训练,我们需要把模型权重和输入数据都转成 FP16,反向传播的时候就会得到 FP16 的梯度。如果此时直接进行更新,因为 梯度 * 学习率 的值往往较小,和模型权重的差距会很大,可能会出现舍入误差的问题。 

所以解决思路是:将 模型权重、激活值、梯度 等数据用 FP16 来存储,同时维护一份 FP32 模型权重副本 用于更新。在反向传播得到 FP16 的梯度以后, 将其转化成 FP32 并 unscale ,最后更新 FP32 的模型权重。因为整个更新过程是在 FP32 的环境中进行的,所以不会出现舍入误差。 

FP32 权重备份解决了反向传播的舍入误差问题。

黑名单


对于那些在 FP16 环境中运行不稳定的模块,我们会将其添加到黑名单中,强制它在 FP32 的精度下运行。比如需要计算 batch 均值的 BN 层就应该在 FP32 下运行,否则会发生舍入误差。还有一些函数对于算法精度要求很高,比如 torch.acos(),也应该在 FP32 下运行。论文中的黑名单只包含 BN 层。 

如何保证黑名单模块在 FP32 环境中运行:以 BN 层为例,将其权重转为 FP32,并且将输入从 FP16 转成 FP32,这样就可以保证整个模块是在 FP32 下运行的。


黑名单解决了某些函数在 FP16 环境下的算术不稳定的问题。

Tensor Core



Tensor Core 可以让 FP16 做矩阵相乘,然后把结果累加到 FP32 的矩阵中。这样既可以享受 FP16 高速的矩阵乘法,又可以利用 FP32 来消除舍入误差。 

搞不懂 Tensor Core 是如何应用到 AMP 中的。有人说 Tensor Core 可以帮助我们利用 FP16 的梯度来更新 FP32 的模型权重。但是阅读了 apex 的源码之后,我发现 FP16 的梯度会先转化为 FP32,再做更新,所以权重更新和 Tensor Core 并无关系。以后弄明白了再回来补充吧。

2.5 一些思考


其实将 FP16 和 FP32 混合起来使用是必然的结果,有以下几个原因:

1. 在网络训练的后期,梯度的值非常小,可能会让 FP16 下溢出。如果不使用 FP32,即使我们通过缩放操作暂时规避了这个问题,权重更新时的 unscale 操作还是会让梯度下溢出;

2. 承接第 1 条,就算梯度能够以 FP16 表示,但是 梯度 * 学习率 可能会下溢出。所以权重更新这步操作还是得在 FP32 下运行;

3. 承接第 2 条,就算 梯度 * 学习率 不会下溢出,其值相对于权重本身也是非常小的。 权重 + 梯度 * 学习率 这步操作中可能会发生舍入误差的问题;

4. 承接第 3 条,就算 权重 + 梯度 * 学习率 不会发生舍入误差,有些算子在 FP16 下也是不稳定的,比如 BN、 torch.acos 等。



NVIDIA apex库代码解读


首先介绍下 apex 提供的几种 opt-level: o1, o2, o3, o4 。注意这里是 字母"o" 不是数字"0"。


图片来自:全网最全-混合精度训练原理

https://zhuanlan.zhihu.com/p/441591808


o0 是纯 FP32,用来当精度的基准。 o3 是纯 FP16,用来当速度的基准。 

重点讲 o1 o2 。我们之前讲的 AMP 策略其实就是 o2 : 除了 BN 层的权重和输入使用 FP32,模型的其余权重和输入都会转化为 FP16。此外还会创建一个 FP32 的权重副本来执行更新操作。 

o2 不同, o1 不再需要 FP32 权重备份,因为 o1 的模型一直都是 FP32。可能有些读者会好奇,既然模型参数是 FP32,那怎么在训练过程中使用 FP16 呢?答案是 o1 建立了一个 PyTorch 函数的黑白名单,对于白名单上的函数,强制要求其用 FP16,即会将函数的参数先转化为 FP16,再执行函数本身。黑名单则强制要求 FP32。

nn.Linear 为例, 这个模块有两个权重参数 weight bias ,输入为 input ,前向传播就是调用了 torch.nn.functional.linear(input, weight, bias) o1 模式会将 input weight bias 先转化为 FP16 格式 input_fp16 weight_fp16 bias_fp16 ,再调用函数 torch.nn.functional.linear(input_fp16, weight_fp16, bias_fp16) 。这样一来就实现了模型参数是 FP32,但是仍然可以使用 FP16 来加速训练。 

o1 还有一个细节: 虽然白名单上的 PyTorch 函数是以 FP16 运行的,但是产生的梯度是 FP32,所以不需要手动将其转成 FP32 再 unscale,直接 unscale 即可。 

个人猜测 PyTorch 会让每个 Tensor 本身的数据类型和梯度的数据类型保持一致,虽然产生了 FP16 的梯度,但是因为权重本身是 FP32,所以框架会将梯度也转化为 FP32。


如果说 o1 是 FP16 + FP32,更加激进的 o2 就是 almost FP16 (几乎全是 FP16)。通常来说 o1 o2 更稳,一般先选择 o1 ,再尝试 o2 看是否掉点,如果不掉点就用 o2

3.1 apex 的 o1 实现


1. 根据黑白名单对 PyTorch 内置的函数进行包装 [4]。白名单函数强制 FP16,黑名单函数强制 FP32。其余函数则根据参数类型自动判断,如果参数都是 FP16,则以 FP16 运行,如果有一个参数为 FP32,则以 FP32 运行。 

2. 将 loss_scale 初始化为一个很大的值 [5]。 

3. 对于每次迭代 
(a). 前向传播:模型权重是 FP32,按照黑白名单自动选择算子精度。 
(b). 将 loss 乘以 loss_scale [6] 
(c). 反向传播,因为模型权重是 FP32,所以即使函数以 FP16 运行,也会得到 FP32 的梯度。
(d). 将梯度 unscale [7],即除以 loss_scale 
(e). 如果检测到 inf 或 nan [8]
i. loss_scale /= 2 [9]
ii. 跳过此次更新 [10]
(f). optimizer.step(),执行此次更新 
(g). 如果连续2000次迭代都没有出现 inf 或 nan,则 loss_scale *= 2 [11]


3.2 apex 的 o2 实现


1. 将除了 BN 层以外的模型权重转化为 FP16 [12],并且包装了 forward 函数 [13],将其参数也转化为 FP16;

2. 维护一个 FP32 的模型权重副本用于更新 [14];

3. 将 loss_scale 初始化为一个很大的值 [15];

4. 对于每次迭代 
(a). 前向传播: 除了 BN 层是 FP32,模型其它部分都是 FP16。 
(b). 将 loss 乘以 loss_scale [16] 
(c). 反向传播,得到 FP16 的梯度 
(d). 将 FP16 梯度转化为 FP32,并 unscale [17]
(e). 如果检测到 inf 或 nan [18]
i. loss_scale /= 2 [19]
ii. 跳过此次更新 [20]
(f). optimizer.step(),执行此次更新 
(g). 如果连续2000次迭代都没有出现 inf 或 nan,则 loss_scale *= 2 [21]

此外,还推荐阅读 MMCV 对于 AMP 的 o2 实现 [22],代码比 apex 更加清晰。但因为我想同时讲 o1 o2 ,就没有选择解读 MMCV 的代码,有兴趣的读者可以进一步研究。

参考链接

[1] https://arxiv.org/abs/1710.03740

[2] https://github.com/NVIDIA/apex

[3] https://en.wikipedia.org/wiki/Round-off_error#Addition

[4] https://github.com/NVIDIA/apex/blob/1403c21acf87b0f2245278309071aef17d80c13b/apex/amp/amp.py#L68

[5] https://github.com/NVIDIA/apex/blob/1403c21acf87b0f2245278309071aef17d80c13b/apex/amp/scaler.py#L40

[6] https://github.com/NVIDIA/apex/blob/1403c21acf87b0f2245278309071aef17d80c13b/apex/amp/handle.py#L113

[7] https://github.com/NVIDIA/apex/blob/1403c21acf87b0f2245278309071aef17d80c13b/apex/amp/_process_optimizer.py#L123

[8] https://github.com/NVIDIA/apex/blob/1403c21acf87b0f2245278309071aef17d80c13b/apex/amp/scaler.py#L202

[9] https://github.com/NVIDIA/apex/blob/1403c21acf87b0f2245278309071aef17d80c13b/apex/amp/scaler.py#L207

[10] https://github.com/NVIDIA/apex/blob/1403c21acf87b0f2245278309071aef17d80c13b/apex/amp/handle.py#L128

[11] https://github.com/NVIDIA/apex/blob/1403c21acf87b0f2245278309071aef17d80c13b/apex/amp/scaler.py#L213

[12] https://github.com/NVIDIA/apex/blob/1403c21acf87b0f2245278309071aef17d80c13b/apex/amp/_initialize.py#L179

[13] https://github.com/NVIDIA/apex/blob/1403c21acf87b0f2245278309071aef17d80c13b/apex/amp/_initialize.py#L194

[14] https://github.com/NVIDIA/apex/blob/1403c21acf87b0f2245278309071aef17d80c13b/apex/amp/_process_optimizer.py#L44

[15] https://github.com/NVIDIA/apex/blob/1403c21acf87b0f2245278309071aef17d80c13b/apex/amp/scaler.py#L40

[16] https://github.com/NVIDIA/apex/blob/1403c21acf87b0f2245278309071aef17d80c13b/apex/amp/handle.py#L113

[17] https://github.com/NVIDIA/apex/blob/1403c21acf87b0f2245278309071aef17d80c13b/apex/amp/scaler.py#L94

[18] https://github.com/NVIDIA/apex/blob/1403c21acf87b0f2245278309071aef17d80c13b/apex/amp/scaler.py#L202

[19] https://github.com/NVIDIA/apex/blob/1403c21acf87b0f2245278309071aef17d80c13b/apex/amp/scaler.py#L207

[20] https://github.com/NVIDIA/apex/blob/1403c21acf87b0f2245278309071aef17d80c13b/apex/amp/handle.py#L128

[21] https://github.com/NVIDIA/apex/blob/1403c21acf87b0f2245278309071aef17d80c13b/apex/amp/scaler.py#L213

[22] https://github.com/open-mmlab/mmcv/blob/f5425ab7611ab2376ddb478b57cb2f46f6054e13/mmcv/runner/hooks/optimizer.py#L344

[23] https://docs.nvidia.com/deeplearning/performance/mixed-precision-training/index.html

[24] https://pytorch.org/docs/stable/amp.html#autocast-op-reference

[25] PyTorch必备神器 | 唯快不破:基于Apex的混合精度加速

[26] https://zhuanlan.zhihu.com/p/103685761

[27] https://zhuanlan.zhihu.com/p/441591808



更多阅读




#投 稿 通 道#

 让你的文字被更多人看到 



如何才能让更多的优质内容以更短路径到达读者群体,缩短读者寻找优质内容的成本呢?答案就是:你不认识的人。


总有一些你不认识的人,知道你想知道的东西。PaperWeekly 或许可以成为一座桥梁,促使不同背景、不同方向的学者和学术灵感相互碰撞,迸发出更多的可能性。 


PaperWeekly 鼓励高校实验室或个人,在我们的平台上分享各类优质内容,可以是最新论文解读,也可以是学术热点剖析科研心得竞赛经验讲解等。我们的目的只有一个,让知识真正流动起来。


📝 稿件基本要求:

• 文章确系个人原创作品,未曾在公开渠道发表,如为其他平台已发表或待发表的文章,请明确标注 

• 稿件建议以 markdown 格式撰写,文中配图以附件形式发送,要求图片清晰,无版权问题

• PaperWeekly 尊重原作者署名权,并将为每篇被采纳的原创首发稿件,提供业内具有竞争力稿酬,具体依据文章阅读量和文章质量阶梯制结算


📬 投稿通道:

• 投稿邮箱:hr@paperweekly.site 

• 来稿请备注即时联系方式(微信),以便我们在稿件选用的第一时间联系作者

• 您也可以直接添加小编微信(pwbot02)快速投稿,备注:姓名-投稿


△长按添加PaperWeekly小编




🔍


现在,在「知乎」也能找到我们了

进入知乎首页搜索「PaperWeekly」

点击「关注」订阅我们的专栏吧



·
·
·

登录查看更多
2

相关内容

【多伦多大学博士论文】深度学习中的训练效率和鲁棒性
专知会员服务
52+阅读 · 2021年6月17日
最新《自动微分》综述教程,71页ppt
专知会员服务
21+阅读 · 2020年11月22日
【CVPR 2020-商汤】8比特数值也能训练卷积神经网络模型
专知会员服务
25+阅读 · 2020年5月7日
TensorFlow Lite指南实战《TensorFlow Lite A primer》,附48页PPT
专知会员服务
69+阅读 · 2020年1月17日
KGCN:使用TensorFlow进行知识图谱的机器学习
专知会员服务
81+阅读 · 2020年1月13日
浅谈混合精度训练imagenet
极市平台
1+阅读 · 2022年5月3日
实操教程|用Pytorch训练神经网络
极市平台
0+阅读 · 2022年4月22日
实操教程|Pytorch转ONNX详解
极市平台
3+阅读 · 2022年4月4日
解决PyTorch半精度(AMP)训练nan问题
CVer
3+阅读 · 2022年1月4日
实践教程 | 解决pytorch半精度amp训练nan问题
极市平台
1+阅读 · 2021年12月15日
实践教程 | 浅谈 PyTorch 中的 tensor 及使用
极市平台
1+阅读 · 2021年12月14日
混合精度训练原理总结
极市平台
1+阅读 · 2021年12月7日
实践教程|PyTorch训练加速技巧
极市平台
0+阅读 · 2021年11月15日
国家自然科学基金
0+阅读 · 2013年12月31日
国家自然科学基金
1+阅读 · 2013年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2011年12月31日
国家自然科学基金
0+阅读 · 2008年12月31日
Arxiv
20+阅读 · 2021年2月28日
Arxiv
11+阅读 · 2021年2月17日
Deformable Style Transfer
Arxiv
14+阅读 · 2020年3月24日
dynnode2vec: Scalable Dynamic Network Embedding
Arxiv
14+阅读 · 2018年12月6日
W-net: Bridged U-net for 2D Medical Image Segmentation
Arxiv
19+阅读 · 2018年7月12日
VIP会员
相关VIP内容
【多伦多大学博士论文】深度学习中的训练效率和鲁棒性
专知会员服务
52+阅读 · 2021年6月17日
最新《自动微分》综述教程,71页ppt
专知会员服务
21+阅读 · 2020年11月22日
【CVPR 2020-商汤】8比特数值也能训练卷积神经网络模型
专知会员服务
25+阅读 · 2020年5月7日
TensorFlow Lite指南实战《TensorFlow Lite A primer》,附48页PPT
专知会员服务
69+阅读 · 2020年1月17日
KGCN:使用TensorFlow进行知识图谱的机器学习
专知会员服务
81+阅读 · 2020年1月13日
相关资讯
浅谈混合精度训练imagenet
极市平台
1+阅读 · 2022年5月3日
实操教程|用Pytorch训练神经网络
极市平台
0+阅读 · 2022年4月22日
实操教程|Pytorch转ONNX详解
极市平台
3+阅读 · 2022年4月4日
解决PyTorch半精度(AMP)训练nan问题
CVer
3+阅读 · 2022年1月4日
实践教程 | 解决pytorch半精度amp训练nan问题
极市平台
1+阅读 · 2021年12月15日
实践教程 | 浅谈 PyTorch 中的 tensor 及使用
极市平台
1+阅读 · 2021年12月14日
混合精度训练原理总结
极市平台
1+阅读 · 2021年12月7日
实践教程|PyTorch训练加速技巧
极市平台
0+阅读 · 2021年11月15日
相关基金
国家自然科学基金
0+阅读 · 2013年12月31日
国家自然科学基金
1+阅读 · 2013年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2011年12月31日
国家自然科学基金
0+阅读 · 2008年12月31日
Top
微信扫码咨询专知VIP会员