davidpomerenke commited on
Commit
eaa7534
Β·
verified Β·
1 Parent(s): b67f6cf

Upload from GitHub Actions: main: checkpoint per fully-evaluated model instead of once at the end

Browse files

The eval loop previously saved only after ALL batches finished, so a run
killed by the GitHub 6h runner cap (or any interruption) persisted nothing
while still being billed for the API calls.

Now evaluate one model at a time and push to HF after each model completes
its entire matrix. Two guarantees:
- progress survives interruption (next run skips fully-logged models), so
a large onboarding completes across multiple runs;
- a model only enters the published aggregate once fully covered AND
healthy (publishable_models excludes models past the auto-blocklist
failure threshold), so a sparse/broken model can't show an inflated mean.

Lifts publishable_models/checkpoint to module level; unit-tested the gate.

Files changed (1) hide show
  1. evals/main.py +109 -70
evals/main.py CHANGED
@@ -5,7 +5,11 @@ from os import environ
5
 
6
  import pandas as pd
7
  from languages import languages
8
- from models import models
 
 
 
 
9
  from rich import print
10
  from tasks import tasks
11
  from tqdm.asyncio import tqdm_asyncio
@@ -31,6 +35,60 @@ ALLOW_HF_PUSH_RESULTS = (
31
  and n_models >= CANONICAL_N_MODELS_FOR_PUSH
32
  )
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  async def evaluate():
35
  start_time = time.time()
36
 
@@ -63,82 +121,63 @@ async def evaluate():
63
  completed = set(completed_df.apply(tuple, axis=1))
64
  combis = combis[~combis.apply(lambda row: tuple(row) in completed, axis=1)]
65
 
66
- print(f"Running {len(combis)} evaluation tasks...")
67
-
68
- # batching (asyncio.gather + rate-limiting can in principle run everything at once, but in practice batching is more efficient / necessary)
69
- batch_size = 2000
70
- batch_results = [
71
- await tqdm_asyncio.gather(
72
- *[tasks[task_name](model, bcp_47, sentence_nr)
73
- for _, (task_name, model, bcp_47, sentence_nr) in batch.iterrows()]
74
- )
75
- for i in tqdm(range(0, len(combis), batch_size), colour='blue', desc='Batches')
76
- for batch in [combis[i:i + batch_size]]
77
- ]
78
- results = [r for batch in batch_results for result in batch for r in result]
79
- results = pd.DataFrame(results) if results else pd.DataFrame(columns=["task", "model", "bcp_47", "metric", "sentence_nr", "score", "origin"])
80
-
81
- # Defense-in-depth: if a huge fraction of NEW rows are errors, an
82
- # infrastructure problem (rate limits, regional ban, dependency outage)
83
- # likely degraded the run rather than the models actually being broken.
84
- # Refuse to push the polluted snapshot so auto_blocklist doesn't kick in
85
- # against innocent models on the next cycle. FatalAPIError already covers
86
- # auth/credit failures upstream β€” this is the catch-all for everything else.
87
- if not results.empty and "status" in results.columns:
88
- new_error_rate = (results["status"] != "ok").mean()
89
- if new_error_rate > 0.8:
90
- raise RuntimeError(
91
- f"Run produced {new_error_rate:.1%} errors in {len(results)} "
92
- f"new rows (threshold 80%). Likely an infra failure; refusing "
93
- f"to push to HF. Investigate the logs above and re-run when "
94
- f"resolved."
95
- )
96
 
97
- # Merge with cached results (immutable log, prefer latest results on conflict)
98
- all_results = pd.concat([old_results, results]).drop_duplicates(
99
- subset=["task", "model", "bcp_47", "metric", "sentence_nr"],
100
- keep="last",
101
- ) if not old_results.empty else results
102
-
103
- # Filter to current models Γ— languages and aggregate.
104
- # Only aggregate over successful evaluations (status == \"ok\" or missing).
105
  current_models = set(models.iloc[:n_models]["id"])
106
  current_languages = set(languages.head(n_languages)["bcp_47"])
107
- if "status" in all_results.columns:
108
- valid_mask = all_results["status"].isna() | (all_results["status"] == "ok")
109
- valid_results = all_results[valid_mask]
110
- else:
111
- valid_results = all_results
112
 
