CUDA进阶之路
按权限分:
Constant cache & Texture cache:
对HOST端可读可写;对线程,只读。
除了以上两个对线程只读,其余都是可读可写,但是共享程度有所区别。
GPU最快的寄存器,kernel中没有特殊声明的自动变量都是放在寄存器中。
用__shared__修饰符修饰的变量存放在shared memory:
__syncthreads()
同步问题很少显示的去设置和使用,在数据、数据结构不够大的情况下。若Register不够了,会用local memory来替代。
更多在以下情况使用:
--ptxas-options=-v
固定内存空间 驻留在设备内存中(DRAM),并缓存在固定缓存中(constant cache):
和Constant Memory类似。驻留在设备内存中,且有一个只读cache。专门为那些在内存访问模式中存在大量空间局部性(Spatial Locality)的图形应用程序设计的。即,一个Thread读取的位置和邻近Thread读取的位置“非常接近”
空间最大,latency最高,GPU最基础的memory
需要
涉及一个问题, 每次是给一连串连续的数据,如果采用行读取,对于A来说,A0,1,A0,2,A0,3来说没人读取就浪费了。如果后面的数据也被相邻的线程使用,效率就会变得更高。需要数据被读取的时候保持:连续线程访问读取连续的数据。称之为:memory transaction对齐,合并访存。
可以的看到在一个step里,一个iteration下每个线程是读取哪些数据。可以看到按列读取的时候,iteration更短。
最常用的为shared memory。
Shared memory可以被设置成16KB,32KB,48KB,剩下的交给L1缓存。
带宽可以使32b or 64bit
shared memory是block级别的,而register是thread级别的
而划分为逻辑块banks就是让shared memory更细分,变成thread级别的,而且比register更牛逼的是,他可以共享!
Shared memory可以被多个线程同时访问,为了克服访问瓶颈。shared memory划分为32个逻辑块banks。
bank具有的物理特性:
1、冲突:一个bank每个周期只能响应一个warp的一个地址申请 ——> 如果同一个warp中的不同线程访问同一个bank中的不同地址(两个必要条件),那么就会发生冲突,called Bank Conflict,这是最低效的。
2、广播:bank也有广播机制。当一个warp中的所有线程访问同一地址的共享内存时,会触发一个广播机制到中所有线程,这是最高效的。
3、多播:允许多个线程同时访问一个bank中的一个地址。
shared memory有两大功能:
一个BLOCK里面做bank和warp的映射,sData的大小是BLOCKSIZE。需要做到同一时间内同一个warp不能有不同线程能对应同一个bank。
一个warp是按线程编号顺序的一个32线程集合。
线程按行排,和bank方向一致,即可避免bank冲突。
//全局中取一个block大小的数据,可由matrix[index]索引到该数据
int ix = blockDim.x * blockIdx.x + threadIdx.x;
int iy = blockDim.y * blockIdx.y + threadIdx.y;
int index = iy * nx + ix; //因为在全局中存储方式是行优先,所以还是iy * nx + ix
__shared__ float sData[BLOCKSIZE][BLOCKSIZE];
if(ix < nx & iy < ny) {
sData[threadIdx.y][threadIdx.x] = matrix[index];
// 将全局的matrix[index]的值放到新设定的sData中的矩阵中去,
// 选择行优先的放法,线程按行排。
__syncthreads();
matrixTest[index] = sData[threadIdx.y][threadIdx.x];
// 当多个线程(束)一起寻址时,不会发生bank conflict
}
若线程按列排,
以上代码,索引原矩阵项不变,即matrix[index]还是原来的(因为原来访问的该怎么访问怎么访问)
唯一需要改的就是在shared memory中线程的排列把sData[threadIdx.y][threadIdx.x]
改为sData[threadIdx.x][threadIdx.y]
就可以实现下面的排列:
想要解决Bank Conflict只需要把sData[BLOCKSIZE][BLOCKSIZE]
改为sData[BLOCKSIZE][BLOCKSIZE + 1]
,索引threadIdx.x
和threadIdx.y
不变。使得造成一个大小的偏移(如果想造成一行的偏移?两个大小的偏移?都可以同理可得)
同样的道理 a[3][4] 和 b[4][5]
索引下标为相同的(2,3),但是偏移量不同。线性矩阵存储本质还是线性的。
//全局中取一个block大小的数据,可由matrix[index]索引到该数据
int ix = blockDim.x * blockIdx.x + threadIdx.x;
int iy = blockDim.y * blockIdx.y + threadIdx.y;
int index = iy * nx + ix; //因为在全局中存储方式是行优先,所以还是iy * nx + ix
__shared__ float sData[BLOCKSIZE][BLOCKSIZE + 1];
if(ix < nx & iy < ny) {
sData[threadIdx.x][threadIdx.y] = matrix[index];
// 将全局的matrix[index]的值放到新设定的sData中的矩阵中去,
// 选择行优先的放法,线程按行排。
__syncthreads();
matrixTest[index] = sData[threadIdx.x][threadIdx.y];
// 当多个线程(束)一起寻址时,不会发生bank conflict
}
当今矩阵运算速度的限制集中在访存的限制,我们的思想是把某些经常被访问的位置的元素在global memory的访存操作转移到对shared memory上,从而优化矩阵运算
如果想用shared memory就不得不面对以下几个问题:
想象着有序地(同步)进行如下操作:
1、把左侧矩阵的当前sub这个block放到tile_a中,上方矩阵放到tile_b中。
2、同步(等待都按行写入后,再进行下面的操作)
3、该block内的行列进行点积操作,并存储到局部变量tmp中。
4、同步(等待都进行完点积运算后,再写下一个block)
具体注释看代码:
naive的矩阵乘和shared memory优化版本,对比着看。
#define BLOCK_SIZE 16
__global__ void gpu_matrix_mult(int *a,int *b, int *c, int m, int n, int k)
{
int row = blockIdx.y * blockDim.y + threadIdx.y;
int col = blockIdx.x * blockDim.x + threadIdx.x;
int sum = 0;
if( col < k && row < m) // m * k
{
for(int i = 0; i < n; i++) // m * n n * k ——> m * k
{
sum += a[row * n + i] * b[i * k + col];
// m * n n * k
}
c[row * k + col] = sum;
}
}
__global__ void gpu_matrix_mult_shared(int *d_a, int *d_b, int *d_result, int n) // 只需要传入n这个参数
{
__shared__ int tile_a[BLOCK_SIZE][BLOCK_SIZE];
__shared__ int tile_b[BLOCK_SIZE][BLOCK_SIZE];
//方阵中 BLOCK_SIZE = blockDim.x = blockDim.y
int row = blockIdx.y * blockDim.y + threadIdx.y;
int col = blockIdx.x * blockDim.x + threadIdx.x;
int tmp = 0;
int idx;
for (int sub = 0; sub < gridDim.x; ++sub) // 每次走一个block大小的"步"(受限于shared_memory大小),能走gridDim.x步。(grid由block构成)
{
// 别忘了需要避免Bank Conflict,选用行优先存入tile_a分块矩阵中。
// 比上面的gpu_matrix_mult多了下面四行代码,实现将d_a复制到tile_a的操作
idx = row * n + sub * BLOCK_SIZE + threadIdx.x;
// 单看a中对应的矩阵块、
// 上方共row行,一行n个矩阵块,所以idx += row * n
// 前面(左侧)共有sub个block_size大小的block,threadIdx.x表示在该block内的哪个位置
tile_a[threadIdx.y][threadIdx.x] = row<n && (sub * BLOCK_SIZE + threadIdx.x)<n? d_a[idx]:0;
idx = (sub * BLOCK_SIZE + threadIdx.y) * n + col;
// 单看b对应的矩阵块
// 上方共有sub * BLOCK_SIZE大小的block,threadIdx.y表示在该block的哪个位置,两个加起来表示上方共这么多行
// col 表示矩阵块左侧有多少个单位。
tile_b[threadIdx.y][threadIdx.x] = col<n && (sub * BLOCK_SIZE + threadIdx.y)<n? d_b[idx]:0;
__syncthreads(); //等所有都放到tile矩阵中后才能保证下方加的tile中的值是对应的元素
for (int k = 0; k < BLOCK_SIZE; ++k) // 对tile_a进行读取。对于线程(row,col)要多次读取block_size大小的一行和一列
{
tmp += tile_a[threadIdx.y][k] * tile_b[k][threadIdx.x];
}
__syncthreads(); //等这一组blcok加完之后才能进行下一个block,要不然提前进入了会导致加错
}
// tmp是在register内的,线程级别的。里面存储了几个block步乘加的所有值,最后写到该位置对应的结果矩阵中。
if(row < n && col < n)
{
d_result[row * n + col] = tmp;
}
}
main函数(套路性):
int main(int argc, char const *argv[])
{
int m=1000;
int n=1000;
int k=1000;
int *h_a, *h_b, *h_c, *h_cc, *h_cs;
CHECK(cudaMallocHost((void **) &h_a, sizeof(int)*m*n));
CHECK(cudaMallocHost((void **) &h_b, sizeof(int)*n*k));
CHECK(cudaMallocHost((void **) &h_c, sizeof(int)*m*k));
CHECK(cudaMallocHost((void **) &h_cc, sizeof(int)*m*k));
CHECK(cudaMallocHost((void **) &h_cs, sizeof(int)*m*k));
cudaEvent_t start, stop,stop_share;
CHECK(cudaEventCreate(&start));
CHECK(cudaEventCreate(&stop));
CHECK(cudaEventCreate(&stop_share));
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
h_a[i * n + j] = 1;
}
}
for (int i = 0; i < n; ++i) {
for (int j = 0; j < k; ++j) {
h_b[i * k + j] = 0;
}
}
int *d_a, *d_b, *d_c, *d_c_share;
CHECK(cudaMalloc((void **) &d_a, sizeof(int)*m*n));
CHECK(cudaMalloc((void **) &d_b, sizeof(int)*n*k));
CHECK(cudaMalloc((void **) &d_c, sizeof(int)*m*k));
CHECK(cudaMalloc((void **) &d_c_share, sizeof(int)*m*k));
CHECK(cudaEventRecord(start));
// copy matrix A and B from host to device memory
CHECK(cudaMemcpy(d_a, h_a, sizeof(int)*m*n, cudaMemcpyHostToDevice));
CHECK(cudaMemcpy(d_b, h_b, sizeof(int)*n*k, cudaMemcpyHostToDevice));
unsigned int grid_rows = (m + BLOCK_SIZE - 1) / BLOCK_SIZE;
unsigned int grid_cols = (k + BLOCK_SIZE - 1) / BLOCK_SIZE;
dim3 dimGrid(grid_cols, grid_rows);
dim3 dimBlock(BLOCK_SIZE, BLOCK_SIZE);
gpu_matrix_mult<<<dimGrid, dimBlock>>>(d_a, d_b, d_c, m,n,k);
CHECK(cudaMemcpy(h_c, d_c, (sizeof(int)*m*k), cudaMemcpyDeviceToHost));
//cudaThreadSynchronize();
CHECK(cudaEventRecord(stop));
CHECK(cudaEventSynchronize(stop));
gpu_matrix_mult_shared<<<dimGrid, dimBlock>>>(d_a, d_b, d_c_share, n);
CHECK(cudaMemcpy(h_cs, d_c_share, (sizeof(int)*m*k), cudaMemcpyDeviceToHost));
CHECK(cudaEventRecord(stop_share));
CHECK(cudaEventSynchronize(stop_share));
float elapsed_time, elapsed_time_share;
CHECK(cudaEventElapsedTime(&elapsed_time, start, stop));
CHECK(cudaEventElapsedTime(&elapsed_time_share, stop, stop_share));
printf("Time_global = %g ms.\n", elapsed_time);
printf("Time_share = %g ms.\n", elapsed_time_share);
CHECK(cudaEventDestroy(start));
CHECK(cudaEventDestroy(stop));
//cpu_matrix_mult(h_a, h_b, h_c, m, n, k);
int ok = 1;
for (int i = 0; i < m; ++i)
{
for (int j = 0; j < k; ++j)
{
if(fabs(h_cs[i*k + j] - 0)>(1.0e-10))
{
printf("hcs: %d hc: %d ",h_cs[i*k + j], h_c[i*k + j]);
ok = 0;
}
}
}
if(ok)
{
printf("Pass!!!\n");
}
else
{
printf("Error!!!\n");
}
// free memory
CHECK(cudaFree(d_a));
CHECK(cudaFree(d_b));
CHECK(cudaFree(d_c));
CHECK(cudaFreeHost(h_a));
CHECK(cudaFreeHost(h_b));
CHECK(cudaFreeHost(h_c));
return 0;
}
读、写、算
cuda core —— 64 物理上同时最多有64个线程并行
逻辑上有很多个线程都可以并行
越来越多的是sm,这个级别的,是sm这个级别。cuda core 多的话同步的代价比较大。
越来越多种类的cuda core 一开始只有int 单精 双精
后来有Tensor core 、 transformer core ?
核函数,int a,b,c 都是在寄存器中的。
超过要补0。因为你选的一个tile_width不一定整除。
编写程序的时候,一个kernel核函数实际上就是最小的一个线程级别的函数。我们只需要管这个线程要实现怎样的操作即可,对于我们写这个函数来说硬件如何实现是透明的。
n个线程同时并行,实现并行运算。一个kernel实际上会启动很多线程,这些线程是逻辑上并行的,但是在物理层却并不一定。
这其实和CPU的多线程有类似之处,多线程如果没有多核支持,在物理层也是无法实现并行的。逻辑上和单核CPU一样,虽然只有一个核,但是还能实现多线程。
但是呢,硬件组织这些线程是有一定硬件逻辑在的。比如规划一个grid中如何组织block,一个block中如何组织线程。
然后对应着物理硬件层面,每个core某一刻只能对应一个thread,每32个thread构成一个warp,warp的意义是什么?block,一个block共享shared memory