Cion-lab commited on
Commit
48949db
·
verified ·
1 Parent(s): ad9e5a2

p6_cli_probe: check run_benchmarks' flag list and PRIMARY_METRIC against the installed 0.4.13 CLI and its shipped YAMLs

Browse files
Files changed (1) hide show
  1. 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)