报错解决:CUDA Runtime error(an illegal memory access was encountered, cudaErrorIllegalAddress = 700)
报错
博主在cuda编程时,遇到了报错:
CUDA Runtime error cudaEventSynchronize(end_) # an illegal memory access was encountered, code = cudaErrorIllegalAddress [ 700 ]
原因
出现该现象,在框架稳定的背景下基本上可以确定是网络中有算子踩显存,因此CUDA上报非法内存访问,错误码为700,可能原因如下:
- 访问越界问题:算子计算过程中使用的size比申请的显存大了,导致访问越界。
- 同步与异步问题:由于GPU的算子执行是host下发到device上异步执行的,host使用了CUDA一些同步接口导致不是device的期望值出现非法内存。
解决
解决方法是首先排查是否是访问越界问题,若不是,接着排查是否是同步与异步问题。
访问越界问题
由于GPU的算子执行是host下发到device上异步执行的,因此执行报错的地方不一定是bug所在,可能是前面的算子有bug,但是device是异步执行的,所以执行到后面才会报错。
因此可以设置环境变量export CUDA_LAUNCH_BLOCKING=1
表示阻塞式执行,即一个算子在device执行完成后,host才会下发一个算子到device上执行,这种完全同步执行方式下如果还是报此错误(非法内存访问)的话,配合算子执行日志就可以初步确定是当前执行算子的问题了。
接着,可以利用Compute Sanitizer
工具进一步检查,并可以进一步验证是否是此算子的问题。
Compute Sanitizer 是一套工具,可以对代码的功能正确性执行不同类型的检查。调试的一个关键挑战是找到错误的根本原因,解决它通常比追踪它更容易,尤其是在并行执行环境中,因为在这种环境中,错误的来源可能是瞬态的。
Compute Sanitizer 通过检查代码是否存在内存访问违规、竞争条件、对未初始化变量的访问以及同步错误,擅长于根本原因调试。所有这些都可能表现为 bug,但其行为不一定会直接导致源代码中的根本原因。
您可能已经熟悉一种用于调试的工具:cuda-memcheck。但是,该工具已在 CUDA 11.6 中被弃用,并在 ZCk 12.0 及更高版本中被删除。Compute Sanitizer 已取代它的位置,提供了额外的功能,如改进的性能和对 Microsoft hardware-accelerated GPU scheduling 的支持,以及对内存检查之外的功能的更广泛支持。
Compute Sanitizer 中有四个主要工具:
- memcheck:用于内存访问错误和泄漏检测
- racecheck:共享内存数据访问危险检测工具
- initcheck:未初始化的设备全局内存访问检测工具
- synccheck:用于线程同步危险检测
除了这些工具, Compute Sanitizer 还有一些额外的功能:
- 用于创建针对 CUDA 应用程序的清理和跟踪工具的 API 。
- 与 NVIDIA 工具扩展(NVTX)集成
- Coredump 支持可用于 cuda-gdb
使用方法:
compute-sanitizer [options] app_name [app_options]
运行如下命令:
compute-sanitizer --launch-timeout=0 --tool=memcheck ./example > opt.txt 2>&1
打开opt.txt,查看输出:
cat opt.txt
示例输出:
========= COMPUTE-SANITIZER
Before: Array 0, 1 .. N-1: 1.000000 1.000000 1.000000
========= Invalid __global__ read of size 4 bytes
========= at 0x70 in /home/pgraham/devblog/NCS/example1.cu:8:scaleArray(float *, float)
========= by thread (255,0,0) in block (3,0,0)
========= Address 0x7f3aae000ffc is out of bounds
========= and is 1 bytes after the nearest allocation at 0x7f3aae000000 of size 4092 bytes
首先,由上述输出得到错误 info Invalid __global__ read
,因为 GPU 正试图读取某个不是合法地址的全局存储器。然后,可以获得报错代码文件和行号,以及导致此问题的实际线程和块。在这种情况下,example1.cu:8
映射到代码中的array[threadGlobalID] = array[threadGlobalID]*value;
。
使用printf
查看索引的值,观察是否越界。
最简单的修改方法是在数组访问前添加对索引的判断条件:if (threadGlobalID<N)
。
同步与异步问题
执行上述的操作后问题不复现,也就是同步执行的这种方式下没有问题,基本上可以确定是有算子里依赖同步执行的结果,因为正常device算子执行是异步执行,所以拿的结果不是预期值,同步执行就掩盖了这个问题。出现这种情况,除了阅读代码去确认是哪个算子里有使用同步操作接口外,一般可以通过二分法去加同步流来去定位(如果一个网络中有100个算子,可以在第50个算子加同步流,如果加了同步流没有复现,说明就前面的某个算子有问题了),同步流添加方法:在GPUDeviceContext::LaunchKernel
函数中调用DoLaunchKernel
后面增加SyncStream()
。
总结
- 算子实现逻辑中针对size需要check,确保访问不越界。
- 由于GPU的算子执行是host下发到device上异步执行的,尽量避免使用CUDA同步接口,如果使用的话,需要确保数据是正确的。
参考文献
- https://developer.nvidia.com/zh-cn/blog/debugging-cuda-more-efficiently-with-nvidia-compute-sanitizer/
- https://www.cnblogs.com/cxl11/articles/15730720.html
- https://stackoverflow.com/questions/27277365/unspecified-launch-failure-on-memcpy/27278218#27278218
- https://blog.csdn.net/Bit_Coders/article/details/113181262
- https://blog.csdn.net/LostUnravel/article/details/133780624
- https://forums.developer.nvidia.com/t/compute-sanitizer-not-quite-a-drop-in-replacement-of-cuda-memcheck/225877/6