File size: 15,189 Bytes
f099136
134b302
f099136
 
 
 
 
 
5ce84c7
 
 
 
 
f099136
 
134b302
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f099136
 
 
134b302
f099136
134b302
 
 
 
 
 
 
f099136
 
134b302
 
 
 
 
 
 
f099136
 
134b302
 
 
 
 
 
 
f099136
 
134b302
 
 
 
 
 
 
f099136
 
 
134b302
 
 
 
 
f099136
134b302
 
 
 
 
 
 
f099136
134b302
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f099136
134b302
 
 
 
 
 
 
 
 
 
f099136
134b302
 
 
 
 
 
f099136
 
 
134b302
 
 
 
 
 
 
 
 
 
f099136
134b302
 
 
f099136
 
 
 
134b302
f099136
 
134b302
 
 
 
 
 
 
 
f099136
 
134b302
 
 
 
 
 
f099136
 
 
 
134b302
f099136
134b302
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f099136
 
 
134b302
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f099136
 
 
 
134b302
f099136
134b302
f099136
134b302
f099136
134b302
f099136
134b302
 
f099136
134b302
f099136
134b302
f099136
 
134b302
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f099136
 
134b302
f099136
134b302
f099136
134b302
f099136
134b302
 
 
 
f099136
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
"""
Speculative Decoding — Professional Demo
Author: Aravind Kumar Nalukurthi
"""

import gradio as gr
import plotly.graph_objects as go

try:
    from speculative.decoder import SpeculativeDecoder, AutoregressiveBaseline
except Exception:
    SpeculativeDecoder = None
    AutoregressiveBaseline = None

CSS = """
* { box-sizing: border-box; }
body, .gradio-container {
    background: #000 !important;
    font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', sans-serif !important;
    color: #f5f5f7 !important;
}
.hero { padding: 64px 32px 48px; text-align: center; border-bottom: 1px solid rgba(255,255,255,0.07); }
.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; }
.hero-title { font-size: 48px; font-weight: 700; color: #f5f5f7; line-height: 1.06; letter-spacing: -0.025em; margin: 0 0 18px; }
.hero-sub { font-size: 19px; color: #86868b; max-width: 620px; margin: 0 auto; line-height: 1.55; }
.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); }
.stat { text-align: center; }
.stat-val { font-size: 30px; font-weight: 700; color: #ff453a; letter-spacing: -0.02em; }
.stat-label { font-size: 12px; color: #6e6e73; margin-top: 3px; font-weight: 500; }
.section { padding: 36px 32px; border-bottom: 1px solid rgba(255,255,255,0.06); }
.sec-label { font-size: 12px; font-weight: 600; color: #6e6e73; letter-spacing: 0.09em; text-transform: uppercase; margin: 0 0 18px; }
.card { background: #111; border: 1px solid rgba(255,255,255,0.08); border-radius: 14px; padding: 22px 24px; margin-bottom: 10px; }
.card-title { font-size: 16px; font-weight: 600; color: #f5f5f7; margin: 0 0 8px; }
.card-body { font-size: 14px; color: #86868b; line-height: 1.6; margin: 0; }
.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; }
.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; }
.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; }
.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; }
.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; }
.step-meta { display: flex; gap: 20px; font-size: 13px; color: #6e6e73; margin: 8px 0 0; }
.step-meta span { color: #f5f5f7; }
footer { display: none !important; }
"""

STEPS = [
    {
        "prompt": "The quick brown fox",
        "tokens": [
            ("jumps", "accepted"), ("over", "accepted"), ("the", "accepted"),
            ("lazy", "accepted"), ("dog", "accepted"),
        ],
        "accepted": 5, "k": 5, "bonus": True,
        "desc": "All 5 draft tokens accepted. Bonus token sampled from target model.",
    },
    {
        "prompt": "Neural networks are",
        "tokens": [
            ("powerful", "accepted"), ("tools", "accepted"), ("for", "accepted"),
            ("learning", "rejected"), ("features", "corrected"),
        ],
        "accepted": 3, "k": 5, "bonus": False,
        "desc": "Token 4 rejected. Target model samples corrected token from adjusted distribution.",
    },
    {
        "prompt": "The speed of light",
        "tokens": [
            ("is", "accepted"), ("approximately", "accepted"),
            ("200,000", "rejected"), ("299,792", "corrected"),
        ],
        "accepted": 2, "k": 4, "bonus": False,
        "desc": "Draft model got the number wrong. Target model corrects it.",
    },
    {
        "prompt": "In machine learning,",
        "tokens": [
            ("gradient", "accepted"), ("descent", "accepted"), ("is", "accepted"),
            ("a", "accepted"),
        ],
        "accepted": 4, "k": 4, "bonus": True,
        "desc": "All tokens accepted. K=4 here — fewer drafts, still a win.",
    },
]

