abhimittal's picture
Upload folder using huggingface_hub
0d67885 verified
Raw
History Blame Contribute Delete
22.2 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Speculative decoding: watch the rejection sampling</title>
<meta name="description" content="Interactive visualization of the acceptance-probability math inside speculative decoding — draft proposes, target verifies, rejects resample from the residual.">
<style>
:root{
color-scheme:light;
--surface:#fcfcfb; --page:#f9f9f7; --ink:#0b0b0b; --ink2:#52514e;
--muted:#898781; --grid:#e1e0d9; --axis:#c3c2b7; --border:rgba(11,11,11,.10);
--target:#2a78d6; --draft:#008300; --residual:#e87ba4;
--accept:#0ca30c; --reject:#d03b3b; --resample:#eb6834; --bonus:#2a78d6;
}
@media (prefers-color-scheme:dark){
:root:not([data-theme="light"]){
color-scheme:dark;
--surface:#1a1a19; --page:#0d0d0d; --ink:#ffffff; --ink2:#c3c2b7;
--muted:#898781; --grid:#2c2c2a; --axis:#383835; --border:rgba(255,255,255,.10);
--target:#3987e5; --draft:#008300; --residual:#d55181;
--accept:#0ca30c; --reject:#d03b3b; --resample:#d95926; --bonus:#3987e5;
}
}
:root[data-theme="dark"]{
color-scheme:dark;
--surface:#1a1a19; --page:#0d0d0d; --ink:#ffffff; --ink2:#c3c2b7;
--muted:#898781; --grid:#2c2c2a; --axis:#383835; --border:rgba(255,255,255,.10);
--target:#3987e5; --draft:#008300; --residual:#d55181;
--accept:#0ca30c; --reject:#d03b3b; --resample:#d95926; --bonus:#3987e5;
}
*{box-sizing:border-box}
body{margin:0;background:var(--page);color:var(--ink);
font-family:system-ui,-apple-system,"Segoe UI",sans-serif;line-height:1.55}
main{max-width:980px;margin:0 auto;padding:24px 16px 80px}
h1{font-size:1.7rem;margin:.2em 0}
h2{font-size:1.2rem;margin:2em 0 .4em}
p{color:var(--ink2);max-width:70ch}
a{color:var(--target)}
code,.mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.92em}
.card{background:var(--surface);border:1px solid var(--border);border-radius:12px;
padding:16px;margin:12px 0}
.row{display:flex;gap:16px;flex-wrap:wrap}
.row>.card{flex:1;min-width:300px;margin:0}
button{background:var(--target);color:#fff;border:0;border-radius:8px;
padding:8px 14px;font:inherit;font-weight:600;cursor:pointer}
button.ghost{background:transparent;color:var(--ink);border:1px solid var(--axis)}
button:disabled{opacity:.5;cursor:default}
select{font:inherit;background:var(--surface);color:var(--ink);
border:1px solid var(--axis);border-radius:8px;padding:6px 8px}
.stat{display:inline-block;margin-right:28px}
.stat b{font-size:1.6rem;font-weight:700}
.stat span{display:block;font-size:.78rem;color:var(--muted)}
.legend{font-size:.82rem;color:var(--ink2);margin:6px 0}
.legend i{display:inline-block;width:10px;height:10px;border-radius:2px;
margin:0 4px 0 14px;vertical-align:baseline}
.chip{display:inline-block;padding:1px 7px;margin:2px;border-radius:4px;color:#fff;
font-family:ui-monospace,Menlo,monospace;font-size:.9em}
#tooltip{position:fixed;pointer-events:none;background:var(--surface);color:var(--ink);
border:1px solid var(--border);border-radius:8px;padding:6px 10px;font-size:.8rem;
box-shadow:0 4px 14px rgba(0,0,0,.15);display:none;z-index:10}
#log{font-size:.9rem;min-height:9em}
#log .step{margin:4px 0;color:var(--ink2)}
#log b{color:var(--ink)}
.hint{font-size:.8rem;color:var(--muted)}
svg text{fill:var(--muted);font-size:11px;font-family:inherit}
svg .val{fill:var(--ink2)}
.topbar{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px}
.katex-ish{background:var(--surface);border:1px solid var(--border);border-radius:8px;
padding:10px 14px;font-family:ui-monospace,Menlo,monospace;font-size:.95em;
overflow-x:auto;color:var(--ink)}
</style>
</head>
<body>
<div id="tooltip"></div>
<main>
<div class="topbar">
<div>
<h1>Speculative decoding: watch the rejection sampling</h1>
<p style="margin:.2em 0">Everyone cites the speedup number. Here is the part nobody shows:
the per-token accept/reject math that makes it <em>lossless</em>.</p>
</div>
<div>
<button class="ghost" id="themeBtn" aria-label="toggle theme">◐ theme</button>
<a href="https://github.com/aabhimittal/attention-trace-differ-for-speculative-decoding" style="margin-left:8px">GitHub</a>
<a href="https://huggingface.co/spaces/abhimittal/speculative-decoding-trace" style="margin-left:8px">Live GPU-free Space</a>
</div>
</div>
<h2>1 · The verification step, live</h2>
<p>A cheap <b style="color:var(--draft)">draft model q</b> proposes a token; the expensive
<b style="color:var(--target)">target model p</b> verifies it. Below is one vocabulary
position with toy 8-token distributions. <b>Drag the bars</b> to reshape either
distribution, or pick a preset.</p>
<div class="card">
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center">
<select id="preset">
<option value="agree">preset: close agreement</option>
<option value="overconfident">preset: draft overconfident</option>
<option value="disagree">preset: target disagrees</option>
<option value="uniform">preset: uniform draft</option>
</select>
<button id="stepBtn">Propose &amp; verify one token</button>
<button class="ghost" id="runBtn">Run 2,000 trials</button>
<button class="ghost" id="resetBtn">Reset tallies</button>
</div>
<div class="legend">
<i style="background:var(--draft);margin-left:0"></i>draft q (proposes)
<i style="background:var(--target)"></i>target p (verifies)
— drag either series to edit; both stay normalized
</div>
<div id="distChart"></div>
<div style="margin-top:8px">
<span class="stat"><b id="alphaStat"></b><span>α = Σ min(p,q) · expected acceptance</span></span>
<span class="stat"><b id="tvStat"></b><span>total variation ½Σ|p−q| = 1−α</span></span>
<span class="stat"><b id="empStat"></b><span id="empLabel">empirical acceptance (0 trials)</span></span>
</div>
</div>
<div class="row">
<div class="card">
<h2 style="margin:0 0 6px">One verification, step by step</h2>
<div id="log"><div class="step hint">Press “Propose &amp; verify one token”.</div></div>
<div id="residualWrap" style="display:none">
<div class="legend"><i style="background:var(--residual);margin-left:0"></i>
residual norm(max(p−q, 0)) — where rejected tokens are re-drawn from</div>
<div id="residualChart"></div>
</div>
</div>
<div class="card">
<h2 style="margin:0 0 6px">Why it’s lossless</h2>
<p style="font-size:.92rem">Tokens that come out of accept-or-resample are distributed
exactly like the target’s <span class="mono">p</span> — the whole point of the scheme.
Run trials and watch the output histogram converge onto the
<b style="color:var(--target)">p markers</b>.</p>
<div class="legend">
<i style="background:var(--accept);margin-left:0"></i>output via acceptance
<i style="background:var(--resample)"></i>output via resample
<i style="background:var(--target)"></i>target p (goal)
</div>
<div id="outChart"></div>
</div>
</div>
<h2>2 · From acceptance rate to speedup</h2>
<p>With acceptance rate α and γ drafted tokens per round, one target forward pass yields
<span class="mono">E[tokens] = (1 − α<sup>γ+1</sup>) / (1 − α)</span>.
The curve below tracks the α of <em>your</em> distributions above — reshape them and watch
the ceiling move. High α → longer accepted runs → more tokens per expensive call.</p>
<div class="card">
<div id="speedChart"></div>
</div>
<h2>3 · The math, in four lines</h2>
<div class="katex-ish">
1. draft samples x ~ q(·) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; (cheap, autoregressive, γ at a time)<br>
2. accept x with prob min(1, p(x)/q(x))&nbsp;&nbsp; (target scores all γ in ONE pass)<br>
3. if rejected: resample x′ ~ norm(max(p − q, 0))<br>
4. P(output = x) = q(x)·min(1, p(x)/q(x)) + P(reject)·residual(x) = p(x) ✓
</div>
<p>Line 4 is the theorem (Leviathan et al. 2023, <a href="https://arxiv.org/abs/2211.17192">arXiv:2211.17192</a>):
acceptance keeps exactly the probability mass where q overlaps p, and the residual puts back
precisely the mass q under-covered. Nothing is approximated — the output distribution
<em>is</em> the target’s.</p>
<h2>4 · Real models, real traces</h2>
<p>The toy above is exact math on toy distributions. The next step is the same
algorithm on <em>actual model logits</em>: <span class="mono">distilgpt2</span> drafting
for <span class="mono">gpt2</span>, quantized ONNX, executed <b>entirely in your
browser</b> with transformers.js — no server, no GPU quota, no cold start.
The repo also ships a Gradio app (KV-cached, with attention-trace diffing and an
optional 70B cloud cross-check) to run locally.</p>
<div class="card" id="spaceCard">
<a href="live.html"><button>Run real models in your browser →</button></a>
<a href="https://huggingface.co/spaces/abhimittal/speculative-decoding-trace" style="margin-left:12px">or open the Hugging Face Space ↗</a>
</div>
<p class="hint" style="margin-top:40px">MIT licensed ·
<a href="https://github.com/aabhimittal/attention-trace-differ-for-speculative-decoding">source</a> ·
built as a visual artifact for the speculative-decoding verification step.</p>
</main>
<script>
"use strict";
const VOCAB = ["the","a","cat","dog","sat","ran","fast","."];
const N = VOCAB.length;
const PRESETS = {
agree: {q:[30,22,14,12,9,6,4,3], p:[27,24,15,11,10,6,4,3]},
overconfident:{q:[62,14,8,6,4,3,2,1], p:[28,20,15,12,10,7,5,3]},
disagree: {q:[35,25,15,10,6,4,3,2], p:[4,6,10,14,18,20,16,12]},
uniform: {q:[1,1,1,1,1,1,1,1], p:[30,20,15,12,9,7,4,3]},
};
let qw = PRESETS.agree.q.slice(), pw = PRESETS.agree.p.slice();
let tally = {n:0, acc:0, outAcc:Array(N).fill(0), outRes:Array(N).fill(0)};
const $ = id => document.getElementById(id);
const norm = w => { const s = w.reduce((a,b)=>a+b,0); return w.map(v=>v/s); };
const fmt = (x,d=3) => x.toFixed(d);
// ---------- theme ----------
$("themeBtn").onclick = () => {
const r = document.documentElement;
const dark = getComputedStyle(document.body).backgroundColor !== "rgb(249, 249, 247)";
r.dataset.theme = dark ? "light" : "dark";
drawAll();
};
// ---------- tooltip ----------
const tip = $("tooltip");
function showTip(e, html){ tip.innerHTML = html; tip.style.display = "block"; moveTip(e); }
function moveTip(e){ tip.style.left = (e.clientX+14)+"px"; tip.style.top = (e.clientY+10)+"px"; }
function hideTip(){ tip.style.display = "none"; }
// ---------- svg helpers ----------
const SVGNS = "http://www.w3.org/2000/svg";
function el(name, attrs, parent){
const n = document.createElementNS(SVGNS, name);
for (const k in attrs) n.setAttribute(k, attrs[k]);
if (parent) parent.appendChild(n);
return n;
}
function cssVar(v){ return getComputedStyle(document.documentElement).getPropertyValue(v).trim(); }
// ---------- main editable grouped bar chart ----------
function drawDist(){
const q = norm(qw), p = norm(pw);
const W = Math.min(920, $("distChart").clientWidth || 920), H = 240;
const padL = 34, padB = 26, padT = 10;
const plotW = W - padL - 8, plotH = H - padT - padB;
const ymax = Math.max(...q, ...p, .35);
const svg = el("svg", {width:"100%", viewBox:`0 0 ${W} ${H}`, role:"img",
"aria-label":"draft and target distributions, drag bars to edit"});
// gridlines
for (let g = 0; g <= 4; g++){
const y = padT + plotH - plotH * g / 4;
el("line", {x1:padL, x2:W-8, y1:y, y2:y, stroke:cssVar("--grid"), "stroke-width":1}, svg);
el("text", {x:padL-6, y:y+4, "text-anchor":"end"}, svg).textContent = fmt(ymax*g/4,2);
}
const group = plotW / N, bw = Math.min(34, group/2 - 6);
const series = [{w:qw, v:q, color:cssVar("--draft"), name:"draft q", off:-bw-1},
{w:pw, v:p, color:cssVar("--target"), name:"target p", off:1}];
series.forEach((s, si) => {
s.v.forEach((v, i) => {
const h = Math.max(2, plotH * v / ymax);
const x = padL + group*i + group/2 + s.off, y = padT + plotH - h;
const r = el("rect", {x, y, width:bw, height:h, fill:s.color, rx:4,
style:"cursor:ns-resize"}, svg);
const hit = el("rect", {x:x-2, y:padT, width:bw+4, height:plotH,
fill:"transparent", style:"cursor:ns-resize"}, svg);
const enter = e => showTip(e, `<b>${VOCAB[i]}</b> · ${s.name} = ${fmt(s.v[i])}`);
[r,hit].forEach(t => { t.onpointermove = e => { moveTip(e); };
t.onpointerenter = enter; t.onpointerleave = hideTip; });
hit.onpointerdown = r.onpointerdown = e => {
e.preventDefault(); (e.target).setPointerCapture(e.pointerId);
const drag = ev => {
const rect = svg.getBoundingClientRect();
const yy = (ev.clientY - rect.top) * (H / rect.height);
let frac = (padT + plotH - yy) / plotH;
frac = Math.max(.005, Math.min(1, frac));
const others = s.w.reduce((a,b,j)=> j===i ? a : a+b, 0);
s.w[i] = frac >= .999 ? others*200 : others * frac / (1 - frac);
drawAll();
};
const up = () => { window.removeEventListener("pointermove", drag);
window.removeEventListener("pointerup", up); };
window.addEventListener("pointermove", drag);
window.addEventListener("pointerup", up);
drag(e);
};
});
});
VOCAB.forEach((t,i) => el("text", {x:padL+group*i+group/2, y:H-8,
"text-anchor":"middle"}, svg).textContent = t);
$("distChart").replaceChildren(svg);
const alpha = q.reduce((a,_,i)=>a+Math.min(q[i],p[i]),0);
$("alphaStat").textContent = fmt(alpha);
$("tvStat").textContent = fmt(1-alpha);
return {q,p,alpha};
}
// ---------- static bar chart (residual / output) ----------
function barChart(mount, values, colors, markers, ariaLabel){
const W = Math.min(440, mount.clientWidth || 440), H = 170;
const padL = 30, padB = 24, padT = 8;
const plotW = W - padL - 8, plotH = H - padT - padB;
const ymax = Math.max(...values.map((v,i)=>v), ...(markers||[0]), .3);
const svg = el("svg", {width:"100%", viewBox:`0 0 ${W} ${H}`, role:"img", "aria-label":ariaLabel});
for (let g = 0; g <= 2; g++){
const y = padT + plotH - plotH*g/2;
el("line",{x1:padL,x2:W-8,y1:y,y2:y,stroke:cssVar("--grid")},svg);
el("text",{x:padL-5,y:y+4,"text-anchor":"end"},svg).textContent = fmt(ymax*g/2,2);
}
const group = plotW / N, bw = Math.min(26, group-10);
values.forEach((v,i)=>{
const h = Math.max(v>0?2:0, plotH*v/ymax);
const x = padL + group*i + (group-bw)/2, y = padT + plotH - h;
const r = el("rect",{x,y,width:bw,height:h,fill:colors[i],rx:4},svg);
r.onpointerenter = e => showTip(e, `<b>${VOCAB[i]}</b> · ${fmt(v)}`);
r.onpointermove = moveTip; r.onpointerleave = hideTip;
if (markers){
const my = padT + plotH - plotH*markers[i]/ymax;
el("line",{x1:x-3,x2:x+bw+3,y1:my,y2:my,stroke:cssVar("--target"),
"stroke-width":2.5},svg);
}
el("text",{x:padL+group*i+group/2,y:H-6,"text-anchor":"middle"},svg).textContent = VOCAB[i];
});
mount.replaceChildren(svg);
}
// ---------- speedup curve ----------
function drawSpeed(alpha){
const W = Math.min(920, $("speedChart").clientWidth || 920), H = 210;
const padL = 38, padB = 28, padT = 12;
const plotW = W - padL - 16, plotH = H - padT - padB;
const gmax = 8;
const E = g => alpha >= .9999 ? g+1 : (1 - Math.pow(alpha, g+1)) / (1 - alpha);
const ymax = Math.max(E(gmax), 2);
const svg = el("svg",{width:"100%",viewBox:`0 0 ${W} ${H}`,role:"img",
"aria-label":"expected tokens per target call vs gamma"});
for (let g = 0; g <= 4; g++){
const y = padT + plotH - plotH*g/4;
el("line",{x1:padL,x2:padL+plotW,y1:y,y2:y,stroke:cssVar("--grid")},svg);
el("text",{x:padL-6,y:y+4,"text-anchor":"end"},svg).textContent = fmt(ymax*g/4,1);
}
const X = g => padL + plotW*(g-1)/(gmax-1), Y = v => padT + plotH - plotH*v/ymax;
let d = "";
for (let g = 1; g <= gmax; g++) d += (g===1?"M":"L") + X(g) + " " + Y(E(g)) + " ";
el("path",{d,fill:"none",stroke:cssVar("--target"),"stroke-width":2},svg);
el("line",{x1:padL,x2:padL+plotW,y1:Y(1),y2:Y(1),stroke:cssVar("--axis"),
"stroke-dasharray":"4 4"},svg);
el("text",{x:padL+plotW,y:Y(1)-6,"text-anchor":"end"},svg).textContent = "plain autoregressive (1×)";
for (let g = 1; g <= gmax; g++){
const c = el("circle",{cx:X(g),cy:Y(E(g)),r:5,fill:cssVar("--target"),
stroke:cssVar("--surface"),"stroke-width":2},svg);
c.onpointerenter = e => showTip(e,
`γ=${g} → <b>${fmt(E(g),2)}</b> tokens per target call<br>at α=${fmt(alpha,2)}`);
c.onpointermove = moveTip; c.onpointerleave = hideTip;
el("text",{x:X(g),y:H-8,"text-anchor":"middle"},svg).textContent = "γ="+g;
}
const lbl = el("text",{x:padL+10,y:padT+14,class:"val"},svg);
lbl.textContent = `α=${fmt(alpha,2)}${fmt(E(gmax),2)} tokens/call at γ=8`;
$("speedChart").replaceChildren(svg);
}
// ---------- output histogram ----------
function drawOut(p){
const n = Math.max(tally.n, 1);
const stackTotals = tally.outAcc.map((a,i)=>(a+tally.outRes[i])/n);
// draw as stacked: accepted portion + resampled portion
const mount = $("outChart");
const W = Math.min(440, mount.clientWidth || 440), H = 190;
const padL = 30, padB = 24, padT = 8;
const plotW = W - padL - 8, plotH = H - padT - padB;
const ymax = Math.max(...stackTotals, ...p, .3);
const svg = el("svg",{width:"100%",viewBox:`0 0 ${W} ${H}`,role:"img",
"aria-label":"empirical output distribution vs target p"});
for (let g = 0; g <= 2; g++){
const y = padT + plotH - plotH*g/2;
el("line",{x1:padL,x2:W-8,y1:y,y2:y,stroke:cssVar("--grid")},svg);
el("text",{x:padL-5,y:y+4,"text-anchor":"end"},svg).textContent = fmt(ymax*g/2,2);
}
const group = plotW / N, bw = Math.min(26, group-10);
for (let i = 0; i < N; i++){
const x = padL + group*i + (group-bw)/2;
const hA = plotH * (tally.outAcc[i]/n) / ymax;
const hR = plotH * (tally.outRes[i]/n) / ymax;
const yA = padT + plotH - hA;
if (hA > 0) el("rect",{x,y:yA,width:bw,height:Math.max(hA,1),fill:cssVar("--accept"),rx:2},svg);
if (hR > 0) el("rect",{x,y:yA-hR-2,width:bw,height:Math.max(hR,1),
fill:cssVar("--resample"),rx:2},svg);
const my = padT + plotH - plotH*p[i]/ymax;
el("line",{x1:x-3,x2:x+bw+3,y1:my,y2:my,stroke:cssVar("--target"),"stroke-width":2.5},svg);
const hit = el("rect",{x:x-2,y:padT,width:bw+4,height:plotH,fill:"transparent"},svg);
hit.onpointerenter = e => showTip(e, `<b>${VOCAB[i]}</b><br>` +
`empirical: ${fmt(stackTotals[i])} (acc ${fmt(tally.outAcc[i]/n)}, ` +
`resampled ${fmt(tally.outRes[i]/n)})<br>target p: ${fmt(p[i])}`);
hit.onpointermove = moveTip; hit.onpointerleave = hideTip;
el("text",{x:padL+group*i+group/2,y:H-6,"text-anchor":"middle"},svg).textContent = VOCAB[i];
}
mount.replaceChildren(svg);
}
// ---------- the actual rejection sampling ----------
function sample(dist){
let u = Math.random(), c = 0;
for (let i = 0; i < dist.length; i++){ c += dist[i]; if (u < c) return i; }
return dist.length - 1;
}
function residualOf(p, q){
const r = p.map((v,i)=>Math.max(v - q[i], 0));
const s = r.reduce((a,b)=>a+b,0);
return s > 0 ? r.map(v=>v/s) : p.slice();
}
function verifyOnce(q, p){
const x = sample(q);
const ratio = p[x] / q[x];
const aProb = Math.min(1, ratio);
const u = Math.random();
const accepted = u < aProb;
let out = x, res = null;
if (!accepted){ res = residualOf(p, q); out = sample(res); }
return {x, ratio, aProb, u, accepted, out, res};
}
function stepOnce(){
const {q, p} = drawDist();
const t = verifyOnce(q, p);
tally.n++; if (t.accepted){ tally.acc++; tally.outAcc[t.out]++; } else tally.outRes[t.out]++;
const log = $("log"); log.replaceChildren();
const add = h => { const d = document.createElement("div"); d.className = "step";
d.innerHTML = h; log.appendChild(d); };
add(`1 · draft samples x ~ q → <span class="chip" style="background:var(--draft)">${VOCAB[t.x]}</span> (q=${fmt(q[t.x])})`);
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>`);
add(`3 · accept prob = min(1, ${fmt(t.ratio,2)}) = <b>${fmt(t.aProb)}</b>; uniform draw u = <b>${fmt(t.u)}</b>`);
if (t.accepted){
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>`);
$("residualWrap").style.display = "none";
} else {
add(`4 · u ≥ ${fmt(t.aProb)} → <b style="color:var(--reject)">✗ REJECTED</b> — the draft over-claimed this token`);
add(`5 · resample from norm(max(p−q,0)) → <span class="chip" style="background:var(--resample)">${VOCAB[t.out]}</span>`);
$("residualWrap").style.display = "block";
barChart($("residualChart"), t.res, VOCAB.map(()=>cssVar("--residual")), null,
"residual distribution");
}
refreshStats(p);
}
function runMany(){
const {q, p} = drawDist();
for (let k = 0; k < 2000; k++){
const t = verifyOnce(q, p);
tally.n++; if (t.accepted){ tally.acc++; tally.outAcc[t.out]++; } else tally.outRes[t.out]++;
}
refreshStats(p);
}
function refreshStats(p){
$("empStat").textContent = tally.n ? fmt(tally.acc/tally.n) : "–";
$("empLabel").textContent = `empirical acceptance (${tally.n.toLocaleString()} trials)`;
drawOut(p);
}
function drawAll(){
const {q, p, alpha} = drawDist();
drawSpeed(alpha);
drawOut(p);
}
$("preset").onchange = e => {
const pr = PRESETS[e.target.value];
qw = pr.q.slice(); pw = pr.p.slice();
$("resetBtn").click();
};
$("stepBtn").onclick = stepOnce;
$("runBtn").onclick = runMany;
$("resetBtn").onclick = () => {
tally = {n:0, acc:0, outAcc:Array(N).fill(0), outRes:Array(N).fill(0)};
$("log").innerHTML = '<div class="step hint">Press “Propose &amp; verify one token”.</div>';
$("residualWrap").style.display = "none";
drawAll(); refreshStats(norm(pw));
};
window.addEventListener("resize", drawAll);
drawAll();
</script>
</body>
</html>