作者:tomoyazhang,騰訊 PCG 后臺開發工程師
,騰訊 PCG 后臺開發工程師
隨著軟件從 1.0 進化到 2.0,也即從圖靈機演進到類深度學習算法。計算用的硬件也在加速從 CPU 到 GPU 等遷移。本文試圖整理從英偉達 2010 年開始,到 2020 年這十年間的架構演進歷史。
CPU and GPU
我們先對 GPU 有一個直觀的認識,如下圖:
眾所周知,由于存儲器的發展慢于處理器,在 CPU 上發展出了多級高速緩存的結構,如上面左圖所示。而在 GPU 中,也存在類似的多級高速緩存結構。只是相比 CPU,GPU 將更多的晶體管用于數值計算,而不是緩存和流控(Flow Control)。這源于兩者不同的設計目標,CPU 的設計目標是并行執行幾十個線程,而 GPU 的目標是要并行執行幾千個線程。
可以在上面右圖看到,GPU 的 Core 數量要遠遠多余 CPU,但是有得必有失,可以看到 GPU 的 Cache 和 Control 要遠遠少于 CPU,這使得 GPU 的單 Core 的自由度要遠遠低于 CPU,會受到諸多限制,而這個限制最終會由程序員承擔。這些限制也使得 GPU 編程與 CPU 多線程編程有著根本區別。
這其中最根本的一個區別可以在上右圖中看出,每一行有多個 Core,卻只有一個 Control,這代表著多個 Core 同一時刻只能執行同樣的指令,這種模式也稱為 SIMT (Single Instruction Multiple Threads). 這與現代 CPU 的 SIMD 倒是有些相似,但卻有根本差別,本文在后面會繼續深入細究。
從 GPU 的架構出發,我們會發現,因為 Cache 和 Control 的缺失,只有 計算密集 與 數據并行的程序適合使用 GPU。
- 計算密集:數值計算 的比例要遠大于 內存操作,因此內存訪問的延時可以被計算掩蓋,從而對 Cache 的需求相對 CPU 沒那么大。
- 數據并行: 大任務可以拆解為執行相同指令的小任務,因此對復雜流程控制的需求較低。
而深度學習恰好滿足以上兩點,本人認為,即使存在比深度學習計算量更低且表達能力更強的模型,但如果不滿足以上兩點,都勢必打不過 GPU 加持下的深度學習。
Fermi
Fermi 是 Nvidia 在 2010 年發布的架構,引入了很多今天也仍然不過時的概念,而比 Fermi 更早之前的架構,也已經找不到太多資料了,所以本文從 Fermi 開始,先來一張總覽。
GPU 通過 Host Interface 讀取 CPU 指令,GigaThread Engine 將特定的數據從 Host Memory 中拷貝到內部的 Framebuffer 中。隨后 GigaThread Engine 創建并分發多個 Thread Blocks 到多個 SM 上。多個 SM 彼此獨立,并獨立調度各自的多個 Thread Wraps 到 SM 內的 CUDA Cores 和其他執行單元上執行。
上面這句話有幾個概念解釋一下:
- SM: 對應于上圖中的 SM 硬件實體,內部有很多的 CUDA Cores;
- Thread Block: 一個 Thread Block 包含多個線程(比如幾百個),多個 Blocks 之間的執行完全獨立,硬件可以任意調度多個 Block 間的執行順序,而 Block 內部的多個線程執行規則由程序員決定,程同時程序員可以決定一共有多少個 Blocks;
- Thread Warp: 32 個線程為一個 Thread Warp,Warp 的調度有特殊規則,本文后面會繼續深入。
由于本文不是講怎么寫 CUDA,所以如果對 SM/Block 的解釋仍然不明白,可以參考這一小節:
https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#scalable-programming-model
上圖存在 16 個 SMs,每個 SM 帶 32 個 Cuda Cores,一共 512 個 Cuda Cores. 這些數量不是固定的,和具體的架構和型號相關。
接下來我們深入看 SM,來一張 SM 總覽:
從上圖可知,SM 內有 32 個 CUDA Cores,每個 CUDA Core 含有一個 Integer arithmetic logic unit (ALU)和一個 Floating point unit(FPU). 并且提供了對于單精度和雙精度浮點數的 FMA 指令。
SM 內還有 16 個 LD/ST 單元,也就是 Load/Store 單元,支持 16 個線程一起從 Cache/DRAM 存取數據。
4 個 SFU,是指 Special Function Unit,用于計算 sin/cos 這類特殊指令。每個 SFU 每個時鐘周期只能一個線程執行一條指令。而一個 Warp(32 線程)就需要執行 8 個時鐘周期。SFU 的流水線是從 Dispatch Unit 解耦的,所以當 SFU 被占用時,Dispatch Unit 會去使用其他的執行單元。
之前一直提到 Warp,但之前只說明了是 32 個線程,我們在這里終于開始詳細說明,首先來看 Dual Warp Scheduler 的概覽。
在之前的 SM 概覽圖以及上圖里,可以注意到 SM 內有兩個 Warp Scheduler 和兩個 Dispatch Unit. 這意味著,同一時刻,會并發運行兩個 warp,每個 warp 會被分發到一個 Cuda Core Group(16 個 CUDA Core), 或者 16 個 load/store 單元,或者 4 個 SFU 上去真正執行,且每次分發只執行 一條 指令,而 Warp Scheduler 維護了多個(比如幾十個)的 Warp 狀態。
這里引入了一個核心的約束,任意時刻,一個 Warp 里的 Thread 都在執行同樣的指令,對于程序員來說,觀測不到一個 warp 里不同 thread 的不同執行情況。
但是眾所周知,不同線程可能會進入不同的分支,這時如何執行一樣的指令?
可以看上圖,當發生分支時,只會執行進入該分支的線程,如果進入該分支的線程少,則會發生資源浪費。
在 SM 概覽圖里,我們可以看到 SM 內 64KB 的 On-Chip Memory,其中 48KB 作為 shared memory, 16KB 作為 L1 Cache. 對于 L1 Cache 以及非 On-Chip 的 L2 Cache,其作用與 CPU 多級緩存結構中的 L1/L2 Cache 非常接近,而 Shared Memory,則是相比 CPU 的一個大區別。無論是 CPU 還是 GPU 中的 L1/L2 Cache,一般意義上都是無法被程序員調度的,而 Shared Memory 設計出來就是讓渡給程序員進行調度的片上高速緩存。
Kepler
2012 年 NVIDIA 發布了 Kepler 架構,我們直接看使用 Kepler 架構的 GTX680 概覽圖:
可以看到,首先 SM 改名成了 SMX,但是所代表的概念沒有大變化,我們先看看 SMX 的內部:
還是 Fermi 中熟悉的名詞,就是數量變多了很多。
本人認為這個 Kepler 架構中最值得一提的是 GPUDirect 技術,可以繞過 CPU/System Memory,完成與本機其他 GPU 或者其他機器 GPU 的直接數據交換。畢竟在 2021 年的當今,Bypass CPU/OS 已經是最重要加速手段之一。
Maxwell
2014 年 NVIDIA 發布了 Maxwell 架構,我們直接看架構圖:
可以看到,這次的 SM 改叫 SMM 了,Core 更多了,也更強大了,這里就不過多介紹了。
Pascal
2016 年 NVIDIA 發布了 Pascal 架構,這是第一個考慮 Deep Learning 的架構,也是一個值得大書筆墨的架構,首先看如下圖 P100。
可以看到,還是一如既往地增加了很多 Cores, 我們細看 SM 內部:
單個 SM 只有 64 個 FP32 Cuda Cores,相比 Maxwell 的 128 和 Kepler 的 192,這個數量要少很多,并且 64 個 Cuda Cores 分為了兩個區塊。需要注意的是,Register File 的大小并未減少,這意味著每個線程可以使用的寄存器更多了,而且單個 SM 也可以并發更多的 thread/warp/block. 由于 Shared Memory 并未減少,同樣意味著每個線程可以使用的 Shared Memory 及其帶寬都會變大。
增加了 32 個 FP64 Cuda Cores, 也就是上圖的 DP Unit. 此外 FP32 Cuda Core 同時具備處理 FP16 的能力,且吞吐率是 FP32 的兩倍,這卻是為了 Deep Learning 準備的了。
這個版本引入了一個很重要的東西:NVLink。
隨著單 GPU 的計算能力越來越難以應對深度學習對算力的需求,人們自然而然開始用多個 GPU 去解決問題。從單機多 GPU 到多機多 GPU,這當中對 GPU 互連的帶寬的需求也越來越多。多機之間,采用 InfiniBand 和 100Gb Ethernet 去通信,在單機內,特別是從單機單 GPU 到達單機 8GPU 以后,PCIe 的帶寬往往就成為了瓶頸。為了解決這個問題,NVIDIA 提供了 NVLink 用以單機內多 GPU 內的點到點通信,帶寬達到了 160GB/s, 大約 5 倍于 PCIe 3 x 16. 下圖是一個典型的單機 8 P100 拓撲。
一些特殊的 CPU 也可以通過 NVLink 與 GPU 連接,比如 IBM 的 POWER8。
Volta
2017 年 NVIDIA 發布了 Volta 架構,這個架構可以說是完全以 Deep Learning 為核心了,相比 Pascal 也是一個大版本。 首先還是一如既往地增加了 SM/Core, 我們就直接看單個 SM 內部吧。
和 Pascal 的改變類似,到了 Volta,直接拆了 4 個區塊,每個區塊多配了一個 L0 指令緩存,而 Shared Memory/Register File 這都沒有變少,也就和 Pascal 的改變一樣,單個線程可使用的資源更多了。單個區塊還多個兩個名為 Tensor Core 的單元,這就是這個版本的核心了??梢酝虏垡幌拢@個版本又把 L1 和 Shared Memory 合并了。
我們首先看 CUDA Core, 可以看到,原本的 CUDA Core 被拆成了 FP32 Cuda Core 和 INT32 Cuda Core,這意味著可以同時執行 FP32 和 INT32 的操作。
眾所周知,DeepLearning 的計算瓶頸在矩陣乘法,在 BLAS 中稱為 GEMM,TensorCore 就是只做 GEMM 計算的單元,可以看到,從這里開始,NVIDIA 從 SIMT 走到了 SIMT+DSA 的混合。
每個 TensorCore 只做如下操作:
D=A*B+C
即:
其中 A, B, C, D 都是 4x4 的矩陣,且 A 和 B 是 FP16 矩陣,C 和 D 可以是 FP16 或者 FP32. 通常,更大的矩陣計算會被拆解為這樣的 4x4 矩陣乘法。
這樣的矩陣乘法是作為 Thread Warp 級別的操作在 CUDA 9 開始暴露給程序員,除此以外,使用 cublas 和 cudnn 當然同樣也會在合適的情況下啟用 TensorCore.
在這個版本中,另一個重要更新是 NVLink, 簡單來說就是更多更快。每個連接提供雙向各自 25GB/s 的帶寬,并且一個 GPU 可以接 6 個 NVLink,而不是 Pascal 時代的 4 個。一個典型的拓撲如下圖:
從 Volta 開始,線程調度發生了變化,在 Pascal 以及之前的 GPU 上,每個 Warp 里的 32 個線程共享一個 Program Counter (簡稱 PC) ,并且使用一個 Active Mask 表示任意時刻哪些線程是可運行的,一個經典的運行如下:
直到第一個分支完整結束,才會執行另一個分支。這意味著同一個 warp 內不同分支失去了并發性,不同分支的線程互相無法發送信號或者交換數據,但同時,不同 warp 之間的線程又保留了并發性,這當中的線程并發存在著不一致,事實上如果程序員不注意這點,很可能導致死鎖。
在 Volta 中解決了這個問題,同 warp 內的線程有獨立的 PC 和棧,如下:
由于運行時仍然要符合 SIMT,所以存在一個調度優化器負責將可運行的線程分組,使用 SIMT 模式執行。經典運行如下:
上圖可以注意到,Z 的執行并沒有被合并,這是因為 Z 可能會產生一些被其他分支需要的數據,所以調度優化器只有在確定安全的情況下才會合并 Z,所以上圖 Z 未合并只是一種情況,一般來說,調度優化器足夠聰明可以發現安全的合并。程序員也可以通過一個 API 來強制合并,如下:
從 Volta 開始,提高了對多進程并發使用 GPU 的支持。在 Pascal 及之前,多個進程對單一 GPU 的使用是經典的時間片方式。從 Volta 開始,多個用不滿 GPU 的進程可以在 GPU 上并行,如下圖:
Turing
2018 年 NVIDIA 發布了 Turing 架構,個人認為是 Volta 的延伸版本,當然首先各種參數加強,不過我們這里就不提參數加強了。
比較重要是的增加了一個 RT Core,全名是 Ray Tracing Core, 顧名思義,這個是給游戲或者仿真用的,因為本人沒有從事過這類工作,就不介紹了。
此外 Turing 里的 Tensor Core 增加了對 INT8/INT4/Binary 的支持,為了加速 deep learning 的 inference, 這個時候深度學習模型的量化部署也漸漸成熟。
Ampere
2020 年 NVIDIA 發布了 Ampere 架構,這就是一個大版本了,里面又細分了 GA100, GA102, GA104, 我們這里就只關注 GA100。
我們先看 GA100 的 SM:
這里面最核心的升級就是 Tensor Core 了。
除了在 Volta 中的 FP16 以及在 Turing 中的 INT8/INT4/Binary,這個版本新加入了 TF32, BF16, FP64 的支持。著重說說 TF32 和 BF16, 如下圖:
FP16 的問題在于表示范圍不夠大,在梯度計算時容易出現 underflow, 而且前后向計算也相對容易出現 overflow, 相對來說,在深度學習計算里,范圍比精度要重要得多,于是有了 BF16,犧牲了精度,保持和 FP32 差不多的范圍,在此前比較知名支持 BF16 的就是 TPU. 而 TF32 的設計,在于即汲取了 BF16 的好處,又保持了一定程度對主流 FP32 的兼容,FP32 只要截斷就是 TF32 了。先截斷成 TF32 計算,再轉成 FP32, 對歷史的工作幾乎無影響,如下圖:
另一個變化則是細粒度的結構化稀疏,深度學習模型壓縮這個領域除了量化,稀疏也是一個大方向,只是稀疏化模型難以利用硬件加速,這個版本的 GPU 則為稀疏提供了一些支持,當前的主要目的則是應用于 Inference 場景。
首先說 NVIDIA 定義的稀疏矩陣,這里稱為 2:4 的結構化稀疏,2:4 的意思是每 4 個元素當中有 2 個值非 0,如下圖:
首先使用正常的稠密 weight 訓練,訓練到收斂后裁剪到 2:4 的結構化稀疏 Tensor,然后走 fine tune 繼續訓練非 0 的 weight, 之后得到的 2:4 結構化稀疏 weight
理想情況下具有和稠密 weight 一樣的精確度,然后使用此稀疏化后的 weight 進行 Inference. 而這個版本的 TensorCore 支持一個 2:4 的結構化稀疏矩陣與另一個稠密矩陣直接相乘。
最后一個比較重要的特性就是 MIG(Multi-Instance GPU)了,雖然業界的計算規模確實越來越大,但也存在不少的任務因為其特性導致無法用滿 GPU 導致資源浪費,所以存在需求在一個 GPU 上跑多個任務,在這之前有些云計算廠商會提供虛擬化方案。而在安培中,會為此需求提供支持,稱為 MIG.
可能會有人有疑問,在 Volta 中引入的多進程支持不是解決了問題嗎?舉個例子,在 Volta 中,雖然多個進程可以并行,但是由于所有進程都可以訪問所有的內存資源,可能存在一個進程把所有的 DRAM 帶寬占滿影響到其他進程的運行,而這些被影響的進程很可能有 Throughput/Latency 要求。所以我們需要更嚴格的隔離。
而在安培 MIG 中,每個 A100 可以被分為 7 個 GPU 實例被不同的任務使用。每個實例的 SMs 有獨立的內存資源,可以保證每個任務有符合預期的穩定的 Throughput/Latency. 用戶可以將這些虛擬的 GPU 實例當成真實的 GPU 使用。
結語
事實上關于各個架構的細節還有很多,限于篇幅這里只能簡單概述。有機會后面再分享一些更具體的關于 CUDA 編程的東西。也歡迎大家與我多多交流(線上線下都歡迎),共同進步。
原文來源:
https://zhuanlan.zhihu.com/p/413145211
Reference
- https://images.nvidia.com/aem-dam/en-zz/Solutions/geforce/ampere/pdf/NVIDIA-ampere-GA102-GPU-Architecture-Whitepaper-V1.pdf
- https://images.nvidia.com/aem-dam/en-zz/Solutions/design-visualization/technologies/turing-architecture/NVIDIA-Turing-Architecture-Whitepaper.pdf
- https://images.nvidia.com/content/volta-architecture/pdf/volta-architecture-whitepaper.pdf
- https://images.nvidia.com/content/pdf/tesla/whitepaper/pascal-architecture-whitepaper.pdf
- https://www.microway.com/download/whitepaper/NVIDIA_Maxwell_GM204_Architecture_Whitepaper.pdf
- https://www.nvidia.com/content/PDF/product-specifications/GeForce_GTX_680_Whitepaper_FINAL.pdf
- http://www.hardwarebg.com/b4k/files/nvidia_gf100_whitepaper.pdf
- https://developer.nvidia.com/content/life-triangle-nvidias-logical-pipeline
- https://blog.nowcoder.net/n/4dcb2f6a55a34de9ae6c9067ba3d3bfb
- https://jcf94.com/2020/05/24/2020-05-24-nvidia-arch/
- https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html
- https://en.wikipedia.org/wiki/Bfloat16_floating-point_format