113
- results_agg = (
114
- valid_results[valid_results["model"].isin(current_models) & valid_results["bcp_47"].isin(current_languages)]
115
- .groupby(["model", "bcp_47", "task", "metric"])
116
- .agg({"score": "mean", "origin": "first"})
117
- .reset_index()
 
 
 
 
 
 
 
 
118
  )
119
-
120
- # results-detailed is append-merged (immutable log); safe to push from any scale.
121
- save(all_results, "results-detailed")
122
 
123
- # The aggregated tables are filtered by current_models Γ— current_languages,
124
- # so a partial-scale run (small N_LANGUAGES / N_MODELS) would truncate the
125
- # published view. Refuse to push in that case; write locally only.
126
- if ALLOW_HF_PUSH_RESULTS:
127
- save(results_agg, "results")
128
- save(models, "models")
129
- save(languages, "languages")
130
- else:
131
- print(
132
- f"[main] partial-scale run "
133
- f"(N_LANGUAGES={n_languages}, N_MODELS={n_models}); "
134
- f"writing aggregates LOCALLY only (skip HF push) to protect the "
135
- f"public dataset. Push from a full-scale run "
136
- f"(>={CANONICAL_N_LANGUAGES_FOR_PUSH} langs, "
137
- f">={CANONICAL_N_MODELS_FOR_PUSH} models)."
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  )
139
- save_local_only(results_agg, "results")
140
- save_local_only(models, "models")
141
- save_local_only(languages, "languages")
 
 
 
142
 
143
  elapsed = time.time() - start_time
144
  print(f"Evaluation completed in {str(timedelta(seconds=int(elapsed)))}")
 
5
 
6
  import pandas as pd
7
  from languages import languages
8
+ from models import (
9
+ models,
10
+ AUTO_BLOCKLIST_MIN_ATTEMPTS,
11
+ AUTO_BLOCKLIST_FAIL_PCT_THRESHOLD,
12
+ )
13
  from rich import print
14
  from tasks import tasks
15
  from tqdm.asyncio import tqdm_asyncio
 
35
  and n_models >= CANONICAL_N_MODELS_FOR_PUSH
36
  )
37
 
38
+ def publishable_models(log_df, cohort_models):
39
+ """Cohort models safe to include in the published aggregate: full coverage
40
+ (guaranteed because we only checkpoint completed models) AND an acceptable
41
+ success rate. A model that is mostly errors over a meaningful sample is
42
+ *broken*, not *low-scoring* β€” keep it out of the aggregate (auto_blocklist
43
+ drops it from the cohort on the next run). This is the guard that prevents
44
+ a sparsely-successful model from showing an inflated mean."""
45
+ if "status" not in log_df.columns:
46
+ return set(cohort_models)
47
+ keep = set()
48
+ in_cohort = log_df[log_df["model"].isin(cohort_models)]
49
+ for mid, grp in in_cohort.groupby("model"):
50
+ total = len(grp)
51
+ failed = (grp["status"] == "error").sum()
52
+ if (total >= AUTO_BLOCKLIST_MIN_ATTEMPTS
53
+ and failed / total * 100 >= AUTO_BLOCKLIST_FAIL_PCT_THRESHOLD):
54
+ continue
55
+ keep.add(mid)
56
+ return keep
57
+
58
+
59
+ def checkpoint(log_df, cohort_models, cohort_languages, note):
60
+ """Push the immutable log + the aggregate (current cohort, healthy
61
+ fully-covered models only) to HuggingFace (or locally if below scale)."""
62
+ if "status" in log_df.columns:
63
+ valid = log_df[log_df["status"].isna() | (log_df["status"] == "ok")]
64
+ else:
65
+ valid = log_df
66
+ keep = publishable_models(log_df, cohort_models)
67
+ agg = (
68
+ valid[valid["model"].isin(keep) & valid["bcp_47"].isin(cohort_languages)]
69
+ .groupby(["model", "bcp_47", "task", "metric"])
70
+ .agg({"score": "mean", "origin": "first"})
71
+ .reset_index()
72
+ )
73
+ # results-detailed is append-merged (immutable log); safe at any scale.
74
+ save(log_df, "results-detailed")
75
+ # The aggregated tables are filtered by cohort_models Γ— cohort_languages, so
76
+ # a partial-scale run would truncate the published view β€” push only from a
77
+ # full-scale run; otherwise local-only.
78
+ if ALLOW_HF_PUSH_RESULTS:
79
+ save(agg, "results")
80
+ save(models, "models")
81
+ save(languages, "languages")
82
+ else:
83
+ save_local_only(agg, "results")
84
+ save_local_only(models, "models")
85
+ save_local_only(languages, "languages")
86
+ print(f" βœ“ checkpoint after {note}: {len(keep)} models published, "
87
+ f"{len(log_df)} detailed rows "
88
+ f"({'HF' if ALLOW_HF_PUSH_RESULTS else 'local-only'})")
89
+ return agg
90
+
91
+
92
  async def evaluate():
