Files
2026-07-13 12:09:03 +08:00

185 lines
6.3 KiB
Python

"""Speculative decoding server — draft/verify scheduler scaffold.
The hard architectural primitive is the draft/verify scheduler: a draft
model proposes k candidate tokens; the target model verifies them in one
batched pass; any accepted prefix is committed and the rejected suffix is
resampled from the target. This scaffold implements the scheduler with
synthetic token probabilities so the accept/reject logic and the throughput
math are observable end to end.
Run: python main.py
"""
from __future__ import annotations
import random
from dataclasses import dataclass, field
# ---------------------------------------------------------------------------
# synthetic models -- probability distributions over a tiny vocabulary
# ---------------------------------------------------------------------------
VOCAB = list("abcdefghij")
def softmax_from(seed: int) -> list[float]:
rnd = random.Random(seed)
weights = [rnd.random() for _ in VOCAB]
total = sum(weights)
return [w / total for w in weights]
def sample(dist: list[float], rng: random.Random) -> int:
r = rng.random()
acc = 0.0
for i, p in enumerate(dist):
acc += p
if r <= acc:
return i
return len(dist) - 1
# ---------------------------------------------------------------------------
# target -- the expensive model we are trying to save calls to
# ---------------------------------------------------------------------------
@dataclass
class TargetModel:
calls: int = 0
tokens_verified: int = 0
def distribution(self, ctx_seed: int) -> list[float]:
return softmax_from(ctx_seed * 7 + 13)
def verify(self, draft_tokens: list[int], ctx_seed: int,
rng: random.Random) -> tuple[list[int], int]:
"""Return (accepted_tokens, resampled_next). In one target call we can
verify draft_tokens in a batched pass: the target produces a prob per
position; we accept up to the first rejection."""
self.calls += 1
self.tokens_verified += len(draft_tokens) + 1
accepted: list[int] = []
for pos, tok in enumerate(draft_tokens):
dist = self.distribution(ctx_seed + pos)
# simple accept criterion: target prob on this token >= 0.5 * max prob
if dist[tok] >= 0.5 * max(dist):
accepted.append(tok)
else:
break
# resample a next token from the target at the position after the accept
ctx = ctx_seed + len(accepted)
dist = self.distribution(ctx)
next_tok = sample(dist, rng)
return accepted, next_tok
# ---------------------------------------------------------------------------
# draft -- a cheaper model that is mostly aligned with target
# ---------------------------------------------------------------------------
@dataclass
class DraftModel:
calls: int = 0
alignment: float = 0.80 # probability that draft picks what target would
def propose(self, ctx_seed: int, k: int, rng: random.Random,
target: TargetModel) -> list[int]:
self.calls += 1
draft_tokens: list[int] = []
for pos in range(k):
dist = target.distribution(ctx_seed + pos)
# with prob alignment, emit target's best; otherwise sample a neighbour
if rng.random() < self.alignment:
draft_tokens.append(max(range(len(dist)), key=lambda i: dist[i]))
else:
draft_tokens.append(sample(dist, rng))
return draft_tokens
# ---------------------------------------------------------------------------
# decode scheduler -- speculative loop + baseline greedy for comparison
# ---------------------------------------------------------------------------
@dataclass
class Metrics:
generated: int = 0
target_calls: int = 0
draft_calls: int = 0
accepted_sum: int = 0
def acceptance_rate(self, k: int) -> float:
if self.target_calls == 0:
return 0.0
return self.accepted_sum / (self.target_calls * k)
def tokens_per_target_call(self) -> float:
return self.generated / max(1, self.target_calls)
def speculative_decode(n_tokens: int, k: int, rng: random.Random,
target: TargetModel, draft: DraftModel) -> Metrics:
m = Metrics()
ctx_seed = 1
while m.generated < n_tokens:
draft_tokens = draft.propose(ctx_seed, k, rng, target)
m.draft_calls += 1
accepted, next_tok = target.verify(draft_tokens, ctx_seed, rng)
m.target_calls += 1
m.accepted_sum += len(accepted)
for tok in accepted:
m.generated += 1
ctx_seed += 1
if m.generated >= n_tokens:
break
if m.generated < n_tokens:
m.generated += 1 # resampled next_tok
ctx_seed += 1
return m
def baseline_decode(n_tokens: int, rng: random.Random,
target: TargetModel) -> Metrics:
m = Metrics()
ctx_seed = 1
while m.generated < n_tokens:
target.calls += 1
m.target_calls += 1
dist = target.distribution(ctx_seed)
_ = sample(dist, rng)
m.generated += 1
ctx_seed += 1
return m
# ---------------------------------------------------------------------------
# sweep -- compare speedup across k and draft alignment
# ---------------------------------------------------------------------------
def main() -> None:
n_tokens = 500
print(f"=== decode {n_tokens} tokens, compare baseline vs speculative ===")
target = TargetModel()
rng = random.Random(7)
base = baseline_decode(n_tokens, rng, target)
print(f"baseline: {base.target_calls} target calls, "
f"{base.tokens_per_target_call():.2f} tok/call")
for alignment in (0.60, 0.75, 0.90):
for k in (2, 4, 6):
target = TargetModel()
draft = DraftModel(alignment=alignment)
rng = random.Random(7)
m = speculative_decode(n_tokens, k, rng, target, draft)
speedup = base.target_calls / max(1, m.target_calls)
print(f" align={alignment:.2f} k={k} "
f"target_calls={m.target_calls:3d} "
f"acceptance={m.acceptance_rate(k):.2f} "
f"tok/call={m.tokens_per_target_call():.2f} "
f"speedup={speedup:.2f}x")
if __name__ == "__main__":
main()