"""DSSD demo: exact self-speculative decoding with pruned exit heads and context copying. Local UI work without a GPU: DSSD_DEMO_BACKEND=tiny python app.py """ import gradio as gr from demo.backend import MAX_NEW_TOKENS, MODELS, generate_updates, load_models from demo.examples import EXAMPLES from demo.render import legend_html def gpu_seconds(model, domain_label, prompt, max_new, compare): # ZeroGPU refuses a call whose requested duration exceeds the visitor's remaining daily quota # (5 min on free accounts), so ask for what the request needs: engine setup + both decodes. return int(30 + int(max_new) * (0.25 if compare else 0.12)) try: # ZeroGPU on Hugging Face Spaces import spaces GPU = spaces.GPU(duration=gpu_seconds) except ImportError: def GPU(f): return f ENGINES = load_models() DOMAINS = {"Web text": "web", "Math (few-shot)": "gsm8k", "Python code": "code"} HEADER = """

DSSD exact self-speculative decoding

A tiny exit head on layer 4 of the frozen model, plus tokens copied from the context, propose a small tree of draft tokens. The full model checks the whole tree in one forward pass and keeps the longest correct branch. The text is exactly what greedy decoding would write, only faster.

✓ identical to greedy decodingno draft model ≈ 280 MB of heads
""" HOW = """
1 · Draft
Copy n-grams found earlier in the context
+ chain of the layer-4 head
+ its top-4 alternatives at the root
→
2 · Verify
One forward pass of the full model over the whole tree (16 rows, row-invariant kernels)
→
3 · Commit
Longest branch that matches the model's own greedy choice, plus one token from the model

The exit heads predict over a small, layer-specific vocabulary (≈ 3k tokens at layer 4) plus a reject class, about 50× cheaper than a full projection. Speedups depend on the GPU and on whether CUDA graphs are used. The paper's measurements (AMD MI210, greedy, batch 1) are 2.50× / 1.72× / 1.46× on web text, GSM8K and HumanEval for Qwen3-8B-Base, and 2.51× / 1.80× / 1.57× for Llama-3.1-8B.

