Speculative-Decoding / experiments /run_experiments.py
ChandraPrakashBathula's picture
Add reproducibility harness and fix UI text bugs found in screenshots
c6b6889
Raw History Blame Contribute Delete
5.84 kB
"""
Headless experiment harness for the speculative decoding laboratory.
Runs a matrix of (prompt x speculation length x repeat), records every raw
measurement to JSON, and never aggregates anything here: aggregation lives in
analyze.py so that the raw records stay auditable and any published number can
be traced back to the run that produced it.
Usage:
python experiments/run_experiments.py # full matrix
python experiments/run_experiments.py --quick # small smoke matrix
python experiments/run_experiments.py --max-new-tokens 96 --repeats 5
Run this on the machine whose numbers you intend to publish. The draft/target
cost ratio c is a property of the hardware, not of the model pair, so results
from one machine do not transfer to another.
"""
import argparse
import json
import platform
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT))
import torch # noqa: E402
from backend.main import ( # noqa: E402
DEVICE,
DTYPE,
DRAFT_MODEL_NAME,
TARGET_MODEL_NAME,
load_models,
run_baseline,
run_speculative,
)
RESULTS_DIR = Path(__file__).resolve().parent / "results"
# Prompts deliberately span genres, because acceptance rate tracks how
# predictable the continuation is: boilerplate definitions are easy for a small
# draft model, open-ended reasoning is not.
PROMPTS = {
"definition": "Explain what a hash map is and give a short example.",
"code": "Write a Python function that reverses a linked list.",
"explanation": "Summarise why transformers replaced RNNs for sequence modelling.",
"list": "List five practical uses of binary search trees.",
"reasoning": "A train leaves at 3pm going 60mph and another at 4pm going 80mph. When does the second catch the first? Reason step by step.",
"creative": "Write an opening paragraph for a story about a lighthouse keeper.",
}
K_VALUES = [1, 2, 3, 4, 6, 8]
def build_record(kind, prompt_id, prompt, k, repeat, payload):
"""Strip the generated text down to a hash plus a preview.
Full text is kept only for the equality check between baseline and
speculative; storing every full continuation would bloat the records file
without adding evidence.
"""
record = {
"kind": kind,
"prompt_id": prompt_id,
"k": k,
"repeat": repeat,
"total_time_ms": payload["total_time_ms"],
"tokens_generated": payload["tokens_generated"],
"target_calls": payload["target_calls"],
"text": payload["text"],
"rounds": payload.get("rounds", []),
}
for key in ("alpha_hat", "c_hat", "predicted_speedup", "draft_time_ms", "target_time_ms", "gamma"):
if key in payload:
record[key] = payload[key]
return record
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--repeats", type=int, default=3, help="timed repeats per cell")
parser.add_argument("--max-new-tokens", type=int, default=64)
parser.add_argument("--quick", action="store_true", help="2 prompts, K in {1,4}, 1 repeat")
parser.add_argument("--out", default=str(RESULTS_DIR / "records.json"))
args = parser.parse_args()
prompts = dict(PROMPTS)
k_values = list(K_VALUES)
repeats = args.repeats
if args.quick:
prompts = {k: prompts[k] for k in list(prompts)[:2]}
k_values = [1, 4]
repeats = 1
print(f"loading models on {DEVICE} ({DTYPE}) ...", flush=True)
load_models()
# One warm-up generation, discarded. The first call pays for lazy CUDA/MPS
# kernel compilation and allocator warm-up, which would otherwise be
# charged to whichever cell happened to run first.
print("warm-up run (discarded) ...", flush=True)
run_speculative(next(iter(prompts.values())), 4, 16)
run_baseline(next(iter(prompts.values())), 16)
records = []
total_cells = len(prompts) * (1 + len(k_values)) * repeats
done = 0
started = time.time()
for prompt_id, prompt in prompts.items():
for repeat in range(repeats):
payload = run_baseline(prompt, args.max_new_tokens)
records.append(build_record("baseline", prompt_id, prompt, None, repeat, payload))
done += 1
print(f"[{done}/{total_cells}] baseline {prompt_id} r{repeat} "
f"{payload['total_time_ms']:.0f}ms", flush=True)
for k in k_values:
payload = run_speculative(prompt, k, args.max_new_tokens)
records.append(build_record("speculative", prompt_id, prompt, k, repeat, payload))
done += 1
print(f"[{done}/{total_cells}] spec {prompt_id} K={k} r{repeat} "
f"{payload['total_time_ms']:.0f}ms "
f"alpha={payload.get('alpha_hat')} c={payload.get('c_hat')}", flush=True)
out_path = Path(args.out)
out_path.parent.mkdir(parents=True, exist_ok=True)
bundle = {
"meta": {
"created_utc": datetime.now(timezone.utc).isoformat(),
"device": DEVICE,
"dtype": str(DTYPE),
"draft_model": DRAFT_MODEL_NAME,
"target_model": TARGET_MODEL_NAME,
"max_new_tokens": args.max_new_tokens,
"repeats": repeats,
"k_values": k_values,
"prompt_ids": list(prompts),
"prompts": prompts,
"torch": torch.__version__,
"platform": platform.platform(),
"wall_clock_s": round(time.time() - started, 1),
},
"records": records,
}
out_path.write_text(json.dumps(bundle, indent=2))
print(f"\nwrote {len(records)} records to {out_path}")
if __name__ == "__main__":
main()