圖形 2.9 GPU硬件架構概述


GPU硬件架構概述


 

GPU是什么?

 

  GPU全稱是Graphics Processing Unit,圖形處理單元。它的功能最初與名字一致,是專門用於繪制圖像和處理圖元數據的特定芯片,后來漸漸加入了其它很多功能。顯卡就是由GPU、散熱器、通訊元件、主板和顯示器連接的各類插槽組成的。

 


 

GPU物理構架

 

  由於納米工藝的引入,GPU可以將數以億記的晶體管和電子器件集成在一個小小的芯片內。從宏觀物理結構上看,現代大多數桌面級GPU的大小跟數枚硬幣同等大小,部分甚至比一枚硬幣還小。

  當GPU結合散熱風扇、PCI插槽、HDMI接口等部件之后,就組成了顯卡,但顯卡不能獨立工作,需要裝載在主板上,結合CPU、內存、顯存、顯示器等硬件設備,組成完整的PC機。

  下面是歷經多次變革的NVIDIA GPU架構:

 

  • 2008 - Tesla:最初給計算處理單元使用,應用於早期的CUDA系列顯卡芯片中,但是並非真正意義上的圖形處理芯片。
  • 2010 - Fermi :第一個完整的GPU計算架構。首款可支持與共享存儲結合純cache層次的GPU架構,支持ECC的GPU架構。
  • 2012 - Kepler:相較於Fermi更快,效率更高,性能更好。
  • 2014 - Maxwell:其全新的立體像素全局光照 (VXGI) 技術首次讓游戲 GPU 能夠提供實時的動態全局光照效果。基於 Maxwell 架構的 GTX 980 和 970 GPU 采用了包括多幀采樣抗鋸齒 (MFAA)、動態超級分辨率 (DSR)、VR Direct 以及超節能設計在內的一系列新技術。
  • 2016 - Pascal :將處理器和數據集成在同一個程序包內,以實現更高的計算效率。1080系列、1060系列都是基於Pascal架構。
  • 2017 - Volta:配備640 個Tensor 核心,每秒可提供超過100 兆次浮點運算(TFLOPS) 的深度學習效能,比前一代的Pascal 架構快5 倍以上。

除了圖上所示的構架外,后續還有

  • 2018 - Turing : 配備了名為 RT Core 的專用光線追蹤處理器,能夠以高達每秒 10 Giga Rays 的速度對光線和聲音在 3D 環境中的傳播進行加速計算。Turing 架構將實時光線追蹤運算加速至上一代 NVIDIA Pascal™ 架構的 25 倍,並能以高出 CPU 30 多倍的速度進行電影效果的最終幀渲染。2060系列、2080系列顯卡也是跳過了Volta直接選擇了Turing架構。
  • 2020 - Ampere : 同時配備第二代RT Core和第三代Tensor Core,將光線相交的處理性能提升了一倍,在渲染有動態模糊的影像時,比Turing快8倍。

 

Tesla構架

 

  • 擁有7組TPC(Texture/Processor Cluster,紋理處理簇)
  • 每個TPC有兩組SM(Stream Multiprocessor,流多處理器)
  • 每個SM包含:8個SP(Streaming Processor,流處理器)
  • 2個SFU(Special Function Unit,特殊函數單元)
  • L1緩存、MT Issue(多線程指令獲取)、C-Cache(常量緩存)、共享內存
  • 除了TPC核心單元,還有與顯存、CPU、系統內存交互的各種部件。

 

Fermi架構

 

  • 擁有16個SM
  • 2個Warp Scheduler(線程束)
  • 兩組共32個Core
  • 16組加載存儲單元(LD/ST)
  • 4個特殊函數單元(SFU)
  • 分發單元(Dispatch Unit)
  • 每個Core:1個FPU(浮點數單元)1個ALU(邏輯運算單元)

 

Maxwell架構

 

  • 采用了Maxwell的GM204,擁有4個GPC
  • 每個GPC有4個SM,對比Tesla架構來說,在處理單元上有了很大的提升。

 

Turing架構

 

  • 6 GPC(圖形處理簇)
  • 36 TPC(紋理處理簇)
  • 72 SM(流多處理器)
  • 每個GPC有6個TPC,每個TPC有2個SM
  • 4,608 CUDA核,72 RT核,576 Tensor核,288 紋理單元
  • 12x32位 GDDR6內存控制器 (共384位)
  • 每個SM包含64 CUDA核(NVIDIA推出的統一計算架構)
  • 每個SM包含8 Tensor核(專為執行張量或矩陣運算而設計的專用執行單元)
  • 每個SM還包含256 KB寄存器文件

 


 

