CUBLAS中的矩陣乘法函數詳解

關於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、B並不等於 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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章