Describe the issue
On macOS (arm64), tearing down a process that has registered the WebGPU plugin EP (libonnxruntime_providers_webgpu.dylib) logs an error:
[error] ... [ep_library_plugin.cc:69 Unload] ReleaseEpFactory failed for: ".../webgpu-ep/libonnxruntime_providers_webgpu.dylib" with error: Unknown exception
It is emitted when the host calls the public C API UnregisterExecutionProviderLibrary(env, name) (or when the last OrtEnv reference is released and Environment::~Environment() auto-unregisters remaining EP libraries).
The process still exits, but (1) the error is noisy/misleading and (2) the underlying WebGpuContextFactory::Cleanup() is not exception-safe, which is a real resource leak if an EP is registered/unregistered repeatedly in a long-lived process.
Root cause
The call chain:
EpLibraryPlugin::Unload() calls the plugin's exported ReleaseEpFactory and logs any non-OK status — onnxruntime/core/session/plugin_ep/ep_library_plugin.cc:69.
- The WebGPU plugin's
ReleaseEpFactory (onnxruntime/core/providers/webgpu/ep/api.cc:63-81) calls CleanupWebGpuContexts() → WebGpuContextFactory::Cleanup().
WebGpuContextFactory::Cleanup() (onnxruntime/core/providers/webgpu/webgpu_context.cc:1049-1061) does delete contexts_ and wgpuInstanceRelease(default_instance_), which tears down Dawn's Metal device/instance.
- On macOS, that Metal teardown raises an Objective-C
NSException. ObjC exceptions propagate through C++ frames on arm64 and are caught only by catch (...), so ReleaseEpFactory's EXCEPTION_TO_RETURNED_STATUS_END converts it to the OrtStatus("Unknown exception") that gets logged. (Not seen on Linux/Vulkan or Windows/D3D12 — Metal-specific.)
Two distinct problems:
- Cosmetic: the caught teardown exception surfaces as an
ERROR log even though the unload is best-effort and otherwise succeeds.
- Correctness (the more important one):
Cleanup() is not exception-safe. If delete contexts_ throws, then contexts_ is not nulled, default_instance_ is never released, and default_instance_ is not nulled. The statics are left dangling, leaking a WGPUInstance (and its Dawn/Metal InstanceBase/physical-device state) on every register→unregister cycle, and breaking a subsequent re-init (CreateContext would reuse a partially-destroyed map).
The current Cleanup() is identical on v1.25.1 and main (6be94de). The plugin EP README already notes WebGPU cleanup as incomplete (onnxruntime/core/providers/webgpu/ep/README.md → "Missing parts": "need a way to do WebGPU cleanup …").
Proposed fix
Make WebGpuContextFactory::Cleanup() exception-safe and idempotent: detach the static state first (so the factory is left clean/reusable even if a destructor throws), then release each resource under an independent guard so a throw in one cannot leak the other.
void WebGpuContextFactory::Cleanup() {
std::lock_guard<std::mutex> lock(mutex_);
// Detach static state first so the factory is left clean/reusable even if a
// destructor throws (on macOS, Dawn's Metal teardown can raise an Objective-C
// NSException). Guard each release independently so a throw in one cannot
// leak the other or leave the statics dangling.
auto* contexts = contexts_;
contexts_ = nullptr;
WGPUInstance instance = default_instance_;
default_instance_ = nullptr;
try {
delete contexts;
} catch (...) {
LOGS_DEFAULT(WARNING) << "Exception while destroying WebGPU contexts during teardown; ignoring.";
}
if (instance != nullptr) {
try {
wgpuInstanceRelease(instance);
} catch (...) {
LOGS_DEFAULT(WARNING) << "Exception while releasing WebGPU instance during teardown; ignoring.";
}
}
}
This removes the misleading ERROR (the plugin's ReleaseEpFactory now returns OK) and, more importantly, guarantees the instance is released and the statics are nulled even when Metal teardown throws. A deeper fix (wrapping the Metal teardown in @try/@catch so Dawn doesn't throw at all) could follow, but the exception-safety fix is correct and minimal on its own.
Urgency
Low for single-shot processes (exits cleanly); higher for long-lived hosts that register/unregister EPs repeatedly (cumulative WGPUInstance/Metal leak).
Platform / version
- ONNX Runtime 1.25.1 (reproduced);
Cleanup() unchanged on main (6be94de).
- macOS (Apple Silicon, arm64), Metal backend.
- WebGPU plugin EP (shared-library) build, registered via
RegisterExecutionProviderLibrary.
Describe the issue
On macOS (arm64), tearing down a process that has registered the WebGPU plugin EP (
libonnxruntime_providers_webgpu.dylib) logs an error:It is emitted when the host calls the public C API
UnregisterExecutionProviderLibrary(env, name)(or when the lastOrtEnvreference is released andEnvironment::~Environment()auto-unregisters remaining EP libraries).The process still exits, but (1) the error is noisy/misleading and (2) the underlying
WebGpuContextFactory::Cleanup()is not exception-safe, which is a real resource leak if an EP is registered/unregistered repeatedly in a long-lived process.Root cause
The call chain:
EpLibraryPlugin::Unload()calls the plugin's exportedReleaseEpFactoryand logs any non-OK status —onnxruntime/core/session/plugin_ep/ep_library_plugin.cc:69.ReleaseEpFactory(onnxruntime/core/providers/webgpu/ep/api.cc:63-81) callsCleanupWebGpuContexts()→WebGpuContextFactory::Cleanup().WebGpuContextFactory::Cleanup()(onnxruntime/core/providers/webgpu/webgpu_context.cc:1049-1061) doesdelete contexts_andwgpuInstanceRelease(default_instance_), which tears down Dawn's Metal device/instance.NSException. ObjC exceptions propagate through C++ frames on arm64 and are caught only bycatch (...), soReleaseEpFactory'sEXCEPTION_TO_RETURNED_STATUS_ENDconverts it to theOrtStatus("Unknown exception")that gets logged. (Not seen on Linux/Vulkan or Windows/D3D12 — Metal-specific.)Two distinct problems:
ERRORlog even though the unload is best-effort and otherwise succeeds.Cleanup()is not exception-safe. Ifdelete contexts_throws, thencontexts_is not nulled,default_instance_is never released, anddefault_instance_is not nulled. The statics are left dangling, leaking aWGPUInstance(and its Dawn/MetalInstanceBase/physical-device state) on every register→unregister cycle, and breaking a subsequent re-init (CreateContextwould reuse a partially-destroyed map).The current
Cleanup()is identical onv1.25.1andmain(6be94de). The plugin EP README already notes WebGPU cleanup as incomplete (onnxruntime/core/providers/webgpu/ep/README.md→ "Missing parts": "need a way to do WebGPU cleanup …").Proposed fix
Make
WebGpuContextFactory::Cleanup()exception-safe and idempotent: detach the static state first (so the factory is left clean/reusable even if a destructor throws), then release each resource under an independent guard so a throw in one cannot leak the other.This removes the misleading
ERROR(the plugin'sReleaseEpFactorynow returns OK) and, more importantly, guarantees the instance is released and the statics are nulled even when Metal teardown throws. A deeper fix (wrapping the Metal teardown in@try/@catchso Dawn doesn't throw at all) could follow, but the exception-safety fix is correct and minimal on its own.Urgency
Low for single-shot processes (exits cleanly); higher for long-lived hosts that register/unregister EPs repeatedly (cumulative
WGPUInstance/Metal leak).Platform / version
Cleanup()unchanged onmain(6be94de).RegisterExecutionProviderLibrary.