@camstack/addon-pipeline 1.2.622 → 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.
Files changed (41) hide show
  1. package/dist/audio-analyzer/index.js +3 -3
  2. package/dist/audio-analyzer/index.mjs +3 -3
  3. package/dist/{custom-model-catalog-CKCK-QcT.js → custom-model-catalog-BTBdgN2p.js} +24 -11
  4. package/dist/{custom-model-catalog-wpMi2U7e.mjs → custom-model-catalog-D5zTw1LU.mjs} +24 -5
  5. package/dist/detection-pipeline/index.js +16 -6
  6. package/dist/detection-pipeline/index.mjs +16 -6
  7. package/dist/{dist-DUFGDd_2.js → dist-BVo3wa7s.js} +636 -71
  8. package/dist/{dist-BF60SjmI.mjs → dist-opSx6v5i.mjs} +576 -71
  9. package/dist/{model-catalogs-CjTm_0O-.js → model-catalogs-CQytR2AN.js} +1 -1
  10. package/dist/{model-catalogs-CqFDhb0B.mjs → model-catalogs-rIvLUVlw.mjs} +1 -1
  11. package/dist/motion-wasm/index.js +1 -1
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/{node-DkphOOM2.mjs → node-CGIYuJMN.mjs} +1 -1
  14. package/dist/{node-DzGVn7bD.js → node-DqLsuN_A.js} +1 -1
  15. package/dist/pipeline-runner/index.js +101 -460
  16. package/dist/pipeline-runner/index.mjs +94 -453
  17. package/dist/{process-memory-CBzfdSvC.mjs → process-memory-Bd-TuWfM.mjs} +1 -1
  18. package/dist/{process-memory-8kKexHYR.js → process-memory-DyxG-0mG.js} +1 -1
  19. package/dist/recorder/index.js +2 -2
  20. package/dist/recorder/index.mjs +2 -2
  21. package/dist/{segment-demux-js-DRLQb1pc.js → segment-demux-js-B4bfTQpK.js} +1 -1
  22. package/dist/{segment-demux-js-DQNoNSJt.mjs → segment-demux-js-CdVRzwSd.mjs} +1 -1
  23. package/dist/stream-broker/_stub.js +2 -2
  24. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BmANAxAe.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DF3FGCR8.mjs} +2 -2
  25. 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
  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-Bb-ETNGC.mjs +26 -0
  27. package/dist/stream-broker/demux-worker-child.js +1 -1
  28. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  29. package/dist/stream-broker/{hostInit-B3hXAfr1.mjs → hostInit-BiyFzSqq.mjs} +2 -2
  30. package/dist/stream-broker/index.js +3 -3
  31. package/dist/stream-broker/index.mjs +3 -3
  32. package/dist/stream-broker/remoteEntry.js +1 -1
  33. package/package.json +1 -1
  34. package/python/accelerator_facts.py +275 -0
  35. package/python/inference_pool.py +19 -2
  36. package/python/postprocessors/ssd.py +12 -4
  37. package/python/postprocessors/test_ssd.py +64 -0
  38. package/python/test_accelerator_facts.py +105 -0
  39. package/python/webgpu_isolated.py +19 -0
  40. 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
  41. 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())
@@ -3150,7 +3150,21 @@ async def _run() -> None:
3150
3150
  )
3151
3151
  sys.stderr.flush()
3152
3152
 
3153
- await writer.send(req_id, {
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
- """COCO-90 label of a raw class index; ``"n/a"`` for a gap, ``str`` if outside."""
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"