File size: 10,133 Bytes
2ab2e60 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 | // alias3-web (alias-web in the paper) · a cybersecurity transformer in the browser (WebGPU).
//
// Two regimes from one loaded 4-bit graph:
// System 1 ask() one typed question, one prefill, no token decoded: a probability per option
// System 2 generate() text from the same weights, token by token, on request
//
// The prefill engine is the vendored runner (vendor/ct0.js: ct0-edge by Eduardo Worrel, MIT, see
// vendor/ct0-edge.LICENSE), unchanged. This file adds the readout the model was trained with:
//
// · the System 1 markers as the model's own special tokens, by id:
// <state> = <|fim_prefix|> <q> = <|fim_middle|> <opt> = <|box_start|>
// </opt> = <|box_end|> <decide> = <|fim_suffix|>
// · the final RMSNorm (heads/norm.bin): the graph exports hidden states before it
// · the complete pointer head (heads/head.bin), read as in training:
// z_i = q(h_<decide>) · k(h_</opt>_i) / (sqrt(256) · T), softmax over the options
// · yes/no and severity as choices over ("Yes", "No") and ("1" … "5"): one mechanism for every question
//
// The page that imports this module needs an import map for the runner and its two runtimes; see index.html.
export { AliasWeb };
const MARKERS = {
state: "<|fim_prefix|>",
q: "<|fim_middle|>",
opt: "<|box_start|>",
optEnd: "<|box_end|>",
decide: "<|fim_suffix|>",
};
const LEVELS = ["1", "2", "3", "4", "5"];
export const DEFAULT_SCORE_Q = "How severe is the security risk in this content?";
// ── head.bin: [u32 lenJSON][json][fp16 q][fp16 qb][fp16 k][fp16 kb] ────────
function f16(h) {
const s = (h & 0x8000) >> 15, e = (h & 0x7c00) >> 10, f = h & 0x03ff;
if (e === 0) return (s ? -1 : 1) * Math.pow(2, -14) * (f / 1024);
if (e === 0x1f) return f ? NaN : s ? -Infinity : Infinity;
return (s ? -1 : 1) * Math.pow(2, e - 15) * (1 + f / 1024);
}
export function loadHead(buf) {
const dv = new DataView(buf);
const jl = dv.getUint32(0, true);
const meta = JSON.parse(new TextDecoder().decode(new Uint8Array(buf, 4, jl)));
let off = 4 + jl;
const take = (n) => { const f = new Float32Array(n); for (let i = 0; i < n; i++) f[i] = f16(dv.getUint16(off + i * 2, true)); off += n * 2; return f; };
const d = meta.headDim, h = meta.hiddenSize;
return { q: take(d * h), qb: take(d), k: take(d * h), kb: take(d), headDim: d, hiddenSize: h,
temperature: meta.temperature ?? 1.0, scale: meta.scale ?? 1 / Math.sqrt(d), scoreOffset: meta.score_offset ?? 0 };
}
// norm.bin: [u32 lenJSON][json {hiddenSize}][fp32 hiddenSize]
async function loadNorm(buf) {
const dv = new DataView(buf);
const jl = dv.getUint32(0, true);
const meta = JSON.parse(new TextDecoder().decode(new Uint8Array(buf, 4, jl)));
const n = meta.hiddenSize, off = 4 + jl, f = new Float32Array(n);
for (let i = 0; i < n; i++) f[i] = dv.getFloat32(off + i * 4, true);
return f;
}
function proj(W, b, x, dim, hidden) {
const y = new Float32Array(dim);
for (let d = 0; d < dim; d++) { let a = b[d]; const r = d * hidden; for (let i = 0; i < hidden; i++) a += W[r + i] * x[i]; y[d] = a; }
return y;
}
const dot = (a, b) => { let s = 0; for (let i = 0; i < a.length; i++) s += a[i] * b[i]; return s; };
const headLogit = (hd, hD, hO) => dot(proj(hd.q, hd.qb, hD, hd.headDim, hd.hiddenSize), proj(hd.k, hd.kb, hO, hd.headDim, hd.hiddenSize)) * hd.scale / hd.temperature + (hd.scoreOffset || 0);
const softmax = (xs) => {
const mx = Math.max(...xs.filter(Number.isFinite));
const e = xs.map((x) => (Number.isFinite(x) ? Math.exp(x - mx) : 0));
const z = e.reduce((a, b) => a + b, 0) || 1;
return e.map((v) => v / z);
};
// text supplied by the caller can never produce a delimiter: <|name|> becomes <¦name¦>, as in training
const SPECIAL_RE = /<\|([A-Za-z0-9_]+)\|>/g;
const sanitize = (t) => t.replace(SPECIAL_RE, "<¦$1¦>");
class AliasWeb {
constructor(tok, prefill, head, mid, normW) { this.tok = tok; this.prefill = prefill; this.head = head; this.mid = mid; this.normW = normW; }
// the graph exports hidden_states before the final norm; the head was trained on the normalised
// state, so the model's final RMSNorm is applied here
norm(h) {
if (!this.normW) return h;
let s = 0; for (let i = 0; i < h.length; i++) s += h[i] * h[i];
const inv = 1 / Math.sqrt(s / h.length);
const o = new Float32Array(h.length);
for (let i = 0; i < h.length; i++) o[i] = h[i] * inv * this.normW[i];
return o;
}
static async load(opts = {}) {
const base = opts.base ?? "./";
const status = opts.status ?? (() => { });
status("loading the runner and the 4-bit graph (2.4 GB)…");
const edge = await import("ct0-edge");
const inner = await edge.CT0.load({
runtime: "genai",
genaiBaseUrl: base + "model/",
genaiModel: "model.onnx",
baseUrl: base + "model/",
status: () => { },
verifyEp: false, // placement was verified once against this graph in Chrome
skipVersionCheck: true, // the import map pins the ONNX Runtime version
genai: { warmup: false },
});
const be = inner.backend ?? inner.be;
if (!be?.prefill) throw new Error("the runner did not expose its prefill backend");
status("loading the head and the final norm…");
const head = loadHead(await (await fetch(base + "heads/head.bin")).arrayBuffer());
const normW = await loadNorm(await (await fetch(base + "heads/norm.bin")).arrayBuffer());
// marker ids from the tokenizer's added_tokens
const tk = await (await fetch(base + "model/tokenizer.json")).json();
const added = new Map((tk.added_tokens ?? []).map((t) => [t.content, t.id]));
const mid = {};
for (const [k, m] of Object.entries(MARKERS)) {
const id = added.get(m);
if (typeof id !== "number") throw new Error("marker without a token id: " + m);
mid[k] = id;
}
const model = new AliasWeb(inner.tok?.tokenizer ?? inner.tok, (ids, positions) => be.prefill(ids, { positions }), head, mid, normW);
model.be = be; model.base = base;
status("warm-up…");
const w = model.encode("warmup", "ok?", ["Yes", "No"]);
await model.prefill(w.ids, []);
status("ready");
return model;
}
encode(state, question, options) {
const t = this.tok;
const idsOf = (text) => Array.from(t.encode(sanitize(text), { add_special_tokens: false }), Number);
const ids = [this.mid.state, ...idsOf(state), this.mid.q, ...idsOf(question)];
const optEnds = [];
for (const o of options) {
ids.push(this.mid.opt, ...idsOf(o));
optEnds.push(ids.length);
ids.push(this.mid.optEnd);
}
ids.push(this.mid.decide);
return { ids, decide: ids.length - 1, optEnds };
}
async ask(q) {
const t0 = performance.now();
let options, question, labels;
if (q.kind === "noul") { options = ["Yes", "No"]; question = q.q ?? ""; labels = ["yes", "no"]; }
else if (q.kind === "score") { options = LEVELS; question = q.q ?? DEFAULT_SCORE_Q; labels = LEVELS; }
else { options = q.options; question = q.q ?? ""; labels = options; }
const enc = this.encode(q.state, question, options);
const p0 = performance.now();
const { hiddens } = await this.prefill(enc.ids, [enc.decide, ...enc.optEnds]);
const prefillMs = performance.now() - p0;
const hDecide = this.norm(hiddens[0]), perOpt = hiddens.slice(1).map((h) => this.norm(h));
const raws = perOpt.map((h) => headLogit(this.head, hDecide, h));
const ps = softmax(raws);
const out = { kind: q.kind, ms: performance.now() - t0, prefillMs, nTokens: enc.ids.length, raws: {} };
labels.forEach((l, i) => out.raws[l] = raws[i]);
if (q.kind === "noul") out.p = ps[0];
else if (q.kind === "score") {
out.per = Object.fromEntries(labels.map((l, i) => [l, ps[i]]));
out.level = 1 + ps.indexOf(Math.max(...ps));
out.expected = ps.reduce((a, p, i) => a + p * (i + 1), 0);
} else {
out.choice = Object.fromEntries(labels.map((l, i) => [l, ps[i]]));
out.top = labels[ps.indexOf(Math.max(...ps))];
out.p = Math.max(...ps);
}
return out;
}
// ── System 2: text from the same 4-bit weights ─────────────────────────
// The graph exposes logits; the runner's backend samples from them (end tokens <|im_end|> and
// <|endoftext|>).
async generate(q) {
const t0 = performance.now();
const state = q.state ?? "";
const prompt = (q.prompt ?? "").trim();
const user = state ? prompt + "\n\n" + state : prompt;
const t = this.tok;
const idsOf = (text) => Array.from(t.encode(sanitize(text), { add_special_tokens: false }), Number);
if (!this._added) {
const tk = await (await fetch(this.base + "model/tokenizer.json")).json();
this._added = new Map((tk.added_tokens ?? []).map((x) => [x.content, x.id]));
}
const imS = this._added.get("<|im_start|>"), imE = this._added.get("<|im_end|>");
// The base family deliberates by default and this checkpoint in 4-bit does not close the block:
// the assistant turn is forced to start after the closing marker. The special tokens go in by id,
// never through idsOf(): the sanitiser would rewrite them.
const body = idsOf("user\n" + user);
const asst = q.forceNoThink === false
? [imS, ...idsOf("assistant\n")]
: [imS, ...idsOf("assistant\n</think>\n\n")];
const ids = [imS, ...body, imE, ...asst];
const out = [];
const tt0 = performance.now();
for await (const tokId of this.be.generate(ids, {
maxNewTokens: q.maxNewTokens ?? 96,
temperature: q.temperature ?? 0.7,
topP: q.topP ?? 0.9,
})) {
out.push(tokId);
if (q.onToken) q.onToken(t.decode([tokId], { skip_special_tokens: true }), out.length);
}
const genMs = performance.now() - tt0;
return {
text: t.decode(out, { skip_special_tokens: true, clean_up_tokenization_spaces: false }).trim(),
tokens: out.length, genMs,
tokPerS: +(out.length / (genMs / 1000)).toFixed(1),
ms: performance.now() - t0,
};
}
}
|