CUDA——"從入門到放棄"

 轉載:https://www.jianshu.com/p/34a504af8d51

1. 知識準備

1.1 中央處理器(CPU)

中央處理器(CPU,Central Processing Unit)是一塊超大規模的集成電路,是一臺計算機的運算核心(Core)和控制核心( Control Unit)。它的功能主要是解釋計算機指令以及處理計算機軟件中的數據。
中央處理器主要包括運算器(算術邏輯運算單元,ALU,Arithmetic Logic Unit)和高速緩衝存儲器(Cache)及實現它們之間聯繫的數據(Data)、控制及狀態的總線(Bus)。它與內部存儲器(Memory)和輸入/輸出(I/O)設備合稱爲電子計算機三大核心部件。

CPU的結構主要包括運算器(ALU, Arithmetic and Logic Unit)、控制單元(CU, Control Unit)、寄存器(Register)、高速緩存器(Cache)和它們之間通訊的數據、控制及狀態的總線

簡單來說就是:計算單元、控制單元和存儲單元,架構如下圖所示:

CPU微架構示意圖

什麼?架構記不住?來,我們換種表示方法:

CPU微架構示意圖(改)

嗯,大概就是這個意思。

從字面上我們也很好理解,計算單元主要執行算術運算、移位等操作以及地址運算和轉換存儲單元主要用於保存運算中產生的數據以及指令等控制單元則對指令譯碼,並且發出爲完成每條指令所要執行的各個操作的控制信號

所以一條指令在CPU中執行的過程是這樣的:讀取到指令後,通過指令總線送到控制器(黃色區域)中進行譯碼,併發出相應的操作控制信號;然後運算器(綠色區域)按照操作指令對數據進行計算,並通過數據總線將得到的數據存入數據緩存器(大塊橙色區域)。過程如下圖所示:

CPU執行指令圖

是不是有點兒複雜?沒關係,這張圖完全不用記住,我們只需要知道,CPU遵循的是馮諾依曼架構,其核心就是:存儲程序,順序執行

講到這裏,有沒有看出問題,沒錯——在這個結構圖中,負責計算的綠色區域佔的面積似乎太小了,而橙色區域的緩存Cache和黃色區域的控制單元佔據了大量空間。

高中化學有句老生常談的話叫:結構決定性質,放在這裏也非常適用。

因爲CPU的架構中需要大量的空間去放置存儲單元(橙色部分)和控制單元(黃色部分),相比之下計算單元(綠色部分)只佔據了很小的一部分,所以它在大規模並行計算能力上極受限制,而更擅長於邏輯控制。

另外,因爲遵循馮諾依曼架構(存儲程序,順序執行),CPU就像是個一板一眼的管家,人們吩咐的事情它總是一步一步來做。但是隨着人們對更大規模與更快處理速度的需求的增加,這位管家漸漸變得有些力不從心。

於是,大家就想,能不能把多個處理器放在同一塊芯片上,讓它們一起來做事,這樣效率不就提高了嗎?

沒錯,GPU便由此誕生了。


1.2 顯卡

顯卡(Video card,Graphics card)全稱顯示接口卡,又稱顯示適配器,是計算機最基本配置、最重要的配件之一。顯卡作爲電腦主機裏的一個重要組成部分,是電腦進行數模信號轉換的設備,承擔輸出顯示圖形的任務。顯卡接在電腦主板上,它將電腦的數字信號轉換成模擬信號讓顯示器顯示出來,同時顯卡還是有圖像處理能力,可協助CPU工作,提高整體的運行速度。對於從事專業圖形設計的人來說顯卡非常重要。 民用和軍用顯卡圖形芯片供應商主要包括AMD(超微半導體)Nvidia(英偉達)2家。現在的top500計算機,都包含顯卡計算核心。在科學計算中,顯卡被稱爲顯示加速卡

爲什麼GPU特別擅長處理圖像數據呢?這是因爲圖像上的每一個像素點都有被處理的需要,而且每個像素點處理的過程和方式都十分相似,也就成了GPU的天然溫牀。
 

GPU微架構示意圖

從架構圖我們就能很明顯的看出,GPU的構成相對簡單,有數量衆多的計算單元和超長的流水線,特別適合處理大量的類型統一的數據。

再把CPU和GPU兩者放在一張圖上看下對比,就非常一目瞭然了。

GPU的工作大部分都計算量大,但沒什麼技術含量,而且要重複很多很多次。

但GPU無法單獨工作,必須由CPU進行控制調用才能工作。CPU可單獨作用,處理複雜的邏輯運算和不同的數據類型,但當需要大量的處理類型統一的數據時,則可調用GPU進行並行計算。

借用知乎上某大佬的說法,就像你有個工作需要計算幾億次一百以內加減乘除一樣,最好的辦法就是僱上幾十個小學生一起算,一人算一部分,反正這些計算也沒什麼技術含量,純粹體力活而已;而CPU就像老教授,積分微分都會算,就是工資高,一個老教授資頂二十個小學生,你要是富士康你僱哪個?

注:GPU中有很多的運算器ALU和很少的緩存cache,緩存的目的不是保存後面需要訪問的數據的,這點和CPU不同,而是爲線程thread提高服務的。如果有很多線程需要訪問同一個相同的數據,緩存會合並這些訪問,然後再去訪問dram。

可愛的你如果對CUDA硬件有更多的興趣,可移步NVIDIA中文官網進一步學習。
 

1.3 內存

內存是計算機中重要的部件之一,它是與CPU進行溝通的橋樑。計算機中所有程序的運行都是在內存中進行的,因此內存的性能對計算機的影響非常大。內存(Memory)也被稱爲內存儲器,其作用是用於暫時存放CPU中的運算數據,以及與硬盤外部存儲器交換的數據。只要計算機在運行中,CPU就會把需要運算的數據調到內存中進行運算,當運算完成後CPU再將結果傳送出來,內存的運行也決定了計算機的穩定運行。 內存是由內存芯片、電路板、金手指等部分組成的。

1.4 顯存

顯存,也被叫做幀緩存,它的作用是用來存儲顯卡芯片處理過或者即將提取的渲染數據。如同計算機的內存一樣,顯存是用來存儲要處理的圖形信息的部件。

1.5 顯卡、顯卡驅動、CUDA之間的關係

顯卡:(GPU)主流是NVIDIA的GPU,深度學習本身需要大量計算。GPU的並行計算能力,在過去幾年裏恰當地滿足了深度學習的需求。AMD的GPU基本沒有什麼支持,可以不用考慮。

驅動:沒有顯卡驅動,就不能識別GPU硬件,不能調用其計算資源。但是呢,NVIDIA在Linux上的驅動安裝特別麻煩,尤其對於新手簡直就是噩夢。得屏蔽第三方顯卡驅動。下面會給出教程。

CUDA:是NVIDIA推出的只能用於自家GPU的並行計算框架。只有安裝這個框架才能夠進行復雜的並行計算。主流的深度學習框架也都是基於CUDA進行GPU並行加速的,幾乎無一例外。還有一個叫做cudnn,是針對深度卷積神經網絡的加速庫。

查看顯卡驅動信息(以實驗室服務器爲例)

ssh [email protected]

輸入服務器密碼登陸
然後,進入cuda

cd /usr/local/cuda-8.0/samples/1_Utilities/deviceQuery

運行其中的可執行文件

