Lien-Feng's picture
Upload 211 files
c3f98a1 verified
Raw
History Blame Contribute Delete
6.24 kB
"""Run the official 10-fold training matrix.
Every ``(configuration, fold)`` pair is trained with the same pinned schedule.
A run counts as complete only when it wrote ``summary.json`` - Ultralytics
emits ``weights/best.pt`` from the first epoch, so a checkpoint alone does not
mean the schedule finished. The matrix is therefore resumable across
invocations without silently keeping a half-trained fold.
Usage
-----
python scripts/03_train.py # everything still missing
python scripts/03_train.py --groups main # one group
python scripts/03_train.py --exp Exp4_2p5D_Strict --folds 0
python scripts/03_train.py --dry-run # just print the plan
"""
from __future__ import annotations
import argparse
import json
import sys
import time
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from luna_rev import config as cfg
from luna_rev import splits
from luna_rev.train import is_complete
LOG_PATH = cfg.RESULTS_DIR / "training_log.jsonl"
#: Seconds to idle after each run so Windows can reap the dataloader workers
#: before the next run starts allocating.
COOLDOWN_S = 8
def run_in_subprocess(exp, fold, force: bool = False, epochs: int | None = None) -> dict:
"""Train one pair in a fresh interpreter and return its record.
Isolating each run guarantees that workers, pinned buffers and the CUDA
context are released before the next one begins; sharing one interpreter
across 120 runs exhausted page-locked host memory partway through.
"""
import subprocess
cmd = [sys.executable, "-m", "luna_rev._train_worker", exp.name, str(fold.index)]
if force:
cmd.append("--force")
if epochs:
cmd += ["--epochs", str(epochs)]
proc = subprocess.run(cmd, cwd=str(cfg.ROOT), capture_output=True, text=True,
encoding="utf-8", errors="replace")
time.sleep(COOLDOWN_S)
for line in reversed((proc.stdout or "").splitlines()):
if line.startswith("TRAIN_RECORD "):
return json.loads(line[len("TRAIN_RECORD "):])
tail = "\n".join((proc.stderr or "").strip().splitlines()[-12:])
print(f" subprocess exit {proc.returncode}\n{tail}", flush=True)
return {"exp": exp.name, "fold": fold.index, "status": "failed",
"returncode": proc.returncode, "stderr_tail": tail}
def build_plan(groups, exp_names, fold_filter) -> list[tuple]:
"""Expand the requested selection into ``(exp, fold)`` pairs."""
folds = {f.index: f for f in splits.get_folds("official")}
experiments = cfg.experiments_for_group(*groups) if groups else list(cfg.ALL_EXPERIMENTS)
if exp_names:
wanted = set(exp_names)
experiments = [e for e in experiments if e.name in wanted]
unknown = wanted - {e.name for e in experiments}
if unknown:
raise SystemExit(f"Unknown experiment(s): {sorted(unknown)}")
plan = []
for exp in experiments:
for k in cfg.folds_for(exp):
if fold_filter is not None and k not in fold_filter:
continue
plan.append((exp, folds[k]))
return plan
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--groups", nargs="*", default=None,
help="main r_sweep w_sweep neg_ablation seeds yolo26")
ap.add_argument("--exp", nargs="*", default=None, help="explicit configuration names")
ap.add_argument("--folds", nargs="*", type=int, default=None)
ap.add_argument("--force", action="store_true", help="retrain even if weights exist")
ap.add_argument("--dry-run", action="store_true")
ap.add_argument("--epochs", type=int, default=None, help="override the schedule (benchmarking)")
args = ap.parse_args()
plan = build_plan(args.groups, args.exp, set(args.folds) if args.folds else None)
pending = [(e, f) for e, f in plan if args.force or not is_complete(e, f.index)]
print(f"planned runs : {len(plan)}")
print(f"already done : {len(plan) - len(pending)}")
print(f"to train : {len(pending)}")
if args.dry_run:
for e, f in pending:
print(f" {e.name:28s} fold{f.index} [{e.model}, r={e.r_sample}, "
f"w_min={e.w_min_px}, neg={e.negatives}, seed={e.seed}]")
return 0
if not pending:
return 0
t_start = time.time()
failures: list[tuple] = []
for i, (exp, fold) in enumerate(pending, 1):
t0 = time.time()
print(f"\n[{i}/{len(pending)}] {exp.name} fold{fold.index} "
f"(elapsed {(time.time() - t_start) / 3600:.2f} h)", flush=True)
rec = run_in_subprocess(exp, fold, force=args.force, epochs=args.epochs)
rec["wall_seconds"] = round(time.time() - t0, 1)
with LOG_PATH.open("a", encoding="utf-8") as fh:
fh.write(json.dumps(rec) + "\n")
if rec["status"] == "failed":
failures.append((exp, fold))
print(f" -> FAILED after {rec['wall_seconds'] / 60:.1f} min "
f"(continuing; will retry at the end)", flush=True)
continue
rate = (time.time() - t_start) / i
print(f" -> {rec['status']} in {rec['wall_seconds'] / 60:.1f} min "
f"| ETA {(len(pending) - i) * rate / 3600:.1f} h", flush=True)
# One retry pass: the failures we have seen are transient resource
# exhaustion, which a fresh process after an idle gap usually clears.
if failures:
print(f"\nRetrying {len(failures)} failed run(s) ...", flush=True)
still_failing = []
for exp, fold in failures:
time.sleep(30)
rec = run_in_subprocess(exp, fold, force=True, epochs=args.epochs)
with LOG_PATH.open("a", encoding="utf-8") as fh:
fh.write(json.dumps({**rec, "retry": True}) + "\n")
if rec["status"] == "failed":
still_failing.append(f"{exp.name} fold{fold.index}")
if still_failing:
print("PERMANENT FAILURES: " + ", ".join(still_failing), flush=True)
return 1
print(f"\nAll done in {(time.time() - t_start) / 3600:.2f} h")
return 0
if __name__ == "__main__":
raise SystemExit(main())