近期,Meta AI团队在生产PyTorch AI模型时遇到了一个难题。这一问题由CUDA非法内存访问引起,号称集结了Meta全公司最牛的AI工程师才搞定,这篇博客记录了他们使用CUDA的core dump来确定报错位置所使用的技巧和实践。
作者|Zachary DeVito
翻译|贾川、程浩源
(资料图片仅供参考)
如果GPU读取了无效内存,那么CUDA API将会开始从发生错误的地方开始,后续所有API调用都会返回cudaErrorIllegalAddress:
因为CUDA kernel是从CPU异步启动,所以在启动异常kernel的地方不会报告此错误,而是在GPU上实际发生异常并传播到CPU之后的任何CUDA API调用时报告此错误。
当然,要是使用CUDA_LAUNCH_BLOCKING=1环境变量,CUDA就会在kernel启动后运行完成才返回,但这会使得程序运行明显变慢,可能会改变报错时机,以致某些不确定性问题不再被触发。
此外,如果有多个线程使用CUDA API,cudaErrorIllegalAddress可能首先在另一个线程上报错,而不是在启动线程上报错。因此,即使在CUDA_LAUNCH_BLOCKING=1的情况下,我也不信任堆栈跟踪呈现的信息。
相反,对于“非法地址(illegal address)”这一bug,我们希望能找到更多、更准确的报错原因。类似于其他处理器,当故障发生时,GPU上的SM会记录有关故障指令的信息。
不幸的是,我意识到没有进程内的方法可以获取这类信息。我们只能在运行之前,通过将cuda-gdb或cuda-memcheck附加到进程中来访问此类信息。但这对于那些发生率很低的bug来说,在这种模式下重新运行这个进程来重现bug是不切实际的。
幸运的是,通过设置环境变量CUDA_ENABLE_COREDUMP_ON_EXCEPTION=1,我们可以使CUDA在发生异常后生成core dumps来呈现GPU的状态,然后用cuda-gdb来检查该文件。
本文讨论了如何从这些core dumps中生成提取信息,以便在没有调试信息的情况下,也能恢复诸多信息,比如参数值和出错指令等。
1
生成core dumps
在有故障的进程上设置 CUDA_ENABLE_COREDUMP_ON_EXCEPTION=1。如此一来,当故障发生时,它会生成一个core dumps文件cudacoredump.hostname.pid。
2
使用cuda-gdb打开core dumps
$ /usr/local/cuda/bin/cuda-gdb(cuda-gdb) target cudacore /tmp/cudacoredump.hostname.pidOpening GPU coredump: /tmp/cudacoredump.hostname.pid
这应该报告一些关于故障发生地点的信息:
CUDA Exception: Warp Illegal AddressThe exception was triggered at PC 0x7ff8b63ce440[Current focus set to CUDA kernel 0, grid 132575338, block (1240,0,0), thread (0,1,0), device 0, sm 1, warp 62, lane 0]#0 0x00007ff8b63ce570 in void (anonymous namespace)::softmax_warp_forward
相关信息如下:
触发Warp Illegal Address的指令地址:The exception was triggered at PC 0x7ff8b63ce440
正在运行的kernel名称:softmax_warp_forward
执行停止的地址:0x00007ff8b63ce570
请注意,GPU的停止地址(...570)是在触发地址(...440)之后。因为内存是异步读取,所以GPU会继续执行指令,之后才能发现故障。在查看寄存器的值时要注意这一点,因为你从中看到的是执行停止时的状态,而错误发生时指令中所使用寄存器的值可能也已经被覆盖。
最后,除非编译生成的代码中包含调试信息,否则将看不到代码行或文件名信息。但通过后续介绍的方法,即使没有如上内容,你也能从转储中恢复大量信息。
3
反汇编kernel
使用disas查看kernel的shader assembly(SASS)列表:
(cuda-gdb) disas...0x00007ff8b63ce420 <+1056>: IADD3 R8, R6.reuse, 0xc0, RZ0x00007ff8b63ce430 <+1072>: IADD3 R18, R6, 0xe0, RZ0x00007ff8b63ce440 <+1088>: LDG.E.U8.SYS R19, [R2+0xe0]0x00007ff8b63ce450 <+1104>: ISETP.GE.AND P3, PT, R8, R13, PT...
要查看错误指令,请找到与之匹配的PC:
0x00007ff8b63ce440 <+1088>: LDG.E.U8.SYS R19, [R2+0xe0]
在这种情况下,LDG是“从全局内存加载”,从地址[R2+0xe0]读取1字节(“U8”)到寄存器R19。出错的原因大概是R2+0xe0越界(out of bounds)了。
4
检查寄存器
使用info reg查看所有GPU寄存器的值:
(cuda-gdb) info regR0 0xb8198 754072R1 0xfffc80 16776320R2 0xff800000 -8388608R3 0xff800000 -8388608R4 0xff800000 -8388608R5 0x7ff8 32760R6 0x0 0R7 0x2 2R8 0x407ce000 1081925632...
虽然这里能看到R2的值,但其实R2在PC...440和...570之间的值已经被覆盖了,因此我们很难找到故障地址的值。
5
读取GPU内存
使用print从内存中读取值:
# read a void* from CUDA"s global memory:(cuda-gdb) print *(void * @global *)0x7ff841000000# read an int from CUDA"s global memory(cuda-gdb) print *(int @global *)0x7ff841000000
6
恢复传递给kernel的参数
kernel的参数在常量“参数”内存中传递。加载它们的指令包括对常量内存的引用,如c[0x0][0x174]:
0x00007ff8b63ce080 <+128>: IMAD R0, R3.reuse, c[0x0][0x174], R6
可以使用以下方法读取此内存:
(cuda-gdb) print *(int @parameter *)0x174152
要真正获取所有kernel参数的值,我们需要了解它们在内存中的排列方式。假设kernel有参数:
_global__ void softmax_warp_forward( output_t *dst, const input_t *src, int batch_size, int stride, int element_count, const bool *mask = nullptr, const int head_chunk_size = -1, bool is_transformer_mask = false) {...
常量内存中参数的布局与将它们放入struct中的布局相同:
struct Args { // offset output_t *dst; // 0 const input_t *src; // 8 int batch_size; // 16 int stride; // 20 int element_count; // 24 // <4 bytes padding> const bool *mask; // 32 const int head_chunk_size; // 40 bool is_transformer_mask; // 44};
这意味着结构体的值通常与其自身大小的下一个倍数对齐(8字节类型与8字节倍数对齐),必要时插入一些填充字节(padding bytes)。
kernel参数的开头不是0x0(低位的地址包含一些关于kernel的额外元数据),你可能需要查看程序集中对c[0x0][...]的所有引用,根据值的使用方式,查看参数缓冲区可能从何处开始。我自己运行时,参数看起来从0x160开始,这是cuda-gdb能对常量内存返回一个合理的值的条件下,对该常量内存的最小引用。
知道了布局和起始地址后,就可以用print来获取值(在print中指定正确的类型):
# stride(cuda-gdb) print *(int @parameter *) (0x160 + 20)152
SASS文档(https://docs.nvidia.com/cuda/cuda-binary-utilities/index.html)有更多关于正在运行的汇编语言的文档,但目前还不甚完善,且会随着GPU的更新换代而有所改变。
(本文经授权后编译发布。原文:
https://github.com/zdevito/zdevito.github.io/blob/main/_posts/2022-07-27-cuda-core-dumps.markdown)
其他人都在看
OneFlow v0.8.0正式发布
9篇分布式机器学习系统经典论文
深度学习硬件的过去、现在和未来
逆向工程:揭示Google Colab未公开的秘密
分布式深度学习编程新范式:Global Tensor
OneEmbedding:单卡训练TB级推荐模型不是梦
大模型训练难?效率超群、易用的“李白”模型库来了
欢迎体验OneFlow v0.8.0:GitHub - Oneflow-Inc/oneflow: OneFlow is a deep learning framework designed to be user-friendly, scalable and efficient.OneFlow is a deep learning framework designed to be user-friendly, scalable and efficient. - GitHub - Oneflow-Inc/oneflow: OneFlow is a deep learning framework designed to be user-friendly, scalable and efficient.https://github.com/Oneflow-Inc/oneflow/