pytorch训练优化-自动混合精度训练(AMP)

Pytorch 版本:1.6及以上的版本,支持CUDA
GPU版本:支持 Tensor core的 CUDA(Volta、Turing、Ampere),在较早版本的GPU(Kepler、Maxwell、Pascal)提升一般

PyTorch 通常在 32 位浮点数据 (FP32) 上进行训练,如果你创建一个Tensor, 默认类型都是 torch.FloatTensor (32-bit floating point)。

NVIDIA 的工程师开发了混合精度训练(AMP),让少量操作在 FP32 中的训练,而大部分网络在 FP16 中运行,因此可以节省时间和内存。

torch.cuda.amp 提供了混合精度的便捷方法,其中某些操作使用 FP32 ,其他操作使用 FP16。神经网络训练过程中的运算主要可以分三类:

  • 可以受益于 FP16 速度提升的数学函数。包括矩阵乘法(线性层)和卷积。
  • 对于 16 位精度可能不够的函数,输入应采用 FP32。例如减法。
  • 其他操作,可以在 FP16 中运行的函数,但在 FP16 中加速并不显着,因此它们的 FP32 -> FP16 转换不值得。

混合精度训练将每个操作与其适当的数据类型相匹配,这可以减少网络的运行时间和内存占用。

bfloat16 vs. float16
bfloat16 是一种z专门用于深度学习的 16 位浮点格式,由 1 个符号位、8 个指数位和 7 个尾数位组成。而行业标准 IEEE 16 位浮点是1 个符号位、5 个指数位和 10 个尾数位。
实验表明使用 bfloat16 可以提高训练效率,因为深度学习模型通常对指数变化更加敏感,而16位使用内存更少。
bfloat16 的指数位和 float32 一样,在训练过程中不容易出现下溢,也就不容易出现 NaN 或者 Inf 之类的错误。
使用 bfloat16: dtype=torch.bfloat16

一、一般的训练流程

通常自动混合精度训练会同时使用 torch.autocasttorch.cuda.amp.GradScaler

假设我们已经定义好了一个模型, 并写好了其他相关代码(懒得写出来了)。

1. torch.autocast
torch.autocast 实例作为上下文管理器,允许脚本区域以混合精度运行。
在这些区域中,CUDA 操作将以 autocast 选择的 dtype 运行,以提高性能,同时保持准确性。

autocast应该只封装前向和 loss 计算, 在 backward() 前退出 autocast,反向计算时数据类型和前向的数据类型一致。

训练部分的代码:

for epoch in range(epochs): 
    for input, target in zip(data, targets):
        # 在 ``autocast`` 下进行前向
        with torch.autocast(device_type=device, dtype=torch.float16):
            output = net(input)
            # output is float16 because linear layers ``autocast`` to float16.
          
            loss = loss_fn(output, target)
            # loss is float32 because ``mse_loss`` layers ``autocast`` to float32.
           
        # 在 backward() 前退出``autocast``
        # 不建议在“autocast”下进行反向传递
        # Backward 在相应前向操作选择的相同“dtype”“autocast”中运行。
        loss.backward()
        opt.step()
        opt.zero_grad() # set_to_none=True here can modestly improve performance

可以 autocast 到 FP16 的 CUDA 操作:
__matmul__, addbmm, addmm, addmv, addr, baddbmm, bmm, chain_matmul, multi_dot, conv1d, conv2d, conv3d, conv_transpose1d, conv_transpose2d, conv_transpose3d, GRUCell, linear, LSTMCell, matmul, mm, mv, prelu, RNNCell

