CANN 昇腾 FP16 vs FP32 精度博弈:深度学习数值精度实战指南
做深度学习训练的都知道,FP32 是黄金标准,但显存不够用;FP16 省显存又提速,但稍微不留神模型就发散。这一篇专门聊聊昇腾 NPU 上的数值精度选择问题,从原理到实战把 FP16/FP32 的选择讲通透。这个问题看起来简单,实际上坑很多。选错了精度,模型要么收敛慢、要么直接 NaN、要么推理精度掉成狗。所有在昇腾上做优化的工程师迟早都会碰到这个抉择。训练用混合精度:O1+Amp,最稳妥的方案推
前言
做深度学习训练的都知道,FP32 是黄金标准,但显存不够用;FP16 省显存又提速,但稍微不留神模型就发散。这一篇专门聊聊昇腾 NPU 上的数值精度选择问题,从原理到实战把 FP16/FP32 的选择讲通透。
这个问题看起来简单,实际上坑很多。选错了精度,模型要么收敛慢、要么直接 NaN、要么推理精度掉成狗。所有在昇腾上做优化的工程师迟早都会碰到这个抉择。
精度基础
先说基本概念:
- FP32(Float32):32位浮点,1位符号、8位指数、23位尾数,精度约为 7 位十进制
- FP16(Float16):16位浮点,1位符号、5位指数、10位尾数,精度约为 3-4 位十进制
- BF16(Brain Float):谷歌出的格式,1位符号、8位指数、7位尾数,精度约为 2-3 位十进制
- TF32(TensorFloat):NVIDIA 的混合精度格式,19位表示
昇腾支持 FP16 和 FP32,TF32 可以用 FP32 模拟,BF16 支持有限。咱们重点说 FP16 和 FP32。
为什么不直接用 FP16
FP16 的问题在于表示范围太小。看一个具体的数字:
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() # 梯度可能是 0(underflow)
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_level="O1", # O1 是推荐的级别
loss_scale="dynamic" # 动态 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等):保持 FP32
- Compute intensive 算子(matmul、conv等):转成 FP16
- BN 和 Loss Scaling:AMP 自动管理
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(f"WARNING: 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,), dtype='fp16')
# 输入也需要转
input_fp16 = input.half()
推理基本不存在 gradient underflow 的问题,只要模型能跑起来就行。
精度 Benchmark
用 ResNet50 做精度对比测试:
| 精度 | Top-1 Acc | 显存(GB) | 延迟(ms) |
|---|---|---|---|
| FP32 | 76.2% | 4.2 | 12.8 |
| FP16 | 76.1% | 2.1 | 8.2 |
| FP32 (优化后) | 76.2% | 3.8 | 10.1 |
结论很清晰:FP16 的精度损失在小数点后一位(0.1%),但显存省一半、速度快 35%。绝大多数场景可以接受。
如果对精度要求极高(比如医学影像分割),可以考虑:
- TF32(如果硬件支持)
- 混合精度:关键层 FP32、普通层 FP16
- 量化到 INT8(再损失 1-2%)
总结
精度选择没有银弹,核心原则就几条:
- 训练用混合精度:O1+Amp,最稳妥的方案
- 推理看场景选:一般场景 FP16 够用,对精度敏感的场景用 FP32
- 梯度要监控:及时发现 underflow 问题
- 备份最重要:重要实验保留一份 FP32 的 checkpoint
昇腾的 AMP 用起来不复杂,按上面的示例代码配置就行。有问题先去 CANN 社区搜一下类似问题的解决方案,那里已经有大量的实践总结。<tool_code>
鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。
更多推荐




所有评论(0)