diskformer 0.2.0 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 (2026-10-07)
4
+ - `fileFetch(blob)`: ingest a GGUF that is already on disk (a `File` the user picked, e.g. from a Hugging Face or
5
+ LM Studio cache) through `ingestGguf({ fetch: fileFetch(file) })`, with no download. The store is byte-identical to a
6
+ download's, and an interrupted file ingest resumes from the same file (`test/ingest-resume.test.mjs`).
7
+ - `engines/`: `load({ localFile })` ingests the pinned GGUF from a local file, refusing a file of another size.
8
+
3
9
  ## 0.2.0 (2026-10-05)
4
10
  - `engines/`: reference WebGPU engines for Gemma 4 26B-A4B (`Gemma4MoeSsd`), Qwen3.6 35B-A3B (`Qwen35MoeSsd`) and
5
11
  Qwen3-30B-A3B (`Qwen3MoeSsd`), copied from LocalMind with import paths rewritten (`scripts/sync-localmind.mjs`).
package/README.md CHANGED
@@ -15,12 +15,12 @@ Plain ES modules, no dependencies, no build step. Chromium with WebGPU. Weights
15
15
  | How | Command |
16
16
  |---|---|
17
17
  | npm | `npm install diskformer` |
18
- | CDN | `import { Gemma4MoeSsd } from 'https://cdn.jsdelivr.net/npm/diskformer@0.2/engines/gemma4_moe_ssd.js'` |
18
+ | CDN | `import { Gemma4MoeSsd } from 'https://cdn.jsdelivr.net/npm/diskformer@0.3/engines/gemma4_moe_ssd.js'` |
19
19
  | Copy | `src/`, `engines/` and `index.js`; nothing else is needed |
20
20
 
21
21
  ## Try it: a 14.4 GB model in 2.5 GB of GPU memory
22
22
 
