PyTorch 源碼解讀之 torch.cuda.amp: 自動混合精度詳解

2021-03-02 機器學習算法工程師

點藍色字關注「機器學習算法工程師

設為星標,乾貨直達!

AI編輯:我是小將

本文作者:OpenMMLab @202011

https://zhuanlan.zhihu.com/p/348554267

本文已由原作者授權轉載

Nvidia 在 Volta 架構中引入 Tensor Core 單元,來支持 FP32 和 FP16 混合精度計算。也在 2018 年提出一個 PyTorch 拓展 apex,來支持模型參數自動混合精度訓練。自動混合精度(Automatic Mixed Precision, AMP)訓練,是在訓練一個數值精度 FP32 的模型,一部分算子的操作時,數值精度為 FP16,其餘算子的操作精度是 FP32,而具體哪些算子用 FP16,哪些用 FP32,不需要用戶關心,amp 自動給它們都安排好了。這樣在不改變模型、不降低模型訓練精度的前提下,可以縮短訓練時間,降低存儲需求,因而能支持更多的 batch size、更大模型和尺寸更大的輸入進行訓練。PyTorch 從 1.6 以後(在此之前 OpenMMLab 已經支持混合精度訓練,即 Fp16OptimizerHook),開始原生支持 amp,即torch.cuda.amp module。2020 ECCV,英偉達官方做了一個 tutorial 推廣 amp。從官方各種文檔網頁 claim 的結果來看,amp 在分類、檢測、圖像生成、3D CNNs、LSTM,以及 NLP 中機器翻譯、語義識別等應用中,都在沒有降低模型訓練精度都前提下,加速了模型的訓練速度。

本文是對torch.cuda.amp工作機制,和 module 中接口使用方法介紹,以及在算法角度上對 amp 不掉點原因進行分析,最後補充一點對 amp 存儲消耗的解釋。

1. 混合精度訓練機制

torch.cuda.amp 給用戶提供了較為方便的混合精度訓練機制,「方便」體現在兩個方面:

以上兩點,分別是通過使用amp.autocast和amp.GradScaler來實現的。

autocast可以作為 Python 上下文管理器和裝飾器來使用,用來指定腳本中某個區域、或者某些函數,按照自動混合精度來運行。混合精度在操作的時候,是先將 FP32 的模型的參數拷貝一份,拷貝的參數轉換成 FP16,而 amp 規定了的 FP16 的算子(例如卷積、全連接),對 FP16 的數值進行操作;FP32 的算子(例如涉及 reduction 的算子,BatchNormalize,softmax...),輸入和輸出是 FP16,計算的精度是 FP32。在反向傳播時,依然是混合精度計算,得到數值精度為 FP16 的梯度。最後,由於 GPU 中的 Tensor Core 天然支持 FP16 乘積的結果與 FP32 的累加(Tensor Core math),優化器的操作是利用 FP16 的梯度對 FP32 的參數進行更新。

對於 FP16 不可避免的問題就是:表示的範圍較窄,如下圖所示,大量非 0 梯度會遇到溢出問題。解決辦法是:對梯度乘一個  的係數,稱為 scale factor,把梯度 shift 到 FP16 的表示範圍。

GradScaler的工作就是在反向傳播前給 loss 乘一個 scale factor,所以之後反向傳播得到的梯度都乘了相同的 scale factor。並且為了不影響學習率,在梯度更新前將梯度unscale。總結amp的基本訓練流程:

維護一個 FP32 數值精度模型的副本

在每個iteration

拷貝並且轉換成 FP16 模型

前向傳播(FP16 的模型參數)

loss 乘 scale factor s

反向傳播(FP16 的模型參數和參數梯度)

參數梯度乘 1/s

利用 FP16 的梯度更新 FP32 的模型參數

但是,這裡會有一個問題,scale factor 應該如何選取?選一個常量顯然是不合適的,因為 loss 和梯度的數值在變,scale factor 需要跟隨 loss 動態變化。健康的 loss 是振蕩中下降,因此GradScaler設計的 scale factor 每隔  個 iteration 乘一個大於 1 的係數,再 scale loss;並且每次更新前檢查溢出問題(檢查梯度中有沒有inf和nan),如果有,scale factor 乘一個小於 1 的係數並跳過該 iteration 的參數更新環節,如果沒有,就正常更新參數。動態更新 scale factor 是 amp 實際操作中的流程。總結 amp 動態 scale factor 的訓練流程:

維護一個 FP32 數值精度模型的副本

初始化 s

