ynFeng's picture
ASD: Approximate Speculative Decoding custom generation method
9c0a4e6 verified
Raw History Blame Contribute Delete
6.25 kB
"""Comprehensive GPU validation for the ASD custom generation method.
Covers:
1. Native strict baseline vs ASD(B=0) -> must be token-identical (gold check)
2. Native strict vs custom (no knobs) -> must be token-identical (lossless default)
3. ASD(B=2.0) relaxed -> runs, deterministic, >= strict acceptance
4. Determinism -> two identical relaxed runs, token-identical
5. Error handling -> do_sample / ensemble_weight / no draft source
6. Throughput + acceptance comparison -> strict vs relaxed
"""
import os
import sys
import time
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
REPO = os.environ.get("ASD_REPO", os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
TARGET = os.environ.get("ASD_TARGET", "Qwen/Qwen2.5-1.5B-Instruct")
DRAFT = os.environ.get("ASD_DRAFT", "Qwen/Qwen2.5-0.5B-Instruct")
MAX_NEW = 128
PROMPTS = [
"Give me a short introduction to large language models.",
"What is the capital of France? Answer in one sentence.",
"Write a Python function that computes the nth Fibonacci number.",
"Explain the difference between TCP and UDP briefly.",
"List three prime numbers greater than 10 and justify one of them.",
]
failures = []
def check(name, cond, detail=""):
status = "PASS" if cond else "FAIL"
print(f"[{status}] {name}" + (f" -- {detail}" if detail else ""))
if not cond:
failures.append(name)
def count_fwd(tok, out, prompt_len):
return out.shape[1] - prompt_len
def main():
tokenizer = AutoTokenizer.from_pretrained(TARGET)
model = AutoModelForCausalLM.from_pretrained(TARGET, dtype="auto", device_map="auto")
assistant = AutoModelForCausalLM.from_pretrained(DRAFT, dtype="auto", device_map="auto")
model.eval()
assistant.eval()
def run(prompt, **extra):
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
t0 = time.perf_counter()
out = model.generate(
**inputs, max_new_tokens=MAX_NEW, do_sample=False, **extra
)
dt = time.perf_counter() - t0
n = out.shape[1] - inputs["input_ids"].shape[1]
return out, n / dt
# ---------- 1&2. gold checks ----------
for prompt in PROMPTS:
strict, _ = run(prompt, assistant_model=assistant)
b0, _ = run(
prompt,
assistant_model=assistant,
custom_generate=REPO,
trust_remote_code=True,
assistant_asd_budget=0.0,
)
noknobs, _ = run(
prompt,
assistant_model=assistant,
custom_generate=REPO,
trust_remote_code=True,
)
check(f"gold B=0 identical ({prompt[:25]!r})", torch.equal(strict, b0))
check(f"gold no-knobs identical ({prompt[:25]!r})", torch.equal(strict, noknobs))
# ---------- 3&4. relaxed run + determinism ----------
for prompt in PROMPTS:
relaxed1, tps1 = run(
prompt,
assistant_model=assistant,
custom_generate=REPO,
trust_remote_code=True,
assistant_asd_budget=2.0,
assistant_asd_local_ratio=0.25,
assistant_asd_max_mismatches=2,
)
relaxed2, _ = run(
prompt,
assistant_model=assistant,
custom_generate=REPO,
trust_remote_code=True,
assistant_asd_budget=2.0,
assistant_asd_local_ratio=0.25,
assistant_asd_max_mismatches=2,
)
check(f"determinism ({prompt[:25]!r})", torch.equal(relaxed1, relaxed2))
check(f"relaxed non-empty ({prompt[:25]!r})", relaxed1.shape[1] > 0)
# ---------- 5. error handling ----------
inputs = tokenizer(PROMPTS[0], return_tensors="pt").to(model.device)
def expect_raise(name, **kw):
try:
model.generate(**inputs, max_new_tokens=8, **kw)
except ValueError:
check(f"raises {name}", True)
except Exception as e: # noqa: BLE001
check(f"raises {name}", False, f"wrong exception: {type(e).__name__}: {e}")
else:
check(f"raises {name}", False, "no exception")
expect_raise(
"do_sample=True",
assistant_model=assistant,
custom_generate=REPO,
trust_remote_code=True,
do_sample=True,
assistant_asd_budget=2.0,
)
expect_raise(
"ensemble_weight conflict",
assistant_model=assistant,
custom_generate=REPO,
trust_remote_code=True,
assistant_ensemble_weight=0.5,
assistant_asd_budget=2.0,
)
expect_raise(
"knobs without draft source",
custom_generate=REPO,
trust_remote_code=True,
assistant_asd_budget=2.0,
)
expect_raise(
"negative budget",
assistant_model=assistant,
custom_generate=REPO,
trust_remote_code=True,
assistant_asd_budget=-1.0,
)
# ---------- 6. throughput + acceptance ----------
print("\n=== throughput comparison ===")
tot_strict_tps = tot_relaxed_tps = 0.0
for prompt in PROMPTS:
strict, s_tps = run(prompt, assistant_model=assistant)
relaxed, r_tps = run(
prompt,
assistant_model=assistant,
custom_generate=REPO,
trust_remote_code=True,
assistant_asd_budget=2.0,
assistant_asd_local_ratio=0.25,
assistant_asd_max_mismatches=2,
)
tot_strict_tps += s_tps
tot_relaxed_tps += r_tps
overlap = sum(
1 for a, b in zip(strict[0].tolist(), relaxed[0].tolist()) if a == b
) / max(len(strict[0]), len(relaxed[0]))
print(
f" strict {s_tps:6.2f} tok/s | relaxed {r_tps:6.2f} tok/s "
f"({(r_tps / s_tps - 1) * 100:+5.1f}%) | prefix overlap {overlap:.2%}"
)
print(
f" MEAN strict {tot_strict_tps / len(PROMPTS):.2f} tok/s | "
f"relaxed {tot_relaxed_tps / len(PROMPTS):.2f} tok/s"
)
print("\n" + ("ALL PASS" if not failures else f"FAILURES: {failures}"))
sys.exit(1 if failures else 0)
if __name__ == "__main__":
main()