@infersec/conduit 1.112.0 → 1.113.0
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/apiClient/index.d.ts +2 -1
- package/dist/cli.js +512 -59
- package/dist/cli.sea.cjs +512 -59
- package/dist/modelManagement/ModelManager.d.ts +6 -0
- package/dist/modelManagement/classifyEngineError.d.ts +12 -0
- package/dist/reporting/engineExecutionReporter.d.ts +54 -0
- package/dist/reporting/index.d.ts +1 -0
- package/dist/requestHandlers/createConduitAnthropicAPIReferenceHandlers.d.ts +1 -1
- package/dist/utils/machineInfo.d.ts +18 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -19972,6 +19972,32 @@ object$5({
|
|
|
19972
19972
|
sizeBytes: number$1().int().nonnegative().nullable()
|
|
19973
19973
|
});
|
|
19974
19974
|
|
|
19975
|
+
const EngineExecutionErrorTypeSchema = _enum$1([
|
|
19976
|
+
"config",
|
|
19977
|
+
"crash",
|
|
19978
|
+
"oom",
|
|
19979
|
+
"prompt",
|
|
19980
|
+
"unknown"
|
|
19981
|
+
]);
|
|
19982
|
+
const EngineExecutionReportPayloadSchema = object$5({
|
|
19983
|
+
avgTps: number$1().nonnegative().finite().default(0),
|
|
19984
|
+
completionTokens: number$1().int().nonnegative().default(0),
|
|
19985
|
+
durationMs: number$1().int().nonnegative().default(0),
|
|
19986
|
+
engineType: LLMEngineSchema.nullable(),
|
|
19987
|
+
engineVersion: string$2().max(64).nullable().default(null),
|
|
19988
|
+
errorDetail: string$2().max(2048).nullable().default(null),
|
|
19989
|
+
errorType: EngineExecutionErrorTypeSchema.nullable(),
|
|
19990
|
+
extraArgs: array$1(tuple([string$2().min(1).max(128), string$2().max(512)]))
|
|
19991
|
+
.max(256)
|
|
19992
|
+
.default([]),
|
|
19993
|
+
finishedAtISO: string$2().datetime({ offset: true }),
|
|
19994
|
+
peakTps: number$1().nonnegative().finite().nullable().default(null),
|
|
19995
|
+
promptTokens: number$1().int().nonnegative().default(0),
|
|
19996
|
+
runAtISO: string$2().datetime({ offset: true }),
|
|
19997
|
+
success: boolean$1(),
|
|
19998
|
+
ttftMs: number$1().int().nonnegative().default(0),
|
|
19999
|
+
totalTokens: number$1().int().nonnegative().default(0)
|
|
20000
|
+
});
|
|
19975
20001
|
const InferenceAgentLLMMetricsPayloadSchema = object$5({
|
|
19976
20002
|
bytes: number$1().int().nonnegative(),
|
|
19977
20003
|
completionTokens: number$1().int().nonnegative(),
|
|
@@ -20310,6 +20336,23 @@ const API_SERVICE_CONDUIT_API_REFERENCE = {
|
|
|
20310
20336
|
}
|
|
20311
20337
|
}
|
|
20312
20338
|
},
|
|
20339
|
+
"/conduit/api/v1/source/:sourceID/engine/execution": {
|
|
20340
|
+
POST: {
|
|
20341
|
+
auth: {
|
|
20342
|
+
type: "api-key"
|
|
20343
|
+
},
|
|
20344
|
+
body: EngineExecutionReportPayloadSchema,
|
|
20345
|
+
parameters: {
|
|
20346
|
+
sourceID: ULIDSchema
|
|
20347
|
+
},
|
|
20348
|
+
response: {
|
|
20349
|
+
schema: object$5({
|
|
20350
|
+
acknowledged: literal(true)
|
|
20351
|
+
}),
|
|
20352
|
+
type: "rest"
|
|
20353
|
+
}
|
|
20354
|
+
}
|
|
20355
|
+
},
|
|
20313
20356
|
"/conduit/api/v1/source/:sourceID/requests/:requestID/chunk": {
|
|
20314
20357
|
POST: {
|
|
20315
20358
|
auth: {
|
|
@@ -113767,6 +113810,19 @@ function createAPIClient({ apiKey, apiURL, inferenceSourceID, logger }) {
|
|
|
113767
113810
|
route: "/conduit/api/v1/source/:sourceID/state"
|
|
113768
113811
|
});
|
|
113769
113812
|
},
|
|
113813
|
+
reportEngineExecution: async (payload) => {
|
|
113814
|
+
await fetchByReference({
|
|
113815
|
+
baseURL: apiURL,
|
|
113816
|
+
body: payload,
|
|
113817
|
+
fetch: fetchWithAPIKey,
|
|
113818
|
+
method: "POST",
|
|
113819
|
+
parameters: {
|
|
113820
|
+
sourceID: inferenceSourceID
|
|
113821
|
+
},
|
|
113822
|
+
reference: API_SERVICE_CONDUIT_API_REFERENCE,
|
|
113823
|
+
route: "/conduit/api/v1/source/:sourceID/engine/execution"
|
|
113824
|
+
});
|
|
113825
|
+
},
|
|
113770
113826
|
reportPromptMetrics: async (payload) => {
|
|
113771
113827
|
await fetchByReference({
|
|
113772
113828
|
baseURL: apiURL,
|
|
@@ -125627,6 +125683,9 @@ class ModelManager extends EventEmitter {
|
|
|
125627
125683
|
lifecycleState = "stopped";
|
|
125628
125684
|
downloadLockHandle = null;
|
|
125629
125685
|
stopRequested = false;
|
|
125686
|
+
lastEngineExitCode = null;
|
|
125687
|
+
lastEngineExitSignal = null;
|
|
125688
|
+
reachedRunningState = false;
|
|
125630
125689
|
modelsDirectory;
|
|
125631
125690
|
constructor({ contextLength, engineConfig, enginePort, engineType, logger, model, root }) {
|
|
125632
125691
|
super();
|
|
@@ -125749,6 +125808,9 @@ class ModelManager extends EventEmitter {
|
|
|
125749
125808
|
this.lifecycleState = "starting";
|
|
125750
125809
|
this.lastEngineError = null;
|
|
125751
125810
|
this.stopRequested = false;
|
|
125811
|
+
this.lastEngineExitCode = null;
|
|
125812
|
+
this.lastEngineExitSignal = null;
|
|
125813
|
+
this.reachedRunningState = false;
|
|
125752
125814
|
this.logger.info("Starting LLM engine", {
|
|
125753
125815
|
agentEngineType: this.engine
|
|
125754
125816
|
});
|
|
@@ -125779,6 +125841,7 @@ class ModelManager extends EventEmitter {
|
|
|
125779
125841
|
throw err;
|
|
125780
125842
|
}
|
|
125781
125843
|
this.lifecycleState = "running";
|
|
125844
|
+
this.reachedRunningState = true;
|
|
125782
125845
|
this.emit("engineReady");
|
|
125783
125846
|
}
|
|
125784
125847
|
async stop() {
|
|
@@ -125803,6 +125866,7 @@ class ModelManager extends EventEmitter {
|
|
|
125803
125866
|
this.lifecycleState = "stopped";
|
|
125804
125867
|
return;
|
|
125805
125868
|
}
|
|
125869
|
+
this.reachedRunningState = false;
|
|
125806
125870
|
this.lifecycleState = "stopping";
|
|
125807
125871
|
this.stopRequested = true;
|
|
125808
125872
|
await processManager.stop();
|
|
@@ -125815,9 +125879,18 @@ class ModelManager extends EventEmitter {
|
|
|
125815
125879
|
this.lifecycleState === "starting" ||
|
|
125816
125880
|
this.lifecycleState === "errored");
|
|
125817
125881
|
}
|
|
125882
|
+
get lastExitCode() {
|
|
125883
|
+
return this.lastEngineExitCode;
|
|
125884
|
+
}
|
|
125885
|
+
get lastExitSignal() {
|
|
125886
|
+
return this.lastEngineExitSignal;
|
|
125887
|
+
}
|
|
125818
125888
|
get state() {
|
|
125819
125889
|
return this.lifecycleState;
|
|
125820
125890
|
}
|
|
125891
|
+
get wasRunning() {
|
|
125892
|
+
return this.reachedRunningState;
|
|
125893
|
+
}
|
|
125821
125894
|
async checkEngineReadiness() {
|
|
125822
125895
|
switch (this.engine) {
|
|
125823
125896
|
case "llama.cpp": {
|
|
@@ -125931,6 +126004,7 @@ class ModelManager extends EventEmitter {
|
|
|
125931
126004
|
if (readiness === "ready") {
|
|
125932
126005
|
this.clearHealthPoll();
|
|
125933
126006
|
this.lifecycleState = "running";
|
|
126007
|
+
this.reachedRunningState = true;
|
|
125934
126008
|
this.emit("engineReady");
|
|
125935
126009
|
}
|
|
125936
126010
|
})
|
|
@@ -125993,6 +126067,8 @@ class ModelManager extends EventEmitter {
|
|
|
125993
126067
|
}));
|
|
125994
126068
|
});
|
|
125995
126069
|
processManager.on("stopped", (code, signal) => {
|
|
126070
|
+
this.lastEngineExitCode = code;
|
|
126071
|
+
this.lastEngineExitSignal = signal;
|
|
125996
126072
|
if (hasTerminated) {
|
|
125997
126073
|
return;
|
|
125998
126074
|
}
|
|
@@ -126060,6 +126136,69 @@ class ModelManager extends EventEmitter {
|
|
|
126060
126136
|
}
|
|
126061
126137
|
}
|
|
126062
126138
|
|
|
126139
|
+
// Ordered most-specific first: a message mentioning a prompt-size failure should classify as
|
|
126140
|
+
// "prompt" even if it also touches memory text; memory outranks config because OOM kills frequently
|
|
126141
|
+
// emit sparse stderr. These are heuristics, not exhaustively enumerated engine vocabularies.
|
|
126142
|
+
const OOM_PATTERNS = [
|
|
126143
|
+
/CUDA out of memory/i,
|
|
126144
|
+
/No available memory for the cache blocks/i,
|
|
126145
|
+
/\bOOMKilled\b/,
|
|
126146
|
+
/out of memory/i,
|
|
126147
|
+
/MemoryError/i,
|
|
126148
|
+
/Cannot allocate memory/i
|
|
126149
|
+
];
|
|
126150
|
+
const PROMPT_PATTERNS = [
|
|
126151
|
+
/prompt is too long/i,
|
|
126152
|
+
/maximum context length exceeded/i,
|
|
126153
|
+
/too many tokens/i
|
|
126154
|
+
];
|
|
126155
|
+
const CONFIG_PATTERNS = [
|
|
126156
|
+
/unrecognized argument/i,
|
|
126157
|
+
/invalid argument/i,
|
|
126158
|
+
/error while loading state_dict/i,
|
|
126159
|
+
/Architecture not understood/i,
|
|
126160
|
+
/No such file or directory/i
|
|
126161
|
+
];
|
|
126162
|
+
/**
|
|
126163
|
+
* Coarse engine-failure classification used only to fill `engine_execution.error_type`. Exit code and
|
|
126164
|
+
* terminating signal are consulted FIRST (SIGKILL/SIGSEGV are reliable OOM signals regardless of
|
|
126165
|
+
* how much stderr the engine produced); the message text then refines the category. Anything
|
|
126166
|
+
* unrecognized is "unknown".
|
|
126167
|
+
*/
|
|
126168
|
+
function classifyEngineFailure({ error, exitCode, signal }) {
|
|
126169
|
+
// 137 = SIGKILL (kernel OOM-killer), 139 = SIGSEGV (illegal memory access). Treat both as OOM
|
|
126170
|
+
// so that memory-pressure deaths do not degrade to "unknown" when stderr is truncated.
|
|
126171
|
+
if (exitCode === 137 || exitCode === 139) {
|
|
126172
|
+
return "oom";
|
|
126173
|
+
}
|
|
126174
|
+
// Direct OS kills (SIGKILL/SIGSEGV) leave no meaningful exit code; honor them like 137/139.
|
|
126175
|
+
if (signal === "SIGKILL" || signal === "SIGSEGV") {
|
|
126176
|
+
return "oom";
|
|
126177
|
+
}
|
|
126178
|
+
const text = `${error.message}`.slice(0, 4000);
|
|
126179
|
+
for (const pattern of PROMPT_PATTERNS) {
|
|
126180
|
+
if (pattern.test(text)) {
|
|
126181
|
+
return "prompt";
|
|
126182
|
+
}
|
|
126183
|
+
}
|
|
126184
|
+
for (const pattern of OOM_PATTERNS) {
|
|
126185
|
+
if (pattern.test(text)) {
|
|
126186
|
+
return "oom";
|
|
126187
|
+
}
|
|
126188
|
+
}
|
|
126189
|
+
for (const pattern of CONFIG_PATTERNS) {
|
|
126190
|
+
if (pattern.test(text)) {
|
|
126191
|
+
return "config";
|
|
126192
|
+
}
|
|
126193
|
+
}
|
|
126194
|
+
// A process that exited abnormally with no recognizable diagnostic: treat as a crash rather
|
|
126195
|
+
// than "unknown" so the two buckets distinguish "we saw nothing" from "it died badly".
|
|
126196
|
+
if (exitCode !== null && exitCode !== 0) {
|
|
126197
|
+
return "crash";
|
|
126198
|
+
}
|
|
126199
|
+
return "unknown";
|
|
126200
|
+
}
|
|
126201
|
+
|
|
126063
126202
|
const EXCEPTION_LINE_PATTERN = /([A-Za-z_][A-Za-z0-9_]*(?:Error|Exception)):\s*(.+)/;
|
|
126064
126203
|
const FALLBACK_DETAIL_MAX_LENGTH = 300;
|
|
126065
126204
|
const FALLBACK_RAW_MAX_LENGTH = 500;
|
|
@@ -137936,12 +138075,20 @@ async function detectVRAMViaSysfs(pciBusSuffix) {
|
|
|
137936
138075
|
const deviceDir = path$1.join(DRM_PATH, entry, "device");
|
|
137937
138076
|
const totalStr = await readSysfsFile(path$1.join(deviceDir, "mem_info_vram_total"));
|
|
137938
138077
|
const usedStr = await readSysfsFile(path$1.join(deviceDir, "mem_info_vram_used"));
|
|
138078
|
+
const gttTotalStr = await readSysfsFile(path$1.join(deviceDir, "mem_info_gtt_total"));
|
|
138079
|
+
const gttUsedStr = await readSysfsFile(path$1.join(deviceDir, "mem_info_gtt_used"));
|
|
137939
138080
|
const totalBytes = totalStr !== null ? parseInt(totalStr, 10) : null;
|
|
137940
138081
|
const usedBytes = usedStr !== null ? parseInt(usedStr, 10) : null;
|
|
138082
|
+
const gttTotalBytes = gttTotalStr !== null ? parseInt(gttTotalStr, 10) : null;
|
|
138083
|
+
const gttUsedBytes = gttUsedStr !== null ? parseInt(gttUsedStr, 10) : null;
|
|
137941
138084
|
const validTotal = totalBytes !== null && Number.isFinite(totalBytes) && totalBytes >= 0;
|
|
137942
138085
|
const validUsed = usedBytes !== null && Number.isFinite(usedBytes) && usedBytes >= 0;
|
|
138086
|
+
const validGttTotal = gttTotalBytes !== null && Number.isFinite(gttTotalBytes) && gttTotalBytes >= 0;
|
|
138087
|
+
const validGttUsed = gttUsedBytes !== null && Number.isFinite(gttUsedBytes) && gttUsedBytes >= 0;
|
|
137943
138088
|
if (validTotal) {
|
|
137944
138089
|
return {
|
|
138090
|
+
gttTotalBytes: validGttTotal ? gttTotalBytes : null,
|
|
138091
|
+
gttUsedBytes: validGttUsed ? gttUsedBytes : null,
|
|
137945
138092
|
memoryTotalBytes: totalBytes,
|
|
137946
138093
|
memoryUsedBytes: validUsed ? usedBytes : null
|
|
137947
138094
|
};
|
|
@@ -137951,36 +138098,74 @@ async function detectVRAMViaSysfs(pciBusSuffix) {
|
|
|
137951
138098
|
catch {
|
|
137952
138099
|
// sysfs not available
|
|
137953
138100
|
}
|
|
137954
|
-
return {
|
|
138101
|
+
return {
|
|
138102
|
+
gttTotalBytes: null,
|
|
138103
|
+
gttUsedBytes: null,
|
|
138104
|
+
memoryTotalBytes: null,
|
|
138105
|
+
memoryUsedBytes: null
|
|
138106
|
+
};
|
|
138107
|
+
}
|
|
138108
|
+
// rocm-smi accepts a single --showmeminfo type per invocation, so VRAM and GTT
|
|
138109
|
+
// arrive from separate calls; both share this per-card parser.
|
|
138110
|
+
function parseRocmSmiMemory({ keyPrefix, stdout }) {
|
|
138111
|
+
const parsed = JSON.parse(stdout);
|
|
138112
|
+
const results = [];
|
|
138113
|
+
const cards = Object.entries(parsed).filter(([key]) => key.startsWith("card"));
|
|
138114
|
+
for (const [, data] of cards) {
|
|
138115
|
+
const bus = data["PCI Bus"] ?? null;
|
|
138116
|
+
const totalStr = data[`${keyPrefix} Total Memory (B)`] ?? null;
|
|
138117
|
+
const usedStr = data[`${keyPrefix} Total Used Memory (B)`] ?? null;
|
|
138118
|
+
const totalBytes = totalStr !== null ? parseInt(totalStr, 10) : null;
|
|
138119
|
+
const usedBytes = usedStr !== null ? parseInt(usedStr, 10) : null;
|
|
138120
|
+
const validTotal = totalBytes !== null && Number.isFinite(totalBytes) && totalBytes >= 0;
|
|
138121
|
+
const validUsed = usedBytes !== null && Number.isFinite(usedBytes) && usedBytes >= 0;
|
|
138122
|
+
if (bus) {
|
|
138123
|
+
results.push({
|
|
138124
|
+
bus,
|
|
138125
|
+
memoryTotalBytes: validTotal ? totalBytes : null,
|
|
138126
|
+
memoryUsedBytes: validUsed ? usedBytes : null
|
|
138127
|
+
});
|
|
138128
|
+
}
|
|
138129
|
+
}
|
|
138130
|
+
return results;
|
|
137955
138131
|
}
|
|
137956
|
-
|
|
138132
|
+
const ROCM_SMI_TIMEOUT_MS = 10_000;
|
|
138133
|
+
async function detectVRAMViaRocmSmi({ logger }) {
|
|
137957
138134
|
try {
|
|
137958
|
-
const
|
|
137959
|
-
"--showbus",
|
|
137960
|
-
|
|
137961
|
-
|
|
137962
|
-
"--json"
|
|
138135
|
+
const [vramResult, gttResult] = await Promise.allSettled([
|
|
138136
|
+
execa("rocm-smi", ["--showbus", "--showmeminfo", "vram", "--json"], {
|
|
138137
|
+
timeout: ROCM_SMI_TIMEOUT_MS
|
|
138138
|
+
}),
|
|
138139
|
+
execa("rocm-smi", ["--showbus", "--showmeminfo", "gtt", "--json"], {
|
|
138140
|
+
timeout: ROCM_SMI_TIMEOUT_MS
|
|
138141
|
+
})
|
|
137963
138142
|
]);
|
|
137964
|
-
|
|
137965
|
-
|
|
137966
|
-
|
|
137967
|
-
|
|
137968
|
-
|
|
137969
|
-
|
|
137970
|
-
|
|
137971
|
-
|
|
137972
|
-
const usedBytes = usedStr !== null ? parseInt(usedStr, 10) : null;
|
|
137973
|
-
const validTotal = totalBytes !== null && Number.isFinite(totalBytes) && totalBytes >= 0;
|
|
137974
|
-
const validUsed = usedBytes !== null && Number.isFinite(usedBytes) && usedBytes >= 0;
|
|
137975
|
-
if (bus) {
|
|
137976
|
-
results.push({
|
|
137977
|
-
bus,
|
|
137978
|
-
memoryTotalBytes: validTotal ? totalBytes : null,
|
|
137979
|
-
memoryUsedBytes: validUsed ? usedBytes : null
|
|
138143
|
+
if (vramResult.status !== "fulfilled")
|
|
138144
|
+
return [];
|
|
138145
|
+
let gttEntries = [];
|
|
138146
|
+
if (gttResult.status === "fulfilled") {
|
|
138147
|
+
try {
|
|
138148
|
+
gttEntries = parseRocmSmiMemory({
|
|
138149
|
+
keyPrefix: "GTT",
|
|
138150
|
+
stdout: gttResult.value.stdout
|
|
137980
138151
|
});
|
|
137981
138152
|
}
|
|
138153
|
+
catch (error) {
|
|
138154
|
+
// Unusable GTT output (e.g. older rocm-smi) degrades to VRAM-only
|
|
138155
|
+
logger.warn("rocm-smi GTT output parse failed", { error: asError(error) });
|
|
138156
|
+
}
|
|
137982
138157
|
}
|
|
137983
|
-
|
|
138158
|
+
const gttByBus = new Map(gttEntries.map(entry => [entry.bus, entry]));
|
|
138159
|
+
return parseRocmSmiMemory({ keyPrefix: "VRAM", stdout: vramResult.value.stdout }).map(vram => {
|
|
138160
|
+
const gtt = gttByBus.get(vram.bus);
|
|
138161
|
+
return {
|
|
138162
|
+
bus: vram.bus,
|
|
138163
|
+
gttTotalBytes: gtt?.memoryTotalBytes ?? null,
|
|
138164
|
+
gttUsedBytes: gtt?.memoryUsedBytes ?? null,
|
|
138165
|
+
memoryTotalBytes: vram.memoryTotalBytes,
|
|
138166
|
+
memoryUsedBytes: vram.memoryUsedBytes
|
|
138167
|
+
};
|
|
138168
|
+
});
|
|
137984
138169
|
}
|
|
137985
138170
|
catch {
|
|
137986
138171
|
return [];
|
|
@@ -138045,8 +138230,9 @@ async function detectGPUsViaNvidiaSmi({ logger }) {
|
|
|
138045
138230
|
vendor: "NVIDIA"
|
|
138046
138231
|
});
|
|
138047
138232
|
}
|
|
138048
|
-
//
|
|
138049
|
-
// memory. Their compute pool is system RAM, so fall back to
|
|
138233
|
+
// Shared/unified-memory devices (e.g. NVIDIA GB10 on DGX Spark) report
|
|
138234
|
+
// "[N/A]" for memory. Their compute pool is system RAM, so fall back to
|
|
138235
|
+
// /proc/meminfo. (AMD iGPUs get the same treatment via GTT merging.)
|
|
138050
138236
|
if (gpus.some(gpu => gpu.memoryTotalBytes === null)) {
|
|
138051
138237
|
const systemMemory = await readSystemMemoryBytes({ logger });
|
|
138052
138238
|
const total = systemMemory.totalBytes;
|
|
@@ -138070,7 +138256,7 @@ async function detectGPUsViaNvidiaSmi({ logger }) {
|
|
|
138070
138256
|
}
|
|
138071
138257
|
}
|
|
138072
138258
|
function buildMergedGPUs(options) {
|
|
138073
|
-
const { lspciGPUs, nvidiaGPUs, rocmVRAM, siGPUs, sysfsVRAMMap } = options;
|
|
138259
|
+
const { lspciGPUs, nvidiaGPUs, rocmVRAM, siGPUs, systemTotalBytes, sysfsVRAMMap } = options;
|
|
138074
138260
|
const rocmByBus = new Map();
|
|
138075
138261
|
for (const entry of rocmVRAM) {
|
|
138076
138262
|
const key = normalizeBusAddress(entry.bus);
|
|
@@ -138111,6 +138297,7 @@ function buildMergedGPUs(options) {
|
|
|
138111
138297
|
gpu: existing,
|
|
138112
138298
|
key,
|
|
138113
138299
|
rocmByBus,
|
|
138300
|
+
systemTotalBytes,
|
|
138114
138301
|
sysfsVRAMMap
|
|
138115
138302
|
});
|
|
138116
138303
|
}
|
|
@@ -138130,30 +138317,73 @@ function buildMergedGPUs(options) {
|
|
|
138130
138317
|
gpu,
|
|
138131
138318
|
key,
|
|
138132
138319
|
rocmByBus,
|
|
138320
|
+
systemTotalBytes,
|
|
138133
138321
|
sysfsVRAMMap
|
|
138134
138322
|
});
|
|
138135
138323
|
byBus.set(key, gpu);
|
|
138136
138324
|
}
|
|
138137
138325
|
return [...byBus.values()];
|
|
138138
138326
|
}
|
|
138139
|
-
|
|
138327
|
+
// Shared-memory GPUs (AMD iGPUs) expose a small dedicated VRAM carve-out via
|
|
138328
|
+
// mem_info_vram_total while the real usable pool - GTT - is carved dynamically
|
|
138329
|
+
// from system RAM. When GTT exceeds VRAM the device is treated as integrated
|
|
138330
|
+
// and the two pools combine (capped by installed RAM). Note that GTT defaults
|
|
138331
|
+
// to half of system RAM even on discrete GPUs, so a dGPU with less VRAM than
|
|
138332
|
+
// that also merges - acceptable, since GTT remains real addressable memory
|
|
138333
|
+
// for amdgpu compute (albeit slower over PCIe).
|
|
138334
|
+
function mergeGTTMemory({ gttTotalBytes, gttUsedBytes, systemTotalBytes, vramTotalBytes, vramUsedBytes }) {
|
|
138335
|
+
if (vramTotalBytes === null || !Number.isFinite(vramTotalBytes)) {
|
|
138336
|
+
return { memoryTotalBytes: null, memoryUsedBytes: null };
|
|
138337
|
+
}
|
|
138338
|
+
const isIntegrated = gttTotalBytes !== null && gttTotalBytes > vramTotalBytes;
|
|
138339
|
+
if (!isIntegrated) {
|
|
138340
|
+
return { memoryTotalBytes: vramTotalBytes, memoryUsedBytes: vramUsedBytes };
|
|
138341
|
+
}
|
|
138342
|
+
const combined = vramTotalBytes + gttTotalBytes;
|
|
138343
|
+
const capped = systemTotalBytes !== null && systemTotalBytes > 0
|
|
138344
|
+
? Math.min(combined, systemTotalBytes)
|
|
138345
|
+
: combined;
|
|
138346
|
+
const hasCompleteUsage = vramUsedBytes !== null &&
|
|
138347
|
+
Number.isFinite(vramUsedBytes) &&
|
|
138348
|
+
gttUsedBytes !== null &&
|
|
138349
|
+
Number.isFinite(gttUsedBytes);
|
|
138350
|
+
const memoryUsedBytes = hasCompleteUsage
|
|
138351
|
+
? Math.min(vramUsedBytes + gttUsedBytes, capped)
|
|
138352
|
+
: null;
|
|
138353
|
+
return { memoryTotalBytes: capped, memoryUsedBytes };
|
|
138354
|
+
}
|
|
138355
|
+
function applySysfsOrRocmVRAM({ gpu, key, rocmByBus, systemTotalBytes, sysfsVRAMMap }) {
|
|
138140
138356
|
const sysfs = sysfsVRAMMap.get(key);
|
|
138141
|
-
let
|
|
138142
|
-
let
|
|
138143
|
-
|
|
138357
|
+
let vramTotalBytes = sysfs?.memoryTotalBytes ?? null;
|
|
138358
|
+
let vramUsedBytes = sysfs?.memoryUsedBytes ?? null;
|
|
138359
|
+
let gttTotalBytes = sysfs?.gttTotalBytes ?? null;
|
|
138360
|
+
let gttUsedBytes = sysfs?.gttUsedBytes ?? null;
|
|
138361
|
+
if (vramTotalBytes === null) {
|
|
138144
138362
|
const rocm = rocmByBus.get(key);
|
|
138145
138363
|
if (rocm) {
|
|
138146
|
-
|
|
138147
|
-
|
|
138148
|
-
|
|
138149
|
-
|
|
138150
|
-
|
|
138364
|
+
vramTotalBytes = rocm.memoryTotalBytes;
|
|
138365
|
+
vramUsedBytes = rocm.memoryUsedBytes;
|
|
138366
|
+
gttTotalBytes = rocm.gttTotalBytes;
|
|
138367
|
+
gttUsedBytes = rocm.gttUsedBytes;
|
|
138368
|
+
}
|
|
138369
|
+
}
|
|
138370
|
+
const merged = mergeGTTMemory({
|
|
138371
|
+
gttTotalBytes,
|
|
138372
|
+
gttUsedBytes,
|
|
138373
|
+
systemTotalBytes,
|
|
138374
|
+
vramTotalBytes,
|
|
138375
|
+
vramUsedBytes
|
|
138376
|
+
});
|
|
138377
|
+
if (merged.memoryTotalBytes === null || !Number.isFinite(merged.memoryTotalBytes))
|
|
138151
138378
|
return;
|
|
138152
|
-
gpu.memoryTotalBytes =
|
|
138153
|
-
gpu.memoryUsedBytes =
|
|
138379
|
+
gpu.memoryTotalBytes = merged.memoryTotalBytes;
|
|
138380
|
+
gpu.memoryUsedBytes =
|
|
138381
|
+
merged.memoryUsedBytes !== null && Number.isFinite(merged.memoryUsedBytes)
|
|
138382
|
+
? merged.memoryUsedBytes
|
|
138383
|
+
: null;
|
|
138154
138384
|
gpu.memoryFreeBytes =
|
|
138155
|
-
|
|
138156
|
-
? Math.max(
|
|
138385
|
+
gpu.memoryUsedBytes !== null
|
|
138386
|
+
? Math.max(gpu.memoryTotalBytes - gpu.memoryUsedBytes, 0)
|
|
138157
138387
|
: null;
|
|
138158
138388
|
}
|
|
138159
138389
|
async function collectMachineMetadata({ logger }) {
|
|
@@ -138164,7 +138394,7 @@ async function collectMachineMetadata({ logger }) {
|
|
|
138164
138394
|
si.graphics(),
|
|
138165
138395
|
detectGPUsViaLspci(),
|
|
138166
138396
|
detectGPUsViaNvidiaSmi({ logger }),
|
|
138167
|
-
detectVRAMViaRocmSmi()
|
|
138397
|
+
detectVRAMViaRocmSmi({ logger })
|
|
138168
138398
|
]);
|
|
138169
138399
|
const cpuInfo = cpuResult.status === "fulfilled" ? cpuResult.value : null;
|
|
138170
138400
|
const memInfo = memResult.status === "fulfilled" ? memResult.value : null;
|
|
@@ -138219,6 +138449,7 @@ async function collectMachineMetadata({ logger }) {
|
|
|
138219
138449
|
nvidiaGPUs: resolvedNvidiaGPUs,
|
|
138220
138450
|
rocmVRAM: resolvedRocmVRAM,
|
|
138221
138451
|
siGPUs,
|
|
138452
|
+
systemTotalBytes: memInfo?.total ?? null,
|
|
138222
138453
|
sysfsVRAMMap
|
|
138223
138454
|
});
|
|
138224
138455
|
const machineMetadata = {
|
|
@@ -138279,6 +138510,119 @@ async function detectDockerVersion() {
|
|
|
138279
138510
|
}
|
|
138280
138511
|
}
|
|
138281
138512
|
|
|
138513
|
+
/**
|
|
138514
|
+
* Flattens flat CLI extra-arg tokens into [arg, value] pairs, sorted by ARG NAME (ascending, ties by
|
|
138515
|
+
* value). `--flag=value` pairs split on the first `=`; a bare `--flag` consumes the following token
|
|
138516
|
+
* as its value when that token does not start with "-" (classic CLI convention); anything else
|
|
138517
|
+
* (flags, non-strings) is dropped.
|
|
138518
|
+
*/
|
|
138519
|
+
function pairExtraArgs(tokens) {
|
|
138520
|
+
if (!Array.isArray(tokens)) {
|
|
138521
|
+
return [];
|
|
138522
|
+
}
|
|
138523
|
+
const list = tokens;
|
|
138524
|
+
const pairs = [];
|
|
138525
|
+
let index = 0;
|
|
138526
|
+
while (index < list.length) {
|
|
138527
|
+
const token = list[index];
|
|
138528
|
+
if (typeof token !== "string" || token.length === 0 || !token.startsWith("-")) {
|
|
138529
|
+
index++;
|
|
138530
|
+
continue;
|
|
138531
|
+
}
|
|
138532
|
+
const separator = token.indexOf("=");
|
|
138533
|
+
if (separator > -1) {
|
|
138534
|
+
const arg = token.slice(0, separator);
|
|
138535
|
+
if (arg.length > 0) {
|
|
138536
|
+
pairs.push([arg, token.slice(separator + 1)]);
|
|
138537
|
+
}
|
|
138538
|
+
index++;
|
|
138539
|
+
continue;
|
|
138540
|
+
}
|
|
138541
|
+
const next = list[index + 1];
|
|
138542
|
+
const consumesNext = typeof next === "string" && next.length > 0 && !next.startsWith("-");
|
|
138543
|
+
if (consumesNext) {
|
|
138544
|
+
pairs.push([token, next]);
|
|
138545
|
+
index += 2;
|
|
138546
|
+
}
|
|
138547
|
+
else {
|
|
138548
|
+
pairs.push([token, ""]);
|
|
138549
|
+
index++;
|
|
138550
|
+
}
|
|
138551
|
+
}
|
|
138552
|
+
return pairs.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0);
|
|
138553
|
+
}
|
|
138554
|
+
/**
|
|
138555
|
+
* Files AT MOST ONE engine_execution report per engine startup. `beginStartup(runAt)` re-arms the
|
|
138556
|
+
* latch on every fresh model start (initial boot or cycle) and stamps the startup epoch used as the
|
|
138557
|
+
* row's `run_at`. Whichever of the three report* paths fires first wins: startup failure, in-flight
|
|
138558
|
+
* crash, or first successful full prompt completion.
|
|
138559
|
+
*/
|
|
138560
|
+
class EngineExecutionReporter {
|
|
138561
|
+
options;
|
|
138562
|
+
currentStartupAt = null;
|
|
138563
|
+
reportedForCurrentStartup = false;
|
|
138564
|
+
constructor(options) {
|
|
138565
|
+
this.options = options;
|
|
138566
|
+
}
|
|
138567
|
+
/** Re-arms the latch; the stamp becomes the row's `run_at` (moment startup began). */
|
|
138568
|
+
beginStartup(runAt) {
|
|
138569
|
+
this.currentStartupAt = runAt;
|
|
138570
|
+
this.reportedForCurrentStartup = false;
|
|
138571
|
+
}
|
|
138572
|
+
/** Reports a startup failure (rejected prepare/start, readiness timeout, pre-ready death). */
|
|
138573
|
+
async reportStartupFailure(report) {
|
|
138574
|
+
await this.file(report, false);
|
|
138575
|
+
}
|
|
138576
|
+
/** Reports a spontaneous crash of an engine that had reached the running state. */
|
|
138577
|
+
async reportRuntimeCrash(report) {
|
|
138578
|
+
await this.file(report, false);
|
|
138579
|
+
}
|
|
138580
|
+
/** Reports the first fully-responded, token-bearing prompt completion since startup. */
|
|
138581
|
+
async reportSuccess(report) {
|
|
138582
|
+
await this.file(report, true);
|
|
138583
|
+
}
|
|
138584
|
+
async file(report, success) {
|
|
138585
|
+
if (this.reportedForCurrentStartup || this.currentStartupAt === null) {
|
|
138586
|
+
return;
|
|
138587
|
+
}
|
|
138588
|
+
// Latch BEFORE the network call: a thrown POST cannot double-file for this startup.
|
|
138589
|
+
this.reportedForCurrentStartup = true;
|
|
138590
|
+
const context = this.options.buildContext();
|
|
138591
|
+
const payload = {
|
|
138592
|
+
avgTps: report.throughput.avgTps,
|
|
138593
|
+
completionTokens: report.usage.completionTokens,
|
|
138594
|
+
durationMs: report.durationMs,
|
|
138595
|
+
engineType: context.engineType,
|
|
138596
|
+
engineVersion: context.engineVersion,
|
|
138597
|
+
errorDetail: success ? null : report.errorDetail,
|
|
138598
|
+
errorType: success ? null : report.errorType,
|
|
138599
|
+
extraArgs: context.extraArgsPairs,
|
|
138600
|
+
finishedAtISO: new Date().toISOString(),
|
|
138601
|
+
peakTps: report.throughput.peakTps,
|
|
138602
|
+
promptTokens: report.usage.promptTokens,
|
|
138603
|
+
runAtISO: this.currentStartupAt.toISOString(),
|
|
138604
|
+
success,
|
|
138605
|
+
ttftMs: report.ttftMs,
|
|
138606
|
+
totalTokens: report.usage.totalTokens
|
|
138607
|
+
};
|
|
138608
|
+
try {
|
|
138609
|
+
await this.options.report(payload);
|
|
138610
|
+
this.options.logger.info("Engine execution outcome reported", {
|
|
138611
|
+
inferenceSourceID: this.options.sourceLabel,
|
|
138612
|
+
success
|
|
138613
|
+
});
|
|
138614
|
+
}
|
|
138615
|
+
catch (error) {
|
|
138616
|
+
// Losing one report is preferable to filing two; the latch stays latched.
|
|
138617
|
+
this.options.logger.warn("Failed to report engine execution outcome", {
|
|
138618
|
+
error: asError(error),
|
|
138619
|
+
inferenceSourceID: this.options.sourceLabel,
|
|
138620
|
+
success
|
|
138621
|
+
});
|
|
138622
|
+
}
|
|
138623
|
+
}
|
|
138624
|
+
}
|
|
138625
|
+
|
|
138282
138626
|
async function createApplication({ abortController, apiClient, configuration, logger }) {
|
|
138283
138627
|
ensureDockerValidEnv();
|
|
138284
138628
|
logger.info("Fetching conduit configuration");
|
|
@@ -138310,6 +138654,87 @@ async function createApplication({ abortController, apiClient, configuration, lo
|
|
|
138310
138654
|
error: asError(error)
|
|
138311
138655
|
});
|
|
138312
138656
|
}
|
|
138657
|
+
const reporter = new EngineExecutionReporter({
|
|
138658
|
+
buildContext: () => {
|
|
138659
|
+
const engineType = (conduitConfiguration.engineConfig?.type ??
|
|
138660
|
+
"llama.cpp");
|
|
138661
|
+
const versions = {
|
|
138662
|
+
exllamav3: machine?.exllamav3Version ?? null,
|
|
138663
|
+
"llama.cpp": machine?.llamaCppVersion ?? null,
|
|
138664
|
+
"mlx-lm": machine?.mlxlmVersion ?? null,
|
|
138665
|
+
sglang: machine?.sglangVersion ?? null,
|
|
138666
|
+
"tensorrt-llm": machine?.tensorrtLlmVersion ?? null,
|
|
138667
|
+
vllm: machine?.vllmVersion ?? null
|
|
138668
|
+
};
|
|
138669
|
+
return {
|
|
138670
|
+
engineType,
|
|
138671
|
+
engineVersion: versions[engineType] ?? null,
|
|
138672
|
+
extraArgsPairs: pairExtraArgs(conduitConfiguration.engineConfig?.extraArgs)
|
|
138673
|
+
};
|
|
138674
|
+
},
|
|
138675
|
+
logger,
|
|
138676
|
+
report: payload => apiClient.reportEngineExecution(payload),
|
|
138677
|
+
sourceLabel: configuration.inferenceSourceID
|
|
138678
|
+
});
|
|
138679
|
+
// Intercept the prompt-metrics chokepoint so the first fully-responded, token-bearing prompt of
|
|
138680
|
+
// each fresh startup files the one-shot engine_execution success report. Handlers close over the
|
|
138681
|
+
// SAME `apiClient` object and read `reportPromptMetrics` at request-dispatch time (which always
|
|
138682
|
+
// follows this point), so the wrapped method is what they invoke.
|
|
138683
|
+
const rawReportPromptMetrics = apiClient.reportPromptMetrics;
|
|
138684
|
+
apiClient.reportPromptMetrics = async (payload) => {
|
|
138685
|
+
if (payload.successful && payload.completionTokens > 0 && payload.latencyMs > 0) {
|
|
138686
|
+
// The one-shot report is kicked off and its settlement attached HERE (before any await): if the
|
|
138687
|
+
// metrics path throws below, the report promise must still be able to log its own rejection.
|
|
138688
|
+
const successReport = reporter
|
|
138689
|
+
.reportSuccess({
|
|
138690
|
+
durationMs: payload.latencyMs,
|
|
138691
|
+
errorDetail: null,
|
|
138692
|
+
errorType: null,
|
|
138693
|
+
throughput: {
|
|
138694
|
+
avgTps: payload.tokensPerSecond,
|
|
138695
|
+
peakTps: null
|
|
138696
|
+
},
|
|
138697
|
+
ttftMs: payload.timeToFirstTokenMs ?? 0,
|
|
138698
|
+
usage: {
|
|
138699
|
+
completionTokens: payload.completionTokens,
|
|
138700
|
+
promptTokens: payload.promptTokens,
|
|
138701
|
+
totalTokens: payload.totalTokens
|
|
138702
|
+
}
|
|
138703
|
+
})
|
|
138704
|
+
.catch(error => {
|
|
138705
|
+
logger.warn("Engine execution success report failed", {
|
|
138706
|
+
error: asError(error)
|
|
138707
|
+
});
|
|
138708
|
+
});
|
|
138709
|
+
await rawReportPromptMetrics(payload);
|
|
138710
|
+
await successReport;
|
|
138711
|
+
return;
|
|
138712
|
+
}
|
|
138713
|
+
await rawReportPromptMetrics(payload);
|
|
138714
|
+
};
|
|
138715
|
+
const SECRET_ARG_MASK_PATTERN = /(-{1,2}[A-Za-z0-9_.]*(?:api[-_]?key|hf[-_]?token|token)(?:\s+|[=:]))\S+/gi;
|
|
138716
|
+
// Assembles the payload shared by the startup-failure and runtime-crash report paths. Stderr
|
|
138717
|
+
// may echo secrets, so mask `--api-key`/token-looking args before they reach the DB.
|
|
138718
|
+
function buildCrashReport(error, exitCode, signal) {
|
|
138719
|
+
const classification = classifyEngineFailure({ error, exitCode, signal });
|
|
138720
|
+
const raw = normalizeEngineError(error.message);
|
|
138721
|
+
const masked = raw.replace(SECRET_ARG_MASK_PATTERN, "$1***");
|
|
138722
|
+
return {
|
|
138723
|
+
durationMs: 0,
|
|
138724
|
+
errorDetail: masked.slice(0, 2048),
|
|
138725
|
+
errorType: classification,
|
|
138726
|
+
ttftMs: 0,
|
|
138727
|
+
throughput: {
|
|
138728
|
+
avgTps: 0,
|
|
138729
|
+
peakTps: null
|
|
138730
|
+
},
|
|
138731
|
+
usage: {
|
|
138732
|
+
completionTokens: 0,
|
|
138733
|
+
promptTokens: 0,
|
|
138734
|
+
totalTokens: 0
|
|
138735
|
+
}
|
|
138736
|
+
};
|
|
138737
|
+
}
|
|
138313
138738
|
const conduitStateManager = new ConduitStateManager({
|
|
138314
138739
|
initialState: {
|
|
138315
138740
|
state: "initialising"
|
|
@@ -138361,6 +138786,17 @@ async function createApplication({ abortController, apiClient, configuration, lo
|
|
|
138361
138786
|
});
|
|
138362
138787
|
stopRequestedByControl = false;
|
|
138363
138788
|
setErrorState({ error: normalizeEngineError(err.message) });
|
|
138789
|
+
// Spontaneous death of a SERVING engine → crash report, suppressed by the latch if the
|
|
138790
|
+
// startup's one-shot outcome was already filed. Startup-path failures report from
|
|
138791
|
+
// `startEngine`'s catch; this listener is the RUNTIME-crash path only.
|
|
138792
|
+
if (modelManager.wasRunning && !err.message.includes("interrupted by stop request")) {
|
|
138793
|
+
const crashReport = buildCrashReport(err, modelManager.lastExitCode, modelManager.lastExitSignal);
|
|
138794
|
+
reporter.reportRuntimeCrash(crashReport).catch(crashReportError => {
|
|
138795
|
+
logger.warn("Engine execution crash report failed", {
|
|
138796
|
+
error: asError(crashReportError)
|
|
138797
|
+
});
|
|
138798
|
+
});
|
|
138799
|
+
}
|
|
138364
138800
|
});
|
|
138365
138801
|
modelManager.on("engineReady", () => {
|
|
138366
138802
|
setOnlineState();
|
|
@@ -138420,24 +138856,40 @@ async function createApplication({ abortController, apiClient, configuration, lo
|
|
|
138420
138856
|
};
|
|
138421
138857
|
async function startEngine() {
|
|
138422
138858
|
logger.info("Engine start requested");
|
|
138423
|
-
|
|
138424
|
-
|
|
138425
|
-
|
|
138426
|
-
|
|
138427
|
-
|
|
138428
|
-
|
|
138429
|
-
|
|
138859
|
+
reporter.beginStartup(new Date());
|
|
138860
|
+
try {
|
|
138861
|
+
conduitStateManager.setState({
|
|
138862
|
+
modelFileName,
|
|
138863
|
+
modelName,
|
|
138864
|
+
state: "downloadingModelFiles",
|
|
138865
|
+
totalProgress: {
|
|
138866
|
+
file: 0,
|
|
138867
|
+
total: 0
|
|
138868
|
+
}
|
|
138869
|
+
});
|
|
138870
|
+
await conduitStateReportManager.reportNow();
|
|
138871
|
+
await modelManager.prepare({
|
|
138872
|
+
onDownloadProgress: reportDownloadProgress
|
|
138873
|
+
});
|
|
138874
|
+
conduitStateManager.setState({
|
|
138875
|
+
state: "bootingEngine"
|
|
138876
|
+
});
|
|
138877
|
+
await conduitStateReportManager.reportNow();
|
|
138878
|
+
await modelManager.start();
|
|
138879
|
+
}
|
|
138880
|
+
catch (error) {
|
|
138881
|
+
const parsedError = asError(error);
|
|
138882
|
+
// Operator-initiated aborts are not startup failures worth reporting.
|
|
138883
|
+
if (!parsedError.message.includes("interrupted by stop request")) {
|
|
138884
|
+
const startupReport = buildCrashReport(parsedError, modelManager.lastExitCode, modelManager.lastExitSignal);
|
|
138885
|
+
reporter.reportStartupFailure(startupReport).catch(startupReportError => {
|
|
138886
|
+
logger.warn("Engine execution startup report failed", {
|
|
138887
|
+
error: asError(startupReportError)
|
|
138888
|
+
});
|
|
138889
|
+
});
|
|
138430
138890
|
}
|
|
138431
|
-
|
|
138432
|
-
|
|
138433
|
-
await modelManager.prepare({
|
|
138434
|
-
onDownloadProgress: reportDownloadProgress
|
|
138435
|
-
});
|
|
138436
|
-
conduitStateManager.setState({
|
|
138437
|
-
state: "bootingEngine"
|
|
138438
|
-
});
|
|
138439
|
-
await conduitStateReportManager.reportNow();
|
|
138440
|
-
await modelManager.start();
|
|
138891
|
+
throw error;
|
|
138892
|
+
}
|
|
138441
138893
|
}
|
|
138442
138894
|
async function stopEngine({ reason }) {
|
|
138443
138895
|
if (!modelManager.canStop) {
|
|
@@ -342892,12 +343344,13 @@ async function runModelFit(options) {
|
|
|
342892
343344
|
console.log();
|
|
342893
343345
|
const feasible = filterFeasibleModels({ detection, models: recommendedModels });
|
|
342894
343346
|
if (feasible.length === 0) {
|
|
343347
|
+
const smallestTierGB = Math.min(...recommendedModels.map(model => model.vramTierGB));
|
|
342895
343348
|
console.error("No recommended models fit this hardware. " +
|
|
342896
343349
|
`Budget: ${formatBytes$1(detection.gpus.some(gpu => gpu.memoryTotalBytes)
|
|
342897
343350
|
? Math.max(...detection.gpus
|
|
342898
343351
|
.map(gpu => gpu.memoryTotalBytes ?? 0)
|
|
342899
343352
|
.filter(bytes => bytes > 0))
|
|
342900
|
-
: detection.memory.totalBytes)}. Smallest tier starts at
|
|
343353
|
+
: detection.memory.totalBytes)}. Smallest tier starts at ${smallestTierGB} GB.`);
|
|
342901
343354
|
process.exitCode = 1;
|
|
342902
343355
|
return;
|
|
342903
343356
|
}
|