自動混合精度範例¶
通常,「自動混合精度訓練」表示使用 torch.autocast 和 torch.amp.GradScaler 一起進行訓練。
torch.autocast 的實例會為選定的區域啟用自動轉換。自動轉換會自動選擇 GPU 作業的精度,以在維持精度的同時提升效能。
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() 之前不需要重複取消縮放梯度。
警告
每個 step 呼叫每個優化器只能呼叫 unscale_ 一次,並且只能在累積了該優化器所分配的所有參數的梯度之後才能呼叫。如果在每個 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() 建立梯度,將它們組合起來建立懲罰值,並將懲罰值加到損失中。
以下是不使用梯度縮放或自動轉換的 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 張量應該縮放。因此,產生的梯度將被縮放,並且應該在組合起來建立懲罰值之前取消縮放。
此外,懲罰項計算是正向傳遞的一部分,因此應該在 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 應該只呼叫一次,在所有用於此迭代的優化器都已執行步驟之後
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()
每個優化器都會檢查其梯度是否存在 inf/NaN,並獨立決定是否跳過該步驟。這可能會導致一個優化器跳過該步驟,而另一個優化器沒有跳過。由於跳過步驟的情況很少發生(每數百次迭代),因此這不應妨礙收斂。如果您在將梯度縮放添加到多優化器模型後觀察到收斂不良,請報告錯誤。
使用多個 GPU¶
這裡描述的問題只會影響 autocast。GradScaler 的用法不變。
單一程序中的 DataParallel¶
即使 torch.nn.DataParallel 產生執行緒以在每個裝置上執行正向傳遞。自動轉換狀態會在每個裝置中傳播,以下操作將會正常運作
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 一樣產生一個側邊執行緒,以便在每個裝置上執行正向傳遞。修復方法相同:將自動轉換應用為模型 forward 方法的一部分,以確保它在側邊執行緒中啟用。
Autocast 和自訂 Autograd 函數¶
如果您的網路使用 自訂 Autograd 函數(torch.autograd.Function 的子類別),如果任何函數
- 採用多個浮點張量輸入, 
- 包裝任何可自動轉換的操作(請參閱 Autocast 操作參考),或 
- 需要特定的 - dtype(例如,如果它包裝僅針對- dtype編譯的 CUDA 擴充)。
在所有情況下,如果您要匯入函數且無法更改其定義,則安全的後備方法是在發生錯誤的任何使用點停用自動轉換並強制以 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 指定的指定裝置上的 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)