已开启
错误码信息优化 #1
KenChow创建于  6月22日
KenChow成员
6月22日 创建

1)告警信息中“graph_id:0”表述不是很合理, 对象和id表达方式尚未统一,涉及到外部接口的需要再次整改。
比如:Invalid_Argument_API_Call_Sequence(E10062): Failed to call LoadGraph. Reason: the graph has been loaded, graph_id:0.

  1. msg打屏内容直接呈现内部接口(GetGraphNode)不合理,需要修改为直接描述成功能描述,方便用户能理解。
    比如: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..

likedislike
KenChow成员
10 天前 评论:

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_acquirePyEval_AcquireThreadtake_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 :138PyGILState_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 完整死锁链

  1. bridge.so 首次 pybind 调用 = 外来线程 2686585 上第一个三件套(EnsureBridgeReady,:131 调用链内部)
    • PyGILState_Ensure() 冷路径:创建临时 tstate A1,注册进 gilstate key,A1.counter=1,经 A1 持有 GIL
    • py::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 + DeleteCurrentA1 被 free、unlink、gilstate key 清空、GIL 释放pybind 私有 TLS 仍指向 A1 —— 悬空
  2. 后续每组三件套:Ensure 冷路径新建临时 tstate;glibc 恰好把 A1 的 chunk 同地址回收给新 tstate,悬空指针"碰巧"别名了活体,① 读 TLS 得到的指针 == 当前 tstate → release=false → 侥幸不死
  3. 第 5 组(RegisterPythonPasses 自己的三件套)::138PyGILState_Ensure 冷路径这次拿到了不同地址的 chunk A2(0x7f6560089860)→ 悬空 B(0x7f6560052830) ≠ A2 → 判定需要 acquire → PyEval_AcquireThread(B)等本线程经 A2 持有的 GIL → 永久自死锁
  4. 主线程 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 里的指针生命周期(永不过期)脱钩。

likedislike