""" CSS = """ .hero h1 { margin: 0 0 .3rem 0; font-size: 1.9rem; } .hero-sub { font-weight: 400; opacity: .65; font-size: 1.1rem; margin-left: .4rem; } .hero p { margin: .2rem 0 .6rem 0; max-width: 60rem; } .badges span { display: inline-block; padding: .15rem .6rem; margin: 0 .4rem .3rem 0; border-radius: 999px; font-size: .82rem; background: var(--block-background-fill); border: 1px solid var(--border-color-primary); } :root { --tok-copy: #cfe4fb; --tok-head: #ffe1b0; --tok-star: #f3d1ec; --tok-draft: #d9f2d0; } .dark { --tok-copy: #1f4b78; --tok-head: #7a5413; --tok-star: #6b2f60; --tok-draft: #2f5a2a; } .tok { border-radius: 3px; padding: 0 1px; } .tok-copy { background: var(--tok-copy); } .tok-head { background: var(--tok-head); } .tok-star { background: var(--tok-star); box-shadow: inset 0 -2px 0 #b3479b; } .tok-draft { background: var(--tok-draft); } .tok-model { background: transparent; border: 1px dashed var(--border-color-primary); } .legend { display: flex; flex-wrap: wrap; gap: .9rem; font-size: .88rem; margin: .2rem 0; } .legend-item { display: inline-flex; align-items: center; gap: .35rem; } .swatch { font-size: .8rem; padding: 0 .3rem; } .dssd-text { white-space: pre-wrap; overflow-wrap: anywhere; font-family: var(--font-mono); font-size: .9rem; line-height: 1.55; min-height: 14rem; max-height: 30rem; overflow-y: auto; padding: .8rem; border-radius: 8px; background: var(--block-background-fill); border: 1px solid var(--border-color-primary); } .cursor { display: inline-block; width: .5em; height: 1em; background: var(--body-text-color); vertical-align: text-bottom; animation: blink 1s steps(1) infinite; opacity: .6; } @keyframes blink { 50% { opacity: 0; } } .panel-title { font-weight: 600; margin-bottom: .2rem; } .stats { display: flex; flex-wrap: wrap; gap: .6rem; margin: .4rem 0 .6rem 0; } .stat { flex: 1 1 8rem; padding: .6rem .8rem; border-radius: 8px; background: var(--block-background-fill); border: 1px solid var(--border-color-primary); } .stat-value { font-size: 1.35rem; font-weight: 700; } .stat-label { font-size: .78rem; opacity: .7; } .stat-main .stat-value { font-size: 1.9rem; color: var(--color-accent); } .stat-ok .stat-value { color: #1a7f37; } .stat-bad .stat-value { color: #cf222e; } .share-bar { display: flex; height: .7rem; border-radius: 999px; overflow: hidden; border: 1px solid var(--border-color-primary); } .share-bar .tok-model { background: var(--border-color-primary); border: 0; } .share-labels { font-size: .8rem; opacity: .75; margin-top: .25rem; } .progress { font-size: .9rem; opacity: .75; padding: .4rem 0; } .replay-clock { font-size: .78rem; opacity: .7; margin-left: .4rem; } .how { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; } .how-step { flex: 1 1 12rem; padding: .7rem; border-radius: 8px; background: var(--block-background-fill); border: 1px solid var(--border-color-primary); font-size: .88rem; } .how-arrow { font-size: 1.4rem; opacity: .5; } .how-note { font-size: .85rem; opacity: .8; margin-top: .6rem; } @media (max-width: 700px) { .how-arrow { display: none; } } """ # Client-side replay of both generations side by side, at the speed measured on the GPU. HEAD = """ """ @GPU def run(model, domain_label, prompt, max_new, compare): try: for u in generate_updates(ENGINES[model], prompt, DOMAINS[domain_label], max_new, compare): yield u.dssd, u.ar, u.stats, u.replay, gr.update(visible=u.done and compare) except ValueError as e: raise gr.Error(str(e)) def example(domain_label, i): ex = EXAMPLES[DOMAINS[domain_label]] i = (i + 1) % len(ex) return ex[i], i with gr.Blocks(title="DSSD: exact self-speculative decoding") as demo: gr.HTML(HEADER) with gr.Row(equal_height=False): with gr.Column(scale=2): prompt = gr.Textbox(label="Prompt (plain-text completion: these are base models)", lines=6, max_lines=14, value=EXAMPLES["web"][0]) with gr.Row(): ex_btn = gr.Button("Another example", size="sm", variant="secondary") ex_idx = gr.State(0) with gr.Column(scale=1): model = gr.Radio(list(ENGINES), value=next(iter(ENGINES)), label="Model") domain = gr.Radio(list(DOMAINS), value="Web text", label="Domain (calibrated settings and stop rule)") max_new = gr.Slider(16, MAX_NEW_TOKENS, value=160, step=16, label="Max new tokens") compare = gr.Checkbox(value=True, label="Also run plain autoregressive decoding and compare") with gr.Row(): go = gr.Button("Generate", variant="primary") stop = gr.Button("Stop", variant="stop") gr.HTML(legend_html()) with gr.Row(equal_height=True): with gr.Column(): gr.HTML('
DSSD
') out_dssd = gr.HTML(elem_id="dssd-out") with gr.Column(): gr.HTML('
Autoregressive (greedy)
') out_ar = gr.HTML(elem_id="ar-out") stats = gr.HTML() replay_btn = gr.Button("▶ Replay both side by side at the measured speed", visible=False, size="sm") replay = gr.HTML(elem_id="replay-holder", visible=True) with gr.Accordion("How it works", open=False): gr.HTML(HOW) gr.Markdown(f"Heads: {', '.join(f'[{r}](https://huggingface.co/{r})' for r in MODELS.values())}. " "Llama-3.1-8B heads: built with Llama.", elem_classes="footer") gen = go.click(run, [model, domain, prompt, max_new, compare], [out_dssd, out_ar, stats, replay, replay_btn]) stop.click(None, cancels=[gen]) replay_btn.click(None, js="() => { window.dssdReplay(); }") domain.change(lambda d: (EXAMPLES[DOMAINS[d]][0], 0), domain, [prompt, ex_idx]) ex_btn.click(example, [domain, ex_idx], [prompt, ex_idx]) demo.queue(default_concurrency_limit=1, max_size=16) if __name__ == "__main__": demo.launch(theme=gr.themes.Soft(primary_hue="indigo", neutral_hue="slate"), css=CSS, head=HEAD)