CUDA C最佳實踐-CUDA Best Practices(一)

這文檔堪稱CUDA官方手冊裏最有用TOP3了。
ps:全文翻譯會累死猿噠,意譯意譯,各位看官湊合一下啦

前言

文檔的作用

這文檔能幹嘛,是用來幫助開發者從NVIDIA GPU上獲取最好的性能的。建議順序閱讀,這文檔將會極大地提升你對程序效率的理解。

面向的對象

你要懂C,還要安裝了CUDA,從這安裝。最好還能看看《CUDA C Programming Guide》這份文檔。(這個文檔的一大特點就是,篇幅不夠的就讓你去看那個Programming Guide)

評估,並行,優化,實施

CUDA程序的循環

這個圖就是整篇文檔的中心了(APOD),首先你要評估你的程序,初始的加速將被實現,測試,並且在最小化的優化下運行,這個循環可以一次又一次地運行,通過再次發現優化機會,再次加速然後運行更快的版本。

評估

對於一個現有的項目,第一步就是評估這個應用來定位和大部分執行時間相關的部分。學會這個,開發者就能估計並行程序的瓶頸並可以加速GPU。需要理解Amdahl’s 和 Gustafson’s laws。

並行

確定了痛點之後,開發者需要並行化程序。可以使用現有的並行化庫或者在編譯器那增加並行標誌。但是許多程序需要重構才能並行而CUDA讓這件事變得容易。

優化

當並行化完成之後,開發者可以將注意力集中在優化。首先要明確應用的需求,在迭代中優化並實施程序,並不需要在一開始就要提升很大速度。而且,優化可以從不同的級別開始,從重疊計算與數據傳輸到細粒度的浮點數操作,同時分析工具能夠幫你提供下一步優化的方向。

實施

優化之後要將實際結果和期望結果比較,再次APOD循環。在進行更深度的優化之前,先把當前的程序部署起來,這樣有很多好處,比如允許使用者對當前的應用進行評估,並且減小了應用的風險因爲這是一種循序漸進的演化而不是改革。

建議和最佳實踐

這個文檔對於優化有個優先級的評價,確保在較低優先級優化進行之前,完成了所有的高級優化。當然這種優先級不是絕對的,文檔只是提供普通的情景。

1.評估應用

bulabula瞎扯,說明並行計算的重要性。爲了適應現代的處理器,包括GPU,第一步最重要的就是要識別出程序痛點,確定是否它能夠被並行化。

2.異構計算

雖然GPU主要用來處理圖像,但是它的計算能力也很強。CPU和GPU是不一樣的,要想高效地使用CUDA瞭解它們之間的不同很重要。

2.1 主機和設備之間的差別

  1. 線程資源

    CPU的線程很少(也就幾十個),而GPU的線程有上萬個。

  2. 線程

    CPU上的線程是龐大的實體,上下文切換對於它們來說耗時很多,而GPU則相反,因爲GPU有很多寄存器分配給線程。簡單來說,CPU是設計來讓少數線程的運行達到最小時延,而GPU是讓大量線程達到最大吞吐量。

  3. 內存

    它們都有各自的內存,並和PCIe總線連接。

這就是關於兩者不同的初步討論,其他的不同,將在文檔的其他部分相繼討論,知道這些不同有助於你的優化工作:儘量在主機上運行順序工作在GPU上運行並行的工作

2.2. 哪部分應該在GPU上運行

  1. 顯然是個大規模做相同運算的數據集。這需要很多的線程
  2. 使用的數據具有很好的一致性的模式的,否則會導致加速比小
  3. 主機與設備之間的數據傳輸要做到最小。
    • 給很少的線程傳遞數據是沒有必要的。比如傳入倆N×N的矩陣,算了和再傳回去。這裏有N^2的計算量但是要傳輸3N^2數據,比例是1:3或者說是O(1),但是要是算乘積的話,就是O(N),這樣就比較好了。或者那些比較複雜的運算比如三角函數之類的。反正記着傳輸數據是有開銷的對了
    • 數據要儘可能的保留在設備上。在兩個Kernel之間,數據要儘可能保存在數據上。比如上面那個兩個矩陣相加,可能在運算完之後還會被用於以後的操作,所以要留下。如果有這種情況,數據要被放在GPU上運行,就算是它可能在主機上運行更快。一個比較慢的Kernel可能會因此收益,第九章會詳細講解。

