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/llama_webgpu.js from johnnyteutonic/ember-chat: direct link, hf CLI and curl.
- Browser
- Download file 22.8 kB
-
https://huggingface.co/spaces/johnnyteutonic/ember-chat/resolve/main/webgpu/llama_webgpu.js
- Command line
-
hf download hf://spaces/johnnyteutonic/ember-chat/webgpu/llama_webgpu.js
-
curl -L -o llama_webgpu.js https://huggingface.co/spaces/johnnyteutonic/ember-chat/resolve/main/webgpu/llama_webgpu.js
22.8 kB
| // The llama forward pass on WebGPU, for ember's browser chat (prototype; LOCAL_LLM_STACK.md | |
| // section 7, Research item 1). ember's WebAssembly keeps tokenising, the chat template, sampling | |
| // and the stop rules; this module turns token ids into next-token logits on the GPU. | |
| // | |
| // Weights stay resident on the GPU (f32; Q8_0/f16 GGUFs are dequantised at load). Each token is | |
| // one command buffer of about 13 dispatches per layer, built from bind groups made once at load; | |
| // only a 16-byte step uniform (position, token id) changes per token, and only the last prompt | |
| // token's logits are read back. The maths follows ember's CPU path (model.cpp, utils.cpp): RMSNorm | |
| // (1/sqrt(mean(x^2) + eps)), RoPE on adjacent pairs with theta^(-i/head_dim), causal softmax | |
| // attention scaled by 1/sqrt(head_dim) with grouped KV heads, SwiGLU. Sums are f32 in a different | |
| // order, so logits match the CPU to rounding, not bit for bit. | |
| import { GGML_Q8_0, f16ToF32, readGguf, tensorF32 } from './gguf.js'; | |
| const WG = 64; | |
| const MAX_CTX = 2048; // attention's workgroup score array | |
| const SHADERS = { | |
| embed: ` | |
| struct S { pos: u32, token: u32, n: u32, _p: u32 }; | |
| @group(0) @binding(0) var<uniform> st: S; | |
| @group(0) @binding(1) var<storage, read> E: array<f32>; | |
| @group(0) @binding(2) var<storage, read_write> x: array<f32>; | |
| @compute @workgroup_size(${WG}) fn main(@builtin(global_invocation_id) g: vec3u) { | |
| if (g.x < st.n) { x[g.x] = E[st.token * st.n + g.x]; } | |
| }`, | |
| rmsnorm: ` | |
| struct P { n: u32, eps: f32, _a: u32, _b: u32 }; | |
| @group(0) @binding(0) var<uniform> p: 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, 256>; | |
| @compute @workgroup_size(256) fn main(@builtin(local_invocation_id) l: vec3u) { | |
| var s = 0.0; | |
| for (var i = l.x; i < p.n; i += 256u) { s += x[i] * x[i]; } | |
| part[l.x] = s; | |
| workgroupBarrier(); | |
| for (var k = 128u; k > 0u; k >>= 1u) { if (l.x < k) { part[l.x] += part[l.x + k]; } workgroupBarrier(); } | |
| let inv = 1.0 / sqrt(part[0] / f32(p.n) + p.eps); | |
| for (var i = l.x; i < p.n; i += 256u) { y[i] = x[i] * inv * w[i]; } | |
| }`, | |
| matvec: ` | |
| struct P { rows: u32, cols: u32, accumulate: u32, _p: u32 }; | |
| @group(0) @binding(0) var<uniform> p: P; | |
| @group(0) @binding(1) var<storage, read> W: array<f32>; | |
| @group(0) @binding(2) var<storage, read> x: array<f32>; | |
| @group(0) @binding(3) var<storage, read_write> y: array<f32>; | |
| var<workgroup> part: array<f32, ${WG}>; | |
| @compute @workgroup_size(${WG}) fn main(@builtin(workgroup_id) wg: vec3u, @builtin(local_invocation_id) l: vec3u) { | |
| let row = wg.x; | |
| let base = row * p.cols; | |
| var s = 0.0; | |
| for (var c = l.x; c < p.cols; c += ${WG}u) { s += W[base + c] * x[c]; } | |
| part[l.x] = s; | |
| workgroupBarrier(); | |
| for (var k = ${WG / 2}u; k > 0u; k >>= 1u) { if (l.x < k) { part[l.x] += part[l.x + k]; } workgroupBarrier(); } | |
| if (l.x == 0u) { if (p.accumulate != 0u) { y[row] = y[row] + part[0]; } else { y[row] = part[0]; } } | |
| }`, | |
| // Q8_0 kept quantised: per 32-weight block an f32 scale and 8 words of 4 int8. Each weight is | |
| // scale * q in f32, the value ember's dequantise-at-load produces, then the same dot as matvec. | |
| matvec_q8: ` | |
| struct P { rows: u32, cols: u32, accumulate: u32, _p: u32 }; | |
| @group(0) @binding(0) var<uniform> p: P; | |
| @group(0) @binding(1) var<storage, read> Q: array<u32>; // [rows][cols / 4] packed int8 | |
| @group(0) @binding(2) var<storage, read> D: array<f32>; // [rows][cols / 32] scales | |
| @group(0) @binding(3) var<storage, read> x: array<f32>; | |
| @group(0) @binding(4) var<storage, read_write> y: array<f32>; | |
| var<workgroup> part: array<f32, ${WG}>; | |
| @compute @workgroup_size(${WG}) fn main(@builtin(workgroup_id) wg: vec3u, @builtin(local_invocation_id) l: vec3u) { | |
| let row = wg.x; | |
| let words = p.cols / 4u; | |
| var s = 0.0; | |
| for (var w = l.x; w < words; w += ${WG}u) { | |
| let packed = Q[row * words + w]; | |
| let sc = D[row * (p.cols / 32u) + w / 8u]; | |
| let c = 4u * w; | |
| for (var b = 0u; b < 4u; b++) { | |
| let q = f32((i32(packed << (24u - 8u * b))) >> 24u); | |
| s += (sc * q) * x[c + b]; | |
| } | |
| } | |
| part[l.x] = s; | |
| workgroupBarrier(); | |
| for (var k = ${WG / 2}u; k > 0u; k >>= 1u) { if (l.x < k) { part[l.x] += part[l.x + k]; } workgroupBarrier(); } | |
| if (l.x == 0u) { if (p.accumulate != 0u) { y[row] = y[row] + part[0]; } else { y[row] = part[0]; } } | |
| }`, | |
| rope_kv: ` | |
| struct S { pos: u32, token: u32, n: u32, _p: u32 }; | |
| struct P { n_heads: u32, n_kv: u32, head_dim: u32, _p: u32 }; | |
| @group(0) @binding(0) var<uniform> st: S; | |
| @group(0) @binding(1) var<uniform> p: P; | |
| @group(0) @binding(2) var<storage, read> cs: array<f32>; // [ctx][head_dim/2][cos, sin] | |
| @group(0) @binding(3) var<storage, read_write> q: array<f32>; | |
| @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>; // [ctx][n_kv * head_dim] | |
| @group(0) @binding(7) var<storage, read_write> vc: array<f32>; | |
| @compute @workgroup_size(${WG}) fn main(@builtin(global_invocation_id) g: vec3u) { | |
| let half = p.head_dim / 2u; | |
| let kvd = p.n_kv * p.head_dim; | |
| let i = g.x; | |
| if (i < p.n_heads * half) { // rotate q in place | |
| let h = i / half; let j = i % half; | |
| let a = h * p.head_dim + 2u * j; | |
| let c = cs[2u * (st.pos * half + j)]; let s = cs[2u * (st.pos * half + j) + 1u]; | |
| let x0 = q[a]; let x1 = q[a + 1u]; | |
| q[a] = x0 * c - x1 * s; q[a + 1u] = x0 * s + x1 * c; | |
| } | |
| if (i < p.n_kv * half) { // rotate k into the cache | |
| let h = i / half; let j = i % half; | |
| let a = h * p.head_dim + 2u * j; | |
| let c = cs[2u * (st.pos * half + j)]; let s = cs[2u * (st.pos * half + j) + 1u]; | |
| let x0 = k[a]; let x1 = k[a + 1u]; | |
| kc[st.pos * kvd + a] = x0 * c - x1 * s; kc[st.pos * kvd + a + 1u] = x0 * s + x1 * c; | |
| } | |
| if (i < kvd) { vc[st.pos * kvd + i] = v[i]; } | |
| }`, | |
| attention: ` | |
| struct S { pos: u32, token: u32, n: u32, _p: u32 }; | |
| struct P { n_heads: u32, n_kv: u32, head_dim: u32, _p: u32 }; | |
| @group(0) @binding(0) var<uniform> st: S; | |
| @group(0) @binding(1) var<uniform> p: P; | |
| @group(0) @binding(2) var<storage, read> q: array<f32>; | |
| @group(0) @binding(3) var<storage, read> kc: array<f32>; | |
| @group(0) @binding(4) var<storage, read> vc: array<f32>; | |
| @group(0) @binding(5) var<storage, read_write> o: array<f32>; | |
| var<workgroup> sc: array<f32, ${MAX_CTX}>; | |
| var<workgroup> part: array<f32, ${WG}>; | |
| @compute @workgroup_size(${WG}) fn main(@builtin(workgroup_id) wg: vec3u, @builtin(local_invocation_id) l: vec3u) { | |
| let h = wg.x; | |
| let kvh = h / (p.n_heads / p.n_kv); | |
| let kvd = p.n_kv * p.head_dim; | |
| let qo = h * p.head_dim; let ko = kvh * p.head_dim; | |
| let n = st.pos + 1u; | |
| let scale = 1.0 / sqrt(f32(p.head_dim)); | |
| var m = -3.4e38; | |
| for (var t = l.x; t < n; t += ${WG}u) { | |
| var s = 0.0; | |
| for (var d = 0u; d < p.head_dim; 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) { let 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]; | |
| for (var d = l.x; d < p.head_dim; d += ${WG}u) { | |
| var acc = 0.0; | |
| for (var t = 0u; t < n; t++) { acc += sc[t] * vc[t * kvd + ko + d]; } | |
| o[qo + d] = acc / zz; | |
| } | |
| }`, | |
| // Mixture of experts, as ember's moe_ffn_f32_cpu (utils.cpp): top-k router logits (ties to the | |
| // lower index), gates = a softmax over the k selected logits (renorm), experts in ascending index. | |
| moe_select: ` | |
| struct P { E: u32, k: u32, _a: u32, _b: u32 }; | |
| @group(0) @binding(0) var<uniform> p: 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() { | |
| var taken: array<bool, 256>; | |
| var pick: array<u32, 16>; | |
| for (var j = 0u; j < p.k; j++) { | |
| var best = 0xffffffffu; | |
| for (var e = 0u; e < p.E; e++) { | |
| if (!taken[e] && (best == 0xffffffffu || r[e] > r[best])) { best = e; } | |
| } | |
| taken[best] = true; pick[j] = best; | |
| } | |
| for (var a = 1u; a < p.k; a++) { // ascending expert index | |
| 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; | |
| for (var j = 0u; j < p.k; j++) { mx = max(mx, r[pick[j]]); } | |
| var z = 0.0; | |
| for (var j = 0u; j < p.k; j++) { z += exp(r[pick[j]] - mx); } | |
| for (var j = 0u; j < p.k; j++) { sel[j] = pick[j]; gate[j] = exp(r[pick[j]] - mx) / z; } | |
| }`, | |
| matvec_exp: ` | |
| struct P { rows: u32, cols: u32, _a: u32, _b: u32 }; | |
| @group(0) @binding(0) var<uniform> p: P; | |
| @group(0) @binding(1) var<storage, read> sel: array<u32>; | |
| @group(0) @binding(2) var<storage, read> W: array<f32>; // [E][rows][cols] | |
| @group(0) @binding(3) var<storage, read> x: array<f32>; | |
| @group(0) @binding(4) var<storage, read_write> y: array<f32>; // [k][rows] | |
| var<workgroup> part: array<f32, ${WG}>; | |
| @compute @workgroup_size(${WG}) fn main(@builtin(workgroup_id) wg: vec3u, @builtin(local_invocation_id) l: vec3u) { | |
| let row = wg.x; let j = wg.y; | |
| let base = (sel[j] * p.rows + row) * p.cols; | |
| var s = 0.0; | |
| for (var c = l.x; c < p.cols; c += ${WG}u) { s += W[base + c] * x[c]; } | |
| part[l.x] = s; | |
| workgroupBarrier(); | |
| for (var k = ${WG / 2}u; k > 0u; k >>= 1u) { if (l.x < k) { part[l.x] += part[l.x + k]; } workgroupBarrier(); } | |
| if (l.x == 0u) { y[j * p.rows + row] = part[0]; } | |
| }`, | |
| moe_down: ` | |
| struct P { rows: u32, cols: u32, k: u32, _a: u32 }; | |
| @group(0) @binding(0) var<uniform> p: 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>; // [E][rows = d][cols = d_ff] | |
| @group(0) @binding(4) var<storage, read> h: array<f32>; // [k][d_ff] | |
| @group(0) @binding(5) var<storage, read_write> x: array<f32>; // residual stream, += sum_j gate_j * down_j h_j | |
| var<workgroup> part: array<f32, ${WG}>; | |
| @compute @workgroup_size(${WG}) fn main(@builtin(workgroup_id) wg: vec3u, @builtin(local_invocation_id) l: vec3u) { | |
| let row = wg.x; | |
| var acc = 0.0; | |
| for (var j = 0u; j < p.k; j++) { | |
| let base = (sel[j] * p.rows + row) * p.cols; | |
| var s = 0.0; | |
| for (var c = l.x; c < p.cols; c += ${WG}u) { s += W[base + c] * h[j * p.cols + c]; } | |
| part[l.x] = s; | |
| workgroupBarrier(); | |
| for (var k = ${WG / 2}u; k > 0u; k >>= 1u) { if (l.x < k) { part[l.x] += part[l.x + k]; } workgroupBarrier(); } | |
| acc += gate[j] * part[0]; | |
| workgroupBarrier(); | |
| } | |
| if (l.x == 0u) { x[row] = x[row] + acc; } | |
| }`, | |
| swiglu: ` | |
| struct P { n: u32, _a: u32, _b: u32, _c: u32 }; | |
| @group(0) @binding(0) var<uniform> p: 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.n) { let a = g[i.x]; g[i.x] = a / (1.0 + exp(-a)) * u[i.x]; } | |
| }`, | |
| }; | |
| // Test hook: one weight matrix through matvec (dequantised f32) and matvec_q8 (kept quantised) | |
| // with the same input x; returns both outputs and a JS f64 reference. | |
| export async function testMatvecQ8(device, ggufBuffer, name, x) { | |
| const g = readGguf(ggufBuffer), t = g.tensors[name]; | |
| const cols = t.dims[0], rows = t.dims[1], S = GPUBufferUsage; | |
| const buf = (arr, extra = 0) => { | |
| const b = device.createBuffer({ size: Math.max(16, arr.byteLength), usage: S.STORAGE | S.COPY_DST | S.COPY_SRC | extra }); | |
| device.queue.writeBuffer(b, 0, arr.buffer, arr.byteOffset, arr.byteLength); | |
| return b; | |
| }; | |
| const f = tensorF32(g, name); | |
| const nb = t.n / 32, raw = new Uint8Array(g.buf, t.start, 34 * nb), dv = new DataView(g.buf, t.start); | |
| const q = new Uint8Array(32 * nb), sc = new Float32Array(nb); | |
| for (let b = 0; b < nb; b++) { sc[b] = f16ToF32(dv.getUint16(34 * b, true)); q.set(raw.subarray(34 * b + 2, 34 * b + 34), 32 * b); } | |
| const P = device.createBuffer({ size: 16, usage: S.UNIFORM | S.COPY_DST }); | |
| device.queue.writeBuffer(P, 0, new Uint32Array([rows, cols, 0, 0])); | |
| const X = buf(x), Y1 = buf(new Float32Array(rows)), Y2 = buf(new Float32Array(rows)); | |
| const run = (code, bufs, out) => { | |
| const pl = device.createComputePipeline({ layout: 'auto', compute: { module: device.createShaderModule({ code }), entryPoint: 'main' } }); | |
| const bg = device.createBindGroup({ layout: pl.getBindGroupLayout(0), entries: bufs.map((b, i) => ({ binding: i, resource: { buffer: b } })) }); | |
| const e = device.createCommandEncoder(), c = e.beginComputePass(); | |
| c.setPipeline(pl); c.setBindGroup(0, bg); c.dispatchWorkgroups(rows); c.end(); | |
| device.queue.submit([e.finish()]); | |
| }; | |
| run(SHADERS.matvec, [P, buf(f), X, Y1]); | |
| run(SHADERS.matvec_q8, [P, buf(q), buf(sc), X, Y2]); | |
| const read = async (b) => { | |
| const r = device.createBuffer({ size: 4 * rows, usage: S.MAP_READ | S.COPY_DST }); | |
| const e = device.createCommandEncoder(); e.copyBufferToBuffer(b, 0, r, 0, 4 * rows); device.queue.submit([e.finish()]); | |
| await r.mapAsync(GPUMapMode.READ); const v = new Float32Array(r.getMappedRange().slice(0)); r.unmap(); return v; | |
| }; | |
| const ref = new Float64Array(rows); | |
| for (let i = 0; i < rows; i++) { let s = 0; for (let c = 0; c < cols; c++) s += f[i * cols + c] * x[c]; ref[i] = s; } | |
| return { rows, cols, f32: await read(Y1), q8: await read(Y2), ref }; | |
| } | |
| export async function createLlama(device, ggufBuffer) { | |
| const g = readGguf(ggufBuffer), M = g.meta; | |
| if (M['general.architecture'] !== 'llama') throw new Error('WebGPU path: llama GGUFs only'); | |
| const d = M['llama.embedding_length'], L = M['llama.block_count'], ff = M['llama.feed_forward_length']; | |
| const nh = M['llama.attention.head_count'], nkv = M['llama.attention.head_count_kv'] || nh; | |
| const eps = M['llama.attention.layer_norm_rms_epsilon'], theta = M['llama.rope.freq_base'] || 10000; | |
| const ctx = Math.min(M['llama.context_length'], MAX_CTX), hd = d / nh, kvd = nkv * hd; | |
| const moe = !!g.tensors['blk.0.ffn_gate_inp.weight']; | |
| const E = moe ? (M['llama.expert_count'] || g.tensors['blk.0.ffn_gate_inp.weight'].n / d) : 0; | |
| const topk = moe ? (M['llama.expert_used_count'] || 2) : 0; | |
| const ffE = moe ? (M['llama.expert_feed_forward_length'] || g.tensors['blk.0.ffn_gate_exps.weight'].n / (E * d)) : 0; | |
| if (moe && (E > 256 || topk > 16)) throw new Error('WebGPU path: at most 256 experts, top-16'); | |
| // Shapes come from the metadata: GGUFs written before the dims fix record 2-D tensors as | |
| // [out, in] (bytes are row-major [out][in] either way), so dims order cannot be trusted. | |
| const vocab = M['llama.vocab_size'] || (M['tokenizer.ggml.tokens'] || []).length || | |
| Math.max(...g.tensors['token_embd.weight'].dims); | |
| const S = GPUBufferUsage; | |
| const storage = (arr) => { | |
| const b = device.createBuffer({ size: Math.max(16, arr.byteLength), usage: S.STORAGE | S.COPY_DST }); | |
| device.queue.writeBuffer(b, 0, arr.buffer, arr.byteOffset, arr.byteLength); | |
| return b; | |
| }; | |
| const scratch = (n) => device.createBuffer({ size: Math.max(16, 4 * n), usage: S.STORAGE | S.COPY_SRC | S.COPY_DST }); | |
| const uniform = (words) => { | |
| const b = device.createBuffer({ size: 16, usage: S.UNIFORM | S.COPY_DST }); | |
| device.queue.writeBuffer(b, 0, words); | |
| return b; | |
| }; | |
| const u4 = (a, b = 0, c = 0, e = 0) => new Uint32Array([a, b, c, e]); | |
| const weight = (name) => storage(tensorF32(g, name)); | |
| // A matrix for matvec: Q8_0 stays quantised (repacked to scales + packed int8), others go to f32. | |
| const mat = (name) => { | |
| const t = g.tensors[name]; | |
| if (t.type !== GGML_Q8_0) return { q8: false, buf: weight(name) }; | |
| const nb = t.n / 32, raw = new Uint8Array(g.buf, t.start, 34 * nb), dv = new DataView(g.buf, t.start); | |
| const q = new Uint8Array(32 * nb), sc = new Float32Array(nb); | |
| for (let b = 0; b < nb; b++) { | |
| sc[b] = f16ToF32(dv.getUint16(34 * b, true)); | |
| q.set(raw.subarray(34 * b + 2, 34 * b + 34), 32 * b); | |
| } | |
| return { q8: true, Q: storage(q), D: storage(sc) }; | |
| }; | |
| const mv = (P, m, x, y, n) => m.q8 ? ['matvec_q8', bind('matvec_q8', [P, m.Q, m.D, x, y]), n] | |
| : ['matvec', bind('matvec', [P, m.buf, x, y]), n]; | |
| const outName = g.tensors['output.weight'] ? 'output.weight' : 'token_embd.weight'; // tied embeddings | |
| // RoPE table exactly as ember builds it: f32 freq = 1 / theta^(i / head_dim), angle pos * freq. | |
| const cs = new Float32Array(ctx * hd); | |
| for (let pos = 0; pos < ctx; pos++) | |
| for (let i = 0; i < hd; i += 2) { | |
| const freq = Math.fround(1 / Math.fround(Math.pow(theta, i / hd))); | |
| const val = Math.fround(pos * freq); | |
| cs[pos * hd + i] = Math.cos(val); cs[pos * hd + i + 1] = Math.sin(val); | |
| } | |
| const W = { | |
| embd: weight('token_embd.weight'), outNorm: weight('output_norm.weight'), out: mat(outName), rope: storage(cs), | |
| layers: Array.from({ length: L }, (_, l) => ({ | |
| attnNorm: weight(`blk.${l}.attn_norm.weight`), q: mat(`blk.${l}.attn_q.weight`), k: mat(`blk.${l}.attn_k.weight`), | |
| v: mat(`blk.${l}.attn_v.weight`), o: mat(`blk.${l}.attn_output.weight`), ffnNorm: weight(`blk.${l}.ffn_norm.weight`), | |
| ...(moe ? { router: mat(`blk.${l}.ffn_gate_inp.weight`), gateE: weight(`blk.${l}.ffn_gate_exps.weight`), | |
| upE: weight(`blk.${l}.ffn_up_exps.weight`), downE: weight(`blk.${l}.ffn_down_exps.weight`) } | |
| : { gate: mat(`blk.${l}.ffn_gate.weight`), up: mat(`blk.${l}.ffn_up.weight`), | |
| down: mat(`blk.${l}.ffn_down.weight`) }), | |
| kc: scratch(ctx * kvd), vc: scratch(ctx * kvd), | |
| })), | |
| }; | |
| const A = { x: scratch(d), xn: scratch(d), q: scratch(d), k: scratch(kvd), v: scratch(kvd), att: scratch(d), | |
| g: scratch(moe ? topk * ffE : ff), u: scratch(moe ? topk * ffE : ff), logits: scratch(vocab), | |
| r: scratch(Math.max(E, 4)), sel: scratch(16), gate: scratch(16) }; | |
| const readback = device.createBuffer({ size: 4 * vocab, usage: S.MAP_READ | S.COPY_DST }); | |
| const step = device.createBuffer({ size: 16, usage: S.UNIFORM | S.COPY_DST }); | |
| const pipe = {}; | |
| for (const [k, code] of Object.entries(SHADERS)) | |
| pipe[k] = device.createComputePipeline({ layout: 'auto', compute: { module: device.createShaderModule({ code }), entryPoint: 'main' } }); | |
| const bind = (k, bufs) => device.createBindGroup({ layout: pipe[k].getBindGroupLayout(0), | |
| entries: bufs.map((b, i) => ({ binding: i, resource: { buffer: b } })) }); | |
| const f32u = (n, e) => { const a = new ArrayBuffer(16); new Uint32Array(a)[0] = n; new Float32Array(a)[1] = e; return a; }; | |
| const pNorm = uniform(f32u(d, eps)), pHeads = uniform(u4(nh, nkv, hd)), pFF = uniform(u4(ff)); | |
| const pMat = (rows, cols, acc) => uniform(u4(rows, cols, acc ? 1 : 0)); | |
| const pQ = pMat(d, d, false), pKV = pMat(kvd, d, false), pO = pMat(d, d, true), pUp = pMat(ff, d, false), | |
| pDown = pMat(d, ff, true), pOut = pMat(vocab, d, false), pEmb = step; | |
| const pRouter = moe ? pMat(E, d, false) : null, pSel = moe ? uniform(u4(E, topk)) : null, | |
| pExp = moe ? uniform(u4(ffE, d)) : null, pMoeDown = moe ? uniform(u4(d, ffE, topk)) : null, | |
| pFFk = moe ? uniform(u4(topk * ffE)) : null; | |
| const ffnSteps = (lw) => moe ? [ | |
| mv(pRouter, lw.router, A.xn, A.r, E), | |
| ['moe_select', bind('moe_select', [pSel, A.r, A.sel, A.gate]), 1], | |
| ['matvec_exp', bind('matvec_exp', [pExp, A.sel, lw.gateE, A.xn, A.g]), [ffE, topk]], | |
| ['matvec_exp', bind('matvec_exp', [pExp, A.sel, lw.upE, A.xn, A.u]), [ffE, topk]], | |
| ['swiglu', bind('swiglu', [pFFk, A.g, A.u]), Math.ceil(topk * ffE / WG)], | |
| ['moe_down', bind('moe_down', [pMoeDown, A.sel, A.gate, lw.downE, A.g, A.x]), d], | |
| ] : [ | |
| mv(pUp, lw.gate, A.xn, A.g, ff), | |
| mv(pUp, lw.up, A.xn, A.u, ff), | |
| ['swiglu', bind('swiglu', [pFF, A.g, A.u]), Math.ceil(ff / WG)], | |
| mv(pDown, lw.down, A.g, A.x, d), | |
| ]; | |
| // The per-token program: [pipeline, bind group, workgroups] in order. | |
| const prog = [['embed', bind('embed', [pEmb, W.embd, A.x]), Math.ceil(d / WG)]]; | |
| for (const lw of W.layers) { | |
| prog.push(['rmsnorm', bind('rmsnorm', [pNorm, A.x, lw.attnNorm, A.xn]), 1], | |
| mv(pQ, lw.q, A.xn, A.q, d), | |
| mv(pKV, lw.k, A.xn, A.k, kvd), | |
| mv(pKV, lw.v, A.xn, A.v, kvd), | |
| ['rope_kv', bind('rope_kv', [step, pHeads, W.rope, A.q, A.k, A.v, lw.kc, lw.vc]), Math.ceil(Math.max(d / 2, kvd) / WG)], | |
| ['attention', bind('attention', [step, pHeads, A.q, lw.kc, lw.vc, A.att]), nh], | |
| mv(pO, lw.o, A.att, A.x, d), | |
| ['rmsnorm', bind('rmsnorm', [pNorm, A.x, lw.ffnNorm, A.xn]), 1], | |
| ...ffnSteps(lw)); | |
| } | |
| prog.push(['rmsnorm', bind('rmsnorm', [pNorm, A.x, W.outNorm, A.xn]), 1], | |
| mv(pOut, W.out, A.xn, A.logits, vocab)); | |
| function submitToken(token, pos, wantLogits) { | |
| device.queue.writeBuffer(step, 0, u4(pos, token, d)); | |
| const enc = device.createCommandEncoder(), pass = enc.beginComputePass(); | |
| for (const [k, bg, n] of prog) { | |
| pass.setPipeline(pipe[k]); pass.setBindGroup(0, bg); | |
| if (Array.isArray(n)) pass.dispatchWorkgroups(n[0], n[1]); else pass.dispatchWorkgroups(n); | |
| } | |
| pass.end(); | |
| if (wantLogits) enc.copyBufferToBuffer(A.logits, 0, readback, 0, 4 * vocab); | |
| device.queue.submit([enc.finish()]); | |
| } | |
| return { | |
| config: { d, L, ff: moe ? ffE : ff, nh, nkv, hd, ctx, vocab, eps, theta, experts: E, topk, | |
| dispatchesPerToken: prog.length }, | |
| // Run tokens at positions startPos.. and return the next-token logits after the last one. | |
| async forward(tokens, startPos) { | |
| if (startPos + tokens.length > ctx) throw new Error('past the context'); | |
| tokens.forEach((t, i) => submitToken(t, startPos + i, i === tokens.length - 1)); | |
| await readback.mapAsync(GPUMapMode.READ); | |
| const out = new Float32Array(readback.getMappedRange().slice(0)); | |
| readback.unmap(); | |
| return out; | |
| }, | |
| }; | |
| } | |