@otto-code/brain 0.8.10 → 0.8.12
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/calibrate.js +9 -0
- package/dist/commands/catalog.d.ts +1 -0
- package/dist/commands/catalog.js +1 -0
- package/dist/commands/pull.d.ts +1 -0
- package/dist/commands/pull.js +12 -3
- package/dist/commands/search.d.ts +1 -0
- package/dist/commands/search.js +12 -2
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +1 -1
- package/dist/config/profile-edit.d.ts +88 -1
- package/dist/config/profile-edit.js +280 -29
- package/dist/config/profiles.js +16 -0
- package/dist/config/schema.d.ts +608 -0
- package/dist/config/schema.js +58 -0
- package/dist/config/store.js +7 -4
- package/dist/gguf.d.ts +7 -0
- package/dist/gguf.js +15 -2
- package/dist/models/download.d.ts +1 -1
- package/dist/models/download.js +2 -2
- package/dist/models/enrich.d.ts +6 -0
- package/dist/models/enrich.js +27 -1
- package/dist/models/index.d.ts +1 -1
- package/dist/models/index.js +4 -3
- package/dist/ops/calibrate.d.ts +38 -3
- package/dist/ops/calibrate.js +68 -19
- package/dist/ops/report.js +51 -1
- package/dist/ops/results.d.ts +57 -11
- package/dist/ops/results.js +75 -10
- package/dist/ops/sweep.d.ts +38 -1
- package/dist/ops/sweep.js +61 -10
- package/dist/runtime/args.d.ts +15 -2
- package/dist/runtime/args.js +60 -5
- package/dist/runtime/managed.js +2 -2
- package/dist/service/activity.d.ts +19 -0
- package/dist/service/activity.js +47 -4
- package/dist/service/host-api.d.ts +25 -4
- package/dist/service/host-api.js +82 -16
- package/dist/service/log-format.d.ts +18 -0
- package/dist/service/log-format.js +32 -0
- package/dist/service/router.d.ts +70 -2
- package/dist/service/router.js +219 -21
- package/dist/service/run-log.d.ts +6 -1
- package/dist/service/run-log.js +46 -4
- package/dist/service/scheduler.d.ts +227 -24
- package/dist/service/scheduler.js +395 -63
- package/dist/service/serve.d.ts +4 -0
- package/dist/service/serve.js +302 -117
- package/dist/service/status-events.d.ts +14 -1
- package/dist/service/status-events.js +111 -12
- package/dist/service/supervisor.d.ts +9 -7
- package/dist/service/supervisor.js +37 -12
- package/dist/sysmon.d.ts +15 -0
- package/dist/sysmon.js +56 -9
- package/dist/tui/app.d.ts +8 -2
- package/dist/tui/app.js +65 -17
- package/dist/types.d.ts +18 -0
- package/dist/vram.d.ts +37 -0
- package/dist/vram.js +57 -18
- package/package.json +1 -1
package/dist/ops/results.d.ts
CHANGED
|
@@ -78,7 +78,10 @@ export interface RecordModel {
|
|
|
78
78
|
* a bad score - `gpuLayers` short of the model's layer count silently runs part
|
|
79
79
|
* of it on the CPU, `parallelSlots` splits the context between slots so the
|
|
80
80
|
* effective window is a fraction of `contextSize`, and `extraArgs` can override
|
|
81
|
-
* anything above.
|
|
81
|
+
* anything above. Schema 3 added the settings that landed after the setup-capture
|
|
82
|
+
* change - `preserveReasoning`, `contextMultiplier`, `cachedChats`, the sampler
|
|
83
|
+
* values, and the hosting-profile identity - so the record keeps tracking the
|
|
84
|
+
* profile instead of silently stopping at the field set it was born with.
|
|
82
85
|
*/
|
|
83
86
|
export interface RecordProfile {
|
|
84
87
|
contextSize: number;
|
|
@@ -93,6 +96,28 @@ export interface RecordProfile {
|
|
|
93
96
|
batchSize: number | null;
|
|
94
97
|
ubatchSize: number | null;
|
|
95
98
|
extraArgs: string[];
|
|
99
|
+
/**
|
|
100
|
+
* Schema 3+. Absent on schema 2 records, which predate the setting.
|
|
101
|
+
* Tri-state: true/false as chosen, null for "the template's own default".
|
|
102
|
+
*/
|
|
103
|
+
preserveReasoning: boolean | null;
|
|
104
|
+
/** Schema 3+. RoPE extension factor; 1 is the GGUF-native window. */
|
|
105
|
+
contextMultiplier: number;
|
|
106
|
+
/** Schema 3+. Chats parked in system RAM; 0 leaves llama.cpp's default. */
|
|
107
|
+
cachedChats: number;
|
|
108
|
+
/** Schema 3+. The sampler the run was served with, server-level for every task. */
|
|
109
|
+
temperature: number | null;
|
|
110
|
+
topP: number | null;
|
|
111
|
+
topK: number | null;
|
|
112
|
+
minP: number | null;
|
|
113
|
+
presencePenalty: number | null;
|
|
114
|
+
repeatPenalty: number | null;
|
|
115
|
+
/**
|
|
116
|
+
* Schema 3+. The Brain-owned hosting profile in effect (id, not text): its
|
|
117
|
+
* chat template and system-prompt addendum change what the model is asked
|
|
118
|
+
* to do, so a run under one profile is not comparable to one under another.
|
|
119
|
+
*/
|
|
120
|
+
hostingProfileId: string | null;
|
|
96
121
|
}
|
|
97
122
|
/**
|
|
98
123
|
* How the run was actually set up, as opposed to how it was configured.
|
|
@@ -173,10 +198,12 @@ export interface RecordTask {
|
|
|
173
198
|
/**
|
|
174
199
|
* One stored benchmark run - the shared shape used across results and report.
|
|
175
200
|
*
|
|
176
|
-
* `schema` is
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
201
|
+
* `schema` is 3 as of the profile-completeness change: 3 carries every profile
|
|
202
|
+
* field that reaches llama-server, including the sampler values and the hosting
|
|
203
|
+
* profile; 2 carried the profile as of the setup-capture change plus `setup`
|
|
204
|
+
* and `suite`; 1 carried six profile fields and neither. Readers must treat
|
|
205
|
+
* everything added after a record's schema as absent rather than assuming it,
|
|
206
|
+
* because those runs are still perfectly good scores - they just cannot say
|
|
180
207
|
* what they were measured with.
|
|
181
208
|
*/
|
|
182
209
|
export interface RunRecord {
|
|
@@ -283,14 +310,33 @@ export interface TaskColumn {
|
|
|
283
310
|
category: string;
|
|
284
311
|
}
|
|
285
312
|
declare function slugify(text: string): string;
|
|
313
|
+
/**
|
|
314
|
+
* llama.cpp's own sampler defaults, the values an untouched profile stores
|
|
315
|
+
* verbatim (see `ProfileSchema`). A key token is only emitted when the profile
|
|
316
|
+
* deviates from these, which is what makes an untouched profile keep its
|
|
317
|
+
* historical key after the widening.
|
|
318
|
+
*/
|
|
319
|
+
declare const ENGINE_SAMPLER_DEFAULTS: {
|
|
320
|
+
readonly temperature: 0.8;
|
|
321
|
+
readonly topP: 0.95;
|
|
322
|
+
readonly topK: 40;
|
|
323
|
+
readonly minP: 0.05;
|
|
324
|
+
readonly presencePenalty: 0;
|
|
325
|
+
readonly repeatPenalty: 1;
|
|
326
|
+
};
|
|
286
327
|
/**
|
|
287
328
|
* Stable identity for "same model, same settings", so reruns can be grouped.
|
|
288
329
|
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
330
|
+
* The base four tokens predate the record and are never rewritten - a stored
|
|
331
|
+
* run's key is its key, and re-deriving the same string for the same setup is
|
|
332
|
+
* what lets old runs stay in their old groups. The tokens added in schema 3 are
|
|
333
|
+
* appended ONLY when they deviate from the engine default, because that is what
|
|
334
|
+
* the key means: the effective configuration, and a default is the historical
|
|
335
|
+
* configuration. An untouched profile therefore keeps exactly the key it had
|
|
336
|
+
* before the setting existed, so variance figures for unchanged setups survive
|
|
337
|
+
* the widening; a run that differs only in, say, `contextMultiplier` gets a
|
|
338
|
+
* new key instead of silently merging into its predecessor's group and
|
|
339
|
+
* contaminating that group's consistency line.
|
|
294
340
|
*/
|
|
295
341
|
declare function configKey(profile: Profile | null): string;
|
|
296
342
|
declare function save({ model, profile, report, gpu, runtime, archiveId, system, args, fit, calibration, suite, timestamp, }: SaveOptions): SaveResult;
|
|
@@ -322,5 +368,5 @@ declare function variance(records?: RunRecord[]): VarianceRow[];
|
|
|
322
368
|
declare function rankModels(records?: RunRecord[]): RankedModel[];
|
|
323
369
|
/** All task ids seen across a set of records, in a stable order. */
|
|
324
370
|
declare function taskColumns(records: RunRecord[]): TaskColumn[];
|
|
325
|
-
export { RESULTS_DIR, save, loadAll, latestPerConfig, grouped, variance, stats, rankModels, taskColumns, configKey, slugify, };
|
|
371
|
+
export { RESULTS_DIR, save, loadAll, latestPerConfig, grouped, variance, stats, rankModels, taskColumns, configKey, slugify, ENGINE_SAMPLER_DEFAULTS, };
|
|
326
372
|
//# sourceMappingURL=results.d.ts.map
|
package/dist/ops/results.js
CHANGED
|
@@ -29,24 +29,77 @@ function slugify(text) {
|
|
|
29
29
|
.replace(/^-|-$/g, "")
|
|
30
30
|
.slice(0, 80);
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* llama.cpp's own sampler defaults, the values an untouched profile stores
|
|
34
|
+
* verbatim (see `ProfileSchema`). A key token is only emitted when the profile
|
|
35
|
+
* deviates from these, which is what makes an untouched profile keep its
|
|
36
|
+
* historical key after the widening.
|
|
37
|
+
*/
|
|
38
|
+
const ENGINE_SAMPLER_DEFAULTS = {
|
|
39
|
+
temperature: 0.8,
|
|
40
|
+
topP: 0.95,
|
|
41
|
+
topK: 40,
|
|
42
|
+
minP: 0.05,
|
|
43
|
+
presencePenalty: 0,
|
|
44
|
+
repeatPenalty: 1,
|
|
45
|
+
};
|
|
32
46
|
/**
|
|
33
47
|
* Stable identity for "same model, same settings", so reruns can be grouped.
|
|
34
48
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
49
|
+
* The base four tokens predate the record and are never rewritten - a stored
|
|
50
|
+
* run's key is its key, and re-deriving the same string for the same setup is
|
|
51
|
+
* what lets old runs stay in their old groups. The tokens added in schema 3 are
|
|
52
|
+
* appended ONLY when they deviate from the engine default, because that is what
|
|
53
|
+
* the key means: the effective configuration, and a default is the historical
|
|
54
|
+
* configuration. An untouched profile therefore keeps exactly the key it had
|
|
55
|
+
* before the setting existed, so variance figures for unchanged setups survive
|
|
56
|
+
* the widening; a run that differs only in, say, `contextMultiplier` gets a
|
|
57
|
+
* new key instead of silently merging into its predecessor's group and
|
|
58
|
+
* contaminating that group's consistency line.
|
|
40
59
|
*/
|
|
41
60
|
function configKey(profile) {
|
|
42
61
|
if (!profile)
|
|
43
62
|
return "unknown";
|
|
44
|
-
|
|
63
|
+
const parts = [
|
|
45
64
|
`ctx${profile.contextSize}`,
|
|
46
65
|
`kv${profile.cacheTypeK}-${profile.cacheTypeV}`,
|
|
47
66
|
`rb${profile.reasoningBudget}`,
|
|
48
67
|
profile.vision ? "vision" : "novision",
|
|
49
|
-
]
|
|
68
|
+
];
|
|
69
|
+
if (profile.contextMultiplier !== undefined && profile.contextMultiplier > 1) {
|
|
70
|
+
parts.push(`x${profile.contextMultiplier}`);
|
|
71
|
+
}
|
|
72
|
+
if (profile.cachedChats !== undefined && profile.cachedChats > 0) {
|
|
73
|
+
parts.push(`cc${profile.cachedChats}`);
|
|
74
|
+
}
|
|
75
|
+
if (profile.preserveReasoning === true)
|
|
76
|
+
parts.push("pr");
|
|
77
|
+
else if (profile.preserveReasoning === false)
|
|
78
|
+
parts.push("nopr");
|
|
79
|
+
// The sampler deviates from the engine default only when the user set it -
|
|
80
|
+
// an untouched profile stores llama.cpp's own defaults verbatim, and those
|
|
81
|
+
// are the default, so they carry no token. Each entry pairs the short token
|
|
82
|
+
// name with the `ENGINE_SAMPLER_DEFAULTS` key the default is looked up by -
|
|
83
|
+
// the two deliberately differ, so conflating them would emit every sampler
|
|
84
|
+
// value on every key.
|
|
85
|
+
const sampler = [
|
|
86
|
+
["temp", profile.temperature, "temperature"],
|
|
87
|
+
["p", profile.topP, "topP"],
|
|
88
|
+
["k", profile.topK, "topK"],
|
|
89
|
+
["minp", profile.minP, "minP"],
|
|
90
|
+
["pres", profile.presencePenalty, "presencePenalty"],
|
|
91
|
+
["rep", profile.repeatPenalty, "repeatPenalty"],
|
|
92
|
+
];
|
|
93
|
+
for (const [name, value, defaultKey] of sampler) {
|
|
94
|
+
if (typeof value === "number" &&
|
|
95
|
+
Number.isFinite(value) &&
|
|
96
|
+
value !== ENGINE_SAMPLER_DEFAULTS[defaultKey]) {
|
|
97
|
+
parts.push(`${name}${value}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (profile.hostingProfileId)
|
|
101
|
+
parts.push(`hp${profile.hostingProfileId}`);
|
|
102
|
+
return parts.join("_");
|
|
50
103
|
}
|
|
51
104
|
/** Read a numeric GGUF metadata field, which is `null` when the header lacked it. */
|
|
52
105
|
function metadataNumber(model, key) {
|
|
@@ -83,7 +136,7 @@ function buildSetup({ args, fit, calibration, }) {
|
|
|
83
136
|
function save({ model, profile, report, gpu = null, runtime = null, archiveId = null, system = null, args = null, fit = null, calibration = null, suite = null, timestamp = new Date(), }) {
|
|
84
137
|
fs.mkdirSync(RESULTS_DIR, { recursive: true });
|
|
85
138
|
const record = {
|
|
86
|
-
schema:
|
|
139
|
+
schema: 3,
|
|
87
140
|
ranAt: timestamp.toISOString(),
|
|
88
141
|
model: {
|
|
89
142
|
id: model?.id ?? null,
|
|
@@ -99,7 +152,9 @@ function save({ model, profile, report, gpu = null, runtime = null, archiveId =
|
|
|
99
152
|
},
|
|
100
153
|
// The whole profile, not a summary of it. `ProfileSchema` is `.passthrough()`
|
|
101
154
|
// and its numeric fields carry defaults, so read them defensively: a profile
|
|
102
|
-
// written by an older brain can be missing anything below `vision
|
|
155
|
+
// written by an older brain can be missing anything below `vision`, and a
|
|
156
|
+
// schema-1-era profile can be missing the sampler values entirely (they
|
|
157
|
+
// were implicit in llama.cpp then).
|
|
103
158
|
profile: profile
|
|
104
159
|
? {
|
|
105
160
|
contextSize: profile.contextSize,
|
|
@@ -114,6 +169,16 @@ function save({ model, profile, report, gpu = null, runtime = null, archiveId =
|
|
|
114
169
|
batchSize: profile.batchSize ?? null,
|
|
115
170
|
ubatchSize: profile.ubatchSize ?? null,
|
|
116
171
|
extraArgs: profile.extraArgs ?? [],
|
|
172
|
+
preserveReasoning: profile.preserveReasoning ?? null,
|
|
173
|
+
contextMultiplier: profile.contextMultiplier ?? 1,
|
|
174
|
+
cachedChats: profile.cachedChats ?? 0,
|
|
175
|
+
temperature: profile.temperature ?? null,
|
|
176
|
+
topP: profile.topP ?? null,
|
|
177
|
+
topK: profile.topK ?? null,
|
|
178
|
+
minP: profile.minP ?? null,
|
|
179
|
+
presencePenalty: profile.presencePenalty ?? null,
|
|
180
|
+
repeatPenalty: profile.repeatPenalty ?? null,
|
|
181
|
+
hostingProfileId: profile.hostingProfileId ?? null,
|
|
117
182
|
}
|
|
118
183
|
: null,
|
|
119
184
|
setup: buildSetup({ args, fit, calibration }),
|
|
@@ -318,5 +383,5 @@ function taskColumns(records) {
|
|
|
318
383
|
}
|
|
319
384
|
return [...seen.entries()].map(([id, category]) => ({ id, category }));
|
|
320
385
|
}
|
|
321
|
-
export { RESULTS_DIR, save, loadAll, latestPerConfig, grouped, variance, stats, rankModels, taskColumns, configKey, slugify, };
|
|
386
|
+
export { RESULTS_DIR, save, loadAll, latestPerConfig, grouped, variance, stats, rankModels, taskColumns, configKey, slugify, ENGINE_SAMPLER_DEFAULTS, };
|
|
322
387
|
//# sourceMappingURL=results.js.map
|
package/dist/ops/sweep.d.ts
CHANGED
|
@@ -7,7 +7,19 @@ import type { Profile } from "../config/schema.js";
|
|
|
7
7
|
* Thinking models default to an unrestricted budget (-1) and will happily burn
|
|
8
8
|
* an entire token allowance reasoning, returning no content at all. The right
|
|
9
9
|
* cap is model-specific, so measure it: run one long-horizon task per candidate
|
|
10
|
-
* budget and
|
|
10
|
+
* budget and take the largest budget that still delivers the whole task. See
|
|
11
|
+
* rankSweepResults for why "largest" and not "fastest".
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* The candidates a sweep tries, in ascending order of thinking room.
|
|
15
|
+
*
|
|
16
|
+
* Capped at 1536 deliberately. The ranking prefers the largest budget that
|
|
17
|
+
* still delivers, so whatever sits at the top of this list is what a healthy
|
|
18
|
+
* thinking model gets recommended - the list itself is the ceiling, and 1536 is
|
|
19
|
+
* the value every hand-set profile has run on without trouble. Each extra
|
|
20
|
+
* candidate also costs a full model load plus a long generation, so the ladder
|
|
21
|
+
* earns its length. `-1` stays because a model that fails under every finite cap
|
|
22
|
+
* still needs an answer; it ranks last and wins only in that case.
|
|
11
23
|
*/
|
|
12
24
|
export declare const DEFAULT_BUDGETS: number[];
|
|
13
25
|
export declare const LONG_TASK: string;
|
|
@@ -75,5 +87,30 @@ export interface SweepOptions {
|
|
|
75
87
|
onProgress?: (event: SweepProgress) => void;
|
|
76
88
|
}
|
|
77
89
|
export declare function sweep({ runtime, model, profile, budgets, maxTokens, temperature, internalPort, supervisor: optionsSupervisor, onProgress, }: SweepOptions): Promise<SweepReport>;
|
|
90
|
+
/**
|
|
91
|
+
* Rank a sweep's trials, best first. `ranked[0].budget` is the recommendation.
|
|
92
|
+
*
|
|
93
|
+
* Delivery decides first: a budget that dropped a file did not do the job.
|
|
94
|
+
* Among the budgets that delivered everything, prefer the **largest** - the most
|
|
95
|
+
* thinking room a model can have without failing to deliver.
|
|
96
|
+
*
|
|
97
|
+
* That tie used to break on content per second, which was backwards. LONG_TASK
|
|
98
|
+
* is pure output, so reasoning earns nothing on it and only costs wall clock:
|
|
99
|
+
* every budget large enough to finish delivered all four files, the tie-break
|
|
100
|
+
* therefore decided every sweep, and the smallest candidate won by construction
|
|
101
|
+
* - 0 included. That is how models ended up capped at 0 and 512, tight enough
|
|
102
|
+
* that llama-server guillotines an ordinary thought mid-sentence, injects
|
|
103
|
+
* `--reasoning-budget-message`, and the model's unfinished reasoning carries on
|
|
104
|
+
* over the content channel as user-visible prose. The sweep's job is to find the
|
|
105
|
+
* cap past which a model degenerates into reasoning forever, not the cap that
|
|
106
|
+
* types fastest. See the "reasoning budget makes thinking bleed into prose"
|
|
107
|
+
* finding in Otto Knowledge.
|
|
108
|
+
*
|
|
109
|
+
* `-1` is the one budget "largest" must not read as large: an unrestricted
|
|
110
|
+
* budget is the failure this package exists to prevent. It ranks below every
|
|
111
|
+
* finite cap and is recommended only when nothing else survived, which means a
|
|
112
|
+
* cap of any size broke the model.
|
|
113
|
+
*/
|
|
114
|
+
export declare function rankSweepResults(results: readonly SweepResult[]): SweepResult[];
|
|
78
115
|
export {};
|
|
79
116
|
//# sourceMappingURL=sweep.d.ts.map
|
package/dist/ops/sweep.js
CHANGED
|
@@ -6,9 +6,23 @@ import { DEFAULT_INTERNAL_PORT, Supervisor } from "../service/supervisor.js";
|
|
|
6
6
|
* Thinking models default to an unrestricted budget (-1) and will happily burn
|
|
7
7
|
* an entire token allowance reasoning, returning no content at all. The right
|
|
8
8
|
* cap is model-specific, so measure it: run one long-horizon task per candidate
|
|
9
|
-
* budget and
|
|
9
|
+
* budget and take the largest budget that still delivers the whole task. See
|
|
10
|
+
* rankSweepResults for why "largest" and not "fastest".
|
|
10
11
|
*/
|
|
11
|
-
|
|
12
|
+
/**
|
|
13
|
+
* The candidates a sweep tries, in ascending order of thinking room.
|
|
14
|
+
*
|
|
15
|
+
* Capped at 1536 deliberately. The ranking prefers the largest budget that
|
|
16
|
+
* still delivers, so whatever sits at the top of this list is what a healthy
|
|
17
|
+
* thinking model gets recommended - the list itself is the ceiling, and 1536 is
|
|
18
|
+
* the value every hand-set profile has run on without trouble. Each extra
|
|
19
|
+
* candidate also costs a full model load plus a long generation, so the ladder
|
|
20
|
+
* earns its length. `-1` stays because a model that fails under every finite cap
|
|
21
|
+
* still needs an answer; it ranks last and wins only in that case.
|
|
22
|
+
*/
|
|
23
|
+
export const DEFAULT_BUDGETS = [0, 512, 1536, -1];
|
|
24
|
+
/** llama.cpp's "no cap at all" sentinel, which a sweep must never recommend. */
|
|
25
|
+
const UNRESTRICTED_BUDGET = -1;
|
|
12
26
|
export const LONG_TASK = "Write a complete Python implementation of a thread-safe LRU cache with TTL " +
|
|
13
27
|
"expiry. Produce FOUR separate complete files, each fully implemented with no " +
|
|
14
28
|
"placeholders or elisions:\n" +
|
|
@@ -87,7 +101,7 @@ export async function sweep({ runtime, model, profile, budgets = DEFAULT_BUDGETS
|
|
|
87
101
|
const supervisor = optionsSupervisor ?? new Supervisor({ runtime, internalPort });
|
|
88
102
|
onProgress({ phase: "loading", budget });
|
|
89
103
|
try {
|
|
90
|
-
await supervisor.start(model, { ...profile, reasoningBudget: budget }
|
|
104
|
+
await supervisor.start(model, { ...profile, reasoningBudget: budget });
|
|
91
105
|
onProgress({ phase: "generating", budget });
|
|
92
106
|
const trial = await runTrial({ supervisor, maxTokens, temperature });
|
|
93
107
|
results.push({ budget, ...trial, error: null });
|
|
@@ -109,13 +123,7 @@ export async function sweep({ runtime, model, profile, budgets = DEFAULT_BUDGETS
|
|
|
109
123
|
await new Promise((resolve) => setTimeout(resolve, 2500));
|
|
110
124
|
}
|
|
111
125
|
}
|
|
112
|
-
|
|
113
|
-
const viable = results.filter((r) => !r.error && r.contentChars > 0);
|
|
114
|
-
const ranked = [...viable].sort((a, b) => {
|
|
115
|
-
if (b.filesDelivered !== a.filesDelivered)
|
|
116
|
-
return b.filesDelivered - a.filesDelivered;
|
|
117
|
-
return b.contentPerSecond - a.contentPerSecond;
|
|
118
|
-
});
|
|
126
|
+
const ranked = rankSweepResults(results);
|
|
119
127
|
return {
|
|
120
128
|
results,
|
|
121
129
|
recommended: ranked.length ? ranked[0].budget : null,
|
|
@@ -123,4 +131,47 @@ export async function sweep({ runtime, model, profile, budgets = DEFAULT_BUDGETS
|
|
|
123
131
|
sweptAt: new Date().toISOString(),
|
|
124
132
|
};
|
|
125
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Rank a sweep's trials, best first. `ranked[0].budget` is the recommendation.
|
|
136
|
+
*
|
|
137
|
+
* Delivery decides first: a budget that dropped a file did not do the job.
|
|
138
|
+
* Among the budgets that delivered everything, prefer the **largest** - the most
|
|
139
|
+
* thinking room a model can have without failing to deliver.
|
|
140
|
+
*
|
|
141
|
+
* That tie used to break on content per second, which was backwards. LONG_TASK
|
|
142
|
+
* is pure output, so reasoning earns nothing on it and only costs wall clock:
|
|
143
|
+
* every budget large enough to finish delivered all four files, the tie-break
|
|
144
|
+
* therefore decided every sweep, and the smallest candidate won by construction
|
|
145
|
+
* - 0 included. That is how models ended up capped at 0 and 512, tight enough
|
|
146
|
+
* that llama-server guillotines an ordinary thought mid-sentence, injects
|
|
147
|
+
* `--reasoning-budget-message`, and the model's unfinished reasoning carries on
|
|
148
|
+
* over the content channel as user-visible prose. The sweep's job is to find the
|
|
149
|
+
* cap past which a model degenerates into reasoning forever, not the cap that
|
|
150
|
+
* types fastest. See the "reasoning budget makes thinking bleed into prose"
|
|
151
|
+
* finding in Otto Knowledge.
|
|
152
|
+
*
|
|
153
|
+
* `-1` is the one budget "largest" must not read as large: an unrestricted
|
|
154
|
+
* budget is the failure this package exists to prevent. It ranks below every
|
|
155
|
+
* finite cap and is recommended only when nothing else survived, which means a
|
|
156
|
+
* cap of any size broke the model.
|
|
157
|
+
*/
|
|
158
|
+
export function rankSweepResults(results) {
|
|
159
|
+
const viable = results.filter((result) => !result.error && result.contentChars > 0);
|
|
160
|
+
return [...viable].sort((a, b) => {
|
|
161
|
+
if (b.filesDelivered !== a.filesDelivered) {
|
|
162
|
+
return b.filesDelivered - a.filesDelivered;
|
|
163
|
+
}
|
|
164
|
+
const rankA = budgetRank(a.budget);
|
|
165
|
+
const rankB = budgetRank(b.budget);
|
|
166
|
+
if (rankA === rankB) {
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
// Compared rather than subtracted: the sentinel's rank is -Infinity, and
|
|
170
|
+
// -Infinity - -Infinity is NaN, which would corrupt the whole sort.
|
|
171
|
+
return rankB > rankA ? 1 : -1;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function budgetRank(budget) {
|
|
175
|
+
return budget === UNRESTRICTED_BUDGET ? Number.NEGATIVE_INFINITY : budget;
|
|
176
|
+
}
|
|
126
177
|
//# sourceMappingURL=sweep.js.map
|
package/dist/runtime/args.d.ts
CHANGED
|
@@ -3,11 +3,24 @@
|
|
|
3
3
|
* needs. Runtime-source agnostic: works the same for an LM Studio runtime or a
|
|
4
4
|
* managed one, since both resolve to a `Runtime` (exe + optional vendorDir).
|
|
5
5
|
*/
|
|
6
|
-
import type { Profile } from "../config/schema.js";
|
|
6
|
+
import type { Calibration, Profile } from "../config/schema.js";
|
|
7
7
|
import type { Model, Runtime } from "../types.js";
|
|
8
8
|
export interface ServeTarget {
|
|
9
9
|
port: number;
|
|
10
10
|
host?: string;
|
|
11
|
+
/** llama.cpp log threshold; 3 preserves its upstream default Info output. */
|
|
12
|
+
logVerbosity?: number;
|
|
13
|
+
/**
|
|
14
|
+
* A directory the engine may use for its slot save/restore/erase actions
|
|
15
|
+
* (`POST /slots`). Passing it is what UNLOCKS the `action=erase` route:
|
|
16
|
+
* llama.cpp refuses every `POST /slots` action when `--slot-save-path` is not
|
|
17
|
+
* set, and the scheduler's cross-chat KV-bleed fix erases a slot's retained
|
|
18
|
+
* KV the moment it is handed to a different chat. The directory must exist -
|
|
19
|
+
* the engine validates it at launch and throws otherwise - so the caller
|
|
20
|
+
* creates it. Absent (null) means the engine's own default: slot actions
|
|
21
|
+
* disabled, which is the pre-fix behavior.
|
|
22
|
+
*/
|
|
23
|
+
slotSavePath?: string | null;
|
|
11
24
|
}
|
|
12
25
|
/**
|
|
13
26
|
* Loader environment the child process needs so it can resolve its shared
|
|
@@ -28,7 +41,7 @@ export declare function buildEnv(runtime: Runtime, baseEnv?: NodeJS.ProcessEnv,
|
|
|
28
41
|
* Only settings that demonstrably matter for stable local inference are emitted -
|
|
29
42
|
* no experimental sampler knobs.
|
|
30
43
|
*/
|
|
31
|
-
export declare function buildArgs(profile: Profile, { port, host }: ServeTarget, model?: Model): string[];
|
|
44
|
+
export declare function buildArgs(profile: Profile, { port, host, logVerbosity, slotSavePath }: ServeTarget, model?: Model, calibration?: Calibration | null): string[];
|
|
32
45
|
/** The same command as a copy-pasteable shell line, for the TUI to display. */
|
|
33
46
|
export declare function formatCommand(runtime: Runtime, args: string[]): string;
|
|
34
47
|
//# sourceMappingURL=args.d.ts.map
|
package/dist/runtime/args.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { promptCacheSize } from "../vram.js";
|
|
2
|
+
const MIB = 1024 * 1024;
|
|
1
3
|
/**
|
|
2
4
|
* Loader environment the child process needs so it can resolve its shared
|
|
3
5
|
* libraries. Both the runtime dir and its vendor dir go first, ahead of the
|
|
@@ -29,7 +31,7 @@ export function buildEnv(runtime, baseEnv = process.env, platform = process.plat
|
|
|
29
31
|
* Only settings that demonstrably matter for stable local inference are emitted -
|
|
30
32
|
* no experimental sampler knobs.
|
|
31
33
|
*/
|
|
32
|
-
export function buildArgs(profile, { port, host = "127.0.0.1" }, model) {
|
|
34
|
+
export function buildArgs(profile, { port, host = "127.0.0.1", logVerbosity = 3, slotSavePath = null }, model, calibration) {
|
|
33
35
|
const args = [
|
|
34
36
|
"-m",
|
|
35
37
|
profile.modelPath ?? "",
|
|
@@ -47,8 +49,19 @@ export function buildArgs(profile, { port, host = "127.0.0.1" }, model) {
|
|
|
47
49
|
host,
|
|
48
50
|
"--port",
|
|
49
51
|
String(port),
|
|
50
|
-
"--no-
|
|
52
|
+
"--no-ui",
|
|
53
|
+
"-lv",
|
|
54
|
+
String(logVerbosity),
|
|
51
55
|
];
|
|
56
|
+
// A slot-save directory unlocks the engine's `POST /slots` actions - the
|
|
57
|
+
// `action=erase` the scheduler needs to wipe a slot's retained KV before it
|
|
58
|
+
// is handed to a different chat (see OWNERSHIP in scheduler.ts). Without it
|
|
59
|
+
// llama.cpp refuses every slot action with "start it with --slot-save-path".
|
|
60
|
+
// The directory must already exist: the engine validates it at launch and
|
|
61
|
+
// throws otherwise, so the caller creates it before building the args.
|
|
62
|
+
if (slotSavePath) {
|
|
63
|
+
args.push("--slot-save-path", slotSavePath);
|
|
64
|
+
}
|
|
52
65
|
if (profile.vision && profile.mmprojPath) {
|
|
53
66
|
args.push("--mmproj", profile.mmprojPath);
|
|
54
67
|
}
|
|
@@ -64,8 +77,44 @@ export function buildArgs(profile, { port, host = "127.0.0.1" }, model) {
|
|
|
64
77
|
args.push("--reasoning-budget-message", profile.reasoningBudgetMessage);
|
|
65
78
|
}
|
|
66
79
|
}
|
|
80
|
+
// Whether the reasoning trace stays in the whole history or is trimmed to the
|
|
81
|
+
// last assistant message. Emitted only on an explicit choice: llama-server's
|
|
82
|
+
// own default is the template's, and there is no third flag spelling for "do
|
|
83
|
+
// what the template says" - the absence of the flag IS that state.
|
|
84
|
+
if (profile.preserveReasoning === true) {
|
|
85
|
+
args.push("--reasoning-preserve");
|
|
86
|
+
}
|
|
87
|
+
else if (profile.preserveReasoning === false) {
|
|
88
|
+
args.push("--no-reasoning-preserve");
|
|
89
|
+
}
|
|
90
|
+
// Sampler settings. Always emitted, because the profile stores llama.cpp's own
|
|
91
|
+
// defaults verbatim, so an untouched profile produces the run it always did
|
|
92
|
+
// while the values stay visible and editable rather than implicit.
|
|
93
|
+
for (const [flag, value] of [
|
|
94
|
+
["--temp", profile.temperature],
|
|
95
|
+
["--top-p", profile.topP],
|
|
96
|
+
["--top-k", profile.topK],
|
|
97
|
+
["--min-p", profile.minP],
|
|
98
|
+
["--presence-penalty", profile.presencePenalty],
|
|
99
|
+
["--repeat-penalty", profile.repeatPenalty],
|
|
100
|
+
]) {
|
|
101
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
102
|
+
args.push(flag, String(value));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
67
105
|
if (profile.parallelSlots)
|
|
68
106
|
args.push("--parallel", String(profile.parallelSlots));
|
|
107
|
+
// Prompt cache: how much host RAM llama-server may use to park the KV state
|
|
108
|
+
// of chats that lost their slot, so returning to one is a bulk copy instead
|
|
109
|
+
// of a full re-prefill. Sized from the chat count the user chose, because a
|
|
110
|
+
// raw MiB figure is unusable without knowing this model's KV bytes/token.
|
|
111
|
+
// Emitted only when the size is real: with no measurement, the theoretical
|
|
112
|
+
// KV cost overestimates by multiples, and reserving multiples of the RAM
|
|
113
|
+
// actually needed is worse than leaving llama.cpp's own default in place.
|
|
114
|
+
const cache = model ? promptCacheSize(model, profile, calibration) : null;
|
|
115
|
+
if (cache && cache.source === "measured") {
|
|
116
|
+
args.push("--cache-ram", String(Math.max(1, Math.round(cache.totalBytes / MIB))));
|
|
117
|
+
}
|
|
69
118
|
if (profile.contextMultiplier > 1) {
|
|
70
119
|
const nativeContext = model?.metadata?.contextLength;
|
|
71
120
|
args.push("--rope-scaling", "yarn", "--rope-scale", String(profile.contextMultiplier), ...(nativeContext ? ["--yarn-orig-ctx", String(nativeContext)] : []), ...(model?.metadata?.arch
|
|
@@ -78,9 +127,15 @@ export function buildArgs(profile, { port, host = "127.0.0.1" }, model) {
|
|
|
78
127
|
args.push("-ub", String(profile.ubatchSize));
|
|
79
128
|
if (profile.chatTemplateFile) {
|
|
80
129
|
args.push("--chat-template-file", profile.chatTemplateFile);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
130
|
+
}
|
|
131
|
+
const templateKwargs = { ...profile.chatTemplateKwargs };
|
|
132
|
+
const preservation = model?.reasoningPreservation;
|
|
133
|
+
if (preservation?.templateArgument) {
|
|
134
|
+
templateKwargs[preservation.templateArgument] =
|
|
135
|
+
profile.preserveReasoning ?? preservation.default ?? false;
|
|
136
|
+
}
|
|
137
|
+
if (Object.keys(templateKwargs).length > 0) {
|
|
138
|
+
args.push("--chat-template-kwargs", JSON.stringify(templateKwargs));
|
|
84
139
|
}
|
|
85
140
|
if (profile.extraArgs && profile.extraArgs.length)
|
|
86
141
|
args.push(...profile.extraArgs);
|
package/dist/runtime/managed.js
CHANGED
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
* also needs `libcublas`, `libcudart` and `libnccl.so.2` - and NCCL ships in
|
|
39
39
|
* neither NVIDIA redistributable. Do not reopen this without a measurement on
|
|
40
40
|
* hardware that does *not* report NV_coopmat2, which is the one case where
|
|
41
|
-
* the gap could still be real.
|
|
42
|
-
*
|
|
41
|
+
* the gap could still be real. Measured 2026-08-04; the full method and
|
|
42
|
+
* numbers are the "cuda vs vulkan" finding in Otto Knowledge.
|
|
43
43
|
* - **No Linux asset ships `libgomp.so.1`**, which `llama-server` hard-links,
|
|
44
44
|
* so a host without `libgomp1` installs a runtime that then exits 127 on
|
|
45
45
|
* spawn. Windows bundles its OpenMP runtime (`libomp140.x86_64.dll`); Linux
|
|
@@ -64,6 +64,14 @@ export interface InferenceActivitySnapshot {
|
|
|
64
64
|
processing: number;
|
|
65
65
|
thinking: number;
|
|
66
66
|
generating: number;
|
|
67
|
+
/**
|
|
68
|
+
* Per-slot stage join, present only when at least one tracked request was
|
|
69
|
+
* pinned to a llama-server slot by the router (host API v3). Keys are the
|
|
70
|
+
* engine's slot ids as reported by `/slots`; values are that request's
|
|
71
|
+
* proxy-side stage. Absent (not just empty) on brains that predate the join,
|
|
72
|
+
* which is how an old client tells "no join data" from "no pinned requests".
|
|
73
|
+
*/
|
|
74
|
+
slotStages?: Record<string, InferenceStage>;
|
|
67
75
|
}
|
|
68
76
|
/**
|
|
69
77
|
* Which in-flight completions are currently mid-thought.
|
|
@@ -91,6 +99,17 @@ export declare class ReasoningTracker {
|
|
|
91
99
|
onChange(listener: () => void): () => void;
|
|
92
100
|
/** A completion was dispatched to llama-server and awaits its first output delta. */
|
|
93
101
|
begin(requestId: string): void;
|
|
102
|
+
/**
|
|
103
|
+
* Record the engine slot this request was pinned to. Called exactly once per
|
|
104
|
+
* request, at dispatch - the pin is injected into the outbound body before
|
|
105
|
+
* the request goes out, so the association exists before the first chunk and
|
|
106
|
+
* `observe` never has to learn about it.
|
|
107
|
+
*
|
|
108
|
+
* Idempotent and self-cleaning: a repeat for the same slot is a no-op, and a
|
|
109
|
+
* different slot replaces it, so a request that somehow moves slots (a
|
|
110
|
+
* restarted engine hands a task out again) reports where it is now.
|
|
111
|
+
*/
|
|
112
|
+
setSlot(requestId: string, slotId: number): void;
|
|
94
113
|
/** Note a chunk of `requestId`'s stream. Cheap enough to call per chunk. */
|
|
95
114
|
observe(requestId: string, text: string): void;
|
|
96
115
|
/** Forget the request. Must be called on end *and* on error, or the flag sticks. */
|