PAYATHON 2026

PyTorch C++ 扩展注册第三个类时发生 core dump,如何排查?

数据小许

结论

现有调用栈不足以确定根因,也无法认定第三个类有问题。栈显示,libtorchtext.so 在 dlopen() 触发的静态注册过程中崩溃,涉及 torch::class_<torchtext::RegexTokenizer> 的析构、std::string 的内存释放和 tcmalloc 的释放路径。

排查时应先核对调试源码与实际加载的二进制,再检查扩展和 PyTorch 的构建兼容性,最后定位内存破坏。交换注册顺序后仍在第三个位置崩溃,触发条件可能与执行顺序、内存布局或累计状态有关,不能由此推断 PyTorch 有“只能注册两个类”的限制。

调用栈中的关键线索

崩溃路径包含:

torch::detail::class_base::~class_base
  -> std::__cxx11::basic_string<...>::~basic_string
  -> std::allocator<char>::deallocate
  -> tcmalloc::...

这里析构的是注册辅助对象。仅凭这个栈,不能认定某个 RegexTokenizer 业务实例已经被创建,也不能认定崩溃由其业务方法引起。

释放内存时崩溃,可能由更早发生的越界写、重复释放、失效指针或二进制接口不一致引起。tcmalloc 出现在栈顶,表明错误在其执行路径中暴露,尚不能判定分配器本身有缺陷。

GDB 还给出了警告:

warning: Source file is more recent than executable.

当前显示的源码可能与调试中的二进制不一致,不能直接依据第 79 行显示的 std::vector<std::string> 定位故障。

1. 先确认加载的是本次构建产物

记录当前环境和目标文件:

uv run python -c 'import sys, torch; print(sys.executable); print(torch.__version__); print(torch.__file__); print(torch.__config__.show())'
stat ./torchtext/lib/libtorchtext.so
ldd ./torchtext/lib/libtorchtext.so

检查构建日志中的最终输出路径。原命令 uv run setup.py build --debug 是否会更新 ./torchtext/lib/libtorchtext.so,取决于项目的构建脚本,不能仅凭构建完成就认定目标库已更新。

清理项目生成的旧目标文件和构建缓存,完整重建后确认实际加载路径指向新产物。清理位置应以构建脚本为准,避免删除源码或必要依赖。

分析已有 core 文件时,还必须保留生成该 core 时使用的共享库和调试符号。重新构建后,应重新复现并生成对应的崩溃记录。

2. 核对 C++ ABI 和 PyTorch 依赖

检查完整的编译和链接命令,重点确认:

  • PyTorch 头文件与链接库来自同一个安装环境。
  • 所有相关 C++ 目标文件使用一致的 _GLIBCXX_USE_CXX11_ABI 设置。
  • 没有混入旧 torchtext 构建产物或另一套 LibTorch。
  • 运行时实际加载的 PyTorch 库与构建时预期一致。

若当前安装提供对应内部属性,可以读取其 ABI 设置作为排查线索:

import torch

abi = getattr(torch._C, "_GLIBCXX_USE_CXX11_ABI", None)
print("PyTorch C++11 ABI:", abi)

这是内部属性;如果不存在,应结合安装信息与构建配置确认。不要直接将宏固定为 0 或 1。发现不一致后,需要修正构建配置并重新编译所有受影响的目标文件。

栈中的 std::__cxx11::basic_string 本身不能证明 ABI 不匹配,但提示可以重点检查这一设置。单凭出现 /usr/include/c++/15.2.1/,也不足以认定编译器版本就是原因。

3. 检查 tcmalloc 的加载来源

查看是否通过环境变量引入了额外动态库:

printenv LD_PRELOAD
printenv LD_LIBRARY_PATH

如果 tcmalloc 确实由 LD_PRELOAD 引入,可以在独立进程中临时移除该变量进行对照:

env -u LD_PRELOAD uv run python -c 'import torch; torch.classes.load_library("./torchtext/lib/libtorchtext.so")'

如果它是某个库的链接依赖,移除 LD_PRELOAD 不会将其排除。还应在 GDB 中查看实际加载的共享库:

info sharedlibrary

即使移除预加载后不再崩溃,也要继续定位。分配器变化可能改变内存布局,使原有错误暂时不再触发,不能据此认定问题已修复。

4. 缩小注册代码并寻找首次错误

确认构建产物一致后,每次使用新进程测试以下变化:

  1. 只保留一个类的最小注册,再逐个增加其他类。
  2. 对触发崩溃的组合,逐步增加 .def(...) 和序列化绑定。
  3. 检查注册过程涉及的静态对象、辅助函数,以及重复定义或不一致的类声明。

每次修改后都要确认目标库已更新。如果旧产物没有替换,交换注册顺序的结果可能无法反映当前源码。

可以先用简单脚本排除交互环境带来的额外状态:

# reproduce.py
from pathlib import Path
import torch

library = Path("./torchtext/lib/libtorchtext.so").resolve()
print("Loading:", library, flush=True)
torch.classes.load_library(str(library))
uv run python reproduce.py

如果仍在释放内存时崩溃,可在工具链支持的情况下为相关本地代码启用 AddressSanitizer。常用编译选项包括:

-g -O1 -fsanitize=address -fno-omit-frame-pointer

链接时也需要配置 sanitizer,并确保其运行库正确加载;诊断时还应避免额外分配器的干扰。优先处理最早报告的非法访问,最终的 free() 崩溃可能只是后果。未插桩的依赖也会限制检测范围。

如何判断修复是否成立

确认修复时,需要将证据对应到具体原因。例如,确认并修正 ABI 设置不一致,或者找到并修复更早发生的越界写,再用相同的加载步骤验证。

目前缺少 PyTorch 版本、完整编译和链接参数、实际动态库加载列表,以及与二进制一致的注册源码,无法给出唯一补丁。原先提到的“签名缺失”没有具体错误信息,也不能认定它与这次 core dump 出于同一原因。

备注:内容仅供参考。