CUDA学习笔记(二)

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 加速的关键

  1. 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);  
  1. 计算强度较小时数据传输的性能对程序总耗时影响更大,双精度版本基本上比单精度版本耗时多一倍。
    编译时候指定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
  1. 程序性能查看 Nvidia Nsight Systems
nvprof ./bin/clock
  1. cuda数学库
    https://docs.nvidia.com/cuda/cuda-math-api/index.html

六. CUDA的内存组织

CUDA 中的内存类型有:全局内存、常量内存、纹理内存、寄存器、局部内存、共享内存

  1. 全局内存(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%,即 合并访问;
否则,即为 非合并访问。

矩阵转置

八. 共享内存的合理使用

作用

  • 减少核函数中对全局内存的访问次数,实现高效的线程块内部的通信;
  • 提高全局内存访问的合并度
  1. 数组规约
    折半规约法,通过线程块对数据分片归约,最后再一并求和。
  2. __syncthreads():实现一个线程块中所有线程按照代码出现的顺序执行指令,但是不同线程块之间依然是独立、异步的。
  3. 共享内存变量,可以在核函数中通过限定符__shared__ 定义一个共享内存变量,

九. 原子函数的合理使用

cuda 中,一个线程的原子操作可以在不受其他线程的任何操作的影响下完成对某个(全局内存或共享内存)数据的一套“读-改-写”操作。

9.1 完全在 GPU 中进行归约

  1. 什么是归约
    [参考]:Cuda归约运算
    归约(redution)是一类并行算法,对传入的O(N)个输入数据,使用一个二元的复合结合律的操作符,生成O(1)的结果。这类操作包括取最小、最大、求和、平方求和、逻辑与、逻辑或、向量点积。归约也是其他高级运算中要用的基础算法。

  2. 有两种方法能够在GPU中得到最终结果

  • 用另一个核函数将较短的数组进一步归约;
  • 在核函数末尾利用原子函数进行归约。
  1. 在代码实现中:
  • 原子函数 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);

猜你喜欢

转载自blog.csdn.net/weixin_36354875/article/details/128287384