Add optional wandb metrics tracking
Mirror Deep CFR training metrics to W&B via a new WandbRunTracker wired through CompositeRunTracker; wandb is an optional extra so default installs and runs stay unchanged. Train CLI gains --wandb/--wandb-project/--wandb-mode/--wandb-name/--wandb-tag, and train() now closes the tracker in a finally block so runs finalize even on early exit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,7 @@ from coolrl_lost_cities.games.classic.deep_cfr.imitation import (
|
||||
from coolrl_lost_cities.games.classic.deep_cfr.policy_gradient import (
|
||||
fine_tune_strategy_policy_gradient,
|
||||
)
|
||||
from coolrl_lost_cities.games.classic.deep_cfr.tracking import RunTracker, WandbRunTracker
|
||||
from coolrl_lost_cities.games.classic.deep_cfr.trainer import DeepCFRTrainer
|
||||
from coolrl_lost_cities.games.classic.game import classic_config
|
||||
|
||||
@@ -135,10 +136,23 @@ def train_command(args: argparse.Namespace) -> None:
|
||||
overrides = _train_overrides_from_args(args)
|
||||
config = _with_overrides(config, overrides)
|
||||
resume_path = _resolve_resume_path(config, args.resume)
|
||||
extra_trackers: list[RunTracker] = []
|
||||
if args.wandb:
|
||||
extra_trackers.append(
|
||||
WandbRunTracker(
|
||||
project=args.wandb_project,
|
||||
name=args.wandb_name or config.run.experiment_name,
|
||||
mode=args.wandb_mode,
|
||||
config=config.to_dict(),
|
||||
run_dir=str(config.checkpoint_path),
|
||||
tags=list(args.wandb_tag) if args.wandb_tag else None,
|
||||
)
|
||||
)
|
||||
trainer = DeepCFRTrainer(
|
||||
config,
|
||||
config.rules.to_lost_cities_config(seed=config.run.seed),
|
||||
device=args.device or config.run.device,
|
||||
extra_trackers=extra_trackers or None,
|
||||
)
|
||||
if resume_path:
|
||||
trainer.load_checkpoint(resume_path)
|
||||
@@ -268,6 +282,27 @@ def main(argv: list[str] | None = None) -> None:
|
||||
train.add_argument("--no-save", action="store_true")
|
||||
train.add_argument("--save-latest-only", action="store_true")
|
||||
train.add_argument("--save-iteration-interval", type=int)
|
||||
train.add_argument(
|
||||
"--wandb",
|
||||
action="store_true",
|
||||
help="Mirror metrics to Weights & Biases (requires wandb extra).",
|
||||
)
|
||||
train.add_argument("--wandb-project", default="coolrl-lost-cities")
|
||||
train.add_argument(
|
||||
"--wandb-name",
|
||||
help="W&B run name. Defaults to config.run.experiment_name.",
|
||||
)
|
||||
train.add_argument(
|
||||
"--wandb-mode",
|
||||
choices=("online", "offline", "disabled"),
|
||||
default="online",
|
||||
)
|
||||
train.add_argument(
|
||||
"--wandb-tag",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Tag to attach to the W&B run (repeatable).",
|
||||
)
|
||||
train.set_defaults(func=train_command)
|
||||
|
||||
evaluate = subparsers.add_parser("eval")
|
||||
|
||||
@@ -61,6 +61,47 @@ class ConsoleRunTracker:
|
||||
pass
|
||||
|
||||
|
||||
class WandbRunTracker:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
project: str,
|
||||
run_dir: str | Path,
|
||||
name: str | None = None,
|
||||
mode: str | None = None,
|
||||
config: dict[str, Any] | None = None,
|
||||
tags: list[str] | None = None,
|
||||
):
|
||||
try:
|
||||
import wandb
|
||||
except ImportError as exc: # pragma: no cover
|
||||
raise RuntimeError(
|
||||
"wandb is not installed. Install with: uv sync --extra wandb"
|
||||
) from exc
|
||||
run_dir_path = Path(run_dir)
|
||||
run_dir_path.mkdir(parents=True, exist_ok=True)
|
||||
self._wandb = wandb
|
||||
self._run = wandb.init(
|
||||
project=project,
|
||||
name=name,
|
||||
mode=mode,
|
||||
config=config or {},
|
||||
dir=str(run_dir_path),
|
||||
tags=tags,
|
||||
reinit=True,
|
||||
)
|
||||
|
||||
def log_event(self, message: str) -> None:
|
||||
# Human-readable events stay in train.log / console; wandb is purely for metrics.
|
||||
pass
|
||||
|
||||
def log_metrics(self, metrics: dict[str, Any], *, step: int) -> None:
|
||||
self._wandb.log(metrics, step=step)
|
||||
|
||||
def close(self) -> None:
|
||||
self._wandb.finish()
|
||||
|
||||
|
||||
class FileRunTracker:
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -159,6 +159,7 @@ class DeepCFRTrainer:
|
||||
*,
|
||||
device: str = "cpu",
|
||||
tracker: RunTracker | None = None,
|
||||
extra_trackers: list[RunTracker] | None = None,
|
||||
) -> None:
|
||||
self.config = config or DeepCFRConfig()
|
||||
self.game_config = game_config or self.config.rules.to_lost_cities_config(
|
||||
@@ -203,8 +204,10 @@ class DeepCFRTrainer:
|
||||
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"
|
||||
self.tracker = tracker or CompositeRunTracker(
|
||||
[
|
||||
if tracker is not None:
|
||||
self.tracker = tracker
|
||||
else:
|
||||
trackers: list[RunTracker] = [
|
||||
FileRunTracker(
|
||||
log_path=self.log_path,
|
||||
metrics_path=self.metrics_path,
|
||||
@@ -212,7 +215,9 @@ class DeepCFRTrainer:
|
||||
),
|
||||
ConsoleRunTracker(),
|
||||
]
|
||||
)
|
||||
if extra_trackers:
|
||||
trackers.extend(extra_trackers)
|
||||
self.tracker = CompositeRunTracker(trackers)
|
||||
self.self_play_league_snapshots: list[list[dict]] = []
|
||||
self._runtime_metrics: dict[str, float | int] = {}
|
||||
|
||||
@@ -546,19 +551,24 @@ class DeepCFRTrainer:
|
||||
stop = self._stop_iteration()
|
||||
run_started = time.perf_counter()
|
||||
iteration = start
|
||||
while iteration <= stop:
|
||||
started = time.perf_counter()
|
||||
item = self.run_iteration(iteration)
|
||||
metrics.append(item)
|
||||
self._maybe_record_self_play_snapshot(iteration)
|
||||
checkpoint_started = time.perf_counter()
|
||||
self._save_iteration_checkpoints(iteration, item)
|
||||
item.runtime_metrics["checkpoint_seconds"] = time.perf_counter() - checkpoint_started
|
||||
elapsed = time.perf_counter() - started
|
||||
self._append_metrics(item, elapsed)
|
||||
if self._time_limit_reached(run_started):
|
||||
break
|
||||
iteration += 1
|
||||
try:
|
||||
while iteration <= stop:
|
||||
started = time.perf_counter()
|
||||
item = self.run_iteration(iteration)
|
||||
metrics.append(item)
|
||||
self._maybe_record_self_play_snapshot(iteration)
|
||||
checkpoint_started = time.perf_counter()
|
||||
self._save_iteration_checkpoints(iteration, item)
|
||||
item.runtime_metrics["checkpoint_seconds"] = (
|
||||
time.perf_counter() - checkpoint_started
|
||||
)
|
||||
elapsed = time.perf_counter() - started
|
||||
self._append_metrics(item, elapsed)
|
||||
if self._time_limit_reached(run_started):
|
||||
break
|
||||
iteration += 1
|
||||
finally:
|
||||
self.tracker.close()
|
||||
return metrics
|
||||
|
||||
def _stop_iteration(self) -> int:
|
||||
|
||||
Reference in New Issue
Block a user