在這篇文章中,我將概述一些在PyTorch中加速深度學習模型訓練時改動最小,影響最大的方法。對於每種方法,我會簡要總結其思想,並估算預期的加速度,並討論一些限制。我將着重於傳達最重要的部分,並為每個部分給出額外的一些資源。大多數情況下,我會專注於可以直接在PyTorch中進行的更改,而不需要引入額外的庫,並且我將假設你正在使用GPU訓練模型。
注1:文末附【PyTorch】學習交流群
注2:整理不易,請點贊支持!
作者:LORENZ KUHN | 編譯:ronghuaiyang
原出處:AI公園
原文鏈接: PyTorch深度學習模型訓練加速指南2021
導讀
簡要介紹在PyTorch中加速深度學習模型訓練的一些最小改動、影響最大的方法。我既喜歡效率又喜歡ML,所以我想我也可以把它寫下來。
比如說,你正在PyTorch中訓練一個深度學習模型。你能做些什么讓你的訓練更快結束?
在這篇文章中,我將概述一些在PyTorch中加速深度學習模型訓練時改動最小,影響最大的方法。對於每種方法,我會簡要總結其思想,並估算預期的加速度,並討論一些限制。我將着重於傳達最重要的部分,並為每個部分給出額外的一些資源。大多數情況下,我會專注於可以直接在PyTorch中進行的更改,而不需要引入額外的庫,並且我將假設你正在使用GPU訓練模型。
1. 考慮使用另外一種學習率策略
你選擇的學習率對收斂速度以及模型的泛化性能有很大的影響。
循環學習率和1Cycle學習率策略都是Leslie N. Smith提出的方法,然后由fast.ai推廣。本質上,1Cycle學習率策略看起來像這樣:

