一、内存管理
- 检查所有CUDA API返回值(如
cudaMalloc
,cudaMemcpy
),避免忽略错误。 - 释放分配的显存:每个
cudaMalloc
必须有对应的cudaFree
。 - 使用
cudaMallocManaged
时注意统一内存的页面迁移,避免频繁CPU/GPU数据迁移。 - 避免主机-设备内存混用:确保设备指针仅用于GPU函数。
- 正确使用
cudaMemcpy
参数:区分cudaMemcpyHostToDevice
和cudaMemcpyDeviceToHost
。 - 对齐内存访问:确保全局内存访问对齐到32字节(合并访问条件)。
- 共享内存初始化:使用
__shared__
时,需手动初始化或在核函数内赋值。 - 避免共享内存Bank冲突:确保同一线程束内的线程访问不同Bank。
- 释放锁页内存:
cudaHostAlloc
分配的主机内存需用cudaFreeHost
释放。 - 避免设备内存泄漏:长期运行的程序需显式释放临时内存。
二、线程与块配置
- 线程块大小限制:每块最多1024线程(多数架构),避免超限。
- 网格和块维度计算:确保总线程数覆盖问题规模,避免边界遗漏。
- 避免线程块内资源不足:共享内存和寄存器使用量需符合硬件限制。
- 用
blockDim.x * blockIdx.x + threadIdx.x
计算全局索引时检查越界。 - 线程束分化最小化:同一线程束内的线程尽量执行相同分支。
- 避免跨线程块的同步:
__syncthreads()
仅同步同一块内的线程。 - 动态共享内存分配:核函数调用时需指定第三参数(如
<<<grid, block, smem_size>>>
)。 - 线程束内投票函数:合理使用
__any_sync
/__all_sync
减少分支。 - 避免死锁:确保所有线程均执行同步操作(如
__syncthreads()
前无分支)。 - 优化块数量:每个SM需足够多的块以隐藏延迟。
三、核函数设计
- 核函数无返回值:必须声明为
__global__ void
。 - 参数传递限制:核函数参数需通过常量内存传递(最大4KB)。
- 避免递归核函数:除非启用动态并行(需特定GPU架构)。
- 设备函数修饰符:
__device__
函数不可被主机调用。 - 避免无限循环:核函数无法被外部终止,需设置退出条件。
- 浮点运算注意精度:GPU单精度(
float
)比双精度(double
)快但精度低。 - 原子操作竞争:慎用
atomicAdd
等,可能成为性能瓶颈。 - 避免核函数参数过多:结构体封装或使用常量内存。
- 核函数内避免系统调用:如
printf
需GPU支持且影响性能。 - 核函数启动前检查参数合法性:如指针非空、尺寸合理。
四、性能优化
- 全局内存合并访问:连续线程访问连续内存地址。
- 共享内存分块缓存:减少重复访问全局内存。
- 使用常量内存:适合只读数据(如
__constant__
)。 - 纹理内存加速访问:适合二维局部性访问模式。
- 寄存器优化:减少寄存器溢出(可用
maxrregcount
编译选项控制)。 - 循环展开:
#pragma unroll
减少分支开销。 - 避免线程发散:同一线程束内的线程执行相同控制流。
- 使用快速数学函数:如
__sinf
代替sinf
(牺牲精度换速度)。 - 隐藏延迟:增加线程块数量以提高SM利用率。
- 使用CUDA Streams:重叠计算与数据传输。
五、调试与错误处理
- 启用
cuda-memcheck
:检测内存越界、未初始化访问。 - 使用
cuda-gdb
或Nsight:断点调试核函数。 - 核函数错误检查:调用后执行
cudaGetLastError()
。 - 同步后检查错误:
cudaDeviceSynchronize()
后调用cudaGetLastError()
。 - 编译时启用警告:
-Wall -Wextra
(注意设备代码兼容性)。 - 打印调试信息:核函数内使用
printf
(需GPU支持)。 - 验证计算结果:将部分结果拷贝回主机并手动检查。
- 使用断言:
assert
在设备代码中需用assert.h
。 - 逐步测试:先验证小规模数据,再扩展。
- 检查设备兼容性:
cudaDeviceProp
查询架构、计算能力等。
六、API与工具链
- 头文件包含:确保包含
cuda_runtime.h
。 - 编译选项正确性:
-arch=sm_XX
匹配目标GPU架构。 - 避免混合工具链:如GCC与NVCC版本兼容性问题。
- 使用
__CUDA_ARCH__
宏:区分主机与设备代码路径。 - Pinned内存慎用:过量分配可能降低系统性能。
- 事件计时:用
cudaEvent_t
替代CPU计时,更精确。 - 多GPU编程:正确设置
cudaSetDevice
。 - 禁用WDDM超时:Windows下修改注册表避免长核函数超时。
- 使用CUDA Samples:参考官方示例代码结构。
- 更新驱动和工具包:避免已知Bug。
七、常见陷阱
- 设备代码不支持C++异常:需手动检查错误。
- 主机指针与设备指针混淆:如对设备指针取地址。
- 未初始化的设备内存:显存不会自动清零,需手动初始化。
- 共享内存竞争:未同步情况下多线程写入同一地址。
- 线程束内未保护共享资源:如无锁修改共享变量。
- 忽略缓存效应:全局内存访问可能被L1/L2缓存影响。
- 错误使用
__restrict__
:确保指针无别名。 - 误用
__global__
和__device__
修饰符。 - 动态并行深度限制:子核函数嵌套层数有限制。
- 未考虑GPU架构差异:如Turing与Ampere的差异。
八、代码规范
- 命名规范:如
d_data
表示设备指针,h_data
表示主机指针。 - 注释核函数参数:说明每个参数的意义和内存位置。
- 模块化代码:分离主机代码和设备代码到不同文件。
- 避免宏定义滥用:使用内联函数或模板代替。
- 版本控制:记录CUDA Toolkit和驱动版本。
- 代码格式化:提高可读性,减少低级语法错误。
- 限制全局变量:优先通过核函数参数传递数据。
- 错误处理封装:用宏或函数统一检查CUDA API结果。
- 文档记录优化策略:如为何选择特定块大小或内存类型。
- 避免硬编码:用常量或宏定义块大小、网格维度等。
九、高级特性
- 流的使用:多流并行需确保资源无冲突。
- 使用CUDA Graphs:优化频繁启动的小核函数。
- Zero-Copy内存:
cudaHostAlloc
的cudaHostAllocMapped
标志。 - Peer-to-Peer访问:多GPU间直接传输需先启用
cudaDeviceEnablePeerAccess
。 - 统一虚拟寻址(UVA):64位系统上简化多GPU编程。
- Cooperative Groups:复杂线程协作时替代
__syncthreads
。 - Tensor Core优化:使用半精度(
half
)或BF16格式。 - JIT编译:
nvrtc
库动态生成核函数。 - CUDA数学库:如CUBLAS、CUDNN的API正确性。
- AMP(自动混合精度):梯度缩放避免下溢。
十、环境与部署
- 检查GPU是否可用:
cudaGetDeviceCount
返回0时需处理。 - 多线程环境安全:CUDA上下文是线程局部的,需正确绑定。
- 避免多进程竞争GPU:如Docker容器需分配GPU资源。
- 显存不足的降级方案:如分块处理或使用内存映射。
- 处理
cudaErrorLaunchTimeout
:缩短核函数执行时间或禁用WDDM超时。 - 交叉编译兼容性:确保生成代码与目标GPU兼容。
- 依赖项打包:动态链接CUDA Runtime或静态编译。
- 错误信息本地化:
cudaGetErrorString
转换为可读信息。 - 监控GPU使用率:
nvidia-smi
或NVML库检测显存、温度等。 - 测试多GPU场景:确保代码在单卡/多卡环境下均正确。
总结
- 始终验证:从简单测试案例逐步扩展到复杂场景。
- 工具优先:利用
nsys
、nvprof
、cuda-memcheck
等定位问题。 - 性能与正确性平衡:优化前确保功能正确,再逐步调优。
通过关注这些细节,可显著减少CUDA程序中的错误并提升性能。