最近在了解GPU架構這方面的內容,由於資料零零散散,所以准備寫兩篇博客整理一下。GPU的架構復雜無比,這兩篇文章也是從宏觀的層面去一窺GPU的工作原理罷了
GPU根據廠商的不同,顯卡型號的不同,GPU的架構也有差別,但是大體的設計基本相同,原理的部分也是相通的。下面我們就以NVIDIA的Fermi架構為藍本,從降低延遲的角度,來講解一下GPU到底是如何利用數據的並行處理來提升性能的。有關GPU的架構細節和邏輯管線的實現細節,我們將在下一篇里再講。
無論是CPU還是GPU,都在使用各種各樣的策略來避免停滯(stall)。
CPU的優化路線有很多,包括使用pipeline,提高主頻,在芯片上集成訪問速度更快的緩存,減少內存訪問的延遲等等。在減少stalls的路上,CPU還采用了很多聰明的技術,比如分支預測,指令重排,寄存器重命名等等。
GPU則采用了另一種不同的策略:throughput。它提供了大量的專用處理器,由於GPU端數據的天然並行性,所以通過數據的大規模並行化處理,來降低延遲。這種設計優點是通過提高吞吐量,數據的整體處理時間減少,隱藏了處理的延遲。但是由於芯片上集成的核越多,留給其他設備的空間就越小,所以像memory cache和logical control這樣的設備就會變少,導致每一路shader program的執行變得延遲很高。了解這個特性,我們來看一個例子,以此來說明如何利用GPU的架構,寫出更高效的代碼。
假如我們有一個mesh要被渲染,光柵化后生成了2000個fragment,那么我們需要調用一個pixel shader program 2000次,假如我們的GPU只有一個shader core(世界最弱雞GPU),它開始執行第一個像素的shader program,執行一些算數指令,操作一下寄存器上的值,由於寄存器是本地的,所以此時並不會發生阻塞,但是當程序執行到某個紋理采樣的操作時,由於紋理數據並不在程序的本地寄存器中,所以需要進行內存的讀取操作,該操作可能要耗費幾百甚至幾千個時鍾周期,所以會阻塞住當前處理器,等待讀取的結果。如果真的只是這樣設計這個GPU,那它真的就是太弱雞了,所以為了讓它稍微好點,我們需要提升它的性能,那如何降低它的延遲呢?我們給每個fragment提供一些本地存儲和寄存器,用來保存該fragment的一些執行狀態,這樣我們就可以在當前fragment等待紋理數據時,切換到另一個fragment,開始執行它的shader program,當它遇到內存讀取操作阻塞時,會再次切換,以此類推,直到2000個shader program都執行到這里。這時第一個fragment的顏色已經返回,可以繼續往下執行了。使用這種方式,可以最大化的提高GPU的效率,雖然在單個像素來看,執行的延遲變高了,但是從2000個像素整體來說,執行的延遲減少了。
現代GPU當然不會弱雞到只有一個shader core,但是它們也同樣采用了這種方式來減低延遲。現代GPU為了提高數據的並行化,使用了SIMT(Single Instruction Multi Thread,SIMD的更高級版本),執行shader program的最小單位是thread,執行相同program的threads打包成組,NVIDIA稱之為warp,AMD稱之為wavefront。一個warp/wavefront在特定數量的GPU shader core上調度執行,warps調度器調度的基本單元就是warp/wavefront。
假如我們有2000個fragment需要執行shader program,以NVIDIA為例,它的GPU包含32個thread,所以要執行這些任務需要2000/32 = 62.5個warps,也就是說要分配63個warps,有一個只使用一半。
一個warp的執行過程跟單個GPU shader core的執行過程是類似的,32個像素的shader program對應的thread,會在32個GPU shader core上同時以lock-step的方式執行,當着色器程序遇到內存讀取操作時,比如訪問紋理(非常耗時),因為32個threads執行的是相同的程序,所以它們會同時遇到該操作,內存讀取意味着該線程組將會阻塞(stall),全部等待內存讀取的結果。為了降低延遲,GPU的warp調度器會將當前阻塞的warp換出,用另一組包含32個線程的warp來代替執行。換出操作跟單核的任務調度一樣的快,因為在換入換出時,每個線程的數據都沒有被觸碰,每個線程都有它自己的寄存器,每個warp都負責記錄它執行到了哪條指令。換入一個新的warp,不過是將GPU 的shader cores切換到另一組線程上繼續執行,除此之外沒有其他額外的開銷。該過程如下圖所示:
在我們這個簡單的例子中,內存讀取的延遲(latency)會導致warp被換出,在實際的應用中,可能更小的延遲就會導致warp的換出操作,因為換入換出的操作開銷非常低。warp-swapping的策略是GPU隱藏延遲(latency)的主要方式。但是有幾個關鍵因素,會影響到該策略的性能,比如說,如果我們只有很少的threads,也就是只能創建很少的warp,會使隱藏延遲出現問題。
shader program的結構是影響性能的主要角色,其中最大的一個影響因素就是每個thread需要的寄存器的數量。在上面例子的講解過程中,我們一直假設例子中的2000個thread都是同時駐留在GPU中的。但是實際上,每個thread綁定的shader program中需要的寄存器越多,產生的threads就越少(因為寄存器的數量是固定的),能夠駐留在GPU中的warp就越少。warps的短缺也就意味着無法使用warp-swapping的策略減緩延遲。warps在GPU中存在的數量稱之為占用率,高占用率意味着有更多的warps可以用來執行,低占用率則會嚴重影響GPU的並行效率。
另一個影響GPU性能的因素就是動態分支(dynamic branching),主要是由if和循環引進的。因為一個warp中的所有線程在執行到if語句時,就會出現分裂,如果大家都是執行的相同的分支,那也沒什么,但但凡有一個線程執行另一個分支,那么整個warp會把兩個分支都執行一遍,然后每個線程扔掉它們各自不需要的結果,這種現象稱為thread divergence。
了解了上面的基本原理,我們可以看出,整個GPU的設計其實也是一種trade-off,用單路數據的高延遲,來換整體數據的吞吐量,以此來最大化GPU的性能,降低stall。在實際的編碼過程中,尤其是shader的編寫過程中,也要嚴肅影響GPU優化策略的幾個因素,只有這樣,才能寫出更加高效的代碼,真正發揮出GPU的潛力。
下一篇會更加詳細的介紹GPU的結構和邏輯管線,如果錯誤,歡迎指正。