2019年6月20日 星期四

[平行運算] 用CUDA實現矩陣相乘

CUDA架構在device方面儲存資料的方式有兩種, 分別是linear memory和CUDA arrays, 在這裡我們使用linear memory, 因此在device的時候, 我們必須把二維矩陣轉為一維矩陣來看待, 其實也蠻簡單的, 官網上的文件是用row-major, 意思是以row為主, 一次把一個row依序塞進一維陣列, 所以要access的時候就會是 M(row, col) = *(陣列初始位址 + row * M.width + col); 也有人是用column major, 其實就是相反過來, M(row, col) = *(陣列初始位址 + row+ col*M.height);

在這裡其實有一個還沒提到的概念, 被稱作shared memory, 這是CUDA架構下的一種記憶體架構, 可以提升計算速度, 部過這個概念要等到下一篇文章我才會寫, 因此下面的程式碼是沒有shared memory的版本!

此外, 這裡的陣列大小選取都是blocksize的倍數, 因為若不是blocksize的倍數, 記憶體在分配上會有技術問題要處理, 此處也先省略

#include <iostream>
#include <cstdlib> 
#include <ctime>
#include <cuda_runtime.h>
#include "device_launch_parameters.h"  
#define BLOCK_SIZE 2
#define A_row 2
#define A_col 3
#define B_row A_col
#define B_col 2
#define C_row A_row
#define C_col B_col

using namespace std;

// 宣告Matrix class
class Matrix{
    
    public:
        int width;
        int height;
        float* elements;
};

__global__ void MatMulKernel(Matrix A, Matrix B, Matrix C)
{
    float ans = 0;
    int row = blockIdx.y * blockDim.y + threadIdx.y;
    int col = blockIdx.x * blockDim.x + threadIdx.x;
    for (int p = 0; p < A.width; ++p)
        ans += A.elements[row * A.width + p]
                * B.elements[p * B.width + col];
    C.elements[row * C.width + col] = ans;
}


int main(void)
{
    float A[A_row * BLOCK_SIZE][A_col * BLOCK_SIZE];
    float B[B_row * BLOCK_SIZE][B_col * BLOCK_SIZE];
    float C[A_row * BLOCK_SIZE][B_col * BLOCK_SIZE]={0};
    
    //隨機分配值給矩陣
    for(int i=0;i< A_row * BLOCK_SIZE;i++)
    {
        for(int j=0;j< A_col * BLOCK_SIZE;j++)
        {
            A[i][j]=rand();        
        }
        
    }
    
    for(int i=0;i< B_col * BLOCK_SIZE;i++)
    {
        for(int j=0;j< B_row * BLOCK_SIZE;j++)
        {
            B[i][j]=rand();
        }
    }
    
    Matrix h_A,h_B,h_C;
    
    //把給定的矩陣轉為Matrix類別
     
    h_A.height = A_row * BLOCK_SIZE;
    h_B.height = B_row * BLOCK_SIZE;
    h_C.height = C_row * BLOCK_SIZE;
     
    h_A.width = A_col * BLOCK_SIZE;
    h_B.width = B_col * BLOCK_SIZE;
    h_C.width = C_col * BLOCK_SIZE;

    h_A.elements = (float*)malloc(A_row*A_col*BLOCK_SIZE*BLOCK_SIZE*sizeof(float));
    h_B.elements = (float*)malloc(B_row*B_col*BLOCK_SIZE*BLOCK_SIZE*sizeof(float));  
    h_C.elements = (float*)malloc(C_row*C_col*BLOCK_SIZE*BLOCK_SIZE*sizeof(float));     

    for(int i=0;i< A_row * BLOCK_SIZE;i++)
    {
        for(int j=0;j< A_col * BLOCK_SIZE;j++)
        {
            *(h_A.elements + i*h_A.width + j) = A[i][j];
        }
        
    }


    for(int i=0;i< B_row * BLOCK_SIZE;i++)
    {
        for(int j=0;j< B_col * BLOCK_SIZE;j++)
        {
            *(h_B.elements + i*h_B.width + j) = B[i][j];
        }
    }
    
    // 把h_A 和 h_B 複製給 device
    Matrix d_A;
    d_A.width = h_A.width; d_A.height = h_A.height;
    size_t size = h_A.width * h_A.height * sizeof(float);
    cudaMalloc(&d_A.elements, size);
    cudaMemcpy(d_A.elements, h_A.elements, size, cudaMemcpyHostToDevice);
    
    Matrix d_B;
    d_B.width = h_B.width; d_B.height = h_B.height;
    size = h_B.width * h_B.height * sizeof(float);
    cudaMalloc(&d_B.elements, size);
    cudaMemcpy(d_B.elements, h_B.elements, size, cudaMemcpyHostToDevice);

    // 在 device memory 裡創出 d_C 的記憶體空間
    Matrix d_C;
    d_C.width = h_C.width; d_C.height = h_C.height;
    size = h_C.width * h_C.height * sizeof(float);
    cudaMalloc(&d_C.elements, size);

    // 實際計算
    dim3 dimBlock(BLOCK_SIZE, BLOCK_SIZE);
    dim3 dimGrid(h_B.width / dimBlock.x, h_A.height / dimBlock.y);
    MatMulKernel<<<dimGrid, dimBlock>>>(d_A, d_B, d_C);
    
    // 把算好的結果從d_C 複製回 h_C
    cudaMemcpy(h_C.elements, d_C.elements, size,
               cudaMemcpyDeviceToHost);
    
    // 驗算
    int ok=1;
    
    for(int i=0;ok && i< A_row * BLOCK_SIZE ;i++)
    {
        for(int j=0;ok && j< B_col * BLOCK_SIZE;j++)
        {
            for(int k=0;k< A_col * BLOCK_SIZE;k++)
            {
                C[i][j]+=A[i][k]*B[k][j];
            }
            if(C[i][j]!=*(h_C.elements+i*h_C.width+j))
            {
                cout<<"fail";
                ok=0;
                break;
            }
        }
    }
    
    if(ok==1)
    {
        cout<<"success"<<endl;
    }

    //釋放記憶體空間
    cudaFree(d_A.elements);
    cudaFree(d_B.elements);
    cudaFree(d_C.elements);
   
    system("pause");
}


