@camstack/addon-pipeline 1.2.621 → 1.2.623
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-Il8UY4D4.js → custom-model-catalog-BTBdgN2p.js} +29 -16
- package/dist/{custom-model-catalog-0_SnSXzN.mjs → custom-model-catalog-D5zTw1LU.mjs} +29 -10
- package/dist/detection-pipeline/index.js +67 -416
- package/dist/detection-pipeline/index.mjs +91 -440
- package/dist/{dist-ClkumZgv.js → dist-BVo3wa7s.js} +711 -302
- package/dist/{dist-DHqkQhTQ.mjs → dist-opSx6v5i.mjs} +651 -296
- package/dist/{model-catalogs-BagalaYp.js → model-catalogs-CQytR2AN.js} +1 -1
- package/dist/{model-catalogs-Z1GZAhSb.mjs → model-catalogs-rIvLUVlw.mjs} +1 -1
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-CkJLxlWI.mjs → node-CGIYuJMN.mjs} +1 -1
- package/dist/{node-0O_61WVW.js → node-DqLsuN_A.js} +1 -1
- package/dist/pipeline-runner/index.js +115 -460
- package/dist/pipeline-runner/index.mjs +108 -453
- package/dist/{process-memory-Dj3UgL1e.mjs → process-memory-Bd-TuWfM.mjs} +1 -1
- package/dist/{process-memory-DdIOhjYB.js → process-memory-DyxG-0mG.js} +1 -1
- package/dist/recorder/index.js +2 -2
- package/dist/recorder/index.mjs +2 -2
- package/dist/{segment-demux-js-BePzacki.js → segment-demux-js-B4bfTQpK.js} +1 -1
- package/dist/{segment-demux-js-Bv0t0-tQ.mjs → segment-demux-js-CdVRzwSd.mjs} +1 -1
- package/dist/session-decode/decode-worker-main.js +12 -21
- package/dist/session-decode/decode-worker-main.mjs +12 -21
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-dNk74ahz.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DF3FGCR8.mjs} +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-oUZNn2Kr.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-Bb-ETNGC.mjs +26 -0
- 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-CH5g78CJ.mjs → hostInit-BiyFzSqq.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/dist/{worker-protocol-D_NzPcnh.mjs → worker-protocol-DngGEAT2.js} +102 -2
- package/dist/{worker-protocol-C-G8qmye.js → worker-protocol-_avq7y8O.mjs} +67 -25
- package/package.json +1 -1
- package/python/accelerator_facts.py +275 -0
- package/python/coreml_load_probe.py +105 -15
- package/python/inference_pool.py +19 -2
- package/python/openvino_compile_probe.py +72 -18
- package/python/postprocessors/ssd.py +12 -4
- package/python/postprocessors/test_ssd.py +64 -0
- package/python/test_accelerator_facts.py +105 -0
- package/python/test_coreml_load_probe.py +83 -0
- package/python/test_openvino_compile_probe.py +114 -0
- package/python/webgpu_isolated.py +19 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-2KkGoQ55.mjs +0 -26
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Dx-WrMx0.mjs +0 -26
|
@@ -12,8 +12,11 @@ happens in a throwaway interpreter. The verdict is remembered next to the
|
|
|
12
12
|
compiled-model cache:
|
|
13
13
|
|
|
14
14
|
ok -> load in-process, never probe this package again
|
|
15
|
-
crashed -> refuse with `CoremlLoadCrashed` (reason `native-load-crashed`)
|
|
16
|
-
now
|
|
15
|
+
crashed -> refuse with `CoremlLoadCrashed` (reason `native-load-crashed`)
|
|
16
|
+
now. ONE crash is only `suspect` (D926): refused without
|
|
17
|
+
probing for a cool-off, then probed again. A SECOND crash
|
|
18
|
+
at least a cool-off after the first caches `crashed`, refused
|
|
19
|
+
on every later load without re-probing; an `ok` resets.
|
|
17
20
|
inconclusive -> (a Python exception in the child, a probe past its bound, a
|
|
18
21
|
child that could not start) remembered NOWHERE; the
|
|
19
22
|
in-process load runs exactly as it did before this gate
|
|
@@ -33,6 +36,7 @@ import platform
|
|
|
33
36
|
import signal
|
|
34
37
|
import subprocess
|
|
35
38
|
import sys
|
|
39
|
+
import time
|
|
36
40
|
from typing import Callable, Optional
|
|
37
41
|
|
|
38
42
|
# One-way: inference_pool imports this module lazily, inside its CoreML branch.
|
|
@@ -43,6 +47,63 @@ VERDICT_CRASHED = "crashed"
|
|
|
43
47
|
VERDICT_INCONCLUSIVE = "inconclusive"
|
|
44
48
|
|
|
45
49
|
PROBE_VERDICT_VERSION = 1
|
|
50
|
+
|
|
51
|
+
# The two-crash rule, shared with `openvino_compile_probe` (D926, D49): one
|
|
52
|
+
# native crash is `suspect`; this many crashes, the last at least a cool-off
|
|
53
|
+
# after the first, is `crashed` and cached for good. A v1 `crashed` record
|
|
54
|
+
# (no `crashes` list) reads as ONE crash of unknown age, so it is re-probed.
|
|
55
|
+
VERDICT_SUSPECT = "suspect"
|
|
56
|
+
CRASHES_TO_CONFIRM = 2
|
|
57
|
+
CRASH_COOL_OFF_SEC = 600.0
|
|
58
|
+
|
|
59
|
+
Clock = Callable[[], float]
|
|
60
|
+
LoadAvg = Callable[[], "tuple[float, float, float]"]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def host_loadavg() -> "tuple[float, float, float]":
|
|
64
|
+
try:
|
|
65
|
+
return os.getloadavg()
|
|
66
|
+
except (OSError, AttributeError): # Windows has no load average
|
|
67
|
+
return (-1.0, -1.0, -1.0)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def crash_at(crash: object) -> float:
|
|
71
|
+
if isinstance(crash, dict):
|
|
72
|
+
at = crash.get("at")
|
|
73
|
+
if isinstance(at, (int, float)):
|
|
74
|
+
return float(at)
|
|
75
|
+
return 0.0
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def crashes_confirmed(crashes: list) -> bool:
|
|
79
|
+
"""Two crashes (or more), the last at least a cool-off after the first."""
|
|
80
|
+
if len(crashes) < CRASHES_TO_CONFIRM:
|
|
81
|
+
return False
|
|
82
|
+
return crash_at(crashes[-1]) - crash_at(crashes[0]) >= CRASH_COOL_OFF_SEC
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def normalise_crash_record(stored: dict) -> Optional[dict]:
|
|
86
|
+
"""A stored verdict with its crash list filled in; an unconfirmed `crashed` reads `suspect`."""
|
|
87
|
+
verdict = stored.get("verdict")
|
|
88
|
+
if verdict not in (VERDICT_OK, VERDICT_CRASHED, VERDICT_SUSPECT):
|
|
89
|
+
return None
|
|
90
|
+
if verdict == VERDICT_OK:
|
|
91
|
+
return stored
|
|
92
|
+
crashes = stored.get("crashes")
|
|
93
|
+
if not isinstance(crashes, list) or not crashes:
|
|
94
|
+
crashes = [{"at": 0.0, "loadavg": None, "detail": str(stored.get("detail") or "")}]
|
|
95
|
+
out = {**stored, "crashes": crashes}
|
|
96
|
+
if verdict == VERDICT_CRASHED and not crashes_confirmed(crashes):
|
|
97
|
+
out = {**out, "verdict": VERDICT_SUSPECT}
|
|
98
|
+
return out
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def record_crash(
|
|
102
|
+
prior: list, detail: str, now: float, load: "tuple[float, float, float]",
|
|
103
|
+
) -> "tuple[str, list]":
|
|
104
|
+
"""Append one crash; return the verdict to store (`suspect` or `crashed`) and the list."""
|
|
105
|
+
crashes = [*prior, {"at": now, "loadavg": [round(x, 2) for x in load], "detail": detail}]
|
|
106
|
+
return (VERDICT_CRASHED if crashes_confirmed(crashes) else VERDICT_SUSPECT), crashes
|
|
46
107
|
PROBE_MARKER = "@@CAMSTACK_COREML_PROBE_OK@@"
|
|
47
108
|
# Long enough for the slowest cold compile measured on a Mac (rfdetr-package,
|
|
48
109
|
# 58 s); the pool's own load bound reports a slow load to the host meanwhile.
|
|
@@ -169,18 +230,21 @@ def _read_verdict(path: str, key: dict) -> Optional[dict]:
|
|
|
169
230
|
return None
|
|
170
231
|
if not isinstance(stored, dict) or stored.get("key") != key:
|
|
171
232
|
return None
|
|
172
|
-
|
|
173
|
-
return None
|
|
174
|
-
return stored
|
|
233
|
+
return normalise_crash_record(stored)
|
|
175
234
|
|
|
176
235
|
|
|
177
|
-
def _write_verdict(
|
|
236
|
+
def _write_verdict(
|
|
237
|
+
path: str, key: dict, verdict: str, detail: str, crashes: "Optional[list]" = None,
|
|
238
|
+
) -> None:
|
|
178
239
|
"""Atomic replace; a verdict that cannot be written only costs a re-probe."""
|
|
179
240
|
tmp = f"{path}.tmp-{os.getpid()}"
|
|
241
|
+
record: dict = {"key": key, "verdict": verdict, "detail": detail}
|
|
242
|
+
if crashes:
|
|
243
|
+
record["crashes"] = crashes
|
|
180
244
|
try:
|
|
181
245
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
182
246
|
with open(tmp, "w", encoding="utf-8") as fh:
|
|
183
|
-
json.dump(
|
|
247
|
+
json.dump(record, fh)
|
|
184
248
|
os.replace(tmp, path)
|
|
185
249
|
except OSError as exc:
|
|
186
250
|
sys.stderr.write(f"CoreML probe: could not persist the verdict for {path} ({exc})\n")
|
|
@@ -196,8 +260,11 @@ def ensure_coreml_loadable(
|
|
|
196
260
|
device: str,
|
|
197
261
|
run: Optional[ProbeRunner] = None,
|
|
198
262
|
os_build: Optional[str] = None,
|
|
263
|
+
now: Optional[Clock] = None,
|
|
264
|
+
loadavg: Optional[LoadAvg] = None,
|
|
199
265
|
) -> None:
|
|
200
266
|
"""Return when `model_path` may be loaded in-process; raise when it would crash."""
|
|
267
|
+
clock = now if now is not None else time.time
|
|
201
268
|
key = {
|
|
202
269
|
"version": PROBE_VERDICT_VERSION,
|
|
203
270
|
"fingerprint": _coreml_model_fingerprint(model_path), # a re-download is a new package
|
|
@@ -207,17 +274,40 @@ def ensure_coreml_loadable(
|
|
|
207
274
|
verdict_path = probe_verdict_path(model_path, device)
|
|
208
275
|
known = _read_verdict(verdict_path, key)
|
|
209
276
|
name = os.path.basename(model_path)
|
|
277
|
+
prior_crashes: list = []
|
|
210
278
|
if known is not None:
|
|
211
279
|
if known["verdict"] == VERDICT_CRASHED:
|
|
212
|
-
|
|
213
|
-
|
|
280
|
+
_log(f"{name} device={device} is a KNOWN loader crash "
|
|
281
|
+
f"({len(known['crashes'])} crashes) — refused")
|
|
214
282
|
raise CoremlLoadCrashed(model_path, device, str(known.get("detail") or "known crash"))
|
|
215
|
-
|
|
283
|
+
if known["verdict"] == VERDICT_OK:
|
|
284
|
+
return
|
|
285
|
+
prior_crashes = list(known["crashes"])
|
|
286
|
+
retry_at = crash_at(prior_crashes[-1]) + CRASH_COOL_OFF_SEC
|
|
287
|
+
if clock() < retry_at:
|
|
288
|
+
_log(f"{name} device={device} crashed the loader once — refused until the "
|
|
289
|
+
f"cool-off ends ({retry_at - clock():.0f}s left)")
|
|
290
|
+
raise CoremlLoadCrashed(
|
|
291
|
+
model_path, device,
|
|
292
|
+
f"{known.get('detail') or 'one crash'}; one crash so far, "
|
|
293
|
+
f"re-probed after {CRASH_COOL_OFF_SEC:g}s",
|
|
294
|
+
)
|
|
216
295
|
runner = run if run is not None else DEFAULT_RUNNER
|
|
217
296
|
verdict, detail = runner(model_path, device)
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if verdict in (VERDICT_OK, VERDICT_CRASHED):
|
|
297
|
+
_log(f"{name} device={device} -> {verdict} ({detail})")
|
|
298
|
+
if verdict == VERDICT_OK:
|
|
221
299
|
_write_verdict(verdict_path, key, verdict, detail)
|
|
222
|
-
|
|
223
|
-
|
|
300
|
+
return
|
|
301
|
+
if verdict != VERDICT_CRASHED:
|
|
302
|
+
return # inconclusive: remembered nowhere; an earlier suspect stands
|
|
303
|
+
load = (loadavg if loadavg is not None else host_loadavg)()
|
|
304
|
+
stored, crashes = record_crash(prior_crashes, detail, clock(), load)
|
|
305
|
+
_log(f"{name} device={device} loader crash {len(crashes)}/{CRASHES_TO_CONFIRM} "
|
|
306
|
+
f"(load {load[0]:.1f}) -> {stored}")
|
|
307
|
+
_write_verdict(verdict_path, key, stored, detail, crashes)
|
|
308
|
+
raise CoremlLoadCrashed(model_path, device, detail)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _log(line: str) -> None:
|
|
312
|
+
sys.stderr.write(f"CoreML probe: {line}\n")
|
|
313
|
+
sys.stderr.flush()
|
package/python/inference_pool.py
CHANGED
|
@@ -3150,7 +3150,21 @@ async def _run() -> None:
|
|
|
3150
3150
|
)
|
|
3151
3151
|
sys.stderr.flush()
|
|
3152
3152
|
|
|
3153
|
-
|
|
3153
|
+
# Compute the estimate scales from (D930). A failed probe is omitted —
|
|
3154
|
+
# never a row of zeros — and must not delay `ready`.
|
|
3155
|
+
accelerator_facts = None
|
|
3156
|
+
try:
|
|
3157
|
+
from accelerator_facts import facts_for_runtime, publish_accelerator_facts
|
|
3158
|
+
|
|
3159
|
+
accelerator_facts = facts_for_runtime(runtime, _runtime_lib, pool_device)
|
|
3160
|
+
if accelerator_facts:
|
|
3161
|
+
publish_accelerator_facts(accelerator_facts)
|
|
3162
|
+
except Exception as exc:
|
|
3163
|
+
sys.stderr.write(f"accelerator facts: probe failed ({exc})\n")
|
|
3164
|
+
sys.stderr.flush()
|
|
3165
|
+
accelerator_facts = None
|
|
3166
|
+
|
|
3167
|
+
ready_payload = {
|
|
3154
3168
|
"status": "ready",
|
|
3155
3169
|
"models": loaded_count,
|
|
3156
3170
|
"total": len(models),
|
|
@@ -3161,7 +3175,10 @@ async def _run() -> None:
|
|
|
3161
3175
|
# the same min() against what it declared, so both ends derive the
|
|
3162
3176
|
# identical effective version (D350).
|
|
3163
3177
|
"protocolVersion": PROTOCOL_VERSION,
|
|
3164
|
-
}
|
|
3178
|
+
}
|
|
3179
|
+
if accelerator_facts is not None:
|
|
3180
|
+
ready_payload["acceleratorFacts"] = accelerator_facts
|
|
3181
|
+
await writer.send(req_id, ready_payload)
|
|
3165
3182
|
|
|
3166
3183
|
# ── Window accumulator (per-model) ──────────────────────────────
|
|
3167
3184
|
#
|
|
@@ -19,13 +19,24 @@ therefore writes the blob the pool then loads warm. Mirrors CoreML's
|
|
|
19
19
|
ok -> load in-process (a cache HIT), never probe this key again
|
|
20
20
|
while the blobs the probe wrote are still on disk
|
|
21
21
|
crashed -> refuse with `OpenvinoLoadCrashed` (reason
|
|
22
|
-
`native-load-crashed`)
|
|
23
|
-
|
|
22
|
+
`native-load-crashed`) now. ONE crash is only `suspect`:
|
|
23
|
+
later loads are refused without probing until a cool-off
|
|
24
|
+
(CRASH_COOL_OFF_SEC) has passed, then probed again. Only a
|
|
25
|
+
SECOND crash at least a cool-off after the first caches
|
|
26
|
+
`crashed` — refused on every later load of the same key,
|
|
27
|
+
without re-probing. An `ok` in between resets the count.
|
|
24
28
|
inconclusive -> (a Python exception in the child, a probe past its bound, a
|
|
25
29
|
child that could not start) remembered NOWHERE; the
|
|
26
30
|
in-process load runs exactly as it did before this gate
|
|
27
31
|
|
|
28
32
|
Only a death by SIGNAL is a crash. CPU-only loads are never probed.
|
|
33
|
+
|
|
34
|
+
Why two (2026-10-23, D926): a single SIGSEGV during an overloaded moment wrote
|
|
35
|
+
`crashed`, and every later request for that model on the GPU was refused for
|
|
36
|
+
good. A destructive verdict needs a second agreeing read (D49). Every crash is
|
|
37
|
+
recorded with its time, the host load average and the child's detail. To
|
|
38
|
+
clear a verdict by hand, delete
|
|
39
|
+
`<models>/.ov-cache/<model>.<DEVICE>.ovprobe.json`.
|
|
29
40
|
"""
|
|
30
41
|
from __future__ import annotations
|
|
31
42
|
|
|
@@ -33,17 +44,30 @@ import json
|
|
|
33
44
|
import os
|
|
34
45
|
import subprocess
|
|
35
46
|
import sys
|
|
47
|
+
import time
|
|
36
48
|
from typing import Callable, Optional
|
|
37
49
|
|
|
38
50
|
# One-way: inference_pool imports this module lazily, inside its OpenVINO branch.
|
|
51
|
+
# The two-crash rule (D926) lives there too, shared by both probes.
|
|
39
52
|
from coreml_load_probe import (
|
|
53
|
+
CRASH_COOL_OFF_SEC,
|
|
54
|
+
CRASHES_TO_CONFIRM,
|
|
40
55
|
VERDICT_CRASHED,
|
|
41
56
|
VERDICT_INCONCLUSIVE,
|
|
42
57
|
VERDICT_OK,
|
|
58
|
+
VERDICT_SUSPECT,
|
|
59
|
+
Clock,
|
|
60
|
+
LoadAvg,
|
|
43
61
|
_signal_name,
|
|
44
62
|
classify_probe_exit,
|
|
63
|
+
crash_at,
|
|
64
|
+
host_loadavg,
|
|
65
|
+
normalise_crash_record,
|
|
66
|
+
record_crash,
|
|
45
67
|
)
|
|
46
68
|
|
|
69
|
+
# The key's version stays 1: a v1 `crashed` file (no `crashes` list) is read
|
|
70
|
+
# as ONE crash of unknown age, so it is re-probed rather than discarded.
|
|
47
71
|
PROBE_VERDICT_VERSION = 1
|
|
48
72
|
PROBE_MARKER = "@@CAMSTACK_OV_PROBE_OK@@"
|
|
49
73
|
# A cold iGPU compile of a large detector takes tens of seconds; past this the
|
|
@@ -178,17 +202,21 @@ def _read_verdict(path: str, key: dict) -> Optional[dict]:
|
|
|
178
202
|
return None
|
|
179
203
|
if not isinstance(stored, dict) or stored.get("key") != key:
|
|
180
204
|
return None
|
|
181
|
-
|
|
182
|
-
return None
|
|
183
|
-
return stored
|
|
205
|
+
return normalise_crash_record(stored)
|
|
184
206
|
|
|
185
207
|
|
|
186
|
-
def _write_verdict(
|
|
208
|
+
def _write_verdict(
|
|
209
|
+
path: str, key: dict, verdict: str, detail: str, blobs: "list[str]",
|
|
210
|
+
crashes: "Optional[list]" = None,
|
|
211
|
+
) -> None:
|
|
187
212
|
"""Atomic replace; a verdict that cannot be written only costs a re-probe."""
|
|
188
213
|
tmp = f"{path}.tmp-{os.getpid()}"
|
|
214
|
+
record: dict = {"key": key, "verdict": verdict, "detail": detail, "blobs": blobs}
|
|
215
|
+
if crashes:
|
|
216
|
+
record["crashes"] = crashes
|
|
189
217
|
try:
|
|
190
218
|
with open(tmp, "w", encoding="utf-8") as fh:
|
|
191
|
-
json.dump(
|
|
219
|
+
json.dump(record, fh)
|
|
192
220
|
os.replace(tmp, path)
|
|
193
221
|
except OSError as exc:
|
|
194
222
|
sys.stderr.write(f"OpenVINO probe: could not persist the verdict for {path} ({exc})\n")
|
|
@@ -214,32 +242,58 @@ def ensure_openvino_loadable(
|
|
|
214
242
|
cache_dir: Optional[str],
|
|
215
243
|
env_tag: str = "",
|
|
216
244
|
run: Optional[ProbeRunner] = None,
|
|
245
|
+
now: Optional[Clock] = None,
|
|
246
|
+
loadavg: Optional[LoadAvg] = None,
|
|
217
247
|
) -> None:
|
|
218
248
|
"""Return when `model_path` may be compiled in-process; raise when it would crash."""
|
|
219
249
|
if not needs_probe(candidates):
|
|
220
250
|
return
|
|
251
|
+
clock = now if now is not None else time.time
|
|
221
252
|
name = os.path.basename(model_path)
|
|
222
253
|
device = str(config.get("device", "AUTO"))
|
|
223
254
|
key = probe_key(model_path, config, candidates, env_tag)
|
|
224
255
|
verdict_path = probe_verdict_path(model_path, config, cache_dir) if cache_dir else None
|
|
256
|
+
prior_crashes: list = []
|
|
225
257
|
if verdict_path is not None:
|
|
226
258
|
known = _read_verdict(verdict_path, key)
|
|
227
259
|
if known is not None:
|
|
228
260
|
if known["verdict"] == VERDICT_CRASHED:
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
)
|
|
232
|
-
sys.stderr.flush()
|
|
261
|
+
_log(f"{name} device={device} is a KNOWN compiler crash "
|
|
262
|
+
f"({len(known['crashes'])} crashes) — refused")
|
|
233
263
|
raise OpenvinoLoadCrashed(model_path, device, str(known.get("detail") or "known crash"))
|
|
234
|
-
if
|
|
264
|
+
if known["verdict"] == VERDICT_SUSPECT:
|
|
265
|
+
prior_crashes = list(known["crashes"])
|
|
266
|
+
retry_at = crash_at(prior_crashes[-1]) + CRASH_COOL_OFF_SEC
|
|
267
|
+
if clock() < retry_at:
|
|
268
|
+
_log(f"{name} device={device} crashed the compiler once — refused until "
|
|
269
|
+
f"the cool-off ends ({retry_at - clock():.0f}s left)")
|
|
270
|
+
raise OpenvinoLoadCrashed(
|
|
271
|
+
model_path, device,
|
|
272
|
+
f"{known.get('detail') or 'one crash'}; one crash so far, "
|
|
273
|
+
f"re-probed after {CRASH_COOL_OFF_SEC:g}s",
|
|
274
|
+
)
|
|
275
|
+
elif cache_dir is not None and _warm(known, cache_dir):
|
|
235
276
|
return # blob present: the in-process load is a cache hit
|
|
236
277
|
before = _blob_names(cache_dir) if cache_dir else set()
|
|
237
278
|
runner = run if run is not None else DEFAULT_RUNNER
|
|
238
279
|
verdict, detail = runner(model_path, config)
|
|
239
|
-
|
|
280
|
+
_log(f"{name} device={device} -> {verdict} ({detail})")
|
|
281
|
+
if verdict == VERDICT_OK:
|
|
282
|
+
if verdict_path is not None and cache_dir is not None:
|
|
283
|
+
written = sorted(_blob_names(cache_dir) - before)
|
|
284
|
+
_write_verdict(verdict_path, key, verdict, detail, written)
|
|
285
|
+
return
|
|
286
|
+
if verdict != VERDICT_CRASHED:
|
|
287
|
+
return # inconclusive: remembered nowhere; an earlier suspect stands
|
|
288
|
+
load = (loadavg if loadavg is not None else host_loadavg)()
|
|
289
|
+
stored, crashes = record_crash(prior_crashes, detail, clock(), load)
|
|
290
|
+
_log(f"{name} device={device} compiler crash {len(crashes)}/{CRASHES_TO_CONFIRM} "
|
|
291
|
+
f"(load {load[0]:.1f}) -> {stored}")
|
|
292
|
+
if verdict_path is not None and cache_dir is not None:
|
|
293
|
+
_write_verdict(verdict_path, key, stored, detail, [], crashes)
|
|
294
|
+
raise OpenvinoLoadCrashed(model_path, device, detail)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _log(line: str) -> None:
|
|
298
|
+
sys.stderr.write(f"OpenVINO probe: {line}\n")
|
|
240
299
|
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)
|
|
@@ -6,7 +6,9 @@ SSD MobileNet V2 and EfficientDet-Lite detectors. Those models embed the
|
|
|
6
6
|
decoded (boxes + per-box class + score, post-NMS):
|
|
7
7
|
|
|
8
8
|
output 0 boxes [1, N, 4] (ymin, xmin, ymax, xmax) normalised 0..1
|
|
9
|
-
output 1 classes [1, N] float class indices (COCO 90-class map
|
|
9
|
+
output 1 classes [1, N] float class indices (COCO 90-class map,
|
|
10
|
+
or the model's own label list when the
|
|
11
|
+
descriptor carries one)
|
|
10
12
|
output 2 scores [1, N] float 0..1
|
|
11
13
|
output 3 count [1] number of valid detections
|
|
12
14
|
|
|
@@ -118,8 +120,10 @@ def _split_outputs(predictions: dict) -> tuple:
|
|
|
118
120
|
return boxes, classes, scores, count
|
|
119
121
|
|
|
120
122
|
|
|
121
|
-
def label_for_class(cls: int) -> str:
|
|
122
|
-
"""
|
|
123
|
+
def label_for_class(cls: int, labels: list | None = None) -> str:
|
|
124
|
+
"""Class name for a raw index; default map is COCO-90 when labels is absent/empty."""
|
|
125
|
+
if labels:
|
|
126
|
+
return labels[cls] if 0 <= cls < len(labels) else str(cls)
|
|
123
127
|
return COCO_90[cls] if 0 <= cls < len(COCO_90) else str(cls)
|
|
124
128
|
|
|
125
129
|
|
|
@@ -134,6 +138,10 @@ def postprocess_ssd(
|
|
|
134
138
|
"""Decode SSD/EfficientDet postprocess outputs to original-frame detections."""
|
|
135
139
|
conf_threshold = float(config.get("confidence", 0.4) or 0.4)
|
|
136
140
|
input_size = int(config.get("inputSize", 300) or 300)
|
|
141
|
+
raw_labels = config.get("labels")
|
|
142
|
+
label_map: list | None = (
|
|
143
|
+
list(raw_labels) if isinstance(raw_labels, list) and len(raw_labels) > 0 else None
|
|
144
|
+
)
|
|
137
145
|
|
|
138
146
|
boxes, classes, scores, count = _split_outputs(predictions)
|
|
139
147
|
if boxes is None or scores is None:
|
|
@@ -172,7 +180,7 @@ def postprocess_ssd(
|
|
|
172
180
|
y2 = max(0.0, min(y2, orig_h))
|
|
173
181
|
if x2 <= x1 or y2 <= y1:
|
|
174
182
|
continue
|
|
175
|
-
label = label_for_class(int(classes[i]))
|
|
183
|
+
label = label_for_class(int(classes[i]), label_map)
|
|
176
184
|
if label == "n/a":
|
|
177
185
|
continue
|
|
178
186
|
detections.append({
|
|
@@ -189,6 +189,70 @@ class ScoreCeilingTest(unittest.TestCase):
|
|
|
189
189
|
self.assertEqual(len([d for d in _decode(STREET, confidence=0.65) if d["class"] == "person"]), 3)
|
|
190
190
|
|
|
191
191
|
|
|
192
|
+
class ConfigLabelsTest(unittest.TestCase):
|
|
193
|
+
"""Custom label maps from the pool; absent labels keep COCO-90 in Python."""
|
|
194
|
+
|
|
195
|
+
_SYNTH = {
|
|
196
|
+
"0": [[[0.1, 0.1, 0.5, 0.5]]],
|
|
197
|
+
"1": [[3.0]],
|
|
198
|
+
"2": [[0.9]],
|
|
199
|
+
"3": [1.0],
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
def test_custom_labels_name_classes(self) -> None:
|
|
203
|
+
# Index 3 is dog and index 29 is car — the Plus map the COCO-90 table misnames.
|
|
204
|
+
labels = ["person", "bicycle", "x", "dog"] + ["pad"] * 25 + ["car"]
|
|
205
|
+
self.assertEqual(labels[3], "dog")
|
|
206
|
+
self.assertEqual(labels[29], "car")
|
|
207
|
+
dog = postprocess_ssd(
|
|
208
|
+
self._SYNTH,
|
|
209
|
+
{"confidence": 0.4, "inputSize": 100, "labels": labels},
|
|
210
|
+
100,
|
|
211
|
+
100,
|
|
212
|
+
1.0,
|
|
213
|
+
(0, 0),
|
|
214
|
+
)["detections"]
|
|
215
|
+
self.assertEqual(len(dog), 1)
|
|
216
|
+
self.assertEqual(dog[0]["class"], "dog")
|
|
217
|
+
car_out = dict(self._SYNTH)
|
|
218
|
+
car_out["1"] = [[29.0]]
|
|
219
|
+
car = postprocess_ssd(
|
|
220
|
+
car_out,
|
|
221
|
+
{"confidence": 0.4, "inputSize": 100, "labels": labels},
|
|
222
|
+
100,
|
|
223
|
+
100,
|
|
224
|
+
1.0,
|
|
225
|
+
(0, 0),
|
|
226
|
+
)["detections"]
|
|
227
|
+
self.assertEqual(len(car), 1)
|
|
228
|
+
self.assertEqual(car[0]["class"], "car")
|
|
229
|
+
|
|
230
|
+
def test_no_labels_key_uses_coco90_and_skips_na(self) -> None:
|
|
231
|
+
out = dict(self._SYNTH)
|
|
232
|
+
out["1"] = [[29.0]]
|
|
233
|
+
dets = postprocess_ssd(
|
|
234
|
+
out,
|
|
235
|
+
{"confidence": 0.4, "inputSize": 100},
|
|
236
|
+
100,
|
|
237
|
+
100,
|
|
238
|
+
1.0,
|
|
239
|
+
(0, 0),
|
|
240
|
+
)["detections"]
|
|
241
|
+
self.assertEqual(dets, [])
|
|
242
|
+
out2 = dict(self._SYNTH)
|
|
243
|
+
out2["1"] = [[3.0]]
|
|
244
|
+
dets2 = postprocess_ssd(
|
|
245
|
+
out2,
|
|
246
|
+
{"confidence": 0.4, "inputSize": 100},
|
|
247
|
+
100,
|
|
248
|
+
100,
|
|
249
|
+
1.0,
|
|
250
|
+
(0, 0),
|
|
251
|
+
)["detections"]
|
|
252
|
+
self.assertEqual(len(dets2), 1)
|
|
253
|
+
self.assertEqual(dets2[0]["class"], "motorcycle")
|
|
254
|
+
|
|
255
|
+
|
|
192
256
|
class NumpyInputTest(unittest.TestCase):
|
|
193
257
|
"""The pool hands ndarrays; they must decode exactly like the lists above."""
|
|
194
258
|
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Unit tests for accelerator compute facts (D930). No OpenVINO import."""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
import unittest
|
|
6
|
+
|
|
7
|
+
import accelerator_facts as facts
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _Core:
|
|
11
|
+
def __init__(self, devices, props):
|
|
12
|
+
self.available_devices = devices
|
|
13
|
+
self._props = props
|
|
14
|
+
self.failed = set()
|
|
15
|
+
|
|
16
|
+
def get_property(self, device, name):
|
|
17
|
+
if (device, name) in self.failed:
|
|
18
|
+
raise RuntimeError(f"probe failed {device} {name}")
|
|
19
|
+
device_props = self._props.get(device, {})
|
|
20
|
+
if name not in device_props:
|
|
21
|
+
raise KeyError(name)
|
|
22
|
+
return device_props[name]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class OpenVinoFactsTest(unittest.TestCase):
|
|
26
|
+
def test_reports_gpu_and_npu_and_null_for_a_failed_query(self):
|
|
27
|
+
core = _Core(
|
|
28
|
+
["CPU", "GPU", "NPU"],
|
|
29
|
+
{
|
|
30
|
+
"GPU": {
|
|
31
|
+
"FULL_DEVICE_NAME": "Intel(R) Graphics",
|
|
32
|
+
"GPU_EXECUTION_UNITS_COUNT": 32,
|
|
33
|
+
"GPU_DEVICE_MAX_FREQUENCY": 1250,
|
|
34
|
+
"DEVICE_GOPS": {"i8": 400, "f16": 200},
|
|
35
|
+
"DEVICE_ARCHITECTURE": "Xe",
|
|
36
|
+
},
|
|
37
|
+
"NPU": {
|
|
38
|
+
"FULL_DEVICE_NAME": "Intel AI Boost",
|
|
39
|
+
"DEVICE_ARCHITECTURE": "NPU",
|
|
40
|
+
"NPU_MAX_TOPS": 10,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
)
|
|
44
|
+
core.failed.add(("GPU", "DEVICE_ARCHITECTURE"))
|
|
45
|
+
rows = facts.collect_openvino_device_facts(core)
|
|
46
|
+
self.assertEqual([row["deviceKey"] for row in rows], ["openvino:gpu", "openvino:npu"])
|
|
47
|
+
gpu = rows[0]
|
|
48
|
+
self.assertEqual(gpu["executionUnits"], 32)
|
|
49
|
+
self.assertEqual(gpu["clockMHz"], 1250)
|
|
50
|
+
self.assertEqual(gpu["gops"], {"int8": 400, "fp16": 200})
|
|
51
|
+
self.assertIsNone(gpu["architecture"])
|
|
52
|
+
self.assertEqual(rows[1]["npuName"], "Intel AI Boost")
|
|
53
|
+
self.assertEqual(rows[1]["tops"], 10)
|
|
54
|
+
self.assertIsNone(rows[1]["executionUnits"])
|
|
55
|
+
|
|
56
|
+
def test_a_zero_eu_count_is_null(self):
|
|
57
|
+
core = _Core(
|
|
58
|
+
["GPU"],
|
|
59
|
+
{"GPU": {"FULL_DEVICE_NAME": "Intel", "GPU_EXECUTION_UNITS_COUNT": 0}},
|
|
60
|
+
)
|
|
61
|
+
rows = facts.collect_openvino_device_facts(core)
|
|
62
|
+
self.assertIsNone(rows[0]["executionUnits"])
|
|
63
|
+
self.assertIsNone(rows[0]["gops"])
|
|
64
|
+
|
|
65
|
+
def test_a_failed_device_list_is_none(self):
|
|
66
|
+
class Boom:
|
|
67
|
+
@property
|
|
68
|
+
def available_devices(self):
|
|
69
|
+
raise RuntimeError("no core")
|
|
70
|
+
|
|
71
|
+
self.assertIsNone(facts.collect_openvino_device_facts(Boom()))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class WebGpuFactsTest(unittest.TestCase):
|
|
75
|
+
def test_reads_adapter_fields_and_leaves_a_missing_cu_null(self):
|
|
76
|
+
row = facts.webgpu_facts_from_device(
|
|
77
|
+
{"vendor": "amd", "architecture": "rdna3", "device": "gfx1103"}
|
|
78
|
+
)
|
|
79
|
+
self.assertEqual(row["deviceKey"], "onnx:webgpu")
|
|
80
|
+
self.assertEqual(row["vendor"], "amd")
|
|
81
|
+
self.assertEqual(row["webgpuArchitecture"], "rdna3")
|
|
82
|
+
self.assertEqual(row["webgpuDevice"], "gfx1103")
|
|
83
|
+
self.assertIsNone(row["computeUnits"])
|
|
84
|
+
self.assertIsNone(facts.webgpu_facts_from_device(None))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class PublishTest(unittest.TestCase):
|
|
88
|
+
def test_merges_by_device_key(self):
|
|
89
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
90
|
+
path = os.path.join(directory, "facts.json")
|
|
91
|
+
facts.publish_accelerator_facts(
|
|
92
|
+
[facts.webgpu_facts_from_device({"vendor": "amd", "compute_units": 12})],
|
|
93
|
+
path,
|
|
94
|
+
)
|
|
95
|
+
gpu = facts._blank("openvino:gpu")
|
|
96
|
+
gpu["executionUnits"] = 24
|
|
97
|
+
facts.publish_accelerator_facts([gpu], path)
|
|
98
|
+
with open(path, "r", encoding="utf-8") as handle:
|
|
99
|
+
payload = json.load(handle)
|
|
100
|
+
keys = [row["deviceKey"] for row in payload["accelerators"]]
|
|
101
|
+
self.assertEqual(keys, ["onnx:webgpu", "openvino:gpu"])
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
unittest.main()
|
|
@@ -224,6 +224,89 @@ class EnsureCoremlLoadableTest(unittest.TestCase):
|
|
|
224
224
|
self.assertEqual(len(runner.calls), 1)
|
|
225
225
|
|
|
226
226
|
|
|
227
|
+
class CoremlCrashNeedsASecondAgreeingReadTest(unittest.TestCase):
|
|
228
|
+
"""One native abort must not ban a package forever (D926, D49)."""
|
|
229
|
+
|
|
230
|
+
def setUp(self) -> None:
|
|
231
|
+
self._tmp = tempfile.TemporaryDirectory()
|
|
232
|
+
self.pkg = _make_mlpackage(self._tmp.name)
|
|
233
|
+
self.t = 2_000_000.0
|
|
234
|
+
|
|
235
|
+
def tearDown(self) -> None:
|
|
236
|
+
self._tmp.cleanup()
|
|
237
|
+
|
|
238
|
+
def ensure(self, runner) -> None:
|
|
239
|
+
probe.ensure_coreml_loadable(
|
|
240
|
+
self.pkg, "all", run=runner, os_build="26.2",
|
|
241
|
+
now=lambda: self.t, loadavg=lambda: (4.0, 3.0, 2.0),
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def stored(self) -> dict:
|
|
245
|
+
with open(probe.probe_verdict_path(self.pkg, "all"), "r", encoding="utf-8") as fh:
|
|
246
|
+
return json.load(fh)
|
|
247
|
+
|
|
248
|
+
def test_a_single_crash_is_retried_after_the_cool_off(self) -> None:
|
|
249
|
+
with self.assertRaises(probe.CoremlLoadCrashed):
|
|
250
|
+
self.ensure(_Runner(probe.VERDICT_CRASHED, "signal 6"))
|
|
251
|
+
self.assertEqual(self.stored()["verdict"], probe.VERDICT_SUSPECT)
|
|
252
|
+
self.t += probe.CRASH_COOL_OFF_SEC + 1
|
|
253
|
+
ok = _Runner(probe.VERDICT_OK)
|
|
254
|
+
self.ensure(ok)
|
|
255
|
+
self.assertEqual(len(ok.calls), 1)
|
|
256
|
+
self.assertEqual(self.stored()["verdict"], probe.VERDICT_OK)
|
|
257
|
+
|
|
258
|
+
def test_within_the_cool_off_a_single_crash_refuses_without_probing(self) -> None:
|
|
259
|
+
crashed = _Runner(probe.VERDICT_CRASHED)
|
|
260
|
+
with self.assertRaises(probe.CoremlLoadCrashed):
|
|
261
|
+
self.ensure(crashed)
|
|
262
|
+
self.t += probe.CRASH_COOL_OFF_SEC - 1
|
|
263
|
+
with self.assertRaises(probe.CoremlLoadCrashed):
|
|
264
|
+
self.ensure(crashed)
|
|
265
|
+
self.assertEqual(len(crashed.calls), 1)
|
|
266
|
+
|
|
267
|
+
def test_two_crashes_a_cool_off_apart_are_cached(self) -> None:
|
|
268
|
+
crashed = _Runner(probe.VERDICT_CRASHED)
|
|
269
|
+
with self.assertRaises(probe.CoremlLoadCrashed):
|
|
270
|
+
self.ensure(crashed)
|
|
271
|
+
self.t += probe.CRASH_COOL_OFF_SEC + 1
|
|
272
|
+
with self.assertRaises(probe.CoremlLoadCrashed):
|
|
273
|
+
self.ensure(crashed)
|
|
274
|
+
stored = self.stored()
|
|
275
|
+
self.assertEqual(stored["verdict"], probe.VERDICT_CRASHED)
|
|
276
|
+
self.assertEqual(len(stored["crashes"]), 2)
|
|
277
|
+
self.assertEqual(stored["crashes"][0]["loadavg"], [4.0, 3.0, 2.0])
|
|
278
|
+
self.t += 10 * probe.CRASH_COOL_OFF_SEC
|
|
279
|
+
with self.assertRaises(probe.CoremlLoadCrashed):
|
|
280
|
+
self.ensure(crashed)
|
|
281
|
+
self.assertEqual(len(crashed.calls), 2)
|
|
282
|
+
|
|
283
|
+
def test_a_success_in_between_resets_the_count(self) -> None:
|
|
284
|
+
crashed = _Runner(probe.VERDICT_CRASHED)
|
|
285
|
+
with self.assertRaises(probe.CoremlLoadCrashed):
|
|
286
|
+
self.ensure(crashed)
|
|
287
|
+
self.t += probe.CRASH_COOL_OFF_SEC + 1
|
|
288
|
+
self.ensure(_Runner(probe.VERDICT_OK))
|
|
289
|
+
# An ok is remembered for good; a changed OS build is what re-probes it.
|
|
290
|
+
with open(probe.probe_verdict_path(self.pkg, "all"), "r", encoding="utf-8") as fh:
|
|
291
|
+
record = json.load(fh)
|
|
292
|
+
self.assertNotIn("crashes", record)
|
|
293
|
+
|
|
294
|
+
def test_a_legacy_single_crash_verdict_is_reprobed(self) -> None:
|
|
295
|
+
key = {
|
|
296
|
+
"version": probe.PROBE_VERDICT_VERSION,
|
|
297
|
+
"fingerprint": ip._coreml_model_fingerprint(self.pkg),
|
|
298
|
+
"device": "all",
|
|
299
|
+
"os": "26.2",
|
|
300
|
+
}
|
|
301
|
+
path = probe.probe_verdict_path(self.pkg, "all")
|
|
302
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
303
|
+
with open(path, "w", encoding="utf-8") as fh:
|
|
304
|
+
json.dump({"key": key, "verdict": probe.VERDICT_CRASHED, "detail": "sig 6"}, fh)
|
|
305
|
+
ok = _Runner(probe.VERDICT_OK)
|
|
306
|
+
self.ensure(ok)
|
|
307
|
+
self.assertEqual(len(ok.calls), 1)
|
|
308
|
+
|
|
309
|
+
|
|
227
310
|
class _FakeCt:
|
|
228
311
|
"""The slice of coremltools `_load_model` touches before the model loads."""
|
|
229
312
|
|