@camstack/addon-pipeline 1.2.622 → 1.2.624
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-CKCK-QcT.js → custom-model-catalog-6zwaRe6H.js} +24 -11
- package/dist/{custom-model-catalog-wpMi2U7e.mjs → custom-model-catalog-Dx6W1kAo.mjs} +24 -5
- package/dist/detection-pipeline/index.js +16 -6
- package/dist/detection-pipeline/index.mjs +16 -6
- package/dist/{dist-DUFGDd_2.js → dist-63VVVhpw.js} +1371 -240
- package/dist/{dist-BF60SjmI.mjs → dist-BK3z-nti.mjs} +1311 -240
- package/dist/{model-catalogs-CqFDhb0B.mjs → model-catalogs-CllYj2Q2.mjs} +1 -1
- package/dist/{model-catalogs-CjTm_0O-.js → model-catalogs-DyOMg-xZ.js} +1 -1
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-DkphOOM2.mjs → node-BAcWHimZ.mjs} +2 -2
- package/dist/{node-DzGVn7bD.js → node-DdaBnHEq.js} +2 -2
- package/dist/pipeline-runner/index.js +101 -460
- package/dist/pipeline-runner/index.mjs +94 -453
- package/dist/{process-memory-8kKexHYR.js → process-memory-CUlknrLl.js} +1 -1
- package/dist/{process-memory-CBzfdSvC.mjs → process-memory-OZwib1pK.mjs} +1 -1
- package/dist/recorder/index.js +21 -2
- package/dist/recorder/index.mjs +21 -2
- package/dist/{segment-demux-js-DQNoNSJt.mjs → segment-demux-js-C3rhCLCG.mjs} +1 -1
- package/dist/{segment-demux-js-DRLQb1pc.js → segment-demux-js-WNlg0SXP.js} +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BmANAxAe.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Di7t7N3r.mjs} +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DDRGMoJ1.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-CPQe09h1.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-B3hXAfr1.mjs → hostInit-CMDvBrqv.mjs} +2 -2
- package/dist/stream-broker/index.js +4 -4
- package/dist/stream-broker/index.mjs +4 -4
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/python/accelerator_facts.py +275 -0
- package/python/inference_pool.py +19 -2
- 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/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-H6RKsaiZ.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-CNoWeZHH.mjs +0 -26
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""Per-accelerator compute the Auto tune estimate scales from (D930).
|
|
2
|
+
|
|
3
|
+
OpenVINO devices are enumerated here — the same place the pool already asks
|
|
4
|
+
`available_devices` — and an AMD WebGPU adapter is read when the plugin
|
|
5
|
+
exposes one. A query that fails is ``None``, never ``0`` (D393): zero would
|
|
6
|
+
scale the device as if it had no compute.
|
|
7
|
+
|
|
8
|
+
The pool publishes the list to the temp file the platform probe reads back
|
|
9
|
+
onto ``HardwareInfo.accelerators``. Importing this module does not import
|
|
10
|
+
OpenVINO or ONNX Runtime.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import tempfile
|
|
18
|
+
from typing import Any, Optional
|
|
19
|
+
|
|
20
|
+
# One file per host. The inference worker writes it; platform-probe reads it.
|
|
21
|
+
# Two pools (GPU and NPU) merge by deviceKey.
|
|
22
|
+
FACTS_FILENAME = "camstack-accelerator-facts.json"
|
|
23
|
+
|
|
24
|
+
_GOPS_INT8 = ("i8", "int8", "u8", "bin")
|
|
25
|
+
_GOPS_FP16 = ("f16", "fp16", "bf16")
|
|
26
|
+
_CLOCK_PROPERTIES = (
|
|
27
|
+
"GPU_DEVICE_MAX_FREQUENCY",
|
|
28
|
+
"GPU_MAX_FREQUENCY",
|
|
29
|
+
"GPU_FREQUENCY",
|
|
30
|
+
)
|
|
31
|
+
_TOPS_PROPERTIES = ("NPU_MAX_TOPS", "DEVICE_TOPS")
|
|
32
|
+
_CU_ATTRIBUTES = ("compute_units", "cu_count", "max_compute_units")
|
|
33
|
+
_VENDOR_ATTRIBUTES = ("vendor", "vendor_name", "vendorName")
|
|
34
|
+
_ARCH_ATTRIBUTES = ("architecture", "arch")
|
|
35
|
+
_DEVICE_ATTRIBUTES = ("device", "device_name", "name", "description")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def facts_path() -> str:
|
|
39
|
+
return os.path.join(tempfile.gettempdir(), FACTS_FILENAME)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _positive(value: Any) -> Optional[float]:
|
|
43
|
+
"""A reported number, or None when the query failed or answered <= 0."""
|
|
44
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
45
|
+
return None
|
|
46
|
+
number = float(value)
|
|
47
|
+
if number <= 0 or number != number: # NaN
|
|
48
|
+
return None
|
|
49
|
+
return number
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _text(value: Any) -> Optional[str]:
|
|
53
|
+
if not isinstance(value, str):
|
|
54
|
+
return None
|
|
55
|
+
stripped = value.strip()
|
|
56
|
+
return stripped or None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _blank(device_key: str) -> dict:
|
|
60
|
+
return {
|
|
61
|
+
"deviceKey": device_key,
|
|
62
|
+
"fullDeviceName": None,
|
|
63
|
+
"executionUnits": None,
|
|
64
|
+
"clockMHz": None,
|
|
65
|
+
"gops": None,
|
|
66
|
+
"architecture": None,
|
|
67
|
+
"npuName": None,
|
|
68
|
+
"tops": None,
|
|
69
|
+
"vendor": None,
|
|
70
|
+
"webgpuArchitecture": None,
|
|
71
|
+
"webgpuDevice": None,
|
|
72
|
+
"computeUnits": None,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _property(core: Any, device: str, name: str) -> Any:
|
|
77
|
+
try:
|
|
78
|
+
return core.get_property(device, name)
|
|
79
|
+
except Exception:
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _first_property(core: Any, device: str, names: tuple) -> Any:
|
|
84
|
+
for name in names:
|
|
85
|
+
value = _property(core, device, name)
|
|
86
|
+
if value is not None:
|
|
87
|
+
return value
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _gops(raw: Any) -> Optional[dict]:
|
|
92
|
+
"""``DEVICE_GOPS`` as ``{int8, fp16}``. None when the query itself failed."""
|
|
93
|
+
if raw is None:
|
|
94
|
+
return None
|
|
95
|
+
try:
|
|
96
|
+
items = list(raw.items())
|
|
97
|
+
except Exception:
|
|
98
|
+
return None
|
|
99
|
+
int8 = None
|
|
100
|
+
fp16 = None
|
|
101
|
+
for key, value in items:
|
|
102
|
+
name = str(key).lower().rsplit(".", 1)[-1]
|
|
103
|
+
if name in _GOPS_INT8:
|
|
104
|
+
int8 = _positive(value)
|
|
105
|
+
elif name in _GOPS_FP16:
|
|
106
|
+
fp16 = _positive(value)
|
|
107
|
+
return {"int8": int8, "fp16": fp16}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _device_key(ov_name: str, index: int) -> Optional[str]:
|
|
111
|
+
upper = ov_name.upper()
|
|
112
|
+
if "NPU" in upper:
|
|
113
|
+
kind = "npu"
|
|
114
|
+
elif "GPU" in upper:
|
|
115
|
+
kind = "gpu"
|
|
116
|
+
else:
|
|
117
|
+
return None
|
|
118
|
+
if index == 0:
|
|
119
|
+
return f"openvino:{kind}"
|
|
120
|
+
return f"openvino:{kind}:{index}"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def collect_openvino_device_facts(core: Any) -> Optional[list]:
|
|
124
|
+
"""One record per GPU and NPU. None when the device list itself failed."""
|
|
125
|
+
try:
|
|
126
|
+
available = list(core.available_devices)
|
|
127
|
+
except Exception:
|
|
128
|
+
return None
|
|
129
|
+
seen = {"gpu": 0, "npu": 0}
|
|
130
|
+
facts: list = []
|
|
131
|
+
for ov_name in available:
|
|
132
|
+
upper = str(ov_name).upper()
|
|
133
|
+
if "NPU" in upper:
|
|
134
|
+
kind = "npu"
|
|
135
|
+
elif "GPU" in upper:
|
|
136
|
+
kind = "gpu"
|
|
137
|
+
else:
|
|
138
|
+
continue
|
|
139
|
+
key = _device_key(str(ov_name), seen[kind])
|
|
140
|
+
seen[kind] += 1
|
|
141
|
+
if key is None:
|
|
142
|
+
continue
|
|
143
|
+
record = _blank(key)
|
|
144
|
+
name = _text(_property(core, ov_name, "FULL_DEVICE_NAME"))
|
|
145
|
+
record["fullDeviceName"] = name
|
|
146
|
+
record["architecture"] = _text(_property(core, ov_name, "DEVICE_ARCHITECTURE"))
|
|
147
|
+
record["gops"] = _gops(_property(core, ov_name, "DEVICE_GOPS"))
|
|
148
|
+
if kind == "gpu":
|
|
149
|
+
record["executionUnits"] = _positive(
|
|
150
|
+
_property(core, ov_name, "GPU_EXECUTION_UNITS_COUNT")
|
|
151
|
+
)
|
|
152
|
+
record["clockMHz"] = _positive(_first_property(core, ov_name, _CLOCK_PROPERTIES))
|
|
153
|
+
else:
|
|
154
|
+
record["npuName"] = name
|
|
155
|
+
record["tops"] = _positive(_first_property(core, ov_name, _TOPS_PROPERTIES))
|
|
156
|
+
facts.append(record)
|
|
157
|
+
sys.stderr.write(
|
|
158
|
+
f"accelerator facts: {key} name={name} "
|
|
159
|
+
f"eu={record['executionUnits']} clockMHz={record['clockMHz']} "
|
|
160
|
+
f"gops={record['gops']} arch={record['architecture']} tops={record['tops']}\n"
|
|
161
|
+
)
|
|
162
|
+
sys.stderr.flush()
|
|
163
|
+
return facts
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _attr(obj: Any, names: tuple) -> Any:
|
|
167
|
+
if obj is None:
|
|
168
|
+
return None
|
|
169
|
+
if isinstance(obj, dict):
|
|
170
|
+
for name in names:
|
|
171
|
+
if name in obj and obj[name] is not None:
|
|
172
|
+
return obj[name]
|
|
173
|
+
return None
|
|
174
|
+
for name in names:
|
|
175
|
+
if hasattr(obj, name):
|
|
176
|
+
value = getattr(obj, name)
|
|
177
|
+
if value is not None:
|
|
178
|
+
return value
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def webgpu_facts_from_device(device: Any) -> Optional[dict]:
|
|
183
|
+
"""Adapter fields off an ORT / Dawn device. None when there is no device."""
|
|
184
|
+
if device is None:
|
|
185
|
+
return None
|
|
186
|
+
record = _blank("onnx:webgpu")
|
|
187
|
+
record["vendor"] = _text(_attr(device, _VENDOR_ATTRIBUTES))
|
|
188
|
+
record["webgpuArchitecture"] = _text(_attr(device, _ARCH_ATTRIBUTES))
|
|
189
|
+
record["webgpuDevice"] = _text(_attr(device, _DEVICE_ATTRIBUTES))
|
|
190
|
+
record["computeUnits"] = _positive(_attr(device, _CU_ATTRIBUTES))
|
|
191
|
+
return record
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def publish_accelerator_facts(facts: list, path: Optional[str] = None) -> None:
|
|
195
|
+
"""Merge ``facts`` into the published file by deviceKey. Atomic replace."""
|
|
196
|
+
target = path if path is not None else facts_path()
|
|
197
|
+
current: list = []
|
|
198
|
+
try:
|
|
199
|
+
with open(target, "r", encoding="utf-8") as handle:
|
|
200
|
+
payload = json.load(handle)
|
|
201
|
+
if isinstance(payload, dict) and isinstance(payload.get("accelerators"), list):
|
|
202
|
+
current = [row for row in payload["accelerators"] if isinstance(row, dict)]
|
|
203
|
+
except (OSError, ValueError):
|
|
204
|
+
current = []
|
|
205
|
+
by_key = {row.get("deviceKey"): row for row in current if isinstance(row.get("deviceKey"), str)}
|
|
206
|
+
for row in facts:
|
|
207
|
+
key = row.get("deviceKey")
|
|
208
|
+
if isinstance(key, str):
|
|
209
|
+
by_key[key] = row
|
|
210
|
+
merged = [by_key[key] for key in sorted(by_key)]
|
|
211
|
+
tmp = f"{target}.tmp-{os.getpid()}"
|
|
212
|
+
with open(tmp, "w", encoding="utf-8") as handle:
|
|
213
|
+
json.dump({"accelerators": merged}, handle)
|
|
214
|
+
os.replace(tmp, target)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def facts_for_runtime(runtime: str, runtime_lib: Any, device: str) -> Optional[list]:
|
|
218
|
+
"""Facts for the runtime this pool just initialised. None when it cannot ask."""
|
|
219
|
+
if runtime == "openvino":
|
|
220
|
+
return collect_openvino_device_facts(runtime_lib)
|
|
221
|
+
if runtime == "onnxruntime" and device.strip().lower() == "webgpu":
|
|
222
|
+
one = _probe_webgpu_adapter()
|
|
223
|
+
return None if one is None else [one]
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _probe_webgpu_adapter() -> Optional[dict]:
|
|
228
|
+
"""Best-effort adapter read. None when the plugin or the device is absent."""
|
|
229
|
+
try:
|
|
230
|
+
import onnxruntime as ort
|
|
231
|
+
import onnxruntime_ep_webgpu as webgpu_ep
|
|
232
|
+
except Exception:
|
|
233
|
+
return None
|
|
234
|
+
try:
|
|
235
|
+
ep_name = webgpu_ep.get_ep_name()
|
|
236
|
+
device = next(
|
|
237
|
+
(item for item in ort.get_ep_devices() if getattr(item, "ep_name", None) == ep_name),
|
|
238
|
+
None,
|
|
239
|
+
)
|
|
240
|
+
except Exception:
|
|
241
|
+
return None
|
|
242
|
+
return webgpu_facts_from_device(device)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _probe_openvino() -> Optional[list]:
|
|
246
|
+
try:
|
|
247
|
+
from openvino import Core
|
|
248
|
+
except Exception as exc:
|
|
249
|
+
sys.stderr.write(f"accelerator facts: OpenVINO unavailable ({exc})\n")
|
|
250
|
+
sys.stderr.flush()
|
|
251
|
+
return None
|
|
252
|
+
try:
|
|
253
|
+
return collect_openvino_device_facts(Core())
|
|
254
|
+
except Exception as exc:
|
|
255
|
+
sys.stderr.write(f"accelerator facts: OpenVINO probe failed ({exc})\n")
|
|
256
|
+
sys.stderr.flush()
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def main() -> int:
|
|
261
|
+
"""Print ``{"accelerators": ...}``. ``null`` when nothing could be asked."""
|
|
262
|
+
ov = _probe_openvino()
|
|
263
|
+
web = _probe_webgpu_adapter()
|
|
264
|
+
if ov is None and web is None:
|
|
265
|
+
sys.stdout.write(json.dumps({"accelerators": None}) + "\n")
|
|
266
|
+
return 0
|
|
267
|
+
facts = [] if ov is None else list(ov)
|
|
268
|
+
if web is not None:
|
|
269
|
+
facts.append(web)
|
|
270
|
+
sys.stdout.write(json.dumps({"accelerators": facts}) + "\n")
|
|
271
|
+
return 0
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
if __name__ == "__main__":
|
|
275
|
+
raise SystemExit(main())
|
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
|
#
|
|
@@ -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()
|
|
@@ -139,6 +139,24 @@ def _read_msg(stream: BinaryIO) -> Any:
|
|
|
139
139
|
return pickle.loads(data)
|
|
140
140
|
|
|
141
141
|
|
|
142
|
+
def _publish_webgpu_facts(device: Any) -> None:
|
|
143
|
+
"""Record adapter compute for the estimate. A failure leaves the session up."""
|
|
144
|
+
try:
|
|
145
|
+
from accelerator_facts import publish_accelerator_facts, webgpu_facts_from_device
|
|
146
|
+
|
|
147
|
+
row = webgpu_facts_from_device(device)
|
|
148
|
+
if row is not None:
|
|
149
|
+
publish_accelerator_facts([row])
|
|
150
|
+
sys.stderr.write(
|
|
151
|
+
f"ONNX WebGPU: adapter vendor={row['vendor']} arch={row['webgpuArchitecture']} "
|
|
152
|
+
f"device={row['webgpuDevice']} cu={row['computeUnits']}\n"
|
|
153
|
+
)
|
|
154
|
+
sys.stderr.flush()
|
|
155
|
+
except Exception as exc:
|
|
156
|
+
sys.stderr.write(f"ONNX WebGPU: adapter facts unavailable ({exc})\n")
|
|
157
|
+
sys.stderr.flush()
|
|
158
|
+
|
|
159
|
+
|
|
142
160
|
def load_onnx_webgpu_session(
|
|
143
161
|
ort: Any,
|
|
144
162
|
path: str,
|
|
@@ -194,6 +212,7 @@ def load_onnx_webgpu_session(
|
|
|
194
212
|
sess_options.add_provider_for_devices([webgpu_device], {})
|
|
195
213
|
session = ort.InferenceSession(path, sess_options=sess_options)
|
|
196
214
|
active = list(session.get_providers())
|
|
215
|
+
_publish_webgpu_facts(webgpu_device)
|
|
197
216
|
sys.stderr.write(
|
|
198
217
|
f"ONNX WebGPU: providers={active} ep={ep_name} device=webgpu "
|
|
199
218
|
f"render-node=ok {_identity()}\n"
|