4、混合精度训练:什么是混合精度训练、AMP自动混合精度、手动控制精度

说到混合精度训练,我得先坦白一件事。

我刚入行那会儿,总觉得训练模型就是堆算力。显存不够?换更大的卡呗。直到有一次,我接了个超大模型的项目,A100 80G 都撑不住。那时候我才意识到——显存优化不是选择题,是生存题。

混合精度训练,就是那个帮我「活下来」的关键技术。

4.1 什么是混合精度训练?

先问个问题:你训练模型时,默认用的什么精度?

大部分人回答是 FP32(32位浮点数)。没错,PyTorch 默认就是它。但你想过没有——真的每一步都需要这么高的精度吗?

我在项目中观察过,很多层的梯度其实很小,用 FP16(16位浮点数)完全够用。FP32 占 4 字节,FP16 只占 2 字节。显存直接砍半!

但问题来了:FP16 的动态范围太小,容易溢出。怎么办?

混合精度训练的思路很简单:

  • 前向和反向传播:用 FP16 计算,省显存、加速
  • 权重更新:用 FP32 保存主权重,保证精度
  • 损失缩放:把损失值放大,避免梯度下溢

说白了,就是「该省的地方省,该保的地方保」。我习惯把 FP32 比作「保险柜」,FP16 比作「零钱包」。日常花销用零钱包,大额交易才开保险柜。

核心收益:显存占用减少 40%-50%,训练速度提升 1.5-3 倍。

4.2 AMP 自动混合精度

手动管理精度切换?太累了。PyTorch 1.6 之后推出了 torch.cuda.amp,也就是 AMP(Automatic Mixed Precision)。

AMP 的核心是两个组件:

  • GradScaler:自动做损失缩放,防止梯度下溢
  • autocast:自动选择每步用 FP16 还是 FP32

我刚开始用 AMP 时,其实有点不放心。总觉得「自动」的东西不靠谱。后来在 BERT 训练中试了一次,效果出奇的好——显存从 12G 降到 7G,速度还快了 40%。

来看个标准用法:

from torch.cuda.amp import autocast, GradScaler

scaler = GradScaler()

for data, target in dataloader:
    optimizer.zero_grad()
    
    with autocast():
        output = model(data)
        loss = loss_fn(output, target)
    
    scaler.scale(loss).backward()
    scaler.step(optimizer)
    scaler.update()

嗯,就这么几行代码。你只需要把前向计算包在 autocast() 里,然后用 scaler 处理反向传播和优化器更新。其他都不用管。

我的习惯:AMP 默认对大部分操作都有效。但像 softmax、layer_norm 这些操作,AMP 会自动用 FP32 计算,保证数值稳定性。

4.3 手动控制精度

AMP 虽好,但有时候你得亲自下场。为什么?

我在做 NLP 模型时遇到过一个问题:某些自定义的 attention 层,AMP 自动选择了 FP16,结果 loss 直接变成 NaN。排查了半天,发现是 FP16 精度不够,导致 softmax 溢出。

这时候就需要手动控制精度了。

手动控制主要有两种方式:

4.3.1 模型级别的精度控制

model.half()  # 整个模型转 FP16
model.float() # 整个模型转 FP32

简单粗暴,但不够灵活。我一般只在推理时用 model.half() 加速。

4.3.2 层级别的精度控制

更精细的做法是:

class MixedPrecisionModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 64, 3).half()   # 这个层用 FP16
        self.conv2 = nn.Conv2d(64, 128, 3).float() # 这个层用 FP32
    
    def forward(self, x):
        x = self.conv1(x.half())   # 输入转 FP16
        x = self.conv2(x.float())  # 输入转 FP32
        return x

你想想看,这样就能针对每个层做定制。比如:

  • 卷积层:对精度不敏感,用 FP16 省显存
  • BatchNorm 层:必须用 FP32,否则训练不稳定
  • Embedding 层:建议用 FP32,避免精度损失

我曾经踩过的坑:手动控制精度时,一定要注意输入输出的类型匹配。FP16 和 FP32 混用,很容易出现类型不匹配的错误。建议在 forward 函数开头统一做类型转换。

4.4 知识体系图

下面这张图,是我自己总结的混合精度训练核心逻辑。你看一眼就能明白整个流程:

混合精度训练核心逻辑 输入数据 (FP32) AMP? 自动混合精度 手动控制精度 autocast 自动选择精度 GradScaler 损失缩放 model.half() / .float() 逐层精度控制 显存节省 + 训练加速

4.5 实战建议

最后,分享几个我在项目中总结的经验:

场景 推荐做法 原因
小模型(<100M 参数) AMP 自动混合精度 省心,效果明显
大模型(>1B 参数) AMP + 手动调整关键层 避免精度瓶颈
推理阶段 model.half() 极致加速
自定义算子 手动控制精度 AMP 可能不兼容

小技巧:如果你不确定哪些层该用 FP16,可以先用 AMP 跑一次,然后打印出每个层的精度选择。PyTorch 提供了 torch.cuda.amp.autocast.list_funcs() 来查看。

混合精度训练,说白了就是「用更少的资源,做更多的事」。我个人觉得,这是每个深度学习工程师必须掌握的技能。不管你是做 CV 还是 NLP,显存永远不够用,速度永远想更快。

嗯,今天就聊到这儿。记住:AMP 是默认选项,手动控制是进阶技能。先用好 AMP,再根据需求做手动优化。

可可精品资料库