寫在前面:
- 本筆記爲 NVIDIA CUDA初級教程視頻的筆記
- 視頻連接:https://www.bilibili.com/video/BV1kx411m7Fk?p=10
- P9和P10講解了矩陣相乘的cuda 實現
使用cuda C完成矩陣相乘算法詳解
矩陣相乘大家應該都不陌生。
設有兩個矩陣M和N,假設M和N都是方陣,維度均爲width × width
如果M和N均爲1000 × 1000的矩陣,總共要進行1000000次點乘。其中,每次點乘有1000次乘法和1000次加法。
Matirx Multiply:CPU實現
先來看看使用普通的c代碼在CPU端如何實現
void MatrixMulOnHost(float* M,float* N,float* P,int width)
{
for(int i=0;i<width;++i)
for(int j=0;i<width;j++)
{
//sum對應每一次點乘(M的某一行×N的某一列)的結果
float sum = 0;
for(int k=0;k<width;k++)
{
float a = M[i*width+k];
float b = N[k*width+j];
sum+=a*b;
}
P[i*width+j]=sum;//乘累加的結果放到對應位置上
}
}
可以看到循環計算結果P矩陣裏的每一個元素。計算過程非常清晰。
從這裏可以看到,這個計算存在非常大的並行性,即結果矩陣P裏的每一個元素結果的計算與P中其他元素是不相關的,沒有依賴性。
所以我們可以在GPU端上實現矩陣相乘。
Matirx Multiply:GPU實現
可以看到總共有3步:
- 管理內存(在GPU上分配空間,將CPU端數據拷貝到GPU端)
- GPU上並行處理(啓動kernel函數)
- 將結果拷貝回到CPU端
第1步:在算法框架中添加 CUDA memory transfers
第2步:CUDA C編程實現kernel
可以看到這裏有2個問題
- 使用線程的索引代替了雙重循環,並行去做就可以
- 不需要鎖或同步,如果數據之間有依賴,存在同步問題,但這裏每一個結果矩陣的元素是獨立的,與別的元素無關,所以不需要鎖存。
第3步 CUDA C編程調用kernel
源代碼
#include <stdio.h>
#include <cuda_runtime.h>
#include <device_launch_parameters.h>
#include <stdlib.h>
#define WIDTH 16
__global__ void MatrixMulKernel(float* Md, float* Nd, float* Pd, int width)
{
int tx = threadIdx.x;
int ty = threadIdx.y;
float Pvalue = 0;
for (int k = 0; k<width; k++)
{
float Mdelement = Md[ty*width + k];
float Ndelement = Nd[k*width + tx];
Pvalue += Mdelement * Ndelement;
}
Pd[ty*width + tx] = Pvalue;
}
int main(void)
{
float M[16][16], N[16][16], P[16][16];
int Width = 16;
int NUM = 192;
//初始化示例數據
for (int i = 0; i<16; i++)
{
for (int j = 0; j<16; j++)
{
M[i][j] = 2.0;
N[i][j] = 3.0;
}
}
int size = Width*Width*sizeof(float);
float *Md, *Nd, *Pd;
cudaMalloc((void**)&Md, size);
cudaMemcpy(Md, M, size, cudaMemcpyHostToDevice);
cudaMalloc((void**)&Nd, size);
cudaMemcpy(Nd, N, size, cudaMemcpyHostToDevice);
cudaMalloc((void**)&Pd, size);
dim3 dimBlock(WIDTH, WIDTH);
dim3 dimGrid(1, 1);
MatrixMulKernel <<<dimGrid, dimBlock >> >(Md, Nd, Pd, Width);
cudaMemcpy(P, Pd, size, cudaMemcpyDeviceToHost);
//打印結果矩陣
for (int i = 0; i<16; i++)
{
for (int j = 0; j<16; j++)
{
printf("%.2f ", P[i][j]);
}
printf("\n");
}
cudaFree(Md);
cudaFree(Nd);
cudaFree(Pd);
return 0;
}
回顧一下並行實現這個樣例的原理:
在這個矩陣相乘的案例中:
- 在算法實現中最主要的性能問題是什麼?
- 矩陣長度限制
- 僅有一個block
- 以G80和GT200爲例 —— 最多512個線程/block
- 矩陣長度限制
- 主要的限制是什麼?
- 很多global memory讀寫訪問(讀Md,Nd,兩次讀後才寫一次,這兩次讀耗時巨大,開銷較大)
優化矩陣相乘(一) —— 去除長度限制
解決第一個問題:去除長度限制
- 將Pd矩陣拆成tile小塊
- 把一個tile佈置到一個block
- 通過threadIdx和blockIdx索引
由於計算結果依然是彼此獨立的,所以每個block可以自己做自己的事情。
優化後的kernel函數如下:
(注意:每個線程計算的是塊內子矩陣的一個元素)
__global__ void MatrixMulKernel_01(float* Md, float* Nd, float* Pd, int width)
{
//計算矩陣Pd和M的行索引
int Row = blockIdx.y*blockDim.y + threadIdx.y;
//計算矩陣Pd和N的列索引
int Col = blockIdx.x*blockDim.x + threadIdx.x;
float Pvalue = 0;
//每個線程計算塊內子矩陣的一個元素
for (int k = 0; k<width; k++)
{
float Mdelement = Md[Row*width + k];
float Ndelement = Nd[k*width + Col];
Pvalue += Mdelement * Ndelement;
}
Pd[Row*width + Col] = Pvalue;
}
這種方式可以適用於大規模的問題
調用kernel代碼如下:
dim3 dimGrid(Width / TILE_WIDTH, Height / TILE_WIDTH);
dim3 dimBlock(TILE_WIDTH, TILE_WIDTH);
MatrixMulKernel_01 <<<dimGrid, dimBlock >> >(Md, Nd, Pd, TILE_WIDTH);
注:如果輸入的數組不是TILE_WIDTH的整數倍怎麼辦?擴充元素到分塊的整數倍後將元素填0
優化矩陣相乘(二) —— 使用共享內存
上述代碼訪存受限與global memory帶寬
- G80 峯值GFLOPS:346.4
- 需要1386 GB/s(每個浮點數4個bit,346*4)的帶寬未達到
- G80 存儲器實際帶寬:86.4GB/s
- 限制代碼 21.6 GFLOPS
- 實際上,代碼運行速度是15 GLOPS
- 必須大幅減少對global memory的訪問
回顧之前的程序,實際上,每個輸入元素被Width個線程讀取(對於每一列都讀取了Width次),所以使用shared memory來減少global memory帶寬需求。
把kernel拆分成多個階段
- 每個階段用Md和Nd的子集累加Pd
- 每個階段有很好的數據局部性
每個線程
- 讀入瓦片內Md和Nd的一個元素存入shared memory
代碼如下:
__global__ void MatrixMulKernel_01(float* Md, float* Nd, float* Pd, int width)
{
//塊內定義shared memory存儲Md和Nd的子集
__shared__ float Mds[TILE_WIDTH][TILE_WIDTH];
__shared__ float Nds[TILE_WIDTH][TILE_WIDTH];
int bx = blockIdx.x; int by = blockIdx.y;
int tx = threadIdx.x; int ty = threadIdx.y;
int Row = by*TILE_WIDTH + ty;
int Col = bx*TILE_WIDTH + tx;
float Pvalue = 0;
//注:多個塊的計算的結果相加後纔得到pd對應元素的值
//width/TILE_WIDTH:階段數目
//m:當前階段的索引
for (int m = 0; m<width/TILE_WIDTH; m++)
{
//從Md和Nd各取一個元素存入shared memory
Mds[ty][tx] = Md[Row*width + (m*TILE_WIDTH + tx)];
Nds[ty][tx] = Nd[Col + (m*TILE_WIDTH + ty)*width];
//等待block內所有線程,即等到整個瓦片存入shared memory
__syncthreads();
//累加點乘的子集
for (int k = 0; k < TILE_WIDTH; k++)
Pvalue += Mds[ty][k] * Nds[k][tx];
//注:如果沒有同步,可以上一次的乘累加沒完成,下一次的數已經過來把乘累加結果沖掉了
__synthreads();
}
//把最終結果寫入global memory
Pd[Row*width + Col] = Pvalue;
}
如何選取TITLE_WIDTH的數值?
-
如果太大的話會怎樣?
- 超出一個塊允許的最大線程數
Fermi — 1024;Kerpler - 1024;具體根據不同的計算能力查表得到。
- 超出shared memory極限
以G80爲例:16KM/SM 並且 B blocks/SM;
2KB/block
1KB給Nds,1KB給Mds(16 * 16 *4)
TITLE_WIDTH = 16
更大的TITLE_WIDTH將導致更少的塊數
Shared memory瓦片化的好處
- global memory 訪問次數減少TILE_WIDTH倍
- 16*16瓦片 減少16倍
- 以G80爲例
- 現在global memory 支持345.6 GFLOPS
- 接近峯值 346.5 GFLOPS
G80線程尺寸的考慮
- 每個thread block有許多個線程
- TILE_WIDTH爲16時:16*16=256個線程
- 需要許多個thread blocks
- 一個1024*1024 Pd 需要:64 * 64 = 4K thread blocks
- 每個thread block執行 2*256 =512次global memory的float讀入,爲了供應256 *(2 *16)= 8K mul/add操作
- 存儲帶寬不再是限制因素
Atomic Functions 原子操作
- 許多原子操作
//算數運算
atomicAdd();
atomicSub();
atomicExch();
atomicMin();
atomicMax();
atomicDec();
atomicCAS();
//位運算
atomicAnd();
atomicOr();
atomicXor();
- 不同塊裏面的線程如何協作?
- CUDA中的線程協作主要是通過共享內存實現的。使用關鍵字**“share”**聲明共享變量
- 共享內存是用於同一個線程塊內的線程之間交流的,不同線程塊之間是無法通過共享內存進行交流的
- 還有更多內容待補充。
- 儘量少用原子操作,爲什麼?
- 原子操作比較耗時,需要在整個系統裏進行排隊
優化的矩陣乘法源代碼
#include <stdio.h>
#include <cuda_runtime.h>
#include <device_launch_parameters.h>
#include <stdlib.h>
/*
* 去除長度限制 & 使用共享內存 的矩陣相乘優化
*/
#define TILE_WIDTH 8
__global__ void MatrixMulKernel_01(float* Md, float* Nd, float* Pd, int width)
{
//塊內定義shared memory存儲Md和Nd的子集
__shared__ float Mds[TILE_WIDTH][TILE_WIDTH];
__shared__ float Nds[TILE_WIDTH][TILE_WIDTH];
int bx = blockIdx.x; int by = blockIdx.y;
int tx = threadIdx.x; int ty = threadIdx.y;
int Row = by*TILE_WIDTH + ty;
int Col = bx*TILE_WIDTH + tx;
float Pvalue = 0;
//注:多個塊的計算的結果相加後纔得到pd對應元素的值
//width/TILE_WIDTH:階段數目
//m:當前階段的索引
for (int m = 0; m<width/TILE_WIDTH; m++)
{
//從Md和Nd各取一個元素存入shared memory
Mds[ty][tx] = Md[Row*width + (m*TILE_WIDTH + tx)];
Nds[ty][tx] = Nd[Col + (m*TILE_WIDTH + ty)*width];
//等待block內所有線程,即等到整個瓦片存入shared memory
__syncthreads();
//累加點乘的子集
for (int k = 0; k < TILE_WIDTH; k++)
Pvalue += Mds[ty][k] * Nds[k][tx];
//注:如果沒有同步,可以上一次的乘累加沒完成,下一次的數已經過來把乘累加結果沖掉了
__syncthreads();
}
//把最終結果寫入global memory
Pd[Row*width + Col] = Pvalue;
}
int main(void)
{
float M[16][16], N[16][16], P[16][16];
int Width = 16;
int Height = 16;
//初始化示例數據
for (int i = 0; i<16; i++)
{
for (int j = 0; j<16; j++)
{
M[i][j] = 2.0;
N[i][j] = 3.0;
}
}
int size = Width*Height*sizeof(float);
float *Md, *Nd, *Pd;
cudaMalloc((void**)&Md, size);
cudaMemcpy(Md, M, size, cudaMemcpyHostToDevice);
cudaMalloc((void**)&Nd, size);
cudaMemcpy(Nd, N, size, cudaMemcpyHostToDevice);
cudaMalloc((void**)&Pd, size);
dim3 dimGrid(Width / TILE_WIDTH, Height / TILE_WIDTH);
dim3 dimBlock(TILE_WIDTH, TILE_WIDTH);
MatrixMulKernel_01 <<<dimGrid, dimBlock >> >(Md, Nd, Pd, Width);
cudaMemcpy(P, Pd, size, cudaMemcpyDeviceToHost);
//打印結果矩陣
for (int i = 0; i<16; i++)
{
for (int j = 0; j<16; j++)
{
printf("%.2f ", P[i][j]);
}
printf("\n");
}
cudaFree(Md);
cudaFree(Nd);
cudaFree(Pd);
return 0;
}