【Graphics Pipeline 2011】Compute Shaders

原文鏈接

下面來(lái)介紹下本系列的最后一篇內(nèi)容:CS。

Execution environment

這個(gè)系列關(guān)注的是架構(gòu)層面的數(shù)據(jù)流,而非具體的shader執(zhí)行過(guò)程。到目前為止介紹的各個(gè)stage,注意力都放在進(jìn)入各個(gè)stage的輸入以及各個(gè)stage的輸出,內(nèi)部工作情況通??梢杂蓴?shù)據(jù)的情況來(lái)推導(dǎo)。而CS的情況跟這些stage有所不同,因?yàn)檫@是一個(gè)單一的而非嵌入到整條管線的stage,也正因?yàn)檫@個(gè)原因,所以用于CS計(jì)算的硬件單元在芯片上的表面積相比其他硬件而言也要小得多。

事實(shí)上,除了從API state中得來(lái)的數(shù)據(jù)比如說(shuō)bound Constant Buffers以及資源之外,CS基本上不需要為輸入數(shù)據(jù)進(jìn)行buffering處理,唯一的一個(gè)就是線程索引(thread index)。這里可能會(huì)讓人產(chǎn)生誤解,因此大家需要記住:CS中的線程指的是dispatch的原子單元(atomic unit),與大家印象中操作系統(tǒng)中的線程有著不小的區(qū)別。CS中的線程有自己的ID以及寄存器,但是沒(méi)有自己的指令計(jì)數(shù)器(Program Counter)或者堆棧(Stack),也不是獨(dú)立scheduled的。

實(shí)際上,CS中的線程的地位跟VS中的頂點(diǎn)以及PS中的像素一樣,它們的表現(xiàn)方式也基本一樣:將一批像素、頂點(diǎn)、線程打包成一個(gè)組(數(shù)目在大概16~64之間),這個(gè)組被稱為Warp(NVidia)或者Wavefront(AMD)(下面統(tǒng)一用Warp表示),各個(gè)元素在lockstep模式下執(zhí)行代碼。CS線程不會(huì)有單獨(dú)的schedule,但是Warp整體會(huì)有:為了避免計(jì)算帶來(lái)的延遲,我們這里雖然不會(huì)將一個(gè)線程切換到另一個(gè)線程,但是會(huì)將一個(gè)Warp切換到另一個(gè)Warp。每個(gè)Warp中的單個(gè)線程并不會(huì)進(jìn)行單獨(dú)的分支路徑計(jì)算,而是跟其他線程同進(jìn)同退,即如果某個(gè)線程需要走A分支,而其他線程需要走B分支,最終的結(jié)果就是所有線程AB分支都要走。簡(jiǎn)而言之,CS中的線程看起來(lái)更像是SIMD中的lanes,而非我們平時(shí)編程中的線程。

上面介紹了線程跟warp的概念,在這個(gè)之上,還有線程組(thread group)的概念。每個(gè)線程組的大小是在shader編譯的時(shí)候確定的,在DX11中,線程組的尺寸是通過(guò)三元組來(lái)指定的XYZ表示的是三個(gè)維度的尺寸。這種機(jī)制是出于對(duì)2D或者3D資源的尋址方便的考慮,此外還有出于對(duì)遍歷算法的性能優(yōu)化的考慮。在宏觀層面,CS的執(zhí)行是分配到多個(gè)線程組,線程組的ID在D3D11中也是使用三元組來(lái)指定的,其基本原理跟前面介紹的線程的三元組原理一致。

線程ID根據(jù)shader的喜好不同,會(huì)按照不同的形式傳遞到CS。這個(gè)ID對(duì)所有的線程來(lái)說(shuō)都是不相同的,是CS的唯一輸入數(shù)據(jù),這一點(diǎn)上面跟其他的shader類型有所不同,當(dāng)然,這只是冰山一角。

Thread Groups

上面的描述會(huì)讓人產(chǎn)生一種線程組是整個(gè)框架層次的一個(gè)比較普通中間部分的錯(cuò)覺(jué)。實(shí)際上,這里還有一點(diǎn)沒(méi)有說(shuō)到:Thread Group Shared Memory(TGSM)。而這個(gè)結(jié)構(gòu)使得線程組變得特別起來(lái)。在DX11硬件中,CS能夠訪問(wèn)的TGSM的大小為32k,這塊空間主要用于同一線程組內(nèi)的線程之間的溝通。這是不同CS線程之間通信的主要方法。

