写在前面:
矩阵相乘大家应该都不陌生。
设有两个矩阵M和N,假设M和N都是方阵,维度均为width × width
如果M和N均为1000 × 1000的矩阵,总共要进行1000000次点乘。其中,每次点乘有1000次乘法和1000次加法。
先来看看使用普通的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端上实现矩阵相乘。
可以看到总共有3步:
可以看到这里有2个问题
#include
#include
#include
#include
#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可以自己做自己的事情。
优化后的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带宽
回顾之前的程序,实际上,每个输入元素被Width个线程读取(对于每一列都读取了Width次),所以使用shared memory来减少global memory带宽需求。
把kernel拆分成多个阶段
每个线程
代码如下:
__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;具体根据不同的计算能力查表得到。
以G80为例:16KM/SM 并且 B blocks/SM;
2KB/block
1KB给Nds,1KB给Mds(16 * 16 *4)
TITLE_WIDTH = 16
更大的TITLE_WIDTH将导致更少的块数
Shared memory瓦片化的好处
G80线程尺寸的考虑
Atomic Functions 原子操作
//算数运算
atomicAdd();
atomicSub();
atomicExch();
atomicMin();
atomicMax();
atomicDec();
atomicCAS();
//位运算
atomicAnd();
atomicOr();
atomicXor();
#include
#include
#include
#include
/*
* 去除长度限制 & 使用共享内存 的矩阵相乘优化
*/
#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;
}