天天看點

《CUDA C程式設計權威指南》——3.6 動态并行

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

在本書中,到目前為止,所有核函數都是從主機線程中被調用的。gpu的工作負載完全在cpu的控制下。cuda的動态并行允許在gpu端直接建立和同步新的gpu核心。在一個核函數中在任意點動态增加gpu應用程式的并行性,是一個令人興奮的新功能。

到目前為止,我們需要把算法設計為單獨的、大規模資料并行的核心啟動。動态并行提供了一個更有層次結構的方法,在這個方法中,并發性可以在一個gpu核心的多個級别中表現出來。使用動态并行可以讓遞歸算法更加清晰易懂,也更容易了解。

有了動态并行,可以推遲到運作時決定需要在gpu上建立多少個塊和網格,可以動态地利用gpu硬體排程器和加載平衡器,并進行調整以适應資料驅動或工作負載。

在gpu端直接建立工作的能力可以減少在主機和裝置之間傳輸執行控制和資料的需求,因為在裝置上執行的線程可以在運作時決定啟動配置。

在本節中,将通過使用動态并行實作遞歸歸約核函數的例子,對如何利用動态并行有一個基本的了解。

通過動态并行,我們已經熟悉了核心執行的概念(網格、塊、啟動配置等),也可以直接在gpu上進行核心調用。相同的核心調用文法被用在一個核心内啟動一個新的核函數。

在動态并行中,核心執行分為兩種類型:父母和孩子。父線程、父線程塊或父網格啟動一個新的網格,即子網格。子線程、子線程塊或子網格被父母啟動。子網格必須在父線程、父線程塊或父網格完成之前完成。隻有在所有的子網格都完成之後,父母才會完成。

圖3-26說明了父網格和子網格的适用範圍。主機線程配置和啟動父網格,父網格配置和啟動子網格。子網格的調用和完成必須進行适當地嵌套,這意味着線上程建立的所有子網格都完成之後,父網格才會完成。如果調用的線程沒有顯式地同步啟動子網格,那麼運作時保證父母和孩子之間的隐式同步。在圖3-26中,在父線程中設定了栅欄,進而可以與其子網格顯式地同步。

《CUDA C程式設計權威指南》——3.6 動态并行

裝置線程中的網格啟動,線上程塊間是可見的。這意味着,線程可能與由該線程啟動的或由相同線程塊中其他線程啟動的子網格同步。線上程塊中,隻有當所有線程建立的所有子網格完成之後,線程塊的執行才會完成。如果塊中所有線程在所有的子網格完成之前退出,那麼在那些子網格上隐式同步會被觸發。

當父母啟動一個子網格,父線程塊與孩子顯式同步之後,孩子才能開始執行。

父網格和子網格共享相同的全局和常量記憶體存儲,但它們有不同的局部記憶體和共享記憶體。有了孩子和父母之間的弱一緻性作為保證,父網格和子網格可以對全局記憶體并發存取。有兩個時刻,子網格和它的父線程見到的記憶體完全相同:子網格開始時和子網格完成時。當父線程優于子網格調用時,所有的全局記憶體操作要保證對子網格是可見的。當父母在子網格完成時進行同步操作後,子網格所有的記憶體操作應保證對父母是可見的。

共享記憶體和局部記憶體分别對于線程塊或線程來說是私有的,同時,在父母和孩子之間不是可見或一緻的。局部記憶體對線程來說是私有存儲,并且對該線程外部不可見。當啟動一個子網格時,向局部記憶體傳遞一個指針作為參數是無效的。

《CUDA C程式設計權威指南》——3.6 動态并行
《CUDA C程式設計權威指南》——3.6 動态并行

在wrox.com上可以下載下傳nestedhelloworld.cu檔案,裡面有本例的所有代碼。可以用以下指令編譯代碼:

《CUDA C程式設計權威指南》——3.6 動态并行

因為動态并行是由裝置運作時庫所支援的,是以nestedhelloworld函數必須在指令行使用-lcudadevrt進行明确連結。

當-rdc标志為true時,它強制生成可重定位的裝置代碼,這是動态并行的一個要求。在本書的第10章将會介紹到更多可重定位裝置代碼的内容。

嵌套核函數的輸出如下:

《CUDA C程式設計權威指南》——3.6 動态并行

從輸出資訊中可見,由主機調用的父網格有1個線程塊和8個線程。nestedhelloworld核函數遞歸地調用三次,每次調用的線程數是上一次的一半。可以用nvvp工具通過以下的指令證明這一點:

《CUDA C程式設計權威指南》——3.6 動态并行

圖3-28所示為由nvvp顯示的嵌套執行。子網格被适當地嵌套,并且每個父網格會等待直到它的子網格執行結束,空白處說明核心在等待子網格執行結束。

現在,試着使用兩個線程塊調用父網格,而不是使用一個。

《CUDA C程式設計權威指南》——3.6 動态并行

嵌套核心程式的輸出如下:

《CUDA C程式設計權威指南》——3.6 動态并行
《CUDA C程式設計權威指南》——3.6 動态并行
《CUDA C程式設計權威指南》——3.6 動态并行

為什麼在輸出資訊裡所有子網格線程塊的id都是0?圖3-29說明了子網格是如何被兩個初始線程塊遞歸調用的。父網格包含兩個線程塊,所有嵌套的子網格仍然隻包含一個線程塊,這是由于線程配置核函數在nestedhelloworld函數裡啟動:

《CUDA C程式設計權威指南》——3.6 動态并行

可以嘗試使用不同的啟動政策。圖3-30所示為另一種生成相同數量并行性的方法,這一部分留給讀者作為練習。

《CUDA C程式設計權威指南》——3.6 動态并行
《CUDA C程式設計權威指南》——3.6 動态并行

歸約可以被表示為一個遞歸函數。本章中的3.4節已經用c語言示範了遞歸歸約。在cuda裡使用動态并行,可以確定cuda裡的遞歸歸約核函數的實作像在c語言中一樣簡單。

下面列出了帶有動态并行的遞歸歸約的核心代碼。這個核函數采取圖3-29所示的方法,原始的網格包含許多線程塊,但所有嵌套的子網格中隻有一個由其父網格的線程0調用的線程塊。核函數的第一步是将全局記憶體位址g_idata轉換為每個線程塊的本地位址。接着,如果滿足停止條件(這是指如果該條件是嵌套執行樹上的葉子),結果就被拷貝回全局記憶體,并且控制立刻傳回到父核心中。如果它不是一片葉子核心,就需要計算本地歸約的大小,一半的線程執行就地歸約。在就地歸約完成後,同步線程塊以保證所有部分和的計算。緊接着,線程0産生一個隻有一個線程塊和一個目前線程塊一半線程數量的子網格。在子網格被調用後,所有子網格會設定一個障礙點。因為在每個線程塊裡,一個線程隻産生一個子網格,是以這個障礙點隻會同步一個子網格。

《CUDA C程式設計權威指南》——3.6 動态并行

在wrox.com上可以下載下傳nestedreduce.cu檔案,裡面有本例的所有代碼。可以用以下指令編譯代碼:

《CUDA C程式設計權威指南》——3.6 動态并行

用kepler k40裝置的輸出結果展示如下,相較于使用相鄰配對方法的核心實作,嵌套核心慢到無法接受:

《CUDA C程式設計權威指南》——3.6 動态并行

正如輸出結果顯示,最初有2 048個線程塊。因為每個線程塊執行8次遞歸,是以總共建立了16 384個子線程塊,用于同步線程塊内部的__syncthreads函數也被調用了16 384次。如此大量的核心調用與同步很可能是造成核心效率很低的主要原因

當一個子網格被調用後,它看到的記憶體與父線程是完全一樣的。因為每一個子線程隻需要父線程的數值來指導部分歸約,是以在每個子網格啟動前執行線程塊内部的同步是沒有必要的。去除所有同步操作會産生如下的核函數:

《CUDA C程式設計權威指南》——3.6 動态并行

在wrox.com上可以下載下傳nestedreducenosync.cu檔案,裡面有本例的完整代碼。編譯運作它。下面列出了在kepler k40裝置上的輸出結果。所需時間減少到了第一次動态并行實作的1/3:

《CUDA C程式設計權威指南》——3.6 動态并行

然而,相較于相鄰配對核心,它的性能仍然很差。需要考慮如何減少由大量的子網格啟動引起的消耗。在目前的實作中,每個線程塊産生一個子網格,并且引起了大量的調用。如果使用了圖3-30展示的方法,當建立的子網格數量減少時,那麼每個子網格中線程塊的數量将會增加,以保持相同數量的并行性。

以下的核函數實作了這種方法:網格中第一個線程塊中的第一個線程在每一步嵌套時都調用子網格。比較這兩個核函數的特征碼,會發現多了一個參數。因為每次嵌套調用時,子線程塊大小會減到其父線程塊大小的一半,父線程塊的次元也必須傳遞給嵌套的子網格。這使得每個線程都能為它的工作負載部分正确計算出消耗部分的全局記憶體偏移位址。值得注意的是,在這個實作中,所有空閑的線程都是在每次核心啟動時被移除的,而對于第一次實作而言,在每個嵌套層的核心執行過程中都會有一半的線程空閑下來。這樣的改變将會釋放一半的被第一個核函數消耗的計算資源,這樣可以讓更多的線程塊活躍起來。

《CUDA C程式設計權威指南》——3.6 動态并行

在wrox.com上可以下載下傳nestedreduce2.cu檔案,裡面有本例的完整代碼。k40 gpu裝置輸出結果如下:

《CUDA C程式設計權威指南》——3.6 動态并行

從這個結果可以看到,遞歸歸約核函數的第三種實作比前兩種實作更快了,大概是由于調用了較少的子網格。可以使用nvprof來驗證性能提高的原因:

《CUDA C程式設計權威指南》——3.6 動态并行

部分輸出結果概括如下。第二列顯示了裝置核心的調用次數。第一個和第二個核心在裝置上共建立了16 384個子網格。gpurecursivereduce2核心中的8層嵌套并行隻建立了8個子網格。

《CUDA C程式設計權威指南》——3.6 動态并行

該遞歸歸約的例子說明了動态并行。對于一個給定的算法,通過使用不同的動态并行技術,可以有多種可能的實作方式。避免大量嵌套調用有助于減少消耗并提升性能。同步對性能與正确性都至關重要,但減少線程塊内部的同步次數可能會使嵌套核心效率更高。因為在每一個嵌套層上裝置運作時系統都要保留額外的記憶體,是以核心嵌套的最大數量可能是受限制的。這種限制的程度依賴于核心,也可能會限制任何使用動态并行應用程式的擴充、性能以及其他的性能。

繼續閱讀