23
- Open the [Hugging Face Space](https://naklitechie-diskformer-chat.static.hf.space/), or serve `examples/chat` yourself.
23
+ Open [diskformer.naklitechie.com](https://diskformer.naklitechie.com/) (also on a [Hugging Face Space](https://huggingface.co/spaces/naklitechie/diskformer-chat)), or serve `examples/chat` yourself.
24
24
  Pick a model and a GPU memory budget. The page downloads the model once into OPFS, resumably, then chats.
25
25
 
26
26
  ```bash
@@ -83,14 +83,14 @@ O(1) LRU and an optional GPU id→slot map that kernels can read. `examples/embe
83
83
 
84
84
  ## Experts on disk
85
85
 
86
- `ingestGguf({ url, key, plan })` streams a GGUF over HTTP ranges into OPFS in your engine's layout. `plan.layout` says
87
- where each tensor goes. `plan.units` lists the byte ranges to copy or split: Q8_0 and Q4_0 blocks become a value plane
88
- and an f16-scale plane, bit-exact. An interrupted ingest resumes from its last 1 GiB checkpoint. `ExpertStreamer`
89
- (also `RecordPool`) keeps a fixed GPU slot pool of expert records. `ensure(layer, ids)` returns pinned slots,
90
- `prefetch(layer, guesses)` never blocks, and uploads go through a mapped staging ring.
86
+ `ingestGguf({ url, key, plan })` streams a GGUF over HTTP ranges into OPFS in your engine's layout: `plan.layout` says
87
+ where each tensor goes, `plan.units` the byte ranges to copy or split (Q8_0 and Q4_0 blocks become a value plane and an
88
+ f16-scale plane, bit-exact). It resumes from its last 1 GiB checkpoint. For a GGUF already on disk, pass a picked `File`
89
+ as `fetch: fileFetch(file)`: no download, same store. `ExpertStreamer` (also `RecordPool`) keeps a fixed GPU slot pool
90
+ of expert records: `ensure(layer, ids)` returns pinned slots, `prefetch(layer, guesses)` never blocks, and uploads go
91
+ through a mapped staging ring.
91
92
 
92
- `measure({ path, recordBytes })` reports the disk's sequential MB/s and random record reads; OPFS speed varies a lot
93
- between machines, so measure where your users run.
93
+ `measure({ path, recordBytes })` reports sequential MB/s and random record reads; OPFS speed varies by machine.
94
94
 
95
95
  ## Commands
96
96
 
@@ -24,7 +24,7 @@ import { OpfsReaderPool, readOpfsText } from '../src/opfs-reader.js';
24
24
  import { ExpertStreamer } from '../src/expert-stream.js';
25
25
  // The store layer is diskformer.js's (github.com/NakliTechie/diskformer.js), byte-identical copies: gguf.js, ingest.js.
26
26
  import { GGML, Q8_BLOCK, Q4_BLOCK, Q6K_BLOCK, parseGguf, tensorBytes } from '../src/gguf.js';
27
- import { ingestGguf as ingestStore, ingestProgress, storeKey, removeStore } from '../src/ingest.js';
27
+ import { ingestGguf as ingestStore, ingestProgress, fileFetch, storeKey, removeStore } from '../src/ingest.js';
28
28
  export { GGML, Q4_BLOCK, Q6K_BLOCK, parseGguf, tensorBytes, splitQ4 } from '../src/gguf.js';
29
29
  export { ingestProgress } from '../src/ingest.js';
30
30
 
@@ -542,8 +542,14 @@ export class Qwen3MoeSsd {
542
542
  static tokenizerFrom(kv) { return new BpeTokenizer(kv); }
543
543
 
544
544
  static async load(modelId = QWEN3_30B_A3B.repo, opts = {}) {
545
- const { fetch: fetchFn = (u, i) => fetch(u, i), onProgress = () => {}, signal } = opts;
545
+ const { onProgress = () => {}, signal, localFile } = opts;
546
546
  const src = opts.source || (modelId === QWEN3_30B_A3B.repo || !modelId ? QWEN3_30B_A3B : { repo: modelId, file: opts.file, revision: opts.revision || 'main' });
547
+ // localFile: the same GGUF already on disk (a File the user picked), ingested instead of downloaded. A size
548
+ // other than the pinned file's is refused before any write; the GGUF header check in the ingest plan follows.
549
+ if (localFile && src.size && localFile.size !== src.size) {
550
+ throw new Error(`${localFile.name} is ${localFile.size.toLocaleString()} bytes, not ${src.file} (${src.size.toLocaleString()} bytes). Pick that file.`);
551
+ }
552
+ const fetchFn = localFile ? fileFetch(localFile) : (opts.fetch || ((u, i) => fetch(u, i)));
547
553
  const url = opts.url || `https://huggingface.co/${src.repo}/resolve/${src.revision}/${src.file}`;
548
554
  const key = opts.key || storeKey(opts.file || src.file || url.split('/').pop());
549
555
  const root = opts.root || OPFS_ROOT; // the OPFS folder for this engine's stores
@@ -567,7 +573,7 @@ export class Qwen3MoeSsd {
567
573
  try { manifest = JSON.parse(await readOpfsText(`${dir}/manifest.json`) || 'null'); } catch (_) { manifest = null; }
568
574
  if (opts.reingest || !manifest || !manifest.complete || manifest.format !== FORMAT) {
569
575
  manifest = await ingestGguf({
570
- url, key, root, fetch: fetchFn, signal, source: { repo: src.repo, file: src.file, revision: src.revision, sha256: src.sha256, size: src.size },
576
+ url, key, root, fetch: fetchFn, signal, source: { repo: src.repo, file: src.file, revision: src.revision, sha256: src.sha256, size: src.size, ...(localFile ? { localFile: localFile.name } : {}) },
571
577
  onProgress: (e) => onProgress(ingestProgress(e)),
572
578
  plan: this.ingestPlan,
573
579
  });
package/index.js CHANGED
@@ -4,5 +4,5 @@ export { OpfsReaderPool, OpfsWriter, canInline, opfsDir, readOpfsText, writeOpfs
4
4
  export { ExpertStreamer, ExpertStreamer as RecordPool } from './src/expert-stream.js';
5
5
  export { RowFile, RowCache, fingerprint } from './src/rows.js';
6
6
  export { parseGguf, tensorBytes, splitQ8, splitQ4, GGML, Q8_BLOCK, Q4_BLOCK, Q6K_BLOCK } from './src/gguf.js';
7
- export { ingestGguf, ingestProgress, readHeader, storeKey, removeStore } from './src/ingest.js';
7
+ export { ingestGguf, ingestProgress, readHeader, fileFetch, storeKey, removeStore } from './src/ingest.js';
8
8
  export { measure } from './src/measure.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diskformer",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
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. A disk tier plus two reference MoE engines.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/ingest.js CHANGED
@@ -17,7 +17,7 @@ import { parseGguf, splitQ8, splitQ4, Q8_BLOCK, Q4_BLOCK } from './gguf.js';
17
17
  export const ROOT = 'diskformer';
18
18
  export const FORMAT = 'diskformer-gguf/1';
19
19
 
20
- // ── Ingest: GGUF (over HTTP Range) → OPFS, in the engine layout ─────────────
20
+ // ── Ingest: GGUF (over HTTP Range, or a local file) → OPFS, in the engine layout ──
21
21
  export const storeKey = (file) => file.replace(/\.gguf$/i, '').toLowerCase().replace(/[^a-z0-9._-]+/g, '-');
22
22
 
23
23
  export async function readHeader(url, fetchFn, signal) {
@@ -33,6 +33,20 @@ export async function readHeader(url, fetchFn, signal) {
33
33
  }
34
34
  }
35
35
 
36
+ // A local file as the source: a fetch-shaped function that answers ingestGguf's Range requests from a Blob (a GGUF
37
+ // the user picked, e.g. from a Hugging Face or LM Studio cache), so nothing is downloaded. Pass it as `fetch`.
38
+ // No ETag: an interrupted ingest resumes from the same file (same header and size), never from a download's state.
39
+ export function fileFetch(blob) {
40
+ return async (_url, { headers = {}, signal } = {}) => {
41
+ if (signal) signal.throwIfAborted();
42
+ const m = /^bytes=(\d+)-(\d+)$/.exec(headers.Range || '');
43
+ if (!m) throw new Error('fileFetch: only Range requests (bytes=a-b) are supported');
44
+ const start = Number(m[1]), end = Math.min(Number(m[2]), blob.size - 1);
45
+ if (start > end) return new Response(null, { status: 416, headers: { 'content-range': `bytes */${blob.size}` } });
46
+ return new Response(blob.slice(start, end + 1), { status: 206, headers: { 'content-range': `bytes ${start}-${end}/${blob.size}` } });
47
+ };
48
+ }
49
+
36
50
  // FNV-1a, for the resume identity: over bytes (the GGUF header) or a string (the plan's output layout).
37
51
  const fnv = (at, n) => { let h = 0x811c9dc5; for (let i = 0; i < n; i++) h = Math.imul(h ^ at(i), 0x01000193) >>> 0; return `${n}:${h.toString(16)}`; };
38
52
  const bytesId = (u8) => fnv((i) => u8[i], u8.length);