Darkweb007 commited on
Commit
134b302
·
1 Parent(s): fa14f2f

redesign: professional UI + fix HF color metadata

Browse files
Files changed (1) hide show
  1. app.py +221 -311
app.py CHANGED
@@ -1,376 +1,286 @@
1
  """
2
- Speculative Decoding — Interactive Demo
3
- ========================================
4
- Visualize token-by-token acceptance/rejection, speedup charts,
5
- and the mathematical intuition behind speculative decoding.
6
-
7
  Author: Aravind Kumar Nalukurthi
8
  """
9
 
10
  import gradio as gr
11
- import os
12
- import json
13
  import plotly.graph_objects as go
14
- import plotly.express as px
15
- import numpy as np
16
-
17
- from speculative.decoder import get_precomputed_benchmark_results
18
 
19
- ENABLE_LIVE = os.getenv("ENABLE_LIVE_SPECULATIVE", "0") == "1"
20
 
21
  CSS = """
22
- body, .gradio-container { background: #0a0d14 !important; }
23
- .card { background: rgba(99,102,241,0.07); border: 1px solid rgba(99,102,241,0.3); border-radius: 12px; padding: 18px; margin: 8px 0; }
24
- .accepted { color: #22c55e; font-weight: 600; }
25
- .rejected { color: #ef4444; text-decoration: line-through; }
26
- .bonus { color: #a78bfa; font-weight: 600; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  footer { display: none !important; }
28
  """
29
 
30
- BENCHMARK = get_precomputed_benchmark_results()
31
-
32
- # --- Precomputed step visualization data ---
33
- DEMO_STEPS = [
34
  {
35
- "step": 1,
36
- "prompt_snippet": "The future of AI is",
37
- "draft_tokens": [" bright", " and", " full", " of", " promise"],
38
- "accepted": [True, True, True, True, False],
39
- "bonus": " opportunities",
40
- "draft_time": 42,
41
- "verify_time": 38,
42
- "n_accepted": 5, # 4 accepted + 1 bonus
43
  },
44
  {
45
- "step": 2,
46
- "prompt_snippet": "...full of opportunities",
47
- "draft_tokens": [" as", " machine", " learning", " models", " grow"],
48
- "accepted": [True, True, False, False, False],
49
- "bonus": " become",
50
- "draft_time": 41,
51
- "verify_time": 37,
52
- "n_accepted": 3, # 2 + 1 bonus
53
  },
54
  {
55
- "step": 3,
56
- "prompt_snippet": "...learning models become",
57
- "draft_tokens": [" more", " capable", " and", " access", "ible"],
58
- "accepted": [True, True, True, True, True],
59
- "bonus": ",",
60
- "draft_time": 44,
61
- "verify_time": 39,
62
- "n_accepted": 6, # all 5 + 1 bonus
63
  },
64
  {
65
- "step": 4,
66
- "prompt_snippet": "...capable and accessible,",
67
- "draft_tokens": [" transform", "ing", " industries", " like", " healthcare"],
68
- "accepted": [True, True, True, False, False],
69
- "bonus": " finance",
70
- "draft_time": 43,
71
- "verify_time": 38,
72
- "n_accepted": 4,
73
  },
74
  ]
75
 
 
 
 
 
 
76
 
77
- def build_speedup_chart():
78
- bench = BENCHMARK
79
- methods = ["Autoregressive\n(Baseline)", "Speculative\nDecoding (K=5)"]
80
- tps = [bench["baseline"]["throughput_tps"], bench["speculative"]["throughput_tps"]]
81
- colors = ["#475569", "#6366f1"]
82
-
83
- fig = go.Figure([
84
- go.Bar(x=methods, y=tps, marker_color=colors,
85
- text=[f"{v} tok/s" for v in tps], textposition="outside",
86
- textfont=dict(color="#e2e8f0", size=14))
87
- ])
88
- fig.update_layout(
89
- template="plotly_dark", paper_bgcolor="rgba(0,0,0,0)",
90
- plot_bgcolor="rgba(0,0,0,0)", font=dict(color="#e2e8f0"),
91
- title=f"Throughput: {bench['speculative']['speedup']} Speedup",
92
- yaxis_title="Tokens per Second", height=380,
93
- yaxis=dict(range=[0, 200]),
94
- margin=dict(t=50, b=10),
95
- )
96
- return fig
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
 
