@otto-code/brain 0.7.5 → 0.8.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/bench/context-corpus.js +3 -3
- package/dist/bench/corpus.js +2 -2
- package/dist/bench/curated-repos.js +3 -3
- package/dist/bench/health.d.ts +1 -1
- package/dist/bench/health.js +2 -2
- package/dist/bench/tasks.js +2 -2
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +1 -1
- package/dist/commands/bench.d.ts +1 -1
- package/dist/commands/bench.js +27 -4
- package/dist/commands/calibrate.d.ts +1 -1
- package/dist/commands/calibrate.js +5 -2
- package/dist/commands/catalog.d.ts +1 -1
- package/dist/commands/config.d.ts +1 -1
- package/dist/commands/config.js +9 -0
- package/dist/commands/lifecycle.js +1 -1
- package/dist/commands/pull.d.ts +1 -1
- package/dist/commands/pull.js +9 -4
- package/dist/commands/report.d.ts +1 -1
- package/dist/commands/rescore.d.ts +1 -1
- package/dist/commands/rescore.js +1 -1
- package/dist/commands/runtime.d.ts +1 -1
- package/dist/commands/scan.d.ts +1 -1
- package/dist/commands/scan.js +6 -1
- package/dist/commands/search.d.ts +1 -1
- package/dist/commands/share.js +2 -2
- package/dist/commands/sweep.d.ts +2 -2
- package/dist/commands/sweep.js +22 -8
- package/dist/commands/ui.d.ts +1 -1
- package/dist/commands/ui.js +37 -5
- package/dist/config/index.d.ts +2 -1
- package/dist/config/index.js +2 -1
- package/dist/config/otto-home.js +1 -1
- package/dist/config/paths.d.ts +1 -0
- package/dist/config/paths.js +4 -0
- package/dist/config/profile-edit.d.ts +94 -0
- package/dist/config/profile-edit.js +269 -0
- package/dist/config/profiles.d.ts +2 -2
- package/dist/config/profiles.js +1 -1
- package/dist/config/schema.d.ts +4 -4
- package/dist/config/schema.js +6 -6
- package/dist/config/store.d.ts +1 -1
- package/dist/config/store.js +1 -1
- package/dist/models/download.js +1 -1
- package/dist/models/enrich.d.ts +2 -2
- package/dist/models/index.d.ts +1 -1
- package/dist/models/index.js +1 -1
- package/dist/models/pick.d.ts +9 -0
- package/dist/models/pick.js +24 -1
- package/dist/ops/report.js +28 -28
- package/dist/ops/results.d.ts +128 -9
- package/dist/ops/results.js +77 -5
- package/dist/output/render.js +1 -1
- package/dist/output/types.d.ts +1 -1
- package/dist/runtime/args.d.ts +1 -1
- package/dist/runtime/args.js +1 -1
- package/dist/runtime/managed.js +4 -4
- package/dist/service/activity.d.ts +83 -0
- package/dist/service/activity.js +216 -0
- package/dist/service/host-api.d.ts +132 -0
- package/dist/service/host-api.js +397 -0
- package/dist/service/http-util.d.ts +27 -0
- package/dist/service/http-util.js +72 -0
- package/dist/service/model-selector.d.ts +2 -2
- package/dist/service/model-selector.js +6 -6
- package/dist/service/router.d.ts +28 -4
- package/dist/service/router.js +128 -94
- package/dist/service/scheduler.d.ts +2 -2
- package/dist/service/scheduler.js +1 -1
- package/dist/service/serve.d.ts +8 -1
- package/dist/service/serve.js +69 -16
- package/dist/service/supervisor.d.ts +6 -0
- package/dist/service/supervisor.js +2 -0
- package/dist/service/tailscale.js +1 -1
- package/dist/service/tls.d.ts +4 -4
- package/dist/service/tls.js +3 -3
- package/dist/sysmon.d.ts +20 -4
- package/dist/sysmon.js +42 -18
- package/dist/tui/app.d.ts +12 -2
- package/dist/tui/app.js +48 -23
- package/dist/vram.d.ts +8 -1
- package/dist/vram.js +6 -3
- package/package.json +1 -1
package/dist/ops/report.js
CHANGED
|
@@ -26,7 +26,7 @@ function escapeHtml(text) {
|
|
|
26
26
|
return String(text).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
27
27
|
}
|
|
28
28
|
function formatGiB(bytes) {
|
|
29
|
-
return bytes ? `${(bytes / 1024 ** 3).toFixed(1)} GB` : "
|
|
29
|
+
return bytes ? `${(bytes / 1024 ** 3).toFixed(1)} GB` : "-";
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
32
|
* Per-model score card: each task labelled directly with its weight, then the
|
|
@@ -77,8 +77,8 @@ function groupedBars(records, columns, weightOf) {
|
|
|
77
77
|
(weight
|
|
78
78
|
? `<text x="${labelWidth - 8}" y="${y + barHeight - 2}" text-anchor="end" class="twt">×${weight}</text>`
|
|
79
79
|
: "") +
|
|
80
|
-
bar(labelWidth, y, score * chartWidth, "scorebar", `${label}: ${(score * 100).toFixed(0)}%${weight ? ` (weight ${weight})` : ""}
|
|
81
|
-
`<text x="${labelWidth + chartWidth + 8}" y="${y + barHeight - 2}" class="val">${task ? `${(score * 100).toFixed(0)}%` : "
|
|
80
|
+
bar(labelWidth, y, score * chartWidth, "scorebar", `${label}: ${(score * 100).toFixed(0)}%${weight ? ` (weight ${weight})` : ""} - ${escapeHtml(task ? task.summary : "not run")}`) +
|
|
81
|
+
`<text x="${labelWidth + chartWidth + 8}" y="${y + barHeight - 2}" class="val">${task ? `${(score * 100).toFixed(0)}%` : "-"}</text>`);
|
|
82
82
|
});
|
|
83
83
|
// The weighted Overall, set off by a divider so it reads as the sum of the
|
|
84
84
|
// task rows above it rather than one more task.
|
|
@@ -138,7 +138,7 @@ function depthChart(series, { valueOf, label, unit, formatValue }) {
|
|
|
138
138
|
// 2px surface ring keeps overlapping markers separable.
|
|
139
139
|
parts.push(`<circle cx="${sx(p.promptTokens || 0).toFixed(1)}" cy="${sy(v).toFixed(1)}" r="4.5" ` +
|
|
140
140
|
`fill="var(--series-${i + 1})" stroke="var(--surface-1)" stroke-width="2">` +
|
|
141
|
-
`<title>${escapeHtml(s.name)}
|
|
141
|
+
`<title>${escapeHtml(s.name)} - ${(p.promptTokens || 0).toLocaleString()} tokens: ` +
|
|
142
142
|
`${formatValue(v)} ${escapeHtml(unit)}</title></circle>`);
|
|
143
143
|
}
|
|
144
144
|
});
|
|
@@ -175,7 +175,7 @@ function runsChart(series) {
|
|
|
175
175
|
for (const p of ordered) {
|
|
176
176
|
parts.push(`<circle cx="${sx(p.runIndex).toFixed(1)}" cy="${sy(p.score).toFixed(1)}" r="4.5" ` +
|
|
177
177
|
`fill="var(--series-${i + 1})" stroke="var(--surface-1)" stroke-width="2">` +
|
|
178
|
-
`<title>${escapeHtml(s.name)}
|
|
178
|
+
`<title>${escapeHtml(s.name)} - run ${p.runIndex}: ${(p.score * 100).toFixed(0)}%</title></circle>`);
|
|
179
179
|
}
|
|
180
180
|
});
|
|
181
181
|
return (`<svg viewBox="0 0 ${width} ${height}" width="100%" role="img" ` +
|
|
@@ -261,12 +261,12 @@ function build(records, allRuns = records) {
|
|
|
261
261
|
const cells = columns
|
|
262
262
|
.map((c) => {
|
|
263
263
|
const task = r.tasks.find((t) => t.id === c.id);
|
|
264
|
-
return `<td class="num">${task ? `${(task.score * 100).toFixed(0)}%` : "
|
|
264
|
+
return `<td class="num">${task ? `${(task.score * 100).toFixed(0)}%` : "-"}</td>`;
|
|
265
265
|
})
|
|
266
266
|
.join("");
|
|
267
|
-
return (`<tr><td>${escapeHtml(r.model.displayName)}</td><td>${escapeHtml(r.model.quant || "
|
|
267
|
+
return (`<tr><td>${escapeHtml(r.model.displayName)}</td><td>${escapeHtml(r.model.quant || "-")}</td>` +
|
|
268
268
|
`<td class="num">${(r.profile?.contextSize || 0).toLocaleString()}</td>` +
|
|
269
|
-
`<td class="num">${r.profile?.reasoningBudget ?? "
|
|
269
|
+
`<td class="num">${r.profile?.reasoningBudget ?? "-"}</td>` +
|
|
270
270
|
`${cells}<td class="num strong">${(r.overall * 100).toFixed(0)}%</td>` +
|
|
271
271
|
`<td class="num">${formatGiB(r.vramBytes)}</td>` +
|
|
272
272
|
`<td class="muted">${escapeHtml(r.ranAt.slice(0, 16).replace("T", " "))}</td></tr>`);
|
|
@@ -290,18 +290,18 @@ function build(records, allRuns = records) {
|
|
|
290
290
|
</header>
|
|
291
291
|
|
|
292
292
|
<section class="tiles">
|
|
293
|
-
${statTile("Best model", best.model.displayName.slice(0, 26), `${(best.overall * 100).toFixed(0)}%
|
|
293
|
+
${statTile("Best model", best.model.displayName.slice(0, 26), `${(best.overall * 100).toFixed(0)}% - ${best.grade}`)}
|
|
294
294
|
${statTile("Runs recorded", records.length, `${columns.length} task categories`)}
|
|
295
|
-
${statTile("Peak generation", fastest ? `${fastest.toFixed(0)} tok/s` : "
|
|
295
|
+
${statTile("Peak generation", fastest ? `${fastest.toFixed(0)} tok/s` : "-", "at shallowest depth measured")}
|
|
296
296
|
${statTile("VRAM at load", formatGiB(best.vramBytes), best.loadSeconds ? `loaded in ${best.loadSeconds.toFixed(1)}s` : "")}
|
|
297
297
|
</section>
|
|
298
298
|
|
|
299
299
|
<section class="card">
|
|
300
300
|
<h2>Scores by task</h2>
|
|
301
|
-
<p class="cap">Each row is one task, scored 0–100% from objectively verifiable outcomes
|
|
301
|
+
<p class="cap">Each row is one task, scored 0–100% from objectively verifiable outcomes - tool calls
|
|
302
302
|
checked against expected name and arguments, generated code compiled and its tests executed. The
|
|
303
303
|
<b>×N</b> after a task is its weight. <b>Overall</b> is the weighted mean of the task scores,
|
|
304
|
-
<span class="mono">Σ(score × weight) ÷ Σ(weight)</span>
|
|
304
|
+
<span class="mono">Σ(score × weight) ÷ Σ(weight)</span> - so a heavier task moves the headline
|
|
305
305
|
number more, and it is not the plain average of the bars above it.</p>
|
|
306
306
|
${groupedBars(ranked, columns, weightOf)}
|
|
307
307
|
</section>
|
|
@@ -325,7 +325,7 @@ function build(records, allRuns = records) {
|
|
|
325
325
|
<section class="card">
|
|
326
326
|
<h2>Throughput against prompt depth</h2>
|
|
327
327
|
<p class="cap">Generation speed at the same depths. A collapse here usually means the KV cache
|
|
328
|
-
spilled out of VRAM
|
|
328
|
+
spilled out of VRAM - the failure this tool's budget check exists to prevent.</p>
|
|
329
329
|
${legend(depthSeries.map((s) => s.name.slice(0, 30)))}
|
|
330
330
|
${depthChart(depthSeries, {
|
|
331
331
|
valueOf: (p) => p.generatePerSecond,
|
|
@@ -340,7 +340,7 @@ function build(records, allRuns = records) {
|
|
|
340
340
|
<section class="card">
|
|
341
341
|
<h2>Does each task actually tell us anything?</h2>
|
|
342
342
|
<p class="cap">A task every model passes cannot rank them, however hard it looks. This is the
|
|
343
|
-
score spread across all runs
|
|
343
|
+
score spread across all runs - low spread means the task is saturated and its contribution to
|
|
344
344
|
the overall figure is inflating everyone equally. The weight is how hard that task pulls on the
|
|
345
345
|
overall score, so a saturated <em>heavy</em> task matters most.</p>
|
|
346
346
|
<div class="scroll">
|
|
@@ -352,9 +352,9 @@ function build(records, allRuns = records) {
|
|
|
352
352
|
const tone = s.verdict === "saturated" ? "bad" : s.verdict === "weak" ? "warn" : "good";
|
|
353
353
|
return (`<tr><td>${escapeHtml(s.category)}</td>` +
|
|
354
354
|
`<td class="num">×${weightOf.get(s.id) ?? "?"}</td>` +
|
|
355
|
-
`<td class="num">${s.min === undefined ? "
|
|
356
|
-
`<td class="num">${s.max === undefined ? "
|
|
357
|
-
`<td class="num strong">${s.spread === null ? "
|
|
355
|
+
`<td class="num">${s.min === undefined ? "-" : `${(s.min * 100).toFixed(0)}%`}</td>` +
|
|
356
|
+
`<td class="num">${s.max === undefined ? "-" : `${(s.max * 100).toFixed(0)}%`}</td>` +
|
|
357
|
+
`<td class="num strong">${s.spread === null ? "-" : `${(s.spread * 100).toFixed(0)} pts`}</td>` +
|
|
358
358
|
`<td class="v-${tone}">${escapeHtml(s.verdict)}</td></tr>`);
|
|
359
359
|
})
|
|
360
360
|
.join("")}</tbody>
|
|
@@ -362,7 +362,7 @@ function build(records, allRuns = records) {
|
|
|
362
362
|
</div>
|
|
363
363
|
${saturated.length
|
|
364
364
|
? `<ul class="notes"><li><strong>${saturated.map((s) => escapeHtml(s.category)).join(", ")}</strong>
|
|
365
|
-
${saturated.length === 1 ? "is" : "are"} saturated
|
|
365
|
+
${saturated.length === 1 ? "is" : "are"} saturated - every model scores the same, so
|
|
366
366
|
${saturated.length === 1 ? "it needs" : "they need"} to get harder before
|
|
367
367
|
${saturated.length === 1 ? "it" : "they"} can contribute to a ranking.</li></ul>`
|
|
368
368
|
: ""}
|
|
@@ -372,7 +372,7 @@ function build(records, allRuns = records) {
|
|
|
372
372
|
<h2>Is the benchmark consistent?</h2>
|
|
373
373
|
<p class="cap">The same model + settings, measured more than once. A method you can trust holds a
|
|
374
374
|
flat line across runs; a jagged one means the score is noisy and small gaps between models are
|
|
375
|
-
not real. Runs are numbered, not dated
|
|
375
|
+
not real. Runs are numbered, not dated - only how many times we have measured matters.</p>
|
|
376
376
|
${consistencySeries.length
|
|
377
377
|
? `
|
|
378
378
|
${legend(consistencySeries.map((s) => s.name.slice(0, 30)))}
|
|
@@ -380,7 +380,7 @@ function build(records, allRuns = records) {
|
|
|
380
380
|
<p class="axl">run number</p>`
|
|
381
381
|
: `
|
|
382
382
|
<p class="empty">No config has been run more than once yet. Re-run a model a few times to see
|
|
383
|
-
its spread
|
|
383
|
+
its spread - the table below shows mean ± spread as repeats accumulate.</p>`}
|
|
384
384
|
<div class="scroll">
|
|
385
385
|
<table>
|
|
386
386
|
<thead><tr><th>Model</th><th class="num">Runs</th><th class="num">Mean</th>
|
|
@@ -392,9 +392,9 @@ function build(records, allRuns = records) {
|
|
|
392
392
|
const tone = v.count < 2 ? "muted" : spreadPts > 8 ? "v-warn" : "v-good";
|
|
393
393
|
return (`<tr><td>${escapeHtml(v.model.displayName)}</td>` +
|
|
394
394
|
`<td class="num">${v.count}</td>` +
|
|
395
|
-
`<td class="num strong">${o.mean === undefined ? "
|
|
396
|
-
`<td class="num ${tone}">${v.count < 2 ? "
|
|
397
|
-
`<td class="num">${v.count < 2 ? "
|
|
395
|
+
`<td class="num strong">${o.mean === undefined ? "-" : `${(o.mean * 100).toFixed(0)}%`}</td>` +
|
|
396
|
+
`<td class="num ${tone}">${v.count < 2 ? "-" : `${spreadPts.toFixed(1)} pts`}</td>` +
|
|
397
|
+
`<td class="num">${v.count < 2 ? "-" : `${((o.min ?? 0) * 100).toFixed(0)}–${((o.max ?? 0) * 100).toFixed(0)}%`}</td></tr>`);
|
|
398
398
|
})
|
|
399
399
|
.join("")}</tbody>
|
|
400
400
|
</table>
|
|
@@ -406,7 +406,7 @@ function build(records, allRuns = records) {
|
|
|
406
406
|
<section class="card">
|
|
407
407
|
<h2>System health during runs</h2>
|
|
408
408
|
<p class="cap">The machine state each score was measured under. A throttled or power-capped run is
|
|
409
|
-
slower and lower for reasons that have nothing to do with the model
|
|
409
|
+
slower and lower for reasons that have nothing to do with the model - read those scores with
|
|
410
410
|
suspicion, or re-run them once the GPU has cooled.</p>
|
|
411
411
|
<div class="scroll">
|
|
412
412
|
<table>
|
|
@@ -418,10 +418,10 @@ function build(records, allRuns = records) {
|
|
|
418
418
|
const throttled = h.thermalThrottle || h.powerThrottle;
|
|
419
419
|
const label = h.thermalThrottle ? "thermal" : h.powerThrottle ? "power" : "no";
|
|
420
420
|
return (`<tr><td>${escapeHtml(r.model.displayName)}</td>` +
|
|
421
|
-
`<td class="num">${h.tempC ? `${h.tempC.max}°C` : "
|
|
422
|
-
`<td class="num">${h.powerW ? `${h.powerW.avg.toFixed(0)} W` : "
|
|
423
|
-
`<td class="num">${h.gpuUtilPct ? `${h.gpuUtilPct.avg.toFixed(0)}%` : "
|
|
424
|
-
`<td class="num">${h.vramUsedMiB ? `${(h.vramUsedMiB.max / 1024).toFixed(1)} GB` : "
|
|
421
|
+
`<td class="num">${h.tempC ? `${h.tempC.max}°C` : "-"}</td>` +
|
|
422
|
+
`<td class="num">${h.powerW ? `${h.powerW.avg.toFixed(0)} W` : "-"}</td>` +
|
|
423
|
+
`<td class="num">${h.gpuUtilPct ? `${h.gpuUtilPct.avg.toFixed(0)}%` : "-"}</td>` +
|
|
424
|
+
`<td class="num">${h.vramUsedMiB ? `${(h.vramUsedMiB.max / 1024).toFixed(1)} GB` : "-"}</td>` +
|
|
425
425
|
`<td class="${throttled ? "v-bad" : "v-good"}">${throttled ? `⚠ ${label}` : "no"}</td></tr>`);
|
|
426
426
|
})
|
|
427
427
|
.join("")}</tbody>
|
package/dist/ops/results.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { GpuInfo, Model, ModelFeatures } from "../types.js";
|
|
2
|
-
import type { Profile } from "../config/schema.js";
|
|
2
|
+
import type { Calibration, Profile } from "../config/schema.js";
|
|
3
|
+
import type { FitResult } from "../vram.js";
|
|
3
4
|
declare const RESULTS_DIR: string;
|
|
4
5
|
/** Aggregate of a numeric health series (nvidia-smi samples during a run). */
|
|
5
6
|
export interface AggStat {
|
|
@@ -49,7 +50,14 @@ export interface BenchReport {
|
|
|
49
50
|
archiveId?: string | null;
|
|
50
51
|
system?: SystemHealth | null;
|
|
51
52
|
}
|
|
52
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* The model identity persisted with a run.
|
|
55
|
+
*
|
|
56
|
+
* The geometry fields are here because a context size is only sensible against
|
|
57
|
+
* them: 32k on a model whose GGUF header says 8k native is a setup error, not a
|
|
58
|
+
* capability, and layer/KV-head counts are what make a KV cost per token
|
|
59
|
+
* explicable rather than just large.
|
|
60
|
+
*/
|
|
53
61
|
export interface RecordModel {
|
|
54
62
|
id: string | null;
|
|
55
63
|
displayName: string;
|
|
@@ -58,15 +66,92 @@ export interface RecordModel {
|
|
|
58
66
|
sizeBytes: number | null;
|
|
59
67
|
publisher: string | null;
|
|
60
68
|
features: ModelFeatures | null;
|
|
69
|
+
/** Native context from the GGUF header - the ceiling `contextSize` sits under. */
|
|
70
|
+
contextLength: number | null;
|
|
71
|
+
blockCount: number | null;
|
|
72
|
+
headCountKv: number | null;
|
|
61
73
|
}
|
|
62
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* The profile persisted with a run: every field that reaches llama-server.
|
|
76
|
+
*
|
|
77
|
+
* Schema 1 kept six of these. The omitted ones are exactly the ones that explain
|
|
78
|
+
* a bad score - `gpuLayers` short of the model's layer count silently runs part
|
|
79
|
+
* of it on the CPU, `parallelSlots` splits the context between slots so the
|
|
80
|
+
* effective window is a fraction of `contextSize`, and `extraArgs` can override
|
|
81
|
+
* anything above.
|
|
82
|
+
*/
|
|
63
83
|
export interface RecordProfile {
|
|
64
84
|
contextSize: number;
|
|
65
85
|
cacheTypeK: string;
|
|
66
86
|
cacheTypeV: string;
|
|
67
87
|
reasoningBudget: number;
|
|
88
|
+
reasoningBudgetMessage: string | null;
|
|
68
89
|
vision: boolean;
|
|
69
90
|
flashAttention: boolean;
|
|
91
|
+
gpuLayers: number | null;
|
|
92
|
+
parallelSlots: number | null;
|
|
93
|
+
batchSize: number | null;
|
|
94
|
+
ubatchSize: number | null;
|
|
95
|
+
extraArgs: string[];
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* How the run was actually set up, as opposed to how it was configured.
|
|
99
|
+
*
|
|
100
|
+
* The gap between the two is where bad scores come from. `fitToBudget` will cut
|
|
101
|
+
* a profile's context down to whatever fits the GPU and run anyway, which is the
|
|
102
|
+
* right call at load time and a silent lie afterwards: the record would say the
|
|
103
|
+
* model scored 41% at 128k context when it was really measured at 16k. Same for
|
|
104
|
+
* the KV budget - a context chosen off the theoretical formula (which
|
|
105
|
+
* overestimates by up to 4x) is a different measurement from one chosen off a
|
|
106
|
+
* calibration, even when the numbers land in the same place.
|
|
107
|
+
*/
|
|
108
|
+
export interface RecordSetup {
|
|
109
|
+
/**
|
|
110
|
+
* The exact llama-server argv this run was served with. Every other field
|
|
111
|
+
* here is a convenience; this one is the ground truth, and it is what makes a
|
|
112
|
+
* run reproducible by hand.
|
|
113
|
+
*/
|
|
114
|
+
args: string[] | null;
|
|
115
|
+
/** True when the VRAM fit had to change the profile for the run to happen. */
|
|
116
|
+
adjusted: boolean;
|
|
117
|
+
/** The context the profile asked for, before any fit adjustment. */
|
|
118
|
+
requestedContextSize: number | null;
|
|
119
|
+
/** Why the fit changed the profile, in the fitter's own words. */
|
|
120
|
+
adjustReason: string | null;
|
|
121
|
+
/** Where the KV bytes/token came from. `theoretical` means nobody measured. */
|
|
122
|
+
kvSource: "measured" | "theoretical" | "unknown" | null;
|
|
123
|
+
/**
|
|
124
|
+
* True when the calibration came from a relative with the same attention
|
|
125
|
+
* geometry rather than from this file. Never present it as measured.
|
|
126
|
+
*/
|
|
127
|
+
kvInherited: boolean;
|
|
128
|
+
kvBytesPerToken: number | null;
|
|
129
|
+
/** The formula's answer, kept beside the measured one to expose the gap. */
|
|
130
|
+
theoreticalKvBytesPerToken: number | null;
|
|
131
|
+
/** What the budget predicted the run would take, against `vramBytes` observed. */
|
|
132
|
+
predictedVramBytes: number | null;
|
|
133
|
+
reserveBytes: number | null;
|
|
134
|
+
headroomBytes: number | null;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Which suite graded the run.
|
|
138
|
+
*
|
|
139
|
+
* Two runs scored on different task sets are not comparable, and neither are a
|
|
140
|
+
* run that was allowed to execute generated code and one that was only allowed
|
|
141
|
+
* to syntax-check it. `configKey` deliberately does not include any of this (see
|
|
142
|
+
* `configKey()` below), so it is recorded here instead of silently merging
|
|
143
|
+
* unlike runs into one group.
|
|
144
|
+
*/
|
|
145
|
+
export interface RecordSuite {
|
|
146
|
+
/** False when `--no-execute` limited grading to a syntax check. */
|
|
147
|
+
execute: boolean | null;
|
|
148
|
+
concurrency: number | null;
|
|
149
|
+
/** Prompt depths for the depth-scaling task, when overridden. */
|
|
150
|
+
depths: number[] | null;
|
|
151
|
+
/** Task ids the run was restricted to (`--only`), when restricted. */
|
|
152
|
+
only: string[] | null;
|
|
153
|
+
/** True when the static suite was replaced by tasks mined from a repo. */
|
|
154
|
+
mined: boolean;
|
|
70
155
|
}
|
|
71
156
|
/** The GPU identity persisted with a run. */
|
|
72
157
|
export interface RecordGpu {
|
|
@@ -85,12 +170,24 @@ export interface RecordTask {
|
|
|
85
170
|
detail: unknown;
|
|
86
171
|
error: string | null;
|
|
87
172
|
}
|
|
88
|
-
/**
|
|
173
|
+
/**
|
|
174
|
+
* One stored benchmark run - the shared shape used across results and report.
|
|
175
|
+
*
|
|
176
|
+
* `schema` is 2 as of the setup-capture change: 2 carries the full profile plus
|
|
177
|
+
* `setup` and `suite`; 1 carried six profile fields and neither. Readers must
|
|
178
|
+
* treat everything added in 2 as absent on an older record rather than assuming
|
|
179
|
+
* it, because those runs are still perfectly good scores - they just cannot say
|
|
180
|
+
* what they were measured with.
|
|
181
|
+
*/
|
|
89
182
|
export interface RunRecord {
|
|
90
183
|
schema: number;
|
|
91
184
|
ranAt: string;
|
|
92
185
|
model: RecordModel;
|
|
93
186
|
profile: RecordProfile | null;
|
|
187
|
+
/** Schema 2+. Absent on runs saved before setup capture. */
|
|
188
|
+
setup: RecordSetup | null;
|
|
189
|
+
/** Schema 2+. Absent on runs saved before setup capture. */
|
|
190
|
+
suite: RecordSuite | null;
|
|
94
191
|
configKey: string;
|
|
95
192
|
archiveId: string | null;
|
|
96
193
|
gpu: RecordGpu | null;
|
|
@@ -106,15 +203,29 @@ export interface RunRecord {
|
|
|
106
203
|
file?: string;
|
|
107
204
|
runIndex?: number;
|
|
108
205
|
}
|
|
109
|
-
/**
|
|
206
|
+
/**
|
|
207
|
+
* Arguments to `save`.
|
|
208
|
+
*
|
|
209
|
+
* Everything past `report` is optional so a caller that cannot supply it still
|
|
210
|
+
* writes a usable record - but every caller that CAN should, because a run saved
|
|
211
|
+
* without its setup is a score nobody can explain later.
|
|
212
|
+
*/
|
|
110
213
|
export interface SaveOptions {
|
|
111
214
|
model: Model | null;
|
|
215
|
+
/** The profile the run was actually served with (post-fit, not as configured). */
|
|
112
216
|
profile: Profile | null;
|
|
113
217
|
report: BenchReport;
|
|
114
218
|
gpu?: GpuInfo | null;
|
|
115
219
|
runtime?: string | null;
|
|
116
220
|
archiveId?: string | null;
|
|
117
221
|
system?: SystemHealth | null;
|
|
222
|
+
/** The argv the supervisor launched, straight from `Supervisor.args`. */
|
|
223
|
+
args?: string[] | null;
|
|
224
|
+
/** The VRAM fit that produced `profile`, including what it had to change. */
|
|
225
|
+
fit?: FitResult | null;
|
|
226
|
+
/** The calibration the fit was computed against, if there was one. */
|
|
227
|
+
calibration?: Calibration | null;
|
|
228
|
+
suite?: Partial<RecordSuite> | null;
|
|
118
229
|
timestamp?: Date;
|
|
119
230
|
}
|
|
120
231
|
/** Result of `save`: the written file path plus the persisted record. */
|
|
@@ -172,9 +283,17 @@ export interface TaskColumn {
|
|
|
172
283
|
category: string;
|
|
173
284
|
}
|
|
174
285
|
declare function slugify(text: string): string;
|
|
175
|
-
/**
|
|
286
|
+
/**
|
|
287
|
+
* Stable identity for "same model, same settings", so reruns can be grouped.
|
|
288
|
+
*
|
|
289
|
+
* Deliberately unchanged when the record grew: this string is the grouping key
|
|
290
|
+
* every stored run was written with, and widening it would not re-key history -
|
|
291
|
+
* it would split each model's past runs from its future ones and quietly reset
|
|
292
|
+
* every variance figure on the page. New settings are recorded as data, not
|
|
293
|
+
* folded in here.
|
|
294
|
+
*/
|
|
176
295
|
declare function configKey(profile: Profile | null): string;
|
|
177
|
-
declare function save({ model, profile, report, gpu, runtime, archiveId, system, timestamp, }: SaveOptions): SaveResult;
|
|
296
|
+
declare function save({ model, profile, report, gpu, runtime, archiveId, system, args, fit, calibration, suite, timestamp, }: SaveOptions): SaveResult;
|
|
178
297
|
/** Every stored run, newest first. */
|
|
179
298
|
declare function loadAll(): RunRecord[];
|
|
180
299
|
/**
|
|
@@ -185,13 +304,13 @@ declare function latestPerConfig(records?: RunRecord[]): RunRecord[];
|
|
|
185
304
|
/**
|
|
186
305
|
* Group every run by model+config (all runs kept, not just the latest), and
|
|
187
306
|
* number them oldest→newest so a run can be plotted against run number. Wall
|
|
188
|
-
* time is deliberately not the axis
|
|
307
|
+
* time is deliberately not the axis - only how many times we have measured it.
|
|
189
308
|
*/
|
|
190
309
|
declare function grouped(records?: RunRecord[]): Group[];
|
|
191
310
|
/** count / mean / sample-std / min / max of a numeric series. */
|
|
192
311
|
declare function stats(values: Array<number | undefined>): Stats | null;
|
|
193
312
|
/**
|
|
194
|
-
* Per-config spread of overall and per-task scores across repeated runs
|
|
313
|
+
* Per-config spread of overall and per-task scores across repeated runs - the
|
|
195
314
|
* measure of whether the benchmark method is itself consistent.
|
|
196
315
|
*/
|
|
197
316
|
declare function variance(records?: RunRecord[]): VarianceRow[];
|
package/dist/ops/results.js
CHANGED
|
@@ -8,6 +8,16 @@ import { fileURLToPath } from "node:url";
|
|
|
8
8
|
* compared and charted later. Runs record the configuration they were measured
|
|
9
9
|
* under, because a score is meaningless without the quant, context size and
|
|
10
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.
|
|
11
21
|
*/
|
|
12
22
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
13
23
|
const ROOT = path.resolve(HERE, "..", "..");
|
|
@@ -19,7 +29,15 @@ function slugify(text) {
|
|
|
19
29
|
.replace(/^-|-$/g, "")
|
|
20
30
|
.slice(0, 80);
|
|
21
31
|
}
|
|
22
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Stable identity for "same model, same settings", so reruns can be grouped.
|
|
34
|
+
*
|
|
35
|
+
* Deliberately unchanged when the record grew: this string is the grouping key
|
|
36
|
+
* every stored run was written with, and widening it would not re-key history -
|
|
37
|
+
* it would split each model's past runs from its future ones and quietly reset
|
|
38
|
+
* every variance figure on the page. New settings are recorded as data, not
|
|
39
|
+
* folded in here.
|
|
40
|
+
*/
|
|
23
41
|
function configKey(profile) {
|
|
24
42
|
if (!profile)
|
|
25
43
|
return "unknown";
|
|
@@ -30,10 +48,42 @@ function configKey(profile) {
|
|
|
30
48
|
profile.vision ? "vision" : "novision",
|
|
31
49
|
].join("_");
|
|
32
50
|
}
|
|
33
|
-
|
|
51
|
+
/** Read a numeric GGUF metadata field, which is `null` when the header lacked it. */
|
|
52
|
+
function metadataNumber(model, key) {
|
|
53
|
+
const value = model?.metadata?.[key];
|
|
54
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Fold the fit and the calibration into the record's setup block.
|
|
58
|
+
*
|
|
59
|
+
* `fit.profile` is the profile that actually ran, so the requested context has
|
|
60
|
+
* to be read from what the fit was HANDED, not from what it returned - by the
|
|
61
|
+
* time `save` sees a profile, the adjustment has already been applied and is
|
|
62
|
+
* invisible.
|
|
63
|
+
*/
|
|
64
|
+
function buildSetup({ args, fit, calibration, }) {
|
|
65
|
+
if (!args && !fit && !calibration) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const budget = fit?.budget ?? null;
|
|
69
|
+
return {
|
|
70
|
+
args: args ?? null,
|
|
71
|
+
adjusted: fit?.adjusted ?? false,
|
|
72
|
+
requestedContextSize: fit?.requestedContextSize ?? null,
|
|
73
|
+
adjustReason: fit?.reason ?? null,
|
|
74
|
+
kvSource: budget?.source ?? null,
|
|
75
|
+
kvInherited: calibration?.inherited === true,
|
|
76
|
+
kvBytesPerToken: budget?.kvBytesPerToken ?? null,
|
|
77
|
+
theoreticalKvBytesPerToken: budget?.theoreticalKvBytesPerToken ?? null,
|
|
78
|
+
predictedVramBytes: budget?.totalBytes ?? null,
|
|
79
|
+
reserveBytes: budget?.reserveBytes ?? null,
|
|
80
|
+
headroomBytes: budget?.headroomBytes ?? null,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function save({ model, profile, report, gpu = null, runtime = null, archiveId = null, system = null, args = null, fit = null, calibration = null, suite = null, timestamp = new Date(), }) {
|
|
34
84
|
fs.mkdirSync(RESULTS_DIR, { recursive: true });
|
|
35
85
|
const record = {
|
|
36
|
-
schema:
|
|
86
|
+
schema: 2,
|
|
37
87
|
ranAt: timestamp.toISOString(),
|
|
38
88
|
model: {
|
|
39
89
|
id: model?.id ?? null,
|
|
@@ -43,15 +93,37 @@ function save({ model, profile, report, gpu = null, runtime = null, archiveId =
|
|
|
43
93
|
sizeBytes: model?.sizeBytes ?? null,
|
|
44
94
|
publisher: model?.publisher ?? null,
|
|
45
95
|
features: model?.features ?? null,
|
|
96
|
+
contextLength: metadataNumber(model, "contextLength"),
|
|
97
|
+
blockCount: metadataNumber(model, "blockCount"),
|
|
98
|
+
headCountKv: metadataNumber(model, "headCountKv"),
|
|
46
99
|
},
|
|
100
|
+
// The whole profile, not a summary of it. `ProfileSchema` is `.passthrough()`
|
|
101
|
+
// and its numeric fields carry defaults, so read them defensively: a profile
|
|
102
|
+
// written by an older brain can be missing anything below `vision`.
|
|
47
103
|
profile: profile
|
|
48
104
|
? {
|
|
49
105
|
contextSize: profile.contextSize,
|
|
50
106
|
cacheTypeK: profile.cacheTypeK,
|
|
51
107
|
cacheTypeV: profile.cacheTypeV,
|
|
52
108
|
reasoningBudget: profile.reasoningBudget,
|
|
109
|
+
reasoningBudgetMessage: profile.reasoningBudgetMessage ?? null,
|
|
53
110
|
vision: profile.vision,
|
|
54
111
|
flashAttention: profile.flashAttention,
|
|
112
|
+
gpuLayers: profile.gpuLayers ?? null,
|
|
113
|
+
parallelSlots: profile.parallelSlots ?? null,
|
|
114
|
+
batchSize: profile.batchSize ?? null,
|
|
115
|
+
ubatchSize: profile.ubatchSize ?? null,
|
|
116
|
+
extraArgs: profile.extraArgs ?? [],
|
|
117
|
+
}
|
|
118
|
+
: null,
|
|
119
|
+
setup: buildSetup({ args, fit, calibration }),
|
|
120
|
+
suite: suite
|
|
121
|
+
? {
|
|
122
|
+
execute: suite.execute ?? null,
|
|
123
|
+
concurrency: suite.concurrency ?? null,
|
|
124
|
+
depths: suite.depths ?? null,
|
|
125
|
+
only: suite.only ?? null,
|
|
126
|
+
mined: suite.mined ?? false,
|
|
55
127
|
}
|
|
56
128
|
: null,
|
|
57
129
|
configKey: configKey(profile),
|
|
@@ -119,7 +191,7 @@ function latestPerConfig(records = loadAll()) {
|
|
|
119
191
|
/**
|
|
120
192
|
* Group every run by model+config (all runs kept, not just the latest), and
|
|
121
193
|
* number them oldest→newest so a run can be plotted against run number. Wall
|
|
122
|
-
* time is deliberately not the axis
|
|
194
|
+
* time is deliberately not the axis - only how many times we have measured it.
|
|
123
195
|
*/
|
|
124
196
|
function grouped(records = loadAll()) {
|
|
125
197
|
const groups = new Map();
|
|
@@ -157,7 +229,7 @@ function stats(values) {
|
|
|
157
229
|
};
|
|
158
230
|
}
|
|
159
231
|
/**
|
|
160
|
-
* Per-config spread of overall and per-task scores across repeated runs
|
|
232
|
+
* Per-config spread of overall and per-task scores across repeated runs - the
|
|
161
233
|
* measure of whether the benchmark method is itself consistent.
|
|
162
234
|
*/
|
|
163
235
|
function variance(records = loadAll()) {
|
package/dist/output/render.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Format renderers for the output layer. chalk lives here and nowhere else
|
|
2
|
+
* Format renderers for the output layer. chalk lives here and nowhere else -
|
|
3
3
|
* command handlers express color declaratively via ColumnDef.color, and only
|
|
4
4
|
* these renderers turn a result into text. Table output is ANSI-aware so colored
|
|
5
5
|
* cells still align.
|
package/dist/output/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The output contract, mirroring @otto-code/cli's output layer: command handlers
|
|
3
|
-
* do not print
|
|
3
|
+
* do not print - they return a typed result plus a schema, and a wrapper renders
|
|
4
4
|
* it in the user-selected format (table / json / yaml / quiet). This is what lets
|
|
5
5
|
* every `otto brain` command support `--format`, `--json`, and `--quiet` uniformly.
|
|
6
6
|
*/
|
package/dist/runtime/args.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export declare function buildEnv(runtime: Runtime, baseEnv?: NodeJS.ProcessEnv):
|
|
|
12
12
|
/**
|
|
13
13
|
* Translate a profile into llama-server arguments.
|
|
14
14
|
*
|
|
15
|
-
* Only settings that demonstrably matter for stable local inference are emitted
|
|
15
|
+
* Only settings that demonstrably matter for stable local inference are emitted -
|
|
16
16
|
* no experimental sampler knobs.
|
|
17
17
|
*/
|
|
18
18
|
export declare function buildArgs(profile: Profile, { port, host }: ServeTarget): string[];
|
package/dist/runtime/args.js
CHANGED
|
@@ -20,7 +20,7 @@ export function buildEnv(runtime, baseEnv = process.env) {
|
|
|
20
20
|
/**
|
|
21
21
|
* Translate a profile into llama-server arguments.
|
|
22
22
|
*
|
|
23
|
-
* Only settings that demonstrably matter for stable local inference are emitted
|
|
23
|
+
* Only settings that demonstrably matter for stable local inference are emitted -
|
|
24
24
|
* no experimental sampler knobs.
|
|
25
25
|
*/
|
|
26
26
|
export function buildArgs(profile, { port, host = "127.0.0.1" }) {
|
package/dist/runtime/managed.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* The self-contained runtime source: otto-brain downloads a pinned llama.cpp
|
|
3
3
|
* build into `$OTTO_HOME/otto-brain/runtimes/` and runs it directly, so the tool
|
|
4
4
|
* needs no other software installed. Downloaded runtimes keep their DLLs in the
|
|
5
|
-
* same directory as the exe (so `vendorDir` is null
|
|
5
|
+
* same directory as the exe (so `vendorDir` is null - buildEnv already puts the
|
|
6
6
|
* runtime dir on PATH, which is what the DLL-stub trap requires).
|
|
7
7
|
*
|
|
8
|
-
* Extraction uses only OS built-ins
|
|
9
|
-
* the bundled `tar` for tarballs
|
|
8
|
+
* Extraction uses only OS built-ins - PowerShell's Expand-Archive for .zip and
|
|
9
|
+
* the bundled `tar` for tarballs - to keep the "nothing else to install" promise.
|
|
10
10
|
*/
|
|
11
11
|
import { spawn } from "node:child_process";
|
|
12
12
|
import fs from "node:fs";
|
|
@@ -105,7 +105,7 @@ async function extractArchive(archivePath, destDir) {
|
|
|
105
105
|
if (/\.zip$/i.test(archivePath)) {
|
|
106
106
|
// PowerShell ships with Windows; -Force overwrites an interrupted extract.
|
|
107
107
|
// Paths are rooted at $OTTO_HOME (under the user profile), so a username with
|
|
108
|
-
// an apostrophe would break
|
|
108
|
+
// an apostrophe would break - or inject into - a raw single-quoted string.
|
|
109
109
|
// Escape single quotes for PowerShell (a literal ' is written as '').
|
|
110
110
|
const psQuote = (value) => `'${value.replace(/'/g, "''")}'`;
|
|
111
111
|
await run("powershell", [
|