混合精度训练调优实战 - 第3节

3. 混合精度训练原理

混合精度训练,说白了就是让模型用FP16算得快,但还得保证精度不掉。这里有个核心矛盾:FP16能表示的数字范围太小了。我刚开始接触时也纳闷,既然FP16这么容易溢出,为什么大家还要用?

答案就在Master Weights里。

什么是Master Weights?

Master Weights,就是一份FP32精度的权重副本。模型前向和反向用FP16算,但权重更新的时候,我们偷偷保留一份FP32的版本。

为什么要这么做?你想想看,FP16的梯度如果直接更新权重,很容易就变成0了。特别是那些小梯度,FP16根本表示不了。我在项目中遇到过,一个BERT-large模型训练到一半,loss突然不降了。查了半天,发现是梯度太小,被FP16截断成了0。

具体流程是这样的:

  1. 维护一份FP32的Master Weights
  2. 前向传播时,把Master Weights转成FP16
  3. 用FP16做前向和反向计算
  4. 拿到FP16的梯度后,转回FP32
  5. 用FP32的梯度更新Master Weights

关键点:权重更新永远在FP32精度下完成。FP16只负责计算,不负责存储。

权重更新的精度管理

我习惯把权重更新比作「存钱」。FP16就像一个小钱包,只能装几百块。FP32就像银行卡,能存几百万。你用小钱包做交易(前向反向),但最终的钱还是存在银行卡里(Master Weights)。

代码实现其实不复杂:

# 伪代码示例
master_weights = model.parameters().float()  # FP32副本

for batch in dataloader:
    # 前向用FP16
    with torch.cuda.amp.autocast():
        loss = model(batch)
    
    # 反向传播
    scaler.scale(loss).backward()
    
    # 梯度转FP32,更新Master Weights
    scaler.step(optimizer)
    scaler.update()

嗯,这里要注意:optimizer里管理的参数必须是Master Weights,不是模型本身的FP16参数。我见过有人搞混,结果模型越训越差。

Loss Scaling(损失缩放)

Loss Scaling,这名字听着高大上,其实道理很简单。FP16能表示的最小正数是2^-24,大约6e-8。如果梯度比这个还小,直接就变0了。

怎么办?把loss放大啊。

我刚开始做混合精度时,总觉得这步是多余的。直到有一次训练一个图像分割模型,发现小目标的梯度全没了。嗯,从那以后我再也不敢跳过Loss Scaling了。

缩放策略

策略 说明 我推荐
固定缩放 手动设置一个缩放因子,比如1024 不推荐,容易过缩放或欠缩放
动态缩放 根据梯度是否溢出自动调整 强烈推荐,AMP默认就是这个
无缩放 直接硬跑FP16 别试,会后悔的

动态缩放是怎么工作的?

  • 如果连续N步没有溢出,就把缩放因子翻倍(比如从1024到2048)
  • 如果某一步溢出了,就回退到上一个安全的缩放因子
  • PyTorch AMP默认N=2000,我个人觉得这个值挺合理的

小技巧:训练初期梯度波动大,容易溢出。我习惯把初始缩放因子设小一点,比如512。等loss稳定了,AMP会自动调大。

自动混合精度(AMP)机制

AMP,全称Automatic Mixed Precision。说白了就是让框架帮你决定:哪些操作用FP16,哪些用FP32。

为什么不能全用FP16?因为有些操作对精度太敏感了。比如BatchNorm的均值和方差,用FP16算会出大问题。我踩过这个坑,训练一个ResNet-50,用了全FP16,结果验证集准确率掉了5个点。

AMP的精度管理策略

PyTorch的AMP主要做三件事:

  1. 自动Cast:前向传播时,自动把输入转成FP16
  2. 白名单机制:某些操作强制用FP32(比如Softmax、BatchNorm)
  3. 黑名单机制:某些操作强制用FP16(比如卷积、矩阵乘法)

我画了一张图,帮你理解AMP的决策流程:

AMP自动精度决策流程 输入Tensor 操作类型? 白名单操作 黑名单操作 强制FP32计算 强制FP16计算 默认:自动Cast到FP16 输出计算结果

AMP的三种模式

PyTorch提供了三种精度模式,我整理了一下:

模式 精度 速度提升 适用场景
O0 纯FP32 1x 调试、精度验证
O1 混合精度(推荐) 1.5-2x 大部分训练任务
O2 几乎全FP16 2-3x 对精度不敏感的任务
O3 纯FP16 3x+ 不推荐,除非你确定没问题

我个人习惯用O1模式。O2虽然快,但我在项目中遇到过几次精度下降的问题。O1是最稳妥的选择。

前向与反向传播中的精度管理

前向传播时,AMP会自动把输入转成FP16。但这里有个坑:如果你的模型里有自定义操作,AMP可能不知道该怎么处理。

我举个例子。有一次我写了一个自定义的损失函数,里面用了torch.log。AMP默认把这个操作也转成了FP16,结果log(0)直接变成了-inf。嗯,从那以后我学会了用@torch.cuda.amp.custom_fwd和@torch.cuda.amp.custom_bwd装饰器。

注意:自定义操作一定要标注精度范围。否则AMP会按默认规则处理,很可能出问题。

反向传播的精度管理

反向传播时,梯度是用FP16计算的。但梯度更新前,必须转回FP32。这个转换由GradScaler自动完成。

我曾经犯过一个错误:手动把梯度转成FP16再更新权重。结果模型直接不收敛了。后来才发现,GradScaler已经帮我做了这件事,我多此一举了。

正确的做法是:

scaler = torch.cuda.amp.GradScaler()

for data, target in dataloader:
    optimizer.zero_grad()
    
    with torch.cuda.amp.autocast():
        output = model(data)
        loss = loss_fn(output, target)
    
    # 反向传播
    scaler.scale(loss).backward()
    
    # 梯度更新(自动处理精度转换)
    scaler.step(optimizer)
    scaler.update()

你看,代码其实很简单。但背后的原理值得你花时间理解。混合精度训练不是魔法,它是一套精心设计的精度管理策略。

我的建议:刚开始用AMP时,先在小模型上跑通。确认精度没问题后,再迁移到大模型。别一上来就训,出了问题你都不知道是哪里错了。

混合精度训练的核心,就是在速度和精度之间找到平衡点。Master Weights保证权重更新的精度,Loss Scaling防止梯度消失,AMP自动管理操作精度。这三者配合,才能让FP16训练既快又稳。