前言
本文將介紹 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 的是引數本身,還是引數的地址,這很關鍵。