./deviceQuery

得到如下信息

./deviceQuery Starting...

 CUDA Device Query (Runtime API) version (CUDART static linking)

Detected 4 CUDA Capable device(s)

Device 0: "GeForce GTX 1080 Ti"
  CUDA Driver Version / Runtime Version          9.0 / 8.0
  CUDA Capability Major/Minor version number:    6.1
  Total amount of global memory:                 11171 MBytes (11713708032 bytes)
  (28) Multiprocessors, (128) CUDA Cores/MP:     3584 CUDA Cores
  GPU Max Clock rate:                            1620 MHz (1.62 GHz)
  Memory Clock rate:                             5505 Mhz
  Memory Bus Width:                              352-bit
  L2 Cache Size:                                 2883584 bytes
  Maximum Texture Dimension Size (x,y,z)         1D=(131072), 2D=(131072, 65536), 3D=(16384, 16384, 16384)
  Maximum Layered 1D Texture Size, (num) layers  1D=(32768), 2048 layers
  Maximum Layered 2D Texture Size, (num) layers  2D=(32768, 32768), 2048 layers
  Total amount of constant memory:               65536 bytes
  Total amount of shared memory per block:       49152 bytes
  Total number of registers available per block: 65536
  Warp size:                                     32
  Maximum number of threads per multiprocessor:  2048
  Maximum number of threads per block:           1024
  Max dimension size of a thread block (x,y,z): (1024, 1024, 64)
  Max dimension size of a grid size    (x,y,z): (2147483647, 65535, 65535)
  Maximum memory pitch:                          2147483647 bytes
  Texture alignment:                             512 bytes
  Concurrent copy and kernel execution:          Yes with 2 copy engine(s)
  Run time limit on kernels:                     No
  Integrated GPU sharing Host Memory:            No
  Support host page-locked memory mapping:       Yes
  Alignment requirement for Surfaces:            Yes
  Device has ECC support:                        Disabled
  Device supports Unified Addressing (UVA):      Yes
  Device PCI Domain ID / Bus ID / location ID:   0 / 2 / 0
  Compute Mode:
     < Default (multiple host threads can use ::cudaSetDevice() with device simultaneously) >

Device 1: "GeForce GTX 1080 Ti"
  CUDA Driver Version / Runtime Version          9.0 / 8.0
  CUDA Capability Major/Minor version number:    6.1
  Total amount of global memory:                 11172 MBytes (11715084288 bytes)
  (28) Multiprocessors, (128) CUDA Cores/MP:     3584 CUDA Cores
  GPU Max Clock rate:                            1620 MHz (1.62 GHz)
  Memory Clock rate:                             5505 Mhz
  Memory Bus Width:                              352-bit
  L2 Cache Size:                                 2883584 bytes
  Maximum Texture Dimension Size (x,y,z)         1D=(131072), 2D=(131072, 65536), 3D=(16384, 16384, 16384)
  Maximum Layered 1D Texture Size, (num) layers  1D=(32768), 2048 layers
  Maximum Layered 2D Texture Size, (num) layers  2D=(32768, 32768), 2048 layers
  Total amount of constant memory:               65536 bytes
  Total amount of shared memory per block:       49152 bytes
  Total number of registers available per block: 65536
  Warp size:                                     32
  Maximum number of threads per multiprocessor:  2048
  Maximum number of threads per block:           1024
  Max dimension size of a thread block (x,y,z): (1024, 1024, 64)
  Max dimension size of a grid size    (x,y,z): (2147483647, 65535, 65535)
  Maximum memory pitch:                          2147483647 bytes
  Texture alignment:                             512 bytes
  Concurrent copy and kernel execution:          Yes with 2 copy engine(s)
  Run time limit on kernels:                     No
  Integrated GPU sharing Host Memory:            No
  Support host page-locked memory mapping:       Yes
  Alignment requirement for Surfaces:            Yes
  Device has ECC support:                        Disabled
  Device supports Unified Addressing (UVA):      Yes
  Device PCI Domain ID / Bus ID / location ID:   0 / 3 / 0
  Compute Mode:
     < Default (multiple host threads can use ::cudaSetDevice() with device simultaneously) >

Device 2: "GeForce GTX 1080 Ti"
  CUDA Driver Version / Runtime Version          9.0 / 8.0
  CUDA Capability Major/Minor version number:    6.1
  Total amount of global memory:                 11172 MBytes (11715084288 bytes)
  (28) Multiprocessors, (128) CUDA Cores/MP:     3584 CUDA Cores
  GPU Max Clock rate:                            1620 MHz (1.62 GHz)
  Memory Clock rate:                             5505 Mhz
  Memory Bus Width:                              352-bit
  L2 Cache Size:                                 2883584 bytes
  Maximum Texture Dimension Size (x,y,z)         1D=(131072), 2D=(131072, 65536), 3D=(16384, 16384, 16384)
  Maximum Layered 1D Texture Size, (num) layers  1D=(32768), 2048 layers
  Maximum Layered 2D Texture Size, (num) layers  2D=(32768, 32768), 2048 layers
  Total amount of constant memory:               65536 bytes
  Total amount of shared memory per block:       49152 bytes
  Total number of registers available per block: 65536
  Warp size:                                     32
  Maximum number of threads per multiprocessor:  2048
  Maximum number of threads per block:           1024
  Max dimension size of a thread block (x,y,z): (1024, 1024, 64)
  Max dimension size of a grid size    (x,y,z): (2147483647, 65535, 65535)
  Maximum memory pitch:                          2147483647 bytes
  Texture alignment:                             512 bytes
  Concurrent copy and kernel execution:          Yes with 2 copy engine(s)
  Run time limit on kernels:                     No
  Integrated GPU sharing Host Memory:            No
  Support host page-locked memory mapping:       Yes
  Alignment requirement for Surfaces:            Yes
  Device has ECC support:                        Disabled
  Device supports Unified Addressing (UVA):      Yes
  Device PCI Domain ID / Bus ID / location ID:   0 / 130 / 0
  Compute Mode:
     < Default (multiple host threads can use ::cudaSetDevice() with device simultaneously) >

Device 3: "GeForce GTX 1080 Ti"
  CUDA Driver Version / Runtime Version          9.0 / 8.0
  CUDA Capability Major/Minor version number:    6.1
  Total amount of global memory:                 11172 MBytes (11715084288 bytes)
  (28) Multiprocessors, (128) CUDA Cores/MP:     3584 CUDA Cores
  GPU Max Clock rate:                            1620 MHz (1.62 GHz)
  Memory Clock rate:                             5505 Mhz
  Memory Bus Width:                              352-bit
  L2 Cache Size:                                 2883584 bytes
  Maximum Texture Dimension Size (x,y,z)         1D=(131072), 2D=(131072, 65536), 3D=(16384, 16384, 16384)
  Maximum Layered 1D Texture Size, (num) layers  1D=(32768), 2048 layers
  Maximum Layered 2D Texture Size, (num) layers  2D=(32768, 32768), 2048 layers
  Total amount of constant memory:               65536 bytes
  Total amount of shared memory per block:       49152 bytes
  Total number of registers available per block: 65536
  Warp size:                                     32
  Maximum number of threads per multiprocessor:  2048
  Maximum number of threads per block:           1024
  Max dimension size of a thread block (x,y,z): (1024, 1024, 64)
  Max dimension size of a grid size    (x,y,z): (2147483647, 65535, 65535)
  Maximum memory pitch:                          2147483647 bytes
  Texture alignment:                             512 bytes
  Concurrent copy and kernel execution:          Yes with 2 copy engine(s)
  Run time limit on kernels:                     No
  Integrated GPU sharing Host Memory:            No
  Support host page-locked memory mapping:       Yes
  Alignment requirement for Surfaces:            Yes
  Device has ECC support:                        Disabled
  Device supports Unified Addressing (UVA):      Yes
  Device PCI Domain ID / Bus ID / location ID:   0 / 131 / 0
  Compute Mode:
     < Default (multiple host threads can use ::cudaSetDevice() with device simultaneously) >