reference:
1. Nvidia developer zone

2019年6月19日 星期三

[平行運算] CUDA入門

最近因為興趣使然, 開始碰了CUDA, 因此參考惹Nvidia的資料, 來寫個筆記

首先是CPU和GPU的比較, 這裡先簡略帶過, 之後有機會再來寫個一篇(是要挖多少坑....), 簡單來說GPU和CPU比較起來, GPU有非常多的核心(core)可以一次做很多事情, 因此如果需要大量計算, 且每次計算結果間彼此獨立的話, 可以使用GPU來同時進行運算, 這個概念被稱為是平行運算(parallel programming)

這樣講還是有點抽象, 因此用向量加法來作例子, 舉例有兩個向量, A=(1,2,3), B=(3,4,5), 要把兩個矩陣相加, 如果一一相加的話, 要加三次, 但如果分給GPU做, 雖然還是要做三次加法, 但三次加法可以同時做, 因此完成的時間變短, 這就是平行運算的精神

CUDA就是Nvidia拿來整合大家熟悉的程式語言(C,C++,java, fortan....), 和GPU間的平台

由於小弟寫比較多的是C/C++語言, 而且官方文件也是用C/C++語言, 因此以下用C/C++語言語法來介紹,

CUDA裡面有所謂的 kernel 概念, kernel內的程式碼是由GPU執行, 用 __global__ 做宣告(注意底標的橫槓有兩個), 這裡參考一下官方文件, 寫法如下:

//目標是在GPU裡面把兩個陣列相加
__global__ void add( int *a, int *b, int *c ) 
{
    int i = threadIdx.x;//threadIdx是CUDA內建用來指向特定thread的變數
    c[i]=a[i]+b[i];  
}

P.S. 在這裡可能有些人會想說什麼是thread, thread 中文譯為執行緒, 在作業系統的課本會花一大堆時間講,不過這裡簡單來說, 可以把thread想成電腦執行一件事情,就稱為thread

看完了GPU的code, 接著來看主程式怎麼和GPU互動

這裡其實有點複雜, 因為牽扯到兩個部分, 第一個是CUDA如何在主程式裡面表示thread, 第二個部分是CPU和GPU彼此間記憶體如何操作, 先看第一部分:

前面提到的threadIdx其實是一個三維的向量, 每組數字代表一個thread; 一群thread可以組成一個block(目前技術一個block最多可以有但1024個thread), 一群block可以組成一個grid; CUDA有一個特殊的語法, add<<<numBlocks, threadsPerBlock>>>(a, b, c); 如果我們不看<<< >>>裡面的內容的話,會發現就和一般的C語言函數一樣, 而加上的部份就是代表CUDA指定要用這麼多的執行緒去執行程式, 寫出來如下:


#include <iostream>
#define N 100
#define numBlocks 1
#define threadsPerBlock 100
__global__ void add( int *a, int *b, int *c ) 
{
    int i = threadIdx.x;
    c[i]=a[i]+b[i];  
}

int main(void )
{
    //處理記憶體的部分,待補
    add<<<numBlocks, threadsPerBlock>>>(a, b, c);
    //處理記憶體的部分,待補
}

寫到這裡, 其實程式還是不會動, 要讓程式會動還要處理記憶體的部份,沒錯大家害怕的指標(pointer)要出現惹, 當然還有超級煩人的動態記憶體配置, malloc!!!

在CUDA裡面, 分為host和device, host一般指得是用CPU處理, device一般指得是用GPU處理, 彼此間的記憶體並不共用, 因此在運算的時候, 要把host記憶體的東西複製給device, 算完後再複製回來, 在這裡為了方便辨識, host的變數都加上h_, device的變數都加上d_, 程式碼如下:

#include <iostream>
#include <cstdlib> 
#include <ctime>   
#define N 100
#define numBlocks 1
#define threadsPerBlock 100

