【程序分析】cublasSgemm 矩陣乘法詳解

目錄

前言

預備知識

cublasSgemm 函數

求解C=AxB

不使用cublasSgemm transa與transb參數

示例程序

使用cublasSgemm transa與transb參數

示例程序

 


前言

cublasSgemm是NV cublas庫的矩陣相乘API,由於cublas中矩陣的存儲是列優先,所以cublasSgemm API的參數讓新手一頭霧水,經過仔細研究和實踐後總結爲本文,便於後來者參考

 

 

預備知識

行優先還是列優先

矩陣存儲示意

矩陣在邏輯上表達爲2維M行K列,但存儲到內存的時候都是按一維佈局,其中按行優先存儲和按列優先存儲的差異如上圖所示

 

行優先存儲到列優先讀取對矩陣的變化影響

如上圖所示,當矩陣按行優先存儲然後又按相反的列優先讀取的話,就會得到元矩陣轉置的結果;同理適用於按列優先存儲然後按行優先讀取。

 

cublasSgemm 函數

cublasStatus_t cublasSgemm(cublasHandle_t handle,
                           cublasOperation_t transa, cublasOperation_t transb,
                           int m, int n, int k,
                           const float           *alpha,
                           const float           *A, int lda,
                           const float           *B, int ldb,
                           const float           *beta,
                           float           *C, int ldc)

  cublasSgemm的官方API說明文檔鏈接 https://docs.nvidia.com/cuda/cublas/index.html

  •   根據文檔說可以知道,cublasSgemm完成了 C = alpha * op ( A ) * op ( B ) + beta * C 的矩陣乘加運算
  •   其中alpha和beta是標量, A B C是以列優先存儲的矩陣
  •   如果 transa的參數是CUBLAS_OP_N 則op(A) = A ,如果是CUBLAS_OP_T 則op(A)=A的轉置
  •   如果 transb的參數是CUBLAS_OP_N 則op(B) = B ,如果是CUBLAS_OP_T 則op(B)=B的轉置

由於API中的矩陣參數也用A B C表示,爲了不和下面例子中的矩陣A B混淆,我們將cublasSgemm中的參數做如下的調整

  • A稱爲乘法左矩陣
  • B稱爲乘法右矩陣
  • C稱爲結果矩陣

所以當alpha =1 並且 beta =0 的時候 cublasSgemm完成了計算: 結果矩陣= op (乘法左矩陣) * op ( 乘法右矩陣) 

 

求解C=AxB

其中(A爲M行K列 B爲K行N列 所以 C爲M行N列)

不使用cublasSgemm transa與transb參數

由於C/C++程序中輸入的A和B是按行存儲,所以在的情況下,cublas其實讀取到的是A和B的轉置矩陣AT和BT

根據線性代數的規則可知CT = (A x B)T = BT x AT 所以cublasSgemm API中幾個參數設置如下

  • 設置了cublasSgemm的transa與transb參數=CUBLAS_OP_N
  • 乘法左矩陣爲BT=參數設置爲B,乘法右矩陣爲AT=參數設置爲A
  • 結果矩陣的行數爲CT的行數=參數設置爲N
  • 結果矩陣的列數爲CT的列數=參數設置爲M
  • 乘法左矩陣列與乘法右矩陣的行=參數設置爲K
  • 按列優先讀取乘法左矩陣B的主維度(即BT有幾行)=參數設置爲N
  • 按列優先讀取乘法右矩陣A的主維度(即AT有幾行)=參數設置爲K
  • 結果矩陣存儲在參數C中,它的主維度(即有幾行)= 參數設置爲N

cublasSgemm(handle,CUBLAS_OP_N,CUBLAS_OP_N, N, M, K,&alpha,d_b, N, d_a, K,&beta, d_c, N)

 

