1. 程式人生 > >GPU程式設計自學10 —— 流並行

GPU程式設計自學10 —— 流並行

深度學習的興起,使得多執行緒以及GPU程式設計逐漸成為演算法工程師無法規避的問題。這裡主要記錄自己的GPU自學歷程。

目錄

十、 流並行

我們前面學習的CUDA並行程式設計,基本上都是在一批資料上利用大量執行緒實現並行。 除此之外, NVIDIA系列GPU還支援另外一種型別的並行性 ——

GPU中的流並行類似於CPU上的任務並行,即每個流都可以看作是一個獨立的任務,每個流中的程式碼操作順序執行。

下面從流並行的基礎到使用來說明。

10.1 頁鎖定記憶體

流並行的使用需要有硬體支援:即必須是支援裝置重疊功能的GPU。

通過下面的程式碼查詢裝置是否支援裝置重疊功能

cudaDeviceProp mprop;
cudaGetDeviceProperties(&mprop,0);
if (!mprop.deviceOverlap)
{
    cout << "Device not support overlaps, so stream is invalid!" << endl;
}

只有支援裝置重疊,GPU在執行一個核函式的同時,才可以同時在裝置與主機之間執行復制操作。 當然,這種複製操作需要在一種特殊的記憶體上才可以進行 —— 頁鎖定記憶體

  • 頁鎖定記憶體: 需要由cudaHostAlloc()分配,又稱為固定記憶體(Pinned Memory)或者不可分頁記憶體。 作業系統將不會對這塊記憶體分頁並交換到磁碟上,從而確保了該記憶體始終駐留在實體記憶體中,因為這塊記憶體將不會被破壞或者重新定位。 由於gpu知道記憶體的實體地址,因此可以通過“直接記憶體訪問(Direct Memory Access,DMA)” 直接在gpu和主機之間複製資料。
  • 可分頁記憶體: malloc()分配的記憶體是標準的、可分頁的(Pagable)主機記憶體。 可分頁記憶體面臨著重定位的問題,因此使用可分頁記憶體進行復制時,複製可能執行兩次操作:從可分頁記憶體複製到一塊“臨時”頁鎖定記憶體,然後從頁鎖定記憶體複製到GPU。

雖然在頁鎖定記憶體上執行復制操作效率比較高,但消耗實體記憶體更多。因此,通常對cudaMemcpy()呼叫的源記憶體或者目標記憶體才使用,而且使用完畢立即釋放。

10.2 流並行機制

流並行是指我們可以建立多個流來執行多個任務, 但每個流都是一個需要按照順序執行的操作佇列。 那麼我們如何實現程式加速? 其核心就在於,在頁鎖定記憶體上的資料複製是獨立於核函式執行的,即我們可以在執行核函式的同時進行資料複製。

這裡的複製需要使用cudaMemcpyAsync(),一個以非同步執行的函式。呼叫cudaMemcpyAsync()時,只是放置一個請求,表示在流中執行一次記憶體複製操作。當函式返回時,我們無法確保複製操作已經結束。我們能夠得到的保證是,複製操作肯定會當下一個被放入流中的操作之前執行。(相比之下,cudaMemcpy()是一個同步執行函式。當函式返回時,複製操作已完成。)

以計算 a + b = c為例,假如我們建立了兩個流,每個流都是按順序執行:

複製a(主機到GPU) -> 複製b(主機到GPU) -> 核函式計算 -> 複製c(GPU到主機)

如上圖,複製操作和核函式執行是分開的,但由於每個流內部需要按順序執行,因此複製c的操作需要等待核函式執行完畢。 於是,整個程式執行的時間線如下圖:(箭頭表示需要等待)

從上面的時間線我們可以啟發式的思考下:如何調整每個流當中的操作順序來獲得最大的收益? 提高重疊率

如下圖所示,假如複製一份資料的時間和執行一次核函式的時間差不多,那麼我們可以採用交叉執行的策略:

由於流0的a和b已經準備完成,因此當複製流1的b時,可以同步執行流0的核函式。 這樣整個時間線,相較於之前的操作很明顯少掉了兩塊操作。

10.3 流並行示例

與流相關的常用函式如下:

// 建立與銷燬
cudaStream_t stream//定義流 
cudaStreamCreate(cudaStream_t * s)//建立流 
cudaStreamDestroy(cudaStream_t s)//銷燬流 

//同步 
cudaStreamSynchronize()//同步單個流:等待該流上的命令都完成 
cudaDeviceSynchronize()//同步所有流:等待整個裝置上流都完成 
cudaStreamWaitEvent()//等待某個事件結束後執行該流上的命令 
cudaStreamQuery()//查詢一個流任務是否完成 

//回撥 
cudaStreamAddCallback()//在任何點插入回撥函式 

//優先順序 
cudaStreamCreateWithPriority() 
cudaDeviceGetStreamPriorityRange()

