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