最近看fastertransformer源碼,接觸了不少底層到東西,cuda源碼各類看不懂,就去學cuda,學了一下子以爲就想放棄,結果翻回去看源碼仍是不懂,反覆幾回,最後乾脆拿出一上午靜靜地把官方文檔啃了啃纔算入門。因此寫這篇文章幫助一樣想要放棄的同窗入門一下。html
網上關於cuda的文章並很少,知乎有一篇很高讚的Cuda入門極簡教程,但看完了有些概念我仍是弄混了,最後發現官方文檔真的香,有時間的朋友建議仍是老老實實讀這個,沒時間的話再看我簡化的入門文章(只准備寫到矩陣乘法)。編程
本文默認的讀者是會用tensorflow等高級API但不太懂底層實現的同窗們哦多線程
CUDA是英偉達2006年末推出的GPU並行計算平臺,支持開發者經過各類語言對GPU計算編程。GPU有如下幾種:架構
每一個系列的GPU會有不一樣的架構,從Kepler到Turing,每代架構都會進行一些改進。再上層也有封裝好的計算庫(cuDNN、cuBLAS),咱們日常經過tensorflow、pytorch調的包都是對這些計算庫的又一層封裝。ide
物理層上,一個GPU會包含顯存和計算單元,svg
計算單元中有多個SM(streaming multiprocessors),每一個SM上都有寄存器、內存和執行任務的SP(streaming processor/cuda core),運行時一個SP執行一個thread。函數
在編程時不須要本身進行線程的調度,只須要指定某個任務須要多少個線程就行了。CUDA中把每一個任務叫作一個Kernel,能夠理解爲GPU上一個線程所執行的函數。實現時是帶有__global__標誌的函數。oop
在調用kernel前須要指定線程數。在cuda中有兩個層級:grid和block,grid包含多個線程塊(block),block中包含多個線程(最多1024個):優化
貼一個矩陣加法的例子,代碼中每一個block處理矩陣的一行,block中的一個線程處理一個element的相加:ui
// Kernel definition
__global__ void MatAdd(float A[N][N], float B[N][N],
float C[N][N])
{
int row = blockIdx.x;
int col = threadIdx.x;
if (row < N && col < N)
C[row][col] = A[row][col] + B[row][col];
}
int main()
{
...
// Kernel invocation
dim3 numBlocks(N); //grid dim
dim3 threadsPerBlock(N); //block dim
MatAdd<<<numBlocks, threadsPerBlock>>>(A, B, C);
...
}複製代碼
其實grid和block都是能夠設置成多維(有x,y,z三個軸)的,好比英偉達文檔中給的例子,就是一個block處理矩陣中的16*16加法:
// Kernel definition
__global__ void MatAdd(float A[N][N], float B[N][N],
float C[N][N])
{
int i = blockIdx.x * blockDim.x + threadIdx.x; //find row
int j = blockIdx.y * blockDim.y + threadIdx.y; //find col
if (i < N && j < N)
C[i][j] = A[i][j] + B[i][j];
}
int main()
{
...
// Kernel invocation
dim3 threadsPerBlock(16, 16);
dim3 numBlocks(N / threadsPerBlock.x, N / threadsPerBlock.y);
MatAdd<<<numBlocks, threadsPerBlock>>>(A, B, C);
...
}複製代碼
事實上,邏輯層雖然一次launch不少個線程,物理層上是沒有那麼多線程同時執行的,CUDA每次的執行都是以wrap(32個線程)爲單位,每一個SM上只有8個SP,4個一組,通常會執行16個線程,緣由是:
一個 warp 裏面有 32 個 threads,分紅兩組 16 threads 的 half-warp。因爲 stream processor 的運算至少有 4 cycles 的 latency,所以對一個 4D 的stream processors 來講,一次至少執行 16 個 threads(即 half-warp)纔能有效隱藏各類運算的 latency( 若是你開始運算,再開一個線程,開始運算,再開一個線程,開始運算,再開一個線程開始運算,這時候第一個線程就ok了,第一個線程再開始運算 , 看起來就沒有延遲了, 每一個處理單元上最少開4個能夠達到隱藏延遲的目的,也就是4*4=16個線程)。也所以,線程數達到隱藏各類latency的程度後,以後數量的提高就沒有太大的做用了。
原文連接: blog.csdn.net/Bruce_0712/…2673
每一個線程能夠訪問到本身的local memory,還有block上的shared memory和全局內存global memory,上面三種內存的訪問速度是從小到大的,全局內存訪問最慢:
在英偉達的概念中,host指CPU,device指GPU,kernel運行時只能訪問到GPU的內存,因此在執行任務先後都須要在host和device之間進行數據交互:
// Host code
int main()
{
int N = ...;
size_t size = N * sizeof(float);
// Allocate input vectors h_A and h_B in host memory 分配host內存
float* h_A = (float*)malloc(size);
float* h_B = (float*)malloc(size);
// Initialize input vectors
...
// Allocate vectors in device memory 分配device內存
float* d_A;
cudaMalloc(&d_A, size);
float* d_B;
cudaMalloc(&d_B, size);
float* d_C;
cudaMalloc(&d_C, size);
// Copy vectors from host memory to device memory 數據從host到device
cudaMemcpy(d_A, h_A, size, cudaMemcpyHostToDevice);
cudaMemcpy(d_B, h_B, size, cudaMemcpyHostToDevice);
// Invoke kernel
int threadsPerBlock = 256;
int blocksPerGrid =
(N + threadsPerBlock - 1) / threadsPerBlock;
VecAdd<<<blocksPerGrid, threadsPerBlock>>>(d_A, d_B, d_C, N);
// Copy result from device memory to host memory 數據從device到host
// h_C contains the result in host memory
cudaMemcpy(h_C, d_C, size, cudaMemcpyDeviceToHost);
// Free device memory
cudaFree(d_A);
cudaFree(d_B);
cudaFree(d_C);
// Free host memory
...
}複製代碼
瞭解了物理層的硬件結構和邏輯層的編程模型後,就能夠進行cuda編程了。我認爲重要的有兩步:
下面主要經過矩陣乘法的官方例子講一下我對第二步的理解。
void main()
{
...
// Invoke kernel
dim3 dimBlock(BLOCK_SIZE, BLOCK_SIZE);
dim3 dimGrid(B.width / dimBlock.x, A.height / dimBlock.y);
MatMulKernel<<<dimGrid, dimBlock>>>(d_A, d_B, d_C);
...
}
// Matrix multiplication kernel called by MatMul()
__global__ void MatMulKernel(Matrix A, Matrix B, Matrix C)
{
// Each thread computes one element of C
// by accumulating results into Cvalue
float Cvalue = 0;
int row = blockIdx.y * blockDim.y + threadIdx.y;
int col = blockIdx.x * blockDim.x + threadIdx.x;
for (int e = 0; e < A.width; ++e)
Cvalue += A.elements[row * A.width + e]
* B.elements[e * B.width + col];
C.elements[row * C.width + col] = Cvalue;
}複製代碼
剛接觸cuda確定會雲裏霧裏,被一堆blockId,blockDim,threadId搞暈,我我的的理解是必定要理解總體的分區,知道每一個線程/kernel要作什麼,而後找到當前線程與目標矩陣元素的對應關係,就能夠看懂代碼了。
好比在invoke kernel的時候咱們能夠看出目標矩陣C被分塊處理,每一個block處理矩陣中一個BLOCK_SIZE*BLOCK_SIZE的區域,而後每一個線程計算一個區域中的一個目標元素(即A的某行*B的某列),以下圖:
(沒有人提問的話我就當作你們都理解了)
上述矩陣乘法作到了計算並行化,但還有一個問題就是對全局內存的頻繁讀寫,每一個element都從全局內存讀,計算完後寫回全局內存,若是不少線程同時進行這樣的操做,會被帶寬所限制,所以一個優化的方案就是把A、B子矩陣讀到block的共享內存中,這樣每一個block內的線程都不用單獨去全局內存中取:
// Matrices are stored in row-major order:
// M(row, col) = *(M.elements + row * M.stride + col)
typedef struct {
int width;
int height;
int stride;
float* elements;
} Matrix;
// Get a matrix element
__device__ float GetElement(const Matrix A, int row, int col)
{
return A.elements[row * A.stride + col];
}
// Set a matrix element
__device__ void SetElement(Matrix A, int row, int col,
float value)
{
A.elements[row * A.stride + col] = value;
}
// Get the BLOCK_SIZExBLOCK_SIZE sub-matrix Asub of A that is
// located col sub-matrices to the right and row sub-matrices down
// from the upper-left corner of A
__device__ Matrix GetSubMatrix(Matrix A, int row, int col)
{
Matrix Asub;
Asub.width = BLOCK_SIZE;
Asub.height = BLOCK_SIZE;
Asub.stride = A.stride;
Asub.elements = &A.elements[A.stride * BLOCK_SIZE * row
+ BLOCK_SIZE * col];
return Asub;
}
// Thread block size
#define BLOCK_SIZE 16
// Matrix multiplication kernel called by MatMul()
__global__ void MatMulKernel(Matrix A, Matrix B, Matrix C)
{
// Block row and column
int blockRow = blockIdx.y;
int blockCol = blockIdx.x;
// Each thread block computes one sub-matrix Csub of C
Matrix Csub = GetSubMatrix(C, blockRow, blockCol);
// Each thread computes one element of Csub
// by accumulating results into Cvalue
float Cvalue = 0;
// Thread row and column within Csub
int row = threadIdx.y;
int col = threadIdx.x;
// Loop over all the sub-matrices of A and B that are
// required to compute Csub
// Multiply each pair of sub-matrices together
// and accumulate the results
for (int m = 0; m < (A.width / BLOCK_SIZE); ++m) {
// Get sub-matrix Asub of A
Matrix Asub = GetSubMatrix(A, blockRow, m);
// Get sub-matrix Bsub of B
Matrix Bsub = GetSubMatrix(B, m, blockCol);
// Shared memory used to store Asub and Bsub respectively
__shared__ float As[BLOCK_SIZE][BLOCK_SIZE];
__shared__ float Bs[BLOCK_SIZE][BLOCK_SIZE];
// Load Asub and Bsub from device memory to shared memory
// Each thread loads one element of each sub-matrix
As[row][col] = GetElement(Asub, row, col);
Bs[row][col] = GetElement(Bsub, row, col);
// Synchronize to make sure the sub-matrices are loaded
// before starting the computation
__syncthreads();
// Multiply Asub and Bsub together
for (int e = 0; e < BLOCK_SIZE; ++e)
Cvalue += As[row][e] * Bs[e][col];
// Synchronize to make sure that the preceding
// computation is done before loading two new
// sub-matrices of A and B in the next iteration
__syncthreads();
}
// Write Csub to device memory
// Each thread writes one element
SetElement(Csub, row, col, Cvalue);
}複製代碼
代碼中的__device__表示運行在GPU上的函數,且只能被GPU上的函數調用,__syncthreads表示線程同步,是比較經常使用的函數。
暫時就更到這裏啦,按照本身的思路來的,可能講的有些簡略,不懂的歡迎提問~不過仍是多看代碼本身想想來的快~