3. 程序分析

3.1. 分析

很多程序用很少的代碼完成了大部分的工作。使用分析器,開發者能夠發現這樣的點並且列出一個並行可能的列表。

3.1.1. 創建一個分析

最重點的就是,要找出執行時間最長的函數。而分析程序的最重要的是要確保工作負載和現實相似。可以使用gprof來測試:

gprof分析器

3.1.2. 分析痛點

從上面的圖我們就能看出來,genTimeStep()這個函數花了幾乎總時間的三分之一,這就是我們應該優化的函數。而且可以看出其他的函數也佔用了一大部分時間比如:calcStats() 和calcSummaryData()。並行化這些函數也可以加速程序,不過,要慢慢來嘛。

3.1.3. 認識哪部分能並行

想要從CUDA中獲得最大的性能提升,首先就要找到並行化現有串行代碼的方法。

3.1.3.1. 強標度與Amdahl定律

這裏這倆就請看這裏吧:並行計算中的Amdahl與Gustafson定律

Amdahl就是看看你的並行部分就算達到最完美了(運行時間是0),那你的程序到底能加速多少。

3.1.3.2. 弱標度與Gustafson定律

Gustafson定律就是假定串行和並行執行的比率保持不變,反映了設置和處理較大問題的額外成本。(其實木有太搞懂)

3.1.3.3. 實現強/弱標度

要知道那種標度適合你的應用程序,對於有些程序來說問題規模是一定的例如兩個分子之間的作用力而另外一些問題規模會隨着處理器的增加而增加例如流體的蒙特卡羅模擬,大的工作量能夠提供大的精度。

4. 並行化程序

確定了痛點之後,開發者需要並行化程序。可以使用現有的並行化庫或者在編譯器那增加並行標誌。但是許多程序需要重構才能並行而CUDA讓這件事變得容易。

5. 開始

雖然對於特定的應用實現並行是複雜的,但是有一些關鍵步驟是需要的。

5.1. 並行庫

CUDA提供了一些並行庫比如cuBLAS , cuFFT之類的。如果和需求比較符合,用這些庫十分方便。除了做線代的cuBLAS庫,做傅里葉變換的cuFFT,特別強調Thrust模板庫。這個庫包含了很多常用的並行算法,可以結合它完成複雜的算法。可以用它來快速完成一個CUDA應用的原型機。

5.2. 並行編譯器

這是通過設置特殊的標記,讓編譯器把代碼並行話的方式。比如在展開操作中使用的#progra unroll這個標記。OpenACC提供了很多這樣的指令。猛戳這裏去OpenACC的官網

5.3. 用代碼實現並行

除了上面那些現成的方法外,當然還是需要程序猿自己手動敲代碼了。我們可以把找到的痛點自己重新寫成並行的。當我們測試發現,很多函數佔用的時間都差不多的時候,這就需要我們重構這個代碼,而且你要知道,將代碼重構成並行的對於未來的架構是有好處的,因此這個工作是值得的。

6. 獲取正確答案

在並行程序裏並不好找到錯誤,因爲它線程太多了,而且浮點數計算等都有可能造成意想不到的錯誤。這一章就介紹那些可能導致錯誤的點並且告訴你如何解決。

6.1. 驗證

6.1.1. 對比參考

首先就要比較新結果與參考結果,確定結果與適用於任何算法的標準相匹配。有些計算想要每位都相同的結果,但是並不總是可能,特別的計算浮點數的時候。值得注意的是,那些被用於驗證數值結果的方法很容易就延伸到驗證結果性能上去。我們既要確定結果正確,又得讓效率上升。

6.1.2. 單元測試

爲了好測試,我們可以把Kernel函數寫成很多個device函數的組合而不是一個大的global函數。(這裏要注意的是,如果你對全局內存什麼也不做,你的編譯器會認爲你的部分代碼是dead code給你去掉,因此,一定在測試的時候做點什麼)另外,如果使用host device來定義而不是隻用device來定義,那這個函數就能夠在CPU上測試,這可以給我們增加測試的自信。

6.2. 調試

可以使用CUDA-GDB,這個我也寫過,詳情見這裏:使用cuda-gdb調試cu程序
或者用NVIDIA Parallel Nsight來調試:http://developer.nvidia.com/nvidia-parallel-nsight
以及一些第三方調試器:http://developer.nvidia.com/debugging-solutions