GPU架構的共性

 

  縱觀上所有GPU架構,可以發現它們雖然有所差異,但存在着很多相同的概念和部件:

  • GPC(圖形處理簇)
  • TPC(紋理處理簇)
  • Thread(線程)
  • SM、SMX、SMM(Stream Multiprocessor,流多處理器)
  • Warp線程束、Warp Scheduler(Warp編排器)
  • SP(Streaming Processor,流處理器)
  • Core(執行數學運算的核心)
  • ALU(邏輯運算單元)
  • FPU(浮點數單元)
  • SFU(特殊函數單元)
  • ROP(render output unit,渲染輸入單元)
  • Load/Store Unit(加載存儲單元)
  • L1 Cache(L1緩存)
  • L2 Cache(L2緩存)
  • Shared Memory(共享內存)
  • Register File(寄存器)

  GPU為什么會有這么多層級且有這么多雷同的部件?因為GPU的任務是天然並行的,現代GPU的架構皆是以高度並行能力而設計的。

  GPC包含着多組TPC,TPC包含多組SM,SM又包含着多組CORE。一組SM中可能包含了Poly Morph Engine(多邊形引擎)、L1 Cache(L1緩存)、Shared Memory(共享內存)、Core(執行數學運算的核心)等組件。而一組CORE中又包含了ALU、FPU、Execution Context(執行上下文)、(Detch)、解碼(Decode)等組件。

  從Fermi開始NVIDIA使用類似的原理架構,使用一個Giga Thread Engine來管理所有正在進行的工作,GPU被划分成多個GPCs(Graphics Processing Cluster),每個GPC擁有多個SM(SMX、SMM)和一個光柵化引擎(Raster Engine),它們其中有很多的連接,最顯著的是Crossbar,它可以連接GPCs和其它功能性模塊(例如ROP或其他子系統)。

  程序員編寫的shader是在SM上完成的。每個SM包含許多為線程執行數學運算的Core(核心)。例如,一個線程可以是頂點或像素着色器調用。這些Core和其它單元由Warp Scheduler驅動,Warp Scheduler管理一組32個線程作為Warp(線程束)並將要執行的指令移交給Dispatch Units。

  GPU中實際有多少這些單元(每個GPC有多少個SM,多少個GPC ......)取決於芯片配置本身。

 


 

GPU邏輯管線

 

  1. 程序通過圖形API(DX、GL、WEBGL)發出drawcall指令,指令會被推送到驅動程序,驅動會檢查指令的合法性,然后會把指令放到GPU可以讀取的Pushbuffer中。
  2. 經過一段時間或者顯式調用flush指令后,驅動程序把Pushbuffer的內容發送給GPU,GPU通過主機接口(Host Interface)接受這些命令,並通過前端(Front End)處理這些命令。
  3. 在圖元分配器(Primitive Distributor)中開始工作分配,處理indexbuffer中的頂點產生三角形分成批次(batches),然后發送給多個GPCs。這一步的理解就是提交上來n個三角形,分配給這幾個GPC同時處理。
  4. 在GPC中,每個SM中的Poly Morph Engine負責通過三角形索引(triangle indices)取出三角形的數據(vertex data),即圖中的Vertex Fetch模塊。
  5. 在獲取數據之后,在SM中以32個線程為一組的線程束(Warp)來調度,來開始處理頂點數據。
  6. SM的warp調度器會按照順序分發指令給整個warp,單個warp中的線程會鎖步(lock-step)執行各自的指令,如果線程碰到不激活執行的情況也會被遮掩(be masked out)
  7. warp中的指令可以被一次完成,也可能經過多次調度,例如通常SM中的LD/ST(加載存取)單元數量明顯少於基礎數學操作單元。
  8. 由於某些指令比其他指令需要更長的時間才能完成,特別是內存加載,warp調度器可能會簡單地切換到另一個沒有內存等待的warp,這是GPU如何克服內存讀取延遲的關鍵,只是簡單地切換活動線程組。
  9. 一旦warp完成了vertex-shader的所有指令,運算結果會被Viewport Transform模塊處理,三角形會被裁剪然后准備柵格化,GPU會使用L1和L2緩存來進行vertex-shader和pixel-shader的數據通信。
  10. 接下來這些三角形將被分割,再分配給多個GPC,三角形的范圍決定着它將被分配到哪個光柵引擎(raster engines),每個raster engines覆蓋了多個屏幕上的tile,這等於把三角形的渲染分配到多個tile上面。也就是像素階段就把按三角形划分變成了按顯示的像素划分了。
  11. SM上的Attribute Setup保證了從vertex-shader來的數據經過插值后是pixel-shade是可讀的。
  12. GPC上的光柵引擎(raster engines)在它接收到的三角形上工作,來負責這些這些三角形的像素信息的生成(同時會處理背面剔除和Early-Z剔除)。
  13. 32個像素線程將被分成一組,或者說8個2x2的像素塊,這是在像素着色器上面的最小工作單元,在這個像素線程內,如果沒有被三角形覆蓋就會被遮掩,SM中的warp調度器會管理像素着色器的任務。
  14. 接下來的階段就和vertex-shader中的邏輯步驟完全一樣,但是變成了在像素着色器線程中執行。 由於不耗費任何性能可以獲取一個像素內的值,導致鎖步執行非常便利,所有的線程可以保證所有的指令可以在同一點。
  15. 最后一步,現在像素着色器已經完成了顏色的計算還有深度值的計算,在這個點上,我們必須考慮三角形的原始api順序,然后才將數據移交給ROP(render output unit,渲染輸入單元),一個ROP內部有很多ROP單元,在ROP單元中處理深度測試,和framebuffer的混合,深度和顏色的設置必須是原子操作,否則兩個不同的三角形在同一個像素點就會有沖突和錯誤。

 


 

