2013年5月11日 星期六

CUDA草稿-矩陣相乘(1)

這是關於CUDA的筆記~ 但沒有照什麼順序寫,就是想到.... 就寫@@


前言



      GPU有許多不同的記憶體種類(global memory, shared memory, texture memory, register, local memory, etc..),其中最常用的是global memory,而global memory的使用在xxx(還沒寫@@)也都有舉例說明如何去使用,一般的流程是,先配置好global memory,然後再將CPU的資料給複製到device global memory中,之後在GPU中提取global memory的值來進行運算並把結果存回global memory中,但是對於某些問題來講,記憶體的存取頻繁,也就有可能因為global memory的使用而造成整體運算效能的降低,因為global memory的存取速度是GPU中的記憶體裡最低的,若我們可以善加利用其他較快的記憶體進行存取,則總體效能應該可以再提升,為了要讓大家了解如何將原本使用global memory存取運算的CUDA程式,改寫成使用shared memory存取運算的CUDA程式,進一步獲得相對應的效能提升,這邊就先以矩陣相乘做範例,來帶大家一步步做喔~

基本的矩陣相乘

        矩陣相乘一直是許多科學運算中會去使用到的基本運算問題, 假設Am*l矩陣(A = [aij] m*l)Bl*n矩陣(B = [bij] l*n),則他們的乘積AB會為一個m*n的矩陣C (C = [cij] m*n),其中
 Ci,j = 


,我們可以看成C矩陣的每一個元素就是Arow向量與Bcolumn向量進行向量內積,也就是每一個Celement都可以看成獨立的向量內積運算的問題,這樣的問題很適合來進行平行運算,而過去在CPU中的平行運算架構中已經有不少對於矩陣相乘的研究,而現在讓我們來看看如何利用CUDA改寫在CPU上的矩陣相乘到GPU的運算環境上。



Example 1: CPU版本
//--------------------------------------------------------------------
//A : m x l
//B : l x n
//C : m x n    (C=A*B)
//--------------------------------------------------------------------

void host_mm(float* C, float* A, float* B, int m, int n, int l)
{ 
    for(int i=0; i<m; i++)
        for(int j=0; j<n; j++) 
        {   
            float s=0;
            for (int k=0; k<l; k++)
            {   
                float a = A[i*l + k]; 
                float b = B[k*n + j]; 
                s += a * b;
            }   
                C[i*n + j] = s;
        }   
}







SBMT(Single Block, Multiple Threads)


        在Example 1裡的host_mm 函式利用C語言在CPU裡實作了傳統的矩陣相乘運算,我們可以看到他的運算需要使用到三個迴圈,也就是需要O(n^3)的時間複雜度,運算起來很耗費時間,在這函式中我們利用i*l + k k*n + j index分別把AB陣列裡的rowcolumn向量取出進行向量內積的運算,之後將其結果存回C陣列i*n + jindex之中,事實上,這樣的運算我們可以將其分為許多向量內積的問題,一開始我們利用CUDA解決這個問題時,先不要想說要利用多少個thread block來進行,考量最簡單的case,一個thread block的情形,那一個thread block裡有許多thread,要如何分配這些thread去做相對應的運算呢? Figure 1提供了一種方法:



Figure 1: SBMT(Single Block, Multiple Threads) method



矩陣相乘得出的每一個element的運算,可以對應到thread block的其中一個thread的運算,也就是這個thread可以將A,B矩陣的rowcolumn給取出來進行向量內積,得出的結果存到此thread對應的C矩陣的某位址中,以圖3.1.1的例子而言,編號為(2,2)thrad負責C矩陣(2,2)位址的值,而其中的運算就是將A的第三個row向量(1,1,1,1)B的第三個column向量(1,2,3,4),進行向量內積的運算,所以為1*1+1*2+1*3+1*4 = 10,這樣的方法我們可以嘗試著寫一個簡單的CUDA程式看看:
Example 2: (SBMT_matrix_global.cu)

