CUDA編程(二) CUDA初始化與核函數

CUDA編程(二)

CUDA初始化與核函數

CUDA初始化

在上一次中已經說過了,CUDA安裝成功之後,新建一個工程還是十分簡單的,直接在新建項目的時候選擇NVIDIA CUDA項目就可以了,我們先新建一個MyCudaTest 工程,刪掉自帶的示例kernel.cu,然後新建項,新建一個CUDA C/C++ File ,我們首先看一下如何初始化CUDA,因此我命名爲InitCuda.cu

這裏寫圖片描述

這裏寫圖片描述

首先我們要使用CUDA的RunTime API 所以 我們需要include cuda_runtime.h

#include <stdio.h> 

//CUDA RunTime API
#include <cuda_runtime.h>

接下來這個函數會調用 runtime API 中 有關初始化CUDA的內容

//CUDA 初始化
bool InitCUDA()
{
    int count;

    //取得支持Cuda的裝置的數目
    cudaGetDeviceCount(&count);

    //沒有符合的硬件
    if (count == 0) {
        fprintf(stderr, "There is no device.\n");
        return false;
    }

    int i;

    for (i = 0; i < count; i++) {
        cudaDeviceProp prop;
        if (cudaGetDeviceProperties(&prop, i) == cudaSuccess) {
            if (prop.major >= 1) {
                break;
            }
        }
    }

    if (i == count) {
        fprintf(stderr, "There is no device supporting CUDA 1.x.\n");
        return false;
    }

    cudaSetDevice(i);

    return true;
}

這段程序首先會調用cudaGetDeviceCount 函數,獲得支持 CUDA 的GPU的數量,如果計算機上沒有支持 CUDA 的裝置,則會傳回 1,而這個1是device 0 ,device0 只是一個仿真裝置,但是CUDA的很多功能都不支持(不支持CUDA1.0以上版本),因此我們要真正確定系統上是否有支持CUDA的裝置,需要對每個device調用cudaGetDeviceProperties,來獲得它們的具體參數,以及所支持的CUDA版本(prop.major 和 prop.minor 分別代表裝置支持的版本號碼,例如 6.5 則 prop.major 爲 6 而prop.minor 爲 5)

cudaGetDeviceProperties除了可以獲得裝置支持的 CUDA 版本之外,還有裝置的名稱、內存的大小、最大的 thread 數目、執行單元的頻率等等。詳情可參考NVIDIA 的 CUDA Programming Guide。

在找到支持 CUDA 1.0 以上的裝置之後,就可以呼叫 cudaSetDevice 函式,把它設爲目前要使用的顯卡。

下面我們在Main函數中調用InitCUDA函數,由於我們使用VS,所以直接ctrl+F5編譯執行就可以了,執行時如果系統上有支持 CUDA 的裝置,應該會顯示 CUDA initialized。

int main() 
{

    if (!InitCUDA()) 
    { 
        return 0; 
    }

    printf("CUDA initialized.\n"); return 0;

}

這裏寫圖片描述

完整程序:

#include <stdio.h> 

//CUDA RunTime API
#include <cuda_runtime.h>

//CUDA 初始化
bool InitCUDA()
{
    int count;

    //取得支持Cuda的裝置的數目
    cudaGetDeviceCount(&count);

    //沒有符合的硬件
    if (count == 0) {
        fprintf(stderr, "There is no device.\n");
        return false;
    }

    int i;

    for (i = 0; i < count; i++) {
        cudaDeviceProp prop;
        if (cudaGetDeviceProperties(&prop, i) == cudaSuccess) {
            if (prop.major >= 1) {
                break;
            }
        }
    }

    if (i == count) {
        fprintf(stderr, "There is no device supporting CUDA 1.x.\n");
        return false;
    }

    cudaSetDevice(i);

    return true;
}

int main() 
{

    if (!InitCUDA()) 
    { 
        return 0; 
    }

    printf("CUDA initialized.\n"); return 0;

}

CUDA核函數

完成了CUDA的初始化檢查操作,下面我們就可以使用CUDA完成一些簡單計算了,這裏我們打算計算一系列數字的立方和。

所以我們先寫了一個隨機函數:

#define DATA_SIZE 1048576

