Measure speculative decoding: all ngram modes lose to plain decoding
Verifying a K-token draft costs ~K times the CPU expert work, because each token routes to its own 8-of-256 experts and nothing is amortized. The loss is largest on structured code output (-51% for ngram-simple), i.e. exactly where speculation should have won. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Executable
+76
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Measure generation throughput on workloads where ngram speculative decoding
|
||||
can actually pay off: output that largely echoes the prompt (refactor / rewrite /
|
||||
RAG-style restatement), plus a free-form control that it cannot help with."""
|
||||
import json, argparse, urllib.request
|
||||
|
||||
CODE = '''
|
||||
class RingBuffer:
|
||||
def __init__(self, capacity):
|
||||
self.capacity = capacity
|
||||
self.data = [None] * capacity
|
||||
self.head = 0
|
||||
self.tail = 0
|
||||
self.size = 0
|
||||
|
||||
def push(self, item):
|
||||
if self.size == self.capacity:
|
||||
raise BufferError("ring buffer is full")
|
||||
self.data[self.tail] = item
|
||||
self.tail = (self.tail + 1) % self.capacity
|
||||
self.size += 1
|
||||
|
||||
def pop(self):
|
||||
if self.size == 0:
|
||||
raise IndexError("pop from empty ring buffer")
|
||||
item = self.data[self.head]
|
||||
self.data[self.head] = None
|
||||
self.head = (self.head + 1) % self.capacity
|
||||
self.size -= 1
|
||||
return item
|
||||
'''
|
||||
|
||||
WORKLOADS = {
|
||||
# Best case: output is mostly a copy of the input with small edits.
|
||||
"rewrite": "Repeat this Python class verbatim, changing only the class name "
|
||||
"to CircularBuffer. Output only code, no commentary:\n" + CODE,
|
||||
# Realistic coding task: structurally predictable, not a copy.
|
||||
"extend": "Here is a Python class:\n" + CODE +
|
||||
"\nRewrite it with an added peek() method and type hints. Output only code.",
|
||||
# Control: novel prose, nothing for an ngram matcher to hit.
|
||||
"prose": "Write a short original story about a lighthouse keeper who collects storms.",
|
||||
}
|
||||
|
||||
|
||||
def run(port, name, prompt, n_predict, no_think):
|
||||
body = {"messages": [{"role": "user", "content": prompt}],
|
||||
"max_tokens": n_predict, "temperature": 0}
|
||||
if no_think:
|
||||
body["chat_template_kwargs"] = {"enable_thinking": False}
|
||||
req = urllib.request.Request(f"http://localhost:{port}/v1/chat/completions",
|
||||
data=json.dumps(body).encode(),
|
||||
headers={"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=1800) as r:
|
||||
d = json.load(r)
|
||||
t = d.get("timings", {})
|
||||
tps = t.get("predicted_per_second") or 0
|
||||
n = t.get("predicted_n") or 0
|
||||
print(f" {name:<10} {n:>5} tok @ {tps:>6.2f} t/s", flush=True)
|
||||
return tps
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--port", type=int, default=18008)
|
||||
ap.add_argument("--label", default="run")
|
||||
ap.add_argument("--n-predict", type=int, default=400)
|
||||
ap.add_argument("--no-think", action="store_true",
|
||||
help="disable reasoning so the measurement is the answer, not the thinking")
|
||||
a = ap.parse_args()
|
||||
|
||||
print(f"[{a.label}]", flush=True)
|
||||
for name, prompt in WORKLOADS.items():
|
||||
try:
|
||||
run(a.port, name, prompt, a.n_predict, a.no_think)
|
||||
except Exception as e:
|
||||
print(f" {name:<10} FAILED {type(e).__name__}: {e}", flush=True)
|
||||
Reference in New Issue
Block a user