jev-web 0.7.2 → 0.9.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/LICENSE +21 -21
- package/README.md +773 -502
- package/docs/demo-model-picker.png +0 -0
- package/package.json +64 -60
- package/src/answers.mjs +36 -36
- package/src/bekko.mjs +439 -439
- package/src/clef.mjs +498 -0
- package/src/config.mjs +65 -65
- package/src/decision2.mjs +382 -382
- package/src/decision3.mjs +571 -0
- package/src/encode.mjs +83 -83
- package/src/index.mjs +196 -112
- package/src/kev.mjs +296 -296
- package/src/laya.mjs +630 -630
- package/src/questions.mjs +46 -39
- package/src/registry.mjs +199 -155
- package/src/session.mjs +151 -151
- package/src/strands.mjs +718 -535
- package/src/vega.mjs +1125 -0
package/src/vega.mjs
ADDED
|
@@ -0,0 +1,1125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vega.mjs — Vega 0.8B family (frozen Qwen3.5 backbone + latent decision-physics engine).
|
|
3
|
+
*
|
|
4
|
+
* Vega is the "System One" decision model by the Laya author: the language model
|
|
5
|
+
* is frozen and never trained; everything learned lives in a small physics
|
|
6
|
+
* engine on top of it (github.com/nandakishorm/vega-08b-public-intents ships
|
|
7
|
+
* the reference `vega_common.py` / `vega_api.py`):
|
|
8
|
+
*
|
|
9
|
+
* prompt (template 0, built by buildVegaPrompt — mirrors build_prompt):
|
|
10
|
+
* [BOS] Observation:\n <state> \n\nMeasurement (choice|score|boolean): <instructions>
|
|
11
|
+
* \nPossible outcomes: \n* <opt> … \nOutcome:
|
|
12
|
+
* perception: one causal read up to the deepest captured layer (13 and 19 of
|
|
13
|
+
* 24), each span mean-pooled -> item feats [B, 2, 3, d] (state / question /
|
|
14
|
+
* last token) + option feats [B, K, 1, d] + state chunk means for the
|
|
15
|
+
* memory reader;
|
|
16
|
+
* engine: the state becomes a world latent, the question drops a particle
|
|
17
|
+
* into a 64-d space whose landscape holds one Gaussian well per option, a
|
|
18
|
+
* damped symplectic integrator with a selective-SSM thermostat runs up to
|
|
19
|
+
* n_steps steps (settled particles freeze), and the Boltzmann occupancy of
|
|
20
|
+
* the wells is the answer distribution;
|
|
21
|
+
* calibration: thermal — tau depends on the particle's physical state
|
|
22
|
+
* (residual kinetic energy, distance to the nearest well, steps used,
|
|
23
|
+
* number of options), fitted per question type and shipped in
|
|
24
|
+
* vega_config.json; split-conformal prediction sets and an abstain flag
|
|
25
|
+
* come from the same energies, plus an "unbound particle" OOD signal.
|
|
26
|
+
*
|
|
27
|
+
* Two ONNX graph layouts ship:
|
|
28
|
+
*
|
|
29
|
+
* fused (warsang/vega-08b-web, the default) — one fused torso that pools
|
|
30
|
+
* in-graph + the fp32 engine, 1.5 GB fp16 torso, base engine only;
|
|
31
|
+
* split (nvkudva/vega-onnx-q8) — an 8-bit embed graph, a cut decoder that
|
|
32
|
+
* emits the residual stream at layers 13/19 (the LM head and layers 20-23
|
|
33
|
+
* never run), the fp32 engine with both gated task adapters, and JS
|
|
34
|
+
* pooling; 840 MB, wasm-only (8-bit MatMulNBits has no WebGPU kernel).
|
|
35
|
+
*
|
|
36
|
+
* JS owns the protocol, the packing, the thermal calibration and the answer
|
|
37
|
+
* decoding in both.
|
|
38
|
+
*/
|
|
39
|
+
import { normalizeQuestions } from "./questions.mjs";
|
|
40
|
+
import { softmaxWithTemperature } from "./answers.mjs";
|
|
41
|
+
import { fetchCachedBytes, fetchOnnxExternalData } from "./laya.mjs";
|
|
42
|
+
|
|
43
|
+
export const VEGA_DEFAULT_MODEL = "warsang/vega-08b-web";
|
|
44
|
+
export const VEGA_DEFAULT_REVISION = "d2bd4604ed770ca614745f07f623b66e89f406e4";
|
|
45
|
+
// The exact fp16 torso ships by default: q8 on this graph drifts on the
|
|
46
|
+
// GatedDeltaNet delta rule (export/vega/04_repo.py measures ~0.46 in the
|
|
47
|
+
// pooled features), so torso_quantized.onnx stays an opt-in experiment.
|
|
48
|
+
export const VEGA_TORSO_FILE = "onnx/torso_fp16.onnx";
|
|
49
|
+
export const VEGA_ENGINE_FILE = "onnx/engine.onnx";
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Split export: nvkudva/vega-onnx-q8 (8-bit embed + cut decoder + fp32 engine)
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
export const VEGA_Q8_DEFAULT_MODEL = "nvkudva/vega-onnx-q8";
|
|
55
|
+
export const VEGA_Q8_DEFAULT_REVISION = "017c096d369ac698087f8c35a6ad8c867a7fe0f4";
|
|
56
|
+
export const VEGA_Q8_DEFAULT_SUBFOLDER = "v2";
|
|
57
|
+
export const VEGA_Q8_EMBED_FILE = "embed_q8.onnx";
|
|
58
|
+
export const VEGA_Q8_DECODER_FILE = "decoder_q8.onnx";
|
|
59
|
+
export const VEGA_Q8_ENGINE_FILE = "engine.onnx";
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Named exports. `fp16` is this repo's own fused export (default); `q8` is
|
|
63
|
+
* nvkudva's split 8-bit export of the same checkpoint, which adds the two
|
|
64
|
+
* gated task adapters (public_intents / typed_decisions) and is wasm-only.
|
|
65
|
+
*/
|
|
66
|
+
export const VEGA_VARIANTS = {
|
|
67
|
+
fp16: {
|
|
68
|
+
model: VEGA_DEFAULT_MODEL,
|
|
69
|
+
revision: VEGA_DEFAULT_REVISION,
|
|
70
|
+
format: "fused",
|
|
71
|
+
description: "fused fp16 torso + fp32 engine, 1.7 GB (the default)",
|
|
72
|
+
},
|
|
73
|
+
q8: {
|
|
74
|
+
model: VEGA_Q8_DEFAULT_MODEL,
|
|
75
|
+
revision: VEGA_Q8_DEFAULT_REVISION,
|
|
76
|
+
subfolder: VEGA_Q8_DEFAULT_SUBFOLDER,
|
|
77
|
+
format: "split",
|
|
78
|
+
description: "8-bit split embed + cut decoder + fp32 engine with adapters, 840 MB, wasm-only",
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** Reference prompt template 0 (the inference surface form). */
|
|
83
|
+
const VEGA_HEAD = "Observation:\n";
|
|
84
|
+
const VEGA_QUESTION = "\n\nMeasurement ({t}): {ins}\nPossible outcomes:";
|
|
85
|
+
const VEGA_OPTION = "\n* {o}";
|
|
86
|
+
const VEGA_END = "\nOutcome:";
|
|
87
|
+
/** Prefixed to short states so the frozen model reads knowing what is asked. */
|
|
88
|
+
const VEGA_QUERY_FIRST = "Question to keep in mind while reading ({t}): {ins}\n\n";
|
|
89
|
+
|
|
90
|
+
/** jev-web question types -> Vega qtypes (choice=0, score=1, boolean=2). */
|
|
91
|
+
export const VEGA_QTYPE = { choice: 0, score: 1, boolean: 2 };
|
|
92
|
+
export const VEGA_QTYPE_NAMES = ["choice", "score", "boolean"];
|
|
93
|
+
|
|
94
|
+
export const VEGA_DEFAULTS = {
|
|
95
|
+
optMaxTokens: 32,
|
|
96
|
+
insMaxTokens: 256,
|
|
97
|
+
memoryChunks: 16,
|
|
98
|
+
memoryMinTokens: 128,
|
|
99
|
+
memoryChunkTokens: 128,
|
|
100
|
+
prefixMinTokens: 4096,
|
|
101
|
+
queryFirstMaxState: 4096,
|
|
102
|
+
maxLen: 73728,
|
|
103
|
+
nSteps: 12,
|
|
104
|
+
risk: "0.1",
|
|
105
|
+
alpha: "0.1",
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Protocol: question defs, option texts, prompt building
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Map a normalized jev-web question to Vega's internal question def.
|
|
114
|
+
* Vega's `boolean` is the noul primitive; its slots are fixed [no, yes].
|
|
115
|
+
*/
|
|
116
|
+
export function vegaQuestionDef(question) {
|
|
117
|
+
// the reference json.dumps non-string instructions with ensure_ascii=True
|
|
118
|
+
const ins = typeof question.instructions === "string"
|
|
119
|
+
? question.instructions
|
|
120
|
+
: vegaJsonDumps(question.instructions, true);
|
|
121
|
+
if (question.type === "noul") {
|
|
122
|
+
return { t: "boolean", ins, crit: { ...(question.criteria ?? {}) } };
|
|
123
|
+
}
|
|
124
|
+
if (question.type === "score") {
|
|
125
|
+
return { t: "score", ins, crit: [...question.options] };
|
|
126
|
+
}
|
|
127
|
+
// choice: criteria {label: description} when provided, else bare options.
|
|
128
|
+
const crit = question.criteria && typeof question.criteria === "object"
|
|
129
|
+
&& !Array.isArray(question.criteria)
|
|
130
|
+
? question.criteria
|
|
131
|
+
: Object.fromEntries(question.options.map((o) => [String(o), null]));
|
|
132
|
+
return { t: "choice", ins, crit };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Python `json.dumps(x, ensure_ascii)` — ", " / ": " separators, control
|
|
137
|
+
* characters escaped, and optionally \uXXXX for everything above ASCII. The
|
|
138
|
+
* reference runtime serializes dict states, non-string instructions and
|
|
139
|
+
* non-string criteria values with it, so the prompt stays byte-identical to
|
|
140
|
+
* the Python original (JSON.stringify uses compact separators and never
|
|
141
|
+
* escapes non-ASCII).
|
|
142
|
+
*/
|
|
143
|
+
export function vegaJsonDumps(value, ensureAscii = false) {
|
|
144
|
+
const esc = (s) => {
|
|
145
|
+
let out = '"';
|
|
146
|
+
for (const ch of s) {
|
|
147
|
+
const c = ch.codePointAt(0);
|
|
148
|
+
if (ch === '"') out += '\\"';
|
|
149
|
+
else if (ch === "\\") out += "\\\\";
|
|
150
|
+
else if (ch === "\n") out += "\\n";
|
|
151
|
+
else if (ch === "\r") out += "\\r";
|
|
152
|
+
else if (ch === "\t") out += "\\t";
|
|
153
|
+
else if (c < 0x20) out += "\\u" + c.toString(16).padStart(4, "0");
|
|
154
|
+
else if (ensureAscii && c > 0x7f) {
|
|
155
|
+
if (c > 0xffff) {
|
|
156
|
+
const u = c - 0x10000;
|
|
157
|
+
out += "\\u" + (0xd800 + (u >> 10)).toString(16).padStart(4, "0");
|
|
158
|
+
out += "\\u" + (0xdc00 + (u & 0x3ff)).toString(16).padStart(4, "0");
|
|
159
|
+
} else out += "\\u" + c.toString(16).padStart(4, "0");
|
|
160
|
+
} else out += ch;
|
|
161
|
+
}
|
|
162
|
+
return out + '"';
|
|
163
|
+
};
|
|
164
|
+
const go = (x) => {
|
|
165
|
+
if (x === null || x === undefined) return "null";
|
|
166
|
+
if (typeof x === "string") return esc(x);
|
|
167
|
+
if (typeof x === "boolean") return x ? "true" : "false";
|
|
168
|
+
if (typeof x === "number") return String(x);
|
|
169
|
+
if (Array.isArray(x)) return "[" + x.map(go).join(", ") + "]";
|
|
170
|
+
return "{" + Object.entries(x).map(([k, v]) => esc(k) + ": " + go(v)).join(", ") + "}";
|
|
171
|
+
};
|
|
172
|
+
return go(value);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** json.dumps of a non-string criterion value (strings pass through). */
|
|
176
|
+
function vegaCritText(v) {
|
|
177
|
+
return typeof v === "string" ? v : vegaJsonDumps(v, false);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The reference render_text: strings verbatim, anything else json.dumps. */
|
|
181
|
+
export function renderVegaText(content) {
|
|
182
|
+
return typeof content === "string" ? content : vegaJsonDumps(content, false);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Option texts in label-index order, exactly as option_texts() renders them. */
|
|
186
|
+
export function vegaOptionTexts(qdef) {
|
|
187
|
+
if (qdef.t === "choice") {
|
|
188
|
+
return Object.entries(qdef.crit).map(([k, v]) => {
|
|
189
|
+
const d = v === null || v === undefined ? "" : vegaCritText(v);
|
|
190
|
+
return d ? `${k} - ${d}` : String(k);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
if (qdef.t === "score") {
|
|
194
|
+
return qdef.crit.map((c, i) => `level ${i} - ${vegaCritText(c)}`);
|
|
195
|
+
}
|
|
196
|
+
const crit = qdef.crit ?? {};
|
|
197
|
+
const no = crit.false === null || crit.false === undefined ? "" : vegaCritText(crit.false);
|
|
198
|
+
const yes = crit.true === null || crit.true === undefined ? "" : vegaCritText(crit.true);
|
|
199
|
+
return [
|
|
200
|
+
`no - ${no || "the statement does not hold"}`,
|
|
201
|
+
`yes - ${yes || "the statement holds"}`,
|
|
202
|
+
];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* One prompt: BOS + [query-first prefix for short states] + head + state +
|
|
207
|
+
* question + options + end, with the token span of every segment.
|
|
208
|
+
*
|
|
209
|
+
* The state comes first so its span never sees the question (causal
|
|
210
|
+
* attention); the question, options and final token have read the whole
|
|
211
|
+
* state. Options are capped per option, and an over-long option list is
|
|
212
|
+
* shrunk evenly before the instructions are (mirrors build_prompt).
|
|
213
|
+
*
|
|
214
|
+
* `truncateState` defaults to the reference's silent truncation; the split
|
|
215
|
+
* export refuses the decision instead (a cut-off record is worse than none),
|
|
216
|
+
* so its runtime passes false and gets a RangeError.
|
|
217
|
+
*
|
|
218
|
+
* @returns {{ids:number[], spans:{state:[number,number], question:[number,number],
|
|
219
|
+
* options:[number,number][], last:number}}}
|
|
220
|
+
*/
|
|
221
|
+
export function buildVegaPrompt({
|
|
222
|
+
encode,
|
|
223
|
+
stateText,
|
|
224
|
+
qdef,
|
|
225
|
+
maxLen,
|
|
226
|
+
bosId = null,
|
|
227
|
+
queryFirstMaxState = VEGA_DEFAULTS.queryFirstMaxState,
|
|
228
|
+
optMaxTokens = VEGA_DEFAULTS.optMaxTokens,
|
|
229
|
+
insMaxTokens = VEGA_DEFAULTS.insMaxTokens,
|
|
230
|
+
truncateState = true,
|
|
231
|
+
}) {
|
|
232
|
+
const options = vegaOptionTexts(qdef);
|
|
233
|
+
const enc = (s) => encode(s, false);
|
|
234
|
+
const bos = bosId === null ? [] : [bosId];
|
|
235
|
+
let head = enc(VEGA_HEAD);
|
|
236
|
+
let qids = enc(VEGA_QUESTION.replace("{t}", qdef.t).replace("{ins}", qdef.ins))
|
|
237
|
+
.slice(0, insMaxTokens);
|
|
238
|
+
let oids = options.map((o) => enc(VEGA_OPTION.replace("{o}", o)).slice(0, optMaxTokens));
|
|
239
|
+
const end = enc(VEGA_END);
|
|
240
|
+
let fixed = bos.length + head.length + qids.length
|
|
241
|
+
+ oids.reduce((n, o) => n + o.length, 0) + end.length;
|
|
242
|
+
if (fixed > maxLen - 16) {
|
|
243
|
+
const per = Math.max(3, Math.floor(
|
|
244
|
+
(maxLen - 16 - bos.length - head.length - end.length - Math.min(qids.length, 64))
|
|
245
|
+
/ Math.max(1, oids.length)));
|
|
246
|
+
oids = oids.map((o) => o.slice(0, per));
|
|
247
|
+
qids = qids.slice(0, Math.max(
|
|
248
|
+
8, maxLen - 16 - bos.length - head.length - end.length
|
|
249
|
+
- oids.reduce((n, o) => n + o.length, 0)));
|
|
250
|
+
fixed = bos.length + head.length + qids.length
|
|
251
|
+
+ oids.reduce((n, o) => n + o.length, 0) + end.length;
|
|
252
|
+
}
|
|
253
|
+
let st = enc(stateText);
|
|
254
|
+
let pre = [];
|
|
255
|
+
if (queryFirstMaxState && st.length < queryFirstMaxState) {
|
|
256
|
+
pre = enc(VEGA_QUERY_FIRST.replace("{t}", qdef.t).replace("{ins}", qdef.ins))
|
|
257
|
+
.slice(0, Math.min(128, insMaxTokens));
|
|
258
|
+
fixed += pre.length;
|
|
259
|
+
}
|
|
260
|
+
const room = Math.max(0, maxLen - fixed);
|
|
261
|
+
if (!truncateState && st.length > room) {
|
|
262
|
+
throw new RangeError(
|
|
263
|
+
`vega: state is ${st.length} tokens, only ${room} fit in maxLen=${maxLen}`
|
|
264
|
+
+ " (this export refuses a truncated state; raise maxLen)");
|
|
265
|
+
}
|
|
266
|
+
st = st.slice(0, room); // truncate_left = false: the head of the state is kept
|
|
267
|
+
const ids = [...bos, ...pre, ...head];
|
|
268
|
+
const s0 = ids.length;
|
|
269
|
+
ids.push(...st);
|
|
270
|
+
const spans = { state: st.length ? [s0, ids.length] : [bos.length, s0] };
|
|
271
|
+
const q0 = ids.length;
|
|
272
|
+
ids.push(...qids);
|
|
273
|
+
spans.question = [q0, ids.length];
|
|
274
|
+
spans.options = [];
|
|
275
|
+
for (const o of oids) {
|
|
276
|
+
const a = ids.length;
|
|
277
|
+
ids.push(...o);
|
|
278
|
+
spans.options.push([a, ids.length]);
|
|
279
|
+
}
|
|
280
|
+
ids.push(...end);
|
|
281
|
+
spans.last = ids.length - 1;
|
|
282
|
+
return { ids, spans };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
// Batch packing: padding, segment means (one-hot pooling), state chunks
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Pack rendered prompts into tensors + the pooling matrix the torso graph
|
|
291
|
+
* consumes: one row per segment (per item: state, question, options, the
|
|
292
|
+
* final token, then C state-chunk rows), each segment's row is 1/len over its
|
|
293
|
+
* token span, zeros elsewhere. The graph computes pool @ hidden, i.e. exact
|
|
294
|
+
* span means, so no hidden states ever cross into JS.
|
|
295
|
+
*
|
|
296
|
+
* @returns {{inputIds:BigInt64Array, attention:BigInt64Array,
|
|
297
|
+
* pool:Float32Array, poolDims:[number,number,number], n:number, L:number,
|
|
298
|
+
* K:number, qtypes:number[], optMask:Uint8Array, nseg:number}}
|
|
299
|
+
*/
|
|
300
|
+
export function packVegaBatch(items, { padId = 0, chunks: C = VEGA_DEFAULTS.memoryChunks } = {}) {
|
|
301
|
+
const n = items.length;
|
|
302
|
+
const L = Math.max(...items.map((it) => it.ids.length));
|
|
303
|
+
const K = Math.max(...items.map((it) => it.options.length));
|
|
304
|
+
const nseg = 3 + K;
|
|
305
|
+
const rows = nseg + C;
|
|
306
|
+
const inputIds = new BigInt64Array(n * L);
|
|
307
|
+
const attention = new BigInt64Array(n * L);
|
|
308
|
+
const pool = new Float32Array(n * rows * L); // [B, nseg + C, L]
|
|
309
|
+
const optMask = new Uint8Array(n * K);
|
|
310
|
+
const qtypes = [];
|
|
311
|
+
items.forEach((it, b) => {
|
|
312
|
+
it.ids.forEach((v, j) => {
|
|
313
|
+
inputIds[b * L + j] = BigInt(v);
|
|
314
|
+
attention[b * L + j] = 1n;
|
|
315
|
+
});
|
|
316
|
+
const setSpan = (row, [a, e]) => {
|
|
317
|
+
const len = Math.max(1, e - a);
|
|
318
|
+
for (let t = a; t < e; t++) pool[(b * rows + row) * L + t] = 1 / len;
|
|
319
|
+
};
|
|
320
|
+
setSpan(0, it.spans.state);
|
|
321
|
+
setSpan(1, it.spans.question);
|
|
322
|
+
it.spans.options.forEach((span, k) => {
|
|
323
|
+
if (k >= K) return;
|
|
324
|
+
setSpan(3 + k, span);
|
|
325
|
+
optMask[b * K + k] = 1;
|
|
326
|
+
});
|
|
327
|
+
pool[(b * rows + 2) * L + it.spans.last] = 1;
|
|
328
|
+
qtypes.push(it.qtype);
|
|
329
|
+
});
|
|
330
|
+
return {
|
|
331
|
+
inputIds, attention, pool, poolDims: [n, rows, L],
|
|
332
|
+
n, L, K, qtypes, optMask, nseg,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* State chunk means for the memory reader: the state span cut into k equal
|
|
338
|
+
* pieces (about one per memoryChunkTokens tokens, at least 2, at most C;
|
|
339
|
+
* states shorter than memoryMinTokens get none) — mirrors _chunk_pool.
|
|
340
|
+
*
|
|
341
|
+
* Fills rows [nseg, nseg + C) of the SAME pooling matrix the segment means
|
|
342
|
+
* use, so the torso graph emits chunk rows in the one forward it already
|
|
343
|
+
* does (a second run would re-read the whole record).
|
|
344
|
+
*
|
|
345
|
+
* @param {Float32Array} pool the [B, nseg + C, L] pooling matrix
|
|
346
|
+
* @param {object[]} items rendered prompts (with spans)
|
|
347
|
+
* @param {number} L padded sequence length
|
|
348
|
+
*/
|
|
349
|
+
export function packVegaChunks(pool, items, L, {
|
|
350
|
+
chunks: C = VEGA_DEFAULTS.memoryChunks,
|
|
351
|
+
chunkMinTokens = VEGA_DEFAULTS.memoryMinTokens,
|
|
352
|
+
chunkTokens = VEGA_DEFAULTS.memoryChunkTokens,
|
|
353
|
+
nseg = 3 + Math.max(...items.map((it) => it.options.length)),
|
|
354
|
+
} = {}) {
|
|
355
|
+
const n = items.length;
|
|
356
|
+
const chunkMask = new Uint8Array(n * C);
|
|
357
|
+
items.forEach((it, b) => {
|
|
358
|
+
const [s0, s1] = it.spans.state;
|
|
359
|
+
const len = s1 - s0;
|
|
360
|
+
if (len < Math.max(chunkMinTokens, 2)) return;
|
|
361
|
+
const k = Math.max(2, Math.min(C, Math.floor(len / Math.max(1, chunkTokens))));
|
|
362
|
+
for (let c = 0; c < k; c++) {
|
|
363
|
+
// token t of the state belongs to chunk floor(t * k / len) (the cid map)
|
|
364
|
+
const row = b * (nseg + C) + nseg + c;
|
|
365
|
+
let count = 0;
|
|
366
|
+
for (let t = s0; t < s1; t++) {
|
|
367
|
+
if (Math.floor(((t - s0) * k) / len) === c) { pool[row * L + t] = 1; count++; }
|
|
368
|
+
}
|
|
369
|
+
if (count > 0) {
|
|
370
|
+
for (let t = s0; t < s1; t++) {
|
|
371
|
+
if (pool[row * L + t]) pool[row * L + t] = 1 / count;
|
|
372
|
+
}
|
|
373
|
+
chunkMask[b * C + c] = 1;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
return chunkMask;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ---------------------------------------------------------------------------
|
|
381
|
+
// Thermal calibration + answer decoding (mirrors thermal_tau / decide)
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
|
|
384
|
+
/** Fallback when a checkpoint ships no calibration block (probabilities raw). */
|
|
385
|
+
export const VEGA_DEFAULT_THETA = [[0], [0], [0]];
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Per-type thermal calibration from the checkpoint's vega_config.json:
|
|
389
|
+
* log_tau = b + w . phys, tau in [0.2, 20]; probs = softmax(logits / tau).
|
|
390
|
+
*
|
|
391
|
+
* `thetaByRow` overrides the per-type lookup with an explicit theta block per
|
|
392
|
+
* item (the split export calibrates each question with the routed adapter's
|
|
393
|
+
* block).
|
|
394
|
+
*
|
|
395
|
+
* @param {{theta:number[][]}} calibration
|
|
396
|
+
* @param {{logits:ArrayLike<number>, rowWidth:number, qtypes:number[],
|
|
397
|
+
* phys:ArrayLike<number>, nOpts:number[], thetaByRow?:number[][][]}} spec
|
|
398
|
+
* phys is [items, 4]
|
|
399
|
+
* @returns {number[][]} probabilities per item, in prompt order
|
|
400
|
+
*/
|
|
401
|
+
export function vegaThermalProbabilities({
|
|
402
|
+
logits, rowWidth, qtypes, phys, nOpts,
|
|
403
|
+
theta = VEGA_DEFAULT_THETA,
|
|
404
|
+
thetaByRow = null,
|
|
405
|
+
}) {
|
|
406
|
+
const rows = Number(logits.length / rowWidth);
|
|
407
|
+
const out = [];
|
|
408
|
+
for (let b = 0; b < rows; b++) {
|
|
409
|
+
const k = nOpts[b];
|
|
410
|
+
const t = thetaByRow ? (thetaByRow[b] ?? theta[0]) : (theta[qtypes[b]] ?? theta[0]);
|
|
411
|
+
let logTau = t[0];
|
|
412
|
+
for (let f = 0; f < 4; f++) logTau += t[1 + f] * phys[b * 4 + f];
|
|
413
|
+
const tau = Math.exp(Math.min(Math.log(20), Math.max(Math.log(0.2), logTau)));
|
|
414
|
+
out.push(softmaxWithTemperature(
|
|
415
|
+
Array.from(logits.slice(b * rowWidth, b * rowWidth + k)), tau));
|
|
416
|
+
}
|
|
417
|
+
return out;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Per-item thermal features: log1p(kinetic), log1p(nearest), steps/steps_max,
|
|
422
|
+
* log(k) — exactly thermal_features().
|
|
423
|
+
*/
|
|
424
|
+
export function vegaThermalFeatures({ kinetic, nearest, stepsUsed, nOpts, maxSteps }) {
|
|
425
|
+
const out = [];
|
|
426
|
+
for (let b = 0; b < nOpts.length; b++) {
|
|
427
|
+
out.push([
|
|
428
|
+
Math.log1p(kinetic[b]),
|
|
429
|
+
Math.log1p(nearest[b]),
|
|
430
|
+
stepsUsed[b] / Math.max(1, maxSteps),
|
|
431
|
+
Math.log(Math.max(2, nOpts[b])),
|
|
432
|
+
]);
|
|
433
|
+
}
|
|
434
|
+
return out;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Vega answers -> jev-web DecideResult answers (additive vega fields:
|
|
439
|
+
* predictionSet / abstain / unbound / settleSteps / energy, and — when the
|
|
440
|
+
* split export routes per question — adapter / gate).
|
|
441
|
+
*
|
|
442
|
+
* @param {{probs:number[][], qtypes:number[], labels:string[][],
|
|
443
|
+
* physics:{nearest:ArrayLike<number>, stepsUsed:ArrayLike<number>},
|
|
444
|
+
* rawLogits:ArrayLike<number>, rowWidth:number,
|
|
445
|
+
* calibration:object, alpha?:string, risk?:string,
|
|
446
|
+
* calibrations?:object[], adapterNames?:string[], gates?:number[][],
|
|
447
|
+
* routes?:number[]}} spec calibrations/gates are per row when routed
|
|
448
|
+
*/
|
|
449
|
+
export function vegaAnswers({
|
|
450
|
+
probs, qtypes, physics, rawLogits, rowWidth, calibration, questions, labels,
|
|
451
|
+
alpha = VEGA_DEFAULTS.alpha, risk = VEGA_DEFAULTS.risk,
|
|
452
|
+
calibrations = null, adapterNames = null, gates = null, routes = null,
|
|
453
|
+
}) {
|
|
454
|
+
const answers = [];
|
|
455
|
+
labels.forEach((optionLabels, r) => {
|
|
456
|
+
const p = probs[r];
|
|
457
|
+
const k = optionLabels.length;
|
|
458
|
+
const qtype = VEGA_QTYPE_NAMES[qtypes[r]];
|
|
459
|
+
const cal = calibrations?.[r] ?? calibration;
|
|
460
|
+
const conf = Math.max(...p);
|
|
461
|
+
const best = p.indexOf(Math.max(...p));
|
|
462
|
+
// LAC prediction set; the top option is always kept so it is never empty.
|
|
463
|
+
const th = cal?.conformal?.[qtype]?.[String(alpha)];
|
|
464
|
+
const predictionSet = th === undefined ? null
|
|
465
|
+
: optionLabels.filter((_, i) => i === best || p[i] >= Math.max(1 - th, 1e-12));
|
|
466
|
+
// energy = -max raw logit over the question's real options
|
|
467
|
+
let bestLogit = -Infinity;
|
|
468
|
+
for (let j = 0; j < k; j++) bestLogit = Math.max(bestLogit, rawLogits[r * rowWidth + j]);
|
|
469
|
+
const a = {
|
|
470
|
+
probabilities: Object.fromEntries(optionLabels.map((l, i) => [l, p[i]])),
|
|
471
|
+
confidence: conf,
|
|
472
|
+
predictionSet,
|
|
473
|
+
abstain: conf < (cal?.abstain_threshold?.[String(risk)] ?? 0),
|
|
474
|
+
unbound: physics.nearest[r] > (cal?.unbound_threshold ?? Infinity),
|
|
475
|
+
settleSteps: physics.stepsUsed[r],
|
|
476
|
+
energy: -bestLogit,
|
|
477
|
+
};
|
|
478
|
+
if (adapterNames) {
|
|
479
|
+
const rt = routes?.[r] ?? -1;
|
|
480
|
+
a.adapter = rt >= 0 ? (adapterNames[rt] ?? null) : null;
|
|
481
|
+
a.gate = gates?.[r]?.length ? Math.max(...gates[r]) : 0;
|
|
482
|
+
}
|
|
483
|
+
if (qtype === "choice") {
|
|
484
|
+
a.type = "choice";
|
|
485
|
+
a.choice = optionLabels[best];
|
|
486
|
+
a.index = best;
|
|
487
|
+
} else if (qtype === "score") {
|
|
488
|
+
a.type = "score";
|
|
489
|
+
a.level = best;
|
|
490
|
+
a.score = p.reduce((acc, pj, j) => acc + pj * j, 0);
|
|
491
|
+
} else {
|
|
492
|
+
a.type = "noul";
|
|
493
|
+
a.noul = p[1];
|
|
494
|
+
}
|
|
495
|
+
answers.push(a);
|
|
496
|
+
});
|
|
497
|
+
return answers;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** Per-question option labels in prompt order (choice keys / score levels / [no, yes]). */
|
|
501
|
+
export function vegaOptionLabels(questions) {
|
|
502
|
+
return questions.map((q) => {
|
|
503
|
+
if (q.type === "noul") return [...q.options];
|
|
504
|
+
if (q.type === "score") return q.options.map((_, i) => i);
|
|
505
|
+
return [...q.options];
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// ---------------------------------------------------------------------------
|
|
510
|
+
// Split export: JS-side perception (positions, past state, pooling, routing)
|
|
511
|
+
// ---------------------------------------------------------------------------
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Adapter routing (reference route_adapters): the most open adapter if its
|
|
515
|
+
* gate reaches the threshold, else -1 (the base engine calibrates the answer).
|
|
516
|
+
*/
|
|
517
|
+
export function vegaRoute(gates, threshold = 0.5) {
|
|
518
|
+
if (!gates?.length) return -1;
|
|
519
|
+
let best = 0;
|
|
520
|
+
for (let i = 1; i < gates.length; i++) if (gates[i] > gates[best]) best = i;
|
|
521
|
+
return gates[best] >= threshold ? best : -1;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** Text M-RoPE positions: 0..L-1 on each of the three rows ([3, 1, L]). */
|
|
525
|
+
export function vegaTextPositions(L) {
|
|
526
|
+
const out = new BigInt64Array(3 * L);
|
|
527
|
+
for (let i = 0; i < L; i++) {
|
|
528
|
+
out[i] = BigInt(i);
|
|
529
|
+
out[L + i] = BigInt(i);
|
|
530
|
+
out[2 * L + i] = BigInt(i);
|
|
531
|
+
}
|
|
532
|
+
return out;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Zero-filled first-read state for every past_* input the cut decoder keeps
|
|
537
|
+
* (conv / recurrent / key-value caches). Shapes come from the graph's own
|
|
538
|
+
* input metadata: batch_size -> 1, any other symbol (past_sequence_length)
|
|
539
|
+
* -> 0.
|
|
540
|
+
*/
|
|
541
|
+
export function emptyVegaPast(session, Tensor) {
|
|
542
|
+
const feeds = {};
|
|
543
|
+
for (const m of session?.inputMetadata ?? []) {
|
|
544
|
+
if (m?.isTensor === false || !String(m.name).startsWith("past_")) continue;
|
|
545
|
+
const dims = (m.shape ?? []).map(
|
|
546
|
+
(d) => (typeof d === "number" ? d : d === "batch_size" ? 1 : 0));
|
|
547
|
+
feeds[m.name] = new Tensor("float32",
|
|
548
|
+
new Float32Array(dims.reduce((a, b) => a * b, 1)), dims);
|
|
549
|
+
}
|
|
550
|
+
return feeds;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Perception._pooled_batch for one text item, over the hidden states the split
|
|
555
|
+
* decoder emits: the state/question/final-token span means at every feature
|
|
556
|
+
* layer, the option span means at the deepest option layer, and the state cut
|
|
557
|
+
* into up to C equal chunk means for the memory reader (states shorter than
|
|
558
|
+
* chunkMinTokens get none). Image states get no chunks, as in vega_vision.py.
|
|
559
|
+
*
|
|
560
|
+
* @returns {{feats:Float32Array, opts:Float32Array, chunks:Float32Array,
|
|
561
|
+
* cmask:Uint8Array}} feats [2, 3, d] (layer, slot, d)
|
|
562
|
+
*/
|
|
563
|
+
export function poolVegaHidden({
|
|
564
|
+
h13, h19, spans, d,
|
|
565
|
+
chunks: C = VEGA_DEFAULTS.memoryChunks,
|
|
566
|
+
chunkMinTokens = VEGA_DEFAULTS.memoryMinTokens,
|
|
567
|
+
chunkTokens = VEGA_DEFAULTS.memoryChunkTokens,
|
|
568
|
+
withChunks = true,
|
|
569
|
+
}) {
|
|
570
|
+
const L = Math.floor(h13.length / d);
|
|
571
|
+
const mean = (h, a, e, out, off) => {
|
|
572
|
+
if (e <= a) return;
|
|
573
|
+
for (let t = a; t < e; t++) {
|
|
574
|
+
for (let j = 0; j < d; j++) out[off + j] += h[t * d + j];
|
|
575
|
+
}
|
|
576
|
+
for (let j = 0; j < d; j++) out[off + j] /= e - a;
|
|
577
|
+
};
|
|
578
|
+
const feats = new Float32Array(2 * 3 * d);
|
|
579
|
+
[h13, h19].forEach((h, l) => {
|
|
580
|
+
mean(h, spans.state[0], spans.state[1], feats, (l * 3 + 0) * d);
|
|
581
|
+
mean(h, spans.question[0], spans.question[1], feats, (l * 3 + 1) * d);
|
|
582
|
+
feats.set(h.subarray((L - 1) * d, L * d), (l * 3 + 2) * d);
|
|
583
|
+
});
|
|
584
|
+
const opts = new Float32Array(spans.options.length * d);
|
|
585
|
+
spans.options.forEach(([a, e], k) => mean(h19, a, e, opts, k * d));
|
|
586
|
+
const chunks = new Float32Array(C * d);
|
|
587
|
+
const cmask = new Uint8Array(C);
|
|
588
|
+
const [s0, s1] = spans.state;
|
|
589
|
+
const m = s1 - s0;
|
|
590
|
+
if (withChunks && m >= Math.max(chunkMinTokens, 2)) {
|
|
591
|
+
const k = Math.max(2, Math.min(C, Math.floor(m / Math.max(1, chunkTokens))));
|
|
592
|
+
// token t goes to chunk floor(t*k/m): chunk c covers [ceil(c*m/k), ceil((c+1)*m/k))
|
|
593
|
+
for (let c = 0; c < k; c++) {
|
|
594
|
+
const a = s0 + Math.ceil((c * m) / k);
|
|
595
|
+
const e = s0 + (c === k - 1 ? m : Math.ceil(((c + 1) * m) / k));
|
|
596
|
+
mean(h19, a, e, chunks, c * d);
|
|
597
|
+
}
|
|
598
|
+
cmask.fill(1, 0, k);
|
|
599
|
+
}
|
|
600
|
+
return { feats, opts, chunks, cmask };
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// ---------------------------------------------------------------------------
|
|
604
|
+
// The decider
|
|
605
|
+
// ---------------------------------------------------------------------------
|
|
606
|
+
|
|
607
|
+
const now = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
|
|
608
|
+
|
|
609
|
+
const hfUrl = (model, revision, file) =>
|
|
610
|
+
`https://huggingface.co/${model}/resolve/${revision ? `${revision}/` : ""}${file}`;
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Session opener shared by both Vega formats: graph bytes (plus an external
|
|
614
|
+
* data sibling when the graph has one) go through the injected fetch and the
|
|
615
|
+
* Cache API, then ORT creates the session; a WebGPU create failure retries on
|
|
616
|
+
* wasm. Hosts that load weights differently (local files, bundled, offline
|
|
617
|
+
* mirrors) replace the whole step with sessionFactory(url, opts).
|
|
618
|
+
*/
|
|
619
|
+
function openVegaSessions({
|
|
620
|
+
ortMod, fetchImpl, onProgress, sessionFactory, executionProviders, device,
|
|
621
|
+
}) {
|
|
622
|
+
const create = sessionFactory
|
|
623
|
+
?? (async (url, opts) => {
|
|
624
|
+
const bytes = await fetchCachedBytes(url, {
|
|
625
|
+
fetchImpl, onProgress: (p) => onProgress?.(p), persist: true,
|
|
626
|
+
});
|
|
627
|
+
const external = await fetchOnnxExternalData(url, { fetchImpl });
|
|
628
|
+
const sessionOpts = external ? { ...opts, externalData: [external] } : opts;
|
|
629
|
+
return ortMod.InferenceSession.create(bytes, sessionOpts);
|
|
630
|
+
});
|
|
631
|
+
return async (url) => {
|
|
632
|
+
try {
|
|
633
|
+
return await create(url, { executionProviders, graphOptimizationLevel: "all" });
|
|
634
|
+
} catch (e) {
|
|
635
|
+
if (device !== "webgpu") throw e;
|
|
636
|
+
onProgress?.({ phase: `webgpu unavailable (${e?.message ?? e}) — retrying on wasm` });
|
|
637
|
+
return create(url, { executionProviders: ["wasm"], graphOptimizationLevel: "all" });
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Create a Vega decider.
|
|
644
|
+
*
|
|
645
|
+
* @param {object} [opts]
|
|
646
|
+
* variant — "fp16" (default, this repo's fused export) or "q8"
|
|
647
|
+
* (nvkudva/vega-onnx-q8: 8-bit split embed + decoder +
|
|
648
|
+
* fp32 engine with adapters, wasm-only). Sets
|
|
649
|
+
* model/revision/subfolder/format; explicit
|
|
650
|
+
* model/revision/format still win.
|
|
651
|
+
* format — "fused" | "split" (normally derived from the variant)
|
|
652
|
+
* model/revision/subfolder — export repo and, for split exports, the
|
|
653
|
+
* version folder that holds the graphs (default "v2")
|
|
654
|
+
* torsoFile/engineFile — fused tograph paths within the repo
|
|
655
|
+
* embedFile/decoderFile — split graph names within the subfolder
|
|
656
|
+
* calibrationModel — repo to read the config from (default: model;
|
|
657
|
+
* point it at the source checkpoint for your own export)
|
|
658
|
+
* maxLen — prompt window; null = checkpoint's max_length
|
|
659
|
+
* alpha / risk — conformal risk level and abstain risk
|
|
660
|
+
* (default: the calibration block's default_risk)
|
|
661
|
+
* device, wasmPaths, sessionFactory, onProgress — as in the other families
|
|
662
|
+
*/
|
|
663
|
+
export async function createVegaDecider(opts = {}) {
|
|
664
|
+
const variant = opts.variant ?? null;
|
|
665
|
+
const preset = variant === null ? null : VEGA_VARIANTS[variant];
|
|
666
|
+
if (variant !== null && !preset) {
|
|
667
|
+
throw new TypeError(
|
|
668
|
+
`vega: unknown variant "${variant}" (${Object.keys(VEGA_VARIANTS).join(", ")})`);
|
|
669
|
+
}
|
|
670
|
+
const format = opts.format ?? preset?.format ?? "fused";
|
|
671
|
+
if (format !== "fused" && format !== "split") {
|
|
672
|
+
throw new TypeError(`vega: format must be "fused" or "split" (got ${JSON.stringify(format)})`);
|
|
673
|
+
}
|
|
674
|
+
// Like strands: a named variant replaces the *default* model/revision, so
|
|
675
|
+
// createDecisionRuntime({family: "vega", variant: "q8"}) works even though
|
|
676
|
+
// the registry passes the default target through; a deliberately different
|
|
677
|
+
// model/revision still wins.
|
|
678
|
+
const isDefaultTarget = (opts.model ?? VEGA_DEFAULT_MODEL) === VEGA_DEFAULT_MODEL
|
|
679
|
+
&& (opts.revision ?? VEGA_DEFAULT_REVISION) === VEGA_DEFAULT_REVISION;
|
|
680
|
+
const usePreset = preset && isDefaultTarget;
|
|
681
|
+
const resolved = {
|
|
682
|
+
model: usePreset ? preset.model : (opts.model ?? VEGA_DEFAULT_MODEL),
|
|
683
|
+
revision: usePreset ? preset.revision : (opts.revision ?? VEGA_DEFAULT_REVISION),
|
|
684
|
+
subfolder: opts.subfolder ?? preset?.subfolder ?? null,
|
|
685
|
+
};
|
|
686
|
+
return format === "split"
|
|
687
|
+
? createVegaSplitDecider(opts, resolved)
|
|
688
|
+
: createVegaFusedDecider({ ...opts, ...resolved });
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/** The fused export (one torso graph + the base engine), the default. */
|
|
692
|
+
async function createVegaFusedDecider({
|
|
693
|
+
model = VEGA_DEFAULT_MODEL,
|
|
694
|
+
revision = VEGA_DEFAULT_REVISION,
|
|
695
|
+
torsoFile = VEGA_TORSO_FILE,
|
|
696
|
+
engineFile = VEGA_ENGINE_FILE,
|
|
697
|
+
calibrationModel = null,
|
|
698
|
+
calibrationRevision = null,
|
|
699
|
+
ort = null,
|
|
700
|
+
transformers = null,
|
|
701
|
+
fetchImpl = fetch,
|
|
702
|
+
wasmPaths = null,
|
|
703
|
+
device = "auto",
|
|
704
|
+
sessionFactory = null,
|
|
705
|
+
onProgress = null,
|
|
706
|
+
maxLen = null,
|
|
707
|
+
alpha = VEGA_DEFAULTS.alpha,
|
|
708
|
+
risk = null,
|
|
709
|
+
scope = globalThis,
|
|
710
|
+
} = {}) {
|
|
711
|
+
const ortMod = ort ?? (await import("onnxruntime-web"));
|
|
712
|
+
const tf = transformers ?? (await import("@huggingface/transformers"));
|
|
713
|
+
if (wasmPaths) {
|
|
714
|
+
try { ortMod.env.wasm.wasmPaths = wasmPaths; } catch { /* older runtime */ }
|
|
715
|
+
}
|
|
716
|
+
const resolvedDevice = device !== "auto"
|
|
717
|
+
? device
|
|
718
|
+
: (scope?.navigator?.gpu ? "webgpu" : "wasm");
|
|
719
|
+
const executionProviders = resolvedDevice === "webgpu" ? ["webgpu", "wasm"] : ["wasm"];
|
|
720
|
+
|
|
721
|
+
// Calibration + architecture constants come from the checkpoint's
|
|
722
|
+
// vega_config.json (thermal theta, conformal thresholds, chunking, window).
|
|
723
|
+
const configUrl = hfUrl(calibrationModel ?? model,
|
|
724
|
+
calibrationRevision ?? (calibrationModel ? "main" : revision),
|
|
725
|
+
"vega_config.json");
|
|
726
|
+
let config = null;
|
|
727
|
+
let configSource = null;
|
|
728
|
+
try {
|
|
729
|
+
const res = await fetchImpl(configUrl);
|
|
730
|
+
if (res?.ok) {
|
|
731
|
+
config = await res.json();
|
|
732
|
+
configSource = configUrl;
|
|
733
|
+
}
|
|
734
|
+
} catch { /* fall through to the shipped defaults */ }
|
|
735
|
+
const cfg = { ...VEGA_DEFAULTS, ...(config ?? {}) };
|
|
736
|
+
const calibration = config?.calibration ?? {};
|
|
737
|
+
const effRisk = risk ?? calibration.default_risk ?? cfg.default_risk ?? VEGA_DEFAULTS.risk;
|
|
738
|
+
const cfgMaxLen = config?.max_len ?? cfg.maxLen;
|
|
739
|
+
const effMaxLen = Number.isFinite(maxLen) && maxLen > 0 ? maxLen : cfgMaxLen;
|
|
740
|
+
|
|
741
|
+
const tok = await tf.AutoTokenizer.from_pretrained(calibrationModel ?? model, {
|
|
742
|
+
revision: calibrationRevision ?? (calibrationModel ? "main" : revision),
|
|
743
|
+
progress_callback: () => {},
|
|
744
|
+
});
|
|
745
|
+
const encode = (text, addSpecial = false) =>
|
|
746
|
+
Array.from(tok(text, { add_special_tokens: addSpecial }).input_ids.data, Number);
|
|
747
|
+
|
|
748
|
+
const open = openVegaSessions({
|
|
749
|
+
ortMod, fetchImpl, onProgress, sessionFactory,
|
|
750
|
+
executionProviders, device: resolvedDevice,
|
|
751
|
+
});
|
|
752
|
+
const torso = await open(hfUrl(model, revision, torsoFile));
|
|
753
|
+
const engine = await open(hfUrl(model, revision, engineFile));
|
|
754
|
+
|
|
755
|
+
const int64 = (arr, dims) => new ortMod.Tensor("int64", arr, dims);
|
|
756
|
+
const f32 = (arr, dims) => new ortMod.Tensor("float32", arr, dims);
|
|
757
|
+
const bool = (arr, dims) => new ortMod.Tensor("bool", arr, dims);
|
|
758
|
+
|
|
759
|
+
return {
|
|
760
|
+
info: {
|
|
761
|
+
family: "vega",
|
|
762
|
+
format: "fused",
|
|
763
|
+
model,
|
|
764
|
+
revision,
|
|
765
|
+
device: resolvedDevice,
|
|
766
|
+
maxLen: effMaxLen,
|
|
767
|
+
alpha: String(alpha),
|
|
768
|
+
risk: String(effRisk),
|
|
769
|
+
calibrationSource: configSource,
|
|
770
|
+
nSteps: cfg.n_steps ?? cfg.nSteps,
|
|
771
|
+
config,
|
|
772
|
+
},
|
|
773
|
+
async decide(state, questions) {
|
|
774
|
+
const t0 = now();
|
|
775
|
+
const normalized = normalizeQuestions(questions);
|
|
776
|
+
const qdefs = normalized.map(vegaQuestionDef);
|
|
777
|
+
const stateText = renderVegaText(state);
|
|
778
|
+
const bosId = tok.bos_token_id ?? null;
|
|
779
|
+
|
|
780
|
+
const items = qdefs.map((qdef) => {
|
|
781
|
+
const { ids, spans } = buildVegaPrompt({
|
|
782
|
+
encode, stateText, qdef, maxLen: effMaxLen,
|
|
783
|
+
bosId,
|
|
784
|
+
queryFirstMaxState: cfg.query_first_max_state ?? cfg.queryFirstMaxState,
|
|
785
|
+
optMaxTokens: cfg.opt_max_tokens ?? cfg.optMaxTokens,
|
|
786
|
+
insMaxTokens: 256,
|
|
787
|
+
});
|
|
788
|
+
return { ids, spans, qtype: VEGA_QTYPE[qdef.t], options: vegaOptionTexts(qdef) };
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
const batch = packVegaBatch(items, {
|
|
792
|
+
chunks: cfg.memory_chunks ?? cfg.memoryChunks,
|
|
793
|
+
});
|
|
794
|
+
const chunkMask = packVegaChunks(batch.pool, items, batch.L, {
|
|
795
|
+
chunks: cfg.memory_chunks ?? cfg.memoryChunks,
|
|
796
|
+
chunkMinTokens: cfg.memory_min_tokens ?? cfg.memoryMinTokens,
|
|
797
|
+
chunkTokens: cfg.memory_chunk_tokens ?? cfg.memoryChunkTokens,
|
|
798
|
+
nseg: batch.nseg,
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
const tRun = now();
|
|
802
|
+
// Torso: one causal read to the deepest captured layer, all span means
|
|
803
|
+
// (segments + state chunks) pooled in-graph from the one forward.
|
|
804
|
+
const torsoOut = await torso.run({
|
|
805
|
+
input_ids: int64(batch.inputIds, [batch.n, batch.L]),
|
|
806
|
+
attention_mask: int64(batch.attention, [batch.n, batch.L]),
|
|
807
|
+
pool: f32(batch.pool, batch.poolDims),
|
|
808
|
+
});
|
|
809
|
+
const pooled = torsoOut.pooled ?? torsoOut[Object.keys(torsoOut)[0]];
|
|
810
|
+
// pooled: [B, nLayers, nseg + C, d] — nLayers = feature_layers,
|
|
811
|
+
// rows 0..2 = state/question/last, 3..3+K = options, then C chunk rows.
|
|
812
|
+
const pd = pooled.dims;
|
|
813
|
+
const d = pd[3];
|
|
814
|
+
const Lc = pd[1];
|
|
815
|
+
const rowsTotal = pd[2];
|
|
816
|
+
const C = rowsTotal - batch.nseg;
|
|
817
|
+
const pv = pooled.data;
|
|
818
|
+
const feats = new Float32Array(batch.n * Lc * 3 * d);
|
|
819
|
+
const opts = new Float32Array(batch.n * batch.K * d);
|
|
820
|
+
const chunks = new Float32Array(batch.n * C * d);
|
|
821
|
+
for (let b = 0; b < batch.n; b++) {
|
|
822
|
+
for (let l = 0; l < Lc; l++) {
|
|
823
|
+
for (let s = 0; s < 3; s++) {
|
|
824
|
+
const row = s; // pooling rows 0/1/2 are state/question/last
|
|
825
|
+
const src = ((b * Lc + l) * rowsTotal + row) * d;
|
|
826
|
+
feats.set(pv.subarray(src, src + d), ((b * Lc + l) * 3 + s) * d);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
// options from the deepest captured layer only (option_layers)
|
|
830
|
+
for (let k = 0; k < batch.K; k++) {
|
|
831
|
+
const src = ((b * Lc + Lc - 1) * rowsTotal + 3 + k) * d;
|
|
832
|
+
opts.set(pv.subarray(src, src + d), (b * batch.K + k) * d);
|
|
833
|
+
}
|
|
834
|
+
// chunk means: deepest feature layer, rows nseg..nseg + C
|
|
835
|
+
for (let c = 0; c < C; c++) {
|
|
836
|
+
const src = ((b * Lc + Lc - 1) * rowsTotal + batch.nseg + c) * d;
|
|
837
|
+
chunks.set(pv.subarray(src, src + d), (b * C + c) * d);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Engine: landscape + integrator -> raw logits and the physics state.
|
|
842
|
+
const engineOut = await engine.run({
|
|
843
|
+
feats: f32(feats, [batch.n, Lc, 3, d]),
|
|
844
|
+
opts: f32(opts, [batch.n, batch.K, d]),
|
|
845
|
+
opt_mask: bool(batch.optMask, [batch.n, batch.K]),
|
|
846
|
+
qtype: int64(batch.qtypes, [batch.n]),
|
|
847
|
+
chunks: f32(chunks, [batch.n, C, d]),
|
|
848
|
+
chunk_mask: bool(chunkMask, [batch.n, C]),
|
|
849
|
+
});
|
|
850
|
+
const logits = engineOut.logits.data;
|
|
851
|
+
const kinetic = engineOut.kinetic.data;
|
|
852
|
+
const nearest = engineOut.nearest.data;
|
|
853
|
+
const stepsUsed = Array.from(engineOut.steps_used.data, Number);
|
|
854
|
+
|
|
855
|
+
const nOpts = items.map((it) => it.options.length);
|
|
856
|
+
const phys = vegaThermalFeatures({
|
|
857
|
+
kinetic, nearest, stepsUsed, nOpts, maxSteps: cfg.n_steps ?? cfg.nSteps,
|
|
858
|
+
});
|
|
859
|
+
const probs = vegaThermalProbabilities({
|
|
860
|
+
logits, rowWidth: batch.K, qtypes: batch.qtypes,
|
|
861
|
+
phys: phys.flat(), nOpts,
|
|
862
|
+
theta: calibration.theta ?? VEGA_DEFAULT_THETA,
|
|
863
|
+
});
|
|
864
|
+
const runMs = now() - tRun;
|
|
865
|
+
const labels = vegaOptionLabels(normalized);
|
|
866
|
+
const answers = vegaAnswers({
|
|
867
|
+
probs, qtypes: batch.qtypes, labels,
|
|
868
|
+
physics: { nearest, stepsUsed },
|
|
869
|
+
rawLogits: logits, rowWidth: batch.K,
|
|
870
|
+
calibration, alpha, risk: effRisk,
|
|
871
|
+
});
|
|
872
|
+
const decode = (ids) => {
|
|
873
|
+
try {
|
|
874
|
+
return tok.decode(Array.from(ids), { skip_special_tokens: false });
|
|
875
|
+
} catch {
|
|
876
|
+
return Array.from(ids).join(" ");
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
return {
|
|
880
|
+
answers,
|
|
881
|
+
truncated: false,
|
|
882
|
+
length: Number(batch.attention.reduce((a, v) => a + v, 0n)),
|
|
883
|
+
prompts: items.map((it) => decode(it.ids)),
|
|
884
|
+
timings: { totalMs: Math.round(now() - t0), runMs: Math.round(runMs) },
|
|
885
|
+
};
|
|
886
|
+
},
|
|
887
|
+
async dispose() {
|
|
888
|
+
try { await torso.release?.(); } catch { /* optional */ }
|
|
889
|
+
try { await engine.release?.(); } catch { /* optional */ }
|
|
890
|
+
},
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* The split export (nvkudva/vega-onnx-q8): an 8-bit embedding graph, a cut
|
|
896
|
+
* decoder that stops after layer 19 and emits the residual stream at layers
|
|
897
|
+
* 13/19 (the LM head and layers 20-23 never run), and the fp32 engine with
|
|
898
|
+
* both gated task adapters. JS pools the hidden states, each question is
|
|
899
|
+
* calibrated with the theta block of the adapter its gate routes it to, and
|
|
900
|
+
* the conformal / abstain / unbound machinery reads that block's thresholds.
|
|
901
|
+
*
|
|
902
|
+
* Text only in v1: the vision encoder (image as the state) is not wired in.
|
|
903
|
+
* The 8-bit graphs are wasm-only (MatMulNBits has no WebGPU kernel), so
|
|
904
|
+
* device "auto" resolves to wasm.
|
|
905
|
+
*/
|
|
906
|
+
async function createVegaSplitDecider({
|
|
907
|
+
embedFile = VEGA_Q8_EMBED_FILE,
|
|
908
|
+
decoderFile = VEGA_Q8_DECODER_FILE,
|
|
909
|
+
engineFile = VEGA_Q8_ENGINE_FILE,
|
|
910
|
+
calibrationModel = null,
|
|
911
|
+
calibrationRevision = null,
|
|
912
|
+
ort = null,
|
|
913
|
+
transformers = null,
|
|
914
|
+
fetchImpl = fetch,
|
|
915
|
+
wasmPaths = null,
|
|
916
|
+
device = "auto",
|
|
917
|
+
sessionFactory = null,
|
|
918
|
+
onProgress = null,
|
|
919
|
+
maxLen = null,
|
|
920
|
+
alpha = VEGA_DEFAULTS.alpha,
|
|
921
|
+
risk = null,
|
|
922
|
+
} = {}, { model, revision, subfolder }) {
|
|
923
|
+
const ortMod = ort ?? (await import("onnxruntime-web"));
|
|
924
|
+
const tf = transformers ?? (await import("@huggingface/transformers"));
|
|
925
|
+
if (wasmPaths) {
|
|
926
|
+
try { ortMod.env.wasm.wasmPaths = wasmPaths; } catch { /* older runtime */ }
|
|
927
|
+
}
|
|
928
|
+
const resolvedDevice = device !== "auto" ? device : "wasm";
|
|
929
|
+
const executionProviders = resolvedDevice === "webgpu" ? ["webgpu", "wasm"] : ["wasm"];
|
|
930
|
+
|
|
931
|
+
const basePath = subfolder ? `${subfolder}/` : "";
|
|
932
|
+
const configRepo = calibrationModel ?? model;
|
|
933
|
+
const configRevision = calibrationRevision ?? (calibrationModel ? "main" : revision);
|
|
934
|
+
const configUrl = hfUrl(configRepo, configRevision, `${basePath}vega_web.json`);
|
|
935
|
+
let config = null;
|
|
936
|
+
let configSource = null;
|
|
937
|
+
try {
|
|
938
|
+
const res = await fetchImpl(configUrl);
|
|
939
|
+
if (res?.ok) {
|
|
940
|
+
config = await res.json();
|
|
941
|
+
configSource = configUrl;
|
|
942
|
+
}
|
|
943
|
+
} catch { /* fall through to the shipped defaults */ }
|
|
944
|
+
const cfg = { ...VEGA_DEFAULTS, ...(config ?? {}) };
|
|
945
|
+
const calibration = config?.calibration ?? {};
|
|
946
|
+
const adapters = Array.isArray(config?.adapters) ? config.adapters : [];
|
|
947
|
+
const effRisk = risk ?? calibration.default_risk ?? cfg.default_risk ?? VEGA_DEFAULTS.risk;
|
|
948
|
+
const cfgMaxLen = config?.max_len ?? cfg.maxLen;
|
|
949
|
+
const effMaxLen = Number.isFinite(maxLen) && maxLen > 0 ? maxLen : cfgMaxLen;
|
|
950
|
+
|
|
951
|
+
// The tokenizer (Qwen3.5) ships inside the version folder, and
|
|
952
|
+
// transformers.js's AutoTokenizer has no subfolder option here, so build
|
|
953
|
+
// PreTrainedTokenizer from the two files directly -- what the export's own
|
|
954
|
+
// runtime does. Older transformers falls back to AutoTokenizer.
|
|
955
|
+
const fetchJson = async (file) => {
|
|
956
|
+
const url = hfUrl(configRepo, configRevision, `${basePath}${file}`);
|
|
957
|
+
const res = await fetchImpl(url);
|
|
958
|
+
if (!res?.ok) throw new Error(`vega: ${url} not found (HTTP ${res?.status ?? "no response"})`);
|
|
959
|
+
return res.json();
|
|
960
|
+
};
|
|
961
|
+
let tok;
|
|
962
|
+
if (typeof tf.PreTrainedTokenizer === "function") {
|
|
963
|
+
const [tokenizerJson, tokenizerConfig] = await Promise.all([
|
|
964
|
+
fetchJson("tokenizer.json"), fetchJson("tokenizer_config.json"),
|
|
965
|
+
]);
|
|
966
|
+
tok = new tf.PreTrainedTokenizer(tokenizerJson, tokenizerConfig);
|
|
967
|
+
} else {
|
|
968
|
+
tok = await tf.AutoTokenizer.from_pretrained(configRepo, { revision: configRevision });
|
|
969
|
+
}
|
|
970
|
+
const encode = (text, addSpecial = false) => {
|
|
971
|
+
if (typeof tok.encode === "function") {
|
|
972
|
+
const ids = tok.encode(text, { add_special_tokens: addSpecial });
|
|
973
|
+
return Array.isArray(ids) ? ids : Array.from(ids?.input_ids?.data ?? ids, Number);
|
|
974
|
+
}
|
|
975
|
+
return Array.from(tok(text, { add_special_tokens: addSpecial }).input_ids.data, Number);
|
|
976
|
+
};
|
|
977
|
+
const decode = (ids) => {
|
|
978
|
+
try {
|
|
979
|
+
return tok.decode(Array.from(ids), { skip_special_tokens: false });
|
|
980
|
+
} catch {
|
|
981
|
+
return Array.from(ids).join(" ");
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
|
|
985
|
+
const open = openVegaSessions({
|
|
986
|
+
ortMod, fetchImpl, onProgress, sessionFactory, executionProviders,
|
|
987
|
+
device: resolvedDevice,
|
|
988
|
+
});
|
|
989
|
+
const embed = await open(hfUrl(model, revision, `${basePath}${embedFile}`));
|
|
990
|
+
const decoder = await open(hfUrl(model, revision, `${basePath}${decoderFile}`));
|
|
991
|
+
const engine = await open(hfUrl(model, revision, `${basePath}${engineFile}`));
|
|
992
|
+
const past = emptyVegaPast(decoder, ortMod.Tensor);
|
|
993
|
+
|
|
994
|
+
const int64 = (arr, dims) => new ortMod.Tensor("int64", arr, dims);
|
|
995
|
+
const f32 = (arr, dims) => new ortMod.Tensor("float32", arr, dims);
|
|
996
|
+
const bool = (arr, dims) => new ortMod.Tensor("bool", arr, dims);
|
|
997
|
+
|
|
998
|
+
return {
|
|
999
|
+
info: {
|
|
1000
|
+
family: "vega",
|
|
1001
|
+
format: "split",
|
|
1002
|
+
model,
|
|
1003
|
+
revision,
|
|
1004
|
+
subfolder: subfolder ?? null,
|
|
1005
|
+
device: resolvedDevice,
|
|
1006
|
+
maxLen: effMaxLen,
|
|
1007
|
+
alpha: String(alpha),
|
|
1008
|
+
risk: String(effRisk),
|
|
1009
|
+
calibrationSource: configSource,
|
|
1010
|
+
nSteps: cfg.n_steps ?? cfg.nSteps,
|
|
1011
|
+
adapters: adapters.map((a) => a.name),
|
|
1012
|
+
config,
|
|
1013
|
+
},
|
|
1014
|
+
async decide(state, questions) {
|
|
1015
|
+
const t0 = now();
|
|
1016
|
+
const normalized = normalizeQuestions(questions);
|
|
1017
|
+
const qdefs = normalized.map(vegaQuestionDef);
|
|
1018
|
+
const stateText = renderVegaText(state);
|
|
1019
|
+
const C = Number(cfg.memory_chunks ?? VEGA_DEFAULTS.memoryChunks);
|
|
1020
|
+
const chunkMinTokens = cfg.memory_min_tokens ?? VEGA_DEFAULTS.memoryMinTokens;
|
|
1021
|
+
const chunkTokens = cfg.memory_chunk_tokens ?? VEGA_DEFAULTS.memoryChunkTokens;
|
|
1022
|
+
|
|
1023
|
+
const items = qdefs.map((qdef) => {
|
|
1024
|
+
const { ids, spans } = buildVegaPrompt({
|
|
1025
|
+
encode, stateText, qdef, maxLen: effMaxLen,
|
|
1026
|
+
bosId: null, // the reference adds no BOS for Qwen, and the export never does
|
|
1027
|
+
queryFirstMaxState: cfg.query_first_max_state ?? cfg.queryFirstMaxState,
|
|
1028
|
+
optMaxTokens: cfg.opt_max_tokens ?? cfg.optMaxTokens,
|
|
1029
|
+
insMaxTokens: 256,
|
|
1030
|
+
truncateState: false, // the export refuses a state that does not fit
|
|
1031
|
+
});
|
|
1032
|
+
return { ids, spans, qtype: VEGA_QTYPE[qdef.t], options: vegaOptionTexts(qdef) };
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
// One decoder pass per question (the export's own reading), pooling in JS.
|
|
1036
|
+
const tRun = now();
|
|
1037
|
+
const rows = [];
|
|
1038
|
+
const prompts = [];
|
|
1039
|
+
for (const it of items) {
|
|
1040
|
+
const L = it.ids.length;
|
|
1041
|
+
const embedOut = await embed.run({
|
|
1042
|
+
input_ids: int64(BigInt64Array.from(it.ids, BigInt), [1, L]),
|
|
1043
|
+
});
|
|
1044
|
+
const embeds = embedOut.inputs_embeds.data;
|
|
1045
|
+
const d = embeds.length / L;
|
|
1046
|
+
const decOut = await decoder.run({
|
|
1047
|
+
...past,
|
|
1048
|
+
inputs_embeds: f32(embeds, [1, L, d]),
|
|
1049
|
+
attention_mask: int64(new BigInt64Array(L).fill(1n), [1, L]),
|
|
1050
|
+
position_ids: int64(vegaTextPositions(L), [3, 1, L]),
|
|
1051
|
+
});
|
|
1052
|
+
const pooled = poolVegaHidden({
|
|
1053
|
+
h13: decOut.h13.data, h19: decOut.h19.data, spans: it.spans, d,
|
|
1054
|
+
chunks: C, chunkMinTokens, chunkTokens,
|
|
1055
|
+
});
|
|
1056
|
+
const K = it.options.length;
|
|
1057
|
+
const eng = await engine.run({
|
|
1058
|
+
feats: f32(pooled.feats, [1, 2, 3, d]),
|
|
1059
|
+
opts: f32(pooled.opts, [1, K, 1, d]),
|
|
1060
|
+
opt_mask: bool(new Uint8Array(K).fill(1), [1, K]),
|
|
1061
|
+
qtype: int64([BigInt(it.qtype)], [1]),
|
|
1062
|
+
chunks: f32(pooled.chunks, [1, C, d]),
|
|
1063
|
+
chunk_mask: bool(pooled.cmask, [1, C]),
|
|
1064
|
+
});
|
|
1065
|
+
rows.push({
|
|
1066
|
+
qtype: it.qtype,
|
|
1067
|
+
K,
|
|
1068
|
+
logits: Array.from(eng.logits.data).slice(0, K),
|
|
1069
|
+
kinetic: eng.kinetic.data[0],
|
|
1070
|
+
nearest: eng.nearest.data[0],
|
|
1071
|
+
stepsUsed: Number(eng.steps_used.data[0]),
|
|
1072
|
+
gates: Array.from(eng.gates.data),
|
|
1073
|
+
});
|
|
1074
|
+
prompts.push(decode(it.ids));
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
const nOpts = rows.map((r) => r.K);
|
|
1078
|
+
const rowWidth = Math.max(...nOpts);
|
|
1079
|
+
const logits = new Float32Array(rows.length * rowWidth);
|
|
1080
|
+
rows.forEach((r, b) => logits.set(r.logits, b * rowWidth));
|
|
1081
|
+
const phys = vegaThermalFeatures({
|
|
1082
|
+
kinetic: rows.map((r) => r.kinetic),
|
|
1083
|
+
nearest: rows.map((r) => r.nearest),
|
|
1084
|
+
stepsUsed: rows.map((r) => r.stepsUsed),
|
|
1085
|
+
nOpts, maxSteps: cfg.n_steps ?? cfg.nSteps,
|
|
1086
|
+
});
|
|
1087
|
+
// Each question is calibrated by the adapter its gate routes it to
|
|
1088
|
+
// (route_adapters: the most open gate, if it reaches the threshold).
|
|
1089
|
+
const gateThreshold = adapters[0]?.gate_threshold ?? 0.5;
|
|
1090
|
+
const gates = rows.map((r) => r.gates);
|
|
1091
|
+
const routes = gates.map((g) => vegaRoute(g, gateThreshold));
|
|
1092
|
+
const calibrations = routes.map(
|
|
1093
|
+
(rt) => (rt >= 0 ? adapters[rt]?.calibration : null) ?? calibration);
|
|
1094
|
+
const thetaByRow = calibrations.map(
|
|
1095
|
+
(cal, b) => cal?.theta?.[rows[b].qtype] ?? VEGA_DEFAULT_THETA[rows[b].qtype]);
|
|
1096
|
+
const probs = vegaThermalProbabilities({
|
|
1097
|
+
logits, rowWidth, qtypes: rows.map((r) => r.qtype),
|
|
1098
|
+
phys: phys.flat(), nOpts, thetaByRow,
|
|
1099
|
+
});
|
|
1100
|
+
const runMs = now() - tRun;
|
|
1101
|
+
const answers = vegaAnswers({
|
|
1102
|
+
probs, qtypes: rows.map((r) => r.qtype), labels: vegaOptionLabels(normalized),
|
|
1103
|
+
physics: {
|
|
1104
|
+
nearest: rows.map((r) => r.nearest),
|
|
1105
|
+
stepsUsed: rows.map((r) => r.stepsUsed),
|
|
1106
|
+
},
|
|
1107
|
+
rawLogits: logits, rowWidth,
|
|
1108
|
+
calibration, calibrations, alpha, risk: effRisk,
|
|
1109
|
+
adapterNames: adapters.map((a) => a.name), gates, routes,
|
|
1110
|
+
});
|
|
1111
|
+
return {
|
|
1112
|
+
answers,
|
|
1113
|
+
truncated: false,
|
|
1114
|
+
length: items.reduce((n, it) => n + it.ids.length, 0),
|
|
1115
|
+
prompts,
|
|
1116
|
+
timings: { totalMs: Math.round(now() - t0), runMs: Math.round(runMs) },
|
|
1117
|
+
};
|
|
1118
|
+
},
|
|
1119
|
+
async dispose() {
|
|
1120
|
+
try { await embed.release?.(); } catch { /* optional */ }
|
|
1121
|
+
try { await decoder.release?.(); } catch { /* optional */ }
|
|
1122
|
+
try { await engine.release?.(); } catch { /* optional */ }
|
|
1123
|
+
},
|
|
1124
|
+
};
|
|
1125
|
+
}
|