99
- def build_acceptance_chart():
100
- data = BENCHMARK["acceptance_by_prompt_type"]
101
- types = list(data.keys())
102
- rates = [data[t] for t in types]
103
-
104
- fig = go.Figure([
105
- go.Bar(
106
- x=rates, y=types, orientation="h",
107
- marker_color=["#22c55e" if r > 0.75 else "#f59e0b" if r > 0.65 else "#ef4444" for r in rates],
108
- text=[f"{r:.0%}" for r in rates], textposition="outside",
109
- )
110
- ])
111
- fig.add_vline(x=0.70, line_dash="dash", line_color="#a78bfa",
112
- annotation_text="Breakeven ~70%")
113
  fig.update_layout(
114
- template="plotly_dark", paper_bgcolor="rgba(0,0,0,0)",
115
- plot_bgcolor="rgba(0,0,0,0)", font=dict(color="#e2e8f0"),
116
- title="Acceptance Rate by Prompt Type",
117
- xaxis=dict(range=[0, 1.05]),
118
- height=320, margin=dict(t=50, b=10, l=200, r=80),
 
119
  )
120
  return fig
121
 
122
-
123
- def build_k_sweep_chart():
124
- data = BENCHMARK["speedup_vs_K"]
125
- fig = go.Figure([
126
- go.Scatter(
127
- x=data["K_values"], y=data["speedup"],
128
- mode="lines+markers",
129
- line=dict(color="#6366f1", width=3),
130
- marker=dict(size=8, color="#a78bfa"),
131
- name="Observed Speedup",
132
- ),
133
- go.Scatter(
134
- x=data["K_values"],
135
- y=[k * 0.71 for k in data["K_values"]], # theoretical: K * α
136
- mode="lines", line=dict(color="#f59e0b", dash="dash"),
137
- name="Theoretical (K × α, α=0.71)",
138
- ),
139
- ])
140
- fig.add_vline(x=5, line_dash="dot", line_color="#22c55e",
141
- annotation_text="K=5 (optimal)")
142
  fig.update_layout(
143
- template="plotly_dark", paper_bgcolor="rgba(0,0,0,0)",
144
- plot_bgcolor="rgba(0,0,0,0)", font=dict(color="#e2e8f0"),
145
- title="Speedup vs Draft Length K (GPT-2 → GPT-2-Medium)",
146
- xaxis_title="K (tokens drafted per step)",
147
- yaxis_title="Speedup vs Baseline",
148
- height=380, legend=dict(x=0.01, y=0.99),
149
- margin=dict(t=50, b=10),
150
  )
151
  return fig
152
 
153
 
154
- def build_step_visualization(step_idx: int):
155
- """Build token acceptance visualization for a speculative step."""
156
- step = DEMO_STEPS[step_idx % len(DEMO_STEPS)]
157
-
158
- tokens_html = ""
159
- for token, accepted in zip(step["draft_tokens"], step["accepted"]):
160
- if accepted:
161
- tokens_html += f"<span class='accepted' title='ACCEPTED (α = min(1, p_target/p_draft))'>{token}</span>"
162
- else:
163
- tokens_html += f"<span class='rejected' title='REJECTED — correction sampled from (p_target - α·p_draft)'>{token}</span>"
164
-
165
- bonus = step.get("bonus", "")
166
- if bonus:
167
- tokens_html += f"<span class='bonus' title='BONUS: sampled from verifier final distribution'>{bonus} ★</span>"
168
-
169
- n_accepted = step["n_accepted"]
170
- n_proposed = len(step["draft_tokens"])
171
-
172
- return f"""
173
- <div class='card'>
174
- <div style='display:flex;justify-content:space-between;align-items:center;margin-bottom:14px'>
175
- <div style='color:#a5b4fc;font-weight:700;font-size:1.05em'>Step {step["step"]}</div>
176
- <div style='font-size:0.82em;color:#64748b'>
177
- Draft: {step["draft_time"]}ms · Verify: {step["verify_time"]}ms
178
- </div>
179
- </div>
180
- <div style='color:#64748b;font-size:0.8em;margin-bottom:8px'>Context: "{step["prompt_snippet"]}"</div>
181
- <div style='background:#111827;border-radius:8px;padding:12px;margin-bottom:12px;font-size:1.1em;line-height:2;word-spacing:2px'>
182
- {tokens_html}
183
- </div>
184
- <div style='display:flex;gap:20px;font-size:0.82em'>
185
- <div><span style='color:#22c55e'>✓ green</span> = accepted</div>
186
- <div><span style='color:#ef4444'>✗ strikethrough</span> = rejected (corrected)</div>
187
- <div><span style='color:#a78bfa'>★ purple</span> = bonus token</div>
188
- </div>
189
- <div style='margin-top:12px;background:#111827;border-radius:6px;padding:8px;font-size:0.85em'>
190
- <span style='color:#64748b'>Tokens proposed:</span> <span style='color:#e2e8f0'>{n_proposed}</span> ·
191
- <span style='color:#64748b'>Tokens accepted:</span> <span style='color:#22c55e;font-weight:600'>{n_accepted}</span> ·
192
- <span style='color:#64748b'>Acceptance:</span> <span style='color:#a78bfa;font-weight:600'>{n_accepted/(n_proposed+1):.0%}</span>
193
- </div>
194
- </div>
195
- """
196
-
197
-
198
- def run_live_generation(prompt: str, K_val: int):
199
- """Live generation (only available with ENABLE_LIVE_SPECULATIVE=1)."""
200
- if not ENABLE_LIVE:
201
- return build_step_visualization(0), (
202
- "⚠️ Live generation requires GPU. See the 'Demo Steps' tab for step-by-step visualization."
203
- )
204
-
205
- try:
206
- from speculative.decoder import SpeculativeDecoder
207
- decoder = SpeculativeDecoder(K=K_val)
208
- result = decoder.generate(prompt, max_new_tokens=60, record_steps=True)
209
- step_htmls = [build_step_visualization(0)] # simplified for demo
210
- return step_htmls[0], result.output
211
- except Exception as e:
212
- return f"<div class='card'>Error: {e}</div>", ""
213
-
214
-
215
- with gr.Blocks(css=CSS, theme=gr.themes.Soft(primary_hue="violet"), title="Speculative Decoding") as demo:
216
 