那么這個(gè)東西在硬件上是怎么實(shí)現(xiàn)的呢?比較簡(jiǎn)單:線程組之間的所有的warps都被同一個(gè)shader unit所處理。shader unit使用的本地存儲(chǔ)空間的大小為至少32k(通常會(huì)稍大一點(diǎn))。而由于線程組間的所有線程都共享同一個(gè)shader unit(因此也共享同一套ALU),因此對(duì)于共享內(nèi)存而言,無(wú)需過(guò)于復(fù)雜的同步機(jī)制或者仲裁機(jī)制:只需要限定在任意給定的cycle中,只有一個(gè)Warp能夠訪問(wèn)內(nèi)存(因?yàn)樵谌我獾腸ycle中,只有一個(gè)warp能夠發(fā)起指令(issue instructions))。當(dāng)然,這個(gè)過(guò)程會(huì)被管線化(可以用于并行處理),但是基本的不變性還是保留下來(lái)了的:每個(gè)shader unit,我們都有一個(gè)對(duì)應(yīng)的TGSM,對(duì)TGSM的訪問(wèn)需要跨越多個(gè)管線stage但是真正對(duì)TGSM的讀寫(xiě)只發(fā)生在其中的一個(gè)stage,而在那個(gè)cycle中的所有的內(nèi)存訪問(wèn)都是來(lái)自于同一個(gè)warp中的。

對(duì)于實(shí)際的共享內(nèi)存間的通信,還有一些沒(méi)有描述清楚。上面的不變性只是保證了即使我們不添加任意的interlock來(lái)阻止同時(shí)訪問(wèn),每個(gè)cycle只有一套warp能夠訪問(wèn)TGSM。這種機(jī)制可以使得硬件的設(shè)計(jì)更簡(jiǎn)潔也更快速。但是由于warp的schedule會(huì)存在隨機(jī)性,這里并沒(méi)有保證從shader程序執(zhí)行的角度來(lái)看,訪問(wèn)是按照某種特定的順序進(jìn)行的;訪問(wèn)順序只決定于在某個(gè)時(shí)間點(diǎn)上誰(shuí)是可執(zhí)行的(不需要等待內(nèi)存訪問(wèn)或者貼圖讀取結(jié)束)。如果繼續(xù)深挖下去,實(shí)際上,由于整個(gè)過(guò)程是管線化的,因此對(duì)于TGSM的寫(xiě)操作需要花費(fèi)一些cycle才能變成可見(jiàn)。這種情況在不同的管線stage對(duì)TGSM分別同時(shí)進(jìn)行讀寫(xiě)的時(shí)候會(huì)有影響。因此我們?cè)谶@里依然會(huì)需要一些同步機(jī)制——barriers。barriers有多種類型,但是基本上都是由如下三種元素組成:

  1. 組同步Barrier(Group Synchronization)。 Group同步Barrier會(huì)強(qiáng)制要求當(dāng)前組內(nèi)的所有線程都抵達(dá)某個(gè)barrier之后才能進(jìn)入下一步操作。一旦某個(gè)warp觸發(fā)了這樣一個(gè)barrier,就會(huì)將之標(biāo)記成不可執(zhí)行,看起來(lái)就像是在等待內(nèi)存或者貼圖訪問(wèn)的結(jié)果一樣。而一旦最后的一個(gè)warp抵達(dá)了這個(gè)barrier,就會(huì)將不可執(zhí)行標(biāo)記移除,放開(kāi)權(quán)限,重新激活前面處于等待過(guò)程中的warp。這里添加了一些約束條件,肯定會(huì)導(dǎo)致一些延遲,不過(guò)好處就是不需要原子內(nèi)存交易(atomic memory transactions)或者之類的東西;除了在微觀層面上執(zhí)行率下降之外,整體還算是比較實(shí)惠。

  2. 組內(nèi)存Barrier(Group Memory Barriers)。由于一個(gè)組內(nèi)的所有線程都是在同一個(gè)shader unit中執(zhí)行的,這種barrier的實(shí)現(xiàn)基本上就等同于一次管線flush,用于確保所有處于途中的共享內(nèi)存操作都是被完成的。對(duì)于當(dāng)前的shader unit而言,已經(jīng)無(wú)需再進(jìn)行與外部資源的同步處理,也就是說(shuō)這種方法也是非常廉價(jià)的。

  3. 設(shè)備內(nèi)存Barrier(Device Memory Barriers)。這個(gè)Barrier會(huì)在所有的內(nèi)存訪問(wèn)結(jié)束之前阻止組內(nèi)的所有線程的執(zhí)行——不論是直接的還是間接的(比如貼圖采樣)。如前所述,GPU上的內(nèi)存訪問(wèn)與貼圖訪問(wèn)都有很高的延遲——量化一下,大概是高于600或者通常都是高于1000個(gè)cycle,因此這種Barrier的操作損傷比較高。