一些GPU技術

 

Early-Z

 

  早期GPU的渲染管線的深度測試是在像素着色器之后才執行,這樣會造成很多本不可見的像素執行了耗性能的像素着色器計算(具體渲染流程可跳轉至渲染流水線章節)。后來,為了減少像素着色器的額外消耗,將深度測試提至像素着色器之前(下圖),這就是Early-Z技術的由來。Early-Z技術可以將很多無效的像素提前剔除,避免它們進入耗時嚴重的像素着色器。Early-Z剔除的最小單位不是1像素,而是像素塊(2*2)。

  但是,以下情況會導致Early-Z失效: 

  • 開啟Alpha Test:由於Alpha Test需要在像素着色器后面的Alpha Test階段比較(DX的discard,OpenGL的clip),所以無法在像素着色器之前就決定該像素是否被剔除。
  • 開啟Alpha Blend:啟用了Alpha混合的像素很多需要與frame buffer做混合,無法執行深度測試,也就無法利用Early-Z技術。
  • 關閉深度測試。Early-Z是建立在深度測試開啟的條件下,如果關閉了深度測試,也就無法啟用Early-Z技術。
  • 開啟Multi-Sampling:多采樣會影響周邊像素,而Early-Z階段無法得知周邊像素是否被裁剪,故無法提前剔除。
  • 以及其它任何導致需要混合后面顏色的操作。

 

SIMD和SIMT

 

  SIMD(Single Instruction Multiple Data)是單指令多數據,在GPU的ALU單元內,一條指令可以處理多維向量(一般是4D)的數據。比如,有以下shader指令:

float4 c = a + b; // a, b都是float4類型

  對於沒有SIMD的處理單元,需要4條指令將4個float數值相加,匯編偽代碼如下:

ADD c.x, a.x, b.x
ADD c.y, a.y, b.y
ADD c.z, a.z, b.z
ADD c.w, a.w, b.w

  但有了SIMD技術,只需一條指令即可處理完:

SIMD_ADD c, a, b

for(i=0;i<n;++i) a[i]=b[i]+c[i];

 

  SIMT(Single Instruction Multiple Threads,單指令多線程)是SIMD的升級版,可對GPU中單個SM中的多個Core同時處理同一指令,並且每個Core存取的數據可以是不同的。

SIMT_ADD c, a, b這個指令會被同時送入在單個SM中被編組的所有Core中,同時執行運算,但a、b 、c的值可以不一樣:

__global__ void add(float *a, float *b, float *c) 
{
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    a[i]=b[i]+c[i]; //no loop!
}

 

co-issue

  

  co-issue是為了解決SIMD運算單元無法充分利用的問題。例如下圖,由於float數量的不同,ALU利用率從100%依次下降為75%、50%、25%。

 

 

  為了解決着色器在低維向量的利用率低的問題,可以通過合並1D與3D或2D與2D的指令。例如下圖,DP3指令用了3D數據,ADD指令只有1D數據,co-issue會自動將它們合並,在同一個ALU只需一個指令周期即可執行完。

 

 

  但是,對於向量運算單元(Vector ALU),如果其中一個變量既是操作數又是存儲數的情況,無法啟用co-issue技術:

 

 


 

