davidpomerenke commited on
Commit
608b646
Β·
verified Β·
1 Parent(s): e94f7b1

Upload from GitHub Actions: blocklist: drop the grace for slow failing models, not just egregious ones

Browse files

Per feedback: the 2-run grace is only worth it when retrying is cheap. main.py
now measures per-model seconds/sample during the run and flags rate-limited
models (> AUTO_BLOCKLIST_SLOW_SEC_PER_SAMPLE = 0.3s/sample); update_blocklist_
strikes excludes a failing model after ONE run if it's egregious (>=80%) OR
slow. The grace now only protects a fast, moderately-failing model β€” i.e. retry
only when the time investment is reasonable.

Files changed (2) hide show
  1. evals/main.py +16 -1
  2. evals/models.py +22 -10
evals/main.py CHANGED
@@ -9,6 +9,7 @@ from models import (
9
  models,
10
  AUTO_BLOCKLIST_MIN_ATTEMPTS,
11
  AUTO_BLOCKLIST_FAIL_PCT_THRESHOLD,
 
12
  update_blocklist_strikes,
13
  FatalAPIError,
14
  )
@@ -177,6 +178,10 @@ async def evaluate():
177
 
178
  results_agg = None
179
  budget_hit = False
 
 
 
 
180
  for mi, model_id in enumerate(pending_models, 1):
181
  # Don't START a new model once over budget β€” stop cleanly between models.
182
  if over_budget():
@@ -186,9 +191,12 @@ async def evaluate():
186
  model_combis = combis[combis["model"] == model_id]
187
  print(f"[{mi}/{len(pending_models)}] {model_id}: {len(model_combis)} new samples")
188
  model_out = []
 
 
189
  for i in tqdm(range(0, len(model_combis), batch_size),
190
  colour="blue", desc=model_id):
191
  batch = model_combis.iloc[i:i + batch_size]
 
192
  rows = [(t, m, b, s) for _, (t, m, b, s) in batch.iterrows()]
193
  # A single combo that throws (e.g. a flaky HF dataset download) must
194
  # NOT crash a multi-hour run. tqdm_asyncio.gather has no
@@ -214,6 +222,13 @@ async def evaluate():
214
  if over_budget():
215
  budget_hit = True
216
  break
 
 
 
 
 
 
 
217
  model_df = pd.DataFrame(model_out) if model_out else pd.DataFrame(columns=all_results.columns)
218
  # Persist whatever completed (full model, or partial batches if the
219
  # budget was hit). Partial work is saved to results-detailed so its
@@ -248,7 +263,7 @@ async def evaluate():
248
  # auto-blocklisted after staying broken across runs β€” not after a single run
249
  # where a provider may have rate-limited us. Non-fatal if it fails.
250
  try:
251
- update_blocklist_strikes(all_results)
252
  except Exception as e:
253
  print(f"[main] could not update blocklist strikes: {e}")
254
 
 
9
  models,
10
  AUTO_BLOCKLIST_MIN_ATTEMPTS,
11
  AUTO_BLOCKLIST_FAIL_PCT_THRESHOLD,
12
+ AUTO_BLOCKLIST_SLOW_SEC_PER_SAMPLE,
13
  update_blocklist_strikes,
14
  FatalAPIError,
15
  )
 
178
 
179
  results_agg = None
180
  budget_hit = False
181
+ # Models that ran slowly this run (heavily rate-limited). If they're also
182
+ # failing, they're excluded without a grace re-attempt β€” retrying an
183
+ # expensive failure isn't worth the time/money.
184
+ slow_models = set()
185
  for mi, model_id in enumerate(pending_models, 1):
186
  # Don't START a new model once over budget β€” stop cleanly between models.
187
  if over_budget():
 
191
  model_combis = combis[combis["model"] == model_id]
192
  print(f"[{mi}/{len(pending_models)}] {model_id}: {len(model_combis)} new samples")
193
  model_out = []
194
+ model_started = time.time()
195
+ attempted = 0
196
  for i in tqdm(range(0, len(model_combis), batch_size),
197
  colour="blue", desc=model_id):
198
  batch = model_combis.iloc[i:i + batch_size]
199
+ attempted += len(batch)
200
  rows = [(t, m, b, s) for _, (t, m, b, s) in batch.iterrows()]
201
  # A single combo that throws (e.g. a flaky HF dataset download) must
202
  # NOT crash a multi-hour run. tqdm_asyncio.gather has no
 
222
  if over_budget():
223
  budget_hit = True
224
  break
225
+ # Flag the model as slow if it took disproportionately long per sample
226
+ # this run (rate-limited). Needs a meaningful sample count to be reliable.
227
+ sec_per_sample = (time.time() - model_started) / max(attempted, 1)
228
+ if attempted >= 100 and sec_per_sample > AUTO_BLOCKLIST_SLOW_SEC_PER_SAMPLE:
229
+ slow_models.add(model_id)
230
+ print(f" ⏱ {model_id}: {sec_per_sample:.2f}s/sample (slow / rate-limited)")
231
+
232
  model_df = pd.DataFrame(model_out) if model_out else pd.DataFrame(columns=all_results.columns)
233
  # Persist whatever completed (full model, or partial batches if the
234
  # budget was hit). Partial work is saved to results-detailed so its
 
263
  # auto-blocklisted after staying broken across runs β€” not after a single run
264
  # where a provider may have rate-limited us. Non-fatal if it fails.
265
  try:
266
+ update_blocklist_strikes(all_results, slow_models=slow_models)
267
  except Exception as e:
