- 先把weight矩阵们转一份FP16的copy;
- 前向、后向计算,全部用FP16的(包括激活、W、激活的梯度、W的梯度);
- W的梯度,需要先从FP16转换为FP32,再更新总的FP32的W;为什么:如果总的W也使用FP16,则由于数值指数范围较小,加上同是FP16的gradients,误差较大(特别是gradients绝对值较小接近0时,就加不上了);
- Optional: 某些网络,需要把Loss扩大S倍,目的是把其激活的梯度扩大S倍,从而不至于其一部分数值太小变成0;如果用了该技术,则需要在最后W的梯度往总梯度上更新之前,把W的梯度再缩小S倍;
问:为了把激活的梯度扩大S倍,为什么通过把Loss扩大S倍来实现:
答:链式法则;L的x的导数,把公式写出来,可以发现要把这个导数扩大S倍,只需要把L对y的导数扩大S倍即可,等价于把L扩大S倍;
问:为什么只提到了把激活的梯度扩大S倍:
答:有些网络,不扩大S倍也行;有些网络,只有激活的梯度(W的梯度,W,激活,都没事),在FP16情况下会落到0;
问:为什么要把W的梯度,最后要缩小S倍:
答:同理,还是根据链式法则(也就是求导公式),如果L扩大了S倍,则W的梯度也就扩大了S倍,所以要最后缩小S倍,才能保证正确;(我认为,先转成FP32, 再缩小S倍,再更新,更精确些,不知道是不是这么实现的?)
问:为什么快:
答:FP16利用了TensorCore;
效果:在收敛效果相近的前提下,可以1. 加快计算速度;2.减少memory占用(从而enable更大的minibatch、更大的model);3.加快显存访问、主存offload速度;
NVIDIA自吹的是平均训练速度达到3X;Pytorch报道的是2X;
另:
AMP支持已经进入Pytorch核心代码了:torch.cuda.amp
细节:
如果要做gradient的norm或者cliping,需要手动先unscale,再执行GradScaler的step更新;
如果使用gradient-accumulation技术(即用多次小batch来模拟一个大batch),每次backward之后,scaled梯度会自动累加起来,最后调用GradScaler的step(自动unscaler)以及optimizer的zero_grad;
手动在Loss中加入W的正则项时,如果使用AMP技术,则要特殊处理一下(即把W先unscale,再在autocast区域里计算正则项并加入到Loss(我估计在autocast里,会先把W进行scale,再计算)),很Ugly...
符号位,指数位,尾数位;
指数位的全0和全1是表示0和NaN这种特殊数据用的;
位数位放在小数点后面,小数点前面隐含是1;
例如+1.10110*(2^(8-127))); 其中8是指数位的东西;127是定死的“指数偏移”(我的理解是,这样指数位就不需要使用补码表示了);其中10110是尾数位的东西;