按上面的參數調用cublasSgemm API (矩陣A按行存儲在指針d_a, 矩陣B按行存儲在指針d_b, 矩陣C的存儲空間指針d_c) 最後從結果矩陣的存儲空間d_c中按行讀取到的就是C=AxB的結果,整個cublasSgemm的計算過程如下圖所示

不使用transa與transb參數的情況下 cublasSgemm 求解矩陣乘法的過程

示例程序

#include <stdio.h>
#include <assert.h>
#include <fstream>
#include <sstream>
#include <iostream>
#include <cmath>
#include <sys/stat.h>
#include <cmath>
#include <time.h>
#include <cuda_runtime_api.h>
#include <cudnn.h>
#include <cublas_v2.h>
#include <memory>
#include <string.h>
#include <cstdint>

#define M 2
#define N 4
#define K 3

void printMatrix(float (*matrix)[N], int row, int col) {
    for(int i=0;i<row;i++)
    {
        std::cout << std::endl;
        std::cout << " [ ";
        for (int j=0; j<col; j++) {
         std::cout << matrix[i][j] << " ";
        }
        std::cout << " ] ";
    }
    std::cout << std::endl;
}

int main(void)
{
        float alpha=1.0;
        float beta=0.0;
        float h_A[M][K]={ {1,2,3}, {4,5,6} };
        float h_B[K][N]={ {1,2,3,4}, {5,6,7,8}, {9,10,11,12} };
        float h_C[M][N] = {0};
        float *d_a,*d_b,*d_c;
        cudaMalloc((void**)&d_a,M*K*sizeof(float));
        cudaMalloc((void**)&d_b,K*N*sizeof(float));
        cudaMalloc((void**)&d_c,M*N*sizeof(float));
        cudaMemcpy(d_a,&h_A,M*K*sizeof(float),cudaMemcpyHostToDevice);
        cudaMemcpy(d_b,&h_B,K*N*sizeof(float),cudaMemcpyHostToDevice);
        cudaMemset(d_c,0,M*N*sizeof(float));
        cublasHandle_t handle;
        cublasCreate(&handle);
        cublasSgemm(handle,CUBLAS_OP_N,CUBLAS_OP_N, N, M, K,&alpha,d_b, N, d_a, K,&beta, d_c, N);
        cudaMemcpy(h_C,d_c,M*N*sizeof(float),cudaMemcpyDeviceToHost);//此處的h_C是按列存儲的CT
        printMatrix(h_C, M, N);//按行讀取h_C相當於做了CTT=C的結果
        return 0;
}

輸入 A  = \begin{bmatrix} 1&2&3\\ 4& 5&6 \end{bmatrix}  B = \begin{bmatrix} 1& 2& 3& 4\\ 5& 6& 7& 8\\ 9& 10& 11& 12 \end{bmatrix}

運行結果 C = AxB = \begin{bmatrix} 38& 44& 50& 56\\ 83& 98& 113& 128 \end{bmatrix}

示例程序的cublasSgemm計算求解過程

 

使用cublasSgemm transa與transb參數

由於C/C++程序中輸入的A和B是按行存儲,所以在的情況下,cublas其實讀取到的是A和B的轉置矩陣AT和BT

設置了cublasSgemm的transa與transb參數後可以在做矩陣運算前對讀取到的AT和BT矩陣做一次轉置,獲得A和B

根據線性代數的規則可知C = A x B 所以cublasSgemm API中幾個參數設置如下

  • 設置了cublasSgemm的transa與transb參數=CUBLAS_OP_T,在進行矩陣運算前對讀取的矩陣做一次轉置
  • 乘法左矩陣爲A=參數設置爲A,乘法右矩陣爲B=參數設置爲B
  • 結果矩陣的行數爲C的行數=參數設置爲M
  • 結果矩陣的列數爲C的列數=參數設置爲N
  • 乘法左矩陣列與乘法右矩陣的行=參數設置爲K
  • 按列優先讀取乘法左矩陣A的主維度(即AT有幾行)=參數設置爲K
  • 按列優先讀取乘法右矩陣B的主維度(即BT有幾行)=參數設置爲N
  • 結果矩陣存儲在參數C中,它的主維度(即有幾行)= 參數設置爲M

