p6_cli_probe: check run_benchmarks' flag list and PRIMARY_METRIC against the installed 0.4.13 CLI and its shipped YAMLs
Browse files- kernels/p6_cli_probe.py +142 -0
kernels/p6_cli_probe.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Phase 6 contract check on free CPU: the CLI flags we pass, and the metric keys we promise.
|
| 2 |
+
|
| 3 |
+
E-047 and E-048 both had the same shape -- a documented lm-eval behaviour that turned out not to be true of
|
| 4 |
+
0.4.13 (`--no-deps` leaves the package unimportable; `--tasks list` is not a listing command). Two
|
| 5 |
+
assumptions of exactly that kind are still unverified and both would only be discovered after the model is
|
| 6 |
+
trained and published:
|
| 7 |
+
|
| 8 |
+
1. `run_task()` builds a command line of eight flags. If 0.4.13's CLI (which was restructured into
|
| 9 |
+
`lm_eval/_cli/harness.py` subcommands -- visible in E-048's traceback) renames or drops one of them, the
|
| 10 |
+
failure lands in Phase 6, not now. So the help text of the harness's own `run` command is read and every
|
| 11 |
+
flag `run_task()` passes is checked against it.
|
| 12 |
+
2. `PRIMARY_METRIC` promises `acc,none` and `exact_match,flexible-extract` will be the keys of
|
| 13 |
+
`results.json`, and docs/05 §7 promises which split each task is scored on and whether it declares
|
| 14 |
+
`num_fewshot`. E-048 could not read that from `TaskManager().task_index` (an `Entry` whose config is not
|
| 15 |
+
populated), so it is read from the source of truth instead: the YAMLs shipped inside the wheel, with
|
| 16 |
+
`include:` chains resolved.
|
| 17 |
+
|
| 18 |
+
**No eval data is touched, and none can be from this code**: it opens `.yaml` files inside site-packages and
|
| 19 |
+
prints `--help`. No task object is constructed, no `datasets.load_dataset` call is reachable here, no
|
| 20 |
+
benchmark row is read -- §3.3 and Phase 6's ordering both stay intact.
|
| 21 |
+
"""
|
| 22 |
+
import glob
|
| 23 |
+
import json
|
| 24 |
+
import os
|
| 25 |
+
import subprocess
|
| 26 |
+
import sys
|
| 27 |
+
|
| 28 |
+
PIN = "0.4.13"
|
| 29 |
+
TASKS = ["arc_challenge", "arc_easy", "hellaswag", "mmlu", "piqa", "truthfulqa_mc1",
|
| 30 |
+
"truthfulqa_mc2", "winogrande", "gsm8k"]
|
| 31 |
+
PRIMARY_METRIC = {"arc_challenge": "acc,none", "arc_easy": "acc,none", "hellaswag": "acc,none",
|
| 32 |
+
"mmlu": "acc,none", "piqa": "acc,none", "truthfulqa_mc1": "acc,none",
|
| 33 |
+
"truthfulqa_mc2": "acc,none", "winogrande": "acc,none",
|
| 34 |
+
"gsm8k": "exact_match,flexible-extract"}
|
| 35 |
+
# Every flag run_benchmarks.py's run_task() passes, in its own words, so the two cannot drift apart.
|
| 36 |
+
OURS = ["--model", "--model_args", "--tasks", "--batch_size", "--seed", "--output_path",
|
| 37 |
+
"--log_samples", "--num_fewshot", "--limit"]
|
| 38 |
+
|
| 39 |
+
rc = subprocess.run([sys.executable, "-m", "pip", "install", "--user", "--quiet",
|
| 40 |
+
"lm-eval==" + PIN]).returncode
|
| 41 |
+
print("pip rc", rc, flush=True)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def sh(argv, timeout=600):
|
| 45 |
+
p = subprocess.run(argv, capture_output=True, text=True, timeout=timeout)
|
| 46 |
+
return p.returncode, (p.stdout or "") + (p.stderr or "")
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
rc, top = sh([sys.executable, "-m", "lm_eval", "--help"])
|
| 50 |
+
print("TOP_HELP rc", rc, "chars", len(top), flush=True)
|
| 51 |
+
# Find the subcommand that evaluates (0.4.13's harness registered `run`; fall back to scanning --help).
|
| 52 |
+
subs = []
|
| 53 |
+
for line in top.splitlines():
|
| 54 |
+
s = line.strip()
|
| 55 |
+
if s.startswith("run") or (s and s.split()[0] in {"run", "eval", "simple"}):
|
| 56 |
+
subs.append(s.split()[0])
|
| 57 |
+
subs = list(dict.fromkeys(subs)) or ["run"]
|
| 58 |
+
helps = {}
|
| 59 |
+
for s in subs:
|
| 60 |
+
rc, h = sh([sys.executable, "-m", "lm_eval", s, "--help"])
|
| 61 |
+
helps[s] = (rc, h)
|
| 62 |
+
print("SUBCOMMAND", s, "help rc", rc, "chars", len(h), flush=True)
|
| 63 |
+
|
| 64 |
+
best = max(helps.items(), key=lambda kv: len(kv[1][1]))[1][1]
|
| 65 |
+
missing = [f for f in OURS if f not in best]
|
| 66 |
+
print("FLAGS_CHECKED", json.dumps({f: (f in best) for f in OURS}), flush=True)
|
| 67 |
+
print("FLAGS_MISSING", missing, flush=True)
|
| 68 |
+
|
| 69 |
+
import yaml # shipped with the image and a dependency of lm_eval
|
| 70 |
+
|
| 71 |
+
tasks_dir = os.path.join(os.path.dirname(__import__("lm_eval").__file__), "tasks")
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def load_yaml(path):
|
| 75 |
+
try:
|
| 76 |
+
with open(path, encoding="utf-8") as fh:
|
| 77 |
+
d = yaml.safe_load(fh)
|
| 78 |
+
return d if isinstance(d, dict) else {}
|
| 79 |
+
except Exception:
|
| 80 |
+
return {}
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def resolve(path, depth=0, seen=None):
|
| 84 |
+
"""Merge a task YAML with whatever it `include`s, the way lm-eval does."""
|
| 85 |
+
seen = seen or set()
|
| 86 |
+
if path in seen or depth > 6 or not os.path.exists(path):
|
| 87 |
+
return {}
|
| 88 |
+
seen.add(path)
|
| 89 |
+
d = load_yaml(path)
|
| 90 |
+
inc = d.pop("include", None)
|
| 91 |
+
if inc:
|
| 92 |
+
base = os.path.normpath(os.path.join(os.path.dirname(path), inc))
|
| 93 |
+
parent = resolve(base + ".yaml", depth + 1, seen) or resolve(base, depth + 1, seen)
|
| 94 |
+
merged = dict(parent)
|
| 95 |
+
merged.update(d)
|
| 96 |
+
return merged
|
| 97 |
+
return d
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def find_yaml(name):
|
| 101 |
+
exact = os.path.join(tasks_dir, name + ".yaml")
|
| 102 |
+
if os.path.exists(exact):
|
| 103 |
+
return exact
|
| 104 |
+
hits = []
|
| 105 |
+
for pat in ("**/" + name + ".yaml", "**/" + name + "/*.yaml", "**/*.yaml"):
|
| 106 |
+
hits = [p for p in glob.glob(os.path.join(tasks_dir, pat), recursive=True)
|
| 107 |
+
if load_yaml(p).get("task") == name or os.path.basename(p) == name + ".yaml"]
|
| 108 |
+
if hits:
|
| 109 |
+
break
|
| 110 |
+
if not hits: # grouped configs declare their fields in a template the group members include
|
| 111 |
+
hits = [p for p in glob.glob(os.path.join(tasks_dir, "**", "*.yaml"), recursive=True)
|
| 112 |
+
if name in os.path.relpath(p, tasks_dir).replace("\\", "/").split("/")]
|
| 113 |
+
return hits[0] if hits else None
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
print("TASKS_DIR", tasks_dir, os.path.isdir(tasks_dir), flush=True)
|
| 117 |
+
report = {}
|
| 118 |
+
for t in TASKS:
|
| 119 |
+
y = find_yaml(t)
|
| 120 |
+
if not y:
|
| 121 |
+
report[t] = {"file": None}
|
| 122 |
+
continue
|
| 123 |
+
c = resolve(y)
|
| 124 |
+
metrics = [(m.get("metric_uri") if isinstance(m, dict) else m)
|
| 125 |
+
for m in (c.get("metric_list") or [])]
|
| 126 |
+
report[t] = {"file": os.path.relpath(y, tasks_dir), "num_fewshot": c.get("num_fewshot"),
|
| 127 |
+
"test_split": c.get("test_split"), "validation_split": c.get("validation_split"),
|
| 128 |
+
"fewshot_split": c.get("fewshot_split"), "metric_list": metrics,
|
| 129 |
+
"dataset_path": c.get("dataset_path"), "dataset_name": c.get("dataset_name"),
|
| 130 |
+
"metric_declared": PRIMARY_METRIC[t] in metrics}
|
| 131 |
+
print("YAML %-16s %-34s fewshot=%-6s test=%-10s val=%-9s fs=%-6s metric_declared=%s" % (
|
| 132 |
+
t, report[t]["file"], c.get("num_fewshot"), c.get("test_split"), c.get("validation_split"),
|
| 133 |
+
c.get("fewshot_split"), report[t]["metric_declared"]), flush=True)
|
| 134 |
+
|
| 135 |
+
ok = not missing and all(report[t].get("metric_declared") for t in TASKS) and \
|
| 136 |
+
all(report[t].get("file") for t in TASKS)
|
| 137 |
+
print("CLI_PROBE_JSON", json.dumps({"subcommands": {k: v[0] for k, v in helps.items()},
|
| 138 |
+
"flags_missing": missing, "tasks": report}, default=str)[:5000],
|
| 139 |
+
flush=True)
|
| 140 |
+
print("VERDICT P6_CLI_PROBE", "OK" if ok else "PROBLEM", "| missing_flags", missing,
|
| 141 |
+
"| metrics_not_declared", [t for t in TASKS if not report[t].get("metric_declared")], flush=True)
|
| 142 |
+
raise SystemExit(0 if ok else 7)
|