天天看點

《CUDA C程式設計權威指南》——3.4 避免分支分化

本節書摘來自華章計算機《cuda c程式設計權威指南》一書中的第3章,第3.4節,作者 [美] 馬克斯·格羅斯曼(max grossman),譯 顔成鋼 殷建 李亮,更多章節内容可以通路雲栖社群“華章計算機”公衆号檢視。

有時,控制流依賴于線程索引。線程束中的條件執行可能引起線程束分化,這會導緻核心性能變差。通過重新組織資料的擷取模式,可以減少或避免線程束分化。在本節裡,将會以并行歸約為例,介紹避免分支分化的基本技術。

假設要對一個有n個元素的整數數組求和。使用如下的串行代碼很容易實作算法:

《CUDA C程式設計權威指南》——3.4 避免分支分化

如果有大量的資料元素會怎麼樣呢?如何通過并行計算快速求和呢?鑒于加法的結合律和交換律,數組元素可以以任何順序求和。是以可以用以下的方法執行并行加法運算:

将輸入向量劃分到更小的資料塊中。

用一個線程計算一個資料塊的部分和。

對每個資料塊的部分和再求和得出最終結果。

并行加法的一個常用方法是使用疊代成對實作。一個資料塊隻包含一對元素,并且一個線程對這兩個元素求和産生一個局部結果。然後,這些局部結果在最初的輸入向量中就地儲存。這些新值被作為下一次疊代求和的輸入值。因為輸入值的數量在每一次疊代後會減半,當輸出向量的長度達到1時,最終的和就已經被計算出來了。

根據每次疊代後輸出元素就地存儲的位置,成對的并行求和實作可以被進一步分為以下兩種類型:

相鄰配對:元素與它們直接相鄰的元素配對

交錯配對:根據給定的跨度配對元素

圖3-19所示為相鄰配對的實作。在每一步實作中,一個線程對兩個相鄰元素進行操作,産生部分和。對于有n個元素的數組,這種實作方式需要n―1次求和,進行log2 n步。

圖3-20所示為交錯配對的實作。值得注意的是,在這種實作方法的每一步中,一個線程的輸入是輸入數組長度的一半。

下列的c語言函數是一個交錯配對方法的遞歸實作:

《CUDA C程式設計權威指南》——3.4 避免分支分化
《CUDA C程式設計權威指南》——3.4 避免分支分化
《CUDA C程式設計權威指南》——3.4 避免分支分化

盡管以上代碼實作的是加法,但任何滿足交換律和結合律的運算都可以代替加法。例如,通過調用max代替求和運算,就可以計算輸入向量中的最大值。其他有效運算的例子有最小值、平均值和乘積。

在向量中執行滿足交換律和結合律的運算,被稱為歸約問題。并行歸約問題是這種運算的并行執行。并行歸約是一種最常見的并行模式,并且是許多并行算法中的一個關鍵運算。

在本節裡,會實作多個不同的并行歸約核函數,并且将測試不同的實作是如何影響核心性能的。

圖3-21所示的是相鄰配對方法的核心實作流程。每個線程将相鄰的兩個元素相加産生部分和。

在這個核心裡,有兩個全局記憶體數組:一個大數組用來存放整個數組,進行歸約;另一個小數組用來存放每個線程塊的部分和。每個線程塊在數組的一部分上獨立地執行操作。循環中疊代一次執行一個歸約步驟。歸約是在就地完成的,這意味着在每一步,全局記憶體裡的值都被部分和替代。__syncthreads語句可以保證,線程塊中的任一線程在進入下一次疊代之前,在目前疊代裡每個線程的所有部分和都被儲存在了全局記憶體中。進入下一次疊代的所有線程都使用上一步産生的數值。在最後一個循環以後,整個線程塊的和被儲存進全局記憶體中。

《CUDA C程式設計權威指南》——3.4 避免分支分化
《CUDA C程式設計權威指南》——3.4 避免分支分化

兩個相鄰元素間的距離被稱為跨度,初始化均為1。在每一次歸約循環結束後,這個間隔就被乘以2。在第一次循環結束後,idata(全局資料指針)的偶數元素将會被部分和替代。在第二次循環結束後,idata的每四個元素将會被新産生的部分和替代。因為線程塊間無法同步,是以每個線程塊産生的部分和被複制回了主機,并且在那兒進行串行求和,如圖3-22所示。

從wrox.com上可以找到reduceinteger.cu完整的源代碼。代碼清單3-3隻列出了主函數。

