"""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 = """
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.
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('