CPU與GPU

 

  CPU 是一個具有多種功能的優秀領導者。它的優點在於調度、管理、協調能力強,但計算能力一般。

  GPU 相當於一個接受 CPU 調度的 “擁有大量計算能力” 的員工。

 

  CPU GPU
延遲容忍度
並行目標 任務(Task) 數據(Data)
核心構架 多線程核心 SIMT核心
線程數量級別 10 10000
吞吐量
緩存需求量
線程獨立性

 

 

 

 

 

 

 

 

 

 

CPU-GPU異構系統

 

根據CPU和GPU是否共享內存,可分為兩種類型的CPU-GPU架構:

  一是分離式架構(Discrete),CPU和GPU各自有獨立的緩存和內存,它們通過PCI-e等總線通訊。這種結構的缺點在於 PCI-e 相對於兩者具有低帶寬和高延遲,數據的傳輸成了其中的性能瓶頸。目前使用非常廣泛,如PC等。

  二是耦合式架構(Couple),CPU 和 GPU 共享內存和緩存。AMD 的 APU 采用的就是這種結構,目前主要使用在游戲主機中,如 PS4、智能手機。

 

 

  在存儲管理方面,分離式結構中 CPU 和 GPU 各自擁有獨立的內存,兩者共享一套虛擬地址空間,必要時會進行內存拷貝。對於耦合式結構,GPU 沒有獨立的內存,與 CPU 共享系統內存,由 MMU 進行存儲管理。

 

GPU資源機制

 

  內存構架:GPU與CPU類似,也有多級緩存結構:寄存器、L1緩存、L2緩存、GPU顯存、系統顯存,它們的存取速度從寄存器到系統內存依次變慢。由此可見,shader直接訪問寄存器、L1、L2緩存還是比較快的,但訪問紋理、常量緩存和全局內存非常慢,會造成很高的延遲。

 

 

  Gpu內存分布在在RAM存儲芯片或者GPU芯片上,他們物理上所在的位置,決定了他們的速度、大小以及訪問規則:

  • 全局內存(Global memory)——位於片外存儲體中。容量大、訪問延遲高、傳輸速度較慢,使用二級緩存(L2 cache)做緩沖。
  • 本地內存(Local memory)——一般位於片內存儲體中,變量、數組、結構體等都存放在此處,但是有大數組、大結構體以至於寄存器區放不下他們,編譯器在編譯階段就會將他們放到片外的DDR芯片中(最好的情況也會被扔到L2 Cache中),且將他們標記為“Local”型
  • 共享內存(Shared memory)——位於每個流處理器組中(SM)中,其訪問速度僅次於寄存器
  • 寄存器內存(Register memory)——位於每個流處理器組中(SM)中,訪問速度最快的存儲體,用於存放線程執行時所需要的變量。
  • 常量內存(Constant memory)——位於每個流處理器(SM)中和片外的RAM存儲器中
  • 紋理內存(Texture memory)——位於每個流處理器(SM)中和片外的RAM存儲器中

 

GPU資源管理模型(分離式架構)

 

 

  • MMIO(Memory Mapped IO)

    • CPU與GPU的交流就是通過MMIO進行的。CPU 通過 MMIO 訪問 GPU 的寄存器狀態。
    • DMA傳輸大量的數據就是通過MMIO進行命令控制的。
    • I/O端口可用於間接訪問MMIO區域,像Nouveau等開源軟件從來不訪問它。
  • GPU Context

    • GPU Context代表了GPU計算的狀態。
    • 在GPU中擁有自己的虛擬地址。
    • GPU 中可以並存多個活躍態下的Context。
  • GPU Channel

    • 任何命令都是由CPU發出。
    • 命令流(command stream)被提交到硬件單元,也就是GPU Channel。
    • 每個GPU Channel關聯一個context,而一個GPU Context可以有多個GPU channel。
    • 每個GPU Context 包含相關channel的 GPU Channel Descriptors , 每個 Descriptor 都是 GPU 內存中的一個對象。
    • 每個 GPU Channel Descriptor 存儲了 Channel 的設置,其中就包括 Page Table 。
    • 每個 GPU Channel 在GPU內存中分配了唯一的命令緩存,這通過MMIO對CPU可見。
    • GPU Context Switching 和命令執行都在GPU硬件內部調度。
  • GPU Page Table

    • GPU Context在虛擬基地空間由Page Table隔離其它的Context 。
    • GPU Page Table隔離CPU Page Table,位於GPU內存中。
    • GPU Page Table的物理地址位於 GPU Channel Descriptor中。
    • GPU Page Table不僅僅將 GPU虛擬地址轉換成GPU內存的物理地址,也可以轉換成CPU的物理地址。因此,GPU Page Table可以將GPU虛擬地址和CPU內存地址統一到GPU統一虛擬地址空間來。
  • PCI-e BAR

    • GPU 設備通過PCI-e總線接入到主機上。 Base Address Registers(BARs) 是 MMIO的窗口,在GPU啟動時候配置。
    • GPU的控制寄存器和內存都映射到了BARs中。
    • GPU設備內存通過映射的MMIO窗口去配置GPU和訪問GPU內存。
  • PFIFO Engine

    • PFIFO是GPU命令提交通過的一個特殊的部件。
    • PFIFO維護了一些獨立命令隊列,也就是Channel。
    • 此命令隊列是Ring Buffer,有PUT和GET的指針。
    • 所有訪問Channel控制區域的執行指令都被PFIFO 攔截下來。
    • GPU驅動使用Channel Descriptor來存儲相關的Channel設定。
    • PFIFO將讀取的命令轉交給PGRAPH Engine。
  • BO

    • Buffer Object (BO),內存的一塊(Block),能夠用於存儲紋理(Texture)、渲染目標(Render Target)、着色代碼(shader code)等等。

    • Nouveau和Gdev經常使用BO。

