Auto-derive run dir from experiment_name + timestamp
Drop checkpoint.directory from config — config defines what an experiment is, not where its outputs go. The CLI now computes the run directory from run.experiment_name plus a timestamp, defaulting to runs/tmp/ for throwaway runs and runs/ when --keep is passed. - Remove CheckpointConfig.directory and DeepCFRConfig.checkpoint_path - DeepCFRTrainer takes run_dir: Path explicitly - CLI: add --keep boolean; --resume requires an explicit path (no shortcut) - Auto path: runs/[tmp/]<YYYY-MM-DD_HHMMSS>_<experiment_name-kebab>/ - Rename 13 configs to kebab-case; strip directory: lines; kebab their experiment_name values - Rewrite AGENTS.md training/run sections; document archive/tmp/<flat> layout, --keep, kebab-case scope - Update tests for new run_dir flow and dropped --resume shortcut Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,8 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -27,7 +29,19 @@ from coolrl_lost_cities.games.classic.deep_cfr.tracking import RunTracker, Wandb
|
||||
from coolrl_lost_cities.games.classic.deep_cfr.trainer import DeepCFRTrainer
|
||||
from coolrl_lost_cities.games.classic.game import classic_config
|
||||
|
||||
_RESUME_LATEST = "__latest__"
|
||||
_SLUG_RE = re.compile(r"[^a-z0-9]+")
|
||||
|
||||
|
||||
def _kebab_slug(value: str) -> str:
|
||||
slug = _SLUG_RE.sub("-", value.strip().lower()).strip("-")
|
||||
return slug or "run"
|
||||
|
||||
|
||||
def _resolve_run_dir(config: DeepCFRConfig, *, keep: bool) -> Path:
|
||||
parent = Path("runs") if keep else Path("runs/tmp")
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d_%H%M%S")
|
||||
slug = _kebab_slug(config.run.experiment_name)
|
||||
return parent / f"{timestamp}_{slug}"
|
||||
|
||||
|
||||
def _load_config(path: str | None) -> DeepCFRConfig:
|
||||
@@ -67,17 +81,6 @@ def _set_path_override(overrides: dict[str, Any], assignment: str) -> None:
|
||||
cursor[keys[-1]] = value
|
||||
|
||||
|
||||
def _resolve_resume_path(config: DeepCFRConfig, resume: str | None) -> str | None:
|
||||
if resume != _RESUME_LATEST:
|
||||
return resume
|
||||
latest_path = config.checkpoint_path / "latest.pt"
|
||||
if not latest_path.exists():
|
||||
raise FileNotFoundError(
|
||||
f"--resume was used without a path, but latest checkpoint does not exist: {latest_path}"
|
||||
)
|
||||
return str(latest_path)
|
||||
|
||||
|
||||
def _train_overrides_from_args(args: argparse.Namespace) -> dict[str, Any]:
|
||||
overrides: dict[str, Any] = {}
|
||||
for assignment in getattr(args, "config_overrides", None) or ():
|
||||
@@ -89,7 +92,10 @@ def train_command(args: argparse.Namespace) -> None:
|
||||
config = _load_config(args.config)
|
||||
overrides = _train_overrides_from_args(args)
|
||||
config = _with_overrides(config, overrides)
|
||||
resume_path = _resolve_resume_path(config, args.resume)
|
||||
if args.resume:
|
||||
run_dir = Path(args.resume).parent
|
||||
else:
|
||||
run_dir = _resolve_run_dir(config, keep=args.keep)
|
||||
extra_trackers: list[RunTracker] = []
|
||||
if args.wandb:
|
||||
extra_trackers.append(
|
||||
@@ -98,18 +104,19 @@ def train_command(args: argparse.Namespace) -> None:
|
||||
name=args.wandb_name or config.run.experiment_name,
|
||||
mode=args.wandb_mode,
|
||||
config=config.to_dict(),
|
||||
run_dir=str(config.checkpoint_path),
|
||||
run_dir=str(run_dir),
|
||||
tags=list(args.wandb_tag) if args.wandb_tag else None,
|
||||
)
|
||||
)
|
||||
trainer = DeepCFRTrainer(
|
||||
config,
|
||||
config.rules.to_lost_cities_config(seed=config.run.seed),
|
||||
run_dir=run_dir,
|
||||
device=config.run.device,
|
||||
extra_trackers=extra_trackers or None,
|
||||
)
|
||||
if resume_path:
|
||||
trainer.load_checkpoint(resume_path)
|
||||
if args.resume:
|
||||
trainer.load_checkpoint(args.resume)
|
||||
trainer.train()
|
||||
|
||||
|
||||
@@ -203,7 +210,15 @@ def main(argv: list[str] | None = None) -> None:
|
||||
|
||||
train = subparsers.add_parser("train")
|
||||
train.add_argument("--config")
|
||||
train.add_argument("--resume", nargs="?", const=_RESUME_LATEST, default=None)
|
||||
train.add_argument(
|
||||
"--resume",
|
||||
help="Path to a checkpoint to resume from (e.g. runs/.../latest.pt).",
|
||||
)
|
||||
train.add_argument(
|
||||
"--keep",
|
||||
action="store_true",
|
||||
help="Place run under runs/ instead of runs/tmp/ (use for real experiments).",
|
||||
)
|
||||
train.add_argument(
|
||||
"--set",
|
||||
action="append",
|
||||
|
||||
@@ -217,16 +217,11 @@ class MemoryConfig(StrictModel):
|
||||
|
||||
|
||||
class CheckpointConfig(StrictModel):
|
||||
directory: str = "runs/deep_cfr/default"
|
||||
save_every: int = 1
|
||||
save_latest: bool = True
|
||||
progress_interval_seconds: float = 20.0
|
||||
exact_resume: bool = False
|
||||
|
||||
@property
|
||||
def path(self) -> Path:
|
||||
return Path(self.directory)
|
||||
|
||||
|
||||
class EvaluationConfig(StrictModel):
|
||||
eval_every: int = 50
|
||||
@@ -278,10 +273,6 @@ class DeepCFRConfig(StrictModel):
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return self.model_dump(mode="json")
|
||||
|
||||
@property
|
||||
def checkpoint_path(self) -> Path:
|
||||
return self.checkpoint.path
|
||||
|
||||
|
||||
def config_from_dict(data: Mapping[str, Any]) -> DeepCFRConfig:
|
||||
return DeepCFRConfig.model_validate(data)
|
||||
|
||||
@@ -157,6 +157,7 @@ class DeepCFRTrainer:
|
||||
config: DeepCFRConfig | None = None,
|
||||
game_config: LostCitiesConfig | None = None,
|
||||
*,
|
||||
run_dir: str | Path | None = None,
|
||||
device: str = "cpu",
|
||||
tracker: RunTracker | None = None,
|
||||
extra_trackers: list[RunTracker] | None = None,
|
||||
@@ -200,7 +201,7 @@ class DeepCFRTrainer:
|
||||
self.strategy_memory = ReservoirMemory(self.config.memory.strategy_capacity)
|
||||
self.rng = np.random.default_rng(self.config.run.seed + 101)
|
||||
self.iteration = 0
|
||||
self.run_dir = self.config.checkpoint_path
|
||||
self.run_dir = Path(run_dir) if run_dir is not None else Path("runs/tmp/default")
|
||||
self.metrics_path = self.run_dir / "metrics.jsonl"
|
||||
self.progress_path = self.run_dir / "runtime_progress.json"
|
||||
self.log_path = self.run_dir / "train.log"
|
||||
|
||||
Reference in New Issue
Block a user