混合精度训练 Mixed Precision Training (AMP)

Mixed Precision Training / Automatic Mixed Precision (AMP)

混合精度训练是在训练神经网络时,把不同的计算(矩阵乘/Attention)用 FP16/BF16,其他容易溢出的计算(Loss 缩放、Softmax、梯度累加)保留 FP32,以此来把训练显存砍半、Tensor Core 计算速度翻倍且不损失精度的通用技巧。

详细解释

**Mixed Precision Training(混合精度训练 / Automatic Mixed Precision,简称 AMP)**一句话:深度学习训练 99% 的计算量都在矩阵乘和 Attention 这些 Tensor Core 友好的大矩阵运算上,这些运算用半精度 FP16/BF16 算出来和全精度 FP32 数值误差在 0.1% 以内完全不影响收敛,但显存占用减半、计算速度直接翻倍(因为 Tensor Core 对 16 位浮点的吞吐量是 32 位的 ×2 到 ×4);但 Loss / Softmax / Batch Sum 这些运算对半精度下溢/上溢敏感,会出现梯度变 0(Underflow)或者 NaN(Overflow)。混合精度就是「大部分 Tensor 运算用 16 位提速省显存、少数敏感运算保留 32 位 + 给 Loss 乘一个缩放系数 Scale 防下溢」的一套组合拳,让你速度翻倍、显存减半、精度不掉。 它是 2018 年之后所有大模型训练的默认「基础设施级技巧」,任何不开启 AMP 的训练基本等同于故意浪费一半钱和一半时间。

AMP 的三大核心技巧(NVIDIA 2018 年《Mixed Precision Training》论文里定下的三大铁律,至今 7 年没变过):(1)Weight Master Copy 主权重保留 FP32 精度——模型权重的「本」永远存在 FP32 的 Master Copy 里,Forward / Backward 时实时拷贝并 Cast 成 FP16/BF16 去算,Update(优化器 step)时把 16 位梯度 Cast 回 FP32 去更新 FP32 的权重——保证权重的累积更新永远是高精度的,不会因为 16 位精度不够越训越偏;(2)Loss Scaling(损失缩放)防梯度 Underflow——FP16 能表示的最小正数大约是 6e-8,很多梯度假正的值就是 1e-9 量级,直接转 FP16 会变成 0(下溢);做法是 Forward 算出来的 Loss 先乘以一个大 Scale(比如 1024 / 4096),再 Backward,这样梯度也跟着被放大 Scale 倍,就不会 Underflow 成 0;在 update FP32 权重之前再把梯度除以 Scale 还原回去;(3)自动精度白名单 / 黑名单——矩阵乘、Convolution、Attention 这些放在白名单用 FP16/BF16 最快;Softmax、LayerNorm 的 Reduction Sum、Loss 计算这些放在黑名单强制用 FP32(或者算的时候动态 Cast 到 FP32);不用你手动一个个 Cast,PyTorch torch.cuda.amp.autocast / TransformerEngine 自动帮你分。

唯元智创 的所有训练任务(SFT / DPO / KTO / 预训练)全部默认打开 AMP(2025 年默认用 BF16 + FP32 Master Weight,旧显卡不支持 BF16 的自动回退到 FP16 + 动态 Loss Scaling),平台还会自动监控每 100 step 的 Inf/Nan 比例:如果 Inf/Nan 比例超过 0.1%,自动把 Loss Scale 调小 + 报警通知用户,不用人工盯着 Loss 曲线看。

FP16 AMP vs BF16 AMP 对比(2025 年选型——新手最常问的问题)

