from __future__ import annotations import argparse import json import os import subprocess import sys from pathlib import Path from .constants import ( DEFAULT_ADAPTER_DIR, DEFAULT_DATA_ROOT, DEFAULT_DPO_ADAPTER_DIR, DEFAULT_DPO_EXPORT_DIR, DEFAULT_DPO_PACK, DEFAULT_EXPORT_DIR, DEFAULT_PACK_DIR, DEFAULT_RL_ADAPTER_DIR, DEFAULT_RL_EXPORT_DIR, GENESIS_DIR, ) def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description="Albedo challenger SFT → export → duel → GRPO") sub = parser.add_subparsers(dest="cmd", required=True) p = sub.add_parser("pack", help="build the SFT jsonl pack from official parquet") p.add_argument("--dataset-root", type=Path, default=DEFAULT_DATA_ROOT) p.add_argument("--out-dir", type=Path, default=DEFAULT_PACK_DIR) p.add_argument("--max-examples", type=int, default=20_000) p.add_argument("--seed", default="sft-pack") p.add_argument("--n-salts", type=int, default=2) p.add_argument("--submit-frac", type=float, default=0.20) p.add_argument("--edit-frac", type=float, default=0.35) p.add_argument( "--expand-submit-salts", action="store_true", help="repeat each gold submit under every salt so all markers/tails are seen", ) p.add_argument("--smoke", action="store_true", help="64-example dry pack") p = sub.add_parser("sft", help="LoRA SFT from genesis on a packed jsonl") p.add_argument("--pack", type=Path, default=None) p.add_argument("--out-dir", type=Path, default=DEFAULT_ADAPTER_DIR) p.add_argument("--model", type=Path, default=GENESIS_DIR) p.add_argument("--max-steps", type=int, default=None) p.add_argument("--epochs", type=float, default=1.0) p.add_argument("--max-seq-len", type=int, default=16384) p.add_argument("--lr", type=float, default=1e-4) p.add_argument("--smoke", action="store_true") p = sub.add_parser("export", help="merge LoRA and copy genesis metadata byte-for-byte") p.add_argument("--adapter", type=Path, default=DEFAULT_ADAPTER_DIR) p.add_argument("--genesis", type=Path, default=GENESIS_DIR) p.add_argument("--out-dir", type=Path, default=DEFAULT_EXPORT_DIR) p.add_argument("--base", type=Path, default=None) p = sub.add_parser("check", help="sha256 metadata vs genesis + albedo check-model") p.add_argument("--path", type=Path, required=True) p.add_argument("--genesis", type=Path, default=GENESIS_DIR) p = sub.add_parser("duel", help="local_eval challenger vs genesis + go/no-go") p.add_argument("--challenger", type=Path, default=DEFAULT_EXPORT_DIR) p.add_argument("--king", type=Path, default=GENESIS_DIR) p.add_argument("--samples", type=int, default=8) p.add_argument("--turns", type=int, default=12) p.add_argument("--seed", default="local-eval") p.add_argument("--skip-king", action="store_true") p = sub.add_parser("chain-pack", help="SFT rows for live sanity chain (edit→submit→reject→edit→submit)") p.add_argument("--dataset-root", type=Path, default=DEFAULT_DATA_ROOT) p.add_argument("--out-dir", type=Path, default=DEFAULT_PACK_DIR) p.add_argument("--max-groups", type=int, default=200) p.add_argument("--seed", default="chain-pack") p.add_argument("--n-salts", type=int, default=3) p.add_argument("--mix-pack", type=Path, default=None) p.add_argument("--mix-n", type=int, default=2000) p.add_argument( "--mix-edit-n", type=int, default=0, help="official at_edit rows to mix (Stage A). Overrides --mix-n when set with --mix-submit-n", ) p.add_argument( "--mix-submit-n", type=int, default=0, help="official submit-after-edit rows to mix (Stage A)", ) p.add_argument("--smoke", action="store_true") p = sub.add_parser( "chain-gold", help="compile 6-turn sessions that pass evaluate_chain, then slice for SFT", ) p.add_argument("--dataset-root", type=Path, default=DEFAULT_DATA_ROOT) p.add_argument("--out-dir", type=Path, default=DEFAULT_PACK_DIR) p.add_argument("--max-groups", type=int, default=150) p.add_argument("--seed", default="chain-gold") p.add_argument("--n-salts", type=int, default=3) p.add_argument("--mix-pack", type=Path, default=None) p.add_argument("--mix-edit-n", type=int, default=800) p.add_argument( "--mix-submit-n", type=int, default=0, help="official post-edit submit rows to mix (keep small vs edits)", ) p.add_argument("--smoke", action="store_true") p = sub.add_parser("chain", help="local live-sanity chain eval (reject first submit)") p.add_argument("--challenger", type=Path, default=DEFAULT_RL_EXPORT_DIR) p.add_argument( "--samples", type=int, default=6, help="3 live-gate files first, then random extras (default 6 = 3+3 transfer)", ) p.add_argument("--turns", type=int, default=32) p.add_argument("--seed", default="chain-eval") p.add_argument( "--no-live-gate", action="store_true", help="skip the 3 oversampled gate files; draw random samples only", ) p = sub.add_parser( "dpo-pack", help="chosen/rejected pairs for the live empty-submit policy (not the local 3 files)", ) p.add_argument("--out-dir", type=Path, default=DEFAULT_PACK_DIR) p.add_argument("--seed", default="dpo-live-fault-v16") p.add_argument( "--narrow", action="store_true", help="only after-submit empty-echo pairs on toml/config paths (v16 random-6 fails)", ) p = sub.add_parser("dpo", help="LoRA DPO on live-fault pairs (prefer this over GRPO)") p.add_argument("--pack", type=Path, default=DEFAULT_DPO_PACK) p.add_argument("--model", type=Path, default=Path("/workspace/models/albedo-challenger-v15")) p.add_argument("--out-dir", type=Path, default=DEFAULT_DPO_ADAPTER_DIR) p.add_argument("--max-steps", type=int, default=30) p.add_argument("--max-seq-len", type=int, default=4096) p.add_argument("--lr", type=float, default=5e-6) p.add_argument("--beta", type=float, default=0.1) p.add_argument("--smoke", action="store_true") p = sub.add_parser( "policy", help="one-turn live-fault prefixes: next command must be sed -i, never a marker", ) p.add_argument("--challenger", type=Path, default=DEFAULT_DPO_EXPORT_DIR) p = sub.add_parser("rl", help="light GRPO on the SFT export (not proxy_score)") p.add_argument("--pack", type=Path, default=None) p.add_argument("--model", type=Path, default=DEFAULT_EXPORT_DIR) p.add_argument("--out-dir", type=Path, default=DEFAULT_RL_ADAPTER_DIR) p.add_argument("--max-steps", type=int, default=40) p.add_argument("--smoke", action="store_true") p = sub.add_parser("inspect", help="print a few packed examples (markers / think / bash)") p.add_argument("--pack", type=Path, required=True) p.add_argument("--n", type=int, default=8) p = sub.add_parser( "judge", help="official GLM-5.2 checklist on a finished local duel (needs OpenRouter)", ) p.add_argument( "--run", type=Path, default=Path("/workspace/data/eval-runs/20260824T182728Z-cdd1388b"), help="eval-run directory (default: v11 8x12)", ) p.add_argument("--limit", type=int, default=2) p.add_argument("--dry-run", action="store_true") args = parser.parse_args(argv) if args.cmd == "pack": from .pack import pack pack( dataset_root=args.dataset_root, out_dir=args.out_dir, max_examples=64 if args.smoke else args.max_examples, seed="smoke" if args.smoke else args.seed, n_salts=args.n_salts, submit_frac=args.submit_frac, edit_frac=args.edit_frac, expand_submit_salts=args.expand_submit_salts, ) return 0 if args.cmd == "sft": _maybe_accelerate() from .sft import default_pack, train_sft train_sft( pack_path=args.pack or default_pack(), output_dir=args.out_dir, model_dir=args.model, max_seq_len=args.max_seq_len, max_steps=args.max_steps, num_epochs=args.epochs, lr=args.lr, smoke=args.smoke, ) return 0 if args.cmd == "export": from .export import export_merged export_merged( adapter_dir=args.adapter, genesis_dir=args.genesis, out_dir=args.out_dir, base_dir=args.base, ) return 0 if args.cmd == "check": from .check import check_model report = check_model(args.path, args.genesis) return 0 if report["ok"] else 1 if args.cmd == "duel": from .duel import duel report = duel( challenger=args.challenger, king=args.king, samples=args.samples, turns=args.turns, seed=args.seed, skip_king=args.skip_king, ) return 0 if report["go"] else 2 if args.cmd == "chain-pack": from .chain_pack import pack_chain mix = args.mix_pack if mix is None: candidate = args.out_dir / "sft-4000-sft-pack.jsonl" mix = candidate if candidate.is_file() else None pack_chain( dataset_root=args.dataset_root, out_dir=args.out_dir, max_groups=8 if args.smoke else args.max_groups, seed="smoke" if args.smoke else args.seed, n_salts=args.n_salts, mix_pack=mix, mix_n=0 if args.smoke else args.mix_n, mix_edit_n=0 if args.smoke else args.mix_edit_n, mix_submit_n=0 if args.smoke else args.mix_submit_n, ) return 0 if args.cmd == "chain-gold": from .chain_gold import pack_chain_gold mix = args.mix_pack if mix is None: candidate = args.out_dir / "sft-4000-sft-pack.jsonl" mix = candidate if candidate.is_file() else None pack_chain_gold( dataset_root=args.dataset_root, out_dir=args.out_dir, max_groups=4 if args.smoke else args.max_groups, seed="smoke" if args.smoke else args.seed, n_salts=args.n_salts, mix_pack=mix, mix_edit_n=0 if args.smoke else args.mix_edit_n, mix_submit_n=0 if args.smoke else args.mix_submit_n, ) return 0 if args.cmd == "chain": from .chain_eval import run_chain report = run_chain( challenger=args.challenger, samples=args.samples, turns=args.turns, seed=args.seed, live_gate=not args.no_live_gate, ) return 0 if report["go"] else 2 if args.cmd == "dpo-pack": from .dpo_pack import pack_dpo pack_dpo(out_dir=args.out_dir, seed=args.seed, narrow=args.narrow) return 0 if args.cmd == "dpo": _maybe_accelerate() from .dpo import train_dpo train_dpo( pack_path=args.pack, output_dir=args.out_dir, model_dir=args.model, max_seq_len=args.max_seq_len, max_steps=args.max_steps, lr=args.lr, beta=args.beta, smoke=args.smoke, ) return 0 if args.cmd == "policy": from .policy_eval import run_policy report = run_policy(challenger=args.challenger) return 0 if report["go"] else 2 if args.cmd == "rl": _maybe_accelerate() from .rl import train_grpo train_grpo( pack_path=args.pack, model_dir=args.model, output_dir=args.out_dir, max_steps=args.max_steps, smoke=args.smoke, ) return 0 if args.cmd == "inspect": return _inspect(args.pack, args.n) if args.cmd == "judge": from local_eval.judge import run_judge result = run_judge(run_dir=args.run, limit=args.limit, dry_run=args.dry_run) if args.dry_run: return 0 return 0 if result.get("state") == "succeeded" else 2 return 2 def _inspect(pack_path: Path, n: int) -> int: from albedo_eval_service.shared.observation_format import first_bash_block from albedo_eval_service.shared.submit_protocol import ANY_MARKER_RE rows = [ json.loads(line) for line in Path(pack_path).read_text().splitlines() if line.strip() ] print(f"n={len(rows)} showing {min(n, len(rows))}", flush=True) for row in rows[:n]: completion = row.get("completion") or "" think_ok = "" in completion and "" in completion bash = first_bash_block(completion) print("=" * 72, flush=True) print( f"{row.get('sample_id')} kind={row.get('kind')} phase={row.get('phase')} " f"rewrite={row.get('rewrite_mode')} marker={row.get('submit_marker')!r}", flush=True, ) print(f" think_closed={think_ok} bash={bash[:140]!r}", flush=True) if row.get("kind") == "submit": print(f" submit={row.get('submit_command')!r} marker_in_bash={bool(ANY_MARKER_RE.search(bash or ''))}", flush=True) return 0 def _maybe_accelerate() -> None: """Re-exec under accelerate when several GPUs are free and we are not already launched.""" if os.environ.get("LOCAL_RANK") is not None or os.environ.get("ALBEDO_NO_ACCELERATE"): return try: import torch nproc = torch.cuda.device_count() except Exception: return if nproc <= 1: return cmd = [ sys.executable, "-m", "accelerate.commands.launch", "--num_processes", str(nproc), "--mixed_precision", "bf16", "-m", "local_train", *sys.argv[1:], ] print("launch:", " ".join(cmd), flush=True) raise SystemExit(subprocess.call(cmd))