"""Pick a teacher empirically: keep-rate + trace-length-vs-ceiling per model. # after setting TFY_API_KEY in .env: python scripts/teacher_bakeoff.py --models claude-fable-5 gpt-5.6-sol --n 40 For each model it distills N solutions (same problems), grades the boxed answer, and reports: keep-rate (higher = better/cheaper data), median/p90 trace length in STUDENT tokens, and the fraction that would overflow the generator's max_length (dropped). Highest keep-rate that mostly fits the ceiling wins. The V-critique job is easier (label is known from PRM800K), so a lighter/cheaper model usually suffices there regardless of who wins for G. """ import argparse import statistics as stats import sys import time from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) from mathcompose.common.chat import load_tokenizer # noqa: E402 from mathcompose.common.math_grade import extract_last_boxed, grade_answer # noqa: E402 from mathcompose.datagen.gen_generator_data import Problem, synthesize_solution # noqa: E402 from mathcompose.teachers import get_teacher # noqa: E402 def load_problems(source, split, n, problem_field, solution_field): """Stream the source (no full download) and collect the first n problems that have a parseable boxed gold answer.""" from datasets import load_dataset ds = load_dataset(source, split=split, streaming=True) out = [] for row in ds: prob = row.get(problem_field) ans = extract_last_boxed(row.get(solution_field) or "") if prob and ans: out.append(Problem(problem=prob, answer=ans)) if len(out) >= n: break return out def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--models", nargs="+", required=True, help="gateway model slugs to compare") ap.add_argument("--teacher", default="promptlens") ap.add_argument("--n", type=int, default=40) ap.add_argument("--source", default="AI-MO/NuminaMath-CoT") ap.add_argument("--split", default="train") ap.add_argument("--problem-field", default="problem") ap.add_argument("--solution-field", default="solution") ap.add_argument("--max-tokens", type=int, default=2048, help="teacher output cap") ap.add_argument("--ceiling", type=int, default=2048, help="generator max_length; traces over this are dropped") ap.add_argument("--base-id", default="Qwen/Qwen2.5-Math-1.5B-Instruct") args = ap.parse_args() problems = load_problems(args.source, args.split, args.n, args.problem_field, args.solution_field) print(f"loaded {len(problems)} problems from {args.source}\n") tok = load_tokenizer(args.base_id) rows = [] for slug in args.models: teacher = get_teacher(args.teacher, model=slug) kept, lens, t0 = 0, [], time.time() for p in problems: try: sol = synthesize_solution(teacher, p.problem, temperature=0.7, max_tokens=args.max_tokens) except Exception as e: print(f" [{slug}] call failed: {type(e).__name__}: {e}") continue n_tok = len(tok(sol)["input_ids"]) lens.append(n_tok) if grade_answer(extract_last_boxed(sol), p.answer): kept += 1 dt = time.time() - t0 n = len(lens) or 1 rows.append({ "model": slug, "keep_rate": kept / len(problems), "median_tok": int(stats.median(lens)) if lens else 0, "p90_tok": int(sorted(lens)[int(0.9 * (len(lens) - 1))]) if lens else 0, "over_ceiling": sum(1 for x in lens if x > args.ceiling) / n, "sec_per_ex": dt / len(problems), }) print(f"\n{'model':<24}{'keep':>8}{'med_tok':>9}{'p90_tok':>9}{'>ceil':>8}{'s/ex':>8}") print("-" * 66) for r in sorted(rows, key=lambda r: -r["keep_rate"]): print(f"{r['model']:<24}{r['keep_rate']:>7.0%}{r['median_tok']:>9}{r['p90_tok']:>9}" f"{r['over_ceiling']:>7.0%}{r['sec_per_ex']:>7.1f}") print("\nPick the highest keep-rate whose p90_tok comfortably fits the ceiling " f"({args.ceiling}). Set it via TFY_MODEL or --model at build time.") return 0 if __name__ == "__main__": raise SystemExit(main())