Source: scripts/train_dynamic_experiment.py · Plain text

scripts/train_dynamic_experiment.py

"""Run the configured two-stage, train-only dynamic carrier experiment."""

from __future__ import annotations

import argparse
import json
from pathlib import Path

import torch

from mcss.training.experiment import (
    DynamicExperimentConfig,
    dynamic_experiment_config_from_mapping,
    run_dynamic_experiment,
)

PROJECT = Path(__file__).resolve().parents[1]


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--episode-index",
        type=Path,
        default=PROJECT / "outputs/dynamic_training_run_20260919/episodes/episode_index_train.json",
    )
    parser.add_argument(
        "--output-dir",
        type=Path,
        default=PROJECT / "outputs/dynamic_training_run_20260919/training",
    )
    parser.add_argument("--device", choices=("auto", "cpu", "cuda"), default="auto")
    parser.add_argument("--resume", action="store_true")
    parser.add_argument(
        "--config-json",
        type=Path,
        help=(
            "JSON object containing DynamicExperimentConfig fields, including "
            "nested carrier/write"
        ),
    )
    parser.add_argument("--init-checkpoint", type=Path)
    parser.add_argument("--prepared-root", type=Path)
    parser.add_argument("--stage-a-steps", type=int)
    parser.add_argument("--stage-b-steps", type=int)
    parser.add_argument("--renderer-samples", type=int)
    parser.add_argument("--ray-chunk-size", type=int)
    parser.add_argument("--warmup-steps", type=int)
    parser.add_argument("--stream-steps", type=int)
    parser.add_argument("--query-count", type=int)
    parser.add_argument("--action-schedule-mode", choices=("legacy_fixed", "mixed"))
    parser.add_argument("--stage-b-actions", nargs="+")
    args = parser.parse_args(argv)

    output_dir = args.output_dir.resolve()
    output_dir.relative_to(PROJECT / "outputs")
    overrides = {
        name: value
        for name, value in {
            "stage_a_steps": args.stage_a_steps,
            "stage_b_steps": args.stage_b_steps,
            "renderer_samples": args.renderer_samples,
            "ray_chunk_size": args.ray_chunk_size,
            "warmup_steps": args.warmup_steps,
            "stream_steps": args.stream_steps,
            "query_count": args.query_count,
            "action_schedule_mode": args.action_schedule_mode,
            "stage_b_actions": args.stage_b_actions,
        }.items()
        if value is not None
    }
    if args.config_json is None:
        config = DynamicExperimentConfig(**overrides)
    else:
        config = dynamic_experiment_config_from_mapping(
            json.loads(args.config_json.read_text(encoding="utf-8")),
            overrides=overrides,
        )
    run_kwargs = {
        "device": _resolve_device(args.device),
        "resume": args.resume,
        "config": config,
        "init_checkpoint": args.init_checkpoint,
    }
    if args.prepared_root is not None:
        run_kwargs["prepared_root"] = args.prepared_root
    result = run_dynamic_experiment(
        args.episode_index,
        output_dir,
        **run_kwargs,
    )
    print(json.dumps(result, indent=2, sort_keys=True))
    return 0


def _resolve_device(requested: str) -> torch.device:
    if requested == "cuda":
        if not torch.cuda.is_available():
            raise RuntimeError("CUDA was requested but is unavailable")
        return torch.device("cuda")
    if requested == "auto" and torch.cuda.is_available():
        return torch.device("cuda")
    return torch.device("cpu")


if __name__ == "__main__":
    raise SystemExit(main())