@camstack/addon-pipeline 1.1.26 → 1.1.28
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 +7 -6
- package/dist/audio-analyzer/index.mjs +7 -6
- package/dist/audio-codec-ffmpeg/index.js +1 -1
- package/dist/audio-codec-ffmpeg/index.mjs +1 -1
- package/dist/constants-CAqYVigN.js +6615 -0
- package/dist/constants-oYgW36Ty.mjs +3587 -0
- package/dist/decoder-ffmpeg/index.js +74 -281
- package/dist/decoder-ffmpeg/index.mjs +73 -280
- package/dist/decoder-nodeav/index.js +1218 -0
- package/dist/decoder-nodeav/index.mjs +1210 -0
- package/dist/detection-pipeline/index.js +157 -219
- package/dist/detection-pipeline/index.mjs +157 -219
- package/dist/{dist-DAIlCdAx.js → dist-Biq62zt4.js} +372 -35
- package/dist/{dist-CgEP_0OL.mjs → dist-C6_wgXqF.mjs} +373 -24
- package/dist/dist-D3Ytt-_Y.js +41111 -0
- package/dist/dist-DluJuxV5.mjs +37976 -0
- package/dist/{frame-handle-plane-Dq20KtKL.mjs → frame-handle-plane-BIoY6nRV.mjs} +13 -5
- package/dist/{frame-handle-plane-DtTRX_0n.js → frame-handle-plane-D6BzyEgy.js} +13 -5
- package/dist/frame-ring-sink-9J0wCdLF.js +444 -0
- package/dist/frame-ring-sink-Cs9vby6v.mjs +409 -0
- package/dist/motion-wasm/index.js +55 -3
- package/dist/motion-wasm/index.mjs +55 -3
- package/dist/node-topology-platform-BkR_k6WT.mjs +15 -0
- package/dist/node-topology-platform-CFZ7F4xW.js +20 -0
- package/dist/pipeline-runner/index.js +50 -16
- package/dist/pipeline-runner/index.mjs +50 -16
- package/dist/recorder/index.js +36 -40
- package/dist/recorder/index.mjs +36 -40
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-5tQlh9h4.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DoReAb4y.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DhY3MZ2C.mjs +26 -0
- package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BJK0-svt.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DtoYGpTp.mjs} +1 -1
- package/dist/stream-broker/{hostInit-DyLqyJaS.mjs → hostInit-C0SuwQhL.mjs} +3 -3
- package/dist/stream-broker/index.js +81 -40
- package/dist/stream-broker/index.mjs +81 -40
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-BDLNwJ_F.js → MaskShapeCanvas-DI4BY7W2-Br2yi8ah.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-CoLjNiUN.js → MotionZonesSettings-NcxxQN8r-BSCcPdPf.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-DJE3OU-q.js → PrivacyMaskSettings-APgPLF7p-ztxvxSjS.js} +1 -1
- package/embed-dist/assets/index-C5UpuPr8.css +2 -0
- package/embed-dist/assets/{index-C-pL8ETk.js → index-CeZV1B-2.js} +10 -10
- package/embed-dist/index.html +2 -2
- package/package.json +31 -3
- package/python/inference_pool.py +108 -121
- package/python/test_inference_pool_device_selection.py +47 -43
- package/python/yamnet_audio.py +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CkOPfV8r.mjs +0 -26
- package/embed-dist/assets/index-DrJ0ee3f.css +0 -2
package/embed-dist/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>CamStack Embed</title>
|
|
7
|
-
<script type="module" crossorigin src="./assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
7
|
+
<script type="module" crossorigin src="./assets/index-CeZV1B-2.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="./assets/index-C5UpuPr8.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-pipeline",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.28",
|
|
4
4
|
"description": "CamStack Pipeline bundle — runner, detection, motion, decoders, audio + stream broker. Multi-entry npm package shipping 7 addons under a single bundle.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|
|
@@ -36,6 +36,10 @@
|
|
|
36
36
|
"import": "./dist/decoder-ffmpeg/index.mjs",
|
|
37
37
|
"require": "./dist/decoder-ffmpeg/index.js"
|
|
38
38
|
},
|
|
39
|
+
"./decoder-nodeav": {
|
|
40
|
+
"import": "./dist/decoder-nodeav/index.mjs",
|
|
41
|
+
"require": "./dist/decoder-nodeav/index.js"
|
|
42
|
+
},
|
|
39
43
|
"./audio-codec-ffmpeg": {
|
|
40
44
|
"import": "./dist/audio-codec-ffmpeg/index.mjs",
|
|
41
45
|
"require": "./dist/audio-codec-ffmpeg/index.js"
|
|
@@ -149,7 +153,7 @@
|
|
|
149
153
|
"category": "pipeline",
|
|
150
154
|
"name": "Decoder (ffmpeg)",
|
|
151
155
|
"version": "0.1.0",
|
|
152
|
-
"description": "Out-of-process video decoder spawning an ffmpeg subprocess —
|
|
156
|
+
"description": "Out-of-process video decoder spawning an ffmpeg subprocess — a decode crash is isolated to the child, never taking down the runner. The DEFAULT decoder backend: it registers the decoder cap only when the node's `decoder` settings section selects `ffmpeg` (the default). ffmpeg is provisioned per node via ctx.deps.ensureFfmpeg().",
|
|
153
157
|
"entry": "./dist/decoder-ffmpeg/index.js",
|
|
154
158
|
"execution": {
|
|
155
159
|
"placement": "any-node",
|
|
@@ -157,7 +161,30 @@
|
|
|
157
161
|
},
|
|
158
162
|
"capabilities": [
|
|
159
163
|
{
|
|
160
|
-
"name": "decoder"
|
|
164
|
+
"name": "decoder",
|
|
165
|
+
"optional": true
|
|
166
|
+
}
|
|
167
|
+
],
|
|
168
|
+
"passive": true,
|
|
169
|
+
"protected": true,
|
|
170
|
+
"icon": "assets/icon.svg",
|
|
171
|
+
"color": "#0ea5e9"
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"id": "decoder-nodeav",
|
|
175
|
+
"category": "pipeline",
|
|
176
|
+
"name": "Decoder (node-av)",
|
|
177
|
+
"version": "0.1.0",
|
|
178
|
+
"description": "In-process video decoder using FFmpeg native bindings (node-av) — no subprocess lifecycle (nothing to spawn, supervise, or leak). Registers the decoder cap only when the node's `decoder` settings section selects `nodeav`; decoder-ffmpeg is the default backend.",
|
|
179
|
+
"entry": "./dist/decoder-nodeav/index.js",
|
|
180
|
+
"execution": {
|
|
181
|
+
"placement": "any-node",
|
|
182
|
+
"heapProfile": "heavy"
|
|
183
|
+
},
|
|
184
|
+
"capabilities": [
|
|
185
|
+
{
|
|
186
|
+
"name": "decoder",
|
|
187
|
+
"optional": true
|
|
161
188
|
}
|
|
162
189
|
],
|
|
163
190
|
"passive": true,
|
|
@@ -299,6 +326,7 @@
|
|
|
299
326
|
"@camstack/system": "*",
|
|
300
327
|
"lucide-react": "^0.511.0",
|
|
301
328
|
"mp4box": "0.5.4",
|
|
329
|
+
"node-av": "^6.0.0",
|
|
302
330
|
"sharp": "^0.35.2",
|
|
303
331
|
"zod": "^4.3.6"
|
|
304
332
|
},
|
package/python/inference_pool.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""Async inference pool — request_id multiplexed, per-runtime concurrency.
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
Per-runtime ML backends (coreml / openvino / onnx):
|
|
5
5
|
- asyncio main loop reads requests from stdin; inference is dispatched
|
|
6
6
|
onto a runtime-specific executor so the reader never blocks.
|
|
7
7
|
- Each request carries a 32-bit id; responses are tagged with the same
|
|
@@ -10,9 +10,10 @@ Architecture mirrors Scrypted's ML plugins (coreml / openvino / onnx):
|
|
|
10
10
|
CoreML → ThreadPoolExecutor(1) — ANE is single-context; one
|
|
11
11
|
Python thread is enough, and avoids GIL thrashing.
|
|
12
12
|
OpenVINO → ThreadPoolExecutor(OPTIMAL_NUMBER_OF_INFER_REQUESTS),
|
|
13
|
-
each
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
each worker checking out an InferRequest from a fixed
|
|
14
|
+
per-model pool (created once, post-compile), with the model
|
|
15
|
+
compiled on ONE explicit device under the LATENCY hint (no
|
|
16
|
+
AUTO — its CPU->GPU handover invalidated in-flight requests).
|
|
16
17
|
ONNX → ThreadPoolExecutor(N) — N independent InferenceSessions
|
|
17
18
|
where N = concurrency setting; each session pinned to
|
|
18
19
|
its own worker thread.
|
|
@@ -48,7 +49,6 @@ import platform
|
|
|
48
49
|
import shutil
|
|
49
50
|
import struct
|
|
50
51
|
import sys
|
|
51
|
-
import threading
|
|
52
52
|
import time
|
|
53
53
|
from collections import deque
|
|
54
54
|
from dataclasses import dataclass, field
|
|
@@ -76,7 +76,7 @@ MSG_INFER_RAW = 0x02
|
|
|
76
76
|
# Each item is dispatched concurrently via asyncio.gather so the
|
|
77
77
|
# predict pool's existing parallelism applies; the saving over N
|
|
78
78
|
# separate calls is one IPC round-trip per item collapsed to one,
|
|
79
|
-
#
|
|
79
|
+
# with batch=4 semantics for fair benchmarking.
|
|
80
80
|
MSG_INFER_BATCH = 0x03
|
|
81
81
|
MSG_CACHE_FRAME = 0x04
|
|
82
82
|
MSG_INFER_CACHED = 0x05
|
|
@@ -166,62 +166,47 @@ class ModelSlot:
|
|
|
166
166
|
_runtime: str = ""
|
|
167
167
|
_runtime_lib: Any = None
|
|
168
168
|
# Max OPTIMAL_NUMBER_OF_INFER_REQUESTS across loaded OpenVINO models — used
|
|
169
|
-
# to size the predict pool so the
|
|
169
|
+
# to size the predict pool so the per-model InferRequest pool is actually fed.
|
|
170
170
|
_ov_optimal_reqs: int = 0
|
|
171
171
|
# Default OpenVINO predict-pool size when models load lazily (so we can't yet
|
|
172
172
|
# query the device's optimal request count). ≈ measured optimal on Intel
|
|
173
173
|
# CPU/iGPU/NPU (4-5). Threads idle-block on infer, so over-provisioning is cheap.
|
|
174
174
|
OV_DEFAULT_CONCURRENCY: int = 4
|
|
175
|
-
# OpenVINO device
|
|
176
|
-
#
|
|
177
|
-
#
|
|
178
|
-
#
|
|
179
|
-
# (
|
|
180
|
-
#
|
|
181
|
-
#
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
gpu (intel only) -> "GPU" (explicit — the N100 case)
|
|
196
|
-
else -> "AUTO"
|
|
175
|
+
# OpenVINO device decision. AUTO is NEVER used: the AUTO plugin runs inference
|
|
176
|
+
# on a CPU "helper" while the GPU JIT-compiles, then hands the model over to the
|
|
177
|
+
# GPU mid-flight. InferRequests created during that window bind to the
|
|
178
|
+
# CPU-helper's Result ports; after handover the served model's ports diverge and
|
|
179
|
+
# req.infer() raises "Cannot find tensor for port opset1::Result score_8" (proven
|
|
180
|
+
# live on the N100 iGPU). Compiling ONE explicit device removes the handover
|
|
181
|
+
# entirely. We resolve an ordered candidate list (NPU > GPU > CPU) and compile the
|
|
182
|
+
# first that succeeds, with CPU as the guaranteed floor.
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _resolve_ov_devices(available: list[str], full_names: dict[str, str]) -> list[str]:
|
|
186
|
+
"""Ordered EXPLICIT device candidates for compile-time failover (NPU>GPU>CPU).
|
|
187
|
+
|
|
188
|
+
Never returns an AUTO/AUTO:.. mode (see the module comment above). `available`
|
|
189
|
+
is `core.available_devices`; `full_names` maps each device to its
|
|
190
|
+
FULL_DEVICE_NAME (missing entries = the probe failed, left unclassified). A
|
|
191
|
+
GPU is only accepted when its FULL_DEVICE_NAME probed successfully (this is
|
|
192
|
+
also how an Intel iGPU vs an NVIDIA dGPU is told apart downstream). Ordering:
|
|
193
|
+
NPU(s) -> GPU(s) -> CPU (always last). The compile loop tries each in order
|
|
194
|
+
and stops at the first that compiles; CPU is the guaranteed fallback.
|
|
197
195
|
"""
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
dgpus: list[str] = []
|
|
196
|
+
npus: list[str] = []
|
|
197
|
+
gpus: list[str] = []
|
|
201
198
|
for device in available:
|
|
202
199
|
full_device_name = full_names.get(device)
|
|
203
|
-
if full_device_name is None:
|
|
204
|
-
continue
|
|
205
|
-
if "NVIDIA" in full_device_name and "dGPU" in full_device_name:
|
|
206
|
-
dgpus.append(device)
|
|
207
200
|
if "NPU" in device:
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
mode = "AUTO:NPU,CPU"
|
|
218
|
-
elif len(dgpus):
|
|
219
|
-
mode = f"AUTO:{','.join(dgpus)},CPU"
|
|
220
|
-
# forcing GPU can cause crashes on older GPU. (Scrypted's comment — the
|
|
221
|
-
# GPU-mode compile failure falls back to AUTO in the compile loop below.)
|
|
222
|
-
elif gpu:
|
|
223
|
-
mode = "GPU"
|
|
224
|
-
return mode
|
|
201
|
+
npus.append(device)
|
|
202
|
+
elif "GPU" in device and full_device_name is not None:
|
|
203
|
+
gpus.append(device)
|
|
204
|
+
ordered: list[str] = [*npus, *gpus]
|
|
205
|
+
if "CPU" in available and "CPU" not in ordered:
|
|
206
|
+
ordered.append("CPU")
|
|
207
|
+
if not ordered:
|
|
208
|
+
ordered = ["CPU"]
|
|
209
|
+
return ordered
|
|
225
210
|
|
|
226
211
|
|
|
227
212
|
# ONNX Runtime execution-provider names used by _resolve_onnx_providers.
|
|
@@ -239,8 +224,7 @@ def _resolve_onnx_providers(
|
|
|
239
224
|
) -> list:
|
|
240
225
|
"""Ordered ONNX Runtime providers — hardware first, CPU always last.
|
|
241
226
|
|
|
242
|
-
|
|
243
|
-
for the AUTO/default case:
|
|
227
|
+
Ordering for the AUTO/default case:
|
|
244
228
|
|
|
245
229
|
darwin -> CoreMLExecutionProvider
|
|
246
230
|
linux/win on x86_64/AMD64 -> ("CUDAExecutionProvider", {"device_id": N})
|
|
@@ -253,8 +237,8 @@ def _resolve_onnx_providers(
|
|
|
253
237
|
(ort.get_available_providers()) are dropped — camstack ships the
|
|
254
238
|
plain `onnxruntime` wheel on most nodes, and requesting an EP the
|
|
255
239
|
build lacks raises at session creation instead of falling back.
|
|
256
|
-
- platform match uses startswith so
|
|
257
|
-
|
|
240
|
+
- platform match uses startswith so a substring test on "win" never
|
|
241
|
+
wrongly matches "darwin".
|
|
258
242
|
"""
|
|
259
243
|
dev = (device or "").strip().lower()
|
|
260
244
|
ordered: list = []
|
|
@@ -265,7 +249,7 @@ def _resolve_onnx_providers(
|
|
|
265
249
|
elif dev == "cpu":
|
|
266
250
|
pass # explicit CPU pin — no hardware EPs
|
|
267
251
|
else:
|
|
268
|
-
# AUTO/default —
|
|
252
|
+
# AUTO/default — platform-driven ordering.
|
|
269
253
|
if plat == "darwin":
|
|
270
254
|
ordered.append(ONNX_COREML_EP)
|
|
271
255
|
if plat.startswith(("linux", "win")) and machine in ("x86_64", "AMD64"):
|
|
@@ -281,8 +265,8 @@ def _resolve_onnx_providers(
|
|
|
281
265
|
# ---------------------------------------------------------------------------
|
|
282
266
|
# CoreML persistent compiled-model cache
|
|
283
267
|
# ---------------------------------------------------------------------------
|
|
284
|
-
# macOS
|
|
285
|
-
#
|
|
268
|
+
# Known macOS behavior: the OS-level compiled-model cache is NOT reused across
|
|
269
|
+
# process restarts — every
|
|
286
270
|
# `ct.models.MLModel(.mlpackage)` load RECOMPILES the model, and the stale OS
|
|
287
271
|
# cache is only cleared on reboot. camstack loads the .mlpackage on every pool
|
|
288
272
|
# spawn, so each spawn paid the full recompile. Fix: after the first compile,
|
|
@@ -472,9 +456,9 @@ def _load_model(slot: ModelSlot, config: dict) -> None:
|
|
|
472
456
|
"ane": ct.ComputeUnit.CPU_AND_NE,
|
|
473
457
|
"all": ct.ComputeUnit.ALL,
|
|
474
458
|
}
|
|
475
|
-
# NOTE
|
|
476
|
-
#
|
|
477
|
-
#
|
|
459
|
+
# NOTE: a macOS behavior can cause the .mlpackage to be RECOMPILED on
|
|
460
|
+
# every load with the compiled cache not reused until reboot. camstack
|
|
461
|
+
# loads the .mlpackage on
|
|
478
462
|
# every pool spawn, so it was exposed to the same trap.
|
|
479
463
|
# _acquire_coreml_model routes through a persistent `.coreml-cache`
|
|
480
464
|
# of the compiled `.mlmodelc` (CompiledMLModel on hit, plain MLModel
|
|
@@ -525,13 +509,13 @@ def _load_model(slot: ModelSlot, config: dict) -> None:
|
|
|
525
509
|
|
|
526
510
|
elif _runtime == "openvino":
|
|
527
511
|
core = _runtime_lib
|
|
528
|
-
|
|
529
|
-
#
|
|
530
|
-
#
|
|
531
|
-
#
|
|
532
|
-
#
|
|
533
|
-
#
|
|
534
|
-
if
|
|
512
|
+
ov_device_req = str(config.get("device", "AUTO")).upper()
|
|
513
|
+
# Device switch (see _resolve_ov_devices above): an AUTO/default request
|
|
514
|
+
# classifies the enumerated devices by FULL_DEVICE_NAME and yields an
|
|
515
|
+
# ORDERED EXPLICIT candidate list (NPU>GPU>CPU) for compile-time
|
|
516
|
+
# failover. An operator-pinned device (config "device" = gpu/cpu/npu) is
|
|
517
|
+
# honored unchanged — compiled directly, no failover.
|
|
518
|
+
if ov_device_req in ("AUTO", "DEFAULT", ""):
|
|
535
519
|
available = list(core.available_devices)
|
|
536
520
|
full_names: dict[str, str] = {}
|
|
537
521
|
for dev in available:
|
|
@@ -539,41 +523,38 @@ def _load_model(slot: ModelSlot, config: dict) -> None:
|
|
|
539
523
|
full_names[dev] = str(core.get_property(dev, "FULL_DEVICE_NAME"))
|
|
540
524
|
except Exception:
|
|
541
525
|
pass
|
|
542
|
-
|
|
526
|
+
candidates = _resolve_ov_devices(available, full_names)
|
|
543
527
|
sys.stderr.write(
|
|
544
|
-
f"OpenVINO:
|
|
528
|
+
f"OpenVINO: explicit device candidates {candidates} "
|
|
545
529
|
f"(available: {available}, names: {full_names})\n"
|
|
546
530
|
)
|
|
547
531
|
sys.stderr.flush()
|
|
548
532
|
else:
|
|
549
|
-
|
|
533
|
+
candidates = [ov_device_req] # operator pin — compiled directly
|
|
534
|
+
sys.stderr.write(f"OpenVINO: operator-pinned device {ov_device_req}\n")
|
|
550
535
|
sys.stderr.flush()
|
|
551
|
-
#
|
|
552
|
-
#
|
|
553
|
-
#
|
|
554
|
-
#
|
|
555
|
-
#
|
|
556
|
-
|
|
557
|
-
ov_config = {"PERFORMANCE_HINT": "THROUGHPUT"}
|
|
536
|
+
# LATENCY (not THROUGHPUT): one deterministic execution stream instead of
|
|
537
|
+
# N. THROUGHPUT spun up multiple internal streams whose lazy request
|
|
538
|
+
# creation collided with the AUTO handover AND multiplied memory (a factor
|
|
539
|
+
# in the N100 OOM-freeze). LATENCY keeps a single stream; camera-level
|
|
540
|
+
# concurrency comes from the fixed InferRequest pool built below.
|
|
541
|
+
ov_config = {"PERFORMANCE_HINT": "LATENCY"}
|
|
558
542
|
# Persist compiled GPU/NPU kernels so the multi-second JIT compile is paid
|
|
559
543
|
# once (first boot) instead of on every pool spawn. The cold compile storm
|
|
560
|
-
# (~25s for the full model set on an N100 iGPU) is
|
|
561
|
-
#
|
|
544
|
+
# (~25s for the full model set on an N100 iGPU) is where frames pile up;
|
|
545
|
+
# caching the kernels shrinks that window on subsequent boots.
|
|
562
546
|
try:
|
|
563
547
|
_cache_dir = os.path.join(os.path.dirname(os.path.abspath(path)), ".ov-cache")
|
|
564
548
|
os.makedirs(_cache_dir, exist_ok=True)
|
|
565
549
|
ov_config["CACHE_DIR"] = _cache_dir
|
|
566
550
|
except OSError:
|
|
567
551
|
pass
|
|
568
|
-
# Compile
|
|
569
|
-
#
|
|
570
|
-
#
|
|
571
|
-
#
|
|
572
|
-
# load/replace error handler reports the failure.
|
|
573
|
-
candidates = [ov_device]
|
|
574
|
-
if "GPU" in ov_device:
|
|
575
|
-
candidates.append("AUTO")
|
|
552
|
+
# Compile-time failover over the explicit candidate list (NO AUTO): try
|
|
553
|
+
# each device in order (NPU>GPU>CPU) and stop at the first that compiles.
|
|
554
|
+
# CPU is the guaranteed floor. If every candidate fails, re-raise — the
|
|
555
|
+
# existing load/replace error handler reports the failure.
|
|
576
556
|
compiled = None
|
|
557
|
+
ov_device = None
|
|
577
558
|
last_exc: Optional[BaseException] = None
|
|
578
559
|
for candidate in candidates:
|
|
579
560
|
try:
|
|
@@ -582,16 +563,16 @@ def _load_model(slot: ModelSlot, config: dict) -> None:
|
|
|
582
563
|
break
|
|
583
564
|
except Exception as exc:
|
|
584
565
|
last_exc = exc
|
|
585
|
-
sys.stderr.write(f"OpenVINO: compile
|
|
566
|
+
sys.stderr.write(f"OpenVINO: compile on {candidate} failed: {exc}\n")
|
|
586
567
|
if candidate != candidates[-1]:
|
|
587
|
-
sys.stderr.write(
|
|
568
|
+
sys.stderr.write("OpenVINO: falling back to next device.\n")
|
|
588
569
|
sys.stderr.flush()
|
|
589
570
|
if compiled is None:
|
|
590
571
|
if last_exc is not None:
|
|
591
572
|
raise last_exc
|
|
592
573
|
raise RuntimeError("OpenVINO: no device available to compile the model")
|
|
593
574
|
# Name the resolved mode + what OpenVINO actually placed the model on
|
|
594
|
-
# (
|
|
575
|
+
# (the EXECUTION_DEVICES property) so a silent CPU
|
|
595
576
|
# placement is visible in the pool stderr.
|
|
596
577
|
try:
|
|
597
578
|
_exec_devices = compiled.get_property("EXECUTION_DEVICES")
|
|
@@ -614,30 +595,36 @@ def _load_model(slot: ModelSlot, config: dict) -> None:
|
|
|
614
595
|
except Exception:
|
|
615
596
|
pass
|
|
616
597
|
|
|
617
|
-
#
|
|
618
|
-
#
|
|
619
|
-
#
|
|
620
|
-
|
|
598
|
+
# Create ALL InferRequests ONCE, now, post-compile. With AUTO gone there
|
|
599
|
+
# is no CPU->GPU handover, so no request can be born mid-handover and bind
|
|
600
|
+
# to stale ports — the score_8 race is structurally removed. Requests are
|
|
601
|
+
# handed out through a thread-safe queue: an InferRequest is never touched
|
|
602
|
+
# by two threads at once (checkout/return), so sharing a fixed pool is safe
|
|
603
|
+
# and avoids the old thread-local lazy creation. Size the pool to the
|
|
604
|
+
# device's optimal count (>= OV_DEFAULT_CONCURRENCY) so it matches the
|
|
605
|
+
# predict-pool worker count sized in _run().
|
|
606
|
+
import queue as _queue
|
|
607
|
+
|
|
608
|
+
try:
|
|
609
|
+
_n_req = int(compiled.get_property("OPTIMAL_NUMBER_OF_INFER_REQUESTS"))
|
|
610
|
+
except Exception:
|
|
611
|
+
_n_req = 0
|
|
612
|
+
_n_req = max(_n_req, OV_DEFAULT_CONCURRENCY)
|
|
613
|
+
_req_pool: "_queue.Queue" = _queue.Queue()
|
|
614
|
+
for _ in range(_n_req):
|
|
615
|
+
_req_pool.put(compiled.create_infer_request())
|
|
616
|
+
sys.stderr.write(f"OpenVINO: created {_n_req} InferRequest(s) up-front on {ov_device}\n")
|
|
617
|
+
sys.stderr.flush()
|
|
621
618
|
|
|
622
|
-
def predict(inp_dict: dict,
|
|
623
|
-
req = getattr(_tls, "req", None)
|
|
624
|
-
if req is None:
|
|
625
|
-
req = _tls.req = _c.create_infer_request()
|
|
619
|
+
def predict(inp_dict: dict, _names=output_names, _pool=_req_pool) -> dict:
|
|
626
620
|
inp = list(inp_dict.values())[0]
|
|
621
|
+
req = _pool.get()
|
|
627
622
|
try:
|
|
628
623
|
result = req.infer(inp)
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
# "Cannot find tensor for port ... score_8". (Reproduced live on
|
|
634
|
-
# the N100 iGPU under concurrent THROUGHPUT threads; the raise is
|
|
635
|
-
# in req.infer(), not the output fetch below.) Recreate the
|
|
636
|
-
# thread-local request post-handover and retry once.
|
|
637
|
-
req = _tls.req = _c.create_infer_request()
|
|
638
|
-
result = req.infer(inp)
|
|
639
|
-
# Fetch outputs by positional index (stable across the handover)
|
|
640
|
-
# rather than ConstOutput port identity — belt-and-braces.
|
|
624
|
+
finally:
|
|
625
|
+
_pool.put(req)
|
|
626
|
+
# Output fetch by positional index — stable and harmless now that the
|
|
627
|
+
# ports no longer diverge (single explicit device, no handover).
|
|
641
628
|
return {name: result[i] for i, name in enumerate(_names)}
|
|
642
629
|
|
|
643
630
|
slot.model = compiled
|
|
@@ -646,7 +633,7 @@ def _load_model(slot: ModelSlot, config: dict) -> None:
|
|
|
646
633
|
elif _runtime == "onnxruntime":
|
|
647
634
|
ort = _runtime_lib
|
|
648
635
|
ort_device = str(config.get("device", "") or "")
|
|
649
|
-
#
|
|
636
|
+
# Ordered providers — hardware EP first, CPU LAST as
|
|
650
637
|
# fallback (see _resolve_onnx_providers): a "cuda"/"coreml" pin puts
|
|
651
638
|
# that EP first, a "cpu" pin compiles CPU only, and the AUTO/default
|
|
652
639
|
# case derives the hardware EP from the platform.
|
|
@@ -658,8 +645,8 @@ def _load_model(slot: ModelSlot, config: dict) -> None:
|
|
|
658
645
|
ort_device, sys.platform, platform.machine(), available_eps,
|
|
659
646
|
)
|
|
660
647
|
session = ort.InferenceSession(path, providers=providers)
|
|
661
|
-
# Report the EP that ACTUALLY initialized
|
|
662
|
-
#
|
|
648
|
+
# Report the EP that ACTUALLY initialized (CPU stripped from the log)
|
|
649
|
+
# so a silent CPU fallback is visible.
|
|
663
650
|
_active_eps = [p for p in session.get_providers() if p != ONNX_CPU_EP]
|
|
664
651
|
_requested = [p[0] if isinstance(p, tuple) else p for p in providers]
|
|
665
652
|
sys.stderr.write(
|
|
@@ -1271,18 +1258,18 @@ async def _run() -> None:
|
|
|
1271
1258
|
sys.stderr.flush()
|
|
1272
1259
|
models.append(slot)
|
|
1273
1260
|
|
|
1274
|
-
# OpenVINO: size the predict pool so the
|
|
1275
|
-
# actually fed by concurrent
|
|
1276
|
-
# concurrency=1 (it predates
|
|
1261
|
+
# OpenVINO: size the predict pool so the fixed InferRequest pool (built per
|
|
1262
|
+
# model in _load_model) is actually fed by concurrent predict workers. The
|
|
1263
|
+
# addon passes concurrency=1 (it predates the request pool) and models load
|
|
1277
1264
|
# lazily AFTER this point, so we can't read OPTIMAL_NUMBER_OF_INFER_REQUESTS
|
|
1278
1265
|
# here — default to OV_DEFAULT_CONCURRENCY (≈ the measured optimal of 4-5
|
|
1279
1266
|
# on Intel CPU/iGPU/NPU). If startup-loaded models report a higher optimal,
|
|
1280
|
-
# use that.
|
|
1267
|
+
# use that. The per-model InferRequest pool is sized to the same target.
|
|
1281
1268
|
if runtime == "openvino":
|
|
1282
1269
|
target = max(_ov_optimal_reqs, OV_DEFAULT_CONCURRENCY)
|
|
1283
1270
|
if target > concurrency:
|
|
1284
1271
|
sys.stderr.write(
|
|
1285
|
-
f"OpenVINO
|
|
1272
|
+
f"OpenVINO LATENCY: predict concurrency {concurrency} -> {target}\n"
|
|
1286
1273
|
)
|
|
1287
1274
|
sys.stderr.flush()
|
|
1288
1275
|
concurrency = target
|