注意
跳转至页面底部下载完整示例代码。
自动混合精度 (Automatic Mixed Precision)#
创建日期:2020年9月15日 | 最后更新:2025年1月30日 | 最后验证:2024年11月5日
作者: Michael Carilli
torch.cuda.amp 提供了混合精度的便捷方法,其中部分操作使用 torch.float32 (float) 数据类型,而其他操作则使用 torch.float16 (half)。一些算子(如线性层和卷积)在 float16 或 bfloat16 下运行速度要快得多;而其他算子(如归约操作)通常需要 float32 的动态范围。混合精度尝试为每个算子匹配最合适的数据类型,这可以减少网络的运行时间和内存占用。
通常,“自动混合精度训练”会同时使用 torch.autocast 和 torch.cuda.amp.GradScaler。
本教程演示了在默认精度下简单网络的性能,然后通过添加 autocast 和 GradScaler 来运行相同的网络,从而实现混合精度并提升性能。
您可以下载并作为独立的 Python 脚本运行此教程。唯一的要求是 PyTorch 1.6 或更高版本以及支持 CUDA 的 GPU。
混合精度主要使支持 Tensor Core 的架构(Volta、Turing、Ampere)受益。在这些架构上,此教程应能显示显著(2-3 倍)的加速。在较早的架构(Kepler、Maxwell、Pascal)上,您可能会观察到适度的加速。运行 nvidia-smi 可显示您 GPU 的架构。
import torch, time, gc
# Timing utilities
start_time = None
def start_timer():
global start_time
gc.collect()
torch.cuda.empty_cache()
torch.cuda.reset_max_memory_allocated()
torch.cuda.synchronize()
start_time = time.time()
def end_timer_and_print(local_msg):
torch.cuda.synchronize()
end_time = time.time()
print("\n" + local_msg)
print("Total execution time = {:.3f} sec".format(end_time - start_time))
print("Max memory used by tensors = {} bytes".format(torch.cuda.max_memory_allocated()))
简单的网络#
以下一系列线性层和 ReLU 激活函数应能在混合精度下表现出加速效果。
def make_model(in_size, out_size, num_layers):
layers = []
for _ in range(num_layers - 1):
layers.append(torch.nn.Linear(in_size, in_size))
layers.append(torch.nn.ReLU())
layers.append(torch.nn.Linear(in_size, out_size))
return torch.nn.Sequential(*tuple(layers)).cuda()
batch_size、in_size、out_size 和 num_layers 的选择要足够大,以使 GPU 工作负载饱和。通常,当 GPU 达到饱和时,混合精度带来的加速最为显著。小型网络可能受限于 CPU,在这种情况下,混合精度不会提高性能。尺寸的选择也确保了线性层参与计算的维度是 8 的倍数,以便在支持 Tensor Core 的 GPU 上使用 Tensor Core(参见下文的 故障排除)。
练习:尝试改变参与计算的尺寸,观察混合精度加速的变化情况。
batch_size = 512 # Try, for example, 128, 256, 513.
in_size = 4096
out_size = 4096
num_layers = 3
num_batches = 50
epochs = 3
device = 'cuda' if torch.cuda.is_available() else 'cpu'
torch.set_default_device(device)
# Creates data in default precision.
# The same data is used for both default and mixed precision trials below.
# You don't need to manually change inputs' ``dtype`` when enabling mixed precision.
data = [torch.randn(batch_size, in_size) for _ in range(num_batches)]
targets = [torch.randn(batch_size, out_size) for _ in range(num_batches)]
loss_fn = torch.nn.MSELoss().cuda()
默认精度#
如果不使用 torch.cuda.amp,以下简单网络将在默认精度 (torch.float32) 下执行所有操作。
net = make_model(in_size, out_size, num_layers)
opt = torch.optim.SGD(net.parameters(), lr=0.001)
start_timer()
for epoch in range(epochs):
for input, target in zip(data, targets):
output = net(input)
loss = loss_fn(output, target)
loss.backward()
opt.step()
opt.zero_grad() # set_to_none=True here can modestly improve performance
end_timer_and_print("Default precision:")
添加 torch.autocast#
torch.autocast 实例作为上下文管理器,允许脚本中的特定区域在混合精度下运行。
在这些区域内,CUDA 操作将以 autocast 选择的 dtype 运行,从而在保持精度的同时提高性能。有关 autocast 为每个算子选择何种精度及其触发条件的详细信息,请参阅 Autocast 算子参考。
for epoch in range(0): # 0 epochs, this section is for illustration only
for input, target in zip(data, targets):
# Runs the forward pass under ``autocast``.
with torch.autocast(device_type=device, dtype=torch.float16):
output = net(input)
# output is float16 because linear layers ``autocast`` to float16.
assert output.dtype is torch.float16
loss = loss_fn(output, target)
# loss is float32 because ``mse_loss`` layers ``autocast`` to float32.
assert loss.dtype is torch.float32
# Exits ``autocast`` before backward().
# Backward passes under ``autocast`` are not recommended.
# Backward ops run in the same ``dtype`` ``autocast`` chose for corresponding forward ops.
loss.backward()
opt.step()
opt.zero_grad() # set_to_none=True here can modestly improve performance
添加 GradScaler#
梯度缩放有助于防止在混合精度训练时,小幅度的梯度因数值下溢(flush to zero)而丢失。
torch.cuda.amp.GradScaler 可以方便地执行梯度缩放的各个步骤。
# Constructs a ``scaler`` once, at the beginning of the convergence run, using default arguments.
# If your network fails to converge with default ``GradScaler`` arguments, please file an issue.
# The same ``GradScaler`` instance should be used for the entire convergence run.
# If you perform multiple convergence runs in the same script, each run should use
# a dedicated fresh ``GradScaler`` instance. ``GradScaler`` instances are lightweight.
scaler = torch.amp.GradScaler("cuda")
for epoch in range(0): # 0 epochs, this section is for illustration only
for input, target in zip(data, targets):
with torch.autocast(device_type=device, dtype=torch.float16):
output = net(input)
loss = loss_fn(output, target)
# Scales loss. Calls ``backward()`` on scaled loss to create scaled gradients.
scaler.scale(loss).backward()
# ``scaler.step()`` first unscales the gradients of the optimizer's assigned parameters.
# If these gradients do not contain ``inf``s or ``NaN``s, optimizer.step() is then called,
# otherwise, optimizer.step() is skipped.
scaler.step(opt)
# Updates the scale for next iteration.
scaler.update()
opt.zero_grad() # set_to_none=True here can modestly improve performance
总结:“自动混合精度”#
(以下内容还演示了 enabled,这是 autocast 和 GradScaler 的一个可选便捷参数。如果设为 False,则调用 autocast 和 GradScaler 等同于空操作。这使得无需 if/else 语句即可在默认精度和混合精度之间进行切换。)
use_amp = True
net = make_model(in_size, out_size, num_layers)
opt = torch.optim.SGD(net.parameters(), lr=0.001)
scaler = torch.amp.GradScaler("cuda" ,enabled=use_amp)
start_timer()
for epoch in range(epochs):
for input, target in zip(data, targets):
with torch.autocast(device_type=device, dtype=torch.float16, enabled=use_amp):
output = net(input)
loss = loss_fn(output, target)
scaler.scale(loss).backward()
scaler.step(opt)
scaler.update()
opt.zero_grad() # set_to_none=True here can modestly improve performance
end_timer_and_print("Mixed precision:")
检查/修改梯度(例如,梯度裁剪)#
由 scaler.scale(loss).backward() 产生的所有梯度都会被缩放。如果您希望在 backward() 和 scaler.step(optimizer) 之间修改或检查参数的 .grad 属性,应首先使用 scaler.unscale_(optimizer) 对其进行反缩放。
for epoch in range(0): # 0 epochs, this section is for illustration only
for input, target in zip(data, targets):
with torch.autocast(device_type=device, dtype=torch.float16):
output = net(input)
loss = loss_fn(output, target)
scaler.scale(loss).backward()
# Unscales the gradients of optimizer's assigned parameters in-place
scaler.unscale_(opt)
# Since the gradients of optimizer's assigned parameters are now unscaled, clips as usual.
# You may use the same value for max_norm here as you would without gradient scaling.
torch.nn.utils.clip_grad_norm_(net.parameters(), max_norm=0.1)
scaler.step(opt)
scaler.update()
opt.zero_grad() # set_to_none=True here can modestly improve performance
保存/恢复检查点#
若要以位级精度保存/恢复启用了 Amp 的运行,请使用 scaler.state_dict 和 scaler.load_state_dict。
保存时,应将 scaler 的状态字典与模型和优化器的状态字典一起保存。请在迭代开始时的前向传播之前,或迭代结束后的 scaler.update() 之后执行此操作。
checkpoint = {"model": net.state_dict(),
"optimizer": opt.state_dict(),
"scaler": scaler.state_dict()}
# Write checkpoint as desired, e.g.,
# torch.save(checkpoint, "filename")
恢复时,将 scaler 的状态字典与模型和优化器的状态字典一起加载。按需读取检查点,例如:
dev = torch.cuda.current_device()
checkpoint = torch.load("filename",
map_location = lambda storage, loc: storage.cuda(dev))
net.load_state_dict(checkpoint["model"])
opt.load_state_dict(checkpoint["optimizer"])
scaler.load_state_dict(checkpoint["scaler"])
如果检查点是来自 没有 使用 Amp 的运行,而您现在想要 在 Amp 下恢复训练,则像往常一样加载模型和优化器状态。由于检查点中不包含保存的 scaler 状态,请使用一个新的 GradScaler 实例。
如果检查点是来自 使用 了 Amp 的运行,而您现在想要 在没有 Amp 的情况下恢复训练,则像往常一样加载模型和优化器状态,并忽略已保存的 scaler 状态。
推理/评估#
autocast 可以单独用于包裹推理或评估的前向传播过程,无需 GradScaler。
进阶主题#
请参阅 自动混合精度示例 以获取进阶用例,包括:
梯度累积
梯度惩罚/双重反向传播
具有多个模型、优化器或损失的网络
多 GPU 训练 (
torch.nn.DataParallel或torch.nn.parallel.DistributedDataParallel)自定义 autograd 函数 (
torch.autograd.Function的子类)
如果您在同一脚本中进行多次收敛测试,每次运行都应使用一个全新的 GradScaler 实例。GradScaler 实例非常轻量。
如果您要向调度器 (dispatcher) 注册自定义 C++ 算子,请参阅调度器教程中的 autocast 部分。
故障排除#
Amp 加速不明显#
您的网络可能未能让 GPU 工作负载饱和,从而受限于 CPU。在这种情况下,Amp 对 GPU 性能的影响微乎其微。
让 GPU 饱和的一个粗略准则是:在不触发内存溢出 (OOM) 的前提下,尽可能增加 batch size 和/或网络尺寸。
尽量避免过度的 CPU-GPU 同步(例如
.item()调用,或打印 CUDA 张量的值)。尽量避免连续执行许多微小的 CUDA 操作(如果可能,将它们合并为少数几个大型 CUDA 操作)。
您的网络可能受限于 GPU 计算能力(大量的矩阵乘法/卷积),但 GPU 不支持 Tensor Core。在这种情况下,预期加速会较小。
矩阵乘法的维度对 Tensor Core 不友好。确保参与矩阵乘法的尺寸是 8 的倍数。(对于带有编码器/解码器的 NLP 模型,这一点可能比较微妙。此外,卷积过去对于 Tensor Core 的使用也有类似的尺寸约束,但对于 CuDNN 7.3 及更高版本,已不存在此类限制。有关指导,请参见此处。)
损失为 inf/NaN#
首先,检查您的网络是否符合某个 进阶用例。另请参阅 优先使用 binary_cross_entropy_with_logits 而非 binary_cross_entropy。
如果您确定 Amp 的用法正确,可能需要提交 Issue。在提交之前,收集以下信息会很有帮助:
分别禁用
autocast或GradScaler(通过在其构造函数中传递enabled=False),查看infs/NaNs是否持续存在。如果您怀疑网络的某一部分(例如复杂的损失函数)发生溢出,请在该前向区域运行
float32,查看infs/NaNs是否依然存在。(autocast文档字符串的最后一个代码片段演示了如何强制子区域在float32下运行,通过局部禁用autocast并将子区域的输入转换为float32来实现)。
类型不匹配错误(可能表现为 CUDNN_STATUS_BAD_PARAM)#
Autocast 试图涵盖所有受益于或需要转换的算子。显式涵盖的算子是基于数值特性以及经验进行选择的。如果您在启用了 autocast 的前向区域或后续的反向传播中看到类型不匹配错误,可能是 autocast 遗漏了某个算子。
请提交带有错误回溯信息的 Issue。在运行脚本前执行 export TORCH_SHOW_CPP_STACKTRACES=1,以提供关于哪个后端算子失败的详细信息。