Nouveau是一個自由及開放源代碼顯卡驅動程序,是為NVidia的顯卡所編寫。

Gdev是一套豐富的開源軟件,用於NVIDIA的GPGPU技術,包括設備驅動程序。

 

CPU-GPU數據流

 

  下圖是分離式架構的CPU-GPU的數據流程圖:

 

  • 將主存的處理數據復制到顯存中。
  • CPU指令驅動GPU。
  • GPU中的每個運算單元並行處理。此步會從顯存存取數據。
  • GPU將顯存結果傳回主存。

 


 

Shader運行機制

 

  Shader代碼也跟傳統的C++等語言類似,需要將面向人類的高級語言(GLSL、HLSL、CGSL)通過編譯器轉成面向機器的二進制指令,二進制指令可轉譯成匯編代碼,以便技術人員查閱和調試。由高級語言編譯成匯編指令的過程通常是在離線階段執行,以減輕運行時的消耗。

  在執行階段,CPU端將shader二進制指令經由PCI-e推送到GPU端,GPU在執行代碼時,會用Context將指令分成若干Channel推送到各個Core的存儲空間。

  下圖為一個假象的Core:一個 GPU Core 包含 8 個 ALU,4 組執行環境(Execution context),每組有 8 個Ctx。這樣,一個 Core 可以並發(Concurrent but interleaved)執行 4 條指令流(Instruction Streams),32 個並發程序片元(Fragment)。

  

 

漫反射例子說明

 

sampler mySamp;
Texture2D<float3> myTex;
float3 lightDir;
 
float4 diffuseShader(float3 norm, float2 uv)
{
    float3 kd;
    kd = myTex.Sample(mySamp, uv);
    kd *= clamp( dot(lightDir, norm), 0.0, 1.0);
    return float4(kd, 1.0);
}

經過編譯后成為匯編代碼:

<diffuseShader>:
sample r0, v4, t0, s0
mul    r3, v0, cb0[0]
madd   r3, v1, cb0[1], r3
madd   r3, v2, cb0[2], r3
clmp   r3, r3, l(0.0), l(1.0)
mul    o0, r0, r3
mul    o1, r1, r3
mul    o2, r2, r3
mov    o3, l(1.0)

 

  在執行階段,匯編代碼會被GPU推送到執行上下文(Execution Context),然后ALU會逐條獲取(Detch)、解碼(Decode)匯編指令為二進制指令,並執行它們。

  而對於SIMT架構的GPU,匯編指令有所不同,變成了SIMT特定指令代碼:

<VEC8_diffuseShader>: 
VEC8_sample vec_r0, vec_v4, t0, vec_s0 
VEC8_mul    vec_r3, vec_v0, cb0[0] 
VEC8_madd   vec_r3, vec_v1, cb0[1], vec_r3 
VEC8_madd   vec_r3, vec_v2, cb0[2], vec_r3
VEC8_clmp   vec_r3, vec_r3, l(0.0), l(1.0) 
VEC8_mul    vec_o0, vec_r0, vec_r3 
VEC8_mul    vec_o1, vec_r1, vec_r3 
VEC8_mul    vec_o2, vec_r2, vec_r3 
VEC8_mov    o3, l(1.0)

 

  並且Context以Core為單位組成共享的結構,同一個Core的多個ALU共享一組Context,如果有多個Core,就會有更多的ALU同時參與shader計算,每個Core執行的數據是不一樣的,可能是頂點、圖元、像素等任何數據:

 

