近期,Meta AI团队在生产PyTorch AI模型时遇到了一个难题。这一问题由CUDA不合法内存访问引起,声称集结了Meta全公司最牛的AI工程师才搞定,这篇博客记载了他们运用CUDA的core dump来确定报错方位所运用的技巧和实践。

作者|Zachary DeVito
翻译|贾川、程浩源

如果GPU读取了无效内存,那么CUDA API将会开端从产生过错的当地开端,后续一切API调用都会回来cudaErrorIllegalAddress:

设备在无效内存地址上运用了加载或存储指令。这使得进程处于不一致的状况,任何后续的CUDA工作都将回来相同的过错。若要持续运用CUDA,进程有必要中止并从头发动。

由于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.pid
Opening GPU coredump: /tmp/cudacoredump.hostname.pid

这应该陈述一些关于毛病产生地点的信息:

CUDA Exception: Warp Illegal Address
The 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<c10::Half, c10::Half, float, 8, false, true>(c10::Half*, c10::Half const*, int, int, int, bool const*, int, bool)<<<(1824,1,1),(32,4,1)>>> ()

相关信息如下:

  • 触发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, RZ
0x00007ff8b63ce430 <+1072>:  IADD3 R18, R6, 0xe0, RZ
0x00007ff8b63ce440 <+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 reg
R0             0xb8198             754072
R1             0xfffc80            16776320
R2             0xff800000          -8388608
R3             0xff800000          -8388608
R4             0xff800000          -8388608
R5             0x7ff8              32760
R6             0x0                 0
R7             0x2                 2
R8             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 *)0x174
152

要真实获取一切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文档(docs.nvidia.com/cuda/cuda-b… )有更多关于正在运转的汇编语言的文档,但目前还不甚完善,且会跟着GPU的更新换代而有所改动。

(本文经授权后编译发布。原文:github.com/zdevito/zde…

欢迎下载体会 OneFlow v0.8.0 最新版别:
github.com/Oneflow-Inc…

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。