> Peer access from GeForce GTX 1080 Ti (GPU0) -> GeForce GTX 1080 Ti (GPU1) : Yes
> Peer access from GeForce GTX 1080 Ti (GPU0) -> GeForce GTX 1080 Ti (GPU2) : No
> Peer access from GeForce GTX 1080 Ti (GPU0) -> GeForce GTX 1080 Ti (GPU3) : No
> Peer access from GeForce GTX 1080 Ti (GPU1) -> GeForce GTX 1080 Ti (GPU0) : Yes
> Peer access from GeForce GTX 1080 Ti (GPU1) -> GeForce GTX 1080 Ti (GPU2) : No
> Peer access from GeForce GTX 1080 Ti (GPU1) -> GeForce GTX 1080 Ti (GPU3) : No
> Peer access from GeForce GTX 1080 Ti (GPU2) -> GeForce GTX 1080 Ti (GPU0) : No
> Peer access from GeForce GTX 1080 Ti (GPU2) -> GeForce GTX 1080 Ti (GPU1) : No
> Peer access from GeForce GTX 1080 Ti (GPU2) -> GeForce GTX 1080 Ti (GPU3) : Yes
> Peer access from GeForce GTX 1080 Ti (GPU3) -> GeForce GTX 1080 Ti (GPU0) : No
> Peer access from GeForce GTX 1080 Ti (GPU3) -> GeForce GTX 1080 Ti (GPU1) : No
> Peer access from GeForce GTX 1080 Ti (GPU3) -> GeForce GTX 1080 Ti (GPU2) : Yes

deviceQuery, CUDA Driver = CUDART, CUDA Driver Version = 9.0, CUDA Runtime Version = 8.0, NumDevs = 4, Device0 = GeForce GTX 1080 Ti, Device1 = GeForce GTX 1080 Ti, Device2 = GeForce GTX 1080 Ti, Device3 = GeForce GTX 1080 Ti
Result = PASS

大家可以在自己PC或者工作機上嘗試一下。

再囉嗦兩句

GPU就是用很多簡單的計算單元去完成大量的計算任務,純粹的人海戰術。這種策略基於一個前提,就是小學生A和小學生B的工作沒有什麼依賴性,是互相獨立的。

但有一點需要強調,雖然GPU是爲了圖像處理而生的,但是我們通過前面的介紹可以發現,它在結構上並沒有專門爲圖像服務的部件,只是對CPU的結構進行了優化與調整,所以現在GPU不僅可以在圖像處理領域大顯身手,它還被用來科學計算、密碼破解、數值分析,海量數據處理(排序,Map-Reduce等),金融分析等需要大規模並行計算的領域。

所以GPU也可以認爲是一種較通用的芯片。

 

2. CUDA軟件構架

CUDA是一種新的操作GPU計算的硬件和軟件架構,它將GPU視作一個數據並行計算設備,而且無需把這些計算映射到圖形API。操作系統的多任務機制可以同時管理CUDA訪問GPU和圖形程序的運行庫,其計算特性支持利用CUDA直觀地編寫GPU核心程序。目前Tesla架構具有在筆記本電腦、臺式機、工作站和服務器上的廣泛可用性,配以C/C++語言的編程環境和CUDA軟件,使這種架構得以成爲最優秀的超級計算平臺。

CUDA軟件層次結構


CUDA在軟件方面組成有:一個CUDA庫、一個應用程序編程接口(API)及其運行庫(Runtime)、兩個較高級別的通用數學庫,即CUFFT和CUBLAS。CUDA改進了DRAM的讀寫靈活性,使得GPU與CPU的機制相吻合。另一方面,CUDA提供了片上(on-chip)共享內存,使得線程之間可以共享數據。應用程序可以利用共享內存來減少DRAM的數據傳送,更少的依賴DRAM的內存帶寬。

 

3. 編程模型

CUDA程序構架分爲兩部分:HostDevice。一般而言,Host指的是CPUDevice指的是GPU。在CUDA程序構架中,主程序還是由CPU來執行,而當遇到數據並行處理的部分,CUDA 就會將程序編譯成GPU能執行的程序,並傳送到GPU。而這個程序在CUDA裏稱做(kernel)。CUDA允許程序員定義稱爲核的C語言函數,從而擴展了C語言,在調用此類函數時,它將由N個不同的CUDA線程並行執行N次,這與普通的C語言函數只執行一次的方式不同。執行核的每個線程都會被分配一個獨特的線程ID,可通過內置的threadIdx變量在內核中訪問此ID。在 CUDA 程序中,主程序在調用任何GPU內核之前,必須對核進行執行配置,即確定線程塊數和每個線程塊中的線程數以及共享內存大小。

3.1 線程層次結構

在GPU中要執行的線程,根據最有效的數據共享來創建塊(Block),其類型有一維、二維或三維。在同一個塊內的線程可彼此協作,通過一些共享存儲器來共享數據,並同步其執行來協調存儲器訪問。一個塊中的所有線程都必須位於同一個處理器核心中。因而,一個處理器核心的有限存儲器資源制約了每個塊的線程數量。在早期的NVIDIA 架構中,一個線程塊最多可以包含 512個線程,而在後期出現的一些設備中則最多可支持1024個線程。一般GPGPU程序線程數目是很多的,所以不能把所有的線程都塞到同一個塊裏。但一個內核可由多個大小相同的線程塊同時執行,因而線程總數應等於每個塊的線程數乘以塊的數量。這些同樣維度和大小的塊將組織爲一個一維或二維線程塊網格(Grid)。具體框架如下圖所示。

線程塊網格

NOTICE:

線程(Thread)
一般通過GPU的一個核進行處理。(可以表示成一維,二維,三維,具體下面再細說)。
線程塊(Block)

  1. 由多個線程組成(可以表示成一維,二維,三維,具體下面再細說)。
  2. 各block是並行執行的,block間無法通信,也沒有執行順序。
  3. 注意線程塊的數量限制爲不超過65535(硬件限制)。

線程格(Grid)
由多個線程塊組成(可以表示成一維,二維,三維,具體下面再細說)。
線程束
在CUDA架構中,線程束是指一個包含32個線程的集合,這個線程集合被“編織在一起”並且“步調一致”的形式執行。在程序中的每一行,線程束中的每個線程都將在不同數據上執行相同的命令。

從硬件上看

SP:最基本的處理單元,streaming processor,也稱爲CUDA core。最後具體的指令和任務都是在SP上處理的。GPU進行並行計算,也就是很多個SP同時做處理。
SM:多個SP加上其他的一些資源組成一個streaming multiprocessor。也叫GPU大核,其他資源如:warp scheduler,register,shared memory等。SM可以看做GPU的心臟(對比CPU核心),register和shared memory是SM的稀缺資源。CUDA將這些資源分配給所有駐留在SM中的threads。因此,這些有限的資源就使每個SM中active warps有非常嚴格的限制,也就限制了並行能力。

