@otto-code/brain 0.8.12 → 0.8.13
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/commands/bench.js +2 -2
- package/dist/commands/calibrate.js +8 -8
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +1 -1
- package/dist/config/profile-edit.js +16 -16
- package/dist/config/profiles.d.ts +19 -3
- package/dist/config/profiles.js +36 -4
- package/dist/config/schema.d.ts +8 -0
- package/dist/config/schema.js +7 -3
- package/dist/ops/archive.d.ts +14 -1
- package/dist/ops/archive.js +9 -5
- package/dist/ops/results.d.ts +21 -1
- package/dist/ops/results.js +10 -5
- package/dist/service/host-api.d.ts +7 -4
- package/dist/service/host-api.js +31 -16
- package/dist/service/process-pool.d.ts +45 -0
- package/dist/service/process-pool.js +271 -0
- package/dist/service/router.d.ts +7 -4
- package/dist/service/router.js +60 -32
- package/dist/service/scheduler.d.ts +26 -10
- package/dist/service/scheduler.js +14 -1
- package/dist/service/serve.js +107 -58
- package/dist/service/status-events.js +1 -0
- package/dist/service/supervisor.js +2 -2
- package/dist/tui/app.js +18 -9
- package/package.json +1 -1
package/dist/commands/bench.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import http from "node:http";
|
|
10
10
|
import path from "node:path";
|
|
11
|
-
import { forModel,
|
|
11
|
+
import { forModel, getCalibrationForBudget, loadBrainConfig, loadProfilesStore, resolveBrainPaths, } from "../config/index.js";
|
|
12
12
|
import { query as queryGpu } from "../gpu.js";
|
|
13
13
|
import { pickModel, scanModels } from "../models/index.js";
|
|
14
14
|
import { CommandError } from "../output/types.js";
|
|
@@ -172,7 +172,7 @@ async function runBenchSuite(options, _command) {
|
|
|
172
172
|
// that ran is the profile that was configured, and the calibration is what
|
|
173
173
|
// says whether the fit's own VRAM figures were measured or guessed. A score
|
|
174
174
|
// read without them is a score nobody can diagnose.
|
|
175
|
-
const calibration =
|
|
175
|
+
const calibration = getCalibrationForBudget(store, model, profile);
|
|
176
176
|
let fit = null;
|
|
177
177
|
const gpu = await queryGpu();
|
|
178
178
|
if (gpu) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getCalibrationForBudget, forModel, loadBrainConfig, loadProfilesStore, putCalibration, saveProfilesStore, } from "../config/index.js";
|
|
2
2
|
import { query as queryGpu } from "../gpu.js";
|
|
3
3
|
import { pickModel, scanModels } from "../models/index.js";
|
|
4
4
|
import { CommandError } from "../output/types.js";
|
|
@@ -35,12 +35,12 @@ export async function runCalibrateCommand(options, _command) {
|
|
|
35
35
|
const catalog = scanModels(config);
|
|
36
36
|
const model = pickModel(catalog, options.model ?? store.lastModelId ?? undefined);
|
|
37
37
|
const profile = forModel(store, model, config.defaults);
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
const prior =
|
|
38
|
+
// The last direct measurement is the best prior on bytes/token available for
|
|
39
|
+
// capping the next sample. It keeps the calibration path stable while a
|
|
40
|
+
// changed profile is awaiting a fresh measurement. Inherited family
|
|
41
|
+
// calibrations are excluded: they were measured on another file and are not
|
|
42
|
+
// a trustworthy budget input here.
|
|
43
|
+
const prior = getCalibrationForBudget(store, model, profile);
|
|
44
44
|
// Announced so the Brain rail can show the host as busy: a calibrate loads the
|
|
45
45
|
// model at several context sizes and will make anything else queue behind it.
|
|
46
46
|
const measurement = await withActivity("calibrate", { target: model.displayName }, () => calibrate({
|
|
@@ -66,7 +66,7 @@ export async function runCalibrateCommand(options, _command) {
|
|
|
66
66
|
? vram.maxContextThatFits({
|
|
67
67
|
model,
|
|
68
68
|
profile,
|
|
69
|
-
calibration:
|
|
69
|
+
calibration: getCalibrationForBudget(store, model, profile),
|
|
70
70
|
totalVramBytes: gpu.totalBytes,
|
|
71
71
|
})
|
|
72
72
|
: null;
|
package/dist/config/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { ensurePrivateDirectory, writePrivateFileAtomicSync } from "./private-fi
|
|
|
4
4
|
export { resolveBrainPaths, packageRoot, type BrainPaths } from "./paths.js";
|
|
5
5
|
export { parseBooleanEnv, applyEnvOverrides } from "./env.js";
|
|
6
6
|
export { loadBrainConfig, loadPersistedConfig, saveBrainConfig, loadProfilesStore, saveProfilesStore, loadCatalog, } from "./store.js";
|
|
7
|
-
export { defaultProfile, forModel, put, calibrationKey, geometryKey, getCalibration, putCalibration, hasStaleCalibration, } from "./profiles.js";
|
|
7
|
+
export { defaultProfile, forModel, put, calibrationKey, geometryKey, getCalibration, getCalibrationForBudget, getLastCalibration, putCalibration, hasStaleCalibration, } from "./profiles.js";
|
|
8
8
|
export { effectiveHostingProfile, resolveHostingProfileForLaunch } from "./hosting-profiles.js";
|
|
9
9
|
export { calibrationInfo, nativeContextLimit, profileFieldDescriptors, profileWarnings, sanitizeProfilePatch, formatReasoningBudget, CACHE_TYPE_CYCLE, REASONING_BUDGET_CYCLE, UNRESTRICTED_REASONING_BUDGET, PRESERVE_REASONING_CYCLE, SAMPLING_RANGES, preserveReasoningFromOption, preserveReasoningOption, type CalibrationInfo, type CalibrationState, type ProfileFieldDescriptor, type ProfileWarning, } from "./profile-edit.js";
|
|
10
10
|
export * from "./schema.js";
|
package/dist/config/index.js
CHANGED
|
@@ -4,7 +4,7 @@ export { ensurePrivateDirectory, writePrivateFileAtomicSync } from "./private-fi
|
|
|
4
4
|
export { resolveBrainPaths, packageRoot } from "./paths.js";
|
|
5
5
|
export { parseBooleanEnv, applyEnvOverrides } from "./env.js";
|
|
6
6
|
export { loadBrainConfig, loadPersistedConfig, saveBrainConfig, loadProfilesStore, saveProfilesStore, loadCatalog, } from "./store.js";
|
|
7
|
-
export { defaultProfile, forModel, put, calibrationKey, geometryKey, getCalibration, putCalibration, hasStaleCalibration, } from "./profiles.js";
|
|
7
|
+
export { defaultProfile, forModel, put, calibrationKey, geometryKey, getCalibration, getCalibrationForBudget, getLastCalibration, putCalibration, hasStaleCalibration, } from "./profiles.js";
|
|
8
8
|
export { effectiveHostingProfile, resolveHostingProfileForLaunch } from "./hosting-profiles.js";
|
|
9
9
|
export { calibrationInfo, nativeContextLimit, profileFieldDescriptors, profileWarnings, sanitizeProfilePatch, formatReasoningBudget, CACHE_TYPE_CYCLE, REASONING_BUDGET_CYCLE, UNRESTRICTED_REASONING_BUDGET, PRESERVE_REASONING_CYCLE, SAMPLING_RANGES, preserveReasoningFromOption, preserveReasoningOption, } from "./profile-edit.js";
|
|
10
10
|
export * from "./schema.js";
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import os from "node:os";
|
|
21
21
|
import { CACHE_TYPE_BYTES, formatGiB, promptCacheSize } from "../vram.js";
|
|
22
|
-
import { getCalibration, hasStaleCalibration } from "./profiles.js";
|
|
22
|
+
import { getCalibration, getCalibrationForBudget, hasStaleCalibration } from "./profiles.js";
|
|
23
23
|
/**
|
|
24
24
|
* Sampler ranges. llama.cpp itself bounds almost none of these - it will take a
|
|
25
25
|
* temperature of 50 - so the bounds are the useful range rather than the legal
|
|
@@ -329,8 +329,10 @@ export function profileWarnings(profile, model, store) {
|
|
|
329
329
|
// A count above 0 can only be priced from a measurement: without one the
|
|
330
330
|
// theoretical KV cost runs to multiples of the real one, and naming a figure
|
|
331
331
|
// derived from it would invite reserving several times the RAM actually
|
|
332
|
-
// needed.
|
|
333
|
-
|
|
332
|
+
// needed. Once a model has been measured, keep its last value while edits are
|
|
333
|
+
// pending recalibration; the stale calibration state makes the reduced trust
|
|
334
|
+
// visible without making the number jump.
|
|
335
|
+
const calibration = model && store ? getCalibrationForBudget(store, model, profile) : null;
|
|
334
336
|
const cache = model ? promptCacheSize(model, profile, calibration) : null;
|
|
335
337
|
const hasCount = (profile.cachedChats ?? 0) > 0;
|
|
336
338
|
if (hasCount && (!cache || cache.source !== "measured")) {
|
|
@@ -359,7 +361,7 @@ export function profileWarnings(profile, model, store) {
|
|
|
359
361
|
warnings.push({
|
|
360
362
|
field: "contextMultiplier",
|
|
361
363
|
severity: "warn",
|
|
362
|
-
message:
|
|
364
|
+
message: "YaRN extrapolates beyond the native context.",
|
|
363
365
|
blocksStart: false,
|
|
364
366
|
});
|
|
365
367
|
}
|
|
@@ -370,7 +372,7 @@ export function profileWarnings(profile, model, store) {
|
|
|
370
372
|
warnings.push({
|
|
371
373
|
field: "cacheTypeK",
|
|
372
374
|
severity: "info",
|
|
373
|
-
message: "Cache types changed since the last measurement.
|
|
375
|
+
message: "Cache types changed since the last measurement.",
|
|
374
376
|
blocksStart: false,
|
|
375
377
|
});
|
|
376
378
|
}
|
|
@@ -382,24 +384,22 @@ export function profileWarnings(profile, model, store) {
|
|
|
382
384
|
* model's layer count, which the UI must never present as measured on this file.
|
|
383
385
|
*/
|
|
384
386
|
export function calibrationInfo(store, model, profile) {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
if (profile.calibrationRequired) {
|
|
387
|
+
const current = getCalibration(store, model, profile);
|
|
388
|
+
const calibration = getCalibrationForBudget(store, model, profile);
|
|
389
|
+
if (!calibration) {
|
|
389
390
|
return {
|
|
390
|
-
state: "theoretical",
|
|
391
|
+
state: hasStaleCalibration(store, model, profile) ? "stale" : "theoretical",
|
|
391
392
|
kvBytesPerToken: null,
|
|
392
393
|
measuredAt: null,
|
|
393
394
|
measuredOn: null,
|
|
394
395
|
};
|
|
395
396
|
}
|
|
396
|
-
|
|
397
|
-
if (!calibration) {
|
|
397
|
+
if (profile.calibrationRequired || !current) {
|
|
398
398
|
return {
|
|
399
|
-
state:
|
|
400
|
-
kvBytesPerToken:
|
|
401
|
-
measuredAt: null,
|
|
402
|
-
measuredOn: null,
|
|
399
|
+
state: "stale",
|
|
400
|
+
kvBytesPerToken: calibration.kvBytesPerToken,
|
|
401
|
+
measuredAt: calibration.measuredAt ?? null,
|
|
402
|
+
measuredOn: calibration.measuredOn ?? null,
|
|
403
403
|
};
|
|
404
404
|
}
|
|
405
405
|
return {
|
|
@@ -14,9 +14,9 @@ export declare function put(store: ProfilesStore, model: Model, profile: Profile
|
|
|
14
14
|
/** Calibration is keyed by cache types, since those change bytes/token. */
|
|
15
15
|
export declare function calibrationKey(profile: Profile): string;
|
|
16
16
|
/**
|
|
17
|
-
* True when this model has
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* True when this model has stored calibrations but none for the profile's
|
|
18
|
+
* current calibration key. Historical entries do not make an exact current
|
|
19
|
+
* measurement stale when the matching key is also present.
|
|
20
20
|
*/
|
|
21
21
|
export declare function hasStaleCalibration(store: ProfilesStore, model: Model, profile: Profile): boolean;
|
|
22
22
|
/**
|
|
@@ -30,5 +30,21 @@ export declare function hasStaleCalibration(store: ProfilesStore, model: Model,
|
|
|
30
30
|
*/
|
|
31
31
|
export declare function geometryKey(model: Model, profile: Profile): string | null;
|
|
32
32
|
export declare function getCalibration(store: ProfilesStore, model: Model, profile: Profile): Calibration | null;
|
|
33
|
+
/**
|
|
34
|
+
* Return the most recent direct calibration stored for a model, regardless of
|
|
35
|
+
* which profile key produced it. A stale measurement is still the stable value
|
|
36
|
+
* the UI and budget math should keep showing until a new calibration replaces
|
|
37
|
+
* it; callers use the calibration state to make its reduced trust visible.
|
|
38
|
+
*/
|
|
39
|
+
export declare function getLastCalibration(store: ProfilesStore, model: Model): Calibration | null;
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the calibration used by VRAM budgeting and launch arguments.
|
|
42
|
+
*
|
|
43
|
+
* An exact or inherited current-profile calibration remains preferred while the
|
|
44
|
+
* profile is current. Once an edit requires recalibration, retain the model's
|
|
45
|
+
* most recent direct measurement instead of jumping to the theoretical formula.
|
|
46
|
+
* The profile/calibration state still tells the caller that this value is stale.
|
|
47
|
+
*/
|
|
48
|
+
export declare function getCalibrationForBudget(store: ProfilesStore, model: Model, profile: Profile): Calibration | null;
|
|
33
49
|
export declare function putCalibration(store: ProfilesStore, model: Model, profile: Profile, measurement: Calibration): ProfilesStore;
|
|
34
50
|
//# sourceMappingURL=profiles.d.ts.map
|
package/dist/config/profiles.js
CHANGED
|
@@ -115,16 +115,16 @@ export function calibrationKey(profile) {
|
|
|
115
115
|
: `${profile.cacheTypeK}:${profile.cacheTypeV}${multiplier}`;
|
|
116
116
|
}
|
|
117
117
|
/**
|
|
118
|
-
* True when this model has
|
|
119
|
-
*
|
|
120
|
-
*
|
|
118
|
+
* True when this model has stored calibrations but none for the profile's
|
|
119
|
+
* current calibration key. Historical entries do not make an exact current
|
|
120
|
+
* measurement stale when the matching key is also present.
|
|
121
121
|
*/
|
|
122
122
|
export function hasStaleCalibration(store, model, profile) {
|
|
123
123
|
const measured = store.calibrations?.[model.id];
|
|
124
124
|
if (!measured)
|
|
125
125
|
return false;
|
|
126
126
|
const key = calibrationKey(profile);
|
|
127
|
-
return Object.
|
|
127
|
+
return !Object.hasOwn(measured, key) && Object.keys(measured).length > 0;
|
|
128
128
|
}
|
|
129
129
|
/**
|
|
130
130
|
* KV cost per token is a property of the attention geometry, not of the particular
|
|
@@ -167,6 +167,38 @@ export function getCalibration(store, model, profile) {
|
|
|
167
167
|
inherited: true,
|
|
168
168
|
};
|
|
169
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Return the most recent direct calibration stored for a model, regardless of
|
|
172
|
+
* which profile key produced it. A stale measurement is still the stable value
|
|
173
|
+
* the UI and budget math should keep showing until a new calibration replaces
|
|
174
|
+
* it; callers use the calibration state to make its reduced trust visible.
|
|
175
|
+
*/
|
|
176
|
+
export function getLastCalibration(store, model) {
|
|
177
|
+
const entries = Object.values(store.calibrations?.[model.id] ?? {});
|
|
178
|
+
if (entries.length === 0)
|
|
179
|
+
return null;
|
|
180
|
+
return entries.reduce((latest, candidate) => {
|
|
181
|
+
const latestTime = latest.measuredAt ? Date.parse(latest.measuredAt) : Number.NEGATIVE_INFINITY;
|
|
182
|
+
const candidateTime = candidate.measuredAt
|
|
183
|
+
? Date.parse(candidate.measuredAt)
|
|
184
|
+
: Number.NEGATIVE_INFINITY;
|
|
185
|
+
return candidateTime >= latestTime ? candidate : latest;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Resolve the calibration used by VRAM budgeting and launch arguments.
|
|
190
|
+
*
|
|
191
|
+
* An exact or inherited current-profile calibration remains preferred while the
|
|
192
|
+
* profile is current. Once an edit requires recalibration, retain the model's
|
|
193
|
+
* most recent direct measurement instead of jumping to the theoretical formula.
|
|
194
|
+
* The profile/calibration state still tells the caller that this value is stale.
|
|
195
|
+
*/
|
|
196
|
+
export function getCalibrationForBudget(store, model, profile) {
|
|
197
|
+
const current = getCalibration(store, model, profile);
|
|
198
|
+
if (!profile.calibrationRequired && current)
|
|
199
|
+
return current;
|
|
200
|
+
return getLastCalibration(store, model) ?? current;
|
|
201
|
+
}
|
|
170
202
|
export function putCalibration(store, model, profile, measurement) {
|
|
171
203
|
if (!store.calibrations)
|
|
172
204
|
store.calibrations = {};
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -1516,6 +1516,10 @@ export declare const BrainConfigSchema: z.ZodObject<{
|
|
|
1516
1516
|
modelsDir: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
1517
1517
|
hfToken: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
1518
1518
|
defaultModel: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
1519
|
+
/** Maximum independently hosted llama-server model processes. */
|
|
1520
|
+
maxLoadedModels: z.ZodDefault<z.ZodNumber>;
|
|
1521
|
+
/** Stable ids selected for residency while model locking is enabled. */
|
|
1522
|
+
lockedModels: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
1519
1523
|
lockModel: z.ZodDefault<z.ZodBoolean>;
|
|
1520
1524
|
allowRemoteConfig: z.ZodDefault<z.ZodBoolean>;
|
|
1521
1525
|
allowInsecureBind: z.ZodDefault<z.ZodBoolean>;
|
|
@@ -1571,6 +1575,8 @@ export declare const BrainConfigSchema: z.ZodObject<{
|
|
|
1571
1575
|
modelsDir: string | null;
|
|
1572
1576
|
hfToken: string | null;
|
|
1573
1577
|
defaultModel: string | null;
|
|
1578
|
+
maxLoadedModels: number;
|
|
1579
|
+
lockedModels: string[];
|
|
1574
1580
|
lockModel: boolean;
|
|
1575
1581
|
allowRemoteConfig: boolean;
|
|
1576
1582
|
allowInsecureBind: boolean;
|
|
@@ -1612,6 +1618,8 @@ export declare const BrainConfigSchema: z.ZodObject<{
|
|
|
1612
1618
|
modelsDir?: string | null | undefined;
|
|
1613
1619
|
hfToken?: string | null | undefined;
|
|
1614
1620
|
defaultModel?: string | null | undefined;
|
|
1621
|
+
maxLoadedModels?: number | undefined;
|
|
1622
|
+
lockedModels?: string[] | undefined;
|
|
1615
1623
|
lockModel?: boolean | undefined;
|
|
1616
1624
|
allowRemoteConfig?: boolean | undefined;
|
|
1617
1625
|
allowInsecureBind?: boolean | undefined;
|
package/dist/config/schema.js
CHANGED
|
@@ -251,9 +251,13 @@ export const BrainConfigSchema = z
|
|
|
251
251
|
// persisted fallback so users can set it once (config set hfToken <token>).
|
|
252
252
|
hfToken: z.string().nullable().default(null),
|
|
253
253
|
defaultModel: z.string().nullable().default(null),
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
254
|
+
/** Maximum independently hosted llama-server model processes. */
|
|
255
|
+
maxLoadedModels: z.number().int().min(1).max(16).default(1),
|
|
256
|
+
/** Stable ids selected for residency while model locking is enabled. */
|
|
257
|
+
lockedModels: z.array(z.string()).default([]),
|
|
258
|
+
// Pin the host to the selected resident set and refuse completion requests
|
|
259
|
+
// that name a different model. With a one-process host this preserves the
|
|
260
|
+
// original single-model lock behavior.
|
|
257
261
|
lockModel: z.boolean().default(false),
|
|
258
262
|
// Sharing/control gates (off by default - a brain is not remotely
|
|
259
263
|
// controllable until its owner opts in). `allowRemoteConfig`: a client with
|
package/dist/ops/archive.d.ts
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import type { Model } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Raw model output archive.
|
|
4
|
+
*
|
|
5
|
+
* Scoring is a separate, replayable pass over stored transcripts rather than
|
|
6
|
+
* something that only happens live. This exists because a bug in the scorer
|
|
7
|
+
* (a filename matcher that attributed every test block to the wrong file) made
|
|
8
|
+
* seven models look identical, and fixing it cost a full re-run on the GPU. The
|
|
9
|
+
* model outputs had been correct all along - only the grading was wrong.
|
|
10
|
+
*
|
|
11
|
+
* With the transcript on disk, a scorer fix re-grades history in seconds.
|
|
12
|
+
*/
|
|
13
|
+
/** Resolve the writable transcript store for a given host environment. */
|
|
14
|
+
declare function resolveArchiveDir(env?: NodeJS.ProcessEnv): string;
|
|
2
15
|
declare const ARCHIVE_DIR: string;
|
|
3
16
|
/** One archived request/response exchange. */
|
|
4
17
|
export interface TranscriptEntry {
|
|
@@ -30,5 +43,5 @@ declare function load(id: string): Record<string, TranscriptEntry[]>;
|
|
|
30
43
|
declare function list(): string[];
|
|
31
44
|
/** Total bytes held, so the archive can be pruned knowingly. */
|
|
32
45
|
declare function size(): number;
|
|
33
|
-
export { ARCHIVE_DIR, runId, runDir, put, load, list, size };
|
|
46
|
+
export { ARCHIVE_DIR, resolveArchiveDir, runId, runDir, put, load, list, size };
|
|
34
47
|
//# sourceMappingURL=archive.d.ts.map
|
package/dist/ops/archive.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
|
-
import {
|
|
4
|
+
import { resolveBrainPaths } from "../config/paths.js";
|
|
5
5
|
/**
|
|
6
6
|
* Raw model output archive.
|
|
7
7
|
*
|
|
@@ -13,9 +13,13 @@ import { fileURLToPath } from "node:url";
|
|
|
13
13
|
*
|
|
14
14
|
* With the transcript on disk, a scorer fix re-grades history in seconds.
|
|
15
15
|
*/
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
/** Resolve the writable transcript store for a given host environment. */
|
|
17
|
+
function resolveArchiveDir(env = process.env) {
|
|
18
|
+
return path.join(resolveBrainPaths(env).resultsDir, "transcripts");
|
|
19
|
+
}
|
|
20
|
+
// Raw benchmark exchanges are host state too. They must follow the score store
|
|
21
|
+
// into OTTO_HOME rather than attempting to write beside the installed package.
|
|
22
|
+
const ARCHIVE_DIR = resolveArchiveDir();
|
|
19
23
|
function runId(model, timestamp = new Date()) {
|
|
20
24
|
const stamp = timestamp.toISOString().replace(/[:.]/g, "-");
|
|
21
25
|
const slug = String(model?.displayName || "unknown")
|
|
@@ -101,5 +105,5 @@ function size() {
|
|
|
101
105
|
}
|
|
102
106
|
return bytes;
|
|
103
107
|
}
|
|
104
|
-
export { ARCHIVE_DIR, runId, runDir, put, load, list, size };
|
|
108
|
+
export { ARCHIVE_DIR, resolveArchiveDir, runId, runDir, put, load, list, size };
|
|
105
109
|
//# sourceMappingURL=archive.js.map
|
package/dist/ops/results.d.ts
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import type { GpuInfo, Model, ModelFeatures } from "../types.js";
|
|
2
2
|
import type { Calibration, Profile } from "../config/schema.js";
|
|
3
3
|
import type { FitResult } from "../vram.js";
|
|
4
|
+
/**
|
|
5
|
+
* Persistent benchmark history.
|
|
6
|
+
*
|
|
7
|
+
* One JSON file per run, so results accumulate across sessions and can be
|
|
8
|
+
* compared and charted later. Runs record the configuration they were measured
|
|
9
|
+
* under, because a score is meaningless without the quant, context size and
|
|
10
|
+
* reasoning budget that produced it.
|
|
11
|
+
*
|
|
12
|
+
* **A run records every value it was measured with, not a summary of them.** A
|
|
13
|
+
* bad score is far more often a bad setup than a bad model, and the difference
|
|
14
|
+
* is only visible from the settings: a context the VRAM fit had to cut, a
|
|
15
|
+
* reasoning budget the model spends entirely on thinking, a KV quant that
|
|
16
|
+
* wrecked recall, weights that fell off the GPU because `gpuLayers` did not
|
|
17
|
+
* cover them, a budget estimated from the formula rather than measured. None of
|
|
18
|
+
* that is recoverable after the fact, so it is all written down at save time -
|
|
19
|
+
* including the exact llama-server argv the run was served with, which is the
|
|
20
|
+
* only true statement of what ran.
|
|
21
|
+
*/
|
|
22
|
+
/** Resolve the writable store for benchmark scores for a given host environment. */
|
|
23
|
+
declare function resolveResultsDir(env?: NodeJS.ProcessEnv): string;
|
|
4
24
|
declare const RESULTS_DIR: string;
|
|
5
25
|
/** Aggregate of a numeric health series (nvidia-smi samples during a run). */
|
|
6
26
|
export interface AggStat {
|
|
@@ -368,5 +388,5 @@ declare function variance(records?: RunRecord[]): VarianceRow[];
|
|
|
368
388
|
declare function rankModels(records?: RunRecord[]): RankedModel[];
|
|
369
389
|
/** All task ids seen across a set of records, in a stable order. */
|
|
370
390
|
declare function taskColumns(records: RunRecord[]): TaskColumn[];
|
|
371
|
-
export { RESULTS_DIR, save, loadAll, latestPerConfig, grouped, variance, stats, rankModels, taskColumns, configKey, slugify, ENGINE_SAMPLER_DEFAULTS, };
|
|
391
|
+
export { RESULTS_DIR, resolveResultsDir, save, loadAll, latestPerConfig, grouped, variance, stats, rankModels, taskColumns, configKey, slugify, ENGINE_SAMPLER_DEFAULTS, };
|
|
372
392
|
//# sourceMappingURL=results.d.ts.map
|
package/dist/ops/results.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { resolveBrainPaths } from "../config/paths.js";
|
|
4
4
|
/**
|
|
5
5
|
* Persistent benchmark history.
|
|
6
6
|
*
|
|
@@ -19,9 +19,14 @@ import { fileURLToPath } from "node:url";
|
|
|
19
19
|
* including the exact llama-server argv the run was served with, which is the
|
|
20
20
|
* only true statement of what ran.
|
|
21
21
|
*/
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
/** Resolve the writable store for benchmark scores for a given host environment. */
|
|
23
|
+
function resolveResultsDir(env = process.env) {
|
|
24
|
+
return resolveBrainPaths(env).resultsDir;
|
|
25
|
+
}
|
|
26
|
+
// Benchmark history is host state, not package data. Keeping it beside the
|
|
27
|
+
// Brain config also makes the service work from packaged/Electron installs,
|
|
28
|
+
// where the package directory may be an archive or read-only.
|
|
29
|
+
const RESULTS_DIR = resolveResultsDir();
|
|
25
30
|
function slugify(text) {
|
|
26
31
|
return String(text)
|
|
27
32
|
.toLowerCase()
|
|
@@ -383,5 +388,5 @@ function taskColumns(records) {
|
|
|
383
388
|
}
|
|
384
389
|
return [...seen.entries()].map(([id, category]) => ({ id, category }));
|
|
385
390
|
}
|
|
386
|
-
export { RESULTS_DIR, save, loadAll, latestPerConfig, grouped, variance, stats, rankModels, taskColumns, configKey, slugify, ENGINE_SAMPLER_DEFAULTS, };
|
|
391
|
+
export { RESULTS_DIR, resolveResultsDir, save, loadAll, latestPerConfig, grouped, variance, stats, rankModels, taskColumns, configKey, slugify, ENGINE_SAMPLER_DEFAULTS, };
|
|
387
392
|
//# sourceMappingURL=results.js.map
|
|
@@ -28,7 +28,7 @@ import * as vram from "../vram.js";
|
|
|
28
28
|
import type { SystemSample } from "../sysmon.js";
|
|
29
29
|
import type { BrainLogPublisher, BrainStatusPublisher } from "./status-events.js";
|
|
30
30
|
import type { Supervisor } from "./supervisor.js";
|
|
31
|
-
import type {
|
|
31
|
+
import type { ModelScheduler } from "./scheduler.js";
|
|
32
32
|
import type { BrainRunLog } from "./run-log.js";
|
|
33
33
|
import type { BrainLogArea } from "./log-format.js";
|
|
34
34
|
/**
|
|
@@ -83,6 +83,8 @@ export interface HostCapabilities {
|
|
|
83
83
|
jobs: boolean;
|
|
84
84
|
/** POST /__host/restart delegates a restart to the service owner. */
|
|
85
85
|
restart: boolean;
|
|
86
|
+
/** Multiple independently supervised model processes are supported. */
|
|
87
|
+
processPool: boolean;
|
|
86
88
|
}
|
|
87
89
|
/** A long-running operation owned by this brain host, not its caller. */
|
|
88
90
|
export interface HostJob {
|
|
@@ -122,8 +124,9 @@ export interface HostApiDeps {
|
|
|
122
124
|
queryGpuInfo: () => Promise<GpuInfo | null>;
|
|
123
125
|
getRanking: () => RankedModel[];
|
|
124
126
|
loadModel: (model: Model) => Promise<void>;
|
|
125
|
-
|
|
126
|
-
scheduler
|
|
127
|
+
unloadModels?: () => Promise<void>;
|
|
128
|
+
/** The process-pool scheduler shared by completions and resident operations. */
|
|
129
|
+
scheduler?: ModelScheduler<Supervisor> | null;
|
|
127
130
|
/** Mirrors POST /__host/config's gate: may a network caller change things? */
|
|
128
131
|
getAllowWrite: () => boolean;
|
|
129
132
|
/** The managed models directory, for disk accounting. Null when unresolvable. */
|
|
@@ -204,7 +207,7 @@ export declare function buildInventoryRow(params: {
|
|
|
204
207
|
gpu: GpuInfo | null;
|
|
205
208
|
ranking: RankedModel[];
|
|
206
209
|
supervisor: Supervisor;
|
|
207
|
-
scheduler?:
|
|
210
|
+
scheduler?: ModelScheduler<Supervisor> | null;
|
|
208
211
|
runtimeBuild?: number | null;
|
|
209
212
|
}): InventoryRow;
|
|
210
213
|
export interface HostApi {
|
package/dist/service/host-api.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { calibrationInfo, profileFieldDescriptors, profileWarnings, sanitizeProfilePatch, } from "../config/profile-edit.js";
|
|
3
3
|
import { familyHostingProfileId, hostingFamily, removeHostingProfileMaterialization, } from "../config/hosting-profiles.js";
|
|
4
|
-
import { forModel,
|
|
4
|
+
import { forModel, getCalibrationForBudget, put } from "../config/profiles.js";
|
|
5
5
|
import { HostingProfileSchema, } from "../config/schema.js";
|
|
6
6
|
import { deleteComponentFile, deleteModelFiles, diskUsage, planDelete, totalModelBytes, } from "../models/manage.js";
|
|
7
7
|
import { deleteDisplayName, updateDisplayName } from "../models/rename-map.js";
|
|
@@ -171,11 +171,13 @@ function hostingProfilesFor(store, model) {
|
|
|
171
171
|
return Object.values(store.hostingProfiles).filter((candidate) => candidate.family === family);
|
|
172
172
|
}
|
|
173
173
|
function stateOf(supervisor, scheduler, model) {
|
|
174
|
-
const
|
|
174
|
+
const residentSupervisor = (typeof scheduler?.supervisorFor === "function" ? scheduler.supervisorFor(model.id) : null) ??
|
|
175
|
+
(supervisor.model?.id === model.id ? supervisor : null);
|
|
176
|
+
const resident = residentSupervisor !== null;
|
|
175
177
|
if (resident) {
|
|
176
|
-
if (
|
|
178
|
+
if (residentSupervisor.state === "starting")
|
|
177
179
|
return "loading";
|
|
178
|
-
if (
|
|
180
|
+
if (residentSupervisor.state === "stopping")
|
|
179
181
|
return "unloading";
|
|
180
182
|
}
|
|
181
183
|
const stats = scheduler?.stats();
|
|
@@ -183,7 +185,7 @@ function stateOf(supervisor, scheduler, model) {
|
|
|
183
185
|
return "active";
|
|
184
186
|
if ((stats?.waitingModelIds[model.id] ?? 0) > 0)
|
|
185
187
|
return "queued";
|
|
186
|
-
if (resident &&
|
|
188
|
+
if (resident && residentSupervisor.state === "ready")
|
|
187
189
|
return "loaded";
|
|
188
190
|
return "not-loaded";
|
|
189
191
|
}
|
|
@@ -197,7 +199,7 @@ function stateOf(supervisor, scheduler, model) {
|
|
|
197
199
|
export function buildInventoryRow(params) {
|
|
198
200
|
const { model, store, defaults, gpu, ranking, supervisor, scheduler = null, runtimeBuild: activeRuntimeBuild = null, } = params;
|
|
199
201
|
const profile = forModel(store, model, defaults);
|
|
200
|
-
const calibration =
|
|
202
|
+
const calibration = getCalibrationForBudget(store, model, profile);
|
|
201
203
|
const budgetOptions = gpu
|
|
202
204
|
? { model, profile, calibration, totalVramBytes: gpu.totalBytes }
|
|
203
205
|
: null;
|
|
@@ -344,6 +346,7 @@ export function createHostApi(deps) {
|
|
|
344
346
|
writable: deps.getAllowWrite(),
|
|
345
347
|
jobs: Boolean(deps.jobs),
|
|
346
348
|
restart: Boolean(deps.restart),
|
|
349
|
+
processPool: true,
|
|
347
350
|
});
|
|
348
351
|
/** Refuse a write unless the owner opted into remote configuration. */
|
|
349
352
|
const guardWrite = (res) => {
|
|
@@ -413,7 +416,9 @@ export function createHostApi(deps) {
|
|
|
413
416
|
// A setting is only unapplied when it was changed on the currently
|
|
414
417
|
// resident model. Edits to an unloaded model take effect naturally
|
|
415
418
|
// when it is next loaded and do not earn a misleading reload badge.
|
|
416
|
-
const requiresRestart = deps.
|
|
419
|
+
const requiresRestart = Boolean((typeof deps.scheduler?.supervisorFor === "function"
|
|
420
|
+
? deps.scheduler.supervisorFor(model.id)
|
|
421
|
+
: null) ?? (deps.supervisor.model?.id === model.id ? deps.supervisor : null));
|
|
417
422
|
if (requiresRestart)
|
|
418
423
|
store.pendingReloadModelIds[model.id] = true;
|
|
419
424
|
deps.saveProfiles(store);
|
|
@@ -421,9 +426,7 @@ export function createHostApi(deps) {
|
|
|
421
426
|
// Return the recomputed budget so an edit costs one round trip rather
|
|
422
427
|
// than a write followed by a read the UI has to sequence.
|
|
423
428
|
const gpu = await deps.queryGpuInfo();
|
|
424
|
-
const calibration = profile
|
|
425
|
-
? null
|
|
426
|
-
: getCalibration(store, model, profile);
|
|
429
|
+
const calibration = getCalibrationForBudget(store, model, profile);
|
|
427
430
|
const options = gpu
|
|
428
431
|
? { model, profile, calibration, totalVramBytes: gpu.totalBytes }
|
|
429
432
|
: null;
|
|
@@ -522,7 +525,7 @@ export function createHostApi(deps) {
|
|
|
522
525
|
const options = {
|
|
523
526
|
model,
|
|
524
527
|
profile,
|
|
525
|
-
calibration:
|
|
528
|
+
calibration: getCalibrationForBudget(store, model, profile),
|
|
526
529
|
totalVramBytes: gpu.totalBytes,
|
|
527
530
|
};
|
|
528
531
|
sendJson(res, {
|
|
@@ -553,11 +556,14 @@ export function createHostApi(deps) {
|
|
|
553
556
|
deps.log?.("model", `loading ${model.displayName}`);
|
|
554
557
|
await deps.loadModel(model);
|
|
555
558
|
deps.log?.("model", `loaded ${model.displayName}`);
|
|
559
|
+
const resident = (typeof deps.scheduler?.supervisorFor === "function"
|
|
560
|
+
? deps.scheduler.supervisorFor(model.id)
|
|
561
|
+
: null) ?? deps.supervisor;
|
|
556
562
|
sendJson(res, {
|
|
557
|
-
status:
|
|
563
|
+
status: resident.status(),
|
|
558
564
|
// What actually got used: loadModel fits the profile to VRAM, so the
|
|
559
565
|
// context here may be lower than the one saved.
|
|
560
|
-
profile:
|
|
566
|
+
profile: resident.profile,
|
|
561
567
|
});
|
|
562
568
|
}
|
|
563
569
|
catch (error) {
|
|
@@ -570,7 +576,10 @@ export function createHostApi(deps) {
|
|
|
570
576
|
void (async () => {
|
|
571
577
|
try {
|
|
572
578
|
deps.log?.("model", "unloading resident model");
|
|
573
|
-
|
|
579
|
+
if (deps.unloadModels)
|
|
580
|
+
await deps.unloadModels();
|
|
581
|
+
else
|
|
582
|
+
await deps.supervisor.stop();
|
|
574
583
|
deps.log?.("model", "resident model unloaded");
|
|
575
584
|
sendJson(res, { status: deps.supervisor.status() });
|
|
576
585
|
}
|
|
@@ -580,7 +589,10 @@ export function createHostApi(deps) {
|
|
|
580
589
|
})();
|
|
581
590
|
};
|
|
582
591
|
const handleDelete = (res, model) => {
|
|
583
|
-
|
|
592
|
+
const resident = typeof deps.scheduler?.supervisorFor === "function"
|
|
593
|
+
? deps.scheduler.supervisorFor(model.id)
|
|
594
|
+
: null;
|
|
595
|
+
if (resident && resident.state !== "stopped") {
|
|
584
596
|
sendError(res, 409, "stop the model before deleting it");
|
|
585
597
|
return;
|
|
586
598
|
}
|
|
@@ -600,7 +612,10 @@ export function createHostApi(deps) {
|
|
|
600
612
|
}
|
|
601
613
|
};
|
|
602
614
|
const handleComponentDelete = (res, model, componentId) => {
|
|
603
|
-
|
|
615
|
+
const resident = typeof deps.scheduler?.supervisorFor === "function"
|
|
616
|
+
? deps.scheduler.supervisorFor(model.id)
|
|
617
|
+
: null;
|
|
618
|
+
if (resident && resident.state !== "stopped") {
|
|
604
619
|
sendError(res, 409, "stop the model before removing a bundle component");
|
|
605
620
|
return;
|
|
606
621
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Model } from "../types.js";
|
|
2
|
+
import type { ModelScheduler, SchedulerStats, SchedulerSubmitOptions } from "./scheduler.js";
|
|
3
|
+
import { Scheduler } from "./scheduler.js";
|
|
4
|
+
import type { Supervisor } from "./supervisor.js";
|
|
5
|
+
export interface ModelProcessPoolOptions {
|
|
6
|
+
initialSupervisor: Supervisor;
|
|
7
|
+
maxModels: number;
|
|
8
|
+
createSupervisor: (index: number) => Supervisor;
|
|
9
|
+
createScheduler: (supervisor: Supervisor, loadModel: (model: Model) => Promise<void>, onChange: () => void) => Scheduler<Supervisor>;
|
|
10
|
+
/** Returns the complete VRAM reservation for the process after it is ready. */
|
|
11
|
+
loadModel: (supervisor: Supervisor, model: Model, reservedElsewhereBytes: number) => Promise<number>;
|
|
12
|
+
onChange?: (() => void) | null;
|
|
13
|
+
logger?: ((message: string) => void) | null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Global admission and eviction boundary for independently hosted models.
|
|
17
|
+
*
|
|
18
|
+
* Each resident model owns one Supervisor/Scheduler pair and therefore one
|
|
19
|
+
* llama-server process, port, KV pool, and lifecycle. Requests for a resident
|
|
20
|
+
* model go directly to that process. An unloaded model claims a free process
|
|
21
|
+
* slot, or evicts the least-recently-used idle process when the configured
|
|
22
|
+
* model limit is full. Busy processes are never evicted; the request remains
|
|
23
|
+
* queued until one becomes idle.
|
|
24
|
+
*/
|
|
25
|
+
export declare class ModelProcessPool implements ModelScheduler<Supervisor> {
|
|
26
|
+
#private;
|
|
27
|
+
constructor(options: ModelProcessPoolOptions);
|
|
28
|
+
get maxModels(): number;
|
|
29
|
+
submit(model: Model, run: (supervisor: Supervisor) => Promise<unknown>, options?: SchedulerSubmitOptions): Promise<unknown>;
|
|
30
|
+
/** Load a model and leave it resident without consuming an inference slot. */
|
|
31
|
+
preload(model: Model): Promise<void>;
|
|
32
|
+
/** Apply the host-owned process limit and retire excess idle processes. */
|
|
33
|
+
configure(maxModels: number): Promise<void>;
|
|
34
|
+
supervisorFor(modelId: string): Supervisor | null;
|
|
35
|
+
supervisors(): Supervisor[];
|
|
36
|
+
/** Complete statuses for every process that currently owns a model. */
|
|
37
|
+
residentSupervisors(): Supervisor[];
|
|
38
|
+
reservationFor(modelId: string): number;
|
|
39
|
+
unload(modelId?: string | null): Promise<void>;
|
|
40
|
+
stop(): Promise<void>;
|
|
41
|
+
forgetSlots(): void;
|
|
42
|
+
stats(): SchedulerStats;
|
|
43
|
+
}
|
|
44
|
+
export declare function normalizeModelLimit(value: number): number;
|
|
45
|
+
//# sourceMappingURL=process-pool.d.ts.map
|