《CUDA C程式設計權威指南》——3.4 避免分支分化
《CUDA C程式設計權威指南》——3.4 避免分支分化
《CUDA C程式設計權威指南》——3.4 避免分支分化
《CUDA C程式設計權威指南》——3.4 避免分支分化
《CUDA C程式設計權威指南》——3.4 避免分支分化
《CUDA C程式設計權威指南》——3.4 避免分支分化

初始化輸入數組,使其包含16m元素:

《CUDA C程式設計權威指南》——3.4 避免分支分化

然後,核心被配置為一維網格和一維塊:

《CUDA C程式設計權威指南》——3.4 避免分支分化

用以下的指令編譯檔案:

《CUDA C程式設計權威指南》——3.4 避免分支分化

運作可執行檔案,以下是運作結果。

《CUDA C程式設計權威指南》——3.4 避免分支分化

在接下來的一節中,這些結果将會被作為性能調節的基準。

《CUDA C程式設計權威指南》——3.4 避免分支分化
《CUDA C程式設計權威指南》——3.4 避免分支分化

注意核心中的下述語句,它為每個線程設定數組通路索引:

《CUDA C程式設計權威指南》——3.4 避免分支分化

因為跨度乘以了2,是以下面的語句使用線程塊的前半部分來執行求和操作:

《CUDA C程式設計權威指南》——3.4 避免分支分化

對于一個有512個線程的塊來說,前8個線程束執行第一輪歸約,剩下8個線程束什麼也不做。在第二輪裡,前4個線程束執行歸約,剩下12個線程束什麼也不做。是以,這樣就徹底不存在分化了。在最後五輪中,當每一輪的線程總數小于線程束的大小時,分化就會出現。在下一節将會介紹如何處理這一問題。

在主函數裡調用基準核心之後,通過以下代碼段可以調用這個新核心。

《CUDA C程式設計權威指南》——3.4 避免分支分化

用reduceneighboredless函數測試,較早的核函數将産生如下報告:

《CUDA C程式設計權威指南》——3.4 避免分支分化

新的實作比原來的快了1.26倍。

可以通過測試不同的名額來解釋這兩個核心之間的不同行為。用inst_per_warp名額來檢視每個線程束上執行指令數量的平均值。

《CUDA C程式設計權威指南》——3.4 避免分支分化

結果總結如下,原來的核心在每個線程束裡執行的指令數是新核心的兩倍多,它是原來實作高分化的一個訓示器:

《CUDA C程式設計權威指南》——3.4 避免分支分化

用gld_throughput名額來檢視記憶體加載吞吐量:

《CUDA C程式設計權威指南》——3.4 避免分支分化

結果總結如下,新的實作擁有更高的加載吞吐量,因為雖然i/o操作數量相同,但是其耗時更短:

《CUDA C程式設計權威指南》——3.4 避免分支分化

與相鄰配對方法相比,交錯配對方法颠倒了元素的跨度。初始跨度是線程塊大小的一半,然後在每次疊代中減少一半(如圖3-24所示)。在每次循環中,每個線程對兩個被目前跨度隔開的元素進行求和,以産生一個部分和。與圖3-23相比,交錯歸約的工作線程沒有變化。但是,每個線程在全局記憶體中的加載/存儲位置是不同的。

《CUDA C程式設計權威指南》——3.4 避免分支分化

交錯歸約的核心代碼如下所示:

《CUDA C程式設計權威指南》——3.4 避免分支分化
《CUDA C程式設計權威指南》——3.4 避免分支分化

注意核函數中的下述語句,兩個元素間的跨度被初始化為線程塊大小的一半,然後在每次循環中減少一半:

《CUDA C程式設計權威指南》——3.4 避免分支分化

下面的語句在第一次疊代時強制線程塊中的前半部分線程執行求和操作,第二次疊代時是線程塊的前四分之一,以此類推:

《CUDA C程式設計權威指南》——3.4 避免分支分化

下面的代碼增加到主函數中,執行交錯歸約的代碼:

《CUDA C程式設計權威指南》——3.4 避免分支分化

用reduceinterleaved函數進行測試,較早的核心函數将産生如下報告:

《CUDA C程式設計權威指南》——3.4 避免分支分化

交錯實作比第一個實作快了1.69倍,比第二個實作快了1.34倍。這種性能的提升主要是由reduceinterleaved函數裡的全局記憶體加載/存儲模式導緻的。在第4章裡會介紹更多有關于全局記憶體加載/存儲模式對核心性能的影響。reduceinterleaved函數和reduceneigh-boredless函數維持相同的線程束分化。

繼續閱讀