@camstack/addon-pipeline 1.2.614 → 1.2.616
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/THIRD_PARTY_MODELS.md +2 -2
- package/dist/audio-analyzer/index.js +3 -3
- package/dist/audio-analyzer/index.mjs +3 -3
- package/dist/{custom-model-catalog-D_mgggfr.js → custom-model-catalog-D0EZW4Xb.js} +3 -3
- package/dist/{custom-model-catalog-CLhfiG1p.mjs → custom-model-catalog-D3NopOC9.mjs} +3 -3
- package/dist/detection-pipeline/index.js +114 -6
- package/dist/detection-pipeline/index.mjs +114 -6
- package/dist/{dist-BGq_y1xM.mjs → dist-BWPVIuD_.mjs} +237 -61
- package/dist/{dist-F6F_xjNj.js → dist-D2aJhYm7.js} +237 -61
- package/dist/{model-catalogs-DrUv77wD.js → model-catalogs-BDJEmiKm.js} +14 -4
- package/dist/{model-catalogs-CF3WVSy_.mjs → model-catalogs-CYoIeMo4.mjs} +14 -4
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-D_K-84ig.js → node-BKs5_GlD.js} +1 -1
- package/dist/{node-DwTqHnch.mjs → node-D48F38hW.mjs} +1 -1
- package/dist/pipeline-runner/index.js +3 -3
- package/dist/pipeline-runner/index.mjs +3 -3
- package/dist/{process-memory-k316CseF.mjs → process-memory-58pkBnTk.mjs} +1 -1
- package/dist/{process-memory-BN3N6YZh.js → process-memory-BY-1p6rF.js} +1 -1
- package/dist/recorder/index.js +114 -11
- package/dist/recorder/index.mjs +114 -11
- package/dist/{segment-demux-js-C4-eYvhD.js → segment-demux-js-DbM8w1dc.js} +1 -1
- package/dist/{segment-demux-js-CWJseQHd.mjs → segment-demux-js-VeHvPIJe.mjs} +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DrWysXZj.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-B6vm1OlX.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-qcb2Czgh.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-BvJiBUdN.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-TnEvW5tZ.mjs} +1 -1
- package/dist/stream-broker/demux-worker-child.js +1 -1
- package/dist/stream-broker/demux-worker-child.mjs +1 -1
- package/dist/stream-broker/{hostInit-D5F0gL07.mjs → hostInit-BnK9Yje5.mjs} +3 -3
- package/dist/stream-broker/index.js +38 -5
- package/dist/stream-broker/index.mjs +38 -5
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/python/inference_pool.py +33 -0
- package/python/openvino_compile_probe.py +245 -0
- package/python/test_openvino_compile_probe.py +298 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-QzO03I5h.mjs +0 -26
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { r as __toESM } from "../chunk-DnnnRqeS.mjs";
|
|
2
|
-
import { $n as parseProfileBrokerId, $t as rankStreams, An as DeviceFeature, At as gifDeriveSpeed, Bn as createEvent, Cn as AUDIO_PRESETS, D as G711_SCALE_CORRECTION_DB, En as buildFfmpegArgs, Fn as CAM_PROFILE_ORDER, I as RATE_CONTROL_RELAXED, In as CamProfileSchema, It as loadContributionCapability, J as addonWidgetsSourceCapability, Jn as isSecretMarker, L as RATE_CONTROL_TIGHT, Lt as logChannelsCapability, Mn as errMsg, Nn as BOOT_RECOVERY_BACKOFF_MS, On as invocationFromEncodeProfile, Ot as ffmpegRate, Pn as BaseAddon, Qn as nodePin, Sn as webrtcSessionCapability, Vt as memoizePinnedFfmpeg, W as RingBuffer, Wn as expandAudioChunkToF32le, Xn as makeSourceBrokerId, Yn as makeProfileBrokerId, Z as armRemovalReconcile, Zn as maskUrlCredentials, ar as boolean, bt as egressTranscodeSharingKey, c as CLIP_STILL_REASON_CODE_HEADER, cr as literal, dt as declareLogChannel, fr as record, hr as EventCategory, ir as array, it as cameraStreamsCapability, jn as DeviceType, jt as gifFromMp4Args, kn as isSoftwareDecode, lr as number, lt as createHwAccelCache, mr as union, o as BatteryStatusSchema, on as resolveEgressDecodeHwAccel, or as discriminatedUnion, ot as clipExportFailureOf, pr as string, pt as deriveBatteryPresence, qn as isEvent, rr as _enum, s as CLIP_BROKER_PACED_RATES, tt as batteryCapability, ur as object, ut as createLogChannelsProvider, vn as streamBrokerCapability, vt as deviceRemovalHookCapability, w as EncodeProfileSchema, xt as egressTransportFromRequest } from "../dist-
|
|
3
|
-
import { a as defaultFfmpegSpawn, i as NO_COST_CLAIM, n as Fmp4FragmentChild, r as Fmp4FragmentPlane, s as probeFfmpegBinary, t as ChildCostRegistry } from "../node-
|
|
2
|
+
import { $n as parseProfileBrokerId, $t as rankStreams, An as DeviceFeature, At as gifDeriveSpeed, Bn as createEvent, Cn as AUDIO_PRESETS, D as G711_SCALE_CORRECTION_DB, En as buildFfmpegArgs, Fn as CAM_PROFILE_ORDER, I as RATE_CONTROL_RELAXED, In as CamProfileSchema, It as loadContributionCapability, J as addonWidgetsSourceCapability, Jn as isSecretMarker, L as RATE_CONTROL_TIGHT, Lt as logChannelsCapability, Mn as errMsg, Nn as BOOT_RECOVERY_BACKOFF_MS, On as invocationFromEncodeProfile, Ot as ffmpegRate, Pn as BaseAddon, Qn as nodePin, Sn as webrtcSessionCapability, Vt as memoizePinnedFfmpeg, W as RingBuffer, Wn as expandAudioChunkToF32le, Xn as makeSourceBrokerId, Yn as makeProfileBrokerId, Z as armRemovalReconcile, Zn as maskUrlCredentials, ar as boolean, bt as egressTranscodeSharingKey, c as CLIP_STILL_REASON_CODE_HEADER, cr as literal, dt as declareLogChannel, fr as record, hr as EventCategory, ir as array, it as cameraStreamsCapability, jn as DeviceType, jt as gifFromMp4Args, kn as isSoftwareDecode, lr as number, lt as createHwAccelCache, mr as union, o as BatteryStatusSchema, on as resolveEgressDecodeHwAccel, or as discriminatedUnion, ot as clipExportFailureOf, pr as string, pt as deriveBatteryPresence, qn as isEvent, rr as _enum, s as CLIP_BROKER_PACED_RATES, tt as batteryCapability, ur as object, ut as createLogChannelsProvider, vn as streamBrokerCapability, vt as deviceRemovalHookCapability, w as EncodeProfileSchema, xt as egressTransportFromRequest } from "../dist-BWPVIuD_.mjs";
|
|
3
|
+
import { a as defaultFfmpegSpawn, i as NO_COST_CLAIM, n as Fmp4FragmentChild, r as Fmp4FragmentPlane, s as probeFfmpegBinary, t as ChildCostRegistry } from "../node-D48F38hW.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
|
-
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-
|
|
6
|
+
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-VeHvPIJe.mjs";
|
|
7
7
|
import { createRequire } from "node:module";
|
|
8
8
|
import * as os$1 from "node:os";
|
|
9
9
|
import { networkInterfaces, tmpdir } from "node:os";
|
|
@@ -5684,6 +5684,13 @@ var RTSP_DESCRIBE_WARMUP_ATTRIBUTION = {
|
|
|
5684
5684
|
label: "describe-warmup",
|
|
5685
5685
|
media: "both"
|
|
5686
5686
|
};
|
|
5687
|
+
/** How long a DESCRIBE waits for the broker to dial upstream and parse the SDP. */
|
|
5688
|
+
var SDP_WAIT_MS = 3e3;
|
|
5689
|
+
/** The numeric device id a broker id (`<deviceId>/<stream>`) belongs to, or undefined. */
|
|
5690
|
+
function deviceIdOfBroker(brokerId) {
|
|
5691
|
+
const head = Number((brokerId ?? "").split("/")[0]);
|
|
5692
|
+
return Number.isInteger(head) ? head : void 0;
|
|
5693
|
+
}
|
|
5687
5694
|
/**
|
|
5688
5695
|
* Single TCP server for all RTSP restream connections.
|
|
5689
5696
|
* Routes clients to the correct RtspRestreamer by a random token path
|
|
@@ -5716,6 +5723,16 @@ var RtspListenServer = class RtspListenServer {
|
|
|
5716
5723
|
brokerWarmUps = /* @__PURE__ */ new Map();
|
|
5717
5724
|
/** brokerId → sentry picture-window opener */
|
|
5718
5725
|
brokerPrestreamOpeners = /* @__PURE__ */ new Map();
|
|
5726
|
+
logger = null;
|
|
5727
|
+
sdpWaitMs = SDP_WAIT_MS;
|
|
5728
|
+
/** Shorten the DESCRIBE SDP wait (tests only). */
|
|
5729
|
+
setSdpWaitMs(ms) {
|
|
5730
|
+
this.sdpWaitMs = ms;
|
|
5731
|
+
}
|
|
5732
|
+
/** The logger the refusal branches write to; absent in unit specs that do not care. */
|
|
5733
|
+
setLogger(logger) {
|
|
5734
|
+
this.logger = logger;
|
|
5735
|
+
}
|
|
5719
5736
|
/** Generate a random token for a stream path */
|
|
5720
5737
|
static generateToken() {
|
|
5721
5738
|
return randomBytes(16).toString("hex");
|
|
@@ -5911,8 +5928,23 @@ var RtspListenServer = class RtspListenServer {
|
|
|
5911
5928
|
};
|
|
5912
5929
|
socket.once("close", releaseOnce);
|
|
5913
5930
|
socket.once("error", releaseOnce);
|
|
5914
|
-
await this.waitForSdp(restreamer, sdpVariant,
|
|
5915
|
-
const
|
|
5931
|
+
const sdpReady = await this.waitForSdp(restreamer, sdpVariant, this.sdpWaitMs);
|
|
5932
|
+
const sdp = restreamer.getSdpFor(sdpVariant) ?? restreamer.getSdp() ?? "";
|
|
5933
|
+
if (!sdpReady || sdp.length === 0) {
|
|
5934
|
+
releaseOnce();
|
|
5935
|
+
this.logger?.warn("RTSP DESCRIBE refused: no SDP after the wait", {
|
|
5936
|
+
tags: { deviceId: deviceIdOfBroker(brokerId) },
|
|
5937
|
+
meta: {
|
|
5938
|
+
brokerId,
|
|
5939
|
+
waitedMs: this.sdpWaitMs,
|
|
5940
|
+
variant: sdpVariant
|
|
5941
|
+
}
|
|
5942
|
+
});
|
|
5943
|
+
socket.write(`RTSP/1.0 503 Service Unavailable\r\nCSeq: 1\r\n\r\n`);
|
|
5944
|
+
socket.destroy();
|
|
5945
|
+
return;
|
|
5946
|
+
}
|
|
5947
|
+
const session = new RtspSession(socket, sdp, () => {
|
|
5916
5948
|
restreamer.removeSession(session.getSessionId());
|
|
5917
5949
|
releaseOnce();
|
|
5918
5950
|
}, isMuted, declaration);
|
|
@@ -18017,6 +18049,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
18017
18049
|
};
|
|
18018
18050
|
constructor(logger) {
|
|
18019
18051
|
this.logger = logger;
|
|
18052
|
+
this.rtspServer.setLogger(logger.child("rtsp-listen"));
|
|
18020
18053
|
this.rtspProvider = new RtspRestreamProviderImpl(this.rtspServer, logger.child("rtsp-restream"));
|
|
18021
18054
|
}
|
|
18022
18055
|
/**
|
|
@@ -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-B6vm1OlX.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.616",
|
|
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
|
@@ -1045,6 +1045,27 @@ def _pin_batch_for_device(core: Any, source: Any, path: str, device: str) -> Any
|
|
|
1045
1045
|
return model
|
|
1046
1046
|
|
|
1047
1047
|
|
|
1048
|
+
# The OpenVINO compile probe (`openvino_compile_probe`) runs the pool's own
|
|
1049
|
+
# `_load_model` in a throwaway child; the child switches the probe off in itself.
|
|
1050
|
+
_ov_probe_enabled: bool = True
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
def _ov_env_tag(core: Any) -> str:
|
|
1054
|
+
"""OpenVINO version + GPU driver: what a persisted probe verdict is valid for."""
|
|
1055
|
+
parts: "list[str]" = []
|
|
1056
|
+
try:
|
|
1057
|
+
from openvino import get_version
|
|
1058
|
+
|
|
1059
|
+
parts.append(str(get_version()))
|
|
1060
|
+
except Exception:
|
|
1061
|
+
pass
|
|
1062
|
+
try:
|
|
1063
|
+
parts.append(str(core.get_property("GPU", "DEVICE_ARCHITECTURE")))
|
|
1064
|
+
except Exception:
|
|
1065
|
+
pass
|
|
1066
|
+
return "|".join(parts)
|
|
1067
|
+
|
|
1068
|
+
|
|
1048
1069
|
def _load_model(slot: ModelSlot, config: dict) -> None:
|
|
1049
1070
|
"""Load a model into a slot using the active runtime. Thin adapter per backend."""
|
|
1050
1071
|
slot.config = dict(config)
|
|
@@ -1163,6 +1184,18 @@ def _load_model(slot: ModelSlot, config: dict) -> None:
|
|
|
1163
1184
|
ov_config["CACHE_DIR"] = _cache_dir
|
|
1164
1185
|
except OSError:
|
|
1165
1186
|
pass
|
|
1187
|
+
# A GPU/NPU compile that has no blob yet runs in a CHILD first
|
|
1188
|
+
# (`openvino_compile_probe`): a compiler SIGSEGV is refused here with
|
|
1189
|
+
# `OpenvinoLoadCrashed` instead of killing this worker, and on success
|
|
1190
|
+
# the child has written the blob, so the compile below is a cache hit.
|
|
1191
|
+
if _ov_probe_enabled:
|
|
1192
|
+
import openvino_compile_probe
|
|
1193
|
+
|
|
1194
|
+
openvino_compile_probe.ensure_openvino_loadable(
|
|
1195
|
+
path, config, candidates,
|
|
1196
|
+
cache_dir=ov_config.get("CACHE_DIR"),
|
|
1197
|
+
env_tag=_ov_env_tag(core),
|
|
1198
|
+
)
|
|
1166
1199
|
# Fix 2 — fold uint8→float/scale/layout preprocessing INTO the compiled
|
|
1167
1200
|
# graph via PrePostProcessor. When it succeeds we compile the PPP model
|
|
1168
1201
|
# object (fed uint8 NHWC at inference time); on ANY failure we fall back
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""Compile an OpenVINO GPU/NPU model in a CHILD process before the pool loads it.
|
|
2
|
+
|
|
3
|
+
Why (2026-10-22, hub with a Core Ultra 7 265K iGPU, OpenVINO 2025.4.1): the
|
|
4
|
+
iGPU compiler SIGSEGVed the pool worker three times in 15 s while
|
|
5
|
+
`compile_model()` ran for a model with no compiled blob (`malloc(): unsorted
|
|
6
|
+
double linked list corrupted`, then `longjmp causes uninitialized stack
|
|
7
|
+
frame`). The compile runs in the same process as the predict threads and every
|
|
8
|
+
other model, so one native crash took the whole `openvino:gpu` worker (and the
|
|
9
|
+
camera's detection with it) down, and the respawn went straight back into the
|
|
10
|
+
same load. Python cannot catch a SIGSEGV. A warm load (blob present) never
|
|
11
|
+
crashed.
|
|
12
|
+
|
|
13
|
+
So the first load of each (model x device request x compile config) happens in
|
|
14
|
+
a throwaway interpreter that runs the pool's OWN `_load_model` — same
|
|
15
|
+
PrePostProcessor, same candidate list, same properties, same CACHE_DIR — and
|
|
16
|
+
therefore writes the blob the pool then loads warm. Mirrors CoreML's
|
|
17
|
+
`coreml_load_probe.py` (D742):
|
|
18
|
+
|
|
19
|
+
ok -> load in-process (a cache HIT), never probe this key again
|
|
20
|
+
while the blobs the probe wrote are still on disk
|
|
21
|
+
crashed -> refuse with `OpenvinoLoadCrashed` (reason
|
|
22
|
+
`native-load-crashed`), now and on every later load of the
|
|
23
|
+
same key, without re-probing
|
|
24
|
+
inconclusive -> (a Python exception in the child, a probe past its bound, a
|
|
25
|
+
child that could not start) remembered NOWHERE; the
|
|
26
|
+
in-process load runs exactly as it did before this gate
|
|
27
|
+
|
|
28
|
+
Only a death by SIGNAL is a crash. CPU-only loads are never probed.
|
|
29
|
+
"""
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
import os
|
|
34
|
+
import subprocess
|
|
35
|
+
import sys
|
|
36
|
+
from typing import Callable, Optional
|
|
37
|
+
|
|
38
|
+
# One-way: inference_pool imports this module lazily, inside its OpenVINO branch.
|
|
39
|
+
from coreml_load_probe import (
|
|
40
|
+
VERDICT_CRASHED,
|
|
41
|
+
VERDICT_INCONCLUSIVE,
|
|
42
|
+
VERDICT_OK,
|
|
43
|
+
_signal_name,
|
|
44
|
+
classify_probe_exit,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
PROBE_VERDICT_VERSION = 1
|
|
48
|
+
PROBE_MARKER = "@@CAMSTACK_OV_PROBE_OK@@"
|
|
49
|
+
# A cold iGPU compile of a large detector takes tens of seconds; past this the
|
|
50
|
+
# probe says nothing and the pool's own load bound (POOL_MODEL_LOAD_BOUNDS)
|
|
51
|
+
# is the only fence, exactly as before the probe existed.
|
|
52
|
+
PROBE_TIMEOUT_SEC = 300.0
|
|
53
|
+
# The config keys that change what `compile_model` is handed (PrePostProcessor
|
|
54
|
+
# plan, input shape, precision hint). A threshold or a class list does not.
|
|
55
|
+
COMPILE_CONFIG_KEYS = (
|
|
56
|
+
"device",
|
|
57
|
+
"inputSize",
|
|
58
|
+
"inputWidth",
|
|
59
|
+
"inputHeight",
|
|
60
|
+
"inputChannels",
|
|
61
|
+
"preprocessMode",
|
|
62
|
+
"inputNormalization",
|
|
63
|
+
"gpuInferencePrecision",
|
|
64
|
+
)
|
|
65
|
+
PROBED_DEVICE_PREFIXES = ("GPU", "NPU")
|
|
66
|
+
|
|
67
|
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
68
|
+
|
|
69
|
+
# The child runs the pool's own loader, with the probe switched off in it.
|
|
70
|
+
_CHILD_CODE = f"""
|
|
71
|
+
import json, sys
|
|
72
|
+
args = json.loads(sys.argv[1])
|
|
73
|
+
sys.path.insert(0, args["pool_dir"])
|
|
74
|
+
import inference_pool as ip
|
|
75
|
+
ip._ov_probe_enabled = False
|
|
76
|
+
ip._init_runtime("openvino", str(args["config"].get("device", "")))
|
|
77
|
+
ip._load_model(ip.ModelSlot(), args["config"])
|
|
78
|
+
print({PROBE_MARKER!r}, flush=True)
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
ProbeRunner = Callable[[str, dict], "tuple[str, str]"]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class OpenvinoLoadCrashed(Exception):
|
|
85
|
+
"""The model kills the native compiler on this device; refused instead of loaded."""
|
|
86
|
+
|
|
87
|
+
reason = "native-load-crashed"
|
|
88
|
+
|
|
89
|
+
def __init__(self, model_path: str, device: str, detail: str) -> None:
|
|
90
|
+
self.model_path = model_path
|
|
91
|
+
self.device = device
|
|
92
|
+
self.detail = detail
|
|
93
|
+
super().__init__(
|
|
94
|
+
f"OpenVINO model {os.path.basename(model_path)} crashes the native compiler "
|
|
95
|
+
f"on device={device} ({detail}); refused so it cannot take the pool down. "
|
|
96
|
+
f"Run this model on another device, or replace it"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def needs_probe(candidates: "list[str]") -> bool:
|
|
101
|
+
"""True when any compile target is a GPU or an NPU (CPU-only loads are never probed)."""
|
|
102
|
+
return any(str(c).upper().startswith(PROBED_DEVICE_PREFIXES) for c in candidates)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def run_probe(
|
|
106
|
+
model_path: str,
|
|
107
|
+
config: dict,
|
|
108
|
+
child_code: str = _CHILD_CODE,
|
|
109
|
+
timeout_sec: float = PROBE_TIMEOUT_SEC,
|
|
110
|
+
) -> "tuple[str, str]":
|
|
111
|
+
"""Compile `model_path` in a fresh interpreter; return (verdict, detail)."""
|
|
112
|
+
payload = json.dumps({"config": {**config, "path": model_path}, "pool_dir": _HERE})
|
|
113
|
+
try:
|
|
114
|
+
proc = subprocess.run(
|
|
115
|
+
[sys.executable, "-c", child_code, payload],
|
|
116
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
|
|
117
|
+
timeout=timeout_sec,
|
|
118
|
+
)
|
|
119
|
+
except subprocess.TimeoutExpired:
|
|
120
|
+
return VERDICT_INCONCLUSIVE, f"probe timed out after {timeout_sec:g}s"
|
|
121
|
+
except (OSError, TypeError, ValueError) as exc:
|
|
122
|
+
return VERDICT_INCONCLUSIVE, f"probe could not start ({exc})"
|
|
123
|
+
marker_seen = any(line.strip() == PROBE_MARKER for line in proc.stdout.splitlines())
|
|
124
|
+
verdict = classify_probe_exit(proc.returncode, marker_seen)
|
|
125
|
+
tail = " | ".join(proc.stderr.strip().splitlines()[-3:])
|
|
126
|
+
if proc.returncode < 0:
|
|
127
|
+
detail = f"probe child died by signal {-proc.returncode} ({_signal_name(-proc.returncode)})"
|
|
128
|
+
else:
|
|
129
|
+
detail = f"probe child exited {proc.returncode}"
|
|
130
|
+
return verdict, f"{detail}{': ' + tail if tail else ''}"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# Looked up at call time, so a test can replace it.
|
|
134
|
+
DEFAULT_RUNNER: ProbeRunner = run_probe
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _file_fingerprint(path: str) -> dict:
|
|
138
|
+
"""size + mtime of the model file and, for an IR, its `.bin` weights."""
|
|
139
|
+
out: dict = {}
|
|
140
|
+
for candidate in (path, os.path.splitext(path)[0] + ".bin"):
|
|
141
|
+
try:
|
|
142
|
+
st = os.stat(candidate)
|
|
143
|
+
except OSError:
|
|
144
|
+
continue
|
|
145
|
+
out[os.path.basename(candidate)] = [st.st_size, st.st_mtime_ns]
|
|
146
|
+
return out
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def probe_key(path: str, config: dict, candidates: "list[str]", env_tag: str) -> dict:
|
|
150
|
+
return {
|
|
151
|
+
"version": PROBE_VERDICT_VERSION,
|
|
152
|
+
"model": _file_fingerprint(path),
|
|
153
|
+
"candidates": [str(c).upper() for c in candidates],
|
|
154
|
+
"compile": {k: config.get(k) for k in COMPILE_CONFIG_KEYS},
|
|
155
|
+
# An OpenVINO / GPU-driver update may fix or break a model.
|
|
156
|
+
"env": env_tag,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def probe_verdict_path(path: str, config: dict, cache_dir: str) -> str:
|
|
161
|
+
stem = os.path.splitext(os.path.basename(path))[0]
|
|
162
|
+
device = str(config.get("device", "AUTO")).upper() or "AUTO"
|
|
163
|
+
return os.path.join(cache_dir, f"{stem}.{device}.ovprobe.json")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _blob_names(cache_dir: str) -> "set[str]":
|
|
167
|
+
try:
|
|
168
|
+
return {n for n in os.listdir(cache_dir) if n.endswith(".blob")}
|
|
169
|
+
except OSError:
|
|
170
|
+
return set()
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _read_verdict(path: str, key: dict) -> Optional[dict]:
|
|
174
|
+
try:
|
|
175
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
176
|
+
stored = json.load(fh)
|
|
177
|
+
except (OSError, ValueError):
|
|
178
|
+
return None
|
|
179
|
+
if not isinstance(stored, dict) or stored.get("key") != key:
|
|
180
|
+
return None
|
|
181
|
+
if stored.get("verdict") not in (VERDICT_OK, VERDICT_CRASHED):
|
|
182
|
+
return None
|
|
183
|
+
return stored
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _write_verdict(path: str, key: dict, verdict: str, detail: str, blobs: "list[str]") -> None:
|
|
187
|
+
"""Atomic replace; a verdict that cannot be written only costs a re-probe."""
|
|
188
|
+
tmp = f"{path}.tmp-{os.getpid()}"
|
|
189
|
+
try:
|
|
190
|
+
with open(tmp, "w", encoding="utf-8") as fh:
|
|
191
|
+
json.dump({"key": key, "verdict": verdict, "detail": detail, "blobs": blobs}, fh)
|
|
192
|
+
os.replace(tmp, path)
|
|
193
|
+
except OSError as exc:
|
|
194
|
+
sys.stderr.write(f"OpenVINO probe: could not persist the verdict for {path} ({exc})\n")
|
|
195
|
+
sys.stderr.flush()
|
|
196
|
+
try:
|
|
197
|
+
os.remove(tmp)
|
|
198
|
+
except OSError:
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _warm(known: dict, cache_dir: str) -> bool:
|
|
203
|
+
"""An `ok` verdict holds only while the blobs its probe wrote are still on disk."""
|
|
204
|
+
blobs = known.get("blobs")
|
|
205
|
+
if not isinstance(blobs, list):
|
|
206
|
+
return False
|
|
207
|
+
return all(os.path.isfile(os.path.join(cache_dir, str(b))) for b in blobs)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def ensure_openvino_loadable(
|
|
211
|
+
model_path: str,
|
|
212
|
+
config: dict,
|
|
213
|
+
candidates: "list[str]",
|
|
214
|
+
cache_dir: Optional[str],
|
|
215
|
+
env_tag: str = "",
|
|
216
|
+
run: Optional[ProbeRunner] = None,
|
|
217
|
+
) -> None:
|
|
218
|
+
"""Return when `model_path` may be compiled in-process; raise when it would crash."""
|
|
219
|
+
if not needs_probe(candidates):
|
|
220
|
+
return
|
|
221
|
+
name = os.path.basename(model_path)
|
|
222
|
+
device = str(config.get("device", "AUTO"))
|
|
223
|
+
key = probe_key(model_path, config, candidates, env_tag)
|
|
224
|
+
verdict_path = probe_verdict_path(model_path, config, cache_dir) if cache_dir else None
|
|
225
|
+
if verdict_path is not None:
|
|
226
|
+
known = _read_verdict(verdict_path, key)
|
|
227
|
+
if known is not None:
|
|
228
|
+
if known["verdict"] == VERDICT_CRASHED:
|
|
229
|
+
sys.stderr.write(
|
|
230
|
+
f"OpenVINO probe: {name} device={device} is a KNOWN compiler crash — refused\n"
|
|
231
|
+
)
|
|
232
|
+
sys.stderr.flush()
|
|
233
|
+
raise OpenvinoLoadCrashed(model_path, device, str(known.get("detail") or "known crash"))
|
|
234
|
+
if cache_dir is not None and _warm(known, cache_dir):
|
|
235
|
+
return # blob present: the in-process load is a cache hit
|
|
236
|
+
before = _blob_names(cache_dir) if cache_dir else set()
|
|
237
|
+
runner = run if run is not None else DEFAULT_RUNNER
|
|
238
|
+
verdict, detail = runner(model_path, config)
|
|
239
|
+
sys.stderr.write(f"OpenVINO probe: {name} device={device} -> {verdict} ({detail})\n")
|
|
240
|
+
sys.stderr.flush()
|
|
241
|
+
if verdict in (VERDICT_OK, VERDICT_CRASHED) and verdict_path is not None and cache_dir is not None:
|
|
242
|
+
written = sorted(_blob_names(cache_dir) - before) if verdict == VERDICT_OK else []
|
|
243
|
+
_write_verdict(verdict_path, key, verdict, detail, written)
|
|
244
|
+
if verdict == VERDICT_CRASHED:
|
|
245
|
+
raise OpenvinoLoadCrashed(model_path, device, detail)
|