GPU Context和延遲

 

  由於SIMT技術的引入,導致很多同一個SM內的很多Core並不是獨立的,當它們當中有部分Core需要訪問到紋理、常量緩存和全局內存時,就會導致非常大的卡頓(Stall)。

  如果有4組上下文(Context),它們共用同一組運算單元ALU。假設第一組Context需要訪問緩存或內存,會導致2~3個周期的延遲,此時調度器會激活第二組Context以利用ALU。

  當第二組Context訪問緩存或內存又卡住,會依次激活第三、第四組Context,直到第一組Context恢復運行或所有都被激活。延遲的后果是每組Context的總體執行時間被拉長了,越多Context可用就越可以提升運算單元的吞吐量。

 


 

Geforce RTX 2060的擴展驗證

 

  NV shader thread group提供了OpenGL的擴展,可以查詢GPU線程、Core、SM、Warp等硬件相關的屬性。如果要開啟次此擴展,需要滿足以下條件:

  • OpenGL 4.3+;
  • GLSL 4.3+;
  • 支持OpenGL 4.3+的NV顯卡;

   下面是具體的字段和代表的意義:

// 開啟擴展
#extension GL_NV_shader_thread_group : require     (or enable)
 
WARP_SIZE_NV    // 單個線程束的線程數量
WARPS_PER_SM_NV // 單個SM的線程束數量
SM_COUNT_NV     // SM數量
 
uniform uint  gl_WarpSizeNV;    // 單個線程束的線程數量
uniform uint  gl_WarpsPerSMNV;  // 單個SM的線程束數量
uniform uint  gl_SMCountNV;     // SM數量
 
in uint  gl_WarpIDNV;       // 當前線程束id
in uint  gl_SMIDNV;         // 當前線程束所在的SM id,取值[0, gl_SMCountNV-1]
in uint  gl_ThreadInWarpNV; // 當前線程id,取值[0, gl_WarpSizeNV-1]
 
in uint  gl_ThreadEqMaskNV; // 是否等於當前線程id的位域掩碼。
in uint  gl_ThreadGeMaskNV; // 是否大於等於當前線程id的位域掩碼。
in uint  gl_ThreadGtMaskNV; // 是否大於當前線程id的位域掩碼。
in uint  gl_ThreadLeMaskNV; // 是否小於等於當前線程id的位域掩碼。
in uint  gl_ThreadLtMaskNV; // 是否小於當前線程id的位域掩碼。
 
in bool  gl_HelperThreadNV; // 當前線程是否協助型線程。

 

  利用以上字段,可以編寫特殊shader代碼轉成顏色信息,可視化了頂點着色器、像素着色器的SM、Warp id,為我們查探GPU的工作機制和流程提供了途徑,以便可視化窺探GPU的工作機制和流程。下面正式進入驗證階段,將以Geforce RTX 2060作為驗證對象,加入擴展所需的代碼,並修改顏色計算:

#version 430 core
#extension GL_NV_shader_thread_group : require
 
uniform uint  gl_WarpSizeNV;    // 單個線程束的線程數量
uniform uint  gl_WarpsPerSMNV;  // 單個SM的線程束數量
uniform uint  gl_SMCountNV;     // SM數量
 
in uint  gl_WarpIDNV;       // 當前線程束id
in uint  gl_SMIDNV;         // 當前線程所在的SM id,取值[0, gl_SMCountNV-1]
in uint  gl_ThreadInWarpNV; // 當前線程id,取值[0, gl_WarpSizeNV-1]
 
out vec4 FragColor;
 
void main()
{
    // SM id
    float lightness = gl_SMIDNV / gl_SMCountNV;
    FragColor = vec4(lightness);
}

由上面的代碼渲染的畫面如下:

 

從上面可分析出一些信息:

  • 畫面共有32個亮度色階,也就是Geforce RTX 2060有32個SM。
  • 單個SM每次渲染16x16為單位的像素塊,也就是每個SM有256個Core。
  • SM之間不是順序分配像素塊,而是無序分配。
  • 不同三角形的接縫處出現斷層,說明同一個像素塊如果分屬不同的三角形,就會分配到不同的SM進行處理。由此推斷,相同面積的區域,如果所屬的三角形越多,就會導致分配給SM的次數越多,消耗的渲染性能也越多。

 

接着修改片元着色器的顏色計算代碼以顯示Warp id:

// warp id
float lightness = gl_WarpIDNV / gl_WarpsPerSMNV;
FragColor = vec4(lightness);

 

得到如下畫面:

 

