@camstack/addon-pipeline 1.2.350 → 1.2.351
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/{custom-model-catalog-B1oKNuAa.js → custom-model-catalog-CPqbnIUh.js} +1 -1
- package/dist/{custom-model-catalog-XCE-YLb2.mjs → custom-model-catalog-DdHOXMKu.mjs} +1 -1
- package/dist/detection-pipeline/index.js +1 -1
- package/dist/detection-pipeline/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +1 -1
- package/dist/pipeline-runner/index.mjs +1 -1
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-VwFAKiPW.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BgC3mSLA.mjs} +2 -2
- package/dist/stream-broker/{hostInit-CozpWK-g.mjs → hostInit-DfOhCBj-.mjs} +2 -2
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/python/inference_pool.py +57 -77
- package/python/test_webgpu_isolated.py +172 -0
- package/python/webgpu_isolated.py +292 -0
|
@@ -5,7 +5,7 @@ Object.defineProperties(exports, {
|
|
|
5
5
|
const require_chunk = require("../chunk-emK7D4bc.js");
|
|
6
6
|
const require_dist = require("../dist-C2jSMJo_.js");
|
|
7
7
|
const require_node = require("../node-B7ZinEw5.js");
|
|
8
|
-
const require_custom_model_catalog = require("../custom-model-catalog-
|
|
8
|
+
const require_custom_model_catalog = require("../custom-model-catalog-CPqbnIUh.js");
|
|
9
9
|
const require_lazy_sharp = require("../lazy-sharp-BuwKlQLR.js");
|
|
10
10
|
const require_process_memory = require("../process-memory-C1s8YIw4.js");
|
|
11
11
|
let node_os = require("node:os");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { n as __require } from "../chunk-DnnnRqeS.mjs";
|
|
2
2
|
import { Gt as modelBindingKey, Hn as hydrateSchema, It as loadContributionCapability, Kn as nodePin, L as PoolMemoryWatchdog, Mn as errMsg$1, Pn as BaseAddon, Pt as linkedLabelStepId, Q as applyModelBoundSettings, Qt as pipelineExecutorCapability, Rn as createEvent, St as evaluateZoneRules, Y as YAMNET_TO_MACRO, Yn as sleep, Yt as pickClusterStepSettings, Zn as array, an as resolveClusterStepModelId, bn as supportedRuntimes$1, cr as EventCategory, ct as collectModelBoundKeys, d as CLUSTER_MODEL_SCOPED_STEPS, g as DEFAULT_CLUSTER_STEP_SETTINGS, hn as stepModelRunsOnFormat, jt as hfModelUrl, k as LABEL_SUPPLY_MACRO, lt as containingParentIndex, mn as runtimeDevices$1, n as APPLE_SA_TO_MACRO, or as string, pn as rootSuppliesOf, pt as defaultDeviceFor$1, qt as overlayClusterStepSettings, rr as object, sr as union, un as resolvePoolMemoryPolicy, vt as detectionPipelineCapability, xt as enumerateInferenceDevices } from "../dist-DFCl7PZV.mjs";
|
|
3
3
|
import { s as readProcessCost } from "../node-C12l0K5V.mjs";
|
|
4
|
-
import { C as resolveModelDefaultConfidence, S as landmarkPrecisionVerdict, _ as ALL_STEPS, b as getStepDefinition, f as localFrameRegistry, g as ALL_PIPELINE_STEPS, l as resolveDefaultDetectionModel, n as mergeCustomModels, o as deviceKeyOf, r as buildSubtreeCanRun, s as resolveDeviceEngine, t as CustomModelCatalog, u as macroGateVerdict, v as getDefaultModelForFormat, x as resolveModelForFormat, y as getStep } from "../custom-model-catalog-
|
|
4
|
+
import { C as resolveModelDefaultConfidence, S as landmarkPrecisionVerdict, _ as ALL_STEPS, b as getStepDefinition, f as localFrameRegistry, g as ALL_PIPELINE_STEPS, l as resolveDefaultDetectionModel, n as mergeCustomModels, o as deviceKeyOf, r as buildSubtreeCanRun, s as resolveDeviceEngine, t as CustomModelCatalog, u as macroGateVerdict, v as getDefaultModelForFormat, x as resolveModelForFormat, y as getStep } from "../custom-model-catalog-DdHOXMKu.mjs";
|
|
5
5
|
import { t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
|
|
6
6
|
import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-3F1cP1ou.mjs";
|
|
7
7
|
import * as os$1 from "node:os";
|
|
@@ -5,7 +5,7 @@ Object.defineProperties(exports, {
|
|
|
5
5
|
const require_chunk = require("../chunk-emK7D4bc.js");
|
|
6
6
|
const require_dist = require("../dist-C2jSMJo_.js");
|
|
7
7
|
const require_node = require("../node-B7ZinEw5.js");
|
|
8
|
-
const require_custom_model_catalog = require("../custom-model-catalog-
|
|
8
|
+
const require_custom_model_catalog = require("../custom-model-catalog-CPqbnIUh.js");
|
|
9
9
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
10
10
|
const require_lazy_sharp = require("../lazy-sharp-BuwKlQLR.js");
|
|
11
11
|
const require_restream_intent = require("../restream-intent-7uDNqMKs.js");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { $t as pipelineRunnerCapability, Bn as defineCustomActions, E as FailureCounters, Et as failureContributionCapability, Ft as linkedStepOf, I as PIPELINE_RAMP, It as loadContributionCapability, Kn as nodePin, M as NativeLeaseAdmissionSchema, Mn as errMsg, N as NativeLeaseSettingsSchema, P as NodeBenchRunnerInputSchema, Pn as BaseAddon, Qn as boolean, Rn as createEvent, U as ROOT_SUPPLIABLE_STEP_IDS, Xn as _enum, Xt as pickDetailCropConvention, Z as allowedSuppliedEffect, Zn as array, Zt as pickNativeLeaseOverride, _ as DEFAULT_DETAIL_CROP_CONVENTION, _t as deriveRootFaceRefineRect, a as BackgroundRunRegistry, cr as EventCategory, er as lazy, fn as resolveRootSupply, ht as deriveDetailCropRect, it as cameraOwnsMotionViaSensor, j as MODEL_FORMATS, ln as resolveModelBoundSetting, m as CROP_SUPPLY_SOURCES, nr as number, or as string, pn as rootSuppliesOf, rr as object, rt as buildBenchStepTree, tr as literal, x as DEVICE_RAMP, y as DEFAULT_NATIVE_LEASE_SETTINGS, zn as customAction } from "../dist-DFCl7PZV.mjs";
|
|
2
2
|
import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-C12l0K5V.mjs";
|
|
3
|
-
import { a as stepsForElectedPool, b as getStepDefinition, d as rootObjectGate, f as localFrameRegistry, h as resolveFrameViewGeometry, i as placeDetailStep, l as resolveDefaultDetectionModel, m as createSessionDecodeCoordinator, p as isSessionDecodeReadGone, s as resolveDeviceEngine, t as CustomModelCatalog, u as macroGateVerdict, y as getStep } from "../custom-model-catalog-
|
|
3
|
+
import { a as stepsForElectedPool, b as getStepDefinition, d as rootObjectGate, f as localFrameRegistry, h as resolveFrameViewGeometry, i as placeDetailStep, l as resolveDefaultDetectionModel, m as createSessionDecodeCoordinator, p as isSessionDecodeReadGone, s as resolveDeviceEngine, t as CustomModelCatalog, u as macroGateVerdict, y as getStep } from "../custom-model-catalog-DdHOXMKu.mjs";
|
|
4
4
|
import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
|
|
5
5
|
import { t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
|
|
6
6
|
import { c as withDetectionIntent, o as preRollRequestOf, t as DETECTION_PRE_ROLL_REQUEST_MS } from "../restream-intent-DyGmLAU9.mjs";
|
|
@@ -18,7 +18,7 @@ var e = {
|
|
|
18
18
|
},
|
|
19
19
|
"@camstack/types": {
|
|
20
20
|
name: "@camstack/types",
|
|
21
|
-
version: "1.2.
|
|
21
|
+
version: "1.2.339",
|
|
22
22
|
scope: ["default"],
|
|
23
23
|
loaded: !1,
|
|
24
24
|
from: "addon_stream_broker_widgets",
|
|
@@ -33,7 +33,7 @@ var e = {
|
|
|
33
33
|
},
|
|
34
34
|
"@camstack/ui-library": {
|
|
35
35
|
name: "@camstack/ui-library",
|
|
36
|
-
version: "1.2.
|
|
36
|
+
version: "1.2.259",
|
|
37
37
|
scope: ["default"],
|
|
38
38
|
loaded: !1,
|
|
39
39
|
from: "addon_stream_broker_widgets",
|
|
@@ -36,7 +36,7 @@ async function r() {
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"@camstack/types": {
|
|
39
|
-
version: "1.2.
|
|
39
|
+
version: "1.2.339",
|
|
40
40
|
scope: "default",
|
|
41
41
|
shareConfig: {
|
|
42
42
|
singleton: !0,
|
|
@@ -81,7 +81,7 @@ async function r() {
|
|
|
81
81
|
}
|
|
82
82
|
},
|
|
83
83
|
"@camstack/ui-library": {
|
|
84
|
-
version: "1.2.
|
|
84
|
+
version: "1.2.259",
|
|
85
85
|
scope: "default",
|
|
86
86
|
shareConfig: {
|
|
87
87
|
singleton: !0,
|
|
@@ -30,7 +30,7 @@ async function d(e) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
async function f() {
|
|
33
|
-
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-
|
|
33
|
+
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BgC3mSLA.mjs")).catch((e) => {
|
|
34
34
|
throw l = void 0, e;
|
|
35
35
|
}), l;
|
|
36
36
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-pipeline",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.351",
|
|
4
4
|
"description": "Pipeline bundle — runner, detection, motion, audio + stream broker. Multi-entry npm package shipping pipeline addons under a single bundle.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|
package/python/inference_pool.py
CHANGED
|
@@ -448,9 +448,6 @@ def _resolve_ov_devices(available: list[str], full_names: dict[str, str]) -> lis
|
|
|
448
448
|
ONNX_CUDA_EP = "CUDAExecutionProvider"
|
|
449
449
|
ONNX_COREML_EP = "CoreMLExecutionProvider"
|
|
450
450
|
ONNX_CPU_EP = "CPUExecutionProvider"
|
|
451
|
-
# Plugin EP (onnxruntime-ep-webgpu) — registered at session load, not in
|
|
452
|
-
# get_available_providers() until register_execution_provider_library runs.
|
|
453
|
-
ONNX_WEBGPU_EP_REGISTRATION = "webgpu_ep_registration"
|
|
454
451
|
|
|
455
452
|
|
|
456
453
|
def _resolve_onnx_providers(
|
|
@@ -471,8 +468,8 @@ def _resolve_onnx_providers(
|
|
|
471
468
|
Deliberate adaptations for camstack:
|
|
472
469
|
- an operator device pin is honored: "cuda"/"coreml" put that EP first
|
|
473
470
|
(CPU still last as fallback); "cpu" compiles CPU only.
|
|
474
|
-
- "webgpu" is NOT handled here — it uses
|
|
475
|
-
|
|
471
|
+
- "webgpu" is NOT handled here — it uses an isolated child process
|
|
472
|
+
(`WebGpuIsolatedSession` / webgpu_isolated.py; D823).
|
|
476
473
|
- hardware entries not present in `available`
|
|
477
474
|
(ort.get_available_providers()) are dropped — camstack ships the
|
|
478
475
|
plain `onnxruntime` wheel on most nodes, and requesting an EP the
|
|
@@ -503,55 +500,10 @@ def _resolve_onnx_providers(
|
|
|
503
500
|
return providers
|
|
504
501
|
|
|
505
502
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
matches the plugin, and attaches it via SessionOptions.add_provider_for_devices.
|
|
511
|
-
Any failure (missing wheel, no adapter, old ORT) falls back to CPU so the
|
|
512
|
-
pool still loads — the stderr line names which path won.
|
|
513
|
-
"""
|
|
514
|
-
try:
|
|
515
|
-
import onnxruntime_ep_webgpu as webgpu_ep # type: ignore
|
|
516
|
-
except Exception as err:
|
|
517
|
-
sys.stderr.write(f"ONNX WebGPU: plugin import failed ({err}); CPU fallback\n")
|
|
518
|
-
sys.stderr.flush()
|
|
519
|
-
return ort.InferenceSession(path, providers=[ONNX_CPU_EP]), [ONNX_CPU_EP]
|
|
520
|
-
|
|
521
|
-
try:
|
|
522
|
-
ort.register_execution_provider_library(
|
|
523
|
-
ONNX_WEBGPU_EP_REGISTRATION, webgpu_ep.get_library_path()
|
|
524
|
-
)
|
|
525
|
-
except Exception as err:
|
|
526
|
-
# Already registered across pool workers is fine; other errors → CPU.
|
|
527
|
-
msg = str(err).lower()
|
|
528
|
-
if "already" not in msg and "exist" not in msg:
|
|
529
|
-
sys.stderr.write(f"ONNX WebGPU: register failed ({err}); CPU fallback\n")
|
|
530
|
-
sys.stderr.flush()
|
|
531
|
-
return ort.InferenceSession(path, providers=[ONNX_CPU_EP]), [ONNX_CPU_EP]
|
|
532
|
-
|
|
533
|
-
ep_name = webgpu_ep.get_ep_name()
|
|
534
|
-
webgpu_device = next(
|
|
535
|
-
(d for d in ort.get_ep_devices() if getattr(d, "ep_name", None) == ep_name),
|
|
536
|
-
None,
|
|
537
|
-
)
|
|
538
|
-
if webgpu_device is None:
|
|
539
|
-
sys.stderr.write(
|
|
540
|
-
f"ONNX WebGPU: no OrtEpDevice for {ep_name!r} "
|
|
541
|
-
f"(Vulkan/libvulkan missing?); CPU fallback\n"
|
|
542
|
-
)
|
|
543
|
-
sys.stderr.flush()
|
|
544
|
-
return ort.InferenceSession(path, providers=[ONNX_CPU_EP]), [ONNX_CPU_EP]
|
|
545
|
-
|
|
546
|
-
sess_options = ort.SessionOptions()
|
|
547
|
-
sess_options.add_provider_for_devices([webgpu_device], {})
|
|
548
|
-
session = ort.InferenceSession(path, sess_options=sess_options)
|
|
549
|
-
active = list(session.get_providers())
|
|
550
|
-
sys.stderr.write(
|
|
551
|
-
f"ONNX WebGPU: providers={active} ep={ep_name} device=webgpu\n"
|
|
552
|
-
)
|
|
553
|
-
sys.stderr.flush()
|
|
554
|
-
return session, active
|
|
503
|
+
# WebGPU: one ORT session per OS process (Dawn/Vulkan SIGSEGVs on a second
|
|
504
|
+
# session in-process — amd-proxmox 2026-10-10). Implementation lives in
|
|
505
|
+
# webgpu_isolated.py; the pool parent never holds a WebGPU session itself.
|
|
506
|
+
from webgpu_isolated import WebGpuIsolatedSession # noqa: E402
|
|
555
507
|
|
|
556
508
|
|
|
557
509
|
# ---------------------------------------------------------------------------
|
|
@@ -1388,13 +1340,32 @@ def _load_model(slot: ModelSlot, config: dict) -> None:
|
|
|
1388
1340
|
# WebGPU uses the plugin-EP SessionOptions path (ORT ≥1.24); everything
|
|
1389
1341
|
# else uses the classic providers= list (see _resolve_onnx_providers).
|
|
1390
1342
|
if ort_device.strip().lower() == "webgpu":
|
|
1391
|
-
|
|
1343
|
+
# Isolated child — never create a WebGPU InferenceSession in this
|
|
1344
|
+
# process (second in-process session SIGSEGVs the pool worker).
|
|
1345
|
+
isolated = WebGpuIsolatedSession.open(path)
|
|
1346
|
+
active_list = isolated.providers
|
|
1392
1347
|
_active_eps = [p for p in active_list if p != ONNX_CPU_EP]
|
|
1393
1348
|
sys.stderr.write(
|
|
1394
1349
|
f"ONNX: execution provider {', '.join(_active_eps) or ONNX_CPU_EP} "
|
|
1395
|
-
f"(device=webgpu)\n"
|
|
1350
|
+
f"(device=webgpu, isolated)\n"
|
|
1396
1351
|
)
|
|
1397
1352
|
sys.stderr.flush()
|
|
1353
|
+
slot.config["_input_name"] = isolated.input_name
|
|
1354
|
+
try:
|
|
1355
|
+
slot.config["_input_shape"] = isolated.input_shape
|
|
1356
|
+
except Exception:
|
|
1357
|
+
pass
|
|
1358
|
+
try:
|
|
1359
|
+
if isolated.input_type == "tensor(uint8)":
|
|
1360
|
+
slot.config["_input_dtype"] = "uint8"
|
|
1361
|
+
except Exception:
|
|
1362
|
+
pass
|
|
1363
|
+
|
|
1364
|
+
def predict(inp_dict: dict, _iso=isolated) -> dict:
|
|
1365
|
+
return _iso.run(inp_dict)
|
|
1366
|
+
|
|
1367
|
+
slot.model = isolated
|
|
1368
|
+
slot.predict_fn = predict
|
|
1398
1369
|
else:
|
|
1399
1370
|
try:
|
|
1400
1371
|
available_eps = list(ort.get_available_providers())
|
|
@@ -1412,29 +1383,29 @@ def _load_model(slot: ModelSlot, config: dict) -> None:
|
|
|
1412
1383
|
f"available={available_eps})\n"
|
|
1413
1384
|
)
|
|
1414
1385
|
sys.stderr.flush()
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1386
|
+
output_names = [o.name for o in session.get_outputs()]
|
|
1387
|
+
slot.config["_input_name"] = session.get_inputs()[0].name
|
|
1388
|
+
# Declared input layout (ArcFace/CLIP are NHWC) → correct preprocess.
|
|
1389
|
+
try:
|
|
1390
|
+
slot.config["_input_shape"] = session.get_inputs()[0].shape
|
|
1391
|
+
except Exception:
|
|
1392
|
+
pass
|
|
1393
|
+
# Declared input DTYPE. super-gradients / Frigate YOLO-NAS exports bake
|
|
1394
|
+
# their preprocessing into the graph and take `tensor(uint8)` — the
|
|
1395
|
+
# preprocess must feed raw uint8 pixels, not the float32 [0,1] tensor
|
|
1396
|
+
# (a hard onnxruntime type error otherwise).
|
|
1397
|
+
try:
|
|
1398
|
+
if session.get_inputs()[0].type == "tensor(uint8)":
|
|
1399
|
+
slot.config["_input_dtype"] = "uint8"
|
|
1400
|
+
except Exception:
|
|
1401
|
+
pass
|
|
1431
1402
|
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1403
|
+
def predict(inp_dict: dict) -> dict:
|
|
1404
|
+
outputs = session.run(output_names, inp_dict)
|
|
1405
|
+
return {name: out for name, out in zip(output_names, outputs)}
|
|
1435
1406
|
|
|
1436
|
-
|
|
1437
|
-
|
|
1407
|
+
slot.model = session
|
|
1408
|
+
slot.predict_fn = predict
|
|
1438
1409
|
|
|
1439
1410
|
elif _runtime == "edgetpu":
|
|
1440
1411
|
Interpreter = _runtime_lib["Interpreter"]
|
|
@@ -1507,6 +1478,15 @@ def _load_model(slot: ModelSlot, config: dict) -> None:
|
|
|
1507
1478
|
|
|
1508
1479
|
|
|
1509
1480
|
def _unload_model(slot: ModelSlot) -> None:
|
|
1481
|
+
model = slot.model
|
|
1482
|
+
# WebGpuIsolatedSession (and any future handle) exposes close().
|
|
1483
|
+
close = getattr(model, "close", None)
|
|
1484
|
+
if callable(close):
|
|
1485
|
+
try:
|
|
1486
|
+
close()
|
|
1487
|
+
except Exception as err:
|
|
1488
|
+
sys.stderr.write(f"model close failed: {err}\n")
|
|
1489
|
+
sys.stderr.flush()
|
|
1510
1490
|
slot.model = None
|
|
1511
1491
|
slot.predict_fn = None
|
|
1512
1492
|
slot.input_names = frozenset()
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Unit tests for WebGPU process isolation (no real ORT/WebGPU required)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import io
|
|
5
|
+
import pickle
|
|
6
|
+
import struct
|
|
7
|
+
import sys
|
|
8
|
+
import unittest
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from unittest.mock import MagicMock, patch
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
13
|
+
|
|
14
|
+
import webgpu_isolated as wi # noqa: E402
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FramingTests(unittest.TestCase):
|
|
18
|
+
def test_roundtrip(self) -> None:
|
|
19
|
+
buf = io.BytesIO()
|
|
20
|
+
payload = {"cmd": "predict", "inputs": {"x": [1, 2, 3]}}
|
|
21
|
+
wi._write_msg(buf, payload)
|
|
22
|
+
buf.seek(0)
|
|
23
|
+
self.assertEqual(wi._read_msg(buf), payload)
|
|
24
|
+
|
|
25
|
+
def test_eof(self) -> None:
|
|
26
|
+
with self.assertRaises(EOFError):
|
|
27
|
+
wi._read_msg(io.BytesIO(b""))
|
|
28
|
+
|
|
29
|
+
def test_short_reads_are_assembled(self) -> None:
|
|
30
|
+
"""Pipe read() may return short; framing must loop until n bytes."""
|
|
31
|
+
payload = pickle.dumps({"ok": True, "n": 42}, protocol=pickle.HIGHEST_PROTOCOL)
|
|
32
|
+
frame = struct.pack(">I", len(payload)) + payload
|
|
33
|
+
|
|
34
|
+
class ShortReader:
|
|
35
|
+
def __init__(self) -> None:
|
|
36
|
+
self._data = frame
|
|
37
|
+
self._i = 0
|
|
38
|
+
|
|
39
|
+
def read(self, n: int) -> bytes:
|
|
40
|
+
# At most 3 bytes per call.
|
|
41
|
+
take = min(3, n, len(self._data) - self._i)
|
|
42
|
+
chunk = self._data[self._i : self._i + take]
|
|
43
|
+
self._i += take
|
|
44
|
+
return chunk
|
|
45
|
+
|
|
46
|
+
self.assertEqual(wi._read_msg(ShortReader()), {"ok": True, "n": 42})
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class IsolatedSessionTests(unittest.TestCase):
|
|
50
|
+
def test_open_load_predict_close(self) -> None:
|
|
51
|
+
replies = [
|
|
52
|
+
{
|
|
53
|
+
"ok": True,
|
|
54
|
+
"providers": ["WebGpuExecutionProvider", "CPUExecutionProvider"],
|
|
55
|
+
"input_name": "images",
|
|
56
|
+
"input_shape": [1, 3, 640, 640],
|
|
57
|
+
"input_type": "tensor(float)",
|
|
58
|
+
"output_names": ["output0"],
|
|
59
|
+
},
|
|
60
|
+
{"ok": True, "outputs": {"output0": [[0.1, 0.2]]}},
|
|
61
|
+
{"ok": True},
|
|
62
|
+
]
|
|
63
|
+
written: list = []
|
|
64
|
+
resp_buf = bytearray()
|
|
65
|
+
|
|
66
|
+
class FakeIpcOut:
|
|
67
|
+
def write(self, b: bytes) -> int:
|
|
68
|
+
# Parse complete frames as they arrive and enqueue replies.
|
|
69
|
+
if not hasattr(self, "_req"):
|
|
70
|
+
self._req = bytearray()
|
|
71
|
+
self._req.extend(b)
|
|
72
|
+
while len(self._req) >= 4:
|
|
73
|
+
(size,) = struct.unpack(">I", self._req[:4])
|
|
74
|
+
if len(self._req) < 4 + size:
|
|
75
|
+
break
|
|
76
|
+
msg = pickle.loads(bytes(self._req[4 : 4 + size]))
|
|
77
|
+
del self._req[: 4 + size]
|
|
78
|
+
written.append(msg)
|
|
79
|
+
reply = pickle.dumps(replies.pop(0))
|
|
80
|
+
resp_buf.extend(struct.pack(">I", len(reply)))
|
|
81
|
+
resp_buf.extend(reply)
|
|
82
|
+
return len(b)
|
|
83
|
+
|
|
84
|
+
def flush(self) -> None:
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
def close(self) -> None:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
class FakeIpcIn:
|
|
91
|
+
def read(self, n: int) -> bytes:
|
|
92
|
+
chunk = bytes(resp_buf[:n])
|
|
93
|
+
del resp_buf[:n]
|
|
94
|
+
return chunk
|
|
95
|
+
|
|
96
|
+
def close(self) -> None:
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
proc = MagicMock()
|
|
100
|
+
proc.pid = 4242
|
|
101
|
+
proc.poll.return_value = None
|
|
102
|
+
proc.wait.return_value = 0
|
|
103
|
+
|
|
104
|
+
fake_out = FakeIpcOut()
|
|
105
|
+
fake_in = FakeIpcIn()
|
|
106
|
+
pipes = iter([(10, 11), (12, 13)])
|
|
107
|
+
|
|
108
|
+
def fake_pipe() -> tuple[int, int]:
|
|
109
|
+
return next(pipes)
|
|
110
|
+
|
|
111
|
+
with (
|
|
112
|
+
patch("webgpu_isolated.os.pipe", side_effect=fake_pipe),
|
|
113
|
+
patch("webgpu_isolated.os.close"),
|
|
114
|
+
patch("webgpu_isolated.os.fdopen", side_effect=[fake_out, fake_in]),
|
|
115
|
+
patch("webgpu_isolated.subprocess.Popen", return_value=proc),
|
|
116
|
+
):
|
|
117
|
+
sess = wi.WebGpuIsolatedSession.open("/tmp/model.onnx", python_exe="python3")
|
|
118
|
+
self.assertEqual(sess.input_name, "images")
|
|
119
|
+
self.assertEqual(written[0]["cmd"], "load")
|
|
120
|
+
out = sess.run({"images": [[1.0]]})
|
|
121
|
+
self.assertEqual(out["output0"], [[0.1, 0.2]])
|
|
122
|
+
self.assertEqual(written[1]["cmd"], "predict")
|
|
123
|
+
sess.close()
|
|
124
|
+
self.assertEqual(written[2]["cmd"], "shutdown")
|
|
125
|
+
|
|
126
|
+
def test_load_failure_kills_child(self) -> None:
|
|
127
|
+
reply = pickle.dumps({"ok": False, "error": "boom"})
|
|
128
|
+
frame = struct.pack(">I", len(reply)) + reply
|
|
129
|
+
|
|
130
|
+
class FakeIpcOut:
|
|
131
|
+
def write(self, b: bytes) -> int:
|
|
132
|
+
return len(b)
|
|
133
|
+
|
|
134
|
+
def flush(self) -> None:
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
def close(self) -> None:
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
class FakeIpcIn:
|
|
141
|
+
def __init__(self) -> None:
|
|
142
|
+
self._data = frame
|
|
143
|
+
self._i = 0
|
|
144
|
+
|
|
145
|
+
def read(self, n: int) -> bytes:
|
|
146
|
+
chunk = self._data[self._i : self._i + n]
|
|
147
|
+
self._i += n
|
|
148
|
+
return chunk
|
|
149
|
+
|
|
150
|
+
def close(self) -> None:
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
proc = MagicMock()
|
|
154
|
+
proc.pid = 1
|
|
155
|
+
proc.poll.return_value = None
|
|
156
|
+
proc.wait.return_value = 0
|
|
157
|
+
pipes = iter([(10, 11), (12, 13)])
|
|
158
|
+
|
|
159
|
+
with (
|
|
160
|
+
patch("webgpu_isolated.os.pipe", side_effect=lambda: next(pipes)),
|
|
161
|
+
patch("webgpu_isolated.os.close"),
|
|
162
|
+
patch("webgpu_isolated.os.fdopen", side_effect=[FakeIpcOut(), FakeIpcIn()]),
|
|
163
|
+
patch("webgpu_isolated.subprocess.Popen", return_value=proc),
|
|
164
|
+
):
|
|
165
|
+
with self.assertRaises(RuntimeError) as ctx:
|
|
166
|
+
wi.WebGpuIsolatedSession.open("/tmp/x.onnx")
|
|
167
|
+
self.assertIn("boom", str(ctx.exception))
|
|
168
|
+
proc.kill.assert_called()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
if __name__ == "__main__":
|
|
172
|
+
unittest.main()
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WebGPU ORT sessions in a dedicated OS process.
|
|
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:
|
|
7
|
+
|
|
8
|
+
- loads a WebGPU session (or CPU fallback) — used by the child worker;
|
|
9
|
+
- hosts the child worker entrypoint (`python webgpu_isolated.py --ipc-in N --ipc-out M`);
|
|
10
|
+
- exposes `WebGpuIsolatedSession` for the pool parent: one child per model.
|
|
11
|
+
|
|
12
|
+
IPC uses dedicated pipe fds (`pass_fds`), never stdin/stdout — ORT/Dawn log on
|
|
13
|
+
fd 1 and a yolov9 raw head on a shared stdout pickle channel truncated the
|
|
14
|
+
frame (2026-10-10). Pipe `read(n)` can also return short; framing uses
|
|
15
|
+
`_read_exact`.
|
|
16
|
+
|
|
17
|
+
OpenVINO / CoreML / classic ONNX stay in-process in inference_pool.py.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import pickle
|
|
23
|
+
import struct
|
|
24
|
+
import subprocess
|
|
25
|
+
import sys
|
|
26
|
+
import threading
|
|
27
|
+
from typing import Any, BinaryIO, Optional
|
|
28
|
+
|
|
29
|
+
ONNX_CPU_EP = "CPUExecutionProvider"
|
|
30
|
+
ONNX_WEBGPU_EP_REGISTRATION = "webgpu_ep_registration"
|
|
31
|
+
|
|
32
|
+
_HDR = struct.Struct(">I")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _read_exact(stream: BinaryIO, n: int) -> bytes:
|
|
36
|
+
"""Read exactly n bytes; pipe read() may return short without EOF."""
|
|
37
|
+
buf = bytearray()
|
|
38
|
+
while len(buf) < n:
|
|
39
|
+
chunk = stream.read(n - len(buf))
|
|
40
|
+
if not chunk:
|
|
41
|
+
raise EOFError("webgpu worker closed")
|
|
42
|
+
buf.extend(chunk)
|
|
43
|
+
return bytes(buf)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _write_msg(stream: BinaryIO, obj: Any) -> None:
|
|
47
|
+
payload = pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL)
|
|
48
|
+
stream.write(_HDR.pack(len(payload)))
|
|
49
|
+
stream.write(payload)
|
|
50
|
+
stream.flush()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _read_msg(stream: BinaryIO) -> Any:
|
|
54
|
+
hdr = _read_exact(stream, _HDR.size)
|
|
55
|
+
(n,) = _HDR.unpack(hdr)
|
|
56
|
+
if n > 512 * 1024 * 1024:
|
|
57
|
+
raise RuntimeError(f"webgpu IPC frame too large: {n}")
|
|
58
|
+
data = _read_exact(stream, n)
|
|
59
|
+
return pickle.loads(data)
|
|
60
|
+
|
|
61
|
+
|
|
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.
|
|
64
|
+
|
|
65
|
+
Same contract as the former inference_pool._load_onnx_webgpu_session.
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
import onnxruntime_ep_webgpu as webgpu_ep # type: ignore
|
|
69
|
+
except Exception as err:
|
|
70
|
+
sys.stderr.write(f"ONNX WebGPU: plugin import failed ({err}); CPU fallback\n")
|
|
71
|
+
sys.stderr.flush()
|
|
72
|
+
return ort.InferenceSession(path, providers=[ONNX_CPU_EP]), [ONNX_CPU_EP]
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
ort.register_execution_provider_library(
|
|
76
|
+
ONNX_WEBGPU_EP_REGISTRATION, webgpu_ep.get_library_path()
|
|
77
|
+
)
|
|
78
|
+
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]
|
|
84
|
+
|
|
85
|
+
ep_name = webgpu_ep.get_ep_name()
|
|
86
|
+
webgpu_device = next(
|
|
87
|
+
(d for d in ort.get_ep_devices() if getattr(d, "ep_name", None) == ep_name),
|
|
88
|
+
None,
|
|
89
|
+
)
|
|
90
|
+
if webgpu_device is None:
|
|
91
|
+
sys.stderr.write(
|
|
92
|
+
f"ONNX WebGPU: no OrtEpDevice for {ep_name!r} "
|
|
93
|
+
f"(Vulkan/libvulkan missing?); CPU fallback\n"
|
|
94
|
+
)
|
|
95
|
+
sys.stderr.flush()
|
|
96
|
+
return ort.InferenceSession(path, providers=[ONNX_CPU_EP]), [ONNX_CPU_EP]
|
|
97
|
+
|
|
98
|
+
sess_options = ort.SessionOptions()
|
|
99
|
+
sess_options.add_provider_for_devices([webgpu_device], {})
|
|
100
|
+
session = ort.InferenceSession(path, sess_options=sess_options)
|
|
101
|
+
active = list(session.get_providers())
|
|
102
|
+
sys.stderr.write(f"ONNX WebGPU: providers={active} ep={ep_name} device=webgpu\n")
|
|
103
|
+
sys.stderr.flush()
|
|
104
|
+
return session, active
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _parse_ipc_fds(argv: list[str]) -> tuple[int, int]:
|
|
108
|
+
ipc_in: Optional[int] = None
|
|
109
|
+
ipc_out: Optional[int] = None
|
|
110
|
+
i = 0
|
|
111
|
+
while i < len(argv):
|
|
112
|
+
if argv[i] == "--ipc-in" and i + 1 < len(argv):
|
|
113
|
+
ipc_in = int(argv[i + 1])
|
|
114
|
+
i += 2
|
|
115
|
+
continue
|
|
116
|
+
if argv[i] == "--ipc-out" and i + 1 < len(argv):
|
|
117
|
+
ipc_out = int(argv[i + 1])
|
|
118
|
+
i += 2
|
|
119
|
+
continue
|
|
120
|
+
i += 1
|
|
121
|
+
if ipc_in is None or ipc_out is None:
|
|
122
|
+
raise SystemExit("webgpu_isolated worker needs --ipc-in FD --ipc-out FD")
|
|
123
|
+
return ipc_in, ipc_out
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _worker_main(argv: list[str]) -> None:
|
|
127
|
+
"""One InferenceSession; commands on --ipc-in, replies on --ipc-out."""
|
|
128
|
+
import numpy as np
|
|
129
|
+
import onnxruntime as ort
|
|
130
|
+
|
|
131
|
+
fd_in, fd_out = _parse_ipc_fds(argv)
|
|
132
|
+
ipc_in = os.fdopen(fd_in, "rb", buffering=0)
|
|
133
|
+
ipc_out = os.fdopen(fd_out, "wb", buffering=0)
|
|
134
|
+
|
|
135
|
+
session = None
|
|
136
|
+
output_names: list[str] = []
|
|
137
|
+
while True:
|
|
138
|
+
try:
|
|
139
|
+
msg = _read_msg(ipc_in)
|
|
140
|
+
except EOFError:
|
|
141
|
+
break
|
|
142
|
+
cmd = msg.get("cmd")
|
|
143
|
+
if cmd == "shutdown":
|
|
144
|
+
_write_msg(ipc_out, {"ok": True})
|
|
145
|
+
break
|
|
146
|
+
if cmd == "load":
|
|
147
|
+
path = msg["path"]
|
|
148
|
+
try:
|
|
149
|
+
session, providers = load_onnx_webgpu_session(ort, path)
|
|
150
|
+
output_names = [o.name for o in session.get_outputs()]
|
|
151
|
+
inp = session.get_inputs()[0]
|
|
152
|
+
_write_msg(
|
|
153
|
+
ipc_out,
|
|
154
|
+
{
|
|
155
|
+
"ok": True,
|
|
156
|
+
"providers": providers,
|
|
157
|
+
"input_name": inp.name,
|
|
158
|
+
"input_shape": list(inp.shape),
|
|
159
|
+
"input_type": inp.type,
|
|
160
|
+
"output_names": output_names,
|
|
161
|
+
},
|
|
162
|
+
)
|
|
163
|
+
except Exception as exc:
|
|
164
|
+
_write_msg(ipc_out, {"ok": False, "error": f"{type(exc).__name__}: {exc}"})
|
|
165
|
+
continue
|
|
166
|
+
if cmd == "predict":
|
|
167
|
+
if session is None:
|
|
168
|
+
_write_msg(ipc_out, {"ok": False, "error": "no session"})
|
|
169
|
+
continue
|
|
170
|
+
try:
|
|
171
|
+
outputs = session.run(output_names, msg["inputs"])
|
|
172
|
+
payload = {
|
|
173
|
+
name: np.ascontiguousarray(out)
|
|
174
|
+
for name, out in zip(output_names, outputs)
|
|
175
|
+
}
|
|
176
|
+
_write_msg(ipc_out, {"ok": True, "outputs": payload})
|
|
177
|
+
except Exception as exc:
|
|
178
|
+
_write_msg(ipc_out, {"ok": False, "error": f"{type(exc).__name__}: {exc}"})
|
|
179
|
+
continue
|
|
180
|
+
_write_msg(ipc_out, {"ok": False, "error": f"unknown cmd {cmd!r}"})
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class WebGpuIsolatedSession:
|
|
184
|
+
"""Parent-side handle: one child process holding one WebGPU ORT session."""
|
|
185
|
+
|
|
186
|
+
def __init__(self, proc: subprocess.Popen, ipc_in: BinaryIO, ipc_out: BinaryIO, meta: dict):
|
|
187
|
+
self._proc = proc
|
|
188
|
+
self._ipc_in = ipc_in # parent reads replies
|
|
189
|
+
self._ipc_out = ipc_out # parent writes commands
|
|
190
|
+
self._lock = threading.Lock()
|
|
191
|
+
self.providers: list[str] = list(meta["providers"])
|
|
192
|
+
self.input_name: str = meta["input_name"]
|
|
193
|
+
self.input_shape = meta["input_shape"]
|
|
194
|
+
self.input_type: str = meta["input_type"]
|
|
195
|
+
self.output_names: list[str] = list(meta["output_names"])
|
|
196
|
+
|
|
197
|
+
@classmethod
|
|
198
|
+
def open(cls, path: str, *, python_exe: Optional[str] = None) -> "WebGpuIsolatedSession":
|
|
199
|
+
script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "webgpu_isolated.py")
|
|
200
|
+
exe = python_exe or sys.executable
|
|
201
|
+
# cmd_r ← parent writes commands; reply_r ← parent reads replies
|
|
202
|
+
cmd_r, cmd_w = os.pipe()
|
|
203
|
+
reply_r, reply_w = os.pipe()
|
|
204
|
+
try:
|
|
205
|
+
proc = subprocess.Popen(
|
|
206
|
+
[exe, script, "--ipc-in", str(cmd_r), "--ipc-out", str(reply_w)],
|
|
207
|
+
pass_fds=(cmd_r, reply_w),
|
|
208
|
+
stdin=subprocess.DEVNULL,
|
|
209
|
+
stdout=None, # inherit — ORT/Dawn logs stay in the pool log
|
|
210
|
+
stderr=None,
|
|
211
|
+
close_fds=True,
|
|
212
|
+
)
|
|
213
|
+
except Exception:
|
|
214
|
+
for fd in (cmd_r, cmd_w, reply_r, reply_w):
|
|
215
|
+
try:
|
|
216
|
+
os.close(fd)
|
|
217
|
+
except OSError:
|
|
218
|
+
pass
|
|
219
|
+
raise
|
|
220
|
+
# Parent keeps cmd_w + reply_r; close the ends the child owns.
|
|
221
|
+
os.close(cmd_r)
|
|
222
|
+
os.close(reply_w)
|
|
223
|
+
ipc_out = os.fdopen(cmd_w, "wb", buffering=0)
|
|
224
|
+
ipc_in = os.fdopen(reply_r, "rb", buffering=0)
|
|
225
|
+
try:
|
|
226
|
+
_write_msg(ipc_out, {"cmd": "load", "path": path})
|
|
227
|
+
reply = _read_msg(ipc_in)
|
|
228
|
+
except Exception:
|
|
229
|
+
ipc_out.close()
|
|
230
|
+
ipc_in.close()
|
|
231
|
+
proc.kill()
|
|
232
|
+
proc.wait(timeout=5)
|
|
233
|
+
raise
|
|
234
|
+
if not reply.get("ok"):
|
|
235
|
+
ipc_out.close()
|
|
236
|
+
ipc_in.close()
|
|
237
|
+
proc.kill()
|
|
238
|
+
proc.wait(timeout=5)
|
|
239
|
+
raise RuntimeError(reply.get("error") or "webgpu child load failed")
|
|
240
|
+
sys.stderr.write(
|
|
241
|
+
f"ONNX WebGPU: isolated child pid={proc.pid} providers={reply['providers']}\n"
|
|
242
|
+
)
|
|
243
|
+
sys.stderr.flush()
|
|
244
|
+
return cls(proc, ipc_in, ipc_out, reply)
|
|
245
|
+
|
|
246
|
+
def run(self, inputs: dict) -> dict:
|
|
247
|
+
with self._lock:
|
|
248
|
+
if self._proc.poll() is not None:
|
|
249
|
+
raise RuntimeError(
|
|
250
|
+
f"webgpu child dead (exit={self._proc.returncode})"
|
|
251
|
+
)
|
|
252
|
+
_write_msg(self._ipc_out, {"cmd": "predict", "inputs": inputs})
|
|
253
|
+
reply = _read_msg(self._ipc_in)
|
|
254
|
+
if not reply.get("ok"):
|
|
255
|
+
raise RuntimeError(reply.get("error") or "webgpu predict failed")
|
|
256
|
+
return reply["outputs"]
|
|
257
|
+
|
|
258
|
+
def close(self) -> None:
|
|
259
|
+
proc = self._proc
|
|
260
|
+
if proc.poll() is not None:
|
|
261
|
+
try:
|
|
262
|
+
self._ipc_out.close()
|
|
263
|
+
except Exception:
|
|
264
|
+
pass
|
|
265
|
+
try:
|
|
266
|
+
self._ipc_in.close()
|
|
267
|
+
except Exception:
|
|
268
|
+
pass
|
|
269
|
+
return
|
|
270
|
+
try:
|
|
271
|
+
_write_msg(self._ipc_out, {"cmd": "shutdown"})
|
|
272
|
+
proc.wait(timeout=5)
|
|
273
|
+
except Exception:
|
|
274
|
+
pass
|
|
275
|
+
try:
|
|
276
|
+
self._ipc_out.close()
|
|
277
|
+
except Exception:
|
|
278
|
+
pass
|
|
279
|
+
try:
|
|
280
|
+
self._ipc_in.close()
|
|
281
|
+
except Exception:
|
|
282
|
+
pass
|
|
283
|
+
if proc.poll() is None:
|
|
284
|
+
proc.kill()
|
|
285
|
+
try:
|
|
286
|
+
proc.wait(timeout=5)
|
|
287
|
+
except Exception:
|
|
288
|
+
pass
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
if __name__ == "__main__":
|
|
292
|
+
_worker_main(sys.argv[1:])
|