diskformer 0.5.4 → 0.6.6
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 +64 -0
- package/README.md +6 -5
- package/engines/NOTICE +3 -2
- package/engines/flashmap_drafter.js +697 -0
- package/engines/flint_moe_ssd.js +487 -0
- package/engines/gemma-4-e2b.js +5677 -0
- package/engines/hy_v3_moe_ssd.js +198 -0
- package/engines/iq_quants.js +511 -0
- package/engines/lfm2_5.js +5942 -0
- package/engines/lfm2_moe_ssd.js +708 -0
- package/engines/ling_moe_ssd.js +903 -0
- package/engines/ple-opfs.js +108 -0
- package/engines/qwen35_dense.js +836 -0
- package/engines/qwen3_moe_ssd.js +2 -0
- package/engines/ternary_bonsai_2_27b.js +14941 -0
- package/engines/ternary_bonsai_2_dflash.js +936 -0
- package/engines/ternary_bonsai_2_mtp.js +643 -0
- package/ggml/dist/BUILD.md +2 -2
- package/ggml/dist/df-chat.mjs +1 -1
- package/ggml/dist/df-chat.wasm +0 -0
- package/ggml/llama.js +4 -3
- package/ggml/worker.mjs +33 -15
- package/package.json +1 -1
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// ple-opfs.js — rows of a large lookup table served from OPFS into a small GPU-resident cache.
|
|
2
|
+
//
|
|
3
|
+
// Rung 1 of LocalMind's SSD-streaming ladder: Gemma 4 E2B's per-layer embedding (PLE) table
|
|
4
|
+
// (262,144 tokens x 35 layers x 256 values; 4-bit codes + one f32 scale per layer = 4,620 bytes a
|
|
5
|
+
// token, ~1.2 GB) stays on disk. The engine keeps only the rows its current tokens need on the
|
|
6
|
+
// GPU and gathers from those with its own, unchanged kernel. The two classes below know nothing
|
|
7
|
+
// about Gemma, so the same row path can carry other tables or MoE experts later.
|
|
8
|
+
//
|
|
9
|
+
// RowFile fixed-size rows in one OPFS file plus manifest.json; synchronous reads through
|
|
10
|
+
// opfs-reader.js in inline mode, so it must live in a dedicated worker (LocalMind runs
|
|
11
|
+
// every engine in one).
|
|
12
|
+
// RowCache `slots` rows on the GPU, split into planes (byte ranges of a row that live in
|
|
13
|
+
// separate GPU buffers), an O(1) LRU, and a CPU id->slot mirror. lookup(ids) makes
|
|
14
|
+
// every id resident and returns its slot, writing missing rows with queue.writeBuffer.
|
|
15
|
+
//
|
|
16
|
+
// createGemmaPle(opts) is the object the patched gemma-4-e2b.js receives as `load(..., { ple })`.
|
|
17
|
+
|
|
18
|
+
import { RowFile, RowCache, fingerprint } from '../src/rows.js';
|
|
19
|
+
|
|
20
|
+
const ROOT_DIR = 'localmind-ssd';
|
|
21
|
+
// RowFile, RowCache and fingerprint are diskformer.js's (rows.js, a byte-identical copy); this file keeps the Gemma parts.
|
|
22
|
+
export { RowFile, RowCache, fingerprint } from '../src/rows.js';
|
|
23
|
+
|
|
24
|
+
// The object gemma-4-e2b.js receives as `load(modelId, { ple })`. The engine calls attach() from
|
|
25
|
+
// its weight loader with the PLE tensors' raw bytes; attach() writes the OPFS file when it is
|
|
26
|
+
// missing or stale, opens it for reading, allocates the GPU slots through the engine and returns
|
|
27
|
+
// the table the engine's graphs gather from. It returns null when OPFS cannot be used, and the
|
|
28
|
+
// engine then keeps the table resident as before.
|
|
29
|
+
// warm: [[firstRow, count], ...] loaded at attach. Gemma 4's vocabulary is in SentencePiece score
|
|
30
|
+
// order, so low ids are the frequent merged pieces, and its single characters form one block from
|
|
31
|
+
// id 236,743, also most frequent first (the first 256 hold 94 of the 108 ASCII characters).
|
|
32
|
+
export const GEMMA4_WARM = [[0, 28672], [236743, 4096]];
|
|
33
|
+
|
|
34
|
+
// Defaults are the measured choice (2026-10-04, M4 Pro): 32,768 slots (151 MB) warmed with the
|
|
35
|
+
// ranges above, a GPU slot map, and part A = 60% of each decode step (a shorter part A ends before
|
|
36
|
+
// the token's readback and the GPU waits; a longer one wastes more on a miss). gpuMap: false gives the
|
|
37
|
+
// simpler mode where the CPU looks every decode token up and decode runs one step at a time.
|
|
38
|
+
// The OPFS directory under localmind-ssd/: one per model, from its repo name
|
|
39
|
+
// ('google/gemma-4-E2B-it-qat-mobile-transformers' -> 'gemma-4-e2b').
|
|
40
|
+
export function gemmaPleKey(repo) {
|
|
41
|
+
return repo ? repo.split('/').pop().toLowerCase().replace(/-it-qat-mobile-transformers$/, '') : 'gemma-4-e2b';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createGemmaPle({ key = null, slots = 32768, warm = GEMMA4_WARM, gpuMap = true, split = 0.6, source = null, onStatus = () => {} } = {}) {
|
|
45
|
+
return {
|
|
46
|
+
async attach({ bits, scale, vocab, hidden, groups, codeBits, device, alloc }) {
|
|
47
|
+
const wordsPerRow = (hidden * codeBits) / 32;
|
|
48
|
+
const bitsBytes = wordsPerRow * 4, scaleBytes = groups * 4, rowBytes = bitsBytes + scaleBytes;
|
|
49
|
+
if (bits.byteLength !== vocab * bitsBytes || scale.byteLength !== vocab * scaleBytes) {
|
|
50
|
+
throw new Error(`PLE: unexpected sizes bits=${bits.byteLength} scale=${scale.byteLength} for vocab ${vocab}`);
|
|
51
|
+
}
|
|
52
|
+
let file;
|
|
53
|
+
try {
|
|
54
|
+
file = await RowFile.open({ key: key || gemmaPleKey(source && source.repo), name: 'ple.bin', rowBytes, rows: vocab, root: ROOT_DIR });
|
|
55
|
+
const fp = fingerprint([bits, scale]);
|
|
56
|
+
let wroteMs = null;
|
|
57
|
+
if (!file.matches(fp)) {
|
|
58
|
+
onStatus({ phase: 'write', bytes: vocab * rowBytes });
|
|
59
|
+
wroteMs = await file.write(fp, (row0, n, dst) => {
|
|
60
|
+
for (let i = 0; i < n; i++) {
|
|
61
|
+
const t = row0 + i, o = i * rowBytes;
|
|
62
|
+
dst.set(bits.subarray(t * bitsBytes, (t + 1) * bitsBytes), o);
|
|
63
|
+
dst.set(scale.subarray(t * scaleBytes, (t + 1) * scaleBytes), o + bitsBytes);
|
|
64
|
+
}
|
|
65
|
+
}, { extra: { source, layout: 'per token: packed codes (u32 words, LSB first) then f32 group scales', vocab, hidden, groups, codeBits } });
|
|
66
|
+
}
|
|
67
|
+
await file.openRead();
|
|
68
|
+
const warmRows = warm.reduce((k, [, c]) => k + c, 0);
|
|
69
|
+
const n = Math.max(slots, warmRows, 256);
|
|
70
|
+
const bitsT = alloc(n * bitsBytes, 'uint32', [n, wordsPerRow], 'ple-cache.bits');
|
|
71
|
+
const scaleT = alloc(n * scaleBytes, 'float32', [n, groups], 'ple-cache.scale');
|
|
72
|
+
let mapT = null;
|
|
73
|
+
if (gpuMap) {
|
|
74
|
+
mapT = alloc(vocab * 4, 'uint32', [vocab], 'ple-cache.map');
|
|
75
|
+
device.queue.writeBuffer(mapT.buffer, 0, new Uint32Array(vocab).fill(0xffffffff));
|
|
76
|
+
}
|
|
77
|
+
const cache = new RowCache({
|
|
78
|
+
file, slots: n, queue: device.queue, mapBuffer: mapT ? mapT.buffer : null,
|
|
79
|
+
planes: [{ offset: 0, bytes: bitsBytes, buffer: bitsT.buffer }, { offset: bitsBytes, bytes: scaleBytes, buffer: scaleT.buffer }],
|
|
80
|
+
});
|
|
81
|
+
let warmMs = 0;
|
|
82
|
+
for (const [row0, count] of warm) warmMs += cache.warm(row0, count);
|
|
83
|
+
onStatus({ phase: 'ready', wroteMs, warmMs, slots: n });
|
|
84
|
+
return {
|
|
85
|
+
mode: 'opfs', slots: n, bitsT, scaleT, mapT, cache, wroteMs, warmMs, warmRows, split,
|
|
86
|
+
lookup: (ids, out) => cache.lookup(ids, out),
|
|
87
|
+
// For a one-off graph over `ids` (the engine's whole-sequence fallback): its own rows,
|
|
88
|
+
// gathered with iota ids, independent of the cache size.
|
|
89
|
+
gather(ids) {
|
|
90
|
+
const b = new Uint32Array(ids.length * wordsPerRow), s = new Float32Array(ids.length * groups);
|
|
91
|
+
const b8 = new Uint8Array(b.buffer), s8 = new Uint8Array(s.buffer), row = new Uint8Array(rowBytes);
|
|
92
|
+
for (let i = 0; i < ids.length; i++) {
|
|
93
|
+
file.readRows(ids[i], 1, row);
|
|
94
|
+
b8.set(row.subarray(0, bitsBytes), i * bitsBytes);
|
|
95
|
+
s8.set(row.subarray(bitsBytes), i * scaleBytes);
|
|
96
|
+
}
|
|
97
|
+
return { bits: b, scale: s };
|
|
98
|
+
},
|
|
99
|
+
close: () => file.close(),
|
|
100
|
+
};
|
|
101
|
+
} catch (err) {
|
|
102
|
+
if (file) file.close();
|
|
103
|
+
onStatus({ phase: 'fallback', error: String((err && err.message) || err) });
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|