cuda 矩陣乘法運算并行

一直很好奇GPU做矩陣運算是怎么并行加速的,今天看了一些粗淺的東西,并總結整理出來。
version:cuda 8

cuda C 中擴展的一些概念

主要包括函數聲明、變量聲明、內存類型聲明、文理內存、原子函數等,常用的有這么幾個:
參考(http://bbs.csdn.net/topics/390798229,原地址已經失效)

  • 主機
    將CPU及系統的內存(內存條)稱為主機。
  • 設備
    將GPU及GPU本身的顯示內存稱為設備。
  • 線程(Thread)
    一般通過GPU的一個核進行處理。
  • 線程塊(Block)
    1. 由多個線程組成(可以表示成一維,二維,三維,具體下面再細說)。
    2. 各block是并行執行的,block間無法通信,也沒有執行順序。
    3. 注意線程塊的數量限制為不超過65535(硬件限制)。
  • 線程格(Grid)
    由多個線程塊組成(可以表示成一維,二維,三維,具體下面再細說)。


  • 線程束
    在CUDA架構中,線程束是指一個包含32個線程的集合,這個線程集合被“編織在一起”并且“步調一致”的形式執行。在程序中的每一行,線程束中的每個線程都將在不同數據上執行相同的命令。
  • 核函數(Kernel)
    1. 在GPU上執行的函數通常稱為核函數。
    2. 一般通過標識符global修飾,調用通過<<<參數1,參數2>>>,用于說明內核函數中的線程數量,以及線程是如何組織的。
    3. 以線程格(Grid)的形式組織,每個線程格由若干個線程塊(block)組成,而每個線程塊又由若干個線程(thread)組成。
    4. 是以block為單位執行的。
    5. 叧能在主機端代碼中調用。
    6. 調用時必須聲明內核函數的執行參數。
    7. 在編程時,必須先為kernel函數中用到的數組或變量分配好足夠的空間,再調用kernel函數,否則在GPU計算時會發生錯誤,例如越界或報錯,甚至導致藍屏和死機。
    //核函數聲明,前面的關鍵字global
__global__ void kernel( void ) {
}

函數修飾符
1. __global__,表明被修飾的函數在設備上執行,但在主機上調用。
2. __device__,表明被修飾的函數在設備上執行,但只能在其他__device__函數或者__global__函數中調用。

常用的GPU內存函數

  • cudaMalloc()
    1. 函數原型: cudaError_t cudaMalloc (void **devPtr, size_t size)。
    2. 函數用處:與C語言中的malloc函數一樣,只是此函數在GPU的內存你分配內存。
    3. 注意事項:
    3.1. 可以將cudaMalloc()分配的指針傳遞給在設備上執行的函數;
    3.2. 可以在設備代碼中使用cudaMalloc()分配的指針進行設備內存讀寫操作;
    3.3. 可以將cudaMalloc()分配的指針傳遞給在主機上執行的函數;
    3.4. 不可以在主機代碼中使用cudaMalloc()分配的指針進行主機內存讀寫操作(即不能進行解引用)。
  • cudaMemcpy()
    1. 函數原型:cudaError_t cudaMemcpy (void *dst, const void *src, size_t count, cudaMemcpyKind kind)。
    2. 函數作用:與c語言中的memcpy函數一樣,只是此函數可以在主機內存和GPU內存之間互相拷貝數據。
    3. 函數參數:cudaMemcpyKind kind表示數據拷貝方向,如果kind賦值為cudaMemcpyDeviceToHost表示數據從設備內存拷貝到主機內存。
    4. 與C中的memcpy()一樣,以同步方式執行,即當函數返回時,復制操作就已經完成了,并且在輸出緩沖區中包含了復制進去的內容。
    5. 相應的有個異步方式執行的函數cudaMemcpyAsync(),這個函數詳解請看下面的流一節有關內容。
  • cudaFree()
    1. 函數原型:cudaError_t cudaFree ( void* devPtr )。
    2. 函數作用:與c語言中的free()函數一樣,只是此函數釋放的是cudaMalloc()分配的內存。
    下面實例用于解釋上面三個函數

GPU內存分類

  • 全局內存
    通俗意義上的設備內存。
  • 共享內存
    1. 位置:設備內存。
    2. 形式:關鍵字__shared__添加到變量聲明中。如__shared__ float cache[10]。
    3. 目的:對于GPU上啟動的每個線程塊,CUDA C編譯器都將創建該共享變量的一個副本。線程塊中的每個線程都共享這塊內存,但線程卻無法看到也不能修改其他線程塊的變量副本。這樣使得一個線程塊中的多個線程能夠在計算上通信和協作。
  • 常量內存
    1. 位置:設備內存
    2. 形式:關鍵字__constant__添加到變量聲明中。如__constant__ float s[10];。
    3. 目的:為了提升性能。常量內存采取了不同于標準全局內存的處理方式。在某些情況下,用常量內存替換全局內存能有效地減少內存帶寬。
    4. 特點:常量內存用于保存在核函數執行期間不會發生變化的數據。變量的訪問限制為只讀。NVIDIA硬件提供了64KB的常量內存。不再需要cudaMalloc()或者cudaFree(),而是在編譯時,靜態地分配空間。
    5. 要求:當我們需要拷貝數據到常量內存中應該使用cudaMemcpyToSymbol(),而cudaMemcpy()會復制到全局內存。
    6. 性能提升的原因:
    6.1. 對常量內存的單次讀操作可以廣播到其他的“鄰近”線程。這將節約15次讀取操作。(為什么是15,因為“鄰近”指半個線程束,一個線程束包含32個線程的集合。)
    6.2. 常量內存的數據將緩存起來,因此對相同地址的連續讀操作將不會產生額外的內存通信量。
  • 紋理內存
    1. 位置:設備內存
    2. 目的:能夠減少對內存的請求并提供高效的內存帶寬。是專門為那些在內存訪問模式中存在大量空間局部性的圖形應用程序設計,意味著一個線程讀取的位置可能與鄰近線程讀取的位置“非常接近”。如下圖:



    3. 紋理變量(引用)必須聲明為文件作用域內的全局變量。
    4. 形式:分為一維紋理內存 和 二維紋理內存。
    4.1. 一維紋理內存
    4.1.1. 用texture<類型>類型聲明,如texture<float> texIn。
    4.1.2. 通過cudaBindTexture()綁定到紋理內存中。
    4.1.3. 通過tex1Dfetch()來讀取紋理內存中的數據。
    4.1.4. 通過cudaUnbindTexture()取消綁定紋理內存。
    4.2. 二維紋理內存
    4.2.1. 用texture<類型,數字>類型聲明,如texture<float,2> texIn。
    4.2.2. 通過cudaBindTexture2D()綁定到紋理內存中。
    4.2.3. 通過tex2D()來讀取紋理內存中的數據。
    4.2.4. 通過cudaUnbindTexture()取消綁定紋理內存。

  • 固定內存
    1. 位置:主機內存。
    2. 概念:也稱為頁鎖定內存或者不可分頁內存,操作系統將不會對這塊內存分頁并交換到磁盤上,從而確保了該內存始終駐留在物理內存中。因此操作系統能夠安全地使某個應用程序訪問該內存的物理地址,因為這塊內存將不會破壞或者重新定位。
    3. 目的:提高訪問速度。由于GPU知道主機內存的物理地址,因此可以通過“直接內存訪問DMA(Direct Memory Access)技術來在GPU和主機之間復制數據。由于DMA在執行復制時無需CPU介入。因此DMA復制過程中使用固定內存是非常重要的。
    4. 缺點:使用固定內存,將失去虛擬內存的所有功能;系統將更快的耗盡內存。
    5. 建議:對cudaMemcpy()函數調用中的源內存或者目標內存,才使用固定內存,并且在不再需要使用它們時立即釋放。
    6. 形式:通過cudaHostAlloc()函數來分配;通過cudaFreeHost()釋放。
    7. 只能以異步方式對固定內存進行復制操作。
  • 原子性
    1. 概念:如果操作的執行過程不能分解為更小的部分,我們將滿足這種條件限制的操作稱為原子操作。
    2. 形式:函數調用,如atomicAdd(addr,y)將生成一個原子的操作序列,這個操作序列包括讀取地址addr處的值,將y增加到這個值,以及將結果保存回地址addr。
    常用線程操作函數
    1. 同步方法__syncthreads(),這個函數的調用,將確保線程塊中的每個線程都執行完__syscthreads()前面的語句后,才會執行下一條語句。

cuda C 做矩陣乘法(Tiled 算法)

為什么看cuda C 做矩陣乘法運算呢?在深度神經網絡中,全連接層、卷積層、池化層,幾乎我們可以想到的所有操作都離不開矩陣運算,卷積層最后其實也是轉化為矩陣的乘法操作進行優化,在【conv2d 實現 caffe&tensorflow】中有介紹原理。
參考視頻地址:https://www.youtube.com/watch?v=SqZaletdPCY


思想: 為了引入共享內存的概念降低GPU帶寬使用,把要計算的兩個矩陣A B 先分解成BLOCK_SIZE=16大小的submatrix,每一個block結構運算一個submatrix乘法,這樣在一個block中所有的線程是共享參數的,不用每次計算都從global memory中重新加載。

template <int BLOCK_SIZE> __global__ void
matrixMulCUDA(float *C, float *A, float *B, int wA, int wB)
{
 // Thread 所在 block 的 location 
 int bx = blockIdx.x;
 int by = blockIdx.y;

 // Thread 的location 
 int tx = threadIdx.x;
 int ty = threadIdx.y;

 // A矩陣16 * 16 子矩陣的起始下標
 int aBegin = wA * BLOCK_SIZE * by;

 // A矩陣16 * 16 子矩陣的終止下標(就是A矩陣一次運算一行,對應著B 矩陣一次運算一列)
 int aEnd = aBegin + wA - 1;

 // A矩陣下標一次移動的步長, 子矩陣是16 * 16,一次處理一個子矩陣,那么步長顯然就是16了
 int aStep = BLOCK_SIZE;

 // B 矩陣子矩陣對應的起始下標
 int bBegin = BLOCK_SIZE * bx;

 // B 矩陣子矩陣對應的步長,一次移動16*widthB,同樣也是隔出16*16的子矩陣出來
 int bStep = BLOCK_SIZE * wB;

 // 累加,得到行 * 列的值
 float Csub = 0;

 // 循環次數等于widthA / 16,把長向量點積運算轉化為兩個短向量點積后的和
 for (int a = aBegin, b = bBegin; a <= aEnd; a += aStep, b += bStep)
 {
 // 定義A的共享子矩陣變量,因為__shared__聲明,所以同一個block中的所有threads都可見,
 //每個thread填充一個元素,并計算一個行列乘積,減小帶寬使用
 __shared__ float As[BLOCK_SIZE][BLOCK_SIZE];

 // 定義A的共享子矩陣變量
 __shared__ float Bs[BLOCK_SIZE][BLOCK_SIZE];

 // 每個block包含16 * 16 個線程,所以每個線程負責一個矩陣元素的拷貝(注意同步)
 As[ty][tx] = A[a + wA * ty + tx];
 Bs[ty][tx] = B[b + wB * ty + tx];

 // Synchronize to make sure the matrices are loaded
 __syncthreads();

 // 每個線程計算 子矩陣的行列乘積,大循環外邊還有累加,累加的是不同子矩陣點積和
 for (int k = 0; k < BLOCK_SIZE; ++k)
 {
 Csub += As[ty][k] * Bs[k][tx];
 }

 // 再次同步
 __syncthreads();
 }

 // 把結果賦值到C矩陣,計算結果對應C下邊的過程
 int c = wB * BLOCK_SIZE * by + BLOCK_SIZE * bx;
 C[c + wB * ty + tx] = Csub;
}

只看源代碼很難理解矩陣加速真正的原理,這是一個坑,還有是輸入矩陣的尺寸大小,只能是BLOCK_SIZE=16的整數倍,不然會出錯(實驗結果也表明確實出錯了,又是一個坑)。
為什么采用Tiled 算法呢?主要是不這么做GPU從global memory讀取數據的代價太大了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,836評論 6 540
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,275評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,904評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,633評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,368評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,736評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,740評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,919評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,481評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,235評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,427評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,968評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,656評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,055評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,348評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,160評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,380評論 2 379

推薦閱讀更多精彩內容

  • CUDA從入門到精通(零):寫在前面 本文原版鏈接: 在老板的要求下,本博主從2012年上高性能計算課程開始接觸C...
    Pitfalls閱讀 3,638評論 1 3
  • 1. CPU vs. GPU 1.1 四種計算機模型 GPU設計的初衷就是為了減輕CPU計算的負載,將一部分圖形計...
    王偵閱讀 20,982評論 3 20
  • CUDA是什么 CUDA,ComputeUnifiedDeviceArchitecture的簡稱,是由NVIDIA...
    Pitfalls閱讀 9,511評論 0 1
  • java 接口的意義-百度 規范、擴展、回調 抽象類的意義-樂視 為其子類提供一個公共的類型封裝子類中得重復內容定...
    交流電1582閱讀 2,259評論 0 11
  • CUDA是一種新的操作GPU計算的硬件和軟件架構,它將GPU視作一個數據并行計算設備,而且無需把這些計算映射到圖形...
    ai領域閱讀 9,184評論 0 8