DX11提供了將上述三種基本Barrier塞入到一個(gè)原子單元中的不同種類的barriers。

Unordered Access Views

我們已經(jīng)介紹了CS的輸入以及執(zhí)行過(guò)程,但是還沒(méi)有介紹過(guò)CS的輸出數(shù)據(jù)的存放位置,這些數(shù)據(jù)是存儲(chǔ)在UAV(unordered access views)中的。UAV跟PS中的RT比較類似(實(shí)際上,在PS中UAV可以跟RT同時(shí)使用),但是存在一些重要的語(yǔ)法區(qū)別:

  • 最重要的區(qū)別在名字上已經(jīng)體現(xiàn)了,UAV的訪問(wèn)是無(wú)序的,即API無(wú)法保證對(duì)于UAV的訪問(wèn)會(huì)按照某種約定好的順序排列。在前面我們說(shuō)過(guò),在渲染primitives的時(shí)候,quads會(huì)按照API的順序進(jìn)行深度測(cè)試,alpha-blending以及回寫(xiě),或者至少?gòu)慕Y(jié)果上來(lái)看是按照順序執(zhí)行的,而為了保證這個(gè)結(jié)果,其中需要花費(fèi)不少的功夫。而UAV就沒(méi)有做這些處理——在shader中需要的時(shí)候,就會(huì)立即觸發(fā)對(duì)UAV的訪問(wèn),最終結(jié)果看起來(lái)會(huì)跟API的調(diào)用順序不太一樣。這里的不一樣并不是完全不一樣,在一個(gè)API調(diào)用內(nèi)部的順序可能無(wú)法保證一致,但是API跟驅(qū)動(dòng)會(huì)共同保證多個(gè)API調(diào)用之間的執(zhí)行順序是一致的。因此如果我們需要通過(guò)一個(gè)復(fù)雜的CS(或者PS)來(lái)將數(shù)據(jù)寫(xiě)入到UAV中,之后再使用第二個(gè)CS來(lái)從這個(gè)UAV中讀取數(shù)據(jù),那么這個(gè)時(shí)候得到的肯定是完整的數(shù)據(jù)而非只完成了部分更新的數(shù)據(jù)。

  • UAV支持隨機(jī)訪問(wèn)。在PS中,每一個(gè)像素在執(zhí)行過(guò)程中,各個(gè)RT的寫(xiě)入位置是相同的,但是卻可以對(duì)UAV中的任意位置進(jìn)行讀寫(xiě)。

  • UAV支持原子操作。在傳統(tǒng)的PS管線中(無(wú)UAV的管線),由于各個(gè)像素寫(xiě)入的位置都是相互獨(dú)立的,因此不需要考慮這個(gè)功能。但是在添加了UAV的PS中,各個(gè)像素執(zhí)行的時(shí)候可能會(huì)對(duì)同一個(gè)UAV位置進(jìn)行訪問(wèn),這就可能導(dǎo)致競(jìng)爭(zhēng),因此需要一套同步機(jī)制來(lái)避免競(jìng)爭(zhēng)。

從CPU程序員的視角來(lái)看,UAV就跟多線程系統(tǒng)中的共享內(nèi)存一樣,不同的是UAV的原子操作,這是GPU跟CPU設(shè)計(jì)中不同的地方。

Atomics

