關於cuBLAS庫中矩陣乘法相關的函數及其輸入輸出進行詳細討論。
▶ 漲姿勢:
● cuBLAS中能用於運算矩陣乘法的函數有4個,分別是 cublasSgemm(單精度實數)、cublasDgemm(雙精度實數)、cublasCgemm(單精度複數)、cublasZgemm(雙精度複數),它們的定義(在 cublas_v2.h 和 cublas_api.h 中)如下。
1 #define cublasSgemm cublasSgemm_v2 2 CUBLASAPI cublasStatus_t CUBLASWINAPI cublasSgemm_v2 3 ( 4 cublasHandle_t handle, 5 cublasOperation_t transa, cublasOperation_t transb, 6 int m, int n, int k, 7 const float *alpha, 8 const float *A, int lda, 9 const float *B, int ldb, 10 const float *beta, 11 float *C, int ldc 12 ); 13 14 #define cublasDgemm cublasDgemm_v2 15 CUBLASAPI cublasStatus_t CUBLASWINAPI cublasDgemm_v2 16 ( 17 cublasHandle_t handle, 18 cublasOperation_t transa, cublasOperation_t transb, 19 int m, int n, int k, 20 const double *alpha, 21 const double *A, int lda, 22 const double *B, int ldb, 23 const double *beta, 24 double *C, int ldc 25 ); 26 27 #define cublasCgemm cublasCgemm_v2 28 CUBLASAPI cublasStatus_t CUBLASWINAPI cublasCgemm_v2 29 ( 30 cublasHandle_t handle, 31 cublasOperation_t transa, cublasOperation_t transb, 32 int m, int n, int k, 33 const cuComplex *alpha, 34 const cuComplex *A, int lda, 35 const cuComplex *B, int ldb, 36 const cuComplex *beta, 37 cuComplex *C, int ldc 38 ); 39 40 #define cublasZgemm cublasZgemm_v2 41 CUBLASAPI cublasStatus_t CUBLASWINAPI cublasZgemm_v2 42 ( 43 cublasHandle_t handle, 44 cublasOperation_t transa, cublasOperation_t transb, 45 int m, int n, int k, 46 const cuDoubleComplex *alpha, 47 const cuDoubleComplex *A, int lda, 48 const cuDoubleComplex *B, int ldb, 49 const cuDoubleComplex *beta, 50 cuDoubleComplex *C, 51 int ldc 52 );
● 四個函數形式相似,均輸入了14個參數。該函數實際上是用於計算 C = α A B +β C 的,其中 Am×k 、Bk×n 、Cm×n 爲矩陣,α 、β 爲標量。這裏先籠統的描述每個參數的意義,再通過後面的分點逐一詳細說明。
cublasHandle_t handle 調用cuBLAS庫時的句柄。
cublasOperation_t transa 是否轉置矩陣A
cublasOperation_t transb 是否轉置矩陣B
int m 矩陣A的行數,等於矩陣C的行數;不再單獨說明
int n 矩陣B的列數,等於矩陣C的列數;不再單獨說明
int k 矩陣A的列數,等於矩陣B的行數;不再單獨說明
const float *alpha 標量 α 的指針,可以是主機指針或設備指針,只需要計算矩陣乘法時命 α = 1.0f;不再單獨說明
const float *A 矩陣(數組)A 的指針,必須是設備指針;不再單獨說明
int lda 矩陣 A 的主維(leading dimension)
const float *B 矩陣(數組)B 的指針,必須是設備指針;不再單獨說明
int ldb 矩陣 B 的主維
const float *beta 標量 β 的指針,可以是主機指針或設備指針,只需要計算矩陣乘法時命 β = 0.0f;不再單獨說明
float *C 矩陣(數組)C 的指針,必須是設備指針;不再單獨說明
int ldc 矩陣 C 的主維
這裏我們採用測試代碼(完整的代碼在本文末尾)中的例子加以說明。其中維度常數 m = 2, n = 4, k = 3。
① cublasDataType_t handle
一個有關cuBLAS庫的上下文的句柄,之後需要傳遞給API函數,即計算乘法的函數, 簡單說就是以下過程。
1 ...// 準備 A, B, C 以及使用的線程網格、線程塊的尺寸 2 3 // 創建句柄 4 cublasHandle_t handle; 5 cublasCreate(&handle); 6 7 // 調用計算函數 8 cublasSgemm(handle, CUBLAS_OP_N, CUBLAS_OP_N, m, n, k, &alpha, *B, n, *A, k, &beta, *C, n); 9 10 // 銷燬句柄 11 cublasDestroy(handle); 12 13 ...// 回收計算結果,順序可以和銷燬句柄交換
另外,創建句柄的函數 cublasCreate() 會返回一個 cublasStatus_t 類型的值,用來判斷句柄是否創建成功,該值在 cublas_api.h 中定義如下,可見只有其等於0的時候才能認爲創建成功。
1 typedef enum{ 2 CUBLAS_STATUS_SUCCESS =0, 3 CUBLAS_STATUS_NOT_INITIALIZED =1, 4 CUBLAS_STATUS_ALLOC_FAILED =3, 5 CUBLAS_STATUS_INVALID_VALUE =7, 6 CUBLAS_STATUS_ARCH_MISMATCH =8, 7 CUBLAS_STATUS_MAPPING_ERROR =11, 8 CUBLAS_STATUS_EXECUTION_FAILED=13, 9 CUBLAS_STATUS_INTERNAL_ERROR =14, 10 CUBLAS_STATUS_NOT_SUPPORTED =15, 11 CUBLAS_STATUS_LICENSE_ERROR =16 12 } cublasStatus_t;
② cublasOperation_t transa, cublasOperation_t transb
兩個“是否需要對輸入矩陣 A、B 進行轉置”的參數,這是 cuBLAS 庫難點之一。簡單地說,cuBLAS 中關於矩陣的存儲方式與 fortran、MATLAB類似,採用的是列優先,而非 C / C++ 中的行優先。
所以,當我們將 C / C++ 中行優先形式保存的數組 A 輸入到cuBLAS中時,會被cuBLAS理解爲列優先存儲。這時如果保持 A 的行、列數不變,則矩陣 A 會發生重排(過程類似 MATLAB 中的 reshape(A, [size(A,2), size(A, 1)])),除非同時交換 A 的行、列數,此時結果才恰好等於 A 的轉置,在一般的調用過程中正是利用了這一條性質。於是 cuBLAS 事先利用這個參數詢問,是否需要將矩陣 A、B 進行轉置。在這裏,我嘗試了大量的例子,結合圖形來說明cuBLAS中對數組的操作。
該參數轉置有以下三種,CUBLAS_OP_N 表示不轉置,CUBLAS_OP_T 表示需要普通轉置,CUBLAS_OP_C 表示需要共軛轉置。例子中僅涉及單精度實數運算,採用 CUBLAS_OP_N 或者 CUBLAS_OP_T 。
1 typedef enum { 2 CUBLAS_OP_N = 0, 3 CUBLAS_OP_T = 1, 4 CUBLAS_OP_C = 2 5 } cublasOperation_t;
1° (錯誤示範)直接將A、B放入函數中,轉置參數均取爲 CUBLAS_OP_N(lda、ldb、ldc 正常地取作各矩陣的行數,後面會解釋其作用,到時候再回過頭來看就好)。
1 cublasSgemm(handle, CUBLAS_OP_N, CUBLAS_OP_N, m, n, k, &alpha, *A, m, *B, k, &beta, *C, m);
過程和結果如下。cuBLAS 自動將輸入的 A、B 理解爲列優先存儲,而且保持 A、B 的行、列數保持不變,即將其當作 A1 和 B1。注意這裏的 A1、B1 並不等於 A、B 的轉置。計算乘積 C1 作爲輸出,最後我們在主機中將輸出C1看作是行優先的矩陣,就成了C2的模樣。顯然這樣計算的結果顯然不是我們期望的 C 。
2° (正確過程)這一般教程上的調用過程。利用了性質 A B = (BT AT)T 來計算矩陣乘積。如前文所述,我們把一個行優先的矩陣看作列優先的同時,交換其行、列數,其結果等價於得到該矩陣的轉置,反之列優先轉行優先的原理相同。所以我們可以在調用該函數的時候,先放入 B 並交換參數 k 和 n 的位置,再放入 A 並交換參數 m 和 k 的位置,這樣就順理成章得到了結果的 C (所有轉換由cuBLAS完成,不需要手工調整數組),注意以下調用語句中紅色的部分。
1 cublasSgemm(handle, CUBLAS_OP_N, CUBLAS_OP_N, n, m, k, &alpha, *B, n, *A, k, &beta, *C, n);
過程和結果如下。cuBLAS讀取 B 和 A 的時候雖然進行了重排,但是
2.5° (嘗試手工轉置,爲 3° 作鋪墊)如果我們並不想運用 2° 中的奇技淫巧,而是希望按順序將 A、B 輸入 cuBLAS 中運算。我們可以先嚐試手工將 A、B 轉化爲列優先存,儲然後再調用該函數,最後將輸出的 C 轉化回行優先即可。轉化過程非常簡單,如以下程序所示。
1 // 行優先轉列優先,只改變存儲方式,不改變矩陣尺寸 2 void rToC(float *a, int ha, int wa)// 輸入數組及其行數、列數 3 { 4 int i; 5 float *temp = (float *)malloc(sizeof(float) * ha * wa);// 存放列優先存儲的臨時數組 6 7 for (i = 0; i < ha * wa; i++) // 找出列優先的第 i 個位置對應行優先座標進行賦值 8 temp[i] = a[i / ha + i % ha * wa]; 9 for (i = 0; i < ha * wa; i++) // 覆蓋原數組 10 a[i] = temp[i]; 11 free(temp); 12 return; 13 } 14 15 // 列優先轉行優先 16 void cToR(float *a, int ha, int wa) 17 { 18 int i; 19 float *temp = (float *)malloc(sizeof(float) * ha * wa); 20 21 for (i = 0; i < ha * wa; i++) // 找出行優先的第 i 個位置對應列優先座標進行賦值 22 temp[i] = a[i / wa + i % wa * ha]; 23 for (i = 0; i < ha * wa; i++) 24 a[i] = temp[i]; 25 free(temp); 26 return; 27 }
這樣一來,我們的調用過程如下(跟 1° 中參數位置一模一樣)注意上面的轉換函數中我沒有交換矩陣 a 的行數、列數,所以在下面的調用中仍然有 m = 2, n = 4, k = 3。
1 rToC(h_A, m, k); 2 rToC(h_B, k, n); 3 4 ...// 準備工作 5 6 cublasSgemm(handle, CUBLAS_OP_N, CUBLAS_OP_N, m, n, k, &alpha, d_A, m, d_B, k, &beta, d_C, m); 7 8 ...// 回收工作 9 10 cToR(h_CUBLAS, m, n);
過程和結果如下。注意經過 rToC處理得到的矩陣,以 A1爲例,它相當於用列優先的方式(豎着走)把 A 遍歷得到的軌跡按行優先的方式存放(橫着放)。可見矩陣 A、B、C 一波三折,但是最終獲得了我們期望的結果(圖中的“列優先”和“行優先”分別代表進入和退出 cuBLAS 時發生的強制轉化,不要理解爲其他意思)。
3° (使用 cuBLAS 自動轉置)有了2.5 ° 的基礎,我們就很好理解參數 cublasOperation_t transa, cublasOperation_t transb 的作用了。這兩個參數就是在問,是否要在 cuBLAS 內部完成上面的 rToC過程。如果我們選擇參數 CUBLAS_OP_T,那麼不再需要手動地將 A、B 進行轉換(但是仍需要對輸出 C 進行轉換),我們直接上代碼。
1 cublasSgemm(handle, CUBLAS_OP_T, CUBLAS_OP_T, m, n, k, &alpha, d_A, k, d_B, n, &beta, d_C, m); 2 3 ...// 回收工作 4 5 cToR(h_CUBLAS, matrix_size.uiHC, matrix_size.uiWC);
過程和結果如下。我們發現主維數 lda、ldb 已經用上了,它們分別等於 A2、B2 的行數(在列優先存儲中矩陣的行數是維度的第一個分量,故稱主維)。其實圖中可以省略 “→ A2” 這一中間步驟,但是爲了突出“轉置”的意味,我們把它畫出來,再經過CUBLAS_OP_T 處理,方便理解。最終我們獲得了期望的結果 C3 ,我們發現這種方式省去了對 A、B 的 rToC 操作,但是 C2 的 cToR 操作還是逃不掉,沒有 2° 來的巧妙。
4° 有了以上的說明,我們就可以對A、B的不同情況再加以利用,得到下面兩種組合,他們都能得到正確的結果。過程圖示就是 2.5° 和 3° 的圖進行組合,其餘不變。
1 // A轉B不轉 2 //rToC(h_A, matrix_size.uiHA,matrix_size.uiWA); 3 rToC(h_B, matrix_size.uiHB, matrix_size.uiWB); 4 5 ... 6 7 cublasSgemm(handle, CUBLAS_OP_T, CUBLAS_OP_N, m, n, k, &alpha, d_A, k, d_B, k, &beta, d_C, m); 8 9 ... 10 11 cToR(h_CUBLAS, matrix_size.uiHC, matrix_size.uiWC);
1 // B轉A不轉 2 rToC(h_A, matrix_size.uiHA,matrix_size.uiWA); 3 //rToC(h_B, matrix_size.uiHB, matrix_size.uiWB); 4 5 ... 6 7 cublasSgemm(handle, CUBLAS_OP_N, CUBLAS_OP_T, m, n, k, &alpha, d_A, m, d_B, n, &beta, d_C, m); 8 9 ... 10 11 cToR(h_CUBLAS, matrix_size.uiHC, matrix_size.uiWC);
③ int lda, int ldb, int ldc
主維(leading dimension)。如果我們想要計算 Am×k Bk×n = Cm×n,那 m、n、k 三個參數已經固定了所有尺寸,爲什麼還要一組主維參數呢?看完了上面部分我們發現,輸入的矩陣 A、B 在整個計算過程中會發生變形,包括行列優先變換和轉置變換,所以需要一組參數告訴該函數,矩陣變形後應該具有什麼樣的尺寸。參考 CUDA 的教程 CUDA Toolkit Documentation v9.0.176,對這幾個參數的說明比較到位。
當參數 transa 爲 CUBLAS_OP_N 時,矩陣 A 在 cuBLAS 中的尺寸實際爲 lda × k,此時要求 lda ≥ max(1, m)(否則由該函數直接報錯,輸出全零的結果);當參數 transa 爲 CUBLAS_OP_T 或 CUBLAS_OP_C 時,矩陣 A 在 cuBLAS 中的尺寸實際爲 lda × m,此時要求 lda ≥ max(1, k) 。
transb 爲 CUBLAS_OP_N 時,B 尺寸爲 ldb × n,要求 ldb ≥ max(1, k); transb 爲 CUBLAS_OP_T 或 CUBLAS_OP_C 時,B尺寸爲 ldb × k,此時要求 ldb ≥ max(1, n) 。
C 尺寸爲 ldc × n,要求 ldc ≥ max(1, m)。
可見,是否需要該函數幫我們轉置矩陣 A 會影響 A 在函數中的存儲。而主維正是在這一過程中起作用。特別地,如果按照一般教程中的方法來調用該函數,三個主維參數是不用特別處理的。
上面的教程中只要求了三個主維參數的下界,爲此,我嘗試調高三個參數,獲得了一些有啓發性的結果。
Ⅰ.ldb + 1
1 cublasSgemm(handle, CUBLAS_OP_N, CUBLAS_OP_N, n, m, k, &alpha, d_B, n + 1, d_A, k, &beta, d_C, n);
結果和過程。注意 B1 中多出來的部分被用零來填充了,計算 C1 的時候是依其尺寸來進行的,若 C1 的尺寸上沒有加 1,則最後一行不進行計算。
Ⅱ. lda + 1
1 cublasSgemm(handle, CUBLAS_OP_N, CUBLAS_OP_N, n, m, k, &alpha, d_B, n, d_A, k + 1, &beta, d_C, n);
結果和過程。注意 A1 在參與乘法時去掉了多出的行(其實應該是計算乘法時行程長 = min (B1 列數,A1 行數) = 3)。
Ⅲ. ldc + 1
1 cublasSgemm(handle, CUBLAS_OP_N, CUBLAS_OP_N, n, m, k, &alpha, d_B, n, d_A, k, &beta, d_C, n + 1);
結果和過程。注意計算出來的 C1 自動補充了零行,但是在輸出的時候連着零一起輸出,並且把最後的兩個元素擠掉了。
▶ 測試用源代碼:
1 #include <assert.h> 2 #include <helper_string.h> 3 #include <cuda_runtime.h> 4 #include <cublas_v2.h> 5 #include <helper_functions.h> 6 #include <helper_cuda.h> 7 8 // 存放各矩陣維數的結構體 9 typedef struct 10 { 11 unsigned int wa, ha, wb, hb, wc, hc; 12 } matrixSize; 13 14 // 行優先轉列優先,只改變存儲方式,不改變矩陣尺寸 15 void rToC(float *a, int ha, int wa) 16 { 17 int i; 18 float *temp = (float *)malloc(sizeof(float) * ha * wa);// 存放列優先存儲的臨時數組 19 20 for (i = 0; i < ha * wa; i++) // 找出列優先的第 i 個位置對應行優先位置進行賦值 21 temp[i] = a[i / ha + i % ha * wa]; 22 for (i = 0; i < ha * wa; i++) // 覆蓋原數組 23 a[i] = temp[i]; 24 free(temp); 25 return; 26 } 27 28 // 列優先轉行優先 29 void cToR(float *a, int ha, int wa) 30 { 31 int i; 32 float *temp = (float *)malloc(sizeof(float) * ha * wa); 33 34 for (i = 0; i < ha * wa; i++) // 找出列優先的第 i 個位置對應行優先位置進行賦值 35 temp[i] = a[i / wa + i % wa * ha]; 36 for (i = 0; i < ha * wa; i++) 37 a[i] = temp[i]; 38 free(temp); 39 return; 40 } 41 42 // 計算矩陣乘法部分 43 int matrixMultiply() 44 { 45 matrixSize ms; 46 ms.wa = 3; 47 ms.ha = 2; 48 ms.wb = 4; 49 ms.hb = ms.wa; 50 ms.wc = ms.wb; 51 ms.hc = ms.ha; 52 53 unsigned int size_A = ms.wa * ms.ha; 54 unsigned int mem_size_A = sizeof(float) * size_A; 55 float *h_A = (float *)malloc(mem_size_A); 56 unsigned int size_B = ms.wb * ms.hb; 57 unsigned int mem_size_B = sizeof(float) * size_B; 58 float *h_B = (float *)malloc(mem_size_B); 59 60 for (int i = 0; i < ms.ha*ms.wa; i++) 61 h_A[i] = i + 1; 62 for (int i = 0; i < ms.hb*ms.wb; i++) 63 h_B[i] = i + 1; 64 65 // 轉換函數,按需取用 66 //rToC(h_A, ms.ha,ms.wa); 67 //rToC(h_B, ms.hb, ms.wb); 68 69 float *d_A, *d_B, *d_C; 70 unsigned int size_C = ms.wc * ms.hc; 71 unsigned int mem_size_C = sizeof(float) * size_C; 72 float *h_CUBLAS = (float *) malloc(mem_size_C); 73 cudaMalloc((void **) &d_A, mem_size_A); 74 cudaMalloc((void **) &d_B, mem_size_B); 75 cudaMemcpy(d_A, h_A, mem_size_A, cudaMemcpyHostToDevice); 76 cudaMemcpy(d_B, h_B, mem_size_B, cudaMemcpyHostToDevice); 77 cudaMalloc((void **) &d_C, mem_size_C); 78 79 dim3 threads(1,1); 80 dim3 grid(1,1); 81 82 //cuBLAS代碼 83 const float alpha = 1.0f; 84 const float beta = 0.0f; 85 int m = ms.ha, n = ms.wb, k = ms.wa; 86 87 cublasHandle_t handle; 88 cublasCreate(&handle); 89 90 cublasSgemm(handle, CUBLAS_OP_N, CUBLAS_OP_N, n, m, k, &alpha, d_B, n, d_A, k, &beta, d_C, n); 91 92 cublasDestroy(handle); 93 94 cudaMemcpy(h_CUBLAS, d_C, mem_size_C, cudaMemcpyDeviceToHost); 95 96 // 轉換函數,按需取用 97 //cToR(h_CUBLAS, ms.hc, ms.wc); 98 99 printf("\nResult C=\n"); 100 for (int i = 0; i < ms.hc*ms.wc; i++) 101 { 102 printf("%3.2f\t", h_CUBLAS[i]); 103 if ((i + 1) % ms.wc == 0) 104 printf("\n"); 105 } 106 107 free(h_A); 108 free(h_B); 109 free(h_CUBLAS); 110 cudaFree(d_A); 111 cudaFree(d_B); 112 cudaFree(d_C); 113 return 0; 114 } 115 116 int main() 117 { 118 matrixMultiply(); 119 getchar(); 120 return 0; 121 }
▶ 輸出結果:
Result C=
38.00 44.00 50.00 56.00
83.00 98.00 113.00 128.00