Download code/scripts/eval_checkpoint.py from viai957/align: direct link, hf CLI and curl.
- Browser
- Download file 8.24 kB
-
https://huggingface.co/viai957/align/resolve/main/code/scripts/eval_checkpoint.py
- Command line
-
hf download hf://viai957/align/code/scripts/eval_checkpoint.py
-
curl -L -o eval_checkpoint.py https://huggingface.co/viai957/align/resolve/main/code/scripts/eval_checkpoint.py
8.24 kB
| """Standalone checkpoint eval: fresh process, single GPU, golden-proven generator. | |
| Promotion of the .tmp/eval_gate.py driver (task #26) into a parameterized | |
| entry point. The design facts it carries forward are all incident-derived: | |
| * FRESH PROCESS, never in-trainer: the train->eval handoff inside one | |
| process produced the rank race (peers built a second 7B while rank 0 | |
| saved) and the orphan-GPU-process incident. A checkpoint on disk plus a | |
| fresh process has neither failure mode. | |
| * EVERYTHING under the __main__ guard: the verifier's scoring pool uses | |
| the 'spawn' context, which RE-IMPORTS __main__ in every worker -- the | |
| unguarded first driver had all 8 workers load their own 7B onto the | |
| eval GPU. | |
| * greedy generation via `golden_generate_fn` (in-repo Engine, same encode | |
| as the golden regression: BOS default, truncation 768). The anchors and | |
| banked SFT rows were measured with HF generate; the generator swap is | |
| certified by the golden regression + checkpoint roundtrip gate, and the | |
| generator is recorded in the report config. | |
| * Atomic report write (.partial -> os.replace): a half-written report | |
| masquerades as a complete one. | |
| Phase-0 usage (the Arm B result side of the primary McNemar): | |
| CUDA_VISIBLE_DEVICES=0 python scripts/eval_checkpoint.py \ | |
| --checkpoint runs/dpo_arm_b_response/checkpoints/micro_XXXXXXXX \ | |
| --name arm-b-final --gsm8k-n 1319 --math500-n 0 | |
| `--quality-gate` additionally runs the SFT-stage quality gate + ledger | |
| records; leave it off for non-SFT checkpoints (the gate's anchors are | |
| SFT-specific). | |
| """ | |
| import argparse | |
| import json | |
| import os | |
| import sys | |
| from pathlib import Path | |
| # Run from a checkout without an install: put the repo root on sys.path first. | |
| sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) | |
| from align.utils.guard import apply_resource_guards | |
| def quality_gate_size_errors(cfg, math500_n: int, gsm8k_n: int, per_set: dict | None = None) -> list[str]: | |
| """Why a run of these sizes may NOT write the SFT quality-gate receipt (empty = fine). | |
| The receipt unlocks DPO. `assert_meets_golden` accepts any point estimate inside the target's own | |
| Wilson CI, which is enormous at tiny n (1/5 passes the 0.09 anchor), so the gate certifies only a | |
| FULL-size evaluation: both sets, at the configured n the anchors were recorded at. `per_set` (the | |
| measured report) is checked too, in case a set held fewer problems than requested.""" | |
| want = {"math500": cfg.eval.math500_n, "gsm8k": cfg.eval.gsm8k_n} | |
| asked = {"math500": math500_n, "gsm8k": gsm8k_n} | |
| errs = [f"{k}: requested n={asked[k]}, the quality gate needs the full configured n={want[k]}" | |
| for k in want if asked[k] != want[k]] | |
| for k, w in want.items(): | |
| got = (per_set or {}).get(k, {}).get("n") if per_set is not None else w | |
| if got != w: | |
| errs.append(f"{k}: measured n={got}, the quality gate needs n={w}") | |
| return errs | |
| def main() -> int: | |
| parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) | |
| parser.add_argument("--checkpoint", required=True, help="checkpoint dir (HF-layout safetensors dir)") | |
| parser.add_argument("--config", default="configs/sft.toml") | |
| parser.add_argument("--name", required=True, help="report model_name and output stem") | |
| parser.add_argument("--out", default=None, help="default: outputs/eval/<name>.json") | |
| parser.add_argument("--math500-n", type=int, default=None, help="0 skips; default: config") | |
| parser.add_argument("--gsm8k-n", type=int, default=None, help="0 skips; default: config") | |
| parser.add_argument( | |
| "--quality-gate", action="store_true", help="SFT-stage gate + ledger records" | |
| ) | |
| args = parser.parse_args() | |
| # Guards BEFORE torch import (env vars are load-bearing); heavy=True takes | |
| # the exclusive job lock -- a 7B generation eval over 1,319 problems is a | |
| # multi-hour GPU job and must serialise behind training like any other. | |
| apply_resource_guards(cpu_threads=8, heavy=True, label=f"eval-{args.name}") | |
| from align.config import load_config # noqa: E402 | |
| from align.eval.golden import golden_generate_fn, load_checkpoint_model # noqa: E402 | |
| from align.eval.harness import EvalConfig, load_eval_set, run_eval # noqa: E402 | |
| from align.eval.verifier import OutcomeVerifier # noqa: E402 | |
| from align.tokenizer import load_tokenizer # noqa: E402 | |
| from align.train.stage_smoke import _manifest_hashes, _mde # noqa: E402 | |
| cfg = load_config(args.config) | |
| checkpoint = args.checkpoint | |
| assert os.path.isdir(checkpoint), checkpoint | |
| out_path = args.out or os.path.join("outputs", "eval", f"{args.name}.json") | |
| math500_n = cfg.eval.math500_n if args.math500_n is None else args.math500_n | |
| gsm8k_n = cfg.eval.gsm8k_n if args.gsm8k_n is None else args.gsm8k_n | |
| if args.quality_gate: | |
| errs = quality_gate_size_errors(cfg, math500_n, gsm8k_n) | |
| if errs: | |
| print("refusing --quality-gate on a subset eval (the receipt unlocks DPO):\n " + "\n ".join(errs), | |
| file=sys.stderr) | |
| return 2 | |
| problems = [] | |
| # 0 in this CLI means SKIP the set (checked here); upstream load_eval_set | |
| # uses limit=0 for "no limit", which this branch structure never passes | |
| # ambiguously -- a negative value falls through to upstream validation. | |
| if math500_n != 0: | |
| problems += load_eval_set(cfg.paths.math500_path, limit=math500_n) | |
| if gsm8k_n != 0: | |
| problems += load_eval_set(cfg.paths.gsm8k_path, limit=gsm8k_n) | |
| if not problems: | |
| print("nothing to evaluate (both sets skipped)", file=sys.stderr) | |
| return 2 | |
| # After every refusal above: a 7B load is the expensive step. | |
| tokenizer = load_tokenizer(checkpoint) | |
| model = load_checkpoint_model(checkpoint, device="cuda") | |
| verifier = OutcomeVerifier() | |
| gen_fn = golden_generate_fn(model, tokenizer, "cuda") | |
| print(f"evaluating {args.name}: {len(problems)} problems from {checkpoint}", flush=True) | |
| rep = run_eval(problems, gen_fn, verifier, EvalConfig(), model_name=args.name) | |
| per_set = rep.per_set | |
| for name, scores in sorted(per_set.items()): | |
| print(f" {name}: n={scores.get('n')} pass@1={scores.get('pass@1')}", flush=True) | |
| report = { | |
| "per_set": per_set, | |
| "config": { | |
| "mde": _mde(cfg), | |
| "n_problems": sum(r.get("n", 0) for r in per_set.values()), | |
| "eval_set_hash": _manifest_hashes(cfg), | |
| "generator": "align Engine greedy (golden_generate_fn)", | |
| "checkpoint": checkpoint, | |
| }, | |
| "pooled": rep.pooled, | |
| "per_problem": rep.per_problem, | |
| } | |
| os.makedirs(os.path.dirname(out_path), exist_ok=True) | |
| tmp = out_path + ".partial" | |
| with open(tmp, "w") as f: | |
| json.dump(report, f, indent=1, default=str) | |
| os.replace(tmp, out_path) | |
| print(f"report written: {out_path}", flush=True) | |
| if not args.quality_gate: | |
| return 0 | |
| from align.train.stage_gates import ( # noqa: E402 | |
| GateLedger, | |
| eval_source_hash, | |
| quality_gate, | |
| write_quality_gate_receipt, | |
| ) | |
| errs = quality_gate_size_errors(cfg, math500_n, gsm8k_n, per_set=per_set) | |
| if errs: # report is already written above; only the receipt is withheld | |
| print("refusing to write the quality-gate receipt:\n " + "\n ".join(errs), file=sys.stderr) | |
| return 2 | |
| run_dir = os.path.join(cfg.paths.checkpoint_dir, "sft") | |
| ledger = GateLedger(run_dir, eval_source_hash=eval_source_hash()) | |
| ledger.record("eval", passed=True, detail=json.dumps(per_set, default=str)) | |
| decision = quality_gate(report, cfg) | |
| ledger.record("quality_gate", passed=decision.passed, detail=decision.halt_message[:400]) | |
| write_quality_gate_receipt(cfg, decision, checkpoint=checkpoint) | |
| print(f"QUALITY GATE: passed={decision.passed}", flush=True) | |
| if not decision.passed: | |
| print(decision.halt_message, file=sys.stderr, flush=True) | |
| return 0 if decision.passed else 1 | |
| if __name__ == "__main__": | |
| # See module docstring: spawn workers re-import __main__; nothing above | |
| # this guard may build a model or touch the GPU. | |
| sys.exit(main()) | |