從Core Dump中提取CUDA的報錯信息

語言: CN / TW / HK

近期,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.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<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, 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:https://github.com/Oneflow-Inc/oneflow/

 


本文分享自微信公眾號 - OneFlow(OneFlowTechnology)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閲讀的你也加入,一起分享。