《基於CUDA的並行程序設計》學習筆記(三)——中

這裏寫圖片描述


第3章 CUDA編程基礎

3.3 CUDA編程模型

CUDA架構第一次引入了主機(host)端與設備(device)端的概念。如下圖所示,一個完整的CUDA程序由主機代碼和設備代碼兩部分組成。主機代碼在主機端CPU上串行執行,是普通的C代碼;設備代碼部分在設備端GPU上並行執行,稱爲內核(kernel)。kernel函數不是一個完整的程序,而是任務中能被分解爲並行執行的步驟的集合。CPU執行的串行程序負責kernel啓動之前進行數據準備和設備初始化的工作,以及在kernel之間進行一些串行計算。GPU執行的並行部分是在被稱爲grid和block的兩個層次並行中完成的,即每個kernel函數存在兩個層次的並行:網絡(grid)中的線程塊(block)間並行金額線程塊中的線程(thread)間並行。

這裏寫圖片描述

3.3.1 執行結構

CUDA程序在執行過程中,主機代碼與設備代碼交替運行,程序從主機端的串行代碼開始運行,當運行至設備代碼時,調用內核函數,並切換到設備端,啓動多個GPU線程(thread)並行執行設備代碼。每一個線程塊(block)均包含多個線程,執行相同的指令,達到線程塊內的並行,同時在每個線程網格(grid)中,實現不同線程塊之間的並行。設備完成計算後,返回主機線程,主機繼續執行串行操作。重複以上過程,直到程度執行完畢。

3.3.2 內核函數

先看一個簡單的C文件,實現長度均爲N的數組A和數據B的相加,結果保存在數組C中。

void vectorAdd(float *A, float *B, float *C, int N)
{
    int x=0;
    while(x<=N-1)
    {
        C[i] = A[i] + B[i];
        x++;
    }
}
int main()
{
    float A[N],B[N],C[N]; // 定義等長數組A,B,C
    vectorAd(A,B,C,N); // 調用內核程序
}

在主函數main中調用函數vectorAdd,使用while循環進行A[x]與B[x]的相加並將結果存入C[x]中。

運行時間:單位元素相加所需時間 x N
再來看一個簡單的cuda文件,實現相同的功能。

// 設備端代碼
__global__ void vectorAdd(float *A, float *B, float *C)
{
    int i = threadIdx.x;
    C[i] = A[i] + B[i];
}
// 主機端代碼
int main()
{
    // 定義等長數組A,B,C
    float A[N],B[N],C[N];
    // 對數組進行賦值
    /* 爲輸入參數和輸出參數分配顯存空間 */
    int size = N*sizeof(float);
    float *d_a;
    float *d_b;
    float *d_c;
    cudaMalloc((void**)&d_a, size);
    cudaMalloc((void**)&d_b, size);
    cudaMalloc((void**)&d_c, size);
    // 把輸入參數從主機端複製到GPU內存
    cudaMemcpy(d_a, A, size, cudaMemcpyHostToDevice);
    cudaMemcpy(d_b, B, size, cudaMemcpyHostToDevice);
    // 調用內核程序
    vectorAdd<<<1,N>>>(d_a, d_b, d_c);
    // 將輸出參數從顯存複製到主機端
    cudaMemcpy(C, d_c, size, cudaMemcpyDeviceToHost);
    // 釋放顯存空間
    cudaFree(d_a);
    cudaFree(d_b);
    cudaFree(d_c);
}

在主函數main中調用內核函數vectorAdd,使得每一個線程並行計算每一個A[x]與B[x]的加法並將結果存入C[x]中。

運行時間:單位元素相加所需時間。

在CUDA程序中,程序員可以自定義稱爲內核的C語言函數,如同上面的vectorAdd函數,用__global__聲明說明符來指定內核程序。和普通的C語言程序一樣,CUDA程序的執行入口仍是main函數。一個完整的CUDA程序包括了在CPU端執行的串行代碼和在GPU端執行的並行代碼,其流程如下圖所示,主要包括以下幾個步驟。

這裏寫圖片描述

3.3.3 線程層次

在內核函數中,通過線程的索引來訪問線程ID。在一維數組中,可以用一維線程塊中的threadIdx直接指向相應ID的線程,但是在二維數據、三維數據中卻不相同。對於大小爲(Dx,Dy)的二維塊,索引爲(x,y)的線程的ID是(x+yDx);對於大小爲(Dx,Dy,Dz)的三維塊,索引爲(x,y,z)的線程的ID是(x+yDx+zDxDy)。

有了針對不同維度的數組的索引方式,可以定義出二維、三維的線程塊,去應對不同情況的數據並行方式。

上一個例子中,使用線程的並行操作實現了高效的數組相加,現在可以以線程塊的並行方式,進一步分析矩陣加法的CUDA編程。

__global__ void MatrixAdd(float A[N][N], float B[N][N], float C[N][N])
{
    int x = blockIdx.x * blockDim.x + threadIdx.x;
    int y = blockIdx.y * blockDim.y + threadIdx.y;
    if(x<N && y<N)
        C[x][y] = A[x][y] + B[x][y];
}
int main()
{
    dim3 threadsPerBlock(16,16); // 定義16*16大小的二維線程塊
    dim3 Grid(N / threadsPerBlock.x, N / threadsPerBlock.y);
    MatrixAdd<<<Grid, threadsPerBlock>>>(A,B,C);
}

