TinyDecide / conformance /make.mjs
TheREZOR's picture
Python, Rust and ESP32 engines, shared conformance set, promo video
f2878d0 verified
Raw History Blame Contribute Delete
14.8 kB
// Builds the conformance fixture every port must pass, from the reference engine (tinydecide.js,
// which matches PyTorch to 3e-6 on ref.json).
// node make.mjs [S768_dir] [full_dir]
// Writes cases.json (inputs, shared by both builds) and expected.<build>.json (reference outputs).
import { readFileSync, writeFileSync, existsSync } from "node:fs";
import { createHash } from "node:crypto";
const here = new URL(".", import.meta.url).pathname;
// Two layouts: this repo (sdk/js next to sdk/conformance) and the Hugging Face repo (engine at the root).
const HF = !existsSync(here + "../js/tinydecide.js");
const JS = here + (HF ? "../" : "../js/");
const { TinyDecide, WordPiece } = await import(JS + "tinydecide.js");
const { makeProtos } = await import(JS + "corrections.js");
const BUILDS = { S768: process.argv[2] || here + (HF ? ".." : "../../web_S768"), full: process.argv[3] || here + (HF ? "../full" : "../../web_fc") };
// ------------------------------------------------------------------ tokenizer strings
const TOK = [
"", " ", "Hello, World!", " leading and trailing ", "tab\there\nnewline\r\nCRLF",
"Café crème brûlée, naïve résumé, jalapeño, Ångström", "é à ö combining marks",
"10 km by the thin space ideographic", "line
separator
paragraph",
"don’t “quote” ‘single’ «guillemets» — en–dash … ellipsis", "zero​widthbom­soft‍zwj",
"bell\u0007 escape\u001b null\u0000 del\u007f c1\u0085", "replacement � char",
"我爱北京 Tokyo 東京タワー ひらがな 한국어", "pizza 🍕 tonight 🎉🎉 👍🏽 family 👨‍👩‍👧",
"İstanbul ŞEHİR ı i̇ I", "ΣΊΣΥΦΟΣ ελληνικά Ωμέγα", "Straße STRASSE ß ẞ", "file flow ligatures",
"ABC fullwidth 123 !?", "Привет, мир! Ёлка Йод Їжак ґанок", "مرحبا بالعالم", "שלום עולם",
"नमस्ते दुनिया", "สวัสดีชาวโลก", "Ελλάδα: Ά Έ Ή Ί Ό Ύ Ώ ΐ ΰ", "DŽ Dž dž LJ titlecase", "ŁÓDŹ Gdańsk Żółć",
"Øresund Æble Œuvre Ðorđe Þór", "x".repeat(101), "a".repeat(100), "abc" + "é".repeat(99),
"unbelievably-hyphenated_words/with\\slashes", "3.14 $1,000.50 €20 £5 ¥300 50% #tag @user",
"email a.b+c@ex-ample.co.uk url https://x.io/a?b=c&d=e#f", "Hey!!! What?!?! ...", "´`^¨~ spacing accents",
"math 𝐀𝐁𝐂 𝑥𝑦 ℝ ℕ ∑ ∫ √ ≤ ≥ ≠", "© ® ™ § ¶ † ‡ • ° ± × ÷", "private  use", "ꞯ ɐ ʃ IPA ŋ",
"Ⅻ Ⅳ roman numerals ①②③", "Džungla Lj Nj", "tschüß MÜNCHEN Öl Äpfel", "ÀÁÂÃÄÅàáâãäå ÈÉÊËèéêë",
"Ċ ċ Ġ ġ Ħ ħ IJ ij Ŀ ŀ ʼn Ŋ ŋ ſ", "Ǎ ǎ Ǐ ǐ Ǒ ǒ Ǔ ǔ Ǖ ǖ", "Ạ ạ Ả ả Ấ ấ Việt Nam tiếng Việt",
];
// Seeded fuzz over long-assigned blocks (no lone surrogates, nothing new in Unicode 16/17).
let seed = 1234567;
const rnd = () => ((seed = (seed * 1103515245 + 12345) >>> 0) / 4294967296);
const POOLS = [[0x20, 0x7e], [0xa0, 0x17f], [0x180, 0x24f], [0x250, 0x2ff], [0x300, 0x36f], [0x370, 0x3ff],
[0x400, 0x4ff], [0x530, 0x58f], [0x5d0, 0x5ea], [0x600, 0x6ff], [0x900, 0x97f], [0xe00, 0xe5b], [0x1e00, 0x1fff],
[0x2000, 0x206f], [0x2100, 0x214f], [0x2190, 0x23ff], [0x2460, 0x24ff], [0x3000, 0x30ff], [0x4e00, 0x4fff],
[0xac00, 0xad00], [0xfb00, 0xfb06], [0xff01, 0xff5e], [0x1d400, 0x1d4ff], [0x1f300, 0x1f64f], [0x1f900, 0x1f9ff]];
const RE_CN = /\p{Cn}/u;
for (let i = 0; i < 240; i++) {
let s = "";
const n = 3 + Math.floor(rnd() * 30);
while ([...s].length < n) {
const [a, b] = rnd() < 0.35 ? POOLS[0] : POOLS[Math.floor(rnd() * POOLS.length)];
const cp = a + Math.floor(rnd() * (b - a + 1));
const ch = String.fromCodePoint(cp);
if (!RE_CN.test(ch)) s += ch;
if (rnd() < 0.15) s += " ";
}
TOK.push(s);
}
// ------------------------------------------------------------------ requests
const LONG = "Dear team, I wanted to give you a full update on the warehouse move before the weekend. " +
"The trucks are booked for Saturday morning at 6:30 and the first load will leave the old site around 7:15. " +
"We still need four more volunteers for the afternoon shift, so please reply if you can help. " +
"Parking at the new building is behind the main entrance, and the code for the side gate is 4471. " +
"Lunch will be provided by the Italian place on Market Street, and there will be vegetarian options. " +
"If anything breaks during the move, write it down on the damage sheet next to the loading bay and tell Maria. " +
"The internet at the new site will not be live until Tuesday, so download anything you need beforehand. " +
"Thanks again for all the hard work this month, it has been a long one and I really appreciate it.";
const APPS = ["reminders", "music", "calendar", "restaurants", "weather"];
const README_QS = [
{ type: "choice", text: "Which app should handle this?", options: APPS },
{ type: "noul", text: "The message is urgent." },
{ type: "score", text: "How positive is the tone?", options: ["negative", "neutral", "positive"] },
{ type: "span", text: "Extract the time." },
];
const NOTE_Q = { type: "choice", text: "What kind of note is this?", options: ["something to buy", "a task to do", "an event with a date or time", "a phone number or email"] };
const SCORE_Q = { type: "score", text: "How urgent is this?", options: ["not urgent", "somewhat urgent", "very urgent"] };
const NOUL_Q = { type: "noul", text: "The message is about food." };
const REQ = [
{ name: "readme", state: "Book a table for 4 at an Italian place near the station on Friday at 7:30", questions: README_QS },
{ name: "multi7", state: "Hey, can you remind me to call Dr. Patel at 555-0142 tomorrow at 9am? It's about my test results.",
questions: [
{ type: "choice", text: "Which app should handle this?", options: ["reminders", "music", "calendar", "phone", "weather"] },
{ type: "noul", text: "The message is urgent." },
{ type: "noul", text: "The message mentions a phone number." },
{ type: "score", text: "How worried does the writer sound?", options: ["calm", "a little worried", "very worried"] },
{ type: "span", text: "Extract the phone number." },
{ type: "span", text: "Extract the time." },
{ type: "choice", text: "What is the topic?", options: ["health", "money", "travel", "work", "family", "sport", "food", "weather"] },
] },
{ name: "long_truncated", state: LONG, questions: [
{ type: "choice", text: "What is this message about?", options: ["a move", "a party", "a complaint", "a job offer"] },
{ type: "span", text: "Extract the gate code." },
{ type: "span", text: "Extract when the trucks leave." },
{ type: "noul", text: "The writer is thankful." } ] },
{ name: "empty_state", state: "", questions: [{ type: "noul", text: "The message is empty." }, { type: "span", text: "Extract the name." },
{ type: "choice", text: "Pick one.", options: ["yes", "no"] }] },
{ name: "unicode_state", state: "Réservez une table pour 2 au Café Müller — demain à 19h30, s’il vous plaît! 🍷 Tél: +33 6 12 34 56 78",
questions: [ { type: "choice", text: "Which language is this?", options: ["French", "German", "English", "Spanish"] },
{ type: "span", text: "Extract the phone number." }, { type: "span", text: "Extract the restaurant name." },
{ type: "noul", text: "The writer is polite." } ] },
{ name: "unicode_question", state: "Привет! Купи молоко и хлеб, пожалуйста. Встреча в 18:00 у метро «Киевская».",
questions: [ { type: "choice", text: "Qu’est-ce que c’est — “shopping” or “meeting”?", options: ["achats 🛒", "réunion", "naïve idée"] },
{ type: "span", text: "Extract the time." }, { type: "span", text: "Extract the place." } ] },
{ name: "cjk_emoji", state: "明天下午3点在東京駅见面 🚄 don't be late!!", questions: [
{ type: "noul", text: "This is about meeting someone." }, { type: "span", text: "Extract the time." },
{ type: "choice", text: "Which script is mostly used?", options: ["Chinese characters", "Latin letters", "Cyrillic"] } ] },
{ name: "noul_with_options", state: "The meeting moved to room 4B.", questions: [
{ type: "noul", text: "The message gives a room.", options: ["no", "yes"] },
{ type: "span", text: "Extract the room." } ] },
{ name: "choice32", state: "I'd like a large pepperoni pizza delivered to 12 Oak Lane please.", questions: [
{ type: "choice", text: "What does the person want?", options: Array.from({ length: 32 }, (_, i) => ["pizza", "sushi", "tacos", "a taxi", "a doctor", "a plumber", "flowers", "a haircut"][i % 8] + " " + (i + 1)) } ] },
{ name: "score5", state: "This is the worst service I've ever had. Never again.", questions: [
{ type: "score", text: "How satisfied is the customer?", options: ["very unhappy", "unhappy", "neutral", "happy", "very happy"] },
{ type: "noul", text: "The customer will come back." } ] },
{ name: "span_numbers", state: "Order #A-77812 shipped on 2026-10-03, total $149.99, tracking 1Z999AA10123456784.", questions: [
{ type: "span", text: "Extract the order number." }, { type: "span", text: "Extract the total." },
{ type: "span", text: "Extract the date." }, { type: "span", text: "Extract the tracking number." }, { type: "span", text: "Extract the email address." } ] },
{ name: "whitespace", state: " \t buy milk \n\n and
eggs ", questions: [NOTE_Q, { type: "span", text: "Extract the items." }] },
{ name: "too_long_question", state: "hello", questions: [{ type: "choice", text: "Which one? " + "very ".repeat(200), options: ["a", "b"] }] },
{ name: "one_option", state: "hello there", questions: [{ type: "choice", text: "Pick one.", options: ["only"] }] },
{ name: "score_no_levels", state: "hello there", questions: [{ type: "noul", text: "It is a greeting." }, { type: "score", text: "How formal is it?", options: [] }] },
{ name: "choice33", state: "hello there", questions: [{ type: "choice", text: "Which number?", options: Array.from({ length: 33 }, (_, i) => String(i + 1)) }] },
];
// protos cases: examples come from the model's own answers on training notes (built per build below)
const PROTO_TRAIN = {
note: [["buy oat milk", "get coffee beans and bread"], ["call the plumber", "fix the bike"], ["dentist thursday 4pm"], []],
score: [["whenever you have time", "no rush"], ["by end of week please"], ["ASAP the server is down", "urgent: fire alarm"]],
noul: [["the car needs new tyres", "meeting at noon"], ["pizza for dinner", "grab some sushi", "bake a cake"]],
};
const PROTO_ASK = [
{ name: "protos_choice", state: "pick up eggs and butter", q: NOTE_Q, kind: "note" },
{ name: "protos_choice_event", state: "lunch with Sam on friday 1pm", q: NOTE_Q, kind: "note" },
{ name: "protos_score", state: "please reply today, the client is waiting", q: SCORE_Q, kind: "score" },
{ name: "protos_noul", state: "make pasta tonight", q: NOUL_Q, kind: "noul" },
{ name: "protos_one_example", state: "buy stamps", q: NOTE_Q, kind: "note", only: 1 },
{ name: "protos_no_center", state: "pick up eggs and butter", q: NOTE_Q, kind: "note", noCenter: true },
];
// ------------------------------------------------------------------ run
const r6 = (x) => Math.round(x * 1e6) / 1e6;
const arr = (a) => Array.from(a, r6);
function outAnswer(a) {
if (a.type === "choice" || a.type === "score") {
const o = { type: a.type, probs: arr(a.probs), pick: a.pick, confidence: r6(a.confidence), z0: arr(a.z0), qvec: arr(a.qvec) };
if (a.type === "score") o.score = r6(a.score);
return o;
}
if (a.type === "noul") return { type: "noul", p: r6(a.p), z0: arr(a.z0), qvec: arr(a.qvec) };
return { type: "span", p_present: r6(a.p_present), p_span: r6(a.p_span), tok: a.tok, text: a.text };
}
const ser = (p) => p && { vec: arr(p.vec), cnt: Array.from(p.cnt), center: p.center ? arr(p.center) : null, lam: p.lam };
const cases = { version: 1, engine_sha256: sha(readFileSync(JS + "tinydecide.js")), tokenizer: TOK, requests: [], protos: [] };
function sha(b) { return createHash("sha256").update(b).digest("hex"); }
let first = true;
for (const [build, dir] of Object.entries(BUILDS)) {
const meta = JSON.parse(readFileSync(dir + "/meta.json", "utf8"));
const bin = readFileSync(dir + "/model.bin");
const m = new TinyDecide(meta, bin.buffer.slice(bin.byteOffset, bin.byteOffset + bin.byteLength));
const tok = new WordPiece(meta.tokenizer);
const exp = { build, model_sha256: sha(bin), meta_sha256: sha(readFileSync(dir + "/meta.json")), tokenizer: [], requests: [], protos: [] };
for (const s of TOK) exp.tokenizer.push(tok.encode(s).ids);
// ref.json inputs are the same for both builds (the Hugging Face repo keeps one, at the root)
const ref = JSON.parse(readFileSync(BUILDS.S768 + "/ref.json", "utf8"));
const all = [...REQ, ...ref.map((r, i) => ({ name: "ref" + i, state: r.state, questions: r.questions }))];
for (const r of all) {
if (first) cases.requests.push({ name: r.name, state: r.state, questions: r.questions });
try {
const got = m.answer({ state: r.state, questions: r.questions });
exp.requests.push({ name: r.name, ids: got.ids, truncated: got.truncated, tokens: got.tokens, answers: got.answers.map(outAnswer) });
} catch (e) {
exp.requests.push({ name: r.name, error: e.message });
}
}
// corrections: examples -> protos (makeProtos) -> answers with protos
for (const c of PROTO_ASK) {
const lists = PROTO_TRAIN[c.kind].map((l, k) => (c.only !== undefined ? (k === 0 ? l.slice(0, c.only) : []) : l).map((st) => {
const a = m.answer({ state: st, questions: [c.q] }).answers[0];
return { v: arr(a.qvec), z: arr(a.z0) };
}));
let p = makeProtos(c.q.type, lists, meta.beta);
if (c.noCenter) p = { vec: p.vec, cnt: p.cnt, center: null, lam: p.lam };
if (first) cases.protos.push({ name: c.name, state: c.state, question: c.q, no_center: !!c.noCenter, examples_from: PROTO_TRAIN[c.kind], only: c.only ?? null });
const got = m.answer({ state: c.state, questions: [c.q] }, [p]);
exp.protos.push({ name: c.name, examples: lists, protos: ser(p), ids: got.ids, answers: got.answers.map(outAnswer) });
}
writeFileSync(here + `expected.${build}.json`, JSON.stringify(exp));
console.log(`${build}: ${exp.tokenizer.length} tokenizer strings, ${exp.requests.length} requests ` +
`(${exp.requests.filter((r) => r.error).length} errors, ${exp.requests.filter((r) => r.truncated).length} truncated), ${exp.protos.length} protos cases`);
first = false;
}
writeFileSync(here + "cases.json", JSON.stringify(cases, null, 1));