在每個 iteration + a 拷貝並且轉換成FP16模型 + b 前向傳播(FP16 的模型參數) + c loss 乘 scale factor s + d 反向傳播(FP16 的模型參數和參數梯度) + e 檢查有沒有inf或者nan的參數梯度 + 如果有:降低 s,回到步驟a + f 參數梯度乘 1/s + g 利用 FP16 的梯度更新 FP32 的模型參數

2. amp模塊的API

用戶使用混合精度訓練基本操作:

# amp依賴Tensor core架構,所以model參數必須是cuda tensor類型
model = Net().cuda()
optimizer = optim.SGD(model.parameters(), ...)
# GradScaler對象用來自動做梯度縮放
scaler = GradScaler()

for epoch in epochs:
for input, target in data:
optimizer.zero_grad()
# 在autocast enable 區域運行forward
with autocast():
# model做一個FP16的副本,forward
output = model(input)
loss = loss_fn(output, target)
# 用scaler,scale loss(FP16),backward得到scaled的梯度(FP16)
scaler.scale(loss).backward()
# scaler 更新參數,會先自動unscale梯度
# 如果有nan或inf,自動跳過
scaler.step(optimizer)
# scaler factor更新
scaler.update()

2.1 autocast類

autocast(enable=True)`` 可以作為上下文管理器和裝飾器來使用,給算子自動安排按照 FP16 或者 FP32 的數值精度來操作。

2.1.1 autocast算子

PyTorch中,只有 CUDA 算子有資格被 autocast,而且只有 「out-of-place」 才可以被 autocast,例如:a.addmm(b, c)是可以被 autocast,但是a.addmm_(b, c)和a.addmm(b, c, out=d)不可以 autocast。amp autocast 成 FP16 的算子有:

autocast 成 FP32 的算子:

剩下沒有列出的算子,像dot,add,cat...都是按數據中較大的數值精度,進行操作,即有 FP32 參與計算,就按 FP32,全是 FP16 參與計算,就是 FP16。

2.1.2 MisMatch error

作為上下文管理器使用時,混合精度計算 enable 區域得到的 FP16 數值精度的變量在 enable 區域外需要顯式的轉成 FP32:

# Creates some tensors in default dtype (here assumed to be float32)
a_float32 = torch.rand((8, 8), device="cuda")
b_float32 = torch.rand((8, 8), device="cuda")
c_float32 = torch.rand((8, 8), device="cuda")
d_float32 = torch.rand((8, 8), device="cuda")

with autocast():
# torch.mm is on autocast's list of ops that should run in float16.
e_float16 = torch.mm(a_float32, b_float32)
# Also handles mixed input types
f_float16 = torch.mm(d_float32, e_float16)

# After exiting autocast, calls f_float16.float() to use with d_float32
g_float32 = torch.mm(d_float32, f_float16.float())

2.1.3 autocast 嵌套使用

# Creates some tensors in default dtype (here assumed to be float32)
a_float32 = torch.rand((8, 8), device="cuda")
b_float32 = torch.rand((8, 8), device="cuda")
c_float32 = torch.rand((8, 8), device="cuda")
d_float32 = torch.rand((8, 8), device="cuda")

with autocast():
e_float16 = torch.mm(a_float32, b_float32)

with autocast(enabled=False):

f_float32 = torch.mm(c_float32, e_float16.float())

g_float16 = torch.mm(d_float32, f_float32)

2.1.4 autocast 作為裝飾器

這種情況一般用於 data parallel 的模型的,autocast 設計為 「thread local」 的,所以只在 main thread 上設 autocast 區域是不 work 的:

model = MyModel()
dp_model = nn.DataParallel(model)

with autocast(): # dp_model's internal threads won't autocast.
#The main thread's autocast state has no effect.
output = dp_model(input) # loss_fn still autocasts, but it's too late...
loss = loss_fn(output)

正確姿勢是對 forward 裝飾:

MyModel(nn.Module):
...
@autocast()
def forward(self, input):
...

另一個正確姿勢是在 forward 的裡面設 autocast 區域:

MyModel(nn.Module):
...
def forward(self, input):
with autocast():
...

forward 函數處理之後,在 main thread 裡 autocast

model = MyModel()
dp_model = nn.DataParallel(model)

with autocast():
output = dp_model(input)
loss = loss_fn(output)

2.1.5 autocast 自定義函數

對於用戶自定義的 autograd 函數,需要用amp.custom_fwd裝飾 forward 函數,amp.custom_bwd裝飾 backward 函數:

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)

調用時再 autocast

mymm = MyMM.apply

with autocast():
output = mymm(input1, input2)

2.1.6 源碼分析

