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

前言

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

并行通信

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

CUDA 的通信發(fā)生在內(nèi)存當(dāng)中甲雅,例如履怯,多個線程可能需要從同一個內(nèi)存地址中讀取數(shù)據(jù)艇潭;也可能出現(xiàn)多個線程需要同時向同一個地址寫入數(shù)據(jù)拼窥;可能出現(xiàn)多個線程之間需要交換數(shù)據(jù)。

并行通信模式(Parallel Communication Patterns)

并行通信存在多種模式蹋凝,通信模式反映了線程所執(zhí)行的任務(wù)與內(nèi)存之間的映射關(guān)系鲁纠。這里將分別介紹五種通信模式:

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

Map

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

Map 模式下,每個線程將從內(nèi)存的特定地址中讀取數(shù)據(jù)進(jìn)行處理仙粱,然后再寫入特定的地址中,它的輸入與輸出具有嚴(yán)格的一對一的關(guān)系彻舰。

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

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

Gather

現(xiàn)在假設(shè)需要求取3個數(shù)據(jù)的平均值,那么在 Gather 模式下尚胞,每個線程將從內(nèi)存中的三個位置讀取數(shù)據(jù)硬霍,然后將這三個數(shù)取平均,寫入指定的內(nèi)存中笼裳。

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

Scatter

Scatter: Tasks compute where to write output.

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

另外在同時寫入多個輸出的時候?qū)⒊霈F(xiàn)沖突的問題抽减,這將在后續(xù)進(jìn)行討論。

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 產(chǎn)生疑惑卵沉。咋看之下,兩者確實非常相似法牲,但是 Stencil 模式中史汗,要求每個線程都嚴(yán)格執(zhí)行相同的模板,但是 Gather 模式卻沒有這個限制拒垃,因此停撞,比如說,在 Gather 模式中就可以按線程索引的奇偶不同恶复,給線程分配不同的操作任務(wù)怜森。

Transpose

Transpose: Tasks re-order data elements in memory.

對于一張圖像,其數(shù)據(jù)在內(nèi)存中的存儲的方式如下:

但是在某些情況下谤牡,可能需要將圖像轉(zhuǎn)置副硅。

通常在涉及到數(shù)組運算矩陣運算圖像操作的時候會需要使用到 Transpose翅萤,但是 Transpose 也適用于其它數(shù)據(jù)結(jié)構(gòu)恐疲。

比如定義了一個結(jié)構(gòu)體 foo ,然后創(chuàng)建一個該結(jié)構(gòu)的結(jié)構(gòu)數(shù)組(AoS)套么,如果想將該結(jié)構(gòu)數(shù)組變換成數(shù)組結(jié)構(gòu)(SoA)培己,也可以通過 Transpose 實現(xiàn)。

總結(jié)

上圖總結(jié)了并行計算的七種計算模式胚泌,除了之前介紹的五種模式以外省咨,還有兩種更加基礎(chǔ)的模式將在接下來進(jìn)行介紹。

GPU

程序員眼中的 GPU

程序員在并行編程中所要做的就是玷室,創(chuàng)建內(nèi)核(C/C++函數(shù))用來處理具體的任務(wù)零蓉。內(nèi)核由許多線程(完整執(zhí)行一段處理程序的通路)組成,圖中的線程都采用曲線繪制穷缤,其原因是敌蜂,每個線程的具體通路可能不相同(即每個線程所執(zhí)行的運算不相同)。

多個線程將組成線程塊津肛,一個線程塊內(nèi)的多個線程負(fù)責(zé)協(xié)同處理一項任務(wù)或者子任務(wù)章喉。

上圖中,程序首先啟動了一個內(nèi)核 foo,等到其中所有的線程都運行完了之后秸脱,結(jié)束內(nèi)核落包。然后又啟動了內(nèi)核 bar,可以注意到撞反,一個內(nèi)核中所具有的線程塊妥色,以及每個線程塊中的線程數(shù)是可以自己配置的參數(shù)。

線程塊與 GPU

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

一個流處理器中包含有多個簡單的處理器內(nèi)存吮便。

