diskformer 0.0.1 → 0.2.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.
- package/CHANGELOG.md +27 -0
- package/README.md +99 -23
- package/engines/LICENSE-APACHE +201 -0
- package/engines/NOTICE +17 -0
- package/engines/gemma4_moe_ssd.js +1291 -0
- package/engines/qwen35_moe_ssd.js +1231 -0
- package/engines/qwen3_moe_ssd.js +1035 -0
- package/index.js +8 -0
- package/package.json +40 -6
- package/src/expert-stream.js +265 -0
- package/src/gguf.js +96 -0
- package/src/ingest.js +177 -0
- package/src/measure.js +61 -0
- package/src/opfs-reader.js +258 -0
- package/src/rows.js +230 -0
|
@@ -0,0 +1,1291 @@
|
|
|
1
|
+
/* gemma4_moe_ssd.js — Gemma 4 26B-A4B (GGUF arch `gemma4`, MoE) with its routed experts on disk:
|
|
2
|
+
* rung 2c of the SSD-streaming ladder. Reuses rung 2a/2b's OPFS ingest, reader pool and expert streamer
|
|
3
|
+
* (qwen3_moe_ssd.js, moe-expert-stream.js); what differs is the file format and the trunk.
|
|
4
|
+
*
|
|
5
|
+
* The math follows llama.cpp b9830's src/models/gemma4.cpp (notes: LocalMind plan/gemma4-26b-a4b-port-notes.md;
|
|
6
|
+
* a numpy copy of it, scripts/gemma4-ref.py, gives per-layer states to compare against). Per layer:
|
|
7
|
+
* attention: RMS norm → Q/K/V (Q4_0) → per-head RMS norm on Q and K (weighted) and V (unweighted; full-attention
|
|
8
|
+
* layers have no V projection and reuse K's) → NeoX RoPE (sliding layers: 256-dim heads, base 1e4; full layers:
|
|
9
|
+
* 512-dim heads, base 1e6, `rope_freqs` leaves all but the first 64 pairs unrotated) → attention with scale 1.0,
|
|
10
|
+
* sliding layers over the last 1024 positions → output projection → post-norm → residual;
|
|
11
|
+
* FFN: a dense GELU MLP (2112) beside the MoE block (128 experts, top 8, GELU, a per-expert down scale), the router
|
|
12
|
+
* on rms_norm(x)·(1/√H)·scale; each branch post-normed, summed, normed again → residual → × layer_output_scale.
|
|
13
|
+
* Embedding: Q6_K rows × √H; head: the same Q6_K table, then a soft cap tanh(l/30)·30.
|
|
14
|
+
*
|
|
15
|
+
* File format (google/gemma-4-26B-A4B-it-qat-q4_0-gguf): matmul weights Q4_0, norms / router / scales F32,
|
|
16
|
+
* the token embedding (tied output head) Q6_K. Experts: fused `ffn_gate_up_exps` [H, 2F, E] (gate rows first)
|
|
17
|
+
* and `ffn_down_exps` [F, H, E], both Q4_0, plus `ffn_down_exps.scale` [E].
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { Qwen3MoeSsd, GGML, Q4_BLOCK, Q6K_BLOCK, tensorBytes, TOK, STORAGE, COPY_SRC, COPY_DST, MAP_READ } from './qwen3_moe_ssd.js';
|
|
21
|
+
import { OpfsReaderPool } from '../src/opfs-reader.js';
|
|
22
|
+
import { QWEN35_BATCH_KERNELS } from './qwen35_moe_ssd.js';
|
|
23
|
+
|
|
24
|
+
export function configFromGgufGemma4(kv) {
|
|
25
|
+
const a = kv['general.architecture'];
|
|
26
|
+
if (a !== 'gemma4') throw new Error(`expected a gemma4 GGUF, got ${a}`);
|
|
27
|
+
const g = (k) => kv[`${a}.${k}`];
|
|
28
|
+
const layers = g('block_count');
|
|
29
|
+
const perLayer = (v) => (Array.isArray(v) ? v : Array(layers).fill(v));
|
|
30
|
+
const swa = perLayer(g('attention.sliding_window_pattern')).map(Boolean);
|
|
31
|
+
if (!g('expert_count')) throw new Error('gemma4 GGUF without experts: this engine is for the MoE (26B-A4B)');
|
|
32
|
+
return {
|
|
33
|
+
arch: a, layers,
|
|
34
|
+
hidden: g('embedding_length'), denseFf: g('feed_forward_length'),
|
|
35
|
+
heads: g('attention.head_count'), kvHeads: perLayer(g('attention.head_count_kv')),
|
|
36
|
+
swa, // true: sliding-window layer
|
|
37
|
+
headDim: swa.map((s) => (s ? g('attention.key_length_swa') : g('attention.key_length'))),
|
|
38
|
+
ropeDims: swa.map((s) => (s ? g('rope.dimension_count_swa') : g('rope.dimension_count'))),
|
|
39
|
+
ropeTheta: swa.map((s) => (s ? g('rope.freq_base_swa') : g('rope.freq_base'))),
|
|
40
|
+
window: g('attention.sliding_window'),
|
|
41
|
+
experts: g('expert_count'), topK: g('expert_used_count'), expertFf: g('expert_feed_forward_length'),
|
|
42
|
+
softcap: g('final_logit_softcapping') || 0,
|
|
43
|
+
eps: g('attention.layer_norm_rms_epsilon'),
|
|
44
|
+
contextLength: g('context_length'),
|
|
45
|
+
vocab: kv['tokenizer.ggml.tokens'].length,
|
|
46
|
+
bos: kv['tokenizer.ggml.bos_token_id'], eos: kv['tokenizer.ggml.eos_token_id'],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// The OPFS layout from the header alone. One expert record = the fused gate/up planes then the down
|
|
51
|
+
// planes, each split into a nibble plane (16 B per 32 values) and an f16 scale plane (2 B per 32).
|
|
52
|
+
export function planLayoutGemma4(gguf) {
|
|
53
|
+
const cfg = configFromGgufGemma4(gguf.kv);
|
|
54
|
+
const H = cfg.hidden, F = cfg.expertFf;
|
|
55
|
+
const nib = (rows, cols) => rows * cols / 2, scl = (rows, cols) => rows * (cols / 32) * 2;
|
|
56
|
+
const parts = {
|
|
57
|
+
guQ: { off: 0, bytes: nib(2 * F, H) }, // gate rows 0..F-1, up rows F..2F-1
|
|
58
|
+
guS: { off: nib(2 * F, H), bytes: scl(2 * F, H) },
|
|
59
|
+
dQ: { off: nib(2 * F, H) + scl(2 * F, H), bytes: nib(H, F) },
|
|
60
|
+
dS: { off: nib(2 * F, H) + scl(2 * F, H) + nib(H, F), bytes: scl(H, F) },
|
|
61
|
+
};
|
|
62
|
+
const record = parts.dS.off + parts.dS.bytes;
|
|
63
|
+
const dense = {};
|
|
64
|
+
let off = 0;
|
|
65
|
+
const take = (n) => { const o = off; off += Math.ceil(n / 256) * 256; return o; };
|
|
66
|
+
for (const t of gguf.tensors) {
|
|
67
|
+
if (/_exps\.weight$/.test(t.name)) continue;
|
|
68
|
+
const n = t.dims.reduce((x, y) => x * y, 1);
|
|
69
|
+
if (t.type === GGML.Q4_0) {
|
|
70
|
+
dense[t.name] = { type: 'q4', dims: t.dims, q: { off: take(n / 2), bytes: n / 2 }, s: { off: take(n / 16), bytes: n / 16 } };
|
|
71
|
+
} else if (t.type === GGML.Q6_K) {
|
|
72
|
+
dense[t.name] = { type: 'q6k', dims: t.dims, raw: { off: take(tensorBytes(t)), bytes: tensorBytes(t) } };
|
|
73
|
+
} else if (t.type === GGML.F32) {
|
|
74
|
+
dense[t.name] = { type: 'f32', dims: t.dims, raw: { off: take(n * 4), bytes: n * 4 } };
|
|
75
|
+
} else throw new Error(`dense tensor ${t.name}: unsupported type ${t.type}`);
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
config: cfg,
|
|
79
|
+
experts: { file: 'experts.bin', record, parts, layers: cfg.layers, perLayer: cfg.experts, bytes: record * cfg.layers * cfg.experts },
|
|
80
|
+
dense: { file: 'dense.bin', bytes: off, tensors: dense },
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Byte ranges of the GGUF to keep, each small enough to transform in memory: one unit per expert slice
|
|
85
|
+
// (kind q4), row bands of dense Q4_0 tensors (kind q4), whole Q6_K / F32 tensors (raw).
|
|
86
|
+
export function planUnitsGemma4(gguf, layout) {
|
|
87
|
+
const units = [];
|
|
88
|
+
const { record, parts, perLayer } = layout.experts;
|
|
89
|
+
for (const t of gguf.tensors) {
|
|
90
|
+
const abs = gguf.dataStart + t.offset;
|
|
91
|
+
const m = /^blk\.(\d+)\.ffn_(gate_up|down)_exps\.weight$/.exec(t.name);
|
|
92
|
+
if (m) {
|
|
93
|
+
if (t.type !== GGML.Q4_0) throw new Error(`${t.name}: experts must be Q4_0`);
|
|
94
|
+
const layer = Number(m[1]), gu = m[2] === 'gate_up';
|
|
95
|
+
const slice = tensorBytes(t) / perLayer;
|
|
96
|
+
for (let e = 0; e < perLayer; e++) {
|
|
97
|
+
const rec = (layer * perLayer + e) * record;
|
|
98
|
+
units.push({ kind: 'q4', src: abs + e * slice, len: slice, file: 'experts', q: rec + (gu ? parts.guQ.off : parts.dQ.off), s: rec + (gu ? parts.guS.off : parts.dS.off) });
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const d = layout.dense.tensors[t.name];
|
|
103
|
+
if (d.type !== 'q4') { units.push({ src: abs, len: d.raw.bytes, file: 'dense', raw: d.raw.off }); continue; }
|
|
104
|
+
const cols = t.dims[0], rows = (d.q.bytes * 2) / cols, rowBytes = (cols / 32) * Q4_BLOCK;
|
|
105
|
+
const band = Math.max(1, Math.floor((8 << 20) / rowBytes));
|
|
106
|
+
for (let r = 0; r < rows; r += band) {
|
|
107
|
+
const nr = Math.min(band, rows - r);
|
|
108
|
+
units.push({ kind: 'q4', src: abs + r * rowBytes, len: nr * rowBytes, file: 'dense', q: d.q.off + r * cols / 2, s: d.s.off + r * (cols / 32) * 2 });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
units.sort((a, b) => a.src - b.src);
|
|
112
|
+
return units;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const GEMMA4_INGEST_PLAN = { layout: planLayoutGemma4, units: planUnitsGemma4 };
|
|
116
|
+
|
|
117
|
+
export const GEMMA4_26B_A4B = {
|
|
118
|
+
repo: 'google/gemma-4-26B-A4B-it-qat-q4_0-gguf',
|
|
119
|
+
file: 'gemma-4-26B_q4_0-it.gguf',
|
|
120
|
+
revision: 'd1c082be9cf3c8a514acf63b8761f4b41935842e',
|
|
121
|
+
sha256: '3eca3b8f6d7baf218a7dd6bba5fb59a56ee25fe2d567b6f5f589b4f697eca51d',
|
|
122
|
+
size: 14439363584,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// The GGUF's chat template (text turns): <bos>, a system turn when there is a system message or thinking is on
|
|
126
|
+
// (<|think|> first), then <|turn>user|model … <turn|> per message, and the model turn opened. With thinking off
|
|
127
|
+
// the model turn starts with an empty thought channel. Earlier model turns lose their thought channels.
|
|
128
|
+
const stripThinking = (t) => t.split('<channel|>').map((p) => (p.includes('<|channel>') ? p.split('<|channel>')[0] : p)).join('').trim();
|
|
129
|
+
export function gemmaChatPrompt(messages, { enableThinking = false } = {}) {
|
|
130
|
+
let s = '<bos>', msgs = messages;
|
|
131
|
+
const sys = msgs.length && msgs[0].role === 'system' ? String(msgs[0].content).trim() : null;
|
|
132
|
+
if (enableThinking || sys !== null) {
|
|
133
|
+
s += '<|turn>system\n' + (enableThinking ? '<|think|>\n' : '') + (sys ?? '') + '<turn|>\n';
|
|
134
|
+
if (sys !== null) msgs = msgs.slice(1);
|
|
135
|
+
}
|
|
136
|
+
for (const m of msgs) {
|
|
137
|
+
const role = m.role === 'assistant' ? 'model' : m.role;
|
|
138
|
+
s += `<|turn>${role}\n${role === 'model' ? stripThinking(String(m.content)) : String(m.content).trim()}<turn|>\n`;
|
|
139
|
+
}
|
|
140
|
+
return s + '<|turn>model\n' + (enableThinking ? '' : '<|channel>thought\n<channel|>');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Tokenizer: SentencePiece-style BPE (GGUF tokenizer.ggml.model = gemma4) ──
|
|
144
|
+
// As llama.cpp's LLAMA_VOCAB_PRE_TYPE_GEMMA4: spaces become ▁, the text splits only into runs of newlines and
|
|
145
|
+
// runs of everything else, merges run on raw UTF-8 characters in rank order (leftmost first on equal ranks),
|
|
146
|
+
// and a run made only of newlines that is itself a token stays whole. A character with no token falls back
|
|
147
|
+
// to <0xXX> byte tokens (SentencePiece byte fallback; llama.cpp drops it instead).
|
|
148
|
+
export class GemmaTokenizer {
|
|
149
|
+
constructor(kv) {
|
|
150
|
+
this.tokens = kv['tokenizer.ggml.tokens'];
|
|
151
|
+
const types = kv['tokenizer.ggml.token_type'] || [];
|
|
152
|
+
const n = this.tokens.length;
|
|
153
|
+
this.ids = new Map(this.tokens.map((t, i) => [t, i]));
|
|
154
|
+
this.ranks = new Map((kv['tokenizer.ggml.merges'] || []).map((m, i) => [m, i]));
|
|
155
|
+
this.isSpecial = new Uint8Array(n); this.byteOf = new Int16Array(n).fill(-1); this.byteTok = new Int32Array(256).fill(-1);
|
|
156
|
+
const special = [];
|
|
157
|
+
for (let i = 0; i < n; i++) {
|
|
158
|
+
if (types[i] === 3 || types[i] === 4) { special.push(this.tokens[i]); this.isSpecial[i] = 1; }
|
|
159
|
+
const m = types[i] === 6 && /^<0x([0-9A-Fa-f]{2})>$/.exec(this.tokens[i]);
|
|
160
|
+
if (m) { const b = parseInt(m[1], 16); this.byteOf[i] = b; this.byteTok[b] = i; }
|
|
161
|
+
}
|
|
162
|
+
special.sort((a, b) => b.length - a.length);
|
|
163
|
+
const esc = (t) => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
164
|
+
this.specialRe = special.length ? new RegExp(`(${special.map(esc).join('|')})`) : null;
|
|
165
|
+
this.utf8 = new TextEncoder();
|
|
166
|
+
}
|
|
167
|
+
encode(text, { parseSpecial = true } = {}) {
|
|
168
|
+
const out = [];
|
|
169
|
+
const pieces = parseSpecial && this.specialRe ? text.split(this.specialRe) : [text];
|
|
170
|
+
for (let i = 0; i < pieces.length; i++) {
|
|
171
|
+
const piece = pieces[i];
|
|
172
|
+
if (!piece) continue;
|
|
173
|
+
if (parseSpecial && i % 2 === 1) { out.push(this.ids.get(piece)); continue; }
|
|
174
|
+
for (const [run] of piece.replaceAll(' ', '▁').matchAll(/[^\n]+|\n+/g)) {
|
|
175
|
+
const whole = run[0] === '\n' ? this.ids.get(run) : undefined;
|
|
176
|
+
if (whole !== undefined) out.push(whole); else this.bpe(run, out);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
// Rank-ordered merges over a linked list of symbols, with a binary heap of candidate pairs.
|
|
182
|
+
bpe(word, out) {
|
|
183
|
+
const text = Array.from(word), n = text.length;
|
|
184
|
+
const prev = Int32Array.from({ length: n }, (_, i) => i - 1), next = Int32Array.from({ length: n }, (_, i) => (i + 1 < n ? i + 1 : -1));
|
|
185
|
+
const heap = [];
|
|
186
|
+
const less = (a, b) => a.rank < b.rank || (a.rank === b.rank && a.left < b.left);
|
|
187
|
+
const push = (e) => { heap.push(e); let i = heap.length - 1; while (i > 0) { const p = (i - 1) >> 1; if (!less(heap[i], heap[p])) break; [heap[i], heap[p]] = [heap[p], heap[i]]; i = p; } };
|
|
188
|
+
const pop = () => {
|
|
189
|
+
const top = heap[0], last = heap.pop();
|
|
190
|
+
if (heap.length) { heap[0] = last; let i = 0; for (;;) { const l = 2 * i + 1, r = l + 1; let m = i; if (l < heap.length && less(heap[l], heap[m])) m = l; if (r < heap.length && less(heap[r], heap[m])) m = r; if (m === i) break; [heap[i], heap[m]] = [heap[m], heap[i]]; i = m; } }
|
|
191
|
+
return top;
|
|
192
|
+
};
|
|
193
|
+
const pair = (left, right) => {
|
|
194
|
+
if (left < 0 || right < 0) return;
|
|
195
|
+
const t = text[left] + text[right], rank = this.ranks.get(text[left] + ' ' + text[right]);
|
|
196
|
+
if (rank !== undefined) push({ rank, left, right, t });
|
|
197
|
+
};
|
|
198
|
+
for (let i = 1; i < n; i++) pair(i - 1, i);
|
|
199
|
+
while (heap.length) {
|
|
200
|
+
const b = pop();
|
|
201
|
+
if (!text[b.left] || !text[b.right] || text[b.left] + text[b.right] !== b.t) continue;
|
|
202
|
+
text[b.left] = b.t; text[b.right] = '';
|
|
203
|
+
next[b.left] = next[b.right];
|
|
204
|
+
if (next[b.right] >= 0) prev[next[b.right]] = b.left;
|
|
205
|
+
pair(prev[b.left], b.left);
|
|
206
|
+
pair(b.left, next[b.left]);
|
|
207
|
+
}
|
|
208
|
+
for (let i = 0; i >= 0 && i < n; i = next[i]) {
|
|
209
|
+
if (!text[i]) continue;
|
|
210
|
+
const id = this.ids.get(text[i]);
|
|
211
|
+
if (id !== undefined) { out.push(id); continue; }
|
|
212
|
+
for (const byte of this.utf8.encode(text[i])) if (this.byteTok[byte] >= 0) out.push(this.byteTok[byte]);
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
tokenBytes(id) {
|
|
217
|
+
if (this.byteOf[id] >= 0) return Uint8Array.of(this.byteOf[id]);
|
|
218
|
+
const t = this.tokens[id];
|
|
219
|
+
return this.utf8.encode(this.isSpecial[id] ? t : t.replaceAll('▁', ' '));
|
|
220
|
+
}
|
|
221
|
+
decode(ids) {
|
|
222
|
+
const chunks = ids.map((id) => this.tokenBytes(id));
|
|
223
|
+
const all = new Uint8Array(chunks.reduce((a, c) => a + c.length, 0));
|
|
224
|
+
let o = 0; for (const c of chunks) { all.set(c, o); o += c.length; }
|
|
225
|
+
return new TextDecoder('utf-8', { ignoreBOM: true }).decode(all);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ── WGSL ────────────────────────────────────────────────────────────────────
|
|
230
|
+
// Q4_0 as two planes (ingest's splitQ4): nibbles, 16 bytes per 32 values (byte j holds values j and j + 16),
|
|
231
|
+
// and one f16 scale per 32. Value = (nibble − 8) · scale, as ggml dequantizes it. Each thread reads one u32 of
|
|
232
|
+
// nibbles: values 4m..4m+3 (low nibbles) and 16+4m..16+4m+3 (high nibbles) of block b.
|
|
233
|
+
const Q4DOT = `
|
|
234
|
+
fn q4dot(wd: u32, d: f32, xb: u32) -> f32 {
|
|
235
|
+
let lo = (f32(wd & 15u) - 8.0) * xin[xb] + (f32((wd >> 8u) & 15u) - 8.0) * xin[xb + 1u]
|
|
236
|
+
+ (f32((wd >> 16u) & 15u) - 8.0) * xin[xb + 2u] + (f32((wd >> 24u) & 15u) - 8.0) * xin[xb + 3u];
|
|
237
|
+
let hi = (f32((wd >> 4u) & 15u) - 8.0) * xin[xb + 16u] + (f32((wd >> 12u) & 15u) - 8.0) * xin[xb + 17u]
|
|
238
|
+
+ (f32((wd >> 20u) & 15u) - 8.0) * xin[xb + 18u] + (f32((wd >> 28u) & 15u) - 8.0) * xin[xb + 19u];
|
|
239
|
+
return d * (lo + hi);
|
|
240
|
+
}`;
|
|
241
|
+
// Q6_K (ggml block_q6_K: ql[128], qh[64], int8 scales[16], f16 d per 256 values). The GGUF's 210-byte blocks
|
|
242
|
+
// sit at 2-byte alignment, so at load q6kPlanes splits the table on the GPU into aligned planes: ql (32 words
|
|
243
|
+
// per block), qh (16), scales (4) and d as f32. q6v reads value t of block bi exactly as dequantize_row_q6_K
|
|
244
|
+
// forms it: (d · scale) · (q − 32).
|
|
245
|
+
const Q6P = `
|
|
246
|
+
@group(0) @binding(0) var<storage, read> ql: array<u32>;
|
|
247
|
+
@group(0) @binding(1) var<storage, read> qh: array<u32>;
|
|
248
|
+
@group(0) @binding(2) var<storage, read> sc: array<u32>;
|
|
249
|
+
@group(0) @binding(3) var<storage, read> dd: array<f32>;
|
|
250
|
+
fn sc8(bi: u32, si: u32) -> f32 { let s8 = (sc[bi * 4u + (si >> 2u)] >> ((si & 3u) * 8u)) & 0xffu; return f32(select(i32(s8), i32(s8) - 256, s8 >= 128u)); }
|
|
251
|
+
fn q6v(bi: u32, t: u32) -> f32 {
|
|
252
|
+
let n = t >> 7u; let r = t & 127u; let qd = r >> 5u; let l = r & 31u;
|
|
253
|
+
let qb = n * 64u + l + (qd & 1u) * 32u;
|
|
254
|
+
let lo = (ql[bi * 32u + (qb >> 2u)] >> ((qb & 3u) * 8u)) & 0xffu;
|
|
255
|
+
let q4 = select(lo & 15u, lo >> 4u, qd >= 2u);
|
|
256
|
+
let hb = n * 32u + l;
|
|
257
|
+
let h = (qh[bi * 16u + (hb >> 2u)] >> ((hb & 3u) * 8u + qd * 2u)) & 3u;
|
|
258
|
+
return dd[bi] * sc8(bi, n * 8u + (l >> 4u) + qd * 2u) * f32(i32(q4 | (h << 4u)) - 32);
|
|
259
|
+
}`;
|
|
260
|
+
// tanh with its argument clamped: tanh(±10) is ±1 in f32, and a naive exp-based tanh would give NaN past ~44.
|
|
261
|
+
const TANH = 'fn tanhc(a: f32) -> f32 { return tanh(clamp(a, -10.0, 10.0)); }';
|
|
262
|
+
const GELU = `${TANH}
|
|
263
|
+
fn gelu(x: f32) -> f32 { return 0.5 * x * (1.0 + tanhc(0.7978845608 * x * (1.0 + 0.044715 * x * x))); }`;
|
|
264
|
+
// The window: sliding layers attend to the last `win` positions (llama.cpp masks p_q − p_k ≥ win).
|
|
265
|
+
const WIN = 'fn winStart(n: u32, win: u32) -> u32 { return select(0u, n - win, win > 0u && n > win); }';
|
|
266
|
+
|
|
267
|
+
const G4 = {
|
|
268
|
+
// The GGUF's Q6_K bytes (raw) → aligned planes; one thread per block.
|
|
269
|
+
q6kPlanes: `
|
|
270
|
+
struct P { blocks: u32 }
|
|
271
|
+
@group(0) @binding(0) var<storage, read> raw: array<u32>;
|
|
272
|
+
@group(0) @binding(1) var<storage, read_write> ql: array<u32>;
|
|
273
|
+
@group(0) @binding(2) var<storage, read_write> qh: array<u32>;
|
|
274
|
+
@group(0) @binding(3) var<storage, read_write> sc: array<u32>;
|
|
275
|
+
@group(0) @binding(4) var<storage, read_write> dd: array<f32>;
|
|
276
|
+
@group(0) @binding(5) var<uniform> p: P;
|
|
277
|
+
fn byteAt(i: u32) -> u32 { return (raw[i >> 2u] >> ((i & 3u) * 8u)) & 0xffu; }
|
|
278
|
+
fn wordAt(i: u32) -> u32 { return byteAt(i) | (byteAt(i + 1u) << 8u) | (byteAt(i + 2u) << 16u) | (byteAt(i + 3u) << 24u); }
|
|
279
|
+
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>, @builtin(num_workgroups) ng: vec3<u32>) {
|
|
280
|
+
let b = g.y * ng.x * 256u + g.x; if (b >= p.blocks) { return; }
|
|
281
|
+
let o = b * 210u;
|
|
282
|
+
for (var i = 0u; i < 32u; i++) { ql[b * 32u + i] = wordAt(o + 4u * i); }
|
|
283
|
+
for (var i = 0u; i < 16u; i++) { qh[b * 16u + i] = wordAt(o + 128u + 4u * i); }
|
|
284
|
+
for (var i = 0u; i < 4u; i++) { sc[b * 4u + i] = wordAt(o + 192u + 4u * i); }
|
|
285
|
+
dd[b] = unpack2x16float(byteAt(o + 208u) | (byteAt(o + 209u) << 8u)).x;
|
|
286
|
+
}`,
|
|
287
|
+
embedQ6K: `
|
|
288
|
+
${TOK}
|
|
289
|
+
struct P { n: u32, nb: u32, scale: f32 }
|
|
290
|
+
${Q6P}
|
|
291
|
+
@group(0) @binding(4) var<storage, read_write> y: array<f32>;
|
|
292
|
+
@group(0) @binding(5) var<uniform> tok: Tok;
|
|
293
|
+
@group(0) @binding(6) var<uniform> p: P;
|
|
294
|
+
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
295
|
+
let i = g.x; if (i >= p.n) { return; }
|
|
296
|
+
y[i] = q6v(tok.token * p.nb + (i >> 8u), i & 255u) * p.scale;
|
|
297
|
+
}`,
|
|
298
|
+
// Logits over the Q6_K planes: 4 rows per 256-thread workgroup, 64 threads per row; thread j takes quad j
|
|
299
|
+
// (4 consecutive values: one ql word, one qh word, one scale) of every block of the row.
|
|
300
|
+
matmulQ6K: `
|
|
301
|
+
struct P { M: u32, N: u32 }
|
|
302
|
+
${Q6P}
|
|
303
|
+
@group(0) @binding(4) var<storage, read> x: array<vec4<f32>>;
|
|
304
|
+
@group(0) @binding(5) var<storage, read_write> y: array<f32>;
|
|
305
|
+
@group(0) @binding(6) var<uniform> p: P;
|
|
306
|
+
var<workgroup> part: array<f32, 256>;
|
|
307
|
+
@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>) {
|
|
308
|
+
let j = l.x & 63u; let m = (wg.y * ng.x + wg.x) * 4u + (l.x >> 6u); let mr = min(m, p.M - 1u);
|
|
309
|
+
let nb = p.N / 256u; let n = j >> 5u; let w = j & 31u; let qd = w >> 3u; let k = w & 7u;
|
|
310
|
+
var acc = 0.0;
|
|
311
|
+
for (var bb = 0u; bb < nb; bb++) {
|
|
312
|
+
let bi = mr * nb + bb;
|
|
313
|
+
let lo = unpack4xU8(ql[bi * 32u + n * 16u + (qd & 1u) * 8u + k]);
|
|
314
|
+
let q4 = select(lo & vec4<u32>(15u), lo >> vec4<u32>(4u), qd >= 2u);
|
|
315
|
+
let h = (unpack4xU8(qh[bi * 16u + n * 8u + k]) >> vec4<u32>(qd * 2u)) & vec4<u32>(3u);
|
|
316
|
+
let q = vec4<f32>(vec4<i32>(q4 | (h << vec4<u32>(4u))) - vec4<i32>(32));
|
|
317
|
+
acc += (dd[bi] * sc8(bi, n * 8u + (k >> 2u) + qd * 2u)) * dot(q, x[bb * 64u + j]);
|
|
318
|
+
}
|
|
319
|
+
part[l.x] = acc; workgroupBarrier();
|
|
320
|
+
for (var s = 32u; s > 0u; s >>= 1u) { if (j < s) { part[l.x] += part[l.x + s]; } workgroupBarrier(); }
|
|
321
|
+
if (j == 0u && m < p.M) { y[m] = part[l.x]; }
|
|
322
|
+
}`,
|
|
323
|
+
// Dense Q4_0 GEMV, 4 output rows per workgroup (2-D dispatch ok).
|
|
324
|
+
matmulQ4: `enable f16;
|
|
325
|
+
struct P { M: u32, N: u32 }
|
|
326
|
+
@group(0) @binding(0) var<storage, read> xin: array<f32>;
|
|
327
|
+
@group(0) @binding(1) var<storage, read> qw: array<u32>;
|
|
328
|
+
@group(0) @binding(2) var<storage, read> qs: array<f16>;
|
|
329
|
+
@group(0) @binding(3) var<storage, read_write> y: array<f32>;
|
|
330
|
+
@group(0) @binding(4) var<uniform> p: P;
|
|
331
|
+
${Q4DOT}
|
|
332
|
+
var<workgroup> part: array<f32, 1024>;
|
|
333
|
+
@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>) {
|
|
334
|
+
let m0 = (wg.y * ng.x + wg.x) * 4u; let t = l.x; let M = p.M; let nw = p.N / 8u; let nb = p.N / 32u;
|
|
335
|
+
if (m0 >= M) { return; }
|
|
336
|
+
let r0 = min(m0, M - 1u); let r1 = min(m0 + 1u, M - 1u); let r2 = min(m0 + 2u, M - 1u); let r3 = min(m0 + 3u, M - 1u);
|
|
337
|
+
var a0 = 0.0; var a1 = 0.0; var a2 = 0.0; var a3 = 0.0;
|
|
338
|
+
for (var w = t; w < nw; w += 256u) {
|
|
339
|
+
let b = w >> 2u; let xb = b * 32u + (w & 3u) * 4u;
|
|
340
|
+
a0 += q4dot(qw[r0 * nw + w], f32(qs[r0 * nb + b]), xb);
|
|
341
|
+
a1 += q4dot(qw[r1 * nw + w], f32(qs[r1 * nb + b]), xb);
|
|
342
|
+
a2 += q4dot(qw[r2 * nw + w], f32(qs[r2 * nb + b]), xb);
|
|
343
|
+
a3 += q4dot(qw[r3 * nw + w], f32(qs[r3 * nb + b]), xb);
|
|
344
|
+
}
|
|
345
|
+
part[t] = a0; part[256u + t] = a1; part[512u + t] = a2; part[768u + t] = a3;
|
|
346
|
+
workgroupBarrier();
|
|
347
|
+
for (var s = 128u; s > 0u; s >>= 1u) {
|
|
348
|
+
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]; }
|
|
349
|
+
workgroupBarrier();
|
|
350
|
+
}
|
|
351
|
+
if (t < 4u && m0 + t < M) { y[m0 + t] = part[t * 256u]; }
|
|
352
|
+
}`,
|
|
353
|
+
// Routed experts, all k at once (workgroup z = top-k position); slots[z] picks the expert's rows in the pool.
|
|
354
|
+
expertQ4: `enable f16;
|
|
355
|
+
struct P { M: u32, N: u32, rowsPerExpert: u32, inputPerSlot: u32 }
|
|
356
|
+
@group(0) @binding(0) var<storage, read> xin: array<f32>;
|
|
357
|
+
@group(0) @binding(1) var<storage, read> qw: array<u32>;
|
|
358
|
+
@group(0) @binding(2) var<storage, read> qs: array<f16>;
|
|
359
|
+
@group(0) @binding(3) var<storage, read_write> y: array<f32>;
|
|
360
|
+
@group(0) @binding(4) var<uniform> p: P;
|
|
361
|
+
@group(0) @binding(5) var<storage, read> slots: array<u32>;
|
|
362
|
+
${Q4DOT}
|
|
363
|
+
var<workgroup> part: array<f32, 1024>;
|
|
364
|
+
@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>) {
|
|
365
|
+
let z = wg.z; let m0 = (wg.y * ng.x + wg.x) * 4u; let t = l.x; let M = p.M; let nw = p.N / 8u; let nb = p.N / 32u;
|
|
366
|
+
if (m0 >= M) { return; }
|
|
367
|
+
let rb = slots[z] * p.rowsPerExpert;
|
|
368
|
+
let xo = select(0u, z * p.N, p.inputPerSlot == 1u);
|
|
369
|
+
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);
|
|
370
|
+
var a0 = 0.0; var a1 = 0.0; var a2 = 0.0; var a3 = 0.0;
|
|
371
|
+
for (var w = t; w < nw; w += 256u) {
|
|
372
|
+
let b = w >> 2u; let xb = xo + b * 32u + (w & 3u) * 4u;
|
|
373
|
+
a0 += q4dot(qw[r0 * nw + w], f32(qs[r0 * nb + b]), xb);
|
|
374
|
+
a1 += q4dot(qw[r1 * nw + w], f32(qs[r1 * nb + b]), xb);
|
|
375
|
+
a2 += q4dot(qw[r2 * nw + w], f32(qs[r2 * nb + b]), xb);
|
|
376
|
+
a3 += q4dot(qw[r3 * nw + w], f32(qs[r3 * nb + b]), xb);
|
|
377
|
+
}
|
|
378
|
+
part[t] = a0; part[256u + t] = a1; part[512u + t] = a2; part[768u + t] = a3;
|
|
379
|
+
workgroupBarrier();
|
|
380
|
+
for (var s = 128u; s > 0u; s >>= 1u) {
|
|
381
|
+
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]; }
|
|
382
|
+
workgroupBarrier();
|
|
383
|
+
}
|
|
384
|
+
if (t < 4u && m0 + t < M) { y[z * M + m0 + t] = part[t * 256u]; }
|
|
385
|
+
}`,
|
|
386
|
+
// gu = k × [gate F | up F] → act = k × F of GELU(gate)·up (ggml_geglu_split).
|
|
387
|
+
geglu: `
|
|
388
|
+
struct P { F: u32, k: u32 }
|
|
389
|
+
@group(0) @binding(0) var<storage, read> gu: array<f32>;
|
|
390
|
+
@group(0) @binding(1) var<storage, read_write> act: array<f32>;
|
|
391
|
+
@group(0) @binding(2) var<uniform> p: P;
|
|
392
|
+
${GELU}
|
|
393
|
+
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
394
|
+
let i = g.x; if (i >= p.k * p.F) { return; }
|
|
395
|
+
let z = i / p.F; let j = i % p.F;
|
|
396
|
+
act[i] = gelu(gu[z * 2u * p.F + j]) * gu[z * 2u * p.F + p.F + j];
|
|
397
|
+
}`,
|
|
398
|
+
// Per-head RMS norm without a weight (V), from x into y. One workgroup per head.
|
|
399
|
+
vNorm: `
|
|
400
|
+
struct P { hd: u32, eps: f32 }
|
|
401
|
+
@group(0) @binding(0) var<storage, read> x: array<f32>;
|
|
402
|
+
@group(0) @binding(1) var<storage, read_write> y: array<f32>;
|
|
403
|
+
@group(0) @binding(2) var<uniform> p: P;
|
|
404
|
+
var<workgroup> red: array<f32, 64>;
|
|
405
|
+
@compute @workgroup_size(64) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
|
|
406
|
+
let base = wg.x * p.hd; let t = l.x;
|
|
407
|
+
var acc = 0.0;
|
|
408
|
+
for (var i = t; i < p.hd; i += 64u) { let v = x[base + i]; acc += v * v; }
|
|
409
|
+
red[t] = acc; workgroupBarrier();
|
|
410
|
+
for (var s = 32u; s > 0u; s >>= 1u) { if (t < s) { red[t] += red[t + s]; } workgroupBarrier(); }
|
|
411
|
+
let scale = 1.0 / sqrt(red[0] / f32(p.hd) + p.eps);
|
|
412
|
+
for (var i = t; i < p.hd; i += 64u) { y[base + i] = x[base + i] * scale; }
|
|
413
|
+
}`,
|
|
414
|
+
// y = ((x · rms) · k) · w — the router's input (k = 1/√H, w = ffn_gate_inp.scale).
|
|
415
|
+
rmsnormK: `
|
|
416
|
+
struct P { n: u32, eps: f32, k: f32 }
|
|
417
|
+
@group(0) @binding(0) var<storage, read> x: array<f32>;
|
|
418
|
+
@group(0) @binding(1) var<storage, read> w: array<f32>;
|
|
419
|
+
@group(0) @binding(2) var<storage, read_write> y: array<f32>;
|
|
420
|
+
@group(0) @binding(3) var<uniform> p: P;
|
|
421
|
+
var<workgroup> red: array<f32, 256>;
|
|
422
|
+
@compute @workgroup_size(256) fn main(@builtin(local_invocation_id) l: vec3<u32>) {
|
|
423
|
+
let t = l.x; var acc = 0.0;
|
|
424
|
+
for (var i = t; i < p.n; i += 256u) { let v = x[i]; acc += v * v; }
|
|
425
|
+
red[t] = acc; workgroupBarrier();
|
|
426
|
+
for (var s = 128u; s > 0u; s >>= 1u) { if (t < s) { red[t] += red[t + s]; } workgroupBarrier(); }
|
|
427
|
+
let scale = 1.0 / sqrt(red[0] / f32(p.n) + p.eps);
|
|
428
|
+
for (var i = t; i < p.n; i += 256u) { y[i] = ((x[i] * scale) * p.k) * w[i]; }
|
|
429
|
+
}`,
|
|
430
|
+
// x += rms_norm(y) · w — the post-attention norm and the residual add.
|
|
431
|
+
rmsnormAdd: `
|
|
432
|
+
struct P { n: u32, eps: f32 }
|
|
433
|
+
@group(0) @binding(0) var<storage, read> y: array<f32>;
|
|
434
|
+
@group(0) @binding(1) var<storage, read> w: array<f32>;
|
|
435
|
+
@group(0) @binding(2) var<storage, read_write> x: array<f32>;
|
|
436
|
+
@group(0) @binding(3) var<uniform> p: P;
|
|
437
|
+
var<workgroup> red: array<f32, 256>;
|
|
438
|
+
@compute @workgroup_size(256) fn main(@builtin(local_invocation_id) l: vec3<u32>) {
|
|
439
|
+
let t = l.x; var acc = 0.0;
|
|
440
|
+
for (var i = t; i < p.n; i += 256u) { let v = y[i]; acc += v * v; }
|
|
441
|
+
red[t] = acc; workgroupBarrier();
|
|
442
|
+
for (var s = 128u; s > 0u; s >>= 1u) { if (t < s) { red[t] += red[t + s]; } workgroupBarrier(); }
|
|
443
|
+
let scale = 1.0 / sqrt(red[0] / f32(p.n) + p.eps);
|
|
444
|
+
for (var i = t; i < p.n; i += 256u) { x[i] = (y[i] * scale) * w[i] + x[i]; }
|
|
445
|
+
}`,
|
|
446
|
+
// x = (rms_norm(mlp + moe) · w + x) · out_scale — the FFN's last norm, the residual add, the layer scalar.
|
|
447
|
+
postFfn: `
|
|
448
|
+
struct P { n: u32, eps: f32 }
|
|
449
|
+
@group(0) @binding(0) var<storage, read> mlp: array<f32>;
|
|
450
|
+
@group(0) @binding(1) var<storage, read> moe: array<f32>;
|
|
451
|
+
@group(0) @binding(2) var<storage, read> w: array<f32>;
|
|
452
|
+
@group(0) @binding(3) var<storage, read> os: array<f32>;
|
|
453
|
+
@group(0) @binding(4) var<storage, read_write> x: array<f32>;
|
|
454
|
+
@group(0) @binding(5) var<uniform> p: P;
|
|
455
|
+
var<workgroup> red: array<f32, 256>;
|
|
456
|
+
@compute @workgroup_size(256) fn main(@builtin(local_invocation_id) l: vec3<u32>) {
|
|
457
|
+
let t = l.x; var acc = 0.0;
|
|
458
|
+
for (var i = t; i < p.n; i += 256u) { let v = mlp[i] + moe[i]; acc += v * v; }
|
|
459
|
+
red[t] = acc; workgroupBarrier();
|
|
460
|
+
for (var s = 128u; s > 0u; s >>= 1u) { if (t < s) { red[t] += red[t + s]; } workgroupBarrier(); }
|
|
461
|
+
let scale = 1.0 / sqrt(red[0] / f32(p.n) + p.eps);
|
|
462
|
+
let k = os[0];
|
|
463
|
+
for (var i = t; i < p.n; i += 256u) { x[i] = (((mlp[i] + moe[i]) * scale) * w[i] + x[i]) * k; }
|
|
464
|
+
}`,
|
|
465
|
+
// moe[h] = Σ_j (dn[j·H + h] · ds[id_j]) · w_j, added in pick order (llama.cpp: down · scale · weight, then e0 + e1 + …).
|
|
466
|
+
moeSum: `
|
|
467
|
+
struct P { H: u32, k: u32 }
|
|
468
|
+
@group(0) @binding(0) var<storage, read> dn: array<f32>;
|
|
469
|
+
@group(0) @binding(1) var<storage, read> sel: array<u32>;
|
|
470
|
+
@group(0) @binding(2) var<storage, read> ds: array<f32>;
|
|
471
|
+
@group(0) @binding(3) var<storage, read_write> moe: array<f32>;
|
|
472
|
+
@group(0) @binding(4) var<uniform> p: P;
|
|
473
|
+
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
474
|
+
let h = g.x; if (h >= p.H) { return; }
|
|
475
|
+
var acc = (dn[h] * ds[sel[0]]) * bitcast<f32>(sel[p.k]);
|
|
476
|
+
for (var j = 1u; j < p.k; j++) { acc = acc + (dn[j * p.H + h] * ds[sel[j]]) * bitcast<f32>(sel[p.k + j]); }
|
|
477
|
+
moe[h] = acc;
|
|
478
|
+
}`,
|
|
479
|
+
// Attention with a window: scores, softmax and the weighted sum over positions [start, seqLen); scale 1.0.
|
|
480
|
+
attnScoreW: `enable f16;
|
|
481
|
+
${TOK}
|
|
482
|
+
struct P { heads: u32, kvHeads: u32, hd: u32, win: u32 }
|
|
483
|
+
@group(0) @binding(0) var<storage, read> q: array<f32>;
|
|
484
|
+
@group(0) @binding(1) var<storage, read> kc: array<f16>;
|
|
485
|
+
@group(0) @binding(2) var<storage, read_write> sc: array<f32>;
|
|
486
|
+
@group(0) @binding(3) var<uniform> tok: Tok;
|
|
487
|
+
@group(0) @binding(4) var<uniform> p: P;
|
|
488
|
+
${WIN}
|
|
489
|
+
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
490
|
+
let st = winStart(tok.seqLen, p.win); let n = tok.seqLen - st; let idx = g.x;
|
|
491
|
+
if (idx >= p.heads * n) { return; }
|
|
492
|
+
let h = idx / n; let j = idx % n; let kvh = h / (p.heads / p.kvHeads);
|
|
493
|
+
let qo = h * p.hd; let ko = (st + j) * p.kvHeads * p.hd + kvh * p.hd;
|
|
494
|
+
var acc = 0.0;
|
|
495
|
+
for (var d = 0u; d < p.hd; d++) { acc += q[qo + d] * f32(kc[ko + d]); }
|
|
496
|
+
sc[h * n + j] = acc;
|
|
497
|
+
}`,
|
|
498
|
+
softmaxW: `
|
|
499
|
+
${TOK}
|
|
500
|
+
struct P { heads: u32, kvHeads: u32, hd: u32, win: u32 }
|
|
501
|
+
@group(0) @binding(0) var<storage, read_write> sc: array<f32>;
|
|
502
|
+
@group(0) @binding(1) var<uniform> tok: Tok;
|
|
503
|
+
@group(0) @binding(2) var<uniform> p: P;
|
|
504
|
+
${WIN}
|
|
505
|
+
var<workgroup> red: array<f32, 256>;
|
|
506
|
+
@compute @workgroup_size(256) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
|
|
507
|
+
let n = tok.seqLen - winStart(tok.seqLen, p.win); let base = wg.x * n; let t = l.x;
|
|
508
|
+
var mx = -3.4e38;
|
|
509
|
+
for (var i = t; i < n; i += 256u) { mx = max(mx, sc[base + i]); }
|
|
510
|
+
red[t] = mx; workgroupBarrier();
|
|
511
|
+
for (var s = 128u; s > 0u; s >>= 1u) { if (t < s) { red[t] = max(red[t], red[t + s]); } workgroupBarrier(); }
|
|
512
|
+
let m = red[0]; workgroupBarrier();
|
|
513
|
+
var sum = 0.0;
|
|
514
|
+
for (var i = t; i < n; i += 256u) { let e = exp(sc[base + i] - m); sc[base + i] = e; sum += e; }
|
|
515
|
+
red[t] = sum; workgroupBarrier();
|
|
516
|
+
for (var s = 128u; s > 0u; s >>= 1u) { if (t < s) { red[t] += red[t + s]; } workgroupBarrier(); }
|
|
517
|
+
let inv = 1.0 / red[0];
|
|
518
|
+
for (var i = t; i < n; i += 256u) { sc[base + i] = sc[base + i] * inv; }
|
|
519
|
+
}`,
|
|
520
|
+
attnOutW: `enable f16;
|
|
521
|
+
${TOK}
|
|
522
|
+
struct P { heads: u32, kvHeads: u32, hd: u32, win: u32 }
|
|
523
|
+
@group(0) @binding(0) var<storage, read> pr: array<f32>;
|
|
524
|
+
@group(0) @binding(1) var<storage, read> vc: array<f16>;
|
|
525
|
+
@group(0) @binding(2) var<storage, read_write> y: array<f32>;
|
|
526
|
+
@group(0) @binding(3) var<uniform> tok: Tok;
|
|
527
|
+
@group(0) @binding(4) var<uniform> p: P;
|
|
528
|
+
${WIN}
|
|
529
|
+
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
530
|
+
let idx = g.x; if (idx >= p.heads * p.hd) { return; }
|
|
531
|
+
let st = winStart(tok.seqLen, p.win); let n = tok.seqLen - st;
|
|
532
|
+
let h = idx / p.hd; let d = idx % p.hd; let kvh = h / (p.heads / p.kvHeads);
|
|
533
|
+
let stride = p.kvHeads * p.hd;
|
|
534
|
+
var acc = 0.0;
|
|
535
|
+
for (var j = 0u; j < n; j++) { acc += pr[h * n + j] * f32(vc[(st + j) * stride + kvh * p.hd + d]); }
|
|
536
|
+
y[idx] = acc;
|
|
537
|
+
}`,
|
|
538
|
+
// Final logit soft cap, in place: l = tanh(l · (1/cap)) · cap.
|
|
539
|
+
softcap: `
|
|
540
|
+
struct P { n: u32, inv: f32, cap: f32 }
|
|
541
|
+
@group(0) @binding(0) var<storage, read_write> lg: array<f32>;
|
|
542
|
+
@group(0) @binding(1) var<uniform> p: P;
|
|
543
|
+
${TANH}
|
|
544
|
+
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>, @builtin(num_workgroups) ng: vec3<u32>) {
|
|
545
|
+
let i = g.y * ng.x * 256u + g.x; if (i >= p.n) { return; }
|
|
546
|
+
lg[i] = tanhc(lg[i] * p.inv) * p.cap;
|
|
547
|
+
}`,
|
|
548
|
+
// Debug capture: y[off + i] = x[i].
|
|
549
|
+
copyAt: `
|
|
550
|
+
struct P { n: u32, off: u32 }
|
|
551
|
+
@group(0) @binding(0) var<storage, read> x: array<f32>;
|
|
552
|
+
@group(0) @binding(1) var<storage, read_write> y: array<f32>;
|
|
553
|
+
@group(0) @binding(2) var<uniform> p: P;
|
|
554
|
+
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
555
|
+
let i = g.x; if (i < p.n) { y[p.off + i] = x[i]; }
|
|
556
|
+
}`,
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
// ── Subgroup Q4_0 GEMVs (decode) ─────────────────────────────────────────────────
|
|
560
|
+
// One 32-lane subgroup per output row, 4 rows per 128-thread workgroup (the scalar kernels' dispatch
|
|
561
|
+
// geometry). A lane takes whole blocks: one vec4<u32> of nibbles, unpack4xU8 of the low and high nibble
|
|
562
|
+
// masks, vec4 dot products against the activations, the block scale once; subgroupAdd reduces the row.
|
|
563
|
+
// Used only when the adapter's subgroups are exactly 32 wide.
|
|
564
|
+
const Q4BLK = `
|
|
565
|
+
fn q4blk(w: vec4<u32>, xb: u32) -> f32 {
|
|
566
|
+
let m = 0x0f0f0f0fu;
|
|
567
|
+
return dot(vec4<f32>(unpack4xU8(w.x & m)) - 8.0, x[xb]) + dot(vec4<f32>(unpack4xU8((w.x >> 4u) & m)) - 8.0, x[xb + 4u])
|
|
568
|
+
+ dot(vec4<f32>(unpack4xU8(w.y & m)) - 8.0, x[xb + 1u]) + dot(vec4<f32>(unpack4xU8((w.y >> 4u) & m)) - 8.0, x[xb + 5u])
|
|
569
|
+
+ dot(vec4<f32>(unpack4xU8(w.z & m)) - 8.0, x[xb + 2u]) + dot(vec4<f32>(unpack4xU8((w.z >> 4u) & m)) - 8.0, x[xb + 6u])
|
|
570
|
+
+ dot(vec4<f32>(unpack4xU8(w.w & m)) - 8.0, x[xb + 3u]) + dot(vec4<f32>(unpack4xU8((w.w >> 4u) & m)) - 8.0, x[xb + 7u]);
|
|
571
|
+
}`;
|
|
572
|
+
const G4SG = {
|
|
573
|
+
matmulQ4: `enable f16;
|
|
574
|
+
enable subgroups;
|
|
575
|
+
struct P { M: u32, N: u32 }
|
|
576
|
+
@group(0) @binding(0) var<storage, read> x: array<vec4<f32>>;
|
|
577
|
+
@group(0) @binding(1) var<storage, read> qw: array<vec4<u32>>;
|
|
578
|
+
@group(0) @binding(2) var<storage, read> qs: array<f16>;
|
|
579
|
+
@group(0) @binding(3) var<storage, read_write> y: array<f32>;
|
|
580
|
+
@group(0) @binding(4) var<uniform> p: P;
|
|
581
|
+
${Q4BLK}
|
|
582
|
+
@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>) {
|
|
583
|
+
let m = (wg.y * ng.x + wg.x) * 4u + l.x / 32u; let lane = l.x % 32u;
|
|
584
|
+
let nb = p.N / 32u; let rb = min(m, p.M - 1u) * nb;
|
|
585
|
+
var acc = 0.0;
|
|
586
|
+
for (var b = lane; b < nb; b += 32u) { acc += q4blk(qw[rb + b], b * 8u) * f32(qs[rb + b]); }
|
|
587
|
+
let total = subgroupAdd(acc);
|
|
588
|
+
if (lane == 0u && m < p.M) { y[m] = total; }
|
|
589
|
+
}`,
|
|
590
|
+
expertQ4: `enable f16;
|
|
591
|
+
enable subgroups;
|
|
592
|
+
struct P { M: u32, N: u32, rowsPerExpert: u32, inputPerSlot: u32 }
|
|
593
|
+
@group(0) @binding(0) var<storage, read> x: array<vec4<f32>>;
|
|
594
|
+
@group(0) @binding(1) var<storage, read> qw: array<vec4<u32>>;
|
|
595
|
+
@group(0) @binding(2) var<storage, read> qs: array<f16>;
|
|
596
|
+
@group(0) @binding(3) var<storage, read_write> y: array<f32>;
|
|
597
|
+
@group(0) @binding(4) var<uniform> p: P;
|
|
598
|
+
@group(0) @binding(5) var<storage, read> slots: array<u32>;
|
|
599
|
+
${Q4BLK}
|
|
600
|
+
@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>) {
|
|
601
|
+
let z = wg.z; let m = (wg.y * ng.x + wg.x) * 4u + l.x / 32u; let lane = l.x % 32u;
|
|
602
|
+
let nb = p.N / 32u; let rb = (slots[z] * p.rowsPerExpert + min(m, p.M - 1u)) * nb;
|
|
603
|
+
let xo = select(0u, z * p.N / 4u, p.inputPerSlot == 1u);
|
|
604
|
+
var acc = 0.0;
|
|
605
|
+
for (var b = lane; b < nb; b += 32u) { acc += q4blk(qw[rb + b], xo + b * 8u) * f32(qs[rb + b]); }
|
|
606
|
+
let total = subgroupAdd(acc);
|
|
607
|
+
if (lane == 0u && m < p.M) { y[z * p.M + m] = total; }
|
|
608
|
+
}`,
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
// ── Chunked prefill: T tokens per layer pass (rung 2b's pattern) ────────────────
|
|
612
|
+
// Rows are tokens, token-major. Generic kernels come from rung 2b (rmsnormB, matmulF32T, topkB, kvStoreB,
|
|
613
|
+
// softmaxB, attnOutB); these are Gemma's.
|
|
614
|
+
const QB = 'struct Q { T: u32, pos0: u32, S: u32, pad: u32 }';
|
|
615
|
+
const G4B = {
|
|
616
|
+
embedQ6KB: `
|
|
617
|
+
struct P { n: u32, nb: u32, scale: f32 }
|
|
618
|
+
${QB}
|
|
619
|
+
${Q6P}
|
|
620
|
+
@group(0) @binding(4) var<storage, read> ids: array<u32>;
|
|
621
|
+
@group(0) @binding(5) var<storage, read_write> y: array<f32>;
|
|
622
|
+
@group(0) @binding(6) var<uniform> p: P;
|
|
623
|
+
@group(0) @binding(7) var<uniform> qd: Q;
|
|
624
|
+
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
625
|
+
let i = g.x; let t = g.y; if (i >= p.n || t >= qd.T) { return; }
|
|
626
|
+
y[t * p.n + i] = q6v(ids[t] * p.nb + (i >> 8u), i & 255u) * p.scale;
|
|
627
|
+
}`,
|
|
628
|
+
// Tiled Q4_0 GEMM: y[t·M + m] = Σ_k x[t·N + k]·W[m][k]. 16 rows × 16 tokens per workgroup; each 32-wide
|
|
629
|
+
// block of W (decoded from its nibbles) and of x goes through shared memory, so a weight serves 16 tokens.
|
|
630
|
+
// Dispatch (ceil(M/16), ceil(T/16)).
|
|
631
|
+
matmulQ4T: `enable f16;
|
|
632
|
+
struct P { M: u32, N: u32 }
|
|
633
|
+
${QB}
|
|
634
|
+
@group(0) @binding(0) var<storage, read> x: array<f32>;
|
|
635
|
+
@group(0) @binding(1) var<storage, read> qw: array<u32>;
|
|
636
|
+
@group(0) @binding(2) var<storage, read> qs: array<f16>;
|
|
637
|
+
@group(0) @binding(3) var<storage, read_write> y: array<f32>;
|
|
638
|
+
@group(0) @binding(4) var<uniform> p: P;
|
|
639
|
+
@group(0) @binding(5) var<uniform> qd: Q;
|
|
640
|
+
var<workgroup> ws: array<f32, 512>;
|
|
641
|
+
var<workgroup> xs: array<f32, 512>;
|
|
642
|
+
@compute @workgroup_size(256) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
|
|
643
|
+
let m0 = wg.x * 16u; let t0 = wg.y * 16u; let r = l.x / 16u; let c = l.x % 16u;
|
|
644
|
+
let nb = p.N / 32u; let nw = p.N / 8u; var acc = 0.0;
|
|
645
|
+
for (var b = 0u; b < nb; b++) {
|
|
646
|
+
for (var i = l.x; i < 512u; i += 256u) {
|
|
647
|
+
let rr = i / 32u; let kk = i % 32u;
|
|
648
|
+
let m = m0 + rr; var wv = 0.0;
|
|
649
|
+
if (m < p.M) {
|
|
650
|
+
let wd = qw[m * nw + b * 4u + ((kk & 15u) >> 2u)];
|
|
651
|
+
wv = (f32((wd >> ((kk & 3u) * 8u + (kk >> 4u) * 4u)) & 15u) - 8.0) * f32(qs[m * nb + b]);
|
|
652
|
+
}
|
|
653
|
+
ws[i] = wv;
|
|
654
|
+
let t = t0 + rr; var xv = 0.0;
|
|
655
|
+
if (t < qd.T) { xv = x[t * p.N + b * 32u + kk]; }
|
|
656
|
+
xs[i] = xv;
|
|
657
|
+
}
|
|
658
|
+
workgroupBarrier();
|
|
659
|
+
for (var kk = 0u; kk < 32u; kk++) { acc += ws[r * 32u + kk] * xs[c * 32u + kk]; }
|
|
660
|
+
workgroupBarrier();
|
|
661
|
+
}
|
|
662
|
+
let m = m0 + r; let t = t0 + c;
|
|
663
|
+
if (m < p.M && t < qd.T) { y[t * p.M + m] = acc; }
|
|
664
|
+
}`,
|
|
665
|
+
// Routed experts for every (token, pick) pair: z = t·K + j, slots[z] its pool slot. Input: the token's row
|
|
666
|
+
// (gate/up, inputMode 0) or the pair's own row (down, inputMode 1).
|
|
667
|
+
expertQ4B: `enable f16;
|
|
668
|
+
struct P { M: u32, N: u32, rowsPerExpert: u32, inputMode: u32, K: u32 }
|
|
669
|
+
@group(0) @binding(0) var<storage, read> xin: array<f32>;
|
|
670
|
+
@group(0) @binding(1) var<storage, read> qw: array<u32>;
|
|
671
|
+
@group(0) @binding(2) var<storage, read> qs: array<f16>;
|
|
672
|
+
@group(0) @binding(3) var<storage, read_write> y: array<f32>;
|
|
673
|
+
@group(0) @binding(4) var<uniform> p: P;
|
|
674
|
+
@group(0) @binding(5) var<storage, read> slots: array<u32>;
|
|
675
|
+
${Q4DOT}
|
|
676
|
+
var<workgroup> part: array<f32, 1024>;
|
|
677
|
+
@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>) {
|
|
678
|
+
let z = wg.z; let m0 = (wg.y * ng.x + wg.x) * 4u; let t = l.x; let M = p.M; let nw = p.N / 8u; let nb = p.N / 32u;
|
|
679
|
+
if (m0 >= M) { return; }
|
|
680
|
+
let rb = slots[z] * p.rowsPerExpert;
|
|
681
|
+
let xo = select((z / p.K) * p.N, z * p.N, p.inputMode == 1u);
|
|
682
|
+
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);
|
|
683
|
+
var a0 = 0.0; var a1 = 0.0; var a2 = 0.0; var a3 = 0.0;
|
|
684
|
+
for (var w = t; w < nw; w += 256u) {
|
|
685
|
+
let b = w >> 2u; let xb = xo + b * 32u + (w & 3u) * 4u;
|
|
686
|
+
a0 += q4dot(qw[r0 * nw + w], f32(qs[r0 * nb + b]), xb);
|
|
687
|
+
a1 += q4dot(qw[r1 * nw + w], f32(qs[r1 * nb + b]), xb);
|
|
688
|
+
a2 += q4dot(qw[r2 * nw + w], f32(qs[r2 * nb + b]), xb);
|
|
689
|
+
a3 += q4dot(qw[r3 * nw + w], f32(qs[r3 * nb + b]), xb);
|
|
690
|
+
}
|
|
691
|
+
part[t] = a0; part[256u + t] = a1; part[512u + t] = a2; part[768u + t] = a3;
|
|
692
|
+
workgroupBarrier();
|
|
693
|
+
for (var s = 128u; s > 0u; s >>= 1u) {
|
|
694
|
+
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]; }
|
|
695
|
+
workgroupBarrier();
|
|
696
|
+
}
|
|
697
|
+
if (t < 4u && m0 + t < M) { y[z * M + m0 + t] = part[t * 256u]; }
|
|
698
|
+
}`,
|
|
699
|
+
// Per-head RMS norm and NeoX RoPE in place for (head wg.x, token wg.y); rope holds hd values per token.
|
|
700
|
+
qkNormRopeB: `
|
|
701
|
+
struct P { hd: u32, row: u32, eps: f32 }
|
|
702
|
+
@group(0) @binding(0) var<storage, read_write> x: array<f32>;
|
|
703
|
+
@group(0) @binding(1) var<storage, read> w: array<f32>;
|
|
704
|
+
@group(0) @binding(2) var<storage, read> rope: array<f32>;
|
|
705
|
+
@group(0) @binding(3) var<uniform> p: P;
|
|
706
|
+
var<workgroup> red: array<f32, 64>;
|
|
707
|
+
@compute @workgroup_size(64) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
|
|
708
|
+
let base = wg.y * p.row + wg.x * p.hd; let rb = wg.y * p.hd; let t = l.x; let half = p.hd / 2u;
|
|
709
|
+
var acc = 0.0;
|
|
710
|
+
for (var i = t; i < p.hd; i += 64u) { let v = x[base + i]; acc += v * v; }
|
|
711
|
+
red[t] = acc; workgroupBarrier();
|
|
712
|
+
for (var s = 32u; s > 0u; s >>= 1u) { if (t < s) { red[t] += red[t + s]; } workgroupBarrier(); }
|
|
713
|
+
let scale = 1.0 / sqrt(red[0] / f32(p.hd) + p.eps);
|
|
714
|
+
for (var i = t; i < half; i += 64u) {
|
|
715
|
+
let a = (x[base + i] * scale) * w[i];
|
|
716
|
+
let b = (x[base + i + half] * scale) * w[i + half];
|
|
717
|
+
let c = rope[rb + i]; let sn = rope[rb + half + i];
|
|
718
|
+
x[base + i] = a * c - b * sn;
|
|
719
|
+
x[base + i + half] = a * sn + b * c;
|
|
720
|
+
}
|
|
721
|
+
}`,
|
|
722
|
+
vNormB: `
|
|
723
|
+
struct P { hd: u32, row: u32, eps: f32 }
|
|
724
|
+
@group(0) @binding(0) var<storage, read> x: array<f32>;
|
|
725
|
+
@group(0) @binding(1) var<storage, read_write> y: array<f32>;
|
|
726
|
+
@group(0) @binding(2) var<uniform> p: P;
|
|
727
|
+
var<workgroup> red: array<f32, 64>;
|
|
728
|
+
@compute @workgroup_size(64) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
|
|
729
|
+
let base = wg.y * p.row + wg.x * p.hd; let t = l.x;
|
|
730
|
+
var acc = 0.0;
|
|
731
|
+
for (var i = t; i < p.hd; i += 64u) { let v = x[base + i]; acc += v * v; }
|
|
732
|
+
red[t] = acc; workgroupBarrier();
|
|
733
|
+
for (var s = 32u; s > 0u; s >>= 1u) { if (t < s) { red[t] += red[t + s]; } workgroupBarrier(); }
|
|
734
|
+
let scale = 1.0 / sqrt(red[0] / f32(p.hd) + p.eps);
|
|
735
|
+
for (var i = t; i < p.hd; i += 64u) { y[base + i] = x[base + i] * scale; }
|
|
736
|
+
}`,
|
|
737
|
+
// Causal (and, on sliding layers, windowed) scores for T queries over S = pos0 + T keys; scale 1.0.
|
|
738
|
+
attnScoreWB: `enable f16;
|
|
739
|
+
struct P { heads: u32, kvHeads: u32, hd: u32, win: u32 }
|
|
740
|
+
${QB}
|
|
741
|
+
@group(0) @binding(0) var<storage, read> q: array<f32>;
|
|
742
|
+
@group(0) @binding(1) var<storage, read> kc: array<f16>;
|
|
743
|
+
@group(0) @binding(2) var<storage, read_write> sc: array<f32>;
|
|
744
|
+
@group(0) @binding(3) var<uniform> p: P;
|
|
745
|
+
@group(0) @binding(4) var<uniform> qd: Q;
|
|
746
|
+
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>, @builtin(num_workgroups) ng: vec3<u32>) {
|
|
747
|
+
let idx = g.y * ng.x * 256u + g.x; let S = qd.S; let T = qd.T;
|
|
748
|
+
if (idx >= p.heads * T * S) { return; }
|
|
749
|
+
let h = idx / (T * S); let rem = idx % (T * S); let t = rem / S; let pos = rem % S; let qp = qd.pos0 + t;
|
|
750
|
+
if (pos > qp || (p.win > 0u && qp - pos >= p.win)) { sc[idx] = -3.4e38; return; }
|
|
751
|
+
let kvh = h / (p.heads / p.kvHeads);
|
|
752
|
+
let qo = t * p.heads * p.hd + h * p.hd; let ko = pos * p.kvHeads * p.hd + kvh * p.hd;
|
|
753
|
+
var acc = 0.0;
|
|
754
|
+
for (var d = 0u; d < p.hd; d++) { acc += q[qo + d] * f32(kc[ko + d]); }
|
|
755
|
+
sc[idx] = acc;
|
|
756
|
+
}`,
|
|
757
|
+
// x += rms_norm(y) · w per token row (one workgroup per token).
|
|
758
|
+
rmsnormAddB: `
|
|
759
|
+
struct P { n: u32, eps: f32 }
|
|
760
|
+
@group(0) @binding(0) var<storage, read> y: array<f32>;
|
|
761
|
+
@group(0) @binding(1) var<storage, read> w: array<f32>;
|
|
762
|
+
@group(0) @binding(2) var<storage, read_write> x: array<f32>;
|
|
763
|
+
@group(0) @binding(3) var<uniform> p: P;
|
|
764
|
+
var<workgroup> red: array<f32, 256>;
|
|
765
|
+
@compute @workgroup_size(256) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
|
|
766
|
+
let b = wg.x * p.n; let t = l.x; var acc = 0.0;
|
|
767
|
+
for (var i = t; i < p.n; i += 256u) { let v = y[b + i]; acc += v * v; }
|
|
768
|
+
red[t] = acc; workgroupBarrier();
|
|
769
|
+
for (var s = 128u; s > 0u; s >>= 1u) { if (t < s) { red[t] += red[t + s]; } workgroupBarrier(); }
|
|
770
|
+
let scale = 1.0 / sqrt(red[0] / f32(p.n) + p.eps);
|
|
771
|
+
for (var i = t; i < p.n; i += 256u) { x[b + i] = (y[b + i] * scale) * w[i] + x[b + i]; }
|
|
772
|
+
}`,
|
|
773
|
+
rmsnormKB: `
|
|
774
|
+
struct P { n: u32, eps: f32, k: f32 }
|
|
775
|
+
@group(0) @binding(0) var<storage, read> x: array<f32>;
|
|
776
|
+
@group(0) @binding(1) var<storage, read> w: array<f32>;
|
|
777
|
+
@group(0) @binding(2) var<storage, read_write> y: array<f32>;
|
|
778
|
+
@group(0) @binding(3) var<uniform> p: P;
|
|
779
|
+
var<workgroup> red: array<f32, 256>;
|
|
780
|
+
@compute @workgroup_size(256) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
|
|
781
|
+
let b = wg.x * p.n; let t = l.x; var acc = 0.0;
|
|
782
|
+
for (var i = t; i < p.n; i += 256u) { let v = x[b + i]; acc += v * v; }
|
|
783
|
+
red[t] = acc; workgroupBarrier();
|
|
784
|
+
for (var s = 128u; s > 0u; s >>= 1u) { if (t < s) { red[t] += red[t + s]; } workgroupBarrier(); }
|
|
785
|
+
let scale = 1.0 / sqrt(red[0] / f32(p.n) + p.eps);
|
|
786
|
+
for (var i = t; i < p.n; i += 256u) { y[b + i] = ((x[b + i] * scale) * p.k) * w[i]; }
|
|
787
|
+
}`,
|
|
788
|
+
// act = GELU(g) · u over T × n (the dense MLP's separate gate and up).
|
|
789
|
+
gegluSplitB: `
|
|
790
|
+
struct P { n: u32 }
|
|
791
|
+
${QB}
|
|
792
|
+
@group(0) @binding(0) var<storage, read> g: array<f32>;
|
|
793
|
+
@group(0) @binding(1) var<storage, read> u: array<f32>;
|
|
794
|
+
@group(0) @binding(2) var<storage, read_write> act: array<f32>;
|
|
795
|
+
@group(0) @binding(3) var<uniform> p: P;
|
|
796
|
+
@group(0) @binding(4) var<uniform> qd: Q;
|
|
797
|
+
${GELU}
|
|
798
|
+
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gi: vec3<u32>) {
|
|
799
|
+
let i = gi.x; let t = gi.y; if (i >= p.n || t >= qd.T) { return; }
|
|
800
|
+
let k = t * p.n + i; act[k] = gelu(g[k]) * u[k];
|
|
801
|
+
}`,
|
|
802
|
+
moeSumB: `
|
|
803
|
+
struct P { H: u32, k: u32 }
|
|
804
|
+
${QB}
|
|
805
|
+
@group(0) @binding(0) var<storage, read> dn: array<f32>;
|
|
806
|
+
@group(0) @binding(1) var<storage, read> sel: array<u32>;
|
|
807
|
+
@group(0) @binding(2) var<storage, read> ds: array<f32>;
|
|
808
|
+
@group(0) @binding(3) var<storage, read_write> moe: array<f32>;
|
|
809
|
+
@group(0) @binding(4) var<uniform> p: P;
|
|
810
|
+
@group(0) @binding(5) var<uniform> qd: Q;
|
|
811
|
+
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gi: vec3<u32>) {
|
|
812
|
+
let h = gi.x; let t = gi.y; if (h >= p.H || t >= qd.T) { return; }
|
|
813
|
+
let sb = t * 2u * p.k; let ob = t * p.k * p.H + h;
|
|
814
|
+
var acc = (dn[ob] * ds[sel[sb]]) * bitcast<f32>(sel[sb + p.k]);
|
|
815
|
+
for (var j = 1u; j < p.k; j++) { acc = acc + (dn[ob + j * p.H] * ds[sel[sb + j]]) * bitcast<f32>(sel[sb + p.k + j]); }
|
|
816
|
+
moe[t * p.H + h] = acc;
|
|
817
|
+
}`,
|
|
818
|
+
postFfnB: `
|
|
819
|
+
struct P { n: u32, eps: f32 }
|
|
820
|
+
@group(0) @binding(0) var<storage, read> mlp: array<f32>;
|
|
821
|
+
@group(0) @binding(1) var<storage, read> moe: array<f32>;
|
|
822
|
+
@group(0) @binding(2) var<storage, read> w: array<f32>;
|
|
823
|
+
@group(0) @binding(3) var<storage, read> os: array<f32>;
|
|
824
|
+
@group(0) @binding(4) var<storage, read_write> x: array<f32>;
|
|
825
|
+
@group(0) @binding(5) var<uniform> p: P;
|
|
826
|
+
var<workgroup> red: array<f32, 256>;
|
|
827
|
+
@compute @workgroup_size(256) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
|
|
828
|
+
let b = wg.x * p.n; let t = l.x; var acc = 0.0;
|
|
829
|
+
for (var i = t; i < p.n; i += 256u) { let v = mlp[b + i] + moe[b + i]; acc += v * v; }
|
|
830
|
+
red[t] = acc; workgroupBarrier();
|
|
831
|
+
for (var s = 128u; s > 0u; s >>= 1u) { if (t < s) { red[t] += red[t + s]; } workgroupBarrier(); }
|
|
832
|
+
let scale = 1.0 / sqrt(red[0] / f32(p.n) + p.eps);
|
|
833
|
+
let k = os[0];
|
|
834
|
+
for (var i = t; i < p.n; i += 256u) { x[b + i] = (((mlp[b + i] + moe[b + i]) * scale) * w[i] + x[b + i]) * k; }
|
|
835
|
+
}`,
|
|
836
|
+
};
|
|
837
|
+
const REUSED_B = ['rmsnormB', 'matmulF32T', 'topkB', 'kvStoreB', 'softmaxB', 'attnOutB'];
|
|
838
|
+
|
|
839
|
+
// ── Engine ──────────────────────────────────────────────────────────────────
|
|
840
|
+
export class Gemma4MoeSsd extends Qwen3MoeSsd {
|
|
841
|
+
static configFrom(kv) { return configFromGgufGemma4(kv); }
|
|
842
|
+
static get ingestPlan() { return GEMMA4_INGEST_PLAN; }
|
|
843
|
+
static tokenizerFrom(kv) { return new GemmaTokenizer(kv); }
|
|
844
|
+
static load(modelId, opts = {}) { return super.load(modelId, { ...opts, source: opts.source || GEMMA4_26B_A4B }); }
|
|
845
|
+
|
|
846
|
+
chatPrompt(messages, opts) { return gemmaChatPrompt(messages, opts); }
|
|
847
|
+
get stopTokenIds() { return [this.cfg.eos, this.tokenizer.ids.get('<turn|>')].filter((t) => t !== undefined); }
|
|
848
|
+
// Thinking: the model writes <|channel>thought\n … <channel|> itself (nothing opens it in the prompt).
|
|
849
|
+
get thinkOpenTokenId() { return this.tokenizer.ids.get('<|channel>') ?? null; }
|
|
850
|
+
get thinkCloseTokenId() { return this.tokenizer.ids.get('<channel|>') ?? null; }
|
|
851
|
+
get thinkInPrompt() { return false; }
|
|
852
|
+
get thinkPreamble() { return 'thought\n'; }
|
|
853
|
+
|
|
854
|
+
get kernels() {
|
|
855
|
+
return (this._kernels ||= {
|
|
856
|
+
...super.kernels, ...G4, ...(this.subgroupKernels ? G4SG : {}),
|
|
857
|
+
...(this.batchPrefill ? { ...Object.fromEntries(REUSED_B.map((k) => [k, QWEN35_BATCH_KERNELS[k]])), ...G4B } : {}),
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
// Subgroup GEMVs replace the scalar Q4_0 ones when subgroups are exactly 32 wide (Apple, most others).
|
|
861
|
+
get subgroupKernels() {
|
|
862
|
+
const i = this.device.adapterInfo || this.opts.adapterInfo || {};
|
|
863
|
+
return this.opts.subgroups !== false && this.device.features.has('subgroups') && i.subgroupMinSize === 32 && i.subgroupMaxSize === 32;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// batchPrefill (default on): prompts go through in chunks of up to prefillChunk tokens, one routing
|
|
867
|
+
// readback per layer per chunk. capture (debug) records one-token steps, so it turns chunking off.
|
|
868
|
+
constructor(device, manifest, gguf, opts) {
|
|
869
|
+
super(device, manifest, gguf, opts);
|
|
870
|
+
this.batchPrefill = (opts.batchPrefill ?? true) && !opts.capture;
|
|
871
|
+
this.chunkTokens = Math.max(2, Math.floor(opts.prefillChunk || 256));
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// A bind group whose entries are buffers or { buffer, offset, size } ranges.
|
|
875
|
+
bindR(name, entries) {
|
|
876
|
+
return this.device.createBindGroup({
|
|
877
|
+
layout: this.pipeline(name).getBindGroupLayout(0),
|
|
878
|
+
entries: entries.map((e, i) => ({ binding: i, resource: e.buffer ? e : { buffer: e } })),
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Dense upload; the Q6_K token table split into aligned planes on the GPU (the raw copy is then freed);
|
|
883
|
+
// the full-attention layers' RoPE frequency factors on the CPU side.
|
|
884
|
+
async uploadDense(onProgress) {
|
|
885
|
+
await super.uploadDense(onProgress);
|
|
886
|
+
const dev = this.device, raw = this.w['token_embd.weight'].raw, blocks = raw.size / Q6K_BLOCK | 0;
|
|
887
|
+
const P = (n) => this.buffer(n, STORAGE, 'dense');
|
|
888
|
+
this.q6 = { ql: P(blocks * 128), qh: P(blocks * 64), sc: P(blocks * 16), dd: P(blocks * 4) };
|
|
889
|
+
const enc = dev.createCommandEncoder(), pass = enc.beginComputePass();
|
|
890
|
+
this.dispatch(pass, 'q6kPlanes', this.bind('q6kPlanes', [raw, this.q6.ql, this.q6.qh, this.q6.sc, this.q6.dd, this.uniform([blocks])]), Math.ceil(blocks / 256));
|
|
891
|
+
pass.end(); dev.queue.submit([enc.finish()]);
|
|
892
|
+
await dev.queue.onSubmittedWorkDone();
|
|
893
|
+
this.gpuBytes.dense -= raw.size; raw.destroy(); this.w['token_embd.weight'].raw = null;
|
|
894
|
+
const d = this.manifest.dense.tensors['rope_freqs.weight'];
|
|
895
|
+
this.ropeFreqs = null;
|
|
896
|
+
if (d) {
|
|
897
|
+
const r = await OpfsReaderPool.open(`${this.opts.dir}/${this.manifest.dense.file}`, { workers: 1 });
|
|
898
|
+
const got = await r.read(d.raw.off, d.raw.bytes);
|
|
899
|
+
this.ropeFreqs = new Float32Array(got.buf.slice(0, d.raw.bytes));
|
|
900
|
+
await r.close();
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
initBuffers() {
|
|
905
|
+
const c = this.cfg, H = c.hidden, K = c.topK, E = c.experts, F = c.expertFf, DF = c.denseFf, Lc = c.layers;
|
|
906
|
+
if (2 * K * 4 > 256) throw new Error(`top-k ${K} > 32`);
|
|
907
|
+
if (E > 256) throw new Error(`${E} experts: the router top-k kernel handles at most 256`);
|
|
908
|
+
const kvn = (l) => c.kvHeads[l] * c.headDim[l], qn = (l) => c.heads * c.headDim[l];
|
|
909
|
+
const QN = Math.max(...c.headDim.map((d) => c.heads * d)), KVN = Math.max(...c.headDim.map((_, l) => kvn(l)));
|
|
910
|
+
const HD = Math.max(...c.headDim);
|
|
911
|
+
this.kc = []; this.vc = [];
|
|
912
|
+
for (let l = 0; l < Lc; l++) {
|
|
913
|
+
this.kc.push(this.buffer(this.maxCtx * kvn(l) * 2, STORAGE, 'kv'));
|
|
914
|
+
this.vc.push(this.buffer(this.maxCtx * kvn(l) * 2, STORAGE, 'kv'));
|
|
915
|
+
}
|
|
916
|
+
const A = (n) => this.buffer(n * 4, STORAGE | COPY_SRC | COPY_DST);
|
|
917
|
+
const gateBytes = DF * 4;
|
|
918
|
+
if (gateBytes % 256) throw new Error(`dense FFN ${DF}: gate/up halves must sit on 256-byte offsets`);
|
|
919
|
+
this.a = {
|
|
920
|
+
x: A(H), xn: A(H), q: A(QN), k: A(KVN), v: A(KVN), vr: A(KVN), att: A(QN), o: A(H), sc: A(c.heads * this.maxCtx),
|
|
921
|
+
mgu: A(2 * DF), ma: A(DF), md: A(H), mlp: A(H), xm: A(H), rt: A(H), moe: A(H), moe2: A(H),
|
|
922
|
+
rl: A(E), sel: A(2 * K), prt: A(H), prl: A(E), psel: A(64 * this.lookahead), slots: A(K),
|
|
923
|
+
gu: A(K * 2 * F), act: A(K * F), dn: A(K * H), logits: A(c.vocab), am: A(4),
|
|
924
|
+
ropeS: A(HD), ropeF: A(HD),
|
|
925
|
+
};
|
|
926
|
+
if (this.opts.capture) this.a.cap = A(2 * Lc * H);
|
|
927
|
+
this.tok = this.buffer(16, 0x40 | COPY_DST);
|
|
928
|
+
this.rbSel = this.buffer(256 * (1 + this.lookahead), MAP_READ | COPY_DST);
|
|
929
|
+
this.rbLogits = this.buffer(c.vocab * 4, MAP_READ | COPY_DST);
|
|
930
|
+
this.rbArg = this.buffer(16, MAP_READ | COPY_DST);
|
|
931
|
+
|
|
932
|
+
const f = (v) => ({ f: v });
|
|
933
|
+
const eps = f(c.eps);
|
|
934
|
+
const u = {
|
|
935
|
+
embed: this.uniform([H, H / 256, f(Math.fround(Math.sqrt(H)))]),
|
|
936
|
+
rmsH: this.uniform([H, eps]), routerK: this.uniform([H, eps, f(Math.fround(1 / Math.fround(Math.sqrt(H))))]),
|
|
937
|
+
mmG: this.uniform([DF, H]), mmD: this.uniform([H, DF]), gegluD: this.uniform([DF, 1]),
|
|
938
|
+
router: this.uniform([E, H]), topk: this.uniform([E, K]),
|
|
939
|
+
gu: this.uniform([2 * F, H, 2 * F, 0]), dn: this.uniform([H, F, H, 1]), gegluE: this.uniform([F, K]), sum: this.uniform([H, K]),
|
|
940
|
+
lm: this.uniform([c.vocab, H]), cap: this.uniform([c.vocab, f(Math.fround(1 / c.softcap)), f(c.softcap)]), am: this.uniform([c.vocab]),
|
|
941
|
+
};
|
|
942
|
+
// Attention uniforms per layer shape (sliding / full).
|
|
943
|
+
const shape = new Map();
|
|
944
|
+
const shapeOf = (l) => {
|
|
945
|
+
const key = `${c.headDim[l]}/${c.kvHeads[l]}/${c.swa[l]}`;
|
|
946
|
+
if (!shape.has(key)) shape.set(key, {
|
|
947
|
+
mmQ: this.uniform([qn(l), H]), mmKV: this.uniform([kvn(l), H]), mmO: this.uniform([H, qn(l)]),
|
|
948
|
+
hd: this.uniform([c.headDim[l], eps]), kvn: this.uniform([kvn(l)]),
|
|
949
|
+
att: this.uniform([c.heads, c.kvHeads[l], c.headDim[l], c.swa[l] ? c.window : 0]),
|
|
950
|
+
});
|
|
951
|
+
return shape.get(key);
|
|
952
|
+
};
|
|
953
|
+
const a = this.a, W = this.w;
|
|
954
|
+
const q6 = [this.q6.ql, this.q6.qh, this.q6.sc, this.q6.dd];
|
|
955
|
+
this.g = { embed: this.bind('embedQ6K', [...q6, a.x, this.tok, u.embed]) };
|
|
956
|
+
this.layers = [];
|
|
957
|
+
for (let l = 0; l < Lc; l++) {
|
|
958
|
+
const p = (n) => W[`blk.${l}.${n}`], s = shapeOf(l), rope = c.swa[l] ? a.ropeS : a.ropeF;
|
|
959
|
+
const hasV = !!p('attn_v.weight');
|
|
960
|
+
const ly = {
|
|
961
|
+
swa: c.swa[l], hasV,
|
|
962
|
+
rmsA: this.bind('rmsnorm', [a.x, p('attn_norm.weight').raw, a.xn, u.rmsH]),
|
|
963
|
+
q: this.bind('matmulQ4', [a.xn, p('attn_q.weight').q, p('attn_q.weight').s, a.q, s.mmQ]),
|
|
964
|
+
k: this.bind('matmulQ4', [a.xn, p('attn_k.weight').q, p('attn_k.weight').s, a.k, s.mmKV]),
|
|
965
|
+
v: hasV ? this.bind('matmulQ4', [a.xn, p('attn_v.weight').q, p('attn_v.weight').s, a.vr, s.mmKV]) : null,
|
|
966
|
+
vn: this.bind('vNorm', [hasV ? a.vr : a.k, a.v, s.hd]), // no V projection: V is K's projection
|
|
967
|
+
ropeQ: this.bind('qkNormRope', [a.q, p('attn_q_norm.weight').raw, rope, s.hd]),
|
|
968
|
+
ropeK: this.bind('qkNormRope', [a.k, p('attn_k_norm.weight').raw, rope, s.hd]),
|
|
969
|
+
kv: this.bind('kvStore', [a.k, a.v, this.kc[l], this.vc[l], this.tok, s.kvn]),
|
|
970
|
+
score: this.bind('attnScoreW', [a.q, this.kc[l], a.sc, this.tok, s.att]),
|
|
971
|
+
soft: this.bind('softmaxW', [a.sc, this.tok, s.att]),
|
|
972
|
+
attOut: this.bind('attnOutW', [a.sc, this.vc[l], a.att, this.tok, s.att]),
|
|
973
|
+
o: this.bind('matmulQ4', [a.att, p('attn_output.weight').q, p('attn_output.weight').s, a.o, s.mmO]),
|
|
974
|
+
postA: this.bind('rmsnormAdd', [a.o, p('post_attention_norm.weight').raw, a.x, u.rmsH]),
|
|
975
|
+
rmsF: this.bind('rmsnorm', [a.x, p('ffn_norm.weight').raw, a.xn, u.rmsH]),
|
|
976
|
+
mg: this.bindR('matmulQ4', [a.xn, p('ffn_gate.weight').q, p('ffn_gate.weight').s, { buffer: a.mgu, offset: 0, size: gateBytes }, u.mmG]),
|
|
977
|
+
mu: this.bindR('matmulQ4', [a.xn, p('ffn_up.weight').q, p('ffn_up.weight').s, { buffer: a.mgu, offset: gateBytes, size: gateBytes }, u.mmG]),
|
|
978
|
+
mact: this.bind('geglu', [a.mgu, a.ma, u.gegluD]),
|
|
979
|
+
md: this.bind('matmulQ4', [a.ma, p('ffn_down.weight').q, p('ffn_down.weight').s, a.md, u.mmD]),
|
|
980
|
+
mpost: this.bind('rmsnorm', [a.md, p('post_ffw_norm_1.weight').raw, a.mlp, u.rmsH]),
|
|
981
|
+
xm: this.bind('rmsnorm', [a.x, p('pre_ffw_norm_2.weight').raw, a.xm, u.rmsH]),
|
|
982
|
+
rp: this.bind('rmsnormK', [a.x, p('ffn_gate_inp.scale').raw, a.rt, u.routerK]),
|
|
983
|
+
router: this.bind('matmulF32', [a.rt, p('ffn_gate_inp.weight').raw, a.rl, u.router]),
|
|
984
|
+
topk: this.bind('topk', [a.rl, a.sel, u.topk]),
|
|
985
|
+
// Routers of layers l+1..l+lookahead on this layer's post-attention state: the prefetch guesses.
|
|
986
|
+
pf: Array.from({ length: Math.min(this.lookahead, Lc - 1 - l) }, (_, i) => {
|
|
987
|
+
const at = (n) => W[`blk.${l + 1 + i}.${n}`];
|
|
988
|
+
return {
|
|
989
|
+
rp: this.bind('rmsnormK', [a.x, at('ffn_gate_inp.scale').raw, a.prt, u.routerK]),
|
|
990
|
+
router: this.bind('matmulF32', [a.prt, at('ffn_gate_inp.weight').raw, a.prl, u.router]),
|
|
991
|
+
topk: this.bindR('topk', [a.prl, { buffer: a.psel, offset: 256 * i, size: 8 * K }, u.topk]),
|
|
992
|
+
};
|
|
993
|
+
}),
|
|
994
|
+
gu: this.bind('expertQ4', [a.xm, this.pool.guQ, this.pool.guS, a.gu, u.gu, a.slots]),
|
|
995
|
+
act: this.bind('geglu', [a.gu, a.act, u.gegluE]),
|
|
996
|
+
dn: this.bind('expertQ4', [a.act, this.pool.dQ, this.pool.dS, a.dn, u.dn, a.slots]),
|
|
997
|
+
sum: this.bind('moeSum', [a.dn, a.sel, p('ffn_down_exps.scale').raw, a.moe, u.sum]),
|
|
998
|
+
mpost2: this.bind('rmsnorm', [a.moe, p('post_ffw_norm_2.weight').raw, a.moe2, u.rmsH]),
|
|
999
|
+
post: this.bind('postFfn', [a.mlp, a.moe2, p('post_ffw_norm.weight').raw, p('layer_output_scale.weight').raw, a.x, u.rmsH]),
|
|
1000
|
+
};
|
|
1001
|
+
if (a.cap) {
|
|
1002
|
+
ly.capA = this.bind('copyAt', [a.x, a.cap, this.uniform([H, l * H])]);
|
|
1003
|
+
ly.capX = this.bind('copyAt', [a.x, a.cap, this.uniform([H, (Lc + l) * H])]);
|
|
1004
|
+
}
|
|
1005
|
+
this.layers.push(ly);
|
|
1006
|
+
}
|
|
1007
|
+
this.g.rmsOut = this.bind('rmsnorm', [a.x, W['output_norm.weight'].raw, a.xn, u.rmsH]);
|
|
1008
|
+
this.g.lm = this.bind('matmulQ6K', [...q6, a.xn, a.logits, u.lm]);
|
|
1009
|
+
this.g.cap = this.bind('softcap', [a.logits, u.cap]);
|
|
1010
|
+
this.g.am = this.bind('argmax', [a.logits, a.am, u.am]);
|
|
1011
|
+
// A chunk pins min(experts, T·K) experts per layer at once; keep that under half the pool.
|
|
1012
|
+
if (Math.min(c.experts, this.chunkTokens * c.topK) > this.poolSlots / 2) this.chunkTokens = Math.max(2, Math.floor(this.poolSlots / 2 / c.topK));
|
|
1013
|
+
if (this.batchPrefill) this.initBatch();
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// Buffers and bind groups for chunked prefill (rows = the chunk's tokens).
|
|
1017
|
+
initBatch() {
|
|
1018
|
+
const c = this.cfg, B = this.chunkTokens, W = this.w, H = c.hidden, K = c.topK, E = c.experts, F = c.expertFf, DF = c.denseFf;
|
|
1019
|
+
const QN = Math.max(...c.headDim.map((d) => c.heads * d)), KVN = Math.max(...c.headDim.map((d, l) => c.kvHeads[l] * d)), HD = Math.max(...c.headDim);
|
|
1020
|
+
const A = (n) => this.buffer(n * 4, STORAGE | COPY_SRC | COPY_DST, 'batch');
|
|
1021
|
+
const b = this.b = {
|
|
1022
|
+
ids: A(B), x: A(B * H), xn: A(B * H), q: A(B * QN), k: A(B * KVN), v: A(B * KVN), vr: A(B * KVN), att: A(B * QN), o: A(B * H),
|
|
1023
|
+
sc: A(c.heads * B * this.maxCtx), mg: A(B * DF), mu: A(B * DF), ma: A(B * DF), md: A(B * H), mlp: A(B * H), xm: A(B * H), rt: A(B * H),
|
|
1024
|
+
rl: A(B * E), sel: A(B * 2 * K), slots: A(B * K), gu: A(B * K * 2 * F), act: A(B * K * F), dn: A(B * K * H), moe: A(B * H), moe2: A(B * H),
|
|
1025
|
+
ropeS: A(B * HD), ropeF: A(B * HD),
|
|
1026
|
+
};
|
|
1027
|
+
this.qb = this.buffer(16, 0x40 | COPY_DST, 'batch');
|
|
1028
|
+
this.rbSelB = this.buffer(B * 2 * K * 4, MAP_READ | COPY_DST, 'batch');
|
|
1029
|
+
const U = (w) => this.uniform(w), eps = { f: c.eps }, qb = this.qb;
|
|
1030
|
+
const u = {
|
|
1031
|
+
embed: U([H, H / 256, { f: Math.fround(Math.sqrt(H)) }]), rmsH: U([H, eps]),
|
|
1032
|
+
routerK: U([H, eps, { f: Math.fround(1 / Math.fround(Math.sqrt(H))) }]), router: U([E, H]), topk: U([E, K]),
|
|
1033
|
+
mg: U([DF, H]), md: U([H, DF]), geglu: U([DF]), gu: U([2 * F, H, 2 * F, 0, K]), dn: U([H, F, H, 1, K]), gegluE: U([F, B * K]), sum: U([H, K]),
|
|
1034
|
+
};
|
|
1035
|
+
const shape = new Map();
|
|
1036
|
+
const shapeOf = (l) => {
|
|
1037
|
+
const hd = c.headDim[l], qn = c.heads * hd, kvn = c.kvHeads[l] * hd, key = `${hd}/${c.kvHeads[l]}/${c.swa[l]}`;
|
|
1038
|
+
if (!shape.has(key)) shape.set(key, {
|
|
1039
|
+
q: U([qn, H]), kv: U([kvn, H]), o: U([H, qn]), ropeQ: U([hd, qn, eps]), ropeK: U([hd, kvn, eps]), kvn: U([kvn]),
|
|
1040
|
+
att: U([c.heads, c.kvHeads[l], hd, c.swa[l] ? c.window : 0]),
|
|
1041
|
+
});
|
|
1042
|
+
return shape.get(key);
|
|
1043
|
+
};
|
|
1044
|
+
const q4 = (name, x, y, uni) => this.bind('matmulQ4T', [x, W[name].q, W[name].s, y, uni, qb]);
|
|
1045
|
+
this.gB = { embed: this.bind('embedQ6KB', [this.q6.ql, this.q6.qh, this.q6.sc, this.q6.dd, b.ids, b.x, u.embed, qb]) };
|
|
1046
|
+
for (let l = 0; l < c.layers; l++) {
|
|
1047
|
+
const n = (t) => `blk.${l}.${t}`, raw = (t) => W[n(t)].raw, sh = shapeOf(l), hasV = !!W[n('attn_v.weight')];
|
|
1048
|
+
const rope = c.swa[l] ? b.ropeS : b.ropeF;
|
|
1049
|
+
this.layers[l].B = {
|
|
1050
|
+
rmsA: this.bind('rmsnormB', [b.x, raw('attn_norm.weight'), b.xn, u.rmsH]),
|
|
1051
|
+
q: q4(n('attn_q.weight'), b.xn, b.q, sh.q), k: q4(n('attn_k.weight'), b.xn, b.k, sh.kv),
|
|
1052
|
+
v: hasV ? q4(n('attn_v.weight'), b.xn, b.vr, sh.kv) : null,
|
|
1053
|
+
vn: this.bind('vNormB', [hasV ? b.vr : b.k, b.v, sh.ropeK]),
|
|
1054
|
+
ropeQ: this.bind('qkNormRopeB', [b.q, raw('attn_q_norm.weight'), rope, sh.ropeQ]),
|
|
1055
|
+
ropeK: this.bind('qkNormRopeB', [b.k, raw('attn_k_norm.weight'), rope, sh.ropeK]),
|
|
1056
|
+
kv: this.bind('kvStoreB', [b.k, b.v, this.kc[l], this.vc[l], sh.kvn, qb]),
|
|
1057
|
+
score: this.bind('attnScoreWB', [b.q, this.kc[l], b.sc, sh.att, qb]),
|
|
1058
|
+
soft: this.bind('softmaxB', [b.sc, qb]),
|
|
1059
|
+
attOut: this.bind('attnOutB', [b.sc, this.vc[l], b.att, sh.att, qb]),
|
|
1060
|
+
o: q4(n('attn_output.weight'), b.att, b.o, sh.o),
|
|
1061
|
+
postA: this.bind('rmsnormAddB', [b.o, raw('post_attention_norm.weight'), b.x, u.rmsH]),
|
|
1062
|
+
rmsF: this.bind('rmsnormB', [b.x, raw('ffn_norm.weight'), b.xn, u.rmsH]),
|
|
1063
|
+
mg: q4(n('ffn_gate.weight'), b.xn, b.mg, u.mg), mu: q4(n('ffn_up.weight'), b.xn, b.mu, u.mg),
|
|
1064
|
+
mact: this.bind('gegluSplitB', [b.mg, b.mu, b.ma, u.geglu, qb]),
|
|
1065
|
+
md: q4(n('ffn_down.weight'), b.ma, b.md, u.md),
|
|
1066
|
+
mpost: this.bind('rmsnormB', [b.md, raw('post_ffw_norm_1.weight'), b.mlp, u.rmsH]),
|
|
1067
|
+
xm: this.bind('rmsnormB', [b.x, raw('pre_ffw_norm_2.weight'), b.xm, u.rmsH]),
|
|
1068
|
+
rp: this.bind('rmsnormKB', [b.x, raw('ffn_gate_inp.scale'), b.rt, u.routerK]),
|
|
1069
|
+
router: this.bind('matmulF32T', [b.rt, raw('ffn_gate_inp.weight'), b.rl, u.router]),
|
|
1070
|
+
topk: this.bind('topkB', [b.rl, b.sel, u.topk]),
|
|
1071
|
+
gu: this.bind('expertQ4B', [b.xm, this.pool.guQ, this.pool.guS, b.gu, u.gu, b.slots]),
|
|
1072
|
+
act: this.bind('geglu', [b.gu, b.act, u.gegluE]),
|
|
1073
|
+
dn: this.bind('expertQ4B', [b.act, this.pool.dQ, this.pool.dS, b.dn, u.dn, b.slots]),
|
|
1074
|
+
sum: this.bind('moeSumB', [b.dn, b.sel, raw('ffn_down_exps.scale'), b.moe, u.sum, qb]),
|
|
1075
|
+
mpost2: this.bind('rmsnormB', [b.moe, raw('post_ffw_norm_2.weight'), b.moe2, u.rmsH]),
|
|
1076
|
+
post: this.bind('postFfnB', [b.mlp, b.moe2, raw('post_ffw_norm.weight'), raw('layer_output_scale.weight'), b.x, u.rmsH]),
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// Layer l over the chunk's T tokens up to its routing (the batched twin of encodeAttention).
|
|
1082
|
+
encodeTrunkB(pass, l, T, S) {
|
|
1083
|
+
const c = this.cfg, g = this.layers[l].B, d = (nm, gr, x, y, z) => this.dispatch(pass, nm, gr, x, y, z);
|
|
1084
|
+
const H = c.hidden, hd = c.headDim[l], QN = c.heads * hd, kvn = c.kvHeads[l] * hd, DF = c.denseFf;
|
|
1085
|
+
const tt = Math.ceil(T / 16), mm = (n) => Math.ceil(n / 16);
|
|
1086
|
+
d('rmsnormB', g.rmsA, T);
|
|
1087
|
+
d('matmulQ4T', g.q, mm(QN), tt); d('matmulQ4T', g.k, mm(kvn), tt);
|
|
1088
|
+
if (g.v) d('matmulQ4T', g.v, mm(kvn), tt);
|
|
1089
|
+
d('vNormB', g.vn, c.kvHeads[l], T);
|
|
1090
|
+
d('qkNormRopeB', g.ropeQ, c.heads, T); d('qkNormRopeB', g.ropeK, c.kvHeads[l], T);
|
|
1091
|
+
d('kvStoreB', g.kv, Math.ceil(kvn / 256), T);
|
|
1092
|
+
d('attnScoreWB', g.score, Math.ceil(c.heads * T * S / 256));
|
|
1093
|
+
d('softmaxB', g.soft, c.heads * T);
|
|
1094
|
+
d('attnOutB', g.attOut, Math.ceil(QN / 256), T);
|
|
1095
|
+
d('matmulQ4T', g.o, mm(H), tt);
|
|
1096
|
+
d('rmsnormAddB', g.postA, T);
|
|
1097
|
+
d('rmsnormB', g.rmsF, T);
|
|
1098
|
+
d('matmulQ4T', g.mg, mm(DF), tt); d('matmulQ4T', g.mu, mm(DF), tt);
|
|
1099
|
+
d('gegluSplitB', g.mact, Math.ceil(DF / 256), T);
|
|
1100
|
+
d('matmulQ4T', g.md, mm(H), tt);
|
|
1101
|
+
d('rmsnormB', g.mpost, T);
|
|
1102
|
+
d('rmsnormB', g.xm, T);
|
|
1103
|
+
d('rmsnormKB', g.rp, T);
|
|
1104
|
+
d('matmulF32T', g.router, c.experts, T);
|
|
1105
|
+
d('topkB', g.topk, T);
|
|
1106
|
+
}
|
|
1107
|
+
encodeExpertsB(pass, l, T) {
|
|
1108
|
+
const c = this.cfg, g = this.layers[l].B, d = (nm, gr, x, y, z) => this.dispatch(pass, nm, gr, x, y, z);
|
|
1109
|
+
d('expertQ4B', g.gu, Math.ceil(2 * c.expertFf / 4), 1, T * c.topK);
|
|
1110
|
+
d('geglu', g.act, Math.ceil(T * c.topK * c.expertFf / 256));
|
|
1111
|
+
d('expertQ4B', g.dn, Math.ceil(c.hidden / 4), 1, T * c.topK);
|
|
1112
|
+
d('moeSumB', g.sum, Math.ceil(c.hidden / 256), T);
|
|
1113
|
+
d('rmsnormB', g.mpost2, T);
|
|
1114
|
+
d('postFfnB', g.post, T);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Feeds T = ids.length tokens at this.position through every layer: one routing readback per layer,
|
|
1118
|
+
// the union of the chunk's experts loaded once. want as in step(), for the last token.
|
|
1119
|
+
async prefillChunk(ids, want = 'none') {
|
|
1120
|
+
const c = this.cfg, dev = this.device, K = c.topK, H = c.hidden, T = ids.length, pos0 = this.position, S = pos0 + T;
|
|
1121
|
+
if (T > this.chunkTokens) throw new Error(`chunk of ${T} > ${this.chunkTokens}`);
|
|
1122
|
+
if (S > this.maxCtx) throw new Error(`context full (${this.maxCtx} tokens)`);
|
|
1123
|
+
const tStart = performance.now();
|
|
1124
|
+
dev.queue.writeBuffer(this.b.ids, 0, Uint32Array.from(ids));
|
|
1125
|
+
dev.queue.writeBuffer(this.qb, 0, new Uint32Array([T, pos0, S, 0]));
|
|
1126
|
+
const ls = c.swa.indexOf(true), lf = c.swa.indexOf(false);
|
|
1127
|
+
const tables = (l, ff, buf) => {
|
|
1128
|
+
if (l < 0) return;
|
|
1129
|
+
const d = c.headDim[l], r = new Float32Array(T * d);
|
|
1130
|
+
for (let t = 0; t < T; t++) r.set(this.ropeTable(d, c.ropeTheta[l], ff, pos0 + t), t * d);
|
|
1131
|
+
dev.queue.writeBuffer(buf, 0, r);
|
|
1132
|
+
};
|
|
1133
|
+
tables(ls, null, this.b.ropeS); tables(lf, this.ropeFreqs, this.b.ropeF);
|
|
1134
|
+
const selBytes = T * 2 * K * 4;
|
|
1135
|
+
let enc = dev.createCommandEncoder(), pass = enc.beginComputePass();
|
|
1136
|
+
this.dispatch(pass, 'embedQ6KB', this.gB.embed, Math.ceil(H / 256), T);
|
|
1137
|
+
this.encodeTrunkB(pass, 0, T, S);
|
|
1138
|
+
pass.end();
|
|
1139
|
+
enc.copyBufferToBuffer(this.b.sel, 0, this.rbSelB, 0, selBytes);
|
|
1140
|
+
dev.queue.submit([enc.finish()]);
|
|
1141
|
+
for (let l = 0; l < c.layers; l++) {
|
|
1142
|
+
const t0 = performance.now();
|
|
1143
|
+
await this.rbSelB.mapAsync(1, 0, selBytes);
|
|
1144
|
+
this.counters.gpuWaitMs += performance.now() - t0;
|
|
1145
|
+
const sel = new Uint32Array(this.rbSelB.getMappedRange(0, selBytes).slice(0));
|
|
1146
|
+
this.rbSelB.unmap();
|
|
1147
|
+
const pairs = new Uint32Array(T * K);
|
|
1148
|
+
for (let t = 0; t < T; t++) for (let j = 0; j < K; j++) pairs[t * K + j] = sel[t * 2 * K + j];
|
|
1149
|
+
const unique = [...new Set(pairs)];
|
|
1150
|
+
const tq = performance.now();
|
|
1151
|
+
const slotsU = await this.xs.ensure(l, unique);
|
|
1152
|
+
this.counters.ensureMs += performance.now() - tq;
|
|
1153
|
+
const slotOf = new Map(unique.map((e, i) => [e, slotsU[i]]));
|
|
1154
|
+
dev.queue.writeBuffer(this.b.slots, 0, pairs.map((e) => slotOf.get(e)));
|
|
1155
|
+
enc = dev.createCommandEncoder(); pass = enc.beginComputePass();
|
|
1156
|
+
this.encodeExpertsB(pass, l, T);
|
|
1157
|
+
if (l + 1 < c.layers) {
|
|
1158
|
+
this.encodeTrunkB(pass, l + 1, T, S);
|
|
1159
|
+
pass.end();
|
|
1160
|
+
enc.copyBufferToBuffer(this.b.sel, 0, this.rbSelB, 0, selBytes);
|
|
1161
|
+
} else {
|
|
1162
|
+
pass.end();
|
|
1163
|
+
enc.copyBufferToBuffer(this.b.x, (T - 1) * H * 4, this.a.x, 0, H * 4);
|
|
1164
|
+
if (want !== 'none') {
|
|
1165
|
+
pass = enc.beginComputePass();
|
|
1166
|
+
this.encodeHead(pass, want);
|
|
1167
|
+
pass.end();
|
|
1168
|
+
if (want === 'argmax') enc.copyBufferToBuffer(this.a.am, 0, this.rbArg, 0, 4);
|
|
1169
|
+
if (want === 'logits') enc.copyBufferToBuffer(this.a.logits, 0, this.rbLogits, 0, c.vocab * 4);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
dev.queue.submit([enc.finish()]);
|
|
1173
|
+
this.xs.release(slotsU);
|
|
1174
|
+
}
|
|
1175
|
+
this.position += T;
|
|
1176
|
+
this.cached.push(...ids);
|
|
1177
|
+
let result = null;
|
|
1178
|
+
if (want === 'argmax') {
|
|
1179
|
+
await this.rbArg.mapAsync(1, 0, 4);
|
|
1180
|
+
result = new Uint32Array(this.rbArg.getMappedRange(0, 4))[0];
|
|
1181
|
+
this.rbArg.unmap();
|
|
1182
|
+
} else if (want === 'logits') {
|
|
1183
|
+
await this.rbLogits.mapAsync(1);
|
|
1184
|
+
result = new Float32Array(this.rbLogits.getMappedRange().slice(0));
|
|
1185
|
+
this.rbLogits.unmap();
|
|
1186
|
+
} else {
|
|
1187
|
+
await dev.queue.onSubmittedWorkDone();
|
|
1188
|
+
}
|
|
1189
|
+
this.counters.tokens += T;
|
|
1190
|
+
this.counters.wallMs += performance.now() - tStart;
|
|
1191
|
+
return result;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// Feeds `ids` after the cached prefix they share; KV past the prefix is simply overwritten (no recurrent
|
|
1195
|
+
// state to rewind). Chunks of up to chunkTokens when there are at least two new tokens.
|
|
1196
|
+
async prefill(ids, want = 'argmax') {
|
|
1197
|
+
let common = 0;
|
|
1198
|
+
while (common < ids.length - 1 && common < this.cached.length && this.cached[common] === ids[common]) common++;
|
|
1199
|
+
if (common < this.cached.length) { this.position = common; this.cached.length = common; }
|
|
1200
|
+
const rest = ids.slice(common);
|
|
1201
|
+
if (!this.batchPrefill || rest.length < 2) return super.prefill(ids, want);
|
|
1202
|
+
let r = null;
|
|
1203
|
+
for (let i = 0; i < rest.length; i += this.chunkTokens) {
|
|
1204
|
+
r = await this.prefillChunk(rest.slice(i, i + this.chunkTokens), i + this.chunkTokens >= rest.length ? want : 'none');
|
|
1205
|
+
}
|
|
1206
|
+
return r;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
encodeEmbed(pass) { this.dispatch(pass, 'embedQ6K', this.g.embed, Math.ceil(this.cfg.hidden / 256)); }
|
|
1210
|
+
encodeHead(pass, want) {
|
|
1211
|
+
const c = this.cfg;
|
|
1212
|
+
this.dispatch(pass, 'rmsnorm', this.g.rmsOut, 1);
|
|
1213
|
+
this.dispatch(pass, 'matmulQ6K', this.g.lm, Math.ceil(c.vocab / 4));
|
|
1214
|
+
if (c.softcap) this.dispatch(pass, 'softcap', this.g.cap, Math.ceil(c.vocab / 256));
|
|
1215
|
+
if (want === 'argmax') this.dispatch(pass, 'argmax', this.g.am, 1);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// Attention, the dense MLP and the router of layer l (all before the routing readback).
|
|
1219
|
+
encodeAttention(pass, l, seqLen) {
|
|
1220
|
+
const c = this.cfg, g = this.layers[l], d = (n, gr, x, y, z) => this.dispatch(pass, n, gr, x, y, z);
|
|
1221
|
+
const hd = c.headDim[l], QN = c.heads * hd, kvn = c.kvHeads[l] * hd;
|
|
1222
|
+
const n = c.swa[l] ? Math.min(seqLen, c.window) : seqLen;
|
|
1223
|
+
d('rmsnorm', g.rmsA, 1);
|
|
1224
|
+
d('matmulQ4', g.q, Math.ceil(QN / 4)); d('matmulQ4', g.k, Math.ceil(kvn / 4));
|
|
1225
|
+
if (g.hasV) d('matmulQ4', g.v, Math.ceil(kvn / 4));
|
|
1226
|
+
d('vNorm', g.vn, c.kvHeads[l]);
|
|
1227
|
+
d('qkNormRope', g.ropeQ, c.heads); d('qkNormRope', g.ropeK, c.kvHeads[l]);
|
|
1228
|
+
d('kvStore', g.kv, Math.ceil(kvn / 256));
|
|
1229
|
+
d('attnScoreW', g.score, Math.ceil(c.heads * n / 256));
|
|
1230
|
+
d('softmaxW', g.soft, c.heads);
|
|
1231
|
+
d('attnOutW', g.attOut, Math.ceil(QN / 256));
|
|
1232
|
+
d('matmulQ4', g.o, Math.ceil(c.hidden / 4));
|
|
1233
|
+
d('rmsnormAdd', g.postA, 1);
|
|
1234
|
+
if (g.capA) d('copyAt', g.capA, Math.ceil(c.hidden / 256));
|
|
1235
|
+
d('rmsnorm', g.rmsF, 1);
|
|
1236
|
+
d('matmulQ4', g.mg, Math.ceil(c.denseFf / 4)); d('matmulQ4', g.mu, Math.ceil(c.denseFf / 4));
|
|
1237
|
+
d('geglu', g.mact, Math.ceil(c.denseFf / 256));
|
|
1238
|
+
d('matmulQ4', g.md, Math.ceil(c.hidden / 4));
|
|
1239
|
+
d('rmsnorm', g.mpost, 1);
|
|
1240
|
+
d('rmsnorm', g.xm, 1);
|
|
1241
|
+
if (this.prefetch) for (const p of g.pf) { d('rmsnormK', p.rp, 1); d('matmulF32', p.router, c.experts); d('topk', p.topk, 1); }
|
|
1242
|
+
d('rmsnormK', g.rp, 1);
|
|
1243
|
+
d('matmulF32', g.router, c.experts);
|
|
1244
|
+
d('topk', g.topk, 1);
|
|
1245
|
+
}
|
|
1246
|
+
encodeExperts(pass, l) {
|
|
1247
|
+
const c = this.cfg, g = this.layers[l], d = (n, gr, x, y, z) => this.dispatch(pass, n, gr, x, y, z);
|
|
1248
|
+
d('expertQ4', g.gu, Math.ceil(2 * c.expertFf / 4), 1, c.topK);
|
|
1249
|
+
d('geglu', g.act, Math.ceil(c.topK * c.expertFf / 256));
|
|
1250
|
+
d('expertQ4', g.dn, Math.ceil(c.hidden / 4), 1, c.topK);
|
|
1251
|
+
d('moeSum', g.sum, Math.ceil(c.hidden / 256));
|
|
1252
|
+
d('rmsnorm', g.mpost2, 1);
|
|
1253
|
+
d('postFfn', g.post, 1);
|
|
1254
|
+
if (g.capX) d('copyAt', g.capX, Math.ceil(c.hidden / 256));
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
// NeoX RoPE angles as llama.cpp's Metal kernel forms them: f32 pos × base^(−2i/d), ÷ the frequency factor
|
|
1258
|
+
// on full-attention layers. One table per layer kind; [0, d/2) = cos, [d/2, d) = sin.
|
|
1259
|
+
// KV cache bytes: per-layer head counts and sizes (sliding and full-attention layers differ).
|
|
1260
|
+
kvBytes() { const c = this.cfg; return c.headDim.reduce((sum, d, l) => sum + 2 * 2 * this.maxCtx * c.kvHeads[l] * d, 0); }
|
|
1261
|
+
ropeTable(d, base, ff, pos) {
|
|
1262
|
+
const half = d / 2, r = new Float32Array(d), inv = Math.fround(-1 / d);
|
|
1263
|
+
for (let i = 0; i < half; i++) {
|
|
1264
|
+
let th = Math.fround(pos * Math.fround(Math.pow(base, Math.fround(inv * 2 * i))));
|
|
1265
|
+
if (ff) th = Math.fround(th / ff[i]);
|
|
1266
|
+
r[i] = Math.cos(th); r[half + i] = Math.sin(th);
|
|
1267
|
+
}
|
|
1268
|
+
return r;
|
|
1269
|
+
}
|
|
1270
|
+
writeTokenUniforms(token, pos) {
|
|
1271
|
+
const c = this.cfg;
|
|
1272
|
+
this.device.queue.writeBuffer(this.tok, 0, new Uint32Array([token, pos, pos + 1, 0]));
|
|
1273
|
+
const ls = c.swa.indexOf(true), lf = c.swa.indexOf(false);
|
|
1274
|
+
if (ls >= 0) this.device.queue.writeBuffer(this.a.ropeS, 0, this.ropeTable(c.headDim[ls], c.ropeTheta[ls], null, pos));
|
|
1275
|
+
if (lf >= 0) this.device.queue.writeBuffer(this.a.ropeF, 0, this.ropeTable(c.headDim[lf], c.ropeTheta[lf], this.ropeFreqs, pos));
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// Debug: the last step's per-layer states — attn_out (after the attention residual) and each layer's output.
|
|
1279
|
+
async readCapture() {
|
|
1280
|
+
const c = this.cfg, n = 2 * c.layers * c.hidden * 4;
|
|
1281
|
+
const rb = this.device.createBuffer({ size: n, usage: MAP_READ | COPY_DST });
|
|
1282
|
+
const enc = this.device.createCommandEncoder();
|
|
1283
|
+
enc.copyBufferToBuffer(this.a.cap, 0, rb, 0, n);
|
|
1284
|
+
this.device.queue.submit([enc.finish()]);
|
|
1285
|
+
await rb.mapAsync(1);
|
|
1286
|
+
const all = new Float32Array(rb.getMappedRange().slice(0));
|
|
1287
|
+
rb.unmap(); rb.destroy();
|
|
1288
|
+
const H = c.hidden, L = c.layers;
|
|
1289
|
+
return { attnOut: Array.from({ length: L }, (_, l) => all.subarray(l * H, (l + 1) * H)), layers: Array.from({ length: L }, (_, l) => all.subarray((L + l) * H, (L + l + 1) * H)) };
|
|
1290
|
+
}
|
|
1291
|
+
}
|