1. CPU vs. GPU
1.1 四種計算機模型
GPU設計的初衷就是為了減輕CPU計算的負載,將一部分圖形計算的功能設計到一塊獨立的處理器中,將矩陣變換、頂點計算和光照計算等操作從 CPU 中轉移到 GPU中,從而一方面加速圖形處理,另一方面減小了 CPU 的工作負載,讓 CPU 有時間去處理其它的事情。
在GPU上的各個處理器采取異步并行的方式對數據流進行處理,根據費林分類法(Flynn's Taxonomy),可以將資訊流(information stream)分成指令(Instruction)和數據(Data)兩種,據此又可分成四種計算機類型:
- 單一指令流單一數據流計算機(SISD):單核CPU
- 單一指令流多數據流計算機(SIMD):GPU的計算模型
- 多指令流單一數據流計算機(MISD):流水線模型
- 多指令流多數據流計算機(MIMD):多核CPU
1.2 CPU 與 GPU 結構差異
1.3 CPU設計理念:低延時
■ ALU:CPU有強大的ALU(算術運算單元),它可以在很少的時鐘周期內完成算術計算。
當今的CPU可以達到64bit 雙精度。執行雙精度浮點源算的加法和乘法只需要1~3個時鐘周期。
CPU的時鐘周期的頻率是非常高的,達到1.532~3gigahertz(千兆HZ, 10的9次方).
■ Cache:大的緩存也可以降低延時。保存很多的數據放在緩存里面,當需要訪問的這些數據,只要在之前訪問過的,如今直接在緩存里面取即可。
■ Control:復雜的邏輯控制單元。
當程序含有多個分支的時候,它通過提供分支預測的能力來降低延時。
數據轉發。 當一些指令依賴前面的指令結果時,數據轉發的邏輯控制單元決定這些指令在pipeline中的位置并且盡可能快的轉發一個指令的結果給后續的指令。這些動作需要很多的對比電路單元和轉發電路單元。
1.4 GPU設計理念:大吞吐量
■ ALU,Cache:GPU的特點是有很多的ALU和很少的cache. 緩存的目的不是保存后面需要訪問的數據的,這點和CPU不同,而是為thread提高服務的。如果有很多線程需要訪問同一個相同的數據,緩存會合并這些訪問,然后再去訪問dram(因為需要訪問的數據保存在dram中而不是cache里面),獲取數據后cache會轉發這個數據給對應的線程,這個時候是數據轉發的角色。但是由于需要訪問dram,自然會帶來延時的問題。
■ Control:控制單元(左邊黃色區域塊)可以把多個的訪問合并成少的訪問。
GPU的雖然有dram延時,卻有非常多的ALU和非常多的thread. 為了平衡內存延時的問題,我們可以中充分利用多的ALU的特性達到一個非常大的吞吐量的效果。盡可能多的分配多的Threads.通常來看GPU ALU會有非常重的pipeline就是因為這樣。
2. Nvidia GPU架構
2.1 硬件架構
■ 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有非常嚴格的限制,也就限制了并行能力。
具有Tesla架構的GPU是具有芯片共享存儲器的一組SIMT(單指令多線程)多處理器。它以一個可伸縮的多線程流處理器(Streaming Multiprocessors,SMs)陣列為中心實現了MIMD(多指令多數據)的異步并行機制,其中每個多處理器包含多個標量處理器(Scalar Processor,SP),為了管理運行各種不同程序的數百個線程,SIMT架構的多處理器會將各線程映射到一個標量處理器核心,各標量線程使用自己的指令地址和寄存器狀態獨立執行。
如上圖所示,每個多處理器(Multiprocessor)都有一個屬于以下四種類型之一的芯片存儲器:
- 每個處理器上有一組本地 32 位寄存器(Registers);
- 并行數據緩存或共享存儲器(Shared Memory),由所有標量處理器核心共享,共享存儲器空間就位于此處;
- 只讀固定緩存(Constant Cache),由所有標量處理器核心共享,可加速從固定存儲器空間進行的讀取操作(這是設備存儲器的一個只讀區域);
- 一個只讀紋理緩存(Texture Cache),由所有標量處理器核心共享,加速從紋理存儲器空間進行的讀取操作(這是設備存儲器的一個只讀區域),每個多處理器都會通過實現不同尋址模型和數據過濾的紋理單元訪問紋理緩存。
多處理器 SIMT 單元以32個并行線程為一組來創建、管理、調度和執行線程,這樣的線程組稱為 warp 塊(束),即以線程束為調度單位,但只有所有32個線程都在諸如內存讀取這樣的操作時,它們就會被掛起,如下所示的狀態變化。當主機CPU上的CUDA程序調用內核網格時,網格的塊將被枚舉并分發到具有可用執行容量的多處理器;SIMT 單元會選擇一個已準備好執行的 warp 塊,并將下一條指令發送到該 warp 塊的活動線程。一個線程塊的線程在一個多處理器上并發執行,在線程塊終止時,將在空閑多處理器上啟動新塊。
2.2 軟件架構
CUDA是一種新的操作GPU計算的硬件和軟件架構,它將GPU視作一個數據并行計算設備,而且無需把這些計算映射到圖形API。操作系統的多任務機制可以同時管理CUDA訪問GPU和圖形程序的運行庫,其計算特性支持利用CUDA直觀地編寫GPU核心程序。目前Tesla架構具有在筆記本電腦、臺式機、工作站和服務器上的廣泛可用性,配以C/C++語言的編程環境和CUDA軟件,使這種架構得以成為最優秀的超級計算平臺。
CUDA在軟件方面組成有:一個CUDA庫、一個應用程序編程接口(API)及其運行庫(Runtime)、兩個較高級別的通用數學庫,即CUFFT和CUBLAS。CUDA改進了DRAM的讀寫靈活性,使得GPU與CPU的機制相吻合。另一方面,CUDA 提供了片上(on-chip)共享內存,使得線程之間可以共享數據。應用程序可以利用共享內存來減少DRAM的數據傳送,更少的依賴DRAM的內存帶寬。
■ thread:一個CUDA的并行程序會被以許多個threads來執行。
■ block:數個threads會被群組成一個block,同一個block中的threads可以同步,也可以通過shared memory通信。
■ grid:多個blocks則會再構成grid。
■ warp:GPU執行程序時的調度單位,目前cuda的warp的大小為32,同在一個warp的線程,以不同數據資源執行相同的指令,這就是所謂 SIMT。
2.3 軟硬件架構對應關系
1)SM像一個獨立的CPU core
從軟件上看,SM更像一個獨立的CPU core。SM(Streaming Multiprocessors)是GPU架構中非常重要的部分,GPU硬件的并行性就是由SM決定的。以Fermi架構為例,其包含以下主要組成部分:
- CUDA cores
- Shared Memory/L1Cache
- Register File
- Load/Store Units
- Special Function Units
- Warp Scheduler
2)同一個block的threads在同一個SM并行執行
GPU中每個sm都設計成支持數以百計的線程并行執行,并且每個GPU都包含了很多的SM,所以GPU支持成百上千的線程并行執行。當一個kernel啟動后,thread會被分配到這些SM中執行。大量的thread可能會被分配到不同的SM,同一個block中的threads必然在同一個SM中并行(SIMT)執行。每個thread擁有它自己的程序計數器和狀態寄存器,并且用該線程自己的數據執行指令,這就是所謂的Single Instruction Multiple Thread。
3)warp是調度和運行的基本單元,一個warp占用一個SM運行
一個SP可以執行一個thread,但是實際上并不是所有的thread能夠在同一時刻執行。Nvidia把32個threads組成一個warp,warp是調度和運行的基本單元。warp中所有threads并行的執行相同的指令。一個warp需要占用一個SM運行,多個warps需要輪流進入SM。由SM的硬件warp scheduler負責調度。目前每個warp包含32個threads(Nvidia保留修改數量的權利)。所以,一個GPU上resident thread最多只有 SM*warp個。
block是軟件概念,一個block只會由一個sm調度,程序員在開發時,通過設定block的屬性,告訴GPU硬件,我有多少個線程,線程怎么組織。而具體怎么調度由sm的warps scheduler負責,block一旦被分配好SM,該block就會一直駐留在該SM中,直到執行結束。一個SM可以同時擁有多個blocks,但需要序列執行。下圖顯示了軟件硬件方面的術語對應關系:
需要注意的是,大部分threads只是邏輯上并行,并不是所有的thread可以在物理上同時執行。例如,遇到分支語句(if else,while,for等)時,各個thread的執行條件不一樣必然產生分支執行,這就導致同一個block中的線程可能會有不同步調。另外,并行thread之間的共享數據會導致競態:多個線程請求同一個數據會導致未定義行為。CUDA提供了cudaThreadSynchronize()來同步同一個block的thread以保證在進行下一步處理之前,所有thread都到達某個時間點。
同一個warp中的thread可以以任意順序執行,active warps被sm資源限制。當一個warp空閑時,SM就可以調度駐留在該SM中另一個可用warp。在并發的warp之間切換是沒什么消耗的,因為硬件資源早就被分配到所有thread和block,所以該新調度的warp的狀態已經存儲在SM中了。不同于CPU,CPU切換線程需要保存/讀取線程上下文(register內容),這是非常耗時的,而GPU為每個threads提供物理register,無需保存/讀取上下文。
4)SIMT和SIMD
CUDA是一種典型的SIMT架構(單指令多線程架構),SIMT和SIMD(Single Instruction, Multiple Data)類似,SIMT應該算是SIMD的升級版,更靈活,但效率略低,SIMT是NVIDIA提出的GPU新概念。二者都通過將同樣的指令廣播給多個執行官單元來實現并行。一個主要的不同就是,SIMD要求所有的vector element在一個統一的同步組里同步的執行,而SIMT允許線程們在一個warp中獨立的執行。SIMT有三個SIMD沒有的主要特征:
- 每個thread擁有自己的instruction address counter
- 每個thread擁有自己的狀態寄存器
- 每個thread可以有自己獨立的執行路徑
3. CUDA C編程入門
3.1 編程模型
CUDA程序構架分為兩部分:Host和Device。一般而言,Host指的是CPU,Device指的是GPU。在CUDA程序構架中,主程序還是由 CPU 來執行,而當遇到數據并行處理的部分,CUDA 就會將程序編譯成 GPU 能執行的程序,并傳送到GPU。而這個程序在CUDA里稱做核(kernel)。CUDA允許程序員定義稱為核的C語言函數,從而擴展了 C 語言,在調用此類函數時,它將由N個不同的CUDA線程并行執行N次,這與普通的C語言函數只執行一次的方式不同。執行核的每個線程都會被分配一個獨特的線程ID,可通過內置的threadIdx變量在內核中訪問此ID。
在 CUDA 程序中,主程序在調用任何 GPU 內核之前,必須對核進行執行配置,即確定線程塊數和每個線程塊中的線程數以及共享內存大小。
1)線程層次結構
在GPU中要執行的線程,根據最有效的數據共享來創建塊(Block),其類型有一維、二維或三維。在同一個塊內的線程可彼此協作,通過一些共享存儲器來共享數據,并同步其執行來協調存儲器訪問。一個塊中的所有線程都必須位于同一個處理器核心中。因而,一個處理器核心的有限存儲器資源制約了每個塊的線程數量。在早起的 NVIDIA 架構中,一個線程塊最多可以包含 512 個線程,而在后期出現的一些設備中則最多可支持1024個線程。一般 GPGPU 程序線程數目是很多的,所以不能把所有的線程都塞到同一個塊里。但一個內核可由多個大小相同的線程塊同時執行,因而線程總數應等于每個塊的線程數乘以塊的數量。這些同樣維度和大小的塊將組織為一個一維或二維線程塊網格(Grid)。具體框架如下圖所示。
核函數只能在主機端調用,其調用形式為:Kernel<<<Dg,Db, Ns, S>>>(param list)
- Dg:用于定義整個grid的維度和尺寸,即一個grid有多少個block。為dim3類型。Dim3 Dg(Dg.x, Dg.y, 1)表示grid中每行有Dg.x個block,每列有Dg.y個block,第三維恒為1(目前一個核函數只有一個grid)。整個grid中共有Dg.x*Dg.y個block,其中Dg.x和Dg.y最大值為65535。
- Db:用于定義一個block的維度和尺寸,即一個block有多少個thread。為dim3類型。Dim3 Db(Db.x, Db.y, Db.z)表示整個block中每行有Db.x個thread,每列有Db.y個thread,高度為Db.z。Db.x和Db.y最大值為512,Db.z最大值為62。一個block中共有Db.xDb.yDb.z個thread。計算能力為1.0,1.1的硬件該乘積的最大值為768,計算能力為1.2,1.3的硬件支持的最大值為1024。
- Ns:是一個可選參數,用于設置每個block除了靜態分配的shared Memory以外,最多能動態分配的shared memory大小,單位為byte。不需要動態分配時該值為0或省略不寫。
- S:是一個cudaStream_t類型的可選參數,初始值為零,表示該核函數處在哪個流之中。
如下是一個CUDA簡單的求和程序:
2)存儲器層次結構
CUDA 設備擁有多個獨立的存儲空間,其中包括:全局存儲器、本地存儲器、共享存儲器、常量存儲器、紋理存儲器和寄存器,如下圖所示。
CUDA線程可在執行過程中訪問多個存儲器空間的數據,如下圖所示其中:
- 每個線程都有一個私有的本地存儲器。
- 每個線程塊都有一個共享存儲器,該存儲器對于塊內的所有線程都是可見的,并且與塊具有相同的生命周期。
- 所有線程都可訪問相同的全局存儲器。
- 此外還有兩個只讀的存儲器空間,可由所有線程訪問,這兩個空間是常量存儲器空間和紋理存儲器空間。全局、固定和紋理存儲器空間經過優化,適于不同的存儲器用途。紋理存儲器也為某些特殊的數據格式提供了不同的尋址模式以及數據過濾,方便 Host對流數據的快速存取。
3)主機(Host)和設備(Device)
如下圖所示,CUDA 假設線程可在物理上獨立的設備上執行,此類設備作為運行C語言程序的主機的協處理器操作。內核在GPU上執行,而C語言程序的其他部分在CPU上執行(即串行代碼在主機上執行,而并行代碼在設備上執行)。此外,CUDA還假設主機和設備均維護自己的DRAM,分別稱為主機存儲器和設備存儲器。因而,一個程序通過調用CUDA運行庫來管理對內核可見的全局、固定和紋理存儲器空間。這種管理包括設備存儲器的分配和取消分配,還包括主機和設備存儲器之間的數據傳輸。
3.2 編程入門
3.2.1 CUDA C基礎
CUDA C是對C/C++語言進行拓展后形成的變種,兼容C/C++語法,文件類型為".cu"文件,編譯器為"nvcc",相比傳統的C/C++,主要添加了以下幾個方面:
- 函數類型限定符
- 執行配置運算符
- 五個內置變量
- 變量類型限定符
- 其他的還有數學函數、原子函數、紋理讀取、綁定函數等
1)函數類型限定符
用來確定某個函數是在CPU還是GPU上運行,以及這個函數是從CPU調用還是從GPU調用
- device表示從GPU調用,在GPU上執行
- global表示從CPU調用,在GPU上執行,也稱之為kernel函數
- host表示在CPU上調用,在CPU上執行
在計算能力3.0及以后的設備中,global類型的函數也可以調用__global類型的函數。
#include <stdio.h>
__device__ void device_func(void) {
}
__global__ void global_func(void) {
device_func();
}
int main() {
printf("%s\n", __FILE__);
global_func<<<1,1>>>();
return 0;
}
2)執行配置運算符
執行配置運算符<<<>>>,用來傳遞內核函數的執行參數。格式如下:
kernel<<<gridDim, blockDim, memSize, stream>>>(para1, para2, ...);
- gridDim表示網格的大小,可以是1,2,3維
- blockDim表示塊的·大小,可以是1,2,3維
- memSize表示動態分配的共享存儲器大小,默認為0
- stream表示執行的流,默認為0
- para1, para2等為核函數參數
#include <stdio.h>
__global__ void func(int a, int b) {
}
int main() {
int a = 0, b = 0;
func<<<128, 128>>>(a, b);
func<<<dim3(128, 128), dim3(16, 16)>>>(a, b);
func<<<dim3(128, 128, 128), dim3(16, 16, 2)>>>(a, b);
return 0;
}
3)五個內置變量
這些內置變量用來在運行時獲得Grid和Block的尺寸及線程索引等信息
- gridDim: 包含三個元素x, y, z的結構體,表示Grid在三個方向上的尺寸,對應于執行配置中的第一個參數
- blockDim: 包含上元素x, y, z的結構體,表示Block在三個方向上的尺寸,對應于執行配置中的第二個參數
- blockIdx: 包含三個元素x, y, z的結構體,分別表示當前線程所在塊在網格中x, y, z方向上的索引
- threadIdx: 包含三個元素x, y, z的結構體,分別表示當前線程在其所在塊中x, y, z方向上的索引
- warpSize: 表明warp的尺寸
4)變量類型限定符
用來確定某個變量在設備上的內存位置
- _device_表示位于全局內存空間,默認類型
- _share_表示位于共享內存空間
- _constant_表示位于常量內存空間
- texture表示其綁定的變量可以被紋理緩存加速訪問
3.2.2 示例
向量的點積——假設向量大小為N,按照上下文的方法,將申請N的空間,用來存放向量元素互乘的結果,然后在CPU上對N個乘積進行累加。
類似于Map-Reduce:
- Map——GPU上對N個變量分別相乘
- Reduce——CPU上對N個乘積進行累加