Spaces:
Sleeping
Sleeping
| // Forward-only transformer, split at layer boundaries, for real HF models. | |
| // | |
| // Every matrix product goes through the verified INT8 units — the same | |
| // block-scaled quantize -> exact LUT/DP4A multiply -> exact int32 accumulate -> | |
| // pinned f32 epilogue as the rest of DaisyChain. Nothing here computes in | |
| // plain float except the elementwise parts that have no matmul in them | |
| // (norms, softmax, RoPE, the activation), exactly as the trainer does it. | |
| // | |
| // The pass is cut into three callable pieces so they can run on different | |
| // machines: | |
| // | |
| // embed(ids) head stage: token (+ learned position) embedding | |
| // runLayers(x) any stage: its own contiguous layers | |
| // readout(x) head stage: final norm + lm_head -> logits | |
| // | |
| // Architecture differences live in the spec (see arch.js), never in branches | |
| // scattered through the maths: `spec.norm`, `spec.gated`, `spec.rope`, | |
| // `spec.kvHeads`. A Llama block and a GPT-2 block take the same path here. | |
| (function (root) { | |
| "use strict"; | |
| let TC, V, SH; | |
| // ---- norms ------------------------------------------------------------------ | |
| // RMSNorm (Llama): x * w / sqrt(mean(x^2) + eps) — no mean subtraction. | |
| // LayerNorm (GPT-2): (x - mu) / sigma * w + b. | |
| // Both are elementwise after a row reduction, so there is no matmul to send | |
| // through the units, and both run in f64-accumulated JS — which IEEE requires | |
| // to be exactly rounded, so every device agrees. | |
| function rmsNorm(x, rows, C, w, eps) { | |
| const y = new Float32Array(rows * C); | |
| for (let r = 0; r < rows; r++) { | |
| let s = 0; | |
| for (let j = 0; j < C; j++) { const v = x[r * C + j]; s += v * v; } | |
| const inv = 1 / Math.sqrt(s / C + eps); | |
| for (let j = 0; j < C; j++) y[r * C + j] = x[r * C + j] * inv * w[j]; | |
| } | |
| return y; | |
| } | |
| function layerNorm(x, rows, C, w, b, eps) { | |
| const y = new Float32Array(rows * C); | |
| for (let r = 0; r < rows; r++) { | |
| let mu = 0; | |
| for (let j = 0; j < C; j++) mu += x[r * C + j]; | |
| mu /= C; | |
| let v = 0; | |
| for (let j = 0; j < C; j++) { const d = x[r * C + j] - mu; v += d * d; } | |
| const inv = 1 / Math.sqrt(v / C + eps); | |
| for (let j = 0; j < C; j++) y[r * C + j] = (x[r * C + j] - mu) * inv * w[j] + (b ? b[j] : 0); | |
| } | |
| return y; | |
| } | |
| function norm(spec, x, rows, C, w, b) { | |
| return spec.norm === "rms" ? rmsNorm(x, rows, C, w, spec.normEps) | |
| : layerNorm(x, rows, C, w, b, spec.normEps); | |
| } | |
| // ---- activations ------------------------------------------------------------ | |
| const silu = (v) => v / (1 + Math.exp(-v)); | |
| // tanh-approximate GELU: what GPT-2 was trained with, so it is the correct | |
| // function here rather than an approximation of the erf form. | |
| function gelu(v) { | |
| return 0.5 * v * (1 + Math.tanh(0.7978845608028654 * (v + 0.044715 * v * v * v))); | |
| } | |
| // ---- RoPE ------------------------------------------------------------------- | |
| // Rotary embeddings applied per head, in the half-split layout HF uses: | |
| // dims [0, hd/2) pair with [hd/2, hd). Positions are absolute, and because | |
| // there is no KV cache every token re-runs the whole window, so position i | |
| // is simply the row index. | |
| function ropeTables(hd, theta, maxT) { | |
| const half = hd >> 1; | |
| const cos = new Float32Array(maxT * half), sin = new Float32Array(maxT * half); | |
| for (let p = 0; p < maxT; p++) | |
| for (let i = 0; i < half; i++) { | |
| const f = p / Math.pow(theta, (2 * i) / hd); | |
| cos[p * half + i] = Math.cos(f); | |
| sin[p * half + i] = Math.sin(f); | |
| } | |
| return { cos, sin, half }; | |
| } | |
| // x is rows x (nHeads*hd), rows = T; rotate each head in place. | |
| function applyRope(x, T, nHeads, hd, rope, posOffset) { | |
| const half = rope.half, stride = nHeads * hd; | |
| for (let t = 0; t < T; t++) { | |
| const p = (posOffset || 0) + t; | |
| for (let h = 0; h < nHeads; h++) { | |
| const o = t * stride + h * hd; | |
| for (let i = 0; i < half; i++) { | |
| const c = rope.cos[p * half + i], s = rope.sin[p * half + i]; | |
| const a = x[o + i], b = x[o + half + i]; | |
| x[o + i] = a * c - b * s; | |
| x[o + half + i] = b * c + a * s; | |
| } | |
| } | |
| } | |
| } | |
| // ---- verified matmul -------------------------------------------------------- | |
| // X is m x k, W is k x n (row-major). Weights are stored k x n at load time | |
| // (see loadStage), so nothing is transposed per call. | |
| async function vmm(X, W, m, k, n, ctx, bias) { | |
| const out = await V.vgemmBlock(X, W, { m, k, n, batch: 1 }, ctx.L, ctx.bgemm, ctx.audit); | |
| if (bias) for (let i = 0; i < m; i++) for (let j = 0; j < n; j++) out[i * n + j] += bias[j]; | |
| return out; | |
| } | |
| // ---- one layer -------------------------------------------------------------- | |
| async function layerForward(S, x, ly) { | |
| const { spec, ctx } = S, C = spec.hidden, T = S.T; | |
| const hd = spec.headDim, nH = spec.heads, nKV = spec.kvHeads; | |
| const qDim = nH * hd, kvDim = nKV * hd; | |
| // ---- attention | |
| const h1 = norm(spec, x, T, C, ly.nrm1, ly.nrm1b); | |
| let q, k, v; | |
| if (spec.qkvFused) { | |
| // GPT-2 packs q,k,v into one (C x 3C) projection — one GEMM, then split. | |
| const qkv = await vmm(h1, ly.Wqkv, T, C, 3 * C, ctx, ly.bqkv); | |
| q = new Float32Array(T * C); k = new Float32Array(T * C); v = new Float32Array(T * C); | |
| for (let t = 0; t < T; t++) { | |
| q.set(qkv.subarray(t * 3 * C, t * 3 * C + C), t * C); | |
| k.set(qkv.subarray(t * 3 * C + C, t * 3 * C + 2 * C), t * C); | |
| v.set(qkv.subarray(t * 3 * C + 2 * C, t * 3 * C + 3 * C), t * C); | |
| } | |
| } else { | |
| [q, k, v] = await Promise.all([ | |
| vmm(h1, ly.Wq, T, C, qDim, ctx, ly.bq), | |
| vmm(h1, ly.Wk, T, C, kvDim, ctx, ly.bk), | |
| vmm(h1, ly.Wv, T, C, kvDim, ctx, ly.bv), | |
| ]); | |
| } | |
| if (spec.rope) { applyRope(q, T, nH, hd, S.rope, 0); applyRope(k, T, nKV, hd, S.rope, 0); } | |
| // Grouped-query attention: several query heads share one kv head. Rather | |
| // than materialising repeated kv (memory this project does not have to | |
| // spare), the head index is mapped when reading. | |
| const group = nH / nKV; | |
| const scale = 1 / Math.sqrt(hd); | |
| const ctxOut = new Float32Array(T * qDim); | |
| // Scores and context are the two attention GEMMs. They are small per head | |
| // (T x hd and T x T), and the fused kernels in webgpu.js expect the | |
| // trainer's uniform-head layout, so at general head geometry they are run | |
| // through the same verified block GEMM per head — every product still goes | |
| // through the units, and the CPU mirror and GPU kernel agree bit-for-bit. | |
| for (let h = 0; h < nH; h++) { | |
| const kvh = Math.floor(h / group); | |
| const qh = new Float32Array(T * hd), kh = new Float32Array(T * hd), vh = new Float32Array(T * hd); | |
| for (let t = 0; t < T; t++) { | |
| qh.set(q.subarray(t * qDim + h * hd, t * qDim + h * hd + hd), t * hd); | |
| kh.set(k.subarray(t * kvDim + kvh * hd, t * kvDim + kvh * hd + hd), t * hd); | |
| vh.set(v.subarray(t * kvDim + kvh * hd, t * kvDim + kvh * hd + hd), t * hd); | |
| } | |
| const khT = TC.transpose(kh, T, hd); // hd x T | |
| const scores = await V.vgemmBlock(qh, khT, { m: T, k: hd, n: T, batch: 1 }, ctx.L, ctx.bgemm, ctx.audit); | |
| const a = new Float32Array(T * T); // causal softmax | |
| for (let i = 0; i < T; i++) { | |
| let mx = -Infinity; | |
| for (let j = 0; j <= i; j++) mx = Math.max(mx, scores[i * T + j] * scale); | |
| let z = 0; | |
| for (let j = 0; j <= i; j++) { const e = Math.exp(scores[i * T + j] * scale - mx); a[i * T + j] = e; z += e; } | |
| for (let j = 0; j <= i; j++) a[i * T + j] /= z; | |
| } | |
| const oh = await V.vgemmBlock(a, vh, { m: T, k: T, n: hd, batch: 1 }, ctx.L, ctx.bgemm, ctx.audit); | |
| for (let t = 0; t < T; t++) ctxOut.set(oh.subarray(t * hd, t * hd + hd), t * qDim + h * hd); | |
| } | |
| const attn = await vmm(ctxOut, ly.Wo, T, qDim, C, ctx, ly.bo); | |
| const x2 = new Float32Array(T * C); | |
| for (let i = 0; i < x2.length; i++) x2[i] = x[i] + attn[i]; | |
| // ---- MLP | |
| const h2 = norm(spec, x2, T, C, ly.nrm2, ly.nrm2b); | |
| let hid; | |
| if (spec.gated) { | |
| // SwiGLU: down(silu(gate(x)) * up(x)) | |
| const [g, u] = await Promise.all([ | |
| vmm(h2, ly.Wgate, T, C, spec.inter, ctx, null), | |
| vmm(h2, ly.Wup, T, C, spec.inter, ctx, null), | |
| ]); | |
| hid = g; | |
| for (let i = 0; i < hid.length; i++) hid[i] = silu(hid[i]) * u[i]; | |
| } else { | |
| hid = await vmm(h2, ly.Wfc, T, C, spec.inter, ctx, ly.bfc); | |
| for (let i = 0; i < hid.length; i++) hid[i] = gelu(hid[i]); | |
| } | |
| const down = await vmm(hid, ly.Wdown, T, spec.inter, C, ctx, ly.bdown); | |
| const out = new Float32Array(T * C); | |
| for (let i = 0; i < out.length; i++) out[i] = x2[i] + down[i]; | |
| return out; | |
| } | |
| // ---- stage ------------------------------------------------------------------ | |
| function makeStage(spec, st, w, ctx, T) { | |
| const S = { spec, st, w, ctx, T: T || Math.min(spec.maxPos, 128) }; | |
| if (spec.rope) S.rope = ropeTables(spec.headDim, spec.ropeTheta, S.T + 1); | |
| return S; | |
| } | |
| function embed(S, ids) { | |
| const { spec } = S, C = spec.hidden, T = S.T; | |
| const x = new Float32Array(T * C); | |
| for (let i = 0; i < T; i++) { | |
| const id = ids[i]; | |
| for (let j = 0; j < C; j++) x[i * C + j] = S.w.emb[id * C + j]; | |
| } | |
| if (S.w.pos) // GPT-2 learned positions | |
| for (let i = 0; i < T; i++) | |
| for (let j = 0; j < C; j++) x[i * C + j] += S.w.pos[i * C + j]; | |
| return x; | |
| } | |
| async function runLayers(S, x) { | |
| for (const ly of S.w.layers) x = await layerForward(S, x, ly); | |
| return x; | |
| } | |
| // Only the last position's logits are needed to pick the next token, and at | |
| // a 150k-token vocabulary that turns the largest GEMM in the model into a | |
| // single row — the biggest single saving in the ring. | |
| async function readout(S, x) { | |
| const { spec, ctx } = S, C = spec.hidden, T = S.T; | |
| const y = norm(spec, x, T, C, S.w.nrmF, S.w.nrmFb); | |
| const last = y.subarray((T - 1) * C, T * C); | |
| return vmm(last, S.w.lmHead, 1, C, spec.vocab, ctx, null); | |
| } | |
| // ---- sampling --------------------------------------------------------------- | |
| function mulberry32(a) { return function () { a |= 0; a = a + 0x6D2B79F5 | 0; let t = Math.imul(a ^ a >>> 15, 1 | a); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; }; } | |
| function pickToken(logits, opts) { | |
| const o = opts || {}, temp = o.temperature ?? 0, vocab = logits.length; | |
| if (!temp) { | |
| let best = 0, bv = -Infinity; | |
| for (let j = 0; j < vocab; j++) if (logits[j] > bv) { bv = logits[j]; best = j; } | |
| return best; | |
| } | |
| const k = Math.min(o.topK || 40, vocab); | |
| const idx = Array.from({ length: vocab }, (_, i) => i).sort((a, b) => logits[b] - logits[a]).slice(0, k); | |
| let mx = -Infinity; | |
| for (const i of idx) mx = Math.max(mx, logits[i] / temp); | |
| let z = 0; | |
| const p = idx.map(i => { const e = Math.exp(logits[i] / temp - mx); z += e; return e; }); | |
| let r = (o.rng || Math.random)() * z; | |
| for (let i = 0; i < idx.length; i++) { r -= p[i]; if (r <= 0) return idx[i]; } | |
| return idx[idx.length - 1]; | |
| } | |
| // ---- single-device reference ------------------------------------------------ | |
| async function generateLocal(stages, ids, nTokens, opts) { | |
| const head = stages[0], T = head.T; | |
| const out = [...ids]; | |
| for (let n = 0; n < nTokens; n++) { | |
| const win = new Int32Array(T); | |
| const tail = out.slice(-T); | |
| for (let i = 0; i < tail.length; i++) win[T - tail.length + i] = tail[i]; | |
| let x = embed(head, win); | |
| for (const S of stages) x = await runLayers(S, x); | |
| out.push(pickToken(await readout(head, x), opts)); | |
| } | |
| return out; | |
| } | |
| const api = { makeStage, embed, runLayers, readout, pickToken, generateLocal, | |
| rmsNorm, layerNorm, norm, ropeTables, applyRope, silu, gelu, mulberry32 }; | |
| if (typeof module !== "undefined" && module.exports) { | |
| TC = require("./traincore.js"); V = require("./verified_core.js"); SH = require("./shard.js"); | |
| module.exports = api; | |
| } else { TC = root.TrainCore; V = root.Verified; SH = root.Shard; root.Infer = api; } | |
| })(typeof self !== "undefined" ? self : this); | |