edgeinfer 0.0.0-stage → 2.0.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 -0
- package/README.md +363 -2
- package/dist/index.d.mts +590 -0
- package/dist/index.d.ts +590 -0
- package/dist/index.js +1827 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1769 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +62 -4
- package/src/download.ts +39 -0
- package/src/edgeinfer.ts +710 -0
- package/src/events.ts +17 -0
- package/src/image-processor.ts +35 -0
- package/src/index.ts +15 -0
- package/src/matryoshka.ts +441 -0
- package/src/model-cache.ts +46 -0
- package/src/onnx-lm.ts +104 -0
- package/src/runtime-manager.ts +338 -0
- package/src/speculative.ts +294 -0
- package/src/tokenizer.ts +419 -0
- package/src/types.ts +19 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1769 @@
|
|
|
1
|
+
// src/events.ts
|
|
2
|
+
var EventEmitter = class {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.listeners = {};
|
|
5
|
+
}
|
|
6
|
+
on(event, callback) {
|
|
7
|
+
if (!this.listeners[event]) this.listeners[event] = [];
|
|
8
|
+
this.listeners[event].push(callback);
|
|
9
|
+
}
|
|
10
|
+
emit(event, ...args) {
|
|
11
|
+
if (this.listeners[event]) {
|
|
12
|
+
this.listeners[event].forEach((cb) => cb(...args));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/download.ts
|
|
18
|
+
function checkDownloadAbort(signal) {
|
|
19
|
+
if (signal?.aborted) throw signal.reason ?? new DOMException("Download aborted", "AbortError");
|
|
20
|
+
}
|
|
21
|
+
async function withDownloadDeadline(operation, options = {}) {
|
|
22
|
+
const timeout = options.downloadTimeoutMs ?? 12e4;
|
|
23
|
+
if (!Number.isFinite(timeout) || timeout <= 0 || timeout > 2147483647) {
|
|
24
|
+
throw new RangeError("downloadTimeoutMs must be between 0 and 2147483647 milliseconds");
|
|
25
|
+
}
|
|
26
|
+
const controller = new AbortController();
|
|
27
|
+
const abort = () => controller.abort(options.signal?.reason);
|
|
28
|
+
if (options.signal?.aborted) abort();
|
|
29
|
+
else options.signal?.addEventListener("abort", abort, { once: true });
|
|
30
|
+
const timer = setTimeout(() => controller.abort(new DOMException("Download deadline exceeded", "TimeoutError")), timeout);
|
|
31
|
+
let onAbort = () => {
|
|
32
|
+
};
|
|
33
|
+
try {
|
|
34
|
+
checkDownloadAbort(controller.signal);
|
|
35
|
+
const cancelled = new Promise((_, reject) => {
|
|
36
|
+
onAbort = () => reject(controller.signal.reason ?? new DOMException("Download aborted", "AbortError"));
|
|
37
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
38
|
+
});
|
|
39
|
+
return await Promise.race([operation(controller.signal), cancelled]);
|
|
40
|
+
} finally {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
controller.signal.removeEventListener("abort", onAbort);
|
|
43
|
+
options.signal?.removeEventListener("abort", abort);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/model-cache.ts
|
|
48
|
+
var ModelCache = class extends EventEmitter {
|
|
49
|
+
constructor(cacheName = "edgeinfer-models") {
|
|
50
|
+
super();
|
|
51
|
+
this.cacheName = cacheName;
|
|
52
|
+
}
|
|
53
|
+
async getModel(url, forceDownload = false, options = {}) {
|
|
54
|
+
return withDownloadDeadline(async (signal) => {
|
|
55
|
+
const cache = typeof caches === "undefined" ? void 0 : await caches.open(this.cacheName);
|
|
56
|
+
checkDownloadAbort(signal);
|
|
57
|
+
const cached = !forceDownload && cache ? await cache.match(url) : void 0;
|
|
58
|
+
checkDownloadAbort(signal);
|
|
59
|
+
if (cached) {
|
|
60
|
+
const buffer2 = await cached.arrayBuffer();
|
|
61
|
+
checkDownloadAbort(signal);
|
|
62
|
+
return buffer2;
|
|
63
|
+
}
|
|
64
|
+
this.emit("downloadStart", url);
|
|
65
|
+
checkDownloadAbort(signal);
|
|
66
|
+
const response = await fetch(url, { signal });
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
await response.body?.cancel();
|
|
69
|
+
throw new Error(`EdgeInfer: failed to fetch model from ${url} - HTTP ${response.status} ${response.statusText}`);
|
|
70
|
+
}
|
|
71
|
+
if (response.status === 206 || response.headers.has("content-range")) {
|
|
72
|
+
await response.body?.cancel();
|
|
73
|
+
throw new Error("EdgeInfer: partial model responses are not supported");
|
|
74
|
+
}
|
|
75
|
+
const buffer = await response.arrayBuffer();
|
|
76
|
+
checkDownloadAbort(signal);
|
|
77
|
+
if (cache) await cache.put(url, new Response(buffer, { headers: response.headers }));
|
|
78
|
+
checkDownloadAbort(signal);
|
|
79
|
+
this.emit("downloadComplete", url);
|
|
80
|
+
return buffer;
|
|
81
|
+
}, options);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// src/runtime-manager.ts
|
|
86
|
+
import * as ort from "onnxruntime-web";
|
|
87
|
+
var ADAPTER_TIMEOUT_MS = 5e3;
|
|
88
|
+
var _RuntimeManager = class _RuntimeManager {
|
|
89
|
+
/**
|
|
90
|
+
* Detect GPU capabilities and recommend the best execution configuration.
|
|
91
|
+
*/
|
|
92
|
+
static async detectCapabilities() {
|
|
93
|
+
if (this.cachedCapabilities) return this.cachedCapabilities;
|
|
94
|
+
const capabilities = {
|
|
95
|
+
provider: "wasm",
|
|
96
|
+
hasWebGPU: false,
|
|
97
|
+
hasWebNN: false,
|
|
98
|
+
estimatedVRAM: 0,
|
|
99
|
+
adapterLimits: null,
|
|
100
|
+
vramProbe: null,
|
|
101
|
+
recommendedQuantization: "int8"
|
|
102
|
+
};
|
|
103
|
+
if (typeof navigator !== "undefined" && "gpu" in navigator) {
|
|
104
|
+
try {
|
|
105
|
+
const gpu = navigator.gpu;
|
|
106
|
+
const adapter = await _RuntimeManager.withTimeout(
|
|
107
|
+
Promise.resolve(gpu.requestAdapter()),
|
|
108
|
+
ADAPTER_TIMEOUT_MS
|
|
109
|
+
);
|
|
110
|
+
if (adapter) {
|
|
111
|
+
capabilities.hasWebGPU = true;
|
|
112
|
+
capabilities.provider = "webgpu";
|
|
113
|
+
const maxBufferSize = adapter.limits?.maxBufferSize || 0;
|
|
114
|
+
const maxStorageSize = adapter.limits?.maxStorageBufferBindingSize || 0;
|
|
115
|
+
capabilities.adapterLimits = {
|
|
116
|
+
maxBufferSize,
|
|
117
|
+
maxStorageBufferBindingSize: maxStorageSize
|
|
118
|
+
};
|
|
119
|
+
const probe = await _RuntimeManager.probeVRAM(adapter);
|
|
120
|
+
capabilities.vramProbe = probe;
|
|
121
|
+
capabilities.estimatedVRAM = probe.available ? probe.maxAllocationBytes : Math.max(maxBufferSize, maxStorageSize);
|
|
122
|
+
const vramGB = capabilities.estimatedVRAM / (1024 * 1024 * 1024);
|
|
123
|
+
if (vramGB >= 4) {
|
|
124
|
+
capabilities.recommendedQuantization = "fp16";
|
|
125
|
+
} else if (vramGB >= 1) {
|
|
126
|
+
capabilities.recommendedQuantization = "int8";
|
|
127
|
+
} else {
|
|
128
|
+
capabilities.recommendedQuantization = "int4";
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (typeof navigator !== "undefined" && "ml" in navigator) {
|
|
135
|
+
try {
|
|
136
|
+
const ml = navigator.ml;
|
|
137
|
+
if (ml) {
|
|
138
|
+
capabilities.hasWebNN = true;
|
|
139
|
+
if (!capabilities.hasWebGPU) {
|
|
140
|
+
capabilities.provider = "webnn";
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
this.cachedCapabilities = capabilities;
|
|
147
|
+
return capabilities;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Get the best available execution provider with smart fallback.
|
|
151
|
+
* Priority: WebGPU → WebNN → WASM
|
|
152
|
+
*/
|
|
153
|
+
static async getBestExecutionProvider() {
|
|
154
|
+
const caps = await this.detectCapabilities();
|
|
155
|
+
return caps.provider;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Create an ONNX inference session with automatic provider selection.
|
|
159
|
+
* Falls back gracefully if the preferred provider fails.
|
|
160
|
+
*
|
|
161
|
+
* Returns BOTH the session and the provider that actually served it. The
|
|
162
|
+
* detected capability (`detectCapabilities().provider`) is only a preference:
|
|
163
|
+
* a provider can be detected as available and still fail to create a session
|
|
164
|
+
* (for example onnxruntime-web only registers the WebGPU execution provider
|
|
165
|
+
* in some builds/versions). Callers must report the returned `provider`, not
|
|
166
|
+
* the detected one, or `EdgeInfer.executionProvider` will lie.
|
|
167
|
+
*/
|
|
168
|
+
static async createSession(modelBuffer, providers) {
|
|
169
|
+
const requestedProviders = providers || [await this.getBestExecutionProvider()];
|
|
170
|
+
for (const provider of requestedProviders) {
|
|
171
|
+
try {
|
|
172
|
+
const session2 = await ort.InferenceSession.create(
|
|
173
|
+
modelBuffer,
|
|
174
|
+
{
|
|
175
|
+
executionProviders: [provider],
|
|
176
|
+
graphOptimizationLevel: "all"
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
return { session: session2, provider };
|
|
180
|
+
} catch (err) {
|
|
181
|
+
console.warn(`EdgeInfer: Failed to create session with provider "${provider}", trying next...`, err);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
console.warn("EdgeInfer: All preferred providers failed, falling back to WASM");
|
|
185
|
+
const session = await ort.InferenceSession.create(modelBuffer, {
|
|
186
|
+
executionProviders: ["wasm"],
|
|
187
|
+
graphOptimizationLevel: "all"
|
|
188
|
+
});
|
|
189
|
+
return { session, provider: "wasm" };
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Empirically probe the largest single storage buffer the adapter's device
|
|
193
|
+
* will allocate. Allocates and destroys one buffer at a time; never relies on
|
|
194
|
+
* `maxBufferSize` as if it were physical VRAM.
|
|
195
|
+
*/
|
|
196
|
+
static async probeVRAM(adapter, options = {}) {
|
|
197
|
+
const limits = {
|
|
198
|
+
maxBufferSize: Number(adapter?.limits?.maxBufferSize) || 0,
|
|
199
|
+
maxStorageBufferBindingSize: Number(adapter?.limits?.maxStorageBufferBindingSize) || 0
|
|
200
|
+
};
|
|
201
|
+
const unavailable = {
|
|
202
|
+
available: false,
|
|
203
|
+
maxAllocationBytes: 0,
|
|
204
|
+
testedAllocations: [],
|
|
205
|
+
failedAllocationBytes: null,
|
|
206
|
+
adapterLimits: limits,
|
|
207
|
+
deviceCreated: false,
|
|
208
|
+
probe: "unavailable"
|
|
209
|
+
};
|
|
210
|
+
if (!adapter || typeof adapter.requestDevice !== "function") return unavailable;
|
|
211
|
+
let device;
|
|
212
|
+
try {
|
|
213
|
+
device = await adapter.requestDevice();
|
|
214
|
+
} catch {
|
|
215
|
+
return unavailable;
|
|
216
|
+
}
|
|
217
|
+
if (!device || typeof device.createBuffer !== "function") {
|
|
218
|
+
return { ...unavailable, deviceCreated: Boolean(device) };
|
|
219
|
+
}
|
|
220
|
+
const MIB = 1024 * 1024;
|
|
221
|
+
const start = Math.max(1, options.startBytes ?? 64 * MIB);
|
|
222
|
+
const configuredMax = Math.max(start, options.maxBytes ?? 4 * 1024 * MIB);
|
|
223
|
+
const max = limits.maxBufferSize > 0 ? Math.min(configuredMax, limits.maxBufferSize) : configuredMax;
|
|
224
|
+
const step = Math.max(1, options.stepBytes ?? 32 * MIB);
|
|
225
|
+
const tested = [];
|
|
226
|
+
const STORAGE = 128;
|
|
227
|
+
const COPY_DST = 8;
|
|
228
|
+
const attempt = (size2) => {
|
|
229
|
+
tested.push(size2);
|
|
230
|
+
try {
|
|
231
|
+
const buffer = device.createBuffer({ size: size2, usage: STORAGE | COPY_DST });
|
|
232
|
+
buffer?.destroy?.();
|
|
233
|
+
return true;
|
|
234
|
+
} catch {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
let low = 0;
|
|
239
|
+
let high = 0;
|
|
240
|
+
let size = Math.min(start, max);
|
|
241
|
+
while (size >= 1) {
|
|
242
|
+
if (attempt(size)) {
|
|
243
|
+
low = size;
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
high = size;
|
|
247
|
+
if (size === 1) break;
|
|
248
|
+
size = Math.max(1, Math.floor(size / 2));
|
|
249
|
+
}
|
|
250
|
+
if (low === 0) {
|
|
251
|
+
device.destroy?.();
|
|
252
|
+
return {
|
|
253
|
+
...unavailable,
|
|
254
|
+
testedAllocations: tested,
|
|
255
|
+
failedAllocationBytes: high || null,
|
|
256
|
+
deviceCreated: true,
|
|
257
|
+
probe: "limits"
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
let next = Math.min(max, low * 2);
|
|
261
|
+
while (next > low && tested.length < 16) {
|
|
262
|
+
if (attempt(next)) {
|
|
263
|
+
low = next;
|
|
264
|
+
next = Math.min(max, low * 2);
|
|
265
|
+
if (low === max) break;
|
|
266
|
+
} else {
|
|
267
|
+
high = next;
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (high === 0) high = Math.min(max, low * 2);
|
|
272
|
+
while (high - low > step && tested.length < 24) {
|
|
273
|
+
const mid = Math.floor((low + high) / 2);
|
|
274
|
+
if (mid <= low || mid >= high) break;
|
|
275
|
+
if (attempt(mid)) low = mid;
|
|
276
|
+
else high = mid;
|
|
277
|
+
}
|
|
278
|
+
device.destroy?.();
|
|
279
|
+
return {
|
|
280
|
+
available: true,
|
|
281
|
+
maxAllocationBytes: low,
|
|
282
|
+
testedAllocations: tested,
|
|
283
|
+
failedAllocationBytes: high > low ? high : null,
|
|
284
|
+
adapterLimits: limits,
|
|
285
|
+
deviceCreated: true,
|
|
286
|
+
probe: "empirical"
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Resolve `promise`, or resolve to `null` if it has not settled within `ms`.
|
|
291
|
+
* Used to stop a hung GPU driver from blocking model loading forever.
|
|
292
|
+
*/
|
|
293
|
+
static withTimeout(promise, ms) {
|
|
294
|
+
let timer;
|
|
295
|
+
return Promise.race([
|
|
296
|
+
promise.then((v) => {
|
|
297
|
+
clearTimeout(timer);
|
|
298
|
+
return v;
|
|
299
|
+
}),
|
|
300
|
+
new Promise((resolve) => {
|
|
301
|
+
timer = setTimeout(() => {
|
|
302
|
+
console.warn(`EdgeInfer: navigator.gpu.requestAdapter() did not settle within ${ms}ms; falling back to WASM.`);
|
|
303
|
+
resolve(null);
|
|
304
|
+
}, ms);
|
|
305
|
+
if (typeof timer?.unref === "function") timer.unref();
|
|
306
|
+
})
|
|
307
|
+
]);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Estimate if a model of given size (bytes) can fit in available VRAM.
|
|
311
|
+
*/
|
|
312
|
+
static async canFitModel(modelSizeBytes) {
|
|
313
|
+
const caps = await this.detectCapabilities();
|
|
314
|
+
if (!caps.hasWebGPU) return true;
|
|
315
|
+
return caps.estimatedVRAM >= modelSizeBytes * 1.5;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Reset cached capabilities (useful for testing or re-detection).
|
|
319
|
+
*/
|
|
320
|
+
static resetCache() {
|
|
321
|
+
this.cachedCapabilities = null;
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
_RuntimeManager.cachedCapabilities = null;
|
|
325
|
+
var RuntimeManager = _RuntimeManager;
|
|
326
|
+
|
|
327
|
+
// src/tokenizer.ts
|
|
328
|
+
var Tokenizer = class _Tokenizer {
|
|
329
|
+
constructor(config) {
|
|
330
|
+
this.vocab = new Map(Object.entries(config.vocab));
|
|
331
|
+
this.inverseVocab = /* @__PURE__ */ new Map();
|
|
332
|
+
for (const [token, id] of this.vocab) {
|
|
333
|
+
this.inverseVocab.set(id, token);
|
|
334
|
+
}
|
|
335
|
+
this.merges = (config.merges || []).map((m) => {
|
|
336
|
+
if (Array.isArray(m)) {
|
|
337
|
+
return [m[0], m[1]];
|
|
338
|
+
}
|
|
339
|
+
const parts = String(m).split(" ");
|
|
340
|
+
return [parts[0], parts[1]];
|
|
341
|
+
});
|
|
342
|
+
this.specialTokens = new Map(Object.entries(config.specialTokens || {}));
|
|
343
|
+
this.maxLength = config.maxLength || 512;
|
|
344
|
+
this.padTokenId = config.padTokenId ?? 0;
|
|
345
|
+
this.unkTokenId = config.unkTokenId ?? 100;
|
|
346
|
+
this.clsTokenId = config.clsTokenId ?? 101;
|
|
347
|
+
this.sepTokenId = config.sepTokenId ?? 102;
|
|
348
|
+
this.bpeCache = /* @__PURE__ */ new Map();
|
|
349
|
+
this.usesBPE = this.merges.length > 0;
|
|
350
|
+
this.bpeSpaceMarker = config.bpeSpaceMarker ?? "";
|
|
351
|
+
this.mergeRanks = /* @__PURE__ */ new Map();
|
|
352
|
+
this.merges.forEach(([first, second], rank) => {
|
|
353
|
+
this.mergeRanks.set(`${first} ${second}`, rank);
|
|
354
|
+
});
|
|
355
|
+
this.lowercase = config.lowercase ?? !this.usesBPE;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Load tokenizer configuration from a JSON URL (tokenizer.json or vocab.json).
|
|
359
|
+
*/
|
|
360
|
+
static async fromUrl(url, options = {}) {
|
|
361
|
+
const config = await withDownloadDeadline(async (signal) => {
|
|
362
|
+
const response = await fetch(url, { signal });
|
|
363
|
+
if (!response.ok) {
|
|
364
|
+
await response.body?.cancel();
|
|
365
|
+
throw new Error(`Failed to load tokenizer from ${url}: ${response.status}`);
|
|
366
|
+
}
|
|
367
|
+
return response.json();
|
|
368
|
+
}, options);
|
|
369
|
+
if (config.model && config.model.vocab) {
|
|
370
|
+
const specialTokens = {};
|
|
371
|
+
if (config.added_tokens) {
|
|
372
|
+
for (const t of config.added_tokens) {
|
|
373
|
+
specialTokens[t.content] = t.id;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return new _Tokenizer({
|
|
377
|
+
vocab: config.model.vocab,
|
|
378
|
+
lowercase: config.normalizer === void 0 ? void 0 : _Tokenizer.normalizerLowercase(config.normalizer),
|
|
379
|
+
merges: config.model.merges || [],
|
|
380
|
+
specialTokens,
|
|
381
|
+
maxLength: config.truncation?.max_length || 512,
|
|
382
|
+
padTokenId: specialTokens["[PAD]"] ?? 0,
|
|
383
|
+
unkTokenId: specialTokens["[UNK]"] ?? 100,
|
|
384
|
+
clsTokenId: specialTokens["[CLS]"] ?? 101,
|
|
385
|
+
sepTokenId: specialTokens["[SEP]"] ?? 102
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
return new _Tokenizer({ vocab: config });
|
|
389
|
+
}
|
|
390
|
+
static normalizerLowercase(normalizer) {
|
|
391
|
+
if (normalizer === void 0) return true;
|
|
392
|
+
if (normalizer === null) return false;
|
|
393
|
+
if (typeof normalizer !== "object") throw new Error("Unsupported tokenizer normalizer");
|
|
394
|
+
const value = normalizer;
|
|
395
|
+
if (value.type === "Lowercase") return true;
|
|
396
|
+
if (value.type === "BertNormalizer" && typeof value.lowercase === "boolean" && value.strip_accents === false && value.clean_text === false && value.handle_chinese_chars === false) {
|
|
397
|
+
return value.lowercase;
|
|
398
|
+
}
|
|
399
|
+
throw new Error("Unsupported tokenizer normalizer; only identity, Lowercase, or casing-only BertNormalizer is supported");
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Encode text into token IDs with attention mask.
|
|
403
|
+
* Handles WordPiece tokenization for BERT-like models.
|
|
404
|
+
*/
|
|
405
|
+
encode(text, options) {
|
|
406
|
+
const addSpecial = options?.addSpecialTokens !== false;
|
|
407
|
+
const maxLen = options?.maxLength || this.maxLength;
|
|
408
|
+
const padding = options?.padding ?? false;
|
|
409
|
+
const words = this.preTokenize(text);
|
|
410
|
+
const tokenIds = [];
|
|
411
|
+
if (addSpecial) {
|
|
412
|
+
tokenIds.push(this.clsTokenId);
|
|
413
|
+
}
|
|
414
|
+
for (const word of words) {
|
|
415
|
+
const subTokens = this.tokenizeWord(word);
|
|
416
|
+
for (const subToken of subTokens) {
|
|
417
|
+
if (tokenIds.length >= maxLen - (addSpecial ? 1 : 0)) break;
|
|
418
|
+
tokenIds.push(subToken);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (addSpecial) {
|
|
422
|
+
tokenIds.push(this.sepTokenId);
|
|
423
|
+
}
|
|
424
|
+
const truncated = tokenIds.slice(0, maxLen);
|
|
425
|
+
if (truncated.length === 0) {
|
|
426
|
+
const fallback = new Int32Array([this.padTokenId]);
|
|
427
|
+
return {
|
|
428
|
+
inputIds: fallback,
|
|
429
|
+
attentionMask: new Int32Array([0]),
|
|
430
|
+
inputIdsInt64: _Tokenizer.toInt64(fallback),
|
|
431
|
+
attentionMaskInt64: new BigInt64Array([0n])
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
const attentionMask = new Int32Array(padding ? maxLen : truncated.length);
|
|
435
|
+
attentionMask.fill(1, 0, truncated.length);
|
|
436
|
+
const inputIds = new Int32Array(padding ? maxLen : truncated.length);
|
|
437
|
+
inputIds.set(truncated);
|
|
438
|
+
if (padding) {
|
|
439
|
+
inputIds.fill(this.padTokenId, truncated.length);
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
inputIds,
|
|
443
|
+
attentionMask,
|
|
444
|
+
inputIdsInt64: _Tokenizer.toInt64(inputIds),
|
|
445
|
+
attentionMaskInt64: _Tokenizer.toInt64(attentionMask)
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Decode token IDs back to text.
|
|
450
|
+
*/
|
|
451
|
+
decode(tokens) {
|
|
452
|
+
const tokenArray = tokens instanceof Int32Array ? Array.from(tokens) : tokens;
|
|
453
|
+
const words = [];
|
|
454
|
+
for (const id of tokenArray) {
|
|
455
|
+
if (id === this.padTokenId || id === this.clsTokenId || id === this.sepTokenId) continue;
|
|
456
|
+
const token = this.inverseVocab.get(id);
|
|
457
|
+
if (!token) {
|
|
458
|
+
words.push("[UNK]");
|
|
459
|
+
} else if (token.startsWith("##")) {
|
|
460
|
+
if (words.length > 0) {
|
|
461
|
+
words[words.length - 1] += token.slice(2);
|
|
462
|
+
} else {
|
|
463
|
+
words.push(token.slice(2));
|
|
464
|
+
}
|
|
465
|
+
} else if (token.startsWith("\u2581") || token.startsWith("\u0120")) {
|
|
466
|
+
words.push(token.slice(1));
|
|
467
|
+
} else {
|
|
468
|
+
words.push(token);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return words.join(" ");
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Get vocabulary size.
|
|
475
|
+
*/
|
|
476
|
+
get vocabSize() {
|
|
477
|
+
return this.vocab.size;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Resolve a special token (e.g. `</s>`, `<|endoftext|>`) to its ID, whether it
|
|
481
|
+
* was declared in `specialTokens` or is present in the main vocabulary.
|
|
482
|
+
*/
|
|
483
|
+
getSpecialTokenId(token) {
|
|
484
|
+
return this.specialTokens.get(token) ?? this.vocab.get(token);
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Pre-tokenize text: lowercase, split on whitespace and punctuation boundaries.
|
|
488
|
+
*/
|
|
489
|
+
preTokenize(text) {
|
|
490
|
+
const normalized = (this.lowercase ? text.toLowerCase() : text).trim();
|
|
491
|
+
if (!normalized) return [];
|
|
492
|
+
const tokens = [];
|
|
493
|
+
let current = "";
|
|
494
|
+
for (const char of normalized) {
|
|
495
|
+
if (/\s/.test(char)) {
|
|
496
|
+
if (current) tokens.push(current);
|
|
497
|
+
current = "";
|
|
498
|
+
} else if (/[.,!?;:'"()\[\]{}\-\/\\@#$%^&*~`|<>]/.test(char) || /\p{P}/u.test(char)) {
|
|
499
|
+
if (current) tokens.push(current);
|
|
500
|
+
tokens.push(char);
|
|
501
|
+
current = "";
|
|
502
|
+
} else {
|
|
503
|
+
current += char;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (current) tokens.push(current);
|
|
507
|
+
return tokens;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Tokenize a single word using the tokenizer's model type: BPE when a merge
|
|
511
|
+
* table is present, WordPiece otherwise.
|
|
512
|
+
*/
|
|
513
|
+
tokenizeWord(word) {
|
|
514
|
+
if (this.usesBPE) return this.tokenizeWordBPE(word);
|
|
515
|
+
if (this.vocab.has(word)) {
|
|
516
|
+
return [this.vocab.get(word)];
|
|
517
|
+
}
|
|
518
|
+
const tokens = [];
|
|
519
|
+
let start = 0;
|
|
520
|
+
while (start < word.length) {
|
|
521
|
+
let end = word.length;
|
|
522
|
+
let found = false;
|
|
523
|
+
while (start < end) {
|
|
524
|
+
let substr = word.slice(start, end);
|
|
525
|
+
if (start > 0) {
|
|
526
|
+
substr = "##" + substr;
|
|
527
|
+
}
|
|
528
|
+
if (this.vocab.has(substr)) {
|
|
529
|
+
tokens.push(this.vocab.get(substr));
|
|
530
|
+
found = true;
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
end--;
|
|
534
|
+
}
|
|
535
|
+
if (!found) {
|
|
536
|
+
tokens.push(this.unkTokenId);
|
|
537
|
+
start++;
|
|
538
|
+
} else {
|
|
539
|
+
start = end;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return tokens;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Apply the merge table to a word, then map the resulting pieces to IDs.
|
|
546
|
+
* Unknown pieces fall back to the unknown token rather than throwing, which
|
|
547
|
+
* matches ONNX Runtime's text-tokenizer behaviour.
|
|
548
|
+
*/
|
|
549
|
+
tokenizeWordBPE(word) {
|
|
550
|
+
const whole = this.vocab.get(
|
|
551
|
+
this.bpeSpaceMarker ? this.bpeSpaceMarker + word : word
|
|
552
|
+
);
|
|
553
|
+
if (whole !== void 0) return [whole];
|
|
554
|
+
const pieces = this.applyBPE(word);
|
|
555
|
+
const ids = [];
|
|
556
|
+
for (let i = 0; i < pieces.length; i++) {
|
|
557
|
+
const piece = i === 0 && this.bpeSpaceMarker ? this.bpeSpaceMarker + pieces[i] : pieces[i];
|
|
558
|
+
const id = this.vocab.get(piece);
|
|
559
|
+
if (id !== void 0) {
|
|
560
|
+
ids.push(id);
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
ids.push(this.unkTokenId);
|
|
564
|
+
}
|
|
565
|
+
return ids;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Apply BPE merges to a word (for BPE-style tokenizers like GPT-2/Llama).
|
|
569
|
+
* Merge rules are applied in ascending rank order until no adjacent pair is
|
|
570
|
+
* present in the merge table; caching is keyed on the original word.
|
|
571
|
+
*/
|
|
572
|
+
applyBPE(word) {
|
|
573
|
+
const cached = this.bpeCache.get(word);
|
|
574
|
+
if (cached) return cached;
|
|
575
|
+
let tokens = word.split("");
|
|
576
|
+
if (tokens.length <= 1 || this.mergeRanks.size === 0) {
|
|
577
|
+
this.bpeCache.set(word, tokens);
|
|
578
|
+
return tokens;
|
|
579
|
+
}
|
|
580
|
+
while (tokens.length > 1) {
|
|
581
|
+
let bestRank = Number.POSITIVE_INFINITY;
|
|
582
|
+
for (let i = 0; i < tokens.length - 1; i++) {
|
|
583
|
+
const rank = this.mergeRanks.get(`${tokens[i]} ${tokens[i + 1]}`);
|
|
584
|
+
if (rank !== void 0 && rank < bestRank) bestRank = rank;
|
|
585
|
+
}
|
|
586
|
+
if (bestRank === Number.POSITIVE_INFINITY) break;
|
|
587
|
+
const merged = [];
|
|
588
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
589
|
+
if (i < tokens.length - 1 && this.mergeRanks.get(`${tokens[i]} ${tokens[i + 1]}`) === bestRank) {
|
|
590
|
+
merged.push(tokens[i] + tokens[i + 1]);
|
|
591
|
+
i++;
|
|
592
|
+
} else {
|
|
593
|
+
merged.push(tokens[i]);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (merged.length === tokens.length) break;
|
|
597
|
+
tokens = merged;
|
|
598
|
+
}
|
|
599
|
+
this.bpeCache.set(word, tokens);
|
|
600
|
+
return tokens;
|
|
601
|
+
}
|
|
602
|
+
static toInt64(values) {
|
|
603
|
+
const out = new BigInt64Array(values.length);
|
|
604
|
+
for (let i = 0; i < values.length; i++) out[i] = BigInt(values[i]);
|
|
605
|
+
return out;
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
// src/image-processor.ts
|
|
610
|
+
var ImageProcessor = class {
|
|
611
|
+
static imageDataToFloat32Array(imageData, options = {}) {
|
|
612
|
+
const { normalize = true, layout = "NCHW" } = options;
|
|
613
|
+
const { width, height, data } = imageData;
|
|
614
|
+
const size = width * height;
|
|
615
|
+
const float32Data = new Float32Array(size * 3);
|
|
616
|
+
for (let i = 0; i < size; i++) {
|
|
617
|
+
const r = data[i * 4];
|
|
618
|
+
const g = data[i * 4 + 1];
|
|
619
|
+
const b = data[i * 4 + 2];
|
|
620
|
+
const nr = normalize ? r / 255 : r;
|
|
621
|
+
const ng = normalize ? g / 255 : g;
|
|
622
|
+
const nb = normalize ? b / 255 : b;
|
|
623
|
+
if (layout === "NCHW") {
|
|
624
|
+
float32Data[i] = nr;
|
|
625
|
+
float32Data[size + i] = ng;
|
|
626
|
+
float32Data[2 * size + i] = nb;
|
|
627
|
+
} else {
|
|
628
|
+
float32Data[i * 3] = nr;
|
|
629
|
+
float32Data[i * 3 + 1] = ng;
|
|
630
|
+
float32Data[i * 3 + 2] = nb;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return float32Data;
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
// src/edgeinfer.ts
|
|
638
|
+
import * as ort2 from "onnxruntime-web";
|
|
639
|
+
function throwIfAborted(signal) {
|
|
640
|
+
if (signal?.aborted) {
|
|
641
|
+
throw signal.reason ?? new DOMException("Generation aborted", "AbortError");
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function normalizeChatMessages(messages) {
|
|
645
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
646
|
+
throw new Error("chat requires at least one message");
|
|
647
|
+
}
|
|
648
|
+
return messages.map((message, index) => {
|
|
649
|
+
if (!message || typeof message !== "object") {
|
|
650
|
+
throw new Error(`chat message ${index} must be an object`);
|
|
651
|
+
}
|
|
652
|
+
if (message.role !== "system" && message.role !== "user" && message.role !== "assistant") {
|
|
653
|
+
throw new Error(`chat message ${index} has an invalid role`);
|
|
654
|
+
}
|
|
655
|
+
if (typeof message.content !== "string") {
|
|
656
|
+
throw new Error(`chat message ${index} content must be a string`);
|
|
657
|
+
}
|
|
658
|
+
return { role: message.role, content: message.content };
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
function defaultChatTemplate(messages) {
|
|
662
|
+
return `${messages.map((message) => `<|${message.role}|>${message.content}`).join("\n")}
|
|
663
|
+
<|assistant|>`;
|
|
664
|
+
}
|
|
665
|
+
var EdgeInfer = class _EdgeInfer {
|
|
666
|
+
constructor(session, modelSize, provider, int64Inputs, tokenizer) {
|
|
667
|
+
this.tokenizer = null;
|
|
668
|
+
this._provider = "wasm";
|
|
669
|
+
this.session = session;
|
|
670
|
+
this._inputNames = session.inputNames;
|
|
671
|
+
this._outputNames = session.outputNames;
|
|
672
|
+
this._modelSize = modelSize;
|
|
673
|
+
this._provider = provider;
|
|
674
|
+
this._int64Inputs = int64Inputs;
|
|
675
|
+
this.tokenizer = tokenizer || null;
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Load a model from URL with automatic caching and optimal provider selection.
|
|
679
|
+
*/
|
|
680
|
+
static async load(modelUrl, options) {
|
|
681
|
+
const cache = new ModelCache(options?.cacheName);
|
|
682
|
+
const buffer = await cache.getModel(modelUrl, options?.forceDownload, options);
|
|
683
|
+
return _EdgeInfer.fromBuffer(buffer, options);
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Create an EdgeInfer instance from a pre-loaded model buffer.
|
|
687
|
+
*/
|
|
688
|
+
static async fromBuffer(buffer, options) {
|
|
689
|
+
checkDownloadAbort(options?.signal);
|
|
690
|
+
const { session, provider } = await RuntimeManager.createSession(
|
|
691
|
+
buffer,
|
|
692
|
+
options?.executionProviders
|
|
693
|
+
);
|
|
694
|
+
try {
|
|
695
|
+
checkDownloadAbort(options?.signal);
|
|
696
|
+
let tokenizer;
|
|
697
|
+
if (options?.tokenizerUrl) {
|
|
698
|
+
tokenizer = await Tokenizer.fromUrl(options.tokenizerUrl, options);
|
|
699
|
+
} else if (options?.tokenizerConfig) {
|
|
700
|
+
tokenizer = new Tokenizer(options.tokenizerConfig);
|
|
701
|
+
}
|
|
702
|
+
checkDownloadAbort(options?.signal);
|
|
703
|
+
return new _EdgeInfer(session, buffer.byteLength, provider, options?.int64Inputs ?? false, tokenizer);
|
|
704
|
+
} catch (error) {
|
|
705
|
+
try {
|
|
706
|
+
await session.release();
|
|
707
|
+
} catch {
|
|
708
|
+
}
|
|
709
|
+
throw error;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Set or replace the tokenizer after model load.
|
|
714
|
+
*/
|
|
715
|
+
setTokenizer(tokenizer) {
|
|
716
|
+
this.tokenizer = tokenizer;
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Low-level tensor inference.
|
|
720
|
+
* Pass raw tensor inputs and get raw tensor outputs.
|
|
721
|
+
*
|
|
722
|
+
* `shapes` optionally gives the ONNX tensor shape per input name. When an
|
|
723
|
+
* input has no explicit shape it defaults to `[1, data.length]`, which is
|
|
724
|
+
* correct for 2-D sequence inputs (`input_ids`, `attention_mask`) but wrong
|
|
725
|
+
* for anything of higher rank — a vision model declaring
|
|
726
|
+
* `pixel_values: [1,3,224,224]` is rank 4 and rejects a rank-2 tensor with
|
|
727
|
+
* "Invalid rank for input". Pass `shapes` for such models.
|
|
728
|
+
*/
|
|
729
|
+
async predict(inputs, shapes) {
|
|
730
|
+
const results = await this.runInference(inputs, shapes);
|
|
731
|
+
const outputMap = {};
|
|
732
|
+
for (const [key, tensor] of Object.entries(results)) {
|
|
733
|
+
outputMap[key] = tensor.data;
|
|
734
|
+
}
|
|
735
|
+
return outputMap;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Classify text using the loaded model.
|
|
739
|
+
* Requires a tokenizer to be configured.
|
|
740
|
+
*/
|
|
741
|
+
async classify(text) {
|
|
742
|
+
this.requireTokenizer();
|
|
743
|
+
const encoding = this.tokenizer.encode(text, { addSpecialTokens: true });
|
|
744
|
+
const inputs = this.textInputs(encoding);
|
|
745
|
+
const result = await this.predict(inputs);
|
|
746
|
+
const output = result[this._outputNames[0]];
|
|
747
|
+
if (!output) return [];
|
|
748
|
+
const probs = this.softmax(output);
|
|
749
|
+
return Array.from(probs).map((score, idx) => ({
|
|
750
|
+
label: `Class_${idx}`,
|
|
751
|
+
score
|
|
752
|
+
})).sort((a, b) => b.score - a.score);
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Generate text embeddings for semantic search / RAG.
|
|
756
|
+
* Returns a Float32Array of embedding dimensions.
|
|
757
|
+
*/
|
|
758
|
+
async embed(text) {
|
|
759
|
+
this.requireTokenizer();
|
|
760
|
+
const encoding = this.tokenizer.encode(text, { addSpecialTokens: true });
|
|
761
|
+
const inputs = this.textInputs(encoding);
|
|
762
|
+
const result = await this.runInference(inputs);
|
|
763
|
+
const output = result[this._outputNames[0]];
|
|
764
|
+
if (!output) return new Float32Array();
|
|
765
|
+
if (output.type !== "float32") {
|
|
766
|
+
throw new Error("EdgeInfer: Embedding output must be a float32 tensor.");
|
|
767
|
+
}
|
|
768
|
+
return this.poolEmbedding(output.data, output.dims, encoding.attentionMask);
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Generate Matryoshka-truncated embeddings at specified dimension.
|
|
772
|
+
* Supports variable-dimension MRL embeddings (64d → 256d → 768d).
|
|
773
|
+
*/
|
|
774
|
+
async embedMatryoshka(text, dimension) {
|
|
775
|
+
const fullEmbedding = await this.embed(text);
|
|
776
|
+
if (dimension >= fullEmbedding.length) return fullEmbedding;
|
|
777
|
+
const truncated = fullEmbedding.slice(0, dimension);
|
|
778
|
+
return this.l2Normalize(truncated);
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Greedy or temperature-sampled autoregressive generation.
|
|
782
|
+
*
|
|
783
|
+
* Decoder models that expose `present.*` outputs and `past_key_values.*`
|
|
784
|
+
* inputs reuse those tensors across steps (KV-cache). Models without cache
|
|
785
|
+
* inputs are re-run over the full sequence each step.
|
|
786
|
+
*/
|
|
787
|
+
async generate(prompt, options = {}) {
|
|
788
|
+
this.requireTokenizer();
|
|
789
|
+
const tokenIds = [];
|
|
790
|
+
for await (const event of this.generateStream(prompt, options)) {
|
|
791
|
+
tokenIds.push(event.tokenId);
|
|
792
|
+
}
|
|
793
|
+
return {
|
|
794
|
+
text: this.tokenizer.decode(tokenIds),
|
|
795
|
+
tokenIds
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Stream generated tokens. The decoder loop is identical to generate(), but
|
|
800
|
+
* each token is yielded as soon as it is chosen so a UI can paint before the
|
|
801
|
+
* full completion finishes.
|
|
802
|
+
*/
|
|
803
|
+
async *generateStream(prompt, options = {}) {
|
|
804
|
+
this.requireTokenizer();
|
|
805
|
+
const maxNewTokens = Math.max(0, options.maxNewTokens ?? 64);
|
|
806
|
+
const temperature = options.temperature ?? 0;
|
|
807
|
+
const topK = options.topK ?? 0;
|
|
808
|
+
const tokenizer = this.tokenizer;
|
|
809
|
+
const eosTokenId = options.eosTokenId ?? tokenizer.getSpecialTokenId("</s>") ?? tokenizer.getSpecialTokenId("<|endoftext|>");
|
|
810
|
+
const encoding = tokenizer.encode(prompt, { addSpecialTokens: true });
|
|
811
|
+
const promptIds = Array.from(encoding.inputIds);
|
|
812
|
+
const int64 = this.useInt64Inputs();
|
|
813
|
+
const makeIds = (values) => int64 ? BigInt64Array.from(values, (v) => BigInt(v)) : Int32Array.from(values);
|
|
814
|
+
const dtype = int64 ? "int64" : "int32";
|
|
815
|
+
let current = promptIds;
|
|
816
|
+
let past = {};
|
|
817
|
+
const generated = [];
|
|
818
|
+
for (let step = 0; step < maxNewTokens; step++) {
|
|
819
|
+
throwIfAborted(options.signal);
|
|
820
|
+
const totalLength = promptIds.length + generated.length;
|
|
821
|
+
const inputs = {
|
|
822
|
+
input_ids: new ort2.Tensor(dtype, makeIds(current), [1, current.length]),
|
|
823
|
+
attention_mask: new ort2.Tensor(dtype, makeIds(new Array(totalLength).fill(1)), [1, totalLength])
|
|
824
|
+
};
|
|
825
|
+
if (this._inputNames.includes("position_ids")) {
|
|
826
|
+
const offset = totalLength - current.length;
|
|
827
|
+
inputs.position_ids = new ort2.Tensor(
|
|
828
|
+
dtype,
|
|
829
|
+
makeIds(current.map((_, i) => offset + i)),
|
|
830
|
+
[1, current.length]
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
for (const [name, tensor] of Object.entries(past)) {
|
|
834
|
+
if (this._inputNames.includes(name)) inputs[name] = tensor;
|
|
835
|
+
}
|
|
836
|
+
const results = await this.session.run(inputs);
|
|
837
|
+
const logits = this.findLogits(results);
|
|
838
|
+
const next = this.pickNextToken(logits, temperature, topK);
|
|
839
|
+
generated.push(next);
|
|
840
|
+
yield {
|
|
841
|
+
tokenId: next,
|
|
842
|
+
text: tokenizer.decode([next]),
|
|
843
|
+
index: generated.length - 1
|
|
844
|
+
};
|
|
845
|
+
if (eosTokenId !== void 0 && next === eosTokenId) break;
|
|
846
|
+
const nextPast = {};
|
|
847
|
+
for (const outputName of this._outputNames) {
|
|
848
|
+
const inputName = _EdgeInfer.pastInputName(outputName);
|
|
849
|
+
if (inputName && this._inputNames.includes(inputName) && results[outputName]) {
|
|
850
|
+
nextPast[inputName] = results[outputName];
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
if (Object.keys(nextPast).length > 0) {
|
|
854
|
+
past = nextPast;
|
|
855
|
+
current = [next];
|
|
856
|
+
} else {
|
|
857
|
+
past = {};
|
|
858
|
+
current = [...promptIds, ...generated];
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Stream a chat completion using a role-marker template. The default template
|
|
864
|
+
* is model-agnostic; pass `chatTemplate` to match the target tokenizer.
|
|
865
|
+
*/
|
|
866
|
+
async *chatStream(messages, options = {}) {
|
|
867
|
+
this.requireTokenizer();
|
|
868
|
+
const normalized = normalizeChatMessages(messages);
|
|
869
|
+
const prompt = (options.chatTemplate ?? defaultChatTemplate)(normalized);
|
|
870
|
+
yield* this.generateStream(prompt, options);
|
|
871
|
+
}
|
|
872
|
+
/** Collect a chat completion. */
|
|
873
|
+
async chat(messages, options = {}) {
|
|
874
|
+
this.requireTokenizer();
|
|
875
|
+
const normalized = normalizeChatMessages(messages);
|
|
876
|
+
const prompt = (options.chatTemplate ?? defaultChatTemplate)(normalized);
|
|
877
|
+
const tokenIds = [];
|
|
878
|
+
for await (const event of this.generateStream(prompt, options)) {
|
|
879
|
+
tokenIds.push(event.tokenId);
|
|
880
|
+
}
|
|
881
|
+
return {
|
|
882
|
+
text: this.tokenizer.decode(tokenIds),
|
|
883
|
+
tokenIds,
|
|
884
|
+
prompt
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
/** Pick the last-token logits from the first output that looks like logits. */
|
|
888
|
+
findLogits(results) {
|
|
889
|
+
const exact = results["logits"];
|
|
890
|
+
if (exact) return exact;
|
|
891
|
+
for (const name of Object.keys(results)) {
|
|
892
|
+
if (name.toLowerCase().includes("logits")) return results[name];
|
|
893
|
+
}
|
|
894
|
+
return results[this._outputNames[0]];
|
|
895
|
+
}
|
|
896
|
+
/** Argmax (temperature 0) or temperature/top-k sampling over last-token logits. */
|
|
897
|
+
pickNextToken(logits, temperature, topK) {
|
|
898
|
+
const dims = logits.dims;
|
|
899
|
+
const vocabSize = dims[dims.length - 1];
|
|
900
|
+
const seqLen = dims.length >= 3 ? dims[dims.length - 2] : 1;
|
|
901
|
+
const data = logits.data;
|
|
902
|
+
const offset = Math.max(0, (seqLen - 1) * vocabSize);
|
|
903
|
+
if (temperature <= 0) {
|
|
904
|
+
let best = 0;
|
|
905
|
+
let bestValue = -Infinity;
|
|
906
|
+
for (let i = 0; i < vocabSize; i++) {
|
|
907
|
+
const value = data[offset + i];
|
|
908
|
+
if (value > bestValue) {
|
|
909
|
+
bestValue = value;
|
|
910
|
+
best = i;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return best;
|
|
914
|
+
}
|
|
915
|
+
const scaled = new Float32Array(vocabSize);
|
|
916
|
+
let maxLogit = -Infinity;
|
|
917
|
+
for (let i = 0; i < vocabSize; i++) {
|
|
918
|
+
scaled[i] = data[offset + i] / temperature;
|
|
919
|
+
if (scaled[i] > maxLogit) maxLogit = scaled[i];
|
|
920
|
+
}
|
|
921
|
+
const candidates = [];
|
|
922
|
+
let sum = 0;
|
|
923
|
+
for (let i = 0; i < vocabSize; i++) {
|
|
924
|
+
const prob = Math.exp(scaled[i] - maxLogit);
|
|
925
|
+
candidates.push({ id: i, prob });
|
|
926
|
+
sum += prob;
|
|
927
|
+
}
|
|
928
|
+
if (topK > 0 && topK < candidates.length) {
|
|
929
|
+
candidates.sort((a, b) => b.prob - a.prob);
|
|
930
|
+
candidates.length = topK;
|
|
931
|
+
sum = candidates.reduce((acc, c) => acc + c.prob, 0);
|
|
932
|
+
}
|
|
933
|
+
let sample2 = Math.random() * sum;
|
|
934
|
+
for (const candidate of candidates) {
|
|
935
|
+
sample2 -= candidate.prob;
|
|
936
|
+
if (sample2 <= 0) return candidate.id;
|
|
937
|
+
}
|
|
938
|
+
return candidates[candidates.length - 1].id;
|
|
939
|
+
}
|
|
940
|
+
static pastInputName(outputName) {
|
|
941
|
+
if (!outputName.startsWith("present")) return null;
|
|
942
|
+
return outputName.replace(/^present/, "past_key_values");
|
|
943
|
+
}
|
|
944
|
+
useInt64Inputs() {
|
|
945
|
+
if (this._int64Inputs) return true;
|
|
946
|
+
const metadata = this.session.inputMetadata;
|
|
947
|
+
return metadata?.["input_ids"]?.type === "int64";
|
|
948
|
+
}
|
|
949
|
+
/** Build input_ids/attention_mask tensors in the dtype the model declares. */
|
|
950
|
+
textInputs(encoding) {
|
|
951
|
+
const int64 = this.useInt64Inputs();
|
|
952
|
+
const inputs = {
|
|
953
|
+
input_ids: int64 ? encoding.inputIdsInt64 : encoding.inputIds,
|
|
954
|
+
attention_mask: int64 ? encoding.attentionMaskInt64 : encoding.attentionMask
|
|
955
|
+
};
|
|
956
|
+
if (this._inputNames.includes("token_type_ids")) {
|
|
957
|
+
inputs["token_type_ids"] = int64 ? new BigInt64Array(encoding.inputIds.length) : new Int32Array(encoding.inputIds.length);
|
|
958
|
+
}
|
|
959
|
+
return inputs;
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Sentiment analysis.
|
|
963
|
+
*/
|
|
964
|
+
async sentiment(text) {
|
|
965
|
+
const results = await this.classify(text);
|
|
966
|
+
if (results.length === 0) return { label: "unknown", score: 0 };
|
|
967
|
+
const best = results[0];
|
|
968
|
+
const labelMap = {
|
|
969
|
+
"Class_0": "negative",
|
|
970
|
+
"Class_1": "positive",
|
|
971
|
+
"Class_2": "neutral"
|
|
972
|
+
};
|
|
973
|
+
return {
|
|
974
|
+
label: labelMap[best.label] || best.label,
|
|
975
|
+
score: best.score
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Classify an image using a vision model.
|
|
980
|
+
*/
|
|
981
|
+
async classifyImage(imageData, layout) {
|
|
982
|
+
const effectiveLayout = layout ?? "NCHW";
|
|
983
|
+
const tensorData = ImageProcessor.imageDataToFloat32Array(imageData, { layout: effectiveLayout });
|
|
984
|
+
const inputName = this._inputNames.find(
|
|
985
|
+
(n) => n === "input" || n === "pixel_values" || n === "image"
|
|
986
|
+
) || this._inputNames[0];
|
|
987
|
+
const result = await this.predict(
|
|
988
|
+
{ [inputName]: tensorData },
|
|
989
|
+
{ [inputName]: _EdgeInfer.imageShape(imageData, effectiveLayout) }
|
|
990
|
+
);
|
|
991
|
+
const output = result[this._outputNames[0]];
|
|
992
|
+
if (!output) return [];
|
|
993
|
+
const probs = this.softmax(output);
|
|
994
|
+
return Array.from(probs).map((score, idx) => ({
|
|
995
|
+
label: `Class_${idx}`,
|
|
996
|
+
score
|
|
997
|
+
})).sort((a, b) => b.score - a.score).slice(0, 10);
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Detect objects in an image.
|
|
1001
|
+
*/
|
|
1002
|
+
async detectObjects(imageData) {
|
|
1003
|
+
const tensorData = ImageProcessor.imageDataToFloat32Array(imageData);
|
|
1004
|
+
const inputName = this._inputNames.find(
|
|
1005
|
+
(n) => n === "input" || n === "pixel_values" || n === "image"
|
|
1006
|
+
) || this._inputNames[0];
|
|
1007
|
+
const result = await this.predict(
|
|
1008
|
+
{ [inputName]: tensorData },
|
|
1009
|
+
{ [inputName]: _EdgeInfer.imageShape(imageData, "NCHW") }
|
|
1010
|
+
);
|
|
1011
|
+
const output = result[this._outputNames[0]];
|
|
1012
|
+
const detections = [];
|
|
1013
|
+
if (output) {
|
|
1014
|
+
for (let i = 0; i < output.length; i += 6) {
|
|
1015
|
+
const confidence = output[i + 4];
|
|
1016
|
+
if (confidence > 0.5) {
|
|
1017
|
+
detections.push({
|
|
1018
|
+
bbox: [output[i], output[i + 1], output[i + 2], output[i + 3]],
|
|
1019
|
+
score: confidence,
|
|
1020
|
+
label: `Object_${Math.round(output[i + 5])}`
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
return detections.sort((a, b) => b.score - a.score);
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Check GPU capabilities and recommended configuration.
|
|
1029
|
+
*/
|
|
1030
|
+
static async getCapabilities() {
|
|
1031
|
+
return RuntimeManager.detectCapabilities();
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Check if WebGPU is available for accelerated inference.
|
|
1035
|
+
*/
|
|
1036
|
+
static async isWebGPUAvailable() {
|
|
1037
|
+
const caps = await RuntimeManager.detectCapabilities();
|
|
1038
|
+
return caps.hasWebGPU;
|
|
1039
|
+
}
|
|
1040
|
+
get inputNames() {
|
|
1041
|
+
return this._inputNames;
|
|
1042
|
+
}
|
|
1043
|
+
get outputNames() {
|
|
1044
|
+
return this._outputNames;
|
|
1045
|
+
}
|
|
1046
|
+
get modelSize() {
|
|
1047
|
+
return this._modelSize;
|
|
1048
|
+
}
|
|
1049
|
+
get executionProvider() {
|
|
1050
|
+
return this._provider;
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Release all resources (ONNX session, GPU memory).
|
|
1054
|
+
*/
|
|
1055
|
+
dispose() {
|
|
1056
|
+
if (this.session) {
|
|
1057
|
+
this.session.release();
|
|
1058
|
+
}
|
|
1059
|
+
this.tokenizer = null;
|
|
1060
|
+
RuntimeManager.resetCache();
|
|
1061
|
+
}
|
|
1062
|
+
// --- Private Helpers ---
|
|
1063
|
+
/** Run without discarding the output tensor dimensions needed by embed(). */
|
|
1064
|
+
async runInference(inputs, shapes) {
|
|
1065
|
+
const tensorInputs = {};
|
|
1066
|
+
for (const [key, data] of Object.entries(inputs)) {
|
|
1067
|
+
let type;
|
|
1068
|
+
if (data instanceof Float32Array) {
|
|
1069
|
+
type = "float32";
|
|
1070
|
+
} else if (data instanceof BigInt64Array) {
|
|
1071
|
+
type = "int64";
|
|
1072
|
+
} else {
|
|
1073
|
+
type = "int32";
|
|
1074
|
+
}
|
|
1075
|
+
const shape = shapes?.[key] ?? [1, data.length];
|
|
1076
|
+
tensorInputs[key] = new ort2.Tensor(type, data, shape);
|
|
1077
|
+
}
|
|
1078
|
+
return this.session.run(tensorInputs);
|
|
1079
|
+
}
|
|
1080
|
+
/** ONNX tensor shape for a 3-channel image tensor in the given layout. */
|
|
1081
|
+
static imageShape(imageData, layout) {
|
|
1082
|
+
const { width, height } = imageData;
|
|
1083
|
+
return layout === "NCHW" ? [1, 3, height, width] : [1, height, width, 3];
|
|
1084
|
+
}
|
|
1085
|
+
requireTokenizer() {
|
|
1086
|
+
if (!this.tokenizer) {
|
|
1087
|
+
throw new Error(
|
|
1088
|
+
"EdgeInfer: No tokenizer configured. Either pass tokenizerUrl/tokenizerConfig in options, or call setTokenizer() before using text-based inference methods."
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
softmax(logits) {
|
|
1093
|
+
let maxLogit = -Infinity;
|
|
1094
|
+
for (let i = 0; i < logits.length; i++) {
|
|
1095
|
+
if (logits[i] > maxLogit) maxLogit = logits[i];
|
|
1096
|
+
}
|
|
1097
|
+
const exps = new Float32Array(logits.length);
|
|
1098
|
+
let sumExp = 0;
|
|
1099
|
+
for (let i = 0; i < logits.length; i++) {
|
|
1100
|
+
exps[i] = Math.exp(logits[i] - maxLogit);
|
|
1101
|
+
sumExp += exps[i];
|
|
1102
|
+
}
|
|
1103
|
+
for (let i = 0; i < exps.length; i++) {
|
|
1104
|
+
exps[i] /= sumExp;
|
|
1105
|
+
}
|
|
1106
|
+
return exps;
|
|
1107
|
+
}
|
|
1108
|
+
poolEmbedding(embeddings, dims, attentionMask) {
|
|
1109
|
+
if ((dims.length === 1 || dims.length === 2 && dims[0] === 1) && dims[dims.length - 1] > 0) {
|
|
1110
|
+
return this.l2Normalize(embeddings);
|
|
1111
|
+
}
|
|
1112
|
+
if (dims.length !== 3 || dims[0] !== 1 || dims[1] < 1 || dims[2] < 1) {
|
|
1113
|
+
throw new Error(
|
|
1114
|
+
`EdgeInfer: Unsupported embedding output shape [${dims.join(", ")}]. Expected [hidden], [1, hidden], or [1, sequence, hidden].`
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
const [, seqLen, hiddenDim] = dims;
|
|
1118
|
+
if (seqLen !== attentionMask.length) {
|
|
1119
|
+
throw new Error(
|
|
1120
|
+
`EdgeInfer: Embedding sequence length ${seqLen} does not match attention mask length ${attentionMask.length}.`
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
const pooled = new Float32Array(hiddenDim);
|
|
1124
|
+
let maskSum = 0;
|
|
1125
|
+
for (let t = 0; t < seqLen; t++) {
|
|
1126
|
+
if (attentionMask[t] === 0) continue;
|
|
1127
|
+
maskSum++;
|
|
1128
|
+
for (let d = 0; d < hiddenDim; d++) {
|
|
1129
|
+
pooled[d] += embeddings[t * hiddenDim + d];
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
if (maskSum > 0) {
|
|
1133
|
+
for (let d = 0; d < hiddenDim; d++) {
|
|
1134
|
+
pooled[d] /= maskSum;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
return this.l2Normalize(pooled);
|
|
1138
|
+
}
|
|
1139
|
+
l2Normalize(vec) {
|
|
1140
|
+
let norm = 0;
|
|
1141
|
+
for (let i = 0; i < vec.length; i++) {
|
|
1142
|
+
norm += vec[i] * vec[i];
|
|
1143
|
+
}
|
|
1144
|
+
norm = Math.sqrt(norm);
|
|
1145
|
+
if (norm === 0) return vec;
|
|
1146
|
+
const result = new Float32Array(vec.length);
|
|
1147
|
+
for (let i = 0; i < vec.length; i++) {
|
|
1148
|
+
result[i] = vec[i] / norm;
|
|
1149
|
+
}
|
|
1150
|
+
return result;
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
|
|
1154
|
+
// src/matryoshka.ts
|
|
1155
|
+
function l2Normalize(vector, dimension = vector.length) {
|
|
1156
|
+
const output = new Float32Array(dimension);
|
|
1157
|
+
if (dimension > vector.length) {
|
|
1158
|
+
throw new Error(`Cannot normalize ${dimension} dims from a ${vector.length}-dim vector`);
|
|
1159
|
+
}
|
|
1160
|
+
let norm = 0;
|
|
1161
|
+
for (let i = 0; i < dimension; i++) norm += vector[i] * vector[i];
|
|
1162
|
+
norm = Math.sqrt(norm);
|
|
1163
|
+
if (norm === 0) return output;
|
|
1164
|
+
for (let i = 0; i < dimension; i++) output[i] = vector[i] / norm;
|
|
1165
|
+
return output;
|
|
1166
|
+
}
|
|
1167
|
+
function truncateAndNormalize(vector, dimension) {
|
|
1168
|
+
if (!Number.isInteger(dimension) || dimension < 1) {
|
|
1169
|
+
throw new Error("dimension must be a positive integer");
|
|
1170
|
+
}
|
|
1171
|
+
return l2Normalize(vector, dimension);
|
|
1172
|
+
}
|
|
1173
|
+
function quantizeInt8(vector, dimension = vector.length) {
|
|
1174
|
+
let maxAbs = 0;
|
|
1175
|
+
for (let i = 0; i < dimension; i++) {
|
|
1176
|
+
const value = Math.abs(vector[i]);
|
|
1177
|
+
if (value > maxAbs) maxAbs = value;
|
|
1178
|
+
}
|
|
1179
|
+
const scale = maxAbs > 0 ? maxAbs / 127 : 1;
|
|
1180
|
+
const data = new Int8Array(dimension);
|
|
1181
|
+
for (let i = 0; i < dimension; i++) {
|
|
1182
|
+
const value = Math.round(vector[i] / scale);
|
|
1183
|
+
data[i] = value > 127 ? 127 : value < -127 ? -127 : value;
|
|
1184
|
+
}
|
|
1185
|
+
return { data, scale };
|
|
1186
|
+
}
|
|
1187
|
+
function dequantizeInt8(data, scale) {
|
|
1188
|
+
const output = new Float32Array(data.length);
|
|
1189
|
+
for (let i = 0; i < data.length; i++) output[i] = data[i] * scale;
|
|
1190
|
+
return output;
|
|
1191
|
+
}
|
|
1192
|
+
function dot(a, b, dimension) {
|
|
1193
|
+
let sum = 0;
|
|
1194
|
+
for (let i = 0; i < dimension; i++) sum += a[i] * b[i];
|
|
1195
|
+
return sum;
|
|
1196
|
+
}
|
|
1197
|
+
var TopKHeap = class {
|
|
1198
|
+
constructor(k) {
|
|
1199
|
+
this.k = k;
|
|
1200
|
+
this.items = [];
|
|
1201
|
+
if (k < 1) throw new Error("top-k must be positive");
|
|
1202
|
+
}
|
|
1203
|
+
push(index, score) {
|
|
1204
|
+
if (this.items.length < this.k) {
|
|
1205
|
+
this.items.push({ index, score });
|
|
1206
|
+
if (this.items.length === this.k) this.heapify();
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
if (score > this.items[0].score) {
|
|
1210
|
+
this.items[0] = { index, score };
|
|
1211
|
+
this.siftDown(0);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
heapify() {
|
|
1215
|
+
for (let i = Math.floor(this.items.length / 2) - 1; i >= 0; i--) this.siftDown(i);
|
|
1216
|
+
}
|
|
1217
|
+
siftDown(start) {
|
|
1218
|
+
let index = start;
|
|
1219
|
+
const length = this.items.length;
|
|
1220
|
+
for (; ; ) {
|
|
1221
|
+
const left = index * 2 + 1;
|
|
1222
|
+
const right = left + 1;
|
|
1223
|
+
let smallest = index;
|
|
1224
|
+
if (left < length && this.items[left].score < this.items[smallest].score) smallest = left;
|
|
1225
|
+
if (right < length && this.items[right].score < this.items[smallest].score) smallest = right;
|
|
1226
|
+
if (smallest === index) break;
|
|
1227
|
+
[this.items[index], this.items[smallest]] = [this.items[smallest], this.items[index]];
|
|
1228
|
+
index = smallest;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
values() {
|
|
1232
|
+
return [...this.items].sort((a, b) => b.score - a.score);
|
|
1233
|
+
}
|
|
1234
|
+
};
|
|
1235
|
+
var MemoryVectorStore = class {
|
|
1236
|
+
constructor() {
|
|
1237
|
+
this.records = [];
|
|
1238
|
+
}
|
|
1239
|
+
async add(records) {
|
|
1240
|
+
this.records.push(...records);
|
|
1241
|
+
}
|
|
1242
|
+
async getAll() {
|
|
1243
|
+
return this.records.slice();
|
|
1244
|
+
}
|
|
1245
|
+
async clear() {
|
|
1246
|
+
this.records = [];
|
|
1247
|
+
}
|
|
1248
|
+
async count() {
|
|
1249
|
+
return this.records.length;
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
var IndexedDbVectorStore = class {
|
|
1253
|
+
constructor(dbName = "edgeinfer-matryoshka", storeName = "vectors") {
|
|
1254
|
+
this.dbName = dbName;
|
|
1255
|
+
this.storeName = storeName;
|
|
1256
|
+
this.dbPromise = null;
|
|
1257
|
+
}
|
|
1258
|
+
open() {
|
|
1259
|
+
if (this.dbPromise) return this.dbPromise;
|
|
1260
|
+
if (typeof indexedDB === "undefined") {
|
|
1261
|
+
return Promise.reject(new Error("IndexedDB is unavailable in this environment"));
|
|
1262
|
+
}
|
|
1263
|
+
this.dbPromise = new Promise((resolve, reject) => {
|
|
1264
|
+
const request = indexedDB.open(this.dbName, 1);
|
|
1265
|
+
request.onupgradeneeded = () => {
|
|
1266
|
+
const db = request.result;
|
|
1267
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
1268
|
+
db.createObjectStore(this.storeName, { keyPath: "id" });
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
request.onsuccess = () => resolve(request.result);
|
|
1272
|
+
request.onerror = () => reject(request.error ?? new Error("IndexedDB open failed"));
|
|
1273
|
+
});
|
|
1274
|
+
return this.dbPromise;
|
|
1275
|
+
}
|
|
1276
|
+
async add(records) {
|
|
1277
|
+
const db = await this.open();
|
|
1278
|
+
await new Promise((resolve, reject) => {
|
|
1279
|
+
const transaction = db.transaction(this.storeName, "readwrite");
|
|
1280
|
+
const store = transaction.objectStore(this.storeName);
|
|
1281
|
+
for (const record of records) {
|
|
1282
|
+
const stored = {
|
|
1283
|
+
id: record.id,
|
|
1284
|
+
text: record.text,
|
|
1285
|
+
metadata: record.metadata,
|
|
1286
|
+
embedding: record.embedding.slice().buffer
|
|
1287
|
+
};
|
|
1288
|
+
store.put(stored);
|
|
1289
|
+
}
|
|
1290
|
+
transaction.oncomplete = () => resolve();
|
|
1291
|
+
transaction.onerror = () => reject(transaction.error ?? new Error("IndexedDB write failed"));
|
|
1292
|
+
transaction.onabort = () => reject(transaction.error ?? new Error("IndexedDB write aborted"));
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
async getAll() {
|
|
1296
|
+
const db = await this.open();
|
|
1297
|
+
return new Promise((resolve, reject) => {
|
|
1298
|
+
const transaction = db.transaction(this.storeName, "readonly");
|
|
1299
|
+
const store = transaction.objectStore(this.storeName);
|
|
1300
|
+
const request = store.getAll();
|
|
1301
|
+
request.onsuccess = () => {
|
|
1302
|
+
const records = request.result.map((stored) => ({
|
|
1303
|
+
id: stored.id,
|
|
1304
|
+
text: stored.text,
|
|
1305
|
+
metadata: stored.metadata,
|
|
1306
|
+
embedding: new Float32Array(stored.embedding)
|
|
1307
|
+
}));
|
|
1308
|
+
resolve(records);
|
|
1309
|
+
};
|
|
1310
|
+
request.onerror = () => reject(request.error ?? new Error("IndexedDB read failed"));
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
async clear() {
|
|
1314
|
+
const db = await this.open();
|
|
1315
|
+
await new Promise((resolve, reject) => {
|
|
1316
|
+
const transaction = db.transaction(this.storeName, "readwrite");
|
|
1317
|
+
transaction.objectStore(this.storeName).clear();
|
|
1318
|
+
transaction.oncomplete = () => resolve();
|
|
1319
|
+
transaction.onerror = () => reject(transaction.error ?? new Error("IndexedDB clear failed"));
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
async count() {
|
|
1323
|
+
const db = await this.open();
|
|
1324
|
+
return new Promise((resolve, reject) => {
|
|
1325
|
+
const transaction = db.transaction(this.storeName, "readonly");
|
|
1326
|
+
const request = transaction.objectStore(this.storeName).count();
|
|
1327
|
+
request.onsuccess = () => resolve(request.result);
|
|
1328
|
+
request.onerror = () => reject(request.error ?? new Error("IndexedDB count failed"));
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
close() {
|
|
1332
|
+
void this.dbPromise?.then((db) => db.close());
|
|
1333
|
+
this.dbPromise = null;
|
|
1334
|
+
}
|
|
1335
|
+
};
|
|
1336
|
+
var MatryoshkaIndex = class _MatryoshkaIndex {
|
|
1337
|
+
constructor(options = {}) {
|
|
1338
|
+
this.ids = [];
|
|
1339
|
+
this.texts = [];
|
|
1340
|
+
this.metadata = [];
|
|
1341
|
+
this.full = new Float32Array(0);
|
|
1342
|
+
this.coarse = new Int8Array(0);
|
|
1343
|
+
this.scales = new Float32Array(0);
|
|
1344
|
+
this.size = 0;
|
|
1345
|
+
this.dimensions = options.dimensions ?? 768;
|
|
1346
|
+
this.coarseDimension = options.coarseDimension ?? 64;
|
|
1347
|
+
this.refineTopK = options.refineTopK ?? 50;
|
|
1348
|
+
this.store = options.store;
|
|
1349
|
+
if (this.coarseDimension > this.dimensions) {
|
|
1350
|
+
throw new Error("coarseDimension cannot exceed dimensions");
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
get count() {
|
|
1354
|
+
return this.size;
|
|
1355
|
+
}
|
|
1356
|
+
async build(records, { persist = true } = {}) {
|
|
1357
|
+
if (records.length === 0) throw new Error("At least one record is required");
|
|
1358
|
+
for (const record of records) {
|
|
1359
|
+
if (record.embedding.length !== this.dimensions) {
|
|
1360
|
+
throw new Error(
|
|
1361
|
+
`Record ${record.id} has ${record.embedding.length} dims; expected ${this.dimensions}`
|
|
1362
|
+
);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
this.ids = new Array(records.length);
|
|
1366
|
+
this.texts = new Array(records.length);
|
|
1367
|
+
this.metadata = new Array(records.length);
|
|
1368
|
+
this.full = new Float32Array(records.length * this.dimensions);
|
|
1369
|
+
this.coarse = new Int8Array(records.length * this.coarseDimension);
|
|
1370
|
+
this.scales = new Float32Array(records.length);
|
|
1371
|
+
this.size = records.length;
|
|
1372
|
+
for (let row = 0; row < records.length; row++) {
|
|
1373
|
+
const record = records[row];
|
|
1374
|
+
const normalized = l2Normalize(record.embedding, this.dimensions);
|
|
1375
|
+
this.ids[row] = record.id;
|
|
1376
|
+
this.texts[row] = record.text;
|
|
1377
|
+
this.metadata[row] = record.metadata;
|
|
1378
|
+
this.full.set(normalized, row * this.dimensions);
|
|
1379
|
+
const quantized = quantizeInt8(normalized, this.coarseDimension);
|
|
1380
|
+
this.coarse.set(quantized.data, row * this.coarseDimension);
|
|
1381
|
+
this.scales[row] = quantized.scale;
|
|
1382
|
+
}
|
|
1383
|
+
if (persist && this.store) {
|
|
1384
|
+
await this.store.clear();
|
|
1385
|
+
const normalizedRecords = records.map((record, row) => ({
|
|
1386
|
+
...record,
|
|
1387
|
+
embedding: this.full.slice(row * this.dimensions, (row + 1) * this.dimensions)
|
|
1388
|
+
}));
|
|
1389
|
+
await this.store.add(normalizedRecords);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
static async load(store, options = {}) {
|
|
1393
|
+
const index = new _MatryoshkaIndex({ ...options, store });
|
|
1394
|
+
const records = await store.getAll();
|
|
1395
|
+
await index.build(records, { persist: false });
|
|
1396
|
+
return index;
|
|
1397
|
+
}
|
|
1398
|
+
search(query, options = {}) {
|
|
1399
|
+
if (this.size === 0) throw new Error("Index is empty; call build() first");
|
|
1400
|
+
const topK = options.topK ?? 10;
|
|
1401
|
+
const refine = options.refine ?? true;
|
|
1402
|
+
const coarseDimension = options.coarseDimension ?? this.coarseDimension;
|
|
1403
|
+
if (coarseDimension > this.coarseDimension) {
|
|
1404
|
+
throw new Error(
|
|
1405
|
+
`coarseDimension ${coarseDimension} exceeds the indexed stride ${this.coarseDimension}`
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
const queryFull = l2Normalize(query, this.dimensions);
|
|
1409
|
+
const queryCoarse = quantizeInt8(queryFull, coarseDimension);
|
|
1410
|
+
const refineTopK = Math.max(
|
|
1411
|
+
topK,
|
|
1412
|
+
Math.min(this.refineTopK, options.refineTopK ?? this.refineTopK, this.size)
|
|
1413
|
+
);
|
|
1414
|
+
const heap = new TopKHeap(refineTopK);
|
|
1415
|
+
for (let row = 0; row < this.size; row++) {
|
|
1416
|
+
const offset = row * this.coarseDimension;
|
|
1417
|
+
let sum = 0;
|
|
1418
|
+
for (let i = 0; i < coarseDimension; i++) {
|
|
1419
|
+
sum += queryCoarse.data[i] * this.coarse[offset + i];
|
|
1420
|
+
}
|
|
1421
|
+
heap.push(row, sum * this.scales[row]);
|
|
1422
|
+
}
|
|
1423
|
+
let candidates = heap.values().map((candidate) => ({ ...candidate, coarseScore: candidate.score }));
|
|
1424
|
+
if (refine) {
|
|
1425
|
+
candidates = candidates.map((candidate) => ({
|
|
1426
|
+
...candidate,
|
|
1427
|
+
score: dot(
|
|
1428
|
+
queryFull,
|
|
1429
|
+
this.full.subarray(
|
|
1430
|
+
candidate.index * this.dimensions,
|
|
1431
|
+
(candidate.index + 1) * this.dimensions
|
|
1432
|
+
),
|
|
1433
|
+
this.dimensions
|
|
1434
|
+
)
|
|
1435
|
+
})).sort((a, b) => b.score - a.score);
|
|
1436
|
+
}
|
|
1437
|
+
return candidates.slice(0, topK).map((candidate) => ({
|
|
1438
|
+
id: this.ids[candidate.index],
|
|
1439
|
+
text: this.texts[candidate.index],
|
|
1440
|
+
metadata: this.metadata[candidate.index],
|
|
1441
|
+
score: candidate.score,
|
|
1442
|
+
coarseScore: candidate.coarseScore
|
|
1443
|
+
}));
|
|
1444
|
+
}
|
|
1445
|
+
exactSearchAtDimension(query, dimension, topK = 10) {
|
|
1446
|
+
if (dimension > this.dimensions) throw new Error("dimension exceeds index dimensions");
|
|
1447
|
+
const queryNormalized = truncateAndNormalize(query, dimension);
|
|
1448
|
+
const scored = [];
|
|
1449
|
+
for (let row = 0; row < this.size; row++) {
|
|
1450
|
+
const vector = truncateAndNormalize(
|
|
1451
|
+
this.full.subarray(row * this.dimensions, (row + 1) * this.dimensions),
|
|
1452
|
+
dimension
|
|
1453
|
+
);
|
|
1454
|
+
scored.push({ index: row, score: dot(queryNormalized, vector, dimension) });
|
|
1455
|
+
}
|
|
1456
|
+
return scored.sort((a, b) => b.score - a.score).slice(0, topK);
|
|
1457
|
+
}
|
|
1458
|
+
recallAtK(queries, groundTruth, dimension, topK = 10) {
|
|
1459
|
+
let hits = 0;
|
|
1460
|
+
let total = 0;
|
|
1461
|
+
for (let q = 0; q < queries.length; q++) {
|
|
1462
|
+
const expected = new Set(groundTruth[q].slice(0, topK));
|
|
1463
|
+
const candidates = this.exactSearchAtDimension(queries[q], dimension, topK);
|
|
1464
|
+
for (const candidate of candidates) {
|
|
1465
|
+
if (expected.has(this.ids[candidate.index])) hits++;
|
|
1466
|
+
}
|
|
1467
|
+
total += Math.min(topK, expected.size);
|
|
1468
|
+
}
|
|
1469
|
+
return total === 0 ? 0 : hits / total;
|
|
1470
|
+
}
|
|
1471
|
+
get idList() {
|
|
1472
|
+
return this.ids;
|
|
1473
|
+
}
|
|
1474
|
+
};
|
|
1475
|
+
async function embedMatryoshka(text, dimension, embed) {
|
|
1476
|
+
const full = await embed(text);
|
|
1477
|
+
return truncateAndNormalize(full, dimension);
|
|
1478
|
+
}
|
|
1479
|
+
function createMatryoshkaEmbedder(embed) {
|
|
1480
|
+
return (text, dimension = 768) => embedMatryoshka(text, dimension, embed);
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// src/speculative.ts
|
|
1484
|
+
var SpeculativeDecoder = class {
|
|
1485
|
+
constructor(options) {
|
|
1486
|
+
if (options.draft.vocabSize !== options.target.vocabSize) {
|
|
1487
|
+
throw new Error("Draft and target models must share a vocabulary size");
|
|
1488
|
+
}
|
|
1489
|
+
const gamma = options.gamma ?? 5;
|
|
1490
|
+
if (!Number.isInteger(gamma) || gamma < 1) {
|
|
1491
|
+
throw new Error("gamma must be a positive integer");
|
|
1492
|
+
}
|
|
1493
|
+
this.draft = options.draft;
|
|
1494
|
+
this.target = options.target;
|
|
1495
|
+
this.gamma = gamma;
|
|
1496
|
+
this.rng = options.rng ?? Math.random;
|
|
1497
|
+
this.eosTokenId = options.eosTokenId;
|
|
1498
|
+
}
|
|
1499
|
+
async generate(prompt, maxNewTokens = 64, { temperature = 1 } = {}) {
|
|
1500
|
+
if (prompt.length === 0) throw new Error("prompt must not be empty");
|
|
1501
|
+
if (maxNewTokens < 1) throw new Error("maxNewTokens must be positive");
|
|
1502
|
+
if (temperature <= 0) throw new Error("temperature must be positive");
|
|
1503
|
+
const started = performance.now();
|
|
1504
|
+
let draftState = await this.draft.prefill(prompt);
|
|
1505
|
+
let targetState = await this.target.prefill(prompt);
|
|
1506
|
+
const output = [];
|
|
1507
|
+
let draftTokens = 0;
|
|
1508
|
+
let acceptedTokens = 0;
|
|
1509
|
+
let bonusTokens = 0;
|
|
1510
|
+
let rejectedTokens = 0;
|
|
1511
|
+
let targetCalls = 0;
|
|
1512
|
+
let draftCalls = 0;
|
|
1513
|
+
while (output.length < maxNewTokens) {
|
|
1514
|
+
const gamma = Math.min(this.gamma, maxNewTokens - output.length);
|
|
1515
|
+
const draft = await this.draftBatch(draftState, gamma, temperature, draftCalls);
|
|
1516
|
+
draftState = draft.state;
|
|
1517
|
+
draftCalls = draft.calls;
|
|
1518
|
+
draftTokens += draft.tokens.length;
|
|
1519
|
+
const prefixLength = targetState.tokens.length;
|
|
1520
|
+
const verification = await this.target.decode(targetState, draft.tokens);
|
|
1521
|
+
targetCalls++;
|
|
1522
|
+
targetState = verification.state;
|
|
1523
|
+
let rejected = false;
|
|
1524
|
+
for (let i = 0; i < draft.tokens.length; i++) {
|
|
1525
|
+
const token = draft.tokens[i];
|
|
1526
|
+
const q = draft.probabilities[i][token];
|
|
1527
|
+
const p = probability(verification.logits[i], token, temperature);
|
|
1528
|
+
if (this.rng() < Math.min(1, p / q)) {
|
|
1529
|
+
output.push(token);
|
|
1530
|
+
acceptedTokens++;
|
|
1531
|
+
if (token === this.eosTokenId || output.length >= maxNewTokens) break;
|
|
1532
|
+
continue;
|
|
1533
|
+
}
|
|
1534
|
+
const residual = residualDistribution(
|
|
1535
|
+
verification.logits[i],
|
|
1536
|
+
draft.probabilities[i],
|
|
1537
|
+
temperature
|
|
1538
|
+
);
|
|
1539
|
+
const replacement = sample(residual, this.rng());
|
|
1540
|
+
output.push(replacement);
|
|
1541
|
+
rejectedTokens++;
|
|
1542
|
+
rejected = true;
|
|
1543
|
+
const keep = prefixLength + i;
|
|
1544
|
+
targetState = this.target.truncate(targetState, keep);
|
|
1545
|
+
draftState = this.draft.truncate(draftState, keep);
|
|
1546
|
+
const targetAdvance2 = await this.target.decode(targetState, [replacement]);
|
|
1547
|
+
targetCalls++;
|
|
1548
|
+
targetState = targetAdvance2.state;
|
|
1549
|
+
const draftAdvance2 = await this.draft.decode(draftState, [replacement]);
|
|
1550
|
+
draftCalls++;
|
|
1551
|
+
draftState = draftAdvance2.state;
|
|
1552
|
+
break;
|
|
1553
|
+
}
|
|
1554
|
+
if (rejected) continue;
|
|
1555
|
+
if (output.length >= maxNewTokens || output[output.length - 1] === this.eosTokenId) break;
|
|
1556
|
+
const bonusDistribution = softmax(
|
|
1557
|
+
verification.logits[draft.tokens.length],
|
|
1558
|
+
temperature
|
|
1559
|
+
);
|
|
1560
|
+
const bonus = sample(bonusDistribution, this.rng());
|
|
1561
|
+
output.push(bonus);
|
|
1562
|
+
bonusTokens++;
|
|
1563
|
+
const targetAdvance = await this.target.decode(targetState, [bonus]);
|
|
1564
|
+
targetCalls++;
|
|
1565
|
+
targetState = targetAdvance.state;
|
|
1566
|
+
const draftAdvance = await this.draft.decode(draftState, [bonus]);
|
|
1567
|
+
draftCalls++;
|
|
1568
|
+
draftState = draftAdvance.state;
|
|
1569
|
+
if (bonus === this.eosTokenId) break;
|
|
1570
|
+
}
|
|
1571
|
+
const elapsedMs = performance.now() - started;
|
|
1572
|
+
const stats = {
|
|
1573
|
+
outputTokens: output.length,
|
|
1574
|
+
draftTokens,
|
|
1575
|
+
acceptedTokens,
|
|
1576
|
+
bonusTokens,
|
|
1577
|
+
rejectedTokens,
|
|
1578
|
+
targetCalls,
|
|
1579
|
+
draftCalls,
|
|
1580
|
+
acceptanceRate: draftTokens === 0 ? 0 : acceptedTokens / draftTokens,
|
|
1581
|
+
targetCallsPerToken: output.length === 0 ? 0 : targetCalls / output.length,
|
|
1582
|
+
elapsedMs,
|
|
1583
|
+
tokensPerSecond: elapsedMs === 0 ? 0 : output.length / elapsedMs * 1e3
|
|
1584
|
+
};
|
|
1585
|
+
return { tokens: output, stats };
|
|
1586
|
+
}
|
|
1587
|
+
async draftBatch(state, gamma, temperature, calls) {
|
|
1588
|
+
let current = state;
|
|
1589
|
+
const tokens = [];
|
|
1590
|
+
const probabilities = [];
|
|
1591
|
+
const first = await this.draft.decode(current, []);
|
|
1592
|
+
calls++;
|
|
1593
|
+
let nextDistribution = first.logits[0];
|
|
1594
|
+
for (let i = 0; i < gamma; i++) {
|
|
1595
|
+
const distribution = softmax(nextDistribution, temperature);
|
|
1596
|
+
const token = sample(distribution, this.rng());
|
|
1597
|
+
tokens.push(token);
|
|
1598
|
+
probabilities.push(distribution);
|
|
1599
|
+
const advanced = await this.draft.decode(current, [token]);
|
|
1600
|
+
calls++;
|
|
1601
|
+
current = advanced.state;
|
|
1602
|
+
nextDistribution = advanced.logits[advanced.logits.length - 1];
|
|
1603
|
+
}
|
|
1604
|
+
return { state: current, tokens, probabilities, calls };
|
|
1605
|
+
}
|
|
1606
|
+
};
|
|
1607
|
+
function softmax(logits, temperature = 1) {
|
|
1608
|
+
let max = -Infinity;
|
|
1609
|
+
for (let i = 0; i < logits.length; i++) max = Math.max(max, logits[i]);
|
|
1610
|
+
const output = new Float32Array(logits.length);
|
|
1611
|
+
let sum = 0;
|
|
1612
|
+
for (let i = 0; i < logits.length; i++) {
|
|
1613
|
+
const value = Math.exp((logits[i] - max) / temperature);
|
|
1614
|
+
output[i] = value;
|
|
1615
|
+
sum += value;
|
|
1616
|
+
}
|
|
1617
|
+
if (sum === 0) return output;
|
|
1618
|
+
for (let i = 0; i < output.length; i++) output[i] /= sum;
|
|
1619
|
+
return output;
|
|
1620
|
+
}
|
|
1621
|
+
function probability(logits, token, temperature = 1) {
|
|
1622
|
+
return softmax(logits, temperature)[token] ?? 0;
|
|
1623
|
+
}
|
|
1624
|
+
function sample(distribution, random) {
|
|
1625
|
+
let cumulative = 0;
|
|
1626
|
+
for (let i = 0; i < distribution.length; i++) {
|
|
1627
|
+
cumulative += distribution[i];
|
|
1628
|
+
if (random <= cumulative) return i;
|
|
1629
|
+
}
|
|
1630
|
+
return distribution.length - 1;
|
|
1631
|
+
}
|
|
1632
|
+
function residualDistribution(targetLogits, draftProbabilities, temperature = 1) {
|
|
1633
|
+
const p = softmax(targetLogits, temperature);
|
|
1634
|
+
const q = draftProbabilities;
|
|
1635
|
+
const residual = new Float32Array(p.length);
|
|
1636
|
+
let sum = 0;
|
|
1637
|
+
for (let i = 0; i < p.length; i++) {
|
|
1638
|
+
const value = Math.max(0, p[i] - q[i]);
|
|
1639
|
+
residual[i] = value;
|
|
1640
|
+
sum += value;
|
|
1641
|
+
}
|
|
1642
|
+
if (sum === 0) return p;
|
|
1643
|
+
for (let i = 0; i < residual.length; i++) residual[i] /= sum;
|
|
1644
|
+
return residual;
|
|
1645
|
+
}
|
|
1646
|
+
var MockLanguageModel = class {
|
|
1647
|
+
constructor(policy, vocabSize, latencyMs = 0) {
|
|
1648
|
+
this.policy = policy;
|
|
1649
|
+
this.vocabSize = vocabSize;
|
|
1650
|
+
this.latencyMs = latencyMs;
|
|
1651
|
+
}
|
|
1652
|
+
async prefill(tokens) {
|
|
1653
|
+
await delay(this.latencyMs);
|
|
1654
|
+
return { tokens: tokens.slice() };
|
|
1655
|
+
}
|
|
1656
|
+
async decode(state, tokens) {
|
|
1657
|
+
await delay(this.latencyMs);
|
|
1658
|
+
const logits = [];
|
|
1659
|
+
for (let i = 0; i <= tokens.length; i++) {
|
|
1660
|
+
logits.push(this.policy([...state.tokens, ...tokens.slice(0, i)]));
|
|
1661
|
+
}
|
|
1662
|
+
return { state: { tokens: [...state.tokens, ...tokens] }, logits };
|
|
1663
|
+
}
|
|
1664
|
+
truncate(state, length) {
|
|
1665
|
+
return { ...state, tokens: state.tokens.slice(0, length), cache: void 0 };
|
|
1666
|
+
}
|
|
1667
|
+
};
|
|
1668
|
+
function delay(ms) {
|
|
1669
|
+
if (ms <= 0) return Promise.resolve();
|
|
1670
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// src/onnx-lm.ts
|
|
1674
|
+
import * as ort3 from "onnxruntime-web";
|
|
1675
|
+
var OnnxCausalLM = class {
|
|
1676
|
+
constructor(session, vocabSize, options = {}) {
|
|
1677
|
+
this.session = session;
|
|
1678
|
+
if (!Number.isInteger(vocabSize) || vocabSize < 1) {
|
|
1679
|
+
throw new Error("vocabSize must be a positive integer");
|
|
1680
|
+
}
|
|
1681
|
+
this.vocabSize = vocabSize;
|
|
1682
|
+
this.inputIdsName = options.inputIdsName ?? "input_ids";
|
|
1683
|
+
this.attentionMaskName = options.attentionMaskName ?? "attention_mask";
|
|
1684
|
+
this.positionIdsName = options.positionIdsName;
|
|
1685
|
+
this.logitsName = options.logitsName ?? "logits";
|
|
1686
|
+
}
|
|
1687
|
+
async prefill(tokens) {
|
|
1688
|
+
if (tokens.length === 0) throw new Error("prefill requires a non-empty prompt");
|
|
1689
|
+
return { tokens: tokens.slice() };
|
|
1690
|
+
}
|
|
1691
|
+
async decode(state, tokens) {
|
|
1692
|
+
const all = [...state.tokens, ...tokens];
|
|
1693
|
+
if (all.length === 0) throw new Error("decode requires at least one token");
|
|
1694
|
+
if (state.tokens.length === 0) throw new Error("decode requires a prefilled state");
|
|
1695
|
+
const feeds = {
|
|
1696
|
+
[this.inputIdsName]: new ort3.Tensor(
|
|
1697
|
+
"int64",
|
|
1698
|
+
BigInt64Array.from(all, (token) => BigInt(token)),
|
|
1699
|
+
[1, all.length]
|
|
1700
|
+
),
|
|
1701
|
+
[this.attentionMaskName]: new ort3.Tensor(
|
|
1702
|
+
"int64",
|
|
1703
|
+
BigInt64Array.from({ length: all.length }, () => 1n),
|
|
1704
|
+
[1, all.length]
|
|
1705
|
+
)
|
|
1706
|
+
};
|
|
1707
|
+
if (this.positionIdsName) {
|
|
1708
|
+
feeds[this.positionIdsName] = new ort3.Tensor(
|
|
1709
|
+
"int64",
|
|
1710
|
+
BigInt64Array.from(all, (_, index) => BigInt(index)),
|
|
1711
|
+
[1, all.length]
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
const outputs = await this.session.run(feeds);
|
|
1715
|
+
const tensor = outputs[this.logitsName];
|
|
1716
|
+
if (!tensor) {
|
|
1717
|
+
throw new Error(`ONNX model did not return "${this.logitsName}"`);
|
|
1718
|
+
}
|
|
1719
|
+
const dims = tensor.dims;
|
|
1720
|
+
const vocab = dims[dims.length - 1];
|
|
1721
|
+
if (vocab !== this.vocabSize) {
|
|
1722
|
+
throw new Error(
|
|
1723
|
+
`ONNX logits vocabulary ${vocab} does not match configured ${this.vocabSize}`
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
const data = tensor.data;
|
|
1727
|
+
const seq = all.length;
|
|
1728
|
+
const expected = seq * vocab;
|
|
1729
|
+
if (data.length < expected) {
|
|
1730
|
+
throw new Error(`ONNX logits tensor is too small (${data.length} < ${expected})`);
|
|
1731
|
+
}
|
|
1732
|
+
const start = state.tokens.length - 1;
|
|
1733
|
+
const logits = [];
|
|
1734
|
+
for (let i = 0; i <= tokens.length; i++) {
|
|
1735
|
+
const position = start + i;
|
|
1736
|
+
const offset = dims.length === 3 ? position * vocab : position * vocab;
|
|
1737
|
+
logits.push(new Float32Array(data.subarray(offset, offset + vocab)));
|
|
1738
|
+
}
|
|
1739
|
+
return { state: { tokens: all }, logits };
|
|
1740
|
+
}
|
|
1741
|
+
truncate(state, length) {
|
|
1742
|
+
return { tokens: state.tokens.slice(0, length) };
|
|
1743
|
+
}
|
|
1744
|
+
};
|
|
1745
|
+
export {
|
|
1746
|
+
EdgeInfer,
|
|
1747
|
+
EventEmitter,
|
|
1748
|
+
ImageProcessor,
|
|
1749
|
+
IndexedDbVectorStore,
|
|
1750
|
+
MatryoshkaIndex,
|
|
1751
|
+
MemoryVectorStore,
|
|
1752
|
+
MockLanguageModel,
|
|
1753
|
+
ModelCache,
|
|
1754
|
+
OnnxCausalLM,
|
|
1755
|
+
RuntimeManager,
|
|
1756
|
+
SpeculativeDecoder,
|
|
1757
|
+
Tokenizer,
|
|
1758
|
+
createMatryoshkaEmbedder,
|
|
1759
|
+
dequantizeInt8,
|
|
1760
|
+
embedMatryoshka,
|
|
1761
|
+
l2Normalize,
|
|
1762
|
+
probability,
|
|
1763
|
+
quantizeInt8,
|
|
1764
|
+
residualDistribution,
|
|
1765
|
+
sample,
|
|
1766
|
+
softmax,
|
|
1767
|
+
truncateAndNormalize
|
|
1768
|
+
};
|
|
1769
|
+
//# sourceMappingURL=index.mjs.map
|