268
  print(f"[main] could not update blocklist strikes: {e}")
269
 
evals/models.py CHANGED
@@ -589,10 +589,16 @@ AUTO_BLOCKLIST_FAIL_PCT_THRESHOLD = 50.0
589
  AUTO_BLOCKLIST_MIN_RUNS = 2
590
  # Fast path: a model failing this badly is broken/unusable, not transiently
591
  # rate-limited β€” exclude it after a SINGLE bad run rather than waiting out the
592
- # grace period, so we don't keep spending on it. The grace only protects the
593
- # borderline band (FAIL_PCT_THRESHOLD .. IMMEDIATE_FAIL_PCT) where a provider
594
- # outage is a plausible explanation.
595
  AUTO_BLOCKLIST_IMMEDIATE_FAIL_PCT = 80.0
 
 
 
 
 
 
 
 
596
 
597
 
598
  def compute_model_health(detailed=None) -> pd.DataFrame:
@@ -639,15 +645,20 @@ def load_auto_blocklist(date: date) -> list[str]:
639
  )
640
 
641
 
642
- def update_blocklist_strikes(detailed=None) -> pd.DataFrame:
643
  """Recompute consecutive-bad-run strikes and persist them to HF. Called once
644
  per eval run (from main.py) after results are merged. A model currently past
645
  the failure threshold gets +1 strike; a model that has recovered (or never
646
  failed) drops to 0 and out of the table. Models reaching AUTO_BLOCKLIST_MIN_RUNS
647
- strikes are excluded by load_auto_blocklist on the NEXT run β€” so every model
648
- gets at least one re-attempt before exclusion."""
 
 
 
 
649
  from datasets_.util import load, save
650
 
 
651
  health = compute_model_health(detailed)
652
  cols = ["model", "strikes", "failed_pct"]
653
  if health.empty:
@@ -668,11 +679,12 @@ def update_blocklist_strikes(detailed=None) -> pd.DataFrame:
668
  fail_map = dict(zip(bad["model"], bad["failed_pct"]))
669
 
670
  def _strikes_for(model, fail_pct):
671
- # Egregious failure β†’ jump straight to the exclusion threshold (no
672
- # grace). Otherwise increment so the borderline band needs MIN_RUNS
673
- # consecutive bad runs.
674
  incremented = int(prior_map.get(model, 0)) + 1
675
- if fail_pct >= AUTO_BLOCKLIST_IMMEDIATE_FAIL_PCT:
 
676
  return max(incremented, AUTO_BLOCKLIST_MIN_RUNS)
677
  return incremented
678
 
 
589
  AUTO_BLOCKLIST_MIN_RUNS = 2
590
  # Fast path: a model failing this badly is broken/unusable, not transiently
591
  # rate-limited β€” exclude it after a SINGLE bad run rather than waiting out the
592
+ # grace period, so we don't keep spending on it.
 
 
593
  AUTO_BLOCKLIST_IMMEDIATE_FAIL_PCT = 80.0
594
+ # The 2-run grace is only worth it when retrying is CHEAP. A model that is
595
+ # failing AND slow (heavily rate-limited) is expensive to retry and unlikely to
596
+ # recover, so it's excluded immediately too. "Slow" = wall-clock seconds per
597
+ # attempted sample above this, measured during the run. Healthy models run near
598
+ # the 20 req/s limit (~0.05 s/sample); a rate-limited one is many times slower
599
+ # (e.g. kimi-k2.6 ran ~0.7 s/sample). So the grace effectively only protects a
600
+ # FAST, moderately-failing model β€” exactly "retry only if cheap to retry".
601
+ AUTO_BLOCKLIST_SLOW_SEC_PER_SAMPLE = 0.3
602
 
603
 
604
  def compute_model_health(detailed=None) -> pd.DataFrame:
 
645
  )
646
 
647
 
648
+ def update_blocklist_strikes(detailed=None, slow_models=None) -> pd.DataFrame:
649
  """Recompute consecutive-bad-run strikes and persist them to HF. Called once
650
  per eval run (from main.py) after results are merged. A model currently past
651
  the failure threshold gets +1 strike; a model that has recovered (or never
652
  failed) drops to 0 and out of the table. Models reaching AUTO_BLOCKLIST_MIN_RUNS
653
+ strikes are excluded by load_auto_blocklist on the NEXT run.
654
+
655
+ The 2-run grace (one free re-attempt) only applies when retrying is cheap.
656
+ A failing model that is ALSO egregiously bad (>=80%) or SLOW (in
657
+ `slow_models`, measured this run) is excluded after a single run β€” we don't
658
+ spend more time/money re-attempting an expensive failure."""
659
  from datasets_.util import load, save
660
 
661
+ slow_models = slow_models or set()
662
  health = compute_model_health(detailed)
663
  cols = ["model", "strikes", "failed_pct"]
664
  if health.empty:
 
679
  fail_map = dict(zip(bad["model"], bad["failed_pct"]))
680
 
681
  def _strikes_for(model, fail_pct):
682
+ # No grace if the failure is egregious OR slow-and-expensive-to-retry;
683
+ # otherwise increment so a fast, moderately-failing model gets one free
684
+ # re-attempt before exclusion.
685
  incremented = int(prior_map.get(model, 0)) + 1
686
+ no_grace = fail_pct >= AUTO_BLOCKLIST_IMMEDIATE_FAIL_PCT or model in slow_models
687
+ if no_grace:
688
  return max(incremented, AUTO_BLOCKLIST_MIN_RUNS)
689
  return incremented
690