diskformer 0.0.1 → 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.
@@ -0,0 +1,1231 @@
1
+ /* qwen35_moe_ssd.js — Qwen3.5/3.6 MoE (Qwen3.6-35B-A3B, GGUF arch `qwen35moe`) in a browser tab,
2
+ * larger than RAM: rung 2b of the SSD-streaming ladder. The routed experts stream from OPFS into
3
+ * a GPU slot pool exactly as in rung 2a (qwen3_moe_ssd.js, which this module extends); what is new
4
+ * is the hybrid trunk:
5
+ * - 3 of every 4 layers are Gated DeltaNet (linear attention): a 4-tap causal conv over the
6
+ * q|k|v projection, L2-normalised q and k, and the gated delta rule on a 128×128 state per
7
+ * value head; output = RMSNorm(o)·w · SiLU(z), then a projection.
8
+ * - every 4th layer is full attention with a per-head output gate (the q projection carries
9
+ * q and gate per head), q/k RMSNorm and NeoX RoPE on the first 64 of 256 dims.
10
+ * - each MoE block adds a shared expert, scaled by sigmoid(x·w_gate).
11
+ * The math follows llama.cpp's qwen35moe graph and its CPU ops (ggml_gated_delta_net, ssm_conv,
12
+ * rope_multi with text positions), so greedy output can be gated against llama-server on the
13
+ * same GGUF. Decode is one token at a time (prefill feeds tokens through the same step).
14
+ *
15
+ * const m = await Qwen35MoeSsd.load(null, { url: '…/Qwen3.6-35B-A3B-Q8_0.gguf', poolBytes });
16
+ */
17
+
18
+ import { Qwen3MoeSsd, STORAGE, COPY_SRC, COPY_DST, MAP_READ, UNIFORM, I8 } from './qwen3_moe_ssd.js';
19
+
20
+ export const QWEN36_35B_A3B = {
21
+ repo: 'unsloth/Qwen3.6-35B-A3B-GGUF',
22
+ file: 'Qwen3.6-35B-A3B-Q8_0.gguf',
23
+ revision: 'a483e9e6cbd595906af30beda3187c2663a1118c',
24
+ sha256: 'd1a395809f65a43a13ad119eb4e7acdef1ac6d68120f39902c8ab96e72794a59',
25
+ size: 36903140320,
26
+ };
27
+
28
+ const SIGMOID = 'fn sigm(x: f32) -> f32 { return 1.0 / (1.0 + exp(-x)); }';
29
+
30
+ const KERNELS = {
31
+ // Per-head RMSNorm (QK-norm), then NeoX RoPE on the first nRot dims (pairs i, i + nRot/2);
32
+ // dims ≥ nRot keep the normed value. src rows are srcStride apart (the q projection holds
33
+ // [q hd | gate hd] per head); with gate = 1 the gate half is copied to gout. One workgroup per head.
34
+ qkNormRopeP: `
35
+ struct P { hd: u32, srcStride: u32, nRot: u32, gate: u32, eps: f32 }
36
+ @group(0) @binding(0) var<storage, read> src: array<f32>;
37
+ @group(0) @binding(1) var<storage, read> w: array<f32>;
38
+ @group(0) @binding(2) var<storage, read> rope: array<f32>;
39
+ @group(0) @binding(3) var<storage, read_write> dst: array<f32>;
40
+ @group(0) @binding(4) var<storage, read_write> gout: array<f32>;
41
+ @group(0) @binding(5) var<uniform> p: P;
42
+ var<workgroup> red: array<f32, 64>;
43
+ @compute @workgroup_size(64) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
44
+ let sb = wg.x * p.srcStride; let ob = wg.x * p.hd; let t = l.x; let half = p.nRot / 2u;
45
+ var acc = 0.0;
46
+ for (var i = t; i < p.hd; i += 64u) { let v = src[sb + i]; acc += v * v; }
47
+ red[t] = acc; workgroupBarrier();
48
+ for (var s = 32u; s > 0u; s >>= 1u) { if (t < s) { red[t] += red[t + s]; } workgroupBarrier(); }
49
+ let scale = 1.0 / sqrt(red[0] / f32(p.hd) + p.eps);
50
+ for (var i = t; i < p.hd; i += 64u) {
51
+ if (i < half) {
52
+ let a = (src[sb + i] * scale) * w[i];
53
+ let b = (src[sb + i + half] * scale) * w[i + half];
54
+ let c = rope[i]; let sn = rope[half + i];
55
+ dst[ob + i] = a * c - b * sn;
56
+ dst[ob + i + half] = a * sn + b * c;
57
+ } else if (i >= p.nRot) {
58
+ dst[ob + i] = (src[sb + i] * scale) * w[i];
59
+ }
60
+ if (p.gate == 1u) { gout[ob + i] = src[sb + p.hd + i]; }
61
+ }
62
+ }`,
63
+ // x[i] *= sigmoid(g[i]) — the full-attention output gate.
64
+ sigmoidMul: `
65
+ struct P { n: u32 }
66
+ @group(0) @binding(0) var<storage, read_write> x: array<f32>;
67
+ @group(0) @binding(1) var<storage, read> g: array<f32>;
68
+ @group(0) @binding(2) var<uniform> p: P;
69
+ ${SIGMOID}
70
+ @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gi: vec3<u32>) {
71
+ let i = gi.x; if (i < p.n) { x[i] = x[i] * sigm(g[i]); }
72
+ }`,
73
+ // Causal depthwise conv, kernel 4, over the q|k|v projection, then SiLU. state holds each
74
+ // channel's previous 3 inputs, oldest first (llama.cpp's conv state), and rolls forward.
75
+ gdnConv: `
76
+ struct P { n: u32 }
77
+ @group(0) @binding(0) var<storage, read> mixed: array<f32>;
78
+ @group(0) @binding(1) var<storage, read> w: array<f32>;
79
+ @group(0) @binding(2) var<storage, read_write> st: array<f32>;
80
+ @group(0) @binding(3) var<storage, read_write> y: array<f32>;
81
+ @group(0) @binding(4) var<uniform> p: P;
82
+ @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gi: vec3<u32>) {
83
+ let c = gi.x; if (c >= p.n) { return; }
84
+ let s0 = st[c * 3u]; let s1 = st[c * 3u + 1u]; let s2 = st[c * 3u + 2u]; let x = mixed[c];
85
+ var sum = 0.0;
86
+ sum += s0 * w[c * 4u]; sum += s1 * w[c * 4u + 1u]; sum += s2 * w[c * 4u + 2u]; sum += x * w[c * 4u + 3u];
87
+ y[c] = sum / (1.0 + exp(-sum));
88
+ st[c * 3u] = s1; st[c * 3u + 1u] = s2; st[c * 3u + 2u] = x;
89
+ }`,
90
+ // The gated delta rule for one token. Workgroup h = value head, thread j = value row j of the
91
+ // head's state M (M[j][i] = S[i][j], llama.cpp's layout; i runs over the key dim). k-head = h % kH.
92
+ // q, k ← L2-normalised (x / sqrt(Σx² + eps)); beta = σ(b); g = softplus(a + dt) · A
93
+ // M ← M·e^g; d_j = β (v_j − Σ_i M[j][i] k_i); M[j][i] += k_i d_j; o_j = (Σ_i M[j][i] q_i) / √dS
94
+ gdnStep: `
95
+ struct P { kH: u32, dS: u32, keyDim: u32, pad: u32, eps: f32, scale: f32 }
96
+ @group(0) @binding(0) var<storage, read> cv: array<f32>;
97
+ @group(0) @binding(1) var<storage, read> bb: array<f32>;
98
+ @group(0) @binding(2) var<storage, read> al: array<f32>;
99
+ @group(0) @binding(3) var<storage, read> dt: array<f32>;
100
+ @group(0) @binding(4) var<storage, read> aa: array<f32>;
101
+ @group(0) @binding(5) var<storage, read_write> M: array<f32>;
102
+ @group(0) @binding(6) var<storage, read_write> o: array<f32>;
103
+ @group(0) @binding(7) var<uniform> p: P;
104
+ ${SIGMOID}
105
+ var<workgroup> kq: array<f32, 128>;
106
+ var<workgroup> qq: array<f32, 128>;
107
+ var<workgroup> rq: array<f32, 128>;
108
+ var<workgroup> rk: array<f32, 128>;
109
+ fn log1p_(u: f32) -> f32 { let y = 1.0 + u; return log(y) - ((y - 1.0) - u) / y; }
110
+ @compute @workgroup_size(128) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
111
+ let h = wg.x; let j = l.x; let dS = p.dS; let kh = h % p.kH;
112
+ let qv = cv[kh * dS + j]; let kv = cv[p.keyDim + kh * dS + j]; let vv = cv[2u * p.keyDim + h * dS + j];
113
+ rq[j] = qv * qv; rk[j] = kv * kv; workgroupBarrier();
114
+ for (var s = 64u; s > 0u; s >>= 1u) { if (j < s) { rq[j] += rq[j + s]; rk[j] += rk[j + s]; } workgroupBarrier(); }
115
+ let n = f32(dS); let inv = 1.0 / sqrt(n);
116
+ qq[j] = (qv * (1.0 / sqrt(rq[0] / n + p.eps / n))) * inv;
117
+ kq[j] = (kv * (1.0 / sqrt(rk[0] / n + p.eps / n))) * inv;
118
+ workgroupBarrier();
119
+ let beta = sigm(bb[h]);
120
+ let x = al[h] + dt[h];
121
+ let sp = select(log1p_(exp(x)), x, x > 20.0);
122
+ let decay = exp(sp * aa[h]);
123
+ let row = (h * dS + j) * dS;
124
+ var sk = 0.0;
125
+ for (var i = 0u; i < dS; i++) { let m = M[row + i] * decay; M[row + i] = m; sk += m * kq[i]; }
126
+ let d = (vv - sk) * beta;
127
+ var acc = 0.0;
128
+ for (var i = 0u; i < dS; i++) { let m = M[row + i] + kq[i] * d; M[row + i] = m; acc += m * qq[i]; }
129
+ o[h * dS + j] = acc * p.scale;
130
+ }`,
131
+ // Per value head: RMSNorm(o)·w · SiLU(z).
132
+ gdnNormGate: `
133
+ struct P { dS: u32, eps: f32 }
134
+ @group(0) @binding(0) var<storage, read> o: array<f32>;
135
+ @group(0) @binding(1) var<storage, read> w: array<f32>;
136
+ @group(0) @binding(2) var<storage, read> z: array<f32>;
137
+ @group(0) @binding(3) var<storage, read_write> y: array<f32>;
138
+ @group(0) @binding(4) var<uniform> p: P;
139
+ var<workgroup> red: array<f32, 128>;
140
+ @compute @workgroup_size(128) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
141
+ let h = wg.x; let j = l.x; let i = h * p.dS + j;
142
+ let v = o[i]; red[j] = v * v; workgroupBarrier();
143
+ for (var s = 64u; s > 0u; s >>= 1u) { if (j < s) { red[j] += red[j + s]; } workgroupBarrier(); }
144
+ let scale = 1.0 / sqrt(red[0] / f32(p.dS) + p.eps);
145
+ let zz = z[i];
146
+ y[i] = ((v * scale) * w[j]) * (zz / (1.0 + exp(-zz)));
147
+ }`,
148
+ // x[h] = (Σ_j w_j·out[j·H + h] + sh[h]·σ(sg)) + x[h]: routed experts, the gated shared expert,
149
+ // then the residual — llama.cpp's order.
150
+ moeAccumShared: `
151
+ struct P { H: u32, k: u32 }
152
+ @group(0) @binding(0) var<storage, read> out: array<f32>;
153
+ @group(0) @binding(1) var<storage, read> sel: array<u32>;
154
+ @group(0) @binding(2) var<storage, read> sh: array<f32>;
155
+ @group(0) @binding(3) var<storage, read> sg: array<f32>;
156
+ @group(0) @binding(4) var<storage, read_write> x: array<f32>;
157
+ @group(0) @binding(5) var<uniform> p: P;
158
+ ${SIGMOID}
159
+ @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gi: vec3<u32>) {
160
+ let h = gi.x; if (h >= p.H) { return; }
161
+ var acc = 0.0;
162
+ for (var j = 0u; j < p.k; j++) { acc += bitcast<f32>(sel[p.k + j]) * out[j * p.H + h]; }
163
+ x[h] = (acc + sh[h] * sigm(sg[0])) + x[h];
164
+ }`,
165
+ };
166
+
167
+
168
+ // ── Batched prefill: a chunk of T prompt tokens per layer ───────────────────────────────────
169
+ // Every batched buffer is token-major (row t holds token pos0 + t). Q is the per-chunk uniform.
170
+ const QB = 'struct Q { T: u32, pos0: u32, S: u32, pad: u32 }';
171
+ const BATCH_KERNELS = {
172
+ embedQ8B: `enable f16;
173
+ struct P { n: u32 }
174
+ ${QB}
175
+ @group(0) @binding(0) var<storage, read> q: array<u32>;
176
+ @group(0) @binding(1) var<storage, read> s: array<f16>;
177
+ @group(0) @binding(2) var<storage, read> ids: array<u32>;
178
+ @group(0) @binding(3) var<storage, read_write> y: array<f32>;
179
+ @group(0) @binding(4) var<uniform> p: P;
180
+ @group(0) @binding(5) var<uniform> qd: Q;
181
+ ${I8}
182
+ @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>) {
183
+ let i = g.x; let t = g.y; if (i >= p.n || t >= qd.T) { return; }
184
+ let e = ids[t] * p.n + i;
185
+ y[t * p.n + i] = i8at(q[e >> 2u], e & 3u) * f32(s[e >> 5u]);
186
+ }`,
187
+ // One workgroup per token row.
188
+ rmsnormB: `
189
+ struct P { n: u32, eps: f32 }
190
+ @group(0) @binding(0) var<storage, read> x: array<f32>;
191
+ @group(0) @binding(1) var<storage, read> w: array<f32>;
192
+ @group(0) @binding(2) var<storage, read_write> y: array<f32>;
193
+ @group(0) @binding(3) var<uniform> p: P;
194
+ var<workgroup> red: array<f32, 256>;
195
+ @compute @workgroup_size(256) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
196
+ let b = wg.x * p.n; let t = l.x; var acc = 0.0;
197
+ for (var i = t; i < p.n; i += 256u) { let v = x[b + i]; acc += v * v; }
198
+ red[t] = acc; workgroupBarrier();
199
+ for (var s = 128u; s > 0u; s >>= 1u) { if (t < s) { red[t] += red[t + s]; } workgroupBarrier(); }
200
+ let scale = 1.0 / sqrt(red[0] / f32(p.n) + p.eps);
201
+ for (var i = t; i < p.n; i += 256u) { y[b + i] = (x[b + i] * scale) * w[i]; }
202
+ }`,
203
+ // Tiled Q8_0 GEMM: y[t·M + m] = Σ_k x[t·N + k]·W[m][k]. A workgroup computes 16 rows × 16 tokens;
204
+ // each 32-wide k block (one Q8_0 block) of W and x goes through shared memory, so every weight
205
+ // byte read serves 16 tokens. Dispatch (ceil(M/16), ceil(T/16)).
206
+ matmulQ8T: `enable f16;
207
+ struct P { M: u32, N: u32 }
208
+ ${QB}
209
+ @group(0) @binding(0) var<storage, read> x: array<f32>;
210
+ @group(0) @binding(1) var<storage, read> qw: array<u32>;
211
+ @group(0) @binding(2) var<storage, read> qs: array<f16>;
212
+ @group(0) @binding(3) var<storage, read_write> y: array<f32>;
213
+ @group(0) @binding(4) var<uniform> p: P;
214
+ @group(0) @binding(5) var<uniform> qd: Q;
215
+ ${I8}
216
+ var<workgroup> ws: array<f32, 512>;
217
+ var<workgroup> xs: array<f32, 512>;
218
+ @compute @workgroup_size(256) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
219
+ let m0 = wg.x * 16u; let t0 = wg.y * 16u; let r = l.x / 16u; let c = l.x % 16u;
220
+ let nb = p.N / 32u; var acc = 0.0;
221
+ for (var b = 0u; b < nb; b++) {
222
+ for (var i = l.x; i < 512u; i += 256u) {
223
+ let rr = i / 32u; let kk = i % 32u;
224
+ let m = m0 + rr; var wv = 0.0;
225
+ if (m < p.M) { let e = m * p.N + b * 32u + kk; wv = i8at(qw[e >> 2u], e & 3u) * f32(qs[m * nb + b]); }
226
+ ws[i] = wv;
227
+ let t = t0 + rr; var xv = 0.0;
228
+ if (t < qd.T) { xv = x[t * p.N + b * 32u + kk]; }
229
+ xs[i] = xv;
230
+ }
231
+ workgroupBarrier();
232
+ for (var kk = 0u; kk < 32u; kk++) { acc += ws[r * 32u + kk] * xs[c * 32u + kk]; }
233
+ workgroupBarrier();
234
+ }
235
+ let m = m0 + r; let t = t0 + c;
236
+ if (m < p.M && t < qd.T) { y[t * p.M + m] = acc; }
237
+ }`,
238
+ // F32 matrix × T vectors, one workgroup per (row, token). Dispatch (M, T).
239
+ matmulF32T: `
240
+ struct P { M: u32, N: u32 }
241
+ @group(0) @binding(0) var<storage, read> x: array<f32>;
242
+ @group(0) @binding(1) var<storage, read> w: array<f32>;
243
+ @group(0) @binding(2) var<storage, read_write> y: array<f32>;
244
+ @group(0) @binding(3) var<uniform> p: P;
245
+ var<workgroup> red: array<f32, 256>;
246
+ @compute @workgroup_size(256) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
247
+ let m = wg.x; let t = wg.y; let i = l.x; var acc = 0.0;
248
+ for (var k = i; k < p.N; k += 256u) { acc += w[m * p.N + k] * x[t * p.N + k]; }
249
+ red[i] = acc; workgroupBarrier();
250
+ for (var s = 128u; s > 0u; s >>= 1u) { if (i < s) { red[i] += red[i + s]; } workgroupBarrier(); }
251
+ if (i == 0u) { y[t * p.M + m] = red[0]; }
252
+ }`,
253
+ // The router top-k for token wg.x (same picks and weights as the one-token kernel).
254
+ topkB: `
255
+ struct P { n: u32, k: u32 }
256
+ @group(0) @binding(0) var<storage, read> lg: array<f32>;
257
+ @group(0) @binding(1) var<storage, read_write> sel: array<u32>;
258
+ @group(0) @binding(2) var<uniform> p: P;
259
+ var<workgroup> red: array<f32, 256>;
260
+ var<workgroup> bi: array<u32, 256>;
261
+ var<workgroup> gmax: f32;
262
+ var<workgroup> gsum: f32;
263
+ var<workgroup> picks: array<u32, 32>;
264
+ var<workgroup> probs: array<f32, 32>;
265
+ @compute @workgroup_size(256) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
266
+ let t = l.x; let n = p.n; let lb = wg.x * n; let sb = wg.x * 2u * p.k;
267
+ red[t] = select(-3.4e38, lg[lb + min(t, n - 1u)], t < n); workgroupBarrier();
268
+ for (var s = 128u; s > 0u; s >>= 1u) { if (t < s) { red[t] = max(red[t], red[t + s]); } workgroupBarrier(); }
269
+ if (t == 0u) { gmax = red[0]; } workgroupBarrier();
270
+ red[t] = select(0.0, exp(lg[lb + min(t, n - 1u)] - gmax), t < n); workgroupBarrier();
271
+ for (var s = 128u; s > 0u; s >>= 1u) { if (t < s) { red[t] += red[t + s]; } workgroupBarrier(); }
272
+ if (t == 0u) { gsum = red[0]; } workgroupBarrier();
273
+ var mine = select(-1.0, exp(lg[lb + min(t, n - 1u)] - gmax) / gsum, t < n);
274
+ for (var j = 0u; j < p.k; j++) {
275
+ red[t] = mine; bi[t] = t; workgroupBarrier();
276
+ for (var s = 128u; s > 0u; s >>= 1u) {
277
+ 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]; } }
278
+ workgroupBarrier();
279
+ }
280
+ if (t == 0u) { picks[j] = bi[0]; probs[j] = red[0]; }
281
+ workgroupBarrier();
282
+ if (t == picks[j]) { mine = -1.0; }
283
+ workgroupBarrier();
284
+ }
285
+ if (t == 0u) {
286
+ var wsum = 0.0;
287
+ for (var j = 0u; j < p.k; j++) { wsum += probs[j]; }
288
+ for (var j = 0u; j < p.k; j++) { sel[sb + j] = picks[j]; sel[sb + p.k + j] = bitcast<u32>(probs[j] / wsum); }
289
+ }
290
+ }`,
291
+ // The 4-tap conv over T tokens in order, rolling the state.
292
+ gdnConvB: `
293
+ struct P { n: u32 }
294
+ ${QB}
295
+ @group(0) @binding(0) var<storage, read> mixed: array<f32>;
296
+ @group(0) @binding(1) var<storage, read> w: array<f32>;
297
+ @group(0) @binding(2) var<storage, read_write> st: array<f32>;
298
+ @group(0) @binding(3) var<storage, read_write> y: array<f32>;
299
+ @group(0) @binding(4) var<uniform> p: P;
300
+ @group(0) @binding(5) var<uniform> qd: Q;
301
+ @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gi: vec3<u32>) {
302
+ let c = gi.x; if (c >= p.n) { return; }
303
+ var s0 = st[c * 3u]; var s1 = st[c * 3u + 1u]; var s2 = st[c * 3u + 2u];
304
+ let w0 = w[c * 4u]; let w1 = w[c * 4u + 1u]; let w2 = w[c * 4u + 2u]; let w3 = w[c * 4u + 3u];
305
+ for (var t = 0u; t < qd.T; t++) {
306
+ let x = mixed[t * p.n + c];
307
+ var sum = 0.0;
308
+ sum += s0 * w0; sum += s1 * w1; sum += s2 * w2; sum += x * w3;
309
+ y[t * p.n + c] = sum / (1.0 + exp(-sum));
310
+ s0 = s1; s1 = s2; s2 = x;
311
+ }
312
+ st[c * 3u] = s0; st[c * 3u + 1u] = s1; st[c * 3u + 2u] = s2;
313
+ }`,
314
+ // The gated delta rule over T tokens in order (one workgroup per value head, as gdnStep).
315
+ gdnStepB: `
316
+ struct P { kH: u32, dS: u32, keyDim: u32, convDim: u32, vH: u32, pad: u32, eps: f32, scale: f32 }
317
+ ${QB}
318
+ @group(0) @binding(0) var<storage, read> cv: array<f32>;
319
+ @group(0) @binding(1) var<storage, read> bb: array<f32>;
320
+ @group(0) @binding(2) var<storage, read> al: array<f32>;
321
+ @group(0) @binding(3) var<storage, read> dt: array<f32>;
322
+ @group(0) @binding(4) var<storage, read> aa: array<f32>;
323
+ @group(0) @binding(5) var<storage, read_write> M: array<f32>;
324
+ @group(0) @binding(6) var<storage, read_write> o: array<f32>;
325
+ @group(0) @binding(7) var<uniform> p: P;
326
+ @group(0) @binding(8) var<uniform> qd: Q;
327
+ ${SIGMOID}
328
+ var<workgroup> kq: array<f32, 128>;
329
+ var<workgroup> qq: array<f32, 128>;
330
+ var<workgroup> rq: array<f32, 128>;
331
+ var<workgroup> rk: array<f32, 128>;
332
+ fn log1p_(u: f32) -> f32 { let y = 1.0 + u; return log(y) - ((y - 1.0) - u) / y; }
333
+ @compute @workgroup_size(128) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
334
+ let h = wg.x; let j = l.x; let dS = p.dS; let kh = h % p.kH;
335
+ let n = f32(dS); let inv = 1.0 / sqrt(n);
336
+ let row = (h * dS + j) * dS;
337
+ let dth = dt[h]; let aah = aa[h];
338
+ for (var t = 0u; t < qd.T; t++) {
339
+ let cb = t * p.convDim;
340
+ let qv = cv[cb + kh * dS + j]; let kv = cv[cb + p.keyDim + kh * dS + j]; let vv = cv[cb + 2u * p.keyDim + h * dS + j];
341
+ rq[j] = qv * qv; rk[j] = kv * kv; workgroupBarrier();
342
+ for (var s = 64u; s > 0u; s >>= 1u) { if (j < s) { rq[j] += rq[j + s]; rk[j] += rk[j + s]; } workgroupBarrier(); }
343
+ qq[j] = (qv * (1.0 / sqrt(rq[0] / n + p.eps / n))) * inv;
344
+ kq[j] = (kv * (1.0 / sqrt(rk[0] / n + p.eps / n))) * inv;
345
+ workgroupBarrier();
346
+ let beta = sigm(bb[t * p.vH + h]);
347
+ let x = al[t * p.vH + h] + dth;
348
+ let sp = select(log1p_(exp(x)), x, x > 20.0);
349
+ let decay = exp(sp * aah);
350
+ var sk = 0.0;
351
+ for (var i = 0u; i < dS; i++) { let m = M[row + i] * decay; M[row + i] = m; sk += m * kq[i]; }
352
+ let d = (vv - sk) * beta;
353
+ var acc = 0.0;
354
+ for (var i = 0u; i < dS; i++) { let m = M[row + i] + kq[i] * d; M[row + i] = m; acc += m * qq[i]; }
355
+ o[t * p.vH * dS + h * dS + j] = acc * p.scale;
356
+ workgroupBarrier();
357
+ }
358
+ }`,
359
+ // RMSNorm(o)·w·SiLU(z) for (value head wg.x, token wg.y).
360
+ gdnNormGateB: `
361
+ struct P { dS: u32, dInner: u32, eps: f32 }
362
+ @group(0) @binding(0) var<storage, read> o: array<f32>;
363
+ @group(0) @binding(1) var<storage, read> w: array<f32>;
364
+ @group(0) @binding(2) var<storage, read> z: array<f32>;
365
+ @group(0) @binding(3) var<storage, read_write> y: array<f32>;
366
+ @group(0) @binding(4) var<uniform> p: P;
367
+ var<workgroup> red: array<f32, 128>;
368
+ @compute @workgroup_size(128) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
369
+ let j = l.x; let i = wg.y * p.dInner + wg.x * p.dS + j;
370
+ let v = o[i]; red[j] = v * v; workgroupBarrier();
371
+ for (var s = 64u; s > 0u; s >>= 1u) { if (j < s) { red[j] += red[j + s]; } workgroupBarrier(); }
372
+ let scale = 1.0 / sqrt(red[0] / f32(p.dS) + p.eps);
373
+ let zz = z[i];
374
+ y[i] = ((v * scale) * w[j]) * (zz / (1.0 + exp(-zz)));
375
+ }`,
376
+ // qkNormRopeP for (head wg.x, token wg.y); rope holds nRot values per token.
377
+ qkNormRopePB: `
378
+ struct P { hd: u32, srcStride: u32, srcRow: u32, dstRow: u32, nRot: u32, gate: u32, eps: f32 }
379
+ @group(0) @binding(0) var<storage, read> src: array<f32>;
380
+ @group(0) @binding(1) var<storage, read> w: array<f32>;
381
+ @group(0) @binding(2) var<storage, read> rope: array<f32>;
382
+ @group(0) @binding(3) var<storage, read_write> dst: array<f32>;
383
+ @group(0) @binding(4) var<storage, read_write> gout: array<f32>;
384
+ @group(0) @binding(5) var<uniform> p: P;
385
+ var<workgroup> red: array<f32, 64>;
386
+ @compute @workgroup_size(64) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
387
+ let tk = wg.y; let sb = tk * p.srcRow + wg.x * p.srcStride; let ob = tk * p.dstRow + wg.x * p.hd; let rb = tk * p.nRot;
388
+ let t = l.x; let half = p.nRot / 2u;
389
+ var acc = 0.0;
390
+ for (var i = t; i < p.hd; i += 64u) { let v = src[sb + i]; acc += v * v; }
391
+ red[t] = acc; workgroupBarrier();
392
+ for (var s = 32u; s > 0u; s >>= 1u) { if (t < s) { red[t] += red[t + s]; } workgroupBarrier(); }
393
+ let scale = 1.0 / sqrt(red[0] / f32(p.hd) + p.eps);
394
+ for (var i = t; i < p.hd; i += 64u) {
395
+ if (i < half) {
396
+ let a = (src[sb + i] * scale) * w[i];
397
+ let b = (src[sb + i + half] * scale) * w[i + half];
398
+ let c = rope[rb + i]; let sn = rope[rb + half + i];
399
+ dst[ob + i] = a * c - b * sn;
400
+ dst[ob + i + half] = a * sn + b * c;
401
+ } else if (i >= p.nRot) {
402
+ dst[ob + i] = (src[sb + i] * scale) * w[i];
403
+ }
404
+ if (p.gate == 1u) { gout[ob + i] = src[sb + p.hd + i]; }
405
+ }
406
+ }`,
407
+ kvStoreB: `enable f16;
408
+ struct P { n: u32 }
409
+ ${QB}
410
+ @group(0) @binding(0) var<storage, read> k: array<f32>;
411
+ @group(0) @binding(1) var<storage, read> v: array<f32>;
412
+ @group(0) @binding(2) var<storage, read_write> kc: array<f16>;
413
+ @group(0) @binding(3) var<storage, read_write> vc: array<f16>;
414
+ @group(0) @binding(4) var<uniform> p: P;
415
+ @group(0) @binding(5) var<uniform> qd: Q;
416
+ @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>) {
417
+ let i = g.x; let t = g.y; if (i >= p.n || t >= qd.T) { return; }
418
+ let pos = qd.pos0 + t;
419
+ kc[pos * p.n + i] = f16(k[t * p.n + i]);
420
+ vc[pos * p.n + i] = f16(v[t * p.n + i]);
421
+ }`,
422
+ // Causal scores for T queries over S = pos0 + T keys; masked keys get -3.4e38.
423
+ attnScoreB: `enable f16;
424
+ struct P { heads: u32, kvHeads: u32, hd: u32, scale: f32 }
425
+ ${QB}
426
+ @group(0) @binding(0) var<storage, read> q: array<f32>;
427
+ @group(0) @binding(1) var<storage, read> kc: array<f16>;
428
+ @group(0) @binding(2) var<storage, read_write> sc: array<f32>;
429
+ @group(0) @binding(3) var<uniform> p: P;
430
+ @group(0) @binding(4) var<uniform> qd: Q;
431
+ @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>, @builtin(num_workgroups) ng: vec3<u32>) {
432
+ let idx = g.y * ng.x * 256u + g.x; let S = qd.S; let T = qd.T;
433
+ if (idx >= p.heads * T * S) { return; }
434
+ let h = idx / (T * S); let rem = idx % (T * S); let t = rem / S; let pos = rem % S;
435
+ if (pos > qd.pos0 + t) { sc[idx] = -3.4e38; return; }
436
+ let kvh = h / (p.heads / p.kvHeads);
437
+ let qo = t * p.heads * p.hd + h * p.hd; let ko = pos * p.kvHeads * p.hd + kvh * p.hd;
438
+ var acc = 0.0;
439
+ for (var d = 0u; d < p.hd; d++) { acc += q[qo + d] * f32(kc[ko + d]); }
440
+ sc[idx] = acc * p.scale;
441
+ }`,
442
+ softmaxB: `
443
+ ${QB}
444
+ @group(0) @binding(0) var<storage, read_write> sc: array<f32>;
445
+ @group(0) @binding(1) var<uniform> qd: Q;
446
+ var<workgroup> red: array<f32, 256>;
447
+ @compute @workgroup_size(256) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
448
+ let n = qd.S; let base = wg.x * n; let t = l.x;
449
+ var mx = -3.4e38;
450
+ for (var i = t; i < n; i += 256u) { mx = max(mx, sc[base + i]); }
451
+ red[t] = mx; workgroupBarrier();
452
+ for (var s = 128u; s > 0u; s >>= 1u) { if (t < s) { red[t] = max(red[t], red[t + s]); } workgroupBarrier(); }
453
+ let m = red[0]; workgroupBarrier();
454
+ var sum = 0.0;
455
+ for (var i = t; i < n; i += 256u) { let e = exp(sc[base + i] - m); sc[base + i] = e; sum += e; }
456
+ red[t] = sum; workgroupBarrier();
457
+ for (var s = 128u; s > 0u; s >>= 1u) { if (t < s) { red[t] += red[t + s]; } workgroupBarrier(); }
458
+ let inv = 1.0 / red[0];
459
+ for (var i = t; i < n; i += 256u) { sc[base + i] = sc[base + i] * inv; }
460
+ }`,
461
+ attnOutB: `enable f16;
462
+ struct P { heads: u32, kvHeads: u32, hd: u32, pad: u32 }
463
+ ${QB}
464
+ @group(0) @binding(0) var<storage, read> pr: array<f32>;
465
+ @group(0) @binding(1) var<storage, read> vc: array<f16>;
466
+ @group(0) @binding(2) var<storage, read_write> y: array<f32>;
467
+ @group(0) @binding(3) var<uniform> p: P;
468
+ @group(0) @binding(4) var<uniform> qd: Q;
469
+ @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>) {
470
+ let QN = p.heads * p.hd; let idx = g.x; let t = g.y;
471
+ if (idx >= QN || t >= qd.T) { return; }
472
+ let h = idx / p.hd; let d = idx % p.hd; let kvh = h / (p.heads / p.kvHeads);
473
+ let stride = p.kvHeads * p.hd; let rb = (h * qd.T + t) * qd.S; let last = qd.pos0 + t;
474
+ var acc = 0.0;
475
+ for (var pos = 0u; pos <= last; pos++) { acc += pr[rb + pos] * f32(vc[pos * stride + kvh * p.hd + d]); }
476
+ y[t * QN + idx] = acc;
477
+ }`,
478
+ sigmoidMulB: `
479
+ struct P { n: u32 }
480
+ ${QB}
481
+ @group(0) @binding(0) var<storage, read_write> x: array<f32>;
482
+ @group(0) @binding(1) var<storage, read> g: array<f32>;
483
+ @group(0) @binding(2) var<uniform> p: P;
484
+ @group(0) @binding(3) var<uniform> qd: Q;
485
+ ${SIGMOID}
486
+ @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gi: vec3<u32>) {
487
+ let i = gi.x; let t = gi.y; if (i >= p.n || t >= qd.T) { return; }
488
+ let k = t * p.n + i; x[k] = x[k] * sigm(g[k]);
489
+ }`,
490
+ addInPlaceB: `
491
+ struct P { n: u32 }
492
+ ${QB}
493
+ @group(0) @binding(0) var<storage, read_write> x: array<f32>;
494
+ @group(0) @binding(1) var<storage, read> y: array<f32>;
495
+ @group(0) @binding(2) var<uniform> p: P;
496
+ @group(0) @binding(3) var<uniform> qd: Q;
497
+ @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gi: vec3<u32>) {
498
+ let i = gi.x; let t = gi.y; if (i >= p.n || t >= qd.T) { return; }
499
+ let k = t * p.n + i; x[k] = x[k] + y[k];
500
+ }`,
501
+ // Routed experts for all (token, pick) pairs: z = t·K + j, slots[z] its pool slot. Input is the
502
+ // token's row (gate/up, inputMode 0) or the pair's own row (down, inputMode 1).
503
+ expertQ8B: `enable f16;
504
+ struct P { M: u32, N: u32, rowsPerExpert: u32, inputMode: u32, K: u32 }
505
+ @group(0) @binding(0) var<storage, read> x: array<f32>;
506
+ @group(0) @binding(1) var<storage, read> qw: array<u32>;
507
+ @group(0) @binding(2) var<storage, read> qs: array<f16>;
508
+ @group(0) @binding(3) var<storage, read_write> y: array<f32>;
509
+ @group(0) @binding(4) var<uniform> p: P;
510
+ @group(0) @binding(5) var<storage, read> slots: array<u32>;
511
+ ${I8}
512
+ var<workgroup> part: array<f32, 1024>;
513
+ @compute @workgroup_size(256) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(num_workgroups) ng: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
514
+ let z = wg.z; let m0 = (wg.y * ng.x + wg.x) * 4u; let t = l.x; let N = p.N; let M = p.M; let nb = N / 32u;
515
+ if (m0 >= M) { return; }
516
+ let rb = slots[z] * p.rowsPerExpert;
517
+ let xb = select((z / p.K) * N, z * N, p.inputMode == 1u);
518
+ let r0 = rb + min(m0, M - 1u); let r1 = rb + min(m0 + 1u, M - 1u); let r2 = rb + min(m0 + 2u, M - 1u); let r3 = rb + min(m0 + 3u, M - 1u);
519
+ let q0 = (r0 * N) >> 2u; let q1 = (r1 * N) >> 2u; let q2 = (r2 * N) >> 2u; let q3 = (r3 * N) >> 2u;
520
+ var a0 = 0.0; var a1 = 0.0; var a2 = 0.0; var a3 = 0.0;
521
+ for (var k = t; k < N; k += 256u) {
522
+ let xv = x[xb + k]; let wi = k >> 2u; let ln = k & 3u; let b = k >> 5u;
523
+ a0 += i8at(qw[q0 + wi], ln) * f32(qs[r0 * nb + b]) * xv;
524
+ a1 += i8at(qw[q1 + wi], ln) * f32(qs[r1 * nb + b]) * xv;
525
+ a2 += i8at(qw[q2 + wi], ln) * f32(qs[r2 * nb + b]) * xv;
526
+ a3 += i8at(qw[q3 + wi], ln) * f32(qs[r3 * nb + b]) * xv;
527
+ }
528
+ part[t] = a0; part[256u + t] = a1; part[512u + t] = a2; part[768u + t] = a3;
529
+ workgroupBarrier();
530
+ for (var s = 128u; s > 0u; s >>= 1u) {
531
+ if (t < s) { part[t] += part[t + s]; part[256u + t] += part[256u + t + s]; part[512u + t] += part[512u + t + s]; part[768u + t] += part[768u + t + s]; }
532
+ workgroupBarrier();
533
+ }
534
+ if (t < 4u && m0 + t < M) { y[z * M + m0 + t] = part[t * 256u]; }
535
+ }`,
536
+ // act = SiLU(g)·u, elementwise over T × n (the shared expert's separate gate and up).
537
+ siluMul2B: `
538
+ struct P { n: u32 }
539
+ ${QB}
540
+ @group(0) @binding(0) var<storage, read> g: array<f32>;
541
+ @group(0) @binding(1) var<storage, read> u: array<f32>;
542
+ @group(0) @binding(2) var<storage, read_write> act: array<f32>;
543
+ @group(0) @binding(3) var<uniform> p: P;
544
+ @group(0) @binding(4) var<uniform> qd: Q;
545
+ @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gi: vec3<u32>) {
546
+ let i = gi.x; let t = gi.y; if (i >= p.n || t >= qd.T) { return; }
547
+ let k = t * p.n + i; let gv = g[k];
548
+ act[k] = (gv / (1.0 + exp(-gv))) * u[k];
549
+ }`,
550
+ moeAccumSharedB: `
551
+ struct P { H: u32, k: u32 }
552
+ ${QB}
553
+ @group(0) @binding(0) var<storage, read> out: array<f32>;
554
+ @group(0) @binding(1) var<storage, read> sel: array<u32>;
555
+ @group(0) @binding(2) var<storage, read> sh: array<f32>;
556
+ @group(0) @binding(3) var<storage, read> sg: array<f32>;
557
+ @group(0) @binding(4) var<storage, read_write> x: array<f32>;
558
+ @group(0) @binding(5) var<uniform> p: P;
559
+ @group(0) @binding(6) var<uniform> qd: Q;
560
+ ${SIGMOID}
561
+ @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gi: vec3<u32>) {
562
+ let h = gi.x; let t = gi.y; if (h >= p.H || t >= qd.T) { return; }
563
+ let sb = t * 2u * p.k; var acc = 0.0;
564
+ for (var j = 0u; j < p.k; j++) { acc += bitcast<f32>(sel[sb + p.k + j]) * out[(t * p.k + j) * p.H + h]; }
565
+ let r = t * p.H + h;
566
+ x[r] = (acc + sh[r] * sigm(sg[t])) + x[r];
567
+ }`,
568
+ };
569
+
570
+ // ── Subgroup Q8_0 GEMV (the LFM2.5 / Gemma 4 kernels' pattern) ──────────────────────────────
571
+ // One 32-lane subgroup per output row, 4 rows per 128-thread workgroup (the same dispatch
572
+ // geometry as matmulQ8/expertQ8). Each lane takes whole Q8_0 blocks: two vec4<u32> of int8
573
+ // codes, unpack4xI8, vec4 dot products against the activations, the block scale applied once;
574
+ // one subgroupAdd reduces the row. Used only when the adapter's subgroups are exactly 32 wide.
575
+ const SG_KERNELS = {
576
+ matmulQ8: `enable f16;
577
+ enable subgroups;
578
+ struct P { M: u32, N: u32 }
579
+ @group(0) @binding(0) var<storage, read> x: array<vec4<f32>>;
580
+ @group(0) @binding(1) var<storage, read> qw: array<vec4<u32>>;
581
+ @group(0) @binding(2) var<storage, read> qs: array<f16>;
582
+ @group(0) @binding(3) var<storage, read_write> y: array<f32>;
583
+ @group(0) @binding(4) var<uniform> p: P;
584
+ @compute @workgroup_size(128) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(num_workgroups) ng: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
585
+ let m = (wg.y * ng.x + wg.x) * 4u + l.x / 32u; let lane = l.x % 32u;
586
+ let nb = p.N / 32u; let mr = min(m, p.M - 1u);
587
+ let wb = mr * nb * 2u; let sb = mr * nb;
588
+ var acc = 0.0;
589
+ for (var b = lane; b < nb; b += 32u) {
590
+ let w0 = qw[wb + b * 2u]; let w1 = qw[wb + b * 2u + 1u]; let xb = b * 8u;
591
+ var s = dot(vec4<f32>(unpack4xI8(w0.x)), x[xb]) + dot(vec4<f32>(unpack4xI8(w0.y)), x[xb + 1u]);
592
+ s += dot(vec4<f32>(unpack4xI8(w0.z)), x[xb + 2u]) + dot(vec4<f32>(unpack4xI8(w0.w)), x[xb + 3u]);
593
+ s += dot(vec4<f32>(unpack4xI8(w1.x)), x[xb + 4u]) + dot(vec4<f32>(unpack4xI8(w1.y)), x[xb + 5u]);
594
+ s += dot(vec4<f32>(unpack4xI8(w1.z)), x[xb + 6u]) + dot(vec4<f32>(unpack4xI8(w1.w)), x[xb + 7u]);
595
+ acc += s * f32(qs[sb + b]);
596
+ }
597
+ let total = subgroupAdd(acc);
598
+ if (lane == 0u && m < p.M) { y[m] = total; }
599
+ }`,
600
+ expertQ8: `enable f16;
601
+ enable subgroups;
602
+ struct P { M: u32, N: u32, rowsPerExpert: u32, inputPerSlot: u32 }
603
+ @group(0) @binding(0) var<storage, read> x: array<vec4<f32>>;
604
+ @group(0) @binding(1) var<storage, read> qw: array<vec4<u32>>;
605
+ @group(0) @binding(2) var<storage, read> qs: array<f16>;
606
+ @group(0) @binding(3) var<storage, read_write> y: array<f32>;
607
+ @group(0) @binding(4) var<uniform> p: P;
608
+ @group(0) @binding(5) var<storage, read> slots: array<u32>;
609
+ @compute @workgroup_size(128) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(num_workgroups) ng: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
610
+ let z = wg.z; let m = (wg.y * ng.x + wg.x) * 4u + l.x / 32u; let lane = l.x % 32u;
611
+ let nb = p.N / 32u; let r = slots[z] * p.rowsPerExpert + min(m, p.M - 1u);
612
+ let wb = r * nb * 2u; let sb = r * nb;
613
+ let xo = select(0u, z * p.N / 4u, p.inputPerSlot == 1u);
614
+ var acc = 0.0;
615
+ for (var b = lane; b < nb; b += 32u) {
616
+ let w0 = qw[wb + b * 2u]; let w1 = qw[wb + b * 2u + 1u]; let xb = xo + b * 8u;
617
+ var s = dot(vec4<f32>(unpack4xI8(w0.x)), x[xb]) + dot(vec4<f32>(unpack4xI8(w0.y)), x[xb + 1u]);
618
+ s += dot(vec4<f32>(unpack4xI8(w0.z)), x[xb + 2u]) + dot(vec4<f32>(unpack4xI8(w0.w)), x[xb + 3u]);
619
+ s += dot(vec4<f32>(unpack4xI8(w1.x)), x[xb + 4u]) + dot(vec4<f32>(unpack4xI8(w1.y)), x[xb + 5u]);
620
+ s += dot(vec4<f32>(unpack4xI8(w1.z)), x[xb + 6u]) + dot(vec4<f32>(unpack4xI8(w1.w)), x[xb + 7u]);
621
+ acc += s * f32(qs[sb + b]);
622
+ }
623
+ let total = subgroupAdd(acc);
624
+ if (lane == 0u && m < p.M) { y[z * p.M + m] = total; }
625
+ }`,
626
+ };
627
+
628
+ // Routing on the GPU (decode): after layer l's top-k, look each pick up in the pool's residency
629
+ // map. All resident → write the slots and carry on; any absent → record l+1 in `st`, after which
630
+ // every guarded kernel returns at once, so the rest of the token costs dispatches, not math.
631
+ const ROUTE_KERNEL = `
632
+ struct P { l: u32, E: u32, K: u32, pad: u32 }
633
+ @group(0) @binding(0) var<storage, read> sel: array<u32>;
634
+ @group(0) @binding(1) var<storage, read> rmap: array<u32>;
635
+ @group(0) @binding(2) var<storage, read_write> slots: array<u32>;
636
+ @group(0) @binding(3) var<storage, read_write> st: array<u32>;
637
+ @group(0) @binding(4) var<storage, read_write> used: array<u32>;
638
+ @group(0) @binding(5) var<uniform> p: P;
639
+ @compute @workgroup_size(1) fn main() {
640
+ if (st[0] != 0u) { return; }
641
+ var miss = false;
642
+ for (var j = 0u; j < p.K; j++) {
643
+ let e = sel[j]; let s = rmap[p.l * p.E + e];
644
+ used[p.l * p.K + j] = e;
645
+ if (s == 0xffffffffu) { miss = true; } else { slots[j] = s; }
646
+ }
647
+ if (miss) { st[0] = p.l + 1u; }
648
+ }`;
649
+ const ABSENT = 0xffffffff;
650
+
651
+ // Adds a read-only `stopF` binding after a kernel's last binding and an early return while it is
652
+ // set. A read-only storage value is uniform, so the return keeps barriers and subgroup ops legal.
653
+ function guarded(src) {
654
+ let last = -1;
655
+ for (const m of src.matchAll(/@binding\((\d+)\)/g)) last = Math.max(last, +m[1]);
656
+ const at = src.indexOf('@compute');
657
+ const out = `${src.slice(0, at)}@group(0) @binding(${last + 1}) var<storage, read> stopF: array<u32>;\n${src.slice(at)}`;
658
+ const withReturn = out.replace(/(@compute[^{]*?\bfn main\([\s\S]*?\)\s*\{)/, '$1\n if (stopF[0] != 0u) { return; }');
659
+ if (withReturn === out) throw new Error('guarded(): no compute entry point');
660
+ return withReturn;
661
+ }
662
+
663
+ export class Qwen35MoeSsd extends Qwen3MoeSsd {
664
+ // Without a url, the pinned Hugging Face file; with one (a local serve, a layer cut), the OPFS
665
+ // directory is named after the file, never after rung 2a's model.
666
+ static async load(modelId = null, opts = {}) {
667
+ const source = opts.source || (opts.url ? { repo: null, file: opts.url.split('/').pop(), revision: null } : QWEN36_35B_A3B);
668
+ return super.load(modelId, { ...opts, source });
669
+ }
670
+
671
+ constructor(device, manifest, gguf, opts) {
672
+ super(device, manifest, gguf, opts);
673
+ const c = this.cfg;
674
+ if (c.arch !== 'qwen35moe') throw new Error(`Qwen35MoeSsd needs a qwen35moe GGUF, got ${c.arch}`);
675
+ if (c.ssm.dConv !== 4) throw new Error(`gdnConv assumes a 4-tap conv, got ${c.ssm.dConv}`);
676
+ if (c.ssm.dState !== 128 || c.ssm.dInner !== c.ssm.vHeads * c.ssm.dState) throw new Error('gdnStep assumes 128-wide heads');
677
+ this.recurrent = Array.from({ length: c.layers }, (_, l) => (l + 1) % c.attnInterval !== 0);
678
+ // Prompts go through in chunks of up to chunkTokens tokens per layer (one routing readback per
679
+ // layer per chunk); decode stays one token at a time.
680
+ this.batchPrefill = opts.batchPrefill ?? true;
681
+ this.chunkTokens = Math.max(2, Math.floor(opts.prefillChunk || 256));
682
+ // gpuRouting: decode syncs with the CPU only at a layer whose experts are not all in the pool
683
+ // (and every routeWindow layers), instead of after every layer's routing. Opt-in at load: it
684
+ // compiles every one-token kernel with a stop guard. Off by default until it is timed on a
685
+ // quiet machine; at 2026-10-04's miss rate (~18 of 40 layers per token need a load) the counts
686
+ // put the gain near 5%. Output is token-identical either way.
687
+ this.routeGuards = !!opts.gpuRouting;
688
+ this.gpuRouting = this.routeGuards;
689
+ // A submit runs at most this many layers' experts. After a stop every kernel left in it still
690
+ // dispatches (about 7 ms for a whole token's worth), so a long submit mostly runs no-ops.
691
+ // Full model, 48 tokens: window 2 → 29.1 syncs and 17.6 no-op layers per token; 3 → 26.2, 36.6.
692
+ this.routeWindow = Math.max(1, Math.floor(opts.routeWindow ?? 2));
693
+ }
694
+
695
+ // Subgroup GEMVs replace the scalar ones when subgroups are exactly 32 wide (Apple, most others).
696
+ get subgroupKernels() {
697
+ const i = this.device.adapterInfo || this.opts.adapterInfo || {};
698
+ return this.opts.subgroups !== false && this.device.features.has('subgroups') && i.subgroupMinSize === 32 && i.subgroupMaxSize === 32;
699
+ }
700
+ // With gpuRouting, every one-token kernel carries the stop guard (chunked prefill's do not).
701
+ get kernels() {
702
+ if (!this._kernels) {
703
+ const k = { ...super.kernels, ...KERNELS, ...(this.subgroupKernels ? SG_KERNELS : {}) };
704
+ if (this.routeGuards) for (const n of Object.keys(k)) k[n] = guarded(k[n]);
705
+ this._kernels = { ...k, ...BATCH_KERNELS, route: ROUTE_KERNEL };
706
+ }
707
+ return this._kernels;
708
+ }
709
+ isGuarded(name) { return this.routeGuards && name !== 'route' && !(name in BATCH_KERNELS); }
710
+
711
+ bind(name, buffers) { return super.bind(name, this.isGuarded(name) ? [...buffers, this.a.stop] : buffers); }
712
+ // Like bind(), but a resource may be { buffer, offset, size }.
713
+ bindR(name, resources) {
714
+ const all = this.isGuarded(name) ? [...resources, this.a.stop] : resources;
715
+ return this.device.createBindGroup({
716
+ layout: this.pipeline(name).getBindGroupLayout(0),
717
+ entries: all.map((r, i) => ({ binding: i, resource: r instanceof GPUBuffer ? { buffer: r } : r })),
718
+ });
719
+ }
720
+
721
+ // For the GPU budget: f16 KV for the attention layers, conv + delta-rule state for the DeltaNet layers.
722
+ kvBytes() {
723
+ const c = this.cfg, s = c.ssm, convDim = 2 * s.kHeads * s.dState + s.dInner;
724
+ return this.recurrent.reduce((sum, r) => sum + (r ? 3 * convDim * 4 + s.vHeads * s.dState * s.dState * 4 : 2 * 2 * this.maxCtx * c.kvHeads * c.headDim), 0);
725
+ }
726
+
727
+ initBuffers() {
728
+ const c = this.cfg, s = c.ssm;
729
+ const H = c.hidden, hd = c.headDim, QN = c.heads * hd, kvn = c.kvHeads * hd, F = c.expertFf, SF = c.shexpFf, K = c.topK;
730
+ const keyDim = s.kHeads * s.dState, convDim = 2 * keyDim + s.dInner;
731
+ if (2 * K * 4 > 256) throw new Error(`top-k ${K} > 32: selections would overlap their 256-byte readback regions`);
732
+ if (c.experts > 256) throw new Error(`${c.experts} experts: the router top-k kernel handles at most 256`);
733
+ if ((SF * 4) % 256) throw new Error('shared expert gate/up halves must be 256-byte aligned');
734
+
735
+ // KV cache (f16) for the attention layers; conv + delta-rule state for the DeltaNet layers.
736
+ this.kc = []; this.vc = []; this.conv = []; this.ssm = [];
737
+ for (let l = 0; l < c.layers; l++) {
738
+ if (this.recurrent[l]) {
739
+ this.conv[l] = this.buffer(3 * convDim * 4, STORAGE | COPY_DST, 'state');
740
+ this.ssm[l] = this.buffer(s.vHeads * s.dState * s.dState * 4, STORAGE | COPY_DST, 'state');
741
+ } else {
742
+ this.kc[l] = this.buffer(this.maxCtx * kvn * 2, STORAGE, 'kv');
743
+ this.vc[l] = this.buffer(this.maxCtx * kvn * 2, STORAGE, 'kv');
744
+ }
745
+ }
746
+ const A = (n) => this.buffer(n * 4, STORAGE | COPY_SRC | COPY_DST);
747
+ this.a = {
748
+ x: A(H), xn: A(H), qg: A(2 * QN), q: A(QN), gate: A(QN), k: A(kvn), kr: A(kvn), v: A(kvn), att: A(QN), o: A(H),
749
+ sc: A(c.heads * this.maxCtx), mixed: A(convDim), cv: A(convDim), z: A(s.dInner), bb: A(s.vHeads), al: A(s.vHeads),
750
+ go: A(s.dInner), gn: A(s.dInner), dummy: A(4),
751
+ rl: A(c.experts), sel: A(2 * K), pxn: A(H), prl: A(c.experts), psel: A(64 * this.lookahead), slots: A(K),
752
+ gu: A(K * 2 * F), act: A(K * F), dn: A(K * H), shgu: A(2 * SF), shact: A(SF), sh: A(H), sg: A(4),
753
+ logits: A(c.vocab), am: A(4), rope: A(c.ropeDims),
754
+ stop: A(4), used: A(c.layers * K), rmap: A(c.layers * c.experts),
755
+ };
756
+ this.tok = this.buffer(16, UNIFORM | COPY_DST);
757
+ this.rbSel = this.buffer(256 * (1 + this.lookahead), MAP_READ | COPY_DST);
758
+ this.rbLogits = this.buffer(c.vocab * 4, MAP_READ | COPY_DST);
759
+ this.rbArg = this.buffer(16, MAP_READ | COPY_DST);
760
+ // One readback per decode sync: [stop | layer's sel | guesses | every layer's picks | argmax].
761
+ this.seg = { sel: 256, psel: 512, used: 512 + 256 * this.lookahead };
762
+ this.seg.am = this.seg.used + Math.ceil(c.layers * K * 4 / 16) * 16;
763
+ this.seg.bytes = this.seg.am + 16;
764
+ this.rbSeg = this.buffer(this.seg.bytes, MAP_READ | COPY_DST);
765
+ this.routeMap = new Uint32Array(c.layers * c.experts);
766
+
767
+ const U = (w) => this.uniform(w), eps = { f: c.eps };
768
+ const u = {
769
+ nH: U([H]), rmsH: U([H, eps]),
770
+ qg: U([2 * QN, H]), kv: U([kvn, H]), o: U([H, QN]),
771
+ ropeQ: U([hd, 2 * hd, c.ropeDims, 1, eps]), ropeK: U([hd, hd, c.ropeDims, 0, eps]), kvn: U([kvn]),
772
+ att: U([c.heads, c.kvHeads, hd, { f: 1 / Math.sqrt(hd) }]), attOut: U([c.heads, c.kvHeads, hd, 0]), gate: U([QN]),
773
+ qkv: U([convDim, H]), z: U([s.dInner, H]), ab: U([s.vHeads, H]), gout: U([H, s.dInner]),
774
+ conv: U([convDim]), step: U([s.kHeads, s.dState, keyDim, 0, eps, { f: 1 / Math.sqrt(s.dState) }]), ng: U([s.dState, eps]),
775
+ router: U([c.experts, H]), topk: U([c.experts, K]),
776
+ gu: U([2 * F, H, 2 * F, 0]), dn: U([H, F, H, 1]), silu: U([F, K]), acc: U([H, K]),
777
+ shgu: U([SF, H]), shsilu: U([SF, 1]), shd: U([H, SF]), shgate: U([1, H]),
778
+ lm: U([c.vocab, H]), am: U([c.vocab]),
779
+ };
780
+ const a = this.a, W = this.w;
781
+ const q8 = (name, x, y, uni) => { const t = W[name]; if (!t || !t.q) throw new Error(`missing Q8_0 tensor ${name}`); return this.bind('matmulQ8', [x, t.q, t.s, y, uni]); };
782
+ const raw = (name) => { const t = W[name]; if (!t || !t.raw) throw new Error(`missing F32 tensor ${name}`); return t.raw; };
783
+ this.g = { embed: this.bind('embedQ8', [W['token_embd.weight'].q, W['token_embd.weight'].s, a.x, this.tok, u.nH]) };
784
+ this.layers = [];
785
+ for (let l = 0; l < c.layers; l++) {
786
+ const n = (t) => `blk.${l}.${t}`;
787
+ const g = { recur: this.recurrent[l], rmsA: this.bind('rmsnorm', [a.x, raw(n('attn_norm.weight')), a.xn, u.rmsH]) };
788
+ if (g.recur) {
789
+ Object.assign(g, {
790
+ qkv: q8(n('attn_qkv.weight'), a.xn, a.mixed, u.qkv),
791
+ z: q8(n('attn_gate.weight'), a.xn, a.z, u.z),
792
+ beta: q8(n('ssm_beta.weight'), a.xn, a.bb, u.ab),
793
+ alpha: q8(n('ssm_alpha.weight'), a.xn, a.al, u.ab),
794
+ conv: this.bind('gdnConv', [a.mixed, raw(n('ssm_conv1d.weight')), this.conv[l], a.cv, u.conv]),
795
+ step: this.bind('gdnStep', [a.cv, a.bb, a.al, raw(n('ssm_dt.bias')), raw(n('ssm_a')), this.ssm[l], a.go, u.step]),
796
+ norm: this.bind('gdnNormGate', [a.go, raw(n('ssm_norm.weight')), a.z, a.gn, u.ng]),
797
+ out: q8(n('ssm_out.weight'), a.gn, a.o, u.gout),
798
+ });
799
+ } else {
800
+ Object.assign(g, {
801
+ q: q8(n('attn_q.weight'), a.xn, a.qg, u.qg),
802
+ k: q8(n('attn_k.weight'), a.xn, a.k, u.kv),
803
+ v: q8(n('attn_v.weight'), a.xn, a.v, u.kv),
804
+ ropeQ: this.bind('qkNormRopeP', [a.qg, raw(n('attn_q_norm.weight')), a.rope, a.q, a.gate, u.ropeQ]),
805
+ ropeK: this.bind('qkNormRopeP', [a.k, raw(n('attn_k_norm.weight')), a.rope, a.kr, a.dummy, u.ropeK]),
806
+ kv: this.bind('kvStore', [a.kr, a.v, this.kc[l], this.vc[l], this.tok, u.kvn]),
807
+ score: this.bind('attnScore', [a.q, this.kc[l], a.sc, this.tok, u.att]),
808
+ soft: this.bind('softmax', [a.sc, this.tok]),
809
+ attOut: this.bind('attnOut', [a.sc, this.vc[l], a.att, this.tok, u.attOut]),
810
+ gate: this.bind('sigmoidMul', [a.att, a.gate, u.gate]),
811
+ o: q8(n('attn_output.weight'), a.att, a.o, u.o),
812
+ });
813
+ }
814
+ Object.assign(g, {
815
+ addO: this.bind('addInPlace', [a.x, a.o, u.nH]),
816
+ rmsF: this.bind('rmsnorm', [a.x, raw(n('post_attention_norm.weight')), a.xn, u.rmsH]),
817
+ router: this.bind('matmulF32', [a.xn, raw(n('ffn_gate_inp.weight')), a.rl, u.router]),
818
+ topk: this.bind('topk', [a.rl, a.sel, u.topk]),
819
+ pf: Array.from({ length: Math.min(this.lookahead, c.layers - 1 - l) }, (_, i) => {
820
+ const at = (t) => `blk.${l + 1 + i}.${t}`;
821
+ return {
822
+ rms: this.bind('rmsnorm', [a.x, raw(at('post_attention_norm.weight')), a.pxn, u.rmsH]),
823
+ router: this.bind('matmulF32', [a.pxn, raw(at('ffn_gate_inp.weight')), a.prl, u.router]),
824
+ topk: this.bindR('topk', [a.prl, { buffer: a.psel, offset: 256 * i, size: 8 * K }, u.topk]),
825
+ };
826
+ }),
827
+ // Shared expert: [gate | up] into shgu, SiLU·up, down; its scalar gate logit into sg.
828
+ shg: this.bindR('matmulQ8', [a.xn, W[n('ffn_gate_shexp.weight')].q, W[n('ffn_gate_shexp.weight')].s, { buffer: a.shgu, offset: 0, size: SF * 4 }, u.shgu]),
829
+ shu: this.bindR('matmulQ8', [a.xn, W[n('ffn_up_shexp.weight')].q, W[n('ffn_up_shexp.weight')].s, { buffer: a.shgu, offset: SF * 4, size: SF * 4 }, u.shgu]),
830
+ shsilu: this.bind('siluMulMoe', [a.shgu, a.shact, u.shsilu]),
831
+ shd: q8(n('ffn_down_shexp.weight'), a.shact, a.sh, u.shd),
832
+ shgate: this.bind('matmulF32', [a.xn, raw(n('ffn_gate_inp_shexp.weight')), a.sg, u.shgate]),
833
+ gu: this.bind('expertQ8', [a.xn, this.pool.guQ, this.pool.guS, a.gu, u.gu, a.slots]),
834
+ silu: this.bind('siluMulMoe', [a.gu, a.act, u.silu]),
835
+ dn: this.bind('expertQ8', [a.act, this.pool.dQ, this.pool.dS, a.dn, u.dn, a.slots]),
836
+ acc: this.bind('moeAccumShared', [a.dn, a.sel, a.sh, a.sg, a.x, u.acc]),
837
+ route: this.bind('route', [a.sel, a.rmap, a.slots, a.stop, a.used, U([l, c.experts, K, 0])]),
838
+ });
839
+ this.layers.push(g);
840
+ }
841
+ this.g.rmsOut = this.bind('rmsnorm', [a.x, raw('output_norm.weight'), a.xn, u.rmsH]);
842
+ this.g.lm = this.bind('matmulQ8', [a.xn, W['output.weight'].q, W['output.weight'].s, a.logits, u.lm]);
843
+ this.g.am = this.bind('argmax', [a.logits, a.am, u.am]);
844
+ // A chunk pins min(experts, T·K) experts per layer at once; keep that under half the pool.
845
+ if (Math.min(c.experts, this.chunkTokens * K) > this.poolSlots / 2) this.chunkTokens = Math.max(2, Math.floor(this.poolSlots / 2 / K));
846
+ if (this.batchPrefill) this.initBatch();
847
+ }
848
+
849
+ // Buffers and bind groups for chunked prefill (rows = tokens of the chunk, token-major).
850
+ initBatch() {
851
+ const c = this.cfg, s = c.ssm, B = this.chunkTokens, a = this.a, W = this.w;
852
+ const H = c.hidden, hd = c.headDim, QN = c.heads * hd, kvn = c.kvHeads * hd, F = c.expertFf, SF = c.shexpFf, K = c.topK;
853
+ const keyDim = s.kHeads * s.dState, convDim = 2 * keyDim + s.dInner;
854
+ const A = (n) => this.buffer(n * 4, STORAGE | COPY_SRC | COPY_DST, 'batch');
855
+ const b = this.b = {
856
+ ids: A(B), x: A(B * H), xn: A(B * H), qg: A(B * 2 * QN), q: A(B * QN), gate: A(B * QN), k: A(B * kvn), kr: A(B * kvn), v: A(B * kvn),
857
+ att: A(B * QN), o: A(B * H), sc: A(c.heads * B * this.maxCtx), mixed: A(B * convDim), cv: A(B * convDim), z: A(B * s.dInner),
858
+ bb: A(B * s.vHeads), al: A(B * s.vHeads), go: A(B * s.dInner), gn: A(B * s.dInner),
859
+ rl: A(B * c.experts), 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),
860
+ shg: A(B * SF), shu: A(B * SF), shact: A(B * SF), sh: A(B * H), sg: A(B), rope: A(B * c.ropeDims), dummy: A(4),
861
+ };
862
+ this.qb = this.buffer(16, UNIFORM | COPY_DST, 'batch');
863
+ this.rbSelB = this.buffer(B * 2 * K * 4, MAP_READ | COPY_DST, 'batch');
864
+ const U = (w) => this.uniform(w), eps = { f: c.eps }, qb = this.qb;
865
+ const u = {
866
+ nH: U([H]), rmsH: U([H, eps]), qg: U([2 * QN, H]), kv: U([kvn, H]), o: U([H, QN]),
867
+ ropeQ: U([hd, 2 * hd, 2 * QN, QN, c.ropeDims, 1, eps]), ropeK: U([hd, hd, kvn, kvn, c.ropeDims, 0, eps]), kvn: U([kvn]),
868
+ att: U([c.heads, c.kvHeads, hd, { f: 1 / Math.sqrt(hd) }]), attOut: U([c.heads, c.kvHeads, hd, 0]), gate: U([QN]),
869
+ qkv: U([convDim, H]), z: U([s.dInner, H]), ab: U([s.vHeads, H]), gout: U([H, s.dInner]), conv: U([convDim]),
870
+ step: U([s.kHeads, s.dState, keyDim, convDim, s.vHeads, 0, eps, { f: 1 / Math.sqrt(s.dState) }]), ng: U([s.dState, s.dInner, eps]),
871
+ router: U([c.experts, H]), topk: U([c.experts, K]),
872
+ gu: U([2 * F, H, 2 * F, 0, K]), dn: U([H, F, H, 1, K]), silu: U([F, B * K]), acc: U([H, K]),
873
+ shgu: U([SF, H]), shd: U([H, SF]), sh2: U([SF]), shgate: U([1, H]),
874
+ };
875
+ const raw = (name) => W[name].raw;
876
+ const q8 = (name, x, y, uni) => this.bind('matmulQ8T', [x, W[name].q, W[name].s, y, uni, qb]);
877
+ this.gB = { embed: this.bind('embedQ8B', [W['token_embd.weight'].q, W['token_embd.weight'].s, b.ids, b.x, u.nH, qb]) };
878
+ for (let l = 0; l < c.layers; l++) {
879
+ const n = (t) => `blk.${l}.${t}`, g = { rmsA: this.bind('rmsnormB', [b.x, raw(n('attn_norm.weight')), b.xn, u.rmsH]) };
880
+ if (this.recurrent[l]) {
881
+ Object.assign(g, {
882
+ qkv: q8(n('attn_qkv.weight'), b.xn, b.mixed, u.qkv), z: q8(n('attn_gate.weight'), b.xn, b.z, u.z),
883
+ beta: q8(n('ssm_beta.weight'), b.xn, b.bb, u.ab), alpha: q8(n('ssm_alpha.weight'), b.xn, b.al, u.ab),
884
+ conv: this.bind('gdnConvB', [b.mixed, raw(n('ssm_conv1d.weight')), this.conv[l], b.cv, u.conv, qb]),
885
+ step: this.bind('gdnStepB', [b.cv, b.bb, b.al, raw(n('ssm_dt.bias')), raw(n('ssm_a')), this.ssm[l], b.go, u.step, qb]),
886
+ norm: this.bind('gdnNormGateB', [b.go, raw(n('ssm_norm.weight')), b.z, b.gn, u.ng]),
887
+ out: q8(n('ssm_out.weight'), b.gn, b.o, u.gout),
888
+ });
889
+ } else {
890
+ Object.assign(g, {
891
+ q: q8(n('attn_q.weight'), b.xn, b.qg, u.qg), k: q8(n('attn_k.weight'), b.xn, b.k, u.kv), v: q8(n('attn_v.weight'), b.xn, b.v, u.kv),
892
+ ropeQ: this.bind('qkNormRopePB', [b.qg, raw(n('attn_q_norm.weight')), b.rope, b.q, b.gate, u.ropeQ]),
893
+ ropeK: this.bind('qkNormRopePB', [b.k, raw(n('attn_k_norm.weight')), b.rope, b.kr, b.dummy, u.ropeK]),
894
+ kv: this.bind('kvStoreB', [b.kr, b.v, this.kc[l], this.vc[l], u.kvn, qb]),
895
+ score: this.bind('attnScoreB', [b.q, this.kc[l], b.sc, u.att, qb]),
896
+ soft: this.bind('softmaxB', [b.sc, qb]),
897
+ attOut: this.bind('attnOutB', [b.sc, this.vc[l], b.att, u.attOut, qb]),
898
+ gate: this.bind('sigmoidMulB', [b.att, b.gate, u.gate, qb]),
899
+ o: q8(n('attn_output.weight'), b.att, b.o, u.o),
900
+ });
901
+ }
902
+ Object.assign(g, {
903
+ addO: this.bind('addInPlaceB', [b.x, b.o, u.nH, qb]),
904
+ rmsF: this.bind('rmsnormB', [b.x, raw(n('post_attention_norm.weight')), b.xn, u.rmsH]),
905
+ router: this.bind('matmulF32T', [b.xn, raw(n('ffn_gate_inp.weight')), b.rl, u.router]),
906
+ topk: this.bind('topkB', [b.rl, b.sel, u.topk]),
907
+ shg: q8(n('ffn_gate_shexp.weight'), b.xn, b.shg, u.shgu), shu: q8(n('ffn_up_shexp.weight'), b.xn, b.shu, u.shgu),
908
+ shact: this.bind('siluMul2B', [b.shg, b.shu, b.shact, u.sh2, qb]),
909
+ shd: q8(n('ffn_down_shexp.weight'), b.shact, b.sh, u.shd),
910
+ shgate: this.bind('matmulF32T', [b.xn, raw(n('ffn_gate_inp_shexp.weight')), b.sg, u.shgate]),
911
+ gu: this.bind('expertQ8B', [b.xn, this.pool.guQ, this.pool.guS, b.gu, u.gu, b.slots]),
912
+ silu: this.bind('siluMulMoe', [b.gu, b.act, u.silu]),
913
+ dn: this.bind('expertQ8B', [b.act, this.pool.dQ, this.pool.dS, b.dn, u.dn, b.slots]),
914
+ acc: this.bind('moeAccumSharedB', [b.dn, b.sel, b.sh, b.sg, b.x, u.acc, qb]),
915
+ });
916
+ this.layers[l].B = g;
917
+ }
918
+ }
919
+
920
+ // Layer l over the chunk's T tokens up to its routing (the batched twin of encodeAttention).
921
+ encodeTrunkB(pass, l, T, S) {
922
+ const c = this.cfg, s = c.ssm, g = this.layers[l].B, d = (nm, gr, x, y, z) => this.dispatch(pass, nm, gr, x, y, z);
923
+ const H = c.hidden, QN = c.heads * c.headDim, kvn = c.kvHeads * c.headDim, convDim = 2 * s.kHeads * s.dState + s.dInner;
924
+ const tt = Math.ceil(T / 16), mm = (n) => Math.ceil(n / 16);
925
+ d('rmsnormB', g.rmsA, T);
926
+ if (this.recurrent[l]) {
927
+ d('matmulQ8T', g.qkv, mm(convDim), tt); d('matmulQ8T', g.z, mm(s.dInner), tt);
928
+ d('matmulQ8T', g.beta, mm(s.vHeads), tt); d('matmulQ8T', g.alpha, mm(s.vHeads), tt);
929
+ d('gdnConvB', g.conv, Math.ceil(convDim / 256));
930
+ d('gdnStepB', g.step, s.vHeads);
931
+ d('gdnNormGateB', g.norm, s.vHeads, T);
932
+ d('matmulQ8T', g.out, mm(H), tt);
933
+ } else {
934
+ d('matmulQ8T', g.q, mm(2 * QN), tt); d('matmulQ8T', g.k, mm(kvn), tt); d('matmulQ8T', g.v, mm(kvn), tt);
935
+ d('qkNormRopePB', g.ropeQ, c.heads, T); d('qkNormRopePB', g.ropeK, c.kvHeads, T);
936
+ d('kvStoreB', g.kv, Math.ceil(kvn / 256), T);
937
+ d('attnScoreB', g.score, Math.ceil(c.heads * T * S / 256));
938
+ d('softmaxB', g.soft, c.heads * T);
939
+ d('attnOutB', g.attOut, Math.ceil(QN / 256), T);
940
+ d('sigmoidMulB', g.gate, Math.ceil(QN / 256), T);
941
+ d('matmulQ8T', g.o, mm(H), tt);
942
+ }
943
+ d('addInPlaceB', g.addO, Math.ceil(H / 256), T);
944
+ d('rmsnormB', g.rmsF, T);
945
+ d('matmulF32T', g.router, c.experts, T);
946
+ d('topkB', g.topk, T);
947
+ d('matmulQ8T', g.shg, mm(c.shexpFf), tt); d('matmulQ8T', g.shu, mm(c.shexpFf), tt);
948
+ d('siluMul2B', g.shact, Math.ceil(c.shexpFf / 256), T);
949
+ d('matmulQ8T', g.shd, mm(H), tt);
950
+ d('matmulF32T', g.shgate, 1, T);
951
+ }
952
+
953
+ encodeExpertsB(pass, l, T) {
954
+ const c = this.cfg, g = this.layers[l].B, d = (nm, gr, x, y, z) => this.dispatch(pass, nm, gr, x, y, z);
955
+ d('expertQ8B', g.gu, Math.ceil(2 * c.expertFf / 4), 1, T * c.topK);
956
+ d('siluMulMoe', g.silu, Math.ceil(T * c.topK * c.expertFf / 256));
957
+ d('expertQ8B', g.dn, Math.ceil(c.hidden / 4), 1, T * c.topK);
958
+ d('moeAccumSharedB', g.acc, Math.ceil(c.hidden / 256), T);
959
+ }
960
+
961
+ // Feeds T = ids.length tokens at this.position through every layer: one routing readback per
962
+ // layer, the union of the chunk's experts loaded once. want: 'none' | 'argmax' | 'logits' for
963
+ // the last token, as in step().
964
+ async prefillChunk(ids, want = 'none') {
965
+ const c = this.cfg, dev = this.device, K = c.topK, H = c.hidden, T = ids.length, pos0 = this.position, S = pos0 + T;
966
+ if (T > this.chunkTokens) throw new Error(`chunk of ${T} > ${this.chunkTokens}`);
967
+ if (S > this.maxCtx) throw new Error(`context full (${this.maxCtx} tokens)`);
968
+ const tStart = performance.now();
969
+ dev.queue.writeBuffer(this.a.stop, 0, new Uint32Array(4)); // the head below uses guarded kernels
970
+ dev.queue.writeBuffer(this.b.ids, 0, Uint32Array.from(ids));
971
+ dev.queue.writeBuffer(this.qb, 0, new Uint32Array([T, pos0, S, 0]));
972
+ const nRot = c.ropeDims, half = nRot / 2, r = new Float32Array(T * nRot), stepR = Math.fround(Math.pow(c.ropeTheta, -2 / nRot));
973
+ for (let t = 0; t < T; t++) {
974
+ let th = Math.fround(pos0 + t);
975
+ for (let i = 0; i < half; i++) { r[t * nRot + i] = Math.cos(th); r[t * nRot + half + i] = Math.sin(th); th = Math.fround(th * stepR); }
976
+ }
977
+ dev.queue.writeBuffer(this.b.rope, 0, r);
978
+ const selBytes = T * 2 * K * 4;
979
+ let enc = dev.createCommandEncoder(), pass = enc.beginComputePass();
980
+ this.dispatch(pass, 'embedQ8B', this.gB.embed, Math.ceil(H / 256), T);
981
+ this.encodeTrunkB(pass, 0, T, S);
982
+ pass.end();
983
+ enc.copyBufferToBuffer(this.b.sel, 0, this.rbSelB, 0, selBytes);
984
+ dev.queue.submit([enc.finish()]);
985
+ for (let l = 0; l < c.layers; l++) {
986
+ const t0 = performance.now();
987
+ await this.rbSelB.mapAsync(1, 0, selBytes);
988
+ this.counters.gpuWaitMs += performance.now() - t0;
989
+ const sel = new Uint32Array(this.rbSelB.getMappedRange(0, selBytes).slice(0));
990
+ this.rbSelB.unmap();
991
+ const pairs = new Uint32Array(T * K);
992
+ for (let t = 0; t < T; t++) for (let j = 0; j < K; j++) pairs[t * K + j] = sel[t * 2 * K + j];
993
+ const unique = [...new Set(pairs)];
994
+ const tq = performance.now();
995
+ const slotsU = await this.xs.ensure(l, unique);
996
+ this.counters.ensureMs += performance.now() - tq;
997
+ const slotOf = new Map(unique.map((e, i) => [e, slotsU[i]]));
998
+ const pairSlots = pairs.map((e) => slotOf.get(e));
999
+ dev.queue.writeBuffer(this.b.slots, 0, pairSlots);
1000
+ enc = dev.createCommandEncoder(); pass = enc.beginComputePass();
1001
+ this.encodeExpertsB(pass, l, T);
1002
+ if (l + 1 < c.layers) {
1003
+ this.encodeTrunkB(pass, l + 1, T, S);
1004
+ pass.end();
1005
+ enc.copyBufferToBuffer(this.b.sel, 0, this.rbSelB, 0, selBytes);
1006
+ } else {
1007
+ pass.end();
1008
+ enc.copyBufferToBuffer(this.b.x, (T - 1) * H * 4, this.a.x, 0, H * 4);
1009
+ if (want !== 'none') {
1010
+ pass = enc.beginComputePass();
1011
+ this.dispatch(pass, 'rmsnorm', this.g.rmsOut, 1);
1012
+ this.dispatch(pass, 'matmulQ8', this.g.lm, Math.ceil(c.vocab / 4));
1013
+ if (want === 'argmax') this.dispatch(pass, 'argmax', this.g.am, 1);
1014
+ pass.end();
1015
+ if (want === 'argmax') enc.copyBufferToBuffer(this.a.am, 0, this.rbArg, 0, 4);
1016
+ if (want === 'logits') enc.copyBufferToBuffer(this.a.logits, 0, this.rbLogits, 0, c.vocab * 4);
1017
+ }
1018
+ }
1019
+ dev.queue.submit([enc.finish()]);
1020
+ this.xs.release(slotsU);
1021
+ }
1022
+ this.position += T;
1023
+ this.cached.push(...ids);
1024
+ let result = null;
1025
+ if (want === 'argmax') {
1026
+ await this.rbArg.mapAsync(1, 0, 4);
1027
+ result = new Uint32Array(this.rbArg.getMappedRange(0, 4))[0];
1028
+ this.rbArg.unmap();
1029
+ } else if (want === 'logits') {
1030
+ await this.rbLogits.mapAsync(1);
1031
+ result = new Float32Array(this.rbLogits.getMappedRange().slice(0));
1032
+ this.rbLogits.unmap();
1033
+ } else {
1034
+ await dev.queue.onSubmittedWorkDone();
1035
+ }
1036
+ this.counters.tokens += T;
1037
+ this.counters.wallMs += performance.now() - tStart;
1038
+ return result;
1039
+ }
1040
+
1041
+ // Layer l up to its routing: the trunk (DeltaNet or gated attention), residual, post-norm,
1042
+ // router + top-k (and the prefetch guesses), and the shared expert, which needs no routing.
1043
+ encodeAttention(pass, l, seqLen) {
1044
+ const c = this.cfg, s = c.ssm, g = this.layers[l], d = (nm, gr, x, y, z) => this.dispatch(pass, nm, gr, x, y, z);
1045
+ const H = c.hidden, QN = c.heads * c.headDim, kvn = c.kvHeads * c.headDim, convDim = 2 * s.kHeads * s.dState + s.dInner;
1046
+ d('rmsnorm', g.rmsA, 1);
1047
+ if (g.recur) {
1048
+ d('matmulQ8', g.qkv, Math.ceil(convDim / 4)); d('matmulQ8', g.z, Math.ceil(s.dInner / 4));
1049
+ d('matmulQ8', g.beta, Math.ceil(s.vHeads / 4)); d('matmulQ8', g.alpha, Math.ceil(s.vHeads / 4));
1050
+ d('gdnConv', g.conv, Math.ceil(convDim / 256));
1051
+ d('gdnStep', g.step, s.vHeads);
1052
+ d('gdnNormGate', g.norm, s.vHeads);
1053
+ d('matmulQ8', g.out, Math.ceil(H / 4));
1054
+ } else {
1055
+ d('matmulQ8', g.q, Math.ceil(2 * QN / 4)); d('matmulQ8', g.k, Math.ceil(kvn / 4)); d('matmulQ8', g.v, Math.ceil(kvn / 4));
1056
+ d('qkNormRopeP', g.ropeQ, c.heads); d('qkNormRopeP', g.ropeK, c.kvHeads);
1057
+ d('kvStore', g.kv, Math.ceil(kvn / 256));
1058
+ d('attnScore', g.score, Math.ceil(c.heads * seqLen / 256));
1059
+ d('softmax', g.soft, c.heads);
1060
+ d('attnOut', g.attOut, Math.ceil(QN / 256));
1061
+ d('sigmoidMul', g.gate, Math.ceil(QN / 256));
1062
+ d('matmulQ8', g.o, Math.ceil(H / 4));
1063
+ }
1064
+ d('addInPlace', g.addO, Math.ceil(H / 256));
1065
+ if (this.prefetch) for (const p of g.pf) { d('rmsnorm', p.rms, 1); d('matmulF32', p.router, c.experts); d('topk', p.topk, 1); }
1066
+ d('rmsnorm', g.rmsF, 1);
1067
+ d('matmulF32', g.router, c.experts);
1068
+ d('topk', g.topk, 1);
1069
+ d('matmulQ8', g.shg, Math.ceil(c.shexpFf / 4)); d('matmulQ8', g.shu, Math.ceil(c.shexpFf / 4));
1070
+ d('siluMulMoe', g.shsilu, Math.ceil(c.shexpFf / 256));
1071
+ d('matmulQ8', g.shd, Math.ceil(H / 4));
1072
+ d('matmulF32', g.shgate, 1);
1073
+ }
1074
+
1075
+ encodeExperts(pass, l) {
1076
+ const c = this.cfg, g = this.layers[l], d = (nm, gr, x, y, z) => this.dispatch(pass, nm, gr, x, y, z);
1077
+ d('expertQ8', g.gu, Math.ceil(2 * c.expertFf / 4), 1, c.topK);
1078
+ d('siluMulMoe', g.silu, Math.ceil(c.topK * c.expertFf / 256));
1079
+ d('expertQ8', g.dn, Math.ceil(c.hidden / 4), 1, c.topK);
1080
+ d('moeAccumShared', g.acc, Math.ceil(c.hidden / 256));
1081
+ }
1082
+
1083
+ // The pool's residency as the route kernel reads it: slot of (layer, expert), or ABSENT. Built
1084
+ // from the streamer's slot keys, which are set only once an expert's upload has been submitted.
1085
+ // Queue order keeps it safe: an upload into a slot runs after every submit made before it.
1086
+ writeRouteMap() {
1087
+ const map = this.routeMap, key = this.xs.slotKey;
1088
+ map.fill(ABSENT);
1089
+ for (let s = 0; s < key.length; s++) if (key[s] >= 0) map[key[s]] = s;
1090
+ this.device.queue.writeBuffer(this.a.rmap, 0, map);
1091
+ }
1092
+
1093
+ // One token with routing on the GPU. Each submit runs up to routeWindow layers and routes the
1094
+ // layer after them; the route kernel stops it early at the first layer with an expert outside
1095
+ // the pool. Either way the CPU then ensures the next layer's experts (loading any absent ones and
1096
+ // prefetching the guesses for the layers after it) and resumes at that layer's experts:
1097
+ // everything it computed before routing (x, xn, sel, shared expert) is still in place, since
1098
+ // every kernel after a stop returns without writing.
1099
+ async step(token, want = 'argmax') {
1100
+ if (!this.gpuRouting) return super.step(token, want);
1101
+ const c = this.cfg, dev = this.device, K = c.topK, L = c.layers, seg = this.seg;
1102
+ if (this.position >= this.maxCtx) throw new Error(`context full (${this.maxCtx} tokens)`);
1103
+ const tStart = performance.now();
1104
+ const pos = this.position, seqLen = pos + 1;
1105
+ this.writeTokenUniforms(token, pos);
1106
+ // resume: -1 starts at the embedding; l ≥ 0 resumes at layer l's experts (its trunk already ran,
1107
+ // and a DeltaNet trunk must not run twice: it advances the recurrent state).
1108
+ let resume = -1, slots = null, result = null;
1109
+ for (;;) {
1110
+ const te = performance.now();
1111
+ this.writeRouteMap();
1112
+ dev.queue.writeBuffer(this.a.stop, 0, new Uint32Array(4));
1113
+ if (slots) dev.queue.writeBuffer(this.a.slots, 0, slots);
1114
+ const enc = dev.createCommandEncoder(), pass = enc.beginComputePass();
1115
+ if (resume < 0) {
1116
+ this.dispatch(pass, 'embedQ8', this.g.embed, Math.ceil(c.hidden / 256));
1117
+ this.encodeAttention(pass, 0, seqLen);
1118
+ this.dispatch(pass, 'route', this.layers[0].route, 1);
1119
+ }
1120
+ const first = Math.max(0, resume), last = Math.min(L - 1, first + this.routeWindow - 1);
1121
+ for (let l = first; l <= last; l++) {
1122
+ this.encodeExperts(pass, l);
1123
+ if (l + 1 < L) { this.encodeAttention(pass, l + 1, seqLen); this.dispatch(pass, 'route', this.layers[l + 1].route, 1); }
1124
+ }
1125
+ if (last === L - 1 && want !== 'none') {
1126
+ this.dispatch(pass, 'rmsnorm', this.g.rmsOut, 1);
1127
+ this.dispatch(pass, 'matmulQ8', this.g.lm, Math.ceil(c.vocab / 4));
1128
+ if (want === 'argmax') this.dispatch(pass, 'argmax', this.g.am, 1);
1129
+ }
1130
+ pass.end();
1131
+ enc.copyBufferToBuffer(this.a.stop, 0, this.rbSeg, 0, 16);
1132
+ enc.copyBufferToBuffer(this.a.sel, 0, this.rbSeg, seg.sel, 8 * K);
1133
+ if (this.prefetch) enc.copyBufferToBuffer(this.a.psel, 0, this.rbSeg, seg.psel, 256 * this.lookahead);
1134
+ enc.copyBufferToBuffer(this.a.used, 0, this.rbSeg, seg.used, L * K * 4);
1135
+ if (last === L - 1 && want === 'argmax') enc.copyBufferToBuffer(this.a.am, 0, this.rbSeg, seg.am, 4);
1136
+ if (last === L - 1 && want === 'logits') enc.copyBufferToBuffer(this.a.logits, 0, this.rbLogits, 0, c.vocab * 4);
1137
+ dev.queue.submit([enc.finish()]);
1138
+ if (slots) { this.xs.release(slots); slots = null; }
1139
+ this.counters.encodeMs += performance.now() - te;
1140
+ this.counters.syncs++;
1141
+ this.counters.layersEncoded += last - first + 1;
1142
+
1143
+ const tw = performance.now();
1144
+ await this.rbSeg.mapAsync(1, 0, seg.bytes);
1145
+ this.counters.gpuWaitMs += performance.now() - tw;
1146
+ const u = new Uint32Array(this.rbSeg.getMappedRange(0, seg.bytes).slice(0));
1147
+ this.rbSeg.unmap();
1148
+ const stopL = u[0] ? u[0] - 1 : last + 1;
1149
+ // Layers this submit routed and ran on the GPU: refresh them in the LRU. (A resumed layer went
1150
+ // through ensure() already.)
1151
+ for (let l = resume + 1; l < stopL; l++) this.xs.touchUsed(l, u.subarray(seg.used / 4 + l * K, seg.used / 4 + (l + 1) * K));
1152
+ if (stopL === L) {
1153
+ if (want === 'argmax') result = u[seg.am / 4];
1154
+ break;
1155
+ }
1156
+ const ids = u.slice(seg.sel / 4, seg.sel / 4 + K);
1157
+ const tq = performance.now();
1158
+ const ensuring = this.xs.ensure(stopL, ids);
1159
+ if (this.prefetch) {
1160
+ const n = this.layers[stopL].pf.length;
1161
+ for (let i = 0; i < n; i++) this.xs.prefetch(stopL + 1 + i, u.subarray(seg.psel / 4 + 64 * i, seg.psel / 4 + 64 * i + K));
1162
+ }
1163
+ slots = await ensuring;
1164
+ this.counters.ensureMs += performance.now() - tq;
1165
+ resume = stopL;
1166
+ }
1167
+ if (want === 'logits') {
1168
+ const tw = performance.now();
1169
+ await this.rbLogits.mapAsync(1);
1170
+ this.counters.gpuWaitMs += performance.now() - tw;
1171
+ result = new Float32Array(this.rbLogits.getMappedRange().slice(0));
1172
+ this.rbLogits.unmap();
1173
+ }
1174
+ this.position++;
1175
+ this.cached.push(token);
1176
+ this.counters.tokens++;
1177
+ this.counters.wallMs += performance.now() - tStart;
1178
+ return result;
1179
+ }
1180
+
1181
+ resetCounters() { super.resetCounters(); this.counters.syncs = 0; this.counters.layersEncoded = 0; }
1182
+ // layersEncodedPerToken − layers = expert blocks dispatched after a stop (no-ops).
1183
+ stats() {
1184
+ const n = Math.max(1, this.counters.tokens);
1185
+ return { ...super.stats(), gpuRouting: this.gpuRouting, routeWindow: this.routeWindow, syncsPerToken: this.counters.syncs / n, layersEncodedPerToken: this.counters.layersEncoded / n };
1186
+ }
1187
+
1188
+ // RoPE angles as ggml's CPU rope cache forms them for text positions (all M-RoPE sections at
1189
+ // the token position): θ = pos, multiplied by base^(-2/nRot) once per pair, in f32.
1190
+ writeTokenUniforms(token, pos) {
1191
+ const c = this.cfg, nRot = c.ropeDims, half = nRot / 2;
1192
+ this.device.queue.writeBuffer(this.tok, 0, new Uint32Array([token, pos, pos + 1, 0]));
1193
+ const r = new Float32Array(nRot), step = Math.fround(Math.pow(c.ropeTheta, -2 / nRot));
1194
+ let th = Math.fround(pos);
1195
+ for (let i = 0; i < half; i++) { r[i] = Math.cos(th); r[half + i] = Math.sin(th); th = Math.fround(th * step); }
1196
+ this.device.queue.writeBuffer(this.a.rope, 0, r);
1197
+ }
1198
+
1199
+ // Qwen3.5/3.6's template opens the reasoning block in the generation prompt when thinking is on.
1200
+ chatPrompt(messages, { enableThinking = true } = {}) {
1201
+ let p = '';
1202
+ for (const m of messages) p += `<|im_start|>${m.role}\n${m.content}<|im_end|>\n`;
1203
+ return p + '<|im_start|>assistant\n' + (enableThinking ? '<think>\n' : '<think>\n\n</think>\n\n');
1204
+ }
1205
+
1206
+ // The recurrent state cannot rewind to an earlier position, so any change before the cached
1207
+ // end restarts from token 0.
1208
+ reset() {
1209
+ super.reset();
1210
+ if (!this.conv) return;
1211
+ const enc = this.device.createCommandEncoder();
1212
+ for (let l = 0; l < this.cfg.layers; l++) if (this.recurrent[l]) { enc.clearBuffer(this.conv[l]); enc.clearBuffer(this.ssm[l]); }
1213
+ this.device.queue.submit([enc.finish()]);
1214
+ }
1215
+
1216
+ async prefill(ids, want = 'argmax') {
1217
+ let common = 0;
1218
+ while (common < ids.length - 1 && common < this.cached.length && this.cached[common] === ids[common]) common++;
1219
+ if (common < this.cached.length) this.reset();
1220
+ const rest = ids.slice(this.cached.length);
1221
+ if (!this.batchPrefill || rest.length < 2) return super.prefill(ids, want);
1222
+ let r = null;
1223
+ for (let i = 0; i < rest.length; i += this.chunkTokens) {
1224
+ r = await this.prefillChunk(rest.slice(i, i + this.chunkTokens), i + this.chunkTokens >= rest.length ? want : 'none');
1225
+ }
1226
+ return r;
1227
+ }
1228
+ }
1229
+
1230
+ // For kernel tests (scripts): the WGSL this module adds.
1231
+ export { KERNELS as QWEN35_KERNELS, BATCH_KERNELS as QWEN35_BATCH_KERNELS, SG_KERNELS as QWEN35_SG_KERNELS };