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 +6 -0
- package/README.md +9 -9
- package/engines/qwen3_moe_ssd.js +9 -3
- package/index.js +1 -1
- package/package.json +1 -1
- package/src/ingest.js +15 -1
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.
|
|
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
|
|
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
|
|
87
|
-
where each tensor goes
|
|
88
|
-
|
|
89
|
-
(also `RecordPool`) keeps a fixed GPU slot pool
|
|
90
|
-
`prefetch(layer, guesses)` never blocks, and uploads go
|
|
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
|
|
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
|
|
package/engines/qwen3_moe_ssd.js
CHANGED
|
@@ -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 {
|
|
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.
|
|
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);
|