autocast 到 FP32 的 CUDA 操作:
__pow__, __rdiv__, __rpow__, __rtruediv__, acos, asin, binary_cross_entropy_with_logits, cosh, cosine_embedding_loss, cdist, cosine_similarity, cross_entropy, cumprod, cumsum, dist, erfinv, exp, expm1, group_norm, hinge_embedding_loss, kl_div, l1_loss, layer_norm, log, log_softmax, log10, log1p, log2, margin_ranking_loss, mse_loss, multilabel_margin_loss, multi_margin_loss, nll_loss, norm, normalize, pdist, poisson_nll_loss, pow, prod, reciprocal, rsqrt, sinh, smooth_l1_loss, soft_margin_loss, softmax, softmin, softplus, sum, renorm, tan, triplet_margin_loss

应该优先选择 binary_cross_entropy_with_logits 而不是 binary_cross_entropy,因为:

torch.nn.function.binary_cross_entropy()(以及包装它的torch.nn.BCELoss)的向后传递可以产生无法在 FP16 中表示的梯度。在启用 autocast 的区域中,前向输入可能是 FP16,这意味着反向梯度必须可以用 FP16 表示。因此,binary_cross_entropy 和 BCELoss 在启用 autocast 的区域中会引发错误。
可以使用 torch.nn.function.binary_cross_entropy_with_logits()torch.nn.BCEWithLogitsLoss 来代替。

2. GradScaler

梯度缩放(gradient scaling)有助于防止在使用混合精度进行训练时,出现梯度下溢,也就是在 FP16 下过小的梯度值会变成 0,因此相应参数的更新将丢失。同样的道理,如果网络中有过小的值,比如防止出现除零而加入的 eps 值如果过小(比如 1e-8),也会导致除零错误出现。

为了防止下溢,梯度缩放将网络的损失乘以比例因子,并对缩放后的损失调用向后传递。然后通过网络向后流动的梯度按相同的因子缩放。换句话说,梯度值具有较大的幅度,因此它们不会刷新为零。

每个参数的梯度(.grad 属性)应该在优化器更新参数之前取消缩放,因此缩放因子不会干扰学习率。

torch.cuda.amp.GradScaler 可以执行梯度缩放步骤。

scaler = torch.cuda.amp.GradScaler()

1+2: Automatic Mixed Precision

use_amp = True

net = make_model(in_size, out_size, num_layers)
opt = torch.optim.SGD(net.parameters(), lr=0.001)
scaler = torch.cuda.amp.GradScaler(enabled=use_amp)

for epoch in range(epochs):
    for input, target in zip(data, targets):
        with torch.autocast(device_type=device, dtype=torch.float16, enabled=use_amp):
            output = net(input)
            loss = loss_fn(output, target)
        # scaler.scale(loss) 缩放梯度,然后进行反向计算
        scaler.scale(loss).backward()
        # scaler.step() 首先取消化器分配参数梯度的缩放,如果梯度中不包括 ``inf`` ``NaN``,就运行 optimizer.step() 
        # 否则会跳过 optimizer.step() 
        scaler.step(opt)
        scaler.update()
        opt.zero_grad() # set_to_none=True here can modestly improve performance

检查 loss scale
训练过程中检查 scale,避免掉到0.

scaler = torch.cuda.amp.GradScaler()
current_loss_scale = scaler.get_scale()
if step % log_iter == 0:
   print('scale:', current_loss_scale)

保存和加载
如果 checkpoint 是在没有 Amp 的情况下保存的,并且你想要使用 Amp 恢复训练,直接从checkpoint 加载模型和优化器状态,然后用新创建的 GradScaler。
如果checkpoint是通过使用 Amp 创建的,并且想要在不使用 Amp 的情况下恢复训练,可以直接从checkpoint 加载模型和优化器状态,忽略保存的 scaler 。

# 保存
checkpoint = {"model": net.state_dict(),
              "optimizer": opt.state_dict(),
              "scaler": scaler.state_dict()}
# Write checkpoint as desired, e.g.,
# torch.save(checkpoint, "filename")

# 加载
dev = torch.cuda.current_device()
checkpoint = torch.load("filename",
                        map_location = lambda storage, loc: storage.cuda(dev))
