CUDA編程手冊(一)

1 簡介

1.1 從圖像處理到通用並行計算

由於市場對實時高清3D圖形計算的強烈需求,可編程圖像處理器(programmable Graphic Processor Unit)或簡稱GPU,已發展成高並行、多核、多線程的處理器,擁有強大算力和高內存帶寬。

以下圖一和圖二比較了CPU和GPU之間浮點運算能力和內存帶寬間方面的差異:

在這裏插入圖片描述

圖一:CPU和GPU每秒浮點運算次數比較

在這裏插入圖片描述

圖二:CPU與GPU之間內存帶寬比較

GPU強大的浮點數運算能力源自它的結構是專爲併發數據計算而設計的,這正是圖像渲染所需要的,相比於CPU的設計,GPU將更多的晶體管用於數據處理,而不像CPU用於數據緩存和流程控制。

在這裏插入圖片描述

圖三:GPU中更多的晶體管用於數據處理

這種設計理論上對於並行計算是有效的,因爲GPU可以用並行計算彌補內存訪問的延遲,而不用通過緩存和流控制來避免內存訪問。

數據並行化處理將數據映射到並行運行的線程。很多需要處理大量數據的應用都可以採用數據並行編程模型來加速計算。比如在3D渲染中像素和頂點被映射到並行運行的不同線程。另外,比如圖像處理、視頻編解碼、立體視覺、模式識別等圖像和多媒體處理等等,都可以採用類似的並行編程模型。實際中除圖形渲染和處理之外很多其他應用可通過數據併發來加速計算。從信號處理、物理模擬到計算金融學和計算生物學等等。

1.2 CUDA: 通用並行計算平臺和編程模型

2006年NVIDIA推出CUDA,通用並行計算平臺和編程模型,利用NVIDIA GPU的並行計算能力解決複雜的計算問題。

CUDA包含軟件環境,開發者可以使用C++作爲高層的開發語言進行並行程序開發。如圖四所示,除C++之外,CUDA還支持一些其他的語言、應用編程接口和基於指令的操作方式。

在這裏插入圖片描述

圖四:CUDA支持很多的編程語言和應用編程接口

1.3 可擴展編程模型

多核CPU和多核GPU的出現,意味着主流的處理芯片都已是並行系統。隨之而來的挑戰是,如何設計能根據不斷增加的處理器內核數量透明的擴展自身並行度的應用軟件。

CUDA的設計初衷就是克服這一挑戰,同時對熟悉標準開發語言(如C語言)的開發者保持一個低的學習曲線。

CUDA的核心包括三個抽象概念:線程的分層結構,共享內存和同步機制。這三個概念以C++語言的擴展的形式暴露給開發者。

這些概念提供了精細粒度的數據並行和線程並行,以及粗粒度的數據並行和任務並行。它們指導開發者將問題分解爲粗粒度的可由線程組並行獨立解決的子問題,每個子問題再分解爲細粒度的可由組內線程合作解決的最終問題。

這種分解方式允許了線程間的合作,同時保證了自動擴展性。每一個線程組可以獨立的被GPU內的任何內核調度執行,以任何順序,並行或線性順序。因此編譯後的CUDA程序可以運行在任何內核數量的GPU上,如圖五所示,只有運行時系統才需要知道準確的物理內核數量。

在這裏插入圖片描述

圖五:自動擴展

注:GPU由一組流式多處理器(Streaming Multiprocessors, SMs)構成。多線程程序會被劃分爲多個線程組,彼此獨立運行在不同的SM中,因此更多的SM數量意味着更短的運行延遲。

2 編程模型

本章我們介紹CUDA編程模型背後的主要概念。更詳細的介紹將在後面的編程接口中給出。

本章使用的vectorAdd的源代碼在CUDA sample裏可以找到。

2.1 內核

CUDA對C++語言進行了擴展,開發者可以定義被稱爲內核的C++函數,當被調用時內核函數會被CUDA線程並行執行N次,而不像普通函數那樣只被執行一次。

內核定義需要使用修飾符__global__\_\_global\_\_,CUDA線程數量在執行配置<<<…>>>中指定。執行內核函數的CUDA線程綁定一個唯一的ID,可以通過內建變量threadIdx獲取。

例如下面的代碼使用內建變量threadIdx,進行數組A和B相加,並將結果存儲到數組C中。

// Kernel definition
__global__ void VecAdd(float* A, float* B, float* C)
{
    int i = threadIdx.x;
    C[i] = A[i] + B[i];
}

int main()
{
    ...
    // Kernel invocation with N threads
    VecAdd<<<1, N>>>(A, B, C);
    ...
}

其中每個cuda線程執行N個加法操作的一個。

2.2 線程層次結構

內建變量threadIdx爲一個包含三個元素的向量,因此每個CUDA線程可以通過一個一維,二維或三維的索引唯一標識,整體的CUDA線程則構成一個一維,二維或三維的線程組,我們稱之爲線程塊(thread block)。這種表示方式可以很方便的操作和計算向量、矩陣或分區內的元素。