cublasSgemm(handle,CUBLAS_OP_T,CUBLAS_OP_T, M, N, K,&alpha,d_a, K, d_b, N,&beta, d_c, M);

紅色的參數標記出與“不使用cublasSgemm transa與transb參數”例子中的不同,按上面的參數調用cublasSgemm API (矩陣A按行存儲在指針d_a, 矩陣B按行存儲在指針d_b, 矩陣C的存儲空間指針d_c) 最後從結果矩陣的存儲空間d_c中按行讀取到的就是C=AxB後CT的結果,所以在C/C++程序中還需要對讀取的結果CT做一次矩陣轉置操作才能獲得最終正確的C。整個cublasSgemm的計算過程如下圖所示

使用transa與transb參數的情況下 cublasSgemm 求解矩陣乘法的過程

 

示例程序

#include <stdio.h>
#include <assert.h>
#include <fstream>
#include <sstream>
#include <iostream>
#include <cmath>
#include <sys/stat.h>
#include <cmath>
#include <time.h>
#include <cuda_runtime_api.h>
#include <cudnn.h>
#include <cublas_v2.h>
#include <memory>
#include <string.h>
#include <cstdint>

#define M 2
#define N 4
#define K 3

void printMatrix2(float* matrix, int row, int col) {
    for(int i=0;i<row;i++)
    {
        std::cout << std::endl;
        std::cout << " [ ";
        for (int j=0; j<col; j++) {
         std::cout << matrix[i][j] << " ";
        }
        std::cout << " ] ";
    }
    std::cout << std::endl;
}

int main(void)
{
        float alpha=1.0;
        float beta=0.0;
        float h_A[M][K]={ {1,2,3}, {4,5,6} };
        float h_B[K][N]={ {1,2,3,4}, {5,6,7,8}, {9,10,11,12} };
        float h_C[M][N] = {0};
        float *d_a,*d_b,*d_c;
        cudaMalloc((void**)&d_a,M*K*sizeof(float));
        cudaMalloc((void**)&d_b,K*N*sizeof(float));
        cudaMalloc((void**)&d_c,M*N*sizeof(float));
        cudaMemcpy(d_a,&h_A,M*K*sizeof(float),cudaMemcpyHostToDevice);
        cudaMemcpy(d_b,&h_B,K*N*sizeof(float),cudaMemcpyHostToDevice);
        cudaMemset(d_c,0,M*N*sizeof(float));
        cublasHandle_t handle;
        cublasCreate(&handle);
        cublasSgemm(handle,CUBLAS_OP_T,CUBLAS_OP_T, M, N, K,&alpha, d_a, K, d_b, N,&beta, d_c, M)
        cudaMemcpy(h_C,d_c,M*N*sizeof(float),cudaMemcpyDeviceToHost);//此處的h_C是按列存儲的C
        printMatrix2(h_C, N, M);//按行優先N行M列的順序讀取h_C相當於做了CT的結果
        return 0;
}

輸入 A  = \begin{bmatrix} 1&2&3\\ 4& 5&6 \end{bmatrix}  B = \begin{bmatrix} 1& 2& 3& 4\\ 5& 6& 7& 8\\ 9& 10& 11& 12 \end{bmatrix}

運行結果由於按行優先N行M列的順序讀取h_C相當於做了C的轉置,得到CT = \begin{bmatrix} 38& 83\\ 44& 98\\ 50& 113\\ 56& 128 \end{bmatrix}

在學習cublasSgemm API訪問計算結果C的二維矩陣過程中,重新複習了一下二維矩陣的指針訪問,可以參考我的另外一篇博文 【程序分析】指針與二維數組的訪問
 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章