先来复习一下各个类型在计算机中的表现形式
- \(32\)位浮点数
绿色数位越多,所能表示的范围越大(既可以表示更小的数,也可以表示更大的数);蓝色数位越多,所能表示的精度越高
- \(16\)位浮点数
好处是可以节省内存。如果我们正在训练神经网络,但是突然发现内存不够了,有哪些解决方法呢?- 我们就可以把所有\(32\)位浮点数转换成\(16\)位浮点数。当然这个样子会导致一些问题,如下
- 很小的数会变成\(0\),很大的数会变成NaN.容易导致梯度消失和梯度爆炸
- 精度更小:在半精度下,\(1.0001\)会变成\(1\).我们可以使用如下的方法来查看类型的信息
主要是关注eps
,这个数代表\(1\)能加的不被舍入到\(1\)的最小的数。也就是说,如果\(1\)加上了比这个数还小的数,那么就会变成\(1\)(或者理解成,比eps
小的数就会直接变成\(0\))
- 同时使用\(16\)位浮点数和\(32\)位浮点数来解决保存的参数精度不足的问题,如下
注意,我们的参数都是存储在FP32里面的,只是在传播过程中要将FP32转换成FP16再进行传播(第六步就是为了下一次的前向传播做准备);但是这个样子显然也会导致梯度;正确的做法还要结合放大策略,如下
Pytorch实现如下
但是现在还存在一个问题,就是缩放太大了会导致梯度爆炸(注意现在是FP16,表示的范围较小),有没有什么方法不用梯度缩放呢?为了保证空间不炸,只有一个方法,就是牺牲精度
虽然精度下降了,但是在实践中这个完全可行。Pytorch实现如下
但是我觉得这个代码写得有一点奇怪啊,autocast
的dtype
默认是FP16的,如果要使用BFP16就需要显式指定,为什么这里不用指定呢?
下面来看一下效果
BFP16比FP64还高的原因:前者具有正则化效果
- 我们就可以把所有\(32\)位浮点数转换成\(16\)位浮点数。当然这个样子会导致一些问题,如下
下面我们进入多GPU训练的场景。先来介绍一下单GPU训练
Model parameters
就是前文说的传播过程中的FP16
多GPU其实比较简单。首先将数据分成若干份,在每个GPU上进行前向传播和反向传播,然后每个GPU就会得到不同的梯度;我们再将每个GPU的梯度进行合并(如求和或者平均)得到一个统一的梯度(这种操作叫做全局规约操作),再去更新所有的GPU(由上面的过程不难知道,更新完的每个GPU都是一模一样的)
通信开销就是在不同GPU之间转移数据的开销。上述方法叫做分布式数据并行
上述方法有一个非常明显的缺点,就是内存占用太大了
我们可以使用一个叫做ZeRO的技术解决,这个技术一共有三种类型。核心思想是让每个GPU不再需要维持所有状态,而是进行分片处理,然后使用通信进行同步
- 类型一:优化器状态分片
注意每一块GPU都是包含所有的前向传播的FP16参数的(只是FP32的主权重分散在各个GPU中),所以可以对自己分到的数据集进行完整的传播(也可以计算所有参数在自己分到的这个数据集上的梯度;这个操作叫做归约分散),然后每个GPU向其他GPU请求自己包含的主权重的梯度(注意这个梯度在不同的GPU上不同,因为每个GPU分到的是不同的数据集)从而对自己的主权重进行更新,然后再通过通信同步状态(这个操作叫做全收集)
上述三种MPI操作的关系如下
所以我们无代价地节约了内存,所以任何时候都应该使用ZeRO - 类型二:优化器状态和梯度分片
- 类型三:完全分片数据并行
一共有\(16\)个GPU和\(16\)个参数(包括填充)
对三种类型的总结如下
我们现在再来看一下GPU显存里面到底存储了什么东西
所以有些时候我们想要加大批量大小是不行的
我们调参的基本工作流程图如下
那么为什么我们要关注高效训练呢?
下面介绍全微调(从视频00:43:00开始一直结束,后面还有个低秩什么的,没学过,所以没看)