當(dāng)你的程序創(chuàng)建了內(nèi)核之后笔呀,GPU 將為內(nèi)核中的線程塊分配流處理器,每個線程塊被分配給一個流處理器髓需,然后這些流處理器以并行的方式進(jìn)行運行许师。

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

CUDA 特征

CUDA 不具備的特征

CUDA 對于內(nèi)核中的線程塊要何時運行微渠、該如何分配流處理以及有多少線程塊需要同時運行等細(xì)節(jié)沒有進(jìn)行任何的控制,這些分配問題都交給 GPU 進(jìn)行控制咧擂。這么做的好處有:

  • 硬件將可以更加高效地執(zhí)行計算
  • 當(dāng)一個線程塊執(zhí)行完成之后逞盆,當(dāng)前的流處理器馬上又可以任意執(zhí)行下一個線程塊
  • 更高的擴展性。因為流處理器的分配交由硬件控制松申,所以程序可以很好地在具有不同流處理器數(shù)量的設(shè)備上進(jìn)行移植云芦。

但是 CUDA 的這種做法也將導(dǎo)致一些后果:

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

CUDA 具備的特征

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

  • 同一個線程塊上的所有線程將同時在同一個流處理器中運行旗笔。
  • 下一個內(nèi)核中的線程塊必須等待當(dāng)前內(nèi)核中的所有塊運行完成之后,才能運行离例。
    • 比如說换团,程序依次定義了兩個內(nèi)核 foobar 悉稠,bar 中的線程塊只有等到 foo 中的所有線程塊都運行完之后才能開始運行宫蛆。

GPU 內(nèi)存模型

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

然后想虎,線程塊也有一塊對應(yīng)的共享內(nèi)存(Shared memory)。共享內(nèi)存只能被對應(yīng)線程塊內(nèi)的線程進(jìn)行訪問叛拷。

另外還有具有全局內(nèi)存(Global memory)舌厨。不僅內(nèi)核中的所有線程可以訪問它,不同內(nèi)核也可以進(jìn)行訪問忿薇。

前邊介紹的局部內(nèi)存裙椭、共享內(nèi)存和全局內(nèi)存都是屬于 GPU 內(nèi)部的內(nèi)存。上圖展示了署浩,CPU 的線程啟動了 GPU 揉燃,然后將主機內(nèi)存(Host memory)中的數(shù)據(jù)拷貝到 GPU 的全局內(nèi)存中,以便于 GPU 內(nèi)核線程可以訪問這些數(shù)據(jù)筋栋。另外 GPU 內(nèi)核線程也可以直接訪問主機內(nèi)存炊汤,這一點將在后邊介紹。

同步

通過共享內(nèi)存和全局內(nèi)存弊攘,線程之間可以互相訪問彼此的計算結(jié)果抢腐,這也意味著線程間可以進(jìn)行協(xié)同計算。但是這樣也存在著風(fēng)險襟交, 如果一個線程在另一個線程寫入數(shù)據(jù)之前就讀取了數(shù)據(jù)怎么辦迈倍?因此線程之間需要同步的機制,來避免這種情形出現(xiàn)婿着。

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

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

屏障語句是 __syncthreads() 提完。

編程模型

現(xiàn)在,可以重新構(gòu)建一下編程模型丘侠。我們擁有線程和線程塊徒欣,并且在線程塊內(nèi),可以創(chuàng)建屏障用于同步線程蜗字。事實上打肝,如果一個程序中創(chuàng)建了多個內(nèi)核,內(nèi)核之間默認(rèn)具有隱性的屏障挪捕,這使得不會出現(xiàn)多個內(nèi)核同時運行的情況粗梭。

然后再將之前介紹的內(nèi)存模型添加進(jìn)來,便得到了 CUDA 级零。

因此断医,CUDA 的核心就是層級計算結(jié)構(gòu)。從線程到線程塊再到內(nèi)核,對應(yīng)著內(nèi)存空間中的局部內(nèi)存鉴嗤、共享內(nèi)存和全局內(nèi)存斩启。

編寫高效的 CUDA 程序

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

