并行編程——Lesson2:GPU硬件和并行通信模式

前言

《并行編程》系列是學習《Intro to Parallel Programming》過程中所做的筆記記錄以及個人一些所思所想。

并行通信

并行計算需要解決的一個問題就是,如何解決線程之間的協同工作(Working together)問題。而協同工作的關鍵則是通信(Communication)。

CUDA 的通信發生在內存當中,例如,多個線程可能需要從同一個內存地址中讀取數據;也可能出現多個線程需要同時向同一個地址寫入數據;可能出現多個線程之間需要交換數據。

并行通信模式(Parallel Communication Patterns)

并行通信存在多種模式,通信模式反映了線程所執行的任務與內存之間的映射關系。這里將分別介紹五種通信模式:

  1. Map
  2. Gather
  3. Scatter
  4. Stencil
  5. Transpose

Map

Map: Tasks read from and write to specific data elements.

Map 模式下,每個線程將從內存的特定地址中讀取數據進行處理,然后再寫入特定的地址中,它的輸入與輸出具有嚴格的一對一的關系。

Map 模式在 GPU 中非常高效,在 CUDA 中也能很容易通過有效的方式表達。

但是 Map 比較不靈活,能處理的問題有限。

Gather

現在假設需要求取3個數據的平均值,那么在 Gather 模式下,每個線程將從內存中的三個位置讀取數據,然后將這三個數取平均,寫入指定的內存中。

這一模式可用于涉及到濾波器的一系列操作。

Scatter

Scatter: Tasks compute where to write output.

與 Gather 模式下,多個輸入一個輸出相反,Scatter 模式是一個輸入多個輸出。

另外在同時寫入多個輸出的時候將出現沖突的問題,這將在后續進行討論。

Stencil

Stencil: Tasks read input from a fixed neighborhood in an array.

常用的模板有:

  1. 2D von Neumann


  1. 2D Moore


  1. 3D von Neumann


看到這里,可能有人會對 Stencil 和 Gather 產生疑惑。咋看之下,兩者確實非常相似,但是 Stencil 模式中,要求每個線程都嚴格執行相同的模板,但是 Gather 模式卻沒有這個限制,因此,比如說,在 Gather 模式中就可以按線程索引的奇偶不同,給線程分配不同的操作任務。

Transpose

Transpose: Tasks re-order data elements in memory.

對于一張圖像,其數據在內存中的存儲的方式如下:

但是在某些情況下,可能需要將圖像轉置。

通常在涉及到數組運算、矩陣運算圖像操作的時候會需要使用到 Transpose,但是 Transpose 也適用于其它數據結構。

比如定義了一個結構體 foo ,然后創建一個該結構的結構數組(AoS),如果想將該結構數組變換成數組結構(SoA),也可以通過 Transpose 實現。

總結

上圖總結了并行計算的七種計算模式,除了之前介紹的五種模式以外,還有兩種更加基礎的模式將在接下來進行介紹。

GPU

程序員眼中的 GPU

程序員在并行編程中所要做的就是,創建內核(C/C++函數)用來處理具體的任務。內核由許多線程(完整執行一段處理程序的通路)組成,圖中的線程都采用曲線繪制,其原因是,每個線程的具體通路可能不相同(即每個線程所執行的運算不相同)。

多個線程將組成線程塊,一個線程塊內的多個線程負責協同處理一項任務或者子任務。

上圖中,程序首先啟動了一個內核 foo,等到其中所有的線程都運行完了之后,結束內核。然后又啟動了內核 bar,可以注意到,一個內核中所具有的線程塊,以及每個線程塊中的線程數是可以自己配置的參數。

線程塊與 GPU

GPU 中包含有許多的流處理器(Streaming Multiprocessor, SM),不同的 GPU 包含有不同數量的流處理器,并且流處理器數量也是衡量 GPU 性能的一項重要指標。

一個流處理器中包含有多個簡單的處理器內存。

當你的程序創建了內核之后,GPU 將為內核中的線程塊分配流處理器,每個線程塊被分配給一個流處理器,然后這些流處理器以并行的方式進行運行。

注意:一個流處理器上允許運行多個線程塊,但是一個線程塊只允許被分配給一個流處理器運行。

CUDA 特征

CUDA 不具備的特征

CUDA 對于內核中的線程塊要何時運行、該如何分配流處理以及有多少線程塊需要同時運行等細節沒有進行任何的控制,這些分配問題都交給 GPU 進行控制。這么做的好處有:

  • 硬件將可以更加高效地執行計算
  • 當一個線程塊執行完成之后,當前的流處理器馬上又可以任意執行下一個線程塊
  • 更高的擴展性。因為流處理器的分配交由硬件控制,所以程序可以很好地在具有不同流處理器數量的設備上進行移植。