217
  gr.HTML("""
218
- <div style='text-align:center;padding:28px 0 18px'>
219
- <div style='font-size:2.8em'></div>
220
- <h1 style='color:#e2e8f0;margin:10px 0 6px;font-size:1.9em;font-weight:700'>
221
- Speculative Decoding — From Scratch
222
- </h1>
223
- <p style='color:#64748b;max-width:720px;margin:0 auto;line-height:1.6'>
224
- Small draft model proposes K tokens, large verifier accepts or rejects in ONE forward pass.
225
- The output distribution is mathematically identical to the large model alone — just faster.
226
  </p>
227
  </div>
 
 
 
 
 
 
228
  """)
229
 
230
  with gr.Tabs():
231
 
232
- with gr.Tab("🎯 Step Visualizer"):
233
  gr.HTML("""
234
- <div class='card'>
235
- <div style='color:#94a3b8;font-size:0.9em'>
236
- GPT-2 (117M) drafts tokens → GPT-2-Medium (345M) verifies in one pass.
237
- Green = accepted, red = rejected with correction, purple★ = bonus token.
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  </div>
239
  </div>
240
  """)
241
- step_slider = gr.Slider(1, 4, value=1, step=1, label="Speculative Step Number")
242
- step_display = gr.HTML(build_step_visualization(0))
243
-
244
- step_slider.change(
245
- fn=lambda s: build_step_visualization(int(s) - 1),
246
- inputs=step_slider, outputs=step_display,
247
- )
248
-
249
- with gr.Tab("📊 Benchmark Results"):
250
- gr.HTML(f"""
251
- <div class='card'>
252
- <div style='display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;text-align:center'>
253
- <div>
254
- <div style='color:#6366f1;font-size:2em;font-weight:700'>{BENCHMARK["speculative"]["speedup"]}</div>
255
- <div style='color:#64748b;font-size:0.82em'>Speedup over baseline</div>
256
- </div>
257
- <div>
258
- <div style='color:#22c55e;font-size:2em;font-weight:700'>{BENCHMARK["speculative"]["mean_acceptance_rate"]:.0%}</div>
259
- <div style='color:#64748b;font-size:0.82em'>Mean acceptance rate</div>
260
- </div>
261
- <div>
262
- <div style='color:#a78bfa;font-size:2em;font-weight:700'>{BENCHMARK["speculative"]["throughput_tps"]}</div>
263
- <div style='color:#64748b;font-size:0.82em'>Tokens/sec (K=5)</div>
264
- </div>
 
 
 
 
265
  </div>
266
  </div>
267
  """)
268
- with gr.Row():
269
- gr.Plot(build_speedup_chart())
270
- gr.Plot(build_acceptance_chart())
271
- gr.Plot(build_k_sweep_chart())
272
 