93
  start_time = time.time()
94
 
 
121
  completed = set(completed_df.apply(tuple, axis=1))
122
  combis = combis[~combis.apply(lambda row: tuple(row) in completed, axis=1)]
123
 
124
+ print(f"Running {len(combis)} evaluation tasks across {combis['model'].nunique()} models...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
 
 
 
 
 
 
 
 
126
  current_models = set(models.iloc[:n_models]["id"])
127
  current_languages = set(languages.head(n_languages)["bcp_47"])
 
 
 
 
 
128
 
129
+ # We evaluate ONE MODEL AT A TIME and checkpoint to HuggingFace after each
130
+ # model finishes its ENTIRE matrix (every task Γ— language Γ— sentence
131
+ # attempted). This buys two things:
132
+ # 1. Progress survives interruption β€” the GitHub-hosted runner's hard 6h
133
+ # cap, or a local laptop sleeping. The next run skips models already
134
+ # fully logged (status=="ok" rows), so a large onboarding completes
135
+ # across however many runs it takes instead of losing everything.
136
+ # 2. A model only enters the PUBLISHED aggregate once it has full
137
+ # benchmark coverage, so a half-evaluated model can never show an
138
+ # inflated score from a sparse sample (the failure mode that once put
139
+ # a small model at rank #1).
140
+ all_results = old_results.copy() if not old_results.empty else pd.DataFrame(
141
+ columns=["task", "model", "bcp_47", "metric", "sentence_nr", "score", "origin", "status"]
142
  )
143
+ dedup_keys = ["task", "model", "bcp_47", "metric", "sentence_nr"]
144
+ batch_size = 2000
 
145
 
146
+ # Cohort order, so each checkpoint boundary is a fully-computed model.
147
+ # Models already fully cached have no pending combis and are skipped.
148
+ pending_models = [m for m in models.iloc[:n_models]["id"].tolist()
149
+ if (combis["model"] == m).any()]
150
+ results_agg = None
151
+ for mi, model_id in enumerate(pending_models, 1):
152
+ model_combis = combis[combis["model"] == model_id]
153
+ print(f"[{mi}/{len(pending_models)}] {model_id}: {len(model_combis)} new samples")
154
+ model_out = []
155
+ for i in tqdm(range(0, len(model_combis), batch_size),
156
+ colour="blue", desc=model_id):
157
+ batch = model_combis.iloc[i:i + batch_size]
158
+ batch_res = await tqdm_asyncio.gather(
159
+ *[tasks[task_name](model, bcp_47, sentence_nr)
160
+ for _, (task_name, model, bcp_47, sentence_nr) in batch.iterrows()]
161
+ )
162
+ model_out.extend(r for result in batch_res for r in result)
163
+ model_df = pd.DataFrame(model_out) if model_out else pd.DataFrame(columns=all_results.columns)
164
+
165
+ if not model_df.empty and "status" in model_df.columns:
166
+ err = (model_df["status"] != "ok").mean()
167
+ if err > 0.8:
168
+ # Logged so auto_blocklist sees it; publish-health filter keeps
169
+ # it out of the aggregate this run.
170
+ print(f" ⚠ {model_id}: {err:.0%} of new rows errored β€” logged, not published")
171
+
172
+ all_results = pd.concat([all_results, model_df]).drop_duplicates(
173
+ subset=dedup_keys, keep="last"
174
  )
175
+ results_agg = checkpoint(all_results, current_models, current_languages, model_id)
176
+
177
+ if results_agg is None:
178
+ # Everything was already cached β€” still refresh the published tables
179
+ # from the existing log (e.g. cohort/cost metadata may have changed).
180
+ results_agg = checkpoint(all_results, current_models, current_languages, "no new work")
181
 
182
  elapsed = time.time() - start_time
183
  print(f"Evaluation completed in {str(timedelta(seconds=int(elapsed)))}")