autocast主要實現接口有:

A. __enter__

def __enter__(self):
self.prev = torch.is_autocast_enabled()
torch.set_autocast_enabled(self._enabled)
torch.autocast_increment_nesting()

B. __exit__

def __exit__(self, *args):

if torch.autocast_decrement_nesting() == 0:
torch.clear_autocast_cache()
torch.set_autocast_enabled(self.prev)
return False

C. __call__

def __call__(self, func):
@functools.wraps(func)
def decorate_autocast(*args, **kwargs):
with self:
return func(*args, **kwargs)
return decorate_autocast

其中torch.*autocast*函數是在 pytorch/aten/src/ATen/autocast_mode.cpp 裡實現。PyTorch ATen 是 A TENsor library for C++11,ATen 部分有大量的代碼是來聲明和定義 Tensor 運算相關的邏輯的。autocast_mode.cpp 實現策略是 「 cache fp16 casts of fp32 model weights」。

2.2 GradScaler 類

torch.cuda.amp.GradScaler(init_scale=65536.0, growth_factor=2.0, backoff_factor=0.5, growth_interval=2000, enabled=True)用於動態 scale 梯度

+. init_scale: scale factor 的初始值 +. growth_factor: 每次 scale factor 的增長係數 +. backoff_factor: scale factor 下降係數 +. growth_interval: 每隔多個 interval 增長 scale factor +. enabled: 是否做 scale

2.2.1 scale(output)方法

對outputs乘 scale factor,並返回,如果enabled=False就原樣返回。

2.2.3 step(optimizer, *args, **kwargs)方法

step 方法在做兩件事情:

注意:GradScaler的step不支持傳 closure。

2.2.4 update(new_scale=None)方法

update方法在每個 iteration 結束前都需要調用,如果參數更新跳過,會給 scale factor 乘backoff_factor,或者到了該增長的 iteration,就給 scale factor 乘growth_factor。也可以用new_scale直接更新 scale factor。

2.3 舉例2.3.1 Gradient clipping

scaler = GradScaler()

for epoch in epochs:
for input, target in data:
optimizer.zero_grad()
with autocast():
output = model(input)
loss = loss_fn(output, target)
scaler.scale(loss).backward()

# unscale 梯度,可以不影響clip的threshold
scaler.unscale_(optimizer)

# clip梯度
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm)

# unscale_()已經被顯式調用了,scaler正常執行step更新參數,有nan/inf也會跳過
scaler.step(optimizer)
scaler.update()

2.3.2 Gradient accumulation

scaler = GradScaler()

for epoch in epochs:
for i, (input, target) in enumerate(data):
with autocast():
output = model(input)
loss = loss_fn(output, target)
# loss 根據 累加的次數歸一一下
loss = loss / iters_to_accumulate

# scale 歸一的loss 並backward
scaler.scale(loss).backward()

if (i + 1) % iters_to_accumulate == 0:
# may unscale_ here if desired
# (e.g., to allow clipping unscaled gradients)

# step() and update() proceed as usual.
scaler.step(optimizer)
scaler.update()
optimizer.zero_grad()

2.3.3. Gradient penalty

scaler = GradScaler()

for epoch in epochs:
for input, target in data:
optimizer.zero_grad()
with autocast():
output = model(input)
loss = loss_fn(output, target)
# 防止溢出,在不是autocast 區域,先用scaled loss 得到 scaled 梯度
scaled_grad_params = torch.autograd.grad(outputs=scaler.scale(loss),
inputs=model.parameters(),
create_graph=True)
# 梯度unscale
inv_scale = 1./scaler.get_scale()
grad_params = [p * inv_scale for p in scaled_grad_params]
# 在autocast 區域,loss 加上梯度懲罰項
with autocast():
grad_norm = 0
for grad in grad_params:
grad_norm += grad.pow(2).sum()
grad_norm = grad_norm.sqrt()
loss = loss + grad_norm

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()

2.3.4. Multiple models

scaler 一個就夠,但 scale(loss) 和 step(optimizer) 要分別執行

scaler = torch.cuda.amp.GradScaler()

for epoch in epochs:
for input, target in data:
optimizer0.zero_grad()
optimizer1.zero_grad()
with autocast():
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()

2.3.5. Multiple GPUs

torch DDP 和 torch DP model 的處理方式一樣

Q1. amp 是如何做到 FP16 和 FP32 混合使用,「還不掉點」

