anyformer 0.3.2 → 0.4.2
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/CHANGELOG.md +46 -0
- package/README.md +3 -3
- package/package.json +1 -1
- package/src/backends/diskformer.js +15 -0
- package/src/backends/localmind.js +47 -12
- package/src/backends/speculative.js +96 -0
- package/src/load.js +11 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.2 — 2026-10-23
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Flint 40B-A2.5B (RecursiveSI/flint-int4-gguf, Q4_1, 26.0 GB) and LFM2.5 24B-A2B (LiquidAI/LFM2.5-24B-A2B-GGUF,
|
|
7
|
+
QAD-Q4_0, 13.6 GB) on the `diskformer` back end, through diskformer's `FlintMoeSsd` and `Lfm2MoeSsd` hand engines.
|
|
8
|
+
Before, both routed to the llama.cpp path (Flint 1.8–3.4 tok/s there against 8.66 on its engine). Numbers from
|
|
9
|
+
LocalMind's gates: Flint ~7.3 GB on the GPU with a 4 GiB pool, 8.66 tok/s; LFM2.5 24B ~16 tok/s at a 4 GiB pool.
|
|
10
|
+
Floors of 4 GB are unmeasured. Not yet run through anyformer in a browser; LocalMind's live check is that check.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- README: the `localmind` back end's kernels ship in diskformer.
|
|
14
|
+
|
|
15
|
+
## 0.4.1 — 2026-10-23
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- `session.speculative` and `session.pleMode` were copied once when `load()` returned, because the session object was
|
|
19
|
+
built with spreads, which read getters. A Bonsai session's `speculative.status` stayed `'loading'` after DFlash or
|
|
20
|
+
MTP attached. They are now read live. A test through `load()` failed on 0.4.0 and passes now.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- `session.engine`: the engine instance the `diskformer` and `localmind` back ends built, read-only, for token-level
|
|
24
|
+
work a caller already does with it (Bonsai 2's tokenizer and think-marker ids in LocalMind's worker). Calls on it
|
|
25
|
+
bypass the session's one-call-at-a-time queue.
|
|
26
|
+
|
|
27
|
+
## 0.4.0 — 2026-10-23
|
|
28
|
+
|
|
29
|
+
What LocalMind's own workers do with its last three engine families (LFM2.5, Gemma 4 E2B/E4B, Ternary Bonsai 2 27B),
|
|
30
|
+
on the `localmind` back end, so those models can move onto anyformer with nothing lost.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
- `load(repo, { fetch })`: the engines' fetch, for a caller's retrying fetch on multi-GB range reads.
|
|
34
|
+
- `load(repo, { ple: true })` for Gemma 4 E2B/E4B: the per-layer-embedding table streams from OPFS (diskformer's
|
|
35
|
+
`ple-opfs.js`); `session.pleMode` reports the mode.
|
|
36
|
+
- `load(repo, { speculative })` for Ternary Bonsai 2 27B: `{ dflash: { block } }` (DFlash 2) or
|
|
37
|
+
`{ mtp: { depth, adaptive } }`, attached after the model is ready so the first reply never waits for it. The drafter
|
|
38
|
+
and the MTP head download once into Cache Storage, under LocalMind's names and keys, so a browser that holds them
|
|
39
|
+
reuses them. `session.speculative` gives `{ status, error, stats }`. Output is greedy-identical either way.
|
|
40
|
+
- `generate(messages, { thinking, assistantPrefix })` on the `localmind` back end: thinking per request (Bonsai's
|
|
41
|
+
template, `reasoning_effort: 'medium'`) and a raw prefix to continue from; chunks carry `token`.
|
|
42
|
+
- `session.reset()` where the engine has it, queued with `generate()`.
|
|
43
|
+
|
|
44
|
+
### Measured
|
|
45
|
+
- LFM2.5 230M through the `localmind` back end: the same reply as before these changes. Node tests cover each option
|
|
46
|
+
with stand-in engines (83/83). Gemma's PLE table and Bonsai's DFlash and MTP have not run through anyformer in a
|
|
47
|
+
browser; LocalMind's A/B per model is that check.
|
|
48
|
+
|
|
3
49
|
## 0.3.2 — 2026-10-23
|
|
4
50
|
|
|
5
51
|
### Changed
|
package/README.md
CHANGED
|
@@ -56,9 +56,9 @@ back end's answer: yes with an estimate (GPU bytes, disk bytes, tok/s when measu
|
|
|
56
56
|
|---|---|---|
|
|
57
57
|
| `transformers` | transformers.js 4.3.1: text generation, image and audio chat (Gemma 4, Qwen3.5), embeddings, speech to text, detection, classification, depth, segmentation | ONNX repos in the onnx-community / Xenova layout |
|
|
58
58
|
| `kohra` | kohra, text-diffusion over onnxruntime-web | MDLM and BD3LM repos tagged `text-diffusion` |
|
|
59
|
-
| `localmind` | LocalMind's hand-written WebGPU kernels, the whole model on the GPU | any LFM2 / LFM2.5 GGUF; Gemma 4 E2B / E4B QAT-mobile; Ternary Bonsai 2 |
|
|
59
|
+
| `localmind` | LocalMind's hand-written WebGPU kernels (shipped in diskformer), the whole model on the GPU | any LFM2 / LFM2.5 GGUF; Gemma 4 E2B / E4B QAT-mobile; Ternary Bonsai 2 |
|
|
60
60
|
| `wllama` | llama.cpp in WebAssembly: WebGPU when the budget holds the model, the CPU otherwise (no WebGPU needed) | any GGUF up to about 3.6 GB that no engine above covers; files over 2 GB are split into OPFS shards once |
|
|
61
|
-
| `diskformer` | diskformer engines: weights in OPFS, only what a step needs on the GPU | Gemma 4 26B-A4B (from 2.5 GB), Qwen3.6 35B-A3B (from 4 GB), Ling 3.0 Flash 124B (from 6.8 GB), MiniCPM-V 4.7 with images (from 4 GB) |
|
|
61
|
+
| `diskformer` | diskformer engines: weights in OPFS, only what a step needs on the GPU | Gemma 4 26B-A4B (from 2.5 GB), Qwen3.6 35B-A3B (from 4 GB), Ling 3.0 Flash 124B (from 6.8 GB), MiniCPM-V 4.7 with images, Flint 40B, LFM2.5 24B (from 4 GB) |
|
|
62
62
|
| `diskformer-llama` | diskformer's llama.cpp build with WebGPU, mixture-of-experts layers paged in from OPFS | GGUFs of the 17 MoE architectures diskformer has checked (Granite 4.0 H, gpt-oss, GLM-4.7-Flash, LFM2 MoE…); a hand engine above wins where it covers the model |
|
|
63
63
|
|
|
64
64
|
Each runtime loads from an exact version or commit in [src/sources.js](src/sources.js); set a back end's `.source`
|
|
@@ -76,7 +76,7 @@ and `threw`. Every other failure is an `AnyformerError` too, with a code from `E
|
|
|
76
76
|
|
|
77
77
|
```js
|
|
78
78
|
plan(repo, { budget, task }) // what would run it, and why the others would not; loads nothing
|
|
79
|
-
load(repo, { budget, task, file, maxCtx, poolBytes, localFile, onProgress, signal }) // → { backend, task, plan, run(input), generate?, think?, prime?, dispose() }
|
|
79
|
+
load(repo, { budget, task, file, maxCtx, poolBytes, localFile, fetch, ple, speculative, onProgress, signal }) // → { backend, task, plan, run(input), generate?, think?, prime?, reset?, speculative?, dispose() }
|
|
80
80
|
pipeline(task, repo, { budget }) // transformers.js's call shape: gen(messages) → [{ generated_text }]
|
|
81
81
|
probe() // WebGPU, shader-f16, subgroups, limits, OPFS quota, memory64
|
|
82
82
|
describe(repo) // Hub repo → formats, architecture, task, tags, files
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anyformer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "One call to run a model in a browser tab: anyformer reads the Hub repo, probes the browser, and picks the back end (transformers.js, LocalMind's WebGPU kernels, diskformer, kohra) that fits the model's format, architecture and your GPU budget.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
@@ -27,6 +27,21 @@ export const DISKFORMER_MODELS = [
|
|
|
27
27
|
engine: 'qwen35_moe_ssd.js', Engine: 'MiniCpmV47Ssd', minBudget: 4e9,
|
|
28
28
|
speeds: [[6.1e9, 14.1]],
|
|
29
29
|
},
|
|
30
|
+
// From LocalMind's gates (M4 Pro, 4 GiB pool; plan/2026-10-22-flint-harness): Flint ~7.3 GB on the GPU at maxCtx
|
|
31
|
+
// 8192 (dense 2.33, pool 4.29, KV 0.41, batch 0.20), 8.66 tok/s at lookahead 1. LFM2.5 24B ~16 tok/s at a 4 GiB
|
|
32
|
+
// pool; its total GPU use was not recorded, so it carries no speed estimate. The smallest working budget of either
|
|
33
|
+
// is unmeasured: 4 GB is a floor, and the engine refuses a smaller fit with the need. Both files are also on the
|
|
34
|
+
// llama.cpp path's checked list; this back end ranks first (Flint on llama.cpp: 1.8–3.4 tok/s).
|
|
35
|
+
{
|
|
36
|
+
repo: 'RecursiveSI/flint-int4-gguf', file: 'flint-int4-q4_1.gguf', size: 25992966976,
|
|
37
|
+
engine: 'flint_moe_ssd.js', Engine: 'FlintMoeSsd', minBudget: 4e9,
|
|
38
|
+
speeds: [[7.3e9, 8.66]],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
repo: 'LiquidAI/LFM2.5-24B-A2B-GGUF', file: 'LFM2.5-24B-A2B-QAD-Q4_0.gguf', size: 13578190880,
|
|
42
|
+
engine: 'lfm2_moe_ssd.js', Engine: 'Lfm2MoeSsd', minBudget: 4e9,
|
|
43
|
+
speeds: [],
|
|
44
|
+
},
|
|
30
45
|
// Two parts, ingested as one file by the engine's splitFetch (engines/ling_moe_ssd.js LING_FLASH, rev 29edce5).
|
|
31
46
|
// Budget and speed from LocalMind's gate (plan/2026-10-22-ling-harness/results.md): 2.51 GB of dense weights on
|
|
32
47
|
// the GPU plus a 4.29 GB expert pool, 5.53 tok/s decode; the OPFS store is 80.7 GB.
|
|
@@ -72,23 +72,33 @@ export const localmindBackend = {
|
|
|
72
72
|
return { ok: true, estimate: { gpuBytes: m.bytes, diskBytes: m.bytes, tokPerSec: MEASURED[model.repo] ?? null } };
|
|
73
73
|
},
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
// Options beyond the shared ones, as LocalMind's workers use the engines:
|
|
76
|
+
// fetch the engines' fetch (a caller's retrying fetch for multi-GB range reads)
|
|
77
|
+
// ple Gemma 4: stream the per-layer-embedding table from OPFS (ple-opfs.js) instead of holding it
|
|
78
|
+
// speculative Ternary Bonsai 2: { dflash: { block } } or { mtp: { depth, adaptive } }, attached after the model is
|
|
79
|
+
// ready, so the first reply never waits for the drafter's download (src/backends/speculative.js)
|
|
80
|
+
async load(model, { maxCtx, fetch, ple, speculative, onProgress, signal }) {
|
|
76
81
|
const m = match(model);
|
|
77
82
|
const e = ENGINES[m.engine];
|
|
78
|
-
const
|
|
83
|
+
const engines = localmindBackend.source;
|
|
84
|
+
const mod = await import(`${engines}/${e.file}`);
|
|
85
|
+
const say = onProgress && ((p) => onProgress({ backend: 'localmind', engine: m.engine, ...p }));
|
|
86
|
+
let pleTable = null;
|
|
87
|
+
if (ple && m.engine === 'gemma4') {
|
|
88
|
+
const { createGemmaPle } = await import(`${engines}/ple-opfs.js`);
|
|
89
|
+
pleTable = await createGemmaPle({ source: { repo: model.repo }, onStatus: (s) => say?.({ status: 'ple', ...s }) });
|
|
90
|
+
}
|
|
79
91
|
const opts = {
|
|
80
|
-
revision: model.revision || 'main', signal,
|
|
81
|
-
|
|
92
|
+
revision: model.revision || 'main', signal, onProgress: say,
|
|
93
|
+
...(fetch && { fetch }),
|
|
94
|
+
...(pleTable && { ple: pleTable }),
|
|
82
95
|
...(m.engine !== 'gemma4' && { file: m.file }),
|
|
83
96
|
...(maxCtx && m.engine !== 'gemma4' && { maxLength: maxCtx }),
|
|
84
97
|
};
|
|
85
98
|
const engine = await mod[e.Engine].load(model.repo, opts);
|
|
86
99
|
signal?.throwIfAborted();
|
|
87
|
-
// Thinking off, as the diskformer back end does: a chat reply, not the model's reasoning. (The qwen35 engine
|
|
88
|
-
// takes template arguments as a field; LocalMind sets the same one.)
|
|
89
|
-
if ('chatTemplateArgs' in engine) engine.chatTemplateArgs = { ...engine.chatTemplateArgs, enable_thinking: false };
|
|
90
100
|
// Compile the kernels now, as LocalMind's workers do, so the first reply is not paying for it.
|
|
91
|
-
|
|
101
|
+
say?.({ status: 'warmup' });
|
|
92
102
|
await engine.warmup?.();
|
|
93
103
|
// Gemma's template has no system turn; LocalMind folds the system prompt into the first user turn the same way.
|
|
94
104
|
const prepare = m.engine !== 'gemma4' ? (msgs) => msgs : (msgs) => {
|
|
@@ -97,18 +107,43 @@ export const localmindBackend = {
|
|
|
97
107
|
const i = rest.findIndex((x) => x.role === 'user');
|
|
98
108
|
return sys && i >= 0 ? rest.map((x, k) => (k === i ? { ...x, content: `${sys}\n\n${x.content}` } : x)) : rest;
|
|
99
109
|
};
|
|
110
|
+
// Speculative decoding, Bonsai only, in the background: a reply that starts before it lands decodes plainly.
|
|
111
|
+
const spec = { status: speculative && m.engine === 'qwen35' ? 'loading' : 'off', runner: null, error: null, disposed: false };
|
|
112
|
+
if (spec.status === 'loading') {
|
|
113
|
+
import('./speculative.js')
|
|
114
|
+
.then(({ attachSpeculative }) => attachSpeculative(engine, speculative, { engines, fetch, signal, onProgress: (p) => say?.({ ...p, speculative: true }) }))
|
|
115
|
+
.then((runner) => {
|
|
116
|
+
// Disposed while the drafter downloaded: detach and free it rather than attach to a dead engine.
|
|
117
|
+
if (spec.disposed) { engine.model.dflashRunner = null; runner.dispose?.(); return; }
|
|
118
|
+
spec.runner = runner; spec.status = 'ready';
|
|
119
|
+
}, (err) => { spec.error = err.message; spec.status = 'error'; });
|
|
120
|
+
}
|
|
100
121
|
let tokens = 0;
|
|
101
122
|
return {
|
|
102
123
|
task: 'text-generation',
|
|
103
124
|
engine,
|
|
104
|
-
|
|
125
|
+
// Gemma 4 with the OPFS table: 'resident' or the streamed mode the engine reports.
|
|
126
|
+
...(m.engine === 'gemma4' && { get pleMode() { return engine.pleMode ?? null; } }),
|
|
127
|
+
// { status: 'off' | 'loading' | 'ready' | 'error', error, stats }; stats are the runner's counters.
|
|
128
|
+
get speculative() { return { status: spec.status, error: spec.error, stats: spec.runner?.stats ?? null }; },
|
|
129
|
+
// thinking: per request, through the qwen35 engine's template arguments (reasoning_effort 'medium', LocalMind's
|
|
130
|
+
// serving recipe); the other two engines have no thinking switch. assistantPrefix: raw text the reply continues
|
|
131
|
+
// from (Bonsai's think-budget continuation). Chunks carry the engine's token id.
|
|
132
|
+
async *generate(messages, { maxNewTokens = 512, signal, thinking = false, assistantPrefix } = {}) {
|
|
133
|
+
if ('chatTemplateArgs' in engine) engine.chatTemplateArgs = { ...engine.chatTemplateArgs, enable_thinking: thinking === true, reasoning_effort: 'medium' };
|
|
105
134
|
tokens = 0;
|
|
106
|
-
for await (const c of engine.generate(prepare(messages), { maxNewTokens, signal })) {
|
|
135
|
+
for await (const c of engine.generate(prepare(messages), { maxNewTokens, signal, ...(assistantPrefix != null && { assistantPrefix }) })) {
|
|
107
136
|
if (c.token != null) tokens++;
|
|
108
|
-
yield { text: c.text, tokens };
|
|
137
|
+
yield { text: c.text, token: c.token, tokens };
|
|
109
138
|
}
|
|
110
139
|
},
|
|
111
|
-
|
|
140
|
+
// Drops the KV and recurrent state, for a new conversation or a shorter history.
|
|
141
|
+
...(typeof engine.reset === 'function' && { reset: () => engine.reset() }),
|
|
142
|
+
dispose: () => {
|
|
143
|
+
spec.disposed = true;
|
|
144
|
+
if (spec.runner) { engine.model.dflashRunner = null; spec.runner.dispose?.(); }
|
|
145
|
+
return engine.dispose?.();
|
|
146
|
+
},
|
|
112
147
|
};
|
|
113
148
|
},
|
|
114
149
|
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Speculative decoding for Ternary Bonsai 2 27B: DFlash 2 (a 5-layer drafter) or MTP (one extra decoder layer that
|
|
2
|
+
// drafts ahead). Both are greedy-identical to the engine's own decode; they only change how many tokens one verify
|
|
3
|
+
// pass accepts. The runners live in diskformer (engines/ternary_bonsai_2_dflash.js and ternary_bonsai_2_mtp.js) and
|
|
4
|
+
// attach to the engine through its specDecodeRunner() seam (engine.model.dflashRunner).
|
|
5
|
+
//
|
|
6
|
+
// Their weights are separate downloads, kept in Cache Storage under the names and keys LocalMind uses, so a browser
|
|
7
|
+
// that already holds them from LocalMind reuses them:
|
|
8
|
+
// 'dflash-drafter-v1' the drafter GGUF (1.14 GB), keyed by DRAFTER_URL
|
|
9
|
+
// 'mtp-head-v1' the MTP head's byte ranges (451 MB) of a grafted GGUF, keyed by MTP_HEAD.url
|
|
10
|
+
|
|
11
|
+
// LocalMind's drafter: naklitechie/Qwen3.8-27B-DFlash2-ternary-bonsai2 (index.html DRAFTER_URL, main febbc93).
|
|
12
|
+
export const DRAFTER_URL = 'https://huggingface.co/naklitechie/Qwen3.8-27B-DFlash2-ternary-bonsai2/resolve/main/Qwen3.8-27B-DFlash2-r3-Q4_K_M.gguf';
|
|
13
|
+
export const DRAFTER_CACHE = 'dflash-drafter-v1';
|
|
14
|
+
export const MTP_CACHE = 'mtp-head-v1';
|
|
15
|
+
|
|
16
|
+
// One file's byte ranges ([[start, length], …], or the whole file) as a Blob, from Cache Storage or downloaded in 32 MB
|
|
17
|
+
// pieces, 8 in flight (Hugging Face's CDN throttles each connection), then stored. A Blob keeps a 1 GB file on disk
|
|
18
|
+
// rather than in the JS heap.
|
|
19
|
+
export async function cachedBlob({ cacheName, url, ranges = null, fileSize = null, fetch = globalThis.fetch, onProgress, signal }) {
|
|
20
|
+
const cache = await caches.open(cacheName);
|
|
21
|
+
const want = ranges ? ranges.reduce((n, [, len]) => n + len, 0) : null;
|
|
22
|
+
const hit = await cache.match(url);
|
|
23
|
+
if (hit) { const b = await hit.blob(); if (want === null || b.size === want) return b; }
|
|
24
|
+
if (!ranges) {
|
|
25
|
+
const probe = await fetch(url, { headers: { Range: 'bytes=0-0' }, signal });
|
|
26
|
+
if (probe.status !== 206) throw new Error(`${url}: HTTP ${probe.status}; the server must support Range`);
|
|
27
|
+
fileSize = Number((probe.headers.get('content-range') || '').split('/')[1]) || 0;
|
|
28
|
+
try { await probe.body?.cancel(); } catch { /* nothing to cancel */ }
|
|
29
|
+
if (!fileSize) throw new Error(`${url}: unknown size`);
|
|
30
|
+
ranges = [[0, fileSize]];
|
|
31
|
+
}
|
|
32
|
+
const PIECE = 32 << 20, STREAMS = 8;
|
|
33
|
+
const jobs = [];
|
|
34
|
+
for (const [start, len] of ranges) for (let o = 0; o < len; o += PIECE) jobs.push([start + o, Math.min(PIECE, len - o)]);
|
|
35
|
+
const total = jobs.reduce((n, [, len]) => n + len, 0);
|
|
36
|
+
const pieces = new Array(jobs.length);
|
|
37
|
+
let next = 0, loaded = 0;
|
|
38
|
+
const worker = async () => {
|
|
39
|
+
for (;;) {
|
|
40
|
+
const i = next++;
|
|
41
|
+
if (i >= jobs.length) return;
|
|
42
|
+
const [start, len] = jobs[i];
|
|
43
|
+
const r = await fetch(url, { headers: { Range: `bytes=${start}-${start + len - 1}` }, signal });
|
|
44
|
+
if (r.status !== 206) throw new Error(`${url}: HTTP ${r.status} on bytes ${start}+${len}`);
|
|
45
|
+
if (fileSize && Number((r.headers.get('content-range') || '').split('/')[1]) !== fileSize) throw new Error(`${url}: the file is not ${fileSize} bytes`);
|
|
46
|
+
const buf = new Uint8Array(await r.arrayBuffer());
|
|
47
|
+
if (buf.byteLength !== len) throw new Error(`${url}: short read on bytes ${start}+${len}`);
|
|
48
|
+
pieces[i] = buf;
|
|
49
|
+
loaded += len;
|
|
50
|
+
onProgress?.({ loaded, total });
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
await Promise.all(Array.from({ length: STREAMS }, worker));
|
|
54
|
+
const blob = new Blob(pieces, { type: 'application/octet-stream' });
|
|
55
|
+
// A failed write only costs a download next time.
|
|
56
|
+
await cache.put(url, new Response(blob, { headers: { 'content-type': 'application/octet-stream', 'content-length': String(blob.size) } })).catch(() => {});
|
|
57
|
+
return blob;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const blobSource = (blob) => async (off, len) => new Uint8Array(await blob.slice(off, off + len).arrayBuffer());
|
|
61
|
+
|
|
62
|
+
// Attaches a runner to a loaded Ternary Bonsai 2 engine. spec: { dflash: { block = 5 } } or
|
|
63
|
+
// { mtp: { depth = 2, adaptive = false } }. engines: the diskformer engines/ URL. Returns the runner; its .stats
|
|
64
|
+
// counts cycles, tokens and accepted drafts (MTP adds drafted and plainTokens).
|
|
65
|
+
export async function attachSpeculative(engine, spec, { engines, fetch, onProgress, signal } = {}) {
|
|
66
|
+
if (spec.dflash) {
|
|
67
|
+
const { DFlashRunner } = await import(`${engines}/ternary_bonsai_2_dflash.js`);
|
|
68
|
+
const blob = await cachedBlob({ cacheName: DRAFTER_CACHE, url: DRAFTER_URL, fetch, signal,
|
|
69
|
+
onProgress: (p) => onProgress?.({ status: 'download', kind: 'dflash', ...p }) });
|
|
70
|
+
signal?.throwIfAborted();
|
|
71
|
+
const runner = await DFlashRunner.create(engine, { drafter: blobSource(blob), block: spec.dflash.block ?? 5,
|
|
72
|
+
onProgress: (p) => onProgress?.({ status: 'load', kind: 'dflash', loaded: p.done, total: p.total }) });
|
|
73
|
+
engine.model.dflashRunner = runner;
|
|
74
|
+
return runner;
|
|
75
|
+
}
|
|
76
|
+
if (spec.mtp) {
|
|
77
|
+
const { MTPRunner, MTP_HEAD } = await import(`${engines}/ternary_bonsai_2_mtp.js`);
|
|
78
|
+
const blob = await cachedBlob({ cacheName: MTP_CACHE, url: MTP_HEAD.url, ranges: MTP_HEAD.ranges, fileSize: MTP_HEAD.fileSize, fetch, signal,
|
|
79
|
+
onProgress: (p) => onProgress?.({ status: 'download', kind: 'mtp', ...p }) });
|
|
80
|
+
signal?.throwIfAborted();
|
|
81
|
+
// Absolute file offsets → offsets in the blob, which holds the ranges back to back.
|
|
82
|
+
const at = [];
|
|
83
|
+
let acc = 0;
|
|
84
|
+
for (const [start, len] of MTP_HEAD.ranges) { at.push([start, len, acc]); acc += len; }
|
|
85
|
+
const head = async (off, len) => {
|
|
86
|
+
const r = at.find(([s, l]) => off >= s && off + len <= s + l);
|
|
87
|
+
if (!r) throw new Error(`MTP head: bytes ${off}+${len} are outside the downloaded ranges`);
|
|
88
|
+
return blobSource(blob)(r[2] + off - r[0], len);
|
|
89
|
+
};
|
|
90
|
+
const runner = await MTPRunner.create(engine, { head, depth: spec.mtp.depth ?? 2, adaptive: !!spec.mtp.adaptive,
|
|
91
|
+
onProgress: (p) => onProgress?.({ status: 'load', kind: 'mtp', loaded: p.done, total: p.total }) });
|
|
92
|
+
engine.model.dflashRunner = runner;
|
|
93
|
+
return runner;
|
|
94
|
+
}
|
|
95
|
+
throw new Error('speculative: pass { dflash: { block } } or { mtp: { depth, adaptive } }');
|
|
96
|
+
}
|
package/src/load.js
CHANGED
|
@@ -41,16 +41,17 @@ export async function plan(repoOrEntry, opts = {}) {
|
|
|
41
41
|
|
|
42
42
|
export async function load(repoOrEntry, options = {}) {
|
|
43
43
|
const [repo, opts] = entry(repoOrEntry, options);
|
|
44
|
-
const { backends = BACKENDS, maxCtx, dtype, poolBytes, localFile, onProgress, signal } = opts;
|
|
44
|
+
const { backends = BACKENDS, maxCtx, dtype, poolBytes, localFile, fetch: engineFetch, ple, speculative, onProgress, signal } = opts;
|
|
45
45
|
let p;
|
|
46
46
|
try { p = await plan(repo, opts); } catch (e) { throw wrap(e, 'LOAD_FAILED', signal, `${repo}: `); }
|
|
47
47
|
if (!p.chosen) throw noBackend(p);
|
|
48
48
|
const backend = backends.find((b) => b.id === p.chosen);
|
|
49
49
|
let session;
|
|
50
50
|
try {
|
|
51
|
-
session = await backend.load(p.model, { budget: p.budget, task: p.task, file: p.file, caps: p.caps, maxCtx, dtype, poolBytes, localFile, onProgress, signal });
|
|
51
|
+
session = await backend.load(p.model, { budget: p.budget, task: p.task, file: p.file, caps: p.caps, maxCtx, dtype, poolBytes, localFile, fetch: engineFetch, ple, speculative, onProgress, signal });
|
|
52
52
|
} catch (e) { throw wrap(e, 'LOAD_FAILED', signal, `${backend.id}: `); }
|
|
53
|
-
|
|
53
|
+
// Added onto serialize()'s object, not spread: a spread would read its getters once and freeze them.
|
|
54
|
+
return Object.assign(serialize(session), { backend: backend.id, task: session.task || p.task || p.model.task || null, ...(session.device && { device: session.device }), plan: p });
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
// content: a string, or an array of { type, … } blocks (text, image, audio) for multimodal models.
|
|
@@ -97,12 +98,13 @@ function serialize(session) {
|
|
|
97
98
|
release();
|
|
98
99
|
}
|
|
99
100
|
};
|
|
100
|
-
|
|
101
|
+
const out = {
|
|
101
102
|
...(generate && { generate }),
|
|
102
103
|
// Where reasoning starts and ends as token ids ({ openId, closeId, inPrompt, preamble, always }), or null.
|
|
103
104
|
...('think' in session && { think: session.think }),
|
|
104
105
|
...(session.prime && { prime: queued(session.prime) }),
|
|
105
106
|
...(session.ensureVision && { ensureVision: queued(session.ensureVision) }),
|
|
107
|
+
...(session.reset && { reset: queued(session.reset) }),
|
|
106
108
|
// The one call every task has. For text generation it is the last chunk of generate().
|
|
107
109
|
async run(input, o = {}) {
|
|
108
110
|
if (generate) { let last; for await (const c of generate(input, o)) last = c; return last; }
|
|
@@ -124,4 +126,9 @@ function serialize(session) {
|
|
|
124
126
|
await session.dispose?.();
|
|
125
127
|
},
|
|
126
128
|
};
|
|
129
|
+
// Read live from the back end's session, never copied: the engine instance it built (diskformer and localmind back
|
|
130
|
+
// ends; read-only, for token-level work a caller already does with the engine; calls on it bypass the queue),
|
|
131
|
+
// Gemma 4's PLE mode, and Bonsai's speculative decoding state, which changes after load.
|
|
132
|
+
for (const k of ['engine', 'pleMode', 'speculative']) if (k in session) Object.defineProperty(out, k, { get: () => session[k], enumerable: true });
|
|
133
|
+
return out;
|
|
127
134
|
}
|