using namespace std;

__global__ void add( int *a, int *b, int *c ) 
{
    int i = threadIdx.x;
    c[i]=a[i]+b[i];  
}

int main( void ) {
    
    int *h_a, *h_b, *h_c;   
    int *d_a, *d_b, *d_c;   
    
    // 在host分配記憶體空間
    h_a = (int*)malloc( N * sizeof(int) ); 
    h_b = (int*)malloc( N * sizeof(int) );
    h_c = (int*)malloc( N * sizeof(int) );

    // 亂數指定值給陣列
    for (int i=0; i<N; i++) {
        h_a[i] = rand();
        h_b[i] = rand();
    }

    // 在device上分配記憶體
     cudaMalloc( (void**)&d_a, N * sizeof(int) );
     cudaMalloc( (void**)&d_b, N * sizeof(int) );
     cudaMalloc( (void**)&d_c, N * sizeof(int) );

    // 把host的東西複製到device上
     cudaMemcpy( d_a, h_a, N * sizeof(int), cudaMemcpyHostToDevice );
     cudaMemcpy( d_b, h_b, N * sizeof(int), cudaMemcpyHostToDevice );

    
    add<<<numBlocks, threadsPerBlock>>>( d_a, d_b, d_c );

    // 把device算出來的結果複製回CPU
    cudaMemcpy( h_c, d_c, N * sizeof(int),cudaMemcpyDeviceToHost );

    //驗算
    int ok=1;
    for(int i=0;i<N;i++)
    {
        if(h_c[i]!=h_a[i]+h_b[i])
        {
            cout<<"fail";
            ok=0;
            break;
        }
    }
    if(ok==1)
        printf("success");
   
    // 釋放host的記憶體
    free( h_a );
    free( h_b );
    free( h_c );

    // 釋放device的記憶體
     cudaFree( d_a );
     cudaFree( d_b );
     cudaFree( d_c );

    return 0;
}


reference:
1. Nvidia developer zone
2. http://selkie.macalester.edu/csinparallel/modules/ConceptDataDecomposition/build/html/Decomposition/CUDA_VecAdd.html

2019年6月1日 星期六

[鼻咽癌] 誘導性化療對鼻咽癌的效果(2019.6更新)

Induction chemotherapy 對 NPC 的治療一直是一個熱門的話題, induction chemotherapy,中文譯為誘導性化療, 其實就是neoajuvant chemotherapy,只是換個名字而已! 小弟嘗試整理一些相關的trial, 希望之後臨床上遇到能更熟悉

1. 之前失敗的 trial (都是用induction CT + RT v.s. RT)

(1) cisplatin, 5-FU : overall survival, disease-free survival, locoregional relapse rate, distant metastatic rate, and median time to relapse在兩組間都沒差異 (論文連結, 1995)
(2) bleomycin, epirubicin, and cisplatin: induction CT 組在disease-free survival的效果比較好, 但是治療相關的死亡數也比較多, 其他 local, regional metastases, overall survival無明顯差異 (論文連結, 1996)
(3) cisplatin, epirubicin: relapse free survival,overall survival 在兩組間無差異(論文連結, 1998)
(4) cisplatin, bleomycin, 5-FU: 5-year overall survival rates 和 relapse-free survival 無明顯差異(論文連結, 2001)

看完上面一堆失敗的, 接下來就是看近代成功的trial惹

2.  induction CT+CCRT v.s. CCRT

(1) TPF regimen (docetaxel, cisplatin, 5-FU):  中國方面有一個phase 3 trial針對locoregionally advanced nasopharyngeal carcinoma (AJCC 7: stage III-IVB, exclude T3-4N0) 做induction CT, 發現induction CT可以改善3-year failure-free survival (80%v.s. 72%)(論文連結, 2016)
(2) GP regimen(Gemcitabine+ Cisplatin): 很令人振奮, 剛好看到新出來的NEJM, 有這方面的evidence惹(論文點此), 中國廣州中山大學的研究, 也是phase 3, locoregionally advanced nasopharyngeal carcinoma, 分成induction CT(gemcitabine+cisplatin) +CCRT V.S. CCRT alone, 發現 induction chemotherapy組可以改善 3-year recurrence-free survival ( 85.3% v.s 76.5%) (stratified hazard ratio for recurrence or death, 0.51; 95% confidence interval [CI], 0.34 to 0.77; P=0.001). 可以改善 Overall survival at 3 years (94.6% v.s. 90.3%) (stratified hazard ratio for death, 0.43; 95% CI, 0.24 to 0.77)

(3) MEPFL(mitomycin C, epirubicin, cisplatin, and 5-fluorouracil/leucovorin):這篇是台灣本土出產論文,也是phase III trail, 針對stage IVA and IVB 的病人, induction CT可以改善disease free survival (5-year rate 61% versus 50%),但對overall survival無明顯影響(論文連結)




reference:
1.  Handbook of Evidence-Based Radiation Oncology 3rd Edition
2.  Perez & Brady's Principles and Practice of Radiation Oncology