PyTorch 中多卡及混合精度使用方法
PyTorch 中多卡及混合精度使用方法
本文參考 maskrcnn-benchmark 和 MMAction2 (MMCV) 中的實現方式,大概是一年前寫的。在最近整理時還發現一篇 Distributed data parallel training in Pytorch,參考了一下,就如這篇裡面所述,即便現在 PyTorch 官網都沒有一個清晰的多卡訓練教程,只能自己看 code。
至於混合精度,之前 NVIDIA 出了 apex,在目前 PyTorch 1.6 中已經把 AMP (Automatic mixed precision) 整合進去了,可以很方便調用,本文只是簡述一下原理。
【2021 更新】最近看到 open-mmlab 寫了一系列文,關於 PyTorch 內部實現的,會更關注 PyTorch 的代碼實現:
多 GPU 和 Process 支持
目前譬如 Object Detection,在 inference 單卡一般可行,train 的話數據一多就會很慢。
原理
早期 PyTorch 中多 GPU 訓練的方式一般為使用 torch.nn.DataParallel()
(或 torch.multiprocessing
),只需 model = nn.DataParallel(model).cuda()
。Model 首先被加載到主 GPU 上,然後複製到其它 GPU 中(DataParallel,多線程)。輸入數據按 batch 維度進行劃分,每個 GPU 分配到的 batch 數量等於輸入總的 batch 除以 GPU 個數(不一定需要整除)。每個 GPU 對各自的輸入數據獨立進行 forward 計算,return 之前又被 DataParallel concat 在一起,因此後續計算 loss 等無需額外 code。loss/gradient 計算後,更新主 GPU 上的模型參數,DataParallel 再將更新後的模型參數複製到其餘 GPU 中,這樣就完成了一次迭代計算。
Mask R-CNN 的訓練使用 PyTorch 1.0 新加入的多進程方式,參考 GitHub/torch.distributed.launch 和 PyTorch/Docs。根據官方文檔中的這部分,這種方式有以下好處:
forward 計算之後不合併 batch,每個 GPU 上對自己的 batch 單獨計算 loss/gradient,但隨後梯度會在各個進程之間合併/計算總和(synchronize),而每個進程都有自己的 optimizer,獨立地進行參數更新,沒有了 parameters/batch tensor 在各 GPU 之間傳輸的時間。
每個 GPU 都有獨立的 Python interpreter,避免了 Python 在多線程時的問題,對於 Python 比 C++ 更影響運行時間的模型幫助很大(比如有很多小模塊組成的複雜模型)
可以用於多機 (node) 多 GPU 訓練
總結,這種方法會給每個 GPU 啟動一個獨立的進程,這個進程獨立地運行著所有的代碼,它們之間的通信可以只有 Gradient 的傳輸(當然比如為了 logging 的需要,有時候會寫一點傳輸 loss 值的代碼)。為了達到上面的目標:
首先,需要讓每個進程知道自己的
rank
和world_size
,也就是第幾個進程和總共有幾個進程。對應torch.distributed.launch
還需要一個
Data Sampler
,它拿到 rank 之後,給每個進程中的 model,在一個 unique (non-overlap) 的數據集的部分中 sample,使得每個進程處理自己的一部分數據。nn.utils.data.DistributedSampler
就是幹這事的還需要 Gradients 之間的平均,處理通信和所有 param 的平均值計算之類的,使得每次 backward 之後所有 GPU 上的梯度都是一個相同的平均值(即 all-reduce)。
nn.DistributedDataParallel
就是一個 model wrapper 負責這件事情最後,需要設定隨機數種子,不然 model 初始化的時候參數都不一樣,梯度平均沒意義
如果對 Data Sampler
不了解,應該看前一篇,鏈接在開頭。如果對 all-reduce
想了解更多,見下文。
使用
對於單機多 GPU,啟動方式為使用 torch.distributed.launch
,只需要 --nproc_per_node=$NGPUS
,後方跟訓練腳本位置和它的參數:
1 | export NGPUS=4 |
torch.distributed.launch
會在 os.environ
中添加 ["WORLD_SIZE"] = $NGPUS
並使用 subprocess.Popen()
啟動 NGPUS 次訓練腳本(並在參數中傳入不同的 Rank)並捕獲 return code。
在訓練腳本內,需要在開頭加上下面的代碼獲得 torch.distributed.launch
傳入的 local_rank
。
1 | # parser.add_argument ... |
init_process_group
註冊了一系列信息,包括如何找到 Process 0,其中同步的 synchronize()
主要是 dist.barrier()
使執行到這裡的進程等待,當所有進程都執行到這裡再繼續往下。
1 | import torch.distributed as dist |
init_process_group
(dist.is_initialized()
) 之後可以用 dist.get_world_size()
dist.get_rank()
等方法獲得需要信息,不需要依靠 args 了。
在實例化完 model 後使用
1 | # model = model.to(f'cuda:{local_rank}') |
對所有參數註冊一個 gradient reduction functions 用於求和梯度。這樣操作時候,每個 Process 獨立進行 forward,在 backward 的時候,gradient 會自動在所有 GPU 之間 all-reduce,且由於 backward 是單向依賴和進行的,所以這個通信和 backward 是同時進行的,進一步加快速度和減少帶寬壓力。
然後配置 DataLoader 的 Sampler,使不同進程的 model 獲得不同的 input batch。
1 | train_sampler = torch.utils.data.distributed.DistributedSampler( |
這一部分只適用與單機多卡 (single node multi gpu),多幾多卡似乎還要額外對 backward 和 optimizer 進行配置,然後加入 os.environ['MASTER_ADDR']
和 os.environ['MASTER_PORT']
設置 node 0 的 ip 和 port,slurm 啟動等,我沒用過,不知道具體。
注意事項
learning rate
每個 GPU 的 memory 是一定的,所以 batch size per GPU 的上限是一定的,如果在 NGPUS = 2
batch size = 2
訓練了模型得到一個精度結果,那麼想在 NGPUS = 1
的情況下使用相同 config 得到基本相同結果,需要進行改動:batch size = 2
,但是只有一個 GPU,那麼等同於總 batch size 除以了 2,所以總的 iteration 也需要 ×2,lr 也要減小 N 倍,因為 grad 小 N 倍。
reduce/gather
雖然 grad 被自動在所有 GPU 求和了,但是為了 log 的需要,一般還要將每個 process 中得到的 loss 傳輸到 process 0,求得平均後輸出。WRITING DISTRIBUTED APPLICATIONS WITH PYTORCH 中介紹了各種 Collective Communication。
https://zhuanlan.zhihu.com/p/100012827
logger/save checkpoint
參考 mmcv/logging,對於 dist.get_rank() != 0
的進程不添加 FileHandler,並設置 log_level=logging.ERROR
。from mmcv.utils import get_logger
-> get_logger('taskname', log_file)
。
混合精度訓練
According to Mixed Precision Training, the steps of fp16 optimizer is as follows.
- Prepare model,
.half()
all modules except batchnorm (groupnorm) - Scale the loss value by a scale factor and convert from fp32 to fp16.
- BP in the fp16 model.
- Copy gradients from fp16 model to fp32 weights.
- Update fp32 weights.
- Copy updated parameters from fp32 weights to fp16 model.
https://github.com/open-mmlab/mmcv/blob/master/mmcv/runner/hooks/optimizer.py
雖然 params 有兩份儲存了,但是最消耗 memory 的是對圖像求導的部分,因此仍然可以降低
PyTorch
參考 PyTorch 源码解读之 torch.cuda.amp: 自动混合精度详解。
Apex (Deprecated)
Apex 是 Nvidia 的 PyTorch 混合精度訓練工具,對於 FP16 安全的操作在訓練中會 Cast 到 FP16,反之則使用 FP32。
FP16 的優缺點可參考 Quora/What is the difference between FP16 and FP32 when doing deep learning?。
1 | from apex import amp |
在構建完 model 和 optimizer 之後,DistributedDataParallel(model)
之前,使用 amp.initialize
初始化
1 | model, optimizer = amp.initialize(model, optimizer, opt_level=amp_opt_level) |
opt_level
可參考官方文檔,大致 00
就是不啟用混合精度,工作在 FP32,01
則是啟用。
在 loss bp 前
1 | with amp.scale_loss(losses, optimizer) as scaled_losses: |
其它無需改變。