CUDA程序使用類似kernelFunc<<<N,M>>>(d_a, d_b, d_c);的語句來啓動kernel函數,其中,<<<>>>運算符中的N和M是主機端告訴設備運行時如何啓動kernel函數的參數,N表示一個grid中有多少個並行的block,M表示一個block中多少個並行thread;(d_a, d_b, d_c)則爲kernel函數的函數參數,和普通C函數一樣。在上面代碼中,執行內核的每個線程都會被分配一個獨特的線程ID,可通過threadIdx變量在內核中訪問此ID。

3.3.4 存儲器結構與線程映射機制

CUDA程序執行過程中,如下圖所示,每一個線程作爲線程塊的一部分,都單獨擁有一個私有的本地存儲器,同時也可訪問多個其他存儲器空間。每一個線程塊作爲網格的一部分,都有一個共享存儲器,可供線程塊內的每一個線程訪問,並且與塊具有相同的生命週期。所有線程都可訪問相同的全局存儲器。另外,所有線程也可以訪問固定存儲器和紋理存儲器,這兩個存儲器均爲只讀。與本地存儲器和共享存儲器不同的是,全局存儲器、固定存儲器和紋理存儲器有持久的生命週期。

這裏寫圖片描述

3.3.5 通信機制

目前,GPU與計算機芯片組的各個部件,如CPU、內存等的通信接口目前主要是PCI-E,它應日益膨脹的通信數據量要求而產生,代替了傳統的AGP接口。當GPU與芯片組通信的數據超過了這個數,就會出現等待響應的情況,影響數據傳輸效率。因此,爲了提高程序的運行效率,在CUDA程序中應儘量少地進行主機端與設備端之間的數據傳輸。

  • block塊內的線程通信,一個block塊是被加載到一個SM(流多處理器)中執行的,而一個SM中的所有線程處理器TP則是通過SM中的共享存儲器和柵欄同步實現通信的;

  • grid內的線程通信,不同的block內的線程不能互訪共享存儲器,它們都能訪問到的存儲器只有全局存儲器,因此,grid內的線程通信要藉助於全局存儲器和memory fence函數,這是由CUDA的架構決定的。CUDA的線程通信特性有效提高了程序的執行效率,並且拓展了GPU的適用範圍。

3.3.6 CUDA的軟件體系

CUDA的軟件體系由以下三部分構成:CUDA庫函數(library)、CUDA運行時API(runtime API)和CUDA驅動API(drive API)構成。CUDA軟件體系機構如下圖所示。

這裏寫圖片描述

CUDA應用程序是用CUDA C語言編寫的,CUDA提供了nvcc編譯器對其進行編譯。對CUDA C語言進行編譯得到的只是GPU端的代碼。而GPU資源的管理、GPU上分配顯存並啓動kernel函數,則由CUDA運行時候API或者CUDA驅動API負責。這兩者不能混合使用,一個程序中只能使用其中一個。

3.4 nvcc編譯器

3.4.1 nvcc編譯流程

kernel函數可用CUDA C語言編寫,也可以用PTX編寫。PTX就是CUDA指令集架構,效率高於像C語言一樣的高級語言。但無論使用PTX還是高級語言,kernel函數都必須通過nvcc編譯成可執行的二進制文件後纔可以在設備上運行。

nvcc是CUDA程序編譯器驅動,簡化了C語言或PTX的編譯流程:它提供了簡單的命令行選項,調用一系列不同的編譯工具集來執行它們。nvcc可編譯同時包含主機代碼(在主機上執行的代碼)和設備代碼(在設備上執行的代碼)的源文件。nvcc的基本流程如下圖所示,包括了以下4個步驟:

  • 分離主機和設備代碼

  • 將主機代碼輸出爲C語言代碼供其他工具編譯,或者nvcc在編譯的最後階段調用主機編譯器將主機代碼輸出爲目標代碼。

  • 將設備代碼編譯成彙編形式(PTX)或/和二進制形式(cubin對象)。

  • 使用CUDA驅動API裝載和執行PTX源碼或cubin對象,或者使用鏈接將PTX源碼或cubin對象加載到主機代碼中並將其作爲已初始化的全局數據數組導入並執行。

這裏寫圖片描述

3.4.2 兼容性分析

  • 二進制兼容性
    二進制.cubin文件根據真實的GPU架構獲取-code參數輸出,參數的選擇需要與GPU硬件架構的能力相匹配。二進制兼容性可以向後兼容,但不保證向前兼容。

  • PTX兼容性
    與二進制.cubin文件兼容性一致,.ptx文件也需要明確根據真實的GPU架構獲取的參數。

  • 應用兼容性
    二進制文件.cubin和.ptx文件被嵌入在最終輸出的C文件中時,嵌入的文件必須與相應的-code參數保持一致。

  • C++兼容性
    nvcc編譯器根據C++語法規則處理並分割CUDA源文件。主機代碼完整支持C++,但設備代碼只支持C++的一個子集,包括:
    (1) 多態;
    (2) 默認參數;
    (3) 運算符重載;
    (4) 命名空間;
    (5) 函數模板。

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