#include <stdio.h>
#include <string.h>
#include <math.h>
#include <sys/time.h>
//--------------------------------------------------------------------
//A : m x l
//B : l x n
//C : m x n    (C=A*B)
//--------------------------------------------------------------------
 
void host_mm(float* C, float* A, float* B, int m, int n, int l){
 
    for(int i=0; i<m; i++)
        for(int j=0; j<n; j++) 
    {
            float s=0;
            for (int k=0; k<l; k++)
        {
                float a = A[i*l + k];
                float b = B[k*n + j];
                s += a * b;
            }
            C[i*n + j] = s;
        }
}
 
//--------------------------------------------------------------------
__global__ void gpu_mm(float* C, float* A, float* B, int m, int n, int l){
    
    //// 2D Thread ID    
    int tx = threadIdx.x;
    int ty = threadIdx.y;
    
    // Pvalue is used to store the element of the matrix
    // that is computed by the thread    
    float Pvalue = 0;
    
    for (int k = 0; k < l; ++k)
    { 
         float Aelement = A[ty * l + k];
         float Belement = B[k * n + tx];
         Pvalue += Aelement * Belement;
    }
    // Write the matrix to device memory;
    // each thread writes one element
    C[ty * n + tx] = Pvalue;
}
 
 
//----------------------------------------------
double diff(float* a, float* b, int n){
    double s=0, r=0;
    for(int k=0; k<n; k++)
    {
        double w=a[k]-b[k];
        s+=w*w;
        r+=a[k]*a[k];
    }
    return sqrt(s/r); 
}
 
 
void random(float* a, int n){
    for(int k=0; k<n; k++){
        a[k]=(float)rand()/RAND_MAX*2-1;
    }
}
 
//----------------------------------------------
void testMatrix(int m, int n, int l)
{
    //initialize
    float *a = (float*)malloc(sizeof(float)*m*l);
    float *b = (float*)malloc(sizeof(float)*l*n);
    float *c1 = (float*)malloc(sizeof(float)*m*n);
    float *c2 = (float*)malloc(sizeof(float)*m*n);
 
    srand(time(0));
    random(a,m*l);
    random(b,l*n);
    memset(c1, 0, sizeof(float)*m*n);
    memset(c2, 0, sizeof(float)*m*n);
        
    float  *ga,*gb,*gc;
    cudaMalloc((void**)&ga, m*l*sizeof(float));
    cudaMalloc((void**)&gb, l*n*sizeof(float));
    cudaMalloc((void**)&gc, m*n*sizeof(float));
 
    cudaMemcpy(ga, a, m*l*sizeof(float), cudaMemcpyHostToDevice);
    cudaMemcpy(gb, b, l*n*sizeof(float), cudaMemcpyHostToDevice);
    cudaMemset(gc, 0, m*n*sizeof(float));
        
    cudaEvent_t start,stop;
    cudaEventCreate(&start);
    cudaEventCreate(&stop);
    cudaEventRecord(start,0); 
 
    //SBMT(Single Block, Multiple Threads)
    gpu_mm<<<dim3(1,1,1), dim3(m, n, 1)>>> (gc,ga,gb,m,n,l);
    cudaThreadSynchronize();
 
    cudaEventRecord(stop,0); 
    cudaEventSynchronize(stop);
 
    float elapsedTime;
    cudaEventElapsedTime(&elapsedTime, start, stop);
    
    cudaMemcpy(c2, gc, m*n*sizeof(float), cudaMemcpyDeviceToHost);        
    
    double c_start,c_stop;
    double CPU_execution_time;
    c_start = (double)clock();
 
    host_mm(c1, a, b, m, n, l);
     
    c_stop = (double)clock();
    CPU_execution_time = (c_stop - c_start)/(double)CLOCKS_PER_SEC;
 
    //check precision        
    double err=diff(c1,c2,m*n);
    printf("err = %g\n", err);       
 
    printf(" ======== Execution Infomation ========\n");
    printf(" Excuetion Time on GPU: %3.20f s\n",elapsedTime/1000);
    printf(" Excuetion Time on CPU: %3.20f s\n",CPU_execution_time);
    printf(" Speed up = %f\n",(CPU_execution_time/(elapsedTime/1000)));
    printf(" ======================================\n\n");
 
 
    free(a); 
    free(b); 
    free(c1); 
    free(c2); 
       
    cudaFree(ga);
    cudaFree(gb);
    cudaFree(gc);
        
}
 
