@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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { r as __toESM } from "../chunk-DnnnRqeS.mjs";
|
|
2
|
-
import {
|
|
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 { Bn as isSoftwareDecode, Cr as record, D as G711_SCALE_CORRECTION_DB, Er as EventCategory, Et as egressTransportFromRequest, Gn as BaseAddon, Hn as DeviceType, Jt as memoizePinnedFfmpeg, K as RingBuffer, Kn as CAM_PROFILE_ORDER, L as RATE_CONTROL_RELAXED, Ln as buildFfmpegArgs, Lt as gifDeriveSpeed, Nn as webrtcSessionCapability, Pn as AUDIO_PRESETS, Pt as ffmpegRate, R as RATE_CONTROL_TIGHT, Rt as gifFromMp4Args, St as deviceRemovalHookCapability, Tr as union, Tt as egressTranscodeSharingKey, Un as errMsg, Ut as loadContributionCapability, Vn as DeviceFeature, Wn as BOOT_RECOVERY_BACKOFF_MS, Wt as logChannelsCapability, X as addonWidgetsSourceCapability, Zn as createEvent, _r as discriminatedUnion, _t as deriveBatteryPresence, ar as isSecretMarker, br as number, c as CLIP_STILL_REASON_CODE_HEADER, cr as maskUrlCredentials, et as armRemovalReconcile, gn as resolveEgressDecodeHwAccel, gr as boolean, hr as array, ht as declareLogChannel, ir as isEvent, it as batteryCapability, kn as streamBrokerCapability, lr as nodePin, lt as clipExportFailureOf, mr as _enum, mt as createLogChannelsProvider, o as BatteryStatusSchema, or as makeProfileBrokerId, pt as createHwAccelCache, qn as CamProfileSchema, s as CLIP_BROKER_PACED_RATES, sn as rankStreams, sr as makeSourceBrokerId, st as cameraStreamsCapability, tr as expandAudioChunkToF32le, ur as parseProfileBrokerId, w as EncodeProfileSchema, wr as string, xr as object, yr as literal, zn as invocationFromEncodeProfile } from "../dist-opSx6v5i.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-CGIYuJMN.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-CdVRzwSd.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";
|
|
@@ -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-DF3FGCR8.mjs")).catch((e) => {
|
|
34
34
|
throw l = void 0, e;
|
|
35
35
|
}), l;
|
|
36
36
|
}
|
|
@@ -60,6 +60,71 @@ function formatNativeLeaseKnobs(resolved) {
|
|
|
60
60
|
return `native lease knobs — ${NATIVE_LEASE_KNOB_NAMES.map(part).join("; ")}`;
|
|
61
61
|
}
|
|
62
62
|
//#endregion
|
|
63
|
+
//#region src/session-decode/session-hwaccel.ts
|
|
64
|
+
var KNOWN_BACKENDS = [
|
|
65
|
+
"videotoolbox",
|
|
66
|
+
"cuda",
|
|
67
|
+
"nvdec",
|
|
68
|
+
"vaapi",
|
|
69
|
+
"qsv",
|
|
70
|
+
"d3d11va",
|
|
71
|
+
"dxva2",
|
|
72
|
+
"amf",
|
|
73
|
+
"vdpau",
|
|
74
|
+
"drm"
|
|
75
|
+
];
|
|
76
|
+
function isHwAccelBackend(value) {
|
|
77
|
+
return typeof value === "string" && KNOWN_BACKENDS.includes(value);
|
|
78
|
+
}
|
|
79
|
+
/** Wire guard for the `start` message's `hwaccel` field. */
|
|
80
|
+
function isSessionHwaccelOffer(value) {
|
|
81
|
+
return value === null || Array.isArray(value) && value.every(isHwAccelBackend);
|
|
82
|
+
}
|
|
83
|
+
/** The worker's decode order per platform — first successful create() wins. */
|
|
84
|
+
function platformDecodeOrder(platform) {
|
|
85
|
+
switch (platform) {
|
|
86
|
+
case "darwin": return ["videotoolbox"];
|
|
87
|
+
case "linux": return [
|
|
88
|
+
"vaapi",
|
|
89
|
+
"qsv",
|
|
90
|
+
"cuda"
|
|
91
|
+
];
|
|
92
|
+
case "win32": return [
|
|
93
|
+
"d3d11va",
|
|
94
|
+
"dxva2",
|
|
95
|
+
"cuda"
|
|
96
|
+
];
|
|
97
|
+
default: return [];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The platform order, restricted to what the resolver offers. An empty offer
|
|
102
|
+
* (VA init failed and no other GPU) means software, with no per-session try.
|
|
103
|
+
*/
|
|
104
|
+
function sessionDecodeCandidates(platform, offer) {
|
|
105
|
+
const order = platformDecodeOrder(platform);
|
|
106
|
+
if (offer === null) return order;
|
|
107
|
+
return order.filter((backend) => offer.includes(backend));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The parent's copy of the resolver's auto answer, refreshed on the acquire
|
|
111
|
+
* leg of every session spawn and read synchronously when `start` is sent. A
|
|
112
|
+
* resolve that fails changes nothing (D49): the last answer stands.
|
|
113
|
+
*/
|
|
114
|
+
var SessionHwaccelSource = class {
|
|
115
|
+
offer = null;
|
|
116
|
+
current() {
|
|
117
|
+
return this.offer;
|
|
118
|
+
}
|
|
119
|
+
async refresh(resolver) {
|
|
120
|
+
if (!resolver) return;
|
|
121
|
+
try {
|
|
122
|
+
const res = await resolver.resolve(null);
|
|
123
|
+
this.offer = [...res.preferred];
|
|
124
|
+
} catch {}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
//#endregion
|
|
63
128
|
//#region src/session-decode/worker-protocol.ts
|
|
64
129
|
/**
|
|
65
130
|
* Typed IPC protocol between the per-session decode coordinator (Task 3 —
|
|
@@ -129,7 +194,7 @@ function isVideoFrameGenOptions(value) {
|
|
|
129
194
|
function isWorkerRequest(value) {
|
|
130
195
|
if (!isRecord(value)) return false;
|
|
131
196
|
switch (value.kind) {
|
|
132
|
-
case "start": return isVideoFrameSource(value.source) && isVideoFrameGenOptions(value.opts) && isWorkerLeaseKnobs(value.lease);
|
|
197
|
+
case "start": return isVideoFrameSource(value.source) && isVideoFrameGenOptions(value.opts) && isWorkerLeaseKnobs(value.lease) && isSessionHwaccelOffer(value.hwaccel);
|
|
133
198
|
case "pull": return true;
|
|
134
199
|
case "toBuffer": return isFiniteNumber(value.frameId) && (value.opts === void 0 || isFrameImageOptions(value.opts));
|
|
135
200
|
case "nativeCrop": return isFiniteNumber(value.requestId) && isFiniteNumber(value.frameId) && isNormalizedBbox(value.bbox) && (value.maxWidth === void 0 || isFiniteNumber(value.maxWidth));
|
|
@@ -160,4 +225,39 @@ function isWorkerReply(value) {
|
|
|
160
225
|
}
|
|
161
226
|
}
|
|
162
227
|
//#endregion
|
|
163
|
-
|
|
228
|
+
Object.defineProperty(exports, "SessionHwaccelSource", {
|
|
229
|
+
enumerable: true,
|
|
230
|
+
get: function() {
|
|
231
|
+
return SessionHwaccelSource;
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
Object.defineProperty(exports, "formatNativeLeaseKnobs", {
|
|
235
|
+
enumerable: true,
|
|
236
|
+
get: function() {
|
|
237
|
+
return formatNativeLeaseKnobs;
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
Object.defineProperty(exports, "isWorkerReply", {
|
|
241
|
+
enumerable: true,
|
|
242
|
+
get: function() {
|
|
243
|
+
return isWorkerReply;
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
Object.defineProperty(exports, "isWorkerRequest", {
|
|
247
|
+
enumerable: true,
|
|
248
|
+
get: function() {
|
|
249
|
+
return isWorkerRequest;
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
Object.defineProperty(exports, "logLevelForLine", {
|
|
253
|
+
enumerable: true,
|
|
254
|
+
get: function() {
|
|
255
|
+
return logLevelForLine;
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
Object.defineProperty(exports, "sessionDecodeCandidates", {
|
|
259
|
+
enumerable: true,
|
|
260
|
+
get: function() {
|
|
261
|
+
return sessionDecodeCandidates;
|
|
262
|
+
}
|
|
263
|
+
});
|
|
@@ -60,6 +60,71 @@ function formatNativeLeaseKnobs(resolved) {
|
|
|
60
60
|
return `native lease knobs — ${NATIVE_LEASE_KNOB_NAMES.map(part).join("; ")}`;
|
|
61
61
|
}
|
|
62
62
|
//#endregion
|
|
63
|
+
//#region src/session-decode/session-hwaccel.ts
|
|
64
|
+
var KNOWN_BACKENDS = [
|
|
65
|
+
"videotoolbox",
|
|
66
|
+
"cuda",
|
|
67
|
+
"nvdec",
|
|
68
|
+
"vaapi",
|
|
69
|
+
"qsv",
|
|
70
|
+
"d3d11va",
|
|
71
|
+
"dxva2",
|
|
72
|
+
"amf",
|
|
73
|
+
"vdpau",
|
|
74
|
+
"drm"
|
|
75
|
+
];
|
|
76
|
+
function isHwAccelBackend(value) {
|
|
77
|
+
return typeof value === "string" && KNOWN_BACKENDS.includes(value);
|
|
78
|
+
}
|
|
79
|
+
/** Wire guard for the `start` message's `hwaccel` field. */
|
|
80
|
+
function isSessionHwaccelOffer(value) {
|
|
81
|
+
return value === null || Array.isArray(value) && value.every(isHwAccelBackend);
|
|
82
|
+
}
|
|
83
|
+
/** The worker's decode order per platform — first successful create() wins. */
|
|
84
|
+
function platformDecodeOrder(platform) {
|
|
85
|
+
switch (platform) {
|
|
86
|
+
case "darwin": return ["videotoolbox"];
|
|
87
|
+
case "linux": return [
|
|
88
|
+
"vaapi",
|
|
89
|
+
"qsv",
|
|
90
|
+
"cuda"
|
|
91
|
+
];
|
|
92
|
+
case "win32": return [
|
|
93
|
+
"d3d11va",
|
|
94
|
+
"dxva2",
|
|
95
|
+
"cuda"
|
|
96
|
+
];
|
|
97
|
+
default: return [];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The platform order, restricted to what the resolver offers. An empty offer
|
|
102
|
+
* (VA init failed and no other GPU) means software, with no per-session try.
|
|
103
|
+
*/
|
|
104
|
+
function sessionDecodeCandidates(platform, offer) {
|
|
105
|
+
const order = platformDecodeOrder(platform);
|
|
106
|
+
if (offer === null) return order;
|
|
107
|
+
return order.filter((backend) => offer.includes(backend));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The parent's copy of the resolver's auto answer, refreshed on the acquire
|
|
111
|
+
* leg of every session spawn and read synchronously when `start` is sent. A
|
|
112
|
+
* resolve that fails changes nothing (D49): the last answer stands.
|
|
113
|
+
*/
|
|
114
|
+
var SessionHwaccelSource = class {
|
|
115
|
+
offer = null;
|
|
116
|
+
current() {
|
|
117
|
+
return this.offer;
|
|
118
|
+
}
|
|
119
|
+
async refresh(resolver) {
|
|
120
|
+
if (!resolver) return;
|
|
121
|
+
try {
|
|
122
|
+
const res = await resolver.resolve(null);
|
|
123
|
+
this.offer = [...res.preferred];
|
|
124
|
+
} catch {}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
//#endregion
|
|
63
128
|
//#region src/session-decode/worker-protocol.ts
|
|
64
129
|
/**
|
|
65
130
|
* Typed IPC protocol between the per-session decode coordinator (Task 3 —
|
|
@@ -129,7 +194,7 @@ function isVideoFrameGenOptions(value) {
|
|
|
129
194
|
function isWorkerRequest(value) {
|
|
130
195
|
if (!isRecord(value)) return false;
|
|
131
196
|
switch (value.kind) {
|
|
132
|
-
case "start": return isVideoFrameSource(value.source) && isVideoFrameGenOptions(value.opts) && isWorkerLeaseKnobs(value.lease);
|
|
197
|
+
case "start": return isVideoFrameSource(value.source) && isVideoFrameGenOptions(value.opts) && isWorkerLeaseKnobs(value.lease) && isSessionHwaccelOffer(value.hwaccel);
|
|
133
198
|
case "pull": return true;
|
|
134
199
|
case "toBuffer": return isFiniteNumber(value.frameId) && (value.opts === void 0 || isFrameImageOptions(value.opts));
|
|
135
200
|
case "nativeCrop": return isFiniteNumber(value.requestId) && isFiniteNumber(value.frameId) && isNormalizedBbox(value.bbox) && (value.maxWidth === void 0 || isFiniteNumber(value.maxWidth));
|
|
@@ -160,27 +225,4 @@ function isWorkerReply(value) {
|
|
|
160
225
|
}
|
|
161
226
|
}
|
|
162
227
|
//#endregion
|
|
163
|
-
|
|
164
|
-
enumerable: true,
|
|
165
|
-
get: function() {
|
|
166
|
-
return formatNativeLeaseKnobs;
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
Object.defineProperty(exports, "isWorkerReply", {
|
|
170
|
-
enumerable: true,
|
|
171
|
-
get: function() {
|
|
172
|
-
return isWorkerReply;
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
Object.defineProperty(exports, "isWorkerRequest", {
|
|
176
|
-
enumerable: true,
|
|
177
|
-
get: function() {
|
|
178
|
-
return isWorkerRequest;
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
Object.defineProperty(exports, "logLevelForLine", {
|
|
182
|
-
enumerable: true,
|
|
183
|
-
get: function() {
|
|
184
|
-
return logLevelForLine;
|
|
185
|
-
}
|
|
186
|
-
});
|
|
228
|
+
export { sessionDecodeCandidates as a, SessionHwaccelSource as i, isWorkerRequest as n, formatNativeLeaseKnobs as o, logLevelForLine as r, isWorkerReply as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-pipeline",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.623",
|
|
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",
|
|
@@ -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())
|