本章内容:
- 通过配置文件驱动的方法优化内核
- 理解线程束执行的本质
- 增大GPU的并行性
- 掌握网格和线程块的启发式配置
- 学习多种CUDA的性能指标和事件
- 了解动态并行与嵌套执行
3.1 CUDA执行模型概述
3.1.1 GPU架构概述
GPU是围绕SM(流式多处理器)来构建的。何为SM,我自己的通俗理解是,我们希望GPU高性能执行多线程计算,而这些线程就是放在SM上的,SM是一个设备层面的概念。之前第二章我们是按照线程块来组织一维、二维和三维的线程模型,然后这些线程就会以块为单位放到SM上,然后每一个线程块的32个线程被组成为一个单位,称作线程束,线程束可以真正实现并行计算,例如包含128个线程的线程块,放在SM上后就会被组织为4个线程束,然后SM来调度线程束,每次让一个线程束进行并行计算。
上图就能很好的体现线程块与SM的关系,我们在代码编写层面,通过网格和线程块组织线程,而实际上线程块以线程束为单位放到SM上,每个线程也对应于一个CUDA核心。一个线程快只能反正一个SM上,SM很大,可以容纳很多线程块,而且一旦线程块选定一个SM,就必须在这个SM上执行完毕,不再切换新的SM 。
3.2 理解线程束执行的本质
3.2.1 线程、线程块、线程束的关系:
线程束 = 32个连续的线程,真正并行
线程块 = 一维、二维、三维布局的线程
3.2.2 线程束分化:
我们之前说过,CUDA编程模型是一种单指令多线程模型,也就是说一个线程束内的所有线程会执行同一个指令(或者说同一个核函数),然而如果出现了一些分支,例如:
if(tid % 2 == 0){
doA();
}else{
}
那么线程束就会出现分化。结论告诉我们,不建议CUDA函数出现太多分化,否则会导致性能下降(有效加载率降低),例如上面的例子,一个线程束汇总只有一半的线程执行 doA(),而其他16个线程上面都不干,但是他们都被调度了,这就造成了严重的资源浪费。
3.2.3 资源分配
线程束的本地执行上下文主要由以下资源组成:程序计数器、寄存器和共享内存。由SM处理的每个线程束的执行上下文,在整个线程束的生存期中是保存在芯片内,因此,从一个执行上下文切换到另一个执行上下文没有损失。
在每个SM中有固定的寄存器和共享内存,如果每个线程消耗的寄存器和共享内存更多,那么这个SM中能装载的线程束就越少。
3.2.4 延迟隐藏
SM依赖于线程束并行,其利用率和活跃的线程束有关系。何为指令延迟:在指令发出和完成之间的时间周期被定义为指令延迟,当每个时间周期内所有的线程调度器都有一个符合被调度条件的线程束时,可以达到计算资源的完全利用。指令延迟主要分为两种:算术指令(大概10-20个指令周期),内存指令(400-800个周期);
举个例子:假如你有一个程序,计算执行一遍要10分钟,如果你就程序运行10分钟的时候干等着啥也不干,这效率有点低,因为计算机很多计算资源没是空闲的,浪费了。如果你在程序执行的时候,再次执行一遍程序,当然换个别的参数,有点像深度学习训练模型,你换了一批数据集,这样在第一个程序执行的过程中,你同时运行了另一个程序,提升了计算机资源的利用率,再比如你每经过1分钟就执行一遍程序,那么在计算机运行的接下来十分钟内,你一共又运行了十遍程序,如下图
黄色代表加载程序,然后蓝色代表程序执行,如果只是串行,那么你完成一个程序需要11s,但是如果你在程序执行的时候加载第二个程序,那么效率就能提高,上图在21秒内,加载了11个程序,平均一个程序是21/11 秒。这就是延时隐藏。
3.2.5 占用率
3.2.6 同步
1、系统级别同步:等待主机和设备完成工作
cudaError_t cudaDeviceSynchronize(void);
2、块级别:同一个线程块中线程的同步:
__device__ void __syncthreads(void);
3.3 并行性表现
头文件和cu文件如下:
#pragma once
#ifndef MYCUDA_H
#define MYCUDA_H
#define CHECK(call)\
{\
const cudaError_t error=call;\
if(error!=cudaSuccess)\
{\
printf("ERROR: %s:%d,",__FILE__,__LINE__);\
printf("code:%d,reason:%s\n",error,cudaGetErrorString(error));\
exit(1);\
}\
}
#include <stdlib.h>
#include <cuda_runtime.h>
#include <stdio.h>
#include <time.h>
#ifdef _WIN32
# include <windows.h>
#else
# include <sys/time.h>
#endif
#ifdef _WIN32
int gettimeofday(struct timeval* tp, void* tzp)
{
time_t clock;
struct tm tm;
SYSTEMTIME wtm;
GetLocalTime(&wtm);
tm.tm_year = wtm.wYear - 1900;
tm.tm_mon = wtm.wMonth - 1;
tm.tm_mday = wtm.wDay;
tm.tm_hour = wtm.wHour;
tm.tm_min = wtm.wMinute;
tm.tm_sec = wtm.wSecond;
tm.tm_isdst = -1;
clock = mktime(&tm);
tp->tv_sec = clock;
tp->tv_usec = wtm.wMilliseconds * 1000;
return (0);
}
#endif
double cpuSecond()
{
struct timeval tp;
gettimeofday(&tp, NULL);
return((double)tp.tv_sec + (double)tp.tv_usec * 1e-6);
}
void initialData(float* ip, int size)
{
time_t t;
srand((unsigned)time(&t));
for (int i = 0; i < size; i++)
{
ip[i] = (float)(rand() & 0xffff) / 1000.0f;
}
}
void initialData_int(int* ip, int size)
{
time_t t;
srand((unsigned)time(&t));
for (int i = 0; i < size; i++)
{
ip[i] = int(rand() & 0xff);
}
}
void printMatrix(float* C, const int nx, const int ny)
{
float* ic = C;
printf("Matrix<%d,%d>:\n", ny, nx);
for (int i = 0; i < ny; i++)
{
for (int j = 0; j < nx; j++)
{
printf("%6f ", ic[j]);
}
ic += nx;
printf("\n");
}
}
void initDevice(int devNum)
{
int dev = devNum;
cudaDeviceProp deviceProp;
CHECK(cudaGetDeviceProperties(&deviceProp, dev));
printf("Using device %d: %s\n", dev, deviceProp.name);
CHECK(cudaSetDevice(dev));
}
void checkResult(float* hostRef, float* gpuRef, const int N)
{
double epsilon = 1.0E-6;
for (int i = 0; i < N; i++)
{
if (abs(hostRef[i] - gpuRef[i]) > epsilon)
{
printf("Results don\'t match!\n");
printf("%f(hostRef[%d] )!= %f(gpuRef[%d])\n", hostRef[i], i, gpuRef[i], i);
return;
}
}
printf("Check result success!\n");
}
#endif//MYCUDA_H
#include "freshman_z.h"
#include <stdio.h>
void sumMatrix2D_CPU(float* MatA, float* MatB, float* MatC, int nx, int ny)
{
float* a = MatA;
float* b = MatB;
float* c = MatC;
for (int j = 0; j < ny; j++)
{
for (int i = 0; i < nx; i++)
{
c[i] = a[i] + b[i];
}
c += nx;
b += nx;
a += nx;
}
}
__global__ void sumMatrix(float* MatA, float* MatB, float* MatC, int nx, int ny)
{
int ix = threadIdx.x + blockDim.x * blockIdx.x;
int iy = threadIdx.y + blockDim.y * blockIdx.y;
int idx = ix + iy * ny;
if (ix < nx && iy < ny)
{
MatC[idx] = MatA[idx] + MatB[idx];
}
}
int main(int argc,char* argv[]) {
printf("ZYH study CUDA ...\n");
initDevice(0);
int nx = 1 << 14;
int ny = 1 << 14;
int nxy = nx * ny;
int nBytes = nxy * sizeof(float);
Malloc
float* A_host = (float*)malloc(nBytes);
float* B_host = (float*)malloc(nBytes);
float* C_host = (float*)malloc(nBytes);
float* C_from_gpu = (float*)malloc(nBytes);
initialData(A_host, nxy);
initialData(B_host, nxy);
cudaMalloc
float* A_dev = NULL;
float* B_dev = NULL;
float* C_dev = NULL;
CHECK(cudaMalloc((void**)&A_dev, nBytes));
CHECK(cudaMalloc((void**)&B_dev, nBytes));
CHECK(cudaMalloc((void**)&C_dev, nBytes));
CHECK(cudaMemcpy(A_dev, A_host, nBytes, cudaMemcpyHostToDevice));
CHECK(cudaMemcpy(B_dev, B_host, nBytes, cudaMemcpyHostToDevice));
int dimx = argc > 2 ? atoi(argv[1]) : 32;
int dimy = argc > 2 ? atoi(argv[2]) : 32;
double iStart, iElaps;
// cpu compute
iStart = cpuSecond();
sumMatrix2D_CPU(A_host, B_host, C_host, nx, ny);
iElaps = cpuSecond() - iStart;
printf("CPU Execution Time elapsed %f sec\n", iElaps);
warm up
2d block and 2d grid
dim3 block_0(32, 32);
dim3 grid_0((nx - 1) / block_0.x + 1, (ny - 1) / block_0.y + 1);
iStart = cpuSecond();
sumMatrix << <grid_0, block_0 >> > (A_dev, B_dev, C_dev, nx, ny);
CHECK(cudaDeviceSynchronize());
printf("Warm Up \n");
2d block and 2d grid
dim3 block(dimx, dimy);
dim3 grid((nx - 1) / block.x + 1, (ny - 1) / block.y + 1);
iStart = cpuSecond();
sumMatrix << <grid, block >> > (A_dev, B_dev, C_dev, nx, ny);
CHECK(cudaDeviceSynchronize());
iElaps = cpuSecond() - iStart;
printf("GPU Execution configuration<<<(%d,%d),(%d,%d)>>> Time elapsed %f sec\n",
grid.x, grid.y, block.x, block.y, iElaps);
CHECK(cudaMemcpy(C_from_gpu, C_dev, nBytes, cudaMemcpyDeviceToHost));
checkResult(C_host, C_from_gpu, nxy);
cudaFree(A_dev);
cudaFree(B_dev);
cudaFree(C_dev);
free(A_host);
free(B_host);
free(C_host);
free(C_from_gpu);
cudaDeviceReset();
return 0;
}
nvcc.exe File4.cu -o test4.exe
demo的运行时间图上所示,很奇怪的是,笔者的实验结构与书中的结果不同?因为GPU型号之类的差异???可能是吧,书里是32和16的线程块大小会有最佳的性能。
3.3.1 使用nvprof工具检测活跃线程束以及内存吞吐量、全局加载效率。
上一章提到了nvprof工具,但是现在新的cuda版本不支持这个工具了,换成ncu,查了一下对应的参数,具体命令如下所示,这个连接有各种cuda性能参数的介绍和表
// 检测活跃线程束
ncu --metrics sm__warps_active.avg.pct_of_peak_sustained_active test4.exe 32 16
//内存吞吐量
ncu --metrics l1tex__t_bytes_pipe_lsu_mem_global_op_ld.sum.per_second test4.exe 32 16
//全局加载效率
ncu --metrics smsp__sass_average_data_bytes_per_sector_mem_global_op_ld.pct test4.exe 32 16
3.3.2 增大并行性
3.4 避免分支分化
何为分支分化?之前讲到过,一个SM上有一个线程束(32个线程)并行执行,但是如果在核函数中出现了判断分支,例如(if(idx % 2 == 0)),这样就会导致奇数线程和偶数线程产生分化,分化会引起效率低下。本节就是要介绍如何避免分化
3.4.1 并行规约问题
假如有一个N个元素的数组,要求和,很简单的思路:
int sum = 0;
for(int i = 0;i < N;i++){
sum += nums[i];
}
我们也能想到一些递归的方法来求解:例如相邻相加和交错相加