自動混合精度範例¶
一般來說,「自動混合精度訓練」指的是同時使用 torch.autocast
和 torch.amp.GradScaler
進行訓練。
torch.autocast
的實例能夠為選定的區域啟用自動轉換 (autocasting)。自動轉換會自動選擇運算的精度,以在保持準確度的同時提高效能。
torch.amp.GradScaler
的實例有助於方便地執行梯度縮放的步驟。梯度縮放通過最小化梯度下溢來改善具有 float16
(預設在 CUDA 和 XPU 上) 梯度的網路的收斂性,如 此處 所述。
torch.autocast
和 torch.amp.GradScaler
是模組化的。在下面的範例中,每一個都按照其各自的文件建議使用。
(此處的範例僅為說明。有關可執行的逐步指南,請參閱 自動混合精度配方。)
典型的混合精度訓練¶
# Creates model and optimizer in default precision
model = Net().cuda()
optimizer = optim.SGD(model.parameters(), ...)
# Creates a GradScaler once at the beginning of training.
scaler = GradScaler()
for epoch in epochs:
for input, target in data:
optimizer.zero_grad()
# Runs the forward pass with autocasting.
with autocast(device_type='cuda', dtype=torch.float16):
output = model(input)
loss = loss_fn(output, target)
# Scales loss. Calls backward() on scaled loss to create scaled gradients.
# Backward passes under autocast are not recommended.
# Backward ops run in the same dtype autocast chose for corresponding forward ops.
scaler.scale(loss).backward()
# scaler.step() first unscales the gradients of the optimizer's assigned params.
# If these gradients do not contain infs or NaNs, optimizer.step() is then called,
# otherwise, optimizer.step() is skipped.
scaler.step(optimizer)
# Updates the scale for next iteration.
scaler.update()
處理未縮放的梯度¶
由 scaler.scale(loss).backward()
產生的所有梯度都會被縮放。如果您希望在 backward()
和 scaler.step(optimizer)
之間修改或檢查參數的 .grad
屬性,您應該首先取消縮放它們。例如,梯度裁剪會操作一組梯度,使其全域範數 (參見 torch.nn.utils.clip_grad_norm_()
) 或最大幅度 (參見 torch.nn.utils.clip_grad_value_()
) 為 某個使用者設定的閾值。如果您嘗試在 *不* 取消縮放的情況下進行裁剪,則梯度的範數/最大幅度也會被縮放,因此您請求的閾值 (該閾值本應是 *未縮放* 梯度的閾值) 將無效。
scaler.unscale_(optimizer)
會取消縮放由 optimizer
的指定參數所持有的梯度。如果您的模型包含已分配給另一個優化器 (例如 optimizer2
) 的其他參數,您可以分別調用 scaler.unscale_(optimizer2)
來取消縮放這些參數的梯度。
梯度裁剪¶
在裁剪之前調用 scaler.unscale_(optimizer)
使您可以像往常一樣裁剪未縮放的梯度
scaler = GradScaler()
for epoch in epochs:
for input, target in data:
optimizer.zero_grad()
with autocast(device_type='cuda', dtype=torch.float16):
output = model(input)
loss = loss_fn(output, target)
scaler.scale(loss).backward()
# Unscales the gradients of optimizer's assigned params in-place
scaler.unscale_(optimizer)
# Since the gradients of optimizer's assigned params are unscaled, clips as usual:
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm)
# optimizer's gradients are already unscaled, so scaler.step does not unscale them,
# although it still skips optimizer.step() if the gradients contain infs or NaNs.
scaler.step(optimizer)
# Updates the scale for next iteration.
scaler.update()
scaler
記錄了此迭代已為此優化器調用 scaler.unscale_(optimizer)
,因此 scaler.step(optimizer)
知道在 (內部) 調用 optimizer.step()
之前不要多餘地取消縮放梯度。
警告
unscale_
應該每個 step
調用只對每個優化器調用一次,並且僅在已累積該優化器指定參數的所有梯度後才調用。在每個 step
之間針對給定的優化器調用 unscale_
兩次會觸發 RuntimeError。
處理縮放的梯度¶
梯度累積¶
梯度累積會對大小為 batch_per_iter * iters_to_accumulate
的有效批次添加梯度 (如果是分佈式,則為 * num_procs
)。應針對有效批次校準縮放,這意味著 inf/NaN 檢查、如果找到 inf/NaN 梯度則跳過步進,並且縮放更新應以有效批次粒度進行。此外,在累積給定有效批次的梯度時,梯度應保持縮放,並且縮放因子應保持恆定。如果在累積完成之前取消縮放梯度 (或縮放因子發生變化),則下一次反向傳播將把縮放的梯度添加到未縮放的梯度 (或按不同因子縮放的梯度),之後就無法恢復累積的未縮放梯度 step
必須應用。
因此,如果您想 unscale_
梯度(例如,允許裁剪未縮放的梯度),請在 step
之前,也就是在即將到來的 step
的所有(已縮放)梯度都已累積之後,再呼叫 unscale_
。此外,只有在您呼叫 step
以獲得完整有效批次的迭代結束時,才呼叫 update
。
scaler = GradScaler()
for epoch in epochs:
for i, (input, target) in enumerate(data):
with autocast(device_type='cuda', dtype=torch.float16):
output = model(input)
loss = loss_fn(output, target)
loss = loss / iters_to_accumulate
# Accumulates scaled gradients.
scaler.scale(loss).backward()
if (i + 1) % iters_to_accumulate == 0:
# may unscale_ here if desired (e.g., to allow clipping unscaled gradients)
scaler.step(optimizer)
scaler.update()
optimizer.zero_grad()
梯度懲罰¶
梯度懲罰的實作通常使用 torch.autograd.grad()
建立梯度,將它們組合起來以建立懲罰值,並將懲罰值加到損失中。
這是一個沒有梯度縮放或自動轉換 (autocasting) 的 L2 懲罰的普通範例。
for epoch in epochs:
for input, target in data:
optimizer.zero_grad()
output = model(input)
loss = loss_fn(output, target)
# Creates gradients
grad_params = torch.autograd.grad(outputs=loss,
inputs=model.parameters(),
create_graph=True)
# Computes the penalty term and adds it to the loss
grad_norm = 0
for grad in grad_params:
grad_norm += grad.pow(2).sum()
grad_norm = grad_norm.sqrt()
loss = loss + grad_norm
loss.backward()
# clip gradients here, if desired
optimizer.step()
要實作帶有梯度縮放的梯度懲罰,傳遞給 torch.autograd.grad()
的 outputs
Tensor 應該要經過縮放。因此,產生的梯度將會是縮放後的,並且在組合以建立懲罰值之前應該先 unscale。
此外,懲罰項的計算是前向傳播的一部分,因此應該在 autocast
上下文中進行。
以下是相同 L2 懲罰的樣子。
scaler = GradScaler()
for epoch in epochs:
for input, target in data:
optimizer.zero_grad()
with autocast(device_type='cuda', dtype=torch.float16):
output = model(input)
loss = loss_fn(output, target)
# Scales the loss for autograd.grad's backward pass, producing scaled_grad_params
scaled_grad_params = torch.autograd.grad(outputs=scaler.scale(loss),
inputs=model.parameters(),
create_graph=True)
# Creates unscaled grad_params before computing the penalty. scaled_grad_params are
# not owned by any optimizer, so ordinary division is used instead of scaler.unscale_:
inv_scale = 1./scaler.get_scale()
grad_params = [p * inv_scale for p in scaled_grad_params]
# Computes the penalty term and adds it to the loss
with autocast(device_type='cuda', dtype=torch.float16):
grad_norm = 0
for grad in grad_params:
grad_norm += grad.pow(2).sum()
grad_norm = grad_norm.sqrt()
loss = loss + grad_norm
# Applies scaling to the backward call as usual.
# Accumulates leaf gradients that are correctly scaled.
scaler.scale(loss).backward()
# may unscale_ here if desired (e.g., to allow clipping unscaled gradients)
# step() and update() proceed as usual.
scaler.step(optimizer)
scaler.update()
使用多個模型、損失和優化器¶
如果您的網路有多個損失,您必須個別在每個損失上呼叫 scaler.scale
。如果您的網路有多個優化器,您可以個別在任何一個優化器上呼叫 scaler.unscale_
,並且您必須個別在每個優化器上呼叫 scaler.step
。
但是,scaler.update
應該只被呼叫一次,在所有在此次迭代中使用的優化器都被 step 之後。
scaler = torch.amp.GradScaler()
for epoch in epochs:
for input, target in data:
optimizer0.zero_grad()
optimizer1.zero_grad()
with autocast(device_type='cuda', dtype=torch.float16):
output0 = model0(input)
output1 = model1(input)
loss0 = loss_fn(2 * output0 + 3 * output1, target)
loss1 = loss_fn(3 * output0 - 5 * output1, target)
# (retain_graph here is unrelated to amp, it's present because in this
# example, both backward() calls share some sections of graph.)
scaler.scale(loss0).backward(retain_graph=True)
scaler.scale(loss1).backward()
# You can choose which optimizers receive explicit unscaling, if you
# want to inspect or modify the gradients of the params they own.
scaler.unscale_(optimizer0)
scaler.step(optimizer0)
scaler.step(optimizer1)
scaler.update()
每個優化器都會檢查其梯度中是否存在 infs/NaNs,並獨立決定是否跳過 step。這可能導致一個優化器跳過 step,而另一個優化器不跳過。由於 step 跳過很少發生(每幾百次迭代一次),這不應該妨礙收斂。如果您在將梯度縮放添加到多優化器模型後觀察到收斂不良,請報告錯誤。
使用多個 GPU¶
此處描述的問題僅影響 autocast
。GradScaler
的使用方式不變。
單一程序中的 DataParallel¶
即使 torch.nn.DataParallel
產生執行緒以在每個裝置上運行前向傳播。autocast 狀態會在每個執行緒中傳播,因此以下方式可以運作:
model = MyModel()
dp_model = nn.DataParallel(model)
# Sets autocast in the main thread
with autocast(device_type='cuda', dtype=torch.float16):
# dp_model's internal threads will autocast.
output = dp_model(input)
# loss_fn also autocast
loss = loss_fn(output)
DistributedDataParallel,每個程序一個 GPU¶
torch.nn.parallel.DistributedDataParallel
的文檔建議每個程序使用一個 GPU 以獲得最佳性能。在這種情況下,DistributedDataParallel
不會在內部產生執行緒,因此 autocast
和 GradScaler
的使用不受影響。
DistributedDataParallel,每個程序多個 GPU¶
在這裡,torch.nn.parallel.DistributedDataParallel
可能會產生一個側執行緒以在每個裝置上運行前向傳播,就像 torch.nn.DataParallel
一樣。修復方法相同:將 autocast 應用於模型的 forward
方法中,以確保它在側執行緒中啟用。
Autocast 和自定義 Autograd 函數¶
如果您的網路使用自定義 autograd 函數(torch.autograd.Function
的子類別),如果任何函數符合以下條件,則需要進行更改以實現 autocast 兼容性:
採用多個浮點 Tensor 輸入,
包裝任何可自動轉換 (autocastable) 的操作(請參閱Autocast Op Reference),或
需要特定的
dtype
(例如,如果它包裝了僅針對dtype
編譯的 CUDA 擴展)。
在所有情況下,如果您正在導入函數並且無法更改其定義,一個安全的後備方法是在發生錯誤的任何使用點禁用 autocast 並強制以 float32
(或 dtype
)執行。
with autocast(device_type='cuda', dtype=torch.float16):
...
with autocast(device_type='cuda', dtype=torch.float16, enabled=False):
output = imported_function(input1.float(), input2.float())
如果您是函式的作者(或可以更改其定義),一個更好的解決方案是使用 torch.amp.custom_fwd()
和 torch.amp.custom_bwd()
修飾器,如下面相關的例子所示。
具有多個輸入或可自動轉換運算的函式¶
將 custom_fwd
和 custom_bwd
(不帶參數) 分別應用到 forward
和 backward
。 這些確保 forward
在目前的自動轉換狀態下執行,且 backward
在與 forward
相同的自動轉換狀態下執行 (可以防止類型不符的錯誤)
class MyMM(torch.autograd.Function):
@staticmethod
@custom_fwd
def forward(ctx, a, b):
ctx.save_for_backward(a, b)
return a.mm(b)
@staticmethod
@custom_bwd
def backward(ctx, grad):
a, b = ctx.saved_tensors
return grad.mm(b.t()), a.t().mm(grad)
現在可以在任何地方調用 MyMM
,而無需禁用自動轉換或手動轉換輸入。
mymm = MyMM.apply
with autocast(device_type='cuda', dtype=torch.float16):
output = mymm(input1, input2)
需要特定 dtype
的函式¶
考慮一個需要 torch.float32
輸入的自定義函式。 將 custom_fwd(device_type='cuda', cast_inputs=torch.float32)
應用到 forward
,並將 custom_bwd(device_type='cuda')
應用到 backward
。 如果 forward
在啟用自動轉換的區域中執行,則修飾器會在參數 device_type 指定的設備上將浮點 Tensor 輸入轉換為 float32
,本例中為 CUDA,並在 forward
和 backward
期間局部禁用自動轉換。
class MyFloat32Func(torch.autograd.Function):
@staticmethod
@custom_fwd(device_type='cuda', cast_inputs=torch.float32)
def forward(ctx, input):
ctx.save_for_backward(input)
...
return fwd_output
@staticmethod
@custom_bwd(device_type='cuda')
def backward(ctx, grad):
...
現在可以在任何地方調用 MyFloat32Func
,而無需手動禁用自動轉換或轉換輸入。
func = MyFloat32Func.apply
with autocast(device_type='cuda', dtype=torch.float16):
# func will run in float32, regardless of the surrounding autocast state
output = func(input)