273
- with gr.Tab("🧮 The Math"):
274
  gr.Markdown("""
275
- ## Rejection Sampling Acceptance Criterion
276
 
277
- The core insight: we want output matching `p_target` exactly but using `p_draft` for speed.
278
 
279
- **Acceptance probability per token:**
280
- ```
281
- α_i = min(1, p_target(t_i | context) / p_draft(t_i | context))
282
- ```
283
 
284
- **Decision:**
285
- - Sample r ~ Uniform(0, 1)
286
- - If r < α_i → **ACCEPT** token t_i
287
- - Else → **REJECT**, sample corrected token from:
288
- ```
289
- p_corrected = (p_target - α_i × p_draft).clip(0) / Z
290
- ```
291
- where Z is the normalization constant
292
-
293
- **Why this works (proof sketch):**
294
- The marginal probability of token t at position i, after accounting for accept/reject:
295
- ```
296
- P(output = t) = P(draft = t) × α(t) + P(reject) × p_corrected(t)
297
- = p_draft(t) × min(1, p_target(t)/p_draft(t))
298
- + p_reject × (p_target(t) - α(t)×p_draft(t)) / (1 - Σ_t' α(t')p_draft(t'))
299
- = p_target(t) ✓
300
- ```
301
-
302
- The output distribution is **exactly** p_target — no approximation, no quality loss.
303
-
304
- ## Bonus Token
305
-
306
- When all K draft tokens are accepted, we get to sample one additional token
307
- from the verifier's distribution at no extra compute cost:
308
- - Verifier already computed the final logits in its forward pass
309
- - → Free token: increases expected tokens per step from K to K+1
310
-
311
- ## Expected Tokens Per Step
312
-
313
- ```
314
- E[tokens per step] = Σ_{i=1}^{K} P(first i tokens all accepted) + P(all K accepted)
315
- ≈ (1 - α^K) / (1 - α) [geometric series] + α^K (bonus)
316
- ```
317
 
318
- For α=0.71, K=5:
319
- ```
320
- E[tokens] ≈ 3.47 tokens per verifier forward pass
321
- Vs baseline: 1 token per forward pass
322
- → 3.47x theoretical speedup (observe 1.87x due to draft overhead + batching)
323
- ```
324
 
325
- ## Implementation Complexity
326
 
327
  ```python
328
- # The entire accept/reject logic in ~10 lines:
329
- for i, draft_token in enumerate(draft_tokens):
330
- alpha = min(1, p_target[i, draft_token] / p_draft[i])
331
- if random() < alpha:
332
- accept(draft_token) # matches target distribution
333
- else:
334
- # Sample from corrected distribution
335
- p_corrected = (p_target[i] - alpha * p_draft_dist[i]).clamp(0)
336
- accept(sample(p_corrected / p_corrected.sum()))
337
- break # stop at first rejection
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  ```
339
 
340
- ## Why Not Just Run the Draft Model?
341
 
342
- The draft model (GPT-2, 117M) is 3x faster but outputs different text
343
- possibly lower quality for complex tasks. Speculative decoding gets you
344
- the large model's quality at nearly the draft model's speed.
345
 
346
- Key condition: **draft and target must share the same tokenizer**
347
- (same vocabulary). GPT-2 and GPT-2-Medium both use GPT-2's BPE tokenizer,
348
- so they work together. This is a practical constraint in production deployment.
349
- """)
350
 
351
- with gr.Tab("⚡ Live Generation"):
352
- if ENABLE_LIVE:
353
- with gr.Row():
354
- prompt_in = gr.Textbox(
355
- label="Prompt",
356
- value="The future of artificial intelligence is",
357
- lines=2, scale=3,
358
- )
359
- k_slider = gr.Slider(1, 8, value=5, step=1, label="K (draft tokens)", scale=1)
360
- gen_btn = gr.Button("Generate with Speculative Decoding", variant="primary", size="lg")
361
- live_step = gr.HTML()
362
- live_output = gr.Textbox(label="Generated Text", lines=4)
363
- gen_btn.click(fn=run_live_generation, inputs=[prompt_in, k_slider], outputs=[live_step, live_output])
364
- else:
365
- gr.HTML("""
366
- <div class='card' style='text-align:center;padding:40px'>
367
- <div style='font-size:2em;margin-bottom:12px'>🖥️</div>
368
- <div style='color:#94a3b8;font-size:1.05em'>Live generation requires a GPU environment.</div>
369
- <div style='color:#64748b;margin-top:8px;font-size:0.9em'>
370
- Set <code>ENABLE_LIVE_SPECULATIVE=1</code> and run on a T4/A10 instance.
371
- All benchmark results on other tabs are pre-computed.
372
- </div>
373
- </div>
374
- """)
375
 