從軟件上看

thread:一個CUDA的並行程序會被以許多個threads來執行。
block:數個threads會被羣組成一個block,同一個block中的threads可以同步,也可以通過shared memory通信。
grid:多個blocks則會再構成grid。
warp:GPU執行程序時的調度單位,目前cuda的warp的大小爲32,同在一個warp的線程,以不同數據資源執行相同的指令,這就是所謂 SIMT。

3.2 存儲器層次結構

CUDA設備擁有多個獨立的存儲空間,其中包括:全局存儲器、本地存儲器、共享存儲器、常量存儲器、紋理存儲器和寄存器,如圖

CUDA設備上的存儲器

NOTICE:

主機(Host)
將CPU及系統的內存(內存條)稱爲主機。
設備(Device)
將GPU及GPU本身的顯示內存稱爲設備。
動態隨機存取存儲器(DRAM)
DRAM(Dynamic Random Access Memory),即動態隨機存取存儲器,最爲常見的系統內存DRAM只能將數據保持很短的時間。爲了保持數據,DRAM使用電容存儲,所以必須隔一段時間刷新(refresh)一次,如果存儲單元沒有被刷新,存儲的信息就會丟失。 (關機就會丟失數據)

CUDA線程可在執行過程中訪問多個存儲器空間的數據,如下圖所示其中:

  • 每個線程都有一個私有的本地存儲器。
  • 每個線程塊都有一個共享存儲器,該存儲器對於塊內的所有線程都是可見的,並且與塊具有相同的生命週期。
  • 所有線程都可訪問相同的全局存儲器。
  • 此外還有兩個只讀的存儲器空間,可由所有線程訪問,這兩個空間是常量存儲器空間和紋理存儲器空間。全局、固定和紋理存儲器空間經過優化,適於不同的存儲器用途。紋理存儲器也爲某些特殊的數據格式提供了不同的尋址模式以及數據過濾,方便Host對流數據的快速存取。
存儲器的應用層次

3.3 主機(Host)和設備(Device)

如下圖所示,CUDA假設線程可在物理上獨立的設備上執行,此類設備作爲運行C語言程序的主機的協處理器操作。內核在GPU上執行,而C語言程序的其他部分在CPU上執行(即串行代碼在主機上執行,而並行代碼在設備上執行)。此外,CUDA還假設主機和設備均維護自己的DRAM,分別稱爲主機存儲器和設備存儲器。因而,一個程序通過調用CUDA運行庫來管理對內核可見的全局、固定和紋理存儲器空間。這種管理包括設備存儲器的分配和取消分配,還包括主機和設備存儲器之間的數據傳輸。

 

4. CUDA軟硬件

4.1 CUDA術語

由於CUDA中存在許多概念和術語,諸如SM、block、SP等多個概念不容易理解,將其與CPU的一些概念進行比較,如下表所示。

CPU GPU 層次
算術邏輯和控制單元 流處理器(SM) 硬件
算術單元 批量處理器(SP) 硬件
進程 Block 軟件
線程 thread 軟件
調度單位 Warp 軟件

4.2 硬件利用率

當爲一個GPU分配一個內核函數,我們關心的是如何才能充分利用GPU的計算能力,但由於不同的硬件有不同的計算能力,SM一次最多能容納的線程數也不盡相同,SM一次最多能容納的線程數量主要與底層硬件的計算能力有關,如下表顯示了在不同的計算能力的設備上,每個線程塊上開啓不同數量的線程時設備的利用率。

計算能力 每個線程塊的線程數 1.0 1.1 1.2 1.3 2.0 2.1 3.0
64 67 50 50 50 33 33 50
96 100 100 75 75 50 50 75
128 100 100 100 100 67 67 100
192 100 100 94 94 100 100 94
96 100 100 100 100 100 100 10
... ... ... ...            

查看顯卡利用率 (以實驗室服務器爲例)
輸入以下命令

nvidia-smi
Thu Aug 23 21:06:36 2018       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 384.130                Driver Version: 384.130                   |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  GeForce GTX 108...  Off  | 00000000:02:00.0 Off |                  N/A |
| 29%   41C    P0    58W / 250W |      0MiB / 11171MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
|   1  GeForce GTX 108...  Off  | 00000000:03:00.0 Off |                  N/A |
| 33%   47C    P0    57W / 250W |      0MiB / 11172MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
|   2  GeForce GTX 108...  Off  | 00000000:82:00.0 Off |                  N/A |
| 36%   49C    P0    59W / 250W |      0MiB / 11172MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
|   3  GeForce GTX 108...  Off  | 00000000:83:00.0 Off |                  N/A |
| 33%   46C    P0    51W / 250W |      0MiB / 11172MiB |      1%      Default |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+

 

5. 並行計算

5.1 併發性

CUDA將問題分解成線程塊的網格,每塊包含多個線程。快可以按任意順序執行。不過在某個時間點上,只有一部分塊處於執行中。一旦被調用到GUP包含的N個“流處理器簇(SM)”中的一個上執行,一個塊必須從開始到結束。網格中的塊可以被分配到任意一個有空閒槽的SM上。起初,可以採用“輪詢調度”策略,以確保分配到每一個SM上的塊數基本相同。對絕大多數內核程序而言,分塊的數量應該是GPU中物理SM數量的八倍或更多倍。

以一個軍隊比喻,假設有一支由士兵(線程)組成的部隊(網格)。部隊被分成若干個連(塊),每個連隊由一位連長來指揮。按照32名士兵一個班(一個線程束),連隊又進一步分成若干個班,每個班由一個班長來指揮。

基於GPU的線程視圖

要執行某個操作,總司令(內核程序/ 主機程序)必須提供操作名稱及相應的數據。每個士兵(線程)只處理分配給他的問題中的一小塊。在連長(負責一個塊)或班長(負責一個束)的控制下,束與束之間的線程或者一個束內部的線程之間,要經常地交換數據。但是,連隊(塊)之間的協同就得由總司令(內核函數/ 主機程序)來控制。

5.2 局部性

對於GPU程序設計,程序員必須處理局部性。對於一個給定的工作,他需要事先思考需要哪些工具或零件(即存儲地址或數據結構),然後一次性地把他們從硬件倉庫(全局內存)可能把與這些數據相關的不同工作都執行了,避免發生“取來--存回--爲了下一個工作再取”。

5.3 緩存一致性

GPU與CPU在緩存上的一個重要差別就是“緩存一致性”問題。對於“緩存一致”的系統,一個內存的寫操作需要通知所有核的各個級別的緩存。因此,無論何時,所有的處理器核看到的內存視圖是完全一樣的。隨着處理器中核數量的增多,這個“通知”的開銷迅速增大,使得“緩存一致性”成爲限制一個處理器中核數量不能太多的一重要因素。“緩存一致”系統中最壞的情況是,一個內存操作會強迫每個核的緩存都進行更新,進而每個核都要對相鄰的內存單元寫操作。

