// ============================================================================= // File : main.js // Project : The Knowledge Lifecycle of Large Language Models // Purpose : Browser-side measurement engine. Loads GPT-2, runs the two // prompt conditions, and renders the narrative, the paired // distributions, the gauges, and the stage diagnosis. // Tech Stack : JavaScript (ES modules), transformers.js 3.4.0, // ONNX Runtime Web (WASM) // Authors : Amey Thakur (https://github.com/Amey-Thakur) // Sarvesh Talele (https://github.com/sarveshtalele) // Repository : https://github.com/Amey-Thakur/LLM-KNOWLEDGE-LIFECYCLE // Release Date: August 18, 2026 // License : CC BY 4.0 // ============================================================================= // The entire computation runs locally: the model is downloaded once from the // Hugging Face Hub, and every forward pass happens in this tab. Deterministic: // identical inputs give identical numbers on every run. import { AutoTokenizer, AutoModelForCausalLM } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.4.0"; const PRESETS = { vioxx: { query: "Question: Is Vioxx safe to prescribe? Answer: Vioxx is considered", context: "Context: In September 2004, Merck voluntarily withdrew Vioxx after trials revealed increased cardiovascular risks.", answer: " withdrawn", // Stated as the direction of the change rather than as the winning token. // At full precision "safe" is the top answer; under the 8-bit weights this // page runs, a function word takes first place. What holds in both builds // is that the corrective document pushes "safe" up and leaves "withdrawn" // nowhere. reading: "The headline case. The withdrawal notice is in the prompt, and the model's confidence in “safe” goes up rather than down: 37.53% to 42.58% at full precision, where the paper measures 12.05 nats, far past the 9.2 failure threshold.", }, monarch: { query: "Question: Who is the current British monarch? Answer: The current British monarch is", context: "Context: Queen Elizabeth II died in September 2022. Charles III acceded to the throne and is the reigning King of the United Kingdom.", answer: " Charles", reading: "A subtler failure. The document does raise the correct answer, but its strongest effect is boosting “ Queen”. Naming a fact, even to correct it, reinforces the old association.", }, twitter: { query: "Question: What is the social network Twitter called today? Answer: Twitter is now called", context: "Context: In July 2023, Twitter was rebranded as X under Elon Musk's ownership.", answer: " X", reading: "Here the document moves the model hard and lifts the correct answer by orders of magnitude. It still answers “Twitter”. Influence without resolution.", }, }; const $ = (id) => document.getElementById(id); let tokenizer = null; let model = null; let last = null; // logits and metadata of the most recent measurement // ---------- tabs ---------- document.querySelectorAll(".tab").forEach((btn) => { btn.addEventListener("click", () => { document.querySelectorAll(".tab").forEach((b) => b.classList.remove("active")); document.querySelectorAll(".panel").forEach((p) => p.classList.remove("active")); btn.classList.add("active"); $(btn.dataset.tab).classList.add("active"); }); }); // ---------- presets ---------- function applyPreset(key) { const p = PRESETS[key]; $("query").value = p.query; $("context").value = p.context; $("answer").value = p.answer; $("preset-reading").textContent = p.reading; document.querySelectorAll(".preset").forEach((b) => b.classList.toggle("active", b.dataset.preset === key)); } document.querySelectorAll(".preset").forEach((btn) => btn.addEventListener("click", () => applyPreset(btn.dataset.preset))); applyPreset("vioxx"); // ---------- model loading (on first Measure, so one button drives everything) ---------- async function ensureModel() { if (model) return; $("load-bar-wrap").hidden = false; const status = $("load-status"); const seen = {}; const progress = (info) => { if (info.status === "progress" && info.total) { seen[info.file] = info.loaded / info.total; const vals = Object.values(seen); const pct = (vals.reduce((a, b) => a + b, 0) / vals.length) * 100; $("load-bar").style.width = pct.toFixed(1) + "%"; status.textContent = "downloading GPT-2, " + pct.toFixed(0) + "% of 128 MB, one time only"; } }; status.textContent = "downloading GPT-2 (128 MB, one time only)"; tokenizer = await AutoTokenizer.from_pretrained("Xenova/gpt2", { progress_callback: progress }); // The Xenova/gpt2 repo uses legacy file naming: dtype "q8" maps to the // "_quantized" suffix, and the merged decoder is the 128 MB build. model = await AutoModelForCausalLM.from_pretrained("Xenova/gpt2", { model_file_name: "decoder_model_merged", dtype: "q8", progress_callback: progress, }); status.textContent = "model ready: GPT-2 base, 8-bit quantized, running locally"; $("load-bar-wrap").hidden = true; } // ---------- measurement core ---------- // Regime boundaries in nats. SYNC is ln 2 exactly, the point at which the // correct answer holds half the probability mass; PERCENT and FAIL are ln 100 // and ln 10000 to the precision they are quoted at. const SYNC = Math.LN2, PERCENT = 4.6, FAIL = 9.2; async function lastLogits(text) { const inputs = await tokenizer(text); const { logits } = await model(inputs); const [, T, V] = logits.dims; return Float32Array.from(logits.data.slice((T - 1) * V, T * V)); } function softmax(row, temperature = 1.0) { const V = row.length; let max = -Infinity; for (let i = 0; i < V; i++) if (row[i] > max) max = row[i]; let sum = 0; const probs = new Float64Array(V); for (let i = 0; i < V; i++) { probs[i] = Math.exp((row[i] - max) / temperature); sum += probs[i]; } for (let i = 0; i < V; i++) probs[i] /= sum; return probs; } /** * Indices of the k largest probabilities, highest first. * * One pass over the vocabulary, maintaining a short sorted list. Repeated scans * with an "already taken" set cost k passes over 50,257 entries per call. */ function topK(probs, k) { const best = []; // indices, kept sorted by descending probability for (let i = 0; i < probs.length; i++) { const p = probs[i]; if (best.length === k && p <= probs[best[k - 1]]) continue; let at = best.length; while (at > 0 && probs[best[at - 1]] < p) at--; best.splice(at, 0, i); if (best.length > k) best.pop(); } return best; } // ---------- rendering ---------- const fmtPct = (p) => p >= 0.0001 ? (p * 100).toFixed(2) + "%" : "<0.01%"; const fmtTok = (t) => JSON.stringify(t).slice(1, -1); function renderBars(pPlain, pCtx, target) { // Union of both top-5 sets, plus the correct answer, ordered by with-context // probability. Linear scale on purpose: a correct answer you cannot see IS // the finding. const ids = [...new Set([...topK(pCtx, 5), ...topK(pPlain, 5), target])]; ids.sort((a, b) => pCtx[b] - pCtx[a]); const maxP = Math.max(pCtx[ids[0]], pPlain[ids[0]], 1e-9); const rows = ids.map((i) => { const tok = fmtTok(tokenizer.decode([i])); const cls = i === target ? "bar-row hit" : "bar-row"; const w0 = Math.max(0.4, (pPlain[i] / maxP) * 100); const w1 = Math.max(0.4, (pCtx[i] / maxP) * 100); return `