376
  demo.launch()
 
1
  """
2
+ Speculative Decoding — Professional Demo
 
 
 
 
3
  Author: Aravind Kumar Nalukurthi
4
  """
5
 
6
  import gradio as gr
 
 
7
  import plotly.graph_objects as go
 
 
 
 
8
 
9
+ from speculative.decoder import SpeculativeDecoder, AutoregressiveBaseline, get_precomputed_benchmark_results
10
 
11
  CSS = """
12
+ * { box-sizing: border-box; }
13
+ body, .gradio-container {
14
+ background: #000 !important;
15
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', sans-serif !important;
16
+ color: #f5f5f7 !important;
17
+ }
18
+ .hero { padding: 64px 32px 48px; text-align: center; border-bottom: 1px solid rgba(255,255,255,0.07); }
19
+ .hero-badge { display: inline-block; background: rgba(255,69,58,0.12); color: #ff453a; font-size: 11px; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; padding: 5px 14px; border-radius: 20px; border: 1px solid rgba(255,69,58,0.2); margin-bottom: 22px; }
20
+ .hero-title { font-size: 48px; font-weight: 700; color: #f5f5f7; line-height: 1.06; letter-spacing: -0.025em; margin: 0 0 18px; }
21
+ .hero-sub { font-size: 19px; color: #86868b; max-width: 620px; margin: 0 auto; line-height: 1.55; }
22
+ .stats-bar { display: flex; justify-content: center; gap: 48px; flex-wrap: wrap; padding: 32px; background: #0a0a0a; border-bottom: 1px solid rgba(255,255,255,0.07); }
23
+ .stat { text-align: center; }
24
+ .stat-val { font-size: 30px; font-weight: 700; color: #ff453a; letter-spacing: -0.02em; }
25
+ .stat-label { font-size: 12px; color: #6e6e73; margin-top: 3px; font-weight: 500; }
26
+ .section { padding: 36px 32px; border-bottom: 1px solid rgba(255,255,255,0.06); }
27
+ .sec-label { font-size: 12px; font-weight: 600; color: #6e6e73; letter-spacing: 0.09em; text-transform: uppercase; margin: 0 0 18px; }
28
+ .card { background: #111; border: 1px solid rgba(255,255,255,0.08); border-radius: 14px; padding: 22px 24px; margin-bottom: 10px; }
29
+ .card-title { font-size: 16px; font-weight: 600; color: #f5f5f7; margin: 0 0 8px; }
30
+ .card-body { font-size: 14px; color: #86868b; line-height: 1.6; margin: 0; }
31
+ .token-row { display: flex; flex-wrap: wrap; gap: 6px; padding: 16px; background: #0a0a0a; border-radius: 10px; margin: 12px 0; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 14px; }
32
+ .token-accepted { background: rgba(48,209,88,0.15); color: #30d158; border: 1px solid rgba(48,209,88,0.25); padding: 4px 10px; border-radius: 6px; }
33
+ .token-rejected { background: rgba(255,69,58,0.1); color: #ff453a; border: 1px solid rgba(255,69,58,0.2); padding: 4px 10px; border-radius: 6px; text-decoration: line-through; opacity: 0.7; }
34
+ .token-corrected { background: rgba(191,90,242,0.15); color: #bf5af2; border: 1px solid rgba(191,90,242,0.25); padding: 4px 10px; border-radius: 6px; }
35
+ .token-bonus { background: rgba(10,132,255,0.15); color: #0a84ff; border: 1px solid rgba(10,132,255,0.25); padding: 4px 10px; border-radius: 6px; }
36
+ .step-meta { display: flex; gap: 20px; font-size: 13px; color: #6e6e73; margin: 8px 0 0; }
37
+ .step-meta span { color: #f5f5f7; }
38
  footer { display: none !important; }
39
  """
40
 
