Files
Mujoco_WASM/training_server/rl/scripts/train.py
T
chenlin cffac29a03
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (pull_request) Has been cancelled
web-platform-ci / Playwright E2E (pull_request) Has been cancelled
feat(training-server): release V0.7.3 整合强化学习服务器
2026-09-01 17:51:42 +08:00

236 lines
7.8 KiB
Python

"""Script to train RL agent with RSL-RL."""
import logging
import os
import sys
from dataclasses import asdict, dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Literal, cast
# 训练器作为仓库内置子集直接从 scripts/ 启动,不要求额外执行 pip install -e。
TRAINER_ROOT = Path(__file__).resolve().parents[1]
if str(TRAINER_ROOT) not in sys.path:
sys.path.insert(0, str(TRAINER_ROOT))
import tyro
import warp as wp
# mjlab 1.2.0 的 GPU CUDA-graph 检查仍访问旧公开路径 wp.context;
# Warp 1.15 已把实现移到 warp._src.context,但保留了相同 runtime 契约。
if not hasattr(wp, "context"):
from warp._src import context as warp_context
wp.context = warp_context # type: ignore[attr-defined]
from mjlab.envs import ManagerBasedRlEnv, ManagerBasedRlEnvCfg
from mjlab.rl import MjlabOnPolicyRunner, RslRlBaseRunnerCfg, RslRlVecEnvWrapper
from mjlab.tasks.registry import list_tasks, load_env_cfg, load_rl_cfg, load_runner_cls
from mjlab.tasks.tracking.mdp import MotionCommandCfg
from mjlab.utils.gpu import select_gpus
from mjlab.utils.os import dump_yaml, get_checkpoint_path
from mjlab.utils.torch import configure_torch_backends
from mjlab.utils.wrappers import VideoRecorder
@dataclass(frozen=True)
class TrainConfig:
env: ManagerBasedRlEnvCfg
agent: RslRlBaseRunnerCfg
motion_file: str | None = None
video: bool = False
video_length: int = 200
video_interval: int = 2000
enable_nan_guard: bool = False
torchrunx_log_dir: str | None = None
gpu_ids: list[int] | Literal["all"] | None = field(default_factory=lambda: [0])
@staticmethod
def from_task(task_id: str) -> "TrainConfig":
env_cfg = load_env_cfg(task_id)
agent_cfg = load_rl_cfg(task_id)
return TrainConfig(env=env_cfg, agent=agent_cfg)
def run_train(task_id: str, cfg: TrainConfig, log_dir: Path) -> None:
cuda_visible = os.environ.get("CUDA_VISIBLE_DEVICES", "")
if cuda_visible == "":
device = "cpu"
seed = cfg.agent.seed
rank = 0
else:
local_rank = int(os.environ.get("LOCAL_RANK", "0"))
rank = int(os.environ.get("RANK", "0"))
# Set EGL device to match the CUDA device.
os.environ["MUJOCO_EGL_DEVICE_ID"] = str(local_rank)
device = f"cuda:{local_rank}"
# Set seed to have diversity in different processes.
seed = cfg.agent.seed + local_rank
configure_torch_backends()
cfg.agent.seed = seed
cfg.env.seed = seed
print(f"[INFO] Training with: device={device}, seed={seed}, rank={rank}")
# Check if this is a tracking task by checking for motion command.
is_tracking_task = "motion" in cfg.env.commands and isinstance(
cfg.env.commands["motion"], MotionCommandCfg
)
if is_tracking_task:
if not cfg.motion_file:
raise ValueError("For tracking tasks, --motion-file must be set ...")
motion_path = Path(cfg.motion_file).expanduser().resolve()
if not motion_path.exists():
raise FileNotFoundError(f"Motion file not found: {motion_path}")
motion_cmd = cfg.env.commands["motion"]
assert isinstance(motion_cmd, MotionCommandCfg)
motion_cmd.motion_file = str(motion_path)
print(f"[INFO] Using motion file: {motion_cmd.motion_file}")
# Check if motion_file is already set (e.g., via CLI --env.commands.motion.motion-file).
if motion_cmd.motion_file and Path(motion_cmd.motion_file).exists():
print(f"[INFO] Using local motion file: {motion_cmd.motion_file}")
# Enable NaN guard if requested.
if cfg.enable_nan_guard:
cfg.env.sim.nan_guard.enabled = True
print(f"[INFO] NaN guard enabled, output dir: {cfg.env.sim.nan_guard.output_dir}")
if rank == 0:
print(f"[INFO] Logging experiment in directory: {log_dir}")
env = ManagerBasedRlEnv(
cfg=cfg.env, device=device, render_mode="rgb_array" if cfg.video else None
)
log_root_path = log_dir.parent # Go up from specific run dir to experiment dir.
resume_path: Path | None = None
if cfg.agent.resume:
# Load checkpoint from local filesystem.
resume_path = get_checkpoint_path(
log_root_path, cfg.agent.load_run, cfg.agent.load_checkpoint
)
# Only record videos on rank 0 to avoid multiple workers writing to the same files.
if cfg.video and rank == 0:
env = VideoRecorder(
env,
video_folder=Path(log_dir) / "videos" / "train",
step_trigger=lambda step: step % cfg.video_interval == 0,
video_length=cfg.video_length,
disable_logger=True,
)
print("[INFO] Recording videos during training.")
env = RslRlVecEnvWrapper(env, clip_actions=cfg.agent.clip_actions)
agent_cfg = asdict(cfg.agent)
env_cfg = asdict(cfg.env)
runner_cls = load_runner_cls(task_id)
if runner_cls is None:
runner_cls = MjlabOnPolicyRunner
runner_kwargs = {}
runner = runner_cls(env, agent_cfg, str(log_dir), device, **runner_kwargs)
runner.add_git_repo_to_log(__file__)
if resume_path is not None:
print(f"[INFO]: Loading model checkpoint from: {resume_path}")
runner.load(str(resume_path))
# Only write config files from rank 0 to avoid race conditions.
if rank == 0:
dump_yaml(log_dir / "params" / "env.yaml", env_cfg)
dump_yaml(log_dir / "params" / "agent.yaml", agent_cfg)
runner.learn(
num_learning_iterations=cfg.agent.max_iterations, init_at_random_ep_len=True
)
env.close()
def launch_training(task_id: str, args: TrainConfig | None = None):
args = args or TrainConfig.from_task(task_id)
# Create log directory once before launching workers.
log_root_path = Path("logs") / "rsl_rl" / args.agent.experiment_name
log_root_path.resolve()
log_dir_name = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
if args.agent.run_name:
log_dir_name += f"_{args.agent.run_name}"
log_dir = log_root_path / log_dir_name
# Select GPUs based on CUDA_VISIBLE_DEVICES and user specification.
selected_gpus, num_gpus = select_gpus(args.gpu_ids)
# Set environment variables for all modes.
if selected_gpus is None:
os.environ["CUDA_VISIBLE_DEVICES"] = ""
else:
os.environ["CUDA_VISIBLE_DEVICES"] = ",".join(map(str, selected_gpus))
os.environ["MUJOCO_GL"] = "egl"
if num_gpus <= 1:
# CPU or single GPU: run directly without torchrunx.
run_train(task_id, args, log_dir)
else:
# Multi-GPU: use torchrunx.
import torchrunx
# torchrunx redirects stdout to logging.
logging.basicConfig(level=logging.INFO)
# Configure torchrunx logging directory.
# Priority: 1) existing env var, 2) user flag, 3) default to {log_dir}/torchrunx.
if "TORCHRUNX_LOG_DIR" not in os.environ:
if args.torchrunx_log_dir is not None:
# User specified a value via flag (could be "" to disable).
os.environ["TORCHRUNX_LOG_DIR"] = args.torchrunx_log_dir
else:
# Default: put logs in training directory.
os.environ["TORCHRUNX_LOG_DIR"] = str(log_dir / "torchrunx")
print(f"[INFO] Launching training with {num_gpus} GPUs", flush=True)
torchrunx.Launcher(
hostnames=["localhost"],
workers_per_host=num_gpus,
backend=None, # Let rsl_rl handle process group initialization.
copy_env_vars=torchrunx.DEFAULT_ENV_VARS_FOR_COPY + ("MUJOCO*",),
).run(run_train, task_id, args, log_dir)
def main():
# Parse first argument to choose the task.
# Import tasks to populate the registry.
import mjlab.tasks # noqa: F401
import src.tasks
all_tasks = list_tasks()
chosen_task, remaining_args = tyro.cli(
tyro.extras.literal_type_from_choices(all_tasks),
add_help=False,
return_unknown_args=True,
config=mjlab.TYRO_FLAGS,
)
args = tyro.cli(
TrainConfig,
args=remaining_args,
default=TrainConfig.from_task(chosen_task),
prog=sys.argv[0] + f" {chosen_task}",
config=mjlab.TYRO_FLAGS,
)
del remaining_args
launch_training(task_id=chosen_task, args=args)
if __name__ == "__main__":
main()