File size: 4,829 Bytes
63f41a6 | 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 | #!/usr/bin/env python
"""Single-turn on-policy RL for Lean provers (lane R). Library: align/rl/lean_rl.py, align/rl/lean_rl_run.py.
# the iteration loop (CPU-only parent; spawns generation, verification and training children in series)
.venv/bin/python scripts/p2/rl_lean.py run configs/p2_rl_lean.toml \\
--set run_dir="RUN" --set problems="POOL.jsonl" [--nproc 4] [--until K] [--dry-run]
# one training iteration (what `run` launches under torchrun; also the Phase 0b probe, docs/RL_LEAN.md)
torchrun --standalone --nproc_per_node=4 scripts/p2/rl_lean.py train-iter CFG --iteration K [--set ...]
# K = -1 writes checkpoint 0 (fresh adapter, fresh optimizer)
# build the loss-mode contrast on the SAME cached rollouts (K1): a second run with loss.mode="ei"
... run CFG --set run_dir="RUN_EI" --set replay_from="RUN" --set 'loss.mode="ei"'
Resumable: re-running `run` continues from the last committed checkpoint; every stage commits atomically.
The parent never takes the heavy-job lock itself: each child does (lean_gen --wait-lock, verify_generations when
workers > 2, train-iter on rank 0), so exactly one heavy child exists at any time.
IMPORT ORDER IS LOAD-BEARING (as in scripts/sft.py): the guard runs before anything imports torch.
"""
from __future__ import annotations
import argparse
import json
import logging
import os
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(REPO))
from align.utils.guard import apply_resource_guards # noqa: E402
def parse_args(argv=None):
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
sub = ap.add_subparsers(dest="cmd", required=True)
for name in ("run", "train-iter"):
p = sub.add_parser(name)
p.add_argument("config")
p.add_argument("--set", dest="overrides", action="append", default=[], metavar="KEY=TOML_VALUE")
if name == "run":
p.add_argument("--nproc", type=int, default=4, help="training ranks (torchrun --nproc_per_node)")
p.add_argument("--until", type=int, default=None, help="stop after this many iterations (resume continues)")
p.add_argument("--dry-run", action="store_true", help="print iteration 0's child command lines, touch nothing")
else:
p.add_argument("--iteration", type=int, required=True, help="-1 = write checkpoint 0")
return ap.parse_args(argv)
def cmd_run(a) -> int:
from align.rl import lean_rl as L
from align.rl import lean_rl_run as R
cfg = L.load_config(a.config, a.overrides)
if not cfg.run_dir or not cfg.problems:
raise SystemExit('run_dir and problems must be set (--set \'run_dir="..."\' --set \'problems="..."\')')
gen, ver = R.LeanGenCliGenerator(cfg), R.VerifyCliVerifier(cfg)
trainer = R.TorchrunTrainer(cfg, os.path.abspath(a.config), a.nproc, a.overrides)
if a.dry_run:
d = R.iter_dir(cfg.run_dir, 0)
print(json.dumps({
"fingerprint": cfg.fingerprint(),
"init": R.torchrun_command(trainer.cfg_path, -1, a.nproc, a.overrides),
"gen": gen.command(0, d, os.path.join(d, "problems.jsonl"), os.path.join(R.ckpt_path(cfg.run_dir, 0), "adapter"),
os.path.join(R.ckpt_path(cfg.run_dir, 0), "merged") if cfg.export_merged else None,
R.derive_seed(cfg.seed, 0)),
"verify": ver.command(d, os.path.join(d, "problems.jsonl")),
"train": R.torchrun_command(trainer.cfg_path, 0, a.nproc, a.overrides)}, indent=1))
return 0
ledger = R.run_loop(cfg, gen, ver, trainer, until=a.until)
print(json.dumps({"iterations_committed": len(ledger), "cumulative": ledger[-1]["cumulative"] if ledger else {}}))
return 0
def cmd_train_iter(a) -> int:
from align.rl import lean_rl as L
cfg = L.load_config(a.config, a.overrides)
apply_resource_guards(cpu_threads=8, heavy=True, label=f"rl-lean-train-{a.iteration}")
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
from align.utils.dist import init_distributed
init_distributed(run_dir=os.path.join(cfg.run_dir, "nccl"))
from align.rl import lean_rl_run as R
trainer = R.InProcessTrainer(cfg, lambda: R.build_real_policy(cfg))
if a.iteration < 0:
trainer.init()
else:
R.clean_partial(cfg.run_dir) if R._rank() == 0 else None
trainer.train(a.iteration)
return 0
def main(argv=None) -> int:
a = parse_args(argv)
if a.cmd == "run":
apply_resource_guards(cpu_threads=2, heavy=False, label="rl-lean-orchestrator")
return cmd_run(a)
return cmd_train_iter(a)
if __name__ == "__main__":
raise SystemExit(main())
|