@otto-code/brain 0.8.9 → 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/tui/app.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Screen, box, meter, style, pad, truncate, width, onKeys } from "./screen.js";
|
|
2
2
|
import { scanModels, managedModelsDir, diskUsage, totalModelBytes, planDelete, deleteModelFiles, listRepoQuants, searchModels, downloadRepoFiles, resolveHfToken, } from "../models/index.js";
|
|
3
3
|
import * as profiles from "../config/profiles.js";
|
|
4
|
-
import { formatReasoningBudget, loadBrainConfig, loadProfilesStore, saveProfilesStore, UNRESTRICTED_REASONING_BUDGET, } from "../config/index.js";
|
|
4
|
+
import { formatReasoningBudget, loadBrainConfig, loadProfilesStore, profileWarnings, saveProfilesStore, UNRESTRICTED_REASONING_BUDGET, } from "../config/index.js";
|
|
5
5
|
import * as vram from "../vram.js";
|
|
6
6
|
import * as gpu from "../gpu.js";
|
|
7
7
|
import { calibrate } from "../ops/calibrate.js";
|
|
@@ -61,6 +61,27 @@ export const FIELDS = [
|
|
|
61
61
|
values: CACHE_CYCLE,
|
|
62
62
|
format: (p) => p.cacheTypeV,
|
|
63
63
|
},
|
|
64
|
+
{
|
|
65
|
+
key: "parallelSlots",
|
|
66
|
+
label: "Parallel slots",
|
|
67
|
+
kind: "number",
|
|
68
|
+
step: 1,
|
|
69
|
+
min: 1,
|
|
70
|
+
max: () => 16,
|
|
71
|
+
format: (p) => String(p.parallelSlots),
|
|
72
|
+
note: (p) => p.parallelSlots > 1 ? `${p.parallelSlots} concurrent requests, sharing one KV pool` : null,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
key: "cachedChats",
|
|
76
|
+
label: "Cached KVs",
|
|
77
|
+
kind: "number",
|
|
78
|
+
step: 1,
|
|
79
|
+
min: 0,
|
|
80
|
+
max: () => 64,
|
|
81
|
+
format: (p) => (p.cachedChats ?? 0) > 0 ? String(p.cachedChats) : `default ${style.grey}(0)${style.reset}`,
|
|
82
|
+
// No inline note: the RAM estimate is the warning's job, and the warning
|
|
83
|
+
// lives in one place (profile-edit.ts). The panel reads it from there.
|
|
84
|
+
},
|
|
64
85
|
{
|
|
65
86
|
key: "flashAttention",
|
|
66
87
|
label: "Flash attention",
|
|
@@ -97,6 +118,21 @@ export const FIELDS = [
|
|
|
97
118
|
},
|
|
98
119
|
note: (p) => p.reasoningBudget === -1 ? "unrestricted budget can consume every token on thinking" : null,
|
|
99
120
|
},
|
|
121
|
+
{
|
|
122
|
+
key: "preserveReasoning",
|
|
123
|
+
label: "Preserve reasoning",
|
|
124
|
+
kind: "cycle",
|
|
125
|
+
// The stored tri-state, in the order PRESERVE_REASONING_CYCLE names it.
|
|
126
|
+
values: [null, true, false],
|
|
127
|
+
format: (p) => p.preserveReasoning === true
|
|
128
|
+
? `${style.brightGreen}on${style.reset}`
|
|
129
|
+
: p.preserveReasoning === false
|
|
130
|
+
? `${style.yellow}off${style.reset}`
|
|
131
|
+
: `${style.grey}template default${style.reset}`,
|
|
132
|
+
// Offered wherever the template has thinking to preserve, not only where it
|
|
133
|
+
// spells the setting as a kwarg. See profileFieldDescriptors for why.
|
|
134
|
+
enabled: (ctx) => Boolean(ctx.model?.metadata?.reasoning || ctx.model?.reasoningPreservation),
|
|
135
|
+
},
|
|
100
136
|
{
|
|
101
137
|
key: "gpuLayers",
|
|
102
138
|
label: "GPU layers",
|
|
@@ -106,16 +142,6 @@ export const FIELDS = [
|
|
|
106
142
|
max: () => 999,
|
|
107
143
|
format: (p) => p.gpuLayers >= 999 ? `all ${style.grey}(999)${style.reset}` : String(p.gpuLayers),
|
|
108
144
|
},
|
|
109
|
-
{
|
|
110
|
-
key: "parallelSlots",
|
|
111
|
-
label: "Parallel slots",
|
|
112
|
-
kind: "number",
|
|
113
|
-
step: 1,
|
|
114
|
-
min: 1,
|
|
115
|
-
max: () => 16,
|
|
116
|
-
format: (p) => String(p.parallelSlots),
|
|
117
|
-
note: (p) => p.parallelSlots > 1 ? `${p.parallelSlots} concurrent requests, sharing one KV pool` : null,
|
|
118
|
-
},
|
|
119
145
|
];
|
|
120
146
|
export class App {
|
|
121
147
|
constructor({ runtime, listenPort, listenHost }) {
|
|
@@ -1179,11 +1205,14 @@ export class App {
|
|
|
1179
1205
|
return;
|
|
1180
1206
|
return this.guard("calibration", async () => {
|
|
1181
1207
|
await this.supervisor.stop();
|
|
1208
|
+
this.supervisor.recordLog(`operation calibrate: ${model.displayName}`);
|
|
1182
1209
|
const measurement = await calibrate({
|
|
1183
1210
|
runtime: this.runtime,
|
|
1184
1211
|
model,
|
|
1185
1212
|
profile,
|
|
1213
|
+
supervisor: this.supervisor,
|
|
1186
1214
|
onProgress: (p) => {
|
|
1215
|
+
this.supervisor.recordLog(`operation calibrate: ${p.phase} ${p.contextSize.toLocaleString()}${p.reason ? `: ${p.reason}` : ""}${p.error ? `: ${p.error}` : ""}`);
|
|
1187
1216
|
if (p.phase === "loading")
|
|
1188
1217
|
this.setStatus(`calibrating: loading at ${p.contextSize.toLocaleString()} ctx…`, "info");
|
|
1189
1218
|
if (p.phase === "measured")
|
|
@@ -1217,6 +1246,7 @@ export class App {
|
|
|
1217
1246
|
this.setStatus(appliedTo !== null
|
|
1218
1247
|
? `${measuredMsg} - context ${before.toLocaleString()} -> ${appliedTo.toLocaleString()}`
|
|
1219
1248
|
: measuredMsg, "good");
|
|
1249
|
+
this.supervisor.recordLog(`operation calibrate: saved measurement for ${model.displayName}`);
|
|
1220
1250
|
});
|
|
1221
1251
|
}
|
|
1222
1252
|
runSweep() {
|
|
@@ -1226,11 +1256,14 @@ export class App {
|
|
|
1226
1256
|
return;
|
|
1227
1257
|
return this.guard("sweep", async () => {
|
|
1228
1258
|
await this.supervisor.stop();
|
|
1259
|
+
this.supervisor.recordLog(`operation sweep: ${model.displayName}`);
|
|
1229
1260
|
const result = await sweep({
|
|
1230
1261
|
runtime: this.runtime,
|
|
1231
1262
|
model,
|
|
1232
1263
|
profile,
|
|
1264
|
+
supervisor: this.supervisor,
|
|
1233
1265
|
onProgress: (p) => {
|
|
1266
|
+
this.supervisor.recordLog(`operation sweep: budget ${p.budget} ${p.phase}${p.error ? `: ${p.error}` : ""}`);
|
|
1234
1267
|
if (p.phase === "loading")
|
|
1235
1268
|
this.setStatus(`sweep: loading with budget ${p.budget}…`, "info");
|
|
1236
1269
|
if (p.phase === "generating")
|
|
@@ -1245,6 +1278,7 @@ export class App {
|
|
|
1245
1278
|
profile.reasoningBudget = result.recommended;
|
|
1246
1279
|
this.persist();
|
|
1247
1280
|
this.setStatus(`sweep complete - reasoning budget set to ${result.recommended}`, "good");
|
|
1281
|
+
this.supervisor.recordLog(`operation sweep: saved budget ${result.recommended}`);
|
|
1248
1282
|
}
|
|
1249
1283
|
else {
|
|
1250
1284
|
this.setStatus("sweep produced no usable result", "warn");
|
|
@@ -1626,14 +1660,14 @@ export class App {
|
|
|
1626
1660
|
const header = this.header(cols);
|
|
1627
1661
|
const status = this.statusPanel(cols - 2);
|
|
1628
1662
|
const footer = this.keybindings(cols - 1);
|
|
1629
|
-
const title = truncate(`${style.brightCyan}
|
|
1663
|
+
const title = truncate(`${style.brightCyan}Brain log${style.reset} ` +
|
|
1630
1664
|
`${style.grey}${s.state}${logs.length ? ` · ${logs.length} lines` : ""} · esc or l to go back${style.reset}`, cols - 1);
|
|
1631
1665
|
// header + blank + title + blank + [body] + blank + status + blank + footer.
|
|
1632
1666
|
const overhead = 1 + 1 + 1 + 1 + 1 + status.length + 1 + footer.length;
|
|
1633
1667
|
const bodyRows = Math.max(1, this.screen.rows - 1 - overhead);
|
|
1634
1668
|
const body = [];
|
|
1635
1669
|
if (logs.length === 0) {
|
|
1636
|
-
body.push(`${style.grey}no logs yet - start a model
|
|
1670
|
+
body.push(`${style.grey}no logs yet - start a model or operation to see Brain output${style.reset}`);
|
|
1637
1671
|
}
|
|
1638
1672
|
else {
|
|
1639
1673
|
for (const line of logs.slice(Math.max(0, logs.length - bodyRows)))
|
|
@@ -1739,9 +1773,23 @@ export class App {
|
|
|
1739
1773
|
if (focused)
|
|
1740
1774
|
focusLine = content.length;
|
|
1741
1775
|
content.push(`${arrow} ${pad(field.label, 17)} ${value}`);
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1776
|
+
// The cachedChats estimate is a ProfileWarning, not a field note: it is
|
|
1777
|
+
// computed once in profile-edit.ts, colored by how much of the machine's
|
|
1778
|
+
// RAM it would take, and the app editor renders the very same object.
|
|
1779
|
+
const cachedWarning = field.key === "cachedChats"
|
|
1780
|
+
? profileWarnings(profile, model, this.store).find((w) => w.field === "cachedChats")
|
|
1781
|
+
: undefined;
|
|
1782
|
+
const note = cachedWarning?.message ?? field.note?.(profile);
|
|
1783
|
+
if (note) {
|
|
1784
|
+
const tone = cachedWarning?.severity === "error"
|
|
1785
|
+
? style.red
|
|
1786
|
+
: cachedWarning?.severity === "warn"
|
|
1787
|
+
? style.yellow
|
|
1788
|
+
: field.key === "cachedChats"
|
|
1789
|
+
? style.grey
|
|
1790
|
+
: style.yellow;
|
|
1791
|
+
content.push(` ${tone} ${note}${style.reset}`);
|
|
1792
|
+
}
|
|
1745
1793
|
}
|
|
1746
1794
|
const sweepResult = this.sweepResult;
|
|
1747
1795
|
if (sweepResult) {
|
|
@@ -1923,7 +1971,7 @@ export class App {
|
|
|
1923
1971
|
item("u", "reset the selected model's name to its default (asks to confirm)");
|
|
1924
1972
|
section("Views");
|
|
1925
1973
|
item("b", "benchmark mode - rank models, run the coding suite");
|
|
1926
|
-
item("l", "view the live
|
|
1974
|
+
item("l", "view the live Brain log");
|
|
1927
1975
|
item("/", "filter the model list (Enter apply, Esc clear)");
|
|
1928
1976
|
item("r", "rescan the models folder");
|
|
1929
1977
|
item("?", "this help");
|
package/dist/types.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface ModelMetadata {
|
|
|
21
21
|
* flags a reasoning model - works for any local model, catalog or not.
|
|
22
22
|
*/
|
|
23
23
|
reasoning?: boolean;
|
|
24
|
+
/** Native template key detected from `preserve_thinking` or `preserve_reasoning`. */
|
|
25
|
+
reasoningPreservationArgument?: string;
|
|
24
26
|
[key: string]: unknown;
|
|
25
27
|
}
|
|
26
28
|
export interface ModelFeatures {
|
|
@@ -28,6 +30,16 @@ export interface ModelFeatures {
|
|
|
28
30
|
imatrix: boolean;
|
|
29
31
|
distilled: boolean;
|
|
30
32
|
}
|
|
33
|
+
/** Catalog-declared native chat-template argument names for reasoning controls. */
|
|
34
|
+
export interface ReasoningTemplate {
|
|
35
|
+
enableThinkingArgument: string;
|
|
36
|
+
effortArgument: string;
|
|
37
|
+
}
|
|
38
|
+
/** The native argument behind Brain's provider-neutral Preserve reasoning setting. */
|
|
39
|
+
export interface ReasoningPreservation {
|
|
40
|
+
templateArgument: string;
|
|
41
|
+
default?: boolean;
|
|
42
|
+
}
|
|
31
43
|
export type ModelComponentRole = "vision_projector" | "speculative_drafter";
|
|
32
44
|
/** A catalog-declared companion artifact resolved against local disk. */
|
|
33
45
|
export interface ModelComponent {
|
|
@@ -69,6 +81,12 @@ export interface Model {
|
|
|
69
81
|
thinking?: boolean;
|
|
70
82
|
/** Per-request reasoning levels accepted by the model's OpenAI-compatible API. */
|
|
71
83
|
reasoningEfforts?: string[];
|
|
84
|
+
/** The model-native default among `reasoningEfforts`, when one is declared. */
|
|
85
|
+
reasoningEffortDefault?: string;
|
|
86
|
+
/** Template argument names needed to apply this model's reasoning controls. */
|
|
87
|
+
reasoningTemplate?: ReasoningTemplate;
|
|
88
|
+
/** Native template argument used by the model-profile Preserve reasoning setting. */
|
|
89
|
+
reasoningPreservation?: ReasoningPreservation;
|
|
72
90
|
/** The catalog's advertised max context, if known. */
|
|
73
91
|
contextMax?: number;
|
|
74
92
|
/** Back-reference: the id of the reconciled catalog entry, if matched. */
|
package/dist/vram.d.ts
CHANGED
|
@@ -18,6 +18,43 @@ export declare function listCacheTypes(): string[];
|
|
|
18
18
|
/** Worst-case KV bytes per token, assuming every layer holds a full cache. */
|
|
19
19
|
export declare function theoreticalKvBytesPerToken(metadata: ModelMetadata | null, cacheTypeK: string, cacheTypeV: string): number | null;
|
|
20
20
|
export type BudgetSource = "measured" | "theoretical" | "unknown";
|
|
21
|
+
/**
|
|
22
|
+
* KV bytes per token for a profile: the measurement when one exists, otherwise
|
|
23
|
+
* the theoretical bound.
|
|
24
|
+
*
|
|
25
|
+
* The two are not interchangeable. The theoretical formula overestimates badly
|
|
26
|
+
* on architectures that only keep a full cache on a subset of layers - measured
|
|
27
|
+
* at 3.5x for Qwen3.8-27B and 20x for Gemma-4-31B on this repo's own stored
|
|
28
|
+
* calibrations - which is the whole reason `calibrate` exists. Callers that
|
|
29
|
+
* spend real memory on this number must therefore branch on `source` rather
|
|
30
|
+
* than treating the theoretical figure as an answer.
|
|
31
|
+
*/
|
|
32
|
+
export declare function kvBytesPerToken(model: Model, profile: Profile, calibration?: Calibration | null): {
|
|
33
|
+
value: number;
|
|
34
|
+
source: BudgetSource;
|
|
35
|
+
theoretical: number | null;
|
|
36
|
+
};
|
|
37
|
+
export interface PromptCacheSize {
|
|
38
|
+
/** One chat at its full per-slot window - the worst case for a single entry. */
|
|
39
|
+
perChatBytes: number;
|
|
40
|
+
/** What `--cache-ram` is set to, in bytes. */
|
|
41
|
+
totalBytes: number;
|
|
42
|
+
source: BudgetSource;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Turn `cachedChats` into a `--cache-ram` byte budget.
|
|
46
|
+
*
|
|
47
|
+
* llama-server parks the KV state of a chat that loses its slot in host RAM and
|
|
48
|
+
* copies it back when that chat returns, instead of re-prefilling its whole
|
|
49
|
+
* conversation. An entry is at most one slot's worth of KV, and a slot is
|
|
50
|
+
* `contextSize / parallelSlots` tokens, so the count the user picks is the
|
|
51
|
+
* count of chats guaranteed to survive. Real conversations rarely fill their
|
|
52
|
+
* window, so in practice more of them fit: the number is a floor, not a cap.
|
|
53
|
+
*
|
|
54
|
+
* Returns null when the size cannot be computed - `cachedChats` is 0 (leave
|
|
55
|
+
* llama.cpp's default alone) or nothing is known about this model's KV cost.
|
|
56
|
+
*/
|
|
57
|
+
export declare function promptCacheSize(model: Model, profile: Profile, calibration?: Calibration | null): PromptCacheSize | null;
|
|
21
58
|
export interface Budget {
|
|
22
59
|
weightsBytes: number;
|
|
23
60
|
mmprojBytes: number;
|
package/dist/vram.js
CHANGED
|
@@ -32,6 +32,52 @@ export function theoreticalKvBytesPerToken(metadata, cacheTypeK, cacheTypeV) {
|
|
|
32
32
|
(keyLength * cacheTypeBytes(cacheTypeK) +
|
|
33
33
|
valueLength * cacheTypeBytes(cacheTypeV)));
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* KV bytes per token for a profile: the measurement when one exists, otherwise
|
|
37
|
+
* the theoretical bound.
|
|
38
|
+
*
|
|
39
|
+
* The two are not interchangeable. The theoretical formula overestimates badly
|
|
40
|
+
* on architectures that only keep a full cache on a subset of layers - measured
|
|
41
|
+
* at 3.5x for Qwen3.8-27B and 20x for Gemma-4-31B on this repo's own stored
|
|
42
|
+
* calibrations - which is the whole reason `calibrate` exists. Callers that
|
|
43
|
+
* spend real memory on this number must therefore branch on `source` rather
|
|
44
|
+
* than treating the theoretical figure as an answer.
|
|
45
|
+
*/
|
|
46
|
+
export function kvBytesPerToken(model, profile, calibration) {
|
|
47
|
+
const theoretical = theoreticalKvBytesPerToken(model.metadata, profile.cacheTypeK, profile.cacheTypeV);
|
|
48
|
+
if (calibration && calibration.kvBytesPerToken > 0) {
|
|
49
|
+
return { value: calibration.kvBytesPerToken, source: "measured", theoretical };
|
|
50
|
+
}
|
|
51
|
+
if (theoretical !== null)
|
|
52
|
+
return { value: theoretical, source: "theoretical", theoretical };
|
|
53
|
+
return { value: 0, source: "unknown", theoretical };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Turn `cachedChats` into a `--cache-ram` byte budget.
|
|
57
|
+
*
|
|
58
|
+
* llama-server parks the KV state of a chat that loses its slot in host RAM and
|
|
59
|
+
* copies it back when that chat returns, instead of re-prefilling its whole
|
|
60
|
+
* conversation. An entry is at most one slot's worth of KV, and a slot is
|
|
61
|
+
* `contextSize / parallelSlots` tokens, so the count the user picks is the
|
|
62
|
+
* count of chats guaranteed to survive. Real conversations rarely fill their
|
|
63
|
+
* window, so in practice more of them fit: the number is a floor, not a cap.
|
|
64
|
+
*
|
|
65
|
+
* Returns null when the size cannot be computed - `cachedChats` is 0 (leave
|
|
66
|
+
* llama.cpp's default alone) or nothing is known about this model's KV cost.
|
|
67
|
+
*/
|
|
68
|
+
export function promptCacheSize(model, profile, calibration) {
|
|
69
|
+
const chats = Math.floor(profile.cachedChats ?? 0);
|
|
70
|
+
if (chats < 1)
|
|
71
|
+
return null;
|
|
72
|
+
const { value, source } = kvBytesPerToken(model, profile, calibration);
|
|
73
|
+
if (value <= 0 || profile.contextSize <= 0)
|
|
74
|
+
return null;
|
|
75
|
+
const perSlotTokens = Math.floor(profile.contextSize / Math.max(1, profile.parallelSlots || 1));
|
|
76
|
+
if (perSlotTokens <= 0)
|
|
77
|
+
return null;
|
|
78
|
+
const perChatBytes = value * perSlotTokens;
|
|
79
|
+
return { perChatBytes, totalBytes: perChatBytes * chats, source };
|
|
80
|
+
}
|
|
35
81
|
/** Compute a VRAM budget for a profile. */
|
|
36
82
|
export function budget({ model, profile, calibration = null, totalVramBytes, reserveBytes = 1.5 * GIB, }) {
|
|
37
83
|
const weights = model.sizeBytes;
|
|
@@ -46,22 +92,8 @@ export function budget({ model, profile, calibration = null, totalVramBytes, res
|
|
|
46
92
|
: 0;
|
|
47
93
|
const mmproj = projector.reduce((total, component) => total + component.bytes, 0) ||
|
|
48
94
|
(components.length ? 0 : componentBytes);
|
|
49
|
-
const theoretical =
|
|
50
|
-
|
|
51
|
-
let source;
|
|
52
|
-
if (calibration && calibration.kvBytesPerToken > 0) {
|
|
53
|
-
kvBytesPerToken = calibration.kvBytesPerToken;
|
|
54
|
-
source = "measured";
|
|
55
|
-
}
|
|
56
|
-
else if (theoretical !== null) {
|
|
57
|
-
kvBytesPerToken = theoretical;
|
|
58
|
-
source = "theoretical";
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
kvBytesPerToken = 0;
|
|
62
|
-
source = "unknown";
|
|
63
|
-
}
|
|
64
|
-
const kv = kvBytesPerToken * profile.contextSize;
|
|
95
|
+
const { value: perToken, source, theoretical } = kvBytesPerToken(model, profile, calibration);
|
|
96
|
+
const kv = perToken * profile.contextSize;
|
|
65
97
|
// The speculative decoder keeps its own KV cache. Until component-specific
|
|
66
98
|
// calibration exists, reserve the same conservative per-token cost.
|
|
67
99
|
const drafterKv = drafters.length * kv;
|
|
@@ -84,7 +116,7 @@ export function budget({ model, profile, calibration = null, totalVramBytes, res
|
|
|
84
116
|
usableBytes: usable,
|
|
85
117
|
totalVramBytes,
|
|
86
118
|
reserveBytes,
|
|
87
|
-
kvBytesPerToken,
|
|
119
|
+
kvBytesPerToken: perToken,
|
|
88
120
|
source,
|
|
89
121
|
theoreticalKvBytesPerToken: theoretical,
|
|
90
122
|
fits: total <= usable,
|
|
@@ -107,7 +139,14 @@ export function maxContextThatFits({ model, profile, calibration, totalVramBytes
|
|
|
107
139
|
const room = probe.usableBytes - fixed;
|
|
108
140
|
if (room <= 0)
|
|
109
141
|
return null;
|
|
110
|
-
|
|
142
|
+
// More than one KV pool can scale with the context - a speculative drafter
|
|
143
|
+
// keeps its own. Derive the per-token cost from the probe rather than from
|
|
144
|
+
// `kvBytesPerToken` alone, so this can never hand back a context that
|
|
145
|
+
// `budget()` then declares does not fit.
|
|
146
|
+
const perToken = (probe.kvBytes + probe.drafterKvBytes) / step;
|
|
147
|
+
if (perToken <= 0)
|
|
148
|
+
return null;
|
|
149
|
+
const tokens = Math.floor(room / perToken);
|
|
111
150
|
const native = model.metadata?.contextLength;
|
|
112
151
|
const contextLimit = typeof native === "number" && native > 0 ? native * profile.contextMultiplier : tokens;
|
|
113
152
|
const capped = Math.min(tokens, contextLimit);
|
package/package.json
CHANGED