@camstack/addon-pipeline 1.2.349 → 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/audio-analyzer/index.js +3 -3
- package/dist/audio-analyzer/index.mjs +3 -3
- package/dist/{custom-model-catalog-ae8UGV5D.js → custom-model-catalog-CPqbnIUh.js} +10 -7
- package/dist/{custom-model-catalog-B9iY9L4J.mjs → custom-model-catalog-DdHOXMKu.mjs} +10 -7
- package/dist/detection-pipeline/index.js +36 -17
- package/dist/detection-pipeline/index.mjs +36 -18
- package/dist/{dist-BrHqpGoN.js → dist-C2jSMJo_.js} +41 -2
- package/dist/{dist-Cr4QsawE.mjs → dist-DFCl7PZV.mjs} +41 -2
- package/dist/{model-catalogs-BHBzN67T.js → model-catalogs-CVhIiNhe.js} +1 -1
- package/dist/{model-catalogs-Bj9lxXcI.mjs → model-catalogs-Cind8NmH.mjs} +1 -1
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-CJ1zilVO.js → node-B7ZinEw5.js} +1 -1
- package/dist/{node-Dym0f_3g.mjs → node-C12l0K5V.mjs} +1 -1
- package/dist/pipeline-runner/index.js +3 -3
- package/dist/pipeline-runner/index.mjs +3 -3
- package/dist/{process-memory-CwDymrXT.mjs → process-memory-3F1cP1ou.mjs} +1 -1
- package/dist/{process-memory-CAMKwMJs.js → process-memory-C1s8YIw4.js} +1 -1
- package/dist/recorder/index.js +2 -2
- package/dist/recorder/index.mjs +2 -2
- package/dist/{segment-demux-js-CjTXPNsG.mjs → segment-demux-js-7l4LsdU4.mjs} +1 -1
- package/dist/{segment-demux-js-BEaJw3G9.js → segment-demux-js-B2IxTWms.js} +1 -1
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CFZDNGBf.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BgC3mSLA.mjs} +2 -2
- 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-Bx_U_Tc7.mjs → hostInit-DfOhCBj-.mjs} +2 -2
- 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 +87 -45
- package/python/requirements-webgpu.txt +7 -0
- package/python/test_inference_pool_device_selection.py +10 -0
- package/python/test_webgpu_isolated.py +172 -0
- package/python/webgpu_isolated.py +292 -0
|
@@ -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",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_segment_demux_js = require("../segment-demux-js-
|
|
1
|
+
const require_segment_demux_js = require("../segment-demux-js-B2IxTWms.js");
|
|
2
2
|
let node_worker_threads = require("node:worker_threads");
|
|
3
3
|
//#region src/stream-broker/recorded/demux-worker-child.ts
|
|
4
4
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as demuxSegmentFullJs } from "../segment-demux-js-
|
|
1
|
+
import { t as demuxSegmentFullJs } from "../segment-demux-js-7l4LsdU4.mjs";
|
|
2
2
|
import { parentPort } from "node:worker_threads";
|
|
3
3
|
//#region src/stream-broker/recorded/demux-worker-child.ts
|
|
4
4
|
/**
|
|
@@ -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,
|
|
@@ -3,12 +3,12 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../chunk-emK7D4bc.js");
|
|
6
|
-
const require_dist = require("../dist-
|
|
7
|
-
const require_node = require("../node-
|
|
6
|
+
const require_dist = require("../dist-C2jSMJo_.js");
|
|
7
|
+
const require_node = require("../node-B7ZinEw5.js");
|
|
8
8
|
const require_restream_intent = require("../restream-intent-7uDNqMKs.js");
|
|
9
9
|
const require_remote_restream = require("../remote-restream-CO36Sr30.js");
|
|
10
10
|
const require_retire_root_keys = require("../retire-root-keys-Csb8z2f3.js");
|
|
11
|
-
const require_segment_demux_js = require("../segment-demux-js-
|
|
11
|
+
const require_segment_demux_js = require("../segment-demux-js-B2IxTWms.js");
|
|
12
12
|
let node_os = require("node:os");
|
|
13
13
|
node_os = require_chunk.__toESM(node_os);
|
|
14
14
|
let node_crypto = require("node:crypto");
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { r as __toESM } from "../chunk-DnnnRqeS.mjs";
|
|
2
|
-
import { $n as discriminatedUnion, An as DeviceFeature, At as gifFromMp4Args, Cn as AUDIO_PRESETS, D as G711_SCALE_CORRECTION_DB, Dt as ffmpegRate, En as buildFfmpegArgs, Fn as CAM_PROFILE_ORDER, Gn as makeSourceBrokerId, Ht as memoizePinnedFfmpeg, In as CamProfileSchema, It as loadContributionCapability, K as RingBuffer, Kn as nodePin, Lt as logChannelsCapability, Mn as errMsg, Nn as BOOT_RECOVERY_BACKOFF_MS, On as invocationFromEncodeProfile, Pn as BaseAddon, Qn as boolean, R as RATE_CONTROL_RELAXED, Rn as createEvent, Sn as webrtcSessionCapability, Un as isEvent, Vn as expandAudioChunkToF32le, Wn as makeProfileBrokerId, X as addonWidgetsSourceCapability, Xn as _enum, Zn as array, ar as record, at as cameraStreamsCapability, bt as egressTransportFromRequest, c as CLIP_STILL_REASON_CODE_HEADER, cr as EventCategory, dt as createLogChannelsProvider, en as rankStreams, ft as declareLogChannel, jn as DeviceType, kn as isSoftwareDecode, kt as gifDeriveSpeed, mt as deriveBatteryPresence, nr as number, nt as batteryCapability, o as BatteryStatusSchema, or as string, qn as parseProfileBrokerId, rr as object, s as CLIP_BROKER_PACED_RATES, sn as resolveEgressDecodeHwAccel, sr as union, st as clipExportFailureOf, tr as literal, ut as createHwAccelCache, vn as streamBrokerCapability, w as EncodeProfileSchema, yt as egressTranscodeSharingKey, z as RATE_CONTROL_TIGHT, zt as maskUrlCredentials } from "../dist-
|
|
3
|
-
import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, o as probeFfmpegBinary, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-
|
|
2
|
+
import { $n as discriminatedUnion, An as DeviceFeature, At as gifFromMp4Args, Cn as AUDIO_PRESETS, D as G711_SCALE_CORRECTION_DB, Dt as ffmpegRate, En as buildFfmpegArgs, Fn as CAM_PROFILE_ORDER, Gn as makeSourceBrokerId, Ht as memoizePinnedFfmpeg, In as CamProfileSchema, It as loadContributionCapability, K as RingBuffer, Kn as nodePin, Lt as logChannelsCapability, Mn as errMsg, Nn as BOOT_RECOVERY_BACKOFF_MS, On as invocationFromEncodeProfile, Pn as BaseAddon, Qn as boolean, R as RATE_CONTROL_RELAXED, Rn as createEvent, Sn as webrtcSessionCapability, Un as isEvent, Vn as expandAudioChunkToF32le, Wn as makeProfileBrokerId, X as addonWidgetsSourceCapability, Xn as _enum, Zn as array, ar as record, at as cameraStreamsCapability, bt as egressTransportFromRequest, c as CLIP_STILL_REASON_CODE_HEADER, cr as EventCategory, dt as createLogChannelsProvider, en as rankStreams, ft as declareLogChannel, jn as DeviceType, kn as isSoftwareDecode, kt as gifDeriveSpeed, mt as deriveBatteryPresence, nr as number, nt as batteryCapability, o as BatteryStatusSchema, or as string, qn as parseProfileBrokerId, rr as object, s as CLIP_BROKER_PACED_RATES, sn as resolveEgressDecodeHwAccel, sr as union, st as clipExportFailureOf, tr as literal, ut as createHwAccelCache, vn as streamBrokerCapability, w as EncodeProfileSchema, yt as egressTranscodeSharingKey, z as RATE_CONTROL_TIGHT, zt as maskUrlCredentials } from "../dist-DFCl7PZV.mjs";
|
|
3
|
+
import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, o as probeFfmpegBinary, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-C12l0K5V.mjs";
|
|
4
4
|
import { a as parseRestreamPath, d as PREBUFFER_MAX_MS, f as capPreRollRequestMs, i as isLiveEdgeOnlyDial, m as resolvePrebuffer, n as NO_DECLARATION, p as prebufferRetentionMs, s as sdpVariantFor, u as withSentryIntent } from "../restream-intent-DyGmLAU9.mjs";
|
|
5
5
|
import { n as profileForStreamId } from "../remote-restream-BeHi78PZ.mjs";
|
|
6
6
|
import { n as RowMapStore, t as retireRootKeys } from "../retire-root-keys-DZvlso2l.mjs";
|
|
7
|
-
import { a as require_mp4box_all, c as parseNalLengthSize, i as runFfmpegOnce, n as demuxSegmentPreferJs, o as avccSampleToAnnexB, r as FfmpegFallbackRefusedError, s as buildKeyframeAnnexB, t as demuxSegmentFullJs } from "../segment-demux-js-
|
|
7
|
+
import { a as require_mp4box_all, c as parseNalLengthSize, i as runFfmpegOnce, n as demuxSegmentPreferJs, o as avccSampleToAnnexB, r as FfmpegFallbackRefusedError, s as buildKeyframeAnnexB, t as demuxSegmentFullJs } from "../segment-demux-js-7l4LsdU4.mjs";
|
|
8
8
|
import { createRequire } from "node:module";
|
|
9
9
|
import * as os$1 from "node:os";
|
|
10
10
|
import { networkInterfaces, tmpdir } from "node:os";
|
|
@@ -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
|
@@ -468,6 +468,8 @@ def _resolve_onnx_providers(
|
|
|
468
468
|
Deliberate adaptations for camstack:
|
|
469
469
|
- an operator device pin is honored: "cuda"/"coreml" put that EP first
|
|
470
470
|
(CPU still last as fallback); "cpu" compiles CPU only.
|
|
471
|
+
- "webgpu" is NOT handled here — it uses an isolated child process
|
|
472
|
+
(`WebGpuIsolatedSession` / webgpu_isolated.py; D823).
|
|
471
473
|
- hardware entries not present in `available`
|
|
472
474
|
(ort.get_available_providers()) are dropped — camstack ships the
|
|
473
475
|
plain `onnxruntime` wheel on most nodes, and requesting an EP the
|
|
@@ -481,8 +483,9 @@ def _resolve_onnx_providers(
|
|
|
481
483
|
ordered.append((ONNX_CUDA_EP, {"device_id": cuda_device_id}))
|
|
482
484
|
elif dev == "coreml":
|
|
483
485
|
ordered.append(ONNX_COREML_EP)
|
|
484
|
-
elif dev
|
|
485
|
-
|
|
486
|
+
elif dev in ("cpu", "webgpu"):
|
|
487
|
+
# webgpu uses the plugin path; if we fall through here it must be CPU only.
|
|
488
|
+
pass
|
|
486
489
|
else:
|
|
487
490
|
# AUTO/default — platform-driven ordering.
|
|
488
491
|
if plat == "darwin":
|
|
@@ -497,6 +500,12 @@ def _resolve_onnx_providers(
|
|
|
497
500
|
return providers
|
|
498
501
|
|
|
499
502
|
|
|
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
|
|
507
|
+
|
|
508
|
+
|
|
500
509
|
# ---------------------------------------------------------------------------
|
|
501
510
|
# CoreML persistent compiled-model cache
|
|
502
511
|
# ---------------------------------------------------------------------------
|
|
@@ -1328,51 +1337,75 @@ def _load_model(slot: ModelSlot, config: dict) -> None:
|
|
|
1328
1337
|
elif _runtime == "onnxruntime":
|
|
1329
1338
|
ort = _runtime_lib
|
|
1330
1339
|
ort_device = str(config.get("device", "") or "")
|
|
1331
|
-
#
|
|
1332
|
-
#
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
slot.config["_input_name"] = session.get_inputs()[0].name
|
|
1355
|
-
# Declared input layout (ArcFace/CLIP are NHWC) → correct preprocess.
|
|
1356
|
-
try:
|
|
1357
|
-
slot.config["_input_shape"] = session.get_inputs()[0].shape
|
|
1358
|
-
except Exception:
|
|
1359
|
-
pass
|
|
1360
|
-
# Declared input DTYPE. super-gradients / Frigate YOLO-NAS exports bake
|
|
1361
|
-
# their preprocessing into the graph and take `tensor(uint8)` — the
|
|
1362
|
-
# preprocess must feed raw uint8 pixels, not the float32 [0,1] tensor
|
|
1363
|
-
# (a hard onnxruntime type error otherwise).
|
|
1364
|
-
try:
|
|
1365
|
-
if session.get_inputs()[0].type == "tensor(uint8)":
|
|
1366
|
-
slot.config["_input_dtype"] = "uint8"
|
|
1367
|
-
except Exception:
|
|
1368
|
-
pass
|
|
1340
|
+
# WebGPU uses the plugin-EP SessionOptions path (ORT ≥1.24); everything
|
|
1341
|
+
# else uses the classic providers= list (see _resolve_onnx_providers).
|
|
1342
|
+
if ort_device.strip().lower() == "webgpu":
|
|
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
|
|
1347
|
+
_active_eps = [p for p in active_list if p != ONNX_CPU_EP]
|
|
1348
|
+
sys.stderr.write(
|
|
1349
|
+
f"ONNX: execution provider {', '.join(_active_eps) or ONNX_CPU_EP} "
|
|
1350
|
+
f"(device=webgpu, isolated)\n"
|
|
1351
|
+
)
|
|
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
|
|
1369
1363
|
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
return {name: out for name, out in zip(output_names, outputs)}
|
|
1364
|
+
def predict(inp_dict: dict, _iso=isolated) -> dict:
|
|
1365
|
+
return _iso.run(inp_dict)
|
|
1373
1366
|
|
|
1374
|
-
|
|
1375
|
-
|
|
1367
|
+
slot.model = isolated
|
|
1368
|
+
slot.predict_fn = predict
|
|
1369
|
+
else:
|
|
1370
|
+
try:
|
|
1371
|
+
available_eps = list(ort.get_available_providers())
|
|
1372
|
+
except Exception:
|
|
1373
|
+
available_eps = [ONNX_CPU_EP]
|
|
1374
|
+
providers = _resolve_onnx_providers(
|
|
1375
|
+
ort_device, sys.platform, platform.machine(), available_eps,
|
|
1376
|
+
)
|
|
1377
|
+
session = ort.InferenceSession(path, providers=providers)
|
|
1378
|
+
_active_eps = [p for p in session.get_providers() if p != ONNX_CPU_EP]
|
|
1379
|
+
_requested = [p[0] if isinstance(p, tuple) else p for p in providers]
|
|
1380
|
+
sys.stderr.write(
|
|
1381
|
+
f"ONNX: execution provider {', '.join(_active_eps) or ONNX_CPU_EP} "
|
|
1382
|
+
f"(device={ort_device or 'auto'}, requested={_requested}, "
|
|
1383
|
+
f"available={available_eps})\n"
|
|
1384
|
+
)
|
|
1385
|
+
sys.stderr.flush()
|
|
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
|
|
1402
|
+
|
|
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)}
|
|
1406
|
+
|
|
1407
|
+
slot.model = session
|
|
1408
|
+
slot.predict_fn = predict
|
|
1376
1409
|
|
|
1377
1410
|
elif _runtime == "edgetpu":
|
|
1378
1411
|
Interpreter = _runtime_lib["Interpreter"]
|
|
@@ -1445,6 +1478,15 @@ def _load_model(slot: ModelSlot, config: dict) -> None:
|
|
|
1445
1478
|
|
|
1446
1479
|
|
|
1447
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()
|
|
1448
1490
|
slot.model = None
|
|
1449
1491
|
slot.predict_fn = None
|
|
1450
1492
|
slot.input_names = frozenset()
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# ONNX Runtime WebGPU plugin EP — AMD iGPU (RADV) and other Vulkan adapters.
|
|
2
|
+
# Requires a compatible core ORT (plugin refuses older builds at register time).
|
|
3
|
+
# Installed lazily when the node offers `onnx:webgpu` (see runtime-packages /
|
|
4
|
+
# shouldInstallWebgpu). Does NOT replace requirements-onnxruntime.txt for the
|
|
5
|
+
# CPU/CUDA path — a node that only needs the floor keeps the lighter pin.
|
|
6
|
+
onnxruntime>=1.24.4,<2
|
|
7
|
+
onnxruntime-ep-webgpu>=0.4.0,<1
|
|
@@ -232,6 +232,16 @@ class ResolveOnnxProvidersTest(unittest.TestCase):
|
|
|
232
232
|
[ONNX_CPU_EP],
|
|
233
233
|
)
|
|
234
234
|
|
|
235
|
+
def test_webgpu_pin_is_cpu_only_in_classic_resolver(self) -> None:
|
|
236
|
+
# WebGPU uses the plugin SessionOptions path; the classic resolver
|
|
237
|
+
# must not inject CUDA when device=webgpu (would race the plugin load).
|
|
238
|
+
self.assertEqual(
|
|
239
|
+
_resolve_onnx_providers(
|
|
240
|
+
"webgpu", "linux", "x86_64", [ONNX_CUDA_EP, ONNX_CPU_EP],
|
|
241
|
+
),
|
|
242
|
+
[ONNX_CPU_EP],
|
|
243
|
+
)
|
|
244
|
+
|
|
235
245
|
def test_cuda_pin_orders_cuda_first_cpu_last(self) -> None:
|
|
236
246
|
self.assertEqual(
|
|
237
247
|
_resolve_onnx_providers(
|
|
@@ -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()
|