6.3. 數值精度

大多數浮點數精度的錯誤都源於浮點數計算和存儲的方式。提供一個網站:floating-point precision

6.3.1. 單精度VS雙精度

計算能力1.3以上的設備都提供雙精度浮點數計算。相比於單精度可以獲得更大的精度。要在使用的時候注意。

6.3.2. 浮點數計算不是可結合的

這個就是說在浮點數中(A+B)+C和A+(B+C)的值不一定相同,所以要注意可能你換了換操作數的位置,就讓結果不在正確,這個問題不僅存在於CUDA中,任何並行浮點數計算的系統都有可能出現這樣的問題。

6.3.3. 把雙精度轉換成單精度

比如

float a;
...
a = a*1.02;

這段代碼在GPU上計算,就會是單精度的,但是跑到主機上運算就會將1.02轉換成雙精度然後所有的結果都變成了雙精度的了,這樣結果就會有差異。而我們把1.02變成1.02f就能固定爲單精度浮點數了。

6.3.4. IEEE 754 標準

所有CUDA設備都遵循IEEE 754 標準,除了某些特殊情況,這些不同要看Features and Technical Specifications of the CUDA C Programming Guide

6.3.5. x86 80-bit 計算

x86機器還能進行80位的浮點數計算,這個和64位的計算有所不同。要獲得比較相近的結果,儘量別讓x86搞這個飛機。是用FLDCW這個指令操作。

7. 優化CUDA應用

當並行化完成之後,開發者可以將注意力集中在優化。首先要明確應用的需求,在迭代中優化並實施程序,並不需要在一開始就要提升很大速度。而且,優化可以從不同的級別開始,從重疊計算與數據傳輸到細粒度的浮點數操作,同時分析工具能夠幫你提供下一步優化的方向。

8. 性能檢測

想要優化代碼,知道怎麼精確測量而且知道帶寬在優化中所扮演的角色十分重要。這章主要就將這倆內容。

8.1. 測時

8.1.1. 使用 CPU 計時器

詳細介紹CPU計時並不在本文的討論範圍之內,但是一定要知道存在這種方法。一定要主義,要讓CPU和GPU事件同步發生,可以調用cudaDeviceSynchronize()這個函數,能夠阻塞CPU線程直到GPU完成工作。雖然也有能夠將CPU和流同步的代碼,但是不適用於計時,因爲流通常是交錯執行的。一定要注意,這種計時的方式會讓GPU的流水線操作停滯,所以要儘量減少使用。

8.1.2. 使用CUDA GPU計時器

使用CUDA提供的API就能計時:

計時

cudaEventRecord()將start和stop放入默認流中。設備將記錄一個時間戳當流到達這個事件的時候。cudaEventElapsedTime()就是返回start和stop的時間差。

8.2. 帶寬

8.2.1. 計算理論帶寬

只需要知道GPU的時鐘頻率和位寬。比如:1.85GHz和384位,雙倍數據速率。是這樣計算的:

(1.85*10^9*(384/8)*2)/10^9 = 177.6 GB/s

這是啥子原理嘞:首先把GHz轉換成Hz,然後384/8是換成字節,×2是雙倍數據速率,/10^9是轉換成GB。

8.2.2. 計算實際帶寬

公式:((Br+Bw)/10^9)/time

就是實際的傳輸數據除以時間。比如有2048*2048矩陣傳輸就要這麼計算:(2048×2048×4×2)/10^9/time

4是一個數四個字節,2是讀寫。

8.2.3. 使用Visual Profiler檢測吞吐量

在計算能力2.0或者更高的設備上,Visual Profiler能夠提供不同內存的吞吐量信息。包括:

  • Requested Global Load Throughput
  • Requested Global Store Throughput
  • Global Load Throughput
  • Global Store Throughput
  • DRAM Read Throughput
  • DRAM Write Throughput

其中requested是Kernel對於數據的請求。

最後,實際的吞吐量和請求的吞吐量都有用。前者可以讓你看到你的代碼能達到硬件的多少效率,而後者通過與前者的比較可以看到聚合操作中有多少內存被浪費。對於全局內存,這個數據由Global Memory Load Efficiency和Global Memory Store Efficiency 顯示。

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