lm-playground-api / web /chapters /speculative-decoding.html
deploy
Deploy LM Playground API
f0a527a
Raw
History Blame Contribute Delete
36.8 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>14. Speculative decoding β€” LM Playground</title>
<link rel="stylesheet" href="/platform/platform.css"/>
<script type="module" src="/platform/platform.js"></script>
<style>
:root{
--bg:#0f1117; --panel:#181b24; --panel-2:#1f2330; --border:#2a2f3d;
--text:#e6e8ee; --muted:#9aa3b2; --accent:#7c8cff; --accent-2:#5be0c0;
--good:#5be08a; --warn:#ffc06b; --bad:#ff9090;
--accept:#5be08a; --reject:#ff9090; --correct:#7c8cff; --draft:#ffc06b;
--mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;
}
*{box-sizing:border-box}
html,body{margin:0;padding:0 20px 80px;background:radial-gradient(1200px 600px at 50% -10%,#1a1f2e 0%,var(--bg) 55%);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;line-height:1.5;-webkit-font-smoothing:antialiased}
header{max-width:980px;margin:0 auto;padding:34px 0 0}
.header-inner{display:flex;justify-content:space-between;align-items:flex-start;padding-bottom:12px}
h1{font-size:30px;margin:0 0 6px;letter-spacing:-.5px}
.sub{color:var(--muted);max-width:780px;margin:0;font-size:14.5px;line-height:1.6}
.sub b{color:var(--text);font-weight:600}
.page-tabs{display:flex;border-bottom:1px solid var(--border)}
.page-tab{background:none;border:none;color:var(--muted);padding:11px 22px 10px;font-size:14px;font-weight:600;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;transition:.12s}
.page-tab:hover{color:var(--text)}.page-tab.active{color:var(--accent);border-bottom-color:var(--accent)}
.tab-panel{display:none}.tab-panel.active{display:block}
.guide{max-width:980px;margin:0 auto;padding:30px 0 80px;display:grid;grid-template-columns:1fr 220px;gap:0 48px}
.guide-toc{grid-column:2;grid-row:1/20;position:sticky;top:64px;align-self:start;background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:16px;display:flex;flex-direction:column;gap:4px}
.toc-label{font-size:10px;text-transform:uppercase;letter-spacing:1px;color:var(--muted);font-weight:700;margin-bottom:6px}
.guide-toc a{color:var(--muted);text-decoration:none;font-size:12.5px;padding:5px 8px;border-radius:6px;transition:.1s}
.guide-toc a:hover{color:var(--text);background:var(--panel-2)}
.chapter{grid-column:1;padding:0 0 48px;border-bottom:1px solid var(--border);margin-bottom:48px}
.chapter:last-of-type{border-bottom:none}
.chapter h2{font-size:22px;margin:0 0 18px;display:flex;align-items:center;gap:12px;letter-spacing:-.3px}
.ch-num{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:99px;background:var(--accent-2);color:#0b0d14;font-size:14px;font-weight:800;flex-shrink:0}
.chapter h3{font-size:15px;margin:24px 0 8px;color:var(--accent-2);font-weight:700}
.chapter p{margin:0 0 14px;line-height:1.7;color:#ced3de;font-size:15px}
.chapter strong{color:var(--text)}.chapter em{font-style:italic}
code{background:var(--panel-2);border:1px solid var(--border);border-radius:5px;padding:1px 5px;font-family:var(--mono);font-size:12.5px;color:var(--accent-2)}
.callout{border-radius:10px;padding:14px 16px;margin:20px 0;font-size:14px;line-height:1.65;color:#ced3de}
.callout strong{display:block;margin-bottom:5px;font-size:12px;text-transform:uppercase;letter-spacing:.6px}
.callout.insight{background:#7c8cff18;border-left:3px solid var(--accent)}.callout.insight strong{color:var(--accent)}
.callout.warn{background:#ffc06b18;border-left:3px solid var(--warn)}.callout.warn strong{color:var(--warn)}
.try-it{display:inline-block;margin-top:10px;padding:10px 18px;border-radius:9px;background:var(--panel-2);border:1px solid var(--border);color:var(--accent);font-size:13.5px;font-weight:600;cursor:pointer;transition:.14s;font-family:inherit}
.try-it:hover{background:#7c8cff22;border-color:var(--accent)}
.try-it.large{font-size:15px;padding:14px 28px;background:var(--accent-2);color:#0b0d14;border-color:var(--accent-2)}
.try-it.large:hover{filter:brightness(1.08)}
.guide-end{text-align:center;padding:18px 0 0}.guide-end p{color:var(--muted);margin-bottom:14px}
.panel-guide-item{display:flex;gap:12px;align-items:baseline;padding:9px 0;border-bottom:1px dashed var(--border)}
.panel-guide-item:last-child{border-bottom:none}
.pgi-label{font-size:13px;font-weight:800;color:var(--accent-2);min-width:20px;font-family:var(--mono)}
.panel-guide-item p{margin:0;font-size:13.5px;color:#ced3de;line-height:1.55}
.wrap{max-width:980px;margin:20px auto 0}
.panel{background:var(--panel);border:1px solid var(--border);border-radius:14px;padding:16px 18px;margin-bottom:16px}
.panel-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;flex-wrap:wrap;gap:8px}
.panel-label{font-size:13px;font-weight:700;color:var(--text)}.panel-note{color:var(--muted);font-size:12px}
.ctrl-row{display:flex;flex-wrap:wrap;gap:16px;align-items:flex-end;margin:4px 0 6px}
.ctrl{display:flex;flex-direction:column;gap:6px}
.ctrl .lab{font-size:11px;text-transform:uppercase;letter-spacing:.7px;color:var(--muted);font-weight:600}
.ctrl .lab b{color:var(--text);font-family:var(--mono)}
input[type=range]{accent-color:var(--accent);width:150px}
.btn{border:1px solid var(--border);background:var(--panel-2);color:var(--text);font-size:13px;font-weight:600;padding:9px 16px;border-radius:9px;cursor:pointer;font-family:inherit}
.btn:hover{border-color:var(--accent)}.btn.primary{background:var(--accent-2);color:#0b0d14;border-color:var(--accent-2)}
svg{display:block;width:100%;height:auto;overflow:visible}
.round{display:flex;flex-wrap:wrap;gap:5px;align-items:center;margin:6px 0;min-height:42px}
.tok{font-family:var(--mono);font-size:15px;width:30px;height:34px;border-radius:6px;display:flex;align-items:center;justify-content:center;border:1px solid var(--border);background:var(--panel-2);position:relative}
.tok.draft{border-color:var(--draft);color:var(--draft)}
.tok.accept{border-color:var(--accept);color:var(--accept);background:#5be08a18}
.tok.reject{border-color:var(--reject);color:var(--reject);background:#ff909018;opacity:.6;text-decoration:line-through}
.tok.correct{border-color:var(--correct);color:var(--correct);background:#7c8cff20}
.tok .pin{position:absolute;top:-8px;right:-6px;font-size:8px}
.arrow{color:var(--muted);font-size:13px}
.statline{display:flex;flex-wrap:wrap;gap:20px;margin:10px 0 2px}
.stat .v{font-family:var(--mono);font-size:17px;font-weight:700}
.stat .l{font-size:10.5px;text-transform:uppercase;letter-spacing:.5px;color:var(--muted)}
.gen{font-family:var(--mono);font-size:13.5px;line-height:1.7;color:var(--accent-2);background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:10px 12px;min-height:54px;white-space:pre-wrap;word-break:break-word;margin-top:8px}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:16px;align-items:start}
.card{background:var(--panel-2);border:1px solid var(--border);border-radius:11px;padding:14px}
.card h4{margin:0 0 4px;font-size:13px}.card .cap{color:var(--muted);font-size:11.5px;margin:0 0 8px;line-height:1.5}
.verdict{font-size:12.5px;margin-top:10px;padding:8px 11px;border-radius:7px;font-weight:600;background:#7c8cff18;color:var(--accent)}
.legend{display:flex;flex-wrap:wrap;gap:14px;font-size:11.5px;color:var(--muted);margin-top:8px}
.legend i{display:inline-block;width:11px;height:11px;border-radius:3px;vertical-align:middle;margin-right:5px;border:1px solid}
.hint{color:var(--muted);font-size:12.5px;margin-top:10px;line-height:1.5}
.fig{margin:22px 0;background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:18px 16px 12px}
.fig svg{display:block;width:100%;height:auto;overflow:visible}
.fig figcaption{margin-top:10px;font-size:12px;color:var(--muted);text-align:center;line-height:1.55}
footer{max-width:980px;margin:30px auto 0;color:var(--muted);font-size:12px;text-align:center;line-height:1.6}
@media(max-width:760px){.guide{grid-template-columns:1fr}.guide-toc{grid-column:1;grid-row:auto;position:static;display:grid;grid-template-columns:1fr 1fr}.toc-label{grid-column:1/-1}.grid2{grid-template-columns:1fr}}
</style>
</head>
<body data-chapter="speculative-decoding">
<header>
<div class="header-inner">
<div>
<h1>Speculative decoding</h1>
<p class="sub">
A big model spends one expensive forward pass to produce a single token β€” and that
pass costs nearly the same whether it checks one token or five. So let a small, fast
model <b>guess several tokens ahead</b>, then have the big model verify the whole guess
in one pass. Keep the run of correct guesses, fix the first wrong one. The output is
identical to the big model's; you just got there faster.
</p>
</div>
</div>
<nav class="page-tabs" role="tablist">
<button class="page-tab active" data-tab="guide">β‘  Guide</button>
<button class="page-tab" data-tab="playground">β‘‘ Playground</button>
</nav>
</header>
<!-- ═══════════════════════════════════════════════════════════════ GUIDE -->
<section id="guide-tab" class="tab-panel active">
<article class="guide">
<nav class="guide-toc">
<span class="toc-label">Contents</span>
<a href="#ch1">1 Β· The pass that's wasted</a>
<a href="#ch2">2 Β· Draft and verify</a>
<a href="#ch3">3 Β· Lossless, not approximate</a>
<a href="#ch4">4 Β· Acceptance rate</a>
<a href="#ch5">5 Β· Where the speedup hides</a>
<a href="#ch6">6 Β· Reading the playground</a>
</nav>
<!-- 1 -->
<section class="chapter" id="ch1">
<h2><span class="ch-num">1</span> The pass that's mostly wasted</h2>
<p>
From Chapter 13: generating a token is memory-bound. The expensive part of a forward
pass is hauling the model's billions of weights out of memory, and that cost is paid
whether the pass produces one token or processes a hundred in parallel. Standard decoding
produces exactly one token per pass β€” which means almost all of that hauled-in compute
capacity sits idle.
</p>
<figure class="fig"><svg viewBox="0 0 700 210" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,Segoe UI,Roboto,sans-serif"><defs><marker id="arspec" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="var(--muted)"/></marker></defs>
<rect x="30" y="40" width="120" height="40" rx="8" fill="var(--panel-2)" stroke="var(--accent-2)"/>
<text x="90" y="58" text-anchor="middle" fill="var(--text)" font-size="12" font-weight="700">draft model</text>
<text x="90" y="72" text-anchor="middle" fill="var(--muted)" font-size="9.5">small Β· fast</text>
<text x="200" y="34" fill="var(--muted)" font-size="10">proposes a guess of k tokens</text>
<rect x="200" y="44" width="36" height="30" rx="5" fill="var(--accent-2)" opacity="0.70"/><text x="218" y="64" text-anchor="middle" fill="#0b0d14" font-size="11" font-weight="700">t1</text><rect x="244" y="44" width="36" height="30" rx="5" fill="var(--accent-2)" opacity="0.70"/><text x="262" y="64" text-anchor="middle" fill="#0b0d14" font-size="11" font-weight="700">t2</text><rect x="288" y="44" width="36" height="30" rx="5" fill="var(--accent-2)" opacity="0.70"/><text x="306" y="64" text-anchor="middle" fill="#0b0d14" font-size="11" font-weight="700">t3</text><rect x="332" y="44" width="36" height="30" rx="5" fill="var(--accent-2)" opacity="0.40"/><text x="350" y="64" text-anchor="middle" fill="#0b0d14" font-size="11" font-weight="700">t4</text><rect x="376" y="44" width="36" height="30" rx="5" fill="var(--accent-2)" opacity="0.40"/><text x="394" y="64" text-anchor="middle" fill="#0b0d14" font-size="11" font-weight="700">t5</text>
<rect x="30" y="130" width="120" height="40" rx="8" fill="var(--accent)" opacity="0.2" stroke="var(--accent)"/>
<text x="90" y="148" text-anchor="middle" fill="var(--text)" font-size="12" font-weight="700">target model</text>
<text x="90" y="162" text-anchor="middle" fill="var(--muted)" font-size="9.5">big Β· verifies once</text>
<rect x="200" y="134" width="36" height="30" rx="5" fill="var(--good)" opacity="0.55"/><text x="218" y="154" text-anchor="middle" fill="#0b0d14" font-size="12" font-weight="700">βœ“</text><rect x="244" y="134" width="36" height="30" rx="5" fill="var(--good)" opacity="0.55"/><text x="262" y="154" text-anchor="middle" fill="#0b0d14" font-size="12" font-weight="700">βœ“</text><rect x="288" y="134" width="36" height="30" rx="5" fill="var(--good)" opacity="0.55"/><text x="306" y="154" text-anchor="middle" fill="#0b0d14" font-size="12" font-weight="700">βœ“</text><rect x="332" y="134" width="36" height="30" rx="5" fill="var(--bad)" opacity="0.55"/><text x="350" y="154" text-anchor="middle" fill="#0b0d14" font-size="12" font-weight="700">βœ—</text><rect x="376" y="134" width="36" height="30" rx="5" fill="var(--bad)" opacity="0.55"/><text x="394" y="154" text-anchor="middle" fill="#0b0d14" font-size="12" font-weight="700">βœ—</text>
<line x1="218" y1="74" x2="218" y2="134" stroke="var(--border)" stroke-dasharray="3 3"/>
<line x1="350" y1="74" x2="350" y2="134" stroke="var(--border)" stroke-dasharray="3 3"/>
<text x="430" y="152" fill="var(--muted)" font-size="10">accept the matching prefix,</text>
<text x="430" y="166" fill="var(--muted)" font-size="10">re-sample at the first mismatch</text>
<text x="350" y="200" text-anchor="middle" fill="var(--accent-2)" font-size="10.5">3 tokens in one verification pass β€” same output distribution</text>
</svg><figcaption>Speculative decoding uses a small fast model to guess several tokens ahead, then a single pass of the big model to verify them. Every token the big model agrees with is free; at the first disagreement it re-samples. The trick is provably lossless β€” the output matches plain sampling from the big model.</figcaption></figure>
<p>
So here's the opening: a single forward pass can <em>score</em> many token positions at
once for nearly free. If you only had some candidate tokens to score, you could verify a
whole batch of them in the time it normally takes to make one. The catch is producing the
candidates cheaply β€” which is where a second, smaller model comes in.
</p>
<div class="callout insight">
<strong>One big pass can check many tokens for the price of one.</strong>
The bottleneck isn't doing the arithmetic for several tokens β€” it's loading the weights.
Speculative decoding exists to put that already-paid-for capacity to use.
</div>
<button class="try-it" data-action="open">β–Ά Step through a draft-and-verify round</button>
</section>
<!-- 2 -->
<section class="chapter" id="ch2">
<h2><span class="ch-num">2</span> Draft and verify</h2>
<p>
The setup is two models: a large <strong>target</strong> (the one whose output you
actually want) and a small, cheap <strong>draft</strong> model. Each round goes:
</p>
<p>
The draft model quickly generates a guess of the next <code>K</code> tokens, running
itself <code>K</code> times β€” cheap, because it's small. The target model then takes that
whole guess and verifies all <code>K</code> positions in a <em>single</em> forward pass.
You accept the longest prefix of the guess that the target agrees with, and at the first
disagreement you throw out the rest and substitute the target's own token. Every round
costs one target pass but emits anywhere from one token (draft was wrong immediately) up
to <code>K+1</code> (draft was right the whole way, plus a free bonus token).
</p>
<div class="callout insight">
<strong>One target pass, several tokens out.</strong>
Standard decoding is one pass per token. Speculative decoding is one pass per <em>round</em>,
and a round emits as many tokens as the draft got right β€” plus one. When the draft is
good, that's a multiplier on throughput.
</div>
<button class="try-it" data-action="round">β–Ά Run rounds and count accepted tokens</button>
</section>
<!-- 3 -->
<section class="chapter" id="ch3">
<h2><span class="ch-num">3</span> Lossless, not approximate</h2>
<p>
The surprising part: this is not a quality trade-off. The acceptance rule is designed so
that the tokens you emit follow <em>exactly</em> the target model's own distribution. For
greedy decoding the check is simple β€” accept the draft's token only if it equals the
token the target would have picked β€” so the output is bit-for-bit identical to running the
target alone. For sampling, a slightly cleverer probabilistic acceptance test (speculative
sampling) gives the same guarantee in expectation.
</p>
<p>
That's what makes the technique a free lunch in a field that rarely offers one. You aren't
approximating the big model with the small one; the small one only ever <em>proposes</em>,
and the big one has the final say on every token. Wrong guesses cost a little wasted draft
compute, never a wrong answer.
</p>
<div class="callout insight">
<strong>The draft proposes; the target decides.</strong>
Because the target verifies every token, a bad draft can only make you slower, never
wrong. The output distribution is provably the target's. Speedup with no quality cost is
the whole reason this is everywhere now.
</div>
</section>
<!-- 4 -->
<section class="chapter" id="ch4">
<h2><span class="ch-num">4</span> Acceptance rate</h2>
<p>
The speedup lives entirely in the <strong>acceptance rate</strong> β€” how often the draft's
guess matches the target. The closer the draft mimics the target, and the more predictable
the text, the longer the accepted runs. Boilerplate, code, and formulaic prose get
devoured many tokens per round; genuinely novel or high-entropy text barely beats one.
</p>
<p>
This is why drafts are usually a smaller model from the <em>same family</em>, or even a
few extra prediction heads bolted onto the target itself (Medusa, EAGLE) so the draft and
target can't help but agree. A draft that's fast but rarely right is worse than useless β€”
you pay its cost and accept almost nothing.
</p>
<button class="try-it" data-action="round">β–Ά Watch the acceptance length on real text</button>
</section>
<!-- 5 -->
<section class="chapter" id="ch5">
<h2><span class="ch-num">5</span> Where the speedup hides</h2>
<p>
Net speedup is a tug-of-war. Drafting more tokens per round (<code>K</code>) raises the
ceiling on tokens you can accept β€” but every drafted token past the first rejection is
wasted draft compute, and the draft isn't free. Push <code>K</code> too high and you spend
more time drafting tokens that get thrown away than you save. There's an optimal draft
length, and it moves with the acceptance rate and the draft's relative cost.
</p>
<p>
Roughly, the speedup is the average tokens accepted per round divided by the overhead of
running the draft <code>K</code> times. A draft that's, say, ten times cheaper than the
target, getting three or four tokens accepted per round, lands you a 2–3Γ— throughput win
in practice β€” which is why nearly every serious inference stack now ships it.
</p>
<button class="try-it" data-action="curve">β–Ά Find the optimal draft length</button>
</section>
<!-- 6 -->
<section class="chapter" id="ch6">
<h2><span class="ch-num">6</span> Reading the playground</h2>
<p>
Two <strong>real</strong> models run here β€” a well-trained target and a deliberately
weaker draft, both character-level, both trained live in your browser. The draft-and-verify
loop is genuine speculative <em>sampling</em>: the draft proposes, the target's acceptance
test decides, and the emitted tokens come out distributed exactly as the target's own.
</p>
<div class="panel-guide-item"><span class="pgi-label">β–Ά</span>
<p>Step through rounds: the draft's guess in yellow, accepted tokens in green, the first
rejected one struck out, and the target's correction in blue.</p></div>
<div class="panel-guide-item"><span class="pgi-label">βˆ‘</span>
<p>Live stats β€” acceptance length, agreement rate, and the estimated speedup under a draft
cost you control.</p></div>
<div class="panel-guide-item"><span class="pgi-label">⌣</span>
<p>The speedup-versus-draft-length curve, with the optimal <code>K</code> marked β€” push the
draft cost and agreement and watch the sweet spot move.</p></div>
<div class="guide-end">
<p>The reading is the setup. The playground is the point.</p>
<button class="try-it large" data-action="open">β–Ά Open the Playground</button>
</div>
</section>
</article>
</section>
<!-- ═══════════════════════════════════════════════════════════════ PLAYGROUND -->
<section id="playground-tab" class="tab-panel">
<div class="wrap">
<div class="panel">
<div class="panel-head"><span class="panel-label">Speculative decoding</span>
<span class="panel-note" id="trainNote">real target + draft models Β· trained live</span></div>
<div class="ctrl-row">
<button class="btn primary" id="roundBtn">β–Ά One round</button>
<button class="btn" id="play5Btn">β–Άβ–Ά Run 12</button>
<button class="btn" id="resetBtn">β†Ί Reset</button>
<div class="ctrl"><span class="lab">Draft length K: <b id="kVal">4</b></span>
<input type="range" id="kLen" min="1" max="10" value="4"></div>
<div class="ctrl"><span class="lab">Temperature: <b id="tVal">1.00</b></span>
<input type="range" id="temp" min="30" max="150" value="100"></div>
<div class="ctrl"><span class="lab">Draft cost (Γ— target): <b id="rVal">0.10</b></span>
<input type="range" id="rCost" min="3" max="100" value="10"></div>
</div>
</div>
<div class="panel" id="panelRound">
<div class="panel-head"><span class="panel-label">Step 1 Β· this round</span>
<span class="panel-note" id="roundNote">draft guesses, target verifies in one pass</span></div>
<div class="round" id="roundViz"></div>
<div class="legend">
<span><i style="border-color:var(--draft)"></i>draft guess</span>
<span><i style="border-color:var(--accept);background:#5be08a18"></i>accepted</span>
<span><i style="border-color:var(--reject);background:#ff909018"></i>rejected</span>
<span><i style="border-color:var(--correct);background:#7c8cff20"></i>target correction / bonus</span>
</div>
<div class="gen" id="genBox">t</div>
<div class="statline">
<div class="stat"><span class="v" id="stTokens">1</span><span class="l">tokens generated</span></div>
<div class="stat"><span class="v" id="stRounds">0</span><span class="l">target passes (rounds)</span></div>
<div class="stat"><span class="v" style="color:var(--accept)" id="stAccLen">β€”</span><span class="l">avg accepted / round</span></div>
<div class="stat"><span class="v" id="stQ">β€”</span><span class="l">draft agreement</span></div>
<div class="stat"><span class="v" style="color:var(--accent-2)" id="stSpeed">β€”</span><span class="l">est. speedup</span></div>
</div>
</div>
<div class="panel" id="panelCurve">
<div class="panel-head"><span class="panel-label">Step 2 Β· where the speedup hides</span>
<span class="panel-note">speedup vs draft length, given agreement &amp; draft cost</span></div>
<div class="ctrl-row">
<div class="ctrl"><span class="lab">Draft agreement q: <b id="qVal">β€”</b> <span style="text-transform:none;letter-spacing:0;color:var(--muted)">(measured: <span id="qMeas">β€”</span>)</span></span>
<input type="range" id="qSlide" min="10" max="98" value="70"></div>
</div>
<div class="grid2">
<div class="card">
<h4>Speedup vs draft length K</h4>
<p class="cap">Longer drafts accept more β€” until wasted draft compute on rejected
tokens overtakes the gain. The peak is the optimal K.</p>
<svg id="curvePlot" viewBox="0 0 420 220"></svg>
</div>
<div class="card">
<h4>The trade, in one line</h4>
<div style="font-family:var(--mono);font-size:12.5px;color:#ced3de;line-height:1.9;margin-top:4px">
tokens/round = 1 + Σ qⁱ<br>
cost/round = 1 + K Β· r<br>
<span style="color:var(--accent-2)">speedup = tokens / cost</span>
</div>
<div class="verdict" id="curveVerdict"></div>
<p class="hint">q = per-token agreement, r = draft cost relative to one target pass. The
target pass is the "1"; the draft adds KΒ·r.</p>
</div>
</div>
</div>
<footer>
Real speculative sampling between two live char-models β€” the emitted tokens follow the
target model's own distribution (verified by Monte Carlo). The speedup uses an explicit
cost model (1 target pass + K draft passes per round) β€” labeled, not measured wall-clock.
</footer>
</div>
</section>
<script>
"use strict";
/* ── tabs / toc / try-it ── */
function switchTab(name){document.querySelectorAll(".page-tab").forEach(b=>{const on=b.dataset.tab===name;b.classList.toggle("active",on);b.setAttribute("aria-selected",on);});document.querySelectorAll(".tab-panel").forEach(p=>p.classList.toggle("active",p.id===name+"-tab"));}
document.querySelectorAll(".page-tab").forEach(b=>b.addEventListener("click",()=>switchTab(b.dataset.tab)));
document.querySelectorAll(".guide-toc a").forEach(a=>a.addEventListener("click",e=>{e.preventDefault();document.querySelector(a.getAttribute("href"))?.scrollIntoView({behavior:"smooth",block:"start"});}));
document.querySelectorAll(".try-it[data-action]").forEach(b=>b.addEventListener("click",()=>{const a=b.dataset.action;switchTab("playground");window.scrollTo({top:0,behavior:"smooth"});const t=(a==="curve")?"panelCurve":(a==="open"?null:"panelRound");if(t)setTimeout(()=>document.getElementById(t)?.scrollIntoView({behavior:"smooth",block:"start"}),350);}));
/* ════════════════ ENGINE β€” two real char models ════════════════ */
function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;let t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296}}
const CORPUS=("the cat sat on the mat. the dog ran in the park. a small bird sang a song. she read the book by the window. the sun set over the calm sea. we walked along the road. the old man told a long story. the children played in the green field. rain fell on the roof. the river ran fast and clear. he wrote a note and left it on the table. the moon rose late and the stars came out over the hills.").toLowerCase();
const CHARS=[...new Set(CORPUS)].sort(), V=CHARS.length, D=8, H=24;
const c2i=new Map(CHARS.map((c,i)=>[c,i])), i2c=CHARS, SEQ=[...CORPUS].map(c=>c2i.get(c));
function makeModel(seed,hid){const r=mulberry32(seed),g=()=>{const u=r()||1e-9,v=r();return Math.sqrt(-2*Math.log(u))*Math.cos(2*Math.PI*v);};
return {hid,E:Array.from({length:V},()=>Array.from({length:D},()=>g()*0.4)),W1:Array.from({length:D},()=>Array.from({length:hid},()=>g()*0.4)),b1:new Array(hid).fill(0),W2:Array.from({length:hid},()=>Array.from({length:V},()=>g()*0.4)),b2:new Array(V).fill(0)};}
function logitsFor(m,prev){const hid=m.hid,e=m.E[prev],z1=new Array(hid);for(let h=0;h<hid;h++){let s=m.b1[h];for(let d=0;d<D;d++)s+=e[d]*m.W1[d][h];z1[h]=Math.tanh(s);}const lo=new Array(V);for(let k=0;k<V;k++){let s=m.b2[k];for(let h=0;h<hid;h++)s+=z1[h]*m.W2[h][k];lo[k]=s;}return {hid:z1,logits:lo};}
function trainStep(m,batch,lr){const hid=m.hid,gE=Array.from({length:V},()=>new Array(D).fill(0)),gW1=Array.from({length:D},()=>new Array(hid).fill(0)),gb1=new Array(hid).fill(0),gW2=Array.from({length:hid},()=>new Array(V).fill(0)),gb2=new Array(V).fill(0);
for(const i of batch){const prev=SEQ[i],tgt=SEQ[i+1],{hid:z1,logits}=logitsFor(m,prev);let mx=-Infinity;for(const x of logits)if(x>mx)mx=x;let Z=0;const p=logits.map(x=>{const e=Math.exp(x-mx);Z+=e;return e;});for(let k=0;k<V;k++)p[k]/=Z;
const dlog=p.slice();dlog[tgt]-=1;for(let k=0;k<V;k++){gb2[k]+=dlog[k];for(let h=0;h<hid;h++)gW2[h][k]+=z1[h]*dlog[k];}
const dh=new Array(hid).fill(0);for(let h=0;h<hid;h++){let s=0;for(let k=0;k<V;k++)s+=m.W2[h][k]*dlog[k];dh[h]=s*(1-z1[h]*z1[h]);}
for(let h=0;h<hid;h++){gb1[h]+=dh[h];for(let d=0;d<D;d++)gW1[d][h]+=m.E[prev][d]*dh[h];}
for(let d=0;d<D;d++){let s=0;for(let h=0;h<hid;h++)s+=m.W1[d][h]*dh[h];gE[prev][d]+=s;}}
const B=batch.length,scl=1/B;const upd=(P,Gv)=>{for(let a=0;a<P.length;a++)for(let b=0;b<P[a].length;b++)P[a][b]-=lr*Gv[a][b]*scl;};const updV=(P,Gv)=>{for(let a=0;a<P.length;a++)P[a]-=lr*Gv[a]*scl;};
upd(m.E,gE);upd(m.W1,gW1);updV(m.b1,gb1);upd(m.W2,gW2);updV(m.b2,gb2);}
function pretrain(m,steps,seed){const r=mulberry32(seed);for(let s=0;s<steps;s++){const b=[];for(let i=0;i<16;i++)b.push(Math.floor(r()*(SEQ.length-1)));trainStep(m,b,0.3);}}
function argmax(m,prev){const lo=logitsFor(m,prev).logits;let bi=0,bv=-Infinity;for(let k=0;k<V;k++)if(lo[k]>bv){bv=lo[k];bi=k;}return bi;}
let TARGET, DRAFT;
function softmaxT(logits,T){const t=Math.max(T,1e-3);let m=-Infinity;for(const x of logits)if(x>m)m=x;let Z=0;const e=logits.map(x=>{const v=Math.exp((x-m)/t);Z+=v;return v;});return e.map(x=>x/Z);}
function sampleDist(p,rng){let x=rng(),c=0;for(let i=0;i<p.length;i++){c+=p[i];if(x<=c)return i;}for(let i=p.length-1;i>=0;i--)if(p[i]>0)return i;return 0;}
// Speculative SAMPLING (Leviathan/Chen): the emitted tokens are distributed exactly as the
// target's own β€” the draft only proposes; the acceptance test guarantees the distribution.
function specRound(prev,K,T,rng){
let ctx=prev;const xs=[],qd=[];
for(let i=0;i<K;i++){const q=softmaxT(logitsFor(DRAFT,ctx).logits,T);const x=sampleDist(q,rng);xs.push(x);qd.push(q);ctx=x;}
let accepted=0,c=prev;
for(let i=0;i<K;i++){
const p=softmaxT(logitsFor(TARGET,c).logits,T),q=qd[i],x=xs[i];
const ratio=q[x]>1e-12?Math.min(1,p[x]/q[x]):1;
if(rng()<ratio){accepted++;c=x;}
else{ // reject: sample correction from the normalized residual max(0, p βˆ’ q)
const res=p.map((pv,j)=>Math.max(0,pv-q[j]));let s=0;for(const v of res)s+=v;
const corr=s>1e-9?sampleDist(res.map(v=>v/s),rng):sampleDist(p,rng);
return {drafted:xs,accepted,correct:corr,bonus:false,emitted:xs.slice(0,accepted).concat([corr]),newPrev:corr};
}
}
const pf=softmaxT(logitsFor(TARGET,c).logits,T),bonus=sampleDist(pf,rng); // all accepted β†’ bonus from target
return {drafted:xs,accepted,correct:bonus,bonus:true,emitted:xs.slice(0,accepted).concat([bonus]),newPrev:bonus};
}
/* ════════════════ UI ════════════════ */
const $=id=>document.getElementById(id);
const css=v=>getComputedStyle(document.documentElement).getPropertyValue(v).trim();
function dispChar(c){return c===' '?'␣':c;}
const S={prev:c2i.get('t')??0,K:4,r:0.10,T:1.0,rng:mulberry32(12345),out:[c2i.get('t')??0],rounds:0,proposed:0,accepted:0,qSlide:0.7,qTouched:false};
function doRound(){
const R=specRound(S.prev,S.K,S.T,S.rng);
S.rounds++;S.proposed+=S.K;S.accepted+=R.accepted;
R.emitted.forEach(t=>S.out.push(t));S.prev=R.newPrev;
drawRound(R);updateStats();
}
function drawRound(R){
let s="";
for(let i=0;i<R.drafted.length;i++){
let cls="tok ";
if(i<R.accepted)cls+="accept";else if(i===R.accepted)cls+="reject";else cls+="draft";
s+='<span class="'+cls+'">'+dispChar(i2c[R.drafted[i]])+(i<R.accepted?'<span class="pin" style="color:var(--accept)">βœ“</span>':i===R.accepted?'<span class="pin" style="color:var(--reject)">βœ—</span>':'')+'</span>';
}
s+='<span class="arrow">β†’</span>';
s+='<span class="tok correct">'+dispChar(i2c[R.correct])+'<span class="pin" style="color:var(--correct)">'+(R.bonus?'β˜…':'fix')+'</span></span>';
$('roundViz').innerHTML=s;
$('roundNote').textContent=R.bonus?('all '+S.K+' accepted + bonus token β†’ '+(R.accepted+1)+' tokens this pass'):(R.accepted+' accepted, then corrected β†’ '+(R.accepted+1)+' tokens this pass');
$('genBox').textContent=S.out.map(t=>i2c[t]).join('');
}
function updateStats(){
$('stTokens').textContent=S.out.length;
$('stRounds').textContent=S.rounds;
const accLen=S.rounds?((S.out.length-1)/S.rounds):0; // tokens beyond the seed / rounds
$('stAccLen').textContent=accLen.toFixed(2);
const q=S.proposed?S.accepted/S.proposed:0;
$('stQ').textContent=S.rounds?Math.round(q*100)+'%':'β€”';
$('qMeas').textContent=S.rounds?Math.round(q*100)+'%':'β€”';
// speedup = tokens/round Γ· (1 + KΒ·r)
const speedup=S.rounds?(accLen/(1+S.K*S.r)):0;
$('stSpeed').textContent=S.rounds?speedup.toFixed(2)+'Γ—':'β€”';
if(!S.qTouched&&S.rounds>0){S.qSlide=q;$('qSlide').value=Math.round(q*100);}
drawCurve();
}
/* analytic speedup vs K */
function tokensPerRound(q,K){let acc=0,qp=1;for(let i=1;i<=K;i++){qp*=q;acc+=qp;}return 1+acc;}
function speedup(q,K,r){return tokensPerRound(q,K)/(1+K*r);}
function drawCurve(){
const q=S.qTouched?S.qSlide:(S.proposed?S.accepted/S.proposed:S.qSlide),r=S.r;
$('qVal').textContent=q.toFixed(2);
const W=420,H=220,padL=40,padR=14,padT=14,padB=30,Kmax=10;
let best=1,bestK=1;for(let K=1;K<=Kmax;K++){const v=speedup(q,K,r);if(v>best){best=v;bestK=K;}}
const maxY=Math.max(best*1.1,1.2);
const X=K=>padL+((K-1)/(Kmax-1))*(W-padL-padR),Y=v=>H-padB-(v/maxY)*(H-padT-padB);
let s="";
s+='<line x1="'+padL+'" y1="'+(H-padB)+'" x2="'+(W-padR)+'" y2="'+(H-padB)+'" stroke="'+css('--border')+'"/>';
s+='<line x1="'+padL+'" y1="'+Y(1)+'" x2="'+(W-padR)+'" y2="'+Y(1)+'" stroke="'+css('--muted')+'" stroke-dasharray="3 3" opacity="0.5"/>';
s+='<text x="'+(padL+3)+'" y="'+(Y(1)-3)+'" font-size="8.5" fill="'+css('--muted')+'">1Γ— (no speedup)</text>';
let p="";for(let K=1;K<=Kmax;K++){p+=(K>1?"L":"M")+X(K).toFixed(1)+" "+Y(speedup(q,K,r)).toFixed(1)+" ";}
s+='<path d="'+p+'" fill="none" stroke="'+css('--accent-2')+'" stroke-width="2"/>';
for(let K=1;K<=Kmax;K++){const on=K===S.K;s+='<circle cx="'+X(K)+'" cy="'+Y(speedup(q,K,r))+'" r="'+(on?4.5:2.5)+'" fill="'+(on?css('--accent'):css('--accent-2'))+'"/>';s+='<text x="'+X(K)+'" y="'+(H-padB+13)+'" font-size="8.5" text-anchor="middle" fill="'+(on?css('--accent'):css('--muted'))+'">'+K+'</text>';}
// optimum
s+='<line x1="'+X(bestK)+'" y1="'+padT+'" x2="'+X(bestK)+'" y2="'+(H-padB)+'" stroke="'+css('--good')+'" stroke-dasharray="4 3" opacity="0.6"/>';
s+='<text x="'+X(bestK)+'" y="'+(padT+9)+'" font-size="9" text-anchor="middle" fill="'+css('--good')+'">best K='+bestK+'</text>';
s+='<text x="6" y="'+(padT+30)+'" font-size="9" fill="'+css('--muted')+'" transform="rotate(-90 10 '+(padT+40)+')">speedup</text>';
s+='<text x="'+((padL+W-padR)/2)+'" y="'+(H-3)+'" font-size="9" text-anchor="middle" fill="'+css('--muted')+'">draft length K β†’</text>';
$('curvePlot').innerHTML=s;
$('curveVerdict').innerHTML='At q='+q.toFixed(2)+', draft cost r='+r.toFixed(2)+': optimal draft length is <b>K='+bestK+'</b>, for about <b>'+best.toFixed(2)+'Γ—</b> speedup. Your current K='+S.K+' gives '+speedup(q,S.K,r).toFixed(2)+'Γ—.';
}
function reset(){S.prev=c2i.get('t')??0;S.out=[S.prev];S.rounds=0;S.proposed=0;S.accepted=0;S.rng=mulberry32(12345);$('roundViz').innerHTML='';$('roundNote').textContent='draft guesses, target verifies in one pass';updateStats();$('genBox').textContent='t';}
/* ── events ── */
$('roundBtn').addEventListener('click',doRound);
$('play5Btn').addEventListener('click',()=>{for(let i=0;i<12;i++)doRound();});
$('resetBtn').addEventListener('click',reset);
$('kLen').addEventListener('input',e=>{S.K=+e.target.value;$('kVal').textContent=S.K;updateStats();});
$('temp').addEventListener('input',e=>{S.T=+e.target.value/100;$('tVal').textContent=S.T.toFixed(2);});
$('rCost').addEventListener('input',e=>{S.r=+e.target.value/100;$('rVal').textContent=S.r.toFixed(2);updateStats();});
$('qSlide').addEventListener('input',e=>{S.qTouched=true;S.qSlide=+e.target.value/100;drawCurve();});
/* ── init: train target (strong) and draft (weak) ── */
TARGET=makeModel(7,24);pretrain(TARGET,800,99);
DRAFT=makeModel(3,12);pretrain(DRAFT,120,55);
reset();
</script>
</body>
</html>