但是 CUDA 的這種做法也將導致一些后果:

  • 對于某一線程塊將在哪個流處理器上運行無法做出任何預測
  • 線程塊之間沒有通信交流。如果線程塊 x 的輸入依賴于線程塊 y 的輸出,而 y 已經完成執行并且退出,這將導致 x 的計算出現問題。這種現象稱為“dead lock”
  • 線程塊中的線程不能永遠執行(比如,死循環),因為它需要在執行完成之后釋放流處理器資源,以便于其它線程塊可以使用

CUDA 具備的特征

CUDA 在程序運行的時候,能夠保證兩點:

  • 同一個線程塊上的所有線程將同時在同一個流處理器中運行。
  • 下一個內核中的線程塊必須等待當前內核中的所有塊運行完成之后,才能運行。
    • 比如說,程序依次定義了兩個內核 foobar ,bar 中的線程塊只有等到 foo 中的所有線程塊都運行完之后才能開始運行。

GPU 內存模型

每個線程都擁有一個局部內存(Local memory),這就好像局部變量一樣,只有對應的線程才能訪問。

然后,線程塊也有一塊對應的共享內存(Shared memory)。共享內存只能被對應線程塊內的線程進行訪問。

另外還有具有全局內存(Global memory)。不僅內核中的所有線程可以訪問它,不同內核也可以進行訪問。

前邊介紹的局部內存、共享內存和全局內存都是屬于 GPU 內部的內存。上圖展示了,CPU 的線程啟動了 GPU ,然后將主機內存(Host memory)中的數據拷貝到 GPU 的全局內存中,以便于 GPU 內核線程可以訪問這些數據。另外 GPU 內核線程也可以直接訪問主機內存,這一點將在后邊介紹。

同步

通過共享內存和全局內存,線程之間可以互相訪問彼此的計算結果,這也意味著線程間可以進行協同計算。但是這樣也存在著風險, 如果一個線程在另一個線程寫入數據之前就讀取了數據怎么辦?因此線程之間需要同步的機制,來避免這種情形出現。

事實上,同步問題是并行計算的一個最基本的問題。而解決同步問題的一個最簡單方法則是屏障(Barrier)。

Barrier: Point in the program where threads stop and wait. When all threads have reached the barrier, they can proceed.

屏障語句是 __syncthreads()

編程模型

現在,可以重新構建一下編程模型。我們擁有線程和線程塊,并且在線程塊內,可以創建屏障用于同步線程。事實上,如果一個程序中創建了多個內核,內核之間默認具有隱性的屏障,這使得不會出現多個內核同時運行的情況。

然后再將之前介紹的內存模型添加進來,便得到了 CUDA 。

因此,CUDA 的核心就是層級計算結構。從線程到線程塊再到內核,對應著內存空間中的局部內存、共享內存和全局內存。

編寫高效的 CUDA 程序

這里將首先從頂層的策略上介紹如何編寫高效的 CUDA 程序。

首先需要知道的是 GPU 具有非常驚人的計算能力,一個高端的 GPU 可以實現每秒超過 3 萬億次的數學運算(3 TFLOPS/s)。但是如果一個 CUDA 程序的大多數時間都花費在了等待內存的讀取或寫入操作的話,這就相當浪費計算能力。所以要編寫高效的 CUDA 程序的第一點是——最大化計算強度

計算強度表達為每個線程計算操作時間除以每個線程在的訪存時間。所以要最大化計算強度,就可以通過最大化分子和最小化分母來實現。然而由于計算操作時間主要受具體算法的計算量限制,所以為了最大化計算強度主要從最小化訪存時間入手。

最小化訪存時間

要最小化訪存時間的一種方式就是,將訪問頻率更高的數據移動到訪問速度更快的內存中。

在之前的介紹當中已經了解了 GPU 線程可以訪問四種類型的內存,其中最快就是局部內存。

局部內存

局部變量的定義是最簡單的。

對于上圖的內核代碼,變量 f 與參數 in 都將存儲于局部內存中。

共享內存

要定義存儲于共享內存中的變量,需要在變量定義語句前加一個 __shared__ 關鍵字進行修飾。定義于共享內存中的變量可以被同一個線程塊中的所有線程所訪問,其生存時間為線程塊的生存時間。

全局內存

全局的內存訪問要稍微麻煩些,但是可以通過指針的機制來實現。

這里傳入內核的參數被定義成一個指針,而這個指針恰恰指向的是全局內存區域。

然后在 CPU 的代碼部分,首先創建了一個長度為 128 的浮點數數組 h_arr,它將存儲于主機內存中(這里通過前綴 h_ 表明當前變量運行于 HOST 中),然后定義了一個指向 GPU 全局內存的指針 d_arr,并通過 cudaMalloc 函數為 d_arr 分配全局存儲區域。

最小化訪存時間的另一個方法是使用合并全局內存訪問(Coalesce global memory accesses)。

單一線程在訪問內存時具有一個特性,就是即使該線程只需要使用到內存中的一小部分,但是程序也會從內存中讀取一段連續的內存塊。因此,如果此時恰好有其它線程也在使用該內存塊中的數據,內存塊就得到復用,從而節省再次讀取內存的時間。