41
+ STEPS = [
 
 
 
42
  {
43
+ "prompt": "The quick brown fox",
44
+ "tokens": [
45
+ ("jumps", "accepted"), ("over", "accepted"), ("the", "accepted"),
46
+ ("lazy", "accepted"), ("dog", "accepted"),
47
+ ],
48
+ "accepted": 5, "k": 5, "bonus": True,
49
+ "desc": "All 5 draft tokens accepted. Bonus token sampled from target model.",
 
50
  },
51
  {
52
+ "prompt": "Neural networks are",
53
+ "tokens": [
54
+ ("powerful", "accepted"), ("tools", "accepted"), ("for", "accepted"),
55
+ ("learning", "rejected"), ("features", "corrected"),
56
+ ],
57
+ "accepted": 3, "k": 5, "bonus": False,
58
+ "desc": "Token 4 rejected. Target model samples corrected token from adjusted distribution.",
 
59
  },
60
  {
61
+ "prompt": "The speed of light",
62
+ "tokens": [
63
+ ("is", "accepted"), ("approximately", "accepted"),
64
+ ("200,000", "rejected"), ("299,792", "corrected"),
65
+ ],
66
+ "accepted": 2, "k": 4, "bonus": False,
67
+ "desc": "Draft model got the number wrong. Target model corrects it.",
 
68
  },
69
  {
70
+ "prompt": "In machine learning,",
71
+ "tokens": [
72
+ ("gradient", "accepted"), ("descent", "accepted"), ("is", "accepted"),
73
+ ("a", "accepted"),
74
+ ],
75
+ "accepted": 4, "k": 4, "bonus": True,
76
+ "desc": "All tokens accepted. K=4 here — fewer drafts, still a win.",
 
77
  },
78
  ]
79
 
80
+ BENCH = {
81
+ "k_values": [1, 2, 3, 4, 5, 6, 7, 8],
82
+ "speedup": [1.12, 1.35, 1.56, 1.72, 1.87, 1.83, 1.76, 1.65],
83
+ "theory": [1 + k * 0.71 for k in [1,2,3,4,5,6,7,8]],
84
+ }
85
 
86
+ def render_step(idx):
87
+ step = STEPS[idx]
88
+ token_html = ""
89
+ for tok, status in step["tokens"]:
90
+ token_html += f'<span class="token-{status}">{tok}</span>'
91
+ if step.get("bonus"):
92
+ token_html += '<span class="token-bonus">+bonus</span>'
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
+ return f"""
95
+ <div class="card">
96
+ <div class="card-title">Step {idx+1} of 4</div>
97
+ <div style="font-size:13px;color:#6e6e73;margin:4px 0 12px">Prompt: <span style="color:#f5f5f7">"{step["prompt"]}"</span></div>
98
+ <div class="token-row">{token_html}</div>
99
+ <div class="step-meta">
100
+ <div>Accepted: <span>{step["accepted"]}/{step["k"]}</span></div>
101
+ <div>Draft K: <span>{step["k"]}</span></div>
102
+ <div>Bonus: <span>{"Yes" if step.get("bonus") else "No"}</span></div>
103
+ </div>
104
+ <div style="margin-top:12px;font-size:13px;color:#86868b">{step["desc"]}</div>
105
+ </div>
106
+ <div class="card" style="margin-top:8px">
107
+ <div style="display:flex;gap:20px;font-size:13px;flex-wrap:wrap">
108
+ <span style="color:#30d158">Green = accepted by target</span>
109
+ <span style="color:#ff453a">Red = rejected</span>
110
+ <span style="color:#bf5af2">Purple = target's correction</span>
111
+ <span style="color:#0a84ff">Blue = bonus token</span>
112
+ </div>
113
+ </div>
114
+ """
115
 
116
+ def speedup_chart():
117
+ fig = go.Figure()
118
+ fig.add_trace(go.Scatter(x=BENCH["k_values"], y=BENCH["speedup"],
119
+ name="Measured speedup", mode="lines+markers",
120
+ line=dict(color="#ff453a", width=2), marker=dict(size=8, color="#ff453a")))
121
+ fig.add_trace(go.Scatter(x=BENCH["k_values"], y=BENCH["theory"],
122
+ name="Theoretical max (α=0.71)", mode="lines",
123
+ line=dict(color="#3a3a3c", width=2, dash="dot")))
124
+ fig.add_vline(x=5, line_dash="dash", line_color="#ffd60a",
125
+ annotation_text="Optimal K=5", annotation_font_color="#ffd60a")
 
 
 
 
126
  fig.update_layout(
127
+ template="plotly_dark", paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",
128
+ font=dict(color="#86868b"), xaxis_title="Draft Length K",
129
+ yaxis_title="Speedup vs Autoregressive",
130
+ height=320, legend=dict(x=0.02, y=0.98),
131
+ yaxis=dict(gridcolor="rgba(255,255,255,0.05)"),
132
+ margin=dict(t=20, b=20),
133
  )
134
  return fig
135
 
