Spaces:
Running
Running
| #!/usr/bin/env python3 | |
| """DesignGym full benchmark β runs every (model x task x seed) combination | |
| against the local server and prints an honest comparison report. | |
| Usage: | |
| conda activate RL | |
| python run_benchmark.py | |
| Prerequisites: the server must NOT be running β this script boots its own. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import os | |
| import signal | |
| import subprocess | |
| import sys | |
| import time | |
| from collections import defaultdict | |
| from typing import Any, Dict, List, Optional | |
| import urllib.request | |
| import urllib.error | |
| # ββ Config ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SERVER_URL = "http://localhost:8000" | |
| TASKS = ["poster_basic_v1", "editorial_cover_v1", "dense_flyer_v1"] | |
| SEEDS = [0, 1, 2] | |
| BACKENDS = [ | |
| {"name": "heuristic", "adapter": None, "policy": "heuristic"}, | |
| {"name": "base", "adapter": "base", "policy": "llm"}, | |
| {"name": "sft_finetuned", "adapter": "sft", "policy": "llm"}, | |
| {"name": "grpo_finetuned","adapter": "grpo", "policy": "llm"}, | |
| ] | |
| TIMEOUT_PER_EPISODE = 300 # seconds | |
| def api(method: str, path: str, body: Optional[dict] = None, timeout: int = TIMEOUT_PER_EPISODE) -> dict: | |
| url = f"{SERVER_URL}{path}" | |
| data = json.dumps(body).encode() if body else None | |
| req = urllib.request.Request(url, data=data, method=method) | |
| if data: | |
| req.add_header("Content-Type", "application/json") | |
| with urllib.request.urlopen(req, timeout=timeout) as resp: | |
| return json.loads(resp.read()) | |
| def wait_for_server(max_wait: int = 120) -> bool: | |
| t0 = time.time() | |
| while time.time() - t0 < max_wait: | |
| try: | |
| api("GET", "/demo/ping", timeout=5) | |
| return True | |
| except Exception: | |
| time.sleep(1) | |
| return False | |
| def wait_for_model_ready(max_wait: int = 180) -> dict: | |
| t0 = time.time() | |
| while time.time() - t0 < max_wait: | |
| try: | |
| info = api("GET", "/demo/backend_info", timeout=5) | |
| if info.get("ready") or info.get("backend") == "none": | |
| return info | |
| time.sleep(2) | |
| except Exception: | |
| time.sleep(2) | |
| return {"backend": "timeout"} | |
| def switch_adapter(adapter_key: str) -> dict: | |
| return api("POST", "/demo/switch_adapter", {"adapter": adapter_key}, timeout=180) | |
| def run_episode(policy: str, task_id: str, seed: int) -> dict: | |
| return api("POST", "/demo/run_episode", { | |
| "policy": policy, | |
| "task_id": task_id, | |
| "seed": seed, | |
| }) | |
| def start_server() -> subprocess.Popen: | |
| env = os.environ.copy() | |
| env["DESIGNGYM_BACKEND"] = "local" | |
| env["DESIGNGYM_ADAPTER"] = "sft" | |
| env["TORCH_NUM_THREADS"] = "4" | |
| proc = subprocess.Popen( | |
| [sys.executable, "-m", "server.app"], | |
| env=env, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.STDOUT, | |
| text=True, | |
| preexec_fn=os.setsid, | |
| ) | |
| return proc | |
| def kill_server(proc: subprocess.Popen): | |
| try: | |
| os.killpg(os.getpgid(proc.pid), signal.SIGTERM) | |
| proc.wait(timeout=10) | |
| except Exception: | |
| try: | |
| os.killpg(os.getpgid(proc.pid), signal.SIGKILL) | |
| except Exception: | |
| pass | |
| # ββ Reporting βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def fmt(n, d=4): | |
| if n is None: | |
| return " N/A " | |
| return f"{n:>{d+3}.{d}f}" | |
| def print_separator(char="β", width=100): | |
| print(char * width) | |
| def print_report(results: List[Dict[str, Any]]): | |
| print("\n") | |
| print_separator("β") | |
| print(" DESIGNGYM BENCHMARK REPORT") | |
| print(f" {len(results)} episodes Β· {len(TASKS)} tasks Β· {len(SEEDS)} seeds Β· {len(BACKENDS)} backends") | |
| print_separator("β") | |
| # ββ Per-run detail table ββ | |
| print(f"\n{'Backend':<16} {'Task':<22} {'Seed':>4} {'Score':>7} {'Instr':>7} {'Steps':>5} {'Reward':>7} {'Time':>6} {'Policy Tags'}") | |
| print_separator("β") | |
| for r in sorted(results, key=lambda x: (x["backend"], x["task_id"], x["seed"])): | |
| tags = set() | |
| for t in r.get("trajectory", []): | |
| tags.add(t.get("policy", "?")) | |
| tag_str = ", ".join(sorted(tags)) | |
| s = r["summary"] | |
| print( | |
| f"{r['backend']:<16} {r['task_id']:<22} {r['seed']:>4} " | |
| f"{fmt(s.get('final_score'), 4)} {fmt(s.get('instruction_score'), 4)} " | |
| f"{s.get('steps_taken', 0):>5} {fmt(s.get('total_reward'), 3)} " | |
| f"{s.get('wall_time_sec', 0):>5.1f}s {tag_str}" | |
| ) | |
| # ββ Aggregate per backend ββ | |
| print("\n") | |
| print_separator("β") | |
| print(" AGGREGATE BY BACKEND") | |
| print_separator("β") | |
| by_backend: Dict[str, List[Dict]] = defaultdict(list) | |
| for r in results: | |
| by_backend[r["backend"]].append(r) | |
| print(f"\n{'Backend':<16} {'Runs':>4} {'Avg Score':>10} {'Avg Instr':>10} {'Avg Steps':>10} {'Avg Reward':>11} {'Avg Time':>9}") | |
| print_separator("β") | |
| backend_agg = {} | |
| for name in ["heuristic", "base", "sft_finetuned", "grpo_finetuned"]: | |
| runs = by_backend.get(name, []) | |
| if not runs: | |
| print(f"{name:<16} {'0':>4} {'β':>9} {'β':>9} {'β':>9} {'β':>10} {'β':>8}") | |
| continue | |
| scores = [r["summary"].get("final_score", 0) for r in runs] | |
| instrs = [r["summary"].get("instruction_score", 0) for r in runs] | |
| steps = [r["summary"].get("steps_taken", 0) for r in runs] | |
| rewards = [r["summary"].get("total_reward", 0) for r in runs] | |
| times = [r["summary"].get("wall_time_sec", 0) for r in runs] | |
| avg_s = sum(scores) / len(scores) | |
| avg_i = sum(instrs) / len(instrs) | |
| avg_st = sum(steps) / len(steps) | |
| avg_r = sum(rewards) / len(rewards) | |
| avg_t = sum(times) / len(times) | |
| backend_agg[name] = {"score": avg_s, "instr": avg_i, "steps": avg_st, "reward": avg_r, "time": avg_t} | |
| print( | |
| f"{name:<16} {len(runs):>4} {avg_s:>10.4f} {avg_i:>10.4f} " | |
| f"{avg_st:>10.1f} {avg_r:>11.3f} {avg_t:>8.1f}s" | |
| ) | |
| # ββ Per-task breakdown ββ | |
| print("\n") | |
| print_separator("β") | |
| print(" BREAKDOWN BY TASK") | |
| print_separator("β") | |
| for task in TASKS: | |
| print(f"\n {task}") | |
| print(f" {'Backend':<16} {'Avg Score':>10} {'Avg Instr':>10} {'Avg Reward':>11}") | |
| print(f" " + "β" * 52) | |
| for name in ["heuristic", "base", "sft_finetuned", "grpo_finetuned"]: | |
| runs = [r for r in by_backend.get(name, []) if r["task_id"] == task] | |
| if not runs: | |
| print(f" {name:<16} {'β':>9} {'β':>9} {'β':>10}") | |
| continue | |
| avg_s = sum(r["summary"]["final_score"] for r in runs) / len(runs) | |
| avg_i = sum(r["summary"]["instruction_score"] for r in runs) / len(runs) | |
| avg_r = sum(r["summary"]["total_reward"] for r in runs) / len(runs) | |
| print(f" {name:<16} {avg_s:>10.4f} {avg_i:>10.4f} {avg_r:>11.3f}") | |
| # ββ Honest assessment ββ | |
| print("\n") | |
| print_separator("β") | |
| print(" HONEST ASSESSMENT") | |
| print_separator("β") | |
| print() | |
| if "sft_finetuned" in backend_agg and "heuristic" in backend_agg: | |
| sft = backend_agg["sft_finetuned"] | |
| heur = backend_agg["heuristic"] | |
| diff = sft["score"] - heur["score"] | |
| if diff > 0.01: | |
| print(f" SFT vs Heuristic: +{diff:.4f} avg score β SFT fine-tune IMPROVES over heuristic") | |
| elif diff < -0.01: | |
| print(f" SFT vs Heuristic: {diff:.4f} avg score β SFT fine-tune WORSE than heuristic") | |
| else: | |
| print(f" SFT vs Heuristic: {diff:+.4f} avg score β roughly EQUAL") | |
| if "grpo_finetuned" in backend_agg and "heuristic" in backend_agg: | |
| grpo = backend_agg["grpo_finetuned"] | |
| heur = backend_agg["heuristic"] | |
| diff = grpo["score"] - heur["score"] | |
| if diff > 0.01: | |
| print(f" GRPO vs Heuristic: +{diff:.4f} avg score β GRPO fine-tune IMPROVES over heuristic") | |
| elif diff < -0.01: | |
| print(f" GRPO vs Heuristic: {diff:.4f} avg score β GRPO fine-tune WORSE than heuristic") | |
| else: | |
| print(f" GRPO vs Heuristic: {diff:+.4f} avg score β roughly EQUAL") | |
| if "sft_finetuned" in backend_agg and "base" in backend_agg: | |
| sft = backend_agg["sft_finetuned"] | |
| base = backend_agg["base"] | |
| diff = sft["score"] - base["score"] | |
| if diff > 0.01: | |
| print(f" SFT vs Base: +{diff:.4f} avg score β LoRA adapter provides real lift over base model") | |
| elif diff < -0.01: | |
| print(f" SFT vs Base: {diff:.4f} avg score β LoRA adapter WORSE than base (check training)") | |
| else: | |
| print(f" SFT vs Base: {diff:+.4f} avg score β no meaningful difference (adapter may not be helping)") | |
| if "grpo_finetuned" in backend_agg and "base" in backend_agg: | |
| grpo = backend_agg["grpo_finetuned"] | |
| base = backend_agg["base"] | |
| diff = grpo["score"] - base["score"] | |
| if diff > 0.01: | |
| print(f" GRPO vs Base: +{diff:.4f} avg score β GRPO adapter provides real lift over base model") | |
| elif diff < -0.01: | |
| print(f" GRPO vs Base: {diff:.4f} avg score β GRPO adapter WORSE than base (check training)") | |
| else: | |
| print(f" GRPO vs Base: {diff:+.4f} avg score β no meaningful difference") | |
| if "sft_finetuned" in backend_agg and "grpo_finetuned" in backend_agg: | |
| sft = backend_agg["sft_finetuned"] | |
| grpo = backend_agg["grpo_finetuned"] | |
| diff = sft["score"] - grpo["score"] | |
| if abs(diff) < 0.005: | |
| print(f" SFT vs GRPO: {diff:+.4f} avg score β virtually identical") | |
| elif diff > 0: | |
| print(f" SFT vs GRPO: +{diff:.4f} avg score β SFT edges out GRPO") | |
| else: | |
| print(f" SFT vs GRPO: {diff:.4f} avg score β GRPO edges out SFT") | |
| # ββ LLM parse success rate ββ | |
| print() | |
| for name in ["base", "sft_finetuned", "grpo_finetuned"]: | |
| runs = by_backend.get(name, []) | |
| if not runs: | |
| continue | |
| total_steps = 0 | |
| llm_ok_steps = 0 | |
| for r in runs: | |
| for t in r.get("trajectory", []): | |
| total_steps += 1 | |
| tag = t.get("policy", "") | |
| if tag.startswith("finetuned_") or tag == "local_base" or tag == "router_base": | |
| llm_ok_steps += 1 | |
| pct = (llm_ok_steps / total_steps * 100) if total_steps else 0 | |
| print(f" {name:<16} LLM steered {llm_ok_steps}/{total_steps} steps ({pct:.0f}%)" | |
| f" β {'GOOD' if pct > 60 else 'LOW: model often falls back to heuristic'}") | |
| print() | |
| print_separator("β") | |
| print() | |
| # ββ Main ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def main(): | |
| print("=" * 60) | |
| print(" DesignGym Full Benchmark") | |
| print(f" {len(BACKENDS)} backends x {len(TASKS)} tasks x {len(SEEDS)} seeds = {len(BACKENDS)*len(TASKS)*len(SEEDS)} episodes") | |
| print("=" * 60) | |
| # Boot server | |
| print("\n[1/4] Starting server ...", flush=True) | |
| server = start_server() | |
| try: | |
| if not wait_for_server(max_wait=90): | |
| print("FATAL: server did not start within 90s") | |
| kill_server(server) | |
| sys.exit(1) | |
| print(" Server is up.", flush=True) | |
| # Wait for initial model warm-up | |
| print("[2/4] Waiting for model warm-up ...", flush=True) | |
| info = wait_for_model_ready(max_wait=180) | |
| print(f" Backend: {info.get('backend')} device={info.get('device')} ready={info.get('ready')}", flush=True) | |
| # Run all combinations | |
| print(f"[3/4] Running {len(BACKENDS)*len(TASKS)*len(SEEDS)} episodes ...\n", flush=True) | |
| results: List[Dict[str, Any]] = [] | |
| total = len(BACKENDS) * len(TASKS) * len(SEEDS) | |
| done = 0 | |
| for backend_cfg in BACKENDS: | |
| bname = backend_cfg["name"] | |
| adapter = backend_cfg["adapter"] | |
| policy = backend_cfg["policy"] | |
| # Switch adapter if needed (or skip for heuristic) | |
| if adapter is not None: | |
| print(f" Switching to adapter={adapter} ...", end=" ", flush=True) | |
| try: | |
| resp = switch_adapter(adapter) | |
| print(f"OK ({resp.get('info', {}).get('load_seconds', '?')}s)", flush=True) | |
| except Exception as e: | |
| print(f"FAILED: {e}", flush=True) | |
| # Still try to run β will fall back | |
| time.sleep(0.5) | |
| for task_id in TASKS: | |
| for seed in SEEDS: | |
| done += 1 | |
| label = f"[{done}/{total}] {bname:<16} {task_id:<22} seed={seed}" | |
| print(f" {label} ...", end=" ", flush=True) | |
| t0 = time.time() | |
| try: | |
| data = run_episode(policy, task_id, seed) | |
| s = data.get("summary", {}) | |
| elapsed = time.time() - t0 | |
| score = s.get("final_score", 0) | |
| print(f"score={score:.4f} time={elapsed:.1f}s", flush=True) | |
| results.append({ | |
| "backend": bname, | |
| "task_id": task_id, | |
| "seed": seed, | |
| "policy": policy, | |
| "summary": s, | |
| "trajectory": data.get("trajectory", []), | |
| }) | |
| except Exception as e: | |
| elapsed = time.time() - t0 | |
| print(f"ERROR: {e} ({elapsed:.1f}s)", flush=True) | |
| results.append({ | |
| "backend": bname, | |
| "task_id": task_id, | |
| "seed": seed, | |
| "policy": policy, | |
| "summary": {"final_score": 0, "instruction_score": 0, | |
| "steps_taken": 0, "total_reward": 0, | |
| "wall_time_sec": elapsed}, | |
| "trajectory": [], | |
| "error": str(e), | |
| }) | |
| # Print report | |
| print("\n[4/4] Generating report ...", flush=True) | |
| print_report(results) | |
| # Save raw JSON | |
| out_path = os.path.join(os.path.dirname(__file__), "benchmark_results.json") | |
| with open(out_path, "w") as f: | |
| json.dump(results, f, indent=2, default=str) | |
| print(f"Raw results saved to {out_path}") | |
| finally: | |
| print("\nShutting down server ...", flush=True) | |
| kill_server(server) | |
| print("Done.") | |
| if __name__ == "__main__": | |
| main() | |