Spaces:
Running
Running
pk_sft_v1 F16 (97.5M parameters, 221 MB) with ember.cpp 9d993f48de: greedy F16 = F32, WebAssembly = native, WebGPU = WebAssembly 40/40 (experiments/release_pk_sft_v1_q8_2026-10-28)
8e107b9 verified Download webgpu/ir_webgpu.js from johnnyteutonic/ember-chat: direct link, hf CLI and curl.
- Browser
- Download file 62.8 kB
-
https://huggingface.co/spaces/johnnyteutonic/ember-chat/resolve/main/webgpu/ir_webgpu.js
- Command line
-
hf download hf://spaces/johnnyteutonic/ember-chat/webgpu/ir_webgpu.js
-
curl -L -o ir_webgpu.js https://huggingface.co/spaces/johnnyteutonic/ember-chat/resolve/main/webgpu/ir_webgpu.js
62.8 kB
| // The paperkiln IR (keramos IR_SPEC) on WebGPU, for ember's browser chat: the documents ember's | |
| // interpreter runs (paperkiln's families and the third-party graphs ember builds from llama.cpp | |
| // metadata) as WGSL, one row per token. The llama path (llama_webgpu.js) stays as it is for the | |
| // files ember serves on its tuned llama path; this module takes every other document whose ops it | |
| // has, and anything else stays in WebAssembly. | |
| // | |
| // How it runs. At load the three node lists (embed, block x repeat, head) are walked once with the | |
| // document's own names: every write gets a fresh GPU buffer (so no dispatch reads and writes one | |
| // buffer), per-layer attributes and `{i}` (weight_index, tensor_index) are resolved, and the result | |
| // is one list of dispatches per token with bind groups made once. Per token only a small step | |
| // buffer (position, token, the turn_state code of the position) changes; the logits of the last | |
| // token of a call are read back. Prefill runs token by token (T = 1 per step), which the IR | |
| // defines to equal prefill (IR_SPEC 3.9, 6.3); recurrent state stays on the GPU between calls. | |
| // | |
| // Numerics follow ember's CPU interpreter op for op (graph_ir.cpp, graph_ir_ops.cpp, | |
| // graph_ir_mamba.cpp; IR_SPEC sections 5-7) in f32. Reductions (matrix products, norms, softmax | |
| // sums) use other summation orders, so results are ROUNDING against the CPU (IR_SPEC 4.2, 4.3): | |
| // gated on greedy tokens, the argmax at every position and logits to rounding | |
| // (tools/webgpu_parity/ir_gate.mjs). | |
| // | |
| // What it refuses (irSupport, then the worker falls back to WebAssembly): recurrence nodes, rope | |
| // heads "first", lists that fork, depth_attend with more than two sources beside its list, expr with | |
| // more than 9 inputs, stored types other than F32, F16, BF16 and Q8_0 for matrices (vectors are read | |
| // at load for every type gguf.js reads), and attention contexts past MAX_KEYS positions. | |
| import { GGML_F16, GGML_F32, GGML_Q8_0, f16ToF32, readGguf, tensorF32 } from './gguf.js'; | |
| const GGML_BF16 = 30; | |
| const WG = 64; | |
| const RG = 256; // single-workgroup reductions | |
| export const MAX_KEYS = 4096; // attention's workgroup score array (16 KB) | |
| const MAXWG = 65535; | |
| const SUPPORTED = new Set(['embedding', 'pos_embedding', 'linear', 'scale', 'split', 'rmsnorm', 'layernorm', 'gelu', | |
| 'silu', 'sigmoid', 'relu', 'add', 'mul', 'sub', 'affine', 'mul_col', 'rms_row', 'rope', 'attention', 'moe_ffn', | |
| 'list', 'append', 'depth_attend', 'expr', 'linear_attention', 'ssm_scan', 'conv1d_causal', 'selective_scan', | |
| 'depth_route']); | |
| // ---- WGSL ---------------------------------------------------------------------------------------- | |
| const MATH = ` | |
| fn tanh_s(x: f32) -> f32 { let e = exp(-2.0 * abs(x)); let t = (1.0 - e) / (1.0 + e); return select(-t, t, x >= 0.0); } | |
| fn erf_s(x: f32) -> f32 { // Numerical Recipes' erfc (fractional error < 1.2e-7) | |
| let z = abs(x); let t = 1.0 / (1.0 + 0.5 * z); | |
| let r = t * exp(-z * z - 1.26551223 + t * (1.00002368 + t * (0.37409196 + t * (0.09678418 + t * (-0.18628806 + | |
| t * (0.27886807 + t * (-1.13520398 + t * (1.48851587 + t * (-0.82215223 + t * 0.17087277))))))))); | |
| return select(r - 1.0, 1.0 - r, x >= 0.0); | |
| } | |
| fn log1p_s(x: f32) -> f32 { let u = 1.0 + x; if (u == 1.0) { return x; } return log(u) * x / (u - 1.0); } | |
| fn pow_s(x: f32, y: f32) -> f32 { | |
| if (y == 0.0) { return 1.0; } | |
| let r = exp2(y * log2(abs(x))); | |
| if (x < 0.0) { | |
| if (fract(y) != 0.0) { return (x - x) / (x - x); } | |
| return select(r, -r, abs(y) % 2.0 == 1.0); | |
| } | |
| return r; | |
| } | |
| `; | |
| // Matrices are stored TRANSPOSED at load ([in][out]: element (row r, column c) at c * rows + r), so | |
| // one invocation per output row reads consecutive addresses across a workgroup (coalesced on a GPU, | |
| // and no barriers: a software adapter runs these about 30x faster than a workgroup per row). By | |
| // stored type (override WT): 0 f32 bits, 1 f16 pairs, 2 Q8_0 (int8 packed four to a word, the f32 | |
| // scale of the row's block of 32 columns at (c / 32) * rows + r), 3 bf16 pairs. Each element is the | |
| // IR_SPEC 4.7 value, exactly. | |
| const WVAL = ` | |
| override WT: u32 = 0u; | |
| fn wv(c: u32, r: u32, rows: u32) -> f32 { | |
| let i = c * rows + r; | |
| if (WT == 0u) { return bitcast<f32>(A[i]); } | |
| if (WT == 1u) { let h = unpack2x16float(A[i >> 1u]); return select(h.x, h.y, (i & 1u) == 1u); } | |
| if (WT == 2u) { let q = f32(i32(A[i >> 2u] << (24u - 8u * (i & 3u))) >> 24u); return D[(c >> 5u) * rows + r] * q; } | |
| return bitcast<f32>(((A[i >> 1u] >> (16u * (i & 1u))) & 0xffffu) << 16u); | |
| }`; | |
| const P = `@group(0) @binding(0) var<storage, read> p: array<u32>;\nfn pf(k: u32) -> f32 { return bitcast<f32>(p[k]); }\n`; | |
| export const IR_SHADERS = { | |
| // p: d, vocab, has_state; st: [pos, token, ...] | |
| embed: `${P} | |
| @group(0) @binding(1) var<storage, read> st: array<u32>; | |
| @group(0) @binding(2) var<storage, read> A: array<u32>; | |
| @group(0) @binding(3) var<storage, read> D: array<f32>; | |
| @group(0) @binding(4) var<storage, read> SW: array<f32>; | |
| @group(0) @binding(5) var<storage, read_write> y: array<f32>; | |
| ${WVAL} | |
| @compute @workgroup_size(${WG}) fn main(@builtin(global_invocation_id) g: vec3u) { | |
| let d = p[0]; let tok = st[1]; | |
| if (g.x >= d) { return; } | |
| if (p[2] != 0u && tok >= p[1]) { y[g.x] = SW[(tok - p[1]) * d + g.x]; } else { y[g.x] = wv(g.x, tok, p[3]); } | |
| }`, | |
| // p: d ; y = x + P[pos] | |
| pos_embed: `${P} | |
| @group(0) @binding(1) var<storage, read> st: array<u32>; | |
| @group(0) @binding(2) var<storage, read> x: array<f32>; | |
| @group(0) @binding(3) var<storage, read> PE: array<f32>; | |
| @group(0) @binding(4) var<storage, read_write> y: array<f32>; | |
| @compute @workgroup_size(${WG}) fn main(@builtin(global_invocation_id) g: vec3u) { | |
| if (g.x < p[0]) { y[g.x] = x[g.x] + PE[st[0] * p[0] + g.x]; } | |
| }`, | |
| // p: rows, cols | |
| matvec: `${P} | |
| @group(0) @binding(1) var<storage, read> A: array<u32>; | |
| @group(0) @binding(2) var<storage, read> D: array<f32>; | |
| @group(0) @binding(3) var<storage, read> x: array<f32>; | |
| @group(0) @binding(4) var<storage, read_write> y: array<f32>; | |
| ${WVAL} | |
| @compute @workgroup_size(${WG}) fn main(@builtin(global_invocation_id) g: vec3u) { | |
| let rows = p[0]; let cols = p[1]; | |
| let row = g.y * ${MAXWG * WG}u + g.x; | |
| if (row >= rows) { return; } | |
| var s = 0.0; | |
| for (var c = 0u; c < cols; c++) { s += wv(c, row, rows) * x[c]; } | |
| y[row] = s; | |
| }`, | |
| // p: n ; y += b | |
| addbias: `${P} | |
| @group(0) @binding(1) var<storage, read> b: array<f32>; | |
| @group(0) @binding(2) var<storage, read_write> y: array<f32>; | |
| @compute @workgroup_size(${WG}) fn main(@builtin(global_invocation_id) g: vec3u) { if (g.x < p[0]) { y[g.x] = y[g.x] + b[g.x]; } }`, | |
| // p: n, f0, f1 ; OP: 0 silu, 1 sigmoid, 2 relu, 3 gelu tanh, 4 gelu erf, 5 affine | |
| ew1: `${P}${MATH} | |
| override OP: u32 = 0u; | |
| @group(0) @binding(1) var<storage, read> x: array<f32>; | |
| @group(0) @binding(2) var<storage, read_write> y: array<f32>; | |
| @compute @workgroup_size(${WG}) fn main(@builtin(global_invocation_id) g: vec3u) { | |
| if (g.x >= p[0]) { return; } | |
| let v = x[g.x]; | |
| var r = 0.0; | |
| if (OP == 0u) { let sg = 1.0 / (1.0 + exp(-v)); r = v * sg; } | |
| else if (OP == 1u) { r = 1.0 / (1.0 + exp(-v)); } | |
| else if (OP == 2u) { r = select(0.0, v, v > 0.0); } | |
| else if (OP == 3u) { let c = 0.7978845608028654; let v3 = 0.044715 * v * v * v; r = 0.5 * v * (1.0 + tanh_s(c * (v + v3))); } | |
| else if (OP == 4u) { r = 0.5 * v * (1.0 + erf_s(v / 1.4142135623730951)); } | |
| else { let pr = v * pf(1u); r = pr + pf(2u); } | |
| y[g.x] = r; | |
| }`, | |
| // p: n ; OP: 0 add, 1 mul, 2 sub | |
| ew2: `${P} | |
| override OP: u32 = 0u; | |
| @group(0) @binding(1) var<storage, read> a: array<f32>; | |
| @group(0) @binding(2) var<storage, read> b: array<f32>; | |
| @group(0) @binding(3) var<storage, read_write> y: array<f32>; | |
| @compute @workgroup_size(${WG}) fn main(@builtin(global_invocation_id) g: vec3u) { | |
| if (g.x >= p[0]) { return; } | |
| if (OP == 0u) { y[g.x] = a[g.x] + b[g.x]; } else if (OP == 1u) { y[g.x] = a[g.x] * b[g.x]; } else { y[g.x] = a[g.x] - b[g.x]; } | |
| }`, | |
| // p: n ; y = x * c[0] | |
| mulcol: `${P} | |
| @group(0) @binding(1) var<storage, read> x: array<f32>; | |
| @group(0) @binding(2) var<storage, read> c: array<f32>; | |
| @group(0) @binding(3) var<storage, read_write> y: array<f32>; | |
| @compute @workgroup_size(${WG}) fn main(@builtin(global_invocation_id) g: vec3u) { if (g.x < p[0]) { y[g.x] = x[g.x] * c[0]; } }`, | |
| // p: n, has_bias ; y = (x * g) + b | |
| scale: `${P} | |
| @group(0) @binding(1) var<storage, read> x: array<f32>; | |
| @group(0) @binding(2) var<storage, read> w: array<f32>; | |
| @group(0) @binding(3) var<storage, read> b: array<f32>; | |
| @group(0) @binding(4) var<storage, read_write> y: array<f32>; | |
| @compute @workgroup_size(${WG}) fn main(@builtin(global_invocation_id) g: vec3u) { | |
| if (g.x >= p[0]) { return; } | |
| let pr = x[g.x] * w[g.x]; | |
| if (p[1] != 0u) { y[g.x] = pr + b[g.x]; } else { y[g.x] = pr; } | |
| }`, | |
| // p: n, src offset, dst offset | |
| copy: `${P} | |
| @group(0) @binding(1) var<storage, read> x: array<f32>; | |
| @group(0) @binding(2) var<storage, read_write> y: array<f32>; | |
| @compute @workgroup_size(${WG}) fn main(@builtin(global_invocation_id) g: vec3u) { if (g.x < p[0]) { y[p[2] + g.x] = x[p[1] + g.x]; } }`, | |
| // p: g, eps ; one workgroup per group of g columns, weight [g] shared | |
| rmsnorm: `${P} | |
| @group(0) @binding(1) var<storage, read> x: array<f32>; | |
| @group(0) @binding(2) var<storage, read> w: array<f32>; | |
| @group(0) @binding(3) var<storage, read_write> y: array<f32>; | |
| var<workgroup> part: array<f32, ${RG}>; | |
| @compute @workgroup_size(${RG}) fn main(@builtin(workgroup_id) wg: vec3u, @builtin(local_invocation_id) l: vec3u) { | |
| let g = p[0]; let o = wg.x * g; | |
| var s = 0.0; | |
| for (var i = l.x; i < g; i += ${RG}u) { s += x[o + i] * x[o + i]; } | |
| part[l.x] = s; | |
| workgroupBarrier(); | |
| for (var k = ${RG / 2}u; k > 0u; k >>= 1u) { if (l.x < k) { part[l.x] += part[l.x + k]; } workgroupBarrier(); } | |
| let inv = 1.0 / sqrt(part[0] / f32(g) + pf(1u)); | |
| for (var i = l.x; i < g; i += ${RG}u) { y[o + i] = x[o + i] * inv * w[i]; } | |
| }`, | |
| // p: n, eps ; (x - mean) / sqrt(var + eps) * w + b | |
| layernorm: `${P} | |
| @group(0) @binding(1) var<storage, read> x: array<f32>; | |
| @group(0) @binding(2) var<storage, read> w: array<f32>; | |
| @group(0) @binding(3) var<storage, read> b: array<f32>; | |
| @group(0) @binding(4) var<storage, read_write> y: array<f32>; | |
| var<workgroup> part: array<f32, ${RG}>; | |
| @compute @workgroup_size(${RG}) fn main(@builtin(local_invocation_id) l: vec3u) { | |
| let n = p[0]; | |
| var s = 0.0; | |
| for (var i = l.x; i < n; i += ${RG}u) { s += x[i]; } | |
| part[l.x] = s; | |
| workgroupBarrier(); | |
| for (var k = ${RG / 2}u; k > 0u; k >>= 1u) { if (l.x < k) { part[l.x] += part[l.x + k]; } workgroupBarrier(); } | |
| let mean = part[0] / f32(n); | |
| workgroupBarrier(); | |
| var v = 0.0; | |
| for (var i = l.x; i < n; i += ${RG}u) { let c = x[i] - mean; v += c * c; } | |
| part[l.x] = v; | |
| workgroupBarrier(); | |
| for (var k = ${RG / 2}u; k > 0u; k >>= 1u) { if (l.x < k) { part[l.x] += part[l.x + k]; } workgroupBarrier(); } | |
| let inv = 1.0 / sqrt(part[0] / f32(n) + pf(1u)); | |
| for (var i = l.x; i < n; i += ${RG}u) { y[i] = (x[i] - mean) * inv * w[i] + b[i]; } | |
| }`, | |
| // p: w, eps ; [1, w] -> [1, 1]: sqrt(ss / w + eps) | |
| rmsrow: `${P} | |
| @group(0) @binding(1) var<storage, read> x: array<f32>; | |
| @group(0) @binding(2) var<storage, read_write> y: array<f32>; | |
| var<workgroup> part: array<f32, ${RG}>; | |
| @compute @workgroup_size(${RG}) fn main(@builtin(local_invocation_id) l: vec3u) { | |
| let n = p[0]; | |
| var s = 0.0; | |
| for (var i = l.x; i < n; i += ${RG}u) { s += x[i] * x[i]; } | |
| part[l.x] = s; | |
| workgroupBarrier(); | |
| for (var k = ${RG / 2}u; k > 0u; k >>= 1u) { if (l.x < k) { part[l.x] += part[l.x + k]; } workgroupBarrier(); } | |
| if (l.x == 0u) { y[0] = sqrt(part[0] / f32(n) + pf(1u)); } | |
| }`, | |
| // p: heads (of this tensor), head_dim, n_rot, neox ; table [ctx][n_rot/2][cos, sin] | |
| rope: `${P} | |
| @group(0) @binding(1) var<storage, read> st: array<u32>; | |
| @group(0) @binding(2) var<storage, read> cs: array<f32>; | |
| @group(0) @binding(3) var<storage, read> x: array<f32>; | |
| @group(0) @binding(4) var<storage, read_write> y: array<f32>; | |
| @compute @workgroup_size(${WG}) fn main(@builtin(global_invocation_id) g: vec3u) { | |
| let hd = p[1]; let nr = p[2]; let half = nr / 2u; | |
| if (g.x >= p[0] * hd) { return; } | |
| let h = g.x / hd; let i = g.x % hd; let b = h * hd; | |
| if (i >= nr) { y[g.x] = x[g.x]; return; } | |
| let tb = 2u * st[0] * half; | |
| if (p[3] != 0u) { | |
| if (i < half) { let c = cs[tb + 2u * i]; let s = cs[tb + 2u * i + 1u]; y[g.x] = x[b + i] * c - x[b + i + half] * s; } | |
| else { let j = i - half; let c = cs[tb + 2u * j]; let s = cs[tb + 2u * j + 1u]; y[g.x] = x[b + j] * s + x[b + i] * c; } | |
| } else { | |
| let j = i / 2u; let c = cs[tb + 2u * j]; let s = cs[tb + 2u * j + 1u]; | |
| if ((i & 1u) == 0u) { y[g.x] = x[g.x] * c - x[g.x + 1u] * s; } else { y[g.x] = x[g.x - 1u] * s + x[g.x] * c; } | |
| } | |
| }`, | |
| // p: kvd, app, mor_step (0xffffffff: none) ; st: [pos, token, ...]; mor: [depth, gate]; cnt: rows per app | |
| kvstore: `${P} | |
| @group(0) @binding(1) var<storage, read> st: array<u32>; | |
| @group(0) @binding(2) var<storage, read> mor: array<u32>; | |
| @group(0) @binding(3) var<storage, read> cnt: array<u32>; | |
| @group(0) @binding(4) var<storage, read> k: array<f32>; | |
| @group(0) @binding(5) var<storage, read> v: array<f32>; | |
| @group(0) @binding(6) var<storage, read_write> kc: array<f32>; | |
| @group(0) @binding(7) var<storage, read_write> vc: array<f32>; | |
| @compute @workgroup_size(${WG}) fn main(@builtin(global_invocation_id) g: vec3u) { | |
| let kvd = p[0]; | |
| if (g.x >= kvd) { return; } | |
| var row = st[0]; | |
| if (p[2] != 0xffffffffu) { if (mor[0] <= p[2]) { return; } row = cnt[p[1]]; } | |
| kc[row * kvd + g.x] = k[g.x]; vc[row * kvd + g.x] = v[g.x]; | |
| }`, | |
| // p: heads, kv_heads, head_dim, scale, window, sinks, mode (0 causal, 1 window, 2 turn_state, 3 MoR rows), app | |
| attn: `${P} | |
| @group(0) @binding(1) var<storage, read> st: array<u32>; | |
| @group(0) @binding(2) var<storage, read> cnt: array<u32>; | |
| @group(0) @binding(3) var<storage, read> codes: array<u32>; | |
| @group(0) @binding(4) var<storage, read> q: array<f32>; | |
| @group(0) @binding(5) var<storage, read> kc: array<f32>; | |
| @group(0) @binding(6) var<storage, read> vc: array<f32>; | |
| @group(0) @binding(7) var<storage, read_write> o: array<f32>; | |
| var<workgroup> sc: array<f32, MAXK>; | |
| var<workgroup> part: array<f32, ${WG}>; | |
| fn visible(t: u32, pos: u32) -> bool { | |
| let mode = p[6]; | |
| if (mode == 1u) { return t < p[5] || t + p[4] >= pos + 1u; } | |
| if (mode == 2u) { return codes[2u * t + 1u] == codes[2u * pos + 1u] || codes[2u * t] == 1u; } | |
| return true; | |
| } | |
| @compute @workgroup_size(${WG}) fn main(@builtin(workgroup_id) wg: vec3u, @builtin(local_invocation_id) l: vec3u) { | |
| let nh = p[0]; let hd = p[2]; let h = wg.x; | |
| let kvh = h / (nh / p[1]); | |
| let kvd = p[1] * hd; | |
| let qo = h * hd; let ko = kvh * hd; | |
| let pos = st[0]; | |
| var n = pos + 1u; | |
| if (p[6] == 3u) { n = cnt[p[7]] + 1u; } | |
| let scale = pf(3u); | |
| var m = -3.40282e38; | |
| for (var t = l.x; t < n; t += ${WG}u) { | |
| if (visible(t, pos)) { | |
| var s = 0.0; | |
| for (var d = 0u; d < hd; d++) { s += q[qo + d] * kc[t * kvd + ko + d]; } | |
| s = s * scale; sc[t] = s; m = max(m, s); | |
| } | |
| } | |
| part[l.x] = m; | |
| workgroupBarrier(); | |
| for (var k = ${WG / 2}u; k > 0u; k >>= 1u) { if (l.x < k) { part[l.x] = max(part[l.x], part[l.x + k]); } workgroupBarrier(); } | |
| let mx = part[0]; | |
| workgroupBarrier(); | |
| var z = 0.0; | |
| for (var t = l.x; t < n; t += ${WG}u) { | |
| var e = 0.0; | |
| if (visible(t, pos)) { e = exp(sc[t] - mx); } | |
| sc[t] = e; z += e; | |
| } | |
| part[l.x] = z; | |
| workgroupBarrier(); | |
| for (var k = ${WG / 2}u; k > 0u; k >>= 1u) { if (l.x < k) { part[l.x] += part[l.x + k]; } workgroupBarrier(); } | |
| let zz = part[0]; | |
| workgroupBarrier(); | |
| for (var t = l.x; t < n; t += ${WG}u) { sc[t] = sc[t] / zz; } | |
| workgroupBarrier(); | |
| for (var d = l.x; d < hd; d += ${WG}u) { | |
| var acc = 0.0; | |
| for (var t = 0u; t < n; t++) { let a = sc[t]; if (a != 0.0) { acc += a * vc[t * kvd + ko + d]; } } | |
| o[qo + d] = acc; | |
| } | |
| }`, | |
| // attn with Shaw's relative key and value vectors (IR_SPEC 5.15 relative_kv, revision 1.4). p: attn's eight, then | |
| // max_distance K, the key table's width, the value table's (0: no values) and where the value table starts in | |
| // rel (the key table then the value table, one buffer: nine storage buffers). Rows are positions (modes 0-2), so | |
| // the distance of row t is min(pos - t, K); a table's row n holds head h's vector at column h * hd when the width | |
| // is heads * hd, at column 0 when it is hd. s = ((q . k) + (q . rk[n])) * scale; o = (sum a v) + (sum a rv[n]). | |
| attn_rel: `${P} | |
| @group(0) @binding(1) var<storage, read> st: array<u32>; | |
| @group(0) @binding(2) var<storage, read> cnt: array<u32>; | |
| @group(0) @binding(3) var<storage, read> codes: array<u32>; | |
| @group(0) @binding(4) var<storage, read> q: array<f32>; | |
| @group(0) @binding(5) var<storage, read> kc: array<f32>; | |
| @group(0) @binding(6) var<storage, read> vc: array<f32>; | |
| @group(0) @binding(7) var<storage, read_write> o: array<f32>; | |
| @group(0) @binding(8) var<storage, read> rel: array<f32>; | |
| var<workgroup> sc: array<f32, MAXK>; | |
| var<workgroup> part: array<f32, ${WG}>; | |
| fn visible(t: u32, pos: u32) -> bool { | |
| let mode = p[6]; | |
| if (mode == 1u) { return t < p[5] || t + p[4] >= pos + 1u; } | |
| if (mode == 2u) { return codes[2u * t + 1u] == codes[2u * pos + 1u] || codes[2u * t] == 1u; } | |
| return true; | |
| } | |
| @compute @workgroup_size(${WG}) fn main(@builtin(workgroup_id) wg: vec3u, @builtin(local_invocation_id) l: vec3u) { | |
| let nh = p[0]; let hd = p[2]; let h = wg.x; | |
| let kvh = h / (nh / p[1]); | |
| let kvd = p[1] * hd; | |
| let qo = h * hd; let ko = kvh * hd; | |
| let K = p[8]; let rkw = p[9]; let rvw = p[10]; let rv0 = p[11]; | |
| let rkc = select(0u, h * hd, rkw != hd); let rvc = select(0u, h * hd, rvw != hd); | |
| let pos = st[0]; | |
| var n = pos + 1u; | |
| if (p[6] == 3u) { n = cnt[p[7]] + 1u; } // never taken (relative_kv is refused under depth_route); keeps the binding | |
| let scale = pf(3u); | |
| var m = -3.40282e38; | |
| for (var t = l.x; t < n; t += ${WG}u) { | |
| if (visible(t, pos)) { | |
| let dr = min(pos - t, K) * rkw + rkc; | |
| var s = 0.0; | |
| for (var d = 0u; d < hd; d++) { s += q[qo + d] * kc[t * kvd + ko + d]; } | |
| var r = 0.0; | |
| for (var d = 0u; d < hd; d++) { r += q[qo + d] * rel[dr + d]; } | |
| s = (s + r) * scale; sc[t] = s; m = max(m, s); | |
| } | |
| } | |
| part[l.x] = m; | |
| workgroupBarrier(); | |
| for (var k = ${WG / 2}u; k > 0u; k >>= 1u) { if (l.x < k) { part[l.x] = max(part[l.x], part[l.x + k]); } workgroupBarrier(); } | |
| let mx = part[0]; | |
| workgroupBarrier(); | |
| var z = 0.0; | |
| for (var t = l.x; t < n; t += ${WG}u) { | |
| var e = 0.0; | |
| if (visible(t, pos)) { e = exp(sc[t] - mx); } | |
| sc[t] = e; z += e; | |
| } | |
| part[l.x] = z; | |
| workgroupBarrier(); | |
| for (var k = ${WG / 2}u; k > 0u; k >>= 1u) { if (l.x < k) { part[l.x] += part[l.x + k]; } workgroupBarrier(); } | |
| let zz = part[0]; | |
| workgroupBarrier(); | |
| for (var t = l.x; t < n; t += ${WG}u) { sc[t] = sc[t] / zz; } | |
| workgroupBarrier(); | |
| for (var d = l.x; d < hd; d += ${WG}u) { | |
| var acc = 0.0; | |
| for (var t = 0u; t < n; t++) { let a = sc[t]; if (a != 0.0) { acc += a * vc[t * kvd + ko + d]; } } | |
| if (rvw != 0u) { | |
| var racc = 0.0; | |
| for (var t = 0u; t < n; t++) { let a = sc[t]; if (a != 0.0) { racc += a * rel[rv0 + min(pos - t, K) * rvw + rvc + d]; } } | |
| acc = acc + racc; | |
| } | |
| o[qo + d] = acc; | |
| } | |
| }`, | |
| // p: E, k, renorm ; top-k router logits (ties to the lower index), ascending expert order, gates | |
| moe_select: `${P} | |
| @group(0) @binding(1) var<storage, read> r: array<f32>; | |
| @group(0) @binding(2) var<storage, read_write> sel: array<u32>; | |
| @group(0) @binding(3) var<storage, read_write> gate: array<f32>; | |
| @compute @workgroup_size(1) fn main() { | |
| let E = p[0]; let K = p[1]; | |
| var taken: array<bool, 256>; | |
| var pick: array<u32, 16>; | |
| for (var j = 0u; j < K; j++) { | |
| var best = 0xffffffffu; | |
| for (var e = 0u; e < E; e++) { if (!taken[e] && (best == 0xffffffffu || r[e] > r[best])) { best = e; } } | |
| taken[best] = true; pick[j] = best; | |
| } | |
| for (var a = 1u; a < K; a++) { let v = pick[a]; var b = a; while (b > 0u && pick[b - 1u] > v) { pick[b] = pick[b - 1u]; b--; } pick[b] = v; } | |
| var mx = -3.4e38; | |
| var z = 0.0; | |
| if (p[2] != 0u) { | |
| for (var j = 0u; j < K; j++) { mx = max(mx, r[pick[j]]); } | |
| for (var j = 0u; j < K; j++) { z += exp(r[pick[j]] - mx); } | |
| } else { | |
| for (var e = 0u; e < E; e++) { mx = max(mx, r[e]); } | |
| for (var e = 0u; e < E; e++) { z += exp(r[e] - mx); } | |
| } | |
| for (var j = 0u; j < K; j++) { sel[j] = pick[j]; gate[j] = exp(r[pick[j]] - mx) / z; } | |
| }`, | |
| // p: rows, cols ; y[j][row] = W[sel[j]] x, each expert's matrix transposed ([E][cols][rows]) | |
| matvec_exp: `${P} | |
| @group(0) @binding(1) var<storage, read> sel: array<u32>; | |
| @group(0) @binding(2) var<storage, read> W: array<f32>; | |
| @group(0) @binding(3) var<storage, read> x: array<f32>; | |
| @group(0) @binding(4) var<storage, read_write> y: array<f32>; | |
| @compute @workgroup_size(${WG}) fn main(@builtin(global_invocation_id) g: vec3u) { | |
| let row = g.x; let j = g.y; let rows = p[0]; let cols = p[1]; | |
| if (row >= rows) { return; } | |
| let base = sel[j] * rows * cols; | |
| var s = 0.0; | |
| for (var c = 0u; c < cols; c++) { s += W[base + c * rows + row] * x[c]; } | |
| y[j * rows + row] = s; | |
| }`, | |
| // p: n ; g = silu(g) * u | |
| swiglu: `${P} | |
| @group(0) @binding(1) var<storage, read_write> g: array<f32>; | |
| @group(0) @binding(2) var<storage, read> u: array<f32>; | |
| @compute @workgroup_size(${WG}) fn main(@builtin(global_invocation_id) i: vec3u) { | |
| if (i.x < p[0]) { let a = g[i.x]; let sg = 1.0 / (1.0 + exp(-a)); g[i.x] = a * sg * u[i.x]; } | |
| }`, | |
| // p: rows (d), cols (d_ff), k ; y = sum_j gate_j down_{sel_j} h_j, ascending j | |
| moe_down: `${P} | |
| @group(0) @binding(1) var<storage, read> sel: array<u32>; | |
| @group(0) @binding(2) var<storage, read> gate: array<f32>; | |
| @group(0) @binding(3) var<storage, read> W: array<f32>; | |
| @group(0) @binding(4) var<storage, read> h: array<f32>; | |
| @group(0) @binding(5) var<storage, read_write> y: array<f32>; | |
| override K: u32 = 1u; | |
| @compute @workgroup_size(${WG}) fn main(@builtin(global_invocation_id) g: vec3u) { | |
| let row = g.x; let rows = p[0]; let cols = p[1]; | |
| if (row >= rows) { return; } | |
| var acc = 0.0; | |
| for (var j = 0u; j < K; j++) { | |
| let base = sel[j] * rows * cols; | |
| var s = 0.0; | |
| for (var c = 0u; c < cols; c++) { s += W[base + c * rows + row] * h[j * cols + c]; } | |
| acc += gate[j] * s; | |
| } | |
| y[row] = acc; | |
| }`, | |
| // p: d, eps ; sources: NB bank rows, then NE extra tensors (overrides) ; weight [d] | |
| depth_attend: `${P} | |
| override NB: u32 = 1u; | |
| override NE: u32 = 0u; | |
| @group(0) @binding(1) var<storage, read> bank: array<f32>; | |
| @group(0) @binding(2) var<storage, read> e1: array<f32>; | |
| @group(0) @binding(3) var<storage, read> e2: array<f32>; | |
| @group(0) @binding(4) var<storage, read> w: array<f32>; | |
| @group(0) @binding(5) var<storage, read_write> y: array<f32>; | |
| var<workgroup> part: array<f32, ${RG}>; | |
| var<workgroup> la: array<f32, 128>; | |
| fn src(i: u32, j: u32, d: u32) -> f32 { | |
| if (i < NB) { return bank[i * d + j]; } | |
| if (i == NB) { return e1[j]; } | |
| return e2[j]; | |
| } | |
| @compute @workgroup_size(${RG}) fn main(@builtin(local_invocation_id) l: vec3u) { | |
| let d = p[0]; let S = NB + NE; | |
| if (S == 1u) { for (var j = l.x; j < d; j += ${RG}u) { y[j] = src(0u, j, d); } return; } | |
| for (var i = 0u; i < S; i++) { | |
| var s = 0.0; | |
| for (var j = l.x; j < d; j += ${RG}u) { let v = src(i, j, d); s += v * v; } | |
| part[l.x] = s; | |
| workgroupBarrier(); | |
| for (var k = ${RG / 2}u; k > 0u; k >>= 1u) { if (l.x < k) { part[l.x] += part[l.x + k]; } workgroupBarrier(); } | |
| let r = 1.0 / sqrt(part[0] / f32(d) + pf(1u)); | |
| workgroupBarrier(); | |
| var lg = 0.0; | |
| for (var j = l.x; j < d; j += ${RG}u) { let nv = src(i, j, d) * r; lg += nv * w[j]; } | |
| part[l.x] = lg; | |
| workgroupBarrier(); | |
| for (var k = ${RG / 2}u; k > 0u; k >>= 1u) { if (l.x < k) { part[l.x] += part[l.x + k]; } workgroupBarrier(); } | |
| if (l.x == 0u) { la[i] = part[0]; } | |
| workgroupBarrier(); | |
| } | |
| if (l.x == 0u) { | |
| var mx = -3.40282e38; | |
| for (var i = 0u; i < S; i++) { mx = max(mx, la[i]); } | |
| var z = 0.0; | |
| for (var i = 0u; i < S; i++) { la[i] = exp(la[i] - mx); z += la[i]; } | |
| for (var i = 0u; i < S; i++) { la[i] = la[i] / z; } | |
| } | |
| workgroupBarrier(); | |
| for (var j = l.x; j < d; j += ${RG}u) { | |
| var o = src(0u, j, d) * la[0]; | |
| for (var i = 1u; i < S; i++) { let pr = src(i, j, d) * la[i]; o = o + pr; } | |
| y[j] = o; | |
| } | |
| } | |
| `, | |
| // p: w, den_floor ; elu+1 linear attention, num/den updated before the output | |
| linattn: `${P} | |
| @group(0) @binding(1) var<storage, read> q: array<f32>; | |
| @group(0) @binding(2) var<storage, read> k: array<f32>; | |
| @group(0) @binding(3) var<storage, read> v: array<f32>; | |
| @group(0) @binding(4) var<storage, read_write> num: array<f32>; | |
| @group(0) @binding(5) var<storage, read_write> den: array<f32>; | |
| @group(0) @binding(6) var<storage, read_write> y: array<f32>; | |
| fn elu1(x: f32) -> f32 { var r = x; if (r <= 0.0) { r = exp(r) - 1.0; } return r + 1.0; } | |
| @compute @workgroup_size(${WG}) fn main(@builtin(global_invocation_id) g: vec3u) { | |
| let c = g.x; | |
| if (c >= p[0]) { return; } | |
| let pk = elu1(k[c]); | |
| let nm = num[c] + pk * v[c]; let dn = den[c] + pk; | |
| num[c] = nm; den[c] = dn; | |
| y[c] = elu1(q[c]) * nm / max(dn, pf(1u)); | |
| }`, | |
| // p: n ; one workgroup: s_t = A s_{t-1} + b u (ascending j), y = c s_t + d u | |
| ssmscan: `${P} | |
| @group(0) @binding(1) var<storage, read> u: array<f32>; | |
| @group(0) @binding(2) var<storage, read> A: array<f32>; | |
| @group(0) @binding(3) var<storage, read> B: array<f32>; | |
| @group(0) @binding(4) var<storage, read> C: array<f32>; | |
| @group(0) @binding(5) var<storage, read> Dv: array<f32>; | |
| @group(0) @binding(6) var<storage, read_write> s: array<f32>; | |
| @group(0) @binding(7) var<storage, read_write> y: array<f32>; | |
| var<workgroup> old: array<f32, 1024>; | |
| @compute @workgroup_size(${RG}) fn main(@builtin(local_invocation_id) l: vec3u) { | |
| let n = p[0]; | |
| for (var i = l.x; i < n; i += ${RG}u) { old[i] = s[i]; } | |
| workgroupBarrier(); | |
| for (var i = l.x; i < n; i += ${RG}u) { | |
| var acc = 0.0; | |
| for (var j = 0u; j < n; j++) { acc += A[i * n + j] * old[j]; } | |
| acc += B[i] * u[i]; | |
| s[i] = acc; | |
| y[i] = C[i] * acc + Dv[0] * u[i]; | |
| } | |
| }`, | |
| // p: C, K, has_bias ; depthwise causal conv over the K - 1 previous inputs, oldest first | |
| conv1d: `${P} | |
| @group(0) @binding(1) var<storage, read> x: array<f32>; | |
| @group(0) @binding(2) var<storage, read> w: array<f32>; | |
| @group(0) @binding(3) var<storage, read> bias: array<f32>; | |
| @group(0) @binding(4) var<storage, read_write> s: array<f32>; | |
| @group(0) @binding(5) var<storage, read_write> y: array<f32>; | |
| @compute @workgroup_size(${WG}) fn main(@builtin(global_invocation_id) g: vec3u) { | |
| let C = p[0]; let K = p[1]; let c = g.x; | |
| if (c >= C) { return; } | |
| var acc = 0.0; | |
| for (var k = 0u; k < K; k++) { | |
| var v = x[c]; | |
| if (k < K - 1u) { v = s[k * C + c]; } | |
| acc = acc + w[c * K + k] * v; | |
| } | |
| if (p[2] != 0u) { y[c] = acc + bias[c]; } else { y[c] = acc; } | |
| for (var k = 0u; k + 2u < K; k++) { s[k * C + c] = s[(k + 1u) * C + c]; } | |
| if (K > 1u) { s[(K - 2u) * C + c] = x[c]; } | |
| }`, | |
| // p: D, N, has_d, softplus ; Mamba-1's selective scan, one channel per thread | |
| selscan: `${P}${MATH} | |
| @group(0) @binding(1) var<storage, read> x: array<f32>; | |
| @group(0) @binding(2) var<storage, read> dt: array<f32>; | |
| @group(0) @binding(3) var<storage, read> Bm: array<f32>; | |
| @group(0) @binding(4) var<storage, read> Cm: array<f32>; | |
| @group(0) @binding(5) var<storage, read> A: array<f32>; // [D * N] then d [D] | |
| @group(0) @binding(6) var<storage, read_write> s: array<f32>; | |
| @group(0) @binding(7) var<storage, read_write> y: array<f32>; | |
| @compute @workgroup_size(${WG}) fn main(@builtin(global_invocation_id) g: vec3u) { | |
| let D = p[0]; let N = p[1]; let c = g.x; | |
| if (c >= D) { return; } | |
| var delta = dt[c]; | |
| if (p[3] != 0u && delta <= 20.0) { delta = log1p_s(exp(delta)); } | |
| let xd = x[c] * delta; | |
| var acc = 0.0; | |
| for (var n = 0u; n < N; n++) { | |
| let decay = s[c * N + n] * exp(delta * A[c * N + n]); | |
| let nv = decay + Bm[n] * xd; | |
| s[c * N + n] = nv; | |
| acc = acc + nv * Cm[n]; | |
| } | |
| if (p[2] != 0u) { y[c] = acc + A[D * N + c] * x[c]; } else { y[c] = acc; } | |
| }`, | |
| // p: R ; MoR routing from the router logits: mor = [depth, gate bits] | |
| mor_route: `${P} | |
| @group(0) @binding(1) var<storage, read> l: array<f32>; | |
| @group(0) @binding(2) var<storage, read_write> mor: array<u32>; | |
| @compute @workgroup_size(1) fn main() { | |
| let R = p[0]; | |
| var jm = 0u; | |
| for (var j = 1u; j < R; j++) { if (l[j] > l[jm]) { jm = j; } } | |
| var z = 0.0; | |
| for (var j = 0u; j < R; j++) { z += exp(l[j] - l[jm]); } | |
| mor[0] = jm + 1u; mor[1] = bitcast<u32>(exp(l[jm] - l[jm]) / z); | |
| }`, | |
| // p: n, step, app, last_of_step, gate_score ; end of a MoR application: keep x for an inactive | |
| // token, count the row written, and at the step's last application gate x against the step input | |
| mor_end: `${P} | |
| @group(0) @binding(1) var<storage, read> mor: array<u32>; | |
| @group(0) @binding(2) var<storage, read> xprev: array<f32>; | |
| @group(0) @binding(3) var<storage, read> xnew: array<f32>; | |
| @group(0) @binding(4) var<storage, read> xin: array<f32>; | |
| @group(0) @binding(5) var<storage, read_write> cnt: array<u32>; | |
| @group(0) @binding(6) var<storage, read_write> y: array<f32>; | |
| @compute @workgroup_size(${WG}) fn main(@builtin(global_invocation_id) g: vec3u) { | |
| let act = mor[0] > p[1]; | |
| if (g.x == 0u && act) { cnt[p[2]] = cnt[p[2]] + 1u; } | |
| if (g.x >= p[0]) { return; } | |
| if (!act) { y[g.x] = xprev[g.x]; return; } | |
| if (p[3] != 0u && p[4] != 0u) { | |
| let gt = bitcast<f32>(mor[1]); | |
| let diff = xnew[g.x] - xin[g.x]; let pr = gt * diff; y[g.x] = xin[g.x] + pr; | |
| } else { y[g.x] = xnew[g.x]; } | |
| }`, | |
| }; | |
| // ---- expr -> WGSL (IR_SPEC 5.19) ------------------------------------------------------------------- | |
| const EXPR_FN = { exp: 1, log: 1, sqrt: 1, tanh: 1, erf: 1, abs: 1, sigmoid: 1, relu: 1, max: 2, min: 2, where: 3, | |
| rowsum: 1, rowmean: 1, rowmax: 1 }; | |
| function f32lit(v) { | |
| const b = new Uint32Array(new Float32Array([v]).buffer)[0]; | |
| return `bitcast<f32>(0x${b.toString(16)}u)`; | |
| } | |
| // Parses an expr string into WGSL for element j, with row reductions hoisted to workgroup scalars. | |
| export function compileExpr(src, nIn) { | |
| const s = src.replace(/[ \t]/g, ''); | |
| let i = 0; | |
| const reds = []; // {kind, body} | |
| const names = ['x', ...'abcdefghijklmnopqrstuv']; | |
| const fail = (m) => { throw new Error(`expr: ${m} at column ${i + 1}`); }; | |
| const peek = () => s[i]; | |
| function expr() { let a = term(); while (peek() === '+' || peek() === '-') { const op = s[i++]; a = `(${a} ${op} ${term()})`; } return a; } | |
| function term() { let a = unary(); while (peek() === '*' || peek() === '/') { const op = s[i++]; a = `(${a} ${op} ${unary()})`; } return a; } | |
| function unary() { if (peek() === '-') { i++; return `(-${unary()})`; } return power(); } | |
| function power() { | |
| const start = i; | |
| const a = primary(); | |
| if (peek() !== '^') return a; | |
| i++; | |
| const bStart = i; | |
| const b = unary(); | |
| const lit = s.slice(bStart, i); | |
| if (/^[0-9]+$/.test(lit) && Number(lit) >= 1 && Number(lit) <= 4) return '(' + Array(Number(lit)).fill(a).join(' * ') + ')'; | |
| void start; | |
| return `pow_s(${a}, ${b})`; | |
| } | |
| function primary() { | |
| const c = peek(); | |
| if (c === undefined) fail('unexpected end'); | |
| if (c === '(') { i++; const e = expr(); if (s[i++] !== ')') fail("expected ')'"); return e; } | |
| const num = /^(?:\d+\.?\d*(?:[eE][+-]?\d+)?|\.\d+(?:[eE][+-]?\d+)?)/.exec(s.slice(i)); | |
| if (num) { i += num[0].length; const v = Math.fround(parseFloat(num[0])); if (!isFinite(v)) fail('non-finite number'); return f32lit(v); } | |
| const id = /^[A-Za-z_][A-Za-z0-9_]*/.exec(s.slice(i)); | |
| if (!id) fail(`unexpected '${c}'`); | |
| const name = id[0]; | |
| i += name.length; | |
| if (name === 'pi') return f32lit(Math.fround(Math.PI)); | |
| if (peek() === '(' && EXPR_FN[name]) { | |
| i++; | |
| const args = [expr()]; | |
| while (peek() === ',') { i++; args.push(expr()); } | |
| if (s[i++] !== ')') fail("expected ')'"); | |
| if (args.length !== EXPR_FN[name]) fail(`${name} takes ${EXPR_FN[name]} argument(s)`); | |
| const [a, b, cc] = args; | |
| switch (name) { | |
| case 'exp': case 'log': case 'sqrt': case 'abs': return `${name}(${a})`; | |
| case 'tanh': return `tanh_s(${a})`; | |
| case 'erf': return `erf_s(${a})`; | |
| case 'sigmoid': return `(1.0 / (1.0 + exp(-(${a}))))`; | |
| case 'relu': return `relu_s(${a})`; | |
| case 'max': return `max_s(${a}, ${b})`; | |
| case 'min': return `min_s(${a}, ${b})`; | |
| case 'where': return `select(${cc}, ${b}, (${a}) > 0.0)`; | |
| default: { reds.push({ kind: name, body: a }); return name === 'rowmean' ? `(red[${reds.length - 1}] / f32(W))` : `red[${reds.length - 1}]`; } | |
| } | |
| } | |
| const k = names.indexOf(name); | |
| if (k < 0 || k >= nIn) fail(`unknown name '${name}'`); | |
| return `in${k}[j]`; | |
| } | |
| const out = expr(); | |
| if (i !== s.length) fail(`unexpected '${s[i]}'`); | |
| return { out, reds }; | |
| } | |
| export function exprShader(src, nIn) { | |
| const { out, reds } = compileExpr(src, nIn); | |
| const binds = Array.from({ length: nIn }, (_, k) => `@group(0) @binding(${k + 1}) var<storage, read> in${k}: array<f32>;`).join('\n'); | |
| const redCode = reds.map((r, k) => r.kind === 'rowmax' | |
| ? `{ var j = 0u; var m = ${r.body}; for (j = 1u; j < W; j++) { let v = ${r.body}; if (v > m) { m = v; } } red[${k}] = m; }` | |
| : `{ var acc = 0.0; for (var j = 0u; j < W; j++) { acc = acc + (${r.body}); } red[${k}] = acc; }`).join('\n '); | |
| return `${P}${MATH} | |
| ${binds} | |
| @group(0) @binding(${nIn + 1}) var<storage, read_write> y: array<f32>; | |
| var<workgroup> red: array<f32, ${Math.max(1, reds.length)}>; | |
| fn relu_s(e: f32) -> f32 { return select(0.0, e, e > 0.0); } | |
| fn max_s(a: f32, b: f32) -> f32 { return select(b, a, a >= b); } | |
| fn min_s(a: f32, b: f32) -> f32 { return select(b, a, a <= b); } | |
| @compute @workgroup_size(${RG}) fn main(@builtin(local_invocation_id) l: vec3u) { | |
| let W = p[0]; // the row width (binding 0 is read, so the auto layout keeps it) | |
| if (l.x == 0u) { | |
| ${redCode} | |
| } | |
| workgroupBarrier(); | |
| for (var j = l.x; j < W; j += ${RG}u) { y[j] = ${out}; } | |
| }`; | |
| } | |
| // ---- support check ------------------------------------------------------------------------------------ | |
| function nodesOf(doc) { return [...doc.embed.nodes, ...doc.block.nodes, ...doc.head.nodes]; } | |
| // {ok: true} when this module runs the document, else {ok: false, reason}. | |
| export function irSupport(doc) { | |
| try { | |
| if (!doc || !doc.block || !doc.embed || !doc.head) return { ok: false, reason: 'not an IR document' }; | |
| for (const n of nodesOf(doc)) { | |
| if (!SUPPORTED.has(n.op)) return { ok: false, reason: `op '${n.op}' has no WGSL kernel` }; | |
| if (n.op === 'rope' && n.rope_heads === 'first') return { ok: false, reason: 'rope_heads "first"' }; | |
| // T5's relative position bias (IR_SPEC 5.15, revision 1.3) has no WGSL kernel: the CPU interpreter runs it. | |
| if (n.op === 'attention' && n.position_bias !== undefined) return { ok: false, reason: 'attention position_bias' }; | |
| // Shaw's relative vectors (revision 1.4) on positions: not under depth_route (compact rows), as the CPU refuses. | |
| if (n.op === 'attention' && n.relative_kv !== undefined && doc.block.nodes.some((b) => b.op === 'depth_route')) | |
| return { ok: false, reason: 'attention relative_kv under depth_route' }; | |
| if (n.op === 'expr') { | |
| if (n.in.length > 9) return { ok: false, reason: 'expr with more than 9 inputs' }; | |
| compileExpr(n.expr, n.in.length); | |
| } | |
| if (n.op === 'depth_attend' && n.in.length > 3) return { ok: false, reason: 'depth_attend with more than two extra sources' }; | |
| if (n.op === 'moe_ffn' && (n.n_experts > 256 || n.top_k > 16)) return { ok: false, reason: 'moe_ffn past 256 experts or top-16' }; | |
| } | |
| return { ok: true }; | |
| } catch (e) { | |
| return { ok: false, reason: String(e.message || e) }; | |
| } | |
| } | |
| // ---- the model -------------------------------------------------------------------------------------- | |
| // createIr(device, ggufBuffer, doc, {ctx}): doc is the IR document (an object or its JSON text) ember's | |
| // interpreter runs for this file; ctx caps the positions held (ember_ctx(), default model.ctx). | |
| export async function createIr(device, ggufBuffer, docIn, opts = {}) { | |
| const doc = typeof docIn === 'string' ? JSON.parse(docIn) : docIn; | |
| const sup = irSupport(doc); | |
| if (!sup.ok) throw new Error('WebGPU IR path: ' + sup.reason); | |
| const g = readGguf(ggufBuffer); | |
| const model = doc.model, L = doc.block.repeat, vocab = model.vocab; | |
| const ctx = Math.min(model.ctx, opts.ctx > 0 ? opts.ctx : model.ctx); | |
| const S = GPUBufferUsage; | |
| const wmap = doc.block.weight_index ? doc.block.weight_index.per_layer : null; | |
| const at = (v, l) => (v && typeof v === 'object' && Array.isArray(v.per_layer)) ? v.per_layer[l] : v; | |
| const tname = (pat, l, n) => { | |
| if (!pat.includes('{i}')) return pat; | |
| if (l < 0) throw new Error(`'${pat}' uses {i} outside the block`); | |
| const idx = (n && n.tensor_index === 'application') || !wmap ? l : wmap[l]; | |
| return pat.replace('{i}', String(idx)); | |
| }; | |
| const has = (n, k) => n[k] !== undefined && n[k] !== null; | |
| // Buffers. | |
| const allBufs = []; | |
| const mk = (bytes, usage) => { const b = device.createBuffer({ size: Math.max(16, Math.ceil(bytes / 4) * 4), usage }); allBufs.push(b); return b; }; | |
| const storage = (arr) => { | |
| const b = mk(arr.byteLength, S.STORAGE | S.COPY_DST); | |
| device.queue.writeBuffer(b, 0, arr.buffer, arr.byteOffset, arr.byteLength & ~3); | |
| if (arr.byteLength & 3) { // a tail of fewer than 4 bytes | |
| const tail = new Uint8Array(4); tail.set(new Uint8Array(arr.buffer, arr.byteOffset + (arr.byteLength & ~3), arr.byteLength & 3)); | |
| device.queue.writeBuffer(b, arr.byteLength & ~3, tail); | |
| } | |
| return b; | |
| }; | |
| const scratch = (n) => mk(4 * Math.max(1, n), S.STORAGE | S.COPY_SRC | S.COPY_DST); | |
| const words = (vals) => { // a parameter block: numbers (u32) or {f: x} (f32) | |
| const a = new ArrayBuffer(4 * Math.max(4, vals.length)), u = new Uint32Array(a), f = new Float32Array(a); | |
| vals.forEach((v, k) => { if (typeof v === 'object') f[k] = v.f; else u[k] = v >>> 0; }); | |
| return storage(new Uint8Array(a)); | |
| }; | |
| const dummy = scratch(4); | |
| // Weights, by name (a shared stored layer is uploaded once). | |
| const tensor = (name) => { const t = g.tensors[name]; if (!t) throw new Error(`GGUF has no tensor '${name}'`); return t; }; | |
| const vecCache = new Map(), matCache = new Map(); | |
| const vecF32 = (name) => { | |
| const t = tensor(name); | |
| if (t.type === GGML_BF16) { const o = new Float32Array(t.n), u = new Uint32Array(o.buffer), dv = new DataView(g.buf, t.start); for (let k = 0; k < t.n; k++) u[k] = dv.getUint16(2 * k, true) << 16; return o; } | |
| return tensorF32(g, name); | |
| }; | |
| const vec = (name, offset = 0) => { | |
| const key = name + '|' + offset; | |
| if (!vecCache.has(key)) { | |
| let v = vecF32(name); | |
| if (offset) v = v.map((x) => Math.fround(x + offset)); | |
| vecCache.set(key, { buf: storage(v), n: v.length }); | |
| } | |
| return vecCache.get(key); | |
| }; | |
| const mat = (name) => { // {wt, A, D, rows, cols} | |
| if (matCache.has(name)) return matCache.get(name); | |
| const t = tensor(name); | |
| const cols = t.dims[0], rows = t.n / cols; | |
| let m; | |
| if (t.type === GGML_F32 || t.type === GGML_F16 || t.type === GGML_BF16) { | |
| const wide = t.type === GGML_F32; | |
| const al = t.start % (wide ? 4 : 2) === 0; // GGUF data is 32-byte aligned; copy only if not | |
| const src = wide ? (al ? new Uint32Array(g.buf, t.start, t.n) : new Uint32Array(g.buf.slice(t.start, t.start + 4 * t.n))) | |
| : (al ? new Uint16Array(g.buf, t.start, t.n) : new Uint16Array(g.buf.slice(t.start, t.start + 2 * t.n))); | |
| const T = wide ? new Uint32Array(t.n) : new Uint16Array(t.n + 1); | |
| for (let r = 0; r < rows; r++) { const o = r * cols; for (let c = 0; c < cols; c++) T[c * rows + r] = src[o + c]; } | |
| m = { wt: t.type === GGML_F32 ? 0 : t.type === GGML_F16 ? 1 : 3, A: storage(wide ? T : new Uint8Array(T.buffer, 0, 2 * t.n)), D: dummy }; | |
| } else if (t.type === GGML_Q8_0) { | |
| if (cols % 32) throw new Error(`Q8_0 tensor '${name}': ne0 ${cols} is not a multiple of 32`); | |
| const nb = cols / 32, dv = new DataView(g.buf, t.start), raw = new Int8Array(g.buf, t.start, 34 * nb * rows); | |
| const q = new Int8Array(t.n), sc = new Float32Array(nb * rows); | |
| for (let r = 0; r < rows; r++) | |
| for (let b = 0; b < nb; b++) { | |
| const o = 34 * (r * nb + b); | |
| sc[b * rows + r] = f16ToF32(dv.getUint16(o, true)); | |
| for (let j = 0; j < 32; j++) q[(32 * b + j) * rows + r] = raw[o + 2 + j]; | |
| } | |
| m = { wt: 2, A: storage(new Uint8Array(q.buffer)), D: storage(sc) }; | |
| } else throw new Error(`WebGPU IR path: tensor '${name}' has GGML type ${t.type}; matrices must be F32, F16, BF16 or Q8_0`); | |
| Object.assign(m, { rows, cols }); | |
| matCache.set(name, m); | |
| return m; | |
| }; | |
| const expCache = new Map(); | |
| const experts = (name, E) => { // a 3-D stack [E][out][in] as E transposed matrices [E][in][out] | |
| if (!expCache.has(name)) { | |
| const t = tensor(name), cols = t.dims[0], rows = t.dims[1], v = vecF32(name), T = new Float32Array(t.n); | |
| for (let e = 0; e < E; e++) { | |
| const b = e * rows * cols; | |
| for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) T[b + c * rows + r] = v[b + r * cols + c]; | |
| } | |
| expCache.set(name, storage(T)); | |
| } | |
| return expCache.get(name); | |
| }; | |
| const weightName = (n, l) => { | |
| let w = tname(n.weight, l, n); | |
| if (!g.tensors[w] && has(n, 'tied_to')) w = tname(n.tied_to, l, n); | |
| return w; | |
| }; | |
| // Pipelines, by shader and override constants. | |
| const pipes = new Map(); | |
| const pipe = (key, code, constants) => { | |
| const k = key + JSON.stringify(constants || {}); | |
| if (!pipes.has(k)) | |
| pipes.set(k, device.createComputePipeline({ layout: 'auto', compute: { module: device.createShaderModule({ code }), entryPoint: 'main', constants: constants || {} } })); | |
| return pipes.get(k); | |
| }; | |
| const prog = []; | |
| const emit = (key, bufs, n, constants, code) => { | |
| const pl = pipe(key, code || IR_SHADERS[key], constants); | |
| const bg = device.createBindGroup({ layout: pl.getBindGroupLayout(0), entries: bufs.map((b, i) => ({ binding: i, resource: { buffer: b } })) }); | |
| const wx = Array.isArray(n) ? n : [Math.min(n, MAXWG), Math.ceil(n / MAXWG)]; // matvec rows: g.y * MAXWG * WG + g.x | |
| prog.push([pl, bg, wx]); | |
| }; | |
| const groups = (n) => Math.ceil(n / WG); | |
| // Per-token inputs: st = [pos, token], the turn_state codes [ctx][c, s], MoR [depth, gate], rows per app. | |
| const st = mk(16, S.STORAGE | S.COPY_DST); | |
| const codes = scratch(2 * ctx); | |
| const morBuf = scratch(4); | |
| const cnt = scratch(Math.max(4, L)); | |
| // Slots. | |
| const slots = Object.fromEntries((doc.block.state || []).map((s) => [s.name, s])); | |
| const kv = [], rec = []; // per application: {slotName: {kc, vc}} / {slotName: {tensor: buf}} | |
| let maxKeys = 0; | |
| for (let l = 0; l < L; l++) { | |
| kv.push({}); rec.push({}); | |
| for (const s of Object.values(slots)) { | |
| if (s.kind === 'kv') { | |
| const rows = Math.min(ctx, s.max_len || ctx); | |
| maxKeys = Math.max(maxKeys, rows); | |
| const kvd = s.kv_heads * s.head_dim; | |
| kv[l][s.name] = { kc: scratch(rows * kvd), vc: scratch(rows * kvd), rows }; | |
| } else if (s.kind === 'recurrent') { | |
| rec[l][s.name] = Object.fromEntries(s.tensors.map((t) => [t.name, scratch(t.shape.reduce((a, b) => a * b, 1))])); | |
| } | |
| } | |
| } | |
| const keysCap = Math.min(MAX_KEYS, Math.floor((device.limits.maxComputeWorkgroupStorageSize - 4 * WG) / 4)); | |
| if (maxKeys > keysCap) throw new Error(`WebGPU IR path: ${maxKeys} kv positions; this device holds ${keysCap} (cap the context)`); | |
| const keysArr = Math.max(WG, Math.ceil(maxKeys / WG) * WG); | |
| const exprIns = Math.max(0, ...nodesOf(doc).filter((n) => n.op === 'expr').map((n) => n.in.length)); | |
| if (exprIns + 2 > device.limits.maxStorageBuffersPerShaderStage) | |
| throw new Error(`WebGPU IR path: an expr with ${exprIns} inputs needs more storage buffers than the device binds`); | |
| const turnState = doc.block.nodes.some((n) => n.op === 'attention' && n.mask === 'turn_state'); | |
| const stateNode = doc.embed.nodes.find((n) => n.op === 'embedding' && has(n, 'state_weight')); | |
| const stateK = stateNode ? tensor(stateNode.state_weight).n / model.d : 0; | |
| // RoPE tables, per (theta, freq_scale, n_rot), as ember builds them. | |
| const ropeTabs = new Map(); | |
| const ropeTable = (theta, fs, nr) => { | |
| const key = `${theta}|${fs}|${nr}`; | |
| if (!ropeTabs.has(key)) { | |
| const half = nr / 2, cs = new Float32Array(ctx * nr); | |
| for (let pos = 0; pos < ctx; pos++) | |
| for (let i = 0; i < nr; i += 2) { | |
| const freq = Math.fround(1 / Math.fround(Math.pow(Math.fround(theta), Math.fround(i / nr)))); | |
| const val = Math.fround(Math.fround(pos * fs) * freq); | |
| cs[(pos * half + i / 2) * 2] = Math.fround(Math.cos(val)); | |
| cs[(pos * half + i / 2) * 2 + 1] = Math.fround(Math.sin(val)); | |
| } | |
| ropeTabs.set(key, storage(cs)); | |
| } | |
| return ropeTabs.get(key); | |
| }; | |
| // List capacity: list inputs plus every append that can run. | |
| const appends = (nodes) => nodes.filter((n) => n.op === 'append').length; | |
| const listCap = Math.max(1, ...nodesOf(doc).filter((n) => n.op === 'list').map((n) => n.in.length)) + | |
| appends(doc.block.nodes) * L + appends(doc.head.nodes) + appends(doc.embed.nodes); | |
| // MoR (IR_SPEC 7.4): the block's first node, a router at the first application of step 0. | |
| const route = doc.block.nodes[0] && doc.block.nodes[0].op === 'depth_route' ? doc.block.nodes[0] : null; | |
| const stepOf = (l) => route ? at(route.step, l) : -1; | |
| const env = new Map(); | |
| const tens = (n, k) => { | |
| const v = env.get(n.in[k]); | |
| if (!v) throw new Error(`IR op '${n.op}': input '${n.in[k]}' is not defined`); | |
| if (v.list) throw new Error(`IR op '${n.op}': input '${n.in[k]}' is a list`); | |
| return v; | |
| }; | |
| const put = (name, w) => { const v = { buf: scratch(w), w }; env.set(name, v); return v; }; | |
| const frozen = new Set(); | |
| function runNode(n, l) { | |
| const op = n.op, out = Array.isArray(n.out) ? n.out : [n.out]; | |
| if (op === 'embedding') { | |
| const wn = tname(n.weight, l, n), m = mat(wn); | |
| const sw = has(n, 'state_weight') ? vec(n.state_weight).buf : dummy; | |
| const y = put(out[0], m.cols); | |
| emit('embed', [words([m.cols, vocab, has(n, 'state_weight') ? 1 : 0, m.rows]), st, m.A, m.D, sw, y.buf], groups(m.cols), { WT: m.wt }); | |
| } else if (op === 'pos_embedding') { | |
| const x = tens(n, 0), pe = vec(weightName(n, l)); | |
| const y = put(out[0], x.w); | |
| emit('pos_embed', [words([x.w]), st, x.buf, pe.buf, y.buf], groups(x.w)); | |
| } else if (op === 'linear') { | |
| const x = tens(n, 0), m = mat(weightName(n, l)); | |
| if (m.cols !== x.w) throw new Error(`IR linear '${n.weight}': input width ${x.w}, weight ne0 ${m.cols}`); | |
| const y = put(out[0], m.rows); | |
| emit('matvec', [words([m.rows, m.cols]), m.A, m.D, x.buf, y.buf], groups(m.rows), { WT: m.wt }); | |
| if (has(n, 'bias')) emit('addbias', [words([m.rows]), vec(tname(n.bias, l, n)).buf, y.buf], groups(m.rows)); | |
| } else if (op === 'scale') { | |
| const x = tens(n, 0), w = vec(weightName(n, l)); | |
| const b = has(n, 'bias') ? vec(tname(n.bias, l, n)).buf : dummy; | |
| const y = put(out[0], x.w); | |
| emit('scale', [words([x.w, has(n, 'bias') ? 1 : 0]), x.buf, w.buf, b, y.buf], groups(x.w)); | |
| } else if (op === 'split') { | |
| const x = tens(n, 0); | |
| let col = 0; | |
| n.sizes.forEach((sz, k) => { const y = put(out[k], sz); emit('copy', [words([sz, col, 0]), x.buf, y.buf], groups(sz)); col += sz; }); | |
| if (col !== x.w) throw new Error('IR split: sizes do not sum to the width'); | |
| } else if (op === 'rmsnorm') { | |
| const x = tens(n, 0), gsz = has(n, 'group') ? n.group : x.w; | |
| const w = vec(weightName(n, l), has(n, 'weight_offset') ? n.weight_offset : 0); | |
| if (x.w % gsz || w.n !== gsz) throw new Error(`IR rmsnorm: group ${gsz} does not fit width ${x.w}`); | |
| const y = put(out[0], x.w); | |
| emit('rmsnorm', [words([gsz, { f: n.eps ?? 1e-5 }]), x.buf, w.buf, y.buf], [x.w / gsz, 1]); | |
| } else if (op === 'layernorm') { | |
| const x = tens(n, 0), w = vec(weightName(n, l)), b = vec(tname(n.bias, l, n)); | |
| const y = put(out[0], x.w); | |
| emit('layernorm', [words([x.w, { f: n.eps ?? 1e-5 }]), x.buf, w.buf, b.buf, y.buf], [1, 1]); | |
| } else if (['silu', 'sigmoid', 'relu', 'gelu', 'affine'].includes(op)) { | |
| const x = tens(n, 0); | |
| let OP = { silu: 0, sigmoid: 1, relu: 2, affine: 5 }[op]; | |
| if (op === 'gelu') { const ap = n.approx ?? 'none'; if (ap !== 'tanh' && ap !== 'none') throw new Error('gelu approx ' + ap); OP = ap === 'tanh' ? 3 : 4; } | |
| const y = put(out[0], x.w); | |
| const pw = op === 'affine' ? [x.w, { f: at(n.scale, l) }, { f: at(n.shift, l) }] : [x.w]; | |
| emit('ew1', [words(pw), x.buf, y.buf], groups(x.w), { OP }); | |
| } else if (op === 'add' || op === 'mul' || op === 'sub') { | |
| const a = tens(n, 0), b = tens(n, 1); | |
| if (a.w !== b.w) throw new Error(`IR ${op}: operand widths differ`); | |
| const y = put(out[0], a.w); | |
| emit('ew2', [words([a.w]), a.buf, b.buf, y.buf], groups(a.w), { OP: { add: 0, mul: 1, sub: 2 }[op] }); | |
| } else if (op === 'mul_col') { | |
| const x = tens(n, 0), c = tens(n, 1); | |
| const y = put(out[0], x.w); | |
| emit('mulcol', [words([x.w]), x.buf, c.buf, y.buf], groups(x.w)); | |
| } else if (op === 'rms_row') { | |
| const x = tens(n, 0), y = put(out[0], 1); | |
| emit('rmsrow', [words([x.w, { f: n.eps }]), x.buf, y.buf], [1, 1]); | |
| } else if (op === 'rope') { | |
| const q = tens(n, 0), k = tens(n, 1), hd = n.head_dim, nr = n.n_rot ?? hd; | |
| const tab = ropeTable(at(n.theta, l), at(n.freq_scale, l) ?? 1, nr); | |
| const neox = n.pairing === 'neox' ? 1 : 0; | |
| if (n.pairing !== 'neox' && n.pairing !== 'interleaved') throw new Error('rope pairing ' + n.pairing); | |
| const qo = put(out[0], q.w), ko = put(out[1], k.w); | |
| emit('rope', [words([q.w / hd, hd, nr, neox]), st, tab, q.buf, qo.buf], groups(q.w)); | |
| emit('rope', [words([k.w / hd, hd, nr, neox]), st, tab, k.buf, ko.buf], groups(k.w)); | |
| } else if (op === 'attention') { | |
| const q = tens(n, 0), k = tens(n, 1), v = tens(n, 2), slot = slots[n.state], c = kv[l][n.state]; | |
| const hd = n.head_dim, nh = n.heads, nkv = n.kv_heads ?? nh; | |
| if (nkv !== slot.kv_heads || hd !== slot.head_dim) throw new Error('IR attention shape differs from its kv slot'); | |
| const s = stepOf(l); | |
| const window = at(has(n, 'window') ? n.window : at(slot.window, l), l) || 0; | |
| const sinks = at(has(n, 'sinks') ? n.sinks : at(slot.sinks, l), l) || 0; | |
| const mode = s >= 0 ? 3 : n.mask === 'turn_state' ? 2 : window > 0 ? 1 : 0; | |
| const scale = has(n, 'scale') ? n.scale : Math.fround(1 / Math.sqrt(hd)); | |
| emit('kvstore', [words([nkv * hd, l, s >= 0 ? s : 0xffffffff]), st, morBuf, cnt, k.buf, v.buf, c.kc, c.vc], groups(nkv * hd)); | |
| const y = put(out[0], nh * hd); | |
| if (has(n, 'relative_kv')) { // Shaw's vectors (revision 1.4): the tables [K + 1, w], w hd or heads * hd | |
| const rel = n.relative_kv, K = rel.max_distance; | |
| const table = (nm) => { | |
| const t = tensor(tname(nm, l, n)); | |
| if (t.dims.length !== 2 || t.dims[1] !== K + 1 || (t.dims[0] !== hd && t.dims[0] !== nh * hd)) | |
| throw new Error(`IR attention relative_kv '${nm}': ne {${t.dims}} is not {head_dim or heads * head_dim, ${K + 1}}`); | |
| return { v: vecF32(tname(nm, l, n)), w: t.dims[0] }; | |
| }; | |
| if (device.limits.maxStorageBuffersPerShaderStage < 9) | |
| throw new Error('WebGPU IR path: attention relative_kv needs 9 storage buffers per shader stage'); | |
| const rk = table(rel.key), rv = has(rel, 'value') ? table(rel.value) : { v: new Float32Array(0), w: 0 }; | |
| const both = new Float32Array(rk.v.length + rv.v.length); | |
| both.set(rk.v); both.set(rv.v, rk.v.length); | |
| emit('attn_rel', [words([nh, nkv, hd, { f: scale }, window, sinks, mode, l, K, rk.w, rv.w, rk.v.length]), st, cnt, codes, | |
| q.buf, c.kc, c.vc, y.buf, storage(both)], [nh, 1], undefined, | |
| IR_SHADERS.attn_rel.replace('MAXK', String(keysArr))); | |
| } else { | |
| emit('attn', [words([nh, nkv, hd, { f: scale }, window, sinks, mode, l]), st, cnt, codes, q.buf, c.kc, c.vc, y.buf], [nh, 1], | |
| undefined, IR_SHADERS.attn.replace('MAXK', String(keysArr))); | |
| } | |
| } else if (op === 'moe_ffn') { | |
| const x = tens(n, 0), E = n.n_experts, K = n.top_k, d = x.w; | |
| const R = mat(tname(n.router, l, n)); | |
| const gn = tname(n.experts.gate, l, n), gt = tensor(gn), ffE = gt.dims[1]; | |
| const G = experts(gn, E), Up = experts(tname(n.experts.up, l, n), E), Dn = experts(tname(n.experts.down, l, n), E); | |
| const r = scratch(E), sel = scratch(16), gate = scratch(16), hg = scratch(K * ffE), hu = scratch(K * ffE); | |
| emit('matvec', [words([E, d]), R.A, R.D, x.buf, r], groups(E), { WT: R.wt }); | |
| emit('moe_select', [words([E, K, (n.gate_norm ?? 'renorm') === 'renorm' ? 1 : 0]), r, sel, gate], [1, 1]); | |
| emit('matvec_exp', [words([ffE, d]), sel, G, x.buf, hg], [groups(ffE), K]); | |
| emit('matvec_exp', [words([ffE, d]), sel, Up, x.buf, hu], [groups(ffE), K]); | |
| emit('swiglu', [words([K * ffE]), hg, hu], groups(K * ffE)); | |
| const y = put(out[0], d); | |
| emit('moe_down', [words([d, ffE]), sel, gate, Dn, hg, y.buf], [groups(d), 1], { K }); | |
| } else if (op === 'list') { | |
| const w = tens(n, 0).w, bank = scratch(listCap * w); | |
| n.in.forEach((nm, k) => { const t = tens(n, k); emit('copy', [words([w, 0, k * w]), t.buf, bank], groups(w)); }); | |
| env.set(out[0], { list: true, bank, count: n.in.length, w, id: {} }); | |
| } else if (op === 'append') { | |
| const Lv = env.get(n.in[0]); | |
| if (!Lv || !Lv.list) throw new Error(`IR append: '${n.in[0]}' is not a list`); | |
| if (frozen.has(Lv.id)) throw new Error('WebGPU IR path: a list appended twice from one value (forked lists)'); | |
| frozen.add(Lv.id); | |
| const x = tens(n, 1); | |
| if (Lv.count >= listCap) throw new Error('IR list capacity'); | |
| emit('copy', [words([x.w, 0, Lv.count * x.w]), x.buf, Lv.bank], groups(x.w)); | |
| env.set(out[0], { list: true, bank: Lv.bank, count: Lv.count + 1, w: Lv.w, id: {} }); | |
| } else if (op === 'depth_attend') { | |
| const Lv = env.get(n.in[0]); | |
| if (!Lv || !Lv.list) throw new Error(`IR depth_attend: '${n.in[0]}' is not a list`); | |
| const ex = n.in.slice(1).map((_, k) => tens(n, k + 1)); | |
| const Sn = Lv.count + ex.length; | |
| if (Sn > 128) throw new Error('depth_attend: more than 128 sources'); | |
| const w = Sn >= 2 ? vec(tname(n.weight, l, n)).buf : dummy; | |
| const y = put(out[0], Lv.w); | |
| emit('depth_attend', [words([Lv.w, { f: n.eps ?? 1e-5 }]), Lv.bank, ex[0] ? ex[0].buf : dummy, ex[1] ? ex[1].buf : dummy, w, y.buf], | |
| [1, 1], { NB: Lv.count, NE: ex.length }); | |
| } else if (op === 'expr') { | |
| const xs = n.in.map((_, k) => tens(n, k)), w = xs[0].w; | |
| if (xs.some((t) => t.w !== w)) throw new Error('IR expr: inputs must share one width'); | |
| const y = put(out[0], w); | |
| emit('expr:' + n.expr + '|' + n.in.length, [words([w]), ...xs.map((t) => t.buf), y.buf], [1, 1], undefined, exprShader(n.expr, n.in.length)); | |
| } else if (op === 'linear_attention') { | |
| const q = tens(n, 0), k = tens(n, 1), v = tens(n, 2), w = n.heads * n.head_dim, r = rec[l][n.state]; | |
| const y = put(out[0], w); | |
| emit('linattn', [words([w, { f: n.den_floor ?? 1e-8 }]), q.buf, k.buf, v.buf, r.num, r.den, y.buf], groups(w)); | |
| } else if (op === 'ssm_scan') { | |
| const u = tens(n, 0), sn = n.n, r = rec[l][n.state]; | |
| if (sn > 1024) throw new Error('ssm_scan: n past 1024'); | |
| const A = vec(tname(n.a, l, n)).buf, B = vec(tname(n.b, l, n)).buf, C = vec(tname(n.c, l, n)).buf, Dd = vec(tname(n.d, l, n)).buf; | |
| const y = put(out[0], sn); | |
| emit('ssmscan', [words([sn]), u.buf, A, B, C, Dd, r.s, y.buf], [1, 1]); | |
| } else if (op === 'conv1d_causal') { | |
| const x = tens(n, 0), C = n.channels, K = n.kernel, r = rec[l][n.state]; | |
| const w = vec(tname(n.weight, l, n)).buf, b = has(n, 'bias') ? vec(tname(n.bias, l, n)).buf : dummy; | |
| const y = put(out[0], C); | |
| emit('conv1d', [words([C, K, has(n, 'bias') ? 1 : 0]), x.buf, w, b, r.x, y.buf], groups(C)); | |
| } else if (op === 'selective_scan') { | |
| const x = tens(n, 0), dt = tens(n, 1), Bm = tens(n, 2), Cm = tens(n, 3), D = n.channels, N = n.d_state, r = rec[l][n.state]; | |
| const a = vecF32(tname(n.a, l, n)), ad = new Float32Array(D * N + D); | |
| ad.set(a.subarray(0, D * N)); | |
| if (has(n, 'd')) ad.set(vecF32(tname(n.d, l, n)).subarray(0, D), D * N); | |
| const y = put(out[0], D); | |
| emit('selscan', [words([D, N, has(n, 'd') ? 1 : 0, (n.dt_softplus ?? true) ? 1 : 0]), x.buf, dt.buf, Bm.buf, Cm.buf, storage(ad), r.s, y.buf], groups(D)); | |
| } else if (op === 'depth_route') { | |
| // handled by the block loop | |
| } else { | |
| throw new Error(`WebGPU IR path: op '${op}'`); | |
| } | |
| } | |
| for (const n of doc.embed.nodes) runNode(n, -1); | |
| let xin = null; | |
| for (let l = 0; l < L; l++) { | |
| const s = stepOf(l); | |
| if (s < 0) { for (const n of doc.block.nodes) runNode(n, l); continue; } | |
| const xname = route.in[0], x0 = env.get(xname); | |
| const first = l === 0 || stepOf(l - 1) !== s, last = l === L - 1 || stepOf(l + 1) !== s; | |
| if (s === 0 && first) { | |
| const R = mat(route.weight), lg = scratch(route.n_rec); | |
| emit('matvec', [words([route.n_rec, R.cols]), R.A, R.D, x0.buf, lg], groups(route.n_rec), { WT: R.wt }); | |
| emit('mor_route', [words([route.n_rec]), lg, morBuf], [1, 1]); | |
| } | |
| if (first) xin = x0; | |
| for (const n of doc.block.nodes.slice(1)) runNode(n, l); | |
| const xnew = env.get(xname); | |
| const y = put(xname, x0.w); | |
| emit('mor_end', [words([x0.w, s, l, last ? 1 : 0, route.gate === 'score' ? 1 : 0]), morBuf, x0.buf, xnew.buf, xin.buf, cnt, y.buf], groups(x0.w)); | |
| } | |
| for (const n of doc.head.nodes) runNode(n, -1); | |
| const lg = env.get('logits'); | |
| if (!lg || lg.list) throw new Error("IR head produced no 'logits'"); | |
| if (lg.w !== vocab) throw new Error(`IR logits are ${lg.w} wide, model.vocab ${vocab}`); | |
| const readback = device.createBuffer({ size: 4 * vocab, usage: S.MAP_READ | S.COPY_DST }); | |
| // Host state: the sequence's ids (turn_state codes), and what is on the GPU. | |
| let seqLen = 0; | |
| const ids = []; | |
| let tsRun = 0, tsDone = 0; | |
| const recBufs = rec.flatMap((r) => Object.values(r).flatMap((t) => Object.values(t))); | |
| const hasRec = recBufs.length > 0; | |
| function reset() { | |
| const enc = device.createCommandEncoder(); | |
| for (const b of recBufs) enc.clearBuffer(b); | |
| enc.clearBuffer(cnt); | |
| device.queue.submit([enc.finish()]); | |
| seqLen = 0; ids.length = 0; tsRun = 0; tsDone = 0; | |
| } | |
| function submitToken(token, pos, wantLogits) { | |
| if (token < 0 || token >= vocab + stateK) throw new Error(`token id ${token} out of range`); | |
| if (token >= vocab && !stateK) throw new Error(`token id ${token} out of range`); | |
| device.queue.writeBuffer(st, 0, new Uint32Array([pos, token, 0, 0])); | |
| if (turnState || stateK) { | |
| const c = token >= vocab ? 1 : 0, sseg = tsDone; | |
| if (token < vocab) tsRun = 0; | |
| else if (token === vocab) tsRun = 1; | |
| else if (token === vocab + tsRun) tsRun++; | |
| else tsRun = 0; | |
| if (stateK && tsRun === stateK) { tsDone++; tsRun = 0; } | |
| device.queue.writeBuffer(codes, 8 * pos, new Uint32Array([c, sseg])); | |
| } | |
| const enc = device.createCommandEncoder(), pass = enc.beginComputePass(); | |
| for (const [pl, bg, n] of prog) { pass.setPipeline(pl); pass.setBindGroup(0, bg); pass.dispatchWorkgroups(n[0], n[1]); } | |
| pass.end(); | |
| if (wantLogits) enc.copyBufferToBuffer(lg.buf, 0, readback, 0, 4 * vocab); | |
| device.queue.submit([enc.finish()]); | |
| } | |
| return { | |
| config: { family_hint: doc.family_hint, d: model.d, L, vocab, ctx, state_k: stateK, dispatchesPerToken: prog.length, | |
| recurrent: hasRec, turn_state: turnState, mor: !!route }, | |
| // Run tokens at positions startPos.. and return the next-token logits after the last one. | |
| // startPos 0 starts a new sequence; otherwise it continues the one on the GPU (startPos must be | |
| // its length when the document has recurrent or routed state, or state tokens). | |
| async forward(tokens, startPos) { | |
| if (startPos + tokens.length > ctx) throw new Error('past the context'); | |
| if (startPos === 0) reset(); | |
| else if (startPos > seqLen || ((hasRec || route || stateK) && startPos !== seqLen)) | |
| throw new Error(`WebGPU IR path: a forward at ${startPos} after ${seqLen} positions`); | |
| if (startPos < seqLen) { // attention-only: later rows are overwritten; the codes are recomputed | |
| ids.length = startPos; | |
| tsRun = 0; tsDone = 0; | |
| for (const t of ids) { if (t < vocab) tsRun = 0; else if (t === vocab) tsRun = 1; else if (t === vocab + tsRun) tsRun++; else tsRun = 0; if (stateK && tsRun === stateK) { tsDone++; tsRun = 0; } } | |
| } | |
| tokens.forEach((t, i) => { submitToken(t, startPos + i, i === tokens.length - 1); ids.push(t); }); | |
| seqLen = startPos + tokens.length; | |
| await readback.mapAsync(GPUMapMode.READ); | |
| const out = new Float32Array(readback.getMappedRange().slice(0)); | |
| readback.unmap(); | |
| return out; | |
| }, | |
| destroy() { for (const b of allBufs) b.destroy(); readback.destroy(); }, | |
| }; | |
| } | |