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,936 @@
1
+ /* ternary_bonsai_2_dflash.js — DFlash 2 speculative decoding for the vendored Ternary
2
+ Bonsai 2 engine: the block drafter (5 Qwen3 layers, WGSL, weights stay Q4_K/Q6_K on
3
+ the GPU) + the generate loop behind ternary_bonsai_2_27b.js's specDecodeRunner()
4
+ seam. Output is greedy-identical to the engine's own decode. Built from
5
+ NakliTechie/dflash-mlx-bonsai2 @ bbc9d7a (Apache-2.0) by scripts/build-bonsai2-dflash.mjs;
6
+ drafter weights: naklitechie/Qwen3.8-27B-DFlash2-ternary-bonsai2 (Q4_K_M GGUF, 1.14 GB). */
7
+ // ---- lab/webgpu/drafter/gguf.js ----
8
+ // Minimal GGUF v3 reader + K-quant dequantizers (Q4_K, Q6_K, F16, F32) for the DFlash2 drafter port.
9
+ // Runs in the browser (fetch + Range) and in Node (fs) through a pluggable byte source: source(offset, length) -> Uint8Array.
10
+ // Layouts follow ggml-quants.c (block_q4_K: d f16, dmin f16, scales[12], qs[128]; block_q6_K: ql[128], qh[64], scales[16] i8, d f16).
11
+ const GGML = { F32: 0, F16: 1, Q4_K: 12, Q6_K: 14 };
12
+ const TYPE_NAME = { 0: 'F32', 1: 'F16', 12: 'Q4_K', 14: 'Q6_K' };
13
+ const BLOCK = { 12: [256, 144], 14: [256, 210], 0: [1, 4], 1: [1, 2] }; // type -> [elements per block, bytes per block]
14
+ function tensorBytes(type, n) { const [be, bb] = BLOCK[type]; if (n % be) throw new Error('n % block'); return (n / be) * bb; }
15
+ function typeName(t) { return TYPE_NAME[t] ?? String(t); }
16
+
17
+ const f16buf = new ArrayBuffer(4), f16u = new Uint32Array(f16buf), f16f = new Float32Array(f16buf);
18
+ function f16ToF32(h) {
19
+ const s = (h & 0x8000) << 16, e = (h >> 10) & 0x1f, m = h & 0x3ff;
20
+ if (e === 0) { if (m === 0) { f16u[0] = s; return f16f[0]; } let mm = m, ee = 113; while (!(mm & 0x400)) { mm <<= 1; ee--; } f16u[0] = s | (ee << 23) | ((mm & 0x3ff) << 13); return f16f[0]; }
21
+ if (e === 31) { f16u[0] = s | 0x7f800000 | (m << 13); return f16f[0]; }
22
+ f16u[0] = s | ((e + 112) << 23) | (m << 13); return f16f[0];
23
+ }
24
+ // f32 -> f16 bits (round to nearest even). Used when Float16Array is unavailable.
25
+ function f32ToF16(v) {
26
+ f16f[0] = v; const x = f16u[0]; const s = (x >>> 16) & 0x8000; let e = (x >>> 23) & 0xff; let m = x & 0x7fffff;
27
+ if (e === 0xff) return s | 0x7c00 | (m ? 0x200 : 0);
28
+ e = e - 127 + 15;
29
+ if (e >= 0x1f) return s | 0x7c00;
30
+ if (e <= 0) { if (e < -10) return s; m = (m | 0x800000) >> (1 - e); if (m & 0x1000) m += 0x2000; return s | (m >> 13); }
31
+ let h = s | (e << 10) | (m >> 13); if (m & 0x1000) { if ((m & 0x2fff) !== 0x1000 || (h & 1)) h += 1; } // RNE
32
+ return h;
33
+ }
34
+ function f32ArrayToF16(f32) {
35
+ if (typeof Float16Array !== 'undefined') { const out = new Float16Array(f32.length); out.set(f32); return new Uint16Array(out.buffer); }
36
+ const out = new Uint16Array(f32.length); for (let i = 0; i < f32.length; ++i) out[i] = f32ToF16(f32[i]); return out;
37
+ }
38
+
39
+ // ---- header ----
40
+ const VT = { UINT8: 0, INT8: 1, UINT16: 2, INT16: 3, UINT32: 4, INT32: 5, FLOAT32: 6, BOOL: 7, STRING: 8, ARRAY: 9, UINT64: 10, INT64: 11, FLOAT64: 12 };
41
+ class Cursor {
42
+ constructor(source) { this.source = source; this.pos = 0; this.buf = null; this.bufStart = 0; }
43
+ async ensure(n) { if (this.buf && this.pos + n <= this.bufStart + this.buf.byteLength) return; const len = Math.max(n, 1 << 20); this.buf = await this.source(this.pos, len); this.bufStart = this.pos; this.dv = new DataView(this.buf.buffer, this.buf.byteOffset, this.buf.byteLength); }
44
+ async u8() { await this.ensure(1); const v = this.dv.getUint8(this.pos - this.bufStart); this.pos += 1; return v; }
45
+ async u16() { await this.ensure(2); const v = this.dv.getUint16(this.pos - this.bufStart, true); this.pos += 2; return v; }
46
+ async u32() { await this.ensure(4); const v = this.dv.getUint32(this.pos - this.bufStart, true); this.pos += 4; return v; }
47
+ async i32() { await this.ensure(4); const v = this.dv.getInt32(this.pos - this.bufStart, true); this.pos += 4; return v; }
48
+ async f32() { await this.ensure(4); const v = this.dv.getFloat32(this.pos - this.bufStart, true); this.pos += 4; return v; }
49
+ async u64() { await this.ensure(8); const v = Number(this.dv.getBigUint64(this.pos - this.bufStart, true)); this.pos += 8; return v; }
50
+ async i64() { await this.ensure(8); const v = Number(this.dv.getBigInt64(this.pos - this.bufStart, true)); this.pos += 8; return v; }
51
+ async f64() { await this.ensure(8); const v = this.dv.getFloat64(this.pos - this.bufStart, true); this.pos += 8; return v; }
52
+ async str() { const n = await this.u64(); await this.ensure(n); const b = this.buf.subarray(this.pos - this.bufStart, this.pos - this.bufStart + n); this.pos += n; return new TextDecoder().decode(b); }
53
+ async value(t, skipStrings) {
54
+ switch (t) {
55
+ case VT.UINT8: case VT.BOOL: return this.u8(); case VT.INT8: { const v = await this.u8(); return v > 127 ? v - 256 : v; }
56
+ case VT.UINT16: return this.u16(); case VT.INT16: { const v = await this.u16(); return v > 32767 ? v - 65536 : v; }
57
+ case VT.UINT32: return this.u32(); case VT.INT32: return this.i32(); case VT.FLOAT32: return this.f32();
58
+ case VT.UINT64: return this.u64(); case VT.INT64: return this.i64(); case VT.FLOAT64: return this.f64();
59
+ case VT.STRING: return this.str();
60
+ case VT.ARRAY: { const st = await this.u32(); const n = await this.u64(); const out = []; for (let i = 0; i < n; ++i) { const v = await this.value(st, skipStrings); if (!(skipStrings && st === VT.STRING) && n <= 4096) out.push(v); } return n > 4096 ? { array_length: n, elem_type: st } : out; }
61
+ default: throw new Error('gguf: bad value type ' + t);
62
+ }
63
+ }
64
+ }
65
+ async function readGGUF(source) {
66
+ const c = new Cursor(source);
67
+ const magic = await c.u32(); if (magic !== 0x46554747) throw new Error('not GGUF');
68
+ const version = await c.u32(); if (version !== 3 && version !== 2) throw new Error('gguf version ' + version);
69
+ const nTensors = await c.u64(), nKv = await c.u64();
70
+ const kv = {};
71
+ for (let i = 0; i < nKv; ++i) { const k = await c.str(); const t = await c.u32(); kv[k] = await c.value(t, true); }
72
+ const tensors = [];
73
+ for (let i = 0; i < nTensors; ++i) {
74
+ const name = await c.str(); const nd = await c.u32(); const ne = []; for (let d = 0; d < nd; ++d) ne.push(await c.u64());
75
+ const type = await c.u32(); const offset = await c.u64();
76
+ const n = ne.reduce((a, b) => a * b, 1);
77
+ tensors.push({ name, ne, type, typeName: typeName(type), offset, n, bytes: tensorBytes(type, n) });
78
+ }
79
+ const alignment = kv['general.alignment'] ?? 32;
80
+ const dataStart = Math.ceil(c.pos / alignment) * alignment;
81
+ for (const t of tensors) t.absOffset = dataStart + t.offset;
82
+ const byName = Object.fromEntries(tensors.map(t => [t.name, t]));
83
+ return { version, kv, tensors, byName, dataStart };
84
+ }
85
+
86
+ // ---- dequantizers: raw block bytes -> Float32Array (n elements) ----
87
+ function scaleMinK4(j, sc) { // get_scale_min_k4
88
+ if (j < 4) return [sc[j] & 63, sc[j + 4] & 63];
89
+ return [(sc[j + 4] & 0xf) | ((sc[j - 4] >> 6) << 4), (sc[j + 4] >> 4) | ((sc[j] >> 6) << 4)];
90
+ }
91
+ function dequantQ4K(raw, n, out, outOff = 0) {
92
+ const nb = n / 256; const sc = new Uint8Array(8); // scratch not needed; index raw directly
93
+ for (let b = 0; b < nb; ++b) {
94
+ const p = b * 144; const d = f16ToF32(raw[p] | (raw[p + 1] << 8)), dmin = f16ToF32(raw[p + 2] | (raw[p + 3] << 8));
95
+ const scales = raw.subarray(p + 4, p + 16); let q = p + 16; let y = outOff + b * 256; let is = 0;
96
+ for (let j = 0; j < 256; j += 64) {
97
+ const [s1, m1] = scaleMinK4(is, scales), [s2, m2] = scaleMinK4(is + 1, scales);
98
+ const d1 = d * s1, mm1 = dmin * m1, d2 = d * s2, mm2 = dmin * m2;
99
+ for (let l = 0; l < 32; ++l) out[y + l] = d1 * (raw[q + l] & 0xf) - mm1;
100
+ for (let l = 0; l < 32; ++l) out[y + 32 + l] = d2 * (raw[q + l] >> 4) - mm2;
101
+ y += 64; q += 32; is += 2;
102
+ }
103
+ }
104
+ return out;
105
+ }
106
+ function dequantQ6K(raw, n, out, outOff = 0) {
107
+ const nb = n / 256;
108
+ for (let b = 0; b < nb; ++b) {
109
+ const p = b * 210; const d = f16ToF32(raw[p + 208] | (raw[p + 209] << 8));
110
+ let ql = p, qh = p + 128, sc = p + 192, y = outOff + b * 256;
111
+ for (let half = 0; half < 2; ++half) {
112
+ for (let l = 0; l < 32; ++l) {
113
+ const is = (l / 16) | 0;
114
+ const s0 = (raw[sc + is] << 24) >> 24, s2 = (raw[sc + is + 2] << 24) >> 24, s4 = (raw[sc + is + 4] << 24) >> 24, s6 = (raw[sc + is + 6] << 24) >> 24;
115
+ const q1 = ((raw[ql + l] & 0xf) | (((raw[qh + l] >> 0) & 3) << 4)) - 32;
116
+ const q2 = ((raw[ql + l + 32] & 0xf) | (((raw[qh + l] >> 2) & 3) << 4)) - 32;
117
+ const q3 = ((raw[ql + l] >> 4) | (((raw[qh + l] >> 4) & 3) << 4)) - 32;
118
+ const q4 = ((raw[ql + l + 32] >> 4) | (((raw[qh + l] >> 6) & 3) << 4)) - 32;
119
+ out[y + l] = d * s0 * q1; out[y + l + 32] = d * s2 * q2; out[y + l + 64] = d * s4 * q3; out[y + l + 96] = d * s6 * q4;
120
+ }
121
+ y += 128; ql += 64; qh += 32; sc += 8;
122
+ }
123
+ }
124
+ return out;
125
+ }
126
+ function dequantF16(raw, n, out, outOff = 0) { for (let i = 0; i < n; ++i) out[outOff + i] = f16ToF32(raw[2 * i] | (raw[2 * i + 1] << 8)); return out; }
127
+ function dequantF32(raw, n, out, outOff = 0) { out.set(new Float32Array(raw.buffer, raw.byteOffset, n), outOff); return out; }
128
+ function dequantize(type, raw, n, out = new Float32Array(n), outOff = 0) {
129
+ switch (type) { case GGML.Q4_K: return dequantQ4K(raw, n, out, outOff); case GGML.Q6_K: return dequantQ6K(raw, n, out, outOff); case GGML.F16: return dequantF16(raw, n, out, outOff); case GGML.F32: return dequantF32(raw, n, out, outOff); default: throw new Error('dequantize: unsupported type ' + typeName(type)); }
130
+ }
131
+ // Dequantize one row of a 2-D tensor whose row length is a multiple of the block size (used for codebook row lookups).
132
+ function dequantRow(type, rawTensorBytes, rowLen, row, out = new Float32Array(rowLen)) {
133
+ const [be, bb] = BLOCK[type]; const blocksPerRow = rowLen / be; const p = row * blocksPerRow * bb;
134
+ return dequantize(type, rawTensorBytes.subarray(p, p + blocksPerRow * bb), rowLen, out, 0);
135
+ }
136
+ // Byte sources
137
+ function fsSource(fd, fs) { return (off, len) => { const b = new Uint8Array(len); const n = fs.readSync(fd, b, 0, len, off); return Promise.resolve(n === len ? b : b.subarray(0, n)); }; }
138
+ function fetchSource(url) { return async (off, len) => { const r = await fetch(url, { headers: { Range: `bytes=${off}-${off + len - 1}` } }); if (r.status !== 206) throw new Error(`range fetch ${url} -> ${r.status} (server must support Range)`); return new Uint8Array(await r.arrayBuffer()); }; }
139
+
140
+ // ---- lab/webgpu/drafter/drafter.js ----
141
+ // DFlash2 drafter forward in WebGPU (WGSL compute), weights from the Q4_K_M GGUF dequantized on the CPU into f16
142
+ // (packed two per u32, unpacked in-shader with unpack2x16float, so no shader-f16 feature is required); activations f32.
143
+ // Semantics follow the MLX oracle (dflash_mlx/draft/dflash2.py + dflash_mlx/model.py DFlashAttention, cache=ContextOnly,
144
+ // first cycle: context positions 0..C-1, block positions C..C+7, non-causal block, sliding window over the context).
145
+
146
+
147
+ const CFG = { H: 5120, I: 17408, L: 5, NH: 32, NKV: 8, HD: 128, BLOCK: 8, GROUP: 16, KCONV: 2, RANK: 256, TOPK: 16, VOCAB: 248320, EPS: 1e-6, THETA: 1e7, WINDOW: 2048, MASK: 248070 };
148
+
149
+ // ---------------- WGSL ----------------
150
+ const WG_GEMM = /* wgsl */`
151
+ struct P { M: u32, K: u32, N: u32, pad: u32 };
152
+ @group(0) @binding(0) var<uniform> p: P;
153
+ @group(0) @binding(1) var<storage, read> X: array<vec4<f32>>;
154
+ @group(0) @binding(2) var<storage, read> W: array<vec4<u32>>;
155
+ @group(0) @binding(3) var<storage, read_write> Y: array<f32>;
156
+ var<workgroup> red: array<f32, 512>;
157
+ // Y[m,n] = sum_k X[m,k] * W[n,k]; W is f16 pairs packed in u32, read 8 at a time (vec4<u32>); K % 8 == 0.
158
+ // 64 threads = 4 n-columns x 16 k-lanes (16 lanes x 16 B = 256 B contiguous per iteration); 8-row tile per wg.y.
159
+ @compute @workgroup_size(64)
160
+ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
161
+ let tid = lid.x; let nl = tid >> 4u; let ks = tid & 15u;
162
+ let n = wg.x * 4u + nl; let m0 = wg.y * 8u;
163
+ let K8 = p.K >> 3u; let K4 = p.K >> 2u;
164
+ var acc: array<f32, 8>;
165
+ for (var m = 0u; m < 8u; m++) { acc[m] = 0.0; }
166
+ if (n < p.N) {
167
+ let wb = n * K8;
168
+ let rows = min(8u, p.M - m0);
169
+ for (var k8 = ks; k8 < K8; k8 += 16u) {
170
+ let wv = W[wb + k8];
171
+ let w0 = unpack2x16float(wv.x); let w1 = unpack2x16float(wv.y); let w2 = unpack2x16float(wv.z); let w3 = unpack2x16float(wv.w);
172
+ let wa = vec4<f32>(w0.x, w0.y, w1.x, w1.y); let wb4 = vec4<f32>(w2.x, w2.y, w3.x, w3.y);
173
+ let x4 = 2u * k8;
174
+ for (var m = 0u; m < rows; m++) {
175
+ let xb = (m0 + m) * K4 + x4;
176
+ acc[m] += dot(wa, X[xb]) + dot(wb4, X[xb + 1u]);
177
+ }
178
+ }
179
+ }
180
+ for (var m = 0u; m < 8u; m++) { red[tid * 8u + m] = acc[m]; }
181
+ workgroupBarrier();
182
+ if (ks == 0u && n < p.N) {
183
+ for (var m = 0u; m < 8u; m++) {
184
+ if (m0 + m < p.M) {
185
+ var s = 0.0;
186
+ for (var j = 0u; j < 16u; j++) { s += red[(nl * 16u + j) * 8u + m]; }
187
+ Y[(m0 + m) * p.N + n] = s;
188
+ }
189
+ }
190
+ }
191
+ }`;
192
+
193
+ // ---- packed-weight GEMMs: the GGUF blocks stay on the GPU as raw bytes and are dequantized in-shader ----
194
+ // Q4_K: block = 256 values, 144 B = 36 u32: [d|dmin] f16 pair, scales[12] (3 u32), qs[128] (32 u32). Lane w of 32 owns qs word w:
195
+ // pair j = w>>3 (sub-blocks 2j: low nibbles, k = 256b+64j+4i.. ; 2j+1: high nibbles, k+32), i = w&7. y = d*sc*sum(q*x) - dmin*mn*sum(x).
196
+ const WG_GEMM_Q4K = /* wgsl */`
197
+ struct P { M: u32, K: u32, N: u32, ks: u32 };
198
+ @group(0) @binding(0) var<uniform> p: P;
199
+ @group(0) @binding(1) var<storage, read> X: array<vec4<f32>>;
200
+ @group(0) @binding(2) var<storage, read> W: array<u32>;
201
+ @group(0) @binding(3) var<storage, read_write> Y: array<f32>;
202
+ var<workgroup> xs: array<vec4<f32>, 512>; // X tile: 8 rows x 256 k (one block) as vec4
203
+ var<workgroup> red: array<f32, 1024>;
204
+ fn sb(s0: u32, s1: u32, s2: u32, i: u32) -> u32 { var w = s0; if (i >= 8u) { w = s2; } else if (i >= 4u) { w = s1; } return (w >> (8u * (i & 3u))) & 0xffu; }
205
+ fn scmin(s0: u32, s1: u32, s2: u32, j: u32) -> vec2<f32> {
206
+ if (j < 4u) { return vec2<f32>(f32(sb(s0, s1, s2, j) & 63u), f32(sb(s0, s1, s2, j + 4u) & 63u)); }
207
+ let a = sb(s0, s1, s2, j + 4u); let b = sb(s0, s1, s2, j - 4u); let c = sb(s0, s1, s2, j);
208
+ return vec2<f32>(f32((a & 0xfu) | ((b >> 6u) << 4u)), f32((a >> 4u) | ((c >> 6u) << 4u)));
209
+ }
210
+ fn nib_lo(w: u32) -> vec4<f32> { return vec4<f32>(f32(w & 0xfu), f32((w >> 8u) & 0xfu), f32((w >> 16u) & 0xfu), f32((w >> 24u) & 0xfu)); }
211
+ fn nib_hi(w: u32) -> vec4<f32> { return vec4<f32>(f32((w >> 4u) & 0xfu), f32((w >> 12u) & 0xfu), f32((w >> 20u) & 0xfu), f32((w >> 28u) & 0xfu)); }
212
+ // 64 threads = 16 column PAIRS x 4 lanes; lane q owns pair j = q of every block (qs words 8q..8q+7 = sub-blocks 2q, 2q+1).
213
+ // Each thread computes 2 adjacent output columns so every staged X load feeds 16 FMAs. X tile staged per block in workgroup memory.
214
+ @compute @workgroup_size(64)
215
+ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
216
+ let tid = lid.x; let col = tid >> 2u; let q = tid & 3u;
217
+ let n0 = wg.x * 32u + col * 2u; let n1 = n0 + 1u; let m0 = wg.y * 8u;
218
+ let nb = p.K >> 8u; let K4 = p.K >> 2u;
219
+ let rows = min(8u, p.M - m0);
220
+ let ok0 = n0 < p.N; let ok1 = n1 < p.N;
221
+ let bStart = (wg.z * nb) / p.ks; let bEnd = ((wg.z + 1u) * nb) / p.ks;
222
+ var a0 = 0.0; var a1 = 0.0; var a2 = 0.0; var a3 = 0.0; var a4 = 0.0; var a5 = 0.0; var a6 = 0.0; var a7 = 0.0;
223
+ var b0 = 0.0; var b1 = 0.0; var b2 = 0.0; var b3 = 0.0; var b4 = 0.0; var b5 = 0.0; var b6 = 0.0; var b7 = 0.0;
224
+ for (var b = bStart; b < bEnd; b++) {
225
+ workgroupBarrier();
226
+ for (var idx = tid; idx < 512u; idx += 64u) { let r = idx >> 6u; let c = idx & 63u; if (r < rows) { xs[idx] = X[(m0 + r) * K4 + b * 64u + c]; } else { xs[idx] = vec4<f32>(0.0); } }
227
+ workgroupBarrier();
228
+ if (ok0) {
229
+ let bb0 = (n0 * nb + b) * 36u; let bb1 = (select(n0, n1, ok1) * nb + b) * 36u;
230
+ let dd0 = unpack2x16float(W[bb0]); let t0 = W[bb0 + 1u]; let t1 = W[bb0 + 2u]; let t2 = W[bb0 + 3u];
231
+ let dd1 = unpack2x16float(W[bb1]); let u0 = W[bb1 + 1u]; let u1 = W[bb1 + 2u]; let u2 = W[bb1 + 3u];
232
+ let lo0 = scmin(t0, t1, t2, 2u * q); let hi0 = scmin(t0, t1, t2, 2u * q + 1u);
233
+ let lo1 = scmin(u0, u1, u2, 2u * q); let hi1 = scmin(u0, u1, u2, 2u * q + 1u);
234
+ let dlo0 = dd0.x * lo0.x; let mlo0 = dd0.y * lo0.y; let dhi0 = dd0.x * hi0.x; let mhi0 = dd0.y * hi0.y;
235
+ let dlo1 = dd1.x * lo1.x; let mlo1 = dd1.y * lo1.y; let dhi1 = dd1.x * hi1.x; let mhi1 = dd1.y * hi1.y;
236
+ for (var i = 0u; i < 8u; i++) {
237
+ let w0 = W[bb0 + 4u + 8u * q + i]; let w1 = W[bb1 + 4u + 8u * q + i];
238
+ let ql0 = nib_lo(w0); let qh0 = nib_hi(w0); let ql1 = nib_lo(w1); let qh1 = nib_hi(w1);
239
+ let c4 = 16u * q + i;
240
+ { let xl = xs[0u + c4]; let xh = xs[8u + c4]; let sl = xl.x + xl.y + xl.z + xl.w; let sh = xh.x + xh.y + xh.z + xh.w; a0 += dlo0 * dot(ql0, xl) - mlo0 * sl + dhi0 * dot(qh0, xh) - mhi0 * sh; b0 += dlo1 * dot(ql1, xl) - mlo1 * sl + dhi1 * dot(qh1, xh) - mhi1 * sh; }
241
+ { let xl = xs[64u + c4]; let xh = xs[72u + c4]; let sl = xl.x + xl.y + xl.z + xl.w; let sh = xh.x + xh.y + xh.z + xh.w; a1 += dlo0 * dot(ql0, xl) - mlo0 * sl + dhi0 * dot(qh0, xh) - mhi0 * sh; b1 += dlo1 * dot(ql1, xl) - mlo1 * sl + dhi1 * dot(qh1, xh) - mhi1 * sh; }
242
+ { let xl = xs[128u + c4]; let xh = xs[136u + c4]; let sl = xl.x + xl.y + xl.z + xl.w; let sh = xh.x + xh.y + xh.z + xh.w; a2 += dlo0 * dot(ql0, xl) - mlo0 * sl + dhi0 * dot(qh0, xh) - mhi0 * sh; b2 += dlo1 * dot(ql1, xl) - mlo1 * sl + dhi1 * dot(qh1, xh) - mhi1 * sh; }
243
+ { let xl = xs[192u + c4]; let xh = xs[200u + c4]; let sl = xl.x + xl.y + xl.z + xl.w; let sh = xh.x + xh.y + xh.z + xh.w; a3 += dlo0 * dot(ql0, xl) - mlo0 * sl + dhi0 * dot(qh0, xh) - mhi0 * sh; b3 += dlo1 * dot(ql1, xl) - mlo1 * sl + dhi1 * dot(qh1, xh) - mhi1 * sh; }
244
+ { let xl = xs[256u + c4]; let xh = xs[264u + c4]; let sl = xl.x + xl.y + xl.z + xl.w; let sh = xh.x + xh.y + xh.z + xh.w; a4 += dlo0 * dot(ql0, xl) - mlo0 * sl + dhi0 * dot(qh0, xh) - mhi0 * sh; b4 += dlo1 * dot(ql1, xl) - mlo1 * sl + dhi1 * dot(qh1, xh) - mhi1 * sh; }
245
+ { let xl = xs[320u + c4]; let xh = xs[328u + c4]; let sl = xl.x + xl.y + xl.z + xl.w; let sh = xh.x + xh.y + xh.z + xh.w; a5 += dlo0 * dot(ql0, xl) - mlo0 * sl + dhi0 * dot(qh0, xh) - mhi0 * sh; b5 += dlo1 * dot(ql1, xl) - mlo1 * sl + dhi1 * dot(qh1, xh) - mhi1 * sh; }
246
+ { let xl = xs[384u + c4]; let xh = xs[392u + c4]; let sl = xl.x + xl.y + xl.z + xl.w; let sh = xh.x + xh.y + xh.z + xh.w; a6 += dlo0 * dot(ql0, xl) - mlo0 * sl + dhi0 * dot(qh0, xh) - mhi0 * sh; b6 += dlo1 * dot(ql1, xl) - mlo1 * sl + dhi1 * dot(qh1, xh) - mhi1 * sh; }
247
+ { let xl = xs[448u + c4]; let xh = xs[456u + c4]; let sl = xl.x + xl.y + xl.z + xl.w; let sh = xh.x + xh.y + xh.z + xh.w; a7 += dlo0 * dot(ql0, xl) - mlo0 * sl + dhi0 * dot(qh0, xh) - mhi0 * sh; b7 += dlo1 * dot(ql1, xl) - mlo1 * sl + dhi1 * dot(qh1, xh) - mhi1 * sh; }
248
+ }
249
+ }
250
+ }
251
+ let rb = tid * 16u;
252
+ red[rb] = a0; red[rb + 1u] = a1; red[rb + 2u] = a2; red[rb + 3u] = a3; red[rb + 4u] = a4; red[rb + 5u] = a5; red[rb + 6u] = a6; red[rb + 7u] = a7;
253
+ red[rb + 8u] = b0; red[rb + 9u] = b1; red[rb + 10u] = b2; red[rb + 11u] = b3; red[rb + 12u] = b4; red[rb + 13u] = b5; red[rb + 14u] = b6; red[rb + 15u] = b7;
254
+ workgroupBarrier();
255
+ if (q == 0u && ok0) {
256
+ let yb = wg.z * p.M * p.N;
257
+ for (var m = 0u; m < 8u; m++) {
258
+ if (m0 + m < p.M) {
259
+ Y[yb + (m0 + m) * p.N + n0] = red[rb + m] + red[rb + 16u + m] + red[rb + 32u + m] + red[rb + 48u + m];
260
+ if (ok1) { Y[yb + (m0 + m) * p.N + n1] = red[rb + 8u + m] + red[rb + 24u + m] + red[rb + 40u + m] + red[rb + 56u + m]; }
261
+ }
262
+ }
263
+ }
264
+ }`;
265
+ // Q6_K (212 B = 53 u32 stride per block): lane q of 4 owns half = q>>1, words t = 4(q&1)..+3 of that half (64 values); 2 columns per thread.
266
+ const WG_GEMM_Q6K = /* wgsl */`
267
+ struct P { M: u32, K: u32, N: u32, ks: u32 };
268
+ @group(0) @binding(0) var<uniform> p: P;
269
+ @group(0) @binding(1) var<storage, read> X: array<vec4<f32>>;
270
+ @group(0) @binding(2) var<storage, read> W: array<u32>;
271
+ @group(0) @binding(3) var<storage, read_write> Y: array<f32>;
272
+ var<workgroup> xs: array<vec4<f32>, 512>;
273
+ var<workgroup> red: array<f32, 1024>;
274
+ fn i8at(w: u32, i: u32) -> f32 { return f32((i32(w << (8u * (3u - i))) >> 24)); }
275
+ fn q6(ql0: u32, ql1: u32, qh: u32, sh: u32) -> vec4<f32> { // (q1, q2, q3, q4) for one byte position sh = 8i
276
+ return vec4<f32>(f32(((ql0 >> sh) & 0xfu) | (((qh >> sh) & 3u) << 4u)), f32(((ql1 >> sh) & 0xfu) | (((qh >> (sh + 2u)) & 3u) << 4u)), f32(((ql0 >> (sh + 4u)) & 0xfu) | (((qh >> (sh + 4u)) & 3u) << 4u)), f32(((ql1 >> (sh + 4u)) & 0xfu) | (((qh >> (sh + 6u)) & 3u) << 4u))) - vec4<f32>(32.0);
277
+ }
278
+ @compute @workgroup_size(64)
279
+ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
280
+ let tid = lid.x; let col = tid >> 2u; let q = tid & 3u;
281
+ let n0 = wg.x * 32u + col * 2u; let n1 = n0 + 1u; let m0 = wg.y * 8u;
282
+ let nb = p.K >> 8u; let K4 = p.K >> 2u;
283
+ let bStart = (wg.z * nb) / p.ks; let bEnd = ((wg.z + 1u) * nb) / p.ks;
284
+ let rows = min(8u, p.M - m0);
285
+ let ok0 = n0 < p.N; let ok1 = n1 < p.N;
286
+ let half = q >> 1u; let t0 = 4u * (q & 1u); let is = t0 >> 2u; let sbase = 8u * half + is;
287
+ var a0 = 0.0; var a1 = 0.0; var a2 = 0.0; var a3 = 0.0; var a4 = 0.0; var a5 = 0.0; var a6 = 0.0; var a7 = 0.0;
288
+ var b0 = 0.0; var b1 = 0.0; var b2 = 0.0; var b3 = 0.0; var b4 = 0.0; var b5 = 0.0; var b6 = 0.0; var b7 = 0.0;
289
+ for (var b = bStart; b < bEnd; b++) {
290
+ workgroupBarrier();
291
+ for (var idx = tid; idx < 512u; idx += 64u) { let r = idx >> 6u; let c = idx & 63u; if (r < rows) { xs[idx] = X[(m0 + r) * K4 + b * 64u + c]; } else { xs[idx] = vec4<f32>(0.0); } }
292
+ workgroupBarrier();
293
+ if (ok0) {
294
+ let bb0 = (n0 * nb + b) * 53u; let bb1 = (select(n0, n1, ok1) * nb + b) * 53u;
295
+ let d0 = unpack2x16float(W[bb0 + 52u]).x; let d1 = unpack2x16float(W[bb1 + 52u]).x;
296
+ let s1 = d0 * i8at(W[bb0 + 48u + (sbase >> 2u)], sbase & 3u); let s2 = d0 * i8at(W[bb0 + 48u + ((sbase + 2u) >> 2u)], (sbase + 2u) & 3u); let s3 = d0 * i8at(W[bb0 + 48u + ((sbase + 4u) >> 2u)], (sbase + 4u) & 3u); let s4 = d0 * i8at(W[bb0 + 48u + ((sbase + 6u) >> 2u)], (sbase + 6u) & 3u);
297
+ let r1 = d1 * i8at(W[bb1 + 48u + (sbase >> 2u)], sbase & 3u); let r2 = d1 * i8at(W[bb1 + 48u + ((sbase + 2u) >> 2u)], (sbase + 2u) & 3u); let r3 = d1 * i8at(W[bb1 + 48u + ((sbase + 4u) >> 2u)], (sbase + 4u) & 3u); let r4 = d1 * i8at(W[bb1 + 48u + ((sbase + 6u) >> 2u)], (sbase + 6u) & 3u);
298
+ for (var tt = 0u; tt < 4u; tt++) {
299
+ let t = t0 + tt;
300
+ let qh0 = W[bb0 + 32u + half * 8u + t]; let ql00 = W[bb0 + half * 16u + t]; let ql01 = W[bb0 + half * 16u + t + 8u];
301
+ let qh1 = W[bb1 + 32u + half * 8u + t]; let ql10 = W[bb1 + half * 16u + t]; let ql11 = W[bb1 + half * 16u + t + 8u];
302
+ // per byte position i: q1..q4 of column 0 -> (q1,q2,q3,q4) vectors over i
303
+ var q1: vec4<f32>; var q2: vec4<f32>; var q3: vec4<f32>; var q4: vec4<f32>; var p1: vec4<f32>; var p2: vec4<f32>; var p3: vec4<f32>; var p4: vec4<f32>;
304
+ for (var i = 0u; i < 4u; i++) { let v = q6(ql00, ql01, qh0, 8u * i); q1[i] = v.x; q2[i] = v.y; q3[i] = v.z; q4[i] = v.w; let u = q6(ql10, ql11, qh1, 8u * i); p1[i] = u.x; p2[i] = u.y; p3[i] = u.z; p4[i] = u.w; }
305
+ let c4 = 32u * half + t;
306
+ { let xb = 0u + c4; let x0 = xs[xb]; let x1 = xs[xb + 8u]; let x2 = xs[xb + 16u]; let x3 = xs[xb + 24u]; a0 += s1 * dot(q1, x0) + s2 * dot(q2, x1) + s3 * dot(q3, x2) + s4 * dot(q4, x3); b0 += r1 * dot(p1, x0) + r2 * dot(p2, x1) + r3 * dot(p3, x2) + r4 * dot(p4, x3); }
307
+ { let xb = 64u + c4; let x0 = xs[xb]; let x1 = xs[xb + 8u]; let x2 = xs[xb + 16u]; let x3 = xs[xb + 24u]; a1 += s1 * dot(q1, x0) + s2 * dot(q2, x1) + s3 * dot(q3, x2) + s4 * dot(q4, x3); b1 += r1 * dot(p1, x0) + r2 * dot(p2, x1) + r3 * dot(p3, x2) + r4 * dot(p4, x3); }
308
+ { let xb = 128u + c4; let x0 = xs[xb]; let x1 = xs[xb + 8u]; let x2 = xs[xb + 16u]; let x3 = xs[xb + 24u]; a2 += s1 * dot(q1, x0) + s2 * dot(q2, x1) + s3 * dot(q3, x2) + s4 * dot(q4, x3); b2 += r1 * dot(p1, x0) + r2 * dot(p2, x1) + r3 * dot(p3, x2) + r4 * dot(p4, x3); }
309
+ { let xb = 192u + c4; let x0 = xs[xb]; let x1 = xs[xb + 8u]; let x2 = xs[xb + 16u]; let x3 = xs[xb + 24u]; a3 += s1 * dot(q1, x0) + s2 * dot(q2, x1) + s3 * dot(q3, x2) + s4 * dot(q4, x3); b3 += r1 * dot(p1, x0) + r2 * dot(p2, x1) + r3 * dot(p3, x2) + r4 * dot(p4, x3); }
310
+ { let xb = 256u + c4; let x0 = xs[xb]; let x1 = xs[xb + 8u]; let x2 = xs[xb + 16u]; let x3 = xs[xb + 24u]; a4 += s1 * dot(q1, x0) + s2 * dot(q2, x1) + s3 * dot(q3, x2) + s4 * dot(q4, x3); b4 += r1 * dot(p1, x0) + r2 * dot(p2, x1) + r3 * dot(p3, x2) + r4 * dot(p4, x3); }
311
+ { let xb = 320u + c4; let x0 = xs[xb]; let x1 = xs[xb + 8u]; let x2 = xs[xb + 16u]; let x3 = xs[xb + 24u]; a5 += s1 * dot(q1, x0) + s2 * dot(q2, x1) + s3 * dot(q3, x2) + s4 * dot(q4, x3); b5 += r1 * dot(p1, x0) + r2 * dot(p2, x1) + r3 * dot(p3, x2) + r4 * dot(p4, x3); }
312
+ { let xb = 384u + c4; let x0 = xs[xb]; let x1 = xs[xb + 8u]; let x2 = xs[xb + 16u]; let x3 = xs[xb + 24u]; a6 += s1 * dot(q1, x0) + s2 * dot(q2, x1) + s3 * dot(q3, x2) + s4 * dot(q4, x3); b6 += r1 * dot(p1, x0) + r2 * dot(p2, x1) + r3 * dot(p3, x2) + r4 * dot(p4, x3); }
313
+ { let xb = 448u + c4; let x0 = xs[xb]; let x1 = xs[xb + 8u]; let x2 = xs[xb + 16u]; let x3 = xs[xb + 24u]; a7 += s1 * dot(q1, x0) + s2 * dot(q2, x1) + s3 * dot(q3, x2) + s4 * dot(q4, x3); b7 += r1 * dot(p1, x0) + r2 * dot(p2, x1) + r3 * dot(p3, x2) + r4 * dot(p4, x3); }
314
+ }
315
+ }
316
+ }
317
+ let rb = tid * 16u;
318
+ red[rb] = a0; red[rb + 1u] = a1; red[rb + 2u] = a2; red[rb + 3u] = a3; red[rb + 4u] = a4; red[rb + 5u] = a5; red[rb + 6u] = a6; red[rb + 7u] = a7;
319
+ red[rb + 8u] = b0; red[rb + 9u] = b1; red[rb + 10u] = b2; red[rb + 11u] = b3; red[rb + 12u] = b4; red[rb + 13u] = b5; red[rb + 14u] = b6; red[rb + 15u] = b7;
320
+ workgroupBarrier();
321
+ if (q == 0u && ok0) {
322
+ let yb = wg.z * p.M * p.N;
323
+ for (var m = 0u; m < 8u; m++) {
324
+ if (m0 + m < p.M) {
325
+ Y[yb + (m0 + m) * p.N + n0] = red[rb + m] + red[rb + 16u + m] + red[rb + 32u + m] + red[rb + 48u + m];
326
+ if (ok1) { Y[yb + (m0 + m) * p.N + n1] = red[rb + 8u + m] + red[rb + 24u + m] + red[rb + 40u + m] + red[rb + 56u + m]; }
327
+ }
328
+ }
329
+ }
330
+ }`;
331
+ // sum the ks partial products [ks, M, N] -> Y [M, N]
332
+ const WG_KSUM = /* wgsl */`
333
+ struct P { n: u32, ks: u32, pad0: u32, pad1: u32 };
334
+ @group(0) @binding(0) var<uniform> p: P;
335
+ @group(0) @binding(1) var<storage, read> Pt: array<f32>;
336
+ @group(0) @binding(2) var<storage, read_write> Y: array<f32>;
337
+ @compute @workgroup_size(256)
338
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) { let i = gid.x; if (i >= p.n) { return; } var s = 0.0; for (var k = 0u; k < p.ks; k++) { s += Pt[k * p.n + i]; } Y[i] = s; }`;
339
+
340
+ const WG_RMSNORM = /* wgsl */`
341
+ struct P { rows: u32, D: u32, eps: f32, pad: u32 };
342
+ @group(0) @binding(0) var<uniform> p: P;
343
+ @group(0) @binding(1) var<storage, read> X: array<f32>;
344
+ @group(0) @binding(2) var<storage, read> W: array<f32>;
345
+ @group(0) @binding(3) var<storage, read_write> Y: array<f32>;
346
+ var<workgroup> red: array<f32, 256>;
347
+ @compute @workgroup_size(256)
348
+ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
349
+ let r = wg.x; let tid = lid.x; let base = r * p.D;
350
+ var s = 0.0;
351
+ for (var i = tid; i < p.D; i += 256u) { let v = X[base + i]; s += v * v; }
352
+ red[tid] = s; workgroupBarrier();
353
+ for (var o = 128u; o > 0u; o >>= 1u) { if (tid < o) { red[tid] += red[tid + o]; } workgroupBarrier(); }
354
+ let inv = inverseSqrt(red[0] / f32(p.D) + p.eps);
355
+ for (var i = tid; i < p.D; i += 256u) { Y[base + i] = X[base + i] * inv * W[i]; }
356
+ }`;
357
+
358
+ // Top-16 per row over logits [R, V]: one workgroup (256 threads) per row; each thread keeps a private top-16 over its strided
359
+ // slice -> storage scratch [R, 256, 16]; 16 threads merge 256 candidates each -> workgroup memory; thread 0 merges the 256.
360
+ const WG_TOPK = /* wgsl */`
361
+ struct P { R: u32, V: u32, pad0: u32, pad1: u32 };
362
+ @group(0) @binding(0) var<uniform> p: P;
363
+ @group(0) @binding(1) var<storage, read> L: array<f32>;
364
+ @group(0) @binding(2) var<storage, read_write> SV: array<f32>;
365
+ @group(0) @binding(3) var<storage, read_write> SI: array<u32>;
366
+ @group(0) @binding(4) var<storage, read_write> OV: array<f32>;
367
+ @group(0) @binding(5) var<storage, read_write> OI: array<u32>;
368
+ var<workgroup> mv: array<f32, 256>;
369
+ var<workgroup> mi: array<u32, 256>;
370
+ @compute @workgroup_size(256)
371
+ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
372
+ let r = wg.x; let tid = lid.x; let base = r * p.V;
373
+ var vals: array<f32, 16>; var ids: array<u32, 16>;
374
+ for (var c = 0u; c < 16u; c++) { vals[c] = -3.0e38; ids[c] = 0u; }
375
+ var minv = -3.0e38; var mini = 0u;
376
+ for (var j = tid; j < p.V; j += 256u) {
377
+ let v = L[base + j];
378
+ if (v > minv) { vals[mini] = v; ids[mini] = j; minv = vals[0]; mini = 0u; for (var c = 1u; c < 16u; c++) { if (vals[c] < minv) { minv = vals[c]; mini = c; } } }
379
+ }
380
+ let sb = (r * 256u + tid) * 16u;
381
+ for (var c = 0u; c < 16u; c++) { SV[sb + c] = vals[c]; SI[sb + c] = ids[c]; }
382
+ storageBarrier(); workgroupBarrier();
383
+ if (tid < 16u) {
384
+ for (var c = 0u; c < 16u; c++) { vals[c] = -3.0e38; ids[c] = 0u; }
385
+ minv = -3.0e38; mini = 0u;
386
+ let s0 = (r * 256u + tid * 16u) * 16u;
387
+ for (var e = 0u; e < 256u; e++) {
388
+ let v = SV[s0 + e];
389
+ if (v > minv) { vals[mini] = v; ids[mini] = SI[s0 + e]; minv = vals[0]; mini = 0u; for (var c = 1u; c < 16u; c++) { if (vals[c] < minv) { minv = vals[c]; mini = c; } } }
390
+ }
391
+ for (var c = 0u; c < 16u; c++) { mv[tid * 16u + c] = vals[c]; mi[tid * 16u + c] = ids[c]; }
392
+ }
393
+ workgroupBarrier();
394
+ if (tid == 0u) {
395
+ for (var c = 0u; c < 16u; c++) { vals[c] = -3.0e38; ids[c] = 0u; }
396
+ minv = -3.0e38; mini = 0u;
397
+ for (var e = 0u; e < 256u; e++) {
398
+ let v = mv[e];
399
+ if (v > minv) { vals[mini] = v; ids[mini] = mi[e]; minv = vals[0]; mini = 0u; for (var c = 1u; c < 16u; c++) { if (vals[c] < minv) { minv = vals[c]; mini = c; } } }
400
+ }
401
+ for (var c = 0u; c < 16u; c++) { OV[r * 16u + c] = vals[c]; OI[r * 16u + c] = ids[c]; }
402
+ }
403
+ }`;
404
+
405
+ // Non-traditional (half-split) RoPE, MLX nn.RoPE(traditional=False): pairs (i, i+HD/2), freq_i = theta^(-2i/HD), position = pos0 + row.
406
+ const WG_ROPE = /* wgsl */`
407
+ struct P { rows: u32, heads: u32, pos0: u32, pad: u32, theta: f32, pad1: f32, pad2: f32, pad3: f32 };
408
+ @group(0) @binding(0) var<uniform> p: P;
409
+ @group(0) @binding(1) var<storage, read_write> X: array<f32>;
410
+ @compute @workgroup_size(64)
411
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
412
+ let i = gid.x & 63u; let rh = gid.x >> 6u;
413
+ if (rh >= p.rows * p.heads) { return; }
414
+ let row = rh / p.heads;
415
+ let pos = f32(p.pos0 + row);
416
+ let freq = exp(-f32(2u * i) / 128.0 * log(p.theta));
417
+ let ang = pos * freq; let c = cos(ang); let s = sin(ang);
418
+ let b = rh * 128u;
419
+ let x1 = X[b + i]; let x2 = X[b + i + 64u];
420
+ X[b + i] = x1 * c - x2 * s;
421
+ X[b + i + 64u] = x1 * s + x2 * c;
422
+ }`;
423
+
424
+ // Attention over [context ; block] keys. Q [L, NH, HD]; Kc/Vc [C, NKV, HD]; Kb/Vb [L, NKV, HD]; O [L, NH, HD].
425
+ // Context key j sits at absolute position Kpos[j] (allowed iff qpos - kpos < window); block keys are always allowed (non-causal block).
426
+ // Capacity: C + L <= 3072 keys per query (sliding window 2048 + sink 64 + block fits); the caller checks.
427
+ const WG_ATTN = /* wgsl */`
428
+ struct P { C: u32, L: u32, qPos0: u32, pad0: u32, window: u32, NH: u32, NKV: u32, pad: u32, scale: f32, pad1: f32, pad2: f32, pad3: f32 };
429
+ @group(0) @binding(0) var<uniform> p: P;
430
+ @group(0) @binding(1) var<storage, read> Q: array<f32>;
431
+ @group(0) @binding(7) var<storage, read> Kpos: array<u32>;
432
+ @group(0) @binding(2) var<storage, read> Kc: array<f32>;
433
+ @group(0) @binding(3) var<storage, read> Vc: array<f32>;
434
+ @group(0) @binding(4) var<storage, read> Kb: array<f32>;
435
+ @group(0) @binding(5) var<storage, read> Vb: array<f32>;
436
+ @group(0) @binding(6) var<storage, read_write> O: array<f32>;
437
+ const MAXK: u32 = 3072u; // 12 KB of the 16 KB default workgroup storage (q + red take 768 B)
438
+ var<workgroup> sc: array<f32, 3072>;
439
+ var<workgroup> q: array<f32, 128>;
440
+ var<workgroup> red: array<f32, 64>;
441
+ @compute @workgroup_size(64)
442
+ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
443
+ let m = wg.x; let h = wg.y; let tid = lid.x;
444
+ let kvh = h / (p.NH / p.NKV);
445
+ let qb = (m * p.NH + h) * 128u;
446
+ q[tid * 2u] = Q[qb + tid * 2u]; q[tid * 2u + 1u] = Q[qb + tid * 2u + 1u];
447
+ workgroupBarrier();
448
+ let nk = p.C + p.L; let qpos = p.qPos0 + m;
449
+ var lmax = -1e30;
450
+ for (var j = tid; j < nk; j += 64u) {
451
+ var s = 0.0; var ok = true; var kb = 0u;
452
+ if (j < p.C) { kb = (j * p.NKV + kvh) * 128u; ok = (qpos - Kpos[j]) < p.window; for (var d = 0u; d < 128u; d++) { s += q[d] * Kc[kb + d]; } }
453
+ else { kb = ((j - p.C) * p.NKV + kvh) * 128u; for (var d = 0u; d < 128u; d++) { s += q[d] * Kb[kb + d]; } }
454
+ s = select(-1e30, s * p.scale, ok);
455
+ sc[j] = s; lmax = max(lmax, s);
456
+ }
457
+ red[tid] = lmax; workgroupBarrier();
458
+ for (var o = 32u; o > 0u; o >>= 1u) { if (tid < o) { red[tid] = max(red[tid], red[tid + o]); } workgroupBarrier(); }
459
+ let gmax = red[0]; workgroupBarrier();
460
+ var lsum = 0.0;
461
+ for (var j = tid; j < nk; j += 64u) { let e = exp(sc[j] - gmax); sc[j] = e; lsum += e; }
462
+ red[tid] = lsum; workgroupBarrier();
463
+ for (var o = 32u; o > 0u; o >>= 1u) { if (tid < o) { red[tid] += red[tid + o]; } workgroupBarrier(); }
464
+ let inv = 1.0 / red[0];
465
+ let d0 = tid * 2u; let d1 = d0 + 1u;
466
+ var a0 = 0.0; var a1 = 0.0;
467
+ for (var j = 0u; j < p.C; j++) { let vb = (j * p.NKV + kvh) * 128u; let pj = sc[j]; a0 += pj * Vc[vb + d0]; a1 += pj * Vc[vb + d1]; }
468
+ for (var j = 0u; j < p.L; j++) { let vb = (j * p.NKV + kvh) * 128u; let pj = sc[p.C + j]; a0 += pj * Vb[vb + d0]; a1 += pj * Vb[vb + d1]; }
469
+ O[qb + d0] = a0 * inv; O[qb + d1] = a1 * inv;
470
+ }`;
471
+
472
+ // Grouped dynamic causal conv (kernel 2, group 16) over the L block rows:
473
+ // Y[l,h] = (RES[l,h] if useRes) + sum_off (BASE[a][off][h] + DYN[l, a*2*G + off*G + g]) * X[l-off, h], g = h / 16, X[-1] = 0.
474
+ const WG_CONV = /* wgsl */`
475
+ struct P { L: u32, H: u32, G: u32, a: u32, useRes: u32, pad0: u32, pad1: u32, pad2: u32 };
476
+ @group(0) @binding(0) var<uniform> p: P;
477
+ @group(0) @binding(1) var<storage, read> X: array<f32>;
478
+ @group(0) @binding(2) var<storage, read> DYN: array<f32>;
479
+ @group(0) @binding(3) var<storage, read> BASE: array<f32>;
480
+ @group(0) @binding(4) var<storage, read> RES: array<f32>;
481
+ @group(0) @binding(5) var<storage, read_write> Y: array<f32>;
482
+ @compute @workgroup_size(64)
483
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
484
+ let idx = gid.x; if (idx >= p.L * p.H) { return; }
485
+ let l = idx / p.H; let h = idx % p.H; let g = h / 16u;
486
+ let dynRow = l * (4u * p.G) + p.a * (2u * p.G);
487
+ var acc = 0.0;
488
+ for (var off = 0u; off < 2u; off++) {
489
+ if (l >= off) {
490
+ let k = BASE[p.a * (2u * p.H) + off * p.H + h] + DYN[dynRow + off * p.G + g];
491
+ acc += k * X[(l - off) * p.H + h];
492
+ }
493
+ }
494
+ if (p.useRes == 1u) { acc += RES[idx]; }
495
+ Y[idx] = acc;
496
+ }`;
497
+
498
+ const WG_SILU_MUL = /* wgsl */`
499
+ struct P { n: u32, pad0: u32, pad1: u32, pad2: u32 };
500
+ @group(0) @binding(0) var<uniform> p: P;
501
+ @group(0) @binding(1) var<storage, read> G: array<f32>;
502
+ @group(0) @binding(2) var<storage, read> U: array<f32>;
503
+ @group(0) @binding(3) var<storage, read_write> Y: array<f32>;
504
+ @compute @workgroup_size(256)
505
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
506
+ let i = gid.x; if (i >= p.n) { return; }
507
+ let g = G[i]; Y[i] = g / (1.0 + exp(-g)) * U[i];
508
+ }`;
509
+
510
+ const WG_SCALE = /* wgsl */`
511
+ struct P { n: u32, pad0: u32, pad1: u32, pad2: u32, s: f32, p1: f32, p2: f32, p3: f32 };
512
+ @group(0) @binding(0) var<uniform> p: P;
513
+ @group(0) @binding(1) var<storage, read> X: array<f32>;
514
+ @group(0) @binding(2) var<storage, read_write> Y: array<f32>;
515
+ @compute @workgroup_size(256)
516
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) { let i = gid.x; if (i >= p.n) { return; } Y[i] = X[i] * p.s; }`;
517
+
518
+ // ---------------- runtime ----------------
519
+ class Drafter {
520
+ constructor(device, opts = {}) {
521
+ this.device = device; this.log = opts.log || (() => {}); this.pipes = {}; this.uniforms = new Map(); this.w = {}; this.stats = { weightBytes: 0, tensors: 0 };
522
+ this.packed = opts.packed !== false; // default: Q4_K/Q6_K stay packed on the GPU, dequantized in-shader; packed:false = f16 dequant at load (step 3)
523
+ this.stats.packed = this.packed;
524
+ // Optional per-op GPU timing: with the 'timestamp-query' feature each op runs in its own pass bracketed by timestamps.
525
+ this.profile = !!opts.profile && device.features.has('timestamp-query');
526
+ if (this.profile) { this.qs = device.createQuerySet({ type: 'timestamp', count: 1024 }); this.qbuf = device.createBuffer({ size: 1024 * 8, usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC }); this.qread = device.createBuffer({ size: 1024 * 8, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST }); }
527
+ for (const [k, src] of Object.entries({ gemm: WG_GEMM, gemm_q4k: WG_GEMM_Q4K, gemm_q6k: WG_GEMM_Q6K, ksum: WG_KSUM, rmsnorm: WG_RMSNORM, topk: WG_TOPK, rope: WG_ROPE, attn: WG_ATTN, conv: WG_CONV, silu: WG_SILU_MUL, scale: WG_SCALE })) {
528
+ const module = device.createShaderModule({ code: src, label: k });
529
+ this.pipes[k] = device.createComputePipeline({ label: k, layout: 'auto', compute: { module, entryPoint: 'main' } });
530
+ }
531
+ }
532
+ buf(n, usage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, label = '') { return this.device.createBuffer({ size: Math.max(16, Math.ceil(n * 4 / 16) * 16), usage, label }); }
533
+ upload(f32, label) { const b = this.buf(f32.length, undefined, label); this.device.queue.writeBuffer(b, 0, f32.buffer, f32.byteOffset, f32.byteLength); return b; }
534
+ uni(vals) { // vals: array of {u: n} | {f: x}; 8 slots of 4 bytes
535
+ const key = JSON.stringify(vals); let b = this.uniforms.get(key); if (b) return b;
536
+ const ab = new ArrayBuffer(48); const dv = new DataView(ab);
537
+ vals.forEach((v, i) => { if ('f' in v) dv.setFloat32(i * 4, v.f, true); else dv.setUint32(i * 4, v.u, true); });
538
+ b = this.device.createBuffer({ size: 48, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); this.device.queue.writeBuffer(b, 0, ab); this.uniforms.set(key, b); return b;
539
+ }
540
+ bind(pipe, entries) { return this.device.createBindGroup({ layout: this.pipes[pipe].getBindGroupLayout(0), entries: entries.map((e, i) => ({ binding: i, resource: e.buffer ? e : { buffer: e } })) }); }
541
+ run(pass, pipe, entries, x, y = 1, z = 1) {
542
+ if (this.profile && this.enc) { const i = this.marks.length; pass = this.enc.beginComputePass({ timestampWrites: { querySet: this.qs, beginningOfPassWriteIndex: 2 * i, endOfPassWriteIndex: 2 * i + 1 } }); this.marks.push(this.opLabel || pipe); }
543
+ pass.setPipeline(this.pipes[pipe]); pass.setBindGroup(0, this.bind(pipe, entries)); pass.dispatchWorkgroups(x, y, z);
544
+ if (this.profile && this.enc) pass.end();
545
+ }
546
+ // profiling bracket: begin() returns a (possibly dummy) pass; end() submits and, when profiling, resolves the timestamps
547
+ begin() { this.enc = this.device.createCommandEncoder(); this.marks = []; return this.profile ? { end() {} } : this.enc.beginComputePass(); }
548
+ end(pass) { pass.end(); if (this.profile) { this.enc.resolveQuerySet(this.qs, 0, 2 * this.marks.length, this.qbuf, 0); this.enc.copyBufferToBuffer(this.qbuf, 0, this.qread, 0, 2 * this.marks.length * 8); } this.device.queue.submit([this.enc.finish()]); this.enc = null; }
549
+ async profileTimes() { // -> [{op, ms}] for the last end() when profiling
550
+ if (!this.profile || !this.marks) return null; await this.qread.mapAsync(GPUMapMode.READ); const t = new BigInt64Array(this.qread.getMappedRange().slice(0)); this.qread.unmap();
551
+ return this.marks.map((op, i) => ({ op, ms: Number(t[2 * i + 1] - t[2 * i]) / 1e6 }));
552
+ }
553
+
554
+ // ---- ops (record into a compute pass) ----
555
+ gemm(pass, X, W, Y, M, K, N) {
556
+ if (W && W.q) {
557
+ if (K % 256) throw new Error('gemm_q: K % 256 != 0');
558
+ const nb = K / 256, wgx = Math.ceil(N / 32), wgy = Math.ceil(M / 8);
559
+ const ks = Math.max(1, Math.min(nb, Math.floor(512 / (wgx * wgy)))); // K-split so that >= ~512 workgroups run for narrow N
560
+ if (ks === 1) { this.run(pass, W.q === 'q4k' ? 'gemm_q4k' : 'gemm_q6k', [this.uni([{ u: M }, { u: K }, { u: N }, { u: 1 }]), X, W.buffer, Y], wgx, wgy, 1); return; }
561
+ const key = ks * M * N; if (!this._part || this._part.n < key) this._part = { n: key, buf: this.buf(key, undefined, 'gemm_partials') };
562
+ this.run(pass, W.q === 'q4k' ? 'gemm_q4k' : 'gemm_q6k', [this.uni([{ u: M }, { u: K }, { u: N }, { u: ks }]), X, W.buffer, this._part.buf], wgx, wgy, ks);
563
+ this.run(pass, 'ksum', [this.uni([{ u: M * N }, { u: ks }, { u: 0 }, { u: 0 }]), this._part.buf, Y], Math.ceil(M * N / 256)); return;
564
+ }
565
+ if (K % 8) throw new Error('gemm: K % 8 != 0'); this.run(pass, 'gemm', [this.uni([{ u: M }, { u: K }, { u: N }, { u: 0 }]), X, W, Y], Math.ceil(N / 4), Math.ceil(M / 8));
566
+ }
567
+ // upload a raw GGUF tensor as a packed weight: Q4_K as-is (36 u32 per block), Q6_K repacked to a 53-u32 stride
568
+ uploadPacked(type, raw, n, label) {
569
+ let bytes;
570
+ if (type === GGML.Q4_K) bytes = raw;
571
+ else if (type === GGML.Q6_K) { const nb = n / 256; bytes = new Uint8Array(nb * 212); for (let b = 0; b < nb; ++b) bytes.set(raw.subarray(b * 210, b * 210 + 210), b * 212); }
572
+ else throw new Error('uploadPacked: ' + type);
573
+ const buf = this.device.createBuffer({ size: Math.ceil(bytes.byteLength / 16) * 16, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, label });
574
+ this.device.queue.writeBuffer(buf, 0, bytes.buffer, bytes.byteOffset, bytes.byteLength);
575
+ return { q: type === GGML.Q4_K ? 'q4k' : 'q6k', buffer: buf, bytes: bytes.byteLength };
576
+ }
577
+ rmsnorm(pass, X, W, Y, rows, D) { this.run(pass, 'rmsnorm', [this.uni([{ u: rows }, { u: D }, { f: CFG.EPS }, { u: 0 }]), X, W, Y], rows); }
578
+ rope(pass, X, rows, heads, pos0) { this.run(pass, 'rope', [this.uni([{ u: rows }, { u: heads }, { u: pos0 }, { u: 0 }, { f: CFG.THETA }, { f: 0 }, { f: 0 }, { f: 0 }]), X], rows * heads); }
579
+ attn(pass, Q, Kc, Vc, Kb, Vb, O, C, L, qPos0, Kpos) { if (C + L > 3072) throw new Error(`attn: ${C + L} keys > 3072 capacity`); this.run(pass, 'attn', [this.uni([{ u: C }, { u: L }, { u: qPos0 }, { u: 0 }, { u: CFG.WINDOW }, { u: CFG.NH }, { u: CFG.NKV }, { u: 0 }, { f: 1 / Math.sqrt(CFG.HD) }, { f: 0 }, { f: 0 }, { f: 0 }]), Q, Kc, Vc, Kb, Vb, O, Kpos], L, CFG.NH); }
580
+ conv(pass, X, DYN, BASE, RES, Y, L, a, useRes) { const G = CFG.H / CFG.GROUP; this.run(pass, 'conv', [this.uni([{ u: L }, { u: CFG.H }, { u: G }, { u: a }, { u: useRes ? 1 : 0 }, { u: 0 }, { u: 0 }, { u: 0 }]), X, DYN, BASE, RES, Y], Math.ceil(L * CFG.H / 64)); }
581
+ silu(pass, G, U, Y, n) { this.run(pass, 'silu', [this.uni([{ u: n }, { u: 0 }, { u: 0 }, { u: 0 }]), G, U, Y], Math.ceil(n / 256)); }
582
+ // top-16 per row of logits L (binding or buffer, f32 [R, V]); returns {ov (f32 [R,16]), oi (u32 [R,16])} (allocated once per R)
583
+ topk(pass, L, R, V) {
584
+ if (!this._topk || this._topk.R !== R) this._topk = { R, sv: this.buf(R * 256 * 16, GPUBufferUsage.STORAGE), si: this.buf(R * 256 * 16, GPUBufferUsage.STORAGE), ov: this.buf(R * 16), oi: this.buf(R * 16) };
585
+ const t = this._topk; this.run(pass, 'topk', [this.uni([{ u: R }, { u: V }, { u: 0 }, { u: 0 }]), L, t.sv, t.si, t.ov, t.oi], R); return t;
586
+ }
587
+ scale(pass, X, Y, n, s) { this.run(pass, 'scale', [this.uni([{ u: n }, { u: 0 }, { u: 0 }, { u: 0 }, { f: s }, { f: 0 }, { f: 0 }, { f: 0 }]), X, Y], Math.ceil(n / 256)); }
588
+
589
+ // ---- weights ----
590
+ // ggufSource: a URL string (Range-fetched) OR a byte-source function (offset, length) => Promise<Uint8Array> (e.g. slices of a cached Blob).
591
+ async loadWeights(ggufSource, onProgress = () => {}) {
592
+ const src = typeof ggufSource === 'function' ? ggufSource : fetchSource(ggufSource); const g = await readGGUF(src); this.gguf = g;
593
+ const need = ['fc.weight', 'enc.output_norm.weight', 'output_norm.weight', 'selector_hidden.weight'];
594
+ for (let i = 0; i < CFG.L; ++i) for (const s of ['attn_norm.weight', 'attn_q.weight', 'attn_k.weight', 'attn_v.weight', 'attn_output.weight', 'attn_q_norm.weight', 'attn_k_norm.weight', 'attn_conv_base', 'attn_conv_proj.weight', 'ffn_norm.weight', 'ffn_gate.weight', 'ffn_up.weight', 'ffn_down.weight', 'ffn_conv_base', 'ffn_conv_proj.weight']) need.push(`blk.${i}.${s}`);
595
+ let done = 0; const t0 = performance.now(); let dequantMs = 0;
596
+ for (const name of need) {
597
+ const t = g.byName[name]; if (!t) throw new Error('missing tensor ' + name);
598
+ const raw = await src(t.absOffset, t.bytes);
599
+ const td = performance.now();
600
+ if (t.type === GGML.F32) { const f = new Float32Array(raw.buffer, raw.byteOffset, t.n); this.w[name] = this.upload(f, name); this.stats.weightBytes += t.n * 4; }
601
+ else if (this.packed) { const pw = this.uploadPacked(t.type, raw, t.n, name); this.w[name] = pw; this.stats.weightBytes += pw.bytes; }
602
+ else { const f32 = dequantize(t.type, raw, t.n); const h = f32ArrayToF16(f32); const b = this.device.createBuffer({ size: h.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, label: name }); this.device.queue.writeBuffer(b, 0, h.buffer, h.byteOffset, h.byteLength); this.w[name] = b; this.stats.weightBytes += h.byteLength; }
603
+ dequantMs += performance.now() - td;
604
+ this.stats.tensors++; done++; onProgress({ done, total: need.length, name, ms: performance.now() - t0 });
605
+ await new Promise(r => setTimeout(r, 0));
606
+ }
607
+ // codebooks stay raw on the CPU (one Q4_K block == one 256-wide row): rows are dequantized on demand by the selector
608
+ this.codebook = {};
609
+ for (const n of ['selector_predecessor.weight', 'selector_successor.weight']) { const t = g.byName[n]; this.codebook[n] = { type: t.type, raw: await src(t.absOffset, t.bytes) }; }
610
+ await this.device.queue.onSubmittedWorkDone();
611
+ this.stats.loadMs = performance.now() - t0; this.stats.dequantMs = dequantMs;
612
+ this.log(`weights: ${this.stats.tensors} tensors, ${(this.stats.weightBytes / 1e9).toFixed(2)} GB on GPU, ${this.stats.loadMs.toFixed(0)} ms (dequant ${dequantMs.toFixed(0)} ms)`);
613
+ }
614
+ codebookRow(which, id) { const cb = this.codebook[which]; return dequantRow(cb.type, cb.raw, CFG.RANK, id); }
615
+
616
+ // ---- forward ----
617
+ // Stage A: projected context cache. features f32 [C, 5H] -> draft_context [C, H] -> per layer ctx K (normed+roped) / V, [C, NKV, HD].
618
+ projectContext(features, C, ctxPos0 = 0) {
619
+ const { H, NKV, HD } = CFG; const dev = this.device;
620
+ const F = this.upload(features, 'features'); const fcOut = this.buf(C * H, undefined, 'fc'); const ctx = this.buf(C * H, undefined, 'draft_context');
621
+ const layers = [];
622
+ const pass = this.begin();
623
+ this.opLabel = 'ctx.fc'; this.gemm(pass, F, this.w['fc.weight'], fcOut, C, 5 * H, H);
624
+ this.rmsnorm(pass, fcOut, this.w['enc.output_norm.weight'], ctx, C, H);
625
+ for (let i = 0; i < CFG.L; ++i) {
626
+ const kRaw = this.buf(C * NKV * HD, undefined, `ctx_k_raw${i}`), k = this.buf(C * NKV * HD, undefined, `ctx_k${i}`), v = this.buf(C * NKV * HD, undefined, `ctx_v${i}`);
627
+ this.gemm(pass, ctx, this.w[`blk.${i}.attn_k.weight`], kRaw, C, H, NKV * HD);
628
+ this.rmsnorm(pass, kRaw, this.w[`blk.${i}.attn_k_norm.weight`], k, C * NKV, HD);
629
+ this.rope(pass, k, C, NKV, ctxPos0);
630
+ this.gemm(pass, ctx, this.w[`blk.${i}.attn_v.weight`], v, C, H, NKV * HD);
631
+ layers.push({ k, v });
632
+ }
633
+ this.opLabel = null; this.end(pass);
634
+ const posArr = new Uint32Array(C); for (let i = 0; i < C; ++i) posArr[i] = ctxPos0 + i; const pos = this.buf(C, undefined, 'ctx_pos'); dev.queue.writeBuffer(pos, 0, posArr);
635
+ return { C, total: C, ctxPos0, ctx, fcOut, layers, F, pos };
636
+ }
637
+ // Stage A' (incremental): a projected-context cache with a fixed capacity, appended a few rows at a time from a GPU
638
+ // feature binding ({buffer, offset, size}: f32 [rows, 5H], e.g. the engine's dspark.features scratch). Positions are
639
+ // absolute (row r of the context sits at position r); no eviction (capacity <= 3064 so the attention kernel fits).
640
+ // Eviction (MLX ContextOnlyDraftKVCache, sink 64 / window 1024 upstream defaults): after an append that leaves more than
641
+ // sink + window rows, keep the first `sink` rows and the last `window` rows (positions travel with the rows; the attention
642
+ // mask uses them, so sink rows older than the 2048 sliding window get masked exactly as in MLX). sink+window = 0 -> no eviction.
643
+ createContext(capacity, opts = {}) {
644
+ const { H, NKV, HD, BLOCK } = CFG; if (capacity + BLOCK > 3072) throw new Error('context capacity + block > 3072 keys');
645
+ const sink = opts.sink ?? 0, window = opts.window ?? 0; if (sink + window > 0 && capacity < sink + window + BLOCK) throw new Error('capacity < sink + window + block');
646
+ const layers = []; for (let i = 0; i < CFG.L; ++i) layers.push({ k: this.buf(capacity * NKV * HD, undefined, `ctx_k${i}`), v: this.buf(capacity * NKV * HD, undefined, `ctx_v${i}`) });
647
+ return { C: 0, total: 0, capacity, sink, window, layers, pos: this.buf(capacity, undefined, 'ctx_pos'), tmp: sink + window > 0 ? this.buf(window * NKV * HD, undefined, 'ctx_tmp') : null, evictions: 0,
648
+ fcOut: this.buf(BLOCK * H, undefined, 'fc'), ctx: this.buf(BLOCK * H, undefined, 'draft_context'), kRaw: this.buf(BLOCK * NKV * HD, undefined, 'k_raw'), incremental: true };
649
+ }
650
+ resetContext(cache) { cache.C = 0; cache.total = 0; cache.evictions = 0; }
651
+ evict(cache) {
652
+ const { NKV, HD } = CFG; const keep = cache.sink + cache.window; if (!keep || cache.C <= keep) return;
653
+ const rowBytes = NKV * HD * 4; const src = cache.C - cache.window; const enc = this.device.createCommandEncoder();
654
+ for (const l of cache.layers) for (const b of [l.k, l.v]) { enc.copyBufferToBuffer(b, src * rowBytes, cache.tmp, 0, cache.window * rowBytes); enc.copyBufferToBuffer(cache.tmp, 0, b, cache.sink * rowBytes, cache.window * rowBytes); }
655
+ enc.copyBufferToBuffer(cache.pos, src * 4, cache.tmp, 0, cache.window * 4); enc.copyBufferToBuffer(cache.tmp, 0, cache.pos, cache.sink * 4, cache.window * 4);
656
+ this.device.queue.submit([enc.finish()]); cache.C = keep; cache.evictions++;
657
+ }
658
+ appendContext(cache, feat, rows) {
659
+ const { H, NKV, HD, BLOCK } = CFG; if (rows < 1 || rows > BLOCK) throw new Error('appendContext: 1..8 rows'); if (cache.C + rows > cache.capacity) throw new Error('context capacity exceeded');
660
+ const rowBytes = NKV * HD * 4; const at = (b) => ({ buffer: b, offset: cache.C * rowBytes, size: rows * rowBytes });
661
+ const posArr = new Uint32Array(rows); for (let r = 0; r < rows; ++r) posArr[r] = cache.total + r; this.device.queue.writeBuffer(cache.pos, cache.C * 4, posArr);
662
+ const pass = this.begin();
663
+ this.opLabel = 'ctx.fc'; this.gemm(pass, feat, this.w['fc.weight'], cache.fcOut, rows, 5 * H, H);
664
+ this.rmsnorm(pass, cache.fcOut, this.w['enc.output_norm.weight'], cache.ctx, rows, H);
665
+ for (let i = 0; i < CFG.L; ++i) {
666
+ this.gemm(pass, cache.ctx, this.w[`blk.${i}.attn_k.weight`], cache.kRaw, rows, H, NKV * HD);
667
+ this.rmsnorm(pass, cache.kRaw, this.w[`blk.${i}.attn_k_norm.weight`], at(cache.layers[i].k), rows * NKV, HD);
668
+ this.rope(pass, at(cache.layers[i].k), rows, NKV, cache.total);
669
+ this.gemm(pass, cache.ctx, this.w[`blk.${i}.attn_v.weight`], at(cache.layers[i].v), rows, H, NKV * HD);
670
+ }
671
+ this.opLabel = null; this.end(pass); cache.C += rows; cache.total += rows; this.evict(cache);
672
+ }
673
+ // Stage B: one draft step over the 8 block rows. noise f32 [L, H] (raw target embed; scaled here by embedScale).
674
+ // Returns the GPU buffers of every stage (for readback) + the selector-projected hidden.
675
+ draftStep(cache, noise, embedScale = 1.0) {
676
+ const { H, I, NH, NKV, HD, BLOCK: L } = CFG; const dev = this.device; const C = cache.C; const G = H / CFG.GROUP;
677
+ const N = noise instanceof Float32Array ? this.upload(noise, 'noise') : noise; // Float32Array -> upload; else a GPU binding {buffer, offset, size}
678
+ const h0 = this.buf(L * H, undefined, 'h0');
679
+ const st = { layers: [] };
680
+ const pass = this.begin();
681
+ this.scale(pass, N, h0, L * H, embedScale);
682
+ let h = h0;
683
+ for (let i = 0; i < CFG.L; ++i) {
684
+ const w = (s) => { this.opLabel = s.replace('.weight', ''); return this.w[`blk.${i}.${s}`]; };
685
+ const normed = this.buf(L * H), dynA = this.buf(L * 4 * G), xin = this.buf(L * H, undefined, `attn_in${i}`);
686
+ this.rmsnorm(pass, h, w('attn_norm.weight'), normed, L, H);
687
+ this.gemm(pass, normed, w('attn_conv_proj.weight'), dynA, L, H, 4 * G);
688
+ this.conv(pass, normed, dynA, w('attn_conv_base'), normed, xin, L, 0, false);
689
+ const q = this.buf(L * NH * HD), qn = this.buf(L * NH * HD), k = this.buf(L * NKV * HD), kn = this.buf(L * NKV * HD), v = this.buf(L * NKV * HD), o = this.buf(L * NH * HD), ao = this.buf(L * H), hA = this.buf(L * H, undefined, `attn_out${i}`);
690
+ this.gemm(pass, xin, w('attn_q.weight'), q, L, H, NH * HD); this.rmsnorm(pass, q, w('attn_q_norm.weight'), qn, L * NH, HD); this.rope(pass, qn, L, NH, cache.total);
691
+ this.gemm(pass, xin, w('attn_k.weight'), k, L, H, NKV * HD); this.rmsnorm(pass, k, w('attn_k_norm.weight'), kn, L * NKV, HD); this.rope(pass, kn, L, NKV, cache.total);
692
+ this.gemm(pass, xin, w('attn_v.weight'), v, L, H, NKV * HD);
693
+ this.opLabel = 'attention'; this.attn(pass, qn, cache.layers[i].k, cache.layers[i].v, kn, v, o, C, L, cache.total, cache.pos);
694
+ this.gemm(pass, o, w('attn_output.weight'), ao, L, NH * HD, H);
695
+ this.conv(pass, ao, dynA, w('attn_conv_base'), h, hA, L, 1, true);
696
+ const normed2 = this.buf(L * H), dynM = this.buf(L * 4 * G), xm = this.buf(L * H), gate = this.buf(L * I), up = this.buf(L * I), act = this.buf(L * I), down = this.buf(L * H), hM = this.buf(L * H, undefined, `out${i}`);
697
+ this.rmsnorm(pass, hA, w('ffn_norm.weight'), normed2, L, H);
698
+ this.gemm(pass, normed2, w('ffn_conv_proj.weight'), dynM, L, H, 4 * G);
699
+ this.conv(pass, normed2, dynM, w('ffn_conv_base'), normed2, xm, L, 0, false);
700
+ this.gemm(pass, xm, w('ffn_gate.weight'), gate, L, H, I); this.gemm(pass, xm, w('ffn_up.weight'), up, L, H, I);
701
+ this.opLabel = 'silu_mul'; this.silu(pass, gate, up, act, L * I);
702
+ this.gemm(pass, act, w('ffn_down.weight'), down, L, I, H);
703
+ this.conv(pass, down, dynM, w('ffn_conv_base'), hA, hM, L, 1, true);
704
+ st.layers.push({ attn_in: xin, attn_out: hA, out: hM, q: qn, k: kn, v, o });
705
+ h = hM;
706
+ }
707
+ this.opLabel = 'output_norm'; const fin = this.buf(L * H, undefined, 'final'); this.rmsnorm(pass, h, this.w['output_norm.weight'], fin, L, H);
708
+ const selH = this.buf((L - 1) * CFG.RANK, undefined, 'sel_hidden');
709
+ this.opLabel = 'selector_hidden'; this.gemm(pass, { buffer: fin, offset: H * 4, size: (L - 1) * H * 4 }, this.w['selector_hidden.weight'], selH, L - 1, H, CFG.RANK);
710
+ this.opLabel = null; this.end(pass);
711
+ st.final = fin; st.selHidden = selH; st.h0 = h0;
712
+ return st;
713
+ }
714
+ // Selector (CPU, f32): candidates [7][16] ids + unary logits [7][16] (from the target head), selHidden [7*256]. Greedy path walk.
715
+ select(anchor, candIds, unary, selHidden) {
716
+ const R = CFG.RANK; let pred = anchor; const path = []; const edgesAll = [];
717
+ for (let pos = 0; pos < candIds.length; ++pos) {
718
+ const pv = this.codebookRow('selector_predecessor.weight', pred); const hp = selHidden.subarray(pos * R, (pos + 1) * R);
719
+ const edges = new Float32Array(candIds[pos].length); let best = -Infinity, bi = 0;
720
+ for (let c = 0; c < candIds[pos].length; ++c) {
721
+ const sv = this.codebookRow('selector_successor.weight', candIds[pos][c]); let e = 0; for (let r = 0; r < R; ++r) e += pv[r] * hp[r] * sv[r];
722
+ edges[c] = e; const s = unary[pos][c] + e; if (s > best) { best = s; bi = c; }
723
+ }
724
+ pred = candIds[pos][bi]; path.push(pred); edgesAll.push(edges);
725
+ }
726
+ return { path, edges: edgesAll };
727
+ }
728
+ async read(buf, n, byteOffset = 0) {
729
+ const s = this.device.createBuffer({ size: Math.ceil(n * 4 / 4) * 4, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST });
730
+ const enc = this.device.createCommandEncoder(); enc.copyBufferToBuffer(buf, byteOffset, s, 0, n * 4); this.device.queue.submit([enc.finish()]);
731
+ await s.mapAsync(GPUMapMode.READ); const out = new Float32Array(s.getMappedRange().slice(0)); s.unmap(); s.destroy(); return out;
732
+ }
733
+ }
734
+
735
+ // ---- lab/webgpu/runner/spec-runner.js ----
736
+ // DFlashRunner — the DFlash 2 speculative generate loop behind the engine's `specDecodeRunner()` seam.
737
+ // The harness in runner.js measures; this file is the reusable class an app attaches to the engine:
738
+ //
739
+ // const runner = await DFlashRunner.create(engine, { drafter: urlOrByteSource, block: 5, onProgress });
740
+ // engine.model.dflashRunner = runner; // the patched engine routes generate() through it
741
+ // ...
742
+ // runner.dispose();
743
+ //
744
+ // Engine contract (ternary_bonsai_2_27b.js after scripts/bonsai2-dflash-patches.mjs): the graph-decode path calls
745
+ // `runner.generate(suffixIds, cache, generationArgs, beginDecode, eosTokenId)` and consumes it as the token stream;
746
+ // `runner.release(cache)` runs before a temporary cache is disposed. Invariant kept from the engine's own loop: at
747
+ // every yield the cache holds the prompt plus every yielded token except the last one (the wrapper commits its prefix
748
+ // cache from `cache.get_seq_length()`, so the cache content must equal the first seqLength tokens of prompt+output).
749
+ //
750
+ // Cycle: capture checkpoint -> draft 7 (noise rows through the drafter, lm_head micro graph, GPU top-16, selector)
751
+ // -> verify block [anchor, drafts[0..block-2]] through the tapped small-M verify session -> accept the longest
752
+ // agreeing prefix (k) + the bonus token -> append rows 0..k of the verify run's features to the drafter context
753
+ // -> if k+1 < block: restore the checkpoint and rewind the recurrent state through the accepted rows (RewindSession).
754
+ // Output is greedy-identical to the engine's own decode (runner/RESULTS.md).
755
+
756
+
757
+ const TAPS = [6, 20, 34, 48, 62]; // entry-of-layer convention == output of the drafter's target_layer_ids [5,19,33,47,61]
758
+ // Prompt prefill mirrors the engine's own chunk plan (graph blocks 16/32/64/128/256/512, the smallest that holds the
759
+ // remainder) through tapped sessions, so it costs what the engine's prefill costs; only the rows inside the drafter's
760
+ // context window (sink + window, the tail of the prompt) are appended to the drafter context.
761
+ const PREFILL_BLOCKS = [16, 32, 64, 128, 256, 512];
762
+
763
+ class DFlashRunner {
764
+ static async create(engine, opts = {}) {
765
+ const r = new DFlashRunner(engine, opts);
766
+ await r.#init(opts);
767
+ // build the per-cache sessions for the engine's persistent cache now, off the first turn's critical path
768
+ const cache = engine.generationState?.cache;
769
+ if (cache) await r.#resources(cache);
770
+ return r;
771
+ }
772
+
773
+ constructor(engine, opts) {
774
+ this.engine = engine; this.inner = engine.model; this.rt = this.inner.runtime; this.dev = this.rt.host.device; this.cfg = this.inner.config;
775
+ this.I = engine.constructor.__dflashInternals;
776
+ if (!this.I || !this.I.lh || !this.I.ch || !this.I.RewindSession) throw new Error('DFlashRunner: engine is missing the dflash internals hook (regenerate ternary_bonsai_2_27b.js)');
777
+ this.block = opts.block ?? 5;
778
+ if (!(this.block >= 2 && this.block <= CFG.BLOCK)) throw new Error(`DFlashRunner: block must be 2..${CFG.BLOCK}`);
779
+ this.sink = opts.sink ?? 64; this.window = opts.window ?? 1024;
780
+ this.smallM = opts.smallM === 'off' ? undefined : { precision: opts.smallM ?? 'f16' };
781
+ this.log = opts.log || (() => {});
782
+ this.H = this.cfg.hidden_size; this.VOC = this.cfg.vocab_size;
783
+ if (this.H !== CFG.H || this.VOC !== CFG.VOCAB) throw new Error(`DFlashRunner: target H/V ${this.H}/${this.VOC} != drafter ${CFG.H}/${CFG.VOCAB}`);
784
+ this.res = new Map(); // cache -> { sessions, rewind, slot, ctx, ctxEnd }
785
+ this.stats = { cycles: 0, tokens: 0, accepted: 0, generations: 0 };
786
+ this.disposed = false;
787
+ }
788
+
789
+ async #init(opts) {
790
+ const { I, inner, rt, dev, H, VOC } = this;
791
+ // drafter on the engine's device
792
+ this.dr = new Drafter(dev, { log: this.log, packed: opts.packed !== false });
793
+ await this.dr.loadWeights(opts.drafter, opts.onProgress || (() => {}));
794
+ // micro graphs on the engine's compile stack (pattern: wgsl-gemm-spike/bench.js)
795
+ class Micro extends I.f0 { constructor(emit) { super(inner, null, 8); this._emit = emit; } buildEmission() { return this._emit(); } }
796
+ const micro = async (name, emit) => { const s = new Micro(() => { const S = new I.ba(); const b = I._i(S); const t = emit(S, b); return { graph: S.finish({ name }), weights: b.boundWeights, states: b.states, ...t }; }); await s.build(); return s; };
797
+ // (a) noise embedding: LlamaEmbed + inverse Hadamard, the same ops the prefill prologue emits
798
+ this.embedS = await micro('dflash-embed', (S, b) => {
799
+ const re = b.w; const ids = S.stepInput('input_ids', 'uint32', [8]); const hid = S.scratch('embed.hidden', 'float32', [8, H]); let h;
800
+ if (inner.embedBits && inner.embedScales) h = S.op('com.xenova.LlamaEmbed', { inputT: ids, bitsT: re('embed.bits', inner.embedBits), scalesT: re('embed.scales', inner.embedScales), hiddenT: hid }, { args: { hiddenSize: H, vocabSize: VOC, seqLen: 8, format: inner.embedFormat ?? 'q8_rows' } }).hiddenT;
801
+ else { const ke = I.pi(re, inner)('top'); h = S.op('com.xenova.LlamaEmbed', { inputT: ids, weightsT: ke, hiddenT: hid }, { args: { embedOffset: inner.offsets.top.embed_tokens, hiddenSize: H, vocabSize: VOC, seqLen: 8 } }).hiddenT; }
802
+ h = I.bi(S, inner, re)(h, 'embed_tokens', true); S.output(h, 'embed'); return {};
803
+ });
804
+ const embedT = this.embedS.compiled.tensor('embed'); this.embedBind = { buffer: embedT.buffer, offset: embedT.byteOffset ?? 0, size: 8 * H * 4 };
805
+ // (b) head: Hadamard rotation of the drafter's final hidden + Lut2SmallMGemm (M = 7) over the lm_head pack
806
+ if (!(inner.lmHeadQ4 && inner.lmHeadQ4Scales && inner.lmHeadLut === 9 && this.cfg.prismHadamard?.weights.includes('output.weight'))) throw new Error('DFlashRunner: expected a prism-rotated lut2 lm_head (lmHeadLut 9)');
807
+ const R = CFG.BLOCK - 1; this.R = R;
808
+ const prec = (this.smallM && this.smallM.precision) || 'f16';
809
+ this.headS = await micro('dflash-head', (S, b) => {
810
+ const re = b.w; const x = S.stepInput('hid', 'float32', [R, H]); const rot = I.bi(S, inner, re)(x, 'lm_head'); const lg = S.scratch('logits', 'float32', [R, VOC]);
811
+ S.op('com.xenova.Lut2SmallMGemm', { aT: S.view(rot, 0, 'float32', [R, H], 'head.in'), bitsT: re('head.bits', inner.lmHeadQ4), scalesT: re('head.scales', inner.lmHeadQ4Scales), yT: lg }, { args: { M: R, inFeatures: H, outFeatures: VOC, blockOffset: 0, outStride: VOC, dstColStart: 0, lut: 9, precision: prec, kSplits: 1 } });
812
+ S.output(lg, 'logits'); return {};
813
+ });
814
+ this.headIn = this.headS.compiled.tensor('hid'); const headLg = this.headS.compiled.tensor('logits'); this.headLgBind = { buffer: headLg.buffer, offset: headLg.byteOffset ?? 0, size: R * VOC * 4 };
815
+ this.log(`DFlashRunner ready: block ${this.block}, drafter ${(this.dr.stats.weightBytes / 1e9).toFixed(2)} GB on GPU`);
816
+ }
817
+
818
+ // ---- per-cache resources ----
819
+ async #resources(cache) {
820
+ let r = this.res.get(cache); if (r) return r;
821
+ const { I, inner } = this; const taps = TAPS, smallM = this.smallM, LV = this.block;
822
+ const sessions = new Map();
823
+ // verify: all-rows head on the small-M route + the recurrence tee; prefill: taps only (next_token from the last row)
824
+ const session = async (T, opts) => { let s = sessions.get(T); if (s) return s; class S extends I.ch { buildEmission() { return I.lh(this.model, this.cache, this.blockLen, opts); } } s = new S(inner, cache, T); await s.build(); sessions.set(T, s); return s; };
825
+ const verify = await session(LV, { tapLayers: taps, allRowsHead: true, ...(smallM ? { smallM } : {}), teeRecurrence: true });
826
+ const prefill = (T) => session(T, { tapLayers: taps });
827
+ await prefill(16); await prefill(512); // the two blocks every turn tends to need: a short suffix, a long first prompt
828
+ const rewind = new I.RewindSession(inner, cache, LV, verify); await rewind.build();
829
+ const slot = await cache.allocateCheckpointSlot();
830
+ const ctx = this.dr.createContext(this.sink + this.window > 0 ? this.sink + this.window + 16 : cache.maxLength + 16, { sink: this.sink, window: this.window });
831
+ r = { sessions, verify, prefill, rewind, slot, ctx, ctxEnd: -1 }; this.res.set(cache, r); return r;
832
+ }
833
+
834
+ release(cache) {
835
+ const r = this.res.get(cache); if (!r) return; this.res.delete(cache);
836
+ for (const s of r.sessions.values()) { try { s.dispose(); } catch (_) {} }
837
+ try { r.rewind.dispose(); } catch (_) {}
838
+ try { this.I.r2(r.slot.storage); } catch (_) {}
839
+ try { this.dr.destroyContext?.(r.ctx); } catch (_) {}
840
+ }
841
+
842
+ dispose() {
843
+ if (this.disposed) return; this.disposed = true;
844
+ for (const cache of [...this.res.keys()]) this.release(cache);
845
+ try { this.embedS.dispose(); } catch (_) {}
846
+ try { this.headS.dispose(); } catch (_) {}
847
+ try { this.dr.dispose?.(); } catch (_) {}
848
+ }
849
+
850
+ // run `ids` (1..T) through a tapped session at past_len = cache.seqLength; advances the cache
851
+ async #runBlock(r, s, ids) {
852
+ const { rt, H } = this; const T = ids.length; const pos = r.cache.seqLength;
853
+ const next = await s.run(new Uint32Array(ids), pos); r.cache.seqLength = pos + T;
854
+ const ft = s.compiled.tensor('dspark.features');
855
+ return { next, pos, feat: { buffer: ft.buffer, offset: ft.byteOffset ?? 0, size: T * 5 * H * 4 }, tokens: () => rt.readTensor(s.compiled.tensor('verify_tokens')) };
856
+ }
857
+
858
+ #embed(ids) { const { rt } = this; rt.host.writeBuffer(this.embedS.compiled.tensor('input_ids').buffer, 0, new Uint32Array(ids)); this.embedS.compiled.collector.enqueue(this.embedS.steps); return this.embedBind; }
859
+
860
+ async #headTop16(finBuf) {
861
+ const { dev, dr, H, R, VOC } = this;
862
+ const e = dev.createCommandEncoder(); e.copyBufferToBuffer(finBuf, H * 4, this.headIn.buffer, this.headIn.byteOffset ?? 0, R * H * 4); dev.queue.submit([e.finish()]); this.headS.compiled.collector.enqueue(this.headS.steps);
863
+ const pass = dr.begin(); const t = dr.topk(pass, this.headLgBind, R, VOC); dr.end(pass);
864
+ const [ov, oiF] = await Promise.all([dr.read(t.ov, R * 16), dr.read(t.oi, R * 16)]); const oi = new Uint32Array(oiF.buffer);
865
+ const cand = [], unary = [];
866
+ for (let r = 0; r < R; ++r) { cand.push(Array.from(oi.subarray(r * 16, r * 16 + 16))); unary.push(Array.from(ov.subarray(r * 16, r * 16 + 16))); }
867
+ return { cand, unary };
868
+ }
869
+
870
+ async #draft(ctx, anchor) {
871
+ const { dr, R } = this;
872
+ const noise = this.#embed([anchor, ...Array(7).fill(CFG.MASK)]); const st = dr.draftStep(ctx, noise, 1.0);
873
+ const hd = await this.#headTop16(st.final);
874
+ const selH = await dr.read(st.selHidden, R * CFG.RANK); const sel = dr.select(anchor, hd.cand, hd.unary, selH);
875
+ for (const l of st.layers) for (const b of Object.values(l)) b.destroy(); st.final.destroy(); st.selHidden.destroy(); st.h0.destroy();
876
+ return sel.path;
877
+ }
878
+
879
+ // ---- the engine seam ----
880
+ async *generate(tokenIds, cache, generationArgs, beginDecode, eosTokenId) {
881
+ const { I, dr, rt, H } = this; const LV = this.block;
882
+ if (tokenIds.length === 0) throw new Error('generation requires at least one input token');
883
+ const { maxNewTokens, stopOnEos, onPrefillDone } = I.dc(generationArgs, {});
884
+ const isEos = (t) => stopOnEos && I.fc(t, eosTokenId);
885
+ const r = await this.#resources(cache); r.cache = cache;
886
+ this.stats.generations++;
887
+ // drafter context: keep it when it still mirrors the cache (prefix reuse without truncation), else rebuild from the suffix
888
+ const past = cache.get_seq_length();
889
+ if (r.ctxEnd !== past) dr.resetContext(r.ctx);
890
+ // prompt prefill in the engine's block sizes through tapped sessions; anchor = next token after the last chunk.
891
+ // Only the last (sink + window) rows of the suffix feed the drafter context — earlier rows would be evicted anyway.
892
+ const ids = Array.from(tokenIds); const keep = this.sink + this.window > 0 ? this.sink + this.window : ids.length; const firstKept = Math.max(0, ids.length - keep);
893
+ let anchor = null;
894
+ for (let i = 0; i < ids.length;) {
895
+ const remaining = ids.length - i; const T = PREFILL_BLOCKS.find(b => b >= remaining) ?? PREFILL_BLOCKS[PREFILL_BLOCKS.length - 1];
896
+ const chunk = ids.slice(i, i + T); const run = await this.#runBlock(r, await r.prefill(T), chunk); anchor = run.next;
897
+ // the drafter's context projection takes <= 8 rows per call: walk the kept feature rows in 8-row slices
898
+ for (let o = Math.max(0, firstKept - i); o < chunk.length; o += CFG.BLOCK) { const n = Math.min(CFG.BLOCK, chunk.length - o); dr.appendContext(r.ctx, { buffer: run.feat.buffer, offset: run.feat.offset + o * 5 * H * 4, size: n * 5 * H * 4 }, n); }
899
+ i += chunk.length;
900
+ }
901
+ await rt.queueIdle();
902
+ onPrefillDone?.({ tokens: tokenIds.length, cache_length: cache.get_seq_length() });
903
+ if (maxNewTokens <= 0 || isEos(anchor)) { r.ctxEnd = cache.get_seq_length(); return; }
904
+ yield anchor; let emitted = 1;
905
+ const eosSeen = { v: false };
906
+ try {
907
+ while (emitted < maxNewTokens && !eosSeen.v) {
908
+ if (cache.get_seq_length() + LV > cache.maxLength) break; // no room for a full verify block
909
+ const drafted = (await this.#draft(r.ctx, anchor)).slice(0, LV - 1);
910
+ const block = [anchor, ...drafted];
911
+ r.slot.checkpoint.capture(); const pos = cache.seqLength;
912
+ const run = await this.#runBlock(r, r.verify, block); const vt = Array.from(await run.tokens());
913
+ let k = 0; while (k < drafted.length && vt[k] === drafted[k]) k++;
914
+ const bonus = vt[k];
915
+ // accepted drafts are already in the cache as rows 1..k; the drafter context takes rows 0..k of this run
916
+ const out = [];
917
+ for (let i = 0; i < k; ++i) { if (isEos(drafted[i])) { eosSeen.v = true; k = i; break; } out.push(drafted[i]); }
918
+ if (!eosSeen.v) { if (isEos(bonus)) eosSeen.v = true; else out.push(bonus); }
919
+ // rows of the verify block the cache keeps: anchor + accepted drafts (k+1), or fewer when maxNewTokens cuts
920
+ // the output short (then the last yielded draft leaves the cache, like the engine's own loop)
921
+ const y = Math.min(out.length, maxNewTokens - emitted);
922
+ const keep = y < out.length ? y : k + 1;
923
+ dr.appendContext(r.ctx, { ...run.feat, size: keep * 5 * H * 4 }, keep);
924
+ if (keep < LV) { r.slot.checkpoint.restore(); r.rewind.run(keep); cache.seqLength = pos + keep; }
925
+ this.stats.cycles++; this.stats.accepted += k;
926
+ for (let i = 0; i < y; ++i) { yield out[i]; emitted++; this.stats.tokens++; }
927
+ anchor = bonus;
928
+ }
929
+ } finally {
930
+ await rt.queueIdle();
931
+ r.ctxEnd = cache.get_seq_length();
932
+ }
933
+ }
934
+ }
935
+
936
+ export { DFlashRunner, Drafter, CFG };