| <!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 & 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 & 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(·) (cheap, autoregressive, γ at a time)<br> |
| 2. accept x with prob min(1, p(x)/q(x)) (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); |
| |
| |
| $("themeBtn").onclick = () => { |
| const r = document.documentElement; |
| const dark = getComputedStyle(document.body).backgroundColor !== "rgb(249, 249, 247)"; |
| r.dataset.theme = dark ? "light" : "dark"; |
| drawAll(); |
| }; |
| |
| |
| 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"; } |
| |
| |
| 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(); } |
| |
| |
| 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"}); |
| |
| 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}; |
| } |
| |
| |
| 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); |
| } |
| |
| |
| 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); |
| } |
| |
| |
| function drawOut(p){ |
| const n = Math.max(tally.n, 1); |
| const stackTotals = tally.outAcc.map((a,i)=>(a+tally.outRes[i])/n); |
| |
| 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); |
| } |
| |
| |
| 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 < ${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 & verify one token”.</div>'; |
| $("residualWrap").style.display = "none"; |
| drawAll(); refreshStats(norm(pw)); |
| }; |
| window.addEventListener("resize", drawAll); |
| drawAll(); |
| </script> |
| </body> |
| </html> |
|
|