首先需要知道的是 GPU 具有非常驚人的計算能力醉锅,一個高端的 GPU 可以實現(xiàn)每秒超過 3 萬億次的數(shù)學(xué)運算(3 TFLOPS/s)兔簇。但是如果一個 CUDA 程序的大多數(shù)時間都花費在了等待內(nèi)存的讀取或?qū)懭氩僮鞯脑挘@就相當(dāng)浪費計算能力硬耍。所以要編寫高效的 CUDA 程序的第一點是——最大化計算強度垄琐。

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

最小化訪存時間

要最小化訪存時間的一種方式就是朦前,將訪問頻率更高的數(shù)據(jù)移動到訪問速度更快的內(nèi)存中。

在之前的介紹當(dāng)中已經(jīng)了解了 GPU 線程可以訪問四種類型的內(nèi)存鹃操,其中最快就是局部內(nèi)存韭寸。

局部內(nèi)存

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

對于上圖的內(nèi)核代碼荆隘,變量 f 與參數(shù) in 都將存儲于局部內(nèi)存中恩伺。

共享內(nèi)存

要定義存儲于共享內(nèi)存中的變量,需要在變量定義語句前加一個 __shared__ 關(guān)鍵字進(jìn)行修飾椰拒。定義于共享內(nèi)存中的變量可以被同一個線程塊中的所有線程所訪問晶渠,其生存時間為線程塊的生存時間。

全局內(nèi)存

全局的內(nèi)存訪問要稍微麻煩些燃观,但是可以通過指針的機制來實現(xiàn)褒脯。

這里傳入內(nèi)核的參數(shù)被定義成一個指針,而這個指針恰恰指向的是全局內(nèi)存區(qū)域缆毁。

然后在 CPU 的代碼部分番川,首先創(chuàng)建了一個長度為 128 的浮點數(shù)數(shù)組 h_arr,它將存儲于主機內(nèi)存中(這里通過前綴 h_ 表明當(dāng)前變量運行于 HOST 中)脊框,然后定義了一個指向 GPU 全局內(nèi)存的指針 d_arr颁督,并通過 cudaMalloc 函數(shù)為 d_arr 分配全局存儲區(qū)域。

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

單一線程在訪問內(nèi)存時具有一個特性沉御,就是即使該線程只需要使用到內(nèi)存中的一小部分,但是程序也會從內(nèi)存中讀取一段連續(xù)的內(nèi)存塊昭灵。因此吠裆,如果此時恰好有其它線程也在使用該內(nèi)存塊中的數(shù)據(jù)聂儒,內(nèi)存塊就得到復(fù)用,從而節(jié)省再次讀取內(nèi)存的時間硫痰。

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

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

相關(guān)性問題(Related problem)

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

當(dāng)多個線程同時參與到對同一塊內(nèi)存地址的讀寫操作時护侮,將引發(fā)沖突從而導(dǎo)致錯誤的計算結(jié)果敌完,這便是相關(guān)性問題。

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

原子內(nèi)存操作

CUDA 提供了若干個原子內(nèi)存操作函數(shù)滨溉,通過這些函數(shù)可以以原子操作的方式訪問內(nèi)存,也就是某一時刻內(nèi)存中的特定地址只能被單一線程所讀寫长赞,從而避免了相關(guān)性問題晦攒。

常見的原子內(nèi)存操作:

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

說明:這些原子內(nèi)存操作函數(shù)的實現(xiàn)借助了硬件來實現(xiàn)原子操作贩据,這里將不進(jìn)行介紹栋操。

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

  • 只支持某些特定的操作(比如饱亮,支持加矾芙、減、最小值和異或等近上,不支持求余蠕啄、求冪等操作)和數(shù)據(jù)類型(主要支持整數(shù))。
  • 沒有順序限制戈锻。盡管使用了原子操作歼跟,但是關(guān)于線程執(zhí)行順序的問題依然沒有定義。
    • 由于浮點數(shù)精度問題格遭,這將導(dǎo)致浮點數(shù)運算出現(xiàn)非關(guān)聯(lián)現(xiàn)象(Non-associative)哈街。具體來說就是可能出現(xiàn) (a + b) + c != a + (b + c),比如拒迅,當(dāng) a = 1, b = 10^99, c= 10^-99 時骚秦。
  • 串行化線程內(nèi)存訪問她倘。原子操作的實現(xiàn)并沒有使用什么神奇的魔法,它僅僅只是串行化了線程對同一個內(nèi)存地址的訪問作箍,所以這將減慢整體的計算速度硬梁。