维度FP16 AMP + 动态 Loss ScalingBF16 AMP(不需要 Loss Scale,FP32 Master Weight 仍保留)
动态范围(能表示的数范围)±65504 上限,±6e-8 下限 → 范围很窄,梯度特别容易下溢±3e38 上限(和 FP32 一样大),±1e-38 下限 → 动态范围和 FP32 一样宽,下溢概率几乎为 0
尾数精度(值的精细程度)10 位尾数(约 3 位十进制小数精度)→ 精度比 BF16 高 2×7 位尾数(约 2 位十进制小数精度)
是否需要 Loss Scaling必须用(否则 Underflow 梯度为 0)❌ 不需要(动态范围够大,几乎不会下溢)
Inf / NaN 出现概率高——Loss Scale 设得不好、梯度突变时容易出现,训练可能偶尔不稳定极低(< 百万分之一 step)——动态范围和 FP32 同级,溢出概率几乎为 0
收敛曲线和最终精度两者对主流 LLM 训练几乎没差别(差异 < 0.2%)相同
代码复杂度需要 scaler = GradScaler() 包裹整个 step 流程只需要 autocast 开 bf16,不用 GradScaler,简单 50%
硬件要求所有 NVIDIA Volta(V100)及之后的显卡均支持(V100/T4/A10/3090/4090/A100 全支持)仅限 Ampere+ 架构(A10/A100/3090/4090/H100/H200/GB200),T4/V100/2080Ti 这些老卡不支持
2025 年推荐度⭐⭐⭐⭐ 老卡(T4/V100)只能用它;新卡不推荐⭐⭐⭐⭐⭐ 所有新显卡一律 BF16,省掉 Loss Scale 调参的坑,写代码简单一倍

落地 AMP 的 5 条实战纪律

  1. 一定要用 Master Weight(FP32 的权重副本),不要省这份显存——很多人觉得「FP32 Master Copy 又占一半模型权重显存,太浪费了」,想直接存 16 位权重做 update;结果就是训练到后期,16 位权重的 Update 累加误差慢慢积累(每次 × 学习率的小增量,加久了就偏差),下游任务分数会掉 1-3%,而且你完全不知道是因为 AMP 丢精度了。Master Weight 只占模型权重一份 FP32 的空间(7B FP32 才 28GB,相比训练时的优化器状态 3 份 FP32 参数+梯度完全是小头),别省这点钱;2025 年的所有主流框架(HF Trainer、DeepSpeed、Megatron)默认都是开 Master Weight 的,你只要不要手动去关就好。
  2. FP16 AMP 如果 Loss 出现 NaN,第一步要做的不是调学习率,是把 Loss Scale 的初始值调小 + 打开动态缩放——默认动态 Loss Scaling(PyTorch GradScaler 的默认参数)在 95% 的情况下会自动调对,出现 NaN 时它会自动把 Scale 砍半然后继续;如果你的训练一上来 Loss 就是 NaN,动态 Scale 砍到 1 还是 NaN,那肯定不是 AMP 的问题,是你的学习率太大了 / 数据有 bug / 梯度裁剪没开,不要甩锅给 AMP。
  3. 绝对不要在 Forward 里手动把所有张量 Cast 到 FP16!——正确做法是用 torch.cuda.amp.autocast(dtype=torch.bfloat16) 上下文管理器把模型 Forward 的代码包起来,autocast 会根据操作类型自动把白名单操作 Cast 到 16 位,黑名单强制 32 位;你手动全 Cast 会把 Softmax、LayerNorm 这些黑名单操作也强制放到 16 位,结果 LayerNorm Sum 下溢 / Softmax 归一化错,直接不收敛。
  4. 评估 / 推理时要不要开 AMP?建议永远开 BF16 推理,别用 FP32——推理没有 Backward 和 Gradient Scaling 这些问题,直接模型 Cast 到 BF16(老卡 FP16)跑就行,显存减半、速度翻倍,精度损失 < 0.1%(大部分人盲测分不出来);2025 年所有的推理服务默认都是 BF16 / FP8,没有人用 FP32 做推理了,浪费钱。
  5. 多机多卡 + ZeRO / FSDP 场景下的 AMP 精度要统一——所有 GPU 必须用同一种精度(要么全 BF16,要么全 FP16),不能一张卡 BF16 一张卡 FP16;否则跨卡 All-Reduce 同步梯度时 Cast 会出莫名其妙的问题,All-reduce 结果对不上导致训练发散。DeepSpeed 配置里在 bf16 节将 enabled 开关设为 true 全局配置一次就好,不要每张卡单独设置。

常见问题

