anyformer 0.4.2 → 0.4.3
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 +19 -0
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/backends/diskformer-llama.js +42 -15
- package/src/sources.js +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.3 — 2026-10-24
|
|
4
|
+
|
|
5
|
+
npm skips 0.4.2: this release carries it (Flint 40B and LFM2.5 24B on the hand engines).
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `diskformer-llama` loads split GGUFs (`name-00001-of-0000n.gguf`): `pickGguf` returns a set named by its first part,
|
|
9
|
+
with every part and their summed size; a set missing a part is refused. Each part downloads into OPFS under its own
|
|
10
|
+
file name and `load` names the first, as diskformer v0.6.5's worker expects. Ling 3.0 Flash (77 GB, 2 parts) now
|
|
11
|
+
routes to the llama.cpp path under the hand engine's 6.8 GB floor (diskformer v0.6.5: 6 GB, 4.1–4.9 tok/s, 6/9 vs
|
|
12
|
+
llama.cpp on the CPU) and stays on the hand engine above it. Ling 3.0 Flash through anyformer in a browser is not yet run.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- diskformer pinned to v0.6.6 (e07e129), whose llama.cpp worker loads split GGUFs.
|
|
16
|
+
- OPFS names for llama.cpp's GGUFs drop the repo's folders (gguf/<file name>); a file in a repo subfolder failed before.
|
|
17
|
+
|
|
18
|
+
### Verified
|
|
19
|
+
- Granite 4.0 H Tiny through `diskformer-llama` on v0.6.6, Chrome on the M4 Pro: 9/9 replies identical to llama.cpp, chat path equal to the reference.
|
|
20
|
+
- LocalMind's checks: through 0.4.1, LFM2.5 230M, Gemma 4 E2B and Ternary Bonsai 2 27B gave identical replies on 21/21 rows in both run orders, with decode within run-to-run spread; through 0.4.2, Flint 40B and LFM2.5 24B gave identical replies 3/3 in four runs, with equal GPU memory and equal or faster decode.
|
|
21
|
+
|
|
3
22
|
## 0.4.2 — 2026-10-23
|
|
4
23
|
|
|
5
24
|
### Added
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ browser storage; no account, no server, no telemetry.
|
|
|
14
14
|
| How | Command |
|
|
15
15
|
|---|---|
|
|
16
16
|
| npm | `npm install anyformer` |
|
|
17
|
-
| CDN | `import { load } from 'https://cdn.jsdelivr.net/npm/anyformer@0.
|
|
17
|
+
| CDN | `import { load } from 'https://cdn.jsdelivr.net/npm/anyformer@0.4.3/+esm';` |
|
|
18
18
|
| Copy | `index.js` and `src/`; nothing else is needed |
|
|
19
19
|
|
|
20
20
|
```js
|
|
@@ -59,7 +59,7 @@ back end's answer: yes with an estimate (GPU bytes, disk bytes, tok/s when measu
|
|
|
59
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
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
|
-
| `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 |
|
|
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…), split sets included; 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`
|
|
65
65
|
(for example `kohraBackend.source`) to load another copy. `BACKENDS` lists the back ends `load()` asks by default: each joins after its browser test passes.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anyformer",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
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",
|
|
@@ -30,22 +30,43 @@ export const CHECKED_FILES = [
|
|
|
30
30
|
'North-Mini-Code-1.0-UD-Q3_K_M.gguf', 'LFM2.5-24B-A2B-QAD-Q4_0.gguf', 'LFM2-8B-A1B-Q4_K_M.gguf',
|
|
31
31
|
'Mellum2-12B-A2.5B-Instruct-Q4_K_M.gguf', 'Ling-mini-2.0-Q4_K_M.gguf', 'granite-4.0-h-tiny-Q4_K_M.gguf',
|
|
32
32
|
'Ling-3.0-tiny-Q4_K_M.gguf', 'flint-int4-q4_1.gguf', 'OLMoE-1B-7B-0125-Instruct-Q4_K_M.gguf',
|
|
33
|
+
'Ling-3.0-flash-Q4_K_M-00001-of-00002.gguf',
|
|
33
34
|
];
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
const SPLIT = /-(\d{5})-of-(\d{5})\.gguf$/i;
|
|
37
|
+
|
|
38
|
+
// The GGUF to load, as { name, size, parts: [{ name, size }] }: the language model (no mmproj vision tower), one file or
|
|
39
|
+
// every part of a split set (name-00001-of-0000n.gguf, named by its first part; size is the sum). The caller's file
|
|
40
|
+
// when given; else the file diskformer checked; else the preferred quant, a single file before a split set.
|
|
37
41
|
export function pickGguf(model, file = null) {
|
|
38
|
-
const lms = model.files.gguf.filter((f) => !/mmproj/i.test(f.name)
|
|
39
|
-
|
|
40
|
-
const
|
|
42
|
+
const lms = model.files.gguf.filter((f) => !/mmproj/i.test(f.name));
|
|
43
|
+
const sets = new Map();
|
|
44
|
+
for (const f of lms.filter((x) => SPLIT.test(x.name))) {
|
|
45
|
+
const base = f.name.replace(SPLIT, '');
|
|
46
|
+
sets.set(base, [...(sets.get(base) || []), f]);
|
|
47
|
+
}
|
|
48
|
+
const candidates = [
|
|
49
|
+
...lms.filter((f) => !SPLIT.test(f.name)).map((f) => ({ name: f.name, size: f.size, parts: [f] })),
|
|
50
|
+
// A set missing a part cannot load.
|
|
51
|
+
...[...sets.values()].filter((p) => p.length === Number(SPLIT.exec(p[0].name)[2])).map((p) => {
|
|
52
|
+
const parts = [...p].sort((a, b) => a.name.localeCompare(b.name));
|
|
53
|
+
return { name: parts[0].name, size: parts.reduce((n, f) => n + f.size, 0), parts };
|
|
54
|
+
}),
|
|
55
|
+
];
|
|
56
|
+
if (file) return candidates.find((f) => f.name === file) || null;
|
|
57
|
+
const checked = candidates.find((f) => CHECKED_FILES.includes(opfsName(f)));
|
|
41
58
|
if (checked) return checked;
|
|
42
59
|
for (const q of QUANTS) {
|
|
43
|
-
const f =
|
|
60
|
+
const f = candidates.find((x) => new RegExp(`(^|[-_.])${q}([-_.]|$)`, 'i').test(x.name.replace(SPLIT, '').replace(/\.gguf$/i, '')));
|
|
44
61
|
if (f) return f;
|
|
45
62
|
}
|
|
46
63
|
return null;
|
|
47
64
|
}
|
|
48
65
|
|
|
66
|
+
// llama.cpp's GGUFs sit in OPFS as gguf/<file name>, without the repo's folders: the worker finds a split set's other
|
|
67
|
+
// parts by name beside the first.
|
|
68
|
+
const opfsName = (f) => f.name.split('/').pop();
|
|
69
|
+
|
|
49
70
|
// OPFS keeps llama.cpp's GGUFs by file name (gguf/<name>), with no repo. anyformer records name → repo here, so
|
|
50
71
|
// stored() can say which repo a file belongs to.
|
|
51
72
|
export const LLAMA_INDEX = 'anyformer-llama.json';
|
|
@@ -82,7 +103,7 @@ export const diskformerLlamaBackend = {
|
|
|
82
103
|
if (task && !diskformerLlamaBackend.tasks.includes(task)) return { ok: false, code: 'task', reason: `task ${task}; diskformer's llama.cpp path runs text generation only` };
|
|
83
104
|
if (!LLAMA_ARCHITECTURES.includes(model.architecture)) return { ok: false, code: 'model', reason: `architecture ${model.architecture || 'unknown'} is not one diskformer's llama.cpp path has checked (${LLAMA_ARCHITECTURES.join(', ')})` };
|
|
84
105
|
const f = pickGguf(model, file);
|
|
85
|
-
if (!f) return { ok: false, code: 'model', reason: file ? `${file} is not a
|
|
106
|
+
if (!f) return { ok: false, code: 'model', reason: file ? `${file} is not a language-model GGUF in the repo (a split set is named by its first part, with every part present)` : `no GGUF at ${QUANTS.join(', ')} in the repo (a split set needs every part)` };
|
|
86
107
|
if (!caps.webgpu) return { ok: false, code: 'webgpu', reason: 'no WebGPU adapter' };
|
|
87
108
|
if (!caps.memory64 || !caps.jspi) return { ok: false, code: 'webgpu', reason: 'the llama.cpp build needs wasm memory64 and JSPI (Chrome or Edge 137+)' };
|
|
88
109
|
if (!caps.opfs) return { ok: false, code: 'opfs', reason: 'no OPFS (navigator.storage.getDirectory)' };
|
|
@@ -100,16 +121,22 @@ export const diskformerLlamaBackend = {
|
|
|
100
121
|
signal?.addEventListener('abort', stop, { once: true });
|
|
101
122
|
let info;
|
|
102
123
|
try {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
124
|
+
// Each part downloads under its own name; progress counts bytes over the whole set.
|
|
125
|
+
let before = 0;
|
|
126
|
+
for (const part of f.parts) {
|
|
127
|
+
const name = opfsName(part);
|
|
128
|
+
const have = (await llama.list()).find((m) => m.name === name && !m.partial);
|
|
129
|
+
if (!have || have.size !== part.size) {
|
|
130
|
+
say({ status: 'download', loaded: before, total: f.size });
|
|
131
|
+
await llama.download({ url: diskformerLlamaBackend.url(model, part), name, size: part.size },
|
|
132
|
+
(p) => say({ status: 'download', loaded: before + p.copied, total: f.size }));
|
|
133
|
+
signal?.throwIfAborted();
|
|
134
|
+
}
|
|
135
|
+
before += part.size;
|
|
109
136
|
}
|
|
110
|
-
await writeIndex({ ...(await readIndex()),
|
|
137
|
+
await writeIndex({ ...(await readIndex()), ...Object.fromEntries(f.parts.map((p) => [opfsName(p), model.repo])) });
|
|
111
138
|
say({ status: 'load' });
|
|
112
|
-
info = await llama.load({ name: f
|
|
139
|
+
info = await llama.load({ name: opfsName(f), budget, ctx: maxCtx }, (p) => say({ status: 'plan', plan: p }));
|
|
113
140
|
signal?.throwIfAborted();
|
|
114
141
|
} catch (e) {
|
|
115
142
|
llama.terminate();
|
package/src/sources.js
CHANGED
|
@@ -7,9 +7,9 @@ export const PINS = {
|
|
|
7
7
|
transformers: '4.3.1',
|
|
8
8
|
wllama: '3.4.1', // LocalMind's pin; its stream-drain workaround (src/backends/wllama.js) is for this version
|
|
9
9
|
kohra: 'f678fac7342c79b58b640540b8b3a7d078b7ab1a',
|
|
10
|
-
// v0.6.
|
|
11
|
-
// Gemma 4 E2B/E4B and Ternary Bonsai 2) under engines/, and the llama.cpp path under ggml
|
|
12
|
-
diskformer: '
|
|
10
|
+
// v0.6.6: every engine (the disk-tier MoE engines, Ling 3.0 Flash, and LocalMind's whole-model engines LFM2.5,
|
|
11
|
+
// Gemma 4 E2B/E4B and Ternary Bonsai 2) under engines/, and the llama.cpp path under ggml/, which loads split GGUFs.
|
|
12
|
+
diskformer: 'e07e12975df054b073672ba2060d0d2c5571dbcd',
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
const gh = (repo, sha, path) => `https://cdn.jsdelivr.net/gh/NakliTechie/${repo}@${sha}/${path}`;
|