線程發(fā)散

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

線程發(fā)散指的是,比如說當(dāng)內(nèi)核代碼中出現(xiàn)條件語句時阶剑,線程運行到條件語句處跃巡,可能有些線程符合條件,而有些線程不符合條件牧愁,此時它們就會發(fā)散開素邪,形成兩條路徑,然后在條件語句塊結(jié)束之后再次聚合到同一條路徑上猪半。

不僅僅只有條件語句才會導(dǎo)致線程發(fā)散兔朦,循環(huán)語句也可能導(dǎo)致。

舉個不太恰當(dāng)?shù)睦幽ト罚谶@個內(nèi)核代碼中有一個循環(huán)烘绽,循環(huán)的次數(shù)是當(dāng)前線程的索引。

所以線程的執(zhí)行路徑如上圖俐填,如果以時間為橫軸繪制線程運行圖則如下圖安接。

由于硬件傾向于同時執(zhí)行完線程,所以當(dāng)線程索引小的線程完成循環(huán)之后英融,它還會繼續(xù)等待其它線程完成循環(huán)盏檐,直至所有線程都完成循環(huán)之后,這些線程才會繼續(xù)執(zhí)行循環(huán)塊之后的代碼驶悟。因此胡野,這里除了最后一個線程充分利用了時間進(jìn)行運算以外,其它線程均無法有效利用時間痕鳍。而這也就是為什么要避免線程發(fā)散的原因硫豆。

總結(jié)

本節(jié)內(nèi)容小結(jié):

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

課堂作業(yè)

本次的課堂作業(yè)是實現(xiàn)圖像模糊铭若,思路相對較簡單洪碳。唯一需要注意的是邊界情況的取值递览。因為當(dāng) filter 的中心位于圖像邊界的時候,它的周圍像素會出現(xiàn)超出圖像的現(xiàn)象瞳腌,這里需要進(jìn)行判斷绞铃。

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市嫂侍,隨后出現(xiàn)的幾起案子儿捧,更是在濱河造成了極大的恐慌,老刑警劉巖吵冒,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異西剥,居然都是意外死亡痹栖,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進(jìn)店門瞭空,熙熙樓的掌柜王于貴愁眉苦臉地迎上來揪阿,“玉大人,你說我怎么就攤上這事咆畏∧衔妫” “怎么了?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵旧找,是天一觀的道長溺健。 經(jīng)常有香客問我,道長钮蛛,這世上最難降的妖魔是什么鞭缭? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮魏颓,結(jié)果婚禮上岭辣,老公的妹妹穿的比我還像新娘。我一直安慰自己甸饱,他們只是感情好沦童,可當(dāng)我...
    茶點故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著叹话,像睡著了一般偷遗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上驼壶,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天鹦肿,我揣著相機與錄音,去河邊找鬼辅柴。 笑死箩溃,一個胖子當(dāng)著我的面吹牛瞭吃,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播涣旨,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼歪架,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了霹陡?” 一聲冷哼從身側(cè)響起和蚪,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎烹棉,沒想到半個月后攒霹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡浆洗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年催束,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片伏社。...
    茶點故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡抠刺,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出摘昌,到底是詐尸還是另有隱情速妖,我是刑警寧澤,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布聪黎,位于F島的核電站罕容,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏稿饰。R本人自食惡果不足惜杀赢,卻給世界環(huán)境...
    茶點故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望湘纵。 院中可真熱鬧脂崔,春花似錦、人聲如沸梧喷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽铺敌。三九已至汇歹,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間偿凭,已是汗流浹背产弹。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人痰哨。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓胶果,卻偏偏與公主長得像,于是被迫代替她去往敵國和親斤斧。 傳聞我的和親對象是個殘疾皇子早抠,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,691評論 2 361

推薦閱讀更多精彩內(nèi)容

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