線程索引與線程ID的關係也比較簡單:對於一維線程塊,二則相等;對於二維線程塊,(DX,Dy)(D_X,D_y)表示線程塊形狀,(x,y)(x, y)表示線程索引,則線程ID爲(x+yDx)(x + yD_x);對於三維線程塊,(Dx,Dy,Dz)(D_x,D_y,D_z)表示線程塊形狀,(x,y,z)(x,y,z)表示線程索引,則線程ID表示爲x+yDx+zDxDyx+yD_x+zD_xD_y.

下面的例子將兩個NxN的A、B矩陣相加,並將結果存如C:

// Kernel definition
__global__ void MatAdd(float A[N][N], float B[N][N],
                       float C[N][N])
{
    int i = threadIdx.x;
    int j = threadIdx.y;
    C[i][j] = A[i][j] + B[i][j];
}

int main()
{
    ...
    // Kernel invocation with one block of N * N * 1 threads
    int numBlocks = 1;
    dim3 threadsPerBlock(N, N);
    MatAdd<<<numBlocks, threadsPerBlock>>>(A, B, C);
    ...
}

線程塊中的線程數量是有限的,因爲塊中線程屬於同一個處理器並共享處理器有限的內存資源。目前每個線程塊中線程數量的上限爲1024.

內核可以被相同形狀的多個線程塊執行,因此線程的總數量等於線程塊中的線程數量乘以線程塊的數量。

線程塊一起又構成一維、二維或三維的網格結構,如圖六所示。

在這裏插入圖片描述

圖六:網格結構的線程塊

塊中的線程數量,以及塊數量都在<<<…>>>中以int或dim3變量類型指定。圖六中的線程塊和網格都是二維的。網格中的每個線程塊都由一個一維、二維或三維的索引唯一標識,索引可以通過內建的變量blockIdx獲取。塊的維度可以通過內建變量blockDim獲取。

vecAdd的例子,加入多個線程塊之後變成:

// 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;
    int j = blockIdx.y * blockDim.y + threadIdx.y;
    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);
    ...
}

在本例中線程塊的shape爲16x16,是任意設置的。塊的數量設置保證矩陣的沒一個元素都對應到一個線程。例子中我們假設矩陣的維度可以被塊的數量整除,實際上不能整除也不會有問題。

線程塊會被獨立運行:執行順序可能使任意的順序,並行或則串行。這種獨立性保證了線程塊可以以人物順序在任意數量的處理器上被調度和執行,如圖五所示,使開發者的程序能隨着處理數量自動的進行擴展。

同一個塊內的線程可以通過共享內存共享數據,並聽過同步機制協調內存的訪問。具體來說,線程可以通過調用內建函數__syncthreads\_\_syncthreads進行同步。除了__syncthreads\_\_syncthreads之外,後面將詳細介紹其他線程同步的機制。

爲了提高線程間協作的效率,要求共享內存是靠近處理器的低延遲內存(類似L1緩存),__syncthreads\_\_syncthreads必須是輕量級的操作。

2.3 內存層級結構

如圖七所示,CUDA線程可以訪問多個內存空間。每個線程有自己的私有內存;每個塊內的線程共享塊內部的共享內存,快內共享內存生命期與塊相同;所有的線程共享全局內存。

另外還有兩個只讀內存空間:常量內存空間和材質內存空間。全局內存、常量內存、材質內存針對不同的場景進行了優化(參考接下來的設備內存訪問章節)。材質內存針對一些特定的數據格式提供不了不同的尋址模式,如數據過濾(參考接下來的材質和外觀內存章節)。

全局、常量和材質內存的生命週期與程序一致,在程序的不同內核調用之間保持不變。

在這裏插入圖片描述

圖七:內存層級結構

2.4 異構編程

如圖八所示,CUDA編程模型假定宿主運行C++程序, 獨立的設備作爲宿主的協處理器運行CUDA線程。

CUDA還假定宿主與獨立設備分別維護各自的內存空間,分別稱之爲宿主內存和設備內存。程序通過調用CUDA運行時接口管理CUDA內核的全局、常量和材質內存。包括內存的申請和釋放,以及宿主和設備內存之間的數據傳輸。

統一內存機制提供了跨越宿主內存和設備內存的方式。統一內存可以被系統中所有的CPU和GPU訪問。詳細見接下來的統一內存編程章節。簡單來說,統一內存機制使得我們不需要顯式地調用cudaMemecpy系列的函數來進行數據傳輸,但是底層的數據傳輸依然是存在的,所以程序的執行時間不會減少。統一內存機制使得代碼更簡潔和易於維護。

在這裏插入圖片描述

圖八:異構編程

2.5 計算能力

設備的計算能力表示爲一個版本號,有時稱之爲"SM版本"。這個版本號標識GPU硬件所支持的特性,因此應用程序可以在運行時以此來判斷當前GPU支持哪些硬件特性和指令。

版本號包含一個主版本號X和一個小版本號Y,記爲X.Y.

主版本號相同的設備內核架構一樣。7的設備基於Volta架構,6的基於Pasca架構,5的基於Maxwell架構,3的基於Kepler架構,2的基於Fermi架構,1的基於Tesla架構。

不同的小版本號對應不同的改進,比如新特性的加入。

注意:不要混淆GPU的計算能力與CUDA的版本號。

從cuda7.0和cuda9.0開始,分別不在支持基於Tesla架構和Fermi架構的GPU.

發佈了53 篇原創文章 · 獲贊 109 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章