1. 程式人生 > >詳解第一個CUDA程式kernel.cu

詳解第一個CUDA程式kernel.cu

CUDA是一個基於NVIDIA GPU的平行計算平臺和程式設計模型,通過呼叫CUDA提供的API,可以開發高效能的並行程式。CUDA安裝好之後,會自動配置好VS編譯環境,按照UCDA模板新建一個工程“Hello CUDA”:


建好之後,發現該工程下已經存在一個專案 kernel.cu。這個是CUDA程式設計的入門示例,實現的功能是兩個整型陣列相加,程式碼如下:

#include "cuda_runtime.h"
#include "device_launch_parameters.h"

#include <stdio.h>

cudaError_t addWithCuda(int *c, const int *a, const int *b, unsigned int size);

__global__ void addKernel(int *c, const int *a, const int *b)
{
	int i = threadIdx.x;
	c[i] = a[i] + b[i];
}

int main()
{
	const int arraySize = 5;
	const int a[arraySize] = { 1, 2, 3, 4, 5 };
	const int b[arraySize] = { 10, 20, 30, 40, 50 };
	int c[arraySize] = { 0 };

	// Add vectors in parallel.
	cudaError_t cudaStatus = addWithCuda(c, a, b, arraySize);
	if (cudaStatus != cudaSuccess) {
		fprintf(stderr, "addWithCuda failed!");
		return 1;
	}

	printf("{1,2,3,4,5} + {10,20,30,40,50} = {%d,%d,%d,%d,%d}\n",
		c[0], c[1], c[2], c[3], c[4]);

	// cudaDeviceReset must be called before exiting in order for profiling and
	// tracing tools such as Nsight and Visual Profiler to show complete traces.
	cudaStatus = cudaDeviceReset();
	if (cudaStatus != cudaSuccess) {
		fprintf(stderr, "cudaDeviceReset failed!");
		return 1;
	}

	return 0;
}

// Helper function for using CUDA to add vectors in parallel.
cudaError_t addWithCuda(int *c, const int *a, const int *b, unsigned int size)
{
	int *dev_a = 0;
	int *dev_b = 0;
	int *dev_c = 0;
	cudaError_t cudaStatus;

	// Choose which GPU to run on, change this on a multi-GPU system.
	cudaStatus = cudaSetDevice(0);
	if (cudaStatus != cudaSuccess) {
		fprintf(stderr, "cudaSetDevice failed!  Do you have a CUDA-capable GPU installed?");
		goto Error;
	}

	// Allocate GPU buffers for three vectors (two input, one output)    .
	cudaStatus = cudaMalloc((void**)&dev_c, size * sizeof(int));
	if (cudaStatus != cudaSuccess) {
		fprintf(stderr, "cudaMalloc failed!");
		goto Error;
	}

	cudaStatus = cudaMalloc((void**)&dev_a, size * sizeof(int));
	if (cudaStatus != cudaSuccess) {
		fprintf(stderr, "cudaMalloc failed!");
		goto Error;
	}

	cudaStatus = cudaMalloc((void**)&dev_b, size * sizeof(int));
	if (cudaStatus != cudaSuccess) {
		fprintf(stderr, "cudaMalloc failed!");
		goto Error;
	}

	// Copy input vectors from host memory to GPU buffers.
	cudaStatus = cudaMemcpy(dev_a, a, size * sizeof(int), cudaMemcpyHostToDevice);
	if (cudaStatus != cudaSuccess) {
		fprintf(stderr, "cudaMemcpy failed!");
		goto Error;
	}

	cudaStatus = cudaMemcpy(dev_b, b, size * sizeof(int), cudaMemcpyHostToDevice);
	if (cudaStatus != cudaSuccess) {
		fprintf(stderr, "cudaMemcpy failed!");
		goto Error;
	}

	// Launch a kernel on the GPU with one thread for each element.
	addKernel << <1, size >> > (dev_c, dev_a, dev_b);

	// Check for any errors launching the kernel
	cudaStatus = cudaGetLastError();
	if (cudaStatus != cudaSuccess) {
		fprintf(stderr, "addKernel launch failed: %s\n", cudaGetErrorString(cudaStatus));
		goto Error;
	}

	// cudaDeviceSynchronize waits for the kernel to finish, and returns
	// any errors encountered during the launch.
	cudaStatus = cudaDeviceSynchronize();
	if (cudaStatus != cudaSuccess) {
		fprintf(stderr, "cudaDeviceSynchronize returned error code %d after launching addKernel!\n", cudaStatus);
		goto Error;
	}

	// Copy output vector from GPU buffer to host memory.
	cudaStatus = cudaMemcpy(c, dev_c, size * sizeof(int), cudaMemcpyDeviceToHost);
	if (cudaStatus != cudaSuccess) {
		fprintf(stderr, "cudaMemcpy failed!");
		goto Error;
	}

Error:
	cudaFree(dev_c);
	cudaFree(dev_a);
	cudaFree(dev_b);

	return cudaStatus;
}

程式首先定義了一個函式addWithCuda,它是呼叫GPU運算的入口函式,返回型別是cudaError_t。