Sylvain寫道:
[1cycle由兩個相同長度的步驟組成,一個是從較低的學習率到較高的學習率,另一個步驟是回到最低的學習速率。最大值應該是使用Learning Rate Finder選擇的值,較低的值可以低十倍。然后,這個周期的長度應該略小於epochs的總數,並且,在訓練的最后一部分,我們應該允許學習率減少超過最小值幾個數量級。
在最好的情況下,與傳統的學習率策略相比,這種策略可以實現巨大的加速 —— Smith稱之為“超級收斂”。例如,使用1Cycle策略,在ImageNet上減少了ResNet-56訓練迭代數的10倍,就可以匹配原始論文的性能。該策略似乎在通用架構和優化器之間運行得很好。
PyTorch實現了這兩個方法,torch.optim.lr_scheduler.CyclicLR和torch.optim.lr_scheduler.OneCycleLR。
這兩個策略的一個缺點是它們引入了許多額外的超參數。為什么會這樣呢?這似乎並不完全清楚,但一個可能的解釋是,定期提高學習率有助於更快的穿越鞍點。
2. 在 DataLoader中使用多個workers和pinned memory
當使用torch.utils.data.DataLoader時,設置num_workers > 0,而不是等於0,設置pin_memory=True而不是默認值False。詳細解釋:https://pytorch.org/docs/stable/data.html。
Szymon Micacz通過使用4個workers和pinned memory,實現了單個訓練epoch的2倍加速。
一個經驗法則,選擇workers的數量設置為可用GPU數量的4倍,更大或更小的workers數量會變慢。
注意,增加num_workers會增加CPU內存消耗。
3. 最大化batch size
這是一個頗有爭議的觀點。一般來說,然而,似乎使用GPU允許的最大的batch size可能會加速你的訓練。注意,如果你修改了batch大小,你還必須調整其他超參數,例如學習率。這里的一個經驗法則是,當你把batch數量翻倍時,學習率也要翻倍。
OpenAI有一篇很好的實證論文關於不同batch size需要的收斂步驟的數量。Daniel Huynh運行一些實驗用不同batch大小(使用上面所討論的1Cycle策略),從batch size 64到512他實現了4倍的加速。
然而,使用大batch的缺點之一是,它們可能會導致泛化能力比使用小batch的模型差。
4. 使用自動混合精度
PyTorch 1.6的發行版包含了對PyTorch進行自動混合精度訓練的本地實現。這里的主要思想是,與在所有地方都使用單精度(FP32)相比,某些操作可以在半精度(FP16)下運行得更快,而且不會損失精度。然后,AMP自動決定應該以何種格式執行何種操作。這允許更快的訓練和更小的內存占用。
AMP的使用看起來像這樣:
import torch
# Creates once at the beginning of training
scaler = torch.cuda.amp.GradScaler()
for data, label in data_iter:
optimizer.zero_grad()
# Casts operations to mixed precision
with torch.cuda.amp.autocast():
loss = model(data)
# Scales the loss, and calls backward()
# to create scaled gradients
scaler.scale(loss).backward()
# Unscales gradients and calls
# or skips optimizer.step()
scaler.step(optimizer)
# Updates the scale for next iteration
scaler.update()
在NVIDIA V100 GPU上對多個NLP和CV的benchmark進行測試,Huang和他的同事們發現使用AMP在FP32訓練收益率常規大約2x,但最高可達5.5x。
目前,只有CUDA ops可以通過這種方式自動轉換。
5. 考慮使用另外的優化器
AdamW是由fast.ai推廣的具有權重衰減(而不是L2正則化)的Adam。現在可以在PyTorch中直接使用,torch.optim.AdamW。無論在誤差還是訓練時間上,AdamW都比Adam表現更好。
Adam和AdamW都可以很好地使用上面描述的1Cycle策略。
還有一些自帶優化器最近受到了很多關注,最著名的是LARS和LAMB。
NVIDA的APEX實現了許多常見優化器的融合版本,如Adam。與Adam的PyTorch實現相比,這種實現避免了大量進出GPU內存的操作,從而使速度提高了5%。
6. 開啟cudNN benchmarking
如果你的模型架構保持不變,你的輸入大小保持不變,設置torch.backends.cudnn.benchmark = True可能是有益的。這使得cudNN能夠測試許多不同的卷積計算方法,然后使用最快的方法。
對於加速的預期有一個粗略的參考,Szymon Migacz達到70%的forward的加速以及27%的forward和backward的加速。
這里需要注意的是,如果你像上面提到的那樣將batch size最大化,那么這種自動調優可能會變得非常緩慢。
7. 注意CPU和GPU之間頻繁的數據傳輸
小心使用tensor.cpu()和tensor.cuda()頻繁地將張量從GPU和CPU之間相互轉換。對於.item()和.numpy()也是一樣,用.detach()代替。
如果你正在創建一個新的張量,你也可以使用關鍵字參數device=torch.device('cuda:0')直接將它分配給你的GPU。
如果你確實需要傳輸數據,在傳輸后使用.to(non_blocking=True)可能會很有用,只要你沒有任何同步點。
如果你真的需要,你可以試試Santosh Gupta的SpeedTorch,雖然不是很確定在什么情況下可以加速。
8. 使用gradient/activation檢查點
直接引用文檔中的話:
檢查點的工作原理是用計算交換內存,並不是存儲整個計算圖的所有中間激活用於向后計算,檢查點不保存中間的激活,而是在向后傳遞中重新計算它們。可以應用於模型的任何部分。
具體來說,在向前傳遞中,function會以torch.no_grad()的方式運行,也就是說,不存儲中間激活。相反,正向傳遞保存輸入和function的參數。在向后傳遞中,將檢索保存的輸入和function,並再次根據function計算向前傳遞,然后跟蹤中間的激活,再使用這些激活值計算梯度。
因此,雖然這可能會略微增加給定batch大小的運行時間,但會顯著減少內存占用。這反過來會允許你進一步增加你正在使用的batch大小,從而更好地利用GPU。
檢查點的pytorch實現為torch.utils.checkpoint,需要想點辦法才能實現的很好。
9. 使用梯度累加
增加batch大小的另一種方法是在調用optimizer.step()之前,在多個.backward()中累積梯度。
在Hugging Face的實現中,梯度累加可以實現如下:
model.zero_grad() # Reset gradients tensors
for i, (inputs, labels) in enumerate(training_set):
predictions = model(inputs) # Forward pass
loss = loss_function(predictions, labels) # Compute loss function
loss = loss / accumulation_steps # Normalize our loss (if averaged)
loss.backward() # Backward pass
if (i+1) % accumulation_steps == 0: # Wait for several backward steps
optimizer.step() # Now we can do an optimizer step
model.zero_grad() # Reset gradients tensors
if (i+1) % evaluation_steps == 0: # Evaluate the model when we...
evaluate_model() # ...have no gradients accumulated
這個方法主要是為了避開GPU內存限制。fastai論壇上的這個討論:https://forums.fast.ai/t/accumulating-gradients/33219/28似乎表明它實際上可以加速訓練,所以可能值得一試。
10. 對於多個GPU使用分布式數據並行
對於分布式訓練加速,一個簡單的方法是使用torch.nn.DistributedDataParallel而不是torch.nn.DataParallel。通過這樣做,每個GPU將由一個專用的CPU核心驅動,避免了DataParallel的GIL問題。
11. 將梯度設為None而不是0
使用.zero_grad(set_to_none=True)而不是.zero_grad()。這樣做會讓內存分配器去處理梯度,而不是主動將它們設置為0。正如在文檔中所說的那樣,這會導致產生一個適度的加速,所以不要期待任何奇跡。
注意,這樣做並不是沒有副作用的!關於這一點的詳細信息請查看文檔。
12. 使用.as_tensor() 而不是 .tensor()
torch.tensor() 會拷貝數據,如果你有一個numpy數組,你想轉為tensor,使用 torch.as_tensor() 或是 torch.from_numpy() 來避免拷貝數據。
13. 需要的時候打開調試工具
Pytorch提供了大量的有用的調試工具,如autograd.profiler,autograd.grad_check和autograd.anomaly_detection。在需要的時候使用它們,在不需要它們的時候關閉它們,因為它們會減慢你的訓練。
14. 使用梯度剪裁
最初是用於RNNs避免爆炸梯度,有一些經驗證據和一些理論支持認為剪裁梯度(粗略地說:gradient = min(gradient, threshold))可以加速收斂。Hugging Face的Transformer實現是關於如何使用梯度剪裁以及其他的一些方法如AMP的一個非常干凈的例子。
在PyTorch中,這可以通過使用torch.nn.utils.clip_grad_norm_實現。我並不完全清楚哪個模型從梯度裁剪中獲益多少,但它似乎對RNN、基於Transformer和ResNets架構以及一系列不同的優化器都非常有用。
15. 在BatchNorm之前不使用bias
這是一個非常簡單的方法:在BatchNormalization 層之前不使用bias。對於二維卷積層,可以將關鍵字bias設為False: torch.nn.Conv2d(..., bias=False, ...)。
你會保存一些參數,然而,與這里提到的其他一些方法相比,我對這個方法的加速期望相對較小。
16. 在驗證的時候關閉梯度計算
這個很直接:在驗證的時候使用 torch.no_grad() 。
17. 對輸入和batch使用歸一化
你可能已經這么做了,但你可能想再檢查一下:
- 你的輸入歸一化了嗎?
- 你是否在使用batch-normalization
來自評論的額外的技巧:使用 JIT融合point-wise的操作
如果你有point-wise的操作,你可以使用PyTorch JIT將它們合並成一個FusionGroup,這樣就可以在單個核上啟動,而不是像默認情況下那樣在多個核上啟動。你還可以節省一些內存的讀寫。
Szymon Migacz展示了如何使用@torch.jit腳本裝飾器來融合GELU中的操作,例如:
@torch.jit.script
def fused_gelu(x):
return x * 0.5 * (1.0 + torch.erf(x / 1.41421))
在本例中,與未融合的版本相比,融合操作將導致fused_gelu的執行速度提高5倍。
一些相關的資源
上面列出的許多技巧來自Szymon Migacz的談話,並發表在:https://pytorch.org/tutorials/recipes/recipes/tuning_guide.html。
PyTorch Lightning的William Falcon有兩篇文章:
https://towardsdatascience.com/7-tips-for-squeezing-maximum-performance-from-pytorch-ca4a40951259
其中有加速訓練的技巧。PyTorch Lightning已經處理了上面默認的一些點。
Hugging Face的Thomas Wolf有很多關於加速深度學習的有趣文章,其中特別關注語言模型。
Sylvain Gugger和Jeremy Howard也有一些文章:
關於學習率策略的:https://sgugger.github.io/the-1cycle-policy.html,
關於找最佳學習率的:https://sgugger.github.io/how-do-you-find-a-good-learning-rate.html
AdamW相關的:https://www.fast.ai/2018/07/02/adam-weight-decay/。
—END—
英文原文:https://efficientdl.com/faster-
導讀
簡要介紹在PyTorch中加速深度學習模型訓練的一些最小改動、影響最大的方法。我既喜歡效率又喜歡ML,所以我想我也可以把它寫下來。
比如說,你正在PyTorch中訓練一個深度學習模型。你能做些什么讓你的訓練更快結束?
在這篇文章中,我將概述一些在PyTorch中加速深度學習模型訓練時改動最小,影響最大的方法。對於每種方法,我會簡要總結其思想,並估算預期的加速度,並討論一些限制。我將着重於傳達最重要的部分,並為每個部分給出額外的一些資源。大多數情況下,我會專注於可以直接在PyTorch中進行的更改,而不需要引入額外的庫,並且我將假設你正在使用GPU訓練模型。
1. 考慮使用另外一種學習率策略
你選擇的學習率對收斂速度以及模型的泛化性能有很大的影響。
循環學習率和1Cycle學習率策略都是Leslie N. Smith提出的方法,然后由fast.ai推廣。本質上,1Cycle學習率策略看起來像這樣:

Sylvain寫道:
[1cycle由兩個相同長度的步驟組成,一個是從較低的學習率到較高的學習率,另一個步驟是回到最低的學習速率。最大值應該是使用Learning Rate Finder選擇的值,較低的值可以低十倍。然后,這個周期的長度應該略小於epochs的總數,並且,在訓練的最后一部分,我們應該允許學習率減少超過最小值幾個數量級。
在最好的情況下,與傳統的學習率策略相比,這種策略可以實現巨大的加速 —— Smith稱之為“超級收斂”。例如,使用1Cycle策略,在ImageNet上減少了ResNet-56訓練迭代數的10倍,就可以匹配原始論文的性能。該策略似乎在通用架構和優化器之間運行得很好。
PyTorch實現了這兩個方法,torch.optim.lr_scheduler.CyclicLR和torch.optim.lr_scheduler.OneCycleLR。
這兩個策略的一個缺點是它們引入了許多額外的超參數。為什么會這樣呢?這似乎並不完全清楚,但一個可能的解釋是,定期提高學習率有助於更快的穿越鞍點。
2. 在 DataLoader中使用多個workers和pinned memory
當使用torch.utils.data.DataLoader時,設置num_workers > 0,而不是等於0,設置pin_memory=True而不是默認值False。詳細解釋:https://pytorch.org/docs/stable/data.html。
Szymon Micacz通過使用4個workers和pinned memory,實現了單個訓練epoch的2倍加速。
一個經驗法則,選擇workers的數量設置為可用GPU數量的4倍,更大或更小的workers數量會變慢。
注意,增加num_workers會增加CPU內存消耗。
3. 最大化batch size
這是一個頗有爭議的觀點。一般來說,然而,似乎使用GPU允許的最大的batch size可能會加速你的訓練。注意,如果你修改了batch大小,你還必須調整其他超參數,例如學習率。這里的一個經驗法則是,當你把batch數量翻倍時,學習率也要翻倍。
OpenAI有一篇很好的實證論文關於不同batch size需要的收斂步驟的數量。Daniel Huynh運行一些實驗用不同batch大小(使用上面所討論的1Cycle策略),從batch size 64到512他實現了4倍的加速。
然而,使用大batch的缺點之一是,它們可能會導致泛化能力比使用小batch的模型差。
4. 使用自動混合精度
PyTorch 1.6的發行版包含了對PyTorch進行自動混合精度訓練的本地實現。這里的主要思想是,與在所有地方都使用單精度(FP32)相比,某些操作可以在半精度(FP16)下運行得更快,而且不會損失精度。然后,AMP自動決定應該以何種格式執行何種操作。這允許更快的訓練和更小的內存占用。
AMP的使用看起來像這樣:
import torch
# Creates once at the beginning of training
scaler = torch.cuda.amp.GradScaler()
for data, label in data_iter:
optimizer.zero_grad()
# Casts operations to mixed precision
with torch.cuda.amp.autocast():
loss = model(data)
# Scales the loss, and calls backward()
# to create scaled gradients
scaler.scale(loss).backward()
# Unscales gradients and calls
# or skips optimizer.step()
scaler.step(optimizer)
# Updates the scale for next iteration
scaler.update()
在NVIDIA V100 GPU上對多個NLP和CV的benchmark進行測試,Huang和他的同事們發現使用AMP在FP32訓練收益率常規大約2x,但最高可達5.5x。
目前,只有CUDA ops可以通過這種方式自動轉換。
5. 考慮使用另外的優化器
AdamW是由fast.ai推廣的具有權重衰減(而不是L2正則化)的Adam。現在可以在PyTorch中直接使用,torch.optim.AdamW。無論在誤差還是訓練時間上,AdamW都比Adam表現更好。
Adam和AdamW都可以很好地使用上面描述的1Cycle策略。
還有一些自帶優化器最近受到了很多關注,最著名的是LARS和LAMB。
NVIDA的APEX實現了許多常見優化器的融合版本,如Adam。與Adam的PyTorch實現相比,這種實現避免了大量進出GPU內存的操作,從而使速度提高了5%。
6. 開啟cudNN benchmarking
如果你的模型架構保持不變,你的輸入大小保持不變,設置torch.backends.cudnn.benchmark = True可能是有益的。這使得cudNN能夠測試許多不同的卷積計算方法,然后使用最快的方法。
對於加速的預期有一個粗略的參考,Szymon Migacz達到70%的forward的加速以及27%的forward和backward的加速。
這里需要注意的是,如果你像上面提到的那樣將batch size最大化,那么這種自動調優可能會變得非常緩慢。
7. 注意CPU和GPU之間頻繁的數據傳輸
小心使用tensor.cpu()和tensor.cuda()頻繁地將張量從GPU和CPU之間相互轉換。對於.item()和.numpy()也是一樣,用.detach()代替。
如果你正在創建一個新的張量,你也可以使用關鍵字參數device=torch.device('cuda:0')直接將它分配給你的GPU。
如果你確實需要傳輸數據,在傳輸后使用.to(non_blocking=True)可能會很有用,只要你沒有任何同步點。
如果你真的需要,你可以試試Santosh Gupta的SpeedTorch,雖然不是很確定在什么情況下可以加速。
8. 使用gradient/activation檢查點
直接引用文檔中的話:
檢查點的工作原理是用計算交換內存,並不是存儲整個計算圖的所有中間激活用於向后計算,檢查點不保存中間的激活,而是在向后傳遞中重新計算它們。可以應用於模型的任何部分。
具體來說,在向前傳遞中,
function會以torch.no_grad()的方式運行,也就是說,不存儲中間激活。相反,正向傳遞保存輸入和function的參數。在向后傳遞中,將檢索保存的輸入和function,並再次根據function計算向前傳遞,然后跟蹤中間的激活,再使用這些激活值計算梯度。
因此,雖然這可能會略微增加給定batch大小的運行時間,但會顯著減少內存占用。這反過來會允許你進一步增加你正在使用的batch大小,從而更好地利用GPU。
檢查點的pytorch實現為torch.utils.checkpoint,需要想點辦法才能實現的很好。
9. 使用梯度累加
增加batch大小的另一種方法是在調用optimizer.step()之前,在多個.backward()中累積梯度。
在Hugging Face的實現中,梯度累加可以實現如下:
model.zero_grad() # Reset gradients tensors
for i, (inputs, labels) in enumerate(training_set):
predictions = model(inputs) # Forward pass
loss = loss_function(predictions, labels) # Compute loss function
loss = loss / accumulation_steps # Normalize our loss (if averaged)
loss.backward() # Backward pass
if (i+1) % accumulation_steps == 0: # Wait for several backward steps
optimizer.step() # Now we can do an optimizer step
model.zero_grad() # Reset gradients tensors
if (i+1) % evaluation_steps == 0: # Evaluate the model when we...
evaluate_model() # ...have no gradients accumulated
這個方法主要是為了避開GPU內存限制。fastai論壇上的這個討論:https://forums.fast.ai/t/accumulating-gradients/33219/28似乎表明它實際上可以加速訓練,所以可能值得一試。
10. 對於多個GPU使用分布式數據並行
對於分布式訓練加速,一個簡單的方法是使用torch.nn.DistributedDataParallel而不是torch.nn.DataParallel。通過這樣做,每個GPU將由一個專用的CPU核心驅動,避免了DataParallel的GIL問題。
11. 將梯度設為None而不是0
使用.zero_grad(set_to_none=True)而不是.zero_grad()。這樣做會讓內存分配器去處理梯度,而不是主動將它們設置為0。正如在文檔中所說的那樣,這會導致產生一個適度的加速,所以不要期待任何奇跡。
注意,這樣做並不是沒有副作用的!關於這一點的詳細信息請查看文檔。
12. 使用.as_tensor() 而不是 .tensor()
torch.tensor() 會拷貝數據,如果你有一個numpy數組,你想轉為tensor,使用 torch.as_tensor() 或是 torch.from_numpy() 來避免拷貝數據。
13. 需要的時候打開調試工具
Pytorch提供了大量的有用的調試工具,如autograd.profiler,autograd.grad_check和autograd.anomaly_detection。在需要的時候使用它們,在不需要它們的時候關閉它們,因為它們會減慢你的訓練。
14. 使用梯度剪裁
最初是用於RNNs避免爆炸梯度,有一些經驗證據和一些理論支持認為剪裁梯度(粗略地說:gradient = min(gradient, threshold))可以加速收斂。Hugging Face的Transformer實現是關於如何使用梯度剪裁以及其他的一些方法如AMP的一個非常干凈的例子。
在PyTorch中,這可以通過使用torch.nn.utils.clip_grad_norm_實現。我並不完全清楚哪個模型從梯度裁剪中獲益多少,但它似乎對RNN、基於Transformer和ResNets架構以及一系列不同的優化器都非常有用。
15. 在BatchNorm之前不使用bias
這是一個非常簡單的方法:在BatchNormalization 層之前不使用bias。對於二維卷積層,可以將關鍵字bias設為False: torch.nn.Conv2d(..., bias=False, ...)。
你會保存一些參數,然而,與這里提到的其他一些方法相比,我對這個方法的加速期望相對較小。
16. 在驗證的時候關閉梯度計算
這個很直接:在驗證的時候使用 torch.no_grad() 。
17. 對輸入和batch使用歸一化
你可能已經這么做了,但你可能想再檢查一下:
- 你的輸入歸一化了嗎?
- 你是否在使用batch-normalization
來自評論的額外的技巧:使用 JIT融合point-wise的操作
如果你有point-wise的操作,你可以使用PyTorch JIT將它們合並成一個FusionGroup,這樣就可以在單個核上啟動,而不是像默認情況下那樣在多個核上啟動。你還可以節省一些內存的讀寫。
Szymon Migacz展示了如何使用@torch.jit腳本裝飾器來融合GELU中的操作,例如:
@torch.jit.script
def fused_gelu(x):
return x * 0.5 * (1.0 + torch.erf(x / 1.41421))
在本例中,與未融合的版本相比,融合操作將導致fused_gelu的執行速度提高5倍。
一些相關的資源
上面列出的許多技巧來自Szymon Migacz的談話,並發表在:https://pytorch.org/tutorials/recipes/recipes/tuning_guide.html。
PyTorch Lightning的William Falcon有兩篇文章:
https://towardsdatascience.com/9-tips-for-training-lightning-fast-neural-networks-in-pytorch-8e63a502f565
https://towardsdatascience.com/7-tips-for-squeezing-maximum-performance-from-pytorch-ca4a40951259
其中有加速訓練的技巧。PyTorch Lightning已經處理了上面默認的一些點。
Hugging Face的Thomas Wolf有很多關於加速深度學習的有趣文章,其中特別關注語言模型。
Sylvain Gugger和Jeremy Howard也有一些文章:
關於學習率策略的:https://sgugger.github.io/the-1cycle-policy.html,
關於找最佳學習率的:https://sgugger.github.io/how-do-you-find-a-good-learning-rate.html
AdamW相關的:https://www.fast.ai/2018/07/02/adam-weight-decay/。
—END—
英文原文:https://efficientdl.com/faster-deep-learning-in-pytorch-a-guide/
