diskformer 0.6.6 → 0.6.7

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 CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.7 (2026-10-24)
4
+ - Ingest on a slow or flaky link (`src/ingest.js`, every hand engine's download): the GGUF header streams with
5
+ `ingest-header` progress instead of arriving silently in one 16 MB piece; a range that fails mid-stream (connection
6
+ dropped, HTTP 5xx or 429) is retried from the last byte consumed, up to 6 times with pauses of 1 to 15 s, and each
7
+ retry reports `ingest-retry`; an abort, a 4xx and a write failure still end the ingest. `ingestProgress` maps both to
8
+ `weights` events (`phase: 'header' | 'retry'`), so a host watchdog sees the download moving. Found by LocalMind's
9
+ production run at 0.1 MB/s (Flint: 3 minutes without a progress event; LFM2.5 24B: "network error" after 0.68 GB).
10
+ Tests: a dropped connection resumes to the same bytes as a clean ingest; a 404 is not retried.
11
+
3
12
  ## 0.6.6 (2026-10-24)
4
13
  - The chat demo (`ggml/chat.html`) lists Ling 3.0 Flash 124B (2 parts, 77 GB, 6 GB budget) and downloads a model
5
14
  published in parts part by part, each under its own name; stop and resume work across parts, and Remove deletes every
package/README.md CHANGED
@@ -18,7 +18,7 @@ Weights stay in the user's own browser storage. The disk tier underneath (`src/`
18
18
  | How | Command |
19
19
  |---|---|
20
20
  | npm | `npm install diskformer` |
21
- | CDN | `import { Llama } from 'https://cdn.jsdelivr.net/npm/diskformer@0.6.4/ggml/llama.js'` |
21
+ | CDN | `import { Llama } from 'https://cdn.jsdelivr.net/npm/diskformer@0.6.6/ggml/llama.js'` |
22
22
  | Copy | `src/`, `engines/` and `index.js`; nothing else is needed |
23
23
 
24
24
  ## Quick start
@@ -27,7 +27,7 @@ Paste into any page served over http(s), in Chrome or Edge. The first run downlo
27
27
  OPFS (resumable); later loads take seconds. Any MoE GGUF on a [checked architecture](docs/coverage.md) loads the same way.
28
28
  ```html
29
29
  <script type="module">
30
- import { Llama } from 'https://cdn.jsdelivr.net/npm/diskformer@0.6.4/ggml/llama.js';
30
+ import { Llama } from 'https://cdn.jsdelivr.net/npm/diskformer@0.6.6/ggml/llama.js';
31
31
  const llama = new Llama();
32
32
  const name = 'granite-4.0-h-tiny-Q4_K_M.gguf';
33
33
  await llama.download({ name, size: 4230976352,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diskformer",
3
- "version": "0.6.6",
3
+ "version": "0.6.7",
4
4
  "description": "Run a model larger than your GPU memory in a browser tab: weights stay in OPFS, WebGPU holds only what each step needs. llama.cpp with paged MoE experts (diskformer/llama), a disk tier, and hand-written MoE engines.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/ingest.js CHANGED
@@ -21,7 +21,10 @@ export const FORMAT = 'diskformer-gguf/1';
21
21
  // ── Ingest: GGUF (over HTTP Range, or a local file) → OPFS, in the engine layout ──
22
22
  export const storeKey = (file) => file.replace(/\.gguf$/i, '').toLowerCase().replace(/[^a-z0-9._-]+/g, '-');
23
23
 
24
- export async function readHeader(url, fetchFn, signal) {
24
+ // The header bytes stream with 'ingest-header' progress ({ loaded, total }): on a slow link the first 16 MB (more for a
25
+ // large tokenizer) take minutes, and a host's watchdog must see the download moving (LocalMind 2026-10-24: at 0.1 MB/s
26
+ // a silent header fetch ended Flint's ingest after 3 minutes).
27
+ export async function readHeader(url, fetchFn, signal, onProgress = () => {}) {
25
28
  let n = 16 << 20;
26
29
  for (;;) {
27
30
  const r = await fetchFn(url, { headers: { Range: `bytes=0-${n - 1}` }, signal, cache: 'no-store' });
@@ -29,11 +32,37 @@ export async function readHeader(url, fetchFn, signal) {
29
32
  const h = (name) => (r.headers && r.headers.get ? r.headers.get(name) : null);
30
33
  const total = /\/(\d+)\s*$/.exec(h('content-range') || '');
31
34
  const file = { size: total ? Number(total[1]) : null, etag: h('etag') }; // for the resume identity
32
- const u8 = new Uint8Array(await r.arrayBuffer());
35
+ const want = Math.min(n, file.size ?? n);
36
+ let u8;
37
+ if (r.body && r.body.getReader) {
38
+ const reader = r.body.getReader(), parts = [];
39
+ let got = 0;
40
+ for (;;) {
41
+ const { done, value } = await reader.read();
42
+ if (done) break;
43
+ parts.push(value); got += value.byteLength;
44
+ onProgress({ status: 'ingest-header', loaded: got, total: want });
45
+ }
46
+ u8 = new Uint8Array(got);
47
+ let at = 0;
48
+ for (const p of parts) { u8.set(p, at); at += p.byteLength; }
49
+ } else {
50
+ u8 = new Uint8Array(await r.arrayBuffer());
51
+ }
33
52
  try { return { gguf: parseGguf(u8), bytes: u8, file }; } catch (e) { if (!e.needBytes) throw e; n = e.needBytes; }
34
53
  }
35
54
  }
36
55
 
56
+ // A network failure while a range streams (the connection dropped, a 5xx or 429) is retried from the last byte
57
+ // consumed, up to RETRIES times with growing pauses (1, 2, 4, 8 s, then 15 s); an abort, a 4xx and a write failure are
58
+ // not. Every retry reports 'ingest-retry' ({ attempt, at, error }).
59
+ const RETRIES = 6;
60
+ const retryable = (e) => { e.retryable = true; return e; };
61
+ const pause = (ms, signal) => new Promise((resolve, reject) => {
62
+ const t = setTimeout(resolve, ms);
63
+ if (signal) signal.addEventListener('abort', () => { clearTimeout(t); reject(signal.reason ?? new Error('aborted')); }, { once: true });
64
+ });
65
+
37
66
  // A local file as the source: a fetch-shaped function that answers ingestGguf's Range requests from a Blob (a GGUF
38
67
  // the user picked, e.g. from a Hugging Face or LM Studio cache), so nothing is downloaded. Pass it as `fetch`.
39
68
  // No ETag: an interrupted ingest resumes from the same file (same header and size), never from a download's state.
@@ -69,7 +98,7 @@ async function ingest({ url, key, plan, root = ROOT, format = FORMAT, fetch: fet
69
98
  rangeBytes = 1 << 30, io = { OpfsWriter, readOpfsText, writeOpfsText } }) {
70
99
  if (!plan || !plan.layout || !plan.units) throw new Error('ingestGguf: plan { layout, units } is required');
71
100
  const t0 = performance.now();
72
- const { gguf, bytes: headerBuf, file } = await readHeader(url, fetchFn, signal);
101
+ const { gguf, bytes: headerBuf, file } = await readHeader(url, fetchFn, signal, onProgress);
73
102
  const layout = plan.layout(gguf);
74
103
  const dir = `${root}/${key}`;
75
104
  const storage = typeof navigator !== 'undefined' && navigator.storage;
@@ -138,26 +167,39 @@ async function ingest({ url, key, plan, root = ROOT, format = FORMAT, fetch: fet
138
167
  let pos = units[startUnit].src;
139
168
  for (let start = pos; start < last; start += rangeBytes) {
140
169
  const end = Math.min(last, start + rangeBytes) - 1;
141
- const r = await fetchFn(url, { headers: { Range: `bytes=${start}-${end}` }, signal, cache: 'no-store' });
142
- if (r.status !== 206) throw new Error(`GGUF range ${start}-${end}: HTTP ${r.status}`);
143
- const reader = r.body.getReader();
144
- for (;;) {
145
- const { done: eof, value } = await reader.read();
146
- if (eof) break;
147
- let off = 0;
148
- while (off < value.byteLength && ui < units.length) {
149
- const u = units[ui];
150
- if (pos < u.src) { const skip = Math.min(u.src - pos, value.byteLength - off); pos += skip; off += skip; continue; }
151
- const take = Math.min(u.len - fill, value.byteLength - off);
152
- unitBuf.set(value.subarray(off, off + take), fill);
153
- fill += take; off += take; pos += take;
154
- if (fill === u.len) { await flushUnit(u); ui++; fill = 0; }
155
- }
156
- done = pos - first;
157
- onProgress({ status: 'ingest', loaded: done, total: last - first, resumedFrom, written, secs: (performance.now() - t0) / 1000 });
158
- if (written - quotaLog[quotaLog.length - 1].written > 4 * 2 ** 30 && storage && storage.estimate) {
159
- const e = await storage.estimate();
160
- quotaLog.push({ written, quota: e.quota, usage: e.usage });
170
+ for (let attempt = 1; pos <= end; attempt++) {
171
+ try {
172
+ // from pos, not start: a retry asks only for the bytes this range has not delivered yet
173
+ const r = await fetchFn(url, { headers: { Range: `bytes=${pos}-${end}` }, signal, cache: 'no-store' }).catch((e) => { throw retryable(e); });
174
+ if (r.status !== 206) {
175
+ const e = new Error(`GGUF range ${pos}-${end}: HTTP ${r.status}`);
176
+ throw r.status >= 500 || r.status === 429 ? retryable(e) : e;
177
+ }
178
+ const reader = r.body.getReader();
179
+ for (;;) {
180
+ const { done: eof, value } = await reader.read().catch((e) => { throw retryable(e); });
181
+ if (eof) break;
182
+ let off = 0;
183
+ while (off < value.byteLength && ui < units.length) {
184
+ const u = units[ui];
185
+ if (pos < u.src) { const skip = Math.min(u.src - pos, value.byteLength - off); pos += skip; off += skip; continue; }
186
+ const take = Math.min(u.len - fill, value.byteLength - off);
187
+ unitBuf.set(value.subarray(off, off + take), fill);
188
+ fill += take; off += take; pos += take;
189
+ if (fill === u.len) { await flushUnit(u); ui++; fill = 0; }
190
+ }
191
+ done = pos - first;
192
+ onProgress({ status: 'ingest', loaded: done, total: last - first, resumedFrom, written, secs: (performance.now() - t0) / 1000 });
193
+ if (written - quotaLog[quotaLog.length - 1].written > 4 * 2 ** 30 && storage && storage.estimate) {
194
+ const e = await storage.estimate();
195
+ quotaLog.push({ written, quota: e.quota, usage: e.usage });
196
+ }
197
+ }
198
+ if (pos <= end) throw retryable(new Error(`GGUF range ended at byte ${pos}, expected ${end + 1}`));
199
+ } catch (e) {
200
+ if ((signal && signal.aborted) || !e.retryable || attempt >= RETRIES) throw e;
201
+ onProgress({ status: 'ingest-retry', attempt, at: pos - first, total: last - first, error: String(e && e.message || e) });
202
+ await pause(Math.min(15000, 1000 * 2 ** (attempt - 1)), signal);
161
203
  }
162
204
  }
163
205
  // Every unit before ui is written: flush, then record ui as the resume point.
@@ -190,8 +232,13 @@ async function ingest({ url, key, plan, root = ROOT, format = FORMAT, fetch: fet
190
232
  // Ingest progress as hosts read it: a 'weights' event with byte counts. The spread goes first, so the
191
233
  // event's own status ('ingest') cannot overwrite 'weights' (LocalMind's 2026-10-04 bug: a host's load
192
234
  // watchdog, seeing no progress, killed a 37 GB first download).
235
+ // The header download and a retry pause count as progress too ('weights' with phase 'header' / 'retry'), so a watchdog
236
+ // that waits for 'weights' keeps a slow or flaky download alive.
193
237
  export function ingestProgress(e) {
194
- return e && e.status === 'ingest' ? { ...e, status: 'weights', loaded: e.loaded, total: e.total } : e;
238
+ if (e && e.status === 'ingest') return { ...e, status: 'weights', loaded: e.loaded, total: e.total };
239
+ if (e && e.status === 'ingest-header') return { ...e, status: 'weights', phase: 'header', loaded: e.loaded, total: e.total };
240
+ if (e && e.status === 'ingest-retry') return { ...e, status: 'weights', phase: 'retry', loaded: e.at, total: e.total };
241
+ return e;
195
242
  }
196
243
 
197
244
  // ── The stores on disk: which are usable, which to remove ──