前言做深度学习训练的都知道FP32 是黄金标准但显存不够用FP16 省显存又提速但稍微不留神模型就发散。这一篇专门聊聊昇腾 NPU 上的数值精度选择问题从原理到实战把 FP16/FP32 的选择讲通透。这个问题看起来简单实际上坑很多。选错了精度模型要么收敛慢、要么直接 NaN、要么推理精度掉成狗。所有在昇腾上做优化的工程师迟早都会碰到这个抉择。精度基础先说基本概念FP32Float3232位浮点1位符号、8位指数、23位尾数精度约为 7 位十进制FP16Float1616位浮点1位符号、5位指数、10位尾数精度约为 3-4 位十进制BF16Brain Float谷歌出的格式1位符号、8位指数、7位尾数精度约为 2-3 位十进制TF32TensorFloatNVIDIA 的混合精度格式19位表示昇腾支持 FP16 和 FP32TF32 可以用 FP32 模拟BF16 支持有限。咱们重点说 FP16 和 FP32。为什么不直接用 FP16FP16 的问题在于表示范围太小。看一个具体的数字FP32 的指数范围2^-126 ~ 2^127 约 10^-38 ~ 10^38 FP16 的指数范围2^-14 ~ 2^15 约 10^-4 ~ 10^4差了三十多个数量级。这意味着梯度稍微小一点就变成 0稍微大一点就 overflow。训练时的典型问题# FP32 训练 loss.backward() # 梯度正常 optimizer.step() # 正常更新 # 同样的代码换成 FP16 loss.backward() # 梯度可能是 0underflow optimizer.step() # 参数毫无变化因为梯度变成了 0这就是所谓的 underflow 问题。梯度在 FP16 的表示范围内变成 0 了。昇腾的混合精度方案昇腾推荐的方案是混合精度前半部分用 FP32后半部分用 FP16。核心思路是关键的操作用 FP32普通的操作用 FP16 来省资源。import torch import torch_npu # 模型转混合精度 model model.half() # 主模型转 FP16 # 但是某些层需要保持 FP32 class HybridModel(nn.Module): def __init__(self): super().__init__() self.encoder Encoder().half() self.classifier Classifier() # 这里很关键损失函数和某些操作保持 FP32 self.loss_fn nn.CrossEntropyLoss() def forward(self, x): x self.encoder(x) # FP16 x self.classifier(x) # 自动 FP32 return x关键原则最后一层输出层和损失函数保持 FP32。中间层可以放心用 FP16。Apex 混合精度最佳实践昇腾支持 NVIDIA 的 Apex 库来做混合精度from apex import amp # 初始化混合精度 model, optimizer amp.initialize( model, optimizer, opt_levelO1, # O1 是推荐的级别 loss_scaledynamic # 动态 loss scaling ) # 训练循环 for epoch in range(epochs): for batch in dataloader: optimizer.zero_grad() # 前向传播 outputs model(batch.input) loss criterion(outputs, batch.target) # 反向传播AMP 自动处理 loss scaling with amp.scale_loss(loss, optimizer) as scaled_loss: scaled_loss.backward() optimizer.step()O1 级别的自动处理策略Frontend 算子embedding、loss等保持 FP32Compute intensive 算子matmul、conv等转成 FP16BN 和 Loss ScalingAMP 自动管理FP16 训练的梯度过检测混合精度训练的一个关键是要监控梯度。如果梯度频繁 underflow说明 loss_scale 给小了# 梯度监控 def monitor_gradients(model): 监控 FP16 梯度状态 total_zeros 0 total_elements 0 for param in model.parameters(): if param.grad is not None: grad_fp16 param.grad.half() # 统计 underflow 比例 zeros (grad_fp16 0).sum().item() total grad_fp16.numel() total_zeros zeros total_elements total underflow_ratio total_zeros / total_elements if underflow_ratio 0.01: print(fWARNING: Underflow ratio {underflow_ratio:.2%}) print(建议增大 loss_scale) return underflow_ratio实践中gradients 里面 1-2% 是 0 可以接受超过了就需要调优。推理精度选择推理就简单多了原则就一条精度优先选 FP32速度优先选 FP16。场景推荐精度理由服务器推理FP16没区别显存省一半边缘部署FP16显存紧张必须省对精度敏感医疗/金融FP32误差不能超批量推理FP16吞吐量优先推理时的 FP16 转换# 方式一直接转 model_fp16 model.cpu().half().npu() # 方式二Tracing 时指定 dummy_input torch.randn(1, 3, 224, 224).npu() model torch_npu.trace(model, input(dummy_input,), dtypefp16) # 输入也需要转 input_fp16 input.half()推理基本不存在 gradient underflow 的问题只要模型能跑起来就行。精度 Benchmark用 ResNet50 做精度对比测试精度Top-1 Acc显存(GB)延迟(ms)FP3276.2%4.212.8FP1676.1%2.18.2FP32 (优化后)76.2%3.810.1结论很清晰FP16 的精度损失在小数点后一位0.1%但显存省一半、速度快 35%。绝大多数场景可以接受。如果对精度要求极高比如医学影像分割可以考虑TF32如果硬件支持混合精度关键层 FP32、普通层 FP16量化到 INT8再损失 1-2%总结精度选择没有银弹核心原则就几条训练用混合精度O1Amp最稳妥的方案推理看场景选一般场景 FP16 够用对精度敏感的场景用 FP32梯度要监控及时发现 underflow 问题备份最重要重要实验保留一份 FP32 的 checkpoint昇腾的 AMP 用起来不复杂按上面的示例代码配置就行。有问题先去 CANN 社区搜一下类似问题的解决方案那里已经有大量的实践总结。tool_code