cudaError_t是一個列舉型別,可以作為幾乎所有CUDA函式的返回型別,用來檢測函式執行期間發生的不同型別的錯誤,一共有80多個錯誤型別,可以在driver_types.h標頭檔案中檢視每一個整型對應的錯誤型別,如果返回0,代表執行成功。

第二個函式addKernel在最前有一個修飾符“__global__”,這個修飾符告訴編譯器,被修飾的函式應該編譯為在GPU而不是在CPU上執行,所以這個函式將被交給編譯裝置程式碼的編譯器——NVCC編譯器來處理,其他普通的函式或語句將交給主機編譯器處理。

這裡“裝置”的概念可以理解為GPU和其視訊記憶體組成的運算單元,“主機”可以理解為CPU和系統記憶體組成的運算單元。在GPU上執行的函式稱為核函式。

addKernel函式定義:

__global__ void addKernel(int *c, const int *a, const int *b)
{
	int i = threadIdx.x;
	c[i] = a[i] + b[i];
}

這個核函式裡有一個陌生的threadIdx.x,表示的是thread在x方向上的索引號,理解這個之前得先了解一下GPU執行緒的層次結構:


CUDA中的執行緒(thread)是裝置中並行運算結構中的最小單位,類似於主機中的執行緒的概念,thread可以以一維、二維、三維的形式組織在一起,threadIdx.x表示的是thread在x方向的索引號,還可能存在thread在y和z方向的索引號threadIdx.y和threadIdx.z。

一維、二維或三維的thread組成一個執行緒塊(Block),一維、二維或三維的執行緒塊(Block)組合成一個執行緒塊網格(Grid),執行緒塊網格(Grid)可以是一維或二維的。通過網格塊(Grid)->執行緒塊(Block)->執行緒(thread)的 順序可以定位到每一個並且唯一的執行緒

回到程式中的addKernel函式上來,這個函式會被GPU上的多個執行緒同時執行一次,執行緒間彼此沒有通訊,相互獨立。到底會有多少個執行緒來分別執行核函式,是在“<<<>>>”符號裡定義的。“<<<>>>”表示執行時配置符號,在本程式中的定義是<<<1,size>>>,表示分配了一個執行緒塊(Block),每個執行緒塊有分配了size個執行緒“<<<>>>”中的 引數並不是傳遞給裝置程式碼的引數,而是定義主機程式碼執行時如何啟動裝置程式碼。以上定義的這些執行緒都是一個維度上的,可以通過thredaIdx.x來獲取執行當前計算任務的執行緒的ID號。

cudaSetDevice函式用來設定要在哪個GPU上執行,如果只有一個GPU,設定為cudaSetDevice(0);

cudaMalloc函式用來為參與運算的資料分配視訊記憶體空間,函式原型:cudaError_t cudaMalloc(void **p, size_t s);

cudaMemcpy函式用於主機記憶體和裝置視訊記憶體以及主機與主機之間,裝置與裝置之間相互拷貝資料,函式原型:

cudaError_t CUDARTAPI cudaMemcpy(void *dst, const void *src, size_t count, enum cudaMemcpyKind kind);

第一個引數dst是目標資料地址,第二個引數src是源資料地址,第三個引數count是資料大小,第四個引數kind定義資料拷貝的型別,有如下幾類列舉型別:

/**
 * CUDA memory copy types
 */
enum __device_builtin__ cudaMemcpyKind
{
    cudaMemcpyHostToHost          =   0,      /**< Host   -> Host */
    cudaMemcpyHostToDevice        =   1,      /**< Host   -> Device */
    cudaMemcpyDeviceToHost        =   2,      /**< Device -> Host */
    cudaMemcpyDeviceToDevice      =   3,      /**< Device -> Device */
    cudaMemcpyDefault             =   4       /**< Direction of the transfer is inferred from the pointer values. Requires unified virtual addressing */
};

接下來在呼叫核函式時候添加了執行時配置符號“<<<>>>”,定義執行緒塊和執行緒的數量,如<<<1,5>>>表示定義了一個執行緒塊,每個執行緒塊包含了5個執行緒。

cudaGetLastError函式用於返回最新的一個執行時呼叫錯誤,對於任何CUDA錯誤,都可以通過函式cudaGetErrorString函式來獲取錯誤的詳細資訊。

cudaDeviceSynchronize函式提供了一個阻塞,用於等待所有的執行緒都執行完各自的計算任務,然後繼續往下執行。

cudaFree函式用於釋放申請的視訊記憶體空間。

cudaDeviceReset函式用於釋放所有申請的視訊記憶體空間和重置裝置狀態;

第一個CUDA程式kernel.cu涉及的內容主要就是這些。CUDA的使用步驟如下:

  1. 主機程式碼執行
  2. 傳輸資料給GPU
  3. 確定Grid、Block大小
  4. 呼叫核心函式,GPU多執行緒執行程式
  5. 傳輸運算結果給CPU
  6. 繼續主機程式碼執行

期間涉及到在裝置上的一些視訊記憶體空間申請、銷燬等操作,從記憶體到視訊記憶體上資料的相互拷貝是一個比較耗時的過程,應該儘量減少這種操作。