1.2 两次复现对比
| 项目 | 第一次(09-09) | 第二次(09-10,本次) |
|---|---|---|
| plog 最后一行 | python_pass_pybind_bridge.cc:111 RegisterPythonPasses: Begin to register... |
:130 Begin... → :393 LoadPassNativeModuleUnlocked: Loaded... → :363 EnsureBridgeModuleUnlocked: Imported...(import 已成功) |
| 卡点 | import _ge_pass_native.so 时,其 pybind11 get_internals() 首调的 PyGILState_Ensure 冷路径 take_gil |
import 成功后,回到 RegisterPythonPasses 自己的 py::gil_scoped_acquire 内 PyEval_AcquireThread → take_gil |
| GIL 持有方式 | 同线程经 pybind 私有 tstate 持有(对 GILState API 不可见) | 同线程经 GILState 注册 tstate A2 持有,但 pybind 私有 TLS 里是悬空指针 B |
差异原因:09-09 17:19 安装侧 artifacts(ge/passes/python_pass_artifacts/cp37-linux-x86_64/)被人重建过,新版 bridge 在全部 10 处 GIL 入口套用了 PyGILState_Ensure + py::gil_scoped_acquire + PyGILState_Release 三件套(反汇编证实:10 组 PyGILState_Ensure → ctor(0x1e7c0) → ... → PyGILState_Release)。该修复解掉了第一次的 import 死锁,但引入了新的死锁(见根因)。注意仓库 ./ge 里的 python_pass_pybind_bridge.cc 仍是旧版(无三件套)。
1.3 本次卡死线程栈(GDB 实测)
主线程(python3.7, LWP 2686378):sess.run → TF_SessionRun → DirectSession::RunInternal → WaitForNotification(等 GE 初始化完成的通知)。
GE init 工作线程(npu_bridge 经 std::call_once 拉起的 C++ 线程,LWP 2686585):
GEInitialize → fusion::LoadPassPlugins → RegisterPythonPassesFromPlugin
→ RegisterPythonPasses (bridge.so 0x16420)
:130 GELOGI("Begin to register...")
:131 EnsureBridgeReady() ← 正常返回(内部完成 import)
:138 PyGILState_Ensure() ← 正常返回
:139 py::gil_scoped_acquire ← 卡死
→ pybind11 ctor (bridge.so 0x1e7c0)
① PyThread_tss_get(internals.tstate) → 得到 B=0x7f6560052830
release = (_PyThreadState_UncheckedGet() != B) → true
→ PyEval_AcquireThread(B) → take_gil(B) → 永久阻塞
1.4 关键现场数据(GDB 实测,全部来自挂死进程)
| 数据 | 值 | 含义 |
|---|---|---|
_PyRuntime.ceval.gil.locked |
1 | GIL 被占用 |
gil.last_holder = tstate_current |
0x7f6560089860(A2) |
GIL 由 A2 持有,A2 的 thread_id = 0x7f65b9a0c6c0 = 线程 2686585 自己 |
| A2 状态 | 链表头、gilstate_counter=1 | :138 的 PyGILState_Ensure 冷路径新建(inc_ref 未执行是因为阻塞在其后) |
| B=0x7f6560052830 | 不在解释器 tstate 链表中、gilstate_counter=0、thread_id 残留为线程 2686585 | 已 free 的 tstate 尸体(悬空指针) |
pybind 私有 TLS(internals.tstate key,线程 2686585) |
B = 0x7f6560052830(实测 PyThread_tss_get) |
① 读到的就是它 |
| gilstate key(同线程) | A2 = 0x7f6560089860 | :138 的 Ensure 注册的活体 |
| 解释器 tstate 链表 | 仅 A2 + 主线程 tstate | B 及其残留邻居(0x7f65600b5970、0x7f6560000090)均已被删除 |
即:线程 2686585 在等自己持有的 GIL,主线程再等它,全进程死锁。
2. 根因
2.1 直接原因
RegisterPythonPasses(:139)的 py::gil_scoped_acquire 构造函数第一步从 pybind11 私有 TLS 读到一个悬空的 tstate 指针 B(已被释放、不在链表),判定"本线程未持 GIL"(B ≠ 当前 tstate A2),于是 PyEval_AcquireThread(B) 等 GIL——而 GIL 正被本线程经 A2 持有,永久自死锁。
2.2 悬空指针的来源
pybind11 2.13.6 detail::get_internals() 首次初始化路径(internals.h:512 起,进程内每个 .so 各有一份 internals_pp):
// 本 so 首次调用、且 state_dict 中没有可复用的 internals 时(else 分支):
internals_ptr = new internals();
PyThreadState *tstate = PyThreadState_Get(); // ← "当前线程"的 tstate
PYBIND11_TLS_KEY_CREATE(internals_ptr->tstate);
PYBIND11_TLS_REPLACE_VALUE(internals_ptr->tstate, tstate); // ★ 种入 TLS,不持有生命周期
实测佐证:TF 的 _pywrap_tensorflow_internal.so、npu_bridge 的 _tf_adapter.so 都不创建 pybind internals(strings 无 __pybind11_internals),所以 bridge.so 的首次 pybind 调用必然走 else 分支;而这次首调发生在外来线程 2686585(GEInitialize 工作线程)上,PyThreadState_Get() 取到的"当前 tstate"正是三件套里 PyGILState_Ensure 创建的临时 tstate。pybind11 的设计假设是初始化发生在主 Python 线程(tstate 长活),该假设被违反。
2.3 完整死锁链
- bridge.so 首次 pybind 调用 = 外来线程 2686585 上第一个三件套(EnsureBridgeReady,:131 调用链内部)
PyGILState_Ensure()冷路径:创建临时 tstate A1,注册进 gilstate key,A1.counter=1,经 A1 持有 GILpy::gil_scoped_acquire:其get_internals()首次初始化走 else 分支,把 A1 种进 pybind 私有 TLS;随后 ② 路径从 gilstate key 找到 A1,复用(release=false)- 内部嵌套的
LoadPassNativeModuleUnlocked/EnsureBridgeModuleUnlocked三件套:嵌套 Ensure 全部热路径(key=A1),import_ge_pass_native.so成功(其 get_internals 找到 bridge 存入 state_dict 的同 ID capsule,直接复用,不再种 TLS)→ plog :393/:363 - 退出:dtor 计数递减 →
PyGILState_Release计数归零 →PyThreadState_Clear + DeleteCurrent→ A1 被 free、unlink、gilstate key 清空、GIL 释放;pybind 私有 TLS 仍指向 A1 —— 悬空
- 后续每组三件套:Ensure 冷路径新建临时 tstate;glibc 恰好把 A1 的 chunk 同地址回收给新 tstate,悬空指针"碰巧"别名了活体,① 读 TLS 得到的指针 == 当前 tstate → release=false → 侥幸不死
- 第 5 组(RegisterPythonPasses 自己的三件套):
:138的PyGILState_Ensure冷路径这次拿到了不同地址的 chunk A2(0x7f6560089860)→ 悬空 B(0x7f6560052830) ≠ A2 → 判定需要 acquire →PyEval_AcquireThread(B)→ 等本线程经 A2 持有的 GIL → 永久自死锁 - 主线程
WaitForNotification等 GE init 线程 → 全进程卡死
2.4 为什么已应用的修复不够
已应用的修复(PyGILState_Ensure 前置 + py::gil_scoped_acquire + PyGILState_Release,即 09-09 报告建议的方案)解决了"外来线程经 pybind 私有 tstate 持 GIL 后,嵌套 PyGILState_Ensure 冷路径自死锁"的第一死锁,但没有料到:
- pybind11 首次初始化会把当时的临时 tstate( Ensure 创建、Release 会销毁的那个)种进私有 TLS;
- 后续每次
gil_scoped_acquire都会读这个 TLS,一旦临时 tstate 已被销毁且新 tstate 没有复用同一地址,就拿到悬空指针; - 悬空指针要么崩溃(野指针),要么如本案一样对"本线程已持有的 GIL"发起 acquire → 自死锁。
本质:三件套让 tstate 的生命周期(随 Release 归零销毁)与 pybind TLS 里的指针生命周期(永不过期)脱钩。


1)告警信息中“graph_id:0”表述不是很合理, 对象和id表达方式尚未统一,涉及到外部接口的需要再次整改。
比如:Invalid_Argument_API_Call_Sequence(E10062): Failed to call LoadGraph. Reason: the graph has been loaded, graph_id:0.
比如:Invalid_Argument_API_Call_Sequence(E10062): Failed to call GetGraphNode. Reason: the graph 0 does not exist in graph_map.
3)个别错误码提示信息(包含trackback),不够准确,接口调用链A-B-C,应该选取报错信息更准确的接口上报。
Invalid_Argument_API_Call_Sequence(E10062): Failed to verify the graph status. Reason: Incompatible with API CompileGraph, graph_id=0.
TraceBack (most recent call last):
Failed to call RunGraphAsync. Reason: Graph 0 has been compiled by calling CompileGraph. RunGraphAsync and CompileGraph are mutually exclusive and cannot be used together..