//----------------------------------------------
int main()
{
 
    int m=32;
    int n=32;
    int l=32;
 
    testMatrix(m,n,l);
 
    return 0;
}






Example 2在100行的地方:

gpu_mm<<<dim3(1,1,1), dim3(m, n, 1)>>> (gc,ga,gb,m,n,l);
為kernel launch,dim3為CUDA內建資料結構,就代表配置grid, block的維度, 我們舉的例子分別代表擁有m*n個thread的thread block與擁有1*1個thread block的kernel,dim3(1,1,1), dim3(m, n, 1),放入"<<< >>>" 這tag之中,這樣便代表呼叫此kernel function的grid擁有1*1個thread block,以及每個block擁有m*n個thread。



kernel function使用了dim3這個內建的資料結構來配置了grid和block的大小,於是我們可以在kernel function裡使用多維度的thread block與thread ID,我們的例子thread block數量只有1,所以沒有多維度的使用問題,而在一個thread block裡的thread數量有m*n個,所以m*n 個thread 都有一個唯一的2D編號來表示,以下程式碼由


int tx = threadIdx.x;
int ty = threadIdx.y;


來得到2D thread ID。




在kernel function呼叫之前的cudaMalloc已經配置好global memory大小的陣列了,所以我們在gpu_mm這個kernel function裡面可以直接提取A,B陣列在global memory裡的值,而我們可以發現gpu_mm 函式裡面只要一個for迴圈來抓取A,B矩陣的row 與column值,之後算出來的值再存回C矩陣對應的元素裡。



__global__ void gpu_mm(float* C, float* A, float* B, int m, int n, int l){
//// 2D Thread ID
int tx = threadIdx.x;
int ty = threadIdx.y;
// Pvalue is used to store the element of the matrix
// that is computed by the thread
float Pvalue = 0;
for (int k = 0; k < l; ++k)
{
float Aelement = A[ty * l + k];
float Belement = B[k * n + tx];
Pvalue += Aelement * Belement;
}
// Write the matrix to device memory;
// each thread writes one element
C[ty * n + tx] = Pvalue;
}

結論




我們的矩陣大小為32*32 = 1024,為了剛好符合我的device一個block的thread個數上限,測試的GPU為GeForce GT 650M,CC(CUDA Capability)為3.0,CPU為2.6 GHz Intel Core-i7,往後沒有特別註明的話都是在此環境下測試,測試的結果如下:



err = 9.88651e-08
 ======== Execution Infomation ========
 Excuetion Time on GPU: 0.00069228798383846879 s
 Excuetion Time on CPU: 0.00012000000000000000 s
 Speed up = 0.173338
 ======================================
我們看到GPU的表現並沒有很好,寫了一大堆結果跑得比CPU慢!那ㄟ安ㄋ?別緊張,我們來看一下這數據背後的意義,首先這個時間的數據都很小,因為一開始我們只有用32*32這麼大的矩陣相乘,說實在的執行時間都極短,測量時間的誤差也大,再來就是我們總共只有使用1024個thread,且只有一個block,那同時間也只有一個SM(Streaming Multiprocessor)在運行而已,總之就是沒有充分使用到GPU資源囉,我們接下來再來看要怎麼改善 (待續...)