CUDA学习笔记(二)
参考教程:
1. QINZHAOYU/CudaSteps
2. cuda编程(一)基础
3. CUDA C/C++ 教程一:加速应用程序
四. CUDA 程序的错误检测
1. 运行CUDA api时候添加如下宏:
#define CHECK(call) \
do { \
const cudaError_t error_code = call; \
if (error_code != cudaSuccess) \
{ \
printf("CUDA ERROR: \n"); \
printf(" FILE: %s\n", __FILE__); \
printf(" LINE: %d\n", __LINE__); \
printf(" ERROR CODE: %d\n", error_code); \
printf(" ERROR TEXT: %s\n", cudaGetErrorString(error_code)); \
exit(1); \
} \
}while(0);
2. 核函数检查
因为核函数没有返回值
,所以无法直接检查核函数错误。间接的方法是,在调用核函数后执行:
CHECK(cudaGetLastError()); // 捕捉同步前的最后一个错误。
CHECK(cudaDeviceSynchronize()); // 同步主机和设备。
3. 设置环境变量 CUDA_LAUNCH_BLOCKING为1
这样所有核函数的调用都将不再是异步的,而是同步的。主机调用一个核函数之后必须等待其执行完,才能向下执行。
一般仅用于程序调试。
export CUDA_LAUNCH_BLOCKING=1
4. CUDA-MEMCHECK 检查内存错误
工具集
:memcheck, racecheck, initcheck, synccheck
cuda-memcheck ./bin/check.exe
详情参考:CUDA-MEMCHECK
五. GPU 加速的关键
- CUDA 事件计时
cudaEvent_t start, stop;
CHECK(cudaEventCreate(&start)); // 创建cuda 事件对象。
CHECK(cudaEventCreate(&stop));
CHECK(cudaEventRecord(start)); // 记录代表开始的事件。
cudaEventQuery(start); // 强制刷新 cuda 执行流。
// run code.
CHECK(cudaEventRecord(stop));
CHECK(cudaEventSynchronize(stop)); // 强制同步,让主机等待cuda事件执行完毕。
float elapsed_time = 0;
CHECK(cudaEventElapsedTime(&curr_time, start, stop)); // 计算 start 和stop间的时间差(ms)。
printf("host memory malloc and copy: %f ms.\n", curr_time - elapsed_time);
- 计算强度较小时数据传输的性能对程序总耗时影响更大,双精度版本基本上比单精度版本耗时多一倍。
编译时候指定C++优化等级-O3
,指定GPU计算能力-arch=sm_50
,双精度版本-DUSE_DP
(耗时长)。
nvcc -O3 -arch=sm_50 -DUSE_DP -o ./bin/clock.exe add.cu clock.cu main.cpp
- 程序性能查看 Nvidia Nsight Systems
nvprof ./bin/clock
- cuda数学库
https://docs.nvidia.com/cuda/cuda-math-api/index.html
六. CUDA的内存组织
CUDA 中的内存类型有:全局内存、常量内存、纹理内存、寄存器、局部内存、共享内存
。
- 全局内存(global memory)
即核函数中所有线程
都可以访问的内存,可读可写,由主机端分配和释放,如cudaMalloc() 的设备内存
特点
:
- 较高的延迟和较低的访问速度,但是容量大(显存);
- 主要为核函数提供数据,并在主机和设备、设备和设备之间传递数据。
- 由主机端维护,期间不同的核函数可以多次访问全局内存。
静态全局内存变量
__device__ real epsilon; // 单个静态全局内存变量, `__device` 表示是设备中的变量。
__device__ real arr[10]; // 固定长度的静态全局内存数组变量。
访问权限:
- 核函数中可以直接访问静态全局内存变量,不必以参数形式传给核函数;
- 主机中不可以直接访问静态全局内存变量,可以通过
cudaMemcpyToSymbol() 和 cudaMemcpyFromSymbol()
调用。
2.常量内存
- 仅有 64 kb,可见范围和生命周期与全局内存一样;
- 具有缓存,从而高速;
- 常量内存仅可读、不可写。
3.纹理内存
(texture memory),类似常量内存。
将某些只读的全局内存数据用 __ldg()
函数通过只读数据缓存(read-only data cache)读取,
既可以达到使用纹理内存的加速效果,
全局内存的读取在默认情况下就利用了 __ldg() 函数,所以不需要显式地使用
。
4.寄存器
- 在核函数中定义的、不加任何限定符的变量一般存放在寄存器
(register)
; - 核函数中不加任何限定符的数组可能放在寄存器,也可能放在局部内存中。
- 寄存器可读可写。
5.共享内存
- 共享内存(shared memory)与寄存器类似,都是
位于芯片上
,读写速度较快
。 - 共享内存对
整个线程块可见
,一个线程块上的所有线程都可以访问共享内存上的数据;共享内存的生命周期也与所属线程块一致。 - 共享内存的主要作用是减少对全局内存的访问,或者改善对全局内存的访问模式。
6.L1 和 L2 缓存
七. 全局内存的合理使用
对全局内存的访问将触发内存事务,即数据传输。
一次 数据传输处理 的数据量在默认情况下是 32 字节
。 一次数据传输中,从全局内存转移到 L2 缓存的一片内存的首地址一定是 32 的整数倍。
也就是说,一次数据传输只能从全局内存读取地址为 0-31 字节、32-63 字节等片段的数据。
合并度
:如果所有数据传输处理的数据都是线程束所需要的,则合并度为 100%,即 合并访问;
否则,即为 非合并访问。
矩阵转置
八. 共享内存的合理使用
作用:
- 减少核函数中对全局内存的访问次数,实现
高效
的线程块内部的通信; - 提高全局内存访问的
合并度
。
- 数组规约
折半规约法,通过线程块对数据分片归约
,最后再一并求和。 __syncthreads()
:实现一个线程块中所有线程按照代码出现的顺序执行指令,但是不同线程块之间依然是独立、异步的。- 共享内存变量,可以在核函数中通过限定符
__shared__
定义一个共享内存变量,
九. 原子函数的合理使用
cuda 中,一个线程的原子操作
可以在不受其他线程的任何操作的影响下
完成对某个(全局内存或共享内存)数据的一套“读-改-写”
操作。
9.1 完全在 GPU 中进行归约
-
什么是
归约
?
[参考]:Cuda归约运算
归约(redution
)是一类并行算法,对传入的O(N)个输入数据,使用一个二元的复合结合律的操作符,生成O(1)的结果。这类操作包括取最小、最大、求和、平方求和、逻辑与、逻辑或、向量点积。归约也是其他高级运算中要用的基础算法。 -
有两种方法能够在GPU中得到最终结果
- 用另一个核函数将较短的数组进一步归约;
- 在核函数末尾利用原子函数进行归约。
- 在代码实现中:
- 原子函数 atomicAdd(·)执行数组的一次完整的读-写操作;
- 传给 cudaMemcpy(·) 的主机内存可以是栈内存,也可以是堆内存;
- 主机函数可以和设备函数同名,但要遵循重载原则(参数列表不一致)。
9.2 原子函数
原子函数对其第一个参数指向的数据进行一次“读-写-改”
的原子操作,是不可分割的操作。第一个参数可以指向全局内存
,也可以指向共享内存
。
原子函数的返回值为所指地址的旧值
。
常见原子函数:
- 加法: T atomicAdd(T *address, T val);
- 减法: T atomicSub(T *address, T val);
- 交换: T atomicExch(T *address, T val);
- 最小值: T atomicMin(T *address, T val);
- 最大值: T atomicMax(T *address, T val);
- 自增: T atomicInc(T *address, T val);
- 自减: T atomicDec(T *address, T val);
- 比较交换: T atomicCAS(T *address, T compare, T val);