7.3 GPU使用
深度學習之所以可以發展迅猛,得益於強大的計算力。在PyTorch中,自然加持GPU加速運算,本節將介紹PyTorch中GPU的使用原理與多GPU使用的DataParallel原理,還有一些針對GPU的實用程式碼片段。
gpu與cpu
在處理器家族中,有兩大陣營,分別是CPU和GPU,它們分工協作,共同完成電腦複雜功能。
但它們兩者主要差別在哪裡?下麵一探究竟。
CPU(central processing unit, 中央處理器)cpu主要包括兩個部分,即控制器、運算器,除此之外還包括快取記憶體等
GPU(Graphics Processing Unit, 圖形處理器)是為處理類型統一並且相互無依賴的大規模資料運算,以及不需要被打斷的純淨的計算環境為設計的處理器,因早期僅有圖形圖像任務中設計大規模統一無依賴的運算,因此該處理器稱為影像處理器,俗稱顯卡。
那麼它們之間主要區別在哪裡呢,來看一張示意圖
綠色的是計算單元,橙紅色的是存儲單元,橙黃色的是控制單元,從示意圖中看出,gpu的重點在計算,cpu的重點在控制,這就是兩者之間的主要差異。
在pytorch中,可以將訓練資料以及模型參數遷移到gpu上,這樣就可以加速模型的運算
在這裡,需要瞭解的是,在pytorch中,兩個資料運算必須在同一個設備上。
PyTorch的設備——torch.device
前面提到,PyTorch的運算需要將運算資料放到同一個設備上,因此,需要瞭解PyTorch中設備有哪些?
目前,PyTorch支援兩種設備,cpu與cuda,為什麼是cuda而不是gpu?因為早期,只有Nvidia的GPU能用於模型訓練加速,因此稱之為cuda。
即使現在支援了AMD顯卡進行加速,仍舊使用cuda來代替gpu。
PyTorch中表示設備通常用torch.device)這個函數進行設置,例如:
>>> torch.device('cuda:0')
device(type='cuda', index=0)
>>> torch.device('cpu')
device(type='cpu')
>>> torch.device('cuda') # current cuda device
device(type='cuda')
Copy
補充資料:
把資料放到GPU——to函數
在pytorch中,只需要將要進行運算的資料放到gpu上,即可使用gpu加速運算
在模型運算過程中,需要放到GPU的主要是兩個:
- 輸入資料——形式為tensor
- 網路模型——形式為module
pytorch中針對這兩種資料都有相應的函數把它們放到gpu上,我們來認識一下這個函數,就是to函數
tensor的to函數: to(*args, \kwargs) → Tensor**
功能:轉換張量的資料類型或者設備
注意事項:to函數不是inplace操作,所以需要重新賦值,這與module的to函數不同
使用:
- 轉換資料類型
x = torch.ones((3, 3))
x = x.to(torch.float64)
- 轉換設備
x = torch.ones((3, 3))
x = x.to("cuda")
module的to函數:to(*args, \kwargs)**
功能:move and/or cast the parameters and buffers,轉換模型中的參數和緩存
注意事項:實行的是inplace操作
使用:
- 轉換資料類型
linear = nn.Linear(2, 2)
print(linear.weight)
linear.to(torch.double)
print(linear.weight)
- 遷移至gpu
gpu1 = torch.device("cuda:1")
linear.to(gpu1)
print(linear.weight)
將torch.device 與 to函數聯合使用,就是第六章混淆矩陣代碼中使用過的方式
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
inputs, labels = inputs.to(device), labels.to(device)
Copy
通常,會採用torch.cuda.is_available()函數來自適應當前設備,若沒有gpu可用,自動設置device為cpu,不會影響代碼的運行。
除了torch.cuda.is_available(),torch庫中還有一些關於cuda的實用函數,下面一起看看。
torch.cuda
在torch.cuda中有幾十個關於guda的函數,詳細請查閱官方文檔)
下面介紹幾個常用的函數。
- torch.cuda.device_count(): 查看可用GPU數量
- torch.cuda.current_device():查看當前使用的設備的序號
- torch.cuda.get_device_name():獲取設備的名稱
- torch.cuda.get_device_capability(device=None):查看設備的計算力
- torch.cuda.is_available():查看cuda是否可用
- torch.cuda.get_device_properties():查看GPU屬性
- torch.cuda.set_device(device):設置可用設備,已不推薦使用,建議通過CUDA_VISIBLE_DEVICES來設置,下文會講解CUDA_VISIBLE_DEVICES的使用。
- torch.cuda.mem_get_info(device=None):查詢gpu空餘顯存以及總顯存。
- torch.cuda.memory_summary(device=None, abbreviated=False):類似模型的summary,它將GPU的詳細資訊進行輸出。
- torch.cuda.empty_cache():清空緩存,釋放顯存碎片。
- torch.backends.cudnn.benchmark = True : 提升運行效率,僅適用於輸入資料較固定的,如卷積
會讓程式在開始時花費一點額外時間,為整個網路的每個卷積層搜索最適合它的卷積實現演算法,進而實現網路的加速讓內置的 cuDNN 的 auto-tuner 自動尋找最適合當前配置的高效演算法,來達到優化運行效率的問題
- torch.backends.cudnn.deterministic: 用以保證實驗的可重複性.
由於cnDNN 每次都會去尋找一遍最優配置,會產生隨機性,為了模型可複現,可設置torch.backends.cudnn.deterministic = True
運行代碼,可看到如下資訊:
device_count: 1
current_device: 0
(8, 6)
NVIDIA GeForce RTX 3060 Laptop GPU
True
['sm_37', 'sm_50', 'sm_60', 'sm_61', 'sm_70', 'sm_75', 'sm_80', 'sm_86', 'compute_37']
_CudaDeviceProperties(name='NVIDIA GeForce RTX 3060 Laptop GPU', major=8, minor=6, total_memory=6144MB, multi_processor_count=30)
(5407899648, 6442450944)
|===========================================================================|
| PyTorch CUDA memory summary, device ID 0 |
|---------------------------------------------------------------------------|
| CUDA OOMs: 0 | cudaMalloc retries: 0 |
|===========================================================================|
| Metric | Cur Usage | Peak Usage | Tot Alloc | Tot Freed |
|---------------------------------------------------------------------------|
| Allocated memory | 0 B | 0 B | 0 B | 0 B |
| from large pool | 0 B | 0 B | 0 B | 0 B |
| from small pool | 0 B | 0 B | 0 B | 0 B |
|---------------------------------------------------------------------------|
| Active memory | 0 B | 0 B | 0 B | 0 B |
| from large pool | 0 B | 0 B | 0 B | 0 B |
| from small pool | 0 B | 0 B | 0 B | 0 B |
|---------------------------------------------------------------------------|
| GPU reserved memory | 0 B | 0 B | 0 B | 0 B |
| from large pool | 0 B | 0 B | 0 B | 0 B |
| from small pool | 0 B | 0 B | 0 B | 0 B |
|---------------------------------------------------------------------------|
| Non-releasable memory | 0 B | 0 B | 0 B | 0 B |
| from large pool | 0 B | 0 B | 0 B | 0 B |
| from small pool | 0 B | 0 B | 0 B | 0 B |
|---------------------------------------------------------------------------|
| Allocations | 0 | 0 | 0 | 0 |
| from large pool | 0 | 0 | 0 | 0 |
| from small pool | 0 | 0 | 0 | 0 |
|---------------------------------------------------------------------------|
| Active allocs | 0 | 0 | 0 | 0 |
| from large pool | 0 | 0 | 0 | 0 |
| from small pool | 0 | 0 | 0 | 0 |
|---------------------------------------------------------------------------|
| GPU reserved segments | 0 | 0 | 0 | 0 |
| from large pool | 0 | 0 | 0 | 0 |
| from small pool | 0 | 0 | 0 | 0 |
|---------------------------------------------------------------------------|
| Non-releasable allocs | 0 | 0 | 0 | 0 |
| from large pool | 0 | 0 | 0 | 0 |
| from small pool | 0 | 0 | 0 | 0 |
|---------------------------------------------------------------------------|
| Oversize allocations | 0 | 0 | 0 | 0 |
|---------------------------------------------------------------------------|
| Oversize GPU segments | 0 | 0 | 0 | 0 |
|===========================================================================|
None
Copy
多gpu訓練——nn.DataParallel
人多力量大的道理在PyTorch的訓練中也是適用的,PyTorch支持多個GPU共同訓練,加快訓練速度。
多GPU可分為單機多卡和多機多卡,這裡僅介紹單機多卡的方式。
單機多卡的實現非常簡單,只需要增加一行代碼:
net = nn.DataParallel(net)
代碼的意思是將一個nn.Module變為一個特殊的nn.Module,這個Module的forward函數實現多GPU調用。
如果對nn.Module的概念以及forward函數不理解的話,請回到第四章進行學習。
首先看一幅示意圖,理解多GPU是如何工作的
整體有四個步驟:
- 資料平均劃為N份
- 模型參數複製N份
- 在N個GPU上同時運算
- 回收N個GPU上的運算結果
瞭解了多gpu運行機制,下面看看DataParallel是如何實現的。
torch.nn.DataParallel(module, device_ids=None, output_device=None, dim=0)
功能:實現模型的資料並行運算
主要參數:
module - 需要並行的module
device_ids: (list of python:int or torch.device) – CUDA devices (default: all devices), eg: [2, 3]
默認採用所有可見gpu,這裡強調了可見gpu,就是說可以設置部分gpu對當前python腳本不可見,這個可以通過系統環境變數設置
output_device: int or torch.device , 設置輸出結果所在設備,預設為 device_ids[0],通常以第1個邏輯gpu為主gpu
原始程式碼分析:
DataParallel仍舊是一個nn.Module類,所以首要關注它的forward函數。
來到/torch/nn/parallel/data_parallel.py的147行:
def forward(self, *inputs, **kwargs):
with torch.autograd.profiler.record_function("DataParallel.forward"):
if not self.device_ids:
return self.module(*inputs, **kwargs)
for t in chain(self.module.parameters(), self.module.buffers()):
if t.device != self.src_device_obj:
raise RuntimeError("module must have its parameters and buffers "
"on device {} (device_ids[0]) but found one of "
"them on device: {}".format(self.src_device_obj, t.device))
inputs, kwargs = self.scatter(inputs, kwargs, self.device_ids)
# for forward function without any inputs, empty list and dict will be created
# so the module can be executed on one device which is the first one in device_ids
if not inputs and not kwargs:
inputs = ((),)
kwargs = ({},)
if len(self.device_ids) == 1:
return self.module(*inputs[0], **kwargs[0])
replicas = self.replicate(self.module, self.device_ids[:len(inputs)])
outputs = self.parallel_apply(replicas, inputs, kwargs)
return self.gather(outputs, self.output_device)
Copy
核心點有4行代碼,分別是:
inputs, kwargs = self.scatter(inputs, kwargs, self.device_ids)
replicas = self.replicate(self.module, self.device_ids[:len(inputs)])
outputs = self.parallel_apply(replicas, inputs, kwargs)
return self.gather(outputs, self.output_device)
Copy
一、數據切分
inputs, kwargs = self.scatter(inputs, kwargs, self.device_ids):利用scatter函數,將資料切分為多塊,為各GPU需要的資料做準備。
scatter函數在torch\nn\parallel\scatter_gather.py第11行。
def scatter(inputs, target_gpus, dim=0):
r"""
Slices tensors into approximately equal chunks and
distributes them across given GPUs. Duplicates
references to objects that are not tensors.
"""
Copy
二、模型分發至GPU
replicas = self.replicate(self.module, self.device_ids[:len(inputs)]):利用replicate函數將模型複製N份,用於多GPU上。
replicate函數在 torch\nn\parallel\replicate.py第78行。
三、執行並行推理
outputs = self.parallel_apply(replicas, inputs, kwargs):多GPU同時進行運算。
四、結果回收
return self.gather(outputs, self.output_device)
為了理解分發過程,請使用具備多GPU的環境,運行配套代碼,可看到如下資訊:
batch size in forward: 4
batch size in forward: 4
batch size in forward: 4
batch size in forward: 4
model outputs.size: torch.Size([16, 3])
CUDA_VISIBLE_DEVICES :0,1,3,2
device_count :4
batchsize設置為16,將16個樣本平均分發給4個GPU,因此在forward函數當中,看到的資料是4個樣本。
多GPU訓練模型的保存與載入
當模型變為了Dataparallel時,其參數名稱會多一個module.欄位,這導致在保存的時候state_dict也會多了module.欄位。
從而,在載入的時候經常出現以下報錯。
RuntimeError: Error(s) in loading state_dict for FooNet:
Missing key(s) in state_dict: "linears.0.weight", "linears.1.weight", "linears.2.weight".
Unexpected key(s) in state_dict: "module.linears.0.weight", "module.linears.1.weight", "module.linears.2.weight".
解決方法是,移除key中的module.:
from collections import OrderedDict
new_state_dict = OrderedDict()
for k, v in state_dict_load.items():
namekey = k[7:] if k.startswith('module.') else k
new_state_dict[namekey] = v
Copy
請結合代碼運行,觀察其使用,並看到如下結果:
state_dict_load:
OrderedDict([('module.linears.0.weight', tensor([[ 0.3337, 0.0317, -0.1331],
[ 0.0431, 0.0454, 0.1235],
[ 0.0575, -0.2903, -0.2634]])), ('module.linears.1.weight', tensor([[ 0.1235, 0.1520, -0.1611],
[ 0.4511, -0.1460, -0.1098],
[ 0.0653, -0.5025, -0.1693]])), ('module.linears.2.weight', tensor([[ 0.3657, -0.1107, -0.2341],
[ 0.0657, -0.0194, -0.3119],
[-0.0477, -0.1008, 0.2462]]))])
new_state_dict:
OrderedDict([('linears.0.weight', tensor([[ 0.3337, 0.0317, -0.1331],
[ 0.0431, 0.0454, 0.1235],
[ 0.0575, -0.2903, -0.2634]])), ('linears.1.weight', tensor([[ 0.1235, 0.1520, -0.1611],
[ 0.4511, -0.1460, -0.1098],
[ 0.0653, -0.5025, -0.1693]])), ('linears.2.weight', tensor([[ 0.3657, -0.1107, -0.2341],
[ 0.0657, -0.0194, -0.3119],
[-0.0477, -0.1008, 0.2462]]))])
Process finished with exit code 0
Copy
使用指定編號的gpu
通常,一台伺服器上有多個用戶,或者是會進行多個任務,此時,對gpu合理的安排使用就尤為重要
在實踐中,通常會設置當前python腳本可見的gpu,然後直接使用nn.dataparallel使用所有gpu即可,不需要手動去設置使用哪些gpu
設置python腳本可見gpu的方法為設置系統環境變數中的CUDA_VISIBLE_DEVICES
設置方法為:
os.environ.setdefault("CUDA_VISIBLE_DEVICES", "2, 3")
Copy
當時之後,即物理設備的2,3號GPU,在程式中分別是0號、1號GPU,這裡需要理解邏輯編號與物理編號的對應關係。
注意事項: CUDA_VISIBLE_DEVICES的設置一定要在 device = torch.device("cuda" if torch.cuda.is_available() else "cpu") 之前!
否則已經調用cuda,python腳本已經獲取當前可見gpu了,再設置就無效了
留言列表