目錄
不使用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的計算過程如下圖所示
示例程序
#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 = B =
運行結果 C = AxB =
使用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的計算過程如下圖所示
示例程序
#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 = B =
運行結果由於按行優先N行M列的順序讀取h_C相當於做了C的轉置,得到CT =
在學習cublasSgemm API訪問計算結果C的二維矩陣過程中,重新複習了一下二維矩陣的指針訪問,可以參考我的另外一篇博文 【程序分析】指針與二維數組的訪問