天天看點

CUDA程式設計(六)進一步并行CUDA程式設計(六)

在之前我們使用thread完成了簡單的并行加速,雖然我們的程式運作速度有了50甚至上百倍的提升,但是根據記憶體帶寬來評估的話我們的程式還遠遠不夠,在上一篇部落格中給大家介紹了一個訪存方面非常重要的優化,我們通過使用連續的記憶體存取模式,取得了令人滿意的優化效果,最終記憶體帶寬也達到了gb/s的級别。

之前也已經提到過了,cuda不僅提供了thread,還提供了grid和block以及share memory這些非常重要的機制,我的顯示卡的thread極限是1024,但是通過block和grid,線程的數量還能成倍增長,甚至用幾萬個線程。是以本篇部落格我們将再次回到線程和并行的角度,進一步的并行加速我們的程式。

CUDA程式設計(六)進一步并行CUDA程式設計(六)

第一篇部落格的時候就給大家說明過thread-block-grid 結構了,這裡我們再複習一下。

在 cuda 架構下,顯示晶片執行時的最小機關是thread。數個 thread 可以組成一個block。一個 block 中的 thread 能存取同一塊共享的記憶體,而且可以快速進行同步的動作。

每一個 block 所能包含的 thread 數目是有限的。不過,執行相同程式的 block,可以組成grid。不同 block 中的 thread 無法存取同一個共享的記憶體,是以無法直接互通或進行同步。是以,不同 block 中的 thread 能合作的程度是比較低的。不過,利用這個模式,可以讓程式不用擔心顯示晶片實際上能同時執行的 thread 數目限制。例如,一個具有很少量執行單元的顯示晶片,可能會把各個 block 中的 thread 順序執行,而非同時執行。不同的 grid 則可以執行不同的程式(即 kernel)。

每個 thread 都有自己的一份 register 和 local memory 的空間。同一個 block 中的每個thread 則有共享的一份 share memory。此外,所有的 thread(包括不同 block 的 thread)都共享一份 global memory、constant memory、和 texture memory。不同的 grid 則有各自的 global memory、constant memory 和 texture memory。

大家可能注意到不同block之間是無法進行同步工作的,不過,在我們的程式中,其實不太需要進行 thread 的同步動作,是以我們可以使用多個 block 來進一步增加thread 的數目。

下面我們就開始繼續修改我們的程式:

先貼一下之前的完整代碼:

我們要去加入多個block來繼續增加我們的線程數量:

首先define一個block的數目

我們準備建立 32 個 blocks,每個 blocks 有 256個 threads,也就是說總共有 32*256= 8192個threads,這裡有一個問題,我們為什麼不用極限的1024個線程呢?那樣就是32*1024 = 32768 個線程,難道不是更好嗎?其實并不是這樣的,從線程運作的原理來看,線程數量達到一定大小後,我們再一味的增加線程也不會取得性能提升了,反而有可能會讓性能下降,感興趣的同學可以改一下數量試一下。另外我們的加和部分是在cpu上進行的,越多的線程意味着越多的結果,而這也意味着cpu上的運算壓力會越來越大。

接着,我們需要修改kernel 部份,加入bid = blockidx.x:

關于修改注釋已經寫得很清楚了。

blockidx.x 和 threadidx.x 一樣是 cuda 内建的變量,它表示的是目前的 block 編号。

另外我們把計算時間的方式改成每個 block 都會記錄開始時間及結束時間。

是以我們result和time變量的長度需要進行更改:

然後在調用核函數的時候,把控制block數量的的參數改成我們的block數:

注意從顯存複制回記憶體的部分也需要修改(由于result和time長度的改變):

此外,由于涉及到block,我們需要采取不同的計時方式,即把每個 block 最早的開始時間,和最晚的結束時間相減,取得總運作時間。

完整程式:

運作結果:

CUDA程式設計(六)進一步并行CUDA程式設計(六)

為了對比我們把block改成1再運作一次:

CUDA程式設計(六)進一步并行CUDA程式設計(六)

我們看到32block 256 thread 連續存取的情況下運作用了133133個時鐘周期

而在 1block 256 thread 連續存取的情況下運作用了3488971個時鐘周期

可以看到我們的速度整整提升了26倍,這個版本的程式,執行的時間減少很多。

我們還是從記憶體帶寬的角度來進行一下評估:

首先計算一下使用的時間:

然後計算使用的帶寬:

資料量仍然沒有變 data_size 1048576,也就是1024*1024 也就是 1m