AMP 训练出来的模型最后 Deploy 推理时,应该存什么精度的权重?存 BF16 还是 FP32?要把 AMP 反解吗?
不需要反解——训练结束时你手里有 FP32 的 Master Weight,直接把它 Cast 到你部署用的精度(BF16 / FP8 / INT4 AWQ)就好;推理一般不存 FP32 权重(太浪费显存,没有人部署用 FP32),主流选择是:(1)如果是 GPU 推理、不做量化 → 直接存 BF16 Safetensors,效果最好、代码最简单;(2)如果显存紧张想跑更大的模型 → 跑一遍 4bit AWQ / GPTQ 量化,存量化后的 INT4 权重(部署时再用对应的量化 Kernel 加载);(3)H100/H200/GB200 等新架构 → 存 FP8 权重,直接吃 TensorCore FP8 原生吞吐量翻倍的福利。踩过的坑:训练完之后保存权重时,很多人误把 AMP 中间状态的 BF16 Activate 版本当成最终权重保存(应该保存 FP32 Master Weight,然后再 Cast 到目标精度)。正确做法:训练结束后调用 model.save_pretrained(),HF Trainer 会自动帮你把 Master Weight 拿出来转成标准精度(默认 BF16)存 Safetensors,不要手动取 optimizer.state_dict() 里的东西。Quantization(量化) 里列的 5 档精度和部署场景,就是基于 BF16 Master Weight 做后续量化和存储的。
训练到中间突然某个 Step 出现 Inf / NaN Loss,是不是 AMP 的锅?怎么排查?
95% 的概率不是 AMP 本身的问题——先做下面四步排查,顺序从快到慢:(1)先复现:把当前 Step 的 batch dump 下来,用相同的参数单卡跑一遍 FP32 精度(关 AMP),看看是不是也 NaN;如果 FP32 也 NaN,100% 是数据 / 学习率 / 梯度裁剪的问题,和 AMP 没关系;(2)如果 FP32 正常但 AMP NaN,那看 FP16 还是 BF16:如果是 FP16 AMP,直接把 Loss Scale 初始值从默认 2^16 改成 2^8(小 256 倍),再跑一遍;如果 BF16 AMP 还 NaN(极少见),说明梯度真的爆炸了,不是 AMP 的问题;(3)看梯度范数曲线(每步 log ‖grad‖):如果梯度范数在 NaN 之前跳到了 1e+4 以上,就是梯度爆炸,立刻调大梯度裁剪阈值(从 1.0 调到 0.1)+ 调小学习率(减半);(4)查训练数据里有没有脏样本:一条极端长的样本(比如 10 万个 Token 的长代码)会让 Attention 的 Softmax 数值爆掉;把当前 Step 里的样本长度打出来,如果 max_length 比数据集平均大 10 倍,直接过滤掉超长样本。99% 的情况下这四步能定位到真实原因;最后剩下 1% 真的是 AMP 实现的 bug(比如你手写 AMP 白名单配置错了),换成默认 Trainer 配置别手搓就好。
DeepSpeed ZeRO Stage 3 / FSDP 全参数分片时,AMP 的 Master Weight 还存在吗?会不会被分片影响?
存在且完全不冲突——ZeRO Stage 3 / FSDP 把 FP32 Master Weight + Optimizer State + Gradient 全部按参数维度分片(Shard)到每张 GPU 上,每张卡只存属于自己那份分片,和 AMP 的「Master Weight FP32 + Forward/Backward Cast 到 16 位」的机制完美兼容,两者正交独立工作、互不干扰。其实 ZeRO / FSDP 的「分片 + 按需 All-Gather 权重」的流程就是:(1)Forward 之前,All-Gather 本层所需的 FP32 Master Weight 分片到完整张量 →(2)Cast 到 BF16 给 Forward 用 →(3)Forward 结果算 Loss →(4)Backward 算 BF16 的梯度 →(5)Reduce-Scatter 把梯度分片回每张卡 →(6)每张卡 Cast 分片梯度到 FP32,更新自己那份 FP32 Master Weight 的分片。这套流程在 DeepSpeed ZeRO-3 + BF16 AMP 的配置里是默认原生支持的,你只要同时开 zero_optimization 的 stage 3 配置和 bf16 的 enabled 为 true 配置就好,不需要额外配置;2025 年训练 70B 以上的模型基本都是 ZeRO-3 加 8 卡 BF16 AMP 加 Gradient Accumulation 三件套组合,显存占用从 70B 全量 SFT 的 2TB 降到 8 乘 A100 80G 卡刚好放下(640G),训练成本从百万美元级降到几千美元。Fine-Tuning(微调) 里列的 70B 规模模型显存计算就包含了 ZeRO-3 + AMP 两者的节省比例。