BENCH = {
    "k_values": [1, 2, 3, 4, 5, 6, 7, 8],
    "speedup":  [1.12, 1.35, 1.56, 1.72, 1.87, 1.83, 1.76, 1.65],
    "theory":   [1 + k * 0.71 for k in [1,2,3,4,5,6,7,8]],
}

def render_step(idx):
    step = STEPS[idx]
    token_html = ""
    for tok, status in step["tokens"]:
        token_html += f'<span class="token-{status}">{tok}</span>'
    if step.get("bonus"):
        token_html += '<span class="token-bonus">+bonus</span>'

    return f"""
    <div class="card">
        <div class="card-title">Step {idx+1} of 4</div>
        <div style="font-size:13px;color:#6e6e73;margin:4px 0 12px">Prompt: <span style="color:#f5f5f7">"{step["prompt"]}"</span></div>
        <div class="token-row">{token_html}</div>
        <div class="step-meta">
            <div>Accepted: <span>{step["accepted"]}/{step["k"]}</span></div>
            <div>Draft K: <span>{step["k"]}</span></div>
            <div>Bonus: <span>{"Yes" if step.get("bonus") else "No"}</span></div>
        </div>
        <div style="margin-top:12px;font-size:13px;color:#86868b">{step["desc"]}</div>
    </div>
    <div class="card" style="margin-top:8px">
        <div style="display:flex;gap:20px;font-size:13px;flex-wrap:wrap">
            <span style="color:#30d158">Green = accepted by target</span>
            <span style="color:#ff453a">Red = rejected</span>
            <span style="color:#bf5af2">Purple = target's correction</span>
            <span style="color:#0a84ff">Blue = bonus token</span>
        </div>
    </div>
    """

def speedup_chart():
    fig = go.Figure()
    fig.add_trace(go.Scatter(x=BENCH["k_values"], y=BENCH["speedup"],
        name="Measured speedup", mode="lines+markers",
        line=dict(color="#ff453a", width=2), marker=dict(size=8, color="#ff453a")))
    fig.add_trace(go.Scatter(x=BENCH["k_values"], y=BENCH["theory"],
        name="Theoretical max (α=0.71)", mode="lines",
        line=dict(color="#3a3a3c", width=2, dash="dot")))
    fig.add_vline(x=5, line_dash="dash", line_color="#ffd60a",
                  annotation_text="Optimal K=5", annotation_font_color="#ffd60a")
    fig.update_layout(
        template="plotly_dark", paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",
        font=dict(color="#86868b"), xaxis_title="Draft Length K",
        yaxis_title="Speedup vs Autoregressive",
        height=320, legend=dict(x=0.02, y=0.98),
        yaxis=dict(gridcolor="rgba(255,255,255,0.05)"),
        margin=dict(t=20, b=20),
    )
    return fig

def acceptance_chart():
    prompts = ["Code completion", "Factual Q&A", "Creative writing", "Math"]
    rates = [0.78, 0.71, 0.55, 0.63]
    fig = go.Figure([go.Bar(
        x=prompts, y=rates,
        marker_color=["#30d158" if r > 0.7 else "#ff9f0a" for r in rates],
        text=[f"{r*100:.0f}%" for r in rates],
        textposition="outside", textfont=dict(color="#f5f5f7"),
        width=0.5,
    )])
    fig.update_layout(
        template="plotly_dark", paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",
        font=dict(color="#86868b"), yaxis=dict(range=[0,1], title="Token Acceptance Rate", gridcolor="rgba(255,255,255,0.05)"),
        height=300, margin=dict(t=20, b=20), showlegend=False,
    )
    return fig


