Team Ai
Apppublic

abhimittal/speculative-decoding-trace

sourceHugging Facemitupdated 3mo agoView on Hugging Face
0likes
index.html467 linesDownload Raw Back to root
1<!DOCTYPE html>2<html lang="en">3<head>4<meta charset="utf-8">5<meta name="viewport" content="width=device-width, initial-scale=1">6<title>Speculative decoding: watch the rejection sampling</title>7<meta name="description" content="Interactive visualization of the acceptance-probability math inside speculative decoding — draft proposes, target verifies, rejects resample from the residual.">8<style>9:root{10  color-scheme:light;11  --surface:#fcfcfb; --page:#f9f9f7; --ink:#0b0b0b; --ink2:#52514e;12  --muted:#898781; --grid:#e1e0d9; --axis:#c3c2b7; --border:rgba(11,11,11,.10);13  --target:#2a78d6; --draft:#008300; --residual:#e87ba4;14  --accept:#0ca30c; --reject:#d03b3b; --resample:#eb6834; --bonus:#2a78d6;15}16@media (prefers-color-scheme:dark){17  :root:not([data-theme="light"]){18    color-scheme:dark;19    --surface:#1a1a19; --page:#0d0d0d; --ink:#ffffff; --ink2:#c3c2b7;20    --muted:#898781; --grid:#2c2c2a; --axis:#383835; --border:rgba(255,255,255,.10);21    --target:#3987e5; --draft:#008300; --residual:#d55181;22    --accept:#0ca30c; --reject:#d03b3b; --resample:#d95926; --bonus:#3987e5;23  }24}25:root[data-theme="dark"]{26  color-scheme:dark;27  --surface:#1a1a19; --page:#0d0d0d; --ink:#ffffff; --ink2:#c3c2b7;28  --muted:#898781; --grid:#2c2c2a; --axis:#383835; --border:rgba(255,255,255,.10);29  --target:#3987e5; --draft:#008300; --residual:#d55181;30  --accept:#0ca30c; --reject:#d03b3b; --resample:#d95926; --bonus:#3987e5;31}32*{box-sizing:border-box}33body{margin:0;background:var(--page);color:var(--ink);34  font-family:system-ui,-apple-system,"Segoe UI",sans-serif;line-height:1.55}35main{max-width:980px;margin:0 auto;padding:24px 16px 80px}36h1{font-size:1.7rem;margin:.2em 0}37h2{font-size:1.2rem;margin:2em 0 .4em}38p{color:var(--ink2);max-width:70ch}39a{color:var(--target)}40code,.mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.92em}41.card{background:var(--surface);border:1px solid var(--border);border-radius:12px;42  padding:16px;margin:12px 0}43.row{display:flex;gap:16px;flex-wrap:wrap}44.row>.card{flex:1;min-width:300px;margin:0}45button{background:var(--target);color:#fff;border:0;border-radius:8px;46  padding:8px 14px;font:inherit;font-weight:600;cursor:pointer}47button.ghost{background:transparent;color:var(--ink);border:1px solid var(--axis)}48button:disabled{opacity:.5;cursor:default}49select{font:inherit;background:var(--surface);color:var(--ink);50  border:1px solid var(--axis);border-radius:8px;padding:6px 8px}51.stat{display:inline-block;margin-right:28px}52.stat b{font-size:1.6rem;font-weight:700}53.stat span{display:block;font-size:.78rem;color:var(--muted)}54.legend{font-size:.82rem;color:var(--ink2);margin:6px 0}55.legend i{display:inline-block;width:10px;height:10px;border-radius:2px;56  margin:0 4px 0 14px;vertical-align:baseline}57.chip{display:inline-block;padding:1px 7px;margin:2px;border-radius:4px;color:#fff;58  font-family:ui-monospace,Menlo,monospace;font-size:.9em}59#tooltip{position:fixed;pointer-events:none;background:var(--surface);color:var(--ink);60  border:1px solid var(--border);border-radius:8px;padding:6px 10px;font-size:.8rem;61  box-shadow:0 4px 14px rgba(0,0,0,.15);display:none;z-index:10}62#log{font-size:.9rem;min-height:9em}63#log .step{margin:4px 0;color:var(--ink2)}64#log b{color:var(--ink)}65.hint{font-size:.8rem;color:var(--muted)}66svg text{fill:var(--muted);font-size:11px;font-family:inherit}67svg .val{fill:var(--ink2)}68.topbar{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px}69.katex-ish{background:var(--surface);border:1px solid var(--border);border-radius:8px;70  padding:10px 14px;font-family:ui-monospace,Menlo,monospace;font-size:.95em;71  overflow-x:auto;color:var(--ink)}72</style>73</head>74<body>75<div id="tooltip"></div>76<main>77<div class="topbar">78  <div>79    <h1>Speculative decoding: watch the rejection sampling</h1>80    <p style="margin:.2em 0">Everyone cites the speedup number. Here is the part nobody shows:81    the per-token accept/reject math that makes it <em>lossless</em>.</p>82  </div>83  <div>84    <button class="ghost" id="themeBtn" aria-label="toggle theme">◐ theme</button>85    <a href="https://github.com/aabhimittal/attention-trace-differ-for-speculative-decoding" style="margin-left:8px">GitHub</a>86    <a href="https://huggingface.co/spaces/abhimittal/speculative-decoding-trace" style="margin-left:8px">Live GPU-free Space</a>87  </div>88</div>89 90<h2>1 · The verification step, live</h2>91<p>A cheap <b style="color:var(--draft)">draft model q</b> proposes a token; the expensive92<b style="color:var(--target)">target model p</b> verifies it. Below is one vocabulary93position with toy 8-token distributions. <b>Drag the bars</b> to reshape either94distribution, or pick a preset.</p>95 96<div class="card">97  <div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center">98    <select id="preset">99      <option value="agree">preset: close agreement</option>100      <option value="overconfident">preset: draft overconfident</option>101      <option value="disagree">preset: target disagrees</option>102      <option value="uniform">preset: uniform draft</option>103    </select>104    <button id="stepBtn">Propose &amp; verify one token</button>105    <button class="ghost" id="runBtn">Run 2,000 trials</button>106    <button class="ghost" id="resetBtn">Reset tallies</button>107  </div>108  <div class="legend">109    <i style="background:var(--draft);margin-left:0"></i>draft q (proposes)110    <i style="background:var(--target)"></i>target p (verifies)111    — drag either series to edit; both stay normalized112  </div>113  <div id="distChart"></div>114  <div style="margin-top:8px">115    <span class="stat"><b id="alphaStat">–</b><span>α = Σ min(p,q) · expected acceptance</span></span>116    <span class="stat"><b id="tvStat">–</b><span>total variation ½Σ|p−q| = 1−α</span></span>117    <span class="stat"><b id="empStat">–</b><span id="empLabel">empirical acceptance (0 trials)</span></span>118  </div>119</div>120 121<div class="row">122  <div class="card">123    <h2 style="margin:0 0 6px">One verification, step by step</h2>124    <div id="log"><div class="step hint">Press “Propose &amp; verify one token”.</div></div>125    <div id="residualWrap" style="display:none">126      <div class="legend"><i style="background:var(--residual);margin-left:0"></i>127        residual norm(max(p−q, 0)) — where rejected tokens are re-drawn from</div>128      <div id="residualChart"></div>129    </div>130  </div>131  <div class="card">132    <h2 style="margin:0 0 6px">Why it’s lossless</h2>133    <p style="font-size:.92rem">Tokens that come out of accept-or-resample are distributed134    exactly like the target’s <span class="mono">p</span> — the whole point of the scheme.135    Run trials and watch the output histogram converge onto the136    <b style="color:var(--target)">p markers</b>.</p>137    <div class="legend">138      <i style="background:var(--accept);margin-left:0"></i>output via acceptance139      <i style="background:var(--resample)"></i>output via resample140      <i style="background:var(--target)"></i>target p (goal)141    </div>142    <div id="outChart"></div>143  </div>144</div>145 146<h2>2 · From acceptance rate to speedup</h2>147<p>With acceptance rate α and γ drafted tokens per round, one target forward pass yields148<span class="mono">E[tokens] = (1 − α<sup>γ+1</sup>) / (1 − α)</span>.149The curve below tracks the α of <em>your</em> distributions above — reshape them and watch150the ceiling move. High α → longer accepted runs → more tokens per expensive call.</p>151<div class="card">152  <div id="speedChart"></div>153</div>154 155<h2>3 · The math, in four lines</h2>156<div class="katex-ish">1571. draft samples x ~ q(·) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; (cheap, autoregressive, γ at a time)<br>1582. accept x with prob min(1, p(x)/q(x))&nbsp;&nbsp; (target scores all γ in ONE pass)<br>1593. if rejected: resample x′ ~ norm(max(p − q, 0))<br>1604. P(output = x) = q(x)·min(1, p(x)/q(x)) + P(reject)·residual(x) = p(x) ✓161</div>162<p>Line 4 is the theorem (Leviathan et al. 2023, <a href="https://arxiv.org/abs/2211.17192">arXiv:2211.17192</a>):163acceptance keeps exactly the probability mass where q overlaps p, and the residual puts back164precisely the mass q under-covered. Nothing is approximated — the output distribution165<em>is</em> the target’s.</p>166 167<h2>4 · Real models, real traces</h2>168<p>The toy above is exact math on toy distributions. The next step is the same169algorithm on <em>actual model logits</em>: <span class="mono">distilgpt2</span> drafting170for <span class="mono">gpt2</span>, quantized ONNX, executed <b>entirely in your171browser</b> with transformers.js — no server, no GPU quota, no cold start.172The repo also ships a Gradio app (KV-cached, with attention-trace diffing and an173optional 70B cloud cross-check) to run locally.</p>174<div class="card" id="spaceCard">175  <a href="live.html"><button>Run real models in your browser →</button></a>176  <a href="https://huggingface.co/spaces/abhimittal/speculative-decoding-trace" style="margin-left:12px">or open the Hugging Face Space ↗</a>177</div>178 179<p class="hint" style="margin-top:40px">MIT licensed ·180<a href="https://github.com/aabhimittal/attention-trace-differ-for-speculative-decoding">source</a> ·181built as a visual artifact for the speculative-decoding verification step.</p>182</main>183 184<script>185"use strict";186const VOCAB = ["the","a","cat","dog","sat","ran","fast","."];187const N = VOCAB.length;188const PRESETS = {189  agree:        {q:[30,22,14,12,9,6,4,3], p:[27,24,15,11,10,6,4,3]},190  overconfident:{q:[62,14,8,6,4,3,2,1],  p:[28,20,15,12,10,7,5,3]},191  disagree:     {q:[35,25,15,10,6,4,3,2], p:[4,6,10,14,18,20,16,12]},192  uniform:      {q:[1,1,1,1,1,1,1,1],     p:[30,20,15,12,9,7,4,3]},193};194let qw = PRESETS.agree.q.slice(), pw = PRESETS.agree.p.slice();195let tally = {n:0, acc:0, outAcc:Array(N).fill(0), outRes:Array(N).fill(0)};196const $ = id => document.getElementById(id);197const norm = w => { const s = w.reduce((a,b)=>a+b,0); return w.map(v=>v/s); };198const fmt = (x,d=3) => x.toFixed(d);199 200// ---------- theme ----------201$("themeBtn").onclick = () => {202  const r = document.documentElement;203  const dark = getComputedStyle(document.body).backgroundColor !== "rgb(249, 249, 247)";204  r.dataset.theme = dark ? "light" : "dark";205  drawAll();206};207 208// ---------- tooltip ----------209const tip = $("tooltip");210function showTip(e, html){ tip.innerHTML = html; tip.style.display = "block"; moveTip(e); }211function moveTip(e){ tip.style.left = (e.clientX+14)+"px"; tip.style.top = (e.clientY+10)+"px"; }212function hideTip(){ tip.style.display = "none"; }213 214// ---------- svg helpers ----------215const SVGNS = "http://www.w3.org/2000/svg";216function el(name, attrs, parent){217  const n = document.createElementNS(SVGNS, name);218  for (const k in attrs) n.setAttribute(k, attrs[k]);219  if (parent) parent.appendChild(n);220  return n;221}222function cssVar(v){ return getComputedStyle(document.documentElement).getPropertyValue(v).trim(); }223 224// ---------- main editable grouped bar chart ----------225function drawDist(){226  const q = norm(qw), p = norm(pw);227  const W = Math.min(920, $("distChart").clientWidth || 920), H = 240;228  const padL = 34, padB = 26, padT = 10;229  const plotW = W - padL - 8, plotH = H - padT - padB;230  const ymax = Math.max(...q, ...p, .35);231  const svg = el("svg", {width:"100%", viewBox:`0 0 ${W} ${H}`, role:"img",232    "aria-label":"draft and target distributions, drag bars to edit"});233  // gridlines234  for (let g = 0; g <= 4; g++){235    const y = padT + plotH - plotH * g / 4;236    el("line", {x1:padL, x2:W-8, y1:y, y2:y, stroke:cssVar("--grid"), "stroke-width":1}, svg);237    el("text", {x:padL-6, y:y+4, "text-anchor":"end"}, svg).textContent = fmt(ymax*g/4,2);238  }239  const group = plotW / N, bw = Math.min(34, group/2 - 6);240  const series = [{w:qw, v:q, color:cssVar("--draft"), name:"draft q", off:-bw-1},241                  {w:pw, v:p, color:cssVar("--target"), name:"target p", off:1}];242  series.forEach((s, si) => {243    s.v.forEach((v, i) => {244      const h = Math.max(2, plotH * v / ymax);245      const x = padL + group*i + group/2 + s.off, y = padT + plotH - h;246      const r = el("rect", {x, y, width:bw, height:h, fill:s.color, rx:4,247        style:"cursor:ns-resize"}, svg);248      const hit = el("rect", {x:x-2, y:padT, width:bw+4, height:plotH,249        fill:"transparent", style:"cursor:ns-resize"}, svg);250      const enter = e => showTip(e, `<b>${VOCAB[i]}</b> · ${s.name} = ${fmt(s.v[i])}`);251      [r,hit].forEach(t => { t.onpointermove = e => { moveTip(e); };252        t.onpointerenter = enter; t.onpointerleave = hideTip; });253      hit.onpointerdown = r.onpointerdown = e => {254        e.preventDefault(); (e.target).setPointerCapture(e.pointerId);255        const drag = ev => {256          const rect = svg.getBoundingClientRect();257          const yy = (ev.clientY - rect.top) * (H / rect.height);258          let frac = (padT + plotH - yy) / plotH;259          frac = Math.max(.005, Math.min(1, frac));260          const others = s.w.reduce((a,b,j)=> j===i ? a : a+b, 0);261          s.w[i] = frac >= .999 ? others*200 : others * frac / (1 - frac);262          drawAll();263        };264        const up = () => { window.removeEventListener("pointermove", drag);265          window.removeEventListener("pointerup", up); };266        window.addEventListener("pointermove", drag);267        window.addEventListener("pointerup", up);268        drag(e);269      };270    });271  });272  VOCAB.forEach((t,i) => el("text", {x:padL+group*i+group/2, y:H-8,273    "text-anchor":"middle"}, svg).textContent = t);274  $("distChart").replaceChildren(svg);275  const alpha = q.reduce((a,_,i)=>a+Math.min(q[i],p[i]),0);276  $("alphaStat").textContent = fmt(alpha);277  $("tvStat").textContent = fmt(1-alpha);278  return {q,p,alpha};279}280 281// ---------- static bar chart (residual / output) ----------282function barChart(mount, values, colors, markers, ariaLabel){283  const W = Math.min(440, mount.clientWidth || 440), H = 170;284  const padL = 30, padB = 24, padT = 8;285  const plotW = W - padL - 8, plotH = H - padT - padB;286  const ymax = Math.max(...values.map((v,i)=>v), ...(markers||[0]), .3);287  const svg = el("svg", {width:"100%", viewBox:`0 0 ${W} ${H}`, role:"img", "aria-label":ariaLabel});288  for (let g = 0; g <= 2; g++){289    const y = padT + plotH - plotH*g/2;290    el("line",{x1:padL,x2:W-8,y1:y,y2:y,stroke:cssVar("--grid")},svg);291    el("text",{x:padL-5,y:y+4,"text-anchor":"end"},svg).textContent = fmt(ymax*g/2,2);292  }293  const group = plotW / N, bw = Math.min(26, group-10);294  values.forEach((v,i)=>{295    const h = Math.max(v>0?2:0, plotH*v/ymax);296    const x = padL + group*i + (group-bw)/2, y = padT + plotH - h;297    const r = el("rect",{x,y,width:bw,height:h,fill:colors[i],rx:4},svg);298    r.onpointerenter = e => showTip(e, `<b>${VOCAB[i]}</b> · ${fmt(v)}`);299    r.onpointermove = moveTip; r.onpointerleave = hideTip;300    if (markers){301      const my = padT + plotH - plotH*markers[i]/ymax;302      el("line",{x1:x-3,x2:x+bw+3,y1:my,y2:my,stroke:cssVar("--target"),303        "stroke-width":2.5},svg);304    }305    el("text",{x:padL+group*i+group/2,y:H-6,"text-anchor":"middle"},svg).textContent = VOCAB[i];306  });307  mount.replaceChildren(svg);308}309 310// ---------- speedup curve ----------311function drawSpeed(alpha){312  const W = Math.min(920, $("speedChart").clientWidth || 920), H = 210;313  const padL = 38, padB = 28, padT = 12;314  const plotW = W - padL - 16, plotH = H - padT - padB;315  const gmax = 8;316  const E = g => alpha >= .9999 ? g+1 : (1 - Math.pow(alpha, g+1)) / (1 - alpha);317  const ymax = Math.max(E(gmax), 2);318  const svg = el("svg",{width:"100%",viewBox:`0 0 ${W} ${H}`,role:"img",319    "aria-label":"expected tokens per target call vs gamma"});320  for (let g = 0; g <= 4; g++){321    const y = padT + plotH - plotH*g/4;322    el("line",{x1:padL,x2:padL+plotW,y1:y,y2:y,stroke:cssVar("--grid")},svg);323    el("text",{x:padL-6,y:y+4,"text-anchor":"end"},svg).textContent = fmt(ymax*g/4,1);324  }325  const X = g => padL + plotW*(g-1)/(gmax-1), Y = v => padT + plotH - plotH*v/ymax;326  let d = "";327  for (let g = 1; g <= gmax; g++) d += (g===1?"M":"L") + X(g) + " " + Y(E(g)) + " ";328  el("path",{d,fill:"none",stroke:cssVar("--target"),"stroke-width":2},svg);329  el("line",{x1:padL,x2:padL+plotW,y1:Y(1),y2:Y(1),stroke:cssVar("--axis"),330    "stroke-dasharray":"4 4"},svg);331  el("text",{x:padL+plotW,y:Y(1)-6,"text-anchor":"end"},svg).textContent = "plain autoregressive (1×)";332  for (let g = 1; g <= gmax; g++){333    const c = el("circle",{cx:X(g),cy:Y(E(g)),r:5,fill:cssVar("--target"),334      stroke:cssVar("--surface"),"stroke-width":2},svg);335    c.onpointerenter = e => showTip(e,336      `γ=${g} → <b>${fmt(E(g),2)}</b> tokens per target call<br>at α=${fmt(alpha,2)}`);337    c.onpointermove = moveTip; c.onpointerleave = hideTip;338    el("text",{x:X(g),y:H-8,"text-anchor":"middle"},svg).textContent = "γ="+g;339  }340  const lbl = el("text",{x:padL+10,y:padT+14,class:"val"},svg);341  lbl.textContent = `α=${fmt(alpha,2)} → ${fmt(E(gmax),2)} tokens/call at γ=8`;342  $("speedChart").replaceChildren(svg);343}344 345// ---------- output histogram ----------346function drawOut(p){347  const n = Math.max(tally.n, 1);348  const stackTotals = tally.outAcc.map((a,i)=>(a+tally.outRes[i])/n);349  // draw as stacked: accepted portion + resampled portion350  const mount = $("outChart");351  const W = Math.min(440, mount.clientWidth || 440), H = 190;352  const padL = 30, padB = 24, padT = 8;353  const plotW = W - padL - 8, plotH = H - padT - padB;354  const ymax = Math.max(...stackTotals, ...p, .3);355  const svg = el("svg",{width:"100%",viewBox:`0 0 ${W} ${H}`,role:"img",356    "aria-label":"empirical output distribution vs target p"});357  for (let g = 0; g <= 2; g++){358    const y = padT + plotH - plotH*g/2;359    el("line",{x1:padL,x2:W-8,y1:y,y2:y,stroke:cssVar("--grid")},svg);360    el("text",{x:padL-5,y:y+4,"text-anchor":"end"},svg).textContent = fmt(ymax*g/2,2);361  }362  const group = plotW / N, bw = Math.min(26, group-10);363  for (let i = 0; i < N; i++){364    const x = padL + group*i + (group-bw)/2;365    const hA = plotH * (tally.outAcc[i]/n) / ymax;366    const hR = plotH * (tally.outRes[i]/n) / ymax;367    const yA = padT + plotH - hA;368    if (hA > 0) el("rect",{x,y:yA,width:bw,height:Math.max(hA,1),fill:cssVar("--accept"),rx:2},svg);369    if (hR > 0) el("rect",{x,y:yA-hR-2,width:bw,height:Math.max(hR,1),370      fill:cssVar("--resample"),rx:2},svg);371    const my = padT + plotH - plotH*p[i]/ymax;372    el("line",{x1:x-3,x2:x+bw+3,y1:my,y2:my,stroke:cssVar("--target"),"stroke-width":2.5},svg);373    const hit = el("rect",{x:x-2,y:padT,width:bw+4,height:plotH,fill:"transparent"},svg);374    hit.onpointerenter = e => showTip(e, `<b>${VOCAB[i]}</b><br>` +375      `empirical: ${fmt(stackTotals[i])} (acc ${fmt(tally.outAcc[i]/n)}, ` +376      `resampled ${fmt(tally.outRes[i]/n)})<br>target p: ${fmt(p[i])}`);377    hit.onpointermove = moveTip; hit.onpointerleave = hideTip;378    el("text",{x:padL+group*i+group/2,y:H-6,"text-anchor":"middle"},svg).textContent = VOCAB[i];379  }380  mount.replaceChildren(svg);381}382 383// ---------- the actual rejection sampling ----------384function sample(dist){385  let u = Math.random(), c = 0;386  for (let i = 0; i < dist.length; i++){ c += dist[i]; if (u < c) return i; }387  return dist.length - 1;388}389function residualOf(p, q){390  const r = p.map((v,i)=>Math.max(v - q[i], 0));391  const s = r.reduce((a,b)=>a+b,0);392  return s > 0 ? r.map(v=>v/s) : p.slice();393}394function verifyOnce(q, p){395  const x = sample(q);396  const ratio = p[x] / q[x];397  const aProb = Math.min(1, ratio);398  const u = Math.random();399  const accepted = u < aProb;400  let out = x, res = null;401  if (!accepted){ res = residualOf(p, q); out = sample(res); }402  return {x, ratio, aProb, u, accepted, out, res};403}404 405function stepOnce(){406  const {q, p} = drawDist();407  const t = verifyOnce(q, p);408  tally.n++; if (t.accepted){ tally.acc++; tally.outAcc[t.out]++; } else tally.outRes[t.out]++;409  const log = $("log"); log.replaceChildren();410  const add = h => { const d = document.createElement("div"); d.className = "step";411    d.innerHTML = h; log.appendChild(d); };412  add(`1 · draft samples x ~ q → <span class="chip" style="background:var(--draft)">${VOCAB[t.x]}</span> (q=${fmt(q[t.x])})`);413  add(`2 · target scores it: p(<b>${VOCAB[t.x]}</b>) = <b>${fmt(p[t.x])}</b> → ratio p/q = <b>${fmt(t.ratio,2)}</b>`);414  add(`3 · accept prob = min(1, ${fmt(t.ratio,2)}) = <b>${fmt(t.aProb)}</b>; uniform draw u = <b>${fmt(t.u)}</b>`);415  if (t.accepted){416    add(`4 · u &lt; ${fmt(t.aProb)} → <b style="color:var(--accept)">✓ ACCEPTED</b> — output <span class="chip" style="background:var(--accept)">${VOCAB[t.out]}</span>`);417    $("residualWrap").style.display = "none";418  } else {419    add(`4 · u ≥ ${fmt(t.aProb)} → <b style="color:var(--reject)">✗ REJECTED</b> — the draft over-claimed this token`);420    add(`5 · resample from norm(max(p−q,0)) → <span class="chip" style="background:var(--resample)">${VOCAB[t.out]}</span>`);421    $("residualWrap").style.display = "block";422    barChart($("residualChart"), t.res, VOCAB.map(()=>cssVar("--residual")), null,423      "residual distribution");424  }425  refreshStats(p);426}427 428function runMany(){429  const {q, p} = drawDist();430  for (let k = 0; k < 2000; k++){431    const t = verifyOnce(q, p);432    tally.n++; if (t.accepted){ tally.acc++; tally.outAcc[t.out]++; } else tally.outRes[t.out]++;433  }434  refreshStats(p);435}436 437function refreshStats(p){438  $("empStat").textContent = tally.n ? fmt(tally.acc/tally.n) : "–";439  $("empLabel").textContent = `empirical acceptance (${tally.n.toLocaleString()} trials)`;440  drawOut(p);441}442 443function drawAll(){444  const {q, p, alpha} = drawDist();445  drawSpeed(alpha);446  drawOut(p);447}448 449$("preset").onchange = e => {450  const pr = PRESETS[e.target.value];451  qw = pr.q.slice(); pw = pr.p.slice();452  $("resetBtn").click();453};454$("stepBtn").onclick = stepOnce;455$("runBtn").onclick = runMany;456$("resetBtn").onclick = () => {457  tally = {n:0, acc:0, outAcc:Array(N).fill(0), outRes:Array(N).fill(0)};458  $("log").innerHTML = '<div class="step hint">Press “Propose &amp; verify one token”.</div>';459  $("residualWrap").style.display = "none";460  drawAll(); refreshStats(norm(pw));461};462window.addEventListener("resize", drawAll);463drawAll();464</script>465</body>466</html>467