.agents/skills/paddle-debug/references/case-studies.md
FLAGS_check_cuda_error=1 FLAGS_use_system_allocator=1 python test_one_hot_v2_op.py
报错:CUDA error(9), invalid configuration argument
TestOneHotOp_ZeroSize 使用 x_shape=[0, 10, 7, 3],即 numel=0one_hot_kernel.cu 中代码顺序:
funcs::set_constant(dev_ctx, out, 0.0); // 先调用 kernel
if (numel == 0) return; // 后检查边界
set_constant 内部启动 CUDA kernel,numel=0 导致 grid size=0,触发 CUDA error(9)将边界检查移到 kernel 调用之前:
if (numel == 0) return; // 先检查边界
funcs::set_constant(dev_ctx, out, 0.0); // 后调用 kernel
paddle/phi/kernels/gpu/one_hot_kernel.cupaddle/phi/kernels/legacy/gpu/one_hot_kernel.cuFLAGS_check_cuda_error=1 可以将异步 CUDA 错误立即暴露FLAGS_check_cuda_error=1 FLAGS_use_system_allocator=1 python test_tril_triu_op.py
6 个与 ZeroSize / ZeroDim 相关的测试用例失败,报错:CUDA error(9), invalid configuration argument
X.shape = [0, 3, 9, 4](numel=0)TrilTriuKernel 和 TrilTriuGradKernel 使用 ForRange 调度 kernelnumel=0 时,ForRange 以 limit=0 被调用,导致 grid_size=0, block_size=0TrilKernel/TriuKernel 有 numel==0 检查,但底层的 TrilTriuKernel 没有在 TrilTriuKernel 和 TrilTriuGradKernel 中添加提前返回:
// 在 kernel 调用前添加
if (x.numel() == 0) {
return; // 提前返回,避免无效的 CUDA kernel 启动
}
paddle/phi/kernels/impl/tril_triu_kernel_impl.h(前向 kernel)paddle/phi/kernels/impl/tril_triu_grad_kernel_impl.h(反向 kernel)| 维度 | one_hot 案例 | tril_triu 案例 |
|---|---|---|
| 修复位置 | .cu 文件 | .h 头文件模板 |
| 修复范围 | 前向 kernel | 前向 + 反向 kernel |
| 入口函数 | 单一入口 | 多入口(tril/triu/tril_triu) |
| 编译验证 | 编译 .cu 即可 | 需重新编译所有引用该头文件的 .cu |
TrilKernel/TriuKernel 虽有检查,但它们调用的 TrilTriuKernel 没有.h 后需重新编译所有引用它的 .cu,并确保 Python 加载的 .so 是最新的build/python/paddle/base/libpaddle.so 可能与 build/paddle/fluid/pybind/libpaddle.so 不同步,需手动复制或重新链接FLAGS_check_cuda_error=1 FLAGS_use_system_allocator=1 python test/compat/test_event_stream_apis.py
test_event_stream_timing_functionality 在 paddle.randn() 时报错:CUDA error(400), invalid resource handle。
直接原因:paddle/phi/api/profiler/event.cc 的 CudaEvent::ElapsedTime() 存在两个缺陷:
cudaEventSynchronize() 的返回值未被检查(直接丢弃)cudaGetLastError() 清除 CUDA last error触发序列:
test_event_stream_error_handling 对未 record 的 Event 调用 elapsed_time()cudaEventSynchronize(unrecorded_event) 返回 cudaErrorInvalidResourceHandle(400),但返回值被忽略cudaEventElapsedTime 也失败,PADDLE_ENFORCE_GPU_SUCCESS 抛出 C++ 异常try/except 捕获异常,但 CUDA last error 未被清除(sticky error 残留)FLAGS_check_cuda_error=1 的 CUDAErrorCheck 调用 cudaGetLastError() 检测到残留错误 400问题代码:
// paddle/phi/api/profiler/event.cc (修复前)
float CudaEvent::ElapsedTime(CudaEvent *end_event) {
float milliseconds = 0;
cudaEventSynchronize(end_event->GetRawCudaEvent()); // ← 返回值未检查!
PADDLE_ENFORCE_GPU_SUCCESS(cudaEventElapsedTime( // ← 异常路径未清除 last error
&milliseconds, event_, end_event->GetRawCudaEvent()));
return milliseconds;
}
import paddle
paddle.device.set_device('gpu:0')
event1 = paddle.device.Event()
event2 = paddle.device.Event()
try:
event1.elapsed_time(event2) # 未 record 的 event → CUDA error(400)
except Exception:
pass # Python 捕获了异常,但 CUDA last error 仍残留
# 后续任何 CUDA 操作都会失败(在 FLAGS_check_cuda_error=1 下)
stream = paddle.device.Stream(device='gpu:0')
with paddle.device.stream_guard(stream):
x = paddle.randn([100, 100]) # ← CUDA error(400)!
C++ 修复(paddle/phi/api/profiler/event.cc):
cudaEventSynchronize 返回值cudaGetLastError() 清除 CUDA last errorfloat CudaEvent::ElapsedTime(CudaEvent *end_event) {
float milliseconds = 0;
gpuError_t sync_err = cudaEventSynchronize(end_event->GetRawCudaEvent());
if (sync_err != cudaSuccess) {
cudaGetLastError(); // 清除 CUDA last error
PADDLE_ENFORCE_GPU_SUCCESS(sync_err);
}
gpuError_t elapsed_err = cudaEventElapsedTime(
&milliseconds, event_, end_event->GetRawCudaEvent());
if (elapsed_err != cudaSuccess) {
cudaGetLastError(); // 清除 CUDA last error
PADDLE_ENFORCE_GPU_SUCCESS(elapsed_err);
}
return milliseconds;
}
测试修复(test/compat/test_event_stream_apis.py):
elapsed_time(这是 CUDA 层的未定义行为)elapsed_timepaddle/phi/api/profiler/event.cc(C++ 核心修复)test/compat/test_event_stream_apis.py(测试修复)| 手段 | 具体操作 | 效果 |
|---|---|---|
| 二分测试 | 通过逐步去掉/保留 unittest 中各测试,定位到 test_event_stream_error_handling | 从 7 个测试缩小到 1 个关键测试 |
| 最小化复现 | 将 unittest 三类交互抽象为 10 行脚本 | 确认了根因链条 |
| 手动清除 CUDA error | 在 Python 中用 ctypes 调 cudaGetLastError() | 验证了 sticky error 假设 |
| 测试执行顺序分析 | 用 unittest.TestLoader 打印测试顺序 | 发现错误只在特定测试序列下出现 |
cudaEventSynchronize 这类"辅助"调用,忽略返回值也会在 CUDA runtime 中留下残留错误cudaGetLastError() 显式清除PADDLE_ENFORCE_GPU_SUCCESS 只检查传入的错误码,不会调用 cudaGetLastError() 来清除 runtime 残留try/except 捕获 C++ 异常后,CUDA last error 仍残留FLAGS_check_cuda_error=1 的放大效应:该 flag 使每个算子前后都调用 cudaDeviceSynchronize() + cudaGetLastError(),能检测到之前任何残留的错误——即使错误发生在完全不相关的代码路径上FLAGS_check_cuda_error=1 FLAGS_use_system_allocator=1 python test/legacy_test/test_newprofiler.py
TestTimerOnly::test_with_dataloader 失败,DataLoader worker 子进程报错:CUDA error(3), initialization error,随后 abort。
test_with_dataloader 通过FLAGS_use_system_allocator=1:默认分配器下不触发(因为有缓存池,不会立即 cudaFree)触发链:
TestProfiler::test_profiler 在主进程中初始化了 CUDA(创建了 GPU tensor)TestTimerOnly::test_with_dataloader 使用 DataLoader(num_workers=2) fork 子进程shared_ptr<Allocation> 引用DenseTensor::~DenseTensor()CUDAAllocator::FreeImpl -> RecordedGpuFree -> RecordedGpuMallocHelper::FreeFree 方法构造 CUDADeviceGuard(dev_id_) -> GetCurrentDeviceId() -> cudaGetDevice()cudaGetDevice() 返回 error 3PADDLE_ENFORCE_GPU_SUCCESS 将此视为致命错误并 abort代码位置:
paddle/phi/backends/gpu/cuda/cuda_info.cc:179 — GetCurrentDeviceId() 中的 PADDLE_ENFORCE_GPU_SUCCESS(cudaGetDevice(&device_id))paddle/phi/core/platform/device/gpu/gpu_info.cc:338 — RecordedGpuMallocHelper::Free() 中的 CUDADeviceGuard guard(dev_id_)在 RecordedGpuMallocHelper::Free() 和 FreeAsync() 中,在 CUDADeviceGuard 之前添加 CUDA context 可用性检查:
{
int device_id;
auto device_err = cudaGetDevice(&device_id);
if (device_err == cudaErrorInitializationError ||
device_err == cudaErrorNoDevice ||
device_err == cudaErrorInsufficientDriver) {
cudaGetLastError(); // 清除 sticky error
return; // 跳过释放,由 OS/driver 回收
}
}
CUDADeviceGuard guard(dev_id_); // 现在安全了
paddle/phi/core/platform/device/gpu/gpu_info.cc(RecordedGpuMallocHelper::Free 和 FreeAsync)| 踩坑点 | 说明 | 解决方法 |
|---|---|---|
| .so 未同步 | ninja phi_gpu 编译了新 .so,但 Python 加载的 build/python/paddle/libs/libphi_core.so 是旧版本 | 手动 cp build/paddle/phi/libphi_core.so build/python/paddle/libs/ |
| 行号不变判断法 | 修改代码后错误消息中行号没变(仍显示 :179),暴露了旧 .so 问题 | 利用行号作为判断 .so 是否更新的 indicator |
| 调用链穷举 | GetCurrentDeviceId 被多处调用,需要确认实际触发路径 | 在崩溃函数中加 backtrace_symbols_fd 临时日志 |
FLAGS_use_system_allocator=1 绕过了缓存池:使问题在正常路径下隐藏的 bug 暴露出来(默认分配器有缓存,不会每次都 cudaFree)ninja 只更新了前者,需要手动同步后者Free 和 FreeAsync 都需要添加保护,不能只修一处compute-sanitizer --tool memcheck --target-processes all python test_put_along_axis.py >run.log 2>&1
compute-sanitizer 报告大量 Invalid __global__ atomic of size 4 bytes 错误,出错 kernel 为 phi::funcs::PickWinnersScatterKernel<long>,访问地址远超分配大小(偏移数十亿字节)。不使用 CUDA Graph 时完全正常。
graph.replay() 阶段触发,capture 阶段无报错atomicMax(&winners[replace_index_self], ...) 处replace_index_self 的值异常巨大,说明上游 ComputeOffset 的输入数据(shape_strides)是垃圾值cudaGraphLaunch → CUDAGraph::Replay(),确认是 graph replay 触发问题代码(paddle/phi/kernels/funcs/gather_scatter_functor.cu,gpu_gather_scatter_functor::operator() 中):
DenseTensor shape_stride_dev;
shape_stride_dev.Resize({3 * ndim});
dev_ctx.Alloc<int64_t>(&shape_stride_dev);
{ // deallocate host once the copy is done
DenseTensor shape_stride_host;
shape_stride_host.Resize({3 * ndim});
dev_ctx.template HostAlloc<int64_t>(&shape_stride_host);
int64_t* host_data = shape_stride_host.data<int64_t>();
// ... 填充 host_data ...
phi::Copy(dev_ctx, shape_stride_host, dev_ctx.GetPlace(), false, &shape_stride_dev);
} // ← shape_stride_host 在此处析构,pinned memory 被释放
触发链:
phi::Copy 在 CUDA Graph capture 期间被录制为 cudaMemcpyAsync(H2D) 节点shape_stride_host 是局部变量,在 {} 作用域结束后析构,pinned memory 被释放graph.replay() 时,CUDA runtime 从已释放的 host 地址读取垃圾数据到 deviceshape_strides → ComputeOffset 计算出错误的偏移 → atomicMax 严重越界 → CUDA error 719影响范围:文件中共有 7 处完全相同模式的 H2D 拷贝(前向 1 处 + 反向 6 处),均存在 CUDA Graph 不兼容问题。
参照 Paddle 已有的 concat_and_split_functor.cu 中的做法,使用 RestoreHostMemIfCapturingCUDAGraph 在 CUDA Graph 捕获期间对 host 数据做快照,确保 graph replay 时 H2D memcpy 的源地址仍然有效。
#include "paddle/phi/backends/gpu/cuda/cuda_graph_with_memory_pool.h"
// 修复后:
{
DenseTensor shape_stride_host;
shape_stride_host.Resize({3 * ndim});
dev_ctx.template HostAlloc<int64_t>(&shape_stride_host);
int64_t* host_data = shape_stride_host.data<int64_t>();
// ... 填充 host_data ...
auto* restored =
phi::backends::gpu::RestoreHostMemIfCapturingCUDAGraph(
host_data, 3 * ndim);
phi::backends::gpu::GpuMemcpyAsync(
shape_stride_dev.data<int64_t>(),
restored,
3 * ndim * sizeof(int64_t),
phi::gpuMemcpyHostToDevice,
stream);
}
RestoreHostMemIfCapturingCUDAGraph 的原理:
new uint8_t[nbytes] + memcpy),通过 AddPostResetCallbackIfCapturingCUDAGraph 注册回调在 graph 重置时释放,确保 graph 整个生命周期内源地址有效paddle/phi/kernels/funcs/gather_scatter_functor.cu(7 处 H2D 拷贝全部修复)| 指标 | 修复前 | 修复后 |
|---|---|---|
| compute-sanitizer 错误数 | 大量 Invalid __global__ atomic | 0 errors |
| CUDA Graph replay | CUDA error 719 (launch failure) | PASS |
RestoreHostMemIfCapturingCUDAGraph 是 Paddle 标准的 CUDA Graph 安全 H2D 模式:任何在 CUDA Graph capture 期间需要 H2D memcpy 且 host 源为临时变量的场景,都应使用此函数做快照保护compute-sanitizer --tool memcheck 能精确检测到越界访问PYTHONPATH=build/python python test_put_along_axis.py
在 CUDA Graph capture 区间内调用 paddle.put_along_axis(x, index, value, axis=1) 时,抛出 CUDA error(906): cudaErrorStreamCaptureImplicit。
manipulation.py),而非 C++ kerneloperation would make the legacy stream depend on a capturing blocking stream问题代码(python/paddle/tensor/manipulation.py,put_along_axis 函数中):
if (paddle.in_dynamic_mode() and indices.numel() == 0) or (
not paddle.in_dynamic_mode() and 0 in indices.shape
):
return paddle.assign(arr)
触发链:
indices.numel() 返回一个 0-d GPU Tensor== 0 触发 Tensor.__eq__ → 返回 boolean GPU Tensorif 语句触发 Tensor.__bool__() → __nonzero__()__nonzero__() 内部调用 np.array(self) → self.numpy(False)numpy() 需要 GPU→CPU 数据拷贝(D2H memcpy + stream sync on legacy stream)影响范围:put_along_axis 和 put_along_axis_(inplace 版本)均受影响。
将 indices.numel() == 0 替换为 0 in indices.shape。shape 是 host 端 Python tuple,不触发任何 GPU 同步:
# 修复前(触发 D2H sync,CUDA Graph 不兼容)
if (paddle.in_dynamic_mode() and indices.numel() == 0) or (
not paddle.in_dynamic_mode() and 0 in indices.shape
):
# 修复后(纯 host 端操作,CUDA Graph safe)
if 0 in indices.shape:
python/paddle/tensor/manipulation.py(put_along_axis 和 put_along_axis_ 两处)| 测试 | 修复前 | 修复后 |
|---|---|---|
put_along_axis 普通模式 | PASS | PASS |
put_along_axis_ (inplace) 普通模式 | PASS | PASS |
put_along_axis CUDA Graph capture + replay | CUDA error(906) | PASS |
put_along_axis_ (inplace) CUDA Graph capture + replay | CUDA error(906) | PASS |
Tensor.__bool__()、Tensor.__nonzero__()、Tensor.numpy() 等方法会触发 GPU→CPU 数据拷贝,在 CUDA Graph capture 期间使用会导致 error 906。这类问题的根因在 Python 层而非 C++ kernel 层,容易被忽略tensor.numel() == 0 是常见的 CUDA Graph 不兼容模式:numel() 返回 GPU Tensor → == 0 触发 __bool__ → numpy() → D2H sync。应替换为 0 in tensor.shape(host 端 tuple 操作,零 GPU 开销)FLAGS_check_cuda_error=1 不可用:该 flag 会在每个算子前后插入 cudaDeviceSynchronize(),这在 capture 期间本身就会触发 error 906,不能用于定位 CUDA Graph 相关问题。应直接运行并观察原始错误栈numpy()、__bool__()、item()、tolist() 等),再检查 C++ 层的 H2D/D2H memcpy 和 stream 使用