【Bug已解决】[WebGPU] OrtReleaseEnv self-deadlocks after a failed Vulkan adapter request 解决方案一、现象长什么样在浏览器/node 的 WebGPU 环境里当创建 ONNX Runtime 的OrtEnv全局环境时如果底层 Vulkan adapter 请求失败比如设备不支持 WebGPU、或 adapter 请求被用户拒绝随后调用OrtReleaseEnv释放环境会死锁self-deadlock——主线程卡在OrtReleaseEnv永远不返回页面/进程无响应。现象# 现象 AVulkan adapter 请求失败后OrtReleaseEnv 永不返回 # 主线程堆栈停在 ort::webgpu::Env::Release() 内部的某个 mutex lock # 现象 B只在“adapter 请求失败”路径触发 # 正常初始化adapter 成功时 ReleaseEnv 正常返回 # 一旦初始化中途 adapter 失败Release 就死锁 # 现象 C死锁表现为“双锁同一 mutex” # 初始化线程在 adapter 失败后持有一个锁退出Release 又试图锁同一个 # 已被逻辑上持有的锁且是同线程递归锁未设 recursive 属性最坑的是现象 A初始化失败本应“干净回退”结果反而让整个进程卡死比直接崩溃还难处理——崩溃至少能 catch死锁只能杀进程。二、背景ONNX Runtime WebGPU EP 的OrtEnv初始化流程大致是① 请求 Vulkan/navigator.gpu.requestAdapter()② adapter 成功则创建 WebGPU 设备并初始化内部状态含若干 mutex 保护的资源表③ 任一环节失败则走错误处理清理。问题出在错误处理路径当 adapter 请求失败时初始化代码在已经持有一个内部 mutex比如保护全局 WebGPU 单例的锁的情况下直接return报错而没有先unlock随后用户调用OrtReleaseEnv做清理Release 逻辑又要去锁同一个 mutex来从全局表里移除这个其实没建成的env。于是同一线程先持锁、后在同一锁上再lock且该 mutex 不是 recursive形成 self-deadlock。这是并发/资源清理审查里经典的问题错误路径下锁的获取与释放不对称导致后续清理在同一个锁上自死锁。三、根因错误路径提前 return 未解锁adapter 失败后初始化函数持有env_mutex直接返回没unlock锁“逻辑上仍被持有”。Release 复用同一非递归 mutexOrtReleaseEnv试图lock(env_mutex)来清理同线程再锁非递归 mutex → self-deadlock现象 C。缺少“初始化失败必须可干净释放”的对拍CI 没测“adapter 失败后 Release 不卡死”死锁长期存在。本质是WebGPU 初始化在 adapter 失败的错误路径下锁未配对释放且 Release 复用同一非递归 mutex导致自死锁缺少失败-释放对拍。四、最小可运行复现下面用 Python 的threading.Lock非递归模拟“同线程持锁后再次锁同一锁 → 自死锁”import threading class OrtEnvBuggy: def __init__(self): self.env_mutex threading.Lock() # 非递归锁 def initialize(self, adapter_ok): self.env_mutex.acquire() # 初始化持锁 if not adapter_ok: # BUG: adapter 失败直接 return忘了 release return False self.env_mutex.release() return True def release(self): self.env_mutex.acquire() # Release 再锁同一锁 # 清理... self.env_mutex.release() env OrtEnvBuggy() print(init:, env.initialize(adapter_okFalse)) # 返回 False但锁没释放 try: env.release() # 同线程再锁 - 死锁这里会卡 except Exception: pass真实运行里env.release()会在self.env_mutex.acquire()处永久阻塞。五、解决方案第一层最小直接修复最小修复初始化错误路径下先释放已持有的锁再返回且用 RAII/try-finally保证锁对称import threading class OrtEnvFixed: def __init__(self): self.env_mutex threading.Lock() def initialize(self, adapter_ok): self.env_mutex.acquire() try: if not adapter_ok: return False # finally 会先释放锁 # ... 成功路径 return True finally: self.env_mutex.release() # 无论成功失败都释放 def release(self): self.env_mutex.acquire() try: # 清理... pass finally: self.env_mutex.release()这一层改动最小错误路径用try/finally保证锁释放自死锁消失。但依赖“每处加锁都写对”下看第二层。六、解决方案第二层结构性改进把“WebGPU env 的锁获取/释放必须对称、且初始化失败必须可干净释放”固化成单一事实来源。下面这个 dataclass 用 RAII 守卫模型描述锁契约并强制“初始化失败 → Release 不死锁”的可验证规则from dataclasses import dataclass, field from typing import Dict, Optional import contextlib import threading dataclass class OrtReleaseEnvDeadlockPolicy: 单一事实来源WebGPU env 的锁对称与失败可释放契约。 _held: Dict[str, int] field(default_factorydict) # 锁名 - 持有计数 _lock threading.Lock() contextlib.contextmanager def guard(self, name: str): RAII 守卫进入加锁退出必解锁。 with self._lock: self._held[name] self._held.get(name, 0) 1 try: yield finally: with self._lock: self._held[name] - 1 if self._held[name] 0: self._held.pop(name, None) def assert_not_held_after_init_failure(self, name: str) - None: 初始化失败后该锁不应仍被持有否则 Release 会自死锁。 if self._held.get(name, 0) 0: raise AssertionError( flock {name} still held after failed init - Release would deadlock) # 用法 policy OrtReleaseEnvDeadlockPolicy() with policy.guard(env_mutex): adapter_ok False if not adapter_ok: pass # 错误路径但 guard 的 finally 已释放 policy.assert_not_held_after_init_failure(env_mutex) # 通过这一层的关键收益RAII 对称guard保证加锁/解锁配对错误路径也释放杜绝自死锁失败可释放断言assert_not_held_after_init_failure在初始化失败后校验锁已释放否则报错单一事实来源所有 WebGPU env 锁约定收口在OrtReleaseEnvDeadlockPolicy。七、解决方案第三层断言 / CI 守护把第二层钉成 pytest挂进 CI确保初始化失败能干净释放、不死锁import contextlib import threading import pytest from your_package.ort_release_deadlock import OrtReleaseEnvDeadlockPolicy def test_init_failure_releases_lock(): # 断言 1adapter 失败后env_mutex 不应仍被持有 p OrtReleaseEnvDeadlockPolicy() with p.guard(env_mutex): adapter_ok False if not adapter_ok: pass p.assert_not_held_after_init_failure(env_mutex) # 通过 def test_release_after_failed_init_no_deadlock(): # 断言 2模拟 Release 在失败后调用不应卡死 p OrtReleaseEnvDeadlockPolicy() with p.guard(env_mutex): if False: # 模拟 adapter 失败分支 pass # Release 逻辑再 guard 一次必须能立即获得锁 with p.guard(env_mutex): pass p.assert_not_held_after_init_failure(env_mutex) def test_recursive_relock_avoided(): # 断言 3同一锁不会在同作用域被重复持有防 self-deadlock p OrtReleaseEnvDeadlockPolicy() with p.guard(env_mutex): # 即便内部再尝试 guard 同名锁也因计数归零前不重复获取而安全 with p.guard(env_mutex): pass p.assert_not_held_after_init_failure(env_mutex)三条断言从“失败释放锁”“失败后 Release 不死锁”“避免递归重锁”三面把自死锁钉死在 CI。八、排查清单WebGPU env 初始化失败反而卡死时进程卡在OrtReleaseEnv先看初始化是否中途失败adapter 请求失败最常见。初始化错误路径是否持锁直接 return必须有try/finally保证unlock。Release 是否复用同一非递归mutex 做清理同线程再锁必死锁现象 C 线索。用第二层OrtReleaseEnvDeadlockPolicyRAII 守卫 assert_not_held_after_init_failure校验。加第三层 pytest断言“失败释放锁、失败后 Release 不死锁、避免递归重锁”。所有“初始化可能失败”的锁都必须保证失败路径与成功路径锁对称。九、小结OrtReleaseEnv自死锁 bug 本质是WebGPU 初始化在 Vulkan adapter 请求失败的错误路径下持锁直接 return 未释放而OrtReleaseEnv又复用同一非递归 mutex 做清理导致同线程自死锁初始化本应干净回退却让进程卡死。修复分三层——第一层错误路径用try/finally保证锁释放第二层用OrtReleaseEnvDeadlockPolicy这个 dataclass 把“锁对称 失败可释放”收口成 RAII 守卫并加断言第三层用三条 pytest 把“失败释放锁、失败后 Release 不死锁、避免递归重锁”钉死在 CI。核心心法任何可能失败的初始化其错误路径的锁必须与成功路径对称释放清理逻辑复用同一非递归锁时必须保证此前已在所有路径释放否则必自死锁。