Download aliasweb.mjs from aliasrobotics/alias3-web: direct link, hf CLI and curl.
- Browser
- Download file 10.1 kB
-
https://huggingface.co/aliasrobotics/alias3-web/resolve/main/aliasweb.mjs
- Command line
-
hf download hf://aliasrobotics/alias3-web/aliasweb.mjs
-
curl -L -o aliasweb.mjs https://huggingface.co/aliasrobotics/alias3-web/resolve/main/aliasweb.mjs
10.1 kB
| // 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, | |
| }; | |
| } | |
| } | |