Spaces:
Sleeping
Sleeping
File size: 9,952 Bytes
74679d4 | 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 | """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 = """
<div class="hero">
<h1>DSSD <span class="hero-sub">exact self-speculative decoding</span></h1>
<p>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 <b>exactly</b> what greedy decoding would write, only faster.</p>
<div class="badges"><span>✓ identical to greedy decoding</span><span>no draft model</span>
<span>≈ 280 MB of heads</span></div>
</div>
"""
HOW = """
<div class="how">
<div class="how-step"><b>1 · Draft</b><br>Copy n-grams found earlier in the context<br>+ chain of the
layer-4 head<br>+ its top-4 alternatives at the root</div>
<div class="how-arrow">→</div>
<div class="how-step"><b>2 · Verify</b><br>One forward pass of the full model over the whole tree
(16 rows, row-invariant kernels)</div>
<div class="how-arrow">→</div>
<div class="how-step"><b>3 · Commit</b><br>Longest branch that matches the model's own greedy choice,
plus one token from the model</div>
</div>
<p class="how-note">The exit heads predict over a small, layer-specific vocabulary (≈ 3k tokens at layer 4)
plus a <i>reject</i> 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.</p>
"""
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 = """
<script>
window.dssdReplay = function () {
const holder = document.querySelector('#replay-holder .replay-data');
if (!holder) return;
const data = JSON.parse(holder.dataset.replay);
const el = {dssd: document.querySelector('#dssd-out .dssd-text'), ar: document.querySelector('#ar-out .dssd-text')};
const esc = s => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
for (const k in el) if (el[k]) el[k].innerHTML = '';
const idx = {dssd: 0, ar: 0}, start = performance.now();
function frame(now) {
const t = (now - start) / 1000; let more = false;
for (const k of ['dssd', 'ar']) {
const arr = data[k] || [], box = el[k]; if (!box) continue;
let html = '';
while (idx[k] < arr.length && arr[idx[k]][0] <= t) {
const [, txt, src] = arr[idx[k]++];
html += (k === 'dssd' && src !== 'model') ? `<span class="tok tok-${src}">${esc(txt)}</span>` : esc(txt);
}
if (html) box.insertAdjacentHTML('beforeend', html);
if (idx[k] < arr.length) more = true;
else if (arr.length && !box.dataset.done) {
box.dataset.done = 1;
box.insertAdjacentHTML('beforeend', `<span class="replay-clock">⏱ ${arr[arr.length - 1][0].toFixed(2)} s</span>`);
}
}
if (more) requestAnimationFrame(frame);
else for (const k in el) if (el[k]) delete el[k].dataset.done;
}
requestAnimationFrame(frame);
};
</script>
"""
@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('<div class="panel-title">DSSD</div>')
out_dssd = gr.HTML(elem_id="dssd-out")
with gr.Column():
gr.HTML('<div class="panel-title">Autoregressive (greedy)</div>')
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)
|