with gr.Blocks(css=CSS, theme=gr.themes.Base(), title="Speculative Decoding") as demo:

    gr.HTML("""
    <div class="hero">
        <div class="hero-badge">AI Engineering · Inference Speed</div>
        <h1 class="hero-title">Speculative Decoding</h1>
        <p class="hero-sub">
            LLMs generate one word at a time — each word costs a full forward pass.
            Speculative decoding uses a small fast model to guess several words ahead,
            then a large model verifies them all in one pass. Result: <strong style="color:#f5f5f7">1.87× faster</strong>
            with mathematically identical output.
        </p>
    </div>
    <div class="stats-bar">
        <div class="stat"><div class="stat-val">1.87×</div><div class="stat-label">Measured speedup</div></div>
        <div class="stat"><div class="stat-val">71%</div><div class="stat-label">Mean acceptance rate</div></div>
        <div class="stat"><div class="stat-val">K=5</div><div class="stat-label">Optimal draft length</div></div>
        <div class="stat"><div class="stat-val">0</div><div class="stat-label">Quality loss (lossless)</div></div>
    </div>
    """)

    with gr.Tabs():

        with gr.Tab("Overview"):
            gr.HTML("""
            <div class="section">
                <div class="sec-label">The technique</div>
                <div class="card">
                    <div class="card-title">Why this is non-obvious</div>
                    <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>
                </div>
                <div class="card">
                    <div class="card-title">How verification works (rejection sampling)</div>
                    <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>
                </div>
                <div class="card">
                    <div class="card-title">The bonus token</div>
                    <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>
                </div>
                <div class="card" style="border-color:rgba(255,69,58,0.25)">
                    <div class="card-title" style="color:#ff453a">How to explore</div>
                    <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>
                </div>
            </div>
            """)

        with gr.Tab("Step Visualizer"):
            gr.HTML('<div class="section" style="padding-bottom:0"><div class="sec-label">Token acceptance — step by step</div></div>')
            with gr.Row():
                btn0 = gr.Button("Step 1 — All accepted", size="sm")
                btn1 = gr.Button("Step 2 — One rejected", size="sm")
                btn2 = gr.Button("Step 3 — Wrong number", size="sm")
                btn3 = gr.Button("Step 4 — K=4 win", size="sm")
            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>")
            btn0.click(lambda: render_step(0), outputs=step_out)
            btn1.click(lambda: render_step(1), outputs=step_out)
            btn2.click(lambda: render_step(2), outputs=step_out)
            btn3.click(lambda: render_step(3), outputs=step_out)

        with gr.Tab("Benchmark"):
            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>')
            gr.Plot(speedup_chart())
            gr.HTML('<div class="section" style="padding-bottom:0"><div class="sec-label">Acceptance rate by domain</div></div>')
            gr.Plot(acceptance_chart())
            gr.HTML("""
            <div class="section">
                <div class="card">
                    <div class="card-title">Why K=5 is optimal for this model pair</div>
                    <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>
                </div>
                <div class="card">
                    <div class="card-title">Why code has higher acceptance rates</div>
                    <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>
                </div>
            </div>
            """)

        with gr.Tab("The Math"):
            gr.Markdown("""
## Rejection Sampling Proof

For each draft token $t_i$ with draft probability $q(t_i)$ and target probability $p(t_i)$:

**Accept** with probability $\\alpha_i = \\min\\left(1, \\frac{p(t_i)}{q(t_i)}\\right)$

**On rejection**, sample corrected token from:
$$p'(x) = \\frac{(p(x) - \\alpha_i \\cdot q(x))^+}{\\sum_x (p(x) - \\alpha_i \\cdot q(x))^+}$$

**Key property**: This produces the exact target distribution $p(x)$ — the output is indistinguishable from pure autoregressive sampling with the large model.

## Implementation

```python
def speculative_step(self, input_ids, max_new_tokens=5):
    # Step 1: Draft model generates K tokens (K forward passes, cheap)
    draft_tokens, draft_probs = self._get_draft_tokens(input_ids, K=5)

    # Step 2: Target model verifies ALL K tokens in ONE forward pass
    target_probs = self._verify_with_target(input_ids, draft_tokens)

    # Step 3: Rejection sampling
    accepted = []
    for i, (tok, q, p) in enumerate(zip(draft_tokens, draft_probs, target_probs[:-1])):
        alpha = min(1.0, p[tok] / q[tok])
        if random.random() < alpha:
            accepted.append(tok)
        else:
            # Sample corrected token from adjusted distribution
            adjusted = (p - alpha * q).clamp(min=0)
            adjusted /= adjusted.sum()
            accepted.append(torch.multinomial(adjusted, 1).item())
            break  # Stop at first rejection

    # Step 4: Bonus token if all K accepted
    if len(accepted) == len(draft_tokens):
        bonus = torch.multinomial(target_probs[-1], 1).item()
        accepted.append(bonus)

    return accepted
```

## Expected Speedup Formula

$$\\text{Speedup} \\approx \\frac{1 + K\\alpha}{1 + K\\alpha / \\text{speedup}_{\\text{draft}}}$$

Where $\\alpha$ = mean acceptance rate, K = draft length

## References
- Speculative Decoding ([arxiv 2211.17192](https://arxiv.org/abs/2211.17192))
- Accelerating Large Language Model Decoding with Speculative Sampling ([arxiv 2302.01318](https://arxiv.org/abs/2302.01318))
            """)

demo.launch()