下面給出一個2個流執行a + b = c的示例, 我們假設資料量非常大,需要將資料拆分,每次計算一部分。

#include <iostream>
#include "cuda_runtime.h"


using namespace std;

#define N   (1024*256)  // 每次處理的資料量
#define SIZE   (N*20)  //資料總量

// 核函式,a + b = c
__global__ void add(int* a, int* b, int* c)
{
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < N)
        c[i] = a[i] + b[i];
}

int main()
{
    // 獲取0號GPU的屬性並判斷是否支援裝置重疊功能
    cudaDeviceProp mprop;
    cudaGetDeviceProperties(&mprop,0);
    if (!mprop.deviceOverlap)
    {
        cout << "Device not support overlaps, so stream is invalid!" << endl;
        return 0;
    }

    // 建立計時事件
    cudaEvent_t     start, stop;
    cudaEventCreate(&start); cudaEventCreate(&stop);
    float           elapsedTime;

    // 建立流
    cudaStream_t    stream0, stream1;
    cudaStreamCreate(&stream0);
    cudaStreamCreate(&stream1);

    // 開闢主機頁鎖定記憶體,並隨機初始化資料
    int *host_a, *host_b, *host_c;
    cudaHostAlloc((void**)&host_a, SIZE*sizeof(int), cudaHostAllocDefault);
    cudaHostAlloc((void**)&host_b, SIZE*sizeof(int), cudaHostAllocDefault);
    cudaHostAlloc((void**)&host_c, SIZE*sizeof(int), cudaHostAllocDefault);
    for (size_t i = 0; i < SIZE; i++)
    {
        host_a[i] = rand();
        host_b[i] = rand();
    }

    // 宣告並開闢相關變數記憶體
    int *dev_a0, *dev_b0, *dev_c0;   //用於流0的資料
    int *dev_a1, *dev_b1, *dev_c1;   //用於流1的資料
    cudaMalloc((void**)&dev_a0,N*sizeof(int));
    cudaMalloc((void**)&dev_b0, N*sizeof(int));
    cudaMalloc((void**)&dev_c0, N*sizeof(int));
    cudaMalloc((void**)&dev_a1, N*sizeof(int));
    cudaMalloc((void**)&dev_b1, N*sizeof(int));
    cudaMalloc((void**)&dev_c1, N*sizeof(int));

    /************************  核心計算部分    ***************************/
    cudaEventRecord(start, 0);
    for (size_t i = 0; i < SIZE; i += 2*N)
    {

        // 複製流0資料a
        cudaMemcpyAsync(dev_a0, host_a + i,   N*sizeof(int), cudaMemcpyHostToDevice, stream0);
        // 複製流1資料a
        cudaMemcpyAsync(dev_a1, host_a + i+N, N*sizeof(int), cudaMemcpyHostToDevice, stream1);
        // 複製流0資料b
        cudaMemcpyAsync(dev_b0, host_b + i,   N*sizeof(int), cudaMemcpyHostToDevice, stream0);
        // 複製流1資料b
        cudaMemcpyAsync(dev_b1, host_b + i+N, N*sizeof(int), cudaMemcpyHostToDevice, stream1);
        // 執行流0核函式
        add << <N / 256, 256, 0, stream0 >> >(dev_a0, dev_b0, dev_c0);
        // 執行流1核函式
        add << <N / 256, 256, 0, stream1 >> >(dev_a1, dev_b1, dev_c1);
        // 複製流0資料c
        cudaMemcpyAsync(host_c + i*N,   dev_c0, N*sizeof(int), cudaMemcpyDeviceToHost, stream0);
        // 複製流1資料c
        cudaMemcpyAsync(host_c + i*N+N, dev_c1, N*sizeof(int), cudaMemcpyDeviceToHost, stream1);
    }

    // 流同步
    cudaStreamSynchronize(stream0);
    cudaStreamSynchronize(stream1);

    // 處理計時
    cudaEventSynchronize(stop);
    cudaEventRecord(stop, 0);
    cudaEventElapsedTime(&elapsedTime, start, stop);
    cout << "GPU time: " << elapsedTime << "ms" << endl;

    // 銷燬所有開闢的記憶體
    cudaFreeHost(host_a); cudaFreeHost(host_b); cudaFreeHost(host_c);
    cudaFree(dev_a0); cudaFree(dev_b0); cudaFree(dev_c0);
    cudaFree(dev_a1); cudaFree(dev_b1); cudaFree(dev_c1);

    // 銷燬流以及計時事件
    cudaStreamDestroy(stream0); cudaStreamDestroy(stream1);
    cudaEventDestroy(start);    cudaEventDestroy(stop);

    return 0;
}

參考資料

  • 《CUDA by Example: An Introduction to General-Purpose GPU Programming》 中文名《GPU高效能程式設計CUDA實戰》