tchbcb commited on
Commit
d272b05
·
verified ·
1 Parent(s): 497855c

ops: one-command eval entry (checkpoint resolve + adapter reassembly + auto backend + provenance report)

Browse files
Files changed (1) hide show
  1. ops/run_eval.py +210 -0
ops/run_eval.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """One-command evaluation entry — works anywhere, no manual steps.
3
+
4
+ Quick start (from the repo root):
5
+
6
+ python ops/run_eval.py # v2 checkpoint from HF + mock game
7
+ python ops/run_eval.py --games all25 # the 25 public games (needs arc-agi)
8
+ python ops/run_eval.py --backend tiny --no-checkpoint --max-actions 60 # CPU harness test
9
+ python ops/run_eval.py --checkpoint /path/to/dir-with-qwenjev_s1.pt # local dir
10
+
11
+ What it does:
12
+ 1. Resolves the checkpoint: `hf:<repo>@<subpath>` (downloads qwenjev_s1.pt +
13
+ hf_lora parts, reassembles adapter_model.safetensors, verifies sha256)
14
+ or a local dir laid out as <dir>/qwenjev_s1.pt + <dir>/hf_lora/...
15
+ 2. Picks the backend: `auto` -> hf (GPU, fp16 base + LoRA merged) when CUDA
16
+ is available, else tiny (CPU). The hf path never quantizes for eval:
17
+ merge=True is illegal on a kbit base (see qwenjev/hf_compat.py).
18
+ 3. Runs the hybrid agent per game (heuristic fallback if System-1 fails to
19
+ load) and writes a JSON report with provenance.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import hashlib
25
+ import json
26
+ import sys
27
+ import time
28
+ from pathlib import Path
29
+
30
+ ROOT = Path(__file__).resolve().parents[1]
31
+ sys.path.insert(0, str(ROOT))
32
+ sys.path.insert(0, str(ROOT / "train"))
33
+
34
+ DEFAULT_CKPT = "hf:tchbcb/qwenjev@t4_assets_v2/checkpoints"
35
+
36
+
37
+ def log(msg: str) -> None:
38
+ print(f"[run_eval {time.strftime('%H:%M:%S')}] {msg}", flush=True)
39
+
40
+
41
+ # --------------------------------------------------------------- checkpoint --
42
+ def resolve_checkpoint(spec: str, cache_root: Path) -> Path | None:
43
+ """Return a dir containing qwenjev_s1.pt (+ hf_lora/), or None if 'none'."""
44
+ if spec == "none":
45
+ return None
46
+ if not spec.startswith("hf:"):
47
+ d = Path(spec)
48
+ if not (d / "qwenjev_s1.pt").exists():
49
+ raise SystemExit(f"[error] {d} has no qwenjev_s1.pt")
50
+ ensure_adapter_safetensors(d / "hf_lora")
51
+ return d
52
+
53
+ repo, _, sub = spec[3:].partition("@")
54
+ sub = sub.strip("/")
55
+ # hf_hub_download(local_dir=out.parent) keeps the <sub>/ path layout,
56
+ # so files land exactly at <out>/qwenjev_s1.pt and <out>/hf_lora/...
57
+ out = cache_root / repo.replace("/", "__") / sub.replace("/", "__")
58
+ out.mkdir(parents=True, exist_ok=True)
59
+ from huggingface_hub import hf_hub_download, list_repo_files
60
+
61
+ wanted = [f for f in list_repo_files(repo, repo_type="model")
62
+ if f.startswith(sub + "/")
63
+ and (f == f"{sub}/qwenjev_s1.pt" or "/hf_lora/" in f)]
64
+ for f in wanted:
65
+ hf_hub_download(repo_id=repo, repo_type="model", filename=f,
66
+ local_dir=out.parent)
67
+ log(f"downloaded {f}")
68
+ if not (out / "qwenjev_s1.pt").exists():
69
+ raise SystemExit(f"[error] download failed: {out / 'qwenjev_s1.pt'} missing")
70
+ ensure_adapter_safetensors(out / "hf_lora")
71
+ return out
72
+
73
+
74
+ def ensure_adapter_safetensors(lora_dir: Path) -> None:
75
+ """Reassemble parts/ -> adapter_model.safetensors when only shards exist."""
76
+ if (lora_dir / "adapter_model.safetensors").exists():
77
+ return
78
+ manifest = lora_dir / "parts" / "manifest.json"
79
+ if not manifest.exists():
80
+ return
81
+ m = json.loads(manifest.read_text())
82
+ nonce = int(m.get("nonce_bytes", 16))
83
+ buf = bytearray()
84
+ for p in m["parts"]:
85
+ piece = (lora_dir / "parts" / p["name"]).read_bytes()
86
+ buf += piece[nonce:]
87
+ h = hashlib.sha256(buf).hexdigest()
88
+ if h != m["sha256"]:
89
+ raise SystemExit(f"[error] adapter sha256 mismatch: {h} != {m['sha256']}")
90
+ (lora_dir / "adapter_model.safetensors").write_bytes(bytes(buf))
91
+ log(f"adapter reassembled: {len(buf)} B, sha256 {h[:12]}... OK")
92
+
93
+
94
+ # ------------------------------------------------------------------ backend --
95
+ def pick_backend(backend: str) -> str:
96
+ if backend != "auto":
97
+ return backend
98
+ try:
99
+ import torch
100
+ return "hf" if torch.cuda.is_available() else "tiny"
101
+ except ImportError:
102
+ raise SystemExit("[error] torch not installed — pip install torch "
103
+ "(GPU: default PyPI wheel; CPU: --index-url "
104
+ "https://download.pytorch.org/whl/cpu)")
105
+
106
+
107
+ # --------------------------------------------------------------------- main --
108
+ def main() -> None:
109
+ ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
110
+ ap.add_argument("--checkpoint", default=DEFAULT_CKPT,
111
+ help=f"hf:<repo>@<subpath> | local dir | none (default {DEFAULT_CKPT})")
112
+ ap.add_argument("--backend", default="auto", choices=["auto", "hf", "tiny"])
113
+ ap.add_argument("--games", nargs="*", default=["mock"],
114
+ help="mock | all25 | explicit game ids (space separated)")
115
+ ap.add_argument("--list-games", action="store_true")
116
+ ap.add_argument("--max-actions", type=int, default=300)
117
+ ap.add_argument("--out", default="eval_report.json")
118
+ ap.add_argument("--cache", default=str(Path.home() / ".cache" / "qwenjev_eval"))
119
+ ap.add_argument("--no-checkpoint", action="store_true",
120
+ help="skip checkpoint entirely (untrained heads / harness test)")
121
+ args = ap.parse_args()
122
+
123
+ from data.collect_selfplay import PUBLIC_GAMES_FALLBACK
124
+ if args.list_games:
125
+ print("\n".join(PUBLIC_GAMES_FALLBACK))
126
+ return
127
+
128
+ games: list[str] = []
129
+ for g in args.games:
130
+ if g == "mock":
131
+ games.append("mock-pushbox")
132
+ elif g == "all25":
133
+ games.extend(PUBLIC_GAMES_FALLBACK)
134
+ else:
135
+ games.append(g)
136
+ if any(not g.startswith("mock") for g in games):
137
+ try:
138
+ import arc_agi # noqa: F401
139
+ except ImportError:
140
+ raise SystemExit(
141
+ "[error] real games need the official offline engine:\n"
142
+ " pip install arc-agi + environment_files/ from the\n"
143
+ " competition Data page (Kaggle: arc-prize-2026-arc-agi-3).\n"
144
+ "On Kaggle the data is attached automatically; locally see train.md §4.1.")
145
+
146
+ backend = pick_backend(args.backend)
147
+ ckpt_dir = None if args.no_checkpoint else resolve_checkpoint(args.checkpoint, Path(args.cache))
148
+
149
+ cfg = {
150
+ "system1": {
151
+ "backend": backend,
152
+ "hf_model": "Qwen/Qwen3-VL-2B-Instruct",
153
+ "max_leaf_tokens": 512,
154
+ "score_levels": 10,
155
+ "leaf_batch_size": 2,
156
+ "eval_leaf_batch": 8,
157
+ },
158
+ "eval_max_actions": args.max_actions,
159
+ "out_dir": str(ckpt_dir) if ckpt_dir else "checkpoints/none",
160
+ }
161
+ if ckpt_dir is None:
162
+ cfg["out_dir"] = "checkpoints/definitely-missing" # build_s1 skips silently
163
+
164
+ from evaluate_local import build_s1, run_game
165
+ s1 = None
166
+ try:
167
+ s1 = build_s1(cfg)
168
+ except Exception as exc:
169
+ log(f"[warn] System-1 unavailable ({exc}); heuristic policy will run")
170
+ if backend == "hf" and s1 is None and not args.no_checkpoint:
171
+ log("[warn] hf backend failed to load — continuing with heuristics only")
172
+
173
+ from qwenjev.hybrid import HybridAgent, HybridConfig
174
+ brain = HybridAgent(s1=s1, s2=None,
175
+ config=HybridConfig(game_time_limit_s=600))
176
+
177
+ from data.collect_selfplay import make_env
178
+ report, t0 = [], time.time()
179
+ for game_id in games:
180
+ try:
181
+ env = make_env(game_id, None)
182
+ res = run_game(env, game_id, brain, max_actions=args.max_actions)
183
+ report.append(res)
184
+ log(f"{game_id}: {res}")
185
+ except Exception as exc:
186
+ log(f"[warn] {game_id}: {exc}")
187
+
188
+ out = {
189
+ "meta": {
190
+ "backend": backend,
191
+ "checkpoint": args.checkpoint if not args.no_checkpoint else "none",
192
+ "games": games,
193
+ "max_actions": args.max_actions,
194
+ "seconds_total": round(time.time() - t0, 1),
195
+ "timestamp": time.strftime("%F %T"),
196
+ },
197
+ "games": report,
198
+ "summary": {
199
+ "played": len(report),
200
+ "wins": sum(1 for r in report if r["win"]),
201
+ "best_level_sum": sum(r["best_level"] for r in report),
202
+ "actions_sum": sum(r["actions"] for r in report),
203
+ },
204
+ }
205
+ Path(args.out).write_text(json.dumps(out, indent=2))
206
+ log(f"done: {out['summary']} -> {args.out}")
207
+
208
+
209
+ if __name__ == "__main__":
210
+ main()