1. 程式人生 > >Cuda學習筆記(一)——sm流處理器簇對blocks的排程策略

Cuda學習筆記(一)——sm流處理器簇對blocks的排程策略

  由於GPU目前在各行各業的廣泛應用,無論是深度學習、大資料、雲端計算等都離不開GPU的並行加速,前陣子自學了Cuda-c程式設計,希望將來的研究工作能夠用得上。
  Cuda系列總共有4篇,這裡主要用於記錄本人學習過程中的一些問題的思考和總結,及網上彙總摘錄的別人的一些總結、看法等,並不適合新手入門。如有錯誤,歡迎各位指正。  

sm流處理器簇對blocks的排程策略

  在cuda中,GPU中的SM(GTX650M有2個sm處理器)被GPU排程器把執行緒塊逐個分配到SM上,每個SM同時處理這些被分配的執行緒塊block,但是每次每個時刻都只能處理一個warp執行緒束,由於有時會存在記憶體讀取等操作導致等待,那麼SM會轉而處理其他的warp來掩蓋這個延遲。並且這些warp不一定都是在同一個執行緒塊block內的,另之所以每次同時處理多個block而不是隻處理一個block並把該block內的執行緒個數設定成支援的最大的執行緒個數,一是可以允許其在支援不同執行緒個數的不同型號的顯示卡上都能處理相同的程式;二是由於gpu並行運算一般存在成千上萬個執行緒,並且執行緒塊個數遠遠大於SM,所以不會存在同一個SM內的執行緒塊幾乎同時執行完畢,而最終執行最後一個被排程的執行緒塊而造成等待時間大於之前sm執行的時間所導致的效率低下。

附錄1:

  GPU執行緒以網格(grid)的方式組織,而每個網格中又包含若干個執行緒塊,在G80/GT200系列中,每一個執行緒塊最多可包含512個執行緒,Fermi架構中每個執行緒塊支援高達1536個執行緒。同一執行緒塊中的眾多執行緒擁有相同的指令地址,不僅能夠並行執行,而且能夠通過共享儲存器(Shared memory)和柵欄(barrier)實現塊內通訊。這樣,同一網格內的不同塊之間存在不需要通訊的粗粒度並行,而一個塊內的執行緒之間又形成了允許通訊的細粒度並行。這些就是CUDA的關鍵特性:執行緒按照粗粒度的執行緒塊和細粒度的執行緒兩個層次進行組織、在細粒度並行的層次通過共享儲存器和柵欄同步實現通訊,這就是CUDA的雙層執行緒模型。
  在執行時,GPU的任務分配單元(global block scheduler)將網格分配到GPU晶片上。啟動CUDA 核心時,需要將網格資訊從CPU傳輸到GPU。任務分配單元根據這些資訊將塊分配到SM上。任務分配單元使用的是輪詢策略:輪詢檢視SM是否還有足夠的資源來執行新的塊,如果有則給SM分配一個新的塊,如果沒有則檢視下一個SM。決定能否分配的因素有:每個塊使用的共享儲存器數量,每個塊使用的暫存器數量,以及其它的一些限制條件。任務分配單元在SM的任務分配中保持平衡,但是程式設計師可以通過更改塊內執行緒數,每個執行緒使用的暫存器數和共享儲存器數來隱式的控制,從而保證SM之間的任務均衡。任務以這種方式劃分能夠使程式獲得了可擴充套件性:由於每個子問題都能在任意一個SM上執行,CUDA程式在核心數量不同的處理器上都能正常執行,這樣就隱藏了硬體差異。
  對於程式設計師來說,他們需要將任務劃分為互不相干的粗粒度子問題(最好是易平行計算),再將每個子問題劃分為能夠使用執行緒處理的問題。同一執行緒塊中的執行緒開始於相同的指令地址,理論上能夠以不同的分支執行。但實際上,在塊內的分支因為SM構架的原因被大大限制了。核心函式實質上是以塊為單位執行的。同一執行緒塊中的執行緒需要SM中的共享儲存器共享資料,因此它們必須在同一個SM中發射。執行緒塊中的每一個執行緒被髮射到一個SP上。任務分配單元可以為每個SM分配最多8個塊。而SM中的執行緒排程單元又將分配到的塊進行細分,將其中的執行緒組織成更小的結構,稱為執行緒束(warp)。在CUDA中,warp對程式設計師來說是透明的,它的大小可能會隨著硬體的發展發生變化,在當前版本的CUDA中,每個warp是由32個執行緒組成的。SM中一條指令的延遲最小為4個指令週期。8個SP採用了發射一次指令,執行4次的流水線結構。所以由32個執行緒組成的Warp是CUDA程式執行的最小單位,並且同一個warp是嚴格序列的,因此在warp內是無須同步的。在一個SM中可能同時有來自不同塊的warp。當一個塊中的warp在進行訪存或者同步等高延遲操作時,另一個塊可以佔用SM中的計算資源。這樣,在SM內就實現了簡單的亂序執行。不同塊之間的執行沒有順序,完全並行。無論是在一次只能處理一個執行緒塊的GPU上,還是在一次能處理數十乃至上百個執行緒塊的GPU上,這一模型都能很好的適用。

