CUDA入门(三):CUDA存储器
前两节中写了如何编写调用大量线程来计算矩阵加法和乘法的 kernel 函数。但这些线程要处理的数据是先从全局储存器中访问,而全局储存器是通过 DRAM 来实现,访问速度慢,而且它的路径上容易发生拥塞现象,只允许很少线程继续访问,因此导致一些 SM 处于空闲状态。下面主要解决存储器的访问速度慢的问题。
1. 存储区访问效率的重要性
矩阵乘法中每个线程就是计算一个结果矩阵中的值:
for(int k = 0;k < width;++k)
PValue += d_M[Row * width + k] * d_N[k * Width + Col]; 该循环每次循环都访问两边存储器,d_M、d_N 各一次,并执行两次浮点运算,乘法一次,加法一次。浮点运算与全局存储器访问操作的比值(compute to Global Memory Access,CFMA)就等于1.0。
当今许多设备的全局存储器访问宽带达到了 200GB/S,每个单精度浮点数占 4 个字节,那么单精度数据的加载速度不高于 50GFLOPS。 如果 CFMA 为 1.0 那么 kernel 函数中每秒可执行浮点数运算的不高于 50GFLOPS,这对于许多高端 GPU 能达到的峰值性能 1500GFLOPS 是挺少的了。所以我们要提高 CFMA 来提高 GPU 的利用率。
2. CUDA 设备的存储器的类型
下图底部,有全局存储器和常数存储器,主机代码可以对两者进行读写。主机可以访问设备全局存储器,与设备之间传输和复制数据。当所有线程同时访问相同位置时,常数存储器为设备提供短延时、高宽带和只读访问。
寄存器和共享存储器是片上存储器。这两种存储器中的变量可以以高度并行的方式访问。寄存器分配给单个线程,每个线程只能访问分配给自己的寄存器。共享存储器分配给线程块,同一个块中的所有线程都可以访问共享存储器中的变量。
下图是现代冯诺依曼模型机中的主存和寄存器,访问主存比访问寄存器中的数据需要多出访存的指令,而且主存利用 DRAM 技术实现,延迟高,所以访问主存中的数据比访问寄存器中的慢。
CUDA 模型中的全局存储器相当于冯诺依曼模型中的主存,寄存器相当于冯诺依曼模型中的寄存器堆。
处理单元和线程: 现代计算机中的一个线程相当于冯诺依曼模型中的一个处理器。现代处理器提供了上下文切换功能,多个线程分时共享处理器,可以暂停一个线程,执行其他线程,然后再重新启动这个线程。一些处理器提供了多个处理单元,允许多个线程同时执行,下图展示了 SIMD 的设计模式,所有的处理单元共享一个 PC 和 IR 寄存器,所有线程执行同一条指令。
共享存储器与寄存器虽然都是片上存储器,但共享存储器是芯片上存储空间的一部分。处理器访问局部存储器还是要执行内存加载操作,与访问全局存储器一样。然而共享存储器在片内访问共享存储器比访问全局存储器有更低的延迟和更高的宽带,由于需要内存加载操作,所以比寄存器有更高的延迟和更低的宽带。
在 CUDA ***享存储器和寄存器之间的一个重要区别是共享存储器中的变量可以被线程块中的所有线程共享,而寄存器对线程是私有的。
变量声明:
local memory 的数据是被保存在显存中的,速度很慢。无法确定大小的数组和较大的结构体都会放在局部存储器中。
使用\local__ ,__shared__ ,__constant__ 时,__device__可选。
CUDA 指针: 用来指向全局存储器中的数据对象。在 kernel 函数和设备函数中有两种使用指针的方法。
1. 如果一个对象是主机端函数分配的,则指向对象的指针由 cudaMalloc() 函数初始化,并且可以作为参数传递给 kernel 函数。
//in host function float *A; cudaMelloc((void **)&A, size);
2. 把全局存储器中变量的地址赋予指针变量,例如:
//in kernel function float *ptr = &GlobalVar;
3. 用分块减少内存流量
我们可以先将要计算的元素分批从全局存储器中加载到每个块内的共享存储器,以此来减少访问全局存储器所带来的内存消耗。下面以两个 4 x 4 方阵、TILE_WIDTH 大小为 16 为例:
block[0][0] 在
阶段 1 会将用 4 个线程将左侧矩阵左上角 2 x 2 的 4 个元素与上方矩阵左上角 2 x 2 的 4 个元素加载到共享存储器的方阵中,然后 4 个线程同时计算对应共享存储器中小向量的内积并累加到 PValue 中,这里的 PValue 是自动类型,每个线程中的寄存器都会保存一个副本。
阶段 2 将将用 4 个线程将左侧矩阵右上角 2 x 2 的 4 个元素与上方矩阵左下角 2 x 2 的 4 个元素加载到共享存储器的方阵中,然后 4 个线程同时计算对应共享存储器中小向量的内积并累加到 PValue 中。此时针对 block[0][0] 的计算结束。
在计算 block[0][0] 同时,block[0][1],block[1][0],block[1][1]也在进行同样的运算,当所有 block 的阶段 2 结束后,矩阵乘法完成。
矩阵乘法的 kernel 函数:
#define TILE_WIDTH 16
__global__ void MatrixMulKernel(float *d_M, float *d_N, float *d_P, int m, int k, int n){
__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 * blockDim.y + ty;
int col = bx * blockDim.x + tx;
float Pvalue = 0; //in every thread
for(int ph = 0; ph < ceil(k/float(TILE_WIDTH));++ph){
if(row < m && (ph * TILE_WIDTH + tx) < k)
Mds[ty][tx] = d_M[row * k + ph * TILE_WIDTH + tx];
else Mds[ty][tx] = 0.0;
if(col < n && (ph * TILE_WIDTH + ty) < k)
Nds[ty][tx] = d_N[(ph * TILE_WIDTH + ty) * n + col];
else Nds[ty][tx] = 0.0;
__syncthreads(); //保证所有需要的元素都加载完成,再进行运算
for(int i = 0;i < TILE_WIDTH;++i){
Pvalue += Mds[ty][i] * Nds[i][tx];
}
__syncthreads(); //保证所有线程都计算完成再进入下一个循环。
}
if (row < m && col < n)//将计算后的矩阵块放到结果矩阵d_P中
d_P[row*n + col] = Pvalue;
} 1. Row 和 Col 是每个线程需要计算的 d_P 元素的行索引和列索引。
2. 第一个循环遍历了计算最终 d_P 的所有阶段,例如上面实例的阶段1 和 阶段 2。16、17 行把要参与计算的两个矩阵块加载到共享存储器中。
Mds[ty][tx] = d_M[Row*Width + m*TILE_WIDTH + tx]; 是每个线程块中的每个线程加载元素d_M[Row*Width + m*TILE_WIDTH + tx]。由于 Row 的值是 by 与 ty 的二元线性函数,所以每个线程都会加载唯一的一个元素到各自块内共享存储器中。
- Nds[ty][tx] = d_N[(n*TILE_WIDTH + ty)*Width + Col]; 是每个线程块中的每个线程加载元素 d_N[(nTILE_WIDTH + ty)Width + Col]。
3. 第二个循环计算每个线程的部分值,循环完所有阶段后即可得到最后的值。
完整的 CUDA 分块矩阵乘法代码:
#include
#include
#define TILE_WIDTH 32
__global__ void MatrixMulKernel(float *d_M, float *d_N, float *d_P, int m, int k, int n){
__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 * blockDim.y + ty;
int col = bx * blockDim.x + tx;
float Pvalue = 0; //in every thread
for(int ph = 0; ph < ceil(k/float(TILE_WIDTH));++ph){
if(row < m && (ph * TILE_WIDTH + tx) < k)
Mds[ty][tx] = d_M[row * k + ph * TILE_WIDTH + tx];
else Mds[ty][tx] = 0.0;
if(col < n && (ph * TILE_WIDTH + ty) < k)
Nds[ty][tx] = d_N[(ph * TILE_WIDTH + ty) * n + col];
else Nds[ty][tx] = 0.0;
__syncthreads();
for(int i = 0;i < TILE_WIDTH;++i){
Pvalue += Mds[ty][i] * Nds[i][tx];
}
__syncthreads();
}
if (row < m && col < n)//将计算后的矩阵块放到结果矩阵d_P中
d_P[row*n + col] = Pvalue;
}
void gemm_cuda_shared(float *A, float *B, float *C, int m, int k, int n){
float *d_A, *d_B, *d_C;
size_t size = m * k * sizeof(float);
cudaMalloc(&d_A, size);
cudaMemcpy(d_A, A, size,
cudaMemcpyHostToDevice);
size = k * n * sizeof(float);
cudaMalloc(&d_B, size);
cudaMemcpy(d_B, B, size,
cudaMemcpyHostToDevice);
size = m * n * sizeof(float);
cudaMalloc(&d_C, size);
dim3 dimBlock(TILE_WIDTH, TILE_WIDTH);
dim3 dimGrid(ceil(m/(float)TILE_WIDTH), ceil(n/(float)TILE_WIDTH));
MatrixMulKernel>>(d_A, d_B, d_C, m, k, n);
cudaMemcpy(C, d_C, size,
cudaMemcpyDeviceToHost);
cudaFree(d_A);
cudaFree(d_B);
cudaFree(d_C);
}
int main(int argc, char ** argv){
if(argc!=4){
printf("please input right parameter a,b,c\n");
exit(0);
}
int m = atoi(argv[1]);
int k = atoi(argv[2]);
int n = atoi(argv[3]);
float *A = (float *)malloc(sizeof(float) * m * k);
float *B = (float *)malloc(sizeof(float) * k * n);
float *C = (float *)malloc(sizeof(float) * m * n);
for (int i = 0; i < k;i ++){
for(int j = 0;j < m;j++){
A[i*m + j] = 1;
}
}
for(int i = 0;i < k;i++){
for(int j = 0;j < n;j++){
B[i*n + j] = 1;
}
}
gemm_cuda_shared(A, B, C, m, k, n);
for (int i = 0; i < m;i ++){
for(int j = 0;j < n;j++){
printf("%f ", C[i*n+j]);
}
printf("\n");
}
free(A);
free(B);
free(C);
return 0;
} 参考:https://icode.best/i/93343835170848
🎉到此 CUDA 存储器的讲解就到此结束了,本文主要讲解各种存储器的用途与声明方式,并使用共享存储器对矩阵乘法进行了优化,整体来说还是比较详细的,如果有讲解不到位或有误的地方恳请大家批评与交流
希望大家多多关注,三连支持。你们的支持是我源源不断创作的动力。