由此可得出一些信息或推論:

  • 畫面共有32個亮度色階,也就是每個SM有32個Warp,每個Warp有8個Core。

  • 每個色塊像素是4x8,由於每個Warp有8個Core,由此推斷每個Core單次要處理2x2的最小單元像素塊。

  • 也是無序分配像素塊。
  • 三角形接縫處出現斷層,同SM的推斷一致。

再修改片元着色器的顏色計算代碼以顯示線程id:

// thread id
float lightness = gl_ThreadInWarpNV / gl_WarpSizeNV;
FragColor = vec4(lightness);

 

得到如下畫面:

 

為了方便分析,用Photoshop對中間局部放大10倍,得到以下畫面:

 

結合上面兩幅圖,也可以得出一些結論:

  • 相較SM、線程束,線程分布圖比較規律。說明同一個Warp的線程分布是規律的。
  • 三角形接縫處出現紊亂,說明是不同的Warp造成了不同的線程。
  • 畫面有32個色階,說明單個Warp有32個線程。
  • 每個像素獨占一個亮度色階,與周邊相鄰像素都不同,說明每個線程只處理一個像素。

再次說明,以上畫面和結論是基於Geforce RTX 2060,不同型號的GPU可能會不一樣,得到的結果和推論也會有所不同。

 


 總結

 

  通過前面介紹的邏輯管線層面和硬件執行層面,可以總結出:

  • 頂點着色器和像素着色都是在同一個單元中執行的(在原來的架構中vs和ps的確是分開的,后來nv把這個統一了)vs是按照三角形來並行處理的,ps是按照像素來並行處理的。
  • vs和ps中的數據是通過L1和L2緩存傳遞的。
  • warp和thread都是邏輯上的概念,sm和sp都是物理上的概念。線程數≠流處理器數。

 


擴展提問

 

1、GPU是如何與CPU協調工作的?

 

  MMIO。CPU與GPU的交流就是通過MMIO進行的。CPU 通過 MMIO 訪問 GPU 的寄存器狀態。DMA傳輸大量的數據也是通過MMIO進行命令控制的。

在分離式架構中數據先從主存復制到顯存中。CPU再向channel發動指令驅動GPU。

GPU中的每個運算單元並行處理,此步會從顯存存取數據。最后將顯存結果傳回主存。

 

2、GPU也有緩存機制嗎?有幾層?它們的速度差異多少?

 

 如圖所示,這個圖其實表述的挺清楚了。這5層結構分別為:寄存器、L1緩存、L2緩存、GPU顯存、系統顯存。

 

儲存類型 寄存器 共享內存 L1緩存 L2緩存 紋理、常量緩存 全局內存
訪問周期 1 1~32 1~32 32~64 400~600 400~600

 

3、GPU的渲染流程有哪些階段?它們的功能分別是什么?

 

  PC上會對每個三角形一次執行頂點着色器和片元着色器。移動平台更多為TBR/TBDR,會先把所有的三角形執行完頂點着色器,再執行片元着色器。

  大體流程為:通過三角形索引取出數據,即Vertex Fetch。然后SM中以線程束(Warp)來調度處理頂點數據。warp完成所有指令后會被Viewport Transform模塊處理,三角形被裁剪准備柵格化並決定將被分配到哪個光柵引擎。然后SM再分warp執行片元着色器。然后將數據移交給ROP(render output unit,渲染輸入單元)中處理深度測試,和framebuffer的混合。

 

4、Early-Z技術是什么?發生在哪個階段?這個階段還會發生什么?會產生什么問題?如何解決?

 

  Early-Z技術指將深度測試提前到片元着色前,提前將無用的像素提前剔除(剔除的不是1像素,而是2x2的像素塊),避免大量無效的片元執行耗時嚴重的片元着色。

  如果開啟了AlphaTest活着AlphaBlend或者shader里有discard/clip指令以及關閉深度測試的情況就會使Early-Z失效。

  Early-Z還會導致深度數據沖突,可以再寫入深度值之前再次與frameBuffer的值做一次對比。

  PC上可能會多執行一次Early-Z。但是TBR架構中會分階段處理頂點着色和片元着色,對於移除看不見的片元其實更有優勢,IOS就有HSR(Hidden Surface Removel)技術,安卓的高通芯片也有類似的技術,因此移動平台一般不用做Early-Z。

 

5、SIMD和SIMT是什么?它們的好處是什么?co-issue呢?

 

SIMD: Single Instrument Multiple Data,單指令多數據;

一條指令可以處理多維向量的數據,原來的多條指令用一條指令即可處理完。

SIMT: Single Instrument Multiple Threads,單指令多線程。

