一、引子
上一講,我帶你一起看了三維圖形在計算機里的渲染過程。這個渲染過程,分成了頂點處理、圖元處理、柵格化、片段處理,以及最后的像素操作。這一連串的過程,
也被稱之為圖形流水線或者渲染管線。
因為要實時計算渲染的像素特別地多,圖形加速卡登上了歷史的舞台。通過3dFx的Voodoo或者NVidia的TNT這樣的圖形加速卡,
CPU就不需要再去處理一個個像素點的圖元處理、柵格化和片段處理這些操作。而3D游戲也是從這個時代發展起來的。
你可以看這張圖,這是“古墓麗影”游戲的多邊形建模的變化。這個變化,則是從1996年到2016年,這20年來顯卡的進步帶來的。
二、Shader的誕生和可編程圖形處理器
1、無論你的顯卡有多快,如果CPU不行,3D畫面一樣還是不行
不知道你有沒有發現,在Voodoo和TNT顯卡的渲染管線里面,沒有“頂點處理“這個步驟。在當時,把多邊形的頂點進行線性變化,轉化到我們的屏幕的坐標系的工作還是由CPU完成的。
所以,CPU的性能越好,能夠支持的多邊形也就越多,對應的多邊形建模的效果自然也就越像真人。而3D游戲的多邊形性能也受限
於我們CPU的性能。無論你的顯卡有多快,如果CPU不行,3D畫面一樣還是不行。
2、1999年NVidia推出的GeForce 256顯卡
所以,1999年NVidia推出的GeForce 256顯卡,就把頂點處理的計算能力,也從CPU里挪到了顯卡里。不過,這對於想要做好3D游戲的程序員們還不夠,
即使到了GeForce 256。整個圖形渲染過程都是在硬件里面固定的管線來完成的。程序員們在加速卡上能做的事情呢,只有改配置來實現不同的圖形渲染效果。如果通
過改配置做不到,我們就沒有什么辦法了。
3、程序員希望我們的GPU也能有一定的可編程能力
這個時候,程序員希望我們的GPU也能有一定的可編程能力。這個編程能力不是像CPU那樣,有非常通用的指令,可以進行任何你希望的操作,
而是在整個的 渲染管線(Graphics Pipeline)的一些特別步驟,能夠自己去定義處理數據的算法或者操作。於是,從2001年的Direct3D 8.0開始,
微軟第一次引入了 可編程管線(Programable Function Pipeline)的概念。
一開始的可編程管線呢,僅限於頂點處理(Vertex Processing)和片段處理(Fragment Processing)部分。比起原來只能通過顯卡和Direct3D這樣的圖形接口提供的固定配置,
程序員們終於也可以開始在圖形效果上開始大顯身手了。
這些可以編程的接口,我們稱之為 Shader,中文名稱就是 着色器。之所以叫“着色器”,是因為一開始這些“可編程”的接口,只能修改頂點處理和片段處理部分的程序邏輯。
我們用這些接口來做的,也主要是光照、亮度、顏色等等的處理,所以叫着色器。
4、Shader的誕生
這些可以編程的接口,我們稱之為 Shader,中文名稱就是 着色器。之所以叫“着色器”,是因為一開始這些“可編程”的接口,只能修改頂點處理和片段處理部分的程序邏輯。
我們用這些接口來做的,也主要是光照、亮度、顏色等等的處理,所以叫着色器
Vertex Shader和Fragment Shader這兩類Shader都是獨立的硬件電路
這個時候的GPU,有兩類Shader,也就是Vertex Shader和Fragment Shader。我們在上一講看到,在進行頂點處理的時候,我們操作的是多邊形的頂點;在片段操作的時候,
我們操作的是屏幕上的像素點。對於頂點的操作,通常比片段要復雜一些。所以一開始,這兩類Shader都是獨立的硬件電路,也各自有獨立的編程接口。因為這么做,
硬件設計起來更加簡單,一塊GPU上也能容納下更多的Shader。
5、獨立的硬件電路存在什么問題
不過呢,大家很快發現,雖然我們在頂點處理和片段處理上的具體邏輯不太一樣,但是里面用到的指令集可以用同一套。而且,雖然把Vertex Shader和Fragment Shader分開,
可以減少硬件設計的復雜程度,但是也帶來了一種浪費,有一半Shader始終沒有被使用。在整個渲染管線里,Vertext Shader運行的時候,Fragment Shader停在那里什么也沒干。
Fragment Shader在運行的時候,Vertext Shader也停在那里發呆。
6、統一着色器架構
本來GPU就不便宜,結果設計的電路有一半時間是閑着的。喜歡精打細算摳出每一分性能的硬件工程師當然受不了了。於是, 統一着色器架構(Unified Shader Architecture)就應運而生了。
既然大家用的指令集是一樣的,那不如就在GPU里面放很多個一樣的Shader硬件電路,然后通過統一調度,把頂點處理、圖元處理、片段處理這些任務,都交給這些Shader去處理,
讓整個GPU盡可能地忙起來。這樣的設計,就是我們現代GPU的設計,就是統一着色器架構。
7、通用圖形處理器
有意思的是,這樣的GPU並不是先在PC里面出現的,而是來自於一台游戲機,就是微軟的XBox 360。后來,這個架構才被用到ATI和NVidia的顯卡里。這個時候的“着色器”的作用,
其實已經和它的名字關系不大了,而是變成了一個通用的抽象計算模塊的名字。
正是因為Shader變成一個“通用”的模塊,才有了把GPU拿來做各種通用計算的用法,也就是 GPGPU(General-Purpose Computing on Graphics Processing Units,通用圖形處理器)。
而正是因為GPU可以拿來做各種通用的計算,才有了過去10年深度學習的火熱。
三、現代GPU的三個核心創意
講完了現代GPU的進化史,那么接下來,我們就來看看,為什么現代的GPU在圖形渲染、深度學習上能那么快
1、芯片瘦身
我們先來回顧一下,之前花了很多講仔細講解的現代CPU。現代CPU里的晶體管變得越來越多,越來越復雜,其實已經不是用來實現“計算”這個核心功能,而是拿來實現處理亂序執行、
進行分支預測,以及我們之后要在存儲器講的高速緩存部分。
而在GPU里,這些電路就顯得有點多余了,GPU的整個處理過程是一個流式處理(Stream Processing)的過程。因為沒有那么多分支條件,或者復雜的依賴關系,
我們可以把GPU里這些對應的電路都可以去掉,做一次小小的瘦身,只留下取指令、指令譯碼、ALU以及執行這些計算需要的寄存器和緩存就好了。一般來說,我們會把這些電路抽象成三個部分,就是下面圖里的取指令和指令譯碼、ALU和執行上下文。
2、多核並行和SIMT
1、多核並行
這樣一來,我們的GPU電路就比CPU簡單很多了。於是,我們就可以在一個GPU里面,塞很多個這樣並行的GPU電路來實現計算,就好像CPU里面的多核CPU一樣。
和CPU不同的是,我們不需要單獨去實現什么多線程的計算。因為GPU的運算是天然並行的。
我們在上一講里面其實已經看到,無論是對多邊形里的頂點進行處理,還是屏幕里面的每一個像素進行處理,每個點的計算都是獨立的。所以,簡單地添加多核的GPU,
就能做到並行加速。不過光這樣加速還是不夠,工程師們覺得,性能還有進一步被壓榨的空間。
2、SIMT技術
我們在第27講里面講過,CPU里有一種叫作SIMD的處理技術。這個技術是說,在做向量計算的時候,我們要執行的指令是一樣的,只是同一個指令的數據有所不同而已。
在GPU的渲染管線里,這個技術可就大有用處了。
無論是頂點去進行線性變換,還是屏幕上臨近像素點的光照和上色,都是在用相同的指令流程進行計算。所以,GPU就借鑒了CPU里面的SIMD,用了一種叫作SIMT(Single Instruction,Multiple Threads)的技術。SIMT呢,比SIMD更加靈活。在SIMD里面,CPU一次性取出了固定長度的多個數據,放到寄存器里面,用一個指令去執行。而SIMT,可以把多條數據,交給不同的線程去處理。
各個線程里面執行的指令流程是一樣的,但是可能根據數據的不同,走到不同的條件分支。這樣,相同的代碼和相同的流程,可能執行不同的具體的指令。這個線程走到的是if的條件分支,
另外一個線程走到的就是else的條件分支了。
於是,我們的GPU設計就可以進一步進化,也就是在取指令和指令譯碼的階段,取出的指令可以給到后面多個不同的ALU並行進行運算。這樣,我們的一個GPU的核里,
就可以放下更多的ALU,同時進行更多的並行運算了。
3、GPU里的“超線程”
雖然GPU里面的主要以數值計算為主。不過既然已經是一個“通用計算”的架構了,GPU里面也避免不了會有if…else這樣的條件分支。但是,在GPU里我們可沒有CPU這樣的分支預測的電路。
這些電路在上面“芯片瘦身”的時候,就已經被我們砍掉了。
所以,GPU里的指令,可能會遇到和CPU類似的“流水線停頓”問題。想到流水線停頓,你應該就能記起,我們之前在CPU里面講過超線程技術。在GPU上,我們一樣可以做類似的事情,
也就是遇到停頓的時候,調度一些別的計算任務給當前的ALU。
和超線程一樣,既然要調度一個不同的任務過來,我們就需要針對這個任務,提供更多的 執行上下文。所以,一個Core里面的 執行上下文的數量,需要比ALU多。
四、GPU在深度學習上的性能差異
在通過芯片瘦身、SIMT以及更多的執行上下文,我們就有了一個更擅長並行進行暴力運算的GPU。這樣的芯片,也正適合我們今天的深度學習的使用場景。
一方面,GPU是一個可以進行“通用計算”的框架,我們可以通過編程,在GPU上實現不同的算法。另一方面,現在的深度學習計算,都是超大的向量和矩陣,海量的訓練樣本的計算。
整個計算過程中,沒有復雜的邏輯和分支,非常適合GPU這樣並行、計算能力強的架構。
我們去看NVidia 2080顯卡的技術規格,就可以算出,它到底有多大的計算能力。
2080一共有46個SM(Streaming Multiprocessor,流式處理器),這個SM相當於GPU里面的GPU Core,所每個SM里面有64個Cuda Core。
你可以認為,這里的Cuda Core就是我們上面說的ALU的數量或者Pixel Shader的數量,46x64呢一共就有2944個Shader。然后,還有184個TMU,TMU就是Texture Mapping Unit,
也就是用來做紋理映射的計算單元,它也可以認為是另一種類型的Shader。
2080的主頻是1515MHz,如果自動超頻(Boost)的話,可以到1700MHz。而NVidia的顯卡,根據硬件架構的設計,每個時鍾周期可以執行兩條指令。所以,能做的浮點數運算的能力,就是:
(2944 + 184)× 1700 MHz × 2 = 10.06 TFLOPS
對照一下官方的技術規格,正好就是10.07TFLOPS。
那么,最新的Intel i9 9900K的性能是多少呢?不到1TFLOPS。而2080顯卡和9900K的價格卻是差不多的。所以,在實際進行深度學習的過程中,用GPU所花費的時間,
往往能減少一到兩個數量級。而大型的深度學習模型計算,往往又是多卡並行,要花上幾天乃至幾個月。這個時候,用CPU顯然就不合適了。
今天,隨着GPGPU的推出,GPU已經不只是一個圖形計算設備,更是一個用來做數值計算的好工具了。同樣,也是因為GPU的快速發展,帶來了過去10年深度學習的繁榮
五、總結延伸
這一講里面,我們講了,GPU一開始是沒有“可編程”能力的,程序員們只能夠通過配置來設計需要用到的圖形渲染效果。隨着“可編程管線”的出現,
程序員們可以在頂點處理和片段處理去實現自己的算法。為了進一步去提升GPU硬件里面的芯片利用率,微軟在XBox 360里面,第一次引入了“統一着色器架構”,使
得GPU變成了一個有“通用計算”能力的架構。
接着,我們從一個CPU的硬件電路出發,去掉了對GPU沒有什么用的分支預測和亂序執行電路,來進行瘦身。之后,基於渲染管線里面頂點處理和片段處理就是天然可以並行的了。
我們在GPU里面可以加上很多個核。
又因為我們的渲染管線里面,整個指令流程是相同的,我們又引入了和CPU里的SIMD類似的SIMT架構。這個改動,進一步增加了GPU里面的ALU的數量。
最后,為了能夠讓GPU不要遭遇流水線停頓,我們又在同一個GPU的計算核里面,加上了更多的執行上下文,讓GPU始終保持繁忙。
GPU里面的多核、多ALU,加上多Context,使得它的並行能力極強。同樣架構的GPU,如果光是做數值計算的話,算力在同樣價格的CPU的十倍以上。
而這個強大計算能力,以及“統一着色器架構”,使得GPU非常適合進行深度學習的計算模式,也就是海量計算,容易並行,並且沒有太多的控制分支邏輯。
使用GPU進行深度學習,往往能夠把深度學習算法的訓練時間,縮短一個,乃至兩個數量級。而GPU現在也越來越多地用在各種科學計算和機器學習上,而不僅僅是用在圖形渲染上了。