相比之下,非“緩存一致”系統不會自動地更新其他核的緩存。它需要由程序員寫清楚每個處理器核輸出的各自不同的目標區域。從程序的視角看,這支持一個核僅負責一個輸出或者一個小的輸出集。通常,CPU遵循“緩存一致性”原則,而GPU則不是。故GPU能夠擴展到一個芯片內具有大數量的核心(流處理器簇)。

5.4 弗林分類法

根據弗林分類法,計算機的結構類型有:

SIMD--單指令,多數據
MIMD--多指令,多數據
SISD--單指令,單數據
MISD--多指令,單數據

5.5 分條 / 分塊

CUDA提供的簡單二維網格模型。對於很多問題,這樣的模型就足夠了。如果在一個塊內,你的工作是線性分佈的,那麼你可以很好地將其他分解成CUDA塊。由於在一個SM內,最多可以分配16個塊,而在一個GPU內有16個(有些是32個)SM,所以問題分成256個甚至更多的塊都可以。實際上,我們更傾向於把一個塊內的元素總數限制爲128、256、或者512,這樣有助於在一個典型的數據集內劃分出更多數量的塊。

5.6 快速傅氏變換(FFT)

FFT: FFT(Fast Fourier Transformation)是離散傅氏變換(DFT)的快速算法。即爲快速傅氏變換。它是根據離散傅氏變換的奇、偶、虛、實等特性,對離散傅立葉變換的算法進行改進獲得的。

由於不是剛需,這裏不展開講。好奇的你可以點擊樓下時光機,通過下面的教程進行學習。
FFT(最詳細最通俗的入門手冊)

5.7 CUDA計算能力的含義

體現GPU計算能力的兩個重要特徵:
1)CUDA核的個數;
2)存儲器大小。
描述GPU性能的兩個重要指標: :
1)計算性能峯值;
2)存儲器帶寬。

參考
1.CUDA計算能力的含義
2.CUDA GPUs

 

6. 實踐

6.1 Ubuntu 系統下環境搭建

6.1.1 系統要求

要搭建 CUDA 環境,我們需要自己的計算機滿足以下這三個條件:
1. 有至少一顆支持 CUDA 的 GPU(我的是GeForece GT 650M)
2. 有滿足版本要求的 gcc 編譯器和鏈接工具
3. 有 NVIDIA 提供的 CUDA 工具包(點擊神奇的小鏈接下載)

6.1.2 準備工作

下面,我們一步一步來驗證自己的系統是否滿足安裝要求。
Step 1: 驗證計算機是否擁有至少一顆支持 CUDA 的 GPU
打開終端(Ctrl + Alt + T),鍵入以下命令:

lspci | grep -i nvidia

可以看到以下內容(結果因人而異,與具體的GPU有關)


 

看到這個就說明至少有一顆支持 CUDA 的 GPU,可以進入下一步了。

Step 2: 驗證一下自己操作系統的版本
鍵入命令:

lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 16.04.4 LTS
Release:    16.04
Codename:   xenial

更多信息請移步Ubuntu查看版本信息

Step 3: 驗證 gcc 編譯器的版本
鍵入命令:

gcc --version

或者

gcc -v

得到如下信息

