DMCRA-LAB / forward.js
AEUPH's picture
DMCRA Lab: elastic level-set quantisation over real GGUF weights, measured end to end
7054026 verified
Raw History Blame Contribute Delete
21.8 kB
/* forward.js — a real llama-family forward pass in the browser, over a GGUF's real weight tensors.
*
* VENDORED from `templates/static/hdc/forward.js` (same repository, same author) for the DMCRA Lab
* project, with exactly ONE change: the quantiser is now a callback (`opts.quantize`) instead of a
* hardcoded ternary branch. The hook is marked "THE DMCRA HOOK" below. With no `quantize` callback
* and no `variant`, behaviour is unchanged, so the vendored maths is still the maths the HDC Space
* measures. The HDC Space itself is frozen and untouched.
*
* It was measured before it was bundle-loadable: next-token top-1 of 16.9% against a uniform chance of
* 0.20% over the 512-symbol stories260K vocabulary, and 5.6% with ternarised weights. This module
* exists so the PAGE can run that pass rather than only the test suite. It is a port, not a second
* implementation — the layer order, the caching and the accounting below are the measured ones, and
* `scripts/hdc_tests/forward.test.mjs` loads THIS file (after weights.js, off the global, exactly as
* index.html does) and asserts it still reproduces those numbers, so the shipped code and the
* measured code cannot drift apart.
*
* Two things it needs that a test-only port did not:
* `requirements(header)` is the gate. It reads the tensor INDEX only (no weight bytes) and reports
* whether a llama pass is runnable on this file, which required tensors are missing, and which are
* present but have no codec — so the page can refuse with a reason instead of throwing mid-pass.
* `topK`/`softmax` turn the logits into the ranked probabilities a page has to display.
*
* CONVENTIONS KEPT FROM THE REFERENCE (stated because they are choices, not facts):
* RoPE rotate-half (NeoX): the pair rotated together is (x[i], x[i + head_dim/2]), with
* theta_i = pos * freq_base^(-2i/head_dim) for i in [0, head_dim/2), at absolute
* positions 0..n-1. llama.cpp selects this for LLM_ARCH_LLAMA. The other common
* convention pairs (x[2i], x[2i+1]) and is deliberately NOT used.
* Scale 1/sqrt(head_dim) on the Q·K scores, per head.
* GQA query head h reads KV head floor(h / (n_head / n_head_kv)).
* Mask causal and inclusive: position t attends over j <= t.
* Layout GGUF stores [in, out] with `in` contiguous, so y[o] = sum_i W[o*in + i] * x[i], which is
* exactly `HDCWeights.exactLinear(x, values, inDim)` — every projection goes through it,
* so this file cannot disagree with the layer the HDC suite measures.
* Ternary (arXiv 2402.17764, BitNet b1.58) applies to 2-D weight matrices only, through the
* project's own `toTernary`/`fromTernary`. Norms and embeddings stay float: a ternary norm
* would be a different model rather than a quantised one.
*
* KNOWN LIMIT, carried over rather than papered over: the pass adds NO attention biases. The reference
* ignores them, so on a file that carries them (Qwen2 does) the output is not that model's exact
* output. `requirements()` reports that condition and sets `exact: false` for it.
*
* Dependencies are taken off the global object (like every other bundle here): `HDCWeights` for
* `readTensor`/`exactLinear`/`toTernary`/`fromTernary`/`tensorByteLength`, and `HDCGGUF` only for type
* names. No GGUF parsing happens in this file.
*/
(function (root) {
"use strict";
var W = root.HDCWeights, G = root.HDCGGUF;
var NEED_W = "forward.js requires HDCWeights (weights.js) to be loaded first";
/* ---------------------------------------------------------------- metadata */
/* `parse` returns `kv` as an ARRAY of {key, type, value}, not an object — so index it explicitly. */
function kvMapFromHeader(header) {
var d = {}, kv = (header && header.kv) || [], i;
for (i = 0; i < kv.length; i++) d[kv[i].key] = kv[i].value;
return d;
}
function num(d, k, def) { return typeof d[k] === "number" ? d[k] : def; }
function str(d, k, def) { return typeof d[k] === "string" ? d[k] : def; }
function normalise(m, defaults) {
var headDim = Math.floor(m.nEmbd / m.nHead);
var out = {}, k;
for (k in m) if (Object.prototype.hasOwnProperty.call(m, k)) out[k] = m[k];
out.headDim = headDim;
out.gqaGroups = Math.max(1, Math.floor(m.nHead / m.nHeadKv));
out.defaulted = (m.defaulted || []).concat(defaults || []);
return out;
}
/* Accepts a header from HDCGGUF.parse(), a raw KV map, or an already-resolved meta object.
* Only what the file genuinely omits is defaulted, and each default is recorded in `defaulted`. */
function resolveMeta(input) {
var d = {}, i;
if (input && input.meta && typeof input.meta === "object" && !Array.isArray(input.meta)) {
if (typeof input.meta.nEmbd === "number") return normalise(input.meta, []);
d = input.meta;
} else if (input && (input.header || input.kv)) {
d = (input.header && input.header.kv) ? kvMapFromHeader(input.header) : input.kv;
if (Array.isArray(d)) d = kvMapFromHeader({ kv: d });
}
var has = function (k) { return d[k] !== undefined; };
if (!Object.keys(d).length && input && typeof input === "object" && !Array.isArray(input) &&
(has("general.architecture") || has("llama.embedding_length") || has("general.name"))) {
d = input; // a plain KV map given directly
}
var defaults = [];
var nEmbd = num(d, "llama.embedding_length", 64);
var eps = num(d, "llama.attention.layer_norm_rms_epsilon", 1e-5);
if (d["llama.attention.layer_norm_rms_epsilon"] === undefined) defaults.push("llama.attention.layer_norm_rms_epsilon=1e-5");
var ropeBase = num(d, "llama.rope.freq_base", 10000);
if (d["llama.rope.freq_base"] === undefined) defaults.push("llama.rope.freq_base=10000");
var nHead = num(d, "llama.attention.head_count", nEmbd / 16);
if (d["llama.attention.head_count"] === undefined) defaults.push("llama.attention.head_count=" + nHead);
var nHeadKv = num(d, "llama.attention.head_count_kv", nHead);
if (d["llama.attention.head_count_kv"] === undefined) defaults.push("llama.attention.head_count_kv=n_head");
var nLayer = num(d, "llama.block_count", 5);
if (d["llama.block_count"] === undefined) defaults.push("llama.block_count=5");
var nFfn = num(d, "llama.feed_forward_length", 172);
if (d["llama.feed_forward_length"] === undefined) defaults.push("llama.feed_forward_length=172");
return normalise({
nLayer: nLayer, nEmbd: nEmbd, nHead: nHead, nHeadKv: nHeadKv, nFfn: nFfn, eps: eps,
ropeBase: ropeBase, vocab: num(d, "llama.vocab_size", 0),
arch: str(d, "general.architecture", "llama")
}, defaults);
}
/* The tensors a llama pass reads, in the order it reads them. */
function requiredTensors(nLayer) {
var req = ["token_embd.weight", "output_norm.weight", "output.weight"], L, p;
for (L = 0; L < nLayer; L++) {
p = "blk." + L + ".";
req.push(p + "attn_q.weight", p + "attn_k.weight", p + "attn_v.weight", p + "attn_output.weight",
p + "attn_norm.weight", p + "ffn_gate.weight", p + "ffn_up.weight", p + "ffn_down.weight");
}
return req;
}
/* The page's gate: can this file be run, and if not, why not? Reads the tensor index only. */
function requirements(header) {
var meta = resolveMeta(header);
var tensors = (header && header.tensors) || [];
var index = {}, i, nm;
for (i = 0; i < tensors.length; i++) index[tensors[i].name] = tensors[i];
var req = requiredTensors(meta.nLayer);
var missing = [], unsupported = [], params = 0, t;
for (i = 0; i < req.length; i++) {
t = index[req[i]];
if (!t) { missing.push(req[i]); continue; }
params += Number(t.params) || 0;
if (W && W.tensorByteLength(t) === null) unsupported.push(req[i]);
}
var biases = [];
for (i = 0; i < tensors.length; i++) {
nm = tensors[i].name;
if (/^blk\.\d+\.attn_[qkv]\.bias$/.test(nm)) biases.push(nm);
}
var vocab = 0;
t = index["token_embd.weight"];
if (t && t.dims && t.dims.length > 1) vocab = Number(t.dims[1]);
var reason = null;
if (!tensors.length) {
reason = "this header has no tensor index — it was parsed with metadataOnly, so the weights " +
"are not described; re-read the file without stopping at the metadata block";
} else if (missing.length) {
reason = missing.length + " required tensor(s) absent, e.g. " + missing.slice(0, 4).join(", ");
} else if (unsupported.length) {
reason = "no codec for " + unsupported.length + " required tensor(s), e.g. " +
unsupported.slice(0, 3).join(", ");
} else if (!(vocab > 0)) {
reason = "token_embd.weight carries no vocabulary dimension";
}
return {
feasible: reason === null,
reason: reason,
exact: biases.length === 0, // false when attention biases exist and are not added
missing: missing,
tensors: missing, // same list, under the name the brief specified
unsupported: unsupported,
biasesIgnored: biases,
note: biases.length
? biases.length + " attention bias tensor(s) are present and are NOT added by this pass, so " +
"the output is not this model's exact output (the reference ignores them too)"
: null,
params: params,
vocab: vocab,
layers: meta.nLayer,
meta: meta
};
}
/* ---------------------------------------------------------------- primitives */
function rmsNorm(x, weight, eps, out) {
var ss = 0, i;
for (i = 0; i < x.length; i++) ss += x[i] * x[i];
var inv = 1 / Math.sqrt(ss / x.length + eps);
for (i = 0; i < x.length; i++) out[i] = x[i] * inv * weight[i];
return out;
}
/* Rotate-half RoPE, in place, on one head's slice. */
function ropeHead(vec, base, headDim, pos, freqBase) {
var half = headDim >> 1, i;
for (i = 0; i < half; i++) {
var theta = pos * Math.pow(freqBase, -2 * i / headDim);
var c = Math.cos(theta), s = Math.sin(theta);
var a = vec[base + i], b = vec[base + i + half];
vec[base + i] = a * c - b * s;
vec[base + i + half] = a * s + b * c;
}
}
function silu(v) { return v / (1 + Math.exp(-v)); }
/* Uses the project's own toTernary/fromTernary so this file cannot disagree with the rest of the
* measurement suite about what "ternary" means: per-block scale = mean(|w|), levels {-1,0,+1}. */
function ternarise(values, blockSize) {
var t = W.toTernary(values, { blockSize: blockSize });
return { values: W.fromTernary(t, blockSize), bits: t.perValueBits, scale: t.scale,
blocks: t.scales.length };
}
/* ---- THE DMCRA HOOK (the only edit to this vendored file).
* The quantiser becomes a callback so the elastic level-set ladder of quant.js can run through
* this same already-verified pass. The old behaviour is preserved exactly: no callback and no
* variant quantises nothing, and `variant:"ternary"` still calls ternarise() above. The callback
* receives the caller's `(values, tensorName)` and returns the reconstruction, so a page can pick
* a different grid per matrix. It may also return its own bit account; if it does not, the pass
* falls back to the old 2-bit-planes-plus-scale arithmetic so a caller is never silently
* over-charged for a code it did not describe. */
function makeQuantiser(opts) {
if (typeof opts.quantize === "function") {
return {
name: opts.quantizeName || "custom",
fn: function (values, name) {
var r = opts.quantize(values, name);
var bits = null;
if (r && r.bits && r.bits.effBits !== undefined) bits = r.bits.effBits;
else if (r && typeof r.bits === "number") bits = r.bits;
var bytes = null;
if (r && r.bits && r.bits.storedBytes !== undefined) bytes = r.bits.storedBytes;
return { values: r.values, bits: bits, bytes: bytes };
}
};
}
if (opts.variant === "ternary") {
return { name: "ternary", fn: function (values) { return ternarise(values, opts.blockSize | 0); } };
}
return null;
}
/* ---------------------------------------------------------------- display helpers */
function softmax(logits) {
var n = logits.length, out = new Float32Array(n), i, max = -Infinity, sum = 0, e;
if (!n) return out;
for (i = 0; i < n; i++) if (logits[i] > max) max = logits[i];
for (i = 0; i < n; i++) { e = Math.exp(logits[i] - max); out[i] = e; sum += e; }
if (sum > 0) for (i = 0; i < n; i++) out[i] /= sum;
return out;
}
/* Ranked next-token candidates: {id, logit, p}. */
function topK(logits, k) {
var n = logits.length, idx = [], i, limit;
for (i = 0; i < n; i++) idx.push(i);
idx.sort(function (a, b) { return logits[b] - logits[a]; });
var p = softmax(logits);
limit = Math.min(k || 8, idx.length);
var out = [];
for (i = 0; i < limit; i++) out.push({ id: idx[i], logit: logits[idx[i]], p: p[idx[i]] });
return out;
}
/* Display-only surface form for the llama/SentencePiece encoding, matching the reference's helper.
* The page has its own decodeToken for the byte-level (gpt2) encoding; this is here so a caller
* without a vocabulary can still label a prediction. */
function surface(tok) {
if (typeof tok !== "string") return String(tok);
var m = /^<0x([0-9A-Fa-f]{2})>$/.exec(tok);
if (m) return String.fromCharCode(parseInt(m[1], 16));
return tok.replace(/\u2581/g, " ");
}
/* ---------------------------------------------------------------- the forward pass */
function makeRead(buffer, header) {
return function (name) {
var t = W.readTensor(buffer, header, name);
if (!t) return null;
return { values: t.values, dims: t.dims, typeName: t.typeName,
supported: t.supported, error: t.error };
};
}
function run(buffer, header, tokenIds, opts) {
if (!W) throw new Error(NEED_W);
opts = opts || {};
var variant = opts.variant === "ternary" ? "ternary" : "exact";
var blockSize = opts.blockSize | 0;
var read = makeRead(buffer, header);
var meta = resolveMeta(header);
var nLayer = meta.nLayer, nEmbd = meta.nEmbd, nHead = meta.nHead, nHeadKv = meta.nHeadKv;
var nFfn = meta.nFfn, eps = meta.eps, ropeBase = meta.ropeBase;
var headDim = meta.headDim, gqaGroups = meta.gqaGroups;
var nTok = tokenIds.length;
if (!nTok) throw new Error("tokenIds must not be empty");
var quantiser = makeQuantiser(opts);
/* One cache per call: a tensor is quantised at most once, and both variants share the reading. */
var cache = new Map();
var quantisedMatrices = 0, totalMatrices = 0, quantBytes = 0, floatBytes = 0;
var quantisedBits = 0, quantisedWeights = 0;
function mat(name) {
if (cache.has(name)) return cache.get(name);
var t = read(name);
if (!t || !t.values) throw new Error("tensor unavailable: " + name +
(t && t.error ? " (" + t.error + ")" : ""));
var v = t.values;
var isMatrix = t.dims && t.dims.length === 2;
var out = v;
if (quantiser && isMatrix) {
var q = quantiser.fn(v, name);
out = q.values;
quantisedMatrices++; totalMatrices++;
floatBytes += v.length * 4;
quantisedWeights += v.length;
if (q.bits !== null) quantisedBits += q.bits * v.length;
quantBytes += q.bytes !== null ? q.bytes
: Math.ceil(v.length / 8) * 2 +
Math.ceil(v.length / (blockSize > 0 ? blockSize : v.length)) * 4;
} else if (isMatrix) { totalMatrices++; floatBytes += v.length * 4; }
cache.set(name, out);
return out;
}
function vec(name) {
if (cache.has("v:" + name)) return cache.get("v:" + name);
var t = read(name);
if (!t || !t.values) throw new Error("tensor unavailable: " + name +
(t && t.error ? " (" + t.error + ")" : ""));
cache.set("v:" + name, t.values);
return t.values;
}
/* 1-D tensors (norms) are read through vec() and never ternarised: a ternary norm would be a
* different model rather than a quantised one. */
// ---- embeddings
var emb = vec("token_embd.weight");
var embDims = read("token_embd.weight").dims; // [nEmbd, vocab]
var nVocab = embDims && embDims.length > 1 ? Number(embDims[1]) : 0;
var H = new Float32Array(nTok * nEmbd);
var t0, i0;
for (t0 = 0; t0 < nTok; t0++) {
var id = tokenIds[t0];
if (!(id >= 0 && id < nVocab)) throw new Error("token id " + id + " outside vocabulary of " + nVocab);
for (i0 = 0; i0 < nEmbd; i0++) H[t0 * nEmbd + i0] = emb[id * nEmbd + i0];
}
// ---- layers
for (var L = 0; L < nLayer; L++) {
var p = "blk." + L + ".";
var wq = mat(p + "attn_q.weight");
var wk = mat(p + "attn_k.weight");
var wv = mat(p + "attn_v.weight");
var wo = mat(p + "attn_output.weight");
var wg = mat(p + "ffn_gate.weight");
var wu = mat(p + "ffn_up.weight");
var wd = mat(p + "ffn_down.weight");
var an = vec(p + "attn_norm.weight");
var fn = vec(p + "ffn_norm.weight");
// norm, then the three projections
var normed = [], t;
for (t = 0; t < nTok; t++) normed.push(new Float32Array(nEmbd));
for (t = 0; t < nTok; t++) rmsNorm(H.subarray(t * nEmbd, (t + 1) * nEmbd), an, eps, normed[t]);
var q = new Float32Array(nTok * nHead * headDim);
var k = new Float32Array(nTok * nHeadKv * headDim);
var v = new Float32Array(nTok * nHeadKv * headDim);
for (t = 0; t < nTok; t++) {
var qq = W.exactLinear(normed[t], wq, nEmbd);
var kk = W.exactLinear(normed[t], wk, nEmbd);
var vv = W.exactLinear(normed[t], wv, nEmbd);
q.set(qq, t * nHead * headDim);
k.set(kk, t * nHeadKv * headDim);
v.set(vv, t * nHeadKv * headDim);
}
// positions 0..n-1; llama applies RoPE at the absolute sequence position
for (t = 0; t < nTok; t++) {
var h;
for (h = 0; h < nHead; h++) ropeHead(q, t * nHead * headDim + h * headDim, headDim, t, ropeBase);
for (h = 0; h < nHeadKv; h++) ropeHead(k, t * nHeadKv * headDim + h * headDim, headDim, t, ropeBase);
}
// causal attention, per query head, with GQA
var attn = new Float32Array(nTok * nEmbd);
var scale = 1 / Math.sqrt(headDim);
var scores = new Float32Array(nTok);
for (t = 0; t < nTok; t++) {
for (var h2 = 0; h2 < nHead; h2++) {
var kv = Math.floor(h2 / gqaGroups);
var qOff = t * nHead * headDim + h2 * headDim;
var best = -Infinity, j, d, s;
for (j = 0; j <= t; j++) {
var kOff = j * nHeadKv * headDim + kv * headDim;
s = 0;
for (d = 0; d < headDim; d++) s += q[qOff + d] * k[kOff + d];
s *= scale;
scores[j] = s;
if (s > best) best = s;
}
var sum = 0;
for (j = 0; j <= t; j++) { var e = Math.exp(scores[j] - best); scores[j] = e; sum += e; }
var oOff = t * nEmbd + h2 * headDim;
for (j = 0; j <= t; j++) {
var w = scores[j] / sum;
if (w === 0) continue;
var vOff = j * nHeadKv * headDim + kv * headDim;
for (d = 0; d < headDim; d++) attn[oOff + d] += w * v[vOff + d];
}
}
}
// attention output projection + residual
for (t = 0; t < nTok; t++) {
var proj = W.exactLinear(attn.subarray(t * nEmbd, (t + 1) * nEmbd), wo, nEmbd);
for (var i = 0; i < nEmbd; i++) H[t * nEmbd + i] += proj[i];
}
// SwiGLU MLP + residual
for (t = 0; t < nTok; t++) {
var hn = new Float32Array(nEmbd);
rmsNorm(H.subarray(t * nEmbd, (t + 1) * nEmbd), fn, eps, hn);
var gate = W.exactLinear(hn, wg, nEmbd);
var up = W.exactLinear(hn, wu, nEmbd);
var act = new Float32Array(nFfn);
for (var a = 0; a < nFfn; a++) act[a] = silu(gate[a]) * up[a];
var down = W.exactLinear(act, wd, nFfn);
for (var o = 0; o < nEmbd; o++) H[t * nEmbd + o] += down[o];
}
}
// ---- final norm and unembedding, from the LAST position
var last = H.subarray((nTok - 1) * nEmbd, nTok * nEmbd);
var on = vec("output_norm.weight");
var hidden = new Float32Array(nEmbd);
rmsNorm(last, on, eps, hidden);
var wOut = mat("output.weight"); // tied to token_embd in some fixtures; that is expected
var logits = W.exactLinear(hidden, wOut, nEmbd);
meta.vocab = nVocab || meta.vocab;
meta.variant = variant;
meta.blockSize = blockSize;
meta.quantiser = quantiser ? quantiser.name : "exact";
meta.quantisedWeights = quantisedWeights;
meta.meanBitsPerWeight = quantisedWeights ? quantisedBits / quantisedWeights : null;
return {
logits: logits, hidden: hidden, meta: meta, kv: meta,
accounting: { quantisedMatrices: quantisedMatrices, totalMatrices: totalMatrices,
floatBytes: floatBytes, quantBytes: quantBytes,
ratio: quantBytes > 0 ? floatBytes / quantBytes : 0 }
};
}
var api = {
run: run, requirements: requirements, resolveMeta: resolveMeta,
softmax: softmax, topK: topK, surface: surface,
requiredTensors: requiredTensors, TYPE_NAMES: G ? G.GGML_TYPES : null
};
if (typeof module !== "undefined" && module.exports) module.exports = api;
root.HDCFORWARD = api;
})(typeof globalThis !== "undefined" ? globalThis : this);