通過減少指令緩存缺失來提高GPU性能
GPU是專門為高速處理大量數據而設計的。它們有大量的計算資源,稱為流多處理器(SMs),以及壹系列的設施來保持它們的數據:高帶寬到內存,相當大的數據緩存,如果壹個活動團隊的數據用完,可以切換到其他工作團隊(warp),而不會有任何開銷。
然而,數據饑餓仍然可能發生,許多代碼優化都集中在這個問題上。在某些情況下,短信並不需要數據,而是需要指令。這篇文章介紹了壹個由於指令緩存丟失而導致GPU工作負載變慢的調查。它描述了如何識別這個瓶頸,以及消除瓶頸以提高性能的技術。
認識到問題
這項研究的起源是基因組學領域的壹個應用,其中許多小的、獨立的問題與DNA樣本的小片段與參考基因組的比對有關,必須得到解決。背景是眾所周知的 史密斯-沃特曼算法 (但它本身對討論並不重要)
在功能強大的 NVIDIA H100 Hopper GPU的中型數據集上運行該程序,具有114個SMs,顯示出良好的前景。 NVIDIA Nsight Compute 工具分析了壹個程序在GPU上的執行情況,證實了SMs非常忙於有用的計算,但有壹個障礙。
構成整個工作負載的許多小問題(每個都由自己的線程處理)可以同時在GPU上運行,因此並非所有的計算資源都壹直被充分利用。這表示為壹個小而非整數的波數。
GPU的工作被分成稱為線程塊的塊,壹個或多個線程塊可以駐留在壹個SM上。如果壹些短信收到的線程塊比其他短信少,那麽它們將耗盡工作,並且在其他短信繼續工作時必須空閑。
用線程塊完全填充所有SMs構成壹個波。NVIDIA insight Compute忠實地報告每個SM的波數。如果這個數字恰好是100.5,則意味著並非所有的SMs都有相同的工作量要做,有些SMs被迫空閑。然而,這種分布不均的影響並不大。
大多數情況下,SMs上的負載是平衡的。例如,如果波的數量只有0.5,情況就會改變。在更大比例的時間裏,SMs經歷了不均勻的工作分配,這被稱為尾部效應。
解決尾部效應
這種現象正是基因組學工作量所體現的。波浪的次數只有1.6次。顯而易見的解決方案是讓GPU做更多的工作(更多的線程,導致每個32個線程的更多扭曲),這通常不是問題。
最初的工作量相對較小,在實際環境中,必須完成較大的問題。但是,通過將子問題的數量增加壹倍、三倍和四倍來增加原始工作負載,會導致性能下降而不是提高。是什麽導致了這個結果?
NVIDIA insight Compute關於這四種工作負載規模的綜合報告揭示了這壹情況。在名為Warp State的部分中,該部分列出了線程無法進行進程的原因,No Instruction的值隨著工作負載大小的增加而顯著增加(圖1)。
柱狀圖的截圖,其中“無指令”的值導致最多的攤位。
圖1所示。從NVIDIA Nsight綜合計算報告中可以得出四種工作負載大小的Warp失速原因
無指令意味著短信不能足夠快地從內存中輸入指令。長記分牌表示SMs不能足夠快地從內存中輸入數據。及時獲取指令是非常重要的,GPU提供了許多站點,在這些站點中,指令可以在獲取後放置,以使它們靠近SMs。這些工作站被稱為指令緩存,它們的級別甚至比數據緩存還要高。
指令緩存缺失明顯增加如此之快的事實,正如由於無指令而導致的warp stall的快速增長所證明的那樣,意味著以下幾點:
- 並不是代碼中最繁忙部分的所有指令都適合緩存。
- 隨著工作負載大小的增加,對更多不同指令的需求也會增加。
後者的原因有些微妙。由經線組成的多個線程塊同時駐留在SM上,但並非所有經線都同時執行。SM內部分為四個分區,每個分區通常可以在每個時鐘周期執行壹個warp指令。
當壹個經線因任何原因停滯時,另壹個也位於SM上的經線可以接管。每個warp都可以獨立於其他warp執行自己的指令流。
在這個程序的主內核開始時,在每個SM上運行的warp基本上是同步的。他們從第壹次指導開始,然後壹直堅持下去。 但是,它們沒有顯式同步。
隨著時間的推移,翹曲機輪流空轉和執行,它們執行的指令越走越遠。這意味著隨著執行的進行,越來越多的不同指令必須處於活動狀態,而這又意味著指令緩存更頻繁地溢出。指令緩存壓力會增加,並且會發生更多的丟失。
解決問題
warp指令流的逐漸漂移是無法控制的,除非通過同步這些流。但是同步通常會降低性能,因為它需要在沒有基本需求的情況下等待彼此。
但是,您可以嘗試減少總體指令占用,以便減少指令緩存溢出的發生頻率,或者根本不發生溢出。
所討論的代碼包含壹組嵌套循環,並且大多數循環是展開的。展開通過使編譯器能夠執行以下操作來提高性能:
重新排序(獨立)指令以更好地調度。
消除壹些可以由循環的連續叠代共享的指令。
減少分支。
為循環的不同叠代中引用的相同變量分配不同的寄存器,以避免必須等待特定的寄存器可用。
展開循環有很多好處,但它確實增加了指令的數量。它還傾向於增加使用的寄存器的數量,這可能會降低性能,因為同時駐留在SMs上的warp可能更少。這種減少的曲速占用伴隨著更少的延遲隱藏。
內核的兩個最外層循環是焦點。實際展開最好留給編譯器,它有無數的啟發式方法來生成好的代碼。也就是說,用戶通過在循環開始前使用提示(在C/ c++中稱為pragmas)來表達展開的預期好處。
它們的形式如下:
#pragma展開X
在 X 可以為空(規範展開)的情況下,編譯器只被告知展開可能是有益的,但沒有給出要展開多少次叠代的任何建議。
為方便起見,我們對展開因子采用了以下表示法:
0 =根本沒有展開程序。
1 =沒有任何數字的unroll pragma (canonical)。
n 大於1 =壹個正數,表示以 n 次叠代為組展開。
#pragma unroll
下壹個實驗包含壹組運行,其中對於代碼中兩個最外層循環的兩個級別, 展開因子在0和4之間變化,為四種工作負載大小中的每壹種生成性能數字。不需要更多的展開,因為實驗表明編譯器不會為這個特定程序的更高展開因子生成不同的代碼。圖2顯示了該套件的結果。
繪制每個工作負載大小的代碼性能的圖表。對於展開因子的每個實例,可執行文件的大小以500 KB為單位顯示。
圖2。 Smith-Waterman代碼在不同工作負載大小和不同循環展開因素下的性能
頂部水平軸顯示最外層循環(頂層)的展開因子。底部的水平軸顯示了第二級循環的展開因子。四條性能曲線(越高越好)上的每個點對應於兩個展開因子,壹個用於水平軸上所示的最外層循環。
圖2還顯示了對於unroll因子的每個實例,可執行文件的大小以500 KB為單位。雖然期望看到可執行文件的大小隨著每壹個更高級別的展開而增加,但情況並非始終如此。展開pragmas是壹些提示,如果它們被認為不是有益的,編譯器可能會忽略它們。
與代碼的初始版本(由標記為A的橢圓表示)相對應的度量用於頂級循環的規範展開,而不用於第二級循環的展開。代碼的異常行為是明顯的,其中較大的工作負載大小導致較差的性能,由於增加指令緩存丟失。
在下壹個單獨的實驗中(用標記為B的橢圓表示),在整套運行之前嘗試,最外層的兩個環都沒有展開。現在異常行為消失了,更大的工作負載帶來了預期的更好的性能。
但是,絕對性能會降低,特別是對於原始工作負載大小而言。NVIDIA Nsight Compute揭示的兩種現象有助於解釋這壹結果。由於指令內存占用更小,對於所有大小的工作負載,指令緩存丟失都減少了,這可以從No instruction warp stall(未描述)下降到幾乎可以忽略不計的值這壹事實中推斷出來。然而,編譯器為每個線程分配了相對大量的寄存器,因此可以駐留在SM上的warp的數量不是最優的。
對展開因子進行全面掃描表明,標記為C的橢圓中的實驗是眾所周知的最佳點。它對應於不展開頂級循環,並以2倍的倍數展開第二級循環。NVIDIA Nsight Compute仍然顯示無指令warp延遲的值可以忽略不計(圖3),並且每個線程的寄存器數量減少,這樣在SM上可以容納比實驗B更多的warp,從而導致更多的延遲隱藏。
曲速狀態圖的條形圖,無指令曲速的值可以忽略不計。
圖3。從NVIDIA Nsight Compute綜合報告中獲得最佳展開因素的四種工作負載大小的翹曲失速原因
雖然最小工作負載的絕對性能仍然落後於實驗A,但差異並不大,較大的工作負載表現越來越好,從而在所有工作負載大小中獲得最佳的平均性能。
進壹步檢查NVIDIA Nsight Compute報告的三種不同的展開場景(A、B和C)闡明了性能結果。
如圖2中的虛線所示,總指令內存占用大小並不是指令緩存壓力的準確度量,因為它們可能包含只執行少量次的代碼段。最好是研究代碼中“最熱”部分的總大小,這可以通過在NVIDIA Nsight Compute的源視圖中尋找指令執行度量的最大值來識別。
場景A、場景B、場景C分別為39360、15680、16912。顯然,與場景A相比,場景B和C的熱指令內存占用要少得多,從而導致較少的指令緩存壓力。
結論
對於指令占用較大的內核,指令緩存缺失可能導致性能下降,這通常是由大量循環展開引起的。當編譯器負責通過編譯程序展開時,它應用於代碼以確定最佳實際展開級別的啟發式方法必然是復雜的,並且程序員並不總是能夠預測。
嘗試使用不同的關於循環展開的編譯器提示,以獲得具有良好warp占用率和減少指令緩存丟失的最佳代碼,這可能是值得的。