1m 個 32 bits 數字的資料量是 4mb。

是以,這個程式實際上使用的記憶體帶寬約為:

這對于我這塊640,頻率僅有797000,已經是一個很不錯的效果了,不過,這個程式雖然在gpu上節省了時間,但是在 cpu 上執行的部份,需要的時間加長了(因為 cpu 現在需要加總 8192 個數字)。為了避免這個問題,下一步我們可以讓每個 block 把自己的每個 thread 的計算結果進行加總。

之前中間提過我們為什麼不用更多的線程,比如一個block 1024個,或者更多的block。這是因為從線程運作的原理來看,線程數量達到一定大小後,我們再一味的增加線程也不會取得性能提升了,反而有可能會讓性能下降。我們可以試驗一下:

1024thread *128block = 101372 個 thread 夠多了吧,我們看下運作結果:

CUDA程式設計(六)進一步并行CUDA程式設計(六)

我們看到最終用了153292個時鐘周期,勁爆的10萬個線程真的變慢了。

為什麼會這樣呢?下面我們從gpu的原理上來講解這個問題。

之前關于為什麼線程不能這麼多的問題,說的還是不是很清楚,其實從硬體角度分析,支援cuda的nvidia 顯示卡,都是由多個multiprocessors 組成。每個 multiprocessor 裡包含了8個stream processors,其組成是四個四個一組,也就是兩組4d的處理器。

每個 multiprocessor 還具有 很多個(比如8192個)寄存器,一定的(比如16kb) share memory,以及 texture cache 和 constant cache

CUDA程式設計(六)進一步并行CUDA程式設計(六)

在 cuda 中,大部份基本的運算動作,都可以由 stream processor 進行。每個 stream processor 都包含一個 fma(fused-multiply-add)單元,可以進行一個乘法和一個加法。比較複雜的運算則會需要比較長的時間。

在執行 cuda 程式的時候,每個 stream processor 就是對應一個 thread。每個 multiprocessor 則對應一個 block。但是我們一個block往往有很大量的線程,題主用到了640個和1024個,遠超一個 multiprocessor 所有的8個 stream processor 。

實際上,雖然一個 multiprocessor 隻有八個 stream processor,但是由于 stream processor 進行各種運算都有 latency,更不用提記憶體存取的 latency,是以 cuda 在執行程式的時候,是以warp 為機關。

比如一個 warp 裡面有 32 個 threads,分成兩組 16 threads 的 half-warp。由于 stream processor 的運算至少有 4 cycles 的 latency,是以對一個 4d 的stream processors 來說,一次至少執行 16 個 threads(即 half-warp)才能有效隐藏各種運算的 latency。也是以,線程數達到隐藏各種latency的程度後,之後數量的提升就沒有太大的作用了。

還有一個重要的原因是,由于 multiprocessor 中并沒有太多别的記憶體,是以每個 thread 的狀态都是直接儲存在multiprocessor 的寄存器中。是以,如果一個 multiprocessor 同時有愈多的 thread 要執行,就會需要愈多的寄存器空間。例如,假設一個 block 裡面有 256 個 threads,每個 thread 用到20 個寄存器,那麼總共就需要 256x20 = 5,120 個寄存器才能儲存每個 thread 的狀态。

而一般每個 multiprocessor 隻有 8,192 個寄存器,是以,如果每個 thread 使用到16 個寄存器,那就表示一個 multiprocessor 的寄存器同時最多隻能維持 512 個 thread 的執行。如果同時進行的 thread 數目超過這個數字,那麼就會需要把一部份的資料儲存在顯示卡記憶體中,就會降低執行的效率了。

這篇部落客要使用block進行了進一步增大了線程數,進行進一步的并行,最終的結果還是比較令人滿意的,至少對于我這塊顯示卡來說已經很不錯了,因為我的顯示卡主頻比較低,如果用一塊1.5ghz的顯示卡,用的時間就會是我的一半,而這時候記憶體帶寬也就基本達到45gb/s左右了。

同時也回答了很多人都會有的疑問,即為什麼我們不搞幾萬個線程。

但是我們也看到了新的問題,我們在cpu端的加和壓力變得很大,那麼我們能不能從gpu上直接完成這個工作呢?我們知道每個block内部的thread之間是可以同步和通訊的,下一步我們将讓每個block把每個thread的計算結果進行加和。

希望我的部落格能幫助到大家~

參考資料:《深入淺出談cuda》