review round 3 (probe/model-card/eval): rank-visible eval failure + cross-rank peak memory + complete run_summary; card derived-and-asserted from config; harness install --no-deps and task ids confirmed; torchrun redirects 3 + tee 3
Browse files- eval/run_benchmarks.py +59 -16
eval/run_benchmarks.py
CHANGED
|
@@ -34,7 +34,8 @@ TASKS = ["arc_challenge", "arc_easy", "hellaswag", "mmlu", "piqa", "truthfulqa_m
|
|
| 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", "
|
|
|
|
| 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"),
|
|
@@ -51,8 +52,11 @@ def sh(argv, label, timeout, env=None):
|
|
| 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():
|
|
@@ -78,6 +82,16 @@ def sh(argv, label, timeout, env=None):
|
|
| 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
|
|
@@ -92,13 +106,18 @@ def install_harness():
|
|
| 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 |
-
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
if rc != 0:
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
|
|
|
| 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)
|
|
@@ -171,6 +190,16 @@ def main():
|
|
| 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)
|
|
@@ -183,7 +212,7 @@ def main():
|
|
| 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")):
|
|
@@ -196,13 +225,22 @@ def main():
|
|
| 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)
|
| 200 |
-
|
| 201 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 205 |
-
len(TASKS) - len(missing), len(TASKS), ver, missing or "none"), flush=True)
|
| 206 |
raise SystemExit(4 if missing else 0)
|
| 207 |
|
| 208 |
|
|
@@ -211,22 +249,27 @@ def push(a, root):
|
|
| 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__":
|
|
|
|
| 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-0.33", "items mix 3 and 4 options"),
|
| 38 |
+
"arc_easy": ("0.25-0.33", "items mix 3 and 4 options"),
|
| 39 |
"hellaswag": ("0.25", "4 continuations"), "mmlu": ("0.25", "4 options, macro over 57 subjects"),
|
| 40 |
"piqa": ("0.50", "2 options"), "winogrande": ("0.50", "2 options"),
|
| 41 |
"truthfulqa_mc1": ("~0.20", "mean over questions of 1/#choices"),
|
|
|
|
| 52 |
e["PYTHONUNBUFFERED"] = "1"
|
| 53 |
e.update(env or {})
|
| 54 |
import threading
|
| 55 |
+
# start_new_session, because _kill() signals getpgid(pid). Without its own group that group is the
|
| 56 |
+
# notebook's, so a timeout on the longest task would SIGTERM the very script watching for it (C1).
|
| 57 |
p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
| 58 |
+
env=e, bufsize=1, cwd=os.path.dirname(os.path.abspath(__file__)),
|
| 59 |
+
start_new_session=True)
|
| 60 |
killed = []
|
| 61 |
|
| 62 |
def _kill():
|
|
|
|
| 82 |
return rc, "\n".join(keep)
|
| 83 |
|
| 84 |
|
| 85 |
+
def tf_version():
|
| 86 |
+
"""Which transformers the harness will actually import -- see the --no-deps note below."""
|
| 87 |
+
rc, out = sh([sys.executable, "-c", "import transformers; print(transformers.__version__)"],
|
| 88 |
+
"probe_transformers", 180)
|
| 89 |
+
for line in out.splitlines():
|
| 90 |
+
if line.strip()[:1].isdigit():
|
| 91 |
+
return line.strip()
|
| 92 |
+
return "?"
|
| 93 |
+
|
| 94 |
+
|
| 95 |
def install_harness():
|
| 96 |
"""Pin the harness in a uv inline-script environment rather than into the Kaggle image's site-packages:
|
| 97 |
transformers 5.0.0 must stay exactly as Gate 3 measured it, and `pip install` into the image would
|
|
|
|
| 106 |
return have
|
| 107 |
print("lm_eval %r is not the pin %s -- installing it as a user package" % (have, PINNED_LM_EVAL),
|
| 108 |
flush=True)
|
| 109 |
+
before = tf_version()
|
| 110 |
+
# --no-deps deliberately. A dependency-resolving install can drop a different transformers into
|
| 111 |
+
# ~/.local, where it shadows the image's copy on sys.path -- the exact thing this file's header says it
|
| 112 |
+
# avoids, and it would move the harness pin silently (review finding C4).
|
| 113 |
+
rc, _ = sh([sys.executable, "-m", "pip", "install", "--user", "--quiet", "--no-deps",
|
| 114 |
+
"lm-eval==%s" % PINNED_LM_EVAL], "pip_lm_eval", 1800)
|
| 115 |
if rc != 0:
|
| 116 |
+
raise SystemExit("could not install the pinned harness; refusing to eval on a different one")
|
| 117 |
+
after = tf_version()
|
| 118 |
+
if before != after:
|
| 119 |
+
raise SystemExit("installing the harness moved transformers %s -> %s: the eval would no longer run "
|
| 120 |
+
"on the stack Gate 3 measured" % (before, after))
|
| 121 |
rc, out = sh([sys.executable, "-c", "import lm_eval, os; "
|
| 122 |
"print(lm_eval.__version__, os.path.dirname(lm_eval.__file__))"],
|
| 123 |
"verify_lm_eval", 300)
|
|
|
|
| 190 |
shutil.rmtree(root, ignore_errors=True)
|
| 191 |
os.makedirs(root, exist_ok=True)
|
| 192 |
|
| 193 |
+
# Confirm the task ids resolve before any GPU time goes into them: one typo among nine would otherwise
|
| 194 |
+
# fail the all-or-nothing smoke gate and yield no table at all (C5; §2 says to confirm with --tasks list).
|
| 195 |
+
rc, out = sh([sys.executable, "-m", "lm_eval", "--tasks", "list"], "list_tasks", 900)
|
| 196 |
+
listed = set(re.findall(r"^[ \t]+([a-z][a-z0-9_]+)[ \t]*$", out, re.M))
|
| 197 |
+
unknown = [t for t in TASKS if listed and t not in listed]
|
| 198 |
+
if unknown:
|
| 199 |
+
raise SystemExit("these task ids do not resolve in lm-eval %s: %s" % (ver, unknown))
|
| 200 |
+
print("task ids checked against the harness:", len(TASKS), "requested,",
|
| 201 |
+
(str(len(listed)) + " ids listed") if listed else "list output unparsed", flush=True)
|
| 202 |
+
|
| 203 |
smoke = {}
|
| 204 |
for t in TASKS:
|
| 205 |
rc, cmd = run_task(a.model, t, None, os.path.join(root, "smoke"), 5, 0.5)
|
|
|
|
| 212 |
json.dump(smoke, open(os.path.join(root, "smoke.json"), "w"), indent=1, default=str)
|
| 213 |
raise SystemExit(3 if bad else 0)
|
| 214 |
|
| 215 |
+
table, push_fail = {}, []
|
| 216 |
for t in TASKS:
|
| 217 |
rec = {"chance": CHANCE.get(t, ("?", "?")), "lm_eval": ver}
|
| 218 |
for shots, key in ((None, "primary"), (5, "secondary_5shot")):
|
|
|
|
| 225 |
json.dumps(rec[key]["rows"], default=str)[:200]), flush=True)
|
| 226 |
table[t] = rec
|
| 227 |
json.dump(table, open(os.path.join(root, "results.json"), "w"), indent=1, default=str)
|
| 228 |
+
if not push(a, root): # after every task: an interruption keeps what finished
|
| 229 |
+
push_fail.append("after " + t)
|
| 230 |
+
if not push(a, root):
|
| 231 |
+
push_fail.append("final")
|
| 232 |
+
def ok(t, key):
|
| 233 |
+
r = table.get(t, {}).get(key, {})
|
| 234 |
+
return bool(r.get("rows")) and r.get("rc") == 0
|
| 235 |
+
|
| 236 |
+
# Both columns and both exit codes. `primary` rows alone would report a full house with an entirely
|
| 237 |
+
# empty 5-shot column, or with a leg that exited non-zero after writing something partial (C2).
|
| 238 |
+
missing = [f"{t}/{k}" for t in TASKS for k in ("primary", "secondary_5shot") if not ok(t, k)]
|
| 239 |
+
missing += push_fail
|
| 240 |
# Nine invocations, eight benchmarks: TruthfulQA is one row of the published table and its mc1 and mc2
|
| 241 |
# are separate task ids in the harness (docs/05-eval-plan.md §2).
|
| 242 |
+
print("VERDICT PHASE6 cells %d/%d (8 benchmarks x 2 columns) lm_eval %s missing %s" % (
|
| 243 |
+
2 * len(TASKS) - len(missing), 2 * len(TASKS), ver, missing or "none"), flush=True)
|
| 244 |
raise SystemExit(4 if missing else 0)
|
| 245 |
|
| 246 |
|
|
|
|
| 249 |
repo = a.repo or a.model
|
| 250 |
if not repo:
|
| 251 |
print("(no --repo and no --model-derived target; results stay local)", flush=True)
|
| 252 |
+
return False
|
| 253 |
import ounce100m_credentials
|
| 254 |
ounce100m_credentials.install()
|
| 255 |
tok = os.environ.get("HF_TOKEN")
|
| 256 |
if tok is None:
|
| 257 |
print("FATAL: no token, refusing to claim results were published", flush=True)
|
| 258 |
+
return False
|
| 259 |
try:
|
| 260 |
from huggingface_hub import HfApi
|
| 261 |
rc = HfApi(token=tok).upload_folder(repo_id=repo, repo_type="model", folder_path=root,
|
| 262 |
path_in_repo="eval", commit_message="Phase 6 results",
|
| 263 |
+
# The --limit 5 smoke rows must not sit in the public repo beside
|
| 264 |
+
# the real table looking like results (C3).
|
| 265 |
+
ignore_patterns=["smoke*", "smoke/*", "smoke.json"],
|
| 266 |
commit_description="lm-eval 0.4.13, task-default shots plus a "
|
| 267 |
"uniform 5-shot column, per docs/05-eval-plan.md")
|
| 268 |
print("PUSHED eval/ ->", getattr(rc, "commit_url", str(rc)[:120]), flush=True)
|
| 269 |
+
return True
|
| 270 |
except Exception as e:
|
| 271 |
print("PUSH_FAILED", type(e).__name__, str(e)[:200], flush=True)
|
| 272 |
+
return False
|
| 273 |
|
| 274 |
|
| 275 |
if __name__ == "__main__":
|