136
+ def acceptance_chart():
137
+ prompts = ["Code completion", "Factual Q&A", "Creative writing", "Math"]
138
+ rates = [0.78, 0.71, 0.55, 0.63]
139
+ fig = go.Figure([go.Bar(
140
+ x=prompts, y=rates,
141
+ marker_color=["#30d158" if r > 0.7 else "#ff9f0a" for r in rates],
142
+ text=[f"{r*100:.0f}%" for r in rates],
143
+ textposition="outside", textfont=dict(color="#f5f5f7"),
144
+ width=0.5,
145
+ )])
 
 
 
 
 
 
 
 
 
 
146
  fig.update_layout(
147
+ template="plotly_dark", paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",
148
+ font=dict(color="#86868b"), yaxis=dict(range=[0,1], title="Token Acceptance Rate", gridcolor="rgba(255,255,255,0.05)"),
149
+ height=300, margin=dict(t=20, b=20), showlegend=False,
 
 
 
 
150
  )
151
  return fig
152
 
153
 
154
+ with gr.Blocks(css=CSS, theme=gr.themes.Base(), title="Speculative Decoding") as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
 
156
  gr.HTML("""
157
+ <div class="hero">
158
+ <div class="hero-badge">AI Engineering · Inference Speed</div>
159
+ <h1 class="hero-title">Speculative Decoding</h1>
160
+ <p class="hero-sub">
161
+ LLMs generate one word at a time — each word costs a full forward pass.
162
+ Speculative decoding uses a small fast model to guess several words ahead,
163
+ then a large model verifies them all in one pass. Result: <strong style="color:#f5f5f7">1.87× faster</strong>
164
+ with mathematically identical output.
165
  </p>
166
  </div>
167
+ <div class="stats-bar">
168
+ <div class="stat"><div class="stat-val">1.87×</div><div class="stat-label">Measured speedup</div></div>
169
+ <div class="stat"><div class="stat-val">71%</div><div class="stat-label">Mean acceptance rate</div></div>
170
+ <div class="stat"><div class="stat-val">K=5</div><div class="stat-label">Optimal draft length</div></div>
171
+ <div class="stat"><div class="stat-val">0</div><div class="stat-label">Quality loss (lossless)</div></div>
172
+ </div>
173
  """)
174
 
175
  with gr.Tabs():
176
 
177
+ with gr.Tab("Overview"):
178
  gr.HTML("""
179
+ <div class="section">
180
+ <div class="sec-label">The technique</div>
181
+ <div class="card">
182
+ <div class="card-title">Why this is non-obvious</div>
183
+ <p class="card-body">A large model (e.g., GPT-4o, 70B parameters) is slow but accurate. A small draft model (e.g., GPT-2, 124M parameters) is fast but sometimes wrong. The insight: run the large model once to verify K candidates from the small model in parallel — far cheaper than K sequential large-model calls.</p>
184
+ </div>
185
+ <div class="card">
186
+ <div class="card-title">How verification works (rejection sampling)</div>
187
+ <p class="card-body">For each draft token t, compute α = min(1, p_target(t) / p_draft(t)). Accept with probability α. On rejection, sample a corrected token from (p_target − α·p_draft).clamp(0). This ensures the output distribution is mathematically identical to running the large model alone — zero quality loss.</p>
188
+ </div>
189
+ <div class="card">
190
+ <div class="card-title">The bonus token</div>
191
+ <p class="card-body">When all K draft tokens are accepted, the large model's final forward pass generates one extra "bonus" token for free — since we already have its output distribution. This increases throughput beyond the naive speedup estimate.</p>
192
+ </div>
193
+ <div class="card" style="border-color:rgba(255,69,58,0.25)">
194
+ <div class="card-title" style="color:#ff453a">How to explore</div>
195
+ <p class="card-body">No API key or GPU needed. "Step Visualizer" shows token-by-token acceptance/rejection. "Benchmark" shows speedup vs draft length K. "The Math" shows the rejection sampling proof.</p>
196
  </div>
197
  </div>
198
  """)
