Spaces:
Sleeping
Sleeping
| <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 & 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> | |
| ; | |
| /* ββ 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> | |