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.
@@ -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
+ }