附錄2:

硬體基本架構
  實際上在 nVidia 的 GPU 裡,最基本的處理單元是所謂的 SP(Streaming Processor),而一顆 nVidia 的 GPU 裡,會有非常多的 SP 可以同時做計算;而數個 SP 會在附加一些其他單元,一起組成一個 SM(Streaming Multiprocessor)。幾個 SM 則會在組成所謂的 TPC(Texture Processing Clusters)。
  在 G80/G92 的架構下,總共會有 128 個 SP,以 8 個 SP 為一組,組成 16 個 SM,再以兩個 SM 為一個 TPC,共分成 8 個 TPC 來運作。而在新一代的 GT200 裡,SP 則是增加到 240 個,還是以 8 個 SP 組成一個 SM,但是改成以 3 個 SM 組成一個 TPC,共 10 組 TPC。下面則是提供了兩種不同表示方式的示意圖。(可參考《NVIDIA G92終極狀態!!》、《NVIDIA D10U繪圖核心》)

對應到 CUDA
  而在 CUDA 中,應該是沒有 TPC 的那一層架構,而是隻要根據 GPU 的 SM、SP 的數量和資源來調整就可以了。
  如果把 CUDA 的 Grid - Block - Thread 架構對應到實際的硬體上的話,會類似對應成 GPU - Streaming Multiprocessor - Streaming Processor;一整個 Grid 會直接丟給 GPU 來執行,而 Block 大致就是對應到 SM,thread 則大致對應到 SP。當然,這個講法並不是很精確,只是一個簡單的比喻而已。

SM 中的 Warp 和 Block
  CUDA 的 device 實際在執行的時候,會以 Block 為單位,把一個個的 block 分配給 SM 進行運算;而 block 中的 thread,又會以「warp」為單位,把 thread 來做分組計算。目前 CUDA 的 warp 大小都是 32,也就是 32 個 thread 會被群組成一個 warp 來一起執行;同一個 warp 裡的 thread,會以不同的資料,執行同樣的指令。此外,在 Compute Capability 1.2 的硬體中,還加入了 warp vote 的功能,可以快速的進行 warp 內的簡單統計。
  基本上 warp 分組的動作是由 SM 自動進行的,會以連續的方式來做分組。比如說如果有一個 block 裡有 128 個 thread 的話,就會被分成四組 warp,第 0-31 個 thread 會是 warp 1、32-63 是 warp 2、64-95 是 warp 3、96-127 是 warp 4。
  而如果 block 裡面的 thread 數量不是 32 的倍數,那他會把剩下的 thread 獨立成一個 warp;比如說 thread 數目是 66 的話,就會有三個 warp:0-31、32-63、64-65。由於最後一個 warp 裡只剩下兩個 thread,所以其實在計算時,就相當於浪費了 30 個 thread 的計算能力;這點是在設定 block 中 thread 數量一定要注意的事!
  一個 SM 一次只會執行一個 block 裡的一個 warp,但是 SM 不見得會一次就把這個 warp 的所有指令都執行完;當遇到正在執行的 warp 需要等待的時候(例如存取 global memory 就會要等好一段時間),就切換到別的 warp 來繼續做運算,藉此避免為了等待而浪費時間。所以理論上效率最好的狀況,就是在 SM 中有夠多的 warp 可以切換,讓在執行的時候,不會有「所有 warp 都要等待的情形發生;因為當所有的 warp 都要等待時,就會變成 SM 無事可做的狀況了~
  下圖就是一個 warp 排程的例子。一開始是先執行 thread block 1 的 warp1,而當他執行到第六行指令的時候,因為需要等待,所以就會先切到 thread block 的 warp2 來執行;一直等到存取結束,且剛好有一個 warp 結束時,才繼續執行 TB1 warp1 的第七行指令。

