@camstack/addon-platform-probe-native 0.1.1
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/index.d.mts +77 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.js +460 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +423 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +68 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { IScopedLogger, PlatformCapabilities, HardwareInfo, PlatformScore, ModelRequirement, ResolvedInferenceConfig, BaseAddon, ProviderRegistration } from '@camstack/types';
|
|
2
|
+
|
|
3
|
+
declare class PlatformScorer {
|
|
4
|
+
private cached;
|
|
5
|
+
private readonly logger;
|
|
6
|
+
constructor(logger?: IScopedLogger);
|
|
7
|
+
/**
|
|
8
|
+
* Probe hardware + runtimes and score all backend combos.
|
|
9
|
+
*
|
|
10
|
+
* An optional `onPhase` callback is invoked at each step so consumers
|
|
11
|
+
* (e.g. the platform-probe addon) can emit live progress events on
|
|
12
|
+
* the event bus. The callback takes a phase id + a typed payload; all
|
|
13
|
+
* phases fire in strict order: `started` → `hardware` → `node-backends`
|
|
14
|
+
* → `python-backends` → `scored` → `done`. On failure, `error` fires
|
|
15
|
+
* once with the exception. Cached after first call.
|
|
16
|
+
*/
|
|
17
|
+
probe(onPhase?: (phase: string, payload?: Record<string, unknown>) => void): Promise<PlatformCapabilities>;
|
|
18
|
+
probeHardware(): Promise<HardwareInfo>;
|
|
19
|
+
private probeNodeBackends;
|
|
20
|
+
private probePythonBackends;
|
|
21
|
+
private scoreBackends;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
declare class InferenceConfigResolver {
|
|
25
|
+
private readonly scores;
|
|
26
|
+
private readonly hardware;
|
|
27
|
+
constructor(scores: readonly PlatformScore[], hardware: HardwareInfo);
|
|
28
|
+
/**
|
|
29
|
+
* Compute accuracy/backend weights based on available system RAM.
|
|
30
|
+
* availableRAM_MB is now sourced from systeminformation (reliable cross-platform),
|
|
31
|
+
* not os.freemem() which is broken on macOS.
|
|
32
|
+
*
|
|
33
|
+
* - > 16 GB available: prefer larger, more accurate models (accuracy 0.6, backend 0.4)
|
|
34
|
+
* - > 8 GB available: balanced (accuracy 0.5, backend 0.5)
|
|
35
|
+
* - <= 8 GB available: prefer speed (accuracy 0.4, backend 0.6)
|
|
36
|
+
*/
|
|
37
|
+
private getWeights;
|
|
38
|
+
/**
|
|
39
|
+
* Given an addon's model requirements, pick the best model + runtime + backend.
|
|
40
|
+
*
|
|
41
|
+
* Algorithm:
|
|
42
|
+
* 1. Filter models by available RAM (minRAM_MB < 25% of available RAM)
|
|
43
|
+
* 2. For each remaining model, find the best platform score whose format
|
|
44
|
+
* is available in the model's formats
|
|
45
|
+
* 3. Pick the model with the highest combined score using RAM-adaptive weights:
|
|
46
|
+
* - High RAM (>16 GB): accuracy × 0.6 + backend × 0.4 (prefer accuracy)
|
|
47
|
+
* - Mid RAM (>8 GB): accuracy × 0.5 + backend × 0.5 (balanced)
|
|
48
|
+
* - Low RAM (<=8 GB): accuracy × 0.4 + backend × 0.6 (prefer speed)
|
|
49
|
+
*/
|
|
50
|
+
resolve(requirements: readonly ModelRequirement[]): ResolvedInferenceConfig;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @camstack/addon-platform-probe-native
|
|
55
|
+
*
|
|
56
|
+
* Infra addon — probes hardware + inference runtimes on the local node and
|
|
57
|
+
* exposes the result via the `platform-probe` capability (singleton per
|
|
58
|
+
* node). Every node in the cluster (hub + agents) installs this package as
|
|
59
|
+
* part of `AddonInstaller.REQUIRED_PACKAGES` / `AGENT_PACKAGES`, so
|
|
60
|
+
* inference addons always find a local provider through
|
|
61
|
+
* `ctx.api.platformProbe.*`.
|
|
62
|
+
*
|
|
63
|
+
* The heavy lifting (hardware detection, backend scoring, RAM-adaptive
|
|
64
|
+
* config resolution) used to live in `@camstack/core/platform/*`. It now
|
|
65
|
+
* lives here so the kernel and core stay free of domain-specific
|
|
66
|
+
* inference logic.
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
declare class PlatformProbeNativeAddon extends BaseAddon {
|
|
70
|
+
private scorer;
|
|
71
|
+
private cachedCaps;
|
|
72
|
+
constructor();
|
|
73
|
+
protected onInitialize(): Promise<ProviderRegistration[]>;
|
|
74
|
+
protected onShutdown(): Promise<void>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export { InferenceConfigResolver, PlatformProbeNativeAddon, PlatformScorer, PlatformProbeNativeAddon as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { IScopedLogger, PlatformCapabilities, HardwareInfo, PlatformScore, ModelRequirement, ResolvedInferenceConfig, BaseAddon, ProviderRegistration } from '@camstack/types';
|
|
2
|
+
|
|
3
|
+
declare class PlatformScorer {
|
|
4
|
+
private cached;
|
|
5
|
+
private readonly logger;
|
|
6
|
+
constructor(logger?: IScopedLogger);
|
|
7
|
+
/**
|
|
8
|
+
* Probe hardware + runtimes and score all backend combos.
|
|
9
|
+
*
|
|
10
|
+
* An optional `onPhase` callback is invoked at each step so consumers
|
|
11
|
+
* (e.g. the platform-probe addon) can emit live progress events on
|
|
12
|
+
* the event bus. The callback takes a phase id + a typed payload; all
|
|
13
|
+
* phases fire in strict order: `started` → `hardware` → `node-backends`
|
|
14
|
+
* → `python-backends` → `scored` → `done`. On failure, `error` fires
|
|
15
|
+
* once with the exception. Cached after first call.
|
|
16
|
+
*/
|
|
17
|
+
probe(onPhase?: (phase: string, payload?: Record<string, unknown>) => void): Promise<PlatformCapabilities>;
|
|
18
|
+
probeHardware(): Promise<HardwareInfo>;
|
|
19
|
+
private probeNodeBackends;
|
|
20
|
+
private probePythonBackends;
|
|
21
|
+
private scoreBackends;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
declare class InferenceConfigResolver {
|
|
25
|
+
private readonly scores;
|
|
26
|
+
private readonly hardware;
|
|
27
|
+
constructor(scores: readonly PlatformScore[], hardware: HardwareInfo);
|
|
28
|
+
/**
|
|
29
|
+
* Compute accuracy/backend weights based on available system RAM.
|
|
30
|
+
* availableRAM_MB is now sourced from systeminformation (reliable cross-platform),
|
|
31
|
+
* not os.freemem() which is broken on macOS.
|
|
32
|
+
*
|
|
33
|
+
* - > 16 GB available: prefer larger, more accurate models (accuracy 0.6, backend 0.4)
|
|
34
|
+
* - > 8 GB available: balanced (accuracy 0.5, backend 0.5)
|
|
35
|
+
* - <= 8 GB available: prefer speed (accuracy 0.4, backend 0.6)
|
|
36
|
+
*/
|
|
37
|
+
private getWeights;
|
|
38
|
+
/**
|
|
39
|
+
* Given an addon's model requirements, pick the best model + runtime + backend.
|
|
40
|
+
*
|
|
41
|
+
* Algorithm:
|
|
42
|
+
* 1. Filter models by available RAM (minRAM_MB < 25% of available RAM)
|
|
43
|
+
* 2. For each remaining model, find the best platform score whose format
|
|
44
|
+
* is available in the model's formats
|
|
45
|
+
* 3. Pick the model with the highest combined score using RAM-adaptive weights:
|
|
46
|
+
* - High RAM (>16 GB): accuracy × 0.6 + backend × 0.4 (prefer accuracy)
|
|
47
|
+
* - Mid RAM (>8 GB): accuracy × 0.5 + backend × 0.5 (balanced)
|
|
48
|
+
* - Low RAM (<=8 GB): accuracy × 0.4 + backend × 0.6 (prefer speed)
|
|
49
|
+
*/
|
|
50
|
+
resolve(requirements: readonly ModelRequirement[]): ResolvedInferenceConfig;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @camstack/addon-platform-probe-native
|
|
55
|
+
*
|
|
56
|
+
* Infra addon — probes hardware + inference runtimes on the local node and
|
|
57
|
+
* exposes the result via the `platform-probe` capability (singleton per
|
|
58
|
+
* node). Every node in the cluster (hub + agents) installs this package as
|
|
59
|
+
* part of `AddonInstaller.REQUIRED_PACKAGES` / `AGENT_PACKAGES`, so
|
|
60
|
+
* inference addons always find a local provider through
|
|
61
|
+
* `ctx.api.platformProbe.*`.
|
|
62
|
+
*
|
|
63
|
+
* The heavy lifting (hardware detection, backend scoring, RAM-adaptive
|
|
64
|
+
* config resolution) used to live in `@camstack/core/platform/*`. It now
|
|
65
|
+
* lives here so the kernel and core stay free of domain-specific
|
|
66
|
+
* inference logic.
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
declare class PlatformProbeNativeAddon extends BaseAddon {
|
|
70
|
+
private scorer;
|
|
71
|
+
private cachedCaps;
|
|
72
|
+
constructor();
|
|
73
|
+
protected onInitialize(): Promise<ProviderRegistration[]>;
|
|
74
|
+
protected onShutdown(): Promise<void>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export { InferenceConfigResolver, PlatformProbeNativeAddon, PlatformScorer, PlatformProbeNativeAddon as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
InferenceConfigResolver: () => InferenceConfigResolver,
|
|
34
|
+
PlatformProbeNativeAddon: () => PlatformProbeNativeAddon,
|
|
35
|
+
PlatformScorer: () => PlatformScorer,
|
|
36
|
+
default: () => src_default
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(src_exports);
|
|
39
|
+
var import_types2 = require("@camstack/types");
|
|
40
|
+
|
|
41
|
+
// src/platform-scorer.ts
|
|
42
|
+
var os = __toESM(require("os"));
|
|
43
|
+
var import_node_child_process = require("child_process");
|
|
44
|
+
var import_node_util = require("util");
|
|
45
|
+
var import_types = require("@camstack/types");
|
|
46
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
47
|
+
var noopLogger = {
|
|
48
|
+
debug() {
|
|
49
|
+
},
|
|
50
|
+
info() {
|
|
51
|
+
},
|
|
52
|
+
warn() {
|
|
53
|
+
},
|
|
54
|
+
error() {
|
|
55
|
+
},
|
|
56
|
+
child() {
|
|
57
|
+
return noopLogger;
|
|
58
|
+
},
|
|
59
|
+
withTags() {
|
|
60
|
+
return noopLogger;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
async function getAvailableRAM_MB() {
|
|
64
|
+
try {
|
|
65
|
+
const si = await import("systeminformation");
|
|
66
|
+
const mem = await si.mem();
|
|
67
|
+
return Math.round(mem.available / 1024 / 1024);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.debug(`systeminformation not available, using platform-specific RAM probe: ${(0, import_types.errMsg)(err)}`);
|
|
70
|
+
const platform2 = os.platform();
|
|
71
|
+
try {
|
|
72
|
+
if (platform2 === "darwin") {
|
|
73
|
+
const { stdout: output } = await execFileAsync("vm_stat", [], { encoding: "utf8", timeout: 3e3 });
|
|
74
|
+
const pageSize = parseInt(output.match(/page size of (\d+)/)?.[1] ?? "16384");
|
|
75
|
+
const free = parseInt(output.match(/Pages free:\s+(\d+)/)?.[1] ?? "0");
|
|
76
|
+
const inactive = parseInt(output.match(/Pages inactive:\s+(\d+)/)?.[1] ?? "0");
|
|
77
|
+
const purgeable = parseInt(output.match(/Pages purgeable:\s+(\d+)/)?.[1] ?? "0");
|
|
78
|
+
return Math.round((free + inactive + purgeable) * pageSize / 1024 / 1024);
|
|
79
|
+
} else if (platform2 === "linux") {
|
|
80
|
+
const { readFileSync } = await import("fs");
|
|
81
|
+
const meminfo = readFileSync("/proc/meminfo", "utf8");
|
|
82
|
+
const match = meminfo.match(/MemAvailable:\s+(\d+)\s+kB/);
|
|
83
|
+
if (match) return Math.round(parseInt(match[1]) / 1024);
|
|
84
|
+
}
|
|
85
|
+
} catch (err2) {
|
|
86
|
+
console.debug(`RAM probe failed, using total RAM fallback: ${(0, import_types.errMsg)(err2)}`);
|
|
87
|
+
}
|
|
88
|
+
return Math.round(os.totalmem() / 1024 / 1024);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
var PlatformScorer = class {
|
|
92
|
+
cached = null;
|
|
93
|
+
logger;
|
|
94
|
+
constructor(logger = noopLogger) {
|
|
95
|
+
this.logger = logger;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Probe hardware + runtimes and score all backend combos.
|
|
99
|
+
*
|
|
100
|
+
* An optional `onPhase` callback is invoked at each step so consumers
|
|
101
|
+
* (e.g. the platform-probe addon) can emit live progress events on
|
|
102
|
+
* the event bus. The callback takes a phase id + a typed payload; all
|
|
103
|
+
* phases fire in strict order: `started` → `hardware` → `node-backends`
|
|
104
|
+
* → `python-backends` → `scored` → `done`. On failure, `error` fires
|
|
105
|
+
* once with the exception. Cached after first call.
|
|
106
|
+
*/
|
|
107
|
+
async probe(onPhase) {
|
|
108
|
+
if (this.cached) return this.cached;
|
|
109
|
+
const start = Date.now();
|
|
110
|
+
onPhase?.("started", {});
|
|
111
|
+
this.logger.info("Probing hardware...");
|
|
112
|
+
const hardware = await this.probeHardware();
|
|
113
|
+
this.logger.info("Hardware detected", {
|
|
114
|
+
meta: {
|
|
115
|
+
platform: hardware.platform,
|
|
116
|
+
arch: hardware.arch,
|
|
117
|
+
cpuModel: hardware.cpuModel,
|
|
118
|
+
cpuCores: hardware.cpuCores,
|
|
119
|
+
ramGB: Math.round(hardware.totalRAM_MB / 1024)
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
if (hardware.gpu) this.logger.info("GPU detected", { meta: { name: hardware.gpu.name } });
|
|
123
|
+
if (hardware.npu) this.logger.info("NPU detected", { meta: { type: hardware.npu.type } });
|
|
124
|
+
onPhase?.("hardware", { hardware });
|
|
125
|
+
this.logger.info("Probing Node.js backends...");
|
|
126
|
+
const nodeBackends = await this.probeNodeBackends();
|
|
127
|
+
this.logger.info("Node backends detected", { meta: { backends: nodeBackends.map((b) => b.id) } });
|
|
128
|
+
onPhase?.("node-backends", { backends: nodeBackends });
|
|
129
|
+
this.logger.info("Probing Python backends...");
|
|
130
|
+
const pythonInfo = await this.probePythonBackends();
|
|
131
|
+
if (pythonInfo.pythonPath) {
|
|
132
|
+
this.logger.info("Python backends detected", {
|
|
133
|
+
meta: {
|
|
134
|
+
pythonPath: pythonInfo.pythonPath,
|
|
135
|
+
backends: pythonInfo.backends.filter((b) => b.available).map((b) => b.id)
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
} else {
|
|
139
|
+
this.logger.info("Python: not found");
|
|
140
|
+
}
|
|
141
|
+
onPhase?.("python-backends", { pythonPath: pythonInfo.pythonPath, backends: pythonInfo.backends });
|
|
142
|
+
const scores = this.scoreBackends(hardware, nodeBackends, pythonInfo.backends);
|
|
143
|
+
const bestScore = scores.find((s) => s.available) ?? scores[scores.length - 1];
|
|
144
|
+
const elapsed = Date.now() - start;
|
|
145
|
+
this.logger.info("Scoring complete", { meta: { elapsedMs: elapsed, combos: scores.length } });
|
|
146
|
+
this.logger.info("Best backend selected", {
|
|
147
|
+
meta: {
|
|
148
|
+
runtime: bestScore.runtime,
|
|
149
|
+
backend: bestScore.backend,
|
|
150
|
+
format: bestScore.format,
|
|
151
|
+
reason: bestScore.reason,
|
|
152
|
+
score: bestScore.score
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
for (const s of scores) {
|
|
156
|
+
this.logger.debug("Score entry", {
|
|
157
|
+
meta: {
|
|
158
|
+
available: s.available,
|
|
159
|
+
runtime: s.runtime,
|
|
160
|
+
backend: s.backend,
|
|
161
|
+
format: s.format,
|
|
162
|
+
score: s.score,
|
|
163
|
+
reason: s.reason
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
this.cached = {
|
|
168
|
+
hardware,
|
|
169
|
+
scores,
|
|
170
|
+
bestScore,
|
|
171
|
+
pythonPath: pythonInfo.pythonPath
|
|
172
|
+
};
|
|
173
|
+
onPhase?.("scored", { scores, bestScore, elapsedMs: elapsed });
|
|
174
|
+
onPhase?.("done", { capabilities: this.cached });
|
|
175
|
+
return this.cached;
|
|
176
|
+
}
|
|
177
|
+
async probeHardware() {
|
|
178
|
+
const platform2 = os.platform();
|
|
179
|
+
const arch2 = os.arch();
|
|
180
|
+
const cpus2 = os.cpus();
|
|
181
|
+
const cpuModel = cpus2[0]?.model ?? "unknown";
|
|
182
|
+
const cpuCores = cpus2.length;
|
|
183
|
+
const totalRAM_MB = Math.round(os.totalmem() / 1024 / 1024);
|
|
184
|
+
const availableRAM_MB = await getAvailableRAM_MB();
|
|
185
|
+
this.logger.debug("RAM probed", { meta: { totalRAM_MB, availableRAM_MB } });
|
|
186
|
+
let gpu = null;
|
|
187
|
+
let npu = null;
|
|
188
|
+
if (platform2 === "darwin" && arch2 === "arm64") {
|
|
189
|
+
gpu = { type: "apple", name: "Apple Silicon GPU" };
|
|
190
|
+
npu = { type: "apple-ane" };
|
|
191
|
+
}
|
|
192
|
+
if (platform2 === "linux") {
|
|
193
|
+
try {
|
|
194
|
+
const { stdout } = await execFileAsync("nvidia-smi", ["--query-gpu=name,memory.total", "--format=csv,noheader"], {
|
|
195
|
+
encoding: "utf8",
|
|
196
|
+
timeout: 5e3
|
|
197
|
+
});
|
|
198
|
+
const output = stdout.trim();
|
|
199
|
+
if (output) {
|
|
200
|
+
const [name, mem] = output.split(",").map((s) => s.trim());
|
|
201
|
+
gpu = {
|
|
202
|
+
type: "nvidia",
|
|
203
|
+
name: name ?? "NVIDIA GPU",
|
|
204
|
+
memoryMB: parseInt(mem ?? "0")
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
} catch (err) {
|
|
208
|
+
this.logger.debug("NVIDIA GPU detection failed", { meta: { error: (0, import_types.errMsg)(err) } });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return { platform: platform2, arch: arch2, cpuModel, cpuCores, totalRAM_MB, availableRAM_MB, gpu, npu };
|
|
212
|
+
}
|
|
213
|
+
async probeNodeBackends() {
|
|
214
|
+
const backends = [
|
|
215
|
+
{ id: "cpu", available: true }
|
|
216
|
+
];
|
|
217
|
+
try {
|
|
218
|
+
const ort = await import("onnxruntime-node");
|
|
219
|
+
const providers = ort.InferenceSession?.getAvailableProviders?.() ?? [];
|
|
220
|
+
for (const p of providers) {
|
|
221
|
+
const n = p.toLowerCase().replace("executionprovider", "");
|
|
222
|
+
if (n === "coreml") backends.push({ id: "coreml", available: true });
|
|
223
|
+
else if (n === "cuda") backends.push({ id: "cuda", available: true });
|
|
224
|
+
else if (n === "tensorrt") backends.push({ id: "tensorrt", available: true });
|
|
225
|
+
}
|
|
226
|
+
} catch (err) {
|
|
227
|
+
this.logger.debug("onnxruntime-node not available", { meta: { error: (0, import_types.errMsg)(err) } });
|
|
228
|
+
}
|
|
229
|
+
if (os.platform() === "darwin" && !backends.some((b) => b.id === "coreml")) {
|
|
230
|
+
backends.push({ id: "coreml", available: true });
|
|
231
|
+
}
|
|
232
|
+
return backends;
|
|
233
|
+
}
|
|
234
|
+
async probePythonBackends() {
|
|
235
|
+
let pythonPath = null;
|
|
236
|
+
for (const cmd of ["python3", "python"]) {
|
|
237
|
+
try {
|
|
238
|
+
await execFileAsync(cmd, ["--version"], { timeout: 5e3 });
|
|
239
|
+
pythonPath = cmd;
|
|
240
|
+
break;
|
|
241
|
+
} catch (err) {
|
|
242
|
+
console.debug(`Python command "${cmd}" not found: ${(0, import_types.errMsg)(err)}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (!pythonPath) return { pythonPath: null, backends: [] };
|
|
246
|
+
const checks = [
|
|
247
|
+
// [pythonModule, backendId, modelFormat]
|
|
248
|
+
["coremltools", "coreml", "coreml"],
|
|
249
|
+
["openvino.runtime", "openvino", "openvino"],
|
|
250
|
+
["torch", "pytorch", "onnx"],
|
|
251
|
+
["onnxruntime", "onnx-py", "onnx"]
|
|
252
|
+
];
|
|
253
|
+
const results = await Promise.all(checks.map(async ([mod, id, format]) => {
|
|
254
|
+
const probeStart = Date.now();
|
|
255
|
+
let available = false;
|
|
256
|
+
try {
|
|
257
|
+
await execFileAsync(pythonPath, ["-c", `import ${mod}`], { timeout: 3e4 });
|
|
258
|
+
available = true;
|
|
259
|
+
} catch (err) {
|
|
260
|
+
console.debug(`Python module "${mod}" not installed: ${(0, import_types.errMsg)(err)}`);
|
|
261
|
+
}
|
|
262
|
+
const probeMs = Date.now() - probeStart;
|
|
263
|
+
this.logger.debug("Python module probed", { meta: { module: mod, available, probeMs } });
|
|
264
|
+
return { mod, id, format, available };
|
|
265
|
+
}));
|
|
266
|
+
const backends = [];
|
|
267
|
+
for (const r of results) {
|
|
268
|
+
if (r.id === "coreml" && os.platform() === "darwin") {
|
|
269
|
+
backends.push({ id: r.id, format: r.format, available: r.available });
|
|
270
|
+
} else if (r.available) {
|
|
271
|
+
backends.push({ id: r.id, format: r.format, available: r.available });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return { pythonPath, backends };
|
|
275
|
+
}
|
|
276
|
+
scoreBackends(hardware, nodeBackends, pythonBackends) {
|
|
277
|
+
const scores = [];
|
|
278
|
+
if (hardware.platform === "darwin" && hardware.arch === "arm64") {
|
|
279
|
+
const pyCoreMl = pythonBackends.find((b) => b.id === "coreml");
|
|
280
|
+
if (pyCoreMl) {
|
|
281
|
+
scores.push({ runtime: "python", backend: "coreml", format: "coreml", score: 95, reason: "Apple Neural Engine (Python CoreML)", available: pyCoreMl.available });
|
|
282
|
+
}
|
|
283
|
+
const nodeCoreMl = nodeBackends.find((b) => b.id === "coreml");
|
|
284
|
+
if (nodeCoreMl) {
|
|
285
|
+
scores.push({ runtime: "node", backend: "coreml", format: "onnx", score: 90, reason: "CoreML via ONNX Runtime", available: nodeCoreMl.available });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (hardware.gpu?.type === "nvidia") {
|
|
289
|
+
const tensorrt = nodeBackends.find((b) => b.id === "tensorrt");
|
|
290
|
+
if (tensorrt) scores.push({ runtime: "node", backend: "tensorrt", format: "onnx", score: 95, reason: "NVIDIA TensorRT", available: true });
|
|
291
|
+
const cuda = nodeBackends.find((b) => b.id === "cuda");
|
|
292
|
+
if (cuda) scores.push({ runtime: "node", backend: "cuda", format: "onnx", score: 85, reason: "NVIDIA CUDA", available: true });
|
|
293
|
+
}
|
|
294
|
+
const openvino = pythonBackends.find((b) => b.id === "openvino");
|
|
295
|
+
if (openvino) {
|
|
296
|
+
const score = hardware.npu?.type === "intel-npu" ? 90 : 80;
|
|
297
|
+
scores.push({ runtime: "python", backend: "openvino", format: "openvino", score, reason: "Intel OpenVINO", available: openvino.available });
|
|
298
|
+
}
|
|
299
|
+
scores.push({ runtime: "node", backend: "cpu", format: "onnx", score: 50, reason: "CPU (ONNX Runtime Node)", available: true });
|
|
300
|
+
const pyOnnx = pythonBackends.find((b) => b.id === "onnx-py");
|
|
301
|
+
if (pyOnnx) {
|
|
302
|
+
scores.push({ runtime: "python", backend: "cpu", format: "onnx", score: 45, reason: "CPU (Python ONNX)", available: pyOnnx.available });
|
|
303
|
+
}
|
|
304
|
+
return scores.sort((a, b) => b.score - a.score);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// src/inference-config-resolver.ts
|
|
309
|
+
var InferenceConfigResolver = class {
|
|
310
|
+
constructor(scores, hardware) {
|
|
311
|
+
this.scores = scores;
|
|
312
|
+
this.hardware = hardware;
|
|
313
|
+
}
|
|
314
|
+
scores;
|
|
315
|
+
hardware;
|
|
316
|
+
/**
|
|
317
|
+
* Compute accuracy/backend weights based on available system RAM.
|
|
318
|
+
* availableRAM_MB is now sourced from systeminformation (reliable cross-platform),
|
|
319
|
+
* not os.freemem() which is broken on macOS.
|
|
320
|
+
*
|
|
321
|
+
* - > 16 GB available: prefer larger, more accurate models (accuracy 0.6, backend 0.4)
|
|
322
|
+
* - > 8 GB available: balanced (accuracy 0.5, backend 0.5)
|
|
323
|
+
* - <= 8 GB available: prefer speed (accuracy 0.4, backend 0.6)
|
|
324
|
+
*/
|
|
325
|
+
getWeights() {
|
|
326
|
+
const ramMB = this.hardware.availableRAM_MB;
|
|
327
|
+
if (ramMB > 16384) return { accuracyWeight: 0.6, backendWeight: 0.4 };
|
|
328
|
+
if (ramMB > 8192) return { accuracyWeight: 0.5, backendWeight: 0.5 };
|
|
329
|
+
return { accuracyWeight: 0.4, backendWeight: 0.6 };
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Given an addon's model requirements, pick the best model + runtime + backend.
|
|
333
|
+
*
|
|
334
|
+
* Algorithm:
|
|
335
|
+
* 1. Filter models by available RAM (minRAM_MB < 25% of available RAM)
|
|
336
|
+
* 2. For each remaining model, find the best platform score whose format
|
|
337
|
+
* is available in the model's formats
|
|
338
|
+
* 3. Pick the model with the highest combined score using RAM-adaptive weights:
|
|
339
|
+
* - High RAM (>16 GB): accuracy × 0.6 + backend × 0.4 (prefer accuracy)
|
|
340
|
+
* - Mid RAM (>8 GB): accuracy × 0.5 + backend × 0.5 (balanced)
|
|
341
|
+
* - Low RAM (<=8 GB): accuracy × 0.4 + backend × 0.6 (prefer speed)
|
|
342
|
+
*/
|
|
343
|
+
resolve(requirements) {
|
|
344
|
+
if (requirements.length === 0) {
|
|
345
|
+
return { modelId: "", runtime: "node", backend: "cpu", format: "onnx", reason: "No models declared" };
|
|
346
|
+
}
|
|
347
|
+
const ramBudget = this.hardware.availableRAM_MB * 0.25;
|
|
348
|
+
const { accuracyWeight, backendWeight } = this.getWeights();
|
|
349
|
+
console.log(`[InferenceConfigResolver] availableRAM: ${this.hardware.availableRAM_MB}MB, budget: ${Math.round(ramBudget)}MB, weights: accuracy=${accuracyWeight}, backend=${backendWeight}`);
|
|
350
|
+
const fits = requirements.filter((m) => m.minRAM_MB < ramBudget);
|
|
351
|
+
const candidates = fits.length > 0 ? fits : [requirements[0]];
|
|
352
|
+
console.log(`[InferenceConfigResolver] ${candidates.length}/${requirements.length} models fit RAM budget`);
|
|
353
|
+
let bestCombo = null;
|
|
354
|
+
for (const model of candidates) {
|
|
355
|
+
for (const score of this.scores) {
|
|
356
|
+
if (!score.available) continue;
|
|
357
|
+
if (!model.formats.includes(score.format)) continue;
|
|
358
|
+
const combined = model.accuracyScore * accuracyWeight + score.score * backendWeight;
|
|
359
|
+
if (!bestCombo || combined > bestCombo.combined) {
|
|
360
|
+
console.log(`[InferenceConfigResolver] New best: ${model.modelId} (accuracy=${model.accuracyScore}) + ${score.backend}/${score.format} (score=${score.score}) \u2192 combined=${Math.round(combined)}`);
|
|
361
|
+
bestCombo = { model, score, combined };
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (!bestCombo) {
|
|
366
|
+
return {
|
|
367
|
+
modelId: candidates[0].modelId,
|
|
368
|
+
runtime: "node",
|
|
369
|
+
backend: "cpu",
|
|
370
|
+
format: "onnx",
|
|
371
|
+
reason: "No compatible backend \u2014 CPU fallback"
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
modelId: bestCombo.model.modelId,
|
|
376
|
+
runtime: bestCombo.score.runtime,
|
|
377
|
+
backend: bestCombo.score.backend,
|
|
378
|
+
format: bestCombo.score.format,
|
|
379
|
+
reason: `${bestCombo.model.name} on ${bestCombo.score.reason} (score: ${Math.round(bestCombo.combined)})`
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
// src/index.ts
|
|
385
|
+
var PlatformProbeNativeAddon = class extends import_types2.BaseAddon {
|
|
386
|
+
scorer = null;
|
|
387
|
+
cachedCaps = null;
|
|
388
|
+
constructor() {
|
|
389
|
+
super({});
|
|
390
|
+
}
|
|
391
|
+
async onInitialize() {
|
|
392
|
+
this.scorer = new PlatformScorer(this.ctx.logger);
|
|
393
|
+
const emitPhase = (phase, payload) => {
|
|
394
|
+
this.ctx.eventBus?.emit({
|
|
395
|
+
id: `platform-probe-${phase}-${Date.now()}`,
|
|
396
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
397
|
+
source: { type: "core", id: "platform-probe-native" },
|
|
398
|
+
category: import_types2.EventCategory.PlatformProbePhase,
|
|
399
|
+
data: { type: "platform-probe.phase", phase, ...payload ?? {} }
|
|
400
|
+
});
|
|
401
|
+
};
|
|
402
|
+
const probePromise = this.scorer.probe(emitPhase).then((caps) => {
|
|
403
|
+
this.cachedCaps = caps;
|
|
404
|
+
this.ctx.logger.info("Platform probe complete", {
|
|
405
|
+
meta: {
|
|
406
|
+
cpuModel: caps.hardware.cpuModel,
|
|
407
|
+
arch: caps.hardware.arch,
|
|
408
|
+
totalRAM_MB: caps.hardware.totalRAM_MB,
|
|
409
|
+
gpu: caps.hardware.gpu?.name ?? "none",
|
|
410
|
+
bestReason: caps.bestScore.reason,
|
|
411
|
+
bestScore: caps.bestScore.score
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
return caps;
|
|
415
|
+
}).catch((err) => {
|
|
416
|
+
const msg = (0, import_types2.errMsg)(err);
|
|
417
|
+
this.ctx.logger.error("Platform probe failed", { meta: { error: msg } });
|
|
418
|
+
emitPhase("error", { message: msg });
|
|
419
|
+
throw err;
|
|
420
|
+
});
|
|
421
|
+
const getCaps = async () => {
|
|
422
|
+
return this.cachedCaps ?? probePromise;
|
|
423
|
+
};
|
|
424
|
+
const provider = {
|
|
425
|
+
getCapabilities: async () => {
|
|
426
|
+
return getCaps();
|
|
427
|
+
},
|
|
428
|
+
getHardware: async () => {
|
|
429
|
+
const caps = await getCaps();
|
|
430
|
+
return caps.hardware;
|
|
431
|
+
},
|
|
432
|
+
resolveInferenceConfig: async (input) => {
|
|
433
|
+
const caps = await getCaps();
|
|
434
|
+
const resolver = new InferenceConfigResolver(caps.scores, caps.hardware);
|
|
435
|
+
return resolver.resolve(input.requirements);
|
|
436
|
+
},
|
|
437
|
+
resolveHwAccel: async (input) => {
|
|
438
|
+
const hwaccel = this.ctx.kernel.hwaccel;
|
|
439
|
+
if (!hwaccel) {
|
|
440
|
+
return { preferred: [] };
|
|
441
|
+
}
|
|
442
|
+
const res = await hwaccel.resolve(input.prefer ?? null);
|
|
443
|
+
return { preferred: res.preferred };
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
return [{ capability: import_types2.platformProbeCapability, provider }];
|
|
447
|
+
}
|
|
448
|
+
async onShutdown() {
|
|
449
|
+
this.scorer = null;
|
|
450
|
+
this.cachedCaps = null;
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
var src_default = PlatformProbeNativeAddon;
|
|
454
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
455
|
+
0 && (module.exports = {
|
|
456
|
+
InferenceConfigResolver,
|
|
457
|
+
PlatformProbeNativeAddon,
|
|
458
|
+
PlatformScorer
|
|
459
|
+
});
|
|
460
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/platform-scorer.ts","../src/inference-config-resolver.ts"],"sourcesContent":["/**\n * @camstack/addon-platform-probe-native\n *\n * Infra addon — probes hardware + inference runtimes on the local node and\n * exposes the result via the `platform-probe` capability (singleton per\n * node). Every node in the cluster (hub + agents) installs this package as\n * part of `AddonInstaller.REQUIRED_PACKAGES` / `AGENT_PACKAGES`, so\n * inference addons always find a local provider through\n * `ctx.api.platformProbe.*`.\n *\n * The heavy lifting (hardware detection, backend scoring, RAM-adaptive\n * config resolution) used to live in `@camstack/core/platform/*`. It now\n * lives here so the kernel and core stay free of domain-specific\n * inference logic.\n */\nimport type {\n ProviderRegistration,\n IPlatformProbeProvider,\n PlatformCapabilities,\n HardwareInfo,\n ModelRequirement,\n ResolvedInferenceConfig,\n HwAccelBackend,\n} from '@camstack/types'\nimport { BaseAddon, platformProbeCapability, errMsg, EventCategory } from '@camstack/types'\nimport { PlatformScorer } from './platform-scorer.js'\nimport { InferenceConfigResolver } from './inference-config-resolver.js'\n\nexport { PlatformScorer } from './platform-scorer.js'\nexport { InferenceConfigResolver } from './inference-config-resolver.js'\n\nexport class PlatformProbeNativeAddon extends BaseAddon {\n private scorer: PlatformScorer | null = null\n private cachedCaps: PlatformCapabilities | null = null\n\n constructor() { super({}) }\n\n protected async onInitialize(): Promise<ProviderRegistration[]> {\n this.scorer = new PlatformScorer(this.ctx.logger)\n\n // Kick off the probe in the background — provider methods await the\n // promise so early consumers block until capabilities are ready.\n // Each phase emits a `platform-probe.phase` event on the bus so the\n // benchmark UI (and any other consumer) can render live progress via\n // `live.onEvent({ category: EventCategory.PlatformProbePhase })`.\n const emitPhase = (phase: string, payload?: Record<string, unknown>): void => {\n this.ctx.eventBus?.emit({\n id: `platform-probe-${phase}-${Date.now()}`,\n timestamp: new Date(),\n source: { type: 'core', id: 'platform-probe-native' },\n category: EventCategory.PlatformProbePhase,\n data: { type: 'platform-probe.phase', phase, ...(payload ?? {}) },\n })\n }\n\n const probePromise: Promise<PlatformCapabilities> = this.scorer\n .probe(emitPhase)\n .then((caps) => {\n this.cachedCaps = caps\n this.ctx.logger.info('Platform probe complete', {\n meta: {\n cpuModel: caps.hardware.cpuModel,\n arch: caps.hardware.arch,\n totalRAM_MB: caps.hardware.totalRAM_MB,\n gpu: caps.hardware.gpu?.name ?? 'none',\n bestReason: caps.bestScore.reason,\n bestScore: caps.bestScore.score,\n },\n })\n return caps\n })\n .catch((err: unknown) => {\n const msg = errMsg(err)\n this.ctx.logger.error('Platform probe failed', { meta: { error: msg } })\n emitPhase('error', { message: msg })\n throw err\n })\n\n const getCaps = async (): Promise<PlatformCapabilities> => {\n return this.cachedCaps ?? probePromise\n }\n\n const provider: IPlatformProbeProvider = {\n getCapabilities: async (): Promise<PlatformCapabilities> => {\n return getCaps()\n },\n getHardware: async (): Promise<HardwareInfo> => {\n const caps = await getCaps()\n return caps.hardware\n },\n resolveInferenceConfig: async (input: {\n readonly requirements: readonly ModelRequirement[]\n }): Promise<ResolvedInferenceConfig> => {\n const caps = await getCaps()\n const resolver = new InferenceConfigResolver(caps.scores, caps.hardware)\n return resolver.resolve(input.requirements)\n },\n resolveHwAccel: async (input: {\n readonly prefer?: string | null\n readonly nodeId?: string\n }): Promise<{ preferred: readonly string[] }> => {\n const hwaccel = this.ctx.kernel.hwaccel\n if (!hwaccel) {\n return { preferred: [] }\n }\n const res = await hwaccel.resolve((input.prefer as HwAccelBackend | 'none' | null) ?? null)\n return { preferred: res.preferred }\n },\n }\n\n return [{ capability: platformProbeCapability, provider }]\n }\n\n protected async onShutdown(): Promise<void> {\n this.scorer = null\n this.cachedCaps = null\n }\n}\n\nexport default PlatformProbeNativeAddon\n","import * as os from 'node:os'\nimport { execFile } from 'node:child_process'\nimport { promisify } from 'node:util'\nimport { errMsg } from '@camstack/types'\nimport type { HardwareInfo, GpuInfo, NpuInfo, PlatformScore, PlatformCapabilities, IScopedLogger } from '@camstack/types'\n\n/**\n * Promisified `execFile`. Used across the scorer so every subprocess\n * spawn yields the event loop — critical at boot because a single sync\n * spawn of a missing Python module hits its 30s timeout and freezes the\n * entire Node process (WS handshakes, tRPC subscriptions, metrics — all\n * stalled). Keeping it async turns the probe into a true background task.\n */\nconst execFileAsync = promisify(execFile)\n\n/** Minimal no-op logger for default parameter */\nconst noopLogger: IScopedLogger = {\n debug() {},\n info() {},\n warn() {},\n error() {},\n child() { return noopLogger },\n withTags() { return noopLogger },\n}\n\n/**\n * Get reliable \"available\" RAM in MB, cross-platform.\n * os.freemem() is unreliable on macOS (reports ~200MB on a 36GB system).\n * Uses systeminformation when available, falls back to native commands.\n */\nasync function getAvailableRAM_MB(): Promise<number> {\n try {\n const si = await import('systeminformation')\n const mem = await si.mem()\n return Math.round(mem.available / 1024 / 1024)\n } catch (err) {\n console.debug(`systeminformation not available, using platform-specific RAM probe: ${errMsg(err)}`)\n const platform = os.platform()\n try {\n if (platform === 'darwin') {\n // Parse vm_stat: (free + inactive + purgeable) * pageSize\n const { stdout: output } = await execFileAsync('vm_stat', [], { encoding: 'utf8', timeout: 3000 })\n const pageSize = parseInt(output.match(/page size of (\\d+)/)?.[1] ?? '16384')\n const free = parseInt(output.match(/Pages free:\\s+(\\d+)/)?.[1] ?? '0')\n const inactive = parseInt(output.match(/Pages inactive:\\s+(\\d+)/)?.[1] ?? '0')\n const purgeable = parseInt(output.match(/Pages purgeable:\\s+(\\d+)/)?.[1] ?? '0')\n return Math.round((free + inactive + purgeable) * pageSize / 1024 / 1024)\n } else if (platform === 'linux') {\n // Parse /proc/meminfo: MemAvailable\n const { readFileSync } = await import('node:fs')\n const meminfo = readFileSync('/proc/meminfo', 'utf8')\n const match = meminfo.match(/MemAvailable:\\s+(\\d+)\\s+kB/)\n if (match) return Math.round(parseInt(match[1]!) / 1024)\n }\n } catch (err) { console.debug(`RAM probe failed, using total RAM fallback: ${errMsg(err)}`) }\n // Ultimate fallback: total RAM (conservative but safe)\n return Math.round(os.totalmem() / 1024 / 1024)\n }\n}\n\nexport class PlatformScorer {\n private cached: PlatformCapabilities | null = null\n private readonly logger: IScopedLogger\n\n constructor(logger: IScopedLogger = noopLogger) {\n this.logger = logger\n }\n\n /**\n * Probe hardware + runtimes and score all backend combos.\n *\n * An optional `onPhase` callback is invoked at each step so consumers\n * (e.g. the platform-probe addon) can emit live progress events on\n * the event bus. The callback takes a phase id + a typed payload; all\n * phases fire in strict order: `started` → `hardware` → `node-backends`\n * → `python-backends` → `scored` → `done`. On failure, `error` fires\n * once with the exception. Cached after first call.\n */\n async probe(onPhase?: (phase: string, payload?: Record<string, unknown>) => void): Promise<PlatformCapabilities> {\n if (this.cached) return this.cached\n\n const start = Date.now()\n onPhase?.('started', {})\n this.logger.info('Probing hardware...')\n const hardware = await this.probeHardware()\n this.logger.info('Hardware detected', {\n meta: {\n platform: hardware.platform,\n arch: hardware.arch,\n cpuModel: hardware.cpuModel,\n cpuCores: hardware.cpuCores,\n ramGB: Math.round(hardware.totalRAM_MB / 1024),\n },\n })\n if (hardware.gpu) this.logger.info('GPU detected', { meta: { name: hardware.gpu.name } })\n if (hardware.npu) this.logger.info('NPU detected', { meta: { type: hardware.npu.type } })\n onPhase?.('hardware', { hardware })\n\n this.logger.info('Probing Node.js backends...')\n const nodeBackends = await this.probeNodeBackends()\n this.logger.info('Node backends detected', { meta: { backends: nodeBackends.map(b => b.id) } })\n onPhase?.('node-backends', { backends: nodeBackends })\n\n this.logger.info('Probing Python backends...')\n const pythonInfo = await this.probePythonBackends()\n if (pythonInfo.pythonPath) {\n this.logger.info('Python backends detected', {\n meta: {\n pythonPath: pythonInfo.pythonPath,\n backends: pythonInfo.backends.filter(b => b.available).map(b => b.id),\n },\n })\n } else {\n this.logger.info('Python: not found')\n }\n onPhase?.('python-backends', { pythonPath: pythonInfo.pythonPath, backends: pythonInfo.backends })\n\n const scores = this.scoreBackends(hardware, nodeBackends, pythonInfo.backends)\n const bestScore = scores.find(s => s.available) ?? scores[scores.length - 1]!\n\n const elapsed = Date.now() - start\n this.logger.info('Scoring complete', { meta: { elapsedMs: elapsed, combos: scores.length } })\n this.logger.info('Best backend selected', {\n meta: {\n runtime: bestScore.runtime,\n backend: bestScore.backend,\n format: bestScore.format,\n reason: bestScore.reason,\n score: bestScore.score,\n },\n })\n for (const s of scores) {\n this.logger.debug('Score entry', {\n meta: {\n available: s.available,\n runtime: s.runtime,\n backend: s.backend,\n format: s.format,\n score: s.score,\n reason: s.reason,\n },\n })\n }\n\n this.cached = {\n hardware,\n scores,\n bestScore,\n pythonPath: pythonInfo.pythonPath,\n }\n onPhase?.('scored', { scores, bestScore, elapsedMs: elapsed })\n onPhase?.('done', { capabilities: this.cached })\n return this.cached\n }\n\n async probeHardware(): Promise<HardwareInfo> {\n const platform = os.platform() as HardwareInfo['platform']\n const arch = os.arch() as HardwareInfo['arch']\n const cpus = os.cpus()\n const cpuModel = cpus[0]?.model ?? 'unknown'\n const cpuCores = cpus.length\n const totalRAM_MB = Math.round(os.totalmem() / 1024 / 1024)\n const availableRAM_MB = await getAvailableRAM_MB()\n this.logger.debug('RAM probed', { meta: { totalRAM_MB, availableRAM_MB } })\n\n let gpu: GpuInfo | null = null\n let npu: NpuInfo | null = null\n\n // macOS Apple Silicon\n if (platform === 'darwin' && arch === 'arm64') {\n gpu = { type: 'apple', name: 'Apple Silicon GPU' }\n npu = { type: 'apple-ane' }\n }\n\n // Linux NVIDIA\n if (platform === 'linux') {\n try {\n const { stdout } = await execFileAsync('nvidia-smi', ['--query-gpu=name,memory.total', '--format=csv,noheader'], {\n encoding: 'utf8', timeout: 5000,\n })\n const output = stdout.trim()\n if (output) {\n const [name, mem] = output.split(',').map(s => s.trim())\n gpu = {\n type: 'nvidia',\n name: name ?? 'NVIDIA GPU',\n memoryMB: parseInt(mem ?? '0'),\n }\n }\n } catch (err) { this.logger.debug('NVIDIA GPU detection failed', { meta: { error: errMsg(err) } }) }\n }\n\n return { platform, arch, cpuModel, cpuCores, totalRAM_MB, availableRAM_MB, gpu, npu }\n }\n\n private async probeNodeBackends(): Promise<Array<{ id: string; available: boolean }>> {\n const backends: Array<{ id: string; available: boolean }> = [\n { id: 'cpu', available: true },\n ]\n\n try {\n const ort = await import('onnxruntime-node') as { InferenceSession?: { getAvailableProviders?: () => string[] } }\n const providers: string[] = ort.InferenceSession?.getAvailableProviders?.() ?? []\n for (const p of providers) {\n const n = p.toLowerCase().replace('executionprovider', '')\n if (n === 'coreml') backends.push({ id: 'coreml', available: true })\n else if (n === 'cuda') backends.push({ id: 'cuda', available: true })\n else if (n === 'tensorrt') backends.push({ id: 'tensorrt', available: true })\n }\n } catch (err) { this.logger.debug('onnxruntime-node not available', { meta: { error: errMsg(err) } }) }\n\n // macOS always has CoreML potential\n if (os.platform() === 'darwin' && !backends.some(b => b.id === 'coreml')) {\n backends.push({ id: 'coreml', available: true })\n }\n\n return backends\n }\n\n private async probePythonBackends(): Promise<{ pythonPath: string | null; backends: Array<{ id: string; format: string; available: boolean }> }> {\n let pythonPath: string | null = null\n for (const cmd of ['python3', 'python']) {\n try {\n await execFileAsync(cmd, ['--version'], { timeout: 5000 })\n pythonPath = cmd\n break\n } catch (err) { console.debug(`Python command \"${cmd}\" not found: ${errMsg(err)}`) }\n }\n\n if (!pythonPath) return { pythonPath: null, backends: [] }\n\n const checks: Array<[string, string, string]> = [\n // [pythonModule, backendId, modelFormat]\n ['coremltools', 'coreml', 'coreml'],\n ['openvino.runtime', 'openvino', 'openvino'],\n ['torch', 'pytorch', 'onnx'],\n ['onnxruntime', 'onnx-py', 'onnx'],\n ]\n\n // Run all module probes in parallel. Each probe is independent and a\n // missing module spends 30s waiting on its own timeout — serial would\n // add up to ~50s of cumulative wait. Parallel caps it at the slowest\n // probe. The execFile promise yields the event loop, so WS handshakes\n // and tRPC calls continue to flow during the wait.\n const results = await Promise.all(checks.map(async ([mod, id, format]) => {\n const probeStart = Date.now()\n let available = false\n try {\n await execFileAsync(pythonPath!, ['-c', `import ${mod}`], { timeout: 30000 })\n available = true\n } catch (err) { console.debug(`Python module \"${mod}\" not installed: ${errMsg(err)}`) }\n const probeMs = Date.now() - probeStart\n this.logger.debug('Python module probed', { meta: { module: mod, available, probeMs } })\n return { mod, id, format, available }\n }))\n\n const backends: Array<{ id: string; format: string; available: boolean }> = []\n for (const r of results) {\n // Always show CoreML on macOS even if not installed\n if (r.id === 'coreml' && os.platform() === 'darwin') {\n backends.push({ id: r.id, format: r.format, available: r.available })\n } else if (r.available) {\n backends.push({ id: r.id, format: r.format, available: r.available })\n }\n }\n\n return { pythonPath, backends }\n }\n\n private scoreBackends(\n hardware: HardwareInfo,\n nodeBackends: Array<{ id: string; available: boolean }>,\n pythonBackends: Array<{ id: string; format: string; available: boolean }>,\n ): PlatformScore[] {\n const scores: PlatformScore[] = []\n\n // macOS Apple Silicon\n if (hardware.platform === 'darwin' && hardware.arch === 'arm64') {\n const pyCoreMl = pythonBackends.find(b => b.id === 'coreml')\n if (pyCoreMl) {\n scores.push({ runtime: 'python', backend: 'coreml', format: 'coreml', score: 95, reason: 'Apple Neural Engine (Python CoreML)', available: pyCoreMl.available })\n }\n const nodeCoreMl = nodeBackends.find(b => b.id === 'coreml')\n if (nodeCoreMl) {\n scores.push({ runtime: 'node', backend: 'coreml', format: 'onnx', score: 90, reason: 'CoreML via ONNX Runtime', available: nodeCoreMl.available })\n }\n }\n\n // NVIDIA\n if (hardware.gpu?.type === 'nvidia') {\n const tensorrt = nodeBackends.find(b => b.id === 'tensorrt')\n if (tensorrt) scores.push({ runtime: 'node', backend: 'tensorrt', format: 'onnx', score: 95, reason: 'NVIDIA TensorRT', available: true })\n const cuda = nodeBackends.find(b => b.id === 'cuda')\n if (cuda) scores.push({ runtime: 'node', backend: 'cuda', format: 'onnx', score: 85, reason: 'NVIDIA CUDA', available: true })\n }\n\n // Intel OpenVINO\n const openvino = pythonBackends.find(b => b.id === 'openvino')\n if (openvino) {\n const score = hardware.npu?.type === 'intel-npu' ? 90 : 80\n scores.push({ runtime: 'python', backend: 'openvino', format: 'openvino', score, reason: 'Intel OpenVINO', available: openvino.available })\n }\n\n // CPU fallbacks\n scores.push({ runtime: 'node', backend: 'cpu', format: 'onnx', score: 50, reason: 'CPU (ONNX Runtime Node)', available: true })\n const pyOnnx = pythonBackends.find(b => b.id === 'onnx-py')\n if (pyOnnx) {\n scores.push({ runtime: 'python', backend: 'cpu', format: 'onnx', score: 45, reason: 'CPU (Python ONNX)', available: pyOnnx.available })\n }\n\n return scores.sort((a, b) => b.score - a.score)\n }\n}\n","import type { PlatformScore, HardwareInfo, ModelRequirement, ResolvedInferenceConfig } from '@camstack/types'\n\nexport class InferenceConfigResolver {\n constructor(\n private readonly scores: readonly PlatformScore[],\n private readonly hardware: HardwareInfo,\n ) {}\n\n /**\n * Compute accuracy/backend weights based on available system RAM.\n * availableRAM_MB is now sourced from systeminformation (reliable cross-platform),\n * not os.freemem() which is broken on macOS.\n *\n * - > 16 GB available: prefer larger, more accurate models (accuracy 0.6, backend 0.4)\n * - > 8 GB available: balanced (accuracy 0.5, backend 0.5)\n * - <= 8 GB available: prefer speed (accuracy 0.4, backend 0.6)\n */\n private getWeights(): { accuracyWeight: number; backendWeight: number } {\n const ramMB = this.hardware.availableRAM_MB\n if (ramMB > 16_384) return { accuracyWeight: 0.6, backendWeight: 0.4 }\n if (ramMB > 8_192) return { accuracyWeight: 0.5, backendWeight: 0.5 }\n return { accuracyWeight: 0.4, backendWeight: 0.6 }\n }\n\n /**\n * Given an addon's model requirements, pick the best model + runtime + backend.\n *\n * Algorithm:\n * 1. Filter models by available RAM (minRAM_MB < 25% of available RAM)\n * 2. For each remaining model, find the best platform score whose format\n * is available in the model's formats\n * 3. Pick the model with the highest combined score using RAM-adaptive weights:\n * - High RAM (>16 GB): accuracy × 0.6 + backend × 0.4 (prefer accuracy)\n * - Mid RAM (>8 GB): accuracy × 0.5 + backend × 0.5 (balanced)\n * - Low RAM (<=8 GB): accuracy × 0.4 + backend × 0.6 (prefer speed)\n */\n resolve(requirements: readonly ModelRequirement[]): ResolvedInferenceConfig {\n if (requirements.length === 0) {\n return { modelId: '', runtime: 'node', backend: 'cpu', format: 'onnx', reason: 'No models declared' }\n }\n\n // Budget: 25% of available RAM (now reliable via systeminformation)\n const ramBudget = this.hardware.availableRAM_MB * 0.25\n const { accuracyWeight, backendWeight } = this.getWeights()\n\n console.log(`[InferenceConfigResolver] availableRAM: ${this.hardware.availableRAM_MB}MB, budget: ${Math.round(ramBudget)}MB, weights: accuracy=${accuracyWeight}, backend=${backendWeight}`)\n\n // Filter models that fit in memory\n const fits = requirements.filter(m => m.minRAM_MB < ramBudget)\n const candidates = fits.length > 0 ? fits : [requirements[0]!] // fallback to first/smallest\n\n console.log(`[InferenceConfigResolver] ${candidates.length}/${requirements.length} models fit RAM budget`)\n\n // For each model, find best compatible platform score\n let bestCombo: { model: ModelRequirement; score: PlatformScore; combined: number } | null = null\n\n for (const model of candidates) {\n for (const score of this.scores) {\n if (!score.available) continue\n if (!model.formats.includes(score.format)) continue\n\n const combined = model.accuracyScore * accuracyWeight + score.score * backendWeight\n\n if (!bestCombo || combined > bestCombo.combined) {\n console.log(`[InferenceConfigResolver] New best: ${model.modelId} (accuracy=${model.accuracyScore}) + ${score.backend}/${score.format} (score=${score.score}) → combined=${Math.round(combined)}`)\n bestCombo = { model, score, combined }\n }\n }\n }\n\n if (!bestCombo) {\n return {\n modelId: candidates[0]!.modelId,\n runtime: 'node',\n backend: 'cpu',\n format: 'onnx',\n reason: 'No compatible backend — CPU fallback',\n }\n }\n\n return {\n modelId: bestCombo.model.modelId,\n runtime: bestCombo.score.runtime,\n backend: bestCombo.score.backend,\n format: bestCombo.score.format,\n reason: `${bestCombo.model.name} on ${bestCombo.score.reason} (score: ${Math.round(bestCombo.combined)})`,\n }\n }\n}\n"],"mappings":"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","names":["import_types","platform","err","arch","cpus"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { BaseAddon, platformProbeCapability, errMsg as errMsg2, EventCategory } from "@camstack/types";
|
|
3
|
+
|
|
4
|
+
// src/platform-scorer.ts
|
|
5
|
+
import * as os from "os";
|
|
6
|
+
import { execFile } from "child_process";
|
|
7
|
+
import { promisify } from "util";
|
|
8
|
+
import { errMsg } from "@camstack/types";
|
|
9
|
+
var execFileAsync = promisify(execFile);
|
|
10
|
+
var noopLogger = {
|
|
11
|
+
debug() {
|
|
12
|
+
},
|
|
13
|
+
info() {
|
|
14
|
+
},
|
|
15
|
+
warn() {
|
|
16
|
+
},
|
|
17
|
+
error() {
|
|
18
|
+
},
|
|
19
|
+
child() {
|
|
20
|
+
return noopLogger;
|
|
21
|
+
},
|
|
22
|
+
withTags() {
|
|
23
|
+
return noopLogger;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
async function getAvailableRAM_MB() {
|
|
27
|
+
try {
|
|
28
|
+
const si = await import("systeminformation");
|
|
29
|
+
const mem = await si.mem();
|
|
30
|
+
return Math.round(mem.available / 1024 / 1024);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
console.debug(`systeminformation not available, using platform-specific RAM probe: ${errMsg(err)}`);
|
|
33
|
+
const platform2 = os.platform();
|
|
34
|
+
try {
|
|
35
|
+
if (platform2 === "darwin") {
|
|
36
|
+
const { stdout: output } = await execFileAsync("vm_stat", [], { encoding: "utf8", timeout: 3e3 });
|
|
37
|
+
const pageSize = parseInt(output.match(/page size of (\d+)/)?.[1] ?? "16384");
|
|
38
|
+
const free = parseInt(output.match(/Pages free:\s+(\d+)/)?.[1] ?? "0");
|
|
39
|
+
const inactive = parseInt(output.match(/Pages inactive:\s+(\d+)/)?.[1] ?? "0");
|
|
40
|
+
const purgeable = parseInt(output.match(/Pages purgeable:\s+(\d+)/)?.[1] ?? "0");
|
|
41
|
+
return Math.round((free + inactive + purgeable) * pageSize / 1024 / 1024);
|
|
42
|
+
} else if (platform2 === "linux") {
|
|
43
|
+
const { readFileSync } = await import("fs");
|
|
44
|
+
const meminfo = readFileSync("/proc/meminfo", "utf8");
|
|
45
|
+
const match = meminfo.match(/MemAvailable:\s+(\d+)\s+kB/);
|
|
46
|
+
if (match) return Math.round(parseInt(match[1]) / 1024);
|
|
47
|
+
}
|
|
48
|
+
} catch (err2) {
|
|
49
|
+
console.debug(`RAM probe failed, using total RAM fallback: ${errMsg(err2)}`);
|
|
50
|
+
}
|
|
51
|
+
return Math.round(os.totalmem() / 1024 / 1024);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
var PlatformScorer = class {
|
|
55
|
+
cached = null;
|
|
56
|
+
logger;
|
|
57
|
+
constructor(logger = noopLogger) {
|
|
58
|
+
this.logger = logger;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Probe hardware + runtimes and score all backend combos.
|
|
62
|
+
*
|
|
63
|
+
* An optional `onPhase` callback is invoked at each step so consumers
|
|
64
|
+
* (e.g. the platform-probe addon) can emit live progress events on
|
|
65
|
+
* the event bus. The callback takes a phase id + a typed payload; all
|
|
66
|
+
* phases fire in strict order: `started` → `hardware` → `node-backends`
|
|
67
|
+
* → `python-backends` → `scored` → `done`. On failure, `error` fires
|
|
68
|
+
* once with the exception. Cached after first call.
|
|
69
|
+
*/
|
|
70
|
+
async probe(onPhase) {
|
|
71
|
+
if (this.cached) return this.cached;
|
|
72
|
+
const start = Date.now();
|
|
73
|
+
onPhase?.("started", {});
|
|
74
|
+
this.logger.info("Probing hardware...");
|
|
75
|
+
const hardware = await this.probeHardware();
|
|
76
|
+
this.logger.info("Hardware detected", {
|
|
77
|
+
meta: {
|
|
78
|
+
platform: hardware.platform,
|
|
79
|
+
arch: hardware.arch,
|
|
80
|
+
cpuModel: hardware.cpuModel,
|
|
81
|
+
cpuCores: hardware.cpuCores,
|
|
82
|
+
ramGB: Math.round(hardware.totalRAM_MB / 1024)
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
if (hardware.gpu) this.logger.info("GPU detected", { meta: { name: hardware.gpu.name } });
|
|
86
|
+
if (hardware.npu) this.logger.info("NPU detected", { meta: { type: hardware.npu.type } });
|
|
87
|
+
onPhase?.("hardware", { hardware });
|
|
88
|
+
this.logger.info("Probing Node.js backends...");
|
|
89
|
+
const nodeBackends = await this.probeNodeBackends();
|
|
90
|
+
this.logger.info("Node backends detected", { meta: { backends: nodeBackends.map((b) => b.id) } });
|
|
91
|
+
onPhase?.("node-backends", { backends: nodeBackends });
|
|
92
|
+
this.logger.info("Probing Python backends...");
|
|
93
|
+
const pythonInfo = await this.probePythonBackends();
|
|
94
|
+
if (pythonInfo.pythonPath) {
|
|
95
|
+
this.logger.info("Python backends detected", {
|
|
96
|
+
meta: {
|
|
97
|
+
pythonPath: pythonInfo.pythonPath,
|
|
98
|
+
backends: pythonInfo.backends.filter((b) => b.available).map((b) => b.id)
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
} else {
|
|
102
|
+
this.logger.info("Python: not found");
|
|
103
|
+
}
|
|
104
|
+
onPhase?.("python-backends", { pythonPath: pythonInfo.pythonPath, backends: pythonInfo.backends });
|
|
105
|
+
const scores = this.scoreBackends(hardware, nodeBackends, pythonInfo.backends);
|
|
106
|
+
const bestScore = scores.find((s) => s.available) ?? scores[scores.length - 1];
|
|
107
|
+
const elapsed = Date.now() - start;
|
|
108
|
+
this.logger.info("Scoring complete", { meta: { elapsedMs: elapsed, combos: scores.length } });
|
|
109
|
+
this.logger.info("Best backend selected", {
|
|
110
|
+
meta: {
|
|
111
|
+
runtime: bestScore.runtime,
|
|
112
|
+
backend: bestScore.backend,
|
|
113
|
+
format: bestScore.format,
|
|
114
|
+
reason: bestScore.reason,
|
|
115
|
+
score: bestScore.score
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
for (const s of scores) {
|
|
119
|
+
this.logger.debug("Score entry", {
|
|
120
|
+
meta: {
|
|
121
|
+
available: s.available,
|
|
122
|
+
runtime: s.runtime,
|
|
123
|
+
backend: s.backend,
|
|
124
|
+
format: s.format,
|
|
125
|
+
score: s.score,
|
|
126
|
+
reason: s.reason
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
this.cached = {
|
|
131
|
+
hardware,
|
|
132
|
+
scores,
|
|
133
|
+
bestScore,
|
|
134
|
+
pythonPath: pythonInfo.pythonPath
|
|
135
|
+
};
|
|
136
|
+
onPhase?.("scored", { scores, bestScore, elapsedMs: elapsed });
|
|
137
|
+
onPhase?.("done", { capabilities: this.cached });
|
|
138
|
+
return this.cached;
|
|
139
|
+
}
|
|
140
|
+
async probeHardware() {
|
|
141
|
+
const platform2 = os.platform();
|
|
142
|
+
const arch2 = os.arch();
|
|
143
|
+
const cpus2 = os.cpus();
|
|
144
|
+
const cpuModel = cpus2[0]?.model ?? "unknown";
|
|
145
|
+
const cpuCores = cpus2.length;
|
|
146
|
+
const totalRAM_MB = Math.round(os.totalmem() / 1024 / 1024);
|
|
147
|
+
const availableRAM_MB = await getAvailableRAM_MB();
|
|
148
|
+
this.logger.debug("RAM probed", { meta: { totalRAM_MB, availableRAM_MB } });
|
|
149
|
+
let gpu = null;
|
|
150
|
+
let npu = null;
|
|
151
|
+
if (platform2 === "darwin" && arch2 === "arm64") {
|
|
152
|
+
gpu = { type: "apple", name: "Apple Silicon GPU" };
|
|
153
|
+
npu = { type: "apple-ane" };
|
|
154
|
+
}
|
|
155
|
+
if (platform2 === "linux") {
|
|
156
|
+
try {
|
|
157
|
+
const { stdout } = await execFileAsync("nvidia-smi", ["--query-gpu=name,memory.total", "--format=csv,noheader"], {
|
|
158
|
+
encoding: "utf8",
|
|
159
|
+
timeout: 5e3
|
|
160
|
+
});
|
|
161
|
+
const output = stdout.trim();
|
|
162
|
+
if (output) {
|
|
163
|
+
const [name, mem] = output.split(",").map((s) => s.trim());
|
|
164
|
+
gpu = {
|
|
165
|
+
type: "nvidia",
|
|
166
|
+
name: name ?? "NVIDIA GPU",
|
|
167
|
+
memoryMB: parseInt(mem ?? "0")
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
} catch (err) {
|
|
171
|
+
this.logger.debug("NVIDIA GPU detection failed", { meta: { error: errMsg(err) } });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return { platform: platform2, arch: arch2, cpuModel, cpuCores, totalRAM_MB, availableRAM_MB, gpu, npu };
|
|
175
|
+
}
|
|
176
|
+
async probeNodeBackends() {
|
|
177
|
+
const backends = [
|
|
178
|
+
{ id: "cpu", available: true }
|
|
179
|
+
];
|
|
180
|
+
try {
|
|
181
|
+
const ort = await import("onnxruntime-node");
|
|
182
|
+
const providers = ort.InferenceSession?.getAvailableProviders?.() ?? [];
|
|
183
|
+
for (const p of providers) {
|
|
184
|
+
const n = p.toLowerCase().replace("executionprovider", "");
|
|
185
|
+
if (n === "coreml") backends.push({ id: "coreml", available: true });
|
|
186
|
+
else if (n === "cuda") backends.push({ id: "cuda", available: true });
|
|
187
|
+
else if (n === "tensorrt") backends.push({ id: "tensorrt", available: true });
|
|
188
|
+
}
|
|
189
|
+
} catch (err) {
|
|
190
|
+
this.logger.debug("onnxruntime-node not available", { meta: { error: errMsg(err) } });
|
|
191
|
+
}
|
|
192
|
+
if (os.platform() === "darwin" && !backends.some((b) => b.id === "coreml")) {
|
|
193
|
+
backends.push({ id: "coreml", available: true });
|
|
194
|
+
}
|
|
195
|
+
return backends;
|
|
196
|
+
}
|
|
197
|
+
async probePythonBackends() {
|
|
198
|
+
let pythonPath = null;
|
|
199
|
+
for (const cmd of ["python3", "python"]) {
|
|
200
|
+
try {
|
|
201
|
+
await execFileAsync(cmd, ["--version"], { timeout: 5e3 });
|
|
202
|
+
pythonPath = cmd;
|
|
203
|
+
break;
|
|
204
|
+
} catch (err) {
|
|
205
|
+
console.debug(`Python command "${cmd}" not found: ${errMsg(err)}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (!pythonPath) return { pythonPath: null, backends: [] };
|
|
209
|
+
const checks = [
|
|
210
|
+
// [pythonModule, backendId, modelFormat]
|
|
211
|
+
["coremltools", "coreml", "coreml"],
|
|
212
|
+
["openvino.runtime", "openvino", "openvino"],
|
|
213
|
+
["torch", "pytorch", "onnx"],
|
|
214
|
+
["onnxruntime", "onnx-py", "onnx"]
|
|
215
|
+
];
|
|
216
|
+
const results = await Promise.all(checks.map(async ([mod, id, format]) => {
|
|
217
|
+
const probeStart = Date.now();
|
|
218
|
+
let available = false;
|
|
219
|
+
try {
|
|
220
|
+
await execFileAsync(pythonPath, ["-c", `import ${mod}`], { timeout: 3e4 });
|
|
221
|
+
available = true;
|
|
222
|
+
} catch (err) {
|
|
223
|
+
console.debug(`Python module "${mod}" not installed: ${errMsg(err)}`);
|
|
224
|
+
}
|
|
225
|
+
const probeMs = Date.now() - probeStart;
|
|
226
|
+
this.logger.debug("Python module probed", { meta: { module: mod, available, probeMs } });
|
|
227
|
+
return { mod, id, format, available };
|
|
228
|
+
}));
|
|
229
|
+
const backends = [];
|
|
230
|
+
for (const r of results) {
|
|
231
|
+
if (r.id === "coreml" && os.platform() === "darwin") {
|
|
232
|
+
backends.push({ id: r.id, format: r.format, available: r.available });
|
|
233
|
+
} else if (r.available) {
|
|
234
|
+
backends.push({ id: r.id, format: r.format, available: r.available });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return { pythonPath, backends };
|
|
238
|
+
}
|
|
239
|
+
scoreBackends(hardware, nodeBackends, pythonBackends) {
|
|
240
|
+
const scores = [];
|
|
241
|
+
if (hardware.platform === "darwin" && hardware.arch === "arm64") {
|
|
242
|
+
const pyCoreMl = pythonBackends.find((b) => b.id === "coreml");
|
|
243
|
+
if (pyCoreMl) {
|
|
244
|
+
scores.push({ runtime: "python", backend: "coreml", format: "coreml", score: 95, reason: "Apple Neural Engine (Python CoreML)", available: pyCoreMl.available });
|
|
245
|
+
}
|
|
246
|
+
const nodeCoreMl = nodeBackends.find((b) => b.id === "coreml");
|
|
247
|
+
if (nodeCoreMl) {
|
|
248
|
+
scores.push({ runtime: "node", backend: "coreml", format: "onnx", score: 90, reason: "CoreML via ONNX Runtime", available: nodeCoreMl.available });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (hardware.gpu?.type === "nvidia") {
|
|
252
|
+
const tensorrt = nodeBackends.find((b) => b.id === "tensorrt");
|
|
253
|
+
if (tensorrt) scores.push({ runtime: "node", backend: "tensorrt", format: "onnx", score: 95, reason: "NVIDIA TensorRT", available: true });
|
|
254
|
+
const cuda = nodeBackends.find((b) => b.id === "cuda");
|
|
255
|
+
if (cuda) scores.push({ runtime: "node", backend: "cuda", format: "onnx", score: 85, reason: "NVIDIA CUDA", available: true });
|
|
256
|
+
}
|
|
257
|
+
const openvino = pythonBackends.find((b) => b.id === "openvino");
|
|
258
|
+
if (openvino) {
|
|
259
|
+
const score = hardware.npu?.type === "intel-npu" ? 90 : 80;
|
|
260
|
+
scores.push({ runtime: "python", backend: "openvino", format: "openvino", score, reason: "Intel OpenVINO", available: openvino.available });
|
|
261
|
+
}
|
|
262
|
+
scores.push({ runtime: "node", backend: "cpu", format: "onnx", score: 50, reason: "CPU (ONNX Runtime Node)", available: true });
|
|
263
|
+
const pyOnnx = pythonBackends.find((b) => b.id === "onnx-py");
|
|
264
|
+
if (pyOnnx) {
|
|
265
|
+
scores.push({ runtime: "python", backend: "cpu", format: "onnx", score: 45, reason: "CPU (Python ONNX)", available: pyOnnx.available });
|
|
266
|
+
}
|
|
267
|
+
return scores.sort((a, b) => b.score - a.score);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// src/inference-config-resolver.ts
|
|
272
|
+
var InferenceConfigResolver = class {
|
|
273
|
+
constructor(scores, hardware) {
|
|
274
|
+
this.scores = scores;
|
|
275
|
+
this.hardware = hardware;
|
|
276
|
+
}
|
|
277
|
+
scores;
|
|
278
|
+
hardware;
|
|
279
|
+
/**
|
|
280
|
+
* Compute accuracy/backend weights based on available system RAM.
|
|
281
|
+
* availableRAM_MB is now sourced from systeminformation (reliable cross-platform),
|
|
282
|
+
* not os.freemem() which is broken on macOS.
|
|
283
|
+
*
|
|
284
|
+
* - > 16 GB available: prefer larger, more accurate models (accuracy 0.6, backend 0.4)
|
|
285
|
+
* - > 8 GB available: balanced (accuracy 0.5, backend 0.5)
|
|
286
|
+
* - <= 8 GB available: prefer speed (accuracy 0.4, backend 0.6)
|
|
287
|
+
*/
|
|
288
|
+
getWeights() {
|
|
289
|
+
const ramMB = this.hardware.availableRAM_MB;
|
|
290
|
+
if (ramMB > 16384) return { accuracyWeight: 0.6, backendWeight: 0.4 };
|
|
291
|
+
if (ramMB > 8192) return { accuracyWeight: 0.5, backendWeight: 0.5 };
|
|
292
|
+
return { accuracyWeight: 0.4, backendWeight: 0.6 };
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Given an addon's model requirements, pick the best model + runtime + backend.
|
|
296
|
+
*
|
|
297
|
+
* Algorithm:
|
|
298
|
+
* 1. Filter models by available RAM (minRAM_MB < 25% of available RAM)
|
|
299
|
+
* 2. For each remaining model, find the best platform score whose format
|
|
300
|
+
* is available in the model's formats
|
|
301
|
+
* 3. Pick the model with the highest combined score using RAM-adaptive weights:
|
|
302
|
+
* - High RAM (>16 GB): accuracy × 0.6 + backend × 0.4 (prefer accuracy)
|
|
303
|
+
* - Mid RAM (>8 GB): accuracy × 0.5 + backend × 0.5 (balanced)
|
|
304
|
+
* - Low RAM (<=8 GB): accuracy × 0.4 + backend × 0.6 (prefer speed)
|
|
305
|
+
*/
|
|
306
|
+
resolve(requirements) {
|
|
307
|
+
if (requirements.length === 0) {
|
|
308
|
+
return { modelId: "", runtime: "node", backend: "cpu", format: "onnx", reason: "No models declared" };
|
|
309
|
+
}
|
|
310
|
+
const ramBudget = this.hardware.availableRAM_MB * 0.25;
|
|
311
|
+
const { accuracyWeight, backendWeight } = this.getWeights();
|
|
312
|
+
console.log(`[InferenceConfigResolver] availableRAM: ${this.hardware.availableRAM_MB}MB, budget: ${Math.round(ramBudget)}MB, weights: accuracy=${accuracyWeight}, backend=${backendWeight}`);
|
|
313
|
+
const fits = requirements.filter((m) => m.minRAM_MB < ramBudget);
|
|
314
|
+
const candidates = fits.length > 0 ? fits : [requirements[0]];
|
|
315
|
+
console.log(`[InferenceConfigResolver] ${candidates.length}/${requirements.length} models fit RAM budget`);
|
|
316
|
+
let bestCombo = null;
|
|
317
|
+
for (const model of candidates) {
|
|
318
|
+
for (const score of this.scores) {
|
|
319
|
+
if (!score.available) continue;
|
|
320
|
+
if (!model.formats.includes(score.format)) continue;
|
|
321
|
+
const combined = model.accuracyScore * accuracyWeight + score.score * backendWeight;
|
|
322
|
+
if (!bestCombo || combined > bestCombo.combined) {
|
|
323
|
+
console.log(`[InferenceConfigResolver] New best: ${model.modelId} (accuracy=${model.accuracyScore}) + ${score.backend}/${score.format} (score=${score.score}) \u2192 combined=${Math.round(combined)}`);
|
|
324
|
+
bestCombo = { model, score, combined };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (!bestCombo) {
|
|
329
|
+
return {
|
|
330
|
+
modelId: candidates[0].modelId,
|
|
331
|
+
runtime: "node",
|
|
332
|
+
backend: "cpu",
|
|
333
|
+
format: "onnx",
|
|
334
|
+
reason: "No compatible backend \u2014 CPU fallback"
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
modelId: bestCombo.model.modelId,
|
|
339
|
+
runtime: bestCombo.score.runtime,
|
|
340
|
+
backend: bestCombo.score.backend,
|
|
341
|
+
format: bestCombo.score.format,
|
|
342
|
+
reason: `${bestCombo.model.name} on ${bestCombo.score.reason} (score: ${Math.round(bestCombo.combined)})`
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// src/index.ts
|
|
348
|
+
var PlatformProbeNativeAddon = class extends BaseAddon {
|
|
349
|
+
scorer = null;
|
|
350
|
+
cachedCaps = null;
|
|
351
|
+
constructor() {
|
|
352
|
+
super({});
|
|
353
|
+
}
|
|
354
|
+
async onInitialize() {
|
|
355
|
+
this.scorer = new PlatformScorer(this.ctx.logger);
|
|
356
|
+
const emitPhase = (phase, payload) => {
|
|
357
|
+
this.ctx.eventBus?.emit({
|
|
358
|
+
id: `platform-probe-${phase}-${Date.now()}`,
|
|
359
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
360
|
+
source: { type: "core", id: "platform-probe-native" },
|
|
361
|
+
category: EventCategory.PlatformProbePhase,
|
|
362
|
+
data: { type: "platform-probe.phase", phase, ...payload ?? {} }
|
|
363
|
+
});
|
|
364
|
+
};
|
|
365
|
+
const probePromise = this.scorer.probe(emitPhase).then((caps) => {
|
|
366
|
+
this.cachedCaps = caps;
|
|
367
|
+
this.ctx.logger.info("Platform probe complete", {
|
|
368
|
+
meta: {
|
|
369
|
+
cpuModel: caps.hardware.cpuModel,
|
|
370
|
+
arch: caps.hardware.arch,
|
|
371
|
+
totalRAM_MB: caps.hardware.totalRAM_MB,
|
|
372
|
+
gpu: caps.hardware.gpu?.name ?? "none",
|
|
373
|
+
bestReason: caps.bestScore.reason,
|
|
374
|
+
bestScore: caps.bestScore.score
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
return caps;
|
|
378
|
+
}).catch((err) => {
|
|
379
|
+
const msg = errMsg2(err);
|
|
380
|
+
this.ctx.logger.error("Platform probe failed", { meta: { error: msg } });
|
|
381
|
+
emitPhase("error", { message: msg });
|
|
382
|
+
throw err;
|
|
383
|
+
});
|
|
384
|
+
const getCaps = async () => {
|
|
385
|
+
return this.cachedCaps ?? probePromise;
|
|
386
|
+
};
|
|
387
|
+
const provider = {
|
|
388
|
+
getCapabilities: async () => {
|
|
389
|
+
return getCaps();
|
|
390
|
+
},
|
|
391
|
+
getHardware: async () => {
|
|
392
|
+
const caps = await getCaps();
|
|
393
|
+
return caps.hardware;
|
|
394
|
+
},
|
|
395
|
+
resolveInferenceConfig: async (input) => {
|
|
396
|
+
const caps = await getCaps();
|
|
397
|
+
const resolver = new InferenceConfigResolver(caps.scores, caps.hardware);
|
|
398
|
+
return resolver.resolve(input.requirements);
|
|
399
|
+
},
|
|
400
|
+
resolveHwAccel: async (input) => {
|
|
401
|
+
const hwaccel = this.ctx.kernel.hwaccel;
|
|
402
|
+
if (!hwaccel) {
|
|
403
|
+
return { preferred: [] };
|
|
404
|
+
}
|
|
405
|
+
const res = await hwaccel.resolve(input.prefer ?? null);
|
|
406
|
+
return { preferred: res.preferred };
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
return [{ capability: platformProbeCapability, provider }];
|
|
410
|
+
}
|
|
411
|
+
async onShutdown() {
|
|
412
|
+
this.scorer = null;
|
|
413
|
+
this.cachedCaps = null;
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
var src_default = PlatformProbeNativeAddon;
|
|
417
|
+
export {
|
|
418
|
+
InferenceConfigResolver,
|
|
419
|
+
PlatformProbeNativeAddon,
|
|
420
|
+
PlatformScorer,
|
|
421
|
+
src_default as default
|
|
422
|
+
};
|
|
423
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/platform-scorer.ts","../src/inference-config-resolver.ts"],"sourcesContent":["/**\n * @camstack/addon-platform-probe-native\n *\n * Infra addon — probes hardware + inference runtimes on the local node and\n * exposes the result via the `platform-probe` capability (singleton per\n * node). Every node in the cluster (hub + agents) installs this package as\n * part of `AddonInstaller.REQUIRED_PACKAGES` / `AGENT_PACKAGES`, so\n * inference addons always find a local provider through\n * `ctx.api.platformProbe.*`.\n *\n * The heavy lifting (hardware detection, backend scoring, RAM-adaptive\n * config resolution) used to live in `@camstack/core/platform/*`. It now\n * lives here so the kernel and core stay free of domain-specific\n * inference logic.\n */\nimport type {\n ProviderRegistration,\n IPlatformProbeProvider,\n PlatformCapabilities,\n HardwareInfo,\n ModelRequirement,\n ResolvedInferenceConfig,\n HwAccelBackend,\n} from '@camstack/types'\nimport { BaseAddon, platformProbeCapability, errMsg, EventCategory } from '@camstack/types'\nimport { PlatformScorer } from './platform-scorer.js'\nimport { InferenceConfigResolver } from './inference-config-resolver.js'\n\nexport { PlatformScorer } from './platform-scorer.js'\nexport { InferenceConfigResolver } from './inference-config-resolver.js'\n\nexport class PlatformProbeNativeAddon extends BaseAddon {\n private scorer: PlatformScorer | null = null\n private cachedCaps: PlatformCapabilities | null = null\n\n constructor() { super({}) }\n\n protected async onInitialize(): Promise<ProviderRegistration[]> {\n this.scorer = new PlatformScorer(this.ctx.logger)\n\n // Kick off the probe in the background — provider methods await the\n // promise so early consumers block until capabilities are ready.\n // Each phase emits a `platform-probe.phase` event on the bus so the\n // benchmark UI (and any other consumer) can render live progress via\n // `live.onEvent({ category: EventCategory.PlatformProbePhase })`.\n const emitPhase = (phase: string, payload?: Record<string, unknown>): void => {\n this.ctx.eventBus?.emit({\n id: `platform-probe-${phase}-${Date.now()}`,\n timestamp: new Date(),\n source: { type: 'core', id: 'platform-probe-native' },\n category: EventCategory.PlatformProbePhase,\n data: { type: 'platform-probe.phase', phase, ...(payload ?? {}) },\n })\n }\n\n const probePromise: Promise<PlatformCapabilities> = this.scorer\n .probe(emitPhase)\n .then((caps) => {\n this.cachedCaps = caps\n this.ctx.logger.info('Platform probe complete', {\n meta: {\n cpuModel: caps.hardware.cpuModel,\n arch: caps.hardware.arch,\n totalRAM_MB: caps.hardware.totalRAM_MB,\n gpu: caps.hardware.gpu?.name ?? 'none',\n bestReason: caps.bestScore.reason,\n bestScore: caps.bestScore.score,\n },\n })\n return caps\n })\n .catch((err: unknown) => {\n const msg = errMsg(err)\n this.ctx.logger.error('Platform probe failed', { meta: { error: msg } })\n emitPhase('error', { message: msg })\n throw err\n })\n\n const getCaps = async (): Promise<PlatformCapabilities> => {\n return this.cachedCaps ?? probePromise\n }\n\n const provider: IPlatformProbeProvider = {\n getCapabilities: async (): Promise<PlatformCapabilities> => {\n return getCaps()\n },\n getHardware: async (): Promise<HardwareInfo> => {\n const caps = await getCaps()\n return caps.hardware\n },\n resolveInferenceConfig: async (input: {\n readonly requirements: readonly ModelRequirement[]\n }): Promise<ResolvedInferenceConfig> => {\n const caps = await getCaps()\n const resolver = new InferenceConfigResolver(caps.scores, caps.hardware)\n return resolver.resolve(input.requirements)\n },\n resolveHwAccel: async (input: {\n readonly prefer?: string | null\n readonly nodeId?: string\n }): Promise<{ preferred: readonly string[] }> => {\n const hwaccel = this.ctx.kernel.hwaccel\n if (!hwaccel) {\n return { preferred: [] }\n }\n const res = await hwaccel.resolve((input.prefer as HwAccelBackend | 'none' | null) ?? null)\n return { preferred: res.preferred }\n },\n }\n\n return [{ capability: platformProbeCapability, provider }]\n }\n\n protected async onShutdown(): Promise<void> {\n this.scorer = null\n this.cachedCaps = null\n }\n}\n\nexport default PlatformProbeNativeAddon\n","import * as os from 'node:os'\nimport { execFile } from 'node:child_process'\nimport { promisify } from 'node:util'\nimport { errMsg } from '@camstack/types'\nimport type { HardwareInfo, GpuInfo, NpuInfo, PlatformScore, PlatformCapabilities, IScopedLogger } from '@camstack/types'\n\n/**\n * Promisified `execFile`. Used across the scorer so every subprocess\n * spawn yields the event loop — critical at boot because a single sync\n * spawn of a missing Python module hits its 30s timeout and freezes the\n * entire Node process (WS handshakes, tRPC subscriptions, metrics — all\n * stalled). Keeping it async turns the probe into a true background task.\n */\nconst execFileAsync = promisify(execFile)\n\n/** Minimal no-op logger for default parameter */\nconst noopLogger: IScopedLogger = {\n debug() {},\n info() {},\n warn() {},\n error() {},\n child() { return noopLogger },\n withTags() { return noopLogger },\n}\n\n/**\n * Get reliable \"available\" RAM in MB, cross-platform.\n * os.freemem() is unreliable on macOS (reports ~200MB on a 36GB system).\n * Uses systeminformation when available, falls back to native commands.\n */\nasync function getAvailableRAM_MB(): Promise<number> {\n try {\n const si = await import('systeminformation')\n const mem = await si.mem()\n return Math.round(mem.available / 1024 / 1024)\n } catch (err) {\n console.debug(`systeminformation not available, using platform-specific RAM probe: ${errMsg(err)}`)\n const platform = os.platform()\n try {\n if (platform === 'darwin') {\n // Parse vm_stat: (free + inactive + purgeable) * pageSize\n const { stdout: output } = await execFileAsync('vm_stat', [], { encoding: 'utf8', timeout: 3000 })\n const pageSize = parseInt(output.match(/page size of (\\d+)/)?.[1] ?? '16384')\n const free = parseInt(output.match(/Pages free:\\s+(\\d+)/)?.[1] ?? '0')\n const inactive = parseInt(output.match(/Pages inactive:\\s+(\\d+)/)?.[1] ?? '0')\n const purgeable = parseInt(output.match(/Pages purgeable:\\s+(\\d+)/)?.[1] ?? '0')\n return Math.round((free + inactive + purgeable) * pageSize / 1024 / 1024)\n } else if (platform === 'linux') {\n // Parse /proc/meminfo: MemAvailable\n const { readFileSync } = await import('node:fs')\n const meminfo = readFileSync('/proc/meminfo', 'utf8')\n const match = meminfo.match(/MemAvailable:\\s+(\\d+)\\s+kB/)\n if (match) return Math.round(parseInt(match[1]!) / 1024)\n }\n } catch (err) { console.debug(`RAM probe failed, using total RAM fallback: ${errMsg(err)}`) }\n // Ultimate fallback: total RAM (conservative but safe)\n return Math.round(os.totalmem() / 1024 / 1024)\n }\n}\n\nexport class PlatformScorer {\n private cached: PlatformCapabilities | null = null\n private readonly logger: IScopedLogger\n\n constructor(logger: IScopedLogger = noopLogger) {\n this.logger = logger\n }\n\n /**\n * Probe hardware + runtimes and score all backend combos.\n *\n * An optional `onPhase` callback is invoked at each step so consumers\n * (e.g. the platform-probe addon) can emit live progress events on\n * the event bus. The callback takes a phase id + a typed payload; all\n * phases fire in strict order: `started` → `hardware` → `node-backends`\n * → `python-backends` → `scored` → `done`. On failure, `error` fires\n * once with the exception. Cached after first call.\n */\n async probe(onPhase?: (phase: string, payload?: Record<string, unknown>) => void): Promise<PlatformCapabilities> {\n if (this.cached) return this.cached\n\n const start = Date.now()\n onPhase?.('started', {})\n this.logger.info('Probing hardware...')\n const hardware = await this.probeHardware()\n this.logger.info('Hardware detected', {\n meta: {\n platform: hardware.platform,\n arch: hardware.arch,\n cpuModel: hardware.cpuModel,\n cpuCores: hardware.cpuCores,\n ramGB: Math.round(hardware.totalRAM_MB / 1024),\n },\n })\n if (hardware.gpu) this.logger.info('GPU detected', { meta: { name: hardware.gpu.name } })\n if (hardware.npu) this.logger.info('NPU detected', { meta: { type: hardware.npu.type } })\n onPhase?.('hardware', { hardware })\n\n this.logger.info('Probing Node.js backends...')\n const nodeBackends = await this.probeNodeBackends()\n this.logger.info('Node backends detected', { meta: { backends: nodeBackends.map(b => b.id) } })\n onPhase?.('node-backends', { backends: nodeBackends })\n\n this.logger.info('Probing Python backends...')\n const pythonInfo = await this.probePythonBackends()\n if (pythonInfo.pythonPath) {\n this.logger.info('Python backends detected', {\n meta: {\n pythonPath: pythonInfo.pythonPath,\n backends: pythonInfo.backends.filter(b => b.available).map(b => b.id),\n },\n })\n } else {\n this.logger.info('Python: not found')\n }\n onPhase?.('python-backends', { pythonPath: pythonInfo.pythonPath, backends: pythonInfo.backends })\n\n const scores = this.scoreBackends(hardware, nodeBackends, pythonInfo.backends)\n const bestScore = scores.find(s => s.available) ?? scores[scores.length - 1]!\n\n const elapsed = Date.now() - start\n this.logger.info('Scoring complete', { meta: { elapsedMs: elapsed, combos: scores.length } })\n this.logger.info('Best backend selected', {\n meta: {\n runtime: bestScore.runtime,\n backend: bestScore.backend,\n format: bestScore.format,\n reason: bestScore.reason,\n score: bestScore.score,\n },\n })\n for (const s of scores) {\n this.logger.debug('Score entry', {\n meta: {\n available: s.available,\n runtime: s.runtime,\n backend: s.backend,\n format: s.format,\n score: s.score,\n reason: s.reason,\n },\n })\n }\n\n this.cached = {\n hardware,\n scores,\n bestScore,\n pythonPath: pythonInfo.pythonPath,\n }\n onPhase?.('scored', { scores, bestScore, elapsedMs: elapsed })\n onPhase?.('done', { capabilities: this.cached })\n return this.cached\n }\n\n async probeHardware(): Promise<HardwareInfo> {\n const platform = os.platform() as HardwareInfo['platform']\n const arch = os.arch() as HardwareInfo['arch']\n const cpus = os.cpus()\n const cpuModel = cpus[0]?.model ?? 'unknown'\n const cpuCores = cpus.length\n const totalRAM_MB = Math.round(os.totalmem() / 1024 / 1024)\n const availableRAM_MB = await getAvailableRAM_MB()\n this.logger.debug('RAM probed', { meta: { totalRAM_MB, availableRAM_MB } })\n\n let gpu: GpuInfo | null = null\n let npu: NpuInfo | null = null\n\n // macOS Apple Silicon\n if (platform === 'darwin' && arch === 'arm64') {\n gpu = { type: 'apple', name: 'Apple Silicon GPU' }\n npu = { type: 'apple-ane' }\n }\n\n // Linux NVIDIA\n if (platform === 'linux') {\n try {\n const { stdout } = await execFileAsync('nvidia-smi', ['--query-gpu=name,memory.total', '--format=csv,noheader'], {\n encoding: 'utf8', timeout: 5000,\n })\n const output = stdout.trim()\n if (output) {\n const [name, mem] = output.split(',').map(s => s.trim())\n gpu = {\n type: 'nvidia',\n name: name ?? 'NVIDIA GPU',\n memoryMB: parseInt(mem ?? '0'),\n }\n }\n } catch (err) { this.logger.debug('NVIDIA GPU detection failed', { meta: { error: errMsg(err) } }) }\n }\n\n return { platform, arch, cpuModel, cpuCores, totalRAM_MB, availableRAM_MB, gpu, npu }\n }\n\n private async probeNodeBackends(): Promise<Array<{ id: string; available: boolean }>> {\n const backends: Array<{ id: string; available: boolean }> = [\n { id: 'cpu', available: true },\n ]\n\n try {\n const ort = await import('onnxruntime-node') as { InferenceSession?: { getAvailableProviders?: () => string[] } }\n const providers: string[] = ort.InferenceSession?.getAvailableProviders?.() ?? []\n for (const p of providers) {\n const n = p.toLowerCase().replace('executionprovider', '')\n if (n === 'coreml') backends.push({ id: 'coreml', available: true })\n else if (n === 'cuda') backends.push({ id: 'cuda', available: true })\n else if (n === 'tensorrt') backends.push({ id: 'tensorrt', available: true })\n }\n } catch (err) { this.logger.debug('onnxruntime-node not available', { meta: { error: errMsg(err) } }) }\n\n // macOS always has CoreML potential\n if (os.platform() === 'darwin' && !backends.some(b => b.id === 'coreml')) {\n backends.push({ id: 'coreml', available: true })\n }\n\n return backends\n }\n\n private async probePythonBackends(): Promise<{ pythonPath: string | null; backends: Array<{ id: string; format: string; available: boolean }> }> {\n let pythonPath: string | null = null\n for (const cmd of ['python3', 'python']) {\n try {\n await execFileAsync(cmd, ['--version'], { timeout: 5000 })\n pythonPath = cmd\n break\n } catch (err) { console.debug(`Python command \"${cmd}\" not found: ${errMsg(err)}`) }\n }\n\n if (!pythonPath) return { pythonPath: null, backends: [] }\n\n const checks: Array<[string, string, string]> = [\n // [pythonModule, backendId, modelFormat]\n ['coremltools', 'coreml', 'coreml'],\n ['openvino.runtime', 'openvino', 'openvino'],\n ['torch', 'pytorch', 'onnx'],\n ['onnxruntime', 'onnx-py', 'onnx'],\n ]\n\n // Run all module probes in parallel. Each probe is independent and a\n // missing module spends 30s waiting on its own timeout — serial would\n // add up to ~50s of cumulative wait. Parallel caps it at the slowest\n // probe. The execFile promise yields the event loop, so WS handshakes\n // and tRPC calls continue to flow during the wait.\n const results = await Promise.all(checks.map(async ([mod, id, format]) => {\n const probeStart = Date.now()\n let available = false\n try {\n await execFileAsync(pythonPath!, ['-c', `import ${mod}`], { timeout: 30000 })\n available = true\n } catch (err) { console.debug(`Python module \"${mod}\" not installed: ${errMsg(err)}`) }\n const probeMs = Date.now() - probeStart\n this.logger.debug('Python module probed', { meta: { module: mod, available, probeMs } })\n return { mod, id, format, available }\n }))\n\n const backends: Array<{ id: string; format: string; available: boolean }> = []\n for (const r of results) {\n // Always show CoreML on macOS even if not installed\n if (r.id === 'coreml' && os.platform() === 'darwin') {\n backends.push({ id: r.id, format: r.format, available: r.available })\n } else if (r.available) {\n backends.push({ id: r.id, format: r.format, available: r.available })\n }\n }\n\n return { pythonPath, backends }\n }\n\n private scoreBackends(\n hardware: HardwareInfo,\n nodeBackends: Array<{ id: string; available: boolean }>,\n pythonBackends: Array<{ id: string; format: string; available: boolean }>,\n ): PlatformScore[] {\n const scores: PlatformScore[] = []\n\n // macOS Apple Silicon\n if (hardware.platform === 'darwin' && hardware.arch === 'arm64') {\n const pyCoreMl = pythonBackends.find(b => b.id === 'coreml')\n if (pyCoreMl) {\n scores.push({ runtime: 'python', backend: 'coreml', format: 'coreml', score: 95, reason: 'Apple Neural Engine (Python CoreML)', available: pyCoreMl.available })\n }\n const nodeCoreMl = nodeBackends.find(b => b.id === 'coreml')\n if (nodeCoreMl) {\n scores.push({ runtime: 'node', backend: 'coreml', format: 'onnx', score: 90, reason: 'CoreML via ONNX Runtime', available: nodeCoreMl.available })\n }\n }\n\n // NVIDIA\n if (hardware.gpu?.type === 'nvidia') {\n const tensorrt = nodeBackends.find(b => b.id === 'tensorrt')\n if (tensorrt) scores.push({ runtime: 'node', backend: 'tensorrt', format: 'onnx', score: 95, reason: 'NVIDIA TensorRT', available: true })\n const cuda = nodeBackends.find(b => b.id === 'cuda')\n if (cuda) scores.push({ runtime: 'node', backend: 'cuda', format: 'onnx', score: 85, reason: 'NVIDIA CUDA', available: true })\n }\n\n // Intel OpenVINO\n const openvino = pythonBackends.find(b => b.id === 'openvino')\n if (openvino) {\n const score = hardware.npu?.type === 'intel-npu' ? 90 : 80\n scores.push({ runtime: 'python', backend: 'openvino', format: 'openvino', score, reason: 'Intel OpenVINO', available: openvino.available })\n }\n\n // CPU fallbacks\n scores.push({ runtime: 'node', backend: 'cpu', format: 'onnx', score: 50, reason: 'CPU (ONNX Runtime Node)', available: true })\n const pyOnnx = pythonBackends.find(b => b.id === 'onnx-py')\n if (pyOnnx) {\n scores.push({ runtime: 'python', backend: 'cpu', format: 'onnx', score: 45, reason: 'CPU (Python ONNX)', available: pyOnnx.available })\n }\n\n return scores.sort((a, b) => b.score - a.score)\n }\n}\n","import type { PlatformScore, HardwareInfo, ModelRequirement, ResolvedInferenceConfig } from '@camstack/types'\n\nexport class InferenceConfigResolver {\n constructor(\n private readonly scores: readonly PlatformScore[],\n private readonly hardware: HardwareInfo,\n ) {}\n\n /**\n * Compute accuracy/backend weights based on available system RAM.\n * availableRAM_MB is now sourced from systeminformation (reliable cross-platform),\n * not os.freemem() which is broken on macOS.\n *\n * - > 16 GB available: prefer larger, more accurate models (accuracy 0.6, backend 0.4)\n * - > 8 GB available: balanced (accuracy 0.5, backend 0.5)\n * - <= 8 GB available: prefer speed (accuracy 0.4, backend 0.6)\n */\n private getWeights(): { accuracyWeight: number; backendWeight: number } {\n const ramMB = this.hardware.availableRAM_MB\n if (ramMB > 16_384) return { accuracyWeight: 0.6, backendWeight: 0.4 }\n if (ramMB > 8_192) return { accuracyWeight: 0.5, backendWeight: 0.5 }\n return { accuracyWeight: 0.4, backendWeight: 0.6 }\n }\n\n /**\n * Given an addon's model requirements, pick the best model + runtime + backend.\n *\n * Algorithm:\n * 1. Filter models by available RAM (minRAM_MB < 25% of available RAM)\n * 2. For each remaining model, find the best platform score whose format\n * is available in the model's formats\n * 3. Pick the model with the highest combined score using RAM-adaptive weights:\n * - High RAM (>16 GB): accuracy × 0.6 + backend × 0.4 (prefer accuracy)\n * - Mid RAM (>8 GB): accuracy × 0.5 + backend × 0.5 (balanced)\n * - Low RAM (<=8 GB): accuracy × 0.4 + backend × 0.6 (prefer speed)\n */\n resolve(requirements: readonly ModelRequirement[]): ResolvedInferenceConfig {\n if (requirements.length === 0) {\n return { modelId: '', runtime: 'node', backend: 'cpu', format: 'onnx', reason: 'No models declared' }\n }\n\n // Budget: 25% of available RAM (now reliable via systeminformation)\n const ramBudget = this.hardware.availableRAM_MB * 0.25\n const { accuracyWeight, backendWeight } = this.getWeights()\n\n console.log(`[InferenceConfigResolver] availableRAM: ${this.hardware.availableRAM_MB}MB, budget: ${Math.round(ramBudget)}MB, weights: accuracy=${accuracyWeight}, backend=${backendWeight}`)\n\n // Filter models that fit in memory\n const fits = requirements.filter(m => m.minRAM_MB < ramBudget)\n const candidates = fits.length > 0 ? fits : [requirements[0]!] // fallback to first/smallest\n\n console.log(`[InferenceConfigResolver] ${candidates.length}/${requirements.length} models fit RAM budget`)\n\n // For each model, find best compatible platform score\n let bestCombo: { model: ModelRequirement; score: PlatformScore; combined: number } | null = null\n\n for (const model of candidates) {\n for (const score of this.scores) {\n if (!score.available) continue\n if (!model.formats.includes(score.format)) continue\n\n const combined = model.accuracyScore * accuracyWeight + score.score * backendWeight\n\n if (!bestCombo || combined > bestCombo.combined) {\n console.log(`[InferenceConfigResolver] New best: ${model.modelId} (accuracy=${model.accuracyScore}) + ${score.backend}/${score.format} (score=${score.score}) → combined=${Math.round(combined)}`)\n bestCombo = { model, score, combined }\n }\n }\n }\n\n if (!bestCombo) {\n return {\n modelId: candidates[0]!.modelId,\n runtime: 'node',\n backend: 'cpu',\n format: 'onnx',\n reason: 'No compatible backend — CPU fallback',\n }\n }\n\n return {\n modelId: bestCombo.model.modelId,\n runtime: bestCombo.score.runtime,\n backend: bestCombo.score.backend,\n format: bestCombo.score.format,\n reason: `${bestCombo.model.name} on ${bestCombo.score.reason} (score: ${Math.round(bestCombo.combined)})`,\n }\n }\n}\n"],"mappings":"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","names":["errMsg","platform","err","arch","cpus","errMsg"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@camstack/addon-platform-probe-native",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Native platform-probe addon — hardware discovery and inference backend scoring for every cluster node",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"camstack",
|
|
7
|
+
"addon",
|
|
8
|
+
"camstack-addon",
|
|
9
|
+
"platform",
|
|
10
|
+
"inference",
|
|
11
|
+
"hardware"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/camstack/server"
|
|
17
|
+
},
|
|
18
|
+
"main": "./dist/index.js",
|
|
19
|
+
"module": "./dist/index.mjs",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"import": "./dist/index.mjs",
|
|
25
|
+
"require": "./dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./package.json": "./package.json"
|
|
28
|
+
},
|
|
29
|
+
"camstack": {
|
|
30
|
+
"displayName": "Platform Probe (Native)",
|
|
31
|
+
"addons": [
|
|
32
|
+
{
|
|
33
|
+
"id": "platform-probe-native",
|
|
34
|
+
"name": "Platform Probe (Native)",
|
|
35
|
+
"version": "0.1.0",
|
|
36
|
+
"description": "Probes hardware and inference runtimes on the local node. Registers the `platform-probe` capability consumed by every inference addon on this node.",
|
|
37
|
+
"entry": "./dist/index.js",
|
|
38
|
+
"protected": true,
|
|
39
|
+
"capabilities": [
|
|
40
|
+
{
|
|
41
|
+
"name": "platform-probe"
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
"files": [
|
|
48
|
+
"dist"
|
|
49
|
+
],
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsup",
|
|
52
|
+
"dev": "tsup --watch",
|
|
53
|
+
"typecheck": "tsc --noEmit",
|
|
54
|
+
"publish": "npm publish --access public"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"@camstack/types": "^0.1.0"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"systeminformation": "^5.0.0"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@camstack/types": "*",
|
|
64
|
+
"tsup": "^8.0.0",
|
|
65
|
+
"typescript": "~5.9.0",
|
|
66
|
+
"zod": "^4.3.6"
|
|
67
|
+
}
|
|
68
|
+
}
|