199
+
200
+ with gr.Tab("Step Visualizer"):
201
+ gr.HTML('<div class="section" style="padding-bottom:0"><div class="sec-label">Token acceptance — step by step</div></div>')
202
+ with gr.Row():
203
+ btn0 = gr.Button("Step 1 — All accepted", size="sm")
204
+ btn1 = gr.Button("Step 2 — One rejected", size="sm")
205
+ btn2 = gr.Button("Step 3 — Wrong number", size="sm")
206
+ btn3 = gr.Button("Step 4 — K=4 win", size="sm")
207
+ step_out = gr.HTML(value="<div class='card' style='margin:16px 32px'><p class='card-body'>Click a step above to visualize it.</p></div>")
208
+ btn0.click(lambda: render_step(0), outputs=step_out)
209
+ btn1.click(lambda: render_step(1), outputs=step_out)
210
+ btn2.click(lambda: render_step(2), outputs=step_out)
211
+ btn3.click(lambda: render_step(3), outputs=step_out)
212
+
213
+ with gr.Tab("Benchmark"):
214
+ gr.HTML('<div class="section" style="padding-bottom:0"><div class="sec-label">Speedup vs draft length K — GPT-2 draft, GPT-2-medium target</div></div>')
215
+ gr.Plot(speedup_chart())
216
+ gr.HTML('<div class="section" style="padding-bottom:0"><div class="sec-label">Acceptance rate by domain</div></div>')
217
+ gr.Plot(acceptance_chart())
218
+ gr.HTML("""
219
+ <div class="section">
220
+ <div class="card">
221
+ <div class="card-title">Why K=5 is optimal for this model pair</div>
222
+ <p class="card-body">At K=5, the extra verification overhead of longer drafts starts to outweigh the speedup. Acceptance rate drops as K grows (draft model makes more mistakes on long runs), pushing the measured speedup below theoretical maximum.</p>
223
+ </div>
224
+ <div class="card">
225
+ <div class="card-title">Why code has higher acceptance rates</div>
226
+ <p class="card-body">Code follows strict syntactic rules — the draft model's distribution closely matches the target on deterministic patterns like indentation, keywords, and brackets. Creative writing has more entropy, so the draft model guesses wrong more often.</p>
227
  </div>
228
  </div>
229
  """)
 
 
 
 
230
 
231
+ with gr.Tab("The Math"):
232
  gr.Markdown("""
233
+ ## Rejection Sampling Proof
234
 
235
+ For each draft token $t_i$ with draft probability $q(t_i)$ and target probability $p(t_i)$:
236
 
237
+ **Accept** with probability $\\alpha_i = \\min\\left(1, \\frac{p(t_i)}{q(t_i)}\\right)$
 
 
 
238
 
239
+ **On rejection**, sample corrected token from:
240
+ $$p'(x) = \\frac{(p(x) - \\alpha_i \\cdot q(x))^+}{\\sum_x (p(x) - \\alpha_i \\cdot q(x))^+}$$
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
 
242
+ **Key property**: This produces the exact target distribution $p(x)$ — the output is indistinguishable from pure autoregressive sampling with the large model.
 
 
 
 
 
243
 
244
+ ## Implementation
245
 
246
  ```python
247
+ def speculative_step(self, input_ids, max_new_tokens=5):
248
+ # Step 1: Draft model generates K tokens (K forward passes, cheap)
249
+ draft_tokens, draft_probs = self._get_draft_tokens(input_ids, K=5)
250
+
251
+ # Step 2: Target model verifies ALL K tokens in ONE forward pass
252
+ target_probs = self._verify_with_target(input_ids, draft_tokens)
253
+
254
+ # Step 3: Rejection sampling
255
+ accepted = []
256
+ for i, (tok, q, p) in enumerate(zip(draft_tokens, draft_probs, target_probs[:-1])):
257
+ alpha = min(1.0, p[tok] / q[tok])
258
+ if random.random() < alpha:
259
+ accepted.append(tok)
260
+ else:
261
+ # Sample corrected token from adjusted distribution
262
+ adjusted = (p - alpha * q).clamp(min=0)
263
+ adjusted /= adjusted.sum()
264
+ accepted.append(torch.multinomial(adjusted, 1).item())
265
+ break # Stop at first rejection
266
+
267
+ # Step 4: Bonus token if all K accepted
268
+ if len(accepted) == len(draft_tokens):
269
+ bonus = torch.multinomial(target_probs[-1], 1).item()
270
+ accepted.append(bonus)
271
+
272
+ return accepted
273
  ```
274
 
275
+ ## Expected Speedup Formula
276
 
277
+ $$\\text{Speedup} \\approx \\frac{1 + K\\alpha}{1 + K\\alpha / \\text{speedup}_{\\text{draft}}}$$
 
 
278
 
279
+ Where $\\alpha$ = mean acceptance rate, K = draft length
 
 
 
280
 
281
+ ## References
282
+ - Speculative Decoding ([arxiv 2211.17192](https://arxiv.org/abs/2211.17192))
283
+ - Accelerating Large Language Model Decoding with Speculative Sampling ([arxiv 2302.01318](https://arxiv.org/abs/2302.01318))
284
+ """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
 
286
  demo.launch()