jev-web 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +625 -502
- package/package.json +60 -60
- package/src/answers.mjs +36 -36
- package/src/bekko.mjs +439 -439
- package/src/config.mjs +65 -65
- package/src/decision2.mjs +382 -382
- package/src/encode.mjs +83 -83
- package/src/index.mjs +137 -112
- package/src/kev.mjs +296 -296
- package/src/laya.mjs +630 -630
- package/src/questions.mjs +39 -39
- package/src/registry.mjs +169 -155
- package/src/session.mjs +151 -151
- package/src/strands.mjs +718 -535
- package/src/vega.mjs +636 -0
package/src/vega.mjs
ADDED
|
@@ -0,0 +1,636 @@
|
|
|
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 graphs (one fused torso -> pooled features, one engine -> logits +
|
|
28
|
+
* physics), mirroring the reference runtime exactly; JS owns the protocol, the
|
|
29
|
+
* packing, the thermal calibration and the answer decoding.
|
|
30
|
+
*/
|
|
31
|
+
import { normalizeQuestions } from "./questions.mjs";
|
|
32
|
+
import { softmaxWithTemperature } from "./answers.mjs";
|
|
33
|
+
import { fetchCachedBytes, fetchOnnxExternalData } from "./laya.mjs";
|
|
34
|
+
|
|
35
|
+
export const VEGA_DEFAULT_MODEL = "warsang/vega-08b-web";
|
|
36
|
+
export const VEGA_DEFAULT_REVISION = "d2bd4604ed770ca614745f07f623b66e89f406e4";
|
|
37
|
+
// The exact fp16 torso ships by default: q8 on this graph drifts on the
|
|
38
|
+
// GatedDeltaNet delta rule (export/vega/04_repo.py measures ~0.46 in the
|
|
39
|
+
// pooled features), so torso_quantized.onnx stays an opt-in experiment.
|
|
40
|
+
export const VEGA_TORSO_FILE = "onnx/torso_fp16.onnx";
|
|
41
|
+
export const VEGA_ENGINE_FILE = "onnx/engine.onnx";
|
|
42
|
+
|
|
43
|
+
/** Reference prompt template 0 (the inference surface form). */
|
|
44
|
+
const VEGA_HEAD = "Observation:\n";
|
|
45
|
+
const VEGA_QUESTION = "\n\nMeasurement ({t}): {ins}\nPossible outcomes:";
|
|
46
|
+
const VEGA_OPTION = "\n* {o}";
|
|
47
|
+
const VEGA_END = "\nOutcome:";
|
|
48
|
+
/** Prefixed to short states so the frozen model reads knowing what is asked. */
|
|
49
|
+
const VEGA_QUERY_FIRST = "Question to keep in mind while reading ({t}): {ins}\n\n";
|
|
50
|
+
|
|
51
|
+
/** jev-web question types -> Vega qtypes (choice=0, score=1, boolean=2). */
|
|
52
|
+
export const VEGA_QTYPE = { choice: 0, score: 1, boolean: 2 };
|
|
53
|
+
export const VEGA_QTYPE_NAMES = ["choice", "score", "boolean"];
|
|
54
|
+
|
|
55
|
+
export const VEGA_DEFAULTS = {
|
|
56
|
+
optMaxTokens: 32,
|
|
57
|
+
insMaxTokens: 256,
|
|
58
|
+
memoryChunks: 16,
|
|
59
|
+
memoryMinTokens: 128,
|
|
60
|
+
memoryChunkTokens: 128,
|
|
61
|
+
prefixMinTokens: 4096,
|
|
62
|
+
queryFirstMaxState: 4096,
|
|
63
|
+
maxLen: 73728,
|
|
64
|
+
nSteps: 12,
|
|
65
|
+
risk: "0.1",
|
|
66
|
+
alpha: "0.1",
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Protocol: question defs, option texts, prompt building
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Map a normalized jev-web question to Vega's internal question def.
|
|
75
|
+
* Vega's `boolean` is the noul primitive; its slots are fixed [no, yes].
|
|
76
|
+
*/
|
|
77
|
+
export function vegaQuestionDef(question) {
|
|
78
|
+
const ins = renderVegaText(question.instructions);
|
|
79
|
+
if (question.type === "noul") {
|
|
80
|
+
return { t: "boolean", ins, crit: { ...(question.criteria ?? {}) } };
|
|
81
|
+
}
|
|
82
|
+
if (question.type === "score") {
|
|
83
|
+
return { t: "score", ins, crit: [...question.options] };
|
|
84
|
+
}
|
|
85
|
+
// choice: criteria {label: description} when provided, else bare options.
|
|
86
|
+
const crit = question.criteria && typeof question.criteria === "object"
|
|
87
|
+
&& !Array.isArray(question.criteria)
|
|
88
|
+
? question.criteria
|
|
89
|
+
: Object.fromEntries(question.options.map((o) => [String(o), null]));
|
|
90
|
+
return { t: "choice", ins, crit };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function flatText(v) {
|
|
94
|
+
return v === null || v === undefined ? "" : String(v);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function renderVegaText(content) {
|
|
98
|
+
return typeof content === "string" ? content : JSON.stringify(content);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Option texts in label-index order, exactly as option_texts() renders them. */
|
|
102
|
+
export function vegaOptionTexts(qdef) {
|
|
103
|
+
if (qdef.t === "choice") {
|
|
104
|
+
return Object.entries(qdef.crit).map(([k, v]) => {
|
|
105
|
+
const d = v === null || v === undefined ? "" : flatText(v);
|
|
106
|
+
return d ? `${k} - ${d}` : String(k);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (qdef.t === "score") {
|
|
110
|
+
return qdef.crit.map((c, i) => `level ${i} - ${flatText(c)}`);
|
|
111
|
+
}
|
|
112
|
+
const crit = qdef.crit ?? {};
|
|
113
|
+
const no = crit.false === null || crit.false === undefined ? "" : flatText(crit.false);
|
|
114
|
+
const yes = crit.true === null || crit.true === undefined ? "" : flatText(crit.true);
|
|
115
|
+
return [
|
|
116
|
+
`no - ${no || "the statement does not hold"}`,
|
|
117
|
+
`yes - ${yes || "the statement holds"}`,
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* One prompt: BOS + [query-first prefix for short states] + head + state +
|
|
123
|
+
* question + options + end, with the token span of every segment.
|
|
124
|
+
*
|
|
125
|
+
* The state comes first so its span never sees the question (causal
|
|
126
|
+
* attention); the question, options and final token have read the whole
|
|
127
|
+
* state. Options are capped per option, and an over-long option list is
|
|
128
|
+
* shrunk evenly before the instructions are (mirrors build_prompt).
|
|
129
|
+
*
|
|
130
|
+
* @returns {{ids:number[], spans:{state:[number,number], question:[number,number],
|
|
131
|
+
* options:[number,number][], last:number}}}
|
|
132
|
+
*/
|
|
133
|
+
export function buildVegaPrompt({
|
|
134
|
+
encode,
|
|
135
|
+
stateText,
|
|
136
|
+
qdef,
|
|
137
|
+
maxLen,
|
|
138
|
+
bosId = null,
|
|
139
|
+
queryFirstMaxState = VEGA_DEFAULTS.queryFirstMaxState,
|
|
140
|
+
optMaxTokens = VEGA_DEFAULTS.optMaxTokens,
|
|
141
|
+
insMaxTokens = VEGA_DEFAULTS.insMaxTokens,
|
|
142
|
+
}) {
|
|
143
|
+
const options = vegaOptionTexts(qdef);
|
|
144
|
+
const enc = (s) => encode(s, false);
|
|
145
|
+
const bos = bosId === null ? [] : [bosId];
|
|
146
|
+
let head = enc(VEGA_HEAD);
|
|
147
|
+
let qids = enc(VEGA_QUESTION.replace("{t}", qdef.t).replace("{ins}", qdef.ins))
|
|
148
|
+
.slice(0, insMaxTokens);
|
|
149
|
+
let oids = options.map((o) => enc(VEGA_OPTION.replace("{o}", o)).slice(0, optMaxTokens));
|
|
150
|
+
const end = enc(VEGA_END);
|
|
151
|
+
let fixed = bos.length + head.length + qids.length
|
|
152
|
+
+ oids.reduce((n, o) => n + o.length, 0) + end.length;
|
|
153
|
+
if (fixed > maxLen - 16) {
|
|
154
|
+
const per = Math.max(3, Math.floor(
|
|
155
|
+
(maxLen - 16 - bos.length - head.length - end.length - Math.min(qids.length, 64))
|
|
156
|
+
/ Math.max(1, oids.length)));
|
|
157
|
+
oids = oids.map((o) => o.slice(0, per));
|
|
158
|
+
qids = qids.slice(0, Math.max(
|
|
159
|
+
8, maxLen - 16 - bos.length - head.length - end.length
|
|
160
|
+
- oids.reduce((n, o) => n + o.length, 0)));
|
|
161
|
+
fixed = bos.length + head.length + qids.length
|
|
162
|
+
+ oids.reduce((n, o) => n + o.length, 0) + end.length;
|
|
163
|
+
}
|
|
164
|
+
let st = enc(stateText);
|
|
165
|
+
let pre = [];
|
|
166
|
+
if (queryFirstMaxState && st.length < queryFirstMaxState) {
|
|
167
|
+
pre = enc(VEGA_QUERY_FIRST.replace("{t}", qdef.t).replace("{ins}", qdef.ins))
|
|
168
|
+
.slice(0, Math.min(128, insMaxTokens));
|
|
169
|
+
fixed += pre.length;
|
|
170
|
+
}
|
|
171
|
+
const room = Math.max(0, maxLen - fixed);
|
|
172
|
+
st = st.slice(0, room); // truncate_left = false: the head of the state is kept
|
|
173
|
+
const ids = [...bos, ...pre, ...head];
|
|
174
|
+
const s0 = ids.length;
|
|
175
|
+
ids.push(...st);
|
|
176
|
+
const spans = { state: st.length ? [s0, ids.length] : [bos.length, s0] };
|
|
177
|
+
const q0 = ids.length;
|
|
178
|
+
ids.push(...qids);
|
|
179
|
+
spans.question = [q0, ids.length];
|
|
180
|
+
spans.options = [];
|
|
181
|
+
for (const o of oids) {
|
|
182
|
+
const a = ids.length;
|
|
183
|
+
ids.push(...o);
|
|
184
|
+
spans.options.push([a, ids.length]);
|
|
185
|
+
}
|
|
186
|
+
ids.push(...end);
|
|
187
|
+
spans.last = ids.length - 1;
|
|
188
|
+
return { ids, spans };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Batch packing: padding, segment means (one-hot pooling), state chunks
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Pack rendered prompts into tensors + the pooling matrix the torso graph
|
|
197
|
+
* consumes: one row per segment (per item: state, question, options, the
|
|
198
|
+
* final token, then C state-chunk rows), each segment's row is 1/len over its
|
|
199
|
+
* token span, zeros elsewhere. The graph computes pool @ hidden, i.e. exact
|
|
200
|
+
* span means, so no hidden states ever cross into JS.
|
|
201
|
+
*
|
|
202
|
+
* @returns {{inputIds:BigInt64Array, attention:BigInt64Array,
|
|
203
|
+
* pool:Float32Array, poolDims:[number,number,number], n:number, L:number,
|
|
204
|
+
* K:number, qtypes:number[], optMask:Uint8Array, nseg:number}}
|
|
205
|
+
*/
|
|
206
|
+
export function packVegaBatch(items, { padId = 0, chunks: C = VEGA_DEFAULTS.memoryChunks } = {}) {
|
|
207
|
+
const n = items.length;
|
|
208
|
+
const L = Math.max(...items.map((it) => it.ids.length));
|
|
209
|
+
const K = Math.max(...items.map((it) => it.options.length));
|
|
210
|
+
const nseg = 3 + K;
|
|
211
|
+
const rows = nseg + C;
|
|
212
|
+
const inputIds = new BigInt64Array(n * L);
|
|
213
|
+
const attention = new BigInt64Array(n * L);
|
|
214
|
+
const pool = new Float32Array(n * rows * L); // [B, nseg + C, L]
|
|
215
|
+
const optMask = new Uint8Array(n * K);
|
|
216
|
+
const qtypes = [];
|
|
217
|
+
items.forEach((it, b) => {
|
|
218
|
+
it.ids.forEach((v, j) => {
|
|
219
|
+
inputIds[b * L + j] = BigInt(v);
|
|
220
|
+
attention[b * L + j] = 1n;
|
|
221
|
+
});
|
|
222
|
+
const setSpan = (row, [a, e]) => {
|
|
223
|
+
const len = Math.max(1, e - a);
|
|
224
|
+
for (let t = a; t < e; t++) pool[(b * rows + row) * L + t] = 1 / len;
|
|
225
|
+
};
|
|
226
|
+
setSpan(0, it.spans.state);
|
|
227
|
+
setSpan(1, it.spans.question);
|
|
228
|
+
it.spans.options.forEach((span, k) => {
|
|
229
|
+
if (k >= K) return;
|
|
230
|
+
setSpan(3 + k, span);
|
|
231
|
+
optMask[b * K + k] = 1;
|
|
232
|
+
});
|
|
233
|
+
pool[(b * rows + 2) * L + it.spans.last] = 1;
|
|
234
|
+
qtypes.push(it.qtype);
|
|
235
|
+
});
|
|
236
|
+
return {
|
|
237
|
+
inputIds, attention, pool, poolDims: [n, rows, L],
|
|
238
|
+
n, L, K, qtypes, optMask, nseg,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* State chunk means for the memory reader: the state span cut into k equal
|
|
244
|
+
* pieces (about one per memoryChunkTokens tokens, at least 2, at most C;
|
|
245
|
+
* states shorter than memoryMinTokens get none) — mirrors _chunk_pool.
|
|
246
|
+
*
|
|
247
|
+
* Fills rows [nseg, nseg + C) of the SAME pooling matrix the segment means
|
|
248
|
+
* use, so the torso graph emits chunk rows in the one forward it already
|
|
249
|
+
* does (a second run would re-read the whole record).
|
|
250
|
+
*
|
|
251
|
+
* @param {Float32Array} pool the [B, nseg + C, L] pooling matrix
|
|
252
|
+
* @param {object[]} items rendered prompts (with spans)
|
|
253
|
+
* @param {number} L padded sequence length
|
|
254
|
+
*/
|
|
255
|
+
export function packVegaChunks(pool, items, L, {
|
|
256
|
+
chunks: C = VEGA_DEFAULTS.memoryChunks,
|
|
257
|
+
chunkMinTokens = VEGA_DEFAULTS.memoryMinTokens,
|
|
258
|
+
chunkTokens = VEGA_DEFAULTS.memoryChunkTokens,
|
|
259
|
+
nseg = 3 + Math.max(...items.map((it) => it.options.length)),
|
|
260
|
+
} = {}) {
|
|
261
|
+
const n = items.length;
|
|
262
|
+
const chunkMask = new Uint8Array(n * C);
|
|
263
|
+
items.forEach((it, b) => {
|
|
264
|
+
const [s0, s1] = it.spans.state;
|
|
265
|
+
const len = s1 - s0;
|
|
266
|
+
if (len < Math.max(chunkMinTokens, 2)) return;
|
|
267
|
+
const k = Math.max(2, Math.min(C, Math.floor(len / Math.max(1, chunkTokens))));
|
|
268
|
+
for (let c = 0; c < k; c++) {
|
|
269
|
+
// token t of the state belongs to chunk floor(t * k / len) (the cid map)
|
|
270
|
+
const row = b * (nseg + C) + nseg + c;
|
|
271
|
+
let count = 0;
|
|
272
|
+
for (let t = s0; t < s1; t++) {
|
|
273
|
+
if (Math.floor(((t - s0) * k) / len) === c) { pool[row * L + t] = 1; count++; }
|
|
274
|
+
}
|
|
275
|
+
if (count > 0) {
|
|
276
|
+
for (let t = s0; t < s1; t++) {
|
|
277
|
+
if (pool[row * L + t]) pool[row * L + t] = 1 / count;
|
|
278
|
+
}
|
|
279
|
+
chunkMask[b * C + c] = 1;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
return chunkMask;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
// Thermal calibration + answer decoding (mirrors thermal_tau / decide)
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
/** Fallback when a checkpoint ships no calibration block (probabilities raw). */
|
|
291
|
+
export const VEGA_DEFAULT_THETA = [[0], [0], [0]];
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Per-type thermal calibration from the checkpoint's vega_config.json:
|
|
295
|
+
* log_tau = b + w . phys, tau in [0.2, 20]; probs = softmax(logits / tau).
|
|
296
|
+
*
|
|
297
|
+
* @param {{theta:number[][]}} calibration
|
|
298
|
+
* @param {{logits:ArrayLike<number>, rowWidth:number, qtypes:number[],
|
|
299
|
+
* phys:ArrayLike<number>, nOpts:number[]}} spec phys is [items, 4]
|
|
300
|
+
* @returns {number[][]} probabilities per item, in prompt order
|
|
301
|
+
*/
|
|
302
|
+
export function vegaThermalProbabilities({
|
|
303
|
+
logits, rowWidth, qtypes, phys, nOpts,
|
|
304
|
+
theta = VEGA_DEFAULT_THETA,
|
|
305
|
+
}) {
|
|
306
|
+
const rows = Number(logits.length / rowWidth);
|
|
307
|
+
const out = [];
|
|
308
|
+
for (let b = 0; b < rows; b++) {
|
|
309
|
+
const k = nOpts[b];
|
|
310
|
+
const t = theta[qtypes[b]] ?? theta[0];
|
|
311
|
+
let logTau = t[0];
|
|
312
|
+
for (let f = 0; f < 4; f++) logTau += t[1 + f] * phys[b * 4 + f];
|
|
313
|
+
const tau = Math.exp(Math.min(Math.log(20), Math.max(Math.log(0.2), logTau)));
|
|
314
|
+
out.push(softmaxWithTemperature(
|
|
315
|
+
Array.from(logits.slice(b * rowWidth, b * rowWidth + k)), tau));
|
|
316
|
+
}
|
|
317
|
+
return out;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Per-item thermal features: log1p(kinetic), log1p(nearest), steps/steps_max,
|
|
322
|
+
* log(k) — exactly thermal_features().
|
|
323
|
+
*/
|
|
324
|
+
export function vegaThermalFeatures({ kinetic, nearest, stepsUsed, nOpts, maxSteps }) {
|
|
325
|
+
const out = [];
|
|
326
|
+
for (let b = 0; b < nOpts.length; b++) {
|
|
327
|
+
out.push([
|
|
328
|
+
Math.log1p(kinetic[b]),
|
|
329
|
+
Math.log1p(nearest[b]),
|
|
330
|
+
stepsUsed[b] / Math.max(1, maxSteps),
|
|
331
|
+
Math.log(Math.max(2, nOpts[b])),
|
|
332
|
+
]);
|
|
333
|
+
}
|
|
334
|
+
return out;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Vega answers -> jev-web DecideResult answers (additive vega fields:
|
|
339
|
+
* predictionSet / abstain / unbound / settleSteps / energy).
|
|
340
|
+
*
|
|
341
|
+
* @param {{probs:number[][], qtypes:number[], labels:string[][],
|
|
342
|
+
* physics:{nearest:ArrayLike<number>, stepsUsed:ArrayLike<number>},
|
|
343
|
+
* rawLogits:ArrayLike<number>, rowWidth:number,
|
|
344
|
+
* calibration:object, alpha?:string, risk?:string}} spec
|
|
345
|
+
*/
|
|
346
|
+
export function vegaAnswers({
|
|
347
|
+
probs, qtypes, physics, rawLogits, rowWidth, calibration, questions, labels,
|
|
348
|
+
alpha = VEGA_DEFAULTS.alpha, risk = VEGA_DEFAULTS.risk,
|
|
349
|
+
}) {
|
|
350
|
+
const answers = [];
|
|
351
|
+
labels.forEach((optionLabels, r) => {
|
|
352
|
+
const p = probs[r];
|
|
353
|
+
const k = optionLabels.length;
|
|
354
|
+
const qtype = VEGA_QTYPE_NAMES[qtypes[r]];
|
|
355
|
+
const conf = Math.max(...p);
|
|
356
|
+
const best = p.indexOf(Math.max(...p));
|
|
357
|
+
// LAC prediction set; the top option is always kept so it is never empty.
|
|
358
|
+
const th = calibration?.conformal?.[qtype]?.[String(alpha)];
|
|
359
|
+
const predictionSet = th === undefined ? null
|
|
360
|
+
: optionLabels.filter((_, i) => i === best || p[i] >= Math.max(1 - th, 1e-12));
|
|
361
|
+
// energy = -max raw logit over the question's real options
|
|
362
|
+
let bestLogit = -Infinity;
|
|
363
|
+
for (let j = 0; j < k; j++) bestLogit = Math.max(bestLogit, rawLogits[r * rowWidth + j]);
|
|
364
|
+
const a = {
|
|
365
|
+
probabilities: Object.fromEntries(optionLabels.map((l, i) => [l, p[i]])),
|
|
366
|
+
confidence: conf,
|
|
367
|
+
predictionSet,
|
|
368
|
+
abstain: conf < (calibration?.abstain_threshold?.[String(risk)] ?? 0),
|
|
369
|
+
unbound: physics.nearest[r] > (calibration?.unbound_threshold ?? Infinity),
|
|
370
|
+
settleSteps: physics.stepsUsed[r],
|
|
371
|
+
energy: -bestLogit,
|
|
372
|
+
};
|
|
373
|
+
if (qtype === "choice") {
|
|
374
|
+
a.type = "choice";
|
|
375
|
+
a.choice = optionLabels[best];
|
|
376
|
+
a.index = best;
|
|
377
|
+
} else if (qtype === "score") {
|
|
378
|
+
a.type = "score";
|
|
379
|
+
a.level = best;
|
|
380
|
+
a.score = p.reduce((acc, pj, j) => acc + pj * j, 0);
|
|
381
|
+
} else {
|
|
382
|
+
a.type = "noul";
|
|
383
|
+
a.noul = p[1];
|
|
384
|
+
}
|
|
385
|
+
answers.push(a);
|
|
386
|
+
});
|
|
387
|
+
return answers;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Per-question option labels in prompt order (choice keys / score levels / [no, yes]). */
|
|
391
|
+
export function vegaOptionLabels(questions) {
|
|
392
|
+
return questions.map((q) => {
|
|
393
|
+
if (q.type === "noul") return [...q.options];
|
|
394
|
+
if (q.type === "score") return q.options.map((_, i) => i);
|
|
395
|
+
return [...q.options];
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
// The decider
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
|
|
403
|
+
const now = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
|
|
404
|
+
|
|
405
|
+
const hfUrl = (model, revision, file) =>
|
|
406
|
+
`https://huggingface.co/${model}/resolve/${revision ? `${revision}/` : ""}${file}`;
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Create a Vega decider.
|
|
410
|
+
*
|
|
411
|
+
* @param {object} [opts]
|
|
412
|
+
* model/revision — browser export repo (torso + engine graphs, tokenizer,
|
|
413
|
+
* vega_config.json with the calibration block)
|
|
414
|
+
* torsoFile/engineFile — graph paths within the repo
|
|
415
|
+
* calibrationModel — repo to read vega_config.json from (default: model;
|
|
416
|
+
* point it at the source checkpoint for your own export)
|
|
417
|
+
* maxLen — prompt window; null = checkpoint's max_length
|
|
418
|
+
* alpha / risk — conformal risk level and abstain risk ("0.1" default)
|
|
419
|
+
* device, wasmPaths, sessionFactory, onProgress — as in the other families
|
|
420
|
+
*/
|
|
421
|
+
export async function createVegaDecider({
|
|
422
|
+
model = VEGA_DEFAULT_MODEL,
|
|
423
|
+
revision = VEGA_DEFAULT_REVISION,
|
|
424
|
+
torsoFile = VEGA_TORSO_FILE,
|
|
425
|
+
engineFile = VEGA_ENGINE_FILE,
|
|
426
|
+
calibrationModel = null,
|
|
427
|
+
calibrationRevision = null,
|
|
428
|
+
ort = null,
|
|
429
|
+
transformers = null,
|
|
430
|
+
fetchImpl = fetch,
|
|
431
|
+
wasmPaths = null,
|
|
432
|
+
device = "auto",
|
|
433
|
+
sessionFactory = null,
|
|
434
|
+
onProgress = null,
|
|
435
|
+
maxLen = null,
|
|
436
|
+
alpha = VEGA_DEFAULTS.alpha,
|
|
437
|
+
risk = VEGA_DEFAULTS.risk,
|
|
438
|
+
scope = globalThis,
|
|
439
|
+
} = {}) {
|
|
440
|
+
const ortMod = ort ?? (await import("onnxruntime-web"));
|
|
441
|
+
const tf = transformers ?? (await import("@huggingface/transformers"));
|
|
442
|
+
if (wasmPaths) {
|
|
443
|
+
try { ortMod.env.wasm.wasmPaths = wasmPaths; } catch { /* older runtime */ }
|
|
444
|
+
}
|
|
445
|
+
const resolvedDevice = device !== "auto"
|
|
446
|
+
? device
|
|
447
|
+
: (scope?.navigator?.gpu ? "webgpu" : "wasm");
|
|
448
|
+
const executionProviders = resolvedDevice === "webgpu" ? ["webgpu", "wasm"] : ["wasm"];
|
|
449
|
+
|
|
450
|
+
// Calibration + architecture constants come from the checkpoint's
|
|
451
|
+
// vega_config.json (thermal theta, conformal thresholds, chunking, window).
|
|
452
|
+
const configUrl = hfUrl(calibrationModel ?? model,
|
|
453
|
+
calibrationRevision ?? (calibrationModel ? "main" : revision),
|
|
454
|
+
"vega_config.json");
|
|
455
|
+
let config = null;
|
|
456
|
+
let configSource = null;
|
|
457
|
+
try {
|
|
458
|
+
const res = await fetchImpl(configUrl);
|
|
459
|
+
if (res?.ok) {
|
|
460
|
+
config = await res.json();
|
|
461
|
+
configSource = configUrl;
|
|
462
|
+
}
|
|
463
|
+
} catch { /* fall through to the shipped defaults */ }
|
|
464
|
+
const cfg = { ...VEGA_DEFAULTS, ...(config ?? {}) };
|
|
465
|
+
const calibration = config?.calibration ?? {};
|
|
466
|
+
const cfgMaxLen = config?.max_len ?? cfg.maxLen;
|
|
467
|
+
const effMaxLen = Number.isFinite(maxLen) && maxLen > 0 ? maxLen : cfgMaxLen;
|
|
468
|
+
|
|
469
|
+
const tok = await tf.AutoTokenizer.from_pretrained(calibrationModel ?? model, {
|
|
470
|
+
revision: calibrationRevision ?? (calibrationModel ? "main" : revision),
|
|
471
|
+
progress_callback: () => {},
|
|
472
|
+
});
|
|
473
|
+
const encode = (text, addSpecial = false) =>
|
|
474
|
+
Array.from(tok(text, { add_special_tokens: addSpecial }).input_ids.data, Number);
|
|
475
|
+
|
|
476
|
+
const createSession = sessionFactory
|
|
477
|
+
?? (async (u, opts) => {
|
|
478
|
+
const bytes = await fetchCachedBytes(u, {
|
|
479
|
+
fetchImpl, onProgress: (p) => onProgress?.(p), persist: true,
|
|
480
|
+
});
|
|
481
|
+
const external = await fetchOnnxExternalData(u, { fetchImpl });
|
|
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);
|
|
499
|
+
|
|
500
|
+
const int64 = (arr, dims) => new ortMod.Tensor("int64", arr, dims);
|
|
501
|
+
const f32 = (arr, dims) => new ortMod.Tensor("float32", arr, dims);
|
|
502
|
+
const bool = (arr, dims) => new ortMod.Tensor("bool", arr, dims);
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
info: {
|
|
506
|
+
family: "vega",
|
|
507
|
+
model,
|
|
508
|
+
revision,
|
|
509
|
+
device: resolvedDevice,
|
|
510
|
+
maxLen: effMaxLen,
|
|
511
|
+
alpha: String(alpha),
|
|
512
|
+
risk: String(risk ?? cfg.default_risk ?? VEGA_DEFAULTS.risk),
|
|
513
|
+
calibrationSource: configSource,
|
|
514
|
+
nSteps: cfg.n_steps ?? cfg.nSteps,
|
|
515
|
+
config,
|
|
516
|
+
},
|
|
517
|
+
async decide(state, questions) {
|
|
518
|
+
const t0 = now();
|
|
519
|
+
const normalized = normalizeQuestions(questions);
|
|
520
|
+
const qdefs = normalized.map(vegaQuestionDef);
|
|
521
|
+
const stateText = renderVegaText(state);
|
|
522
|
+
const bosId = tok.bos_token_id ?? null;
|
|
523
|
+
|
|
524
|
+
const items = qdefs.map((qdef) => {
|
|
525
|
+
const { ids, spans } = buildVegaPrompt({
|
|
526
|
+
encode, stateText, qdef, maxLen: effMaxLen,
|
|
527
|
+
bosId,
|
|
528
|
+
queryFirstMaxState: cfg.query_first_max_state ?? cfg.queryFirstMaxState,
|
|
529
|
+
optMaxTokens: cfg.opt_max_tokens ?? cfg.optMaxTokens,
|
|
530
|
+
insMaxTokens: 256,
|
|
531
|
+
});
|
|
532
|
+
return { ids, spans, qtype: VEGA_QTYPE[qdef.t], options: vegaOptionTexts(qdef) };
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
const batch = packVegaBatch(items, {
|
|
536
|
+
chunks: cfg.memory_chunks ?? cfg.memoryChunks,
|
|
537
|
+
});
|
|
538
|
+
const chunkMask = packVegaChunks(batch.pool, items, batch.L, {
|
|
539
|
+
chunks: cfg.memory_chunks ?? cfg.memoryChunks,
|
|
540
|
+
chunkMinTokens: cfg.memory_min_tokens ?? cfg.memoryMinTokens,
|
|
541
|
+
chunkTokens: cfg.memory_chunk_tokens ?? cfg.memoryChunkTokens,
|
|
542
|
+
nseg: batch.nseg,
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
const tRun = now();
|
|
546
|
+
// Torso: one causal read to the deepest captured layer, all span means
|
|
547
|
+
// (segments + state chunks) pooled in-graph from the one forward.
|
|
548
|
+
const torsoOut = await torso.run({
|
|
549
|
+
input_ids: int64(batch.inputIds, [batch.n, batch.L]),
|
|
550
|
+
attention_mask: int64(batch.attention, [batch.n, batch.L]),
|
|
551
|
+
pool: f32(batch.pool, batch.poolDims),
|
|
552
|
+
});
|
|
553
|
+
const pooled = torsoOut.pooled ?? torsoOut[Object.keys(torsoOut)[0]];
|
|
554
|
+
// pooled: [B, nLayers, nseg + C, d] — nLayers = feature_layers,
|
|
555
|
+
// rows 0..2 = state/question/last, 3..3+K = options, then C chunk rows.
|
|
556
|
+
const pd = pooled.dims;
|
|
557
|
+
const d = pd[3];
|
|
558
|
+
const Lc = pd[1];
|
|
559
|
+
const rowsTotal = pd[2];
|
|
560
|
+
const C = rowsTotal - batch.nseg;
|
|
561
|
+
const pv = pooled.data;
|
|
562
|
+
const feats = new Float32Array(batch.n * Lc * 3 * d);
|
|
563
|
+
const opts = new Float32Array(batch.n * batch.K * d);
|
|
564
|
+
const chunks = new Float32Array(batch.n * C * d);
|
|
565
|
+
for (let b = 0; b < batch.n; b++) {
|
|
566
|
+
for (let l = 0; l < Lc; l++) {
|
|
567
|
+
for (let s = 0; s < 3; s++) {
|
|
568
|
+
const row = s; // pooling rows 0/1/2 are state/question/last
|
|
569
|
+
const src = ((b * Lc + l) * rowsTotal + row) * d;
|
|
570
|
+
feats.set(pv.subarray(src, src + d), ((b * Lc + l) * 3 + s) * d);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
// options from the deepest captured layer only (option_layers)
|
|
574
|
+
for (let k = 0; k < batch.K; k++) {
|
|
575
|
+
const src = ((b * Lc + Lc - 1) * rowsTotal + 3 + k) * d;
|
|
576
|
+
opts.set(pv.subarray(src, src + d), (b * batch.K + k) * d);
|
|
577
|
+
}
|
|
578
|
+
// chunk means: deepest feature layer, rows nseg..nseg + C
|
|
579
|
+
for (let c = 0; c < C; c++) {
|
|
580
|
+
const src = ((b * Lc + Lc - 1) * rowsTotal + batch.nseg + c) * d;
|
|
581
|
+
chunks.set(pv.subarray(src, src + d), (b * C + c) * d);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Engine: landscape + integrator -> raw logits and the physics state.
|
|
586
|
+
const engineOut = await engine.run({
|
|
587
|
+
feats: f32(feats, [batch.n, Lc, 3, d]),
|
|
588
|
+
opts: f32(opts, [batch.n, batch.K, d]),
|
|
589
|
+
opt_mask: bool(batch.optMask, [batch.n, batch.K]),
|
|
590
|
+
qtype: int64(batch.qtypes, [batch.n]),
|
|
591
|
+
chunks: f32(chunks, [batch.n, C, d]),
|
|
592
|
+
chunk_mask: bool(chunkMask, [batch.n, C]),
|
|
593
|
+
});
|
|
594
|
+
const logits = engineOut.logits.data;
|
|
595
|
+
const kinetic = engineOut.kinetic.data;
|
|
596
|
+
const nearest = engineOut.nearest.data;
|
|
597
|
+
const stepsUsed = Array.from(engineOut.steps_used.data, Number);
|
|
598
|
+
|
|
599
|
+
const nOpts = items.map((it) => it.options.length);
|
|
600
|
+
const phys = vegaThermalFeatures({
|
|
601
|
+
kinetic, nearest, stepsUsed, nOpts, maxSteps: cfg.n_steps ?? cfg.nSteps,
|
|
602
|
+
});
|
|
603
|
+
const probs = vegaThermalProbabilities({
|
|
604
|
+
logits, rowWidth: batch.K, qtypes: batch.qtypes,
|
|
605
|
+
phys: phys.flat(), nOpts,
|
|
606
|
+
theta: calibration.theta ?? VEGA_DEFAULT_THETA,
|
|
607
|
+
});
|
|
608
|
+
const runMs = now() - tRun;
|
|
609
|
+
const labels = vegaOptionLabels(normalized);
|
|
610
|
+
const answers = vegaAnswers({
|
|
611
|
+
probs, qtypes: batch.qtypes, labels,
|
|
612
|
+
physics: { nearest, stepsUsed },
|
|
613
|
+
rawLogits: logits, rowWidth: batch.K,
|
|
614
|
+
calibration, alpha, risk,
|
|
615
|
+
});
|
|
616
|
+
const decode = (ids) => {
|
|
617
|
+
try {
|
|
618
|
+
return tok.decode(Array.from(ids), { skip_special_tokens: false });
|
|
619
|
+
} catch {
|
|
620
|
+
return Array.from(ids).join(" ");
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
return {
|
|
624
|
+
answers,
|
|
625
|
+
truncated: false,
|
|
626
|
+
length: Number(batch.attention.reduce((a, v) => a + v, 0n)),
|
|
627
|
+
prompts: items.map((it) => decode(it.ids)),
|
|
628
|
+
timings: { totalMs: Math.round(now() - t0), runMs: Math.round(runMs) },
|
|
629
|
+
};
|
|
630
|
+
},
|
|
631
|
+
async dispose() {
|
|
632
|
+
try { await torso.release?.(); } catch { /* optional */ }
|
|
633
|
+
try { await engine.release?.(); } catch { /* optional */ }
|
|
634
|
+
},
|
|
635
|
+
};
|
|
636
|
+
}
|