所以如果多個線程同時讀取或者寫入連續的全局內存位置,此時 GPU 的效率的是最高的,而這種訪問模式被稱為合并(Coalesced)

但是當多個線程所訪問的全局內存位置不連續或者甚至隨機的時候,此時 GPU 便無法繼續保持高效,因為很可能需要分別讀取全局內存中的多個塊,這樣就增加了訪存時間。

相關性問題(Related problem)

Related problem: lots of threads reading and writing same memory locations

當多個線程同時參與到對同一塊內存地址的讀寫操作時,將引發沖突從而導致錯誤的計算結果,這便是相關性問題。

解決該相關性問題的一個方法是使用原子內存操作(Atomic memory operations)。

原子內存操作

CUDA 提供了若干個原子內存操作函數,通過這些函數可以以原子操作的方式訪問內存,也就是某一時刻內存中的特定地址只能被單一線程所讀寫,從而避免了相關性問題。

常見的原子內存操作:

  • atomicAdd(),原子相加
  • atomicMin(),原子最小值
  • atomicXOR(),原子異或
  • atomicCAS(),比較并且交換(Compare-and-Swap)

說明:這些原子內存操作函數的實現借助了硬件來實現原子操作,這里將不進行介紹。

但是這些原子操作也存在一些局限性。

  • 只支持某些特定的操作(比如,支持加、減、最小值和異或等,不支持求余、求冪等操作)和數據類型(主要支持整數)。
  • 沒有順序限制。盡管使用了原子操作,但是關于線程執行順序的問題依然沒有定義。
    • 由于浮點數精度問題,這將導致浮點數運算出現非關聯現象(Non-associative)。具體來說就是可能出現 (a + b) + c != a + (b + c),比如,當 a = 1, b = 10^99, c= 10^-99 時。
  • 串行化線程內存訪問。原子操作的實現并沒有使用什么神奇的魔法,它僅僅只是串行化了線程對同一個內存地址的訪問,所以這將減慢整體的計算速度。

線程發散

前邊已經介紹過了,要使得 CUDA 程序高效的一個關鍵點是——最大化計算強度。然后另外一個關鍵點是——避免線程發散(Thread divergence)。

線程發散指的是,比如說當內核代碼中出現條件語句時,線程運行到條件語句處,可能有些線程符合條件,而有些線程不符合條件,此時它們就會發散開,形成兩條路徑,然后在條件語句塊結束之后再次聚合到同一條路徑上。

不僅僅只有條件語句才會導致線程發散,循環語句也可能導致。

舉個不太恰當的例子,在這個內核代碼中有一個循環,循環的次數是當前線程的索引。

所以線程的執行路徑如上圖,如果以時間為橫軸繪制線程運行圖則如下圖。

由于硬件傾向于同時執行完線程,所以當線程索引小的線程完成循環之后,它還會繼續等待其它線程完成循環,直至所有線程都完成循環之后,這些線程才會繼續執行循環塊之后的代碼。因此,這里除了最后一個線程充分利用了時間進行運算以外,其它線程均無法有效利用時間。而這也就是為什么要避免線程發散的原因。

總結

本節內容小結:

  • 通信模式
    • gather, scatter, stencil, transpose
  • GPU 硬件與編程模型
    • 流處理器,線程,線程塊
    • 線程同步
    • 內存模型(局部,共享,全局,主機),原子操作
  • 高效 GPU 編程
    • 減少訪存花銷(使用更快的內存,合并全局內存訪問)
    • 避免線程發散

課堂作業

本次的課堂作業是實現圖像模糊,思路相對較簡單。唯一需要注意的是邊界情況的取值。因為當 filter 的中心位于圖像邊界的時候,它的周圍像素會出現超出圖像的現象,這里需要進行判斷。

課程作業完成代碼:
https://github.com/un-knight/cs344-parallel-programming

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,763評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377

推薦閱讀更多精彩內容

  • CUDA從入門到精通(零):寫在前面 本文原版鏈接: 在老板的要求下,本博主從2012年上高性能計算課程開始接觸C...
    Pitfalls閱讀 3,638評論 1 3
  • 1. CPU vs. GPU 1.1 四種計算機模型 GPU設計的初衷就是為了減輕CPU計算的負載,將一部分圖形計...
    王偵閱讀 20,982評論 3 20
  • 前言 《并行編程》系列是學習《Intro to Parallel Programming》過程中所做的筆記記錄以及...
    葉俊賢閱讀 6,843評論 0 7
  • 一直很好奇GPU做矩陣運算是怎么并行加速的,今天看了一些粗淺的東西,并總結整理出來。version:cuda 8 ...
    bidai541閱讀 10,352評論 0 3
  • CUDA是什么 CUDA,ComputeUnifiedDeviceArchitecture的簡稱,是由NVIDIA...
    Pitfalls閱讀 9,511評論 0 1