Cion-lab commited on
Commit
8b75198
·
verified ·
1 Parent(s): 67cb594

Phase 6 tooling: the eight benchmarks exactly as docs/05-eval-plan.md froze them (lm-eval 0.4.13, hf backend, fp16, one card, task-default shots + uniform 5-shot column, --limit 5 smoke gate first, incremental push after every task)

Browse files
Files changed (1) hide show
  1. eval/run_benchmarks.py +233 -0
eval/run_benchmarks.py ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Phase 6: the eight academic benchmarks, run exactly as docs/05-eval-plan.md froze them.
2
+
3
+ Written before any score exists, and it refuses to deviate from the pin:
4
+
5
+ * `lm-eval` **0.4.13**, backend `model=hf`, `dtype=float16`, one T4, no `--trust_remote_code`, no chat
6
+ template -- a base model with a chat template would be an unearned capability (§3.9).
7
+ * PRIMARY column = each task's own default `num_fewshot`, which means **no `--num_fewshot` flag at all**.
8
+ The point of using the harness's file rather than a number is that nobody chose it per task, so it cannot
9
+ be tuned per task later either.
10
+ * SECONDARY column = a uniform 5-shot run of every task, reported alongside, never instead.
11
+ * One invocation per task, all eight in a single job, and each task's result file is pushed to the Hub the
12
+ moment it finishes so an interruption never loses a completed task (§5 Phase 6).
13
+ * A `--limit 5` smoke pass over all eight runs first, because transformers 5.0.0 on the Kaggle image versus
14
+ a harness pinned in 2024 is an unverified combination (§5) and finding that out after 6 hours of GPU time
15
+ is not acceptable. Nothing is published if the smoke pass fails.
16
+
17
+ No credentials in this file: `ounce100m_credentials.install()` fetches the token at run time (D-006), and
18
+ the token never enters a log line or a published artifact.
19
+ """
20
+
21
+ import argparse
22
+ import json
23
+ import os
24
+ import re
25
+ import shutil
26
+ import signal
27
+ import subprocess
28
+ import sys
29
+ import time
30
+
31
+ PINNED_LM_EVAL = "0.4.13"
32
+ # Every row here is the harness's own task id, not a name invented for this script.
33
+ TASKS = ["arc_challenge", "arc_easy", "hellaswag", "mmlu", "piqa", "truthfulqa_mc1",
34
+ "truthfulqa_mc2", "winogrande", "gsm8k"]
35
+ # Chance level of the *metric being reported*, from the number of answer options in the task, not from a
36
+ # remembered leaderboard. Where a metric is not chance-normalised that is said rather than guessed at.
37
+ CHANCE = {"arc_challenge": ("0.25", "4 options"), "arc_easy": ("0.25", "4 options"),
38
+ "hellaswag": ("0.25", "4 continuations"), "mmlu": ("0.25", "4 options, macro over 57 subjects"),
39
+ "piqa": ("0.50", "2 options"), "winogrande": ("0.50", "2 options"),
40
+ "truthfulqa_mc1": ("~0.20", "mean over questions of 1/#choices"),
41
+ "truthfulqa_mc2": ("n/a", "mc2 is not chance-normalised"),
42
+ "gsm8k": ("0.00", "free-form exact match")}
43
+
44
+
45
+ def sh(argv, label, timeout, env=None):
46
+ """Streamed like every other long-running stage in this project -- a buffered 6-hour eval job would
47
+ report nothing at all if the platform took the instance."""
48
+ print("=== " + label, flush=True)
49
+ t0 = time.time()
50
+ e = dict(os.environ)
51
+ e["PYTHONUNBUFFERED"] = "1"
52
+ e.update(env or {})
53
+ import threading
54
+ p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
55
+ env=e, bufsize=1, cwd=os.path.dirname(os.path.abspath(__file__)))
56
+ killed = []
57
+
58
+ def _kill():
59
+ killed.append(True)
60
+ try:
61
+ os.killpg(os.getpgid(p.pid), signal.SIGTERM)
62
+ except Exception:
63
+ p.kill()
64
+
65
+ timer = threading.Timer(timeout, _kill)
66
+ timer.daemon = True
67
+ timer.start()
68
+ keep = []
69
+ for line in p.stdout:
70
+ line = line.rstrip("\n")
71
+ keep.append(line)
72
+ del keep[:-400]
73
+ print(" |", line[:260], flush=True)
74
+ timer.cancel()
75
+ rc = p.wait()
76
+ print("%s_RC %s%s seconds %.1f" % (label, rc, " TIMEOUT" if killed else "", time.time() - t0),
77
+ flush=True)
78
+ return rc, "\n".join(keep)
79
+
80
+
81
+ def install_harness():
82
+ """Pin the harness in a uv inline-script environment rather than into the Kaggle image's site-packages:
83
+ transformers 5.0.0 must stay exactly as Gate 3 measured it, and `pip install` into the image would
84
+ resolve a different one."""
85
+ rc, out = sh([sys.executable, "-c", "import lm_eval; print(lm_eval.__version__)"], "probe_lm_eval", 120)
86
+ have = ""
87
+ for line in out.splitlines():
88
+ if re.match(r"^\s*\|?\s*[0-9]+\.[0-9]+", line):
89
+ have = line.strip().strip("|").strip()
90
+ if have == PINNED_LM_EVAL:
91
+ print("lm_eval", have, "already present", flush=True)
92
+ return have
93
+ print("lm_eval %r is not the pin %s -- installing it as a user package" % (have, PINNED_LM_EVAL),
94
+ flush=True)
95
+ rc, _ = sh([sys.executable, "-m", "pip", "install", "--user", "--quiet",
96
+ "lm-eval[mathtasks]==%s" % PINNED_LM_EVAL], "pip_lm_eval", 1800)
97
+ if rc != 0:
98
+ rc, _ = sh([sys.executable, "-m", "pip", "install", "--user", "--quiet",
99
+ "lm-eval==%s" % PINNED_LM_EVAL], "pip_lm_eval_plain", 1800)
100
+ if rc != 0:
101
+ raise SystemExit("could not install the pinned harness; refusing to eval on a different one")
102
+ rc, out = sh([sys.executable, "-c", "import lm_eval, os; "
103
+ "print(lm_eval.__version__, os.path.dirname(lm_eval.__file__))"],
104
+ "verify_lm_eval", 300)
105
+ got = [l.split()[0] for l in out.splitlines() if l.startswith(PINNED_LM_EVAL)]
106
+ if not got:
107
+ raise SystemExit("lm_eval is not importable at the pinned version after install: "
108
+ + out[-400:])
109
+ return PINNED_LM_EVAL
110
+
111
+
112
+ def run_task(model, task, shots, outdir, limit, gpu_h):
113
+ """One harness invocation. `shots=None` means do not pass the flag at all, which is the PRIMARY column."""
114
+ argv = [sys.executable, "-m", "lm_eval", "--model", "hf",
115
+ "--model_args", ",".join(["pretrained=" + model, "dtype=float16",
116
+ "trust_remote_code=False"]),
117
+ "--tasks", task, "--batch_size", "8", "--seed", "42", "--output_path", outdir,
118
+ "--log_samples"]
119
+ if shots is not None:
120
+ argv += ["--num_fewshot", str(shots)]
121
+ if limit:
122
+ argv += ["--limit", str(limit)]
123
+ rc, out = sh(["nvidia-smi", "--query-gpu=memory.used", "--format=csv,noheader"], "gpu_before", 60)
124
+ rc, out = sh(argv, "EVAL_%s_%s" % (task, "default" if shots is None else "%dshot" % shots),
125
+ timeout=gpu_h * 3600)
126
+ return rc, " ".join(argv)
127
+
128
+
129
+ def harvest(outdir, task):
130
+ """Find the rows the harness just wrote. It nests results under <timestamp>/<task>/results.json, so
131
+ glob rather than assume a layout that changes between releases."""
132
+ hits = []
133
+ for root, _d, files in os.walk(outdir):
134
+ for fn in files:
135
+ if fn.startswith("results") and fn.endswith(".json"):
136
+ try:
137
+ j = json.load(open(os.path.join(root, fn)))
138
+ except Exception:
139
+ continue
140
+ if task in (j.get("results") or {}):
141
+ hits.append((os.path.getmtime(os.path.join(root, fn)), os.path.join(root, fn), j))
142
+ if not hits:
143
+ return {}
144
+ hits.sort()
145
+ path, j = hits[-1][1], hits[-1][2]
146
+ r = j["results"][task]
147
+ keep = {k: v for k, v in r.items() if not isinstance(v, (dict, list)) and k != "alias"}
148
+ return {"file": path, "rows": keep, "n_task_versions": len(j.get("task_versions") or {}),
149
+ "config_fewshot": (j.get("config") or {}).get("num_fewshot"),
150
+ "model_repr": (j.get("config") or {}).get("model", [None])[0]
151
+ if isinstance((j.get("config") or {}).get("model"), list) else None,
152
+ "date": j.get("date"), "git_hash": j.get("git_hash")}
153
+
154
+
155
+ def main():
156
+ ap = argparse.ArgumentParser()
157
+ ap.add_argument("--model", default="", help="Hub model id, e.g. Cion-lab/ounce106m-v1")
158
+ ap.add_argument("--repo", default="", help="where to push results (defaults to --model)")
159
+ ap.add_argument("--smoke-only", action="store_true", help="the --limit 5 pass and nothing else")
160
+ ap.add_argument("--full-gpu-hours", type=float, default=4.0,
161
+ help="per-task ceiling for the full table; MMLU is by far the longest")
162
+ a = ap.parse_args()
163
+ if not a.model:
164
+ raise SystemExit("--model is required (the public Hub repo, loaded clean-room)")
165
+
166
+ import ounce100m_credentials
167
+ print("creds:", json.dumps(ounce100m_credentials.install(verify=True)), flush=True)
168
+ ver = install_harness()
169
+ os.environ["CUDA_VISIBLE_DEVICES"] = "0" # one card, no gather path that could reorder exemplars
170
+ root = os.path.join(os.path.dirname(os.path.abspath(__file__)), "results")
171
+ shutil.rmtree(root, ignore_errors=True)
172
+ os.makedirs(root, exist_ok=True)
173
+
174
+ smoke = {}
175
+ for t in TASKS:
176
+ rc, cmd = run_task(a.model, t, None, os.path.join(root, "smoke"), 5, 0.5)
177
+ smoke[t] = {"rc": rc, "rows": harvest(os.path.join(root, "smoke"), t).get("rows", {}),
178
+ "cmd": cmd}
179
+ print("SMOKE %s rc %s rows %d" % (t, rc, len(smoke[t]["rows"])), flush=True)
180
+ bad = [t for t in TASKS if smoke[t]["rc"] != 0 or not smoke[t]["rows"]]
181
+ print("SMOKE_PASS" if not bad else "SMOKE_FAIL " + str(bad), flush=True)
182
+ if bad or a.smoke_only:
183
+ json.dump(smoke, open(os.path.join(root, "smoke.json"), "w"), indent=1, default=str)
184
+ raise SystemExit(3 if bad else 0)
185
+
186
+ table = {}
187
+ for t in TASKS:
188
+ rec = {"chance": CHANCE.get(t, ("?", "?")), "lm_eval": ver}
189
+ for shots, key in ((None, "primary"), (5, "secondary_5shot")):
190
+ d = os.path.join(root, key, t)
191
+ rc, cmd = run_task(a.model, t, shots, d, 0, a.full_gpu_hours)
192
+ h = harvest(d, t)
193
+ rec[key] = {"rc": rc, "rows": h.get("rows", {}), "command": cmd,
194
+ "harness_fewshot": h.get("config_fewshot"), "results_file": h.get("file")}
195
+ print("DONE %s %s rc %s rows %s" % (t, key, rc,
196
+ json.dumps(rec[key]["rows"], default=str)[:200]), flush=True)
197
+ table[t] = rec
198
+ json.dump(table, open(os.path.join(root, "results.json"), "w"), indent=1, default=str)
199
+ push(a, root) # after every task: an interruption keeps what finished
200
+ push(a, root)
201
+ missing = [t for t in TASKS if not table.get(t, {}).get("primary", {}).get("rows")]
202
+ # Nine invocations, eight benchmarks: TruthfulQA is one row of the published table and its mc1 and mc2
203
+ # are separate task ids in the harness (docs/05-eval-plan.md §2).
204
+ print("VERDICT PHASE6 runs %d/%d (8 benchmarks) lm_eval %s missing %s" % (
205
+ len(TASKS) - len(missing), len(TASKS), ver, missing or "none"), flush=True)
206
+ raise SystemExit(4 if missing else 0)
207
+
208
+
209
+ def push(a, root):
210
+ """Publish into the model repo, which is where a reader expects the evaluation to live (§8)."""
211
+ repo = a.repo or a.model
212
+ if not repo:
213
+ print("(no --repo and no --model-derived target; results stay local)", flush=True)
214
+ return
215
+ import ounce100m_credentials
216
+ ounce100m_credentials.install()
217
+ tok = os.environ.get("HF_TOKEN")
218
+ if tok is None:
219
+ print("FATAL: no token, refusing to claim results were published", flush=True)
220
+ return
221
+ try:
222
+ from huggingface_hub import HfApi
223
+ rc = HfApi(token=tok).upload_folder(repo_id=repo, repo_type="model", folder_path=root,
224
+ path_in_repo="eval", commit_message="Phase 6 results",
225
+ commit_description="lm-eval 0.4.13, task-default shots plus a "
226
+ "uniform 5-shot column, per docs/05-eval-plan.md")
227
+ print("PUSHED eval/ ->", getattr(rc, "commit_url", str(rc)[:120]), flush=True)
228
+ except Exception as e:
229
+ print("PUSH_FAILED", type(e).__name__, str(e)[:200], flush=True)
230
+
231
+
232
+ if __name__ == "__main__":
233
+ main()