gcc (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Step 4: 驗證系統內核版本
鍵入命令:

uname -r

得到如下信息

對照官方提供的對各種 Linux 發行版的安裝要求進行安裝

6.1.3 搭建 CUDA 環境

Step 1: 安裝 CUDA 工具包
在前面幾項驗證都順利通過以後就來到最關鍵的一步。首先下載對應自己系統版本的 CUDA 工具包(以CUDA Toolkit 9.2 爲例),然後進入到安裝包所在目錄:

sudo dpkg -i cuda-repo-ubuntu1604-9-2-local_9.2.148-1_amd64.deb
sudo apt-key add /var/cuda-repo-<version>/7fa2af80.pub
sudo apt-get update
sudo apt-get install cuda

NOTICE:

Other installation options are available in the form of meta-packages. For example, to install all the library packages, replace "cuda" with the "cuda-libraries-9-2" meta package. For more information on all the available meta packages click here.

此時靜靜地等待安裝完成。不出意外,一段時間後安裝完成了。
Step 2: 設置環境變量
首先在 PATH 變量中加入 /usr/local/cuda-9.2/bin,在Terminal中執行:

export PATH=/usr/local/cuda-9.2/bin:$PATH

然後在 LD_LIBRARY_PATH 變量中添加 /usr/local/cuda-9.2/lib64,執行:

export  LD_LIBRARY_PATH=/usr/local/cuda-9.2/lib64:$LD_LIBRARY_PATH

Step 3: 驗證環境搭建是否成功
首先執行命令:

nvcc -V

關於測試...聰明的你一定想起來了,我們前面是講過怎麼做的。
對,沒錯,就在1.5小節,話不多說,自行上翻吧。

看到通過測試,到這裏,64位 Ubuntu 16.04 系統下 CUDA 環境搭建就完成了。

6.2 CUDA編程

6.2.1 核函數

1. 在GPU上執行的函數通常稱爲核函數。
2. 一般通過標識符__global__修飾,調用通過<<<參數1,參數2>>>,用於說明內核函數中的線程數量,以及線程是如何組織的。
3. 以線程格(Grid)的形式組織,每個線程格由若干個線程塊(block)組成,而每個線程塊又由若干個線程(thread)組成。
4.是以block爲單位執行的。
5. 叧能在主機端代碼中調用。
6. 調用時必須聲明內核函數的執行參數。
7. 在編程時,必須先爲kernel函數中用到的數組或變量分配好足夠的空間,再調用kernel函數,否則在GPU計算時會發生錯誤,例如越界或報錯,甚至導致藍屏和死機。

看完基本知識,裝好CUDA以後,就可以開始寫第一個CUDA程序了:

#include <cuda_runtime.h>
 
int main(){
printf("Hello world!\n");
}

慢着,這個程序和C有什麼區別?用到顯卡了嗎?

答:沒有區別,沒用顯卡。如果你非要用顯卡乾點什麼事情的話,可以改成這個樣子:

/*
 * @file_name HelloWorld.cu  後綴名稱.cu
 */

#include <stdio.h>
#include <cuda_runtime.h>  //頭文件

//核函數聲明,前面的關鍵字__global__
__global__ void kernel( void ) {
}

int main( void ) {
    //核函數的調用,注意<<<1,1>>>,第一個1,代表線程格里只有一個線程塊;第二個1,代表一個線程塊裏只有一個線程。
    kernel<<<1,1>>>();
    printf( "Hello, World!\n" );
    return 0;
}

6.2.2 dim3結構類型

  1. dim3是基於uint3定義的矢量類型,相當亍由3個unsigned int型組成的結構體。uint3類型有三個數據成員unsigned int x; unsigned int y; unsigned int z;
  2. 可使用於一維、二維或三維的索引來標識線程,構成一維、二維或三維線程塊。
  3. dim3結構類型變量用在覈函數調用的<<<,>>>中。
  4. 相關的幾個內置變量
    4.1. threadIdx,顧名思義獲取線程thread的ID索引;如果線程是一維的那麼就取threadIdx.x,二維的還可以多取到一個值threadIdx.y,以此類推到三維threadIdx.z
    4.2. blockIdx,線程塊的ID索引;同樣有blockIdx.xblockIdx.yblockIdx.z
    4.3. blockDim,線程塊的維度,同樣有blockDim.xblockDim.yblockDim.z
    4.4. gridDim,線程格的維度,同樣有gridDim.xgridDim.ygridDim.z
  5. 對於一維的block,線程的threadID=threadIdx.x
  6. 對於大小爲(blockDim.x, blockDim.y)的 二維block,線程的threadID=threadIdx.x+threadIdx.y*blockDim.x
  7. 對於大小爲(blockDim.x, blockDim.y, blockDim.z)的 三維 block,線程的threadID=threadIdx.x+threadIdx.y*blockDim.x+threadIdx.z*blockDim.x*blockDim.y
  8. 對於計算線程索引偏移增量爲已啓動線程的總數。如stride = blockDim.x * gridDim.x; threadId += stride

6.2.3 函數修飾符

1.__global__,表明被修飾的函數在設備上執行,但在主機上調用。

2.__device__,表明被修飾的函數在設備上執行,但只能在其他__device__函數或者__global__函數中調用。

6.2.4 常用的GPU內存函數

cudaMalloc()
1. 函數原型: cudaError_t cudaMalloc (void **devPtr, size_t size)
2. 函數用處:與C語言中的malloc函數一樣,只是此函數在GPU的內存你分配內存。
3. 注意事項:
3.1. 可以將cudaMalloc()分配的指針傳遞給在設備上執行的函數;
3.2. 可以在設備代碼中使用cudaMalloc()分配的指針進行設備內存讀寫操作;
3.3. 可以將cudaMalloc()分配的指針傳遞給在主機上執行的函數;
3.4. 不可以在主機代碼中使用cudaMalloc()分配的指針進行主機內存讀寫操作(即不能進行解引用)。

cudaMemcpy()
1. 函數原型:cudaError_t cudaMemcpy (void *dst, const void *src, size_t count, cudaMemcpyKind kind)
2. 函數作用:與c語言中的memcpy函數一樣,只是此函數可以在主機內存和GPU內存之間互相拷貝數據。
3. 函數參數:cudaMemcpyKind kind表示數據拷貝方向,如果kind賦值爲cudaMemcpyDeviceToHost表示數據從設備內存拷貝到主機內存。
4. 與C中的memcpy()一樣,以同步方式執行,即當函數返回時,複製操作就已經完成了,並且在輸出緩衝區中包含了複製進去的內容。
5. 相應的有個異步方式執行的函數cudaMemcpyAsync(),這個函數詳解請看下面的流一節有關內容。

cudaFree()
1. 函數原型:cudaError_t cudaFree ( void* devPtr )
2. 函數作用:與c語言中的free()函數一樣,只是此函數釋放的是cudaMalloc()分配的內存。
下面實例用於解釋上面三個函數

#include <stdio.h>
#include <cuda_runtime.h>
__global__ void add( int a, int b, int *c ) {
    *c = a + b;
}
int main( void ) {
    int c;
    int *dev_c;
    //cudaMalloc()
    cudaMalloc( (void**)&dev_c, sizeof(int) );
    //核函數執行
    add<<<1,1>>>( 2, 7, dev_c );   
    //cudaMemcpy()
    cudaMemcpy( &c, dev_c, sizeof(int),cudaMemcpyDeviceToHost ) ;
    printf( "2 + 7 = %d\n", c );
    //cudaFree()
    cudaFree( dev_c );
 
    return 0;
}

6.2.5 GPU內存分類

全局內存
通俗意義上的設備內存。

共享內存
1. 位置:設備內存。
2. 形式:關鍵字__shared__添加到變量聲明中。如__shared__ float cache[10]
3. 目的:對於GPU上啓動的每個線程塊,CUDA C編譯器都將創建該共享變量的一個副本。線程塊中的每個線程都共享這塊內存,但線程卻無法看到也不能修改其他線程塊的變量副本。這樣使得一個線程塊中的多個線程能夠在計算上通信和協作。

常量內存
1. 位置:設備內存
2. 形式:關鍵字__constant__添加到變量聲明中。如__constant__ float s[10];。
3. 目的:爲了提升性能。常量內存採取了不同於標準全局內存的處理方式。在某些情況下,用常量內存替換全局內存能有效地減少內存帶寬。
4. 特點:常量內存用於保存在覈函數執行期間不會發生變化的數據。變量的訪問限制爲只讀。NVIDIA硬件提供了64KB的常量內存。不再需要cudaMalloc()或者cudaFree(),而是在編譯時,靜態地分配空間。
5. 要求:當我們需要拷貝數據到常量內存中應該使用cudaMemcpyToSymbol(),而cudaMemcpy()會複製到全局內存。
6. 性能提升的原因:
6.1. 對常量內存的單次讀操作可以廣播到其他的“鄰近”線程。這將節約15次讀取操作。(爲什麼是15,因爲“鄰近”指半個線程束,一個線程束包含32個線程的集合。)
6.2. 常量內存的數據將緩存起來,因此對相同地址的連續讀操作將不會產生額外的內存通信量。

紋理內存
1. 位置:設備內存
2. 目的:能夠減少對內存的請求並提供高效的內存帶寬。是專門爲那些在內存訪問模式中存在大量空間局部性的圖形應用程序設計,意味着一個線程讀取的位置可能與鄰近線程讀取的位置“非常接近”。如下圖:

3. 紋理變量(引用)必須聲明爲文件作用域內的全局變量。
4. 形式:分爲一維紋理內存 和 二維紋理內存。
4.1. 一維紋理內存
4.1.1. 用texture<類型>類型聲明,如texture<float> texIn
4.1.2. 通過cudaBindTexture()綁定到紋理內存中。
4.1.3. 通過tex1Dfetch()來讀取紋理內存中的數據。
4.1.4. 通過cudaUnbindTexture()取消綁定紋理內存。
4.2. 二維紋理內存
4.2.1. 用texture<類型,數字>類型聲明,如texture<float,2> texIn
4.2.2. 通過cudaBindTexture2D()綁定到紋理內存中。
4.2.3. 通過tex2D()來讀取紋理內存中的數據。
4.2.4. 通過cudaUnbindTexture()取消綁定紋理內存。

 

固定內存
1. 位置:主機內存。
2. 概念:也稱爲頁鎖定內存或者不可分頁內存,操作系統將不會對這塊內存分頁並交換到磁盤上,從而確保了該內存始終駐留在物理內存中。因此操作系統能夠安全地使某個應用程序訪問該內存的物理地址,因爲這塊內存將不會破壞或者重新定位。
3. 目的:提高訪問速度。由於GPU知道主機內存的物理地址,因此可以通過“直接內存訪問DMA(Direct Memory Access)技術來在GPU和主機之間複製數據。由於DMA在執行復制時無需CPU介入。因此DMA複製過程中使用固定內存是非常重要的。
4. 缺點:使用固定內存,將失去虛擬內存的所有功能;系統將更快的耗盡內存。
5. 建議:對cudaMemcpy()函數調用中的源內存或者目標內存,才使用固定內存,並且在不再需要使用它們時立即釋放。
6. 形式:通過cudaHostAlloc()函數來分配;通過cudaFreeHost()釋放。
7. 只能以異步方式對固定內存進行復制操作。

原子性
1. 概念:如果操作的執行過程不能分解爲更小的部分,我們將滿足這種條件限制的操作稱爲原子操作。
2. 形式:函數調用,如atomicAdd(addr,y)將生成一個原子的操作序列,這個操作序列包括讀取地址addr處的值,將y增加到這個值,以及將結果保存回地址addr

6.2.6 常用線程操作函數

同步方法__syncthreads(),這個函數的調用,將確保線程塊中的每個線程都執行完__syscthreads()前面的語句後,纔會執行下一條語句。

使用事件來測量性能
1. 用途:爲了測量GPU在某個任務上花費的時間。CUDA中的事件本質上是一個GPU時間戳。由於事件是直接在GPU上實現的。因此不適用於對同時包含設備代碼和主機代碼的混合代碼設計。
2. 形式:首先創建一個事件,然後記錄事件,再計算兩個事件之差,最後銷燬事件。如:

cudaEvent_t start, stop;
cudaEventCreate( &start );
cudaEventCreate( &stop );
cudaEventRecord( start, 0 );
//do something
cudaEventRecord( stop, 0 );
float   elapsedTime;
cudaEventElapsedTime( &elapsedTime,start, stop );
cudaEventDestroy( start );
cudaEventDestroy( stop );

6.2.7 流

  1. 扯一扯:併發重點在於一個極短時間段內運行多個不同的任務;並行重點在於同時運行一個任務。
  2. 任務並行性:是指並行執行兩個或多個不同的任務,而不是在大量數據上執行同一個任務。
  3. 概念:CUDA流表示一個GPU操作隊列,並且該隊列中的操作將以指定的順序執行。我們可以在流中添加一些操作,如核函數啓動,內存複製以及事件的啓動和結束等。這些操作的添加到流的順序也是它們的執行順序。可以將每個流視爲GPU上的一個任務,並且這些任務可以並行執行。
  4. 硬件前提:必須是支持設備重疊功能的GPU。支持設備重疊功能,即在執行一個核函數的同時,還能在設備與主機之間執行復制操作。
  5. 聲明與創建:聲明cudaStream_t stream;,創建cudaSteamCreate(&stream);。
  6. cudaMemcpyAsync():前面在cudaMemcpy()中提到過,這是一個以異步方式執行的函數。在調用cudaMemcpyAsync()時,只是放置一個請求,表示在流中執行一次內存複製操作,這個流是通過參數stream來指定的。當函數返回時,我們無法確保複製操作是否已經啓動,更無法保證它是否已經結束。我們能夠得到的保證是,複製操作肯定會當下一個被放入流中的操作之前執行。傳遞給此函數的主機內存指針必須是通過cudaHostAlloc()分配好的內存。(流中要求固定內存)
  7. 流同步:通過cudaStreamSynchronize()來協調。
  8. 流銷燬:在退出應用程序之前,需要銷燬對GPU操作進行排隊的流,調用cudaStreamDestroy()
  9. 針對多個流:
    9.1. 記得對流進行同步操作。
    9.2. 將操作放入流的隊列時,應採用寬度優先方式,而非深度優先的方式,換句話說,不是首先添加第0個流的所有操作,再依次添加後面的第1,2,…個流。而是交替進行添加,比如將a的複製操作添加到第0個流中,接着把a的複製操作添加到第1個流中,再繼續其他的類似交替添加的行爲。
    9.3. 要牢牢記住操作放入流中的隊列中的順序影響到CUDA驅動程序調度這些操作和流以及執行的方式。

TIPS:

  1. 當線程塊的數量爲GPU中處理數量的2倍時,將達到最優性能。
  2. 核函數執行的第一個計算就是計算輸入數據的偏移。每個線程的起始偏移都是0到線程數量減1之間的某個值。然後,對偏移的增量爲已啓動線程的總數。

6.2.8 這是一個栗子

我們嘗試用一個程序來比較cuda/c在GPU/CPU的運行效率,來不及了,快上車。
這是一個CUDA程序,請保存文件名爲“文件名.cu”,在你的PC或者服務器上運行。

#include "cuda_runtime.h"
#include "device_launch_parameters.h"
 
#include <stdio.h>
#include <time.h>
 
#define N (1024*1024)
#define M (10000)
#define THREADS_PER_BLOCK 1024
 
void serial_add(double *a, double *b, double *c, int n, int m)
{
    for(int index=0;index<n;index++)
    {
        for(int j=0;j<m;j++)
        {
            c[index] = a[index]*a[index] + b[index]*b[index];
        }
    }
}
 
__global__ void vector_add(double *a, double *b, double *c)
{
    int index = blockIdx.x * blockDim.x + threadIdx.x;
        for(int j=0;j<M;j++)
        {
            c[index] = a[index]*a[index] + b[index]*b[index];
        }
}
 
int main()
{
    clock_t start,end;
 
    double *a, *b, *c;
    int size = N * sizeof( double );
 
    a = (double *)malloc( size );
    b = (double *)malloc( size );
    c = (double *)malloc( size );
 
    for( int i = 0; i < N; i++ )
    {
        a[i] = b[i] = i;
        c[i] = 0;
    }
 
    start = clock();
    serial_add(a, b, c, N, M);
 
    printf( "c[%d] = %f\n",0,c[0] );
    printf( "c[%d] = %f\n",N-1, c[N-1] );
 
    end = clock();
 
    float time1 = ((float)(end-start))/CLOCKS_PER_SEC;
    printf("CPU: %f seconds\n",time1);
 
    start = clock();
    double *d_a, *d_b, *d_c;
 
 
    cudaMalloc( (void **) &d_a, size );
    cudaMalloc( (void **) &d_b, size );
    cudaMalloc( (void **) &d_c, size );
 
 
    cudaMemcpy( d_a, a, size, cudaMemcpyHostToDevice );
    cudaMemcpy( d_b, b, size, cudaMemcpyHostToDevice );
 
    vector_add<<< (N + (THREADS_PER_BLOCK-1)) / THREADS_PER_BLOCK, THREADS_PER_BLOCK >>>( d_a, d_b, d_c );
 
    cudaMemcpy( c, d_c, size, cudaMemcpyDeviceToHost );
 
 
    printf( "c[%d] = %f\n",0,c[0] );
    printf( "c[%d] = %f\n",N-1, c[N-1] );
 
 
    free(a);
    free(b);
    free(c);
    cudaFree( d_a );
    cudaFree( d_b );
    cudaFree( d_c );
 
    end = clock();
    float time2 = ((float)(end-start))/CLOCKS_PER_SEC;
    printf("CUDA: %f seconds, Speedup: %f\n",time2, time1/time2);
 
    return 0;
}

效率對比
我們通過修改count的值並且加大循環次數來觀察變量的效率的差別。

運行結果:


可見在數據量大的情況下效率還是相當不錯的。

 

7. GPU or FPGA

GPU優勢
1.從峯值性能來說,GPU(10Tflops)遠遠高於FPGA(<1TFlops);

2.GPU相對於FPGA還有一個優勢就是內存接口, GPU的內存接口(傳統的GDDR5,最近更是用上了HBM和HBM2)的帶寬遠好於FPGA的傳統DDR接口(大約帶寬高4-5倍);

3.功耗方面,雖然GPU的功耗遠大於FPGA的功耗,但是如果要比較功耗應該比較在執行效率相同時需要的功耗。如果FPGA的架構優化能做到很好以致於一塊FPGA的平均性能能夠接近一塊GPU,那麼FPGA方案的總功耗遠小於GPU,散熱問題可以大大減輕。反之,如果需要二十塊FPGA才能實現一塊GPU的平均性能,那麼FPGA在功耗方面並沒有優勢。

4.FPGA缺點有三點:
第一,基本單元的計算能力有限。爲了實現可重構特性,FPGA 內部有大量極細粒度的基本單元,但是每個單元的計算能力(主要依靠LUT 查找表)都遠遠低於CPU 和GPU 中的ALU模塊。
第二,速度和功耗相對專用定製芯片(ASIC)仍然存在不小差距。
第三,FPGA 價格較爲昂貴,在規模放量的情況下單塊FPGA 的成本要遠高於專用定製芯片。最後誰能勝出, 完全取決於FPGA架構優化能否彌補峯值性能的劣勢。

5.個人更推薦: CPU+FPGA的組合模式; 其中FPGA用於整形計算,cpu進行浮點計算和調度,此組合的擁有更高的單位功耗性能和更低的時延。最後更想GPU穩定開放,發揮其長處, 達到真正的物美價廉!

FPGA優勢
人工智能目前仍處於早期階段,未來人工智能的主戰場是在推理環節,遠沒有爆發。未來勝負尚未可知,各家技術路線都有機會勝出。目前英偉達的GPU在訓練場景中佔據着絕對領導地位,但是在未來,專注於推理環節的FPGA必將會發揮巨大的價值。

FPGA和GPU內都有大量的計算單元,因此它們的計算能力都很強。在進行神經網絡運算的時候,兩者的速度會比CPU快很多。但是GPU由於架構固定,硬件原生支持的指令也就固定了,而FPGA則是可編程的。其可編程性是關鍵,因爲它讓軟件與終端應用公司能夠提供與其競爭對手不同的解決方案,並且能夠靈活地針對自己所用的算法修改電路。

在平均性能方面,GPU遜於FPGA,FPGA可以根據特定的應用去編程硬件,例如如果應用裏面的加法運算非常多就可以把大量的邏輯資源去實現加法器,而GPU一旦設計完就不能改動了,所以不能根據應用去調整硬件資源。
目前機器學習大多使用SIMD架構,即只需一條指令可以平行處理大量數據,因此用GPU很適合。但是有些應用是MISD,即單一數據需要用許多條指令平行處理,這種情況下用FPGA做一個MISD的架構就會比GPU有優勢。 所以,對於平均性能,看的就是FPGA加速器架構上的優勢是否能彌補運行速度上的劣勢。如果FPGA上的架構優化可以帶來相比GPU架構兩到三個數量級的優勢,那麼FPGA在平均性能上會好於GPU。

在功耗能效比方面,同樣由於FPGA的靈活性,在架構優化到很好時,一塊FPGA的平均性能能夠接近一塊GPU,那麼FPGA方案的總功耗遠小於GPU,散熱問題可以大大減輕。 能效比的比較也是類似,能效指的是完成程序執行消耗的能量,而能量消耗等於功耗乘以程序的執行時間。雖然GPU的功耗遠大於FPGA的功耗,但是如果FPGA執行相同程序需要的時間比GPU長几十倍,那FPGA在能效比上就沒有優勢了;反之如果FPGA上實現的硬件架構優化得很適合特定的機器學習應用,執行算法所需的時間僅僅是GPU的幾倍或甚至於接近GPU,那麼FPGA的能效比就會比GPU強。

在峯值性能比方面,雖然GPU的峯值性能(10Tflops)遠大於FPGA的峯值性能(<1Tflops),但針對特定的場景來講吞吐量並不比GPU差。

 

8. 深度學習的三種硬件方案:ASIC,FPGA,GPU

8.1 對深度學習硬件平臺的要求

要想明白“深度學習”需要怎樣的硬件,必須瞭解深度學習的工作原理。首先在表層上,我們有一個巨大的數據集,並選定了一種深度學習模型。每個模型都有一些內部參數需要調整,以便學習數據。而這種參數調整實際上可以歸結爲優化問題,在調整這些參數時,就相當於在優化特定的約束條件

  • 矩陣相乘(Matrix Multiplication)——幾乎所有的深度學習模型都包含這一運算,它的計算十分密集。

  • 卷積(Convolution)——這是另一個常用的運算,佔用了模型中大部分的每秒浮點運算(浮點/秒)。

  • 循環層(Recurrent Layers )——模型中的反饋層,並且基本上是前兩個運算的組合。

  • All Reduce——這是一個在優化前對學習到的參數進行傳遞或解析的運算序列。在跨硬件分佈的深度學習網絡上執行同步優化時(如AlphaGo的例子),這一操作尤其有效。

除此之外,深度學習的硬件加速器需要具備數據級別和流程化的並行性、多線程和高內存帶寬等特性。 另外,由於數據的訓練時間很長,所以硬件架構必須低功耗。 因此,效能功耗比(Performance per Watt)是硬件架構的評估標準之一。

CNN在應用中,一般採用GPU加速,請解釋爲什麼GPU可以有加速效果,主要加速算法的哪一個部分?

這裏默認gpu加速是指NVIDIA的CUDA加速。CPU是中央處理單元,gpu是圖形處理單元,gpu由上千個流處理器(core)作爲運算器。執行採用單指令多線程(SIMT)模式。相比於單核CPU(向量機)流水線式的串行操作,雖然gpu單個core計算能力很弱,但是通過大量線程進行同時計算,在數據量很大是會活動較爲可觀的加速效果。

具體到cnn,利用gpu加速主要是在conv(卷積)過程上。conv過程同理可以像以上的向量加法一樣通過cuda實現並行化。具體的方法很多,不過最好的還是利用fft(快速傅里葉變換)進行快速卷積。NVIDIA提供了cufft庫實現fft,複數乘法則可以使用cublas庫裏的對應的level3的cublasCgemm函數。

GPU加速的基本準則就是“人多力量大”。CNN說到底主要問題就是計算量大,但是卻可以比較有效的拆分成並行問題。隨便拿一個層的filter來舉例子,假設某一層有n個filter,每一個需要對上一層輸入過來的map進行卷積操作。那麼,這個卷積操作並不需要按照線性的流程去做,每個濾波器互相之間並不影響,可以大家同時做,然後大家生成了n張新的譜之後再繼續接下來的操作。既然可以並行,那麼同一時間處理單元越多,理論上速度優勢就會越大。所以,處理問題就變得很簡單粗暴,就像NV那樣,暴力增加顯卡單元數(當然,顯卡的架構、內部數據的傳輸速率、算法的優化等等也都很重要)。

GPU主要是針對圖形顯示及渲染等技術的出衆,而其中的根本是因爲處理矩陣算法能力的強大,剛好CNN中涉及大量的卷積,也就是矩陣乘法等,所以在這方面具有優勢。

機器學習的算法一定得經過gpu加速嗎?

不一定。只有需要大量浮點數計算,例如矩陣乘法,才需要GPU加速。 用CNN對圖像進行分類就是一個需要大量浮點數計算的典型案例,通常需要GPU加速

對於ASICFPGA分佈式計算,這裏不再展開講,有興趣的小夥伴可以,自行學習。不過....說不定某天博主心情好,就會梳理一下這幾種硬件方案在端到端上應用的區別了。

菜鳥入門教程就到這裏了,聰明的你一定不滿足這個入門教程,如有興趣進一步學習CUDA編程,可移步NVIDIA官方的課程平臺CUDA ZONE(PS:中文網站,英文課程)


 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章