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- 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()
|