ameythakur commited on
Commit
992ee34
Β·
verified Β·
1 Parent(s): a3f332c

Knowledge Lifecycle

Browse files
Files changed (1) hide show
  1. main.js +165 -49
main.js CHANGED
@@ -1,6 +1,19 @@
1
- // LLM Knowledge Lifecycle: browser-side measurement on GPT-2.
 
 
 
 
 
 
 
 
 
 
 
 
2
  // The entire computation runs locally: the model is downloaded once from the
3
- // Hugging Face Hub, and every forward pass happens in this tab.
 
4
 
5
  import { AutoTokenizer, AutoModelForCausalLM } from
6
  "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.4.0";
@@ -10,7 +23,7 @@ const PRESETS = {
10
  query: "Question: Is Vioxx safe to prescribe? Answer: Vioxx is considered",
11
  context: "Context: In September 2004, Merck voluntarily withdrew Vioxx after trials revealed increased cardiovascular risks.",
12
  answer: " withdrawn",
13
- reading: "The paper's headline case. The withdrawal notice is in the prompt, yet the correct answer stays near one in a hundred thousand. Expect D_sync deep past the 9.2-nat failure threshold; the paper's fp32 run measures 12.05 nats with I_ctx = 0.033.",
14
  },
15
  monarch: {
16
  query: "Question: Who is the current British monarch? Answer: The current British monarch is",
@@ -29,6 +42,7 @@ const PRESETS = {
29
  const $ = (id) => document.getElementById(id);
30
  let tokenizer = null;
31
  let model = null;
 
32
 
33
  // ---------- tabs ----------
34
  document.querySelectorAll(".tab").forEach((btn) => {
@@ -54,9 +68,9 @@ document.querySelectorAll(".preset").forEach((btn) =>
54
  btn.addEventListener("click", () => applyPreset(btn.dataset.preset)));
55
  applyPreset("vioxx");
56
 
57
- // ---------- model loading ----------
58
- $("load-btn").addEventListener("click", async () => {
59
- $("load-btn").disabled = true;
60
  $("load-bar-wrap").hidden = false;
61
  const status = $("load-status");
62
  const seen = {};
@@ -66,47 +80,44 @@ $("load-btn").addEventListener("click", async () => {
66
  const vals = Object.values(seen);
67
  const pct = (vals.reduce((a, b) => a + b, 0) / vals.length) * 100;
68
  $("load-bar").style.width = pct.toFixed(1) + "%";
69
- status.textContent = "downloading " + pct.toFixed(0) + "%";
70
  }
71
  };
72
- try {
73
- status.textContent = "downloading";
74
- tokenizer = await AutoTokenizer.from_pretrained("Xenova/gpt2", { progress_callback: progress });
75
- // The Xenova/gpt2 repo uses legacy file naming: dtype "q8" maps to the
76
- // "_quantized" suffix, and the merged decoder is the 128 MB build.
77
- model = await AutoModelForCausalLM.from_pretrained("Xenova/gpt2", {
78
- model_file_name: "decoder_model_merged",
79
- dtype: "q8",
80
- progress_callback: progress,
81
- });
82
- status.textContent = "ready (GPT-2 base, 8-bit quantized, running locally)";
83
- $("load-bar").style.width = "100%";
84
- $("run").disabled = false;
85
- } catch (e) {
86
- status.textContent = "failed to load: " + e.message;
87
- $("load-btn").disabled = false;
88
- }
89
- });
90
 
91
- // ---------- measurement ----------
92
- async function nextTokenDistribution(text) {
93
  const inputs = await tokenizer(text);
94
  const { logits } = await model(inputs);
95
  const [, T, V] = logits.dims;
96
- const row = logits.data.slice((T - 1) * V, T * V);
97
- // stable softmax
 
 
 
98
  let max = -Infinity;
99
  for (let i = 0; i < V; i++) if (row[i] > max) max = row[i];
100
  let sum = 0;
101
  const probs = new Float64Array(V);
102
- for (let i = 0; i < V; i++) { probs[i] = Math.exp(row[i] - max); sum += probs[i]; }
103
  for (let i = 0; i < V; i++) probs[i] /= sum;
104
  return probs;
105
  }
106
 
107
  function topK(probs, k) {
108
- const idx = [];
109
  const taken = new Set();
 
110
  for (let n = 0; n < k; n++) {
111
  let best = -1, bp = -1;
112
  for (let i = 0; i < probs.length; i++) {
@@ -118,27 +129,115 @@ function topK(probs, k) {
118
  return idx;
119
  }
120
 
121
- function renderTop(tableId, probs, targetId) {
122
- const rows = topK(probs, 8).map((i) => {
123
- const tok = tokenizer.decode([i]);
124
- const hit = i === targetId ? ' class="hit"' : "";
125
- return `<tr${hit}><td>${JSON.stringify(tok).slice(1, -1)}</td><td>${(probs[i] * 100).toFixed(4)}%</td></tr>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  });
127
- $(tableId).innerHTML = "<tr><th>token</th><th>probability</th></tr>" + rows.join("");
 
 
128
  }
129
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  $("run").addEventListener("click", async () => {
131
  const runBtn = $("run");
132
  runBtn.disabled = true;
133
- runBtn.textContent = "measuring...";
134
  try {
 
 
 
 
135
  const query = $("query").value.trim();
136
  const context = $("context").value.trim();
 
137
  let answer = $("answer").value;
138
  if (!answer.startsWith(" ")) answer = " " + answer.trim();
 
 
 
 
139
 
140
- const pPlain = await nextTokenDistribution(query);
141
- const pCtx = await nextTokenDistribution(context + "\n" + query);
 
 
 
142
 
143
  const ids = tokenizer.encode(answer);
144
  const pieces = ids.map((i) => tokenizer.decode([i]));
@@ -152,6 +251,9 @@ $("run").addEventListener("click", async () => {
152
  if (pCtx[i] > 0 && pPlain[i] > 0) ictx += pCtx[i] * Math.log(pCtx[i] / pPlain[i]);
153
  }
154
 
 
 
 
155
  let chipText, chipClass, detail;
156
  if (dsync > 9.2) {
157
  if (ictx < 0.05) {
@@ -172,32 +274,46 @@ $("run").addEventListener("click", async () => {
172
  detail = "correct answer holds at least half the probability mass";
173
  }
174
 
175
- // Marker positions follow the drawn zone boundaries: D_sync thresholds
176
  // 0.7 / 4.6 / 9.2 sit at 5% / 33% / 66%, scale capped at 14 nats.
177
  const dPos = dsync <= 0.7 ? (dsync / 0.7) * 5
178
  : dsync <= 4.6 ? 5 + ((dsync - 0.7) / 3.9) * 28
179
  : dsync <= 9.2 ? 33 + ((dsync - 4.6) / 4.6) * 33
180
  : Math.min(100, 66 + ((dsync - 9.2) / 4.8) * 34);
181
- // I_ctx thresholds 0.05 / 0.5 sit at 10% / 50%, scale capped at 2 nats.
182
  const iPos = ictx <= 0.05 ? (ictx / 0.05) * 10
183
  : ictx <= 0.5 ? 10 + ((ictx - 0.05) / 0.45) * 40
184
  : Math.min(100, 50 + ((ictx - 0.5) / 1.5) * 50);
185
 
 
 
 
 
 
 
 
 
 
186
  const chip = $("verdict-chip");
187
  chip.textContent = chipText;
188
  chip.className = "chip " + chipClass;
189
- $("r-verdict-detail").textContent = detail;
 
 
190
  $("g-marker").style.left = dPos.toFixed(1) + "%";
191
  $("g-ictx").style.left = iPos.toFixed(1) + "%";
192
-
193
- $("r-tok").textContent = `${JSON.stringify(pieces)} (${ids.length} piece(s); first piece measured)`;
194
- $("r-p0").textContent = pt0.toExponential(2);
195
- $("r-p1").textContent = pt1.toExponential(2);
196
  $("r-dsync").textContent = dsync.toFixed(2) + " nats";
197
  $("r-ictx").textContent = ictx.toFixed(3) + " nats";
198
- renderTop("t-plain", pPlain, target);
199
- renderTop("t-ctx", pCtx, target);
 
 
 
 
 
 
200
  $("results").hidden = false;
 
 
201
  } finally {
202
  runBtn.disabled = false;
203
  runBtn.textContent = "Measure";
 
1
+ // =============================================================================
2
+ // File : main.js
3
+ // Project : The Knowledge Lifecycle of Large Language Models
4
+ // Purpose : Browser-side D_sync measurement engine: loads GPT-2, runs both prompt
5
+ // conditions, and renders the narrative, bars, gauges, and diagnostics.
6
+ // Tech Stack : JavaScript (ES modules), transformers.js 3.4.0, ONNX Runtime Web (WASM)
7
+ // Authors : Amey Thakur (https://github.com/Amey-Thakur)
8
+ // Sarvesh Talele (https://github.com/sarveshtalele)
9
+ // Repository : https://github.com/Amey-Thakur/LLM-KNOWLEDGE-LIFECYCLE
10
+ // Release Date: August 16, 2026
11
+ // License : CC BY 4.0
12
+ // =============================================================================
13
+
14
  // The entire computation runs locally: the model is downloaded once from the
15
+ // Hugging Face Hub, and every forward pass happens in this tab. Deterministic:
16
+ // identical inputs give identical numbers on every run.
17
 
18
  import { AutoTokenizer, AutoModelForCausalLM } from
19
  "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.4.0";
 
23
  query: "Question: Is Vioxx safe to prescribe? Answer: Vioxx is considered",
24
  context: "Context: In September 2004, Merck voluntarily withdrew Vioxx after trials revealed increased cardiovascular risks.",
25
  answer: " withdrawn",
26
+ reading: "The paper's headline case. The withdrawal notice sits in the prompt, and the model still answers β€œsafe”. Expect D_sync deep past the 9.2-nat failure threshold; the paper's fp32 run measures 12.05 nats with I_ctx = 0.033.",
27
  },
28
  monarch: {
29
  query: "Question: Who is the current British monarch? Answer: The current British monarch is",
 
42
  const $ = (id) => document.getElementById(id);
43
  let tokenizer = null;
44
  let model = null;
45
+ let last = null; // logits and metadata of the most recent measurement
46
 
47
  // ---------- tabs ----------
48
  document.querySelectorAll(".tab").forEach((btn) => {
 
68
  btn.addEventListener("click", () => applyPreset(btn.dataset.preset)));
69
  applyPreset("vioxx");
70
 
71
+ // ---------- model loading (on first Measure, so one button drives everything) ----------
72
+ async function ensureModel() {
73
+ if (model) return;
74
  $("load-bar-wrap").hidden = false;
75
  const status = $("load-status");
76
  const seen = {};
 
80
  const vals = Object.values(seen);
81
  const pct = (vals.reduce((a, b) => a + b, 0) / vals.length) * 100;
82
  $("load-bar").style.width = pct.toFixed(1) + "%";
83
+ status.textContent = "downloading GPT-2, " + pct.toFixed(0) + "% of 128 MB, one time only";
84
  }
85
  };
86
+ status.textContent = "downloading GPT-2 (128 MB, one time only)";
87
+ tokenizer = await AutoTokenizer.from_pretrained("Xenova/gpt2", { progress_callback: progress });
88
+ // The Xenova/gpt2 repo uses legacy file naming: dtype "q8" maps to the
89
+ // "_quantized" suffix, and the merged decoder is the 128 MB build.
90
+ model = await AutoModelForCausalLM.from_pretrained("Xenova/gpt2", {
91
+ model_file_name: "decoder_model_merged",
92
+ dtype: "q8",
93
+ progress_callback: progress,
94
+ });
95
+ status.textContent = "model ready: GPT-2 base, 8-bit quantized, running locally";
96
+ $("load-bar-wrap").hidden = true;
97
+ }
 
 
 
 
 
 
98
 
99
+ // ---------- measurement core ----------
100
+ async function lastLogits(text) {
101
  const inputs = await tokenizer(text);
102
  const { logits } = await model(inputs);
103
  const [, T, V] = logits.dims;
104
+ return Float32Array.from(logits.data.slice((T - 1) * V, T * V));
105
+ }
106
+
107
+ function softmax(row, temperature = 1.0) {
108
+ const V = row.length;
109
  let max = -Infinity;
110
  for (let i = 0; i < V; i++) if (row[i] > max) max = row[i];
111
  let sum = 0;
112
  const probs = new Float64Array(V);
113
+ for (let i = 0; i < V; i++) { probs[i] = Math.exp((row[i] - max) / temperature); sum += probs[i]; }
114
  for (let i = 0; i < V; i++) probs[i] /= sum;
115
  return probs;
116
  }
117
 
118
  function topK(probs, k) {
 
119
  const taken = new Set();
120
+ const idx = [];
121
  for (let n = 0; n < k; n++) {
122
  let best = -1, bp = -1;
123
  for (let i = 0; i < probs.length; i++) {
 
129
  return idx;
130
  }
131
 
132
+ // ---------- rendering ----------
133
+ const fmtPct = (p) => p >= 0.0001 ? (p * 100).toFixed(2) + "%" : "<0.01%";
134
+ const fmtTok = (t) => JSON.stringify(t).slice(1, -1);
135
+
136
+ function renderBars(pPlain, pCtx, target) {
137
+ // Union of both top-5 sets, plus the correct answer, ordered by with-context
138
+ // probability. Linear scale on purpose: a correct answer you cannot see IS
139
+ // the finding.
140
+ const ids = [...new Set([...topK(pCtx, 5), ...topK(pPlain, 5), target])];
141
+ ids.sort((a, b) => pCtx[b] - pCtx[a]);
142
+ const maxP = Math.max(pCtx[ids[0]], pPlain[ids[0]], 1e-9);
143
+ const rows = ids.map((i) => {
144
+ const tok = fmtTok(tokenizer.decode([i]));
145
+ const cls = i === target ? "bar-row hit" : "bar-row";
146
+ const w0 = Math.max(0.4, (pPlain[i] / maxP) * 100);
147
+ const w1 = Math.max(0.4, (pCtx[i] / maxP) * 100);
148
+ return `<div class="${cls}">
149
+ <span class="bar-tok">${tok}</span>
150
+ <span class="bar-pair">
151
+ <span class="bar b-plain" style="width:${w0.toFixed(1)}%"></span><em>${fmtPct(pPlain[i])}</em>
152
+ <span class="bar b-ctx" style="width:${w1.toFixed(1)}%"></span><em>${fmtPct(pCtx[i])}</em>
153
+ </span>
154
+ </div>`;
155
  });
156
+ $("bars").innerHTML =
157
+ '<div class="bar-row bar-head"><span class="bar-tok"></span><span class="bar-pair"><em>without document</em><em>with document</em></span></div>'
158
+ + rows.join("");
159
  }
160
 
161
+ function narrative(m) {
162
+ const s = [];
163
+ const topCtxTok = fmtTok(m.topCtx);
164
+ const ansTok = fmtTok(m.answerPiece).trim();
165
+ s.push(`With the corrective document in its prompt, the model's most likely continuation is β€œ${topCtxTok}” at ${fmtPct(m.pTopCtx)}.`);
166
+ const odds = Math.round(1 / m.pt1);
167
+ s.push(`The correct answer β€œ${ansTok}” receives ${fmtPct(m.pt1)}: about one chance in ${odds.toLocaleString()}.`);
168
+ if (m.topCtx === m.topPlain && m.ictx < 0.05) {
169
+ s.push(`The document changed almost nothing. The model's whole distribution moved by ${m.ictx.toFixed(3)} nats, and its preferred answer is the same one it gives with no document at all.`);
170
+ } else if (m.pt1 > m.pt0 * 1.5 && m.topCtx !== fmtTok(m.answerPiece)) {
171
+ const factor = (m.pt1 / m.pt0).toFixed(1);
172
+ s.push(`The document helped: it multiplied the correct answer's probability by ${factor}. It still loses.`);
173
+ } else if (m.pTopCtx > m.pTopPlainSameTok) {
174
+ s.push(`Counterintuitively, the document made the leading wrong answer stronger, raising it from ${fmtPct(m.pTopPlainSameTok)} to ${fmtPct(m.pTopCtx)}. Mentioning a fact, even to correct it, reinforces the association.`);
175
+ }
176
+ if (m.topCtx === fmtTok(m.answerPiece)) {
177
+ s.push("Here the document won: the model's top answer is the correct one. This is what synchronization looks like.");
178
+ }
179
+ return s.join(" ");
180
+ }
181
+
182
+ function renderStagebar(m) {
183
+ const r = $("sb-retrieve"), u = $("sb-update"), note = $("stagenote");
184
+ $("stagebar").classList.add("diagnosed");
185
+ document.querySelectorAll(".stagebar .s1, .stagebar .s2, .stagebar .s5")
186
+ .forEach((el) => el.classList.add("dimstage"));
187
+ r.textContent = "Retrieve βœ“";
188
+ if (m.dsync > 9.2) {
189
+ u.textContent = "Update βœ—";
190
+ note.textContent = "Retrieve succeeded: the document is in the context. Update failed: the conflict was resolved in favor of stale memory.";
191
+ } else if (m.dsync > 0.7) {
192
+ u.textContent = "Update β–³";
193
+ note.textContent = "Retrieve succeeded. Update is partial: the document shifted the model, but the stale memory still leads.";
194
+ } else {
195
+ u.textContent = "Update βœ“";
196
+ note.textContent = "Retrieve succeeded and Update resolved the conflict: the context controls the answer.";
197
+ }
198
+ }
199
+
200
+ function renderTemperature() {
201
+ if (!last) return;
202
+ const T = parseFloat($("temp").value);
203
+ $("t-value").textContent = T.toFixed(1);
204
+ const p = softmax(last.rowCtx, T);
205
+ const pAns = Math.max(p[last.target], 1e-12);
206
+ const top = topK(p, 1)[0];
207
+ const draws = Math.round(1 / pAns);
208
+ $("temp-readout").textContent =
209
+ `At temperature ${T.toFixed(1)}, sampling one answer with the document present: ` +
210
+ `the correct answer comes up about once in ${draws.toLocaleString()} draws; ` +
211
+ `the most likely token is β€œ${fmtTok(tokenizer.decode([top]))}” at ${fmtPct(p[top])}. ` +
212
+ (T > 1.0 ? "Higher temperature flattens the distribution but cannot rescue an answer this far down."
213
+ : "Lower temperature sharpens the model's existing preference.");
214
+ }
215
+ $("temp").addEventListener("input", renderTemperature);
216
+
217
+ // ---------- the one-button flow ----------
218
  $("run").addEventListener("click", async () => {
219
  const runBtn = $("run");
220
  runBtn.disabled = true;
 
221
  try {
222
+ runBtn.textContent = model ? "measuring..." : "loading model...";
223
+ await ensureModel();
224
+ runBtn.textContent = "measuring...";
225
+
226
  const query = $("query").value.trim();
227
  const context = $("context").value.trim();
228
+ const repeat = parseInt($("repeat").value, 10);
229
  let answer = $("answer").value;
230
  if (!answer.startsWith(" ")) answer = " " + answer.trim();
231
+ if (!query || !context || !answer.trim()) {
232
+ $("load-status").textContent = "enter a query, a corrective document, and the correct continuation";
233
+ return;
234
+ }
235
 
236
+ const doc = Array(repeat).fill(context).join("\n");
237
+ const rowPlain = await lastLogits(query);
238
+ const rowCtx = await lastLogits(doc + "\n" + query);
239
+ const pPlain = softmax(rowPlain);
240
+ const pCtx = softmax(rowCtx);
241
 
242
  const ids = tokenizer.encode(answer);
243
  const pieces = ids.map((i) => tokenizer.decode([i]));
 
251
  if (pCtx[i] > 0 && pPlain[i] > 0) ictx += pCtx[i] * Math.log(pCtx[i] / pPlain[i]);
252
  }
253
 
254
+ const topPlainId = topK(pPlain, 1)[0];
255
+ const topCtxId = topK(pCtx, 1)[0];
256
+
257
  let chipText, chipClass, detail;
258
  if (dsync > 9.2) {
259
  if (ictx < 0.05) {
 
274
  detail = "correct answer holds at least half the probability mass";
275
  }
276
 
277
+ // Gauge markers follow the drawn zone boundaries: D_sync thresholds
278
  // 0.7 / 4.6 / 9.2 sit at 5% / 33% / 66%, scale capped at 14 nats.
279
  const dPos = dsync <= 0.7 ? (dsync / 0.7) * 5
280
  : dsync <= 4.6 ? 5 + ((dsync - 0.7) / 3.9) * 28
281
  : dsync <= 9.2 ? 33 + ((dsync - 4.6) / 4.6) * 33
282
  : Math.min(100, 66 + ((dsync - 9.2) / 4.8) * 34);
 
283
  const iPos = ictx <= 0.05 ? (ictx / 0.05) * 10
284
  : ictx <= 0.5 ? 10 + ((ictx - 0.05) / 0.45) * 40
285
  : Math.min(100, 50 + ((ictx - 0.5) / 1.5) * 50);
286
 
287
+ const m = {
288
+ pt0, pt1, dsync, ictx,
289
+ answerPiece: pieces[0],
290
+ topPlain: fmtTok(tokenizer.decode([topPlainId])),
291
+ topCtx: fmtTok(tokenizer.decode([topCtxId])),
292
+ pTopCtx: pCtx[topCtxId],
293
+ pTopPlainSameTok: pPlain[topCtxId],
294
+ };
295
+
296
  const chip = $("verdict-chip");
297
  chip.textContent = chipText;
298
  chip.className = "chip " + chipClass;
299
+ $("r-verdict-detail").textContent = detail + (repeat > 1 ? ` (document repeated ${repeat}x)` : "");
300
+ $("narrative").textContent = narrative(m);
301
+ renderBars(pPlain, pCtx, target);
302
  $("g-marker").style.left = dPos.toFixed(1) + "%";
303
  $("g-ictx").style.left = iPos.toFixed(1) + "%";
 
 
 
 
304
  $("r-dsync").textContent = dsync.toFixed(2) + " nats";
305
  $("r-ictx").textContent = ictx.toFixed(3) + " nats";
306
+ $("r-p0").textContent = pt0.toExponential(2);
307
+ $("r-p1").textContent = pt1.toExponential(2);
308
+ $("r-tok").textContent = `${JSON.stringify(pieces)} (${ids.length} piece(s); first piece measured)`;
309
+ renderStagebar({ dsync });
310
+
311
+ last = { rowCtx, target };
312
+ renderTemperature();
313
+
314
  $("results").hidden = false;
315
+ } catch (e) {
316
+ $("load-status").textContent = "error: " + e.message;
317
  } finally {
318
  runBtn.disabled = false;
319
  runBtn.textContent = "Measure";