這裡寫圖片描述

  實際上,warp 也是 CUDA 中,每一個 SM 執行的最小單位;如果 GPU 有 16 組 SM 的話,也就代表他真正在執行的 thread 數目會是 32x16 個。不過由於 CUDA 是要透過 warp 的切換來隱藏 thread 的延遲、等待,來達到大量平行化的目的,所以會用所謂的 active thread 這個名詞來代表一個 SM 裡同時可以處理的 thread 數目。
  而在 block 的方面,一個 SM 可以同時處理多個 thread block,當其中有 block 的所有 thread 都處理完後,他就會再去找其他還沒處理的 block 來處理。假設有 16 個 SM、64 個 block、每個 SM 可以同時處理三個 block 的話,那一開始執行時,device 就會同時處理 48 個 block;而剩下的 16 個 block 則會等 SM 有處理完 block 後,再進到 SM 中處理,直到所有 block 都處理結束。
  為一個多處理器指定了一個或多個要執行的執行緒塊時,它會將其分成warp塊,並由SIMT單元進行排程。將塊分割為warp的方法總是相同的,每個warp都包含連續的執行緒,遞增執行緒索引,第一個warp中包含全域性執行緒過索引0-31。每發出一條指令時,SIMT單元都會選擇一個已準備好執行的warp塊,並將指令傳送到該warp塊的活動執行緒。Warp塊每次執行一條通用指令,因此在warp塊的全部32個執行緒執行同一條路徑時,可達到最高效率。如果一個warp塊的執行緒通過獨立於資料的條件分支而分散,warp塊將連續執行所使用的各分支路徑,而禁用未在此路徑上的執行緒,完成所有路徑時,執行緒重新匯聚到同一執行路徑下,其執行時間為各時間總和。分支僅在warp塊內出現,不同的warp塊總是獨立執行的–無論它們執行的是通用的程式碼路徑還是彼此無關的程式碼路徑。

建議的數值?
  在 Compute Capability 1.0/1.1 中,每個 SM 最多可以同時管理 768 個 thread(768 active threads)或 8 個 block(8 active blocks);而每一個 warp 的大小,則是 32 個 thread,也就是一個 SM 最多可以有 768 / 32 = 24 個 warp(24 active warps)。到了 Compute Capability 1.2 的話,則是 active warp 則是變為 32,所以 active thread 也增加到 1024。
  在這裡,先以 Compute Capability 1.0/1.1 的數字來做計算。根據上面的資料,如果一個 block 裡有 128 個 thread 的話,那一個 SM 可以容納 6 個 block;如果一個 block 有 256 個 thread 的話,那 SM 就只能容納 3 個 block。不過如果一個 block 只有 64 個 thread 的話,SM 可以容納的 block 不會是 12 個,而是他本身的數量限制的 8 個。
  因此在 Compute Capability 1.0/1.1 的硬體上,決定 block 大小的時候,最好讓裡面的 thread 數目是 warp 數量(32)的倍數(可以的話,是 64 的倍數會更好);而在一個 SM 裡,最好也要同時存在複數個 block。如果再希望能滿足最多 24 個 warp 的情形下,block 裡的 thread 數目似乎會是 96(一個 SM 中有 8 個 block)、128(一個 SM 中有 6 個 block)、192(一個 SM 中有 4 個 block)、256(一個 SM 中有 3 個 block) 這些數字了~
  Compute Capability 1.0/1.1 的硬體上,每個grid最多可以允許65535×65535個block。每個block最多可以允許512個thread,但是第三維上的最大值為64。而官方的建議則是一個 block 裡至少要有 64 個 thread,192 或 256 個也是通常比較合適的數字(請參考 Programming Guide)。
  但是是否這些數字就是最合適的呢?其實也不盡然。因為實際上,一個 SM 可以允許的 block 數量,還要另外考慮到他所用到 SM 的資源:shared memory、registers 等。在 G80 中,每個 SM 有 16KB 的 shared memory 和 8192 個 register。而在同一個 SM 裡的 block 和 thread,則要共享這些資源;如果資源不夠多個 block 使用的話,那 CUDA 就會減少 Block 的量,來讓資源夠用。在這種情形下,也會因此讓 SM 的 thread 數量變少,而不到最多的 768 個。
  比如說如果一個 thread 要用到 16 個 register 的話(在 kernel 中宣告的變數),那一個 SM 的 8192 個 register 實際上只能讓 512 個 thread 來使用;而如果一個 thread 要用 32 個 register,那一個 SM 就只能有 256 個 thread 了~而 shared memory 由於是 thread block 共享的,因此變成是要看一個 block 要用多少的 shread memory、一個 SM 的 16KB 能分給多少個 block 了。
  所以雖然說當一個 SM 裡的 thread 越多時,越能隱藏 latency,但是也會讓每個 thread 能使用的資源更少。因此,這點也就是在優化時要做取捨的了。