CUDA編程從放棄到入門

剛纔英偉達的朋友指出此文有不少錯誤,你們先別看了,待我明天回爐重造


最近看fastertransformer源碼,接觸了不少底層到東西,cuda源碼各類看不懂,就去學cuda,學了一下子以爲就想放棄,結果翻回去看源碼仍是不懂,反覆幾回,最後乾脆拿出一上午靜靜地把官方文檔啃了啃纔算入門。因此寫這篇文章幫助一樣想要放棄的同窗入門一下。html

網上關於cuda的文章並很少,知乎有一篇很高讚的Cuda入門極簡教程,但看完了有些概念我仍是弄混了,最後發現官方文檔真的香,有時間的朋友建議仍是老老實實讀這個,沒時間的話再看我簡化的入門文章(只准備寫到矩陣乘法)。編程

本文默認的讀者是會用tensorflow等高級API但不太懂底層實現的同窗們哦多線程

1. 物理層

CUDA是英偉達2006年末推出的GPU並行計算平臺,支持開發者經過各類語言對GPU計算編程。GPU有如下幾種:架構

  • Tesla系列:數值計算
  • Quadro系列:高級的圖形建模
  • GeForce系列:打遊戲
  • Tegra系列:移動設備

每一個系列的GPU會有不一樣的架構,從Kepler到Turing,每代架構都會進行一些改進。再上層也有封裝好的計算庫(cuDNN、cuBLAS),咱們日常經過tensorflow、pytorch調的包都是對這些計算庫的又一層封裝。ide

物理層上,一個GPU會包含顯存和計算單元,svg

計算單元中有多個SM(streaming multiprocessors),每一個SM上都有寄存器、內存和執行任務的SP(streaming processor/cuda core),運行時一個SP執行一個thread。函數

2. 邏輯層

在編程時不須要本身進行線程的調度,只須要指定某個任務須要多少個線程就行了。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

3. 內存分配

每一個線程能夠訪問到本身的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
    ...
}複製代碼

3. CUDA矩陣乘法

瞭解了物理層的硬件結構和邏輯層的編程模型後,就能夠進行cuda編程了。我認爲重要的有兩步:

  1. Kernel launch前的線程分配(我也沒太懂,同窗們多看些源碼吧)
  2. Kernel的實現(如何根據當前thread找到運算的單元、以及如何高效運算)

下面主要經過矩陣乘法的官方例子講一下我對第二步的理解。

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表示線程同步,是比較經常使用的函數。


暫時就更到這裏啦,按照本身的思路來的,可能講的有些簡略,不懂的歡迎提問~不過仍是多看代碼本身想想來的快~

相關文章
相關標籤/搜索