在當(dāng)前的CPU中,對(duì)于共享內(nèi)存訪問(wèn)的策略設(shè)計(jì)大多是通過(guò)層級(jí)內(nèi)存(memory hierarchy比如多級(jí)緩存)來(lái)實(shí)現(xiàn)。如果想要向共享內(nèi)存的某個(gè)位置進(jìn)行寫(xiě)操作,那么當(dāng)前活動(dòng)的core必須要保證對(duì)這條緩存行(cache line)擁有獨(dú)占權(quán)(exclusive ownership),而這個(gè)獨(dú)占權(quán)是通過(guò)所謂的緩存一致性協(xié)議(cache coherency protocol)來(lái)實(shí)現(xiàn)的,常用的協(xié)議為MESI 以及其衍生協(xié)議。具體詳情各位自行了解,關(guān)鍵的一點(diǎn)是,由于內(nèi)存的寫(xiě)操作需要擁有獨(dú)占權(quán),因此兩個(gè)core不會(huì)同時(shí)擁有對(duì)同一個(gè)內(nèi)存位置的寫(xiě)權(quán)限。在這種模型下,原子操作可以通過(guò)維持獨(dú)占權(quán)直到寫(xiě)操作結(jié)束來(lái)實(shí)現(xiàn)。

在這類模型中,原子操作的實(shí)現(xiàn)是通過(guò)常規(guī)的Core ALU加上load/store units完成的,大多數(shù)關(guān)鍵的事件都是在緩存中發(fā)生的。這種實(shí)現(xiàn)方案的優(yōu)點(diǎn)是原子操作(或多或少)是常規(guī)的內(nèi)存訪問(wèn),雖然其中還包含了一些額外的要求;缺點(diǎn)是存在一系列的問(wèn)題:

  1. 最嚴(yán)重的問(wèn)題是,緩存一致性的最標(biāo)準(zhǔn)的實(shí)現(xiàn)方式——snooping——要求處于協(xié)議中的所有agents都能夠相互通信,這種約束會(huì)嚴(yán)重限制擴(kuò)展性。當(dāng)然,關(guān)于這個(gè)問(wèn)題有許多解決方案(主要是使用所謂的Directory-based(基于目錄的)一致性協(xié)議)),但是這些解決方案會(huì)導(dǎo)致內(nèi)存訪問(wèn)方案的延遲性以及復(fù)雜性增加。

  2. 另一個(gè)問(wèn)題是,內(nèi)存transactions以及l(fā)ocks都是發(fā)生在緩存行級(jí)別的,如果兩個(gè)不相關(guān)但是卻需要頻繁更新的變量共享同一個(gè)緩存行,就可能導(dǎo)致多個(gè)core之間的“乒乓”加鎖,從而導(dǎo)致大量的一致性transactions(使得性能下降),這就是著名的false sharing(偽共享)。這個(gè)問(wèn)題可以通過(guò)軟件來(lái)規(guī)避,只要確保不相關(guān)的屬性不會(huì)被放置到同一個(gè)緩存行中就可以。但是在GPU中,app既不能得知或控制緩存行的尺寸,也無(wú)法得知或控制運(yùn)行時(shí)內(nèi)存的layout,因此這個(gè)問(wèn)題會(huì)更嚴(yán)重一點(diǎn)。

