@camstack/addon-pipeline 1.2.359 → 1.2.361
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audio-analyzer/index.js +3 -3
- package/dist/audio-analyzer/index.mjs +3 -3
- package/dist/{custom-model-catalog-BZFt7qC7.js → custom-model-catalog-BsuqZLaN.js} +2 -2
- package/dist/{custom-model-catalog-Bdp__rWS.mjs → custom-model-catalog-DWFL7iBE.mjs} +2 -2
- package/dist/detection-pipeline/index.js +4 -4
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-TseSUtZJ.mjs → dist-73R05PFN.mjs} +99 -92
- package/dist/{dist-8okS4wxl.js → dist-DFGiI5lr.js} +98 -91
- package/dist/{model-catalogs-C0aOsH6-.js → model-catalogs-BefhGOAc.js} +1 -1
- package/dist/{model-catalogs-BSCj4g0O.mjs → model-catalogs-OLyh3TdB.mjs} +1 -1
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-I5Y2Dzmr.mjs → node-C0-Ik_JH.mjs} +3 -2
- package/dist/{node-BStoDcFQ.js → node-CdOP9ELN.js} +4 -2
- package/dist/pipeline-runner/index.js +3 -3
- package/dist/pipeline-runner/index.mjs +3 -3
- package/dist/{process-memory-D4H6J3CG.mjs → process-memory-DKKVQV5j.mjs} +1 -1
- package/dist/{process-memory-7YJfeXUK.js → process-memory-DO-ZBWdK.js} +1 -1
- package/dist/recorder/index.js +1675 -2
- package/dist/recorder/index.mjs +1675 -2
- package/dist/{segment-demux-js-RgT0biID.mjs → segment-demux-js-C_27CmCq.mjs} +1 -1
- package/dist/{segment-demux-js-BwQRJ24B.js → segment-demux-js-Dr3e9r4m.js} +1 -1
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DqdW2pDb.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DJeFG_hB.mjs} +1 -1
- package/dist/stream-broker/demux-worker-child.js +1 -1
- package/dist/stream-broker/demux-worker-child.mjs +1 -1
- package/dist/stream-broker/{hostInit-xe-7xOfx.mjs → hostInit-Buho1uzb.mjs} +1 -1
- package/dist/stream-broker/index.js +3 -3
- package/dist/stream-broker/index.mjs +3 -3
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/python/inference_pool.py +12 -13
- package/python/pool_platform.py +214 -0
- package/python/test_inference_pool_windows.py +86 -0
- package/python/test_pool_platform.py +141 -0
- package/python/test_webgpu_isolated.py +132 -0
- package/python/webgpu_isolated.py +115 -17
|
@@ -168,5 +168,137 @@ class IsolatedSessionTests(unittest.TestCase):
|
|
|
168
168
|
proc.kill.assert_called()
|
|
169
169
|
|
|
170
170
|
|
|
171
|
+
class _FakeEpDevice:
|
|
172
|
+
ep_name = "WebGpuExecutionProvider"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class _FakeSessionOptions:
|
|
176
|
+
def __init__(self) -> None:
|
|
177
|
+
self.intra_op_num_threads = 0
|
|
178
|
+
self.devices: list = []
|
|
179
|
+
|
|
180
|
+
def add_provider_for_devices(self, devices, options) -> None:
|
|
181
|
+
self.devices = list(devices)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class _FakeSession:
|
|
185
|
+
def __init__(self, path: str, sess_options=None, providers=None) -> None:
|
|
186
|
+
self.path = path
|
|
187
|
+
self.sess_options = sess_options
|
|
188
|
+
self._providers = (
|
|
189
|
+
list(providers)
|
|
190
|
+
if providers is not None
|
|
191
|
+
else ["WebGpuExecutionProvider", "CPUExecutionProvider"]
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def get_providers(self) -> list[str]:
|
|
195
|
+
return self._providers
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class _FakeOrt:
|
|
199
|
+
"""Mirrors the one ORT behaviour that matters: a second registration RAISES."""
|
|
200
|
+
|
|
201
|
+
SessionOptions = _FakeSessionOptions
|
|
202
|
+
InferenceSession = _FakeSession
|
|
203
|
+
|
|
204
|
+
def __init__(self) -> None:
|
|
205
|
+
self.register_calls = 0
|
|
206
|
+
|
|
207
|
+
def register_execution_provider_library(self, name: str, path: str) -> None:
|
|
208
|
+
self.register_calls += 1
|
|
209
|
+
if self.register_calls > 1:
|
|
210
|
+
raise RuntimeError(f"EP library is already registered under {name}")
|
|
211
|
+
|
|
212
|
+
def get_ep_devices(self) -> list:
|
|
213
|
+
return [_FakeEpDevice()]
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class LoaderTests(unittest.TestCase):
|
|
217
|
+
"""D845: register once per process; refuse a render node that cannot be opened."""
|
|
218
|
+
|
|
219
|
+
def setUp(self) -> None:
|
|
220
|
+
wi._webgpu_registered = False
|
|
221
|
+
fake_plugin = MagicMock()
|
|
222
|
+
fake_plugin.get_library_path.return_value = "/fake/libwebgpu_ep.so"
|
|
223
|
+
fake_plugin.get_ep_name.return_value = "WebGpuExecutionProvider"
|
|
224
|
+
self._plugin_patch = patch.dict(sys.modules, {"onnxruntime_ep_webgpu": fake_plugin})
|
|
225
|
+
self._plugin_patch.start()
|
|
226
|
+
|
|
227
|
+
def tearDown(self) -> None:
|
|
228
|
+
self._plugin_patch.stop()
|
|
229
|
+
wi._webgpu_registered = False
|
|
230
|
+
|
|
231
|
+
def test_registers_the_plugin_once_across_sessions(self) -> None:
|
|
232
|
+
ort = _FakeOrt()
|
|
233
|
+
s1, p1 = wi.load_onnx_webgpu_session(ort, "/m/a.onnx", render_node_ok=True)
|
|
234
|
+
s2, p2 = wi.load_onnx_webgpu_session(ort, "/m/b.onnx", render_node_ok=True)
|
|
235
|
+
self.assertEqual(ort.register_calls, 1)
|
|
236
|
+
self.assertEqual(p1, ["WebGpuExecutionProvider", "CPUExecutionProvider"])
|
|
237
|
+
self.assertEqual(p2, p1)
|
|
238
|
+
# Both sessions were placed on the WebGPU device, not the CPU fallback.
|
|
239
|
+
self.assertEqual(len(s1.sess_options.devices), 1)
|
|
240
|
+
self.assertEqual(len(s2.sess_options.devices), 1)
|
|
241
|
+
|
|
242
|
+
def test_a_registration_that_raises_is_a_cpu_fallback_not_a_crash(self) -> None:
|
|
243
|
+
"""Disarm check: without the guard the second load re-registers and the
|
|
244
|
+
fake raises — the loader must then hand back CPU providers, never a
|
|
245
|
+
WebGPU session built on a poisoned registration."""
|
|
246
|
+
ort = _FakeOrt()
|
|
247
|
+
wi.load_onnx_webgpu_session(ort, "/m/a.onnx", render_node_ok=True)
|
|
248
|
+
wi._webgpu_registered = False # simulate the pre-D845 loader forgetting
|
|
249
|
+
_, providers = wi.load_onnx_webgpu_session(ort, "/m/b.onnx", render_node_ok=True)
|
|
250
|
+
self.assertEqual(ort.register_calls, 2)
|
|
251
|
+
self.assertEqual(providers, [wi.ONNX_CPU_EP])
|
|
252
|
+
|
|
253
|
+
def test_bounds_the_cpu_thread_pool(self) -> None:
|
|
254
|
+
ort = _FakeOrt()
|
|
255
|
+
session, _ = wi.load_onnx_webgpu_session(ort, "/m/a.onnx", render_node_ok=True)
|
|
256
|
+
self.assertEqual(session.sess_options.intra_op_num_threads, wi.WEBGPU_INTRA_OP_THREADS)
|
|
257
|
+
|
|
258
|
+
def test_refuses_without_an_openable_render_node(self) -> None:
|
|
259
|
+
ort = _FakeOrt()
|
|
260
|
+
with self.assertRaises(wi.WebGpuDeviceUnavailable) as ctx:
|
|
261
|
+
wi.load_onnx_webgpu_session(ort, "/m/a.onnx", render_node_ok=False)
|
|
262
|
+
self.assertIn("/dev/dri/renderD", str(ctx.exception))
|
|
263
|
+
self.assertIn("uid=", str(ctx.exception))
|
|
264
|
+
self.assertEqual(ort.register_calls, 0)
|
|
265
|
+
|
|
266
|
+
def test_no_render_node_check_outside_linux(self) -> None:
|
|
267
|
+
ort = _FakeOrt()
|
|
268
|
+
with patch.object(wi, "render_node_accessible", return_value=None):
|
|
269
|
+
_, providers = wi.load_onnx_webgpu_session(ort, "/m/a.onnx")
|
|
270
|
+
self.assertEqual(providers[0], "WebGpuExecutionProvider")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class RenderNodeProbeTests(unittest.TestCase):
|
|
274
|
+
def test_non_linux_is_not_checked(self) -> None:
|
|
275
|
+
self.assertIsNone(wi.render_node_accessible(platform="darwin"))
|
|
276
|
+
|
|
277
|
+
def test_missing_dir_or_no_nodes(self) -> None:
|
|
278
|
+
def missing(_: str) -> list[str]:
|
|
279
|
+
raise FileNotFoundError
|
|
280
|
+
|
|
281
|
+
self.assertFalse(wi.render_node_accessible(platform="linux", listdir=missing))
|
|
282
|
+
self.assertFalse(
|
|
283
|
+
wi.render_node_accessible(platform="linux", listdir=lambda _: ["card1", "by-path"])
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def test_denied_node_is_false_and_granted_is_true(self) -> None:
|
|
287
|
+
def nodes(_: str) -> list[str]:
|
|
288
|
+
return ["card1", "renderD128"]
|
|
289
|
+
|
|
290
|
+
self.assertFalse(
|
|
291
|
+
wi.render_node_accessible(platform="linux", listdir=nodes, access=lambda p, m: False)
|
|
292
|
+
)
|
|
293
|
+
seen: list[str] = []
|
|
294
|
+
|
|
295
|
+
def grant(p: str, m: int) -> bool:
|
|
296
|
+
seen.append(p)
|
|
297
|
+
return True
|
|
298
|
+
|
|
299
|
+
self.assertTrue(wi.render_node_accessible(platform="linux", listdir=nodes, access=grant))
|
|
300
|
+
self.assertEqual(seen, ["/dev/dri/renderD128"])
|
|
301
|
+
|
|
302
|
+
|
|
171
303
|
if __name__ == "__main__":
|
|
172
304
|
unittest.main()
|
|
@@ -1,14 +1,30 @@
|
|
|
1
1
|
"""
|
|
2
2
|
WebGPU ORT sessions in a dedicated OS process.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
in the same Python process (measured amd-proxmox 2026-10-10). Two processes,
|
|
6
|
-
one session each, work concurrently. This module:
|
|
4
|
+
One child process per WebGPU model (D823). This module:
|
|
7
5
|
|
|
8
|
-
- loads a WebGPU session
|
|
6
|
+
- loads a WebGPU session — used by the child worker;
|
|
9
7
|
- hosts the child worker entrypoint (`python webgpu_isolated.py --ipc-in N --ipc-out M`);
|
|
10
8
|
- exposes `WebGpuIsolatedSession` for the pool parent: one child per model.
|
|
11
9
|
|
|
10
|
+
Two facts measured on amd-proxmox 2026-10-13 (D845) shape the loader:
|
|
11
|
+
|
|
12
|
+
1. The SIGSEGV that motivated D823 was NOT a second session. It is a second
|
|
13
|
+
`register_execution_provider_library` call for a name that is already
|
|
14
|
+
registered: ORT raises "EP library is already registered", and the next
|
|
15
|
+
`InferenceSession` on that EP then dies in `_create_inference_session`.
|
|
16
|
+
Register ONCE per process (`_ensure_webgpu_ep_registered`) and three
|
|
17
|
+
sessions in one process load and run — serialised; concurrent `run()` on
|
|
18
|
+
distinct sessions aborts the process, so a shared process would need one
|
|
19
|
+
lock. The child-per-model layout stays for crash isolation.
|
|
20
|
+
2. Dawn never reports a software adapter. With `/dev/dri/renderD*` not
|
|
21
|
+
openable (the entrypoint dropped the render group), the session still
|
|
22
|
+
answered `providers=[WebGpuExecutionProvider, …]` and ran at 224 ms per
|
|
23
|
+
yolov9t-320 frame with gpu_busy 0 % — 15× the GPU's 13 ms, for a week,
|
|
24
|
+
looking like a weak iGPU. A render node that cannot be opened is
|
|
25
|
+
therefore REFUSED before any session is built; the error names the
|
|
26
|
+
uid/gid so the fix (device ownership in the entrypoint) is one read away.
|
|
27
|
+
|
|
12
28
|
IPC uses dedicated pipe fds (`pass_fds`), never stdin/stdout — ORT/Dawn log on
|
|
13
29
|
fd 1 and a yolov9 raw head on a shared stdout pickle channel truncated the
|
|
14
30
|
frame (2026-10-10). Pipe `read(n)` can also return short; framing uses
|
|
@@ -24,13 +40,77 @@ import struct
|
|
|
24
40
|
import subprocess
|
|
25
41
|
import sys
|
|
26
42
|
import threading
|
|
27
|
-
from typing import Any, BinaryIO, Optional
|
|
43
|
+
from typing import Any, BinaryIO, Callable, Optional
|
|
28
44
|
|
|
29
45
|
ONNX_CPU_EP = "CPUExecutionProvider"
|
|
30
46
|
ONNX_WEBGPU_EP_REGISTRATION = "webgpu_ep_registration"
|
|
31
47
|
|
|
48
|
+
# ORT's CPU thread pool in a WebGPU child only feeds buffer uploads and the
|
|
49
|
+
# rare CPU-placed node; the per-frame cost is Dawn's dispatch on ONE thread.
|
|
50
|
+
# Measured 2026-10-13 (yolov9t-320 warm p50): 14.2 ms at 1, 2 and 6 threads —
|
|
51
|
+
# so the default (one spinning thread per core, per child, ten children on a
|
|
52
|
+
# six-core node) buys nothing and costs CPU. Two keeps a CPU-placed node from
|
|
53
|
+
# serialising fully.
|
|
54
|
+
WEBGPU_INTRA_OP_THREADS = 2
|
|
55
|
+
|
|
56
|
+
RENDER_NODE_DIR = "/dev/dri"
|
|
57
|
+
RENDER_NODE_PREFIX = "renderD"
|
|
58
|
+
|
|
32
59
|
_HDR = struct.Struct(">I")
|
|
33
60
|
|
|
61
|
+
_webgpu_registered = False
|
|
62
|
+
_webgpu_register_lock = threading.Lock()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class WebGpuDeviceUnavailable(RuntimeError):
|
|
66
|
+
"""The webgpu device was asked for and cannot reach a GPU. Never served from CPU."""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def render_node_accessible(
|
|
70
|
+
*,
|
|
71
|
+
platform: str = sys.platform,
|
|
72
|
+
listdir: Callable[[str], list[str]] = os.listdir,
|
|
73
|
+
access: Callable[[str, int], bool] = os.access,
|
|
74
|
+
) -> Optional[bool]:
|
|
75
|
+
"""True when some /dev/dri/renderD* is readable+writable by this process.
|
|
76
|
+
|
|
77
|
+
None on a platform without DRM render nodes (nothing to check); False when
|
|
78
|
+
the directory is absent, empty of render nodes, or every node is denied.
|
|
79
|
+
"""
|
|
80
|
+
if not platform.startswith("linux"):
|
|
81
|
+
return None
|
|
82
|
+
try:
|
|
83
|
+
names = [n for n in listdir(RENDER_NODE_DIR) if n.startswith(RENDER_NODE_PREFIX)]
|
|
84
|
+
except OSError:
|
|
85
|
+
return False
|
|
86
|
+
if not names:
|
|
87
|
+
return False
|
|
88
|
+
return any(access(os.path.join(RENDER_NODE_DIR, n), os.R_OK | os.W_OK) for n in names)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _identity() -> str:
|
|
92
|
+
try:
|
|
93
|
+
return f"uid={os.getuid()} gid={os.getgid()} groups={sorted(os.getgroups())}"
|
|
94
|
+
except (AttributeError, OSError):
|
|
95
|
+
return "uid=? gid=? groups=?"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _ensure_webgpu_ep_registered(ort: Any, webgpu_ep: Any) -> None:
|
|
99
|
+
"""Register the plugin EP library at most once per process.
|
|
100
|
+
|
|
101
|
+
A second registration under the same name raises and leaves the next
|
|
102
|
+
session creation to SIGSEGV (the real cause behind D823's "second session
|
|
103
|
+
crashes"). Idempotent across callers and threads.
|
|
104
|
+
"""
|
|
105
|
+
global _webgpu_registered
|
|
106
|
+
with _webgpu_register_lock:
|
|
107
|
+
if _webgpu_registered:
|
|
108
|
+
return
|
|
109
|
+
ort.register_execution_provider_library(
|
|
110
|
+
ONNX_WEBGPU_EP_REGISTRATION, webgpu_ep.get_library_path()
|
|
111
|
+
)
|
|
112
|
+
_webgpu_registered = True
|
|
113
|
+
|
|
34
114
|
|
|
35
115
|
def _read_exact(stream: BinaryIO, n: int) -> bytes:
|
|
36
116
|
"""Read exactly n bytes; pipe read() may return short without EOF."""
|
|
@@ -59,11 +139,29 @@ def _read_msg(stream: BinaryIO) -> Any:
|
|
|
59
139
|
return pickle.loads(data)
|
|
60
140
|
|
|
61
141
|
|
|
62
|
-
def load_onnx_webgpu_session(
|
|
63
|
-
|
|
142
|
+
def load_onnx_webgpu_session(
|
|
143
|
+
ort: Any,
|
|
144
|
+
path: str,
|
|
145
|
+
*,
|
|
146
|
+
render_node_ok: Optional[bool] = None,
|
|
147
|
+
) -> tuple[Any, list[str]]:
|
|
148
|
+
"""Build an InferenceSession on the WebGPU plugin EP.
|
|
64
149
|
|
|
65
|
-
|
|
150
|
+
Refuses (`WebGpuDeviceUnavailable`) when no DRM render node is openable —
|
|
151
|
+
Dawn would otherwise serve the session from software and still call it
|
|
152
|
+
WebGPU. A missing plugin or a missing Vulkan adapter still falls back to
|
|
153
|
+
the CPU EP, and says so in `providers`.
|
|
66
154
|
"""
|
|
155
|
+
accessible = render_node_accessible() if render_node_ok is None else render_node_ok
|
|
156
|
+
if accessible is False:
|
|
157
|
+
raise WebGpuDeviceUnavailable(
|
|
158
|
+
f"ONNX WebGPU: no openable {RENDER_NODE_DIR}/{RENDER_NODE_PREFIX}* "
|
|
159
|
+
f"({_identity()}) — Dawn would run this model on a software adapter at "
|
|
160
|
+
f"~15× the GPU latency while reporting WebGpuExecutionProvider; refusing "
|
|
161
|
+
f"device webgpu. The container entrypoint must own or group-grant the "
|
|
162
|
+
f"render node to the camstack user (compose group_add is dropped by gosu)."
|
|
163
|
+
)
|
|
164
|
+
|
|
67
165
|
try:
|
|
68
166
|
import onnxruntime_ep_webgpu as webgpu_ep # type: ignore
|
|
69
167
|
except Exception as err:
|
|
@@ -72,15 +170,11 @@ def load_onnx_webgpu_session(ort: Any, path: str) -> tuple[Any, list[str]]:
|
|
|
72
170
|
return ort.InferenceSession(path, providers=[ONNX_CPU_EP]), [ONNX_CPU_EP]
|
|
73
171
|
|
|
74
172
|
try:
|
|
75
|
-
ort
|
|
76
|
-
ONNX_WEBGPU_EP_REGISTRATION, webgpu_ep.get_library_path()
|
|
77
|
-
)
|
|
173
|
+
_ensure_webgpu_ep_registered(ort, webgpu_ep)
|
|
78
174
|
except Exception as err:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
sys.stderr.flush()
|
|
83
|
-
return ort.InferenceSession(path, providers=[ONNX_CPU_EP]), [ONNX_CPU_EP]
|
|
175
|
+
sys.stderr.write(f"ONNX WebGPU: register failed ({err}); CPU fallback\n")
|
|
176
|
+
sys.stderr.flush()
|
|
177
|
+
return ort.InferenceSession(path, providers=[ONNX_CPU_EP]), [ONNX_CPU_EP]
|
|
84
178
|
|
|
85
179
|
ep_name = webgpu_ep.get_ep_name()
|
|
86
180
|
webgpu_device = next(
|
|
@@ -96,10 +190,14 @@ def load_onnx_webgpu_session(ort: Any, path: str) -> tuple[Any, list[str]]:
|
|
|
96
190
|
return ort.InferenceSession(path, providers=[ONNX_CPU_EP]), [ONNX_CPU_EP]
|
|
97
191
|
|
|
98
192
|
sess_options = ort.SessionOptions()
|
|
193
|
+
sess_options.intra_op_num_threads = WEBGPU_INTRA_OP_THREADS
|
|
99
194
|
sess_options.add_provider_for_devices([webgpu_device], {})
|
|
100
195
|
session = ort.InferenceSession(path, sess_options=sess_options)
|
|
101
196
|
active = list(session.get_providers())
|
|
102
|
-
sys.stderr.write(
|
|
197
|
+
sys.stderr.write(
|
|
198
|
+
f"ONNX WebGPU: providers={active} ep={ep_name} device=webgpu "
|
|
199
|
+
f"render-node=ok {_identity()}\n"
|
|
200
|
+
)
|
|
103
201
|
sys.stderr.flush()
|
|
104
202
|
return session, active
|
|
105
203
|
|