Files
zeekay 573311de8f benchmarks: full RouterBench replication with trained checkpoint
Wire --policy checkpoint to the frozen-backbone heads-only format:
- heads-only state_dict load (strict=False), public backbone from base
- batch-embed all prompts (batch 64, fp16 on MPS), cache to out/bench/*.emb.npz
- apply route head, project 28-class catalog onto RouterBench's 11 models via
  documented family/tier bridge (benchmarks/checkpoint_map.yaml)
- load_matrix returns row-aligned prompts so checkpoint scores line up with Q/C
- build_profiles: BENCH_SAMPLE=0 means full suite (fix df.sample(n=0) emptying)

Full run (36,497 ex x 11 = 401,467 rows, 36,481 prompts, M4 Max/MPS, 27.8 min):
oracle AIQ 0.8701, prior 0.7427, interpolation hull 0.7054,
zen-router checkpoint AIQ 0.6248 (NEGATIVE transfer, reported plainly:
0 catalog overlap + 0 training rows for mapped targets).
2026-07-07 18:31:10 -07:00

163 lines
6.3 KiB
Python

"""Convert a public routing-research suite into zen-router profile rows.
A "profile row" is the eval-row format the rest of the repo already speaks
(training/build_dataset.py --evals):
{"prompt", "model", "quality", "cost", "latency_ms", "task"}
One source example (a prompt answered by N models) expands to N rows, one per
model, carrying that model's correctness score (quality), its cost, and the
mapped task. This is the RouterBench / FrugalGPT substrate: per-example,
per-model quality and cost, from which a cost-quality routing curve is measured
(see benchmarks/replicate.py).
Default suite: routerbench (the one `kind: profile` suite). `raw` and
`preference` suites have no per-model scores yet and are refused with a clear
message -- they need generation / a Bradley-Terry reduction first.
Usage:
uv run --with datasets,huggingface_hub,pandas \
python benchmarks/build_profiles.py --suite routerbench --sample 200 \
--out data/profiles-routerbench.jsonl
"""
from __future__ import annotations
import argparse
import json
import math
from pathlib import Path
from typing import Iterable, Iterator
import yaml
ROOT = Path(__file__).resolve().parents[1]
def load_yaml(path: Path) -> dict:
return yaml.safe_load(path.read_text())
def map_task(eval_name: str, task_map: dict[str, str]) -> str:
"""Longest-prefix match of an eval_name to a router task; else 'general'."""
name = (eval_name or "").lower()
for prefix in sorted((k for k in task_map if k), key=len, reverse=True):
if name.startswith(prefix.lower()):
return task_map[prefix]
return task_map.get("", "general")
def _clean(value) -> float | None:
"""Coerce a score/cost cell to float, dropping NaN/None."""
if value is None:
return None
try:
f = float(value)
except (TypeError, ValueError):
return None
return None if math.isnan(f) else f
def rows_from_records(
records: Iterable[dict],
suite: dict,
price_map: dict[str, float] | None = None,
) -> Iterator[dict]:
"""Pure conversion: wide per-model records -> flat profile rows.
Each record is one prompt with, per model M: a bare "<M>" score column, an
optional "<M><cost_suffix>" cost column, and (unused) response column.
Cost is native when present; otherwise priced as tokens x prices.yaml, and
if neither is available the row's cost is 0.0 (documented, not silent-NaN).
"""
sch = suite["schema"]
prompt_field = sch["prompt"]
task_field = sch.get("task")
cost_suffix = sch.get("cost_suffix", "|total_cost")
task_map = suite.get("task_map", {})
model_map = suite.get("model_map", {})
prices = price_map or {}
for rec in records:
prompt = rec.get(prompt_field)
if prompt is None:
continue
prompt = str(prompt)
task = map_task(rec.get(task_field, ""), task_map) if task_field else suite.get("default_task", "general")
for src_model in suite["models"]:
quality = _clean(rec.get(src_model))
if quality is None:
continue # model didn't answer this example
native_cost = _clean(rec.get(src_model + cost_suffix))
if native_cost is not None:
cost = native_cost
elif src_model in prices:
# No token counts in these suites -> price is the fallback rate
# per response; documented in prices.yaml as approximate.
cost = float(prices[src_model])
else:
cost = 0.0
yield {
"prompt": prompt,
"model": model_map.get(src_model, src_model),
"quality": quality,
"cost": cost,
"latency_ms": None, # RouterBench has no latency signal
"task": task,
}
def load_profile_records(suite: dict, variant: str, sample: int | None) -> list[dict]:
"""Download a `profile` suite and return wide per-model records (df rows)."""
import pandas as pd # noqa: PLC0415
from huggingface_hub import hf_hub_download # noqa: PLC0415
fname = suite["files"][variant]
path = hf_hub_download(suite["hf_id"], fname, repo_type="dataset")
df = pd.read_pickle(path)
if sample is not None and sample < len(df):
df = df.sample(n=sample, random_state=0).reset_index(drop=True)
return df.to_dict("records")
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--suite", default="routerbench")
ap.add_argument("--variant", default="0shot", help="profile file variant (routerbench: 0shot|5shot)")
ap.add_argument("--sample", type=int, default=None, help="random N examples (CI-sized runs)")
ap.add_argument("--suites", type=Path, default=ROOT / "benchmarks/suites.yaml")
ap.add_argument("--prices", type=Path, default=ROOT / "benchmarks/prices.yaml")
ap.add_argument("--out", type=Path, default=None)
args = ap.parse_args()
registry = load_yaml(args.suites)["suites"]
if args.suite not in registry:
raise SystemExit(f"unknown suite '{args.suite}'. known: {', '.join(registry)}")
suite = registry[args.suite]
if suite["kind"] != "profile":
raise SystemExit(
f"suite '{args.suite}' is kind={suite['kind']}: no per-model scores to convert. "
"Only `profile` suites (e.g. routerbench) build directly; `raw` needs generation, "
"`preference` needs a Bradley-Terry reduction."
)
price_map = load_yaml(args.prices).get("prices", {}) if args.prices.exists() else {}
# sample <= 0 means "use the whole suite" (0 is the documented full-run value);
# only a positive N subsamples. Guards df.sample(n=0) emptying the frame.
sample = args.sample if args.sample and args.sample > 0 else None
records = load_profile_records(suite, args.variant, sample)
rows = list(rows_from_records(records, suite, price_map))
out = args.out or (ROOT / f"data/profiles-{args.suite}.jsonl")
out.parent.mkdir(parents=True, exist_ok=True)
with out.open("w") as f:
for r in rows:
f.write(json.dumps(r, ensure_ascii=False) + "\n")
n_prompts = len({r["prompt"] for r in rows})
print(f"{args.suite}: {len(records)} examples x {len(suite['models'])} models "
f"-> {len(rows)} rows ({n_prompts} prompts) -> {out}")
if __name__ == "__main__":
main()