net.load_state_dict(checkpoint["model"])
opt.load_state_dict(checkpoint["optimizer"])
scaler.load_state_dict(checkpoint["scaler"])

二、 多个XX

多个 model,loss, optimizer
如果有多个损失,则必须分别对每个损失调用 scaler.scale。如果网络有多个优化器,可以分别对其中任何一个优化器调用scaler.unscale_,并且必须对每个优化器单独调用 scaler.step。
但是,scaler.update 只能调用一次.

scaler = torch.cuda.amp.GradScaler()

for epoch in epochs:
    for input, target in data:
        optimizer0.zero_grad()
        optimizer1.zero_grad()
        with autocast(device_type='cuda', dtype=torch.float16):
            output0 = model0(input)
            output1 = model1(input)
            loss0 = loss_fn(2 * output0 + 3 * output1, target)
            loss1 = loss_fn(3 * output0 - 5 * output1, target)

        scaler.scale(loss0).backward()
        scaler.scale(loss1).backward()

        # You can choose which optimizers receive explicit unscaling, if you
        # want to inspect or modify the gradients of the params they own.
        scaler.unscale_(optimizer0)

        scaler.step(optimizer0)
        scaler.step(optimizer1)

        scaler.update()

多个 GPU
autocast 状态会在每个线程上传播,不管是在单个进程的多线程,还是每个 GPU一个进程。(和原来的没什么区别)

model = MyModel()
dp_model = nn.DataParallel(model)

# Sets autocast in the main thread
with autocast(device_type='cuda', dtype=torch.float16):
    # dp_model's internal threads will autocast.
    output = dp_model(input)
    # loss_fn also autocast
    loss = loss_fn(output)

多个GPU一个进程,这里 torch.nn.parallel.DistributedDataParallel可能会产生一个侧线程来在每个设备上运行前向传递,就像 torch.nn.DataParallel 一样。修复方法是相同的:将自动转换作为模型前向方法的一部分应用,以确保它在侧线程中启用。

MyModel(nn.Module):
    ...
    @autocast()
    def forward(self, input):
       ...

# Alternatively
MyModel(nn.Module):
    ...
    def forward(self, input):
        with autocast():
            ...

三、常见问题

  1. 加速有限,可能的原因有:
  • 显卡不支持
  • GPU饱和
  • FP32 -> FP16 的转换消耗了过多时间,应该避免多个小的 CUDA操作
  • 过多的CPU和GPU的通信
  • matmul 操作的尺寸应该是 8 的倍数
  1. loss 是 inf/NaN
  • 如果网络中有较小的数字,转成 FP16 就会变成0,导致出现inf/NaN,先去掉 GradScaler 检查前向过程中是不是会有这种问题。
  • 如果前向过程中出现 NaN,一般是前向过程中某些步骤蕴含求和求平均的操作导致了上溢,找到这些可能出现上溢的地方,手动固定为 FP32 就可以了。
  1. loss scale 掉到了0
    通常也是因为上溢,找到上溢的层固定到 FP32 就可以了。

  2. 混合精度下 transformer 的位置编码碰撞问题
    目前广泛采用的位置编码算法比如 Rope 和 Alibi, 需要为每个位置生成一个整型的 position_id,在 float16/bfloat16 下浮点数精度不足,导致整数范围超过 256 时, bfloat16 无法准确表示每个整数,因此相邻的若干个 token 会共享一个位置编码。
    解决思路也是保证 position_id 的精度在 FP32上就可以了。
    (在图像里ViT上下文没这么长的就不用担心这个问题)

参考:

  1. Pytorch AMP 教程
  2. https://pytorch.org/docs/stable/notes/amp_examples.html
  3. https://pytorch.org/docs/stable/amp.html#autocast-op-reference
  4. Pytorch中混合精度训练的使用和debug
  5. Llama也中招,混合精度下位置编码竟有大坑,百川智能给出修复方案
  6. To Bfloat or not to Bfloat? That is the Question!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容