abhimittal/speculative-decoding-trace
0
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 & 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 & 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(·) (cheap, autoregressive, γ at a time)<br>1582. accept x with prob min(1, p(x)/q(x)) (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 < ${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 & 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 