@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.
Files changed (36) hide show
  1. package/dist/audio-analyzer/index.js +3 -3
  2. package/dist/audio-analyzer/index.mjs +3 -3
  3. package/dist/{custom-model-catalog-BZFt7qC7.js → custom-model-catalog-BsuqZLaN.js} +2 -2
  4. package/dist/{custom-model-catalog-Bdp__rWS.mjs → custom-model-catalog-DWFL7iBE.mjs} +2 -2
  5. package/dist/detection-pipeline/index.js +4 -4
  6. package/dist/detection-pipeline/index.mjs +4 -4
  7. package/dist/{dist-TseSUtZJ.mjs → dist-73R05PFN.mjs} +99 -92
  8. package/dist/{dist-8okS4wxl.js → dist-DFGiI5lr.js} +98 -91
  9. package/dist/{model-catalogs-C0aOsH6-.js → model-catalogs-BefhGOAc.js} +1 -1
  10. package/dist/{model-catalogs-BSCj4g0O.mjs → model-catalogs-OLyh3TdB.mjs} +1 -1
  11. package/dist/motion-wasm/index.js +1 -1
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/{node-I5Y2Dzmr.mjs → node-C0-Ik_JH.mjs} +3 -2
  14. package/dist/{node-BStoDcFQ.js → node-CdOP9ELN.js} +4 -2
  15. package/dist/pipeline-runner/index.js +3 -3
  16. package/dist/pipeline-runner/index.mjs +3 -3
  17. package/dist/{process-memory-D4H6J3CG.mjs → process-memory-DKKVQV5j.mjs} +1 -1
  18. package/dist/{process-memory-7YJfeXUK.js → process-memory-DO-ZBWdK.js} +1 -1
  19. package/dist/recorder/index.js +1675 -2
  20. package/dist/recorder/index.mjs +1675 -2
  21. package/dist/{segment-demux-js-RgT0biID.mjs → segment-demux-js-C_27CmCq.mjs} +1 -1
  22. package/dist/{segment-demux-js-BwQRJ24B.js → segment-demux-js-Dr3e9r4m.js} +1 -1
  23. 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
  24. package/dist/stream-broker/demux-worker-child.js +1 -1
  25. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  26. package/dist/stream-broker/{hostInit-xe-7xOfx.mjs → hostInit-Buho1uzb.mjs} +1 -1
  27. package/dist/stream-broker/index.js +3 -3
  28. package/dist/stream-broker/index.mjs +3 -3
  29. package/dist/stream-broker/remoteEntry.js +1 -1
  30. package/package.json +1 -1
  31. package/python/inference_pool.py +12 -13
  32. package/python/pool_platform.py +214 -0
  33. package/python/test_inference_pool_windows.py +86 -0
  34. package/python/test_pool_platform.py +141 -0
  35. package/python/test_webgpu_isolated.py +132 -0
  36. 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
- ORT WebGPU EP (Dawn/Vulkan) SIGSEGVs when a second InferenceSession is created
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 (or CPU fallback) — used by the child worker;
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(ort: Any, path: str) -> tuple[Any, list[str]]:
63
- """Build an InferenceSession on the WebGPU plugin EP, CPU fallback on miss.
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
- Same contract as the former inference_pool._load_onnx_webgpu_session.
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.register_execution_provider_library(
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
- msg = str(err).lower()
80
- if "already" not in msg and "exist" not in msg:
81
- sys.stderr.write(f"ONNX WebGPU: register failed ({err}); CPU fallback\n")
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(f"ONNX WebGPU: providers={active} ep={ep_name} device=webgpu\n")
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