jev-web 0.8.0 → 0.9.1
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/README.md +180 -32
- package/docs/demo-model-picker.png +0 -0
- package/package.json +9 -5
- package/src/clef.mjs +498 -0
- package/src/decision3.mjs +571 -0
- package/src/index.mjs +59 -0
- package/src/questions.mjs +8 -1
- package/src/registry.mjs +34 -4
- package/src/vega.mjs +539 -50
package/src/vega.mjs
CHANGED
|
@@ -24,9 +24,17 @@
|
|
|
24
24
|
* vega_config.json; split-conformal prediction sets and an abstain flag
|
|
25
25
|
* come from the same energies, plus an "unbound particle" OOD signal.
|
|
26
26
|
*
|
|
27
|
-
* Two ONNX
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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.
|
|
30
38
|
*/
|
|
31
39
|
import { normalizeQuestions } from "./questions.mjs";
|
|
32
40
|
import { softmaxWithTemperature } from "./answers.mjs";
|
|
@@ -40,6 +48,37 @@ export const VEGA_DEFAULT_REVISION = "d2bd4604ed770ca614745f07f623b66e89f406e4";
|
|
|
40
48
|
export const VEGA_TORSO_FILE = "onnx/torso_fp16.onnx";
|
|
41
49
|
export const VEGA_ENGINE_FILE = "onnx/engine.onnx";
|
|
42
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
|
+
|
|
43
82
|
/** Reference prompt template 0 (the inference surface form). */
|
|
44
83
|
const VEGA_HEAD = "Observation:\n";
|
|
45
84
|
const VEGA_QUESTION = "\n\nMeasurement ({t}): {ins}\nPossible outcomes:";
|
|
@@ -75,7 +114,10 @@ export const VEGA_DEFAULTS = {
|
|
|
75
114
|
* Vega's `boolean` is the noul primitive; its slots are fixed [no, yes].
|
|
76
115
|
*/
|
|
77
116
|
export function vegaQuestionDef(question) {
|
|
78
|
-
|
|
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);
|
|
79
121
|
if (question.type === "noul") {
|
|
80
122
|
return { t: "boolean", ins, crit: { ...(question.criteria ?? {}) } };
|
|
81
123
|
}
|
|
@@ -90,28 +132,70 @@ export function vegaQuestionDef(question) {
|
|
|
90
132
|
return { t: "choice", ins, crit };
|
|
91
133
|
}
|
|
92
134
|
|
|
93
|
-
|
|
94
|
-
|
|
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);
|
|
95
178
|
}
|
|
96
179
|
|
|
180
|
+
/** The reference render_text: strings verbatim, anything else json.dumps. */
|
|
97
181
|
export function renderVegaText(content) {
|
|
98
|
-
return typeof content === "string" ? content :
|
|
182
|
+
return typeof content === "string" ? content : vegaJsonDumps(content, false);
|
|
99
183
|
}
|
|
100
184
|
|
|
101
185
|
/** Option texts in label-index order, exactly as option_texts() renders them. */
|
|
102
186
|
export function vegaOptionTexts(qdef) {
|
|
103
187
|
if (qdef.t === "choice") {
|
|
104
188
|
return Object.entries(qdef.crit).map(([k, v]) => {
|
|
105
|
-
const d = v === null || v === undefined ? "" :
|
|
189
|
+
const d = v === null || v === undefined ? "" : vegaCritText(v);
|
|
106
190
|
return d ? `${k} - ${d}` : String(k);
|
|
107
191
|
});
|
|
108
192
|
}
|
|
109
193
|
if (qdef.t === "score") {
|
|
110
|
-
return qdef.crit.map((c, i) => `level ${i} - ${
|
|
194
|
+
return qdef.crit.map((c, i) => `level ${i} - ${vegaCritText(c)}`);
|
|
111
195
|
}
|
|
112
196
|
const crit = qdef.crit ?? {};
|
|
113
|
-
const no = crit.false === null || crit.false === undefined ? "" :
|
|
114
|
-
const yes = crit.true === null || crit.true === undefined ? "" :
|
|
197
|
+
const no = crit.false === null || crit.false === undefined ? "" : vegaCritText(crit.false);
|
|
198
|
+
const yes = crit.true === null || crit.true === undefined ? "" : vegaCritText(crit.true);
|
|
115
199
|
return [
|
|
116
200
|
`no - ${no || "the statement does not hold"}`,
|
|
117
201
|
`yes - ${yes || "the statement holds"}`,
|
|
@@ -127,6 +211,10 @@ export function vegaOptionTexts(qdef) {
|
|
|
127
211
|
* state. Options are capped per option, and an over-long option list is
|
|
128
212
|
* shrunk evenly before the instructions are (mirrors build_prompt).
|
|
129
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
|
+
*
|
|
130
218
|
* @returns {{ids:number[], spans:{state:[number,number], question:[number,number],
|
|
131
219
|
* options:[number,number][], last:number}}}
|
|
132
220
|
*/
|
|
@@ -139,6 +227,7 @@ export function buildVegaPrompt({
|
|
|
139
227
|
queryFirstMaxState = VEGA_DEFAULTS.queryFirstMaxState,
|
|
140
228
|
optMaxTokens = VEGA_DEFAULTS.optMaxTokens,
|
|
141
229
|
insMaxTokens = VEGA_DEFAULTS.insMaxTokens,
|
|
230
|
+
truncateState = true,
|
|
142
231
|
}) {
|
|
143
232
|
const options = vegaOptionTexts(qdef);
|
|
144
233
|
const enc = (s) => encode(s, false);
|
|
@@ -169,6 +258,11 @@ export function buildVegaPrompt({
|
|
|
169
258
|
fixed += pre.length;
|
|
170
259
|
}
|
|
171
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
|
+
}
|
|
172
266
|
st = st.slice(0, room); // truncate_left = false: the head of the state is kept
|
|
173
267
|
const ids = [...bos, ...pre, ...head];
|
|
174
268
|
const s0 = ids.length;
|
|
@@ -294,20 +388,26 @@ export const VEGA_DEFAULT_THETA = [[0], [0], [0]];
|
|
|
294
388
|
* Per-type thermal calibration from the checkpoint's vega_config.json:
|
|
295
389
|
* log_tau = b + w . phys, tau in [0.2, 20]; probs = softmax(logits / tau).
|
|
296
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
|
+
*
|
|
297
395
|
* @param {{theta:number[][]}} calibration
|
|
298
396
|
* @param {{logits:ArrayLike<number>, rowWidth:number, qtypes:number[],
|
|
299
|
-
* phys:ArrayLike<number>, nOpts:number[]}} spec
|
|
397
|
+
* phys:ArrayLike<number>, nOpts:number[], thetaByRow?:number[][][]}} spec
|
|
398
|
+
* phys is [items, 4]
|
|
300
399
|
* @returns {number[][]} probabilities per item, in prompt order
|
|
301
400
|
*/
|
|
302
401
|
export function vegaThermalProbabilities({
|
|
303
402
|
logits, rowWidth, qtypes, phys, nOpts,
|
|
304
403
|
theta = VEGA_DEFAULT_THETA,
|
|
404
|
+
thetaByRow = null,
|
|
305
405
|
}) {
|
|
306
406
|
const rows = Number(logits.length / rowWidth);
|
|
307
407
|
const out = [];
|
|
308
408
|
for (let b = 0; b < rows; b++) {
|
|
309
409
|
const k = nOpts[b];
|
|
310
|
-
const t = theta[qtypes[b]] ?? theta[0];
|
|
410
|
+
const t = thetaByRow ? (thetaByRow[b] ?? theta[0]) : (theta[qtypes[b]] ?? theta[0]);
|
|
311
411
|
let logTau = t[0];
|
|
312
412
|
for (let f = 0; f < 4; f++) logTau += t[1 + f] * phys[b * 4 + f];
|
|
313
413
|
const tau = Math.exp(Math.min(Math.log(20), Math.max(Math.log(0.2), logTau)));
|
|
@@ -336,26 +436,31 @@ export function vegaThermalFeatures({ kinetic, nearest, stepsUsed, nOpts, maxSte
|
|
|
336
436
|
|
|
337
437
|
/**
|
|
338
438
|
* Vega answers -> jev-web DecideResult answers (additive vega fields:
|
|
339
|
-
* predictionSet / abstain / unbound / settleSteps / energy
|
|
439
|
+
* predictionSet / abstain / unbound / settleSteps / energy, and — when the
|
|
440
|
+
* split export routes per question — adapter / gate).
|
|
340
441
|
*
|
|
341
442
|
* @param {{probs:number[][], qtypes:number[], labels:string[][],
|
|
342
443
|
* physics:{nearest:ArrayLike<number>, stepsUsed:ArrayLike<number>},
|
|
343
444
|
* rawLogits:ArrayLike<number>, rowWidth:number,
|
|
344
|
-
* calibration:object, alpha?:string, risk?:string
|
|
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
|
|
345
448
|
*/
|
|
346
449
|
export function vegaAnswers({
|
|
347
450
|
probs, qtypes, physics, rawLogits, rowWidth, calibration, questions, labels,
|
|
348
451
|
alpha = VEGA_DEFAULTS.alpha, risk = VEGA_DEFAULTS.risk,
|
|
452
|
+
calibrations = null, adapterNames = null, gates = null, routes = null,
|
|
349
453
|
}) {
|
|
350
454
|
const answers = [];
|
|
351
455
|
labels.forEach((optionLabels, r) => {
|
|
352
456
|
const p = probs[r];
|
|
353
457
|
const k = optionLabels.length;
|
|
354
458
|
const qtype = VEGA_QTYPE_NAMES[qtypes[r]];
|
|
459
|
+
const cal = calibrations?.[r] ?? calibration;
|
|
355
460
|
const conf = Math.max(...p);
|
|
356
461
|
const best = p.indexOf(Math.max(...p));
|
|
357
462
|
// LAC prediction set; the top option is always kept so it is never empty.
|
|
358
|
-
const th =
|
|
463
|
+
const th = cal?.conformal?.[qtype]?.[String(alpha)];
|
|
359
464
|
const predictionSet = th === undefined ? null
|
|
360
465
|
: optionLabels.filter((_, i) => i === best || p[i] >= Math.max(1 - th, 1e-12));
|
|
361
466
|
// energy = -max raw logit over the question's real options
|
|
@@ -365,11 +470,16 @@ export function vegaAnswers({
|
|
|
365
470
|
probabilities: Object.fromEntries(optionLabels.map((l, i) => [l, p[i]])),
|
|
366
471
|
confidence: conf,
|
|
367
472
|
predictionSet,
|
|
368
|
-
abstain: conf < (
|
|
369
|
-
unbound: physics.nearest[r] > (
|
|
473
|
+
abstain: conf < (cal?.abstain_threshold?.[String(risk)] ?? 0),
|
|
474
|
+
unbound: physics.nearest[r] > (cal?.unbound_threshold ?? Infinity),
|
|
370
475
|
settleSteps: physics.stepsUsed[r],
|
|
371
476
|
energy: -bestLogit,
|
|
372
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
|
+
}
|
|
373
483
|
if (qtype === "choice") {
|
|
374
484
|
a.type = "choice";
|
|
375
485
|
a.choice = optionLabels[best];
|
|
@@ -396,6 +506,100 @@ export function vegaOptionLabels(questions) {
|
|
|
396
506
|
});
|
|
397
507
|
}
|
|
398
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
|
+
|
|
399
603
|
// ---------------------------------------------------------------------------
|
|
400
604
|
// The decider
|
|
401
605
|
// ---------------------------------------------------------------------------
|
|
@@ -405,20 +609,87 @@ const now = () => (typeof performance !== "undefined" ? performance.now() : Date
|
|
|
405
609
|
const hfUrl = (model, revision, file) =>
|
|
406
610
|
`https://huggingface.co/${model}/resolve/${revision ? `${revision}/` : ""}${file}`;
|
|
407
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
|
+
|
|
408
642
|
/**
|
|
409
643
|
* Create a Vega decider.
|
|
410
644
|
*
|
|
411
645
|
* @param {object} [opts]
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
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;
|
|
416
657
|
* point it at the source checkpoint for your own export)
|
|
417
658
|
* maxLen — prompt window; null = checkpoint's max_length
|
|
418
|
-
* alpha / risk — conformal risk level and abstain risk
|
|
659
|
+
* alpha / risk — conformal risk level and abstain risk
|
|
660
|
+
* (default: the calibration block's default_risk)
|
|
419
661
|
* device, wasmPaths, sessionFactory, onProgress — as in the other families
|
|
420
662
|
*/
|
|
421
|
-
export async function createVegaDecider({
|
|
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({
|
|
422
693
|
model = VEGA_DEFAULT_MODEL,
|
|
423
694
|
revision = VEGA_DEFAULT_REVISION,
|
|
424
695
|
torsoFile = VEGA_TORSO_FILE,
|
|
@@ -434,7 +705,7 @@ export async function createVegaDecider({
|
|
|
434
705
|
onProgress = null,
|
|
435
706
|
maxLen = null,
|
|
436
707
|
alpha = VEGA_DEFAULTS.alpha,
|
|
437
|
-
risk =
|
|
708
|
+
risk = null,
|
|
438
709
|
scope = globalThis,
|
|
439
710
|
} = {}) {
|
|
440
711
|
const ortMod = ort ?? (await import("onnxruntime-web"));
|
|
@@ -463,6 +734,7 @@ export async function createVegaDecider({
|
|
|
463
734
|
} catch { /* fall through to the shipped defaults */ }
|
|
464
735
|
const cfg = { ...VEGA_DEFAULTS, ...(config ?? {}) };
|
|
465
736
|
const calibration = config?.calibration ?? {};
|
|
737
|
+
const effRisk = risk ?? calibration.default_risk ?? cfg.default_risk ?? VEGA_DEFAULTS.risk;
|
|
466
738
|
const cfgMaxLen = config?.max_len ?? cfg.maxLen;
|
|
467
739
|
const effMaxLen = Number.isFinite(maxLen) && maxLen > 0 ? maxLen : cfgMaxLen;
|
|
468
740
|
|
|
@@ -473,29 +745,12 @@ export async function createVegaDecider({
|
|
|
473
745
|
const encode = (text, addSpecial = false) =>
|
|
474
746
|
Array.from(tok(text, { add_special_tokens: addSpecial }).input_ids.data, Number);
|
|
475
747
|
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
const sessionOpts = external ? { ...opts, externalData: [external] } : opts;
|
|
483
|
-
return ortMod.InferenceSession.create(bytes, sessionOpts);
|
|
484
|
-
});
|
|
485
|
-
const makeSession = async (file) => {
|
|
486
|
-
try {
|
|
487
|
-
return await createSession(hfUrl(model, revision, file), {
|
|
488
|
-
executionProviders, graphOptimizationLevel: "all",
|
|
489
|
-
});
|
|
490
|
-
} catch (e) {
|
|
491
|
-
if (resolvedDevice !== "webgpu") throw e;
|
|
492
|
-
onProgress?.({ phase: `webgpu unavailable (${e?.message ?? e}) — retrying on wasm` });
|
|
493
|
-
return createSession(hfUrl(model, revision, file),
|
|
494
|
-
{ executionProviders: ["wasm"], graphOptimizationLevel: "all" });
|
|
495
|
-
}
|
|
496
|
-
};
|
|
497
|
-
const torso = await makeSession(torsoFile);
|
|
498
|
-
const engine = await makeSession(engineFile);
|
|
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));
|
|
499
754
|
|
|
500
755
|
const int64 = (arr, dims) => new ortMod.Tensor("int64", arr, dims);
|
|
501
756
|
const f32 = (arr, dims) => new ortMod.Tensor("float32", arr, dims);
|
|
@@ -504,12 +759,13 @@ export async function createVegaDecider({
|
|
|
504
759
|
return {
|
|
505
760
|
info: {
|
|
506
761
|
family: "vega",
|
|
762
|
+
format: "fused",
|
|
507
763
|
model,
|
|
508
764
|
revision,
|
|
509
765
|
device: resolvedDevice,
|
|
510
766
|
maxLen: effMaxLen,
|
|
511
767
|
alpha: String(alpha),
|
|
512
|
-
risk: String(
|
|
768
|
+
risk: String(effRisk),
|
|
513
769
|
calibrationSource: configSource,
|
|
514
770
|
nSteps: cfg.n_steps ?? cfg.nSteps,
|
|
515
771
|
config,
|
|
@@ -611,7 +867,7 @@ export async function createVegaDecider({
|
|
|
611
867
|
probs, qtypes: batch.qtypes, labels,
|
|
612
868
|
physics: { nearest, stepsUsed },
|
|
613
869
|
rawLogits: logits, rowWidth: batch.K,
|
|
614
|
-
calibration, alpha, risk,
|
|
870
|
+
calibration, alpha, risk: effRisk,
|
|
615
871
|
});
|
|
616
872
|
const decode = (ids) => {
|
|
617
873
|
try {
|
|
@@ -634,3 +890,236 @@ export async function createVegaDecider({
|
|
|
634
890
|
},
|
|
635
891
|
};
|
|
636
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
|
+
}
|