模型量化、模型壓縮的算法挺多的,但都做不 amp 這樣,對多數模型訓練不掉點(但是實操中,聽有經驗的大神介紹,完全不到點還是有點難度的)。amp 能做成這樣,因為它對模型沒有壓縮和量化,維護的還是一個 32 位的模型。只是用 16 位去表示原來 32 位的梯度:通常模型訓練依賴 FP32 的精度,因為梯度會有一部分 FP16 表示不了,而 scale factor 把梯度 shift 到 FP16 能表示範圍,使得梯度方面精度的損失較小,可能 forward 時候的直接的精度壓縮是訓練最大的損失。

Q2. 沒有 Tensor Core 架構能否使用 amp

沒有 Tensor Core 架構的 GPU 試用 amp,速度反而下降,但顯存會明顯減少。作者在 Turing 架構的 GTX 1660 上試用 amp,運算時間增加了一倍,但顯存不到原來的一半。

Q3. 為什麼 amp 中有兩份參數,存儲消耗反而更小

相比與模型參數,對中間層結果的存儲更是 deep learning 的 bottleneck。當對中間結果的存儲砍半,整個存儲消耗就基本上原來的一半。

·················END·················

相關焦點

  • PyTorch1.6:新增自動混合精度訓練、Windows版開發維護權移交微軟
    機器之心報導機器之心編輯部剛剛,Facebook 通過 PyTorch 官方博客宣布:PyTorch 1.6 正式發布!新版本增加了一個 amp 子模塊,支持本地自動混合精度訓練。一些亮點包括:在英偉達的幫助下增加了對自動混合精度(AMP)訓練的本地支持,並且具有穩定的功能;增加了對 TensorPipe 的本地支持;在前端 API 增加了對複雜張量的支持;提供張量級內存消耗信息的新分析工具;
  • 使用AMP和Tensor Cores得到更快速,更節省內存的PyTorch模型
    這篇文章的主要內容是關於如何利用Tensor Cores和自動混合精度更快地訓練深度學習網絡。什麼是Tensor Cores?()   optimizer.step()為了充分利用自動混合精度訓練的優勢,我們首先需要安裝apex庫。
  • PyTorch 源碼解讀之 torch.autograd
    前言本篇筆記以介紹 pytorch 中的 autograd 模塊功能為主,主要涉及 torch/autograd 下代碼,不涉及底層的 C++ 實現。本文涉及的源碼以 PyTorch 1.7 為準。這裡的反常現象,是由於機器精度帶來的誤差所致:test_input的類型為torch.float32,因此在 eps 過小的情況下,產生了較大的精度誤差(計算數值梯度時,eps 作為被除數),因而與真實精度間產生了較大的 gap。將test_input換為float64的 tensor 後,不再出現這一現象。
  • Pytorch中的Distributed Data Parallel與混合精度訓練(Apex)
    為了展示如何做到這些,這裡有一個在MNIST上訓練的例子,並且之後把它修改為可以在多節點多GPU上運行,最終修改的版本還可以支持混合精度運算。import torchimport torch.nn as nnimport torch.distributed as distfrom apex.parallel import DistributedDataParallel as DDPfrom apex import amp之後,我們訓練了一個MNIST分類的簡單卷積網絡
  • 當代研究生應當掌握的5種Pytorch並行訓練方法(單機多卡)
    在 API 層面,pytorch 為我們提供了 torch.distributed.launch 啟動器,用於在命令行分布式地執行 python 文件。繞開 torch.distributed.launch 自動控制開啟和退出進程的一些小毛病~使用時,只需要調用 torch.multiprocessing.spawn,torch.multiprocessing 就會幫助我們自動創建進程。
  • PyTorch 1.6 發布:原生支持自動混合精度訓練並進入穩定階段 - OS...
    部分更新亮點包括: 原生支持自動混合精度訓練(automatic mixed-precision training),並已進入穩定階段 為 tensor-aware 增加對 TensorPipe 的原生支持 在前端 API 增加了對 complex tensor 的支持 新的分析工具提供了張量級的內存消耗信息 針對分布式數據並行訓練和遠程過程調用的多項改進和新功能此外,從該版本起,新功能的狀態將分為三種
  • 提升PyTorch訓練速度,小哥哥總結了17種方法!
    Pytorch 已經實現了這兩種方法:「torch.optim.lr_scheduler.CyclicLR」和「torch.optim.lr_scheduler.OneCycleLR」。參考文檔:https://pytorch.org/docs/stable/optim.html2.
  • 這可能是關於Pytorch底層算子擴展最詳細的總結了!
    只需要定義新算子的kernel實現,然後添加配置信息,就可以自動生成:torch.xxx()、torch.nn.functional.xxx()以及tensor.xxx()方法,而不用去關注算子與pytorch是如何銜接,以及如何把算子添加到tensor的屬性中等其他細節。
  • 9個讓PyTorch模型訓練提速的技巧
    我們會講到:使用DataLoadersDataLoader中的workers數量Batch size梯度累計保留的計算圖移動到單個16-bit 混合精度訓練DataLoaders 中的 workers 的數量另一個加速的神奇之處是允許批量並行加載。因此,您可以一次裝載nb_workers個batch,而不是一次裝載一個batch。
  • 【PyTorch】torch.nn.Module 源碼分析
    源碼分析torch.nn.Module 這個類的內部有多達 48 個函數,這個類是 PyTorch 中所有 neural network module 的基類,自己創建的網絡模型都是這個類的子類,下邊是一個示例。
  • pytorch常見的坑匯總
    ——————————————————————————好像扯遠了,回歸pytorch,首先讓我比較尷尬的是pytorch並沒有一套屬於自己的數據結構以及數據讀取算法,dataloader個人感覺其實就是類似於tf中的feed,並沒有任何速度以及性能上的提升。先總結一下遇到的坑:1.
  • onnx實現對pytorch模型推理加速
    2.torch.load:使用pickle unpickle工具將pickle的對象文件反序列化為內存。# 第一種:保存和加載整個模型Save:torch.save(model_object, 'model.pth')Load:model = torch.load('model.pth')model.eval()#第二種:僅保存和加載模型參數(推薦使用)Save:torch.save(model.state_dict
  • 深度學習框架搭建之PyTorch
    深度學習框架搭建之PyTorchPyTorch 簡介PyTorch 是由 Facebook 推出,目前一款發展與流行勢頭非常強勁的深度學習框架。/zhanghang1989/PyTorch-Multi-Style-Transfer圖像風格轉移 CycleGAN:http://github.com/junvanz/pytorch-CycleGAN-and-pix2pix自動圖像描述  Image Captioning:https://github.com/ruotianluo/imageCaptioning.pytorch
  • 【Pytorch】PyTorch的4分鐘教程,手把手教你完成線性回歸
    下文出現的所有功能函數,均可以在中文文檔中查看具體參數和實現細節,先附上pytorch中文文檔連結:https://pytorch-cn.readthedocs.io/zh/latest/package_references/torch/
  • 深度學習大講堂之pytorch入門
    今天小天就帶大家從數據操作、自動求梯度和神經網絡設計的pytorch版本三個方面來入門pytorch。`` objects to move tensors in and out of GPUif torch.cuda.is_available(): device = torch.device("cuda") # a CUDA device object y = torch.ones_like(x, device=device) # directly create a
  • PyTorch 源碼解讀之 BN & SyncBN
    num_batches_trackedPyTorch 0.4 後新加入,當 momentum 設置為 None 時,使用 num_batches_tracked 計算每一輪更新的動量affine默認為 True,訓練 weight 和 bias;否則不更新它們的值weight公式中的 \gamma,初始化為全 1 tensorbias公式中的 \beta,初始化為全 0 tensor這裡貼一下 PyTorch 的源碼
  • 從零開始搭建深度學習伺服器:TensorFlow + PyTorch + Torch
    PyTorch:首先在PyTorch的官網下載對應的pip安裝文件:然後用virtualenv的方式安裝,非常方便:mkdir pytorchcd pytorch/virtualenv venvsource venv/bin/activatepip install /path/to
  • 教程 | 從頭開始了解PyTorch的簡單實現
    pytorch_tensor = torch.Tensor(10, 20)print("type: ", type(pytorch_tensor), " and size: ", pytorch_tensor.shape )如果你需要一個兼容 NumPy 的表徵,或者你想從現有的 NumPy 對象中創建一個 PyTorch 張量
  • PyTorch常見的12坑
    () 和 Tensor.cuda() 的作用效果差異無論是對於模型還是數據,cuda()函數都能實現從CPU到GPU的內存遷移,但是他們的作用效果有所不同。對於Tensor:和nn.Module不同,調用tensor.cuda()只是返回這個tensor對象在GPU內存上的拷貝,而不會對自身進行改變。因此必須對tensor進行重新賦值,即tensor=tensor.cuda().例子:
  • 重磅| Torch7團隊開源PyTorch:Python優先的深度學習框架
    選自PyTorch.org機器之心編譯參與:吳攀、李澤南、李亞洲Torch7 團隊開源了 PyTorch。據官網介紹,PyTorch 是一個 Python 優先的深度學習框架,能夠在強大的 GPU 加速基礎上實現張量和動態神經網絡。