Batch-effects-leaderboard / scripts /run_three_arm_experiment.py
spell0's picture
Deploy latest leaderboard app
abb519c
Raw History Blame Contribute Delete
4.91 kB
#!/usr/bin/env python3
"""Resume-safe sequential launcher for the three 30-candidate HPO arms."""
import argparse, json, math, subprocess, sys, time, uuid
from pathlib import Path
ROOT=Path(__file__).resolve().parent.parent
def parse_args(argv=None):
p=argparse.ArgumentParser(description=__doc__)
p.add_argument("--output-dir",type=Path,required=True); p.add_argument("--resume",action="store_true")
p.add_argument("--candidate-budget",type=int,default=30)
p.add_argument("--n-epochs",type=int,default=1000); p.add_argument("--n-repeats",type=int,default=3)
p.add_argument("--batch-size",type=int,default=32); p.add_argument("--num-workers",type=int,default=4)
p.add_argument("--device",choices=("cpu","cuda"),default="cuda"); p.add_argument("--seed",type=int,default=42)
p.add_argument("--no-wandb",action="store_true"); p.add_argument("--smoke",action="store_true")
p.add_argument(
"--validation-eval-policy",
choices=("never", "on-source-improvement", "every-trial"),
default="never",
)
p.add_argument("--plan",action="store_true",help="Print commands without creating state or running arms")
return p.parse_args(argv)
def atomic(path,obj):
t=path.with_suffix(".tmp"); t.write_text(json.dumps(obj,indent=2)+"\n"); t.replace(path)
def main(argv=None):
a=parse_args(argv); a.output_dir.mkdir(parents=True,exist_ok=True); statep=a.output_dir/"orchestrator_state.json"
if a.n_repeats != 3:
raise ValueError("The three-arm comparison requires grouped CV=3 for every arm")
if a.candidate_budget < 1:
raise ValueError("candidate-budget must be positive")
if statep.exists() and not a.resume: raise FileExistsError("pass --resume")
state=json.loads(statep.read_text()) if statep.exists() else {"experiment_id":uuid.uuid4().hex[:8],"arms":{}}
common=["--n-epochs",str(a.n_epochs),"--n-repeats",str(a.n_repeats),"--device",a.device,"--seed",str(a.seed)]
population_size=6
generations=math.ceil(a.candidate_budget/population_size)
arms=[
("meta_evolution",[sys.executable,str(ROOT/"scripts/evolve_meta_model.py"),"--population-size",str(population_size),"--generations",str(generations),"--candidate-budget",str(a.candidate_budget),"--elite-count","2","--tournament-size","3","--no-hf-meta-policy-push"]),
("dataset_conditional",[sys.executable,str(ROOT/"scripts/run_optuna_comparison.py"),"--n-trials",str(a.candidate_budget),"--meta-hidden-size","64"]),
("global_shared",[sys.executable,str(ROOT/"scripts/run_global_shared_optuna.py"),"--n-trials",str(a.candidate_budget)])]
if a.plan:
for name,cmd in arms:
print(name, " ".join(cmd))
return 0
for name,cmd in arms:
rec=state["arms"].setdefault(name,{"status":"pending","wandb_run_name":f"three-arm-{state['experiment_id']}-{name}"})
out=a.output_dir/name; cmd += ["--output-dir",str(out),"--wandb-run-name",rec["wandb_run_name"],*common]
metadata_path=out/"run_metadata.json"
completed_budget=0
if metadata_path.exists():
completed_budget=int(json.loads(metadata_path.read_text()).get("candidate_budget",0))
if rec["status"]=="succeeded" and completed_budget >= a.candidate_budget:
continue
if name!="meta_evolution": cmd += ["--batch-size",str(a.batch_size),"--num-workers",str(a.num_workers)]
else: cmd += ["--num-workers",str(a.num_workers)]
if name == "dataset_conditional":
cmd += ["--validation-eval-policy", a.validation_eval_policy]
if a.no_wandb: cmd.append("--no-wandb")
resume_markers = {
"meta_evolution": (out / "checkpoint.npz", out / "state.json"),
"dataset_conditional": (out / "run_metadata.json", out / "optuna.sqlite3"),
"global_shared": (out / "run_metadata.json", out / "optuna.sqlite3"),
}
# An orchestrator retry is not necessarily an arm resume: preflight can
# fail before the arm creates any checkpoint. Forward --resume only when
# the arm has all of its own durable resume artifacts.
if a.resume and all(path.exists() for path in resume_markers[name]):
cmd.append("--resume")
if a.smoke:
cmd += ["--smoke-evaluator"] if name=="meta_evolution" else (["--smoke"] if name=="global_shared" else [])
if name=="dataset_conditional": raise ValueError("dataset-conditional arm has no smoke evaluator; use its tests")
rec.update(status="running",command=cmd,started_at=time.time()); atomic(statep,state)
result=subprocess.run(cmd,cwd=ROOT)
rec.update(status="succeeded" if result.returncode==0 else "failed",returncode=result.returncode,finished_at=time.time()); atomic(statep,state)
if result.returncode: return result.returncode
return 0
if __name__=="__main__": raise SystemExit(main())