File size: 8,472 Bytes
4ae3b85 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | #!/usr/bin/env python3
"""Offline verification reconciler / trigger (§5.7).
Run by an admin whose HF token can write the central bucket, read the private
audit bucket, and launch org jobs (the same token the Space holds):
HF_TOKEN=hf_... python scripts/verify_submissions.py reconcile [--dry-run]
HF_TOKEN=hf_... python scripts/verify_submissions.py trigger [--dry-run] [--limit N]
``reconcile`` is the restart-safety net for the in-Space verifier: its watcher
threads are in-memory (DESIGN §1), so a Space restart mid-verification loses
the verdict even though the job finishes and writes ``summary.json``. This
mode scans ``verification_runs/*`` in the audit bucket for completed runs
whose index entry is still ``pending``/absent, then computes the verdict,
writes it through the SAME compare-and-set (human verdicts win), and posts the
same announcement — all via ``app.verifier``, one canonical implementation, so
online and offline behavior cannot drift. Idempotent: a run already recorded
(or human-decided) is skipped, so it is safe to re-run or schedule.
``trigger`` is the backfill for results that were posted before the trigger
existed (or whose launch failed): it walks ``agent-run`` results from the
highest claimed TPS down and fires the same ``maybe_trigger`` the POST hook
uses. Launches run SYNCHRONOUSLY (the script polls the job to terminal state,
~15–40 min each), and each recorded ``valid`` raises the champion bar for the
candidates after it — champion-search semantics. This mode SPENDS org credits;
use ``--dry-run`` first.
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from app.config import Settings # noqa: E402
from app.hub import HubClient # noqa: E402
from app.jobs import JobRunner # noqa: E402
from app.naming import agent_from_filename # noqa: E402
from app.read_model import ReadModel # noqa: E402
from app.verification import PENDING, VerificationStatusStore # noqa: E402
from app.verifier import Verifier, compute_verdict # noqa: E402
def build_verifier(settings: Settings) -> tuple[Verifier, HubClient, ReadModel]:
hub = HubClient(settings)
read_model = ReadModel(hub, settings)
verification = VerificationStatusStore(
hub, runs_prefix=settings.verification_runs_prefix
)
runner = JobRunner(settings, hub)
# Inline spawn: watchers run synchronously so the script doesn't exit
# while a job is still being supervised.
verifier = Verifier(
settings, hub, read_model, verification, runner,
spawn=lambda _name, fn: fn(),
)
return verifier, hub, read_model
def _run_dirs(hub: HubClient, settings: Settings) -> dict[str, set[str]]:
"""{result filename: {leaf files in its verification run dir}}."""
prefix = settings.verification_runs_prefix
out: dict[str, set[str]] = {}
for e in hub.list_bucket_dir(settings.audit_bucket, prefix):
rel = e.rel_path[len(prefix) + 1 :]
if "/" not in rel:
continue
filename, leaf = rel.split("/", 1)
out.setdefault(filename, set()).add(leaf)
return out
def _reported_tps(
hub: HubClient,
read_model: ReadModel,
settings: Settings,
filename: str,
) -> float | None:
"""The TPS the result claimed: from verification_request.json (written at
launch), falling back to the promoted result's frontmatter."""
raw = hub.read_audit_bytes(
f"{settings.verification_runs_prefix}/{filename}/verification_request.json"
)
if raw is not None:
try:
tps = json.loads(raw.decode("utf-8")).get("reported_tps")
if isinstance(tps, (int, float)) and not isinstance(tps, bool) and tps > 0:
return float(tps)
except (json.JSONDecodeError, UnicodeDecodeError):
pass
rec = read_model.record("results", filename)
if rec is not None:
tps = rec.frontmatter.get("tps")
if isinstance(tps, (int, float)) and not isinstance(tps, bool) and tps > 0:
return float(tps)
return None
def reconcile(settings: Settings, dry_run: bool) -> int:
verifier, hub, read_model = build_verifier(settings)
index = read_model.verification_index()
runs = _run_dirs(hub, settings)
print(f"run_dirs={len(runs)} indexed={len(index)}")
healed = skipped = undecided = 0
for filename, leaves in sorted(runs.items()):
state = index.get(filename, PENDING)
if state != PENDING:
skipped += 1
continue
if "summary.json" not in leaves:
print(f" {filename}: no summary.json yet (job running/failed); skipping")
skipped += 1
continue
raw = hub.read_audit_bytes(
f"{settings.verification_runs_prefix}/{filename}/summary.json"
)
if raw is None:
skipped += 1
continue
try:
summary = json.loads(raw.decode("utf-8"))
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
print(f" {filename}: unparseable summary.json ({exc}); skipping")
skipped += 1
continue
reported_tps = _reported_tps(hub, read_model, settings, filename)
if reported_tps is None:
print(f" {filename}: no reported TPS on record; skipping")
skipped += 1
continue
owner = agent_from_filename(filename) or ""
if dry_run:
verdict, details = compute_verdict(
reported_tps, summary, tps_tol=settings.tps_tol, ppl_cap=settings.ppl_cap
)
print(f" {filename}: would record {verdict} ({details})")
healed += verdict is not None
undecided += verdict is None
continue
verdict = verifier.record_verdict(
filename,
owner=owner,
reported_tps=reported_tps,
summary=summary,
job_id=summary.get("job_id"),
)
print(f" {filename}: {verdict or 'left pending / deferred'}")
healed += verdict is not None
undecided += verdict is None
print(f"healed={healed} undecided={undecided} skipped={skipped}")
return 0
def _claimed_tps(record) -> float:
tps = record.frontmatter.get("tps")
if isinstance(tps, bool) or not isinstance(tps, (int, float)):
return 0.0
return float(tps)
def trigger(settings: Settings, dry_run: bool, limit: int | None) -> int:
verifier, _hub, read_model = build_verifier(settings)
records = read_model.records("results")
candidates = sorted(
(r for r in records if not r.parse_error), key=_claimed_tps, reverse=True
)
launched = 0
for r in candidates:
if limit is not None and launched >= limit:
break
# Re-check per candidate: each recorded `valid` raises the bar (T*)
# for everything after it — champion search from the top claim down.
if not verifier.should_verify(r.filename, r.frontmatter):
continue
if dry_run:
print(f" would verify {r.filename} (tps={r.frontmatter.get('tps')})")
launched += 1
continue
print(f" verifying {r.filename} (tps={r.frontmatter.get('tps')}) ...")
if verifier.maybe_trigger(r.filename, r.frontmatter):
launched += 1
print(f"{'would launch' if dry_run else 'launched'}={launched}")
return 0
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
sub = ap.add_subparsers(dest="mode", required=True)
p_rec = sub.add_parser("reconcile", help="record verdicts for completed runs")
p_rec.add_argument("--dry-run", action="store_true", help="report only, write nothing")
p_trg = sub.add_parser("trigger", help="launch verification for unverified SOTA claims")
p_trg.add_argument("--dry-run", action="store_true", help="report only, launch nothing")
p_trg.add_argument("--limit", type=int, default=None, help="max launches this run")
args = ap.parse_args()
# The offline tool is always 'enabled' — the env flag gates only the
# in-Space trigger.
settings = Settings(VERIFIER_ENABLED=True)
if args.mode == "reconcile":
return reconcile(settings, args.dry_run)
return trigger(settings, args.dry_run, args.limit)
if __name__ == "__main__":
raise SystemExit(main())
|