int data[DATA_SIZE];

//產生大量0-9之間的隨機數
void GenerateNumbers(int *number, int size)
{
    for (int i = 0; i < size; i++) {
        number[i] = rand() % 10;
    }
}

//生成隨機數(main中調用)
//GenerateNumbers(data, DATA_SIZE);

該函數會產生一大堆 0 ~ 9 之間的隨機數,然後我們要對他們進行立方和操作。

那麼我們如何讓這個工作在顯卡上完成呢?首先第一件事很顯而易見,這些數字不能放在內存裏了,而是要複製到GPU的顯存上。下面我們就來看一下數據複製的部分。

Host&Device架構:

這裏寫圖片描述

上一次已經講過關於CUDA架構的一些基礎了,這裏再稍微複習一下,在 CUDA 的架構下,一個程序分爲兩個部份:host 端和 device 端。Host 端是指在 CPU 上執行的部份,而 device 端則是在顯示芯片上執行的部份。Device 端的程序又稱爲 “kernel”。通常 host 端程序會將數據準備好後,複製到顯卡的內存中,再由顯示芯片執行 device 端程序,完成後再由 host 端程序將結果從顯卡的內存中取回。

我們需要把產生的數據複製到Device端的RAM,才能在顯卡上完成計算,因此我們首先開闢一塊合適的顯存,然後把隨機數從內存複製進去。

    //生成隨機數
    GenerateNumbers(data, DATA_SIZE);

    /*把數據複製到顯卡內存中*/

    int* gpudata, *result;

    //cudaMalloc 取得一塊顯卡內存 ( 其中result用來存儲計算結果 )
    cudaMalloc((void**)&gpudata, sizeof(int)* DATA_SIZE);
    cudaMalloc((void**)&result, sizeof(int));

    //cudaMemcpy 將產生的隨機數複製到顯卡內存中 
    //cudaMemcpyHostToDevice - 從內存複製到顯卡內存
    //cudaMemcpyDeviceToHost - 從顯卡內存複製到內存
    cudaMemcpy(gpudata, data, sizeof(int)* DATA_SIZE,cudaMemcpyHostToDevice);

註釋已經寫得比較明白了。cudaMalloc 和 cudaMemcpy 的用法和一般的 malloc 及 memcpy 類似,不過 cudaMemcpy 則多出一個參數,指示覆制內存的方向。在這裏因爲是從主內存複製到顯卡內存,所以使用 cudaMemcpyHostToDevice。如果是從顯卡內存到主內存,則使用cudaMemcpyDeviceToHost。

完成了從內存到顯存的數據拷貝之後,我們接下來就要在顯卡上完成計算了,如何讓程序跑在顯卡上?答案是核函數。

CUDA核函數:

要寫在顯示芯片上執行的程序。在 CUDA 中,在函數前面加上__global__ 表示這個函式是要在顯示芯片上執行的,所以我們只要在正常函數之前加上一個__global__就行了:

// __global__ 函數 (GPU上執行) 計算立方和
__global__ static void sumOfSquares(int *num, int* result)
{
    int sum = 0;

    int i;

    for (i = 0; i < DATA_SIZE; i++) {

        sum += num[i] * num[i] * num[i];

    }

    *result = sum;

}

在顯示芯片上執行的程序有一些限制,首先最明顯的一個限制——不能有傳回值,還有一些其他的限制,後面會慢慢提到。

執行核函數:

寫好核函數之後需要讓CUDA執行這個函數。

在 CUDA 中,要執行一個核函數,使用以下的語法:

    函數名稱<<<block 數目, thread 數目, shared memory 大小>>>(參數...);

這裏我們先不去並行,只是單純地完成GPU計算,所以我們讓block = 1,thread = 1,share memory = 0

    sumOfSquares<<<1, 1, 0>>>(gpudata, result);

計算完了,千萬別忘了還要把結果從顯示芯片複製回主內存上,然後釋放掉內存~

    int sum;

    //cudaMemcpy 將結果從顯存中複製回內存
    cudaMemcpy(&sum, result, sizeof(int), cudaMemcpyDeviceToHost);

    //Free
    cudaFree(gpudata);
    cudaFree(result);

