當單純只用於圖形的GPU,搖身一變,成為可做為通用性計算的GPGPU時,和CPU一同組成的異構性計算系統,就可以提供更豐富,而且更高效的計算模式。

那麼,你應該會想問,身為一個程式設計者,若是想運用這種異構式的計算模式,究竟可以採取那些方式來著手呢?

在目前,有幾種方式可以讓你利用異構式計算的模式來設計程式,像是Nvidia的CUDA(Compute Unified Device Architecture)、Khronos Group的OpenCL(Open Computing Language),以及微軟的C++ AMP(Accelerated Massive Parallelism)。

首先要提的是CUDA。從名稱上來看,CUDA意思是「統一的計算架構」,其實它就是Nvidia所提出來的GPGPU模型。

在這個模型底下,一方面定義了硬體上GPGPU的架構,以及和CPU協同合作的方式,另一方面,也定義了程式設計的模型,使得程式設計者,得以使用C語言來對應GPGPU的操控,進而寫出得以在GPGPU上執行的程式碼,並不需要學習如何運用特定的語言、指令,或額外的描述方式。

CUDA的運作架構
在CUDA的模型底下,一個程式可以區分為兩個部份,分別是在CPU上執行,以及在GPU上執行的部份。CPU這一端被稱為「host」,而GPU這一端則是「device」。在GPU上執行的程式,也被稱為是「kernel」。

在硬體來看,CPU和GPU各有其可存取的記憶體空間,當然,各自也只能操作各自記憶體空間中的資料。

CPU端被稱為host,自然是因為它必須主控整個計算的進行除了CPU本身要執行的計算工作之外,也要負責將程式派送到GPU上執行。在CPU上的程式負責準備好要在GPU上計算的資料,接著將這些資料由host端的記憶體空間,複製一份至device端的記憶體空間中,以利GPU運用、計算。而在GPU完成計算之後,再將device端的記憶體空間之計算結果,複製回host端。

可以想見的,受限於頻寬的關係,CPU存取顯示卡(即device端)上的記憶體速度並不是非常快,所以,在host端和device端複製資料的代價並不低。這是異構式計算所必須付出的額外負擔之一。在設計程式時,也必須考慮到在兩端複製資料的成本,盡可能降低複製資料的動作。否則,從異構式計算中得到的效能,會被這額外負擔吃掉不少。

特別的執行緒與記憶體使用方式
在GPU上執行的程式,可以用一個三階層次的架構來區分。最上層的是格(Grid),在同一個格中分為多個區塊(Block)。在同一個格裡的區塊,都執行著相同的程式,而一個區塊內又包含了多個執行緒。

執行緒是GPU上最基本的執行單位,每個執行緒都有自己的一組暫存器和執行緒內的記憶體(local memory)。在GPU裡,會有數量相較而言較多的暫存器,這使得即使執行緒間做切換,也不會像CPU中的執行緒切換一樣,會在context switch上帶來額外負擔。

你可以想見,即使是不同的執行緒間,也會有共用資料的需求,所以,除了執行緒內的記憶體之外,GPU裡還有所謂的共享記憶體(shared memory),但是並不是任意的執行緒之間,都可以共享記憶體,這共享記憶體僅限於同一個區塊中的執行緒可以共用,使得這些執行緒可以進行更快速的合作。

同一格裡的執行緒還共用全域記憶體(global memory)、常數記憶體(constant memory),以及材質記憶體(texture memory)。從區域記憶體、共享記憶體、到全域記憶體、常數記憶體,以及材質記憶體,便構成了CUDA的記憶體模型。你必須了解各種記憶體類型的特性及限制,才能寫出有效的程式碼。

GPU內含的流處理器與執行緒

CUDA中的GPU擁有多個多處理器,而每個多處理器中,又有多個流處理器(stream processor)。

在CUDA的架構中,大多數的運算都是流處理器負責。而執行程式時,便是由一個流處理器,來對應一個執行緒的執行,而一個多處理器,便對應到一個區塊。

如果你留意過GPU的規格,你或許會注意到,像一個多處理器中可能含有八個流處理器,卻可以對應到數目遠超過八個執行緒的執行緒數量。這其中的道理究竟在那裡呢?這應該要從GPU的一個基本哲學來說起。

GPU在計算上的優勢,就是高度的平行化來處理資料,而在這之下,它處理一些問題的基本方式就和CPU不同,例如,在面對存取記憶體時延遲的問題時,其哲學就不同。

在CPU上,處理記憶體存取的延遲,通常利用快取記憶體的方式。利用存取速度高的快取記憶體,讓程式所要使用的資料盡可能在快取中取得,自然不用到存取速度較慢的主記憶體中去找。

但是GPU就不同了,GPU往往本身沒有快取,或者容量很小,因為這並非它解決記憶體存取延遲問題的方式。它解決這個問題的方式,就是利用高度的平行化,來讓記憶體的延遲不致於產生太大的影響。這是怎麼辦到的呢?

接續著前段中所提到的,一個多處理器裡僅有八個流處理器,但對應到數量遠多於八的執行緒,這就是為了讓執行延遲不致於產生真正效應的方式。雖然同時間只能執行八個執行緒,但是,硬體上會自動將同一個區塊裡的執行緒分組,這個分組的名稱就稱為「warp」。

每個warp裡的執行緒數量都是固定的,例如32個。同一個warp裡的執行緒,執行相同的程式碼,但處理不同的資料。當然,如果區塊內總執行緒的數量,並不是恰好為32的整數倍時,就會有一個warp中的執行緒數量不足32,而這種狀況就成了執行時的浪費。

所以,設定執行緒的數目也會影響到效能。

每次多處理器在執行時,只會執行一個warp內的執行緒,但是當這個warp內的執行緒,因為一些情況(例如對全域記憶體的存取)而必須停滯執行時,就可以立即切換到另一個warp中的執行緒來執行。這麼一來,對記憶體存取的延遲,就會被「掩飾」了。

因為同時間並沒有浪費執行的能力,雖然正在等待,但也仍然在執行指令。這就是利用高度平行化的方式,來處理記憶體存取延遲的方式。由於硬體設計的方式,使得在切換另一組執行緒來執行時速度很快,所以,也不會有context switch造成的額外代價。

不過,這並不代表你應該盡可能增加同區塊內執行緒的數量。

這是因為暫存器數目雖然很多,但是仍然是有限,若是執行緒數量,乘上每個執行緒所需的暫存器數量,超過一個多處理器中暫存器的總數,就會使得部份資料得儲放在記憶體中,而這就會讓context switch付出代價,因而降低執行效能。

在本文中,我們介紹了CUDA的模型,以及硬體的特性。想要寫出高效的程式碼,勢必要對底層的運作方式有所了解,因為,光是執行緒數量參數的設定,就足以深深影響到效能。

 

專欄作者

熱門新聞

Advertisement