CUDA Semantics
文章大部分內容來自PyTorch-CUDA Semantics,如若侵權請聯繫本人進行刪除,本文僅用作個人學習之用,不做他用,轉載請註明出處。
基本介紹
pytorch提供torch.cuda
來創建和運行CUDA操作。torch.cuda
會一直跟蹤當前選中的GPU,所有分配的CUDA tensors都會默認在該GPU上創建分配內存空間。但是,仍然可以通過torch.cuda.device
來改變選中的GPU(即去選擇另外一個GPU)。
一旦一個tensor被分配了空間,則可以操作這個tensor,而不需要管選中了哪個GPU(根據我的理解是:這個Tensor創建的時候已經指定了在哪個GPU上創建,所以之後並不需要再關心)。在這個tensor上的相關計算後的結果仍會保存在該tensor所在的GPU的存儲空間中。
默認情況下,跨GPU的運算操作是不被允許的,除了copy_()
、to()
、cuda()
等涉及存儲拷貝的函數。除非啓用了點對點存儲訪問(peer-to-peer memory access),否則任何嘗試在不同的GPU上執行相關計算的操作都會引發錯誤。
代碼示例:
cuda = torch.device('cuda') # 默認的CUDA設備
cuda0 = torch.device('cuda:0')
cuda2 = torch.device('cuda:2') # GPU2
x = torch.tensor([1.,2.],device=cuda0) # 在cuda0上創建一個tensor x
y = torch.tensor([1.,2.]).cuda()
with torch.cuda.device(1):
a = torch.tensor([1.,2.],device=cuda) # 在GPU1上分配一個tensor
b = torch.tensor([1.,2.]).cuda() # 將tensor從CPU傳輸到GPU1
b2 = torch.tensor([1.,2.]).to(device=cuda) # 將tensor從CPU傳輸到GPU1
c = a + b
z = x + y
# 在GPU2上做tensor相關的操作
d = torch.randn(2,device=cuda2)
e = torch.randn(2).to(cuda2)
f = torch.randn(2).cuda(cuda2)
異步執行(Asynchronous execution)
默認情況下,GPU的操作是異步的。在使用GPU的情況下調用某個函數(執行某個功能),操作會排隊進入某個特定的GPU設備,但是不會等到以後執行(可能不是按進入隊列的順序來順序執行)。因爲GPU的並行計算的特性,這些操作可以在GPU中並行執行。
通常來說,異步計算的過程對於調用者是不可見的,原因有兩點:第一、每一個GPU設備以操作序列進入設備的順序來執行這個操作序列;第二、當數據在CPU和GPU之間或者GPU之間進行拷貝時,PyTorch會自動執行必要的同步操作。於是,計算過程的外在表現就是彷彿每一個操作都在同步執行。
使用者可以通過設置環境變量CUDA_LAUNCH_BLOCKING=1
來強制使用同步計算。當一個error
在GPU上發生時,可以手動設置來排錯。
異步計算的一個後果就是當沒有進行時間同步時,時間的測量(耗時測量)就會不準確。爲了得到精確的時間測量,要麼在測量前調用torch.cuda.synchronize()
,要麼使用torch.cuda.Event
來記錄時間,詳細代碼示例如下:
# 使用torch.cuda.Event的方式
start_event = torch.cuda.Event(enable_timing=True)
end_event = torch.cuda.Event(enable_time=True)
start_event.record()
# DO SOMETHING...
end_event.record()
torch.cuda.synchronize() # 等待事件被記錄
elapsed_time_ms = start_event.elapsed_time(end_time)
作爲一個例外(As an exception),一些形如to()
和copy()
的函數允許顯示的non_blocking
參數,這樣可以使得調用者避開(bypass)不必要時刻的同步操作。另外的例外情況就是CUDA streams
。
CUDA流(CUDA streams)
一個CUDA stream
是一個相信執行序列,該序列屬於特定的GPU設備。通常不需要顯示創建,因爲每一個GPU設備默認情況下會創建一個自己的stream
。
stream
的操作序列以特們被創建的順序在stream
中序列化好,但是來自不同streams
的操作序列可以以一個相對順序併發執行,除非顯示調用了synchronize()
或者wait_stream()
等同步函數的情況下不能併發執行。以下舉出流使用的示例代碼:
cuda = torch.device('cuda')
s = torch.cuda.Stream() # 創建流對象
A = torch.empty((100,100),device=cuda).normal_(0.0,1.0)
with torch.cuda.streams(s):
# sum()函數可能在normal_()函數結束之前就已經開始執行了,體現了併發執行
B = torch.sum(A)
噹噹前的stream
是默認的stream
,當發生數據移動時,PyTorch
會自動執行一些必要的同步操作。然而,當沒有使用默認的stream
時,則需要調用者來確保正常的同步過程。
內存管理
PyTorch
會使用緩存內存分配器(caching memory allocator
)來加速內存分配。使用該機制可以使得在內存回收時速度更快,因爲這過程沒有GPU設備的同步過程。但是,被allocator
管理的未使用的內存在nvidia-smi
上還會顯示這部分內存仍被佔用。使用者可以使用memory_allocated()
和max_memory_allocated()
來對tensors
佔用的內存進行監控,可以使用memory_reserved()
和max_memory_reserved()
來監控caching allocator
管理的內存數量。可以調用empty_cache()
來釋放所有沒有使用的cached memory
,這樣這部分內存就可以被其他的GPU應用使用了。被tensors
佔用的GPU內存不會被釋放因此可以使用的GPU內存的數量不會增加,除非把這部分內存顯式釋放掉。
對於更多的advanced users
,Pytorch
提供了memory_stats()
來監控內存數量,提供了memory_snapshot()
函數來捕獲內存分配狀態。可以通過這兩個函數來查看自己的代碼對於內存的使用情況。
cuFFT plan cache
對於每一個CUDA
設備,一個關於cuFFT
計劃的LRU算法被用來加速在CUDA tensor
上運行的FFT方法,這些tensors
具有相同的形狀和配置。由於一些cuFFT
計劃可能分配GPU內存,這些caches
有一個最大容量。
可以通過以下API參數來查詢當前設備的cache
的配置情況:
torch.backends.cuda.cufft_plan_cache.max_size
:給定cache
的容量(在CUDA 10 及以上版本是4096,老版本是1023)。通過設置該屬性可以修改容量torch.backends.cuda.cufft_plan_cache.size
:當前cache
中仍存在的plans
的數量
-torch.backends.cuda.cufft_plan_cache.clear()
:清空cache
爲了在non-default
設備上控制和查詢plan caches
,可以通過torch.device
或者GPU設備的索引來索引torch.backends.cuda.cufft_plan_cache
對象,並訪問以上描述到的三個屬性。舉例如下:
torch.backends.cuda.cufft.plan_cache[1].max_size=10 # 設置GPU設備1的容量爲10
設備不可知代碼(Device-agnostic code)
由於Pytorch
的結構,使用者需要顯示寫設備不可知的代碼。第一步是決定是都使用GPU。一個通用的做法是使用Python
的argparse
模塊來讀取用戶參數,結合使用is_available()
,使用一個標記來不適用CUDA。
import argparse
import torch
parse = argparse.ArgumentParser(description='Pytorch example')
parse.add_argument('--disable-cuda',action='store_true',help='DisableCUDA')
args = parser.parse_args()
args.device = None
if not args.disable_cuda and torch.cuda.is_available():
args.device = torch.device('cuda')
else:
args.device = torch.device('cpu')
# 在設備上創建tensor和神經網絡(根據參數才知道設備是CPU還是GPU)
x = torch.empty((8,42),device=args.device)
net = Network().to(device=args.device)
使用數據加載器(dataloader
)加載數據:
cuda0 = torch.device('cuda:0')
for i,x in enumerate(train_loader):
x = x.to(cuda0)
使用torch.cuda.device
來使用控制 tensor創建時所在的GPU設備
print("Outside device is 0")
with torch.cuda.device(1): # 選擇GPU設備1
print("Inside device is 1")
print("Outside device is still 0")
當存在一個tensor
前提下,想要在相同的GPU設備上創建一個新的tensor
,此時可以使用torch.Tensor.new_*
這一類的方法。torch.*
類的方法使用需要當前GPU的context
信息和傳入的屬性參數,torch.Tensor.new_*
方法保留設備信息和tensor
的其他屬性。
以下代碼展示了在參數的前向傳播過程中模型創建時,新的tensors
在其內部創建的過程。
cuda = torch.device('cuda')
x_cpu = torch.empty(2)
x_gpu = torch.empty(2,device=cuda)
x_cpu_long = torch.empty(2,dtype=torch.int64)
# 在CPU(因爲x_cpu是CPU上的Tensor)上使用new_full創建一個tensor,元素值爲0.3
y_cpu = x_cpu.new_full([3,2],fill_value=0.3)
print(y_cpu)
# 在GPU(因爲x_gpu是GPU上的Tensor)上使用new_full創建一個tensor,元素值爲-5
y_gpu = x_gpu.new_full([3,2],fill_value=-5)
print(y_gpu)
# 在CPU上創建Tensor
y_cpu_long = x_cpu_long.new_tensor([[1,2,3]])
print(y_cpu_long)
輸出結果如下:
tensor([[ 0.3000, 0.3000],
[ 0.3000, 0.3000],
[ 0.3000, 0.3000]])
tensor([[-5.0000, -5.0000],
[-5.0000, -5.0000],
[-5.0000, -5.0000]], device='cuda:0')
tensor([[ 1, 2, 3]])
tensor的new_full函數說明如下:
創建同類型和同大小的tensor:ones_like()
或者zeros_like()
x_cpu = torch.empty(2,3)
x_gpu = torch.empty(2,3)
y_cpu = torch.ones_like(x_cpu)
y_gpu = torch.zeros_like(x_gpu)
使用鎖內存緩存(pinned memory buffer)
當來自固定(頁面鎖定)內存時,主機到GPU的內存拷貝要更快。CPU中的tensors
和storages
向外暴露pin_memory()
方法,然後返回一個對象的拷貝,數據放在鎖住區域(pinned region
)。
一旦對一個tensor
或者storage
進行鎖定,則可以使用異步GPU拷貝操作。只需要將參數non_blocking=True
傳遞到to()
或者cuda()
方法中即可。這樣可以使得數據傳輸和計算過程一起進行。
可以將pin_memory=True
傳遞到DataLoader
的構造器中來返回批量數據,這部分數據被放置在鎖住內存(pinned memory
)中。
使用 nn.DataParallel來代替multiprocessing
許多涉及到批量數據處理和多塊GPU的案例應該默認使用DataParallel
來利用多塊GPU。甚至使用GIL,一個Python
的單進程程序可以滲入到多塊GPU中。