最後我們把結果打印出來就大功告成了:

    printf("GPUsum: %d \n", sum);

之後我們再用CPU計算一下來驗證一下上面的過程是否有錯,這一步還是十分必要的:

    sum = 0;

    for (int i = 0; i < DATA_SIZE; i++) {
        sum += data[i] * data[i] * data[i];
    }

    printf("CPUsum: %d \n", sum);

完整程序:

程序代碼:

#include <stdio.h>
#include <stdlib.h>

//CUDA RunTime API
#include <cuda_runtime.h>

#define DATA_SIZE 1048576

int data[DATA_SIZE];

//產生大量0-9之間的隨機數
void GenerateNumbers(int *number, int size)
{
    for (int i = 0; i < size; i++) {
        number[i] = rand() % 10;
    }
}

//CUDA 初始化
bool InitCUDA()
{
    int count;

    //取得支持Cuda的裝置的數目
    cudaGetDeviceCount(&count);

    if (count == 0) {
        fprintf(stderr, "There is no device.\n");
        return false;
    }

    int i;

    for (i = 0; i < count; i++) {
        cudaDeviceProp prop;
        if (cudaGetDeviceProperties(&prop, i) == cudaSuccess) {
            if (prop.major >= 1) {
                break;
            }
        }
    }

    if (i == count) {
        fprintf(stderr, "There is no device supporting CUDA 1.x.\n");
        return false;
    }

    cudaSetDevice(i);

    return true;
}


// __global__ 函數 (GPU上執行) 計算立方和
__global__ static void sumOfSquares(int *num, int* result)
{
    int sum = 0;

    int i;

    for (i = 0; i < DATA_SIZE; i++) {

        sum += num[i] * num[i] * num[i];

    }

    *result = sum;

}





int main()
{

    //CUDA 初始化
    if (!InitCUDA()) {
        return 0;
    }

    //生成隨機數
    GenerateNumbers(data, DATA_SIZE);

    /*把數據複製到顯卡內存中*/

    int* gpudata, *result;

    //cudaMalloc 取得一塊顯卡內存 ( 其中result用來存儲計算結果 )
    cudaMalloc((void**)&gpudata, sizeof(int)* DATA_SIZE);
    cudaMalloc((void**)&result, sizeof(int));

    //cudaMemcpy 將產生的隨機數複製到顯卡內存中 
    //cudaMemcpyHostToDevice - 從內存複製到顯卡內存
    //cudaMemcpyDeviceToHost - 從顯卡內存複製到內存
    cudaMemcpy(gpudata, data, sizeof(int)* DATA_SIZE, cudaMemcpyHostToDevice);

    // 在CUDA 中執行函數 語法:函數名稱<<<block 數目, thread 數目, shared memory 大小>>>(參數...);
    sumOfSquares << <1, 1, 0 >> >(gpudata, result);


    /*把結果從顯示芯片複製回主內存*/

    int sum;

    //cudaMemcpy 將結果從顯存中複製回內存
    cudaMemcpy(&sum, result, sizeof(int), cudaMemcpyDeviceToHost);

    //Free
    cudaFree(gpudata);
    cudaFree(result);

    printf("GPUsum: %d \n", sum);

    sum = 0;

    for (int i = 0; i < DATA_SIZE; i++) {
        sum += data[i] * data[i] * data[i];
    }

    printf("CPUsum: %d \n", sum);

    return 0;
}

運行結果:

這裏寫圖片描述

總結:

這次給大家介紹了CUDA的初始化和如何在顯卡上運行程序,即先將數據從內存複製到顯存,再寫好運算的核函數,之後用CUDA調用核函數,完成GPU上的計算,之後當然不要忘記將結果複製回內存,釋放掉顯存。

總的來說一個CUDA程序的骨架已經搭建起來了,而GPU計算的重中之重即並行加速還沒有進行介紹,不過在加速之前我們還有一件非常重要的事情需要考慮,那就是我們的程序到底有沒有加速,也就是我們要輸出程序的運行時間,這個時間我們需要使用CUDA提供的一個Clock函數,可以取得GPU執行單元的頻率,所以下一篇博客我將主要講解這個函數~希望能給大家的學習帶來幫助~

參考資料:《深入淺出談CUDA》

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