Pytorch:CUDA Semantics(語義)

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。一個通用的做法是使用Pythonargparse模塊來讀取用戶參數,結合使用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中的tensorsstorages向外暴露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中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章