GPU 是專門為高速處理大量數(shù)據(jù)而設(shè)計(jì)的。他們擁有大量的計(jì)算資源,稱為流式多處理器( SMs ),以及一系列保持?jǐn)?shù)據(jù)供應(yīng)的設(shè)施:高帶寬的內(nèi)存、相當(dāng)大的數(shù)據(jù)緩存,以及在活動(dòng)團(tuán)隊(duì)數(shù)據(jù)耗盡時(shí)切換到其他工作團(tuán)隊(duì)( warp )而無(wú)需任何開(kāi)銷的能力。
然而,數(shù)據(jù)饑餓仍可能發(fā)生,許多代碼優(yōu)化都集中在這個(gè)問(wèn)題上。在某些情況下,? SM 渴望的不是數(shù)據(jù),而是指令。這篇文章介紹了一個(gè) GPU 工作負(fù)載的調(diào)查,該工作負(fù)載由于指令緩存未命中而速度減慢。它描述了如何識(shí)別這個(gè)瓶頸,以及消除它以提高性能的技術(shù)。
認(rèn)識(shí)到問(wèn)題
這項(xiàng)研究的起源源于基因組學(xué)領(lǐng)域的一項(xiàng)應(yīng)用,其中需要解決將 DNA 樣本的小片段與參考基因組比對(duì)的許多小而獨(dú)立的問(wèn)題。背景是眾所周知的 Smith-Waterman 算法(但這本身對(duì)討論并不重要)。
在強(qiáng)大的 NVIDIA H100 Hopper GPU,具有 114 個(gè) SM,顯示出良好的前景。使用 NVIDIA Nsight Compute( NCU )工具分析程序,可以證實(shí) SM 在 GPU 上進(jìn)行有用的計(jì)算,但也存在問(wèn)題。
組成整體工作負(fù)載的許多小問(wèn)題(每個(gè)問(wèn)題由自己的線程處理)可以同時(shí)在 GPU 上運(yùn)行,因此并非所有的計(jì)算資源都一直被完全使用。這被表示為一個(gè)小的非整數(shù)數(shù)量的波。 GPU 的工作被劃分為稱為線程塊的塊,一個(gè)或多個(gè)可以駐留在 SM 上。如果一些 SM 接收到的線程塊比其他 SM 少,則它們將耗盡工作,并且在其他 SM 繼續(xù)工作時(shí)必須空閑。
用螺紋塊完全填滿所有 SM 構(gòu)成一個(gè)波。 NCU 盡職盡責(zé)地報(bào)告每個(gè) SM 的波數(shù)。如果這個(gè)數(shù)字恰好是 100 . 5 ,這意味著并非所有 SM 都有相同的工作量要做,有些 SM 被迫閑置。但分布不均的影響并不大。大多數(shù)時(shí)候, SM 上的負(fù)載是平衡的。例如,如果波浪的數(shù)量?jī)H為 0 . 5 ,則情況會(huì)發(fā)生變化。在更大比例的時(shí)間里, SM 經(jīng)歷了不均衡的工作分配,這被稱為“尾部”效應(yīng)。
解決尾部效應(yīng)
這種現(xiàn)象正是基因組學(xué)工作量所體現(xiàn)的。海浪的數(shù)量只有 1 . 6 次。顯而易見(jiàn)的解決方案是給 GPU 更多的工作要做(更多的線程,導(dǎo)致每個(gè)線程 32 個(gè)線程的更多翹曲),這通常不是問(wèn)題。最初的工作量相對(duì)較小,在實(shí)際環(huán)境中需要解決更大的問(wèn)題。然而,通過(guò)將子問(wèn)題的數(shù)量增加一倍( 2x )、三倍( 3x )和四倍( 4x )來(lái)增加工作負(fù)載( 1x ),性能非但沒(méi)有提高,反而惡化。是什么導(dǎo)致了這種結(jié)果?
NCU 關(guān)于這四種工作量規(guī)模的綜合報(bào)告揭示了這一情況。在名為 Warp State 的部分中,列出了線程無(wú)法取得進(jìn)展的原因,“ No Instruction ”的值隨著工作負(fù)載大小的增加而顯著增加(圖 1 )。
“無(wú)指令”表示無(wú)法從內(nèi)存以足夠快的速度向 SM 提供指令“長(zhǎng)記分牌”表示 SM 無(wú)法以足夠快的速度從內(nèi)存中獲得數(shù)據(jù)。及時(shí)獲取指令至關(guān)重要,因此 GPU 提供了許多站點(diǎn),一旦獲取指令,就可以將其放置在這些站點(diǎn),以使其靠近 SM 。這些站點(diǎn)被稱為指令緩存,其級(jí)別甚至比數(shù)據(jù)緩存更多。
圖 1 。 NVIDIA Nsight Compute 合并報(bào)告中四種工作負(fù)載大小的扭曲失速原因截圖
為了了解指令緩存瓶頸發(fā)生在哪里,我們的團(tuán)隊(duì)再次運(yùn)行了相同的工作負(fù)載,但這次指示 NCU 使用名為 Metrics 的功能收集比以前更多的信息。此功能用于指定未包含在常規(guī)性能報(bào)告中的性能計(jì)數(shù)器的用戶定義列表。在這種特殊情況下,使用了與指令緩存相關(guān)的大量計(jì)數(shù)器:
gcc__raw_l15_instr_hit, gcc__raw_l15_instr_hit_under_miss, gcc__raw_l15_instr_miss, sm__icc_requests, sm__icc_requests_lookup_hit, sm__icc_requests_lookup_miss, sm__icc_requests_lookup_miss_covered, sm__icc_requests_lookup_miss_to_gcc, sm__raw_icc_covered_miss, sm__raw_icc_covered_miss_tpc, sm__raw_icc_hit, sm__raw_icc_hit_tpc, sm__raw_icc_request_tpc_1b_apm, sm__raw_icc_true_hits_tpc_1b_apm, sm__raw_icc_true_miss, sm__raw_icc_true_miss_tpc, sm__raw_icc_unlock_all_tpc, sm__raw_l0icache_hits_sctlall, sm__raw_l0icache_requests_sctlall, sm__raw_l0icache_requests_to_icc_sctlall, smsp__l0icache_fills, smsp__l0icache_requests, smsp__l0icache_requests_hit, smsp__l0icache_requests_miss, smsp__raw_l0icache_hits, smsp__raw_l0icache_requests_to_icc
結(jié)果是,在所有測(cè)量的數(shù)量中,成本相對(duì)較高的 icc 緩存未命中尤其會(huì)隨著工作負(fù)載大小的增加而不成比例地增加(圖 2 )。 icc 緩存是一個(gè)指令緩存,位于 SM 本身,非常接近實(shí)際的指令執(zhí)行引擎。
圖 2 :與 icc 指令緩存請(qǐng)求相關(guān)的性能計(jì)數(shù)器,包括快速增加的 icc 未命中,用于不斷增加的大小的工作負(fù)載
icc 未命中的增加如此之快,這意味著,首先,并非代碼中最繁忙部分的所有指令都適合 icc 。其次,隨著工作負(fù)載大小的增加,對(duì)更多不同指令的需求也會(huì)增加。后者的原因有些微妙。由扭曲組成的多個(gè)線程塊同時(shí)駐留在 SM 上,但并非所有扭曲都同時(shí)執(zhí)行。
SM 內(nèi)部分為四個(gè)分區(qū),每個(gè)分區(qū)通常每個(gè)時(shí)鐘周期可以執(zhí)行一條 warp 指令。當(dāng)一個(gè)經(jīng)線由于任何原因而停滯時(shí),另一個(gè)同樣位于 SM 上的經(jīng)線可以接管。每個(gè)扭曲都可以獨(dú)立于其他扭曲執(zhí)行自己的指令流。在這個(gè)程序的主內(nèi)核開(kāi)始時(shí),在每個(gè) SM 上運(yùn)行的扭曲大多是同步的。他們從第一個(gè)指令開(kāi)始,一直在蹣跚前行。
然而,它們并沒(méi)有明確地同步,隨著時(shí)間的推移,扭曲輪流空轉(zhuǎn)和執(zhí)行,它們將在執(zhí)行的指令方面越來(lái)越偏離。這意味著隨著執(zhí)行的進(jìn)行,一組不斷增長(zhǎng)的不同指令必須是活動(dòng)的,這反過(guò)來(lái)意味著 icc 溢出的頻率更高。指令緩存壓力增大,會(huì)發(fā)生更多未命中。
解決問(wèn)題
扭曲指令流的逐漸漂移無(wú)法控制,除非通過(guò)同步這些流。但同步通常會(huì)降低性能,因?yàn)樵跊](méi)有基本需求的情況下,它需要扭曲來(lái)相互等待。然而,可以嘗試減少整個(gè)指令占用空間,這樣從 icc 溢出的指令發(fā)生的頻率就會(huì)降低,而且可能根本不會(huì)發(fā)生。
有問(wèn)題的代碼包含嵌套循環(huán)的集合,并且大多數(shù)循環(huán)都是展開(kāi)的。展開(kāi)通過(guò)使編譯器能夠:
重新排序(獨(dú)立)指令以實(shí)現(xiàn)更好的調(diào)度
消除循環(huán)的連續(xù)迭代可以共享的一些指令
減少分支
為循環(huán)的不同迭代中引用的同一變量分配不同的寄存器,以避免必須等待特定寄存器可用
展開(kāi)循環(huán)帶來(lái)了許多好處,但它確實(shí)增加了指令的數(shù)量。它還傾向于增加所使用的寄存器數(shù)量,這可能會(huì)降低性能,因?yàn)橥瑫r(shí)存在于 SM 上的翹曲可能更少。這種扭曲占用率的降低帶來(lái)了更少的延遲隱藏。
內(nèi)核最外層的兩個(gè)循環(huán)是焦點(diǎn)。實(shí)際的展開(kāi)最好留給編譯器,它有無(wú)數(shù)的啟發(fā)式方法來(lái)生成好的代碼。也就是說(shuō),用戶通過(guò)在循環(huán)的頂部之前使用提示(在 C ++中稱為 pragmas )來(lái)表達(dá)展開(kāi)的預(yù)期好處。其形式如下:
#pragma unroll X
其中X可以是空的(規(guī)范展開(kāi)),編譯器只被告知展開(kāi)可能是有益的,但沒(méi)有給出任何建議要展開(kāi)多少迭代?;蛘呤?n),其中n是一個(gè)正數(shù),表示按組展開(kāi)n迭代。為了方便起見(jiàn),采用了以下符號(hào)。展開(kāi)因子 0 表示根本沒(méi)有展開(kāi)雜注,展開(kāi)因子 1 表示沒(méi)有任何數(shù)字的展開(kāi)雜注(規(guī)范),展開(kāi)因子為n大于 1 表示:
#pragma unroll (n)
下一個(gè)實(shí)驗(yàn)包括一組運(yùn)行,其中代碼中最外層兩個(gè)循環(huán)的兩個(gè)級(jí)別的展開(kāi)因子都在 0 到 4 之間變化,從而為四種工作負(fù)載大小中的每一種產(chǎn)生性能數(shù)據(jù)。不需要進(jìn)行更多的展開(kāi),因?yàn)閷?shí)驗(yàn)表明,編譯器不會(huì)為該特定程序的較高展開(kāi)因子生成不同的代碼。圖 3 顯示了套件的結(jié)果。
頂部水平軸顯示最外層循環(huán)(頂層)的展開(kāi)系數(shù)。底部水平軸顯示第二級(jí)循環(huán)的展開(kāi)因子。四條性能曲線中的任何一條上的每個(gè)點(diǎn)(越高越好)對(duì)應(yīng)于兩個(gè)展開(kāi)因子,一個(gè)用于水平軸上所示的最外循環(huán)中的每一個(gè)。
圖 3 還顯示了對(duì)于展開(kāi)因子的每個(gè)實(shí)例,可執(zhí)行文件的大?。ㄒ?500KB 為單位)。雖然人們的期望可能是隨著每一個(gè)更高級(jí)別的展開(kāi),可執(zhí)行文件的大小都會(huì)增加,但事實(shí)并非總是如此。展開(kāi)雜注是編譯器可能會(huì)忽略的提示,如果它們不被認(rèn)為是有益的。
圖 3 。Smith Waterman 碼在不同工作負(fù)載大小和不同循環(huán)展開(kāi)因子下的性能
對(duì)應(yīng)于代碼的初始版本(由標(biāo)記為 A 的橢圓指示)的測(cè)量用于頂層循環(huán)的規(guī)范展開(kāi),而不用于第二級(jí)循環(huán)的展開(kāi)。代碼的異常行為是顯而易見(jiàn)的,由于 icc 未命中的增加,較大的工作負(fù)載大小會(huì)導(dǎo)致較差的性能。
在下一個(gè)孤立的實(shí)驗(yàn)中(由標(biāo)記為 B 的橢圓表示),在全套運(yùn)行之前嘗試,最外面的兩個(gè)循環(huán)都沒(méi)有展開(kāi)?,F(xiàn)在,異常行為已經(jīng)消失,更大的工作負(fù)載大小會(huì)帶來(lái)預(yù)期的更好性能。但是,絕對(duì)性能會(huì)降低,尤其是對(duì)于原始工作負(fù)載( 1x )大小。 NCU 揭示的兩個(gè)現(xiàn)象有助于解釋這一結(jié)果。由于指令占用空間較小,對(duì)于所有大小的工作負(fù)載, icc 未命中幾乎已降至零。然而,編譯器為每個(gè)線程分配了相對(duì)大量的寄存器,因此可以駐留在 SM 上的扭曲數(shù)量不是最佳的。
對(duì)展開(kāi)因子進(jìn)行全面掃描表明,標(biāo)記為 C 的橢圓中的實(shí)驗(yàn)是眾所周知的最佳點(diǎn)。它對(duì)應(yīng)于不展開(kāi)頂級(jí)循環(huán),而展開(kāi)第二級(jí)循環(huán)的因子 2 。 NCU 仍然顯示出幾乎沒(méi)有 icc 未命中,并且每個(gè)線程的寄存器數(shù)量減少,因此與實(shí)驗(yàn) B 相比, SM 上可以容納更多的扭曲,從而導(dǎo)致更多的延遲隱藏。
雖然最小工作負(fù)載的絕對(duì)性能仍落后于實(shí)驗(yàn) A ,但差異不大,而且較大的工作負(fù)載表現(xiàn)得越來(lái)越好,從而在所有工作負(fù)載大小中獲得最佳的平均性能。
結(jié)論
指令緩存未命中可能會(huì)導(dǎo)致指令占用空間大的內(nèi)核性能下降,這通常是由大量循環(huán)展開(kāi)引起的。當(dāng)編譯器負(fù)責(zé)通過(guò)雜注展開(kāi)時(shí),它應(yīng)用于代碼以確定最佳實(shí)際展開(kāi)級(jí)別的啟發(fā)式方法必然很復(fù)雜,程序員并不總是可以預(yù)測(cè)的。試驗(yàn)關(guān)于循環(huán)展開(kāi)的不同編譯器提示,以獲得具有良好扭曲占用率和減少指令緩存未命中的最佳代碼,可能是值得的。
-
NVIDIA
+關(guān)注
關(guān)注
14文章
4994瀏覽量
103168 -
gpu
+關(guān)注
關(guān)注
28文章
4743瀏覽量
128997 -
人工智能
+關(guān)注
關(guān)注
1791文章
47352瀏覽量
238779
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論