當(dāng)前的GPU是通過(guò)對(duì)內(nèi)存層級(jí)架構(gòu)進(jìn)行重新組織來(lái)解決偽共享問(wèn)題的。硬件上增加了一個(gè)專屬的原子unit(因?yàn)槭菍俚模虼巳绻袃蓚€(gè)邏輯單元均需要處理某個(gè)緩存行,則統(tǒng)一需要通過(guò)這個(gè)專屬單元進(jìn)行,這就不會(huì)出現(xiàn)前面的false sharing了)來(lái)直接處理最底層(lowest-level)的共享緩存層級(jí)(shared cache hierarchy),而放棄在shader unit內(nèi)部對(duì)原子操作進(jìn)行處理(前面說(shuō)過(guò)會(huì)導(dǎo)致競(jìng)爭(zhēng))。因?yàn)橹挥幸粋€(gè)這種緩存,不論緩存行是否處于緩存中(存在就表示當(dāng)前處理的就是這個(gè)緩存行,不存在就表示當(dāng)前處理的是內(nèi)存的拷貝,后面實(shí)際讀寫(xiě)時(shí)會(huì)先加載到緩存中),一致性問(wèn)題都不會(huì)存在。原子操作包含(在對(duì)應(yīng)的內(nèi)存位置不在緩存中時(shí))將對(duì)應(yīng)的內(nèi)存位置添加到緩存中,之后使用atomic unit上的一個(gè)專屬的整數(shù)ALU在緩存上直接進(jìn)行對(duì)應(yīng)的讀-改-寫(xiě)操作。如果某個(gè)atomic unit在某個(gè)內(nèi)存位置上處于繁忙狀態(tài),那么對(duì)于這個(gè)內(nèi)存位置的其他操作都將處于阻礙狀態(tài)。而由于GPU中存在多個(gè)atomic units,因此有必要確保這些unit不會(huì)同時(shí)訪問(wèn)同一個(gè)內(nèi)存位置,一個(gè)簡(jiǎn)單的做法是為每個(gè)atomic unit分派一套獨(dú)有的地址(靜態(tài)而非動(dòng)態(tài))。而這套方案可以通過(guò)hash函數(shù)將每個(gè)atomic unit的索引轉(zhuǎn)換到對(duì)應(yīng)的內(nèi)存地址來(lái)實(shí)現(xiàn)(注意,因?yàn)樵诠俜轿臋n中沒(méi)有找到對(duì)應(yīng)的信息,因此這里給出的方案只是推測(cè))。

如果某個(gè)shader unit想要對(duì)一個(gè)給定的內(nèi)存地址進(jìn)行原子操作,首先就需要找到這個(gè)內(nèi)存地址對(duì)應(yīng)的atomic unit,之后等到這個(gè)atomic unit準(zhǔn)備接收新的指令的時(shí)候,將操作提交上去(之后如果需要獲取原子操作的結(jié)果的話,還需要繼續(xù)等待,直到操作完成)。atomic unit可能每個(gè)時(shí)刻只能處理一條指令,或者擁有一條包含了重要(outstanding)請(qǐng)求的FIFO隊(duì)列。當(dāng)然,這里有多種方案來(lái)確保原子操作的處理過(guò)程是公平的,從而保證shader unit可以正常往前執(zhí)行。

最后一點(diǎn)要注意的,不論是設(shè)備內(nèi)存訪問(wèn),還是內(nèi)存或者貼圖讀取,還是UAV的寫(xiě)入,都會(huì)觸發(fā)重要的原子操作,shader unit需要及時(shí)跟蹤其對(duì)應(yīng)的重要原子操作,并確保在觸碰到設(shè)備內(nèi)存訪問(wèn)的barrier之前這些操作都已經(jīng)處于完成狀態(tài)。

Structured buffers and append/consume buffers

Structured buffers可以看成是對(duì)驅(qū)動(dòng)內(nèi)部的shader編譯器的一種提示數(shù)據(jù),用于告訴shader編譯器這個(gè)數(shù)據(jù)是怎么使用的——如名字所示,這個(gè)buffer包含了一系列具有固定stride的數(shù)據(jù)元素,這些數(shù)據(jù)元素在訪問(wèn)的時(shí)候會(huì)按照一個(gè)整體進(jìn)行——不過(guò)這些數(shù)據(jù)依然會(huì)編譯到常規(guī)的內(nèi)存訪問(wèn)中(compile down to regular memory accesses,啥意思?)。這個(gè)buffer會(huì)對(duì)驅(qū)動(dòng)對(duì)buffer訪問(wèn)的位置以及內(nèi)存中的layout進(jìn)行偏移,但是不會(huì)為這個(gè)模型增加其他的新功能。

Append/consume buffers也是差不多的,這個(gè)buffer可以使用現(xiàn)存的原子指令來(lái)實(shí)現(xiàn)。實(shí)際上,這個(gè)buffer的實(shí)施方案中也確實(shí)包含了這種方法,不過(guò)有一點(diǎn)不一樣,append/consume指針并不是資源中的一個(gè)顯式的位置,而是通過(guò)特殊的原子指令來(lái)訪問(wèn)的資源之外的一個(gè)邊帶(side-band)數(shù)據(jù)(跟structured buffers一樣,append/consume buffer中數(shù)據(jù)的聲明同樣表明了數(shù)據(jù)在內(nèi)存中的位置)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容