系列文章目录
- 【深度学习 DeBug 小技巧!】RuntimeError: CUDA error: device-side assert triggeredCUDA(用CPU debug 解决后再用GPU)
- 【分布式训练 debug】VS Code Debug 技巧:launch.json实用参数
- 【分布式训练(2)】深入理解 DeepSpeed 的 ZeRO 内存优化策略 (三阶段的区别)
- 【分布式训练(3)】accelerator + deepspeed debug 报错 "Timed out waiting for debuggee to spawn" 解决方法✅
- 【分布式训练(4)】accelerator.sync_gradients 和 checkpointing 深入理解
- 【分布式训练(5)】无法 kill PID?如何 kill 休眠中的 GPU 占用进程
- 【分布式训练(6)】深入理解多卡训练时 training steps, epoch 的相关概念
文章目录
- 系列文章目录
- [在 VS Code 中无 Attach 调试 Python 调用的 C++ 扩展](#在 VS Code 中无 Attach 调试 Python 调用的 C++ 扩展)
-
- [1. 问题背景](#1. 问题背景)
- [2. 思路:用 GDB 启动 Python,让 .so 在 GDB 控制下加载](#2. 思路:用 GDB 启动 Python,让 .so 在 GDB 控制下加载)
- [3. 前置条件:扩展必须带调试符号编译](#3. 前置条件:扩展必须带调试符号编译)
-
- [3.1 在扩展的构建里加「Debug 模式」](#3.1 在扩展的构建里加「Debug 模式」)
- [3.2 重新安装扩展(带调试符号)](#3.2 重新安装扩展(带调试符号))
- [3.3 确认 .so 带调试信息(可选)](#3.3 确认 .so 带调试信息(可选))
- [4. VS Code launch.json 配置🔥](#4. VS Code launch.json 配置🔥)
- [5. 使用流程与现象](#5. 使用流程与现象)
- [6. 在 GDB 里查看 PyTorch Tensor 的"实际数据"](#6. 在 GDB 里查看 PyTorch Tensor 的“实际数据”)
- [7. 避免误入 PyTorch 内部实现](#7. 避免误入 PyTorch 内部实现)
- [8. 小结](#8. 小结)
在 VS Code 中无 Attach 调试 Python 调用的 C++ 扩展
本文总结一种在 VS Code 下无需手动 attach 即可对「Python 调用的 PyTorch C++ 扩展」进行 C++ 源码级调试的方法,适用于任何通过 torch.utils.cpp_extension 或 pybind11 构建的 .so 扩展。
1. 问题背景
典型调用链:
Python 脚本 (.py)
→ import 某包
→ 调用包里的 Python 函数(如 mesh_to_flexible_dual_grid)
→ 该函数内部调用 _C.xxx_cpu(...) # 即 .so 里的 C++ 函数
→ C++ 实现 (.cpp)
- debugpy 只能断
.py,进不了.cpp。 - GDB 只能断
.cpp,不认识.py。 - 若用 GDB attach 到已运行的 Python 进程,需要改 ptrace 权限、选进程,流程繁琐。
目标:用 GDB 直接启动 Python,一次 F5 即可在 C++ 里断点,无需 attach。
2. 思路:用 GDB 启动 Python,让 .so 在 GDB 控制下加载
让 GDB 作为"主进程",用 program = python、args = [你的脚本.py, ...] 启动 Python;脚本里 import 到扩展时,动态库会在同一 GDB 会话中加载,因此:
- 在
.cpp里下的断点(或对 C++ 函数名的 pending breakpoint)会在执行到该 C++ 代码时命中。 - 无需再 attach 到别的进程。
代价:这一次调试会话里,不能同时用 debugpy 断 Python 源码(只能二选一:本次跑要么用 GDB 断 C++,要么用 debugpy 断 Python)。
3. 前置条件:扩展必须带调试符号编译
默认的 pip install -e . 或 python setup.py build 通常用 -O3 且不带 -g,GDB 无法把断点对应到源码行。
3.1 在扩展的构建里加「Debug 模式」
例如在 setup.py 里用环境变量控制编译选项:
python
# 调试 C++ 时使用: DEBUG=1 pip install -e .
BUILD_DEBUG = os.environ.get("DEBUG", "0") == "1"
# 在 CUDAExtension / setuptools 的 extra_compile_args 里:
extra_compile_args={
"cxx": (["-O0", "-g", "-std=c++17"] if BUILD_DEBUG else ["-O3", "-std=c++17"]),
"nvcc": (["-O0", "-g", "-G", "-std=c++17"] if BUILD_DEBUG else ["-O3", "-std=c++17"]) + cc_flag,
}
- Release :不加
DEBUG,用-O3,无-g。 - Debug :
DEBUG=1时用-O0 -g(及 nvcc 的-G),方便断点与单步。
3.2 重新安装扩展(带调试符号)
bash
cd /path/to/your/extension # 你的 C++ 扩展工程根目录
rm -rf build/ dist/ *.egg-info */*.egg-info # 清理旧产物(重要,避免用错 .so)
DEBUG=1 pip install -e . --no-build-isolation
--no-build-isolation 可避免构建环境与当前环境不一致导致的 ABI 问题。若遇 undefined symbol: ... TensorImpl::incref_pyobject ... 等,多半是之前用别的 PyTorch 版本编的,清理后按上面步骤用当前环境重编即可。
3.3 确认 .so 带调试信息(可选)
bash
readelf -S /path/to/your/_C*.so | grep debug
若有 .debug_info、.debug_line 等 section,说明带调试符号。
4. VS Code launch.json 配置🔥
在项目或工作区下 .vscode/launch.json 里增加一个 cppdbg 配置,用 GDB 启动 Python:
json
{
"name": "▶ GDB(C++): your_script",
"type": "cppdbg",
"request": "launch",
"program": "/absolute/path/to/your/python", // 例如 conda 环境里的 python
"args": [
"/absolute/path/to/your/script.py",
"--your-arg", "value"
],
"cwd": "/absolute/path/to/working/dir",
"MIMode": "gdb",
"miDebuggerPath": "/usr/bin/gdb",
"setupCommands": [
{ "text": "set breakpoint pending on", "ignoreFailures": true },
{ "text": "break your_cpp_function_name", "ignoreFailures": true }
]
}
要点:
- program :本机 Python 解释器绝对路径(建议用当前 conda/venv 的
python)。 - args:你的入口脚本及其参数,和平时命令行一致即可。
- cwd:脚本的工作目录,影响 import、相对路径等。
- set breakpoint pending on:允许在符号尚未加载时下断点(.so 是动态加载的,启动时符号还没有)。
- break your_cpp_function_name:在 C++ 函数名上先下一个 pending 断点,等 .so 加载后会自动绑定;函数名要和 C++ 里一致(可省略,改在源码里点行号断点也行)。
再保留一个 debugpy 配置,用于只断 Python 的情况,例如:
json
{
"name": "▶ Python: your_script",
"type": "debugpy",
"request": "launch",
"program": "/absolute/path/to/your/script.py",
"args": ["--your-arg", "value"],
"python": "/absolute/path/to/your/python",
"cwd": "/absolute/path/to/working/dir",
"console": "integratedTerminal",
"justMyCode": false
}
日常用法:要断 C++ 选 GDB 配置,要断 Python 选 debugpy 配置。
5. 使用流程与现象
- 在 .cpp 里需要停下的行左侧点断点(例如扩展的
xxx_cpu函数入口)。 - 选择 「▶ GDB(C++): your_script」,F5 启动。
- 启动后断点可能显示为灰色(未验证):因为此时 .so 还未加载,GDB 尚未解析到该源文件,属于正常现象。
- 继续运行(F5 或 Continue),当 Python 执行到
import你的扩展并调用到对应 C++ 函数时,断点会命中,此时断点变为红色,光标停在 .cpp 对应行。 - 之后即可在 C++ 中单步(F10 Step Over、F11 Step Into)、查看变量、查看调用栈等。
若希望「一启动就停」,可在 setupCommands 里对 Python 入口下断点(例如 break Py_Main),再 Continue 到你的 C++ 断点;一般不必,直接等执行到 C++ 即可。
6. 在 GDB 里查看 PyTorch Tensor 的"实际数据"
Variables 面板里看到的往往是 torch::Tensor 的内部结构(如 impl_、target_ 等),而不是元素值。可以在调试控制台用 GDB 命令查看。
假设 C++ 里已有:
cpp
const float* v_ptr = vertices.data_ptr<float>();
const int* f_ptr = faces.data_ptr<int>();
且已单步到这两行之后,则:
-
查看前 9 个 float(3 个顶点 × 3 分量):
text-exec p *v_ptr@9 -
查看前 9 个 int(3 个面 × 3 个顶点索引):
text-exec p *f_ptr@9 -
查看 tensor 的 shape(若 C++ 里有
vertices):text-exec p vertices.sizes()
语法要点:-exec 是 VS Code 里把命令发给 GDB;p 即 print;*ptr@N 表示从指针 ptr 起连续 N 个元素(类型由 ptr 推导)。这样就能在 C++ 调试时直接看到"实际数据"而不是内部结构。
7. 避免误入 PyTorch 内部实现
单步时若用 Step Into (F11) 进入 tensor.size()、tensor.data_ptr() 等,会进到 PyTorch 的 TensorImpl.h、intrusive_ptr.h 等内部实现,对排查业务逻辑帮助不大。
建议:
- 在业务 C++ 代码里尽量用 Step Over (F10) 逐行走。
- 若已误入 PyTorch 内部,用 Step Out (Shift+F11) 返回到你的 .cpp。
8. 小结
| 项目 | 说明 |
|---|---|
| 目标 | 不 attach,用 GDB 直接调试 Python 所调用的 C++ 扩展 |
| 做法 | launch 配置里用 cppdbg,program 填 Python,args 填你的脚本;.so 在运行中被 GDB 加载,断点即可命中 |
| 前提 | 扩展用 -O0 -g 重编(如 DEBUG=1 pip install -e .),并清理旧 build |
| 断点 | 下在 .cpp 或对 C++ 函数名 break;pending on,.so 加载后自动生效 |
| 看数据 | 用 -exec p *ptr@N 等 GDB 命令在控制台查看指针指向的数组 |
| 单步 | 多用 F10 避免进 PyTorch 内部;误入后用 Shift+F11 跳出 |
按上述方式配置一次后,之后只需选对 launch、F5,即可在 C++ 里稳定断点和查看数据,无需再手动 attach 进程。