前言
本文將介紹 CUDA 編程的基本模式,所有 CUDA 程序都基於此模式編寫,即使是調用庫,庫的底層也是這個模式實現的。
模式描述
1. 定義需要在 device 端執行的核函數。( 函數聲明前加 _golbal_ 關鍵字 )
2. 在顯存中為待運算的數據以及需要存放結果的變量開辟顯存空間。( cudaMalloc 函數實現 )
3. 將待運算的數據傳輸進顯存。( cudaMemcpy,cublasSetVector 等函數實現 )
4. 調用 device 端函數,同時要將需要為 device 端函數創建的塊數線程數等參數傳遞進 <<<>>>。( 注: <<<>>>下方編譯器可能顯示語法錯誤,不用管 )
5. 從顯存中獲取結果變量。( cudaMemcpy,cublasGetVector 等函數實現 )
6. 釋放申請的顯存空間。( cudaFree 實現 )
PS:每個 device 端函數在被調用時都能獲取到調用它的具體塊號,線程號,從而實現並行( 獲取方法請參考下面的編程規范說明以及代碼示例 )。
編程規范說明
在 CUDA 標准編程模式中,增加了一些編程規范,在這里簡要說明:
函數聲明關鍵字:
1. __device__
表明此函數只能在 GPU 中被調用,在 GPU 中執行。這類函數只能被 __global__ 類型函數或 __device__ 類型函數調用。
2. __global__
表明此函數在 CPU 上調用,在 GPU 中執行。這也是以后會常提到的 "內核函數",有時為了便於理解也稱 "device" 端函數。
3. __host__
表明此函數在 CPU 上調用和執行,這也是默認情況。
內核函數配置運算符 <<<>>> - 這個運算符在調用內核函數的時候使用,一般情況下傳遞進三個參數:
1. 塊數
2. 線程數
3. 共享內存大小 (此參數默認為0 )
內核函數中的幾個系統變量 - 這幾個變量可以在內核函數中使用,從而控制塊與線程的工作:
1. gridDim:塊數
2. blockDim:塊中線程數
3. blockIdx:塊編號 (0 - gridDim-1)
4. threadIdx:線程編號 (0 - blockDim-1)
知道這些已經足夠編寫 CUDA 程序了,更多的編程說明將在以后的文章中介紹。
代碼示例
該程序采用 CUDA 並行化思想來對數組進行求和 (代碼下方如果出現紅色波浪線無視之):
1 // 相關 CUDA 庫 2 #include "cuda_runtime.h" 3 #include "cuda.h" 4 #include "device_launch_parameters.h" 5 6 #include <iostream> 7 #include <cstdlib> 8 9 using namespace std; 10 11 const int N = 100; 12 13 // 塊數 14 const int BLOCK_data = 3; 15 // 各塊中的線程數 16 const int THREAD_data = 10; 17 18 // CUDA初始化函數 19 bool InitCUDA() 20 { 21 int deviceCount; 22 23 // 獲取顯示設備數 24 cudaGetDeviceCount (&deviceCount); 25 26 if (deviceCount == 0) 27 { 28 cout << "找不到設備" << endl; 29 return EXIT_FAILURE; 30 } 31 32 int i; 33 for (i=0; i<deviceCount; i++) 34 { 35 cudaDeviceProp prop; 36 if (cudaGetDeviceProperties(&prop,i)==cudaSuccess) // 獲取設備屬性 37 { 38 if (prop.major>=1) //cuda計算能力 39 { 40 break; 41 } 42 } 43 } 44 45 if (i==deviceCount) 46 { 47 cout << "找不到支持 CUDA 計算的設備" << endl; 48 return EXIT_FAILURE; 49 } 50 51 cudaSetDevice(i); // 選定使用的顯示設備 52 53 return EXIT_SUCCESS; 54 } 55 56 // 此函數在主機端調用,設備端執行。 57 __global__ 58 static void Sum (int *data,int *result) 59 { 60 // 取得線程號 61 const int tid = threadIdx.x; 62 // 獲得塊號 63 const int bid = blockIdx.x; 64 65 int sum = 0; 66 67 // 有點像網格計算的思路 68 for (int i=bid*THREAD_data+tid; i<N; i+=BLOCK_data*THREAD_data) 69 { 70 sum += data[i]; 71 } 72 73 // result 數組存放各個線程的計算結果 74 result[bid*THREAD_data+tid] = sum; 75 } 76 77 int main () 78 { 79 // 初始化 CUDA 編譯環境 80 if (InitCUDA()) { 81 return EXIT_FAILURE; 82 } 83 cout << "成功建立 CUDA 計算環境" << endl << endl; 84 85 // 建立,初始化,打印測試數組 86 int *data = new int [N]; 87 cout << "測試矩陣: " << endl; 88 for (int i=0; i<N; i++) 89 { 90 data[i] = rand()%10; 91 cout << data[i] << " "; 92 if ((i+1)%10 == 0) cout << endl; 93 } 94 cout << endl; 95 96 int *gpudata, *result; 97 98 // 在顯存中為計算對象開辟空間 99 cudaMalloc ((void**)&gpudata, sizeof(int)*N); 100 // 在顯存中為結果對象開辟空間 101 cudaMalloc ((void**)&result, sizeof(int)*BLOCK_data*THREAD_data); 102 103 // 將數組數據傳輸進顯存 104 cudaMemcpy (gpudata, data, sizeof(int)*N, cudaMemcpyHostToDevice); 105 // 調用 kernel 函數 - 此函數可以根據顯存地址以及自身的塊號,線程號處理數據。 106 Sum<<<BLOCK_data,THREAD_data,0>>> (gpudata,result); 107 108 // 在內存中為計算對象開辟空間 109 int *sumArray = new int[THREAD_data*BLOCK_data]; 110 // 從顯存獲取處理的結果 111 cudaMemcpy (sumArray, result, sizeof(int)*THREAD_data*BLOCK_data, cudaMemcpyDeviceToHost); 112 113 // 釋放顯存 114 cudaFree (gpudata); 115 cudaFree (result); 116 117 // 計算 GPU 每個線程計算出來和的總和 118 int final_sum=0; 119 for (int i=0; i<THREAD_data*BLOCK_data; i++) 120 { 121 final_sum += sumArray[i]; 122 } 123 124 cout << "GPU 求和結果為: " << final_sum << endl; 125 126 // 使用 CPU 對矩陣進行求和並將結果對照 127 final_sum = 0; 128 for (int i=0; i<N; i++) 129 { 130 final_sum += data[i]; 131 } 132 cout << "CPU 求和結果為: " << final_sum << endl; 133 134 getchar(); 135 136 return 0; 137 }
運行測試
PS:矩陣元素是隨機生成的
小結
1. 掌握本節知識的關鍵除了要掌握各個API,還要深刻理解內核函數中的塊及線程變量的控制,或者說施展 :)
2. 一定要明確傳遞進 API 的是參數本身,還是參數的地址,這很關鍵。