Skip to content

Commit d63c5e2

Browse files
authored
Merge pull request d-run#421 from windsonsea/cuda
Add a blog: CUDA Core Dump
2 parents f120283 + 7fac207 commit d63c5e2

File tree

5 files changed

+681
-1
lines changed

5 files changed

+681
-1
lines changed

docs/zh/docs/blogs/2025/cuda.md

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
# CUDA Core Dump:调试内存访问问题的有效工具
2+
3+
> 英文原稿转载自 [blog.vllm.ai](https://blog.vllm.ai/2025/08/11/cuda-debugging.html)
4+
5+
你是否曾经在开发 CUDA kernel 时,测试经常遇到非法内存访问(简称 IMA),却不知道该如何调试?在开发 vLLM(一个高性能的 LLM 推理引擎)时,我们一次又一次地体会到了这种痛苦。
6+
7+
如果你是曾经遇到过此类问题的开发者,那么这篇博客就是为你准备的!我们将揭示一些在 vLLM 中用于调试复杂问题(如 IMA)的高级调试技巧。
8+
9+
例如,这是一个来自 PyTorch 的错误信息:
10+
11+
```text
12+
RuntimeError: CUDA error: an illegal memory access was encountered
13+
CUDA kernel errors might be asynchronously reported at some other API call, so the stacktrace below might be incorrect.
14+
For debugging consider passing CUDA_LAUNCH_BLOCKING=1
15+
Compile with `TORCH_USE_CUDA_DSA` to enable device-side assertions.
16+
```
17+
18+
棘手之处在于:CUDA kernel 错误可能在其他 API 调用中被异步报告,因此下面的堆栈跟踪可能是错误的。根据我们的经验,这类异常的 Python 堆栈跟踪基本 **总是错误的,几乎没什么用** 。为了解决这个问题,错误信息建议在运行代码时添加 `CUDA_LAUNCH_BLOCKING=1`。然而,这里仍然有两个问题:
19+
20+
1. 很多人使用 `kernel<<<>>>` 语法启动 CUDA kernel 时,并不会检查 kernel 启动状态,例如[这段代码](https://github.com/pytorch/pytorch/blob/5e320eea665f773b78f6d3bfdbb1898b8e09e051/aten/src/ATen/native/cuda/SortStable.cu#L117)。在这种情况下,即便加了 `CUDA_LAUNCH_BLOCKING=1`,依然无法定位到出错的 kernel。
21+
2. 如果非法内存访问发生在 CUDA graph 中的某个 kernel 内,即使加了 `CUDA_LAUNCH_BLOCKING=1`,我们也只能看到在启动 CUDA graph 时出现了问题,依然无法精准定位到出错的具体 kernel。
22+
23+
要精确定位这类问题,我们需要在非法内存访问发生的那一刻立刻做出响应。当然,这不是用户直接能做到的——它必须由 CUDA 驱动本身提供支持。
24+
25+
[CUDA core dump 功能](https://docs.nvidia.com/cuda/cuda-gdb/index.html#gpu-core-dump-support)正是为此设计的。它允许 CUDA 驱动在发生非法内存访问时转储 GPU 状态,用户之后可以分析该状态,从而找出是哪个 kernel 引发了问题,以及具体的非法访问是什么。
26+
27+
## 什么是 Core Dump?
28+
29+
GPU 本质上是一个高度并行的处理器,其中的许多概念在 CPU 上都有对应。
30+
31+
[Core dump](https://en.wikipedia.org/wiki/Core_dump) 是由 CPU 与操作系统共同提供的功能。当程序在执行过程中崩溃时,操作系统可以记录程序的内存数据、运行状态等信息,以便后续分析与调试。程序崩溃是一个硬件级概念,当 CPU 在执行某条指令时遇到错误,会进入 `trap` 状态,此时操作系统接管程序并执行相应的异常处理过程(默认情况下,这会直接终止程序,但也可以配置生成 core dump 以便分析。例如,通过 `ulimit -c 1` 可启用 core dump 生成,通过 `echo "core.%e.%p" > /proc/sys/kernel/core_pattern` 可指定 core dump 文件路径)。
32+
33+
类似地,GPU 上的 core dump 功能需要 GPU 硬件与 GPU 驱动的协作。当 GPU 上的某个线程在执行中崩溃时,GPU 硬件需要触发异常并将其传递给 GPU 驱动,由驱动立即处理该异常。然而,据[论坛讨论](https://forums.developer.nvidia.com/t/difference-in-error-handling-between-driver-api-and-runtime-api/336389)所述,GPU 驱动在处理异常时的默认行为是将当前 CUDA 上下文标记为不可用,而不是终止程序。
34+
35+
## 如何启用 CUDA Core Dump
36+
37+
启用 CUDA core dump 非常简单,只需设置 `CUDA_ENABLE_COREDUMP_ON_EXCEPTION=1` 环境变量即可。不过,为了更顺畅的使用体验,你还应额外设置几个环境变量:
38+
39+
1. 默认情况下,CUDA core dump 会将转储文件保存在当前目录,并且不会输出文件路径。你可以启用 `CUDA_COREDUMP_SHOW_PROGRESS=1` 环境变量来显示 core dump 过程的进度与细节。最重要的是,它会在过程结束后显示 core dump 文件的路径,方便后续调试与分析。
40+
2. 许多任务运行在容器内,当任务失败时,容器会被销毁,从而无法保留 core dump 文件。在这种情况下,你可以通过 `CUDA_COREDUMP_FILE` 环境变量指定 core dump 文件的路径模板。例如,可以将其保存到持久化存储目录:`CUDA_COREDUMP_FILE="/persistent_dir/cuda_coredump_%h.%p.%t"`,其中 `%h` 表示主机名,`%p` 表示进程 ID,`%t` 表示转储时间戳。
41+
3. 默认情况下,core dump 会保存整个 GPU 上下文。对于像大模型推理这样几乎占满 GPU 显存的程序来说,完整 core dump 会非常庞大(数百 GiB 数据)。你可以通过设置 `CUDA_COREDUMP_GENERATION_FLAGS='skip_nonrelocated_elf_images,skip_global_memory,skip_shared_memory,skip_local_memory'` 跳过保存 GPU 显存、共享内存和本地内存,从而大幅减小 core dump 文件体积。
42+
43+
文档中还提到,将 `skip_abort` 添加到 `CUDA_COREDUMP_GENERATION_FLAGS` 中,可以在 core dump 完成后防止 CPU 进程中止,这样 CPU 进程可以记录自己的错误堆栈,提供更多调试信息。但实验表明,这个功能存在一个严重[缺陷](https://forums.developer.nvidia.com/t/cuda-core-dump-with-skip-abort-will-ignore-an-illegal-memory-access-error/341802/3),可能导致 GPU 上的非法内存访问错误被忽略。此时,后续代码可能继续运行,但程序的内存数据已经被破坏。对于训练任务来说,这是完全不可接受的;对于推理任务,也是不安全的。因此,这个功能并不可靠,不推荐使用。
44+
45+
另外,文档称启用 `CUDA_ENABLE_COREDUMP_ON_EXCEPTION=1` 不仅会启用 CUDA core dump,还会默认生成 CPU core dump。但实际上,我们发现 CPU core dump 中几乎没有有用信息,而且难以分析。
46+
47+
如果你想进行实时调试,也可以启用 `CUDA_DEVICE_WAITS_ON_EXCEPTION=1` 环境变量。它不会使用 core dump,而是在异常发生时立刻暂停 GPU 执行,并挂起等待用户附加调试器(如 cuda-gdb)检查 GPU 状态,此时完整的 GPU 内存仍然保留。不过,这种方式自动化程度较低,需要更多人工干预。
48+
49+
总结来说,推荐使用以下组合环境变量启用 CUDA core dump:
50+
51+
```
52+
CUDA_ENABLE_COREDUMP_ON_EXCEPTION=1 CUDA_COREDUMP_SHOW_PROGRESS=1 CUDA_COREDUMP_GENERATION_FLAGS='skip_nonrelocated_elf_images,skip_global_memory,skip_shared_memory,skip_local_memory' CUDA_COREDUMP_FILE="/persistent_dir/cuda_coredump_%h.%p.%t"
53+
```
54+
55+
## 使用 CUDA Core Dump 的示例
56+
57+
### 调试不当的 Kernel 启动
58+
59+
```cpp
60+
// test.cu
61+
#include <cuda_runtime.h>
62+
#include <stdio.h>
63+
#include <stdlib.h>
64+
65+
// CUDA 错误检测宏
66+
#define cuda_check(call) do { \
67+
cudaError_t err = call; \
68+
if (err != cudaSuccess) { \
69+
printf("CUDA Error at %s:%d - %s: %s\n", __FILE__, __LINE__, #call, cudaGetErrorString(err)); \
70+
exit(EXIT_FAILURE); \
71+
} \
72+
} while(0)
73+
74+
// 具有非法内存访问的 Kernel — 访问越界
75+
__global__ void illegalMemoryAccessKernel(int* data, int size) {
76+
int idx = blockIdx.x * blockDim.x + threadIdx.x;
77+
78+
// 这会导致非法内存访问 — 访问超分配范围
79+
// 我们分配了 'size' 元素,却访问到 size * 2
80+
if (idx < size * 2) { // 访问两倍长度区域
81+
data[idx - 1000000000] = idx; // 对 idx == 0 时将引发越界访问
82+
}
83+
}
84+
85+
// 正常的 Kernel — 访问安全
86+
__global__ void normalKernel(int* data, int size) {
87+
int idx = blockIdx.x * blockDim.x + threadIdx.x;
88+
89+
if (idx < size) {
90+
data[idx] = idx;
91+
}
92+
}
93+
94+
int main() {
95+
printf("CUDA Illegal Memory Access Test\n");
96+
printf("===============================\n\n");
97+
98+
int size = 100;
99+
int* h_data = (int*)malloc(size * sizeof(int));
100+
int* d_data;
101+
102+
// 初始化主机内存
103+
for (int i = 0; i < size; i++) {
104+
h_data[i] = 0;
105+
}
106+
107+
// 分配设备内存
108+
cuda_check(cudaMalloc(&d_data, (unsigned long long)(size) * sizeof(int)));
109+
cuda_check(cudaMemcpy(d_data, h_data, size * sizeof(int), cudaMemcpyHostToDevice));
110+
111+
// 启动具有非法访问的 kernel
112+
int blockSize = 256;
113+
int numBlocks = (size + blockSize - 1) / blockSize;
114+
115+
printf("Launching kernel with out-of-bounds access...\n");
116+
illegalMemoryAccessKernel<<<numBlocks, blockSize>>>(d_data, size);
117+
118+
normalKernel<<<numBlocks, blockSize>>>(d_data, size);
119+
120+
cuda_check(cudaMemcpy(h_data, d_data, size * sizeof(int), cudaMemcpyDeviceToHost));
121+
for (int i = 0; i < 5; i++) {
122+
printf("%d ", h_data[i]);
123+
}
124+
printf("\n");
125+
126+
// 同步以捕捉运行时错误
127+
cuda_check(cudaDeviceSynchronize());
128+
129+
printf("Test completed.\n");
130+
131+
// 清理
132+
cuda_check(cudaFree(d_data));
133+
free(h_data);
134+
135+
return 0;
136+
}
137+
```
138+
139+
这段代码顺序启动两个 kernel(`illegalMemoryAccessKernel` 和 `normalKernel`)。正常运行时,你会看到类似这样的错误提示:
140+
141+
```
142+
CUDA Error at test.cu:62 - cudaMemcpy(h_data, d_data, size * sizeof(int), cudaMemcpyDeviceToHost): an illegal memory access was encountered
143+
```
144+
145+
错误只会在 `cudaMemcpy` 返回值中检测到。即使设置了 `CUDA_LAUNCH_BLOCKING=1`,仍然无法识别是哪一个 kernel 出错。
146+
147+
通过添加 CUDA core dump 相关环境变量,可以观察到如下信息:
148+
149+
```
150+
[00:40:46.606413] coredump: SM 123/132 has finished state collection
151+
[00:40:46.606419] coredump: SM 124/132 has finished state collection
152+
[00:40:46.611453] coredump: Detected an exception of type CUDBG_EXCEPTION_WARP_ILLEGAL_ADDRESS (14)
153+
[00:40:46.611458] coredump: - Device: 0
154+
[00:40:46.611460] coredump: - SM: 124
155+
[00:40:46.611462] coredump: - Warp: exception was detected after the warp has exited
156+
[00:40:46.611465] coredump: - PC 0x7f31abb9f6d0
157+
[00:40:46.611467] coredump: SM 125/132 has finished state collection
158+
159+
[00:40:46.806153] coredump: Writing ELF file to /tmp/cuda_coredump_xxx.1799919.1754898045
160+
161+
[1] 1799919 IOT instruction (core dumped) CUDA_ENABLE_COREDUMP_ON_EXCEPTION=1 CUDA_COREDUMP_SHOW_PROGRESS=1 = = ./test3
162+
```
163+
164+
当 GPU 线程触发非法内存访问时,CPU 立即生成 core dump 文件并触发 CPU 异常,直接终止程序。这时我们获得了 core dump 文件 `/tmp/cuda_coredump_xxx.1799919.1754898045`。可以使用 `cuda-gdb` 打开它:
165+
166+
```bash
167+
$ cuda-gdb
168+
(cuda-gdb) target cudacore /tmp/cuda_coredump_xxx.1799919.1754898045
169+
Opening GPU coredump: /tmp/cuda_coredump_xxx.1799919.1754898045
170+
171+
CUDA Exception: Warp Illegal Address
172+
The exception was triggered at PC 0x7f31abb9f6d0 illegalMemoryAccessKernel(int*, int)
173+
[Current focus set to CUDA kernel 0, grid 1, block (0,0,0), thread (0,0,0), device 0, sm 124, warp 0, lane 0]
174+
#0 0x00007f31abb9f6e0 in illegalMemoryAccessKernel(int*, int)<<<(1,1,1),(256,1,1)>>> ()
175+
```
176+
177+
我们可以清晰看到,异常由 `illegalMemoryAccessKernel` 引发,定位在 kernel 0、grid 1、block (0,0,0)、thread (0,0,0)、device 0、sm 124、warp 0、lane 0。
178+
179+
### 调试 CUDA Graph 中的 Kernel 异常
180+
181+
以下是一个更复杂的例子:在 CUDA graph 中插入一个非法访问 kernel:
182+
183+
```python
184+
# core_dump.py
185+
import torch
186+
import torch.nn as nn
187+
188+
from dataclasses import dataclass
189+
190+
@dataclass
191+
class CupyWrapper:
192+
data_ptr: int
193+
size_in_bytes: int
194+
195+
@property
196+
def __cuda_array_interface__(self):
197+
return {
198+
"shape": (self.size_in_bytes,),
199+
"typestr": '|u1',
200+
"data": (self.data_ptr, False),
201+
"version": 3,
202+
}
203+
204+
def from_buffer(data_ptr: int, size_in_bytes: int) -> torch.Tensor:
205+
out = torch.as_tensor(CupyWrapper(data_ptr, size_in_bytes))
206+
assert data_ptr == out.data_ptr(), "not zero-copy convert, something must be wrong!"
207+
return out
208+
209+
210+
class NeuralNetwork(nn.Module):
211+
def __init__(self):
212+
super(NeuralNetwork, self).__init__()
213+
# 第 1 层: [B, 10] -> [B, 20] + ReLU 激活
214+
self.layer1 = nn.Linear(10, 20)
215+
self.relu = nn.ReLU()
216+
# 第 2 层: [B, 20] -> [B, 30]
217+
self.layer2 = nn.Linear(20, 30)
218+
self.num_called = 0
219+
220+
def forward(self, x):
221+
# 输入形状: [B,10]
222+
x = self.layer1(x)
223+
x = self.relu(x)
224+
self.num_called += 1
225+
if self.num_called > 1:
226+
y = from_buffer(x.data_ptr(), x.numel() * 1024 * 1024)
227+
# 会触发非法内存访问
228+
y.fill_(1)
229+
x = self.layer2(x)
230+
return x
231+
232+
233+
# 使用示例
234+
if __name__ == "__main__":
235+
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
236+
print(f"Using device: {device}")
237+
238+
model = NeuralNetwork().to(device)
239+
batch_size = 4
240+
input_tensor = torch.randn(batch_size, 10).to(device)
241+
242+
print(f"Input shape: {input_tensor.shape}")
243+
print(f"Input device: {input_tensor.device}")
244+
245+
with torch.no_grad():
246+
output = model(input_tensor) # 先 warmup
247+
g = torch.cuda.CUDAGraph()
248+
with torch.cuda.graph(g):
249+
output = model(input_tensor) # 捕获 graph
250+
g.replay() # 重放 graph
251+
252+
print(f"Output shape: {output.shape}")
253+
print(f"Output device: {output.device}")
254+
print(f"Output: {output.sum()}")
255+
256+
print("\nModel architecture:")
257+
print(model)
258+
259+
total_params = sum(p.numel() for p in model.parameters())
260+
print(f"\nTotal parameters: {total_params}")
261+
print(f"Model device: {next(model.parameters()).device}")
262+
```
263+
264+
直接执行会报如下错误:
265+
266+
```text
267+
Using device: cuda
268+
Input shape: torch.Size([4, 10])
269+
Input device: cuda:0
270+
Output shape: torch.Size([4, 30])
271+
Output device: cuda:0
272+
Traceback (most recent call last):
273+
File "core_dump.py", line 76, in <module>
274+
print(f"Output: {output.sum()}")
275+
RuntimeError: CUDA error: an illegal memory access was encountered
276+
...
277+
```
278+
279+
输出在 `output.sum()` 时才触发,因为此处进行了设备同步,但我们仍不知道哪个 kernel 触发了 IMA。
280+
281+
添加 `CUDA_LAUNCH_BLOCKING=1` 后,错误信息变为:
282+
283+
```text
284+
...
285+
File "core_dump.py", line 71, in <module>
286+
g.replay()
287+
RuntimeError: CUDA error: an illegal memory access was encountered
288+
...
289+
```
290+
291+
这让我们推断异常发生在 CUDA graph 内的某个 kernel,但仍无法得知具体是哪一个。
292+
293+
再加上 core dump 的环境变量组合,我们就可以轻松定位:
294+
295+
```text
296+
(cuda-gdb) target cudacore /tmp/cuda_coredump_flow-matic.1929094.1754901120
297+
Opening GPU coredump: /tmp/cuda_coredump_flow-matic.1929094.1754901120
298+
299+
CUDA Exception: Warp Illegal Address
300+
The exception was triggered at PC 0x7fc2afba5e30 void at::native::vectorized_elementwise_kernel<4, at::native::FillFunctor<unsigned char>, std::array<char*, 1ul> >(int, at::native::FillFunctor<unsigned char>, std::array<char*, 1ul>)
301+
[Current focus set to CUDA kernel 0, grid 9, block (17454,0,0), thread (0,0,0), device 0, sm 0, warp 1, lane 0]
302+
#0 0x00007fc2afba5e70 in void at::native::vectorized_elementwise_kernel<4, at::native::FillFunctor<unsigned char>, std::array<char*, 1ul> >(int, at::native::FillFunctor<unsigned char>, std::array<char*, 1ul>)<<<(40960,1,1),(128,1,1)>>> ()
303+
```
304+
305+
很明显,这是一段 `fill` 函数,grid size 达到 `40960`,说明 `y = from_buffer(x.data_ptr(), x.numel() * 1024 * 1024); y.fill_(1);` 无视了 x 的真实长度,扩大了 1 百万倍然后填充,因而触发了 IMA 异常。
306+
307+
在部分 GPU 上,这一行可能报 `invalid argument` 而非 IMA,因为 grid size 超出最大限制。在此情况下,CUDA core dump 不会被触发,需要将扩容比例(`1024 * 1024`)适当调低,以免超过限制。
308+
309+
## 限制与注意事项
310+
311+
1. 理论上,CUDA core dump 应能捕获 GPU 某个线程引发的各种异常。但实际上,在某些 GPU 和驱动版本上,诸如“在全局/共享地址空间执行操作不支持”等异常可能无法触发 core dump。幸运的是, **非法内存访问** 通常能可靠触发 core dump,满足大多数调试需求。
312+
2. 对于硬件相关错误(如“通过 nvlink 非法访问跨 GPU 的内存”或硬件故障),这些并非由具体线程引发,无法归因某个 GPU 线程,不会触发 core dump。
313+
3. 通过 driver API 使用不当造成的错误属于[非粘性错误](https://forums.developer.nvidia.com/t/difference-in-error-handling-between-driver-api-and-runtime-api/336389),与 GPU 本身无关,会在 driver API 层面报告,同样不会触发 core dump。例如 `cudaMalloc` 时显存不足,不会生成 core dump。
314+
4. 在涉及多 GPU 通信的分布式程序中,通常会通过映射将其他 GPU 的内存映射到当前 GPU。如果另一个 GPU 上的程序退出,映射就失效,再访问它会触发 IMA,但这属于分布式关机期间的特定现象,不属于典型 IMA 问题。使用 core dump 时要区分这种“假阳性”。
315+
5. 启用 CUDA core dump 会对 CUDA kernel 性能有轻微影响(因为每个线程退出时需要检查错误并记录信息),因此不建议在生产环境开启。建议仅在可靠复现 IMA 错误以进行调试时使用。
316+
317+
## 总结
318+
319+
本文解析了 CUDA core dump 的原理与使用场景。这种调试方式对定位不正确的 kernel 启动与 CUDA graph 内的 kernel 异常特别有效,是调试 `illegal memory access` 及更多问题的强大工具。
320+
321+
例如,我们近期在 vLLM 中使用此技术调试了一个复杂的 IMA 问题(详见 [相关 PR](https://github.com/vllm-project/vllm/pull/22593))。基本情况是,我们为 MRope 添加了一个 triton kernel,但该 kernel 隐含假设 `head_size == rotary_dim`(即完整 Rope)。当 `head_size != rotary_dim`(部分 Rope)时,该 kernel 会触发 IMA,这在新 GLM-4.5V 模型中发生。若无 CUDA core dump,错误被误报为 `Failed: Cuda error /workspace/csrc/custom_all_reduce.cuh:453 'an illegal memory access was encountered'`,极具误导性。利用 core dump,我们轻松定位问题至 MRope kernel 并修复。请注意,对于更复杂的 IMA 问题,我们仍需以最小可复现示例隔离 kernel,然后结合如 [Compute Sanitizer](https://docs.nvidia.com/compute-sanitizer/ComputeSanitizer/index.html#memcheck-tool) 的专用工具进一步调查。
322+
323+
vLLM 项目旨在为所有人提供简单、高效、低成本的 LLM 推理服务,而易于调试也是重要组成部分。我们将持续分享更多调试技巧与方法,共同构建强大的 LLM 推理生态。如你有 vLLM 使用或调试经验,欢迎提交 PR 分享(地址在[博客仓库](https://github.com/vllm-project/vllm-project.github.io))。
324+
325+
## 致谢
326+
327+
感谢 Ze Long、Vikram Sharma Mailthody、Jeremy Iverson、Sandarbh Jain(来自 NVIDIA)给予的有益讨论。感谢 Red Hat 的 Lucas Wilkinson 帮助润色初稿。

docs/zh/docs/blogs/index.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ hide:
77

88
本频道将紧跟技术趋势,收集 AI 行业新闻。
99

10+
* [CUDA Core Dump:调试内存访问问题的有效工具](./2025/cuda.md)
11+
12+
揭示一些在 vLLM 中用于调试复杂问题(如 IMA)的高级调试技巧。
13+
1014
* [LMCache 上线即支持 GPT-OSS(20B/120B)](./2025/lmcache.md)
1115

1216
LMCache 上线就支持 OpenAI 新发布的 GPT-OSS 模型(20B 和 120B 参数)!本文将提供为 GPT-OSS

0 commit comments

Comments
 (0)