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,708 @@
1
+ /* lfm2_moe_ssd.js — Liquid AI LFM2.5-24B-A2B (GGUF arch `lfm2moe`) with its routed experts on disk: rung 2d of
2
+ * the SSD-streaming ladder. Reuses the Gemma 4 rung's Q4_0 / Q6_K kernels and chunked prefill (gemma4_moe_ssd.js)
3
+ * on the rung 2a base (qwen3_moe_ssd.js, moe-expert-stream.js); what differs is the trunk.
4
+ *
5
+ * The math follows llama.cpp b9.. src/models/lfm2.cpp (graph shared by lfm2 and lfm2moe). 40 layers, each:
6
+ * x += op(rms_norm(x) · attn_norm), where op is
7
+ * short conv (30 layers): in_proj (Q4_0, H → 3H) splits into b | c | xx; bx = b · xx; a depthwise causal conv of
8
+ * width 3 over [bx two tokens back, one back, now] (conv.weight [3, H], tap 0 = oldest); y = c · conv;
9
+ * out_proj (H → H). The state is the last two bx rows per layer.
10
+ * attention (10 layers): Q/K/V (32 / 8 / 8 heads × 64), per-head weighted RMS norm on Q and K, NeoX RoPE
11
+ * (base 1e7, all 64 dims), softmax(QKᵀ/8) V, output projection.
12
+ * x += ffn(rms_norm(x) · ffn_norm), where ffn is a dense SwiGLU (11776) on the first 2 layers and a MoE on the rest:
13
+ * 64 experts, top 4 by sigmoid(router) + exp_probs_b (the bias only picks), weights = the picked sigmoids over
14
+ * their sum (clamped at 2⁻¹⁴), SwiGLU experts (1536).
15
+ * Embedding: Q6_K rows (no scale); head: rms_norm(x) · token_embd_norm, then the same Q6_K table (tied).
16
+ *
17
+ * File format (LiquidAI/LFM2.5-24B-A2B-GGUF, QAD-Q4_0): matmul weights Q4_0, norms / conv / router / bias F32, the
18
+ * token embedding Q6_K. Experts: separate ffn_gate_exps / ffn_up_exps [H, F, E] and ffn_down_exps [F, H, E], Q4_0.
19
+ *
20
+ * Layer indexing: the expert store, the streamer and the step loop count only the 38 MoE layers (cfg.layers);
21
+ * the two dense layers run inside MoE layer 0's trunk pass. cfg.modelLayers = 40 for everything per model layer.
22
+ */
23
+
24
+ import { Qwen3MoeSsd, BpeTokenizer, GGML, Q4_BLOCK, tensorBytes, TOK, STORAGE, COPY_SRC, COPY_DST, MAP_READ } from './qwen3_moe_ssd.js';
25
+ import { Gemma4MoeSsd } from './gemma4_moe_ssd.js';
26
+ import { Q8_BLOCK } from '../src/gguf.js';
27
+ import { QWEN35_BATCH_KERNELS } from './qwen35_moe_ssd.js';
28
+
29
+ export function configFromGgufLfm2Moe(kv) {
30
+ const a = kv['general.architecture'];
31
+ if (a !== 'lfm2moe') throw new Error(`expected an lfm2moe GGUF, got ${a}`);
32
+ const g = (k) => kv[`${a}.${k}`];
33
+ const modelLayers = g('block_count'), lead = g('leading_dense_block_count') || 0;
34
+ const kvH = g('attention.head_count_kv');
35
+ const kvHeads = Array.isArray(kvH) ? kvH : Array(modelLayers).fill(kvH);
36
+ if (g('expert_gating_func') !== 2) throw new Error(`lfm2moe expert_gating_func ${g('expert_gating_func')}: this engine implements sigmoid (2)`);
37
+ const H = g('embedding_length'), heads = g('attention.head_count'), hd = H / heads, attnHeads = kvHeads.filter((n) => n > 0);
38
+ if (new Set(attnHeads).size !== 1) throw new Error('lfm2moe: attention layers with different KV head counts');
39
+ const layers = modelLayers - lead, theta = g('rope.freq_base');
40
+ return {
41
+ arch: a, modelLayers, lead, layers,
42
+ attn: kvHeads.map((n) => n > 0), // per model layer: attention (else short conv)
43
+ hidden: H, denseFf: g('feed_forward_length'), heads, kvHeadsAttn: attnHeads[0],
44
+ lCache: g('shortconv.l_cache'),
45
+ experts: g('expert_count'), topK: g('expert_used_count'), expertFf: g('expert_feed_forward_length'),
46
+ eps: g('attention.layer_norm_rms_epsilon'), contextLength: g('context_length'),
47
+ vocab: kv['tokenizer.ggml.tokens'].length,
48
+ bos: kv['tokenizer.ggml.bos_token_id'], eos: kv['tokenizer.ggml.eos_token_id'],
49
+ // The shape Gemma4MoeSsd's prefill and RoPE code read, per MoE layer: one kind of RoPE, no sliding window.
50
+ swa: Array(layers).fill(false), headDim: Array(layers).fill(hd), ropeTheta: Array(layers).fill(theta), softcap: 0,
51
+ kvHeads: Array(layers).fill(attnHeads[0]),
52
+ };
53
+ }
54
+
55
+ // The OPFS layout from the header alone. One expert record = gate rows then up rows (nibble plane, then scale
56
+ // plane), then down; the store holds the 38 MoE layers, indexed from 0.
57
+ // `cfg` lets another architecture with the same store shape reuse it (hy_v3: hy_v3_moe_ssd.js).
58
+ export function planLayoutLfm2Moe(gguf, cfg = configFromGgufLfm2Moe(gguf.kv)) {
59
+ const H = cfg.hidden, F = cfg.expertFf;
60
+ // Expert planes: Q4_0 → a nibble plane (16 B per 32 values), Q8_0 → an int8 plane (32 B); both with an f16 scale plane.
61
+ const q8 = gguf.tensors.find((t) => /_exps\.weight$/.test(t.name))?.type === GGML.Q8_0;
62
+ const nib = (rows, cols) => rows * cols / (q8 ? 1 : 2), scl = (rows, cols) => rows * (cols / 32) * 2;
63
+ const parts = {
64
+ guQ: { off: 0, bytes: nib(2 * F, H) },
65
+ guS: { off: nib(2 * F, H), bytes: scl(2 * F, H) },
66
+ dQ: { off: nib(2 * F, H) + scl(2 * F, H), bytes: nib(H, F) },
67
+ dS: { off: nib(2 * F, H) + scl(2 * F, H) + nib(H, F), bytes: scl(H, F) },
68
+ };
69
+ const record = parts.dS.off + parts.dS.bytes;
70
+ const dense = {};
71
+ let off = 0;
72
+ const take = (n) => { const o = off; off += Math.ceil(n / 256) * 256; return o; };
73
+ for (const t of gguf.tensors) {
74
+ if (/_exps\.weight$/.test(t.name)) continue;
75
+ const n = t.dims.reduce((x, y) => x * y, 1);
76
+ if (t.type === GGML.Q4_0) {
77
+ dense[t.name] = { type: 'q4', dims: t.dims, q: { off: take(n / 2), bytes: n / 2 }, s: { off: take(n / 16), bytes: n / 16 } };
78
+ } else if (t.type === GGML.Q8_0) {
79
+ dense[t.name] = { type: 'q8', dims: t.dims, q: { off: take(n), bytes: n }, s: { off: take(n / 16), bytes: n / 16 } };
80
+ } else if (t.type === GGML.Q6_K) {
81
+ dense[t.name] = { type: 'q6k', dims: t.dims, raw: { off: take(tensorBytes(t)), bytes: tensorBytes(t) } };
82
+ } else if (t.type === GGML.F32) {
83
+ dense[t.name] = { type: 'f32', dims: t.dims, raw: { off: take(n * 4), bytes: n * 4 } };
84
+ } else throw new Error(`dense tensor ${t.name}: unsupported type ${t.type} (this engine reads Q4_0 or Q8_0 files)`);
85
+ }
86
+ return {
87
+ config: cfg,
88
+ experts: { file: 'experts.bin', record, parts, layers: cfg.layers, perLayer: cfg.experts, bytes: record * cfg.layers * cfg.experts },
89
+ dense: { file: 'dense.bin', bytes: off, tensors: dense },
90
+ };
91
+ }
92
+
93
+ export function planUnitsLfm2Moe(gguf, layout) {
94
+ const units = [];
95
+ const { record, parts, perLayer } = layout.experts, lead = layout.config.lead;
96
+ const half = { q: parts.guQ.bytes / 2, s: parts.guS.bytes / 2 };
97
+ for (const t of gguf.tensors) {
98
+ const abs = gguf.dataStart + t.offset;
99
+ const m = /^blk\.(\d+)\.ffn_(gate|up|down)_exps\.weight$/.exec(t.name);
100
+ if (m) {
101
+ if (t.type !== GGML.Q4_0 && t.type !== GGML.Q8_0) throw new Error(`${t.name}: experts must be Q4_0 or Q8_0`);
102
+ const kind = t.type === GGML.Q4_0 ? { kind: 'q4' } : {};
103
+ const layer = Number(m[1]) - lead, which = m[2];
104
+ const slice = tensorBytes(t) / perLayer;
105
+ const qOff = which === 'down' ? parts.dQ.off : parts.guQ.off + (which === 'up' ? half.q : 0);
106
+ const sOff = which === 'down' ? parts.dS.off : parts.guS.off + (which === 'up' ? half.s : 0);
107
+ for (let e = 0; e < perLayer; e++) {
108
+ const rec = (layer * perLayer + e) * record;
109
+ units.push({ ...kind, src: abs + e * slice, len: slice, file: 'experts', q: rec + qOff, s: rec + sOff });
110
+ }
111
+ continue;
112
+ }
113
+ const d = layout.dense.tensors[t.name];
114
+ if (d.type !== 'q4' && d.type !== 'q8') { units.push({ src: abs, len: d.raw.bytes, file: 'dense', raw: d.raw.off }); continue; }
115
+ const q4 = d.type === 'q4', cols = t.dims[0], rows = (q4 ? d.q.bytes * 2 : d.q.bytes) / cols, rowBytes = (cols / 32) * (q4 ? Q4_BLOCK : Q8_BLOCK);
116
+ const band = Math.max(1, Math.floor((8 << 20) / rowBytes));
117
+ for (let r = 0; r < rows; r += band) {
118
+ const nr = Math.min(band, rows - r);
119
+ units.push({ ...(q4 ? { kind: 'q4' } : {}), src: abs + r * rowBytes, len: nr * rowBytes, file: 'dense', q: d.q.off + r * (q4 ? cols / 2 : cols), s: d.s.off + r * (cols / 32) * 2 });
120
+ }
121
+ }
122
+ units.sort((a, b) => a.src - b.src);
123
+ return units;
124
+ }
125
+
126
+ export const LFM2MOE_INGEST_PLAN = { layout: planLayoutLfm2Moe, units: planUnitsLfm2Moe };
127
+
128
+ export const LFM25_24B_A2B = {
129
+ repo: 'LiquidAI/LFM2.5-24B-A2B-GGUF',
130
+ file: 'LFM2.5-24B-A2B-QAD-Q4_0.gguf',
131
+ revision: 'f97fc8a443da86c71818adf91baecde5b7ef4c50',
132
+ sha256: '89617cc4d3a52e973ee8dea72c7df04285f582107385796ba903f81e540e4afa',
133
+ size: 13578190880,
134
+ };
135
+
136
+ // ── Tokenizer: byte-level BPE, tokenizer.ggml.pre = lfm2 ──
137
+ // llama.cpp maps `lfm2` to LLAMA_VOCAB_PRE_TYPE_LLAMA3: the Llama 3 split regex (digits in runs of up to 3) and
138
+ // ignore_merges (a pre-token that is itself in the vocab is emitted whole, before any merging).
139
+ const LLAMA3_PRE = /(?:'[sS]|'[tT]|'[rR][eE]|'[vV][eE]|'[mM]|'[lL][lL]|'[dD])|[^\r\n\p{L}\p{N}]?\p{L}+|\p{N}{1,3}| ?[^\s\p{L}\p{N}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+/gu;
140
+ export class Lfm2Tokenizer extends BpeTokenizer {
141
+ constructor(kv) { super(kv); this.preRe = LLAMA3_PRE; }
142
+ bpe(word) {
143
+ const id = this.ids.get(word);
144
+ return id !== undefined ? [id] : super.bpe(word);
145
+ }
146
+ }
147
+
148
+ // The GGUF's chat template (text turns): <|startoftext|>, a system turn if any, then <|im_start|>role\n…<|im_end|>\n per
149
+ // message; assistant turns before the last user turn lose their reasoning (the text up to </think>). The generation
150
+ // prompt opens the reasoning block: the model is reasoning-only, so there is no thinking-off form.
151
+ export function lfm2ChatPrompt(messages) {
152
+ let s = '<|startoftext|>', msgs = messages;
153
+ if (msgs.length && msgs[0].role === 'system') {
154
+ if (msgs[0].content) s += `<|im_start|>system\n${msgs[0].content}<|im_end|>\n`;
155
+ msgs = msgs.slice(1);
156
+ }
157
+ const lastUser = msgs.map((m) => m.role).lastIndexOf('user');
158
+ msgs.forEach((m, i) => {
159
+ let c = String(m.content ?? '');
160
+ // Earlier replies lose their reasoning: the model's own </think> and LocalMind's <|think|> … <|/think|> markers.
161
+ if (m.role === 'assistant' && i < lastUser) {
162
+ c = c.replace(/<\|think\|>[\s\S]*?<\|\/think\|>/g, '');
163
+ if (c.includes('</think>')) c = c.split('</think>').pop();
164
+ c = c.trim();
165
+ }
166
+ s += `<|im_start|>${m.role}\n${c}<|im_end|>\n`;
167
+ });
168
+ return s + '<|im_start|>assistant\n<think>';
169
+ }
170
+
171
+ // ── WGSL ────────────────────────────────────────────────────────────────────
172
+ // Router: probs = sigmoid(l); pick the top k of probs + bias (lowest index on ties); weights = picked probs over
173
+ // their sum, the sum clamped at 6.103515625e-5, times expert_weights_scale (llama.cpp build_moe_ffn; 1 for LFM).
174
+ // sel = k ids then k weights (f32 bits).
175
+ const TOPK_SIG_BODY = `
176
+ let t = l.x; let n = p.n;
177
+ var mine = -3.4e38; var pr = 0.0;
178
+ if (t < n) { pr = 1.0 / (1.0 + exp(-lg[lb + t])); mine = pr + bias[t]; }
179
+ for (var j = 0u; j < p.k; j++) {
180
+ red[t] = mine; bi[t] = t; workgroupBarrier();
181
+ for (var s = 128u; s > 0u; s >>= 1u) {
182
+ if (t < s) { let o = red[t + s]; if (o > red[t] || (o == red[t] && bi[t + s] < bi[t])) { red[t] = o; bi[t] = bi[t + s]; } }
183
+ workgroupBarrier();
184
+ }
185
+ if (t == 0u) { picks[j] = bi[0]; }
186
+ workgroupBarrier();
187
+ if (t == picks[j]) { probs[j] = pr; mine = -3.4e38; }
188
+ workgroupBarrier();
189
+ }
190
+ if (t == 0u) {
191
+ var wsum = 0.0;
192
+ for (var j = 0u; j < p.k; j++) { wsum += probs[j]; }
193
+ wsum = max(wsum, 6.103515625e-5);
194
+ for (var j = 0u; j < p.k; j++) { sel[sb + j] = picks[j]; sel[sb + p.k + j] = bitcast<u32>(probs[j] / wsum * p.scale); }
195
+ }
196
+ }`;
197
+ const TOPK_SIG_DECL = `
198
+ struct P { n: u32, k: u32, scale: f32 }
199
+ @group(0) @binding(0) var<storage, read> lg: array<f32>;
200
+ @group(0) @binding(1) var<storage, read> bias: array<f32>;
201
+ @group(0) @binding(2) var<storage, read_write> sel: array<u32>;
202
+ @group(0) @binding(3) var<uniform> p: P;
203
+ var<workgroup> red: array<f32, 256>;
204
+ var<workgroup> bi: array<u32, 256>;
205
+ var<workgroup> picks: array<u32, 32>;
206
+ var<workgroup> probs: array<f32, 32>;`;
207
+
208
+ const L2 = {
209
+ topkSig: `${TOPK_SIG_DECL}
210
+ @compute @workgroup_size(256) fn main(@builtin(local_invocation_id) l: vec3<u32>) {
211
+ let lb = 0u; let sb = 0u;${TOPK_SIG_BODY}`,
212
+ // Short conv for one token, one thread per channel: bcx = [b | c | xx] (3H); st holds the last two bx per channel
213
+ // (oldest first) and moves on by one.
214
+ shortConv: `
215
+ struct P { H: u32 }
216
+ @group(0) @binding(0) var<storage, read> bcx: array<f32>;
217
+ @group(0) @binding(1) var<storage, read> w: array<f32>;
218
+ @group(0) @binding(2) var<storage, read_write> st: array<f32>;
219
+ @group(0) @binding(3) var<storage, read_write> y: array<f32>;
220
+ @group(0) @binding(4) var<uniform> p: P;
221
+ @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>) {
222
+ let i = g.x; if (i >= p.H) { return; }
223
+ let bx = bcx[i] * bcx[2u * p.H + i];
224
+ let s0 = st[2u * i]; let s1 = st[2u * i + 1u];
225
+ let conv = w[3u * i] * s0 + w[3u * i + 1u] * s1 + w[3u * i + 2u] * bx;
226
+ y[i] = bcx[p.H + i] * conv;
227
+ st[2u * i] = s1; st[2u * i + 1u] = bx;
228
+ }`,
229
+ // Debug capture: y[off + i] = x[i].
230
+ copyAt: `
231
+ struct P { n: u32, off: u32 }
232
+ @group(0) @binding(0) var<storage, read> x: array<f32>;
233
+ @group(0) @binding(1) var<storage, read_write> y: array<f32>;
234
+ @group(0) @binding(2) var<uniform> p: P;
235
+ @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>) {
236
+ let i = g.x; if (i < p.n) { y[p.off + i] = x[i]; }
237
+ }`,
238
+ };
239
+
240
+ const QB = 'struct Q { T: u32, pos0: u32, S: u32, pad: u32 }';
241
+ const L2B = {
242
+ topkSigB: `${TOPK_SIG_DECL}
243
+ @compute @workgroup_size(256) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
244
+ let lb = wg.x * p.n; let sb = wg.x * 2u * p.k;${TOPK_SIG_BODY}`,
245
+ // The chunk's T tokens in order, one thread per channel.
246
+ shortConvB: `
247
+ struct P { H: u32 }
248
+ ${QB}
249
+ @group(0) @binding(0) var<storage, read> bcx: array<f32>;
250
+ @group(0) @binding(1) var<storage, read> w: array<f32>;
251
+ @group(0) @binding(2) var<storage, read_write> st: array<f32>;
252
+ @group(0) @binding(3) var<storage, read_write> y: array<f32>;
253
+ @group(0) @binding(4) var<uniform> p: P;
254
+ @group(0) @binding(5) var<uniform> qd: Q;
255
+ @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>) {
256
+ let i = g.x; if (i >= p.H) { return; }
257
+ var s0 = st[2u * i]; var s1 = st[2u * i + 1u];
258
+ let w0 = w[3u * i]; let w1 = w[3u * i + 1u]; let w2 = w[3u * i + 2u];
259
+ for (var t = 0u; t < qd.T; t++) {
260
+ let r = t * 3u * p.H;
261
+ let bx = bcx[r + i] * bcx[r + 2u * p.H + i];
262
+ y[t * p.H + i] = bcx[r + p.H + i] * (w0 * s0 + w1 * s1 + w2 * bx);
263
+ s0 = s1; s1 = bx;
264
+ }
265
+ st[2u * i] = s0; st[2u * i + 1u] = s1;
266
+ }`,
267
+ // act = SiLU(g) · u over T × n (the dense FFN's separate gate and up).
268
+ siluSplitB: `
269
+ struct P { n: u32 }
270
+ ${QB}
271
+ @group(0) @binding(0) var<storage, read> g: array<f32>;
272
+ @group(0) @binding(1) var<storage, read> u: array<f32>;
273
+ @group(0) @binding(2) var<storage, read_write> act: array<f32>;
274
+ @group(0) @binding(3) var<uniform> p: P;
275
+ @group(0) @binding(4) var<uniform> qd: Q;
276
+ @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gi: vec3<u32>) {
277
+ let i = gi.x; let t = gi.y; if (i >= p.n || t >= qd.T) { return; }
278
+ let k = t * p.n + i; let g0 = g[k]; act[k] = (g0 / (1.0 + exp(-g0))) * u[k];
279
+ }`,
280
+ // x[t] += Σ_j w_tj · dn[t·k + j] per token (the routed experts' weighted sum and the residual add).
281
+ moeAccumB: `
282
+ struct P { H: u32, k: u32 }
283
+ ${QB}
284
+ @group(0) @binding(0) var<storage, read> dn: array<f32>;
285
+ @group(0) @binding(1) var<storage, read> sel: array<u32>;
286
+ @group(0) @binding(2) var<storage, read_write> x: array<f32>;
287
+ @group(0) @binding(3) var<uniform> p: P;
288
+ @group(0) @binding(4) var<uniform> qd: Q;
289
+ @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gi: vec3<u32>) {
290
+ let h = gi.x; let t = gi.y; if (h >= p.H || t >= qd.T) { return; }
291
+ let sb = t * 2u * p.k; let ob = t * p.k * p.H + h;
292
+ var acc = 0.0;
293
+ for (var j = 0u; j < p.k; j++) { acc += bitcast<f32>(sel[sb + p.k + j]) * dn[ob + j * p.H]; }
294
+ x[t * p.H + h] = x[t * p.H + h] + acc;
295
+ }`,
296
+ // Per-head weighted RMS norm and NeoX RoPE over all hd dims, in place, for (head wg.x, token wg.y).
297
+ qkNormRopeB: `
298
+ struct P { hd: u32, row: u32, eps: f32 }
299
+ @group(0) @binding(0) var<storage, read_write> x: array<f32>;
300
+ @group(0) @binding(1) var<storage, read> w: array<f32>;
301
+ @group(0) @binding(2) var<storage, read> rope: array<f32>;
302
+ @group(0) @binding(3) var<uniform> p: P;
303
+ var<workgroup> red: array<f32, 64>;
304
+ @compute @workgroup_size(64) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
305
+ let base = wg.y * p.row + wg.x * p.hd; let rb = wg.y * p.hd; let t = l.x; let half = p.hd / 2u;
306
+ var acc = 0.0;
307
+ for (var i = t; i < p.hd; i += 64u) { let v = x[base + i]; acc += v * v; }
308
+ red[t] = acc; workgroupBarrier();
309
+ for (var s = 32u; s > 0u; s >>= 1u) { if (t < s) { red[t] += red[t + s]; } workgroupBarrier(); }
310
+ let scale = 1.0 / sqrt(red[0] / f32(p.hd) + p.eps);
311
+ workgroupBarrier();
312
+ for (var i = t; i < half; i += 64u) {
313
+ let a = (x[base + i] * scale) * w[i];
314
+ let b = (x[base + i + half] * scale) * w[i + half];
315
+ let c = rope[rb + i]; let sn = rope[rb + half + i];
316
+ x[base + i] = a * c - b * sn;
317
+ x[base + i + half] = a * sn + b * c;
318
+ }
319
+ }`,
320
+ };
321
+ const REUSED_B = ['rmsnormB', 'matmulF32T', 'kvStoreB', 'attnScoreB', 'softmaxB', 'attnOutB', 'addInPlaceB'];
322
+
323
+ // ── Engine ──────────────────────────────────────────────────────────────────
324
+ export class Lfm2MoeSsd extends Gemma4MoeSsd {
325
+ static configFrom(kv) { return configFromGgufLfm2Moe(kv); }
326
+ static get ingestPlan() { return LFM2MOE_INGEST_PLAN; }
327
+ static tokenizerFrom(kv) { return new Lfm2Tokenizer(kv); }
328
+ static load(modelId, opts = {}) { return Qwen3MoeSsd.load.call(this, modelId, { ...opts, source: opts.source || LFM25_24B_A2B }); }
329
+
330
+ // Prefetch one MoE layer ahead by default. Measured 2026-10-14 (M4 Pro, 4 GB pool, 376-token prompt, 256 tokens warm):
331
+ // lookahead 2 → 4.0 tok/s (344 ms of SSD reads per token: the extra guesses evict experts still in use), lookahead 1
332
+ // → 16.1 tok/s, prefetch off → 15.0 tok/s. 8 GB pool (1,365 slots, the binding cap) with lookahead 1 → 23.7 tok/s.
333
+ constructor(device, manifest, gguf, opts) {
334
+ super(device, manifest, gguf, opts);
335
+ this.lookahead = Math.max(1, Math.floor(opts.lookahead ?? 1));
336
+ }
337
+
338
+ // Kernel names for the matrix and expert weights: Q4_0 here; an engine reading a Q8_0 file (hy_v3_moe_ssd.js) swaps
339
+ // in the Q8_0 kernels, which take the same bindings and dispatch sizes.
340
+ get kq() { return { mm: 'matmulQ4', mmT: 'matmulQ4T', ex: 'expertQ4', exB: 'expertQ4B' }; }
341
+ // Attention kernel names (decode, then chunk); an engine with sinks or a window (flint_moe_ssd.js) rebinds its own.
342
+ get attnK() { return { score: 'attnScore', soft: 'softmax', out: 'attnOut', scoreB: 'attnScoreB', softB: 'softmaxB', outB: 'attnOutB' }; }
343
+
344
+ chatPrompt(messages) { return lfm2ChatPrompt(messages); }
345
+ get stopTokenIds() { return [this.cfg.eos, this.tokenizer.ids.get('<|im_end|>'), this.tokenizer.ids.get('<|endoftext|>')].filter((t) => t !== undefined); }
346
+ get thinkOpenTokenId() { return this.tokenizer.ids.get('<think>') ?? null; }
347
+ get thinkCloseTokenId() { return this.tokenizer.ids.get('</think>') ?? null; }
348
+ get thinkInPrompt() { return true; }
349
+ get alwaysThinks() { return true; }
350
+ get thinkPreamble() { return ''; }
351
+
352
+ get kernels() {
353
+ return (this._kernels ||= {
354
+ ...super.kernels, ...L2,
355
+ ...(this.batchPrefill ? { ...Object.fromEntries(REUSED_B.map((k) => [k, QWEN35_BATCH_KERNELS[k]])), ...L2B } : {}),
356
+ });
357
+ }
358
+
359
+ // KV cache only on the attention layers.
360
+ kvBytes() { const c = this.cfg, hd = c.attnHeadDim || c.hidden / c.heads; return c.attn.filter(Boolean).length * 2 * 2 * this.maxCtx * c.kvHeadsAttn * hd; }
361
+
362
+ // The Q6_K table into planes (Gemma's uploadDense); this model has no rope_freqs.
363
+ async uploadDense(onProgress) { await super.uploadDense(onProgress); this.ropeFreqs = null; }
364
+
365
+ initBuffers() {
366
+ const c = this.cfg, H = c.hidden, K = c.topK, E = c.experts, F = c.expertFf, DF = c.denseFf, ML = c.modelLayers;
367
+ const hd = c.attnHeadDim || H / c.heads, QN = c.heads * hd, KVN = c.kvHeadsAttn * hd, SF = c.shexpFf || 0;
368
+ if (2 * K * 4 > 256) throw new Error(`top-k ${K} > 32`);
369
+ if (E > 256) throw new Error(`${E} experts: the router top-k kernel handles at most 256`);
370
+ if ((DF * 4) % 256) throw new Error(`dense FFN ${DF}: gate/up halves must sit on 256-byte offsets`);
371
+ this.kc = []; this.vc = []; this.cs = [];
372
+ for (let L = 0; L < ML; L++) {
373
+ this.kc.push(c.attn[L] ? this.buffer(this.maxCtx * KVN * 2, STORAGE, 'kv') : null);
374
+ this.vc.push(c.attn[L] ? this.buffer(this.maxCtx * KVN * 2, STORAGE, 'kv') : null);
375
+ this.cs.push(c.attn[L] ? null : this.buffer((c.lCache - 1) * H * 4, STORAGE | COPY_DST, 'kv'));
376
+ }
377
+ const A = (n) => this.buffer(n * 4, STORAGE | COPY_SRC | COPY_DST);
378
+ this.a = {
379
+ x: A(H), xn: A(H), bcx: A(3 * H), y: A(H), q: A(QN), k: A(KVN), v: A(KVN), att: A(QN), o: A(H), sc: A(c.heads * this.maxCtx),
380
+ mgu: A(2 * DF), ma: A(DF), rl: A(E), sel: A(2 * K), pxn: A(H), prl: A(E), psel: A(64 * this.lookahead), slots: A(K),
381
+ gu: A(K * 2 * F), act: A(K * F), dn: A(K * H), logits: A(c.vocab), am: A(4), ropeS: A(hd), ropeF: A(hd),
382
+ };
383
+ if (this.opts.capture) this.a.cap = A(ML * H);
384
+ this.tok = this.buffer(16, 0x40 | COPY_DST);
385
+ this.rbSel = this.buffer(256 * (1 + this.lookahead), MAP_READ | COPY_DST);
386
+ this.rbLogits = this.buffer(c.vocab * 4, MAP_READ | COPY_DST);
387
+ this.rbArg = this.buffer(16, MAP_READ | COPY_DST);
388
+
389
+ const f = (v) => ({ f: v }), eps = f(c.eps);
390
+ const u = {
391
+ embed: this.uniform([H, H / 256, f(1)]), rmsH: this.uniform([H, eps]), nH: this.uniform([H]),
392
+ inProj: this.uniform([3 * H, H]), mmH: this.uniform([H, H]), conv: this.uniform([H]),
393
+ mmQ: this.uniform([QN, H]), mmO: this.uniform([H, QN]), mmKV: this.uniform([KVN, H]), qk: this.uniform([hd, eps]), kvn: this.uniform([KVN]),
394
+ att: this.uniform([c.heads, c.kvHeadsAttn, hd, f(Math.fround(1 / Math.sqrt(hd)))]), attOut: this.uniform([c.heads, c.kvHeadsAttn, hd, 0]),
395
+ mmG: this.uniform([DF, H]), mmD: this.uniform([H, DF]), siluD: this.uniform([DF, 1]),
396
+ router: this.uniform([E, H]), topk: this.uniform([E, K, f(c.expScale ?? 1)]),
397
+ ...(SF ? { mmSG: this.uniform([SF, H]), mmSD: this.uniform([H, SF]), siluS: this.uniform([SF, 1]) } : {}),
398
+ gu: this.uniform([2 * F, H, 2 * F, 0]), dn: this.uniform([H, F, H, 1]), siluE: this.uniform([F, K]), acc: this.uniform([H, K]),
399
+ lm: this.uniform([c.vocab, H]), am: this.uniform([c.vocab]),
400
+ };
401
+ const a = this.a, W = this.w, gateBytes = DF * 4;
402
+ // The Q6_K token table (LFM2.5); an engine with another table type binds embed and lm itself (this.q6 unset).
403
+ const q6 = this.q6 ? [this.q6.ql, this.q6.qh, this.q6.sc, this.q6.dd] : null;
404
+ this.g = { embed: q6 ? this.bind('embedQ6K', [...q6, a.x, this.tok, u.embed]) : null };
405
+ // Per model layer: the operator (conv or attention) and, for the dense layers, the whole FFN.
406
+ this.mlayers = [];
407
+ for (let L = 0; L < ML; L++) {
408
+ const p = (n) => W[`blk.${L}.${n}`];
409
+ const ly = { attn: c.attn[L], rmsA: this.bind('rmsnorm', [a.x, p('attn_norm.weight').raw, a.xn, u.rmsH]) };
410
+ if (ly.attn) {
411
+ Object.assign(ly, {
412
+ q: this.bind(this.kq.mm, [a.xn, p('attn_q.weight').q, p('attn_q.weight').s, a.q, u.mmQ]),
413
+ k: this.bind(this.kq.mm, [a.xn, p('attn_k.weight').q, p('attn_k.weight').s, a.k, u.mmKV]),
414
+ v: this.bind(this.kq.mm, [a.xn, p('attn_v.weight').q, p('attn_v.weight').s, a.v, u.mmKV]),
415
+ ropeQ: this.bind('qkNormRope', [a.q, p('attn_q_norm.weight').raw, a.ropeF, u.qk]),
416
+ ropeK: this.bind('qkNormRope', [a.k, p('attn_k_norm.weight').raw, a.ropeF, u.qk]),
417
+ kv: this.bind('kvStore', [a.k, a.v, this.kc[L], this.vc[L], this.tok, u.kvn]),
418
+ score: this.bind('attnScore', [a.q, this.kc[L], a.sc, this.tok, u.att]),
419
+ soft: this.bind('softmax', [a.sc, this.tok]),
420
+ attOut: this.bind('attnOut', [a.sc, this.vc[L], a.att, this.tok, u.attOut]),
421
+ o: this.bind(this.kq.mm, [a.att, p('attn_output.weight').q, p('attn_output.weight').s, a.o, u.mmO]),
422
+ });
423
+ } else {
424
+ Object.assign(ly, {
425
+ inProj: this.bind(this.kq.mm, [a.xn, p('shortconv.in_proj.weight').q, p('shortconv.in_proj.weight').s, a.bcx, u.inProj]),
426
+ conv: this.bind('shortConv', [a.bcx, p('shortconv.conv.weight').raw, this.cs[L], a.y, u.conv]),
427
+ o: this.bind(this.kq.mm, [a.y, p('shortconv.out_proj.weight').q, p('shortconv.out_proj.weight').s, a.o, u.mmH]),
428
+ });
429
+ }
430
+ ly.addO = this.bind('addInPlace', [a.x, a.o, u.nH]);
431
+ ly.rmsF = this.bind('rmsnorm', [a.x, p('ffn_norm.weight').raw, a.xn, u.rmsH]);
432
+ if (L < c.lead) {
433
+ Object.assign(ly, {
434
+ mg: this.bindR(this.kq.mm, [a.xn, p('ffn_gate.weight').q, p('ffn_gate.weight').s, { buffer: a.mgu, offset: 0, size: gateBytes }, u.mmG]),
435
+ mu: this.bindR(this.kq.mm, [a.xn, p('ffn_up.weight').q, p('ffn_up.weight').s, { buffer: a.mgu, offset: gateBytes, size: gateBytes }, u.mmG]),
436
+ mact: this.bind('siluMulMoe', [a.mgu, a.ma, u.siluD]),
437
+ md: this.bind(this.kq.mm, [a.ma, p('ffn_down.weight').q, p('ffn_down.weight').s, a.o, u.mmD]),
438
+ });
439
+ }
440
+ if (a.cap) ly.cap = this.bind('copyAt', [a.x, a.cap, this.uniform([H, L * H])]);
441
+ this.mlayers.push(ly);
442
+ }
443
+ // Per MoE layer l (model layer l + lead): router, prefetch guesses, experts.
444
+ this.layers = [];
445
+ for (let l = 0; l < c.layers; l++) {
446
+ const L = l + c.lead, p = (n) => W[`blk.${L}.${n}`];
447
+ this.layers.push({
448
+ router: this.bind('matmulF32', [a.xn, p('ffn_gate_inp.weight').raw, a.rl, u.router]),
449
+ topk: this.bind('topkSig', [a.rl, (p('exp_probs_b.bias') || p('exp_probs_b')).raw, a.sel, u.topk]),
450
+ // Routers of MoE layers l+1..l+lookahead on this layer's post-operator state: the prefetch guesses.
451
+ pf: Array.from({ length: Math.min(this.lookahead, c.layers - 1 - l) }, (_, i) => {
452
+ const at = (n) => W[`blk.${L + 1 + i}.${n}`];
453
+ return {
454
+ rms: this.bind('rmsnorm', [a.x, at('ffn_norm.weight').raw, a.pxn, u.rmsH]),
455
+ router: this.bind('matmulF32', [a.pxn, at('ffn_gate_inp.weight').raw, a.prl, u.router]),
456
+ topk: this.bindR('topkSig', [a.prl, (at('exp_probs_b.bias') || at('exp_probs_b')).raw, { buffer: a.psel, offset: 256 * i, size: 8 * K }, u.topk]),
457
+ };
458
+ }),
459
+ gu: this.bind(this.kq.ex, [a.xn, this.pool.guQ, this.pool.guS, a.gu, u.gu, a.slots]),
460
+ act: this.bind('siluMulMoe', [a.gu, a.act, u.siluE]),
461
+ dn: this.bind(this.kq.ex, [a.act, this.pool.dQ, this.pool.dS, a.dn, u.dn, a.slots]),
462
+ acc: this.bind('moeAccum', [a.dn, a.sel, a.x, u.acc]),
463
+ // The shared expert (always on, no gate; hy_v3): x += down(SiLU(gate(xn)) · up(xn)).
464
+ ...(SF ? { sh: {
465
+ g: this.bindR(this.kq.mm, [a.xn, p('ffn_gate_shexp.weight').q, p('ffn_gate_shexp.weight').s, { buffer: a.mgu, offset: 0, size: SF * 4 }, u.mmSG]),
466
+ u: this.bindR(this.kq.mm, [a.xn, p('ffn_up_shexp.weight').q, p('ffn_up_shexp.weight').s, { buffer: a.mgu, offset: SF * 4, size: SF * 4 }, u.mmSG]),
467
+ act: this.bind('siluMulMoe', [a.mgu, a.ma, u.siluS]),
468
+ d: this.bind(this.kq.mm, [a.ma, p('ffn_down_shexp.weight').q, p('ffn_down_shexp.weight').s, a.o, u.mmSD]),
469
+ add: this.bind('addInPlace', [a.x, a.o, u.nH]),
470
+ } } : {}),
471
+ });
472
+ }
473
+ this.g.rmsOut = this.bind('rmsnorm', [a.x, (W['token_embd_norm.weight'] || W['output_norm.weight']).raw, a.xn, u.rmsH]);
474
+ this.g.lm = q6 ? this.bind('matmulQ6K', [...q6, a.xn, a.logits, u.lm]) : null;
475
+ this.g.am = this.bind('argmax', [a.logits, a.am, u.am]);
476
+ if (Math.min(c.experts, this.chunkTokens * c.topK) > this.poolSlots / 2) this.chunkTokens = Math.max(2, Math.floor(this.poolSlots / 2 / c.topK));
477
+ if (this.batchPrefill) this.initBatch();
478
+ }
479
+
480
+ initBatch() {
481
+ const c = this.cfg, B = this.chunkTokens, W = this.w, H = c.hidden, K = c.topK, E = c.experts, F = c.expertFf, DF = c.denseFf;
482
+ const hd = c.attnHeadDim || H / c.heads, QN = c.heads * hd, KVN = c.kvHeadsAttn * hd, SF = c.shexpFf || 0;
483
+ const A = (n) => this.buffer(n * 4, STORAGE | COPY_SRC | COPY_DST, 'batch');
484
+ const b = this.b = {
485
+ ids: A(B), x: A(B * H), xn: A(B * H), bcx: A(B * 3 * H), y: A(B * H), q: A(B * QN), k: A(B * KVN), v: A(B * KVN),
486
+ att: A(B * QN), o: A(B * H), sc: A(c.heads * B * this.maxCtx), mg: A(B * DF), mu: A(B * DF), ma: A(B * DF),
487
+ rl: A(B * E), sel: A(B * 2 * K), slots: A(B * K), gu: A(B * K * 2 * F), act: A(B * K * F), dn: A(B * K * H),
488
+ ropeS: A(B * hd), ropeF: A(B * hd),
489
+ };
490
+ this.qb = this.buffer(16, 0x40 | COPY_DST, 'batch');
491
+ this.rbSelB = this.buffer(B * 2 * K * 4, MAP_READ | COPY_DST, 'batch');
492
+ const U = (w) => this.uniform(w), eps = { f: c.eps }, qb = this.qb;
493
+ const u = {
494
+ embed: U([H, H / 256, { f: 1 }]), rmsH: U([H, eps]), nH: U([H]), inProj: U([3 * H, H]), mmH: U([H, H]), mmO: U([H, QN]), conv: U([H]),
495
+ q: U([QN, H]), kv: U([KVN, H]), ropeQ: U([hd, QN, eps]), ropeK: U([hd, KVN, eps]), kvn: U([KVN]),
496
+ att: U([c.heads, c.kvHeadsAttn, hd, { f: Math.fround(1 / Math.sqrt(hd)) }]), attOut: U([c.heads, c.kvHeadsAttn, hd, 0]),
497
+ mg: U([DF, H]), md: U([H, DF]), silu: U([DF]), router: U([E, H]), topk: U([E, K, { f: c.expScale ?? 1 }]),
498
+ ...(SF ? { sg: U([SF, H]), sd: U([H, SF]), siluS: U([SF]) } : {}),
499
+ gu: U([2 * F, H, 2 * F, 0, K]), dn: U([H, F, H, 1, K]), siluE: U([F, B * K]), acc: U([H, K]),
500
+ };
501
+ const q4 = (name, x, y, uni) => this.bind(this.kq.mmT, [x, W[name].q, W[name].s, y, uni, qb]);
502
+ this.gB = { embed: this.q6 ? this.bind('embedQ6KB', [this.q6.ql, this.q6.qh, this.q6.sc, this.q6.dd, b.ids, b.x, u.embed, qb]) : null };
503
+ for (let L = 0; L < c.modelLayers; L++) {
504
+ const n = (t) => `blk.${L}.${t}`, raw = (t) => W[n(t)].raw, ly = this.mlayers[L];
505
+ const B_ = { rmsA: this.bind('rmsnormB', [b.x, raw('attn_norm.weight'), b.xn, u.rmsH]) };
506
+ if (ly.attn) {
507
+ Object.assign(B_, {
508
+ q: q4(n('attn_q.weight'), b.xn, b.q, u.q), k: q4(n('attn_k.weight'), b.xn, b.k, u.kv), v: q4(n('attn_v.weight'), b.xn, b.v, u.kv),
509
+ ropeQ: this.bind('qkNormRopeB', [b.q, raw('attn_q_norm.weight'), b.ropeF, u.ropeQ]),
510
+ ropeK: this.bind('qkNormRopeB', [b.k, raw('attn_k_norm.weight'), b.ropeF, u.ropeK]),
511
+ kv: this.bind('kvStoreB', [b.k, b.v, this.kc[L], this.vc[L], u.kvn, qb]),
512
+ score: this.bind('attnScoreB', [b.q, this.kc[L], b.sc, u.att, qb]),
513
+ soft: this.bind('softmaxB', [b.sc, qb]),
514
+ attOut: this.bind('attnOutB', [b.sc, this.vc[L], b.att, u.attOut, qb]),
515
+ o: q4(n('attn_output.weight'), b.att, b.o, u.mmO),
516
+ });
517
+ } else {
518
+ Object.assign(B_, {
519
+ inProj: q4(n('shortconv.in_proj.weight'), b.xn, b.bcx, u.inProj),
520
+ conv: this.bind('shortConvB', [b.bcx, raw('shortconv.conv.weight'), this.cs[L], b.y, u.conv, qb]),
521
+ o: q4(n('shortconv.out_proj.weight'), b.y, b.o, u.mmH),
522
+ });
523
+ }
524
+ B_.addO = this.bind('addInPlaceB', [b.x, b.o, u.nH, qb]);
525
+ B_.rmsF = this.bind('rmsnormB', [b.x, raw('ffn_norm.weight'), b.xn, u.rmsH]);
526
+ if (L < c.lead) {
527
+ Object.assign(B_, {
528
+ mg: q4(n('ffn_gate.weight'), b.xn, b.mg, u.mg), mu: q4(n('ffn_up.weight'), b.xn, b.mu, u.mg),
529
+ mact: this.bind('siluSplitB', [b.mg, b.mu, b.ma, u.silu, qb]),
530
+ md: q4(n('ffn_down.weight'), b.ma, b.o, u.md),
531
+ });
532
+ }
533
+ ly.B = B_;
534
+ }
535
+ for (let l = 0; l < c.layers; l++) {
536
+ const raw = (t) => W[`blk.${l + c.lead}.${t}`].raw;
537
+ this.layers[l].B = {
538
+ router: this.bind('matmulF32T', [b.xn, raw('ffn_gate_inp.weight'), b.rl, u.router]),
539
+ topk: this.bind('topkSigB', [b.rl, (W[`blk.${l + c.lead}.exp_probs_b.bias`] || W[`blk.${l + c.lead}.exp_probs_b`]).raw, b.sel, u.topk]),
540
+ gu: this.bind(this.kq.exB, [b.xn, this.pool.guQ, this.pool.guS, b.gu, u.gu, b.slots]),
541
+ act: this.bind('siluMulMoe', [b.gu, b.act, u.siluE]),
542
+ dn: this.bind(this.kq.exB, [b.act, this.pool.dQ, this.pool.dS, b.dn, u.dn, b.slots]),
543
+ acc: this.bind('moeAccumB', [b.dn, b.sel, b.x, u.acc, qb]),
544
+ ...(SF ? { sh: {
545
+ g: q4(`blk.${l + c.lead}.ffn_gate_shexp.weight`, b.xn, b.mg, u.sg), u: q4(`blk.${l + c.lead}.ffn_up_shexp.weight`, b.xn, b.mu, u.sg),
546
+ act: this.bind('siluSplitB', [b.mg, b.mu, b.ma, u.siluS, qb]),
547
+ d: q4(`blk.${l + c.lead}.ffn_down_shexp.weight`, b.ma, b.o, u.sd),
548
+ add: this.bind('addInPlaceB', [b.x, b.o, u.nH, qb]),
549
+ } } : {}),
550
+ };
551
+ }
552
+ }
553
+
554
+ // ── forward, one token ──
555
+ // Model layer L's operator and residual add, then its FFN norm; the dense layers also run their FFN.
556
+ encodeModelLayer(pass, L, seqLen) {
557
+ const c = this.cfg, g = this.mlayers[L], d = (n, gr, x, y, z) => this.dispatch(pass, n, gr, x, y, z);
558
+ const H = c.hidden, hd = c.attnHeadDim || H / c.heads, QN = c.heads * hd, KVN = c.kvHeadsAttn * hd;
559
+ d('rmsnorm', g.rmsA, 1);
560
+ if (g.attn) {
561
+ d(this.kq.mm, g.q, Math.ceil(QN / 4)); d(this.kq.mm, g.k, Math.ceil(KVN / 4)); d(this.kq.mm, g.v, Math.ceil(KVN / 4));
562
+ d('qkNormRope', g.ropeQ, c.heads); d('qkNormRope', g.ropeK, c.kvHeadsAttn);
563
+ d('kvStore', g.kv, Math.ceil(KVN / 256));
564
+ const ak = this.attnK;
565
+ d(ak.score, g.score, Math.ceil(c.heads * seqLen / 256));
566
+ d(ak.soft, g.soft, c.heads);
567
+ d(ak.out, g.attOut, Math.ceil(QN / 256));
568
+ d(this.kq.mm, g.o, Math.ceil(H / 4));
569
+ } else {
570
+ d(this.kq.mm, g.inProj, Math.ceil(3 * H / 4));
571
+ d('shortConv', g.conv, Math.ceil(H / 256));
572
+ d(this.kq.mm, g.o, Math.ceil(H / 4));
573
+ }
574
+ d('addInPlace', g.addO, Math.ceil(H / 256));
575
+ d('rmsnorm', g.rmsF, 1);
576
+ if (L < c.lead) {
577
+ d(this.kq.mm, g.mg, Math.ceil(c.denseFf / 4)); d(this.kq.mm, g.mu, Math.ceil(c.denseFf / 4));
578
+ d('siluMulMoe', g.mact, Math.ceil(c.denseFf / 256));
579
+ d(this.kq.mm, g.md, Math.ceil(H / 4));
580
+ d('addInPlace', g.addO, Math.ceil(H / 256));
581
+ if (g.cap) d('copyAt', g.cap, Math.ceil(H / 256));
582
+ }
583
+ }
584
+ // MoE layer l up to its routing: (the dense layers first, for l = 0), the operator, the guesses, the router.
585
+ encodeAttention(pass, l, seqLen) {
586
+ const c = this.cfg, g = this.layers[l], d = (n, gr, x, y, z) => this.dispatch(pass, n, gr, x, y, z);
587
+ if (l === 0) for (let L = 0; L < c.lead; L++) this.encodeModelLayer(pass, L, seqLen);
588
+ this.encodeModelLayer(pass, l + c.lead, seqLen);
589
+ if (this.prefetch) for (const p of g.pf) { d('rmsnorm', p.rms, 1); d('matmulF32', p.router, c.experts); d('topkSig', p.topk, 1); }
590
+ d('matmulF32', g.router, c.experts);
591
+ d('topkSig', g.topk, 1);
592
+ }
593
+ encodeExperts(pass, l) {
594
+ const c = this.cfg, g = this.layers[l], d = (n, gr, x, y, z) => this.dispatch(pass, n, gr, x, y, z);
595
+ d(this.kq.ex, g.gu, Math.ceil(2 * c.expertFf / 4), 1, c.topK);
596
+ d('siluMulMoe', g.act, Math.ceil(c.topK * c.expertFf / 256));
597
+ d(this.kq.ex, g.dn, Math.ceil(c.hidden / 4), 1, c.topK);
598
+ d('moeAccum', g.acc, Math.ceil(c.hidden / 256));
599
+ if (g.sh) {
600
+ const s = g.sh;
601
+ d(this.kq.mm, s.g, Math.ceil(c.shexpFf / 4)); d(this.kq.mm, s.u, Math.ceil(c.shexpFf / 4));
602
+ d('siluMulMoe', s.act, Math.ceil(c.shexpFf / 256));
603
+ d(this.kq.mm, s.d, Math.ceil(c.hidden / 4));
604
+ d('addInPlace', s.add, Math.ceil(c.hidden / 256));
605
+ }
606
+ const cap = this.mlayers[l + c.lead].cap;
607
+ if (cap) d('copyAt', cap, Math.ceil(c.hidden / 256));
608
+ }
609
+ encodeHead(pass, want) {
610
+ const c = this.cfg;
611
+ this.dispatch(pass, 'rmsnorm', this.g.rmsOut, 1);
612
+ this.dispatch(pass, this.lmKernel || 'matmulQ6K', this.g.lm, Math.ceil(c.vocab / 4));
613
+ if (want === 'argmax') this.dispatch(pass, 'argmax', this.g.am, 1);
614
+ }
615
+
616
+ // ── forward, a chunk of T tokens ──
617
+ encodeModelLayerB(pass, L, T, S) {
618
+ const c = this.cfg, g = this.mlayers[L].B, d = (nm, gr, x, y, z) => this.dispatch(pass, nm, gr, x, y, z);
619
+ const H = c.hidden, hd = c.attnHeadDim || H / c.heads, QN = c.heads * hd, KVN = c.kvHeadsAttn * hd, tt = Math.ceil(T / 16), mm = (n) => Math.ceil(n / 16);
620
+ d('rmsnormB', g.rmsA, T);
621
+ if (this.mlayers[L].attn) {
622
+ d(this.kq.mmT, g.q, mm(QN), tt); d(this.kq.mmT, g.k, mm(KVN), tt); d(this.kq.mmT, g.v, mm(KVN), tt);
623
+ d('qkNormRopeB', g.ropeQ, c.heads, T); d('qkNormRopeB', g.ropeK, c.kvHeadsAttn, T);
624
+ d('kvStoreB', g.kv, Math.ceil(KVN / 256), T);
625
+ const ak = this.attnK;
626
+ d(ak.scoreB, g.score, Math.ceil(c.heads * T * S / 256));
627
+ d(ak.softB, g.soft, c.heads * T);
628
+ d(ak.outB, g.attOut, Math.ceil(QN / 256), T);
629
+ d(this.kq.mmT, g.o, mm(H), tt);
630
+ } else {
631
+ d(this.kq.mmT, g.inProj, mm(3 * H), tt);
632
+ d('shortConvB', g.conv, Math.ceil(H / 256));
633
+ d(this.kq.mmT, g.o, mm(H), tt);
634
+ }
635
+ d('addInPlaceB', g.addO, Math.ceil(H / 256), T);
636
+ d('rmsnormB', g.rmsF, T);
637
+ if (L < c.lead) {
638
+ d(this.kq.mmT, g.mg, mm(c.denseFf), tt); d(this.kq.mmT, g.mu, mm(c.denseFf), tt);
639
+ d('siluSplitB', g.mact, Math.ceil(c.denseFf / 256), T);
640
+ d(this.kq.mmT, g.md, mm(H), tt);
641
+ d('addInPlaceB', g.addO, Math.ceil(H / 256), T);
642
+ }
643
+ }
644
+ encodeTrunkB(pass, l, T, S) {
645
+ const c = this.cfg, g = this.layers[l].B;
646
+ if (l === 0) for (let L = 0; L < c.lead; L++) this.encodeModelLayerB(pass, L, T, S);
647
+ this.encodeModelLayerB(pass, l + c.lead, T, S);
648
+ this.dispatch(pass, 'matmulF32T', g.router, c.experts, T);
649
+ this.dispatch(pass, 'topkSigB', g.topk, T);
650
+ }
651
+ encodeExpertsB(pass, l, T) {
652
+ const c = this.cfg, g = this.layers[l].B, d = (nm, gr, x, y, z) => this.dispatch(pass, nm, gr, x, y, z);
653
+ d(this.kq.exB, g.gu, Math.ceil(2 * c.expertFf / 4), 1, T * c.topK);
654
+ d('siluMulMoe', g.act, Math.ceil(T * c.topK * c.expertFf / 256));
655
+ d(this.kq.exB, g.dn, Math.ceil(c.hidden / 4), 1, T * c.topK);
656
+ d('moeAccumB', g.acc, Math.ceil(c.hidden / 256), T);
657
+ if (g.sh) {
658
+ const s = g.sh, tt = Math.ceil(T / 16), mm = (n) => Math.ceil(n / 16);
659
+ d(this.kq.mmT, s.g, mm(c.shexpFf), tt); d(this.kq.mmT, s.u, mm(c.shexpFf), tt);
660
+ d('siluSplitB', s.act, Math.ceil(c.shexpFf / 256), T);
661
+ d(this.kq.mmT, s.d, mm(c.hidden), tt);
662
+ d('addInPlaceB', s.add, Math.ceil(c.hidden / 256), T);
663
+ }
664
+ }
665
+
666
+ // The conv state cannot rewind, so any change before the cached end restarts from token 0.
667
+ reset() {
668
+ super.reset();
669
+ if (!this.cs) return;
670
+ const enc = this.device.createCommandEncoder();
671
+ for (const s of this.cs) if (s) enc.clearBuffer(s);
672
+ this.device.queue.submit([enc.finish()]);
673
+ }
674
+ async prefill(ids, want = 'argmax') {
675
+ let common = 0;
676
+ while (common < ids.length - 1 && common < this.cached.length && this.cached[common] === ids[common]) common++;
677
+ if (common < this.cached.length) this.reset();
678
+ const rest = ids.slice(this.cached.length);
679
+ let r = null;
680
+ if (!this.batchPrefill || rest.length < 2) {
681
+ for (let i = 0; i < rest.length; i++) r = await this.step(rest[i], i === rest.length - 1 ? want : 'none');
682
+ return r;
683
+ }
684
+ for (let i = 0; i < rest.length; i += this.chunkTokens) {
685
+ r = await this.prefillChunk(rest.slice(i, i + this.chunkTokens), i + this.chunkTokens >= rest.length ? want : 'none');
686
+ }
687
+ return r;
688
+ }
689
+
690
+ // The template has no thinking-off form; enableThinking is ignored.
691
+ async *generateTurn(messages, opts = {}) { yield* super.generateTurn(messages, { ...opts, enableThinking: true }); }
692
+
693
+ // Debug: each model layer's output from the last one-token step.
694
+ async readCapture() {
695
+ const c = this.cfg, H = c.hidden, n = c.modelLayers * H * 4;
696
+ const rb = this.device.createBuffer({ size: n, usage: MAP_READ | COPY_DST });
697
+ const enc = this.device.createCommandEncoder();
698
+ enc.copyBufferToBuffer(this.a.cap, 0, rb, 0, n);
699
+ this.device.queue.submit([enc.finish()]);
700
+ await rb.mapAsync(1);
701
+ const all = new Float32Array(rb.getMappedRange().slice(0));
702
+ rb.unmap(); rb.destroy();
703
+ return { layers: Array.from({ length: c.modelLayers }, (_, L) => all.subarray(L * H, (L + 1) * H)) };
704
+ }
705
+ }
706
+
707
+ // For kernel tests (scripts): the WGSL this module adds.
708
+ export { L2 as LFM2MOE_KERNELS, L2B as LFM2MOE_BATCH_KERNELS };