單個SM中的多個Core同時處理同一指令,並且每個Core存取的數據可以是不同的即a、b 、c的值可以不一樣。

co-issue是為了盡可能充分利用SIMD,將低維向量合並成Vector4以提高ALU的利用率。

 

6、GPU是並行處理的么?若是,硬件層是如何設計和實現的?

 

是。

  多個SM,每個SM有多個Warp,每個Warp又有多個Core,每個Core又有1個FPU和ALU等

有大量具有計算功能的Core和大量的線程調度得以掩蓋IO延遲。

 

7、GPC、TPC、SM是什么?Warp又是什么?它們和Core、Thread之間的關系如何?

 

GPC: Graphics Processor Cluster,圖形處理簇;

TPC: Texture Processor Cluster,紋理處理簇;

SM:Stream Multiprocessor,流多處理器;

Warp:線程束,GPU並行計算的最小粒度;

1個GPU可以有多個GPC,1個GPC可以有多個TPC,1個TPC可以有多個SM。

每個SM包含許多Core。他們由Warp Scheduler驅動,其Warp Scheduler管理一組32個Threads

 

8、頂點着色器(VS)和像素着色器(PS)可以是同一處理單元嗎?為什么?

 

  現在可,DirectX10引入了一種叫做統一着色器架構(Unified shader Architecture)的技術,Core(SP,流處理器)成了頂點處理單元和像素處理單元的統一。傳統的頂點和像素分離渲染架構存在嚴重的資源分配不均的問題,兩種單元渲染任務量不同,效率低下。而SP架構是統一結構,不再區分頂點和像素渲染,進行不同渲染任務時都能保證效率。

 

9、像素着色器(PS)的最小處理單位是1像素嗎?為什么?會帶來什么影響?

 

不是,是2x2的像素塊。

在像素着色器中,會將相鄰的四個像素作為不可分隔的一組,送入同一個SM內4個不同的Core。

能精簡SM架構,減少硬件單元數量和尺寸,降低功耗提高效能比,同時也提供ddx ddy導數解決了mipmap等問題。

但可能會導致overdraw,即只有1個fragment需要繪制,但真正執行時也是4個fragment的消耗。對於三角形邊緣的fragment,這種情況尤其明顯。

 

10、Shader中的if、for等語句會降低渲染效率嗎?為什么?

 

  絕大多數情況會,由於SIMD的特性,每個ALU的數據不一樣,導致if-else語句在某些ALU中執行的是true分支,有些ALU執行的是false分支,拉長了整個執行周期。for循環也是同理,for循環條件的不同也會導致線程的有效執行時間不同,可能最快的ALU執行完了,最慢的ALU才執行了1/N。但最快的那個得等最慢的執行完,才能繼續執行下一組命令,整個warp的耗時都是以最長時間為准,造成了算力的浪費,降低了渲染效率。

  那還有極少數情況是什么呢,就是32個線程都走到if或者else里面,當然,這很難發生。

 

11、如下圖,渲染相同面積的圖形,三角形數量少(左)的還是數量多(右)的效率更快?為什么?

 

三角形數量少的效率更快。

Vertex Fetch階段中,更少的三角形意味着更少的vertex data。

而更多的三角形則可能意味着更多的overdraw。

即相同面積的區域,三角形越多消耗性能越多。

 

12、GPU Context是什么?有什么作用?

 

GPU Context代表了GPU計算的狀態,含運行指令和數據狀態等信息。

GPU能以Context將指令推送到各個Core,而多個Context可以輪流執行,當某個Context就比較大的卡頓時可以快速調度執行其他的Context,提高運算單元吞吐量,也提升了GPU算力的利用率。

 

13、造成渲染瓶頸的問題很可能有哪些?該如何避免或優化它們?

 

CPU、GPU交互效率限制

——合批,LOD,減少頂點數、三角形數,貼圖優化,調整視錐,特效、動畫避免在CPU端每幀修改提交數據。

 

overdraw

——確保Early-Z有效,確保HSR的效果,控制物體數量,尤其是數量多面積小的東西。

 

shader效率

——盡可能不適用分支循環,慎用像素裁剪之類可能會影響Early-Z或HSR的語句,減少消耗大的語句(如采樣,復雜的數學函數)。

 


 

參考

 

圖形學筆記(一) 底層知識背景

【技術美術百人計划】圖形 2.7.2 GPU硬件架構概述

知乎-NVIDIA GPU 架構梳理-捏太陽

Data Transfer Matters for GPU Computing

OpenGL官網:NV extensions

 

跳轉回百人合集


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM