diff --git a/AGENTS.md b/AGENTS.md index 08523399..b6954a4f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,8 +26,9 @@ MotrixLab 是构建在 MotrixSim 仿真后端之上的强化学习框架,提 - `motrix_deploy_unitree`:Unitree 硬件配置、sensor/actuation 接线与 SDK2 DDS 硬件后端插件 - `motrix_deploy_tasks`:具体部署任务实现 -`scripts/` 提供训练、可视化、评估入口。内置环境由环境 package 注册并通过项目 registry 暴露;simulator backend -通过项目 backend registry 选择,core package 不依赖任何具体 simulator 实现。 +`motrix_rl.commands.train` / `play` 提供安装后的 `mxtrain` / `mxplay` 入口,`scripts/` 保留可视化等工具。 +内置环境由环境 package 注册并通过项目 registry 暴露;simulator backend 通过项目 backend registry 选择, +core package 不依赖任何具体 simulator 实现。 ## 编码规范 @@ -88,7 +89,7 @@ Python 方法,由 manager 运行时契约定义(`wiki/design/manager/runtime ### RL 任务配置规范 - RL provider 配置使用对应插件包的 `@dataclass` 配置类:SKRL 使用 `motrix_rl_skrl.config.SkrlCfg`,RSLRL 使用 `motrix_rl_rslrl.cfg.RslrlCfg`,FastSAC 使用 `motrix_rl_builtin.fastsac.config.FastSacCfg`。 -- provider 配置的字段结构必须与对应的 `configs/algo_base/*.yaml` 保持一致;不要使用已经删除的 `@rlcfg` 或旧的嵌套结构。 +- provider 配置的字段结构必须与对应的 `motrix_rl/src/motrix_rl/configs/algo_base/*.yaml` 保持一致;不要使用已经删除的 `@rlcfg` 或旧的嵌套结构。 - 超参数在配置对象初始化或明确的配置组装流程中赋值;同一环境的多个变体应通过配置复用,子变体只覆写实际差异。 - SKRL 和 RSLRL 的 provider 配置结构不同,不要为了统一接口强行增加无用的兼容层;框架通过 provider 的 `config_type` 和 trainer context 选择对应实现。 @@ -154,28 +155,28 @@ sh install.sh --python 3.11 ## 常用命令 -train/play/view CLI 由 [Hydra](https://hydra.cc/) 驱动,参数使用 `key=value` 语法(不是 `--flag`)。可用选项见 -`configs/*.yaml`。 +train/play/view CLI 由 [Hydra](https://hydra.cc/) 驱动,参数使用 `key=value` 语法(不是 `--flag`)。 +CLI 用法与项目配置接入见 [motrix_rl/README.md](motrix_rl/README.md)。 ### 训练 SKRL(默认): ```bash -python scripts/train.py task=cartpole/skrl.ppo +mxtrain task=cartpole/skrl.ppo ``` RSLRL: ```bash -python scripts/train.py task=cartpole/rslrl.ppo +mxtrain task=cartpole/rslrl.ppo ``` 直接覆写框架运行时设置和类型化 RL 参数: ```bash -python scripts/train.py task=cartpole/skrl.ppo num_envs=64 algo.agent.learning_rate=1e-3 -python scripts/train.py task=cartpole/skrl.ppo logging.interval=20 checkpoint.interval=100 +mxtrain task=cartpole/skrl.ppo num_envs=64 algo.agent.learning_rate=1e-3 +mxtrain task=cartpole/skrl.ppo logging.interval=20 checkpoint.interval=100 ``` ### 环境可视化 @@ -195,19 +196,25 @@ python scripts/view.py robot=g1-29dof ### 评估 ```bash -python scripts/play.py env=cartpole +mxplay env=cartpole ``` 指定 policy 文件: ```bash -python scripts/play.py env=cartpole policy= +mxplay env=cartpole policy= ``` ### ONNX 导出 ```bash -python scripts/export_onnx.py run_dir= output=/tmp/policy.onnx +mxexport onnx run_dir= output=/tmp/policy.onnx +``` + +### 部署 artifact 导出 + +```bash +mxexport deploy run_dir= output=artifacts/policy.deploy ``` ### 渲染 @@ -215,7 +222,7 @@ python scripts/export_onnx.py run_dir= output=/tmp/policy.onnx 训练时开启可视化: ```bash -python scripts/train.py task=cartpole/skrl.ppo render=true +mxtrain task=cartpole/skrl.ppo render=true ``` ### TensorBoard diff --git a/README.md b/README.md index 2a5393b9..d8729e06 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ Activate the installed environment (Windows PowerShell: `.venv\Scripts\Activate. ```bash source .venv/bin/activate -python scripts/train.py task=microduck-walk-flat/motrix.fastsac play=true +mxtrain task=microduck-walk-flat/motrix.fastsac play=true ``` While training, the built-in dashboard shows live run progress, episode statistics, throughput, rewards, and system health: @@ -124,7 +124,7 @@ Training finishes in minutes: mean return and episode length typically converge Replay the latest trained policy without retraining (for example, after stopping training early with Ctrl+C): ```bash -python scripts/play.py env=microduck-walk-flat +mxplay env=microduck-walk-flat ``` A trained microduck policy replayed in the viewer: diff --git a/README.zh-CN.md b/README.zh-CN.md index 3368c20c..8b5f624a 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -98,7 +98,7 @@ powershell -ExecutionPolicy Bypass -File install.ps1 ```bash source .venv/bin/activate -python scripts/train.py task=microduck-walk-flat/motrix.fastsac play=true +mxtrain task=microduck-walk-flat/motrix.fastsac play=true ``` 训练过程中,内置面板会实时显示运行进度、回合统计、吞吐、奖励与系统健康状态: @@ -124,7 +124,7 @@ microduck 的训练数分钟内即可完成:平均回报与回合长度通常 无需重新训练即可回放最近一次训练得到的策略(例如提前 Ctrl+C 中断训练之后): ```bash -python scripts/play.py env=microduck-walk-flat +mxplay env=microduck-walk-flat ``` 训练好的 microduck 策略在查看器中的回放效果: diff --git a/bench/bench_wbt_kernel.py b/bench/bench_wbt_kernel.py index 8326a9da..94907b6c 100644 --- a/bench/bench_wbt_kernel.py +++ b/bench/bench_wbt_kernel.py @@ -19,7 +19,6 @@ import numba import numpy as np -import motrix_envs # noqa: F401 registers built-in environments from motrix_env_core import registry from motrix_env_core.manager import ManagerEnv from motrix_env_core.numba.kernel import clone_kernel_value, get_num_threads, get_threading_layer diff --git a/bench/env_benchmark/workload.py b/bench/env_benchmark/workload.py index 007d8825..660d9768 100644 --- a/bench/env_benchmark/workload.py +++ b/bench/env_benchmark/workload.py @@ -17,7 +17,6 @@ def load_runtime(): os.environ.setdefault("GOMP_SPINCOUNT", "0") import numpy as np - import motrix_envs # noqa: F401 registers built-in environments from motrix_env_core import registry return np, registry diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 1849bea9..bd827b7d 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -16,10 +16,10 @@ services: count: all capabilities: [gpu] command: - - scripts/train.py - - train_backend=${MOTRIX_TRAIN_BACKEND:-jax} + - mxtrain + - task=${MOTRIX_ENV:-cartpole}/skrl.ppo + - task.train_backend=${MOTRIX_TRAIN_BACKEND:-jax} - num_envs=${MOTRIX_NUM_ENVS:-2048} - - env=${MOTRIX_ENV:-cartpole} restart: no motrixlab-tensorboard: diff --git a/docs/scripts/generate_env_docs.py b/docs/scripts/generate_env_docs.py index 9121ee64..8ccac3a0 100644 --- a/docs/scripts/generate_env_docs.py +++ b/docs/scripts/generate_env_docs.py @@ -8,7 +8,6 @@ import argparse from pathlib import Path -import motrix_envs # noqa: F401 registers built-in environments from motrix_env_core import registry REPO_ROOT = Path(__file__).resolve().parents[2] diff --git a/docs/scripts/generate_env_snapshot.py b/docs/scripts/generate_env_snapshot.py index 7a167354..e69e4eaa 100644 --- a/docs/scripts/generate_env_snapshot.py +++ b/docs/scripts/generate_env_snapshot.py @@ -11,7 +11,6 @@ import imageio.v2 as imageio -import motrix_envs # noqa: F401 registers built-in environments from motrix_env_core import registry from motrix_env_core.array.env import ArrayEnv from motrix_env_core.sim.backend import RenderConfig diff --git a/docs/scripts/generate_video.py b/docs/scripts/generate_video.py index a67332df..f92e2ab0 100644 --- a/docs/scripts/generate_video.py +++ b/docs/scripts/generate_video.py @@ -4,9 +4,9 @@ """Generate a documentation video from the latest playable environment policy. Examples: - uv run docs/scripts/generate_video.py cartpole - uv run docs/scripts/generate_video.py cartpole --output cartpole_demo.mp4 --force - uv run docs/scripts/generate_video.py g1-wbt-dance # records one complete motion clip + python docs/scripts/generate_video.py cartpole + python docs/scripts/generate_video.py cartpole --output cartpole_demo.mp4 --force + python docs/scripts/generate_video.py g1-wbt-dance # records one complete motion clip """ from __future__ import annotations @@ -17,7 +17,6 @@ from dataclasses import dataclass, replace from pathlib import Path -import motrix_envs # noqa: F401 registers built-in environments from motrix_env_core import registry from motrix_env_core.renderer import RenderConfig from motrix_envs.locomotion.wbt.cfg import WbtEnvCfg @@ -96,7 +95,7 @@ def _default_train_command(env_name: str) -> str | None: config = candidates[sorted(candidates)[0]] if config is None: return None - return f"uv run scripts/train.py task={env_name}/{config.stem}" + return f"python -m motrix_rl.commands.train task={env_name}/{config.name.removesuffix('.yaml')}" def _recording_path(env_name: str, output: Path | None) -> Path: diff --git a/docs/source/en/user_guide/envs/ball_balance.md b/docs/source/en/user_guide/envs/ball_balance.md index d5de342d..3c284320 100644 --- a/docs/source/en/user_guide/envs/ball_balance.md +++ b/docs/source/en/user_guide/envs/ball_balance.md @@ -25,13 +25,13 @@ python scripts/view.py env=microduck-ball-balance Train with the built-in Motrix FastSAC config (the only training config provided for this task; `num_envs=2048`, 20000 iterations): ```bash -python scripts/train.py task=microduck-ball-balance/motrix.fastsac +mxtrain task=microduck-ball-balance/motrix.fastsac ``` Evaluate a trained policy: ```bash -python scripts/play.py env=microduck-ball-balance +mxplay env=microduck-ball-balance ``` ## Action space diff --git a/docs/source/en/user_guide/envs/basic/acrobot.md b/docs/source/en/user_guide/envs/basic/acrobot.md index 4c51de54..6aabc51b 100644 --- a/docs/source/en/user_guide/envs/basic/acrobot.md +++ b/docs/source/en/user_guide/envs/basic/acrobot.md @@ -70,13 +70,13 @@ python scripts/view.py env=acrobot ```bash # Train with default parameters -python scripts/train.py task=acrobot/skrl.ppo +mxtrain task=acrobot/skrl.ppo # Customize parallel environments -python scripts/train.py task=acrobot/skrl.ppo num_envs=1024 +mxtrain task=acrobot/skrl.ppo num_envs=1024 # Enable rendering during training -python scripts/train.py task=acrobot/skrl.ppo render=true +mxtrain task=acrobot/skrl.ppo render=true ``` ### 3. View Training Progress @@ -89,10 +89,10 @@ tensorboard --logdir runs/acrobot ```bash # Auto-discover best policy (recommended) -python scripts/play.py env=acrobot +mxplay env=acrobot # Manually specify a checkpoint from a metadata-backed run -python scripts/play.py env=acrobot policy=/path/to/run/checkpoints/policy-file +mxplay env=acrobot policy=/path/to/run/checkpoints/policy-file ``` > **Tip**: Policies are auto-selected from `runs/acrobot/`. Use `policy=...` to select a checkpoint whose parent run contains `metadata.json`. diff --git a/docs/source/en/user_guide/envs/basic/cartpole.md b/docs/source/en/user_guide/envs/basic/cartpole.md index cc28a167..8eef8aca 100644 --- a/docs/source/en/user_guide/envs/basic/cartpole.md +++ b/docs/source/en/user_guide/envs/basic/cartpole.md @@ -21,7 +21,7 @@ python scripts/view.py env=cartpole ### 2. Start Training ```bash -python scripts/train.py task=cartpole/skrl.ppo +mxtrain task=cartpole/skrl.ppo ``` ### 3. View Training Progress @@ -33,7 +33,7 @@ tensorboard --logdir runs/cartpole ### 4. Test Training Results ```bash -python scripts/play.py env=cartpole +mxplay env=cartpole ``` > **Tip**: The system finds the latest metadata-backed run under `runs/cartpole/` and loads its `best_policy` artifact. Use `policy=...` to select another checkpoint from a metadata-backed run. diff --git a/docs/source/en/user_guide/envs/basic/pendulum.md b/docs/source/en/user_guide/envs/basic/pendulum.md index 0b05fad6..a05c7086 100644 --- a/docs/source/en/user_guide/envs/basic/pendulum.md +++ b/docs/source/en/user_guide/envs/basic/pendulum.md @@ -69,13 +69,13 @@ python scripts/view.py env=pendulum ```bash # Train with default parameters -python scripts/train.py task=pendulum/skrl.ppo +mxtrain task=pendulum/skrl.ppo # Customize parallel environments -python scripts/train.py task=pendulum/skrl.ppo num_envs=1024 +mxtrain task=pendulum/skrl.ppo num_envs=1024 # Enable rendering during training -python scripts/train.py task=pendulum/skrl.ppo render=true +mxtrain task=pendulum/skrl.ppo render=true ``` ### 3. View Training Progress @@ -88,10 +88,10 @@ tensorboard --logdir runs/pendulum ```bash # Auto-discover best policy (recommended) -python scripts/play.py env=pendulum +mxplay env=pendulum # Manually specify a checkpoint from a metadata-backed run -python scripts/play.py env=pendulum policy=/path/to/run/checkpoints/policy-file +mxplay env=pendulum policy=/path/to/run/checkpoints/policy-file ``` > **Tip**: Policies are auto-selected from `runs/pendulum/`. Use `policy=...` to select a checkpoint whose parent run contains `metadata.json`. diff --git a/docs/source/en/user_guide/envs/basic/stewart.md b/docs/source/en/user_guide/envs/basic/stewart.md index 33a78ab9..89eefac9 100644 --- a/docs/source/en/user_guide/envs/basic/stewart.md +++ b/docs/source/en/user_guide/envs/basic/stewart.md @@ -163,8 +163,8 @@ python scripts/view.py env=stewart-disturb-xy ### 2. Start Training ```bash -python scripts/train.py task=stewart-static/skrl.ppo -python scripts/train.py task=stewart-disturb-xy/skrl.ppo +mxtrain task=stewart-static/skrl.ppo +mxtrain task=stewart-disturb-xy/skrl.ppo ``` ### Training Notes @@ -183,8 +183,8 @@ tensorboard --logdir runs/stewart-disturb-xy ### 4. Test Training Results ```bash -python scripts/play.py env=stewart-static -python scripts/play.py env=stewart-disturb-xy +mxplay env=stewart-static +mxplay env=stewart-disturb-xy ``` --- diff --git a/docs/source/en/user_guide/envs/dm_control/bring_ball.md b/docs/source/en/user_guide/envs/dm_control/bring_ball.md index 63907aa9..ca64e70c 100644 --- a/docs/source/en/user_guide/envs/dm_control/bring_ball.md +++ b/docs/source/en/user_guide/envs/dm_control/bring_ball.md @@ -143,7 +143,7 @@ python scripts/view.py env=dm-manipulator-bring-ball ### 2. Start Training ```bash -python scripts/train.py task=dm-manipulator-bring-ball/skrl.ppo.torch +mxtrain task=dm-manipulator-bring-ball/skrl.ppo.torch ``` ### 3. View Training Progress @@ -155,7 +155,7 @@ tensorboard --logdir runs/dm-manipulator-bring-ball ### 4. Test Training Results ```bash -python scripts/play.py env=dm-manipulator-bring-ball +mxplay env=dm-manipulator-bring-ball ``` --- diff --git a/docs/source/en/user_guide/envs/dm_control/dm_cheetah.md b/docs/source/en/user_guide/envs/dm_control/dm_cheetah.md index 75ddf3ad..c55439d9 100644 --- a/docs/source/en/user_guide/envs/dm_control/dm_cheetah.md +++ b/docs/source/en/user_guide/envs/dm_control/dm_cheetah.md @@ -108,7 +108,7 @@ python scripts/view.py env=dm-cheetah ### 2. Start Training ```bash -python scripts/train.py task=dm-cheetah/skrl.ppo +mxtrain task=dm-cheetah/skrl.ppo ``` ### 3. View Training Progress @@ -120,7 +120,7 @@ tensorboard --logdir runs/dm-cheetah ### 4. Test Training Results ```bash -python scripts/play.py env=dm-cheetah +mxplay env=dm-cheetah ``` --- diff --git a/docs/source/en/user_guide/envs/dm_control/dm_finger.md b/docs/source/en/user_guide/envs/dm_control/dm_finger.md index 491a3b87..4987e383 100644 --- a/docs/source/en/user_guide/envs/dm_control/dm_finger.md +++ b/docs/source/en/user_guide/envs/dm_control/dm_finger.md @@ -145,15 +145,15 @@ python scripts/view.py env=dm-finger-turn-hard ### 2. Start Training ```bash -python scripts/train.py task=dm-finger-spin/skrl.ppo task.train_backend=torch +mxtrain task=dm-finger-spin/skrl.ppo task.train_backend=torch ``` ```bash -python scripts/train.py task=dm-finger-turn-easy/skrl.ppo task.train_backend=torch +mxtrain task=dm-finger-turn-easy/skrl.ppo task.train_backend=torch ``` ```bash -python scripts/train.py task=dm-finger-turn-hard/skrl.ppo task.train_backend=torch +mxtrain task=dm-finger-turn-hard/skrl.ppo task.train_backend=torch ``` ### 3. View Training Progress @@ -164,10 +164,10 @@ tensorboard --logdir runs/dm-finger-spin ### 4. Test Training Results -`scripts/play.py` auto-discovers the latest metadata-backed run under `runs/{env-name}/` and loads its `best_policy` artifact. Use `policy=...` to select a checkpoint explicitly: +`mxplay` auto-discovers the latest metadata-backed run under `runs/{env-name}/` and loads its `best_policy` artifact. Use `policy=...` to select a checkpoint explicitly: ```bash -python scripts/play.py env=dm-finger-turn-hard +mxplay env=dm-finger-turn-hard ``` --- diff --git a/docs/source/en/user_guide/envs/dm_control/dm_hopper.md b/docs/source/en/user_guide/envs/dm_control/dm_hopper.md index e1fcbb39..eecfc3d3 100644 --- a/docs/source/en/user_guide/envs/dm_control/dm_hopper.md +++ b/docs/source/en/user_guide/envs/dm_control/dm_hopper.md @@ -115,8 +115,8 @@ python scripts/view.py env=dm-hopper-hop ### 2. Start Training ```bash -python scripts/train.py task=dm-hopper-stand/skrl.ppo -python scripts/train.py task=dm-hopper-hop/skrl.ppo +mxtrain task=dm-hopper-stand/skrl.ppo +mxtrain task=dm-hopper-hop/skrl.ppo ``` ### 3. View Training Progress @@ -128,8 +128,8 @@ tensorboard --logdir runs/dm-hopper-stand ### 4. Test Training Results ```bash -python scripts/play.py env=dm-hopper-stand -python scripts/play.py env=dm-hopper-hop +mxplay env=dm-hopper-stand +mxplay env=dm-hopper-hop ``` --- diff --git a/docs/source/en/user_guide/envs/dm_control/dm_humanoid.md b/docs/source/en/user_guide/envs/dm_control/dm_humanoid.md index 62f8fde0..194a95af 100644 --- a/docs/source/en/user_guide/envs/dm_control/dm_humanoid.md +++ b/docs/source/en/user_guide/envs/dm_control/dm_humanoid.md @@ -193,9 +193,9 @@ python scripts/view.py env=dm-humanoid-run ### 2. Start Training ```bash -python scripts/train.py task=dm-humanoid-stand/skrl.ppo -python scripts/train.py task=dm-humanoid-walk/skrl.ppo -python scripts/train.py task=dm-humanoid-run/skrl.ppo +mxtrain task=dm-humanoid-stand/skrl.ppo +mxtrain task=dm-humanoid-walk/skrl.ppo +mxtrain task=dm-humanoid-run/skrl.ppo ``` ### 3. View Training Progress @@ -207,9 +207,9 @@ tensorboard --logdir runs/dm-humanoid-walk ### 4. Test Training Results ```bash -python scripts/play.py env=dm-humanoid-stand -python scripts/play.py env=dm-humanoid-walk -python scripts/play.py env=dm-humanoid-run +mxplay env=dm-humanoid-stand +mxplay env=dm-humanoid-walk +mxplay env=dm-humanoid-run ``` --- diff --git a/docs/source/en/user_guide/envs/dm_control/dm_lqr.md b/docs/source/en/user_guide/envs/dm_control/dm_lqr.md index 1f156718..1305c8d0 100644 --- a/docs/source/en/user_guide/envs/dm_control/dm_lqr.md +++ b/docs/source/en/user_guide/envs/dm_control/dm_lqr.md @@ -164,8 +164,8 @@ python scripts/view.py env=dm-lqr-6-2 ### 2. Start Training ```bash -python scripts/train.py task=dm-lqr-2-1/skrl.ppo -python scripts/train.py task=dm-lqr-6-2/skrl.ppo +mxtrain task=dm-lqr-2-1/skrl.ppo +mxtrain task=dm-lqr-6-2/skrl.ppo ``` ### 3. View Training Progress @@ -178,8 +178,8 @@ tensorboard --logdir runs/dm-lqr-6-2 ### 4. Test Training Results ```bash -python scripts/play.py env=dm-lqr-2-1 -python scripts/play.py env=dm-lqr-6-2 +mxplay env=dm-lqr-2-1 +mxplay env=dm-lqr-6-2 ``` --- diff --git a/docs/source/en/user_guide/envs/dm_control/dm_point_mass.md b/docs/source/en/user_guide/envs/dm_control/dm_point_mass.md index dc10b04e..0b6eea61 100644 --- a/docs/source/en/user_guide/envs/dm_control/dm_point_mass.md +++ b/docs/source/en/user_guide/envs/dm_control/dm_point_mass.md @@ -121,7 +121,7 @@ python scripts/view.py env=point_mass ### 2. Start Training ```bash -python scripts/train.py task=point_mass/skrl.ppo +mxtrain task=point_mass/skrl.ppo ``` ### 3. View Training Progress @@ -133,7 +133,7 @@ tensorboard --logdir runs/point_mass ### 4. Test Training Results ```bash -python scripts/play.py env=point_mass +mxplay env=point_mass ``` --- diff --git a/docs/source/en/user_guide/envs/dm_control/dm_quadruped.md b/docs/source/en/user_guide/envs/dm_control/dm_quadruped.md index 60467388..77ab8b7d 100644 --- a/docs/source/en/user_guide/envs/dm_control/dm_quadruped.md +++ b/docs/source/en/user_guide/envs/dm_control/dm_quadruped.md @@ -237,10 +237,10 @@ python scripts/view.py env=dm-quadruped-fetch ### 2. Start Training ```bash -python scripts/train.py task=dm-quadruped-walk/skrl.ppo -python scripts/train.py task=dm-quadruped-run/skrl.ppo -python scripts/train.py task=dm-quadruped-escape/skrl.ppo -python scripts/train.py task=dm-quadruped-fetch/skrl.ppo +mxtrain task=dm-quadruped-walk/skrl.ppo +mxtrain task=dm-quadruped-run/skrl.ppo +mxtrain task=dm-quadruped-escape/skrl.ppo +mxtrain task=dm-quadruped-fetch/skrl.ppo ``` ### 3. View Training Progress @@ -255,10 +255,10 @@ tensorboard --logdir runs/dm-quadruped-fetch ### 4. Test Training Results ```bash -python scripts/play.py env=dm-quadruped-walk -python scripts/play.py env=dm-quadruped-run -python scripts/play.py env=dm-quadruped-escape -python scripts/play.py env=dm-quadruped-fetch +mxplay env=dm-quadruped-walk +mxplay env=dm-quadruped-run +mxplay env=dm-quadruped-escape +mxplay env=dm-quadruped-fetch ``` --- diff --git a/docs/source/en/user_guide/envs/dm_control/dm_reacher.md b/docs/source/en/user_guide/envs/dm_control/dm_reacher.md index 944abf35..64cd3182 100644 --- a/docs/source/en/user_guide/envs/dm_control/dm_reacher.md +++ b/docs/source/en/user_guide/envs/dm_control/dm_reacher.md @@ -108,7 +108,7 @@ python scripts/view.py env=dm-reacher ### 2. Start Training ```bash -python scripts/train.py task=dm-reacher/skrl.ppo +mxtrain task=dm-reacher/skrl.ppo ``` ### 3. View Training Progress @@ -120,7 +120,7 @@ tensorboard --logdir runs/dm-reacher ### 4. Test Training Results ```bash -python scripts/play.py env=dm-reacher +mxplay env=dm-reacher ``` ## Expected Training Results diff --git a/docs/source/en/user_guide/envs/dm_control/dm_walker.md b/docs/source/en/user_guide/envs/dm_control/dm_walker.md index be2b6fb2..c087a624 100644 --- a/docs/source/en/user_guide/envs/dm_control/dm_walker.md +++ b/docs/source/en/user_guide/envs/dm_control/dm_walker.md @@ -26,19 +26,19 @@ Walker2D is a 2D planar bipedal robot with multiple joints and actuators: 1. **dm-stander**: Static standing task (move_speed = 0.0) ```bash -python scripts/train.py task=dm-stander/skrl.ppo +mxtrain task=dm-stander/skrl.ppo ``` 2. **dm-walker**: Walking task (move_speed = 1.0) ```bash -python scripts/train.py task=dm-walker/skrl.ppo +mxtrain task=dm-walker/skrl.ppo ``` 3. **dm-runner**: Running task (move_speed = 5.0) ```bash -python scripts/train.py task=dm-runner/skrl.ppo +mxtrain task=dm-runner/skrl.ppo ``` ## Quick Start @@ -54,9 +54,9 @@ python scripts/view.py env=dm-runner ### 2. Start Training ```bash -python scripts/train.py task=dm-stander/skrl.ppo -python scripts/train.py task=dm-walker/skrl.ppo -python scripts/train.py task=dm-runner/skrl.ppo +mxtrain task=dm-stander/skrl.ppo +mxtrain task=dm-walker/skrl.ppo +mxtrain task=dm-runner/skrl.ppo ``` ### 3. View Training Progress @@ -68,9 +68,9 @@ tensorboard --logdir runs/dm-walker ### 4. Test Training Results ```bash -python scripts/play.py env=dm-stander -python scripts/play.py env=dm-walker -python scripts/play.py env=dm-runner +mxplay env=dm-stander +mxplay env=dm-walker +mxplay env=dm-runner ``` ## Reward Function Design diff --git a/docs/source/en/user_guide/envs/humanoid_velocity_tracking.md b/docs/source/en/user_guide/envs/humanoid_velocity_tracking.md index 6291dbdf..20fd428a 100644 --- a/docs/source/en/user_guide/envs/humanoid_velocity_tracking.md +++ b/docs/source/en/user_guide/envs/humanoid_velocity_tracking.md @@ -94,15 +94,15 @@ Choose an environment ID and one of its training configs from the table, then re ```bash python scripts/view.py env=ENV_ID num_envs=1 -python scripts/train.py task=ENV_ID/TRAINING_CONFIG -python scripts/play.py env=ENV_ID num_envs=16 +mxtrain task=ENV_ID/TRAINING_CONFIG +mxplay env=ENV_ID num_envs=16 ``` For example, train the K1 uneven-terrain task with asynchronous FastSAC: ```bash -python scripts/train.py task=k1-walk-rough/motrix.fastsac algo.asynchronous=true +mxtrain task=k1-walk-rough/motrix.fastsac algo.asynchronous=true ``` -`view.py` applies random actions and is intended for scene and model inspection. Use `play.py` with a trained policy to +`view.py` applies random actions and is intended for scene and model inspection. Use `mxplay` with a trained policy to inspect the learned gait. diff --git a/docs/source/en/user_guide/envs/humanoid_velocity_tracking/adding_robot.md b/docs/source/en/user_guide/envs/humanoid_velocity_tracking/adding_robot.md index 963483e5..5f97edad 100644 --- a/docs/source/en/user_guide/envs/humanoid_velocity_tracking/adding_robot.md +++ b/docs/source/en/user_guide/envs/humanoid_velocity_tracking/adding_robot.md @@ -130,9 +130,10 @@ registry.env("-walk-flat")(HumanoidVelocityTrackingEnv) registry.env("-walk-terrain")(HumanoidVelocityTrackingEnv) ``` -Import the new `` module from `motrix_envs/locomotion/humanoid/__init__.py`, so `import motrix_envs` performs both -config and environment registration. Each `registry.env(...)` ID must exactly match its `@registry.envcfg(...)` ID. If -the task only has a flat-ground config, register only that ID. See +Import the new `` module from `motrix_envs/locomotion/humanoid/__init__.py` so the built-in registration +callback includes it. For a standalone package, follow [Environment registration](../../tutorial/building_envs/direct_env.md#environment-registration). +Each `registry.env(...)` ID must exactly match its `@registry.envcfg(...)` ID. If the task only has a flat-ground config, +register only that ID. See [Config Overrides and Tuning](config_tuning.md) for deriving a terrain config. ## 5. Add training configs @@ -166,8 +167,8 @@ Use this progression: python scripts/view.py robot= python scripts/view.py env=-walk-flat python scripts/view.py env=-walk-terrain -python scripts/train.py task=-walk-flat/motrix.fastsac -python scripts/train.py task=-walk-flat/motrix.fastsac algo.asynchronous=false +mxtrain task=-walk-flat/motrix.fastsac +mxtrain task=-walk-flat/motrix.fastsac algo.asynchronous=false python -m pytest motrix_envs/tests/test_humanoid_walk.py -q python -m pytest motrix_rl/tests/test_task_configs.py -q ``` diff --git a/docs/source/en/user_guide/envs/manipulation/bounce_ball.md b/docs/source/en/user_guide/envs/manipulation/bounce_ball.md index 3adc7ae4..0b355cbb 100644 --- a/docs/source/en/user_guide/envs/manipulation/bounce_ball.md +++ b/docs/source/en/user_guide/envs/manipulation/bounce_ball.md @@ -417,7 +417,7 @@ python scripts/view.py env=bounce_ball ### 2. Start Training ```bash -python scripts/train.py task=bounce_ball/skrl.ppo +mxtrain task=bounce_ball/skrl.ppo ``` ### 3. View Training Progress @@ -429,7 +429,7 @@ tensorboard --logdir runs/bounce_ball ### 4. Test Training Results ```bash -python scripts/play.py env=bounce_ball +mxplay env=bounce_ball ``` --- diff --git a/docs/source/en/user_guide/envs/manipulation/franka_lift_cube.md b/docs/source/en/user_guide/envs/manipulation/franka_lift_cube.md index 32b43f56..aac56c57 100644 --- a/docs/source/en/user_guide/envs/manipulation/franka_lift_cube.md +++ b/docs/source/en/user_guide/envs/manipulation/franka_lift_cube.md @@ -233,13 +233,13 @@ Target position is randomly sampled in the following range: ### Training ```bash -python scripts/train.py task=franka-lift-cube/skrl.ppo +mxtrain task=franka-lift-cube/skrl.ppo ``` ### Policy Evaluation ```bash -python scripts/play.py env=franka-lift-cube +mxplay env=franka-lift-cube ``` ### TensorBoard diff --git a/docs/source/en/user_guide/envs/manipulation/franka_open_cabinet.md b/docs/source/en/user_guide/envs/manipulation/franka_open_cabinet.md index 5ec45c01..73a953a1 100644 --- a/docs/source/en/user_guide/envs/manipulation/franka_open_cabinet.md +++ b/docs/source/en/user_guide/envs/manipulation/franka_open_cabinet.md @@ -234,13 +234,13 @@ Cabinet is fixed on ground with drawer in closed state (joint position at 0). ### Training ```bash -python scripts/train.py task=franka-open-cabinet/skrl.ppo +mxtrain task=franka-open-cabinet/skrl.ppo ``` ### Policy Evaluation ```bash -python scripts/play.py env=franka-open-cabinet +mxplay env=franka-open-cabinet ``` ### TensorBoard diff --git a/docs/source/en/user_guide/envs/manipulation/rm65_insert_peg.md b/docs/source/en/user_guide/envs/manipulation/rm65_insert_peg.md index fa814f34..7f3dc920 100644 --- a/docs/source/en/user_guide/envs/manipulation/rm65_insert_peg.md +++ b/docs/source/en/user_guide/envs/manipulation/rm65_insert_peg.md @@ -269,13 +269,13 @@ The task currently provides an Async FastSAC configuration with `2048` parallel ### Training ```bash -python scripts/train.py task=rm65_insert_peg/motrix.fastsac +mxtrain task=rm65_insert_peg/motrix.fastsac ``` ### Policy Evaluation ```bash -python scripts/play.py env=rm65_insert_peg num_envs=16 +mxplay env=rm65_insert_peg num_envs=16 ``` ### TensorBoard diff --git a/docs/source/en/user_guide/envs/manipulation/rm65_open_cabinet.md b/docs/source/en/user_guide/envs/manipulation/rm65_open_cabinet.md index 4c1c26ed..221a4821 100644 --- a/docs/source/en/user_guide/envs/manipulation/rm65_open_cabinet.md +++ b/docs/source/en/user_guide/envs/manipulation/rm65_open_cabinet.md @@ -241,13 +241,13 @@ In addition, the maximum episode length is `30s`. ### Training ```bash -python scripts/train.py task=rm65-open-cabinet/skrl.ppo task.train_backend=torch +mxtrain task=rm65-open-cabinet/skrl.ppo task.train_backend=torch ``` ### Policy Evaluation ```bash -python scripts/play.py env=rm65-open-cabinet +mxplay env=rm65-open-cabinet ``` ### TensorBoard diff --git a/docs/source/en/user_guide/envs/manipulation/shadow_hand_repose.md b/docs/source/en/user_guide/envs/manipulation/shadow_hand_repose.md index 3e940167..94288533 100644 --- a/docs/source/en/user_guide/envs/manipulation/shadow_hand_repose.md +++ b/docs/source/en/user_guide/envs/manipulation/shadow_hand_repose.md @@ -308,13 +308,13 @@ The environment uses a consecutive success counter: ### Training ```bash -python scripts/train.py task=shadow-hand-repose/skrl.ppo +mxtrain task=shadow-hand-repose/skrl.ppo ``` ### Policy Evaluation ```bash -python scripts/play.py env=shadow-hand-repose +mxplay env=shadow-hand-repose ``` ### Environment Visualization diff --git a/docs/source/en/user_guide/envs/quadruped_locomotion/go1_rough_terrain.md b/docs/source/en/user_guide/envs/quadruped_locomotion/go1_rough_terrain.md index 0bf26506..e7722504 100644 --- a/docs/source/en/user_guide/envs/quadruped_locomotion/go1_rough_terrain.md +++ b/docs/source/en/user_guide/envs/quadruped_locomotion/go1_rough_terrain.md @@ -27,6 +27,6 @@ physical parameters. ```bash python scripts/view.py env=go1-stairs-terrain-walk -python scripts/train.py task=go1-stairs-terrain-walk/skrl.ppo -python scripts/play.py env=go1-stairs-terrain-walk +mxtrain task=go1-stairs-terrain-walk/skrl.ppo +mxplay env=go1-stairs-terrain-walk ``` diff --git a/docs/source/en/user_guide/envs/quadruped_velocity_tracking.md b/docs/source/en/user_guide/envs/quadruped_velocity_tracking.md index 9ca02444..5152954d 100644 --- a/docs/source/en/user_guide/envs/quadruped_velocity_tracking.md +++ b/docs/source/en/user_guide/envs/quadruped_velocity_tracking.md @@ -115,15 +115,15 @@ Choose an environment ID and one of its training configs from the table, then re ```bash python scripts/view.py env=ENV_ID num_envs=1 -python scripts/train.py task=ENV_ID/TRAINING_CONFIG -python scripts/play.py env=ENV_ID num_envs=16 +mxtrain task=ENV_ID/TRAINING_CONFIG +mxplay env=ENV_ID num_envs=16 ``` For example, train the Go2 rough-terrain task with RSL-RL PPO: ```bash -python scripts/train.py task=go2-walk-rough/rslrl.ppo +mxtrain task=go2-walk-rough/rslrl.ppo ``` -`view.py` applies random actions and is intended for scene and model inspection. Use `play.py` with a trained policy to +`view.py` applies random actions and is intended for scene and model inspection. Use `mxplay` with a trained policy to inspect the learned gait. diff --git a/docs/source/en/user_guide/envs/quadruped_velocity_tracking/adding_robot.md b/docs/source/en/user_guide/envs/quadruped_velocity_tracking/adding_robot.md index 2f179165..f01e0bab 100644 --- a/docs/source/en/user_guide/envs/quadruped_velocity_tracking/adding_robot.md +++ b/docs/source/en/user_guide/envs/quadruped_velocity_tracking/adding_robot.md @@ -168,7 +168,8 @@ registry.env("-walk-flat")(QuadrupedWalkTask) registry.env("-walk-rough")(QuadrupedWalkTask) ``` -Import the module from `motrix_envs/locomotion/quadruped/__init__.py`, so `import motrix_envs` performs registration. +Import the module from `motrix_envs/locomotion/quadruped/__init__.py` so the built-in registration callback includes it. +For a standalone package, follow [Environment registration](../../tutorial/building_envs/direct_env.md#environment-registration). Each `registry.env(...)` ID must exactly match its `@registry.envcfg(...)` ID. Register only the flat ID when no rough config is provided. @@ -199,7 +200,7 @@ Use this progression: python scripts/view.py robot= python scripts/view.py env=-walk-flat python scripts/view.py env=-walk-rough -python scripts/train.py task=-walk-flat/rslrl.ppo +mxtrain task=-walk-flat/rslrl.ppo ``` During preview, inspect the default pose, action direction, all four foot contacts, foot-position reference frames, and diff --git a/docs/source/en/user_guide/envs/whole_body_tracking/adding_wbt_task.md b/docs/source/en/user_guide/envs/whole_body_tracking/adding_wbt_task.md index eaeb5335..480a57b9 100644 --- a/docs/source/en/user_guide/envs/whole_body_tracking/adding_wbt_task.md +++ b/docs/source/en/user_guide/envs/whole_body_tracking/adding_wbt_task.md @@ -84,8 +84,9 @@ Bind the Env ID to the shared implementation in the same `g1.py` module: registry.env("g1-wbt-dance1-subject1")(ManagerEnv) ``` -The `motrix_envs.locomotion.wbt` package already imports this module, so `import motrix_envs` executes the registration. The -`envcfg` and `env` names must match exactly. +The `motrix_envs.locomotion.wbt` package already imports this module, so the built-in registration callback includes it. +For a standalone package, follow [Environment registration](../../tutorial/building_envs/direct_env.md#environment-registration). +The `envcfg` and `env` names must match exactly. ## 5. Add Training Tasks @@ -129,7 +130,7 @@ Task for the same robot first, and tune algorithm hyperparameters only after the Use a small smoke test for Hydra composition, registry creation, tensor shapes, and the training path: ```bash -python scripts/train.py task=g1-wbt-dance1-subject1/motrix.fastsac \ +mxtrain task=g1-wbt-dance1-subject1/motrix.fastsac \ algo.asynchronous=true num_envs=64 algo.trainer.num_learning_iterations=100 ``` @@ -137,13 +138,13 @@ Check for missing motion, joint, or body names; systematic NaNs, joint-limit vio reset; and verify that `state.reward_terms` and `state.metrics` reach the logs. Then start the default training run: ```bash -python scripts/train.py task=g1-wbt-dance1-subject1/motrix.fastsac algo.asynchronous=true +mxtrain task=g1-wbt-dance1-subject1/motrix.fastsac algo.asynchronous=true ``` After training creates a metadata-backed run, play the policy: ```bash -python scripts/play.py env=g1-wbt-dance1-subject1 num_envs=16 +mxplay env=g1-wbt-dance1-subject1 num_envs=16 ``` Play mode starts at frame 0, disables reset noise and adaptive sampling, and restarts from the beginning at the clip end. A @@ -153,10 +154,10 @@ separate play environment is not required. - The motion passes schema loading and target-robot kinematic replay, with `fps == 1 / ctrl_dt`. - `envcfg`, `env`, and Training Tasks use the same Env ID. -- The package import path executes the new config and environment registrations. +- Registry discovery invokes the installed callback and reaches the new config and environment registrations. - Hydra discovers the `motrix.fastsac` Training Task, and `algo.asynchronous` selects either execution topology. - Small-scale observation, action, reward, terminated, and truncated shapes are correct. - Resets do not systematically produce NaNs, hard-limit violations, or immediate bad tracking. - Reward, termination, and adaptive-sampling metrics reach the logs. -- `play.py` discovers the run and plays the policy from frame 0 through the complete clip. +- `mxplay` discovers the run and plays the policy from frame 0 through the complete clip. - The new motion includes source and license information and is managed through Git LFS. diff --git a/docs/source/en/user_guide/envs/whole_body_tracking/index.md b/docs/source/en/user_guide/envs/whole_body_tracking/index.md index 00558871..40539578 100644 --- a/docs/source/en/user_guide/envs/whole_body_tracking/index.md +++ b/docs/source/en/user_guide/envs/whole_body_tracking/index.md @@ -165,20 +165,20 @@ Choose an environment ID and one of its training configs from the built-in task `TRAINING_CONFIG`: ```bash -python scripts/train.py task=ENV_ID/TRAINING_CONFIG +mxtrain task=ENV_ID/TRAINING_CONFIG ``` For example, train the G1 dance-tracking task with asynchronous FastSAC: ```bash -python scripts/train.py task=g1-wbt-dance/motrix.fastsac algo.asynchronous=true +mxtrain task=g1-wbt-dance/motrix.fastsac algo.asynchronous=true ``` The built-in configs use 2048 parallel environments and task-specific learning-iteration budgets. Use a small smoke test only to validate registry creation, tensor shapes, and the training entry point; it is not expected to produce a useful policy: ```bash -python scripts/train.py task=g1-wbt-dance/motrix.fastsac \ +mxtrain task=g1-wbt-dance/motrix.fastsac \ algo.asynchronous=true num_envs=64 algo.trainer.num_learning_iterations=100 ``` @@ -189,10 +189,10 @@ adaptive-sampling statistics are written to `state.metrics`. See [Task Environme ### Play the policy ```bash -python scripts/play.py env=ENV_ID num_envs=16 +mxplay env=ENV_ID num_envs=16 ``` -`play.py` selects the best policy from the latest metadata-backed run for the environment. The WBT play config starts at +`mxplay` selects the best policy from the latest metadata-backed run for the environment. The WBT play config starts at motion frame 0, disables reset noise and adaptive sampling, and removes the 10-second training time limit. At the clip end it restarts from frame 0. See [Training Artifacts: the runs Directory and Checkpoint Structure](../../tutorial/training/runs_and_checkpoints.md) for run and checkpoint selection. diff --git a/docs/source/en/user_guide/getting_started/container_deployment.md b/docs/source/en/user_guide/getting_started/container_deployment.md index 29a86fa5..9a63ee54 100644 --- a/docs/source/en/user_guide/getting_started/container_deployment.md +++ b/docs/source/en/user_guide/getting_started/container_deployment.md @@ -98,7 +98,7 @@ If you only want to run the training container without using Docker Compose: docker run --gpus all \ -v $(pwd)/runs:/root/motrixlab/runs \ motphys/motrixlab:latest \ - scripts/train.py task=cartpole/skrl.ppo task.train_backend=jax num_envs=2048 + mxtrain task=cartpole/skrl.ppo task.train_backend=jax num_envs=2048 ``` ### Persisting Training Results diff --git a/docs/source/en/user_guide/getting_started/hello_motrixlab.md b/docs/source/en/user_guide/getting_started/hello_motrixlab.md index b592aa8e..62b05c79 100644 --- a/docs/source/en/user_guide/getting_started/hello_motrixlab.md +++ b/docs/source/en/user_guide/getting_started/hello_motrixlab.md @@ -19,7 +19,7 @@ This will open a visualization window showing the cartpole physics simulation en Start training the cartpole balancing task: ```bash -python scripts/train.py task=cartpole/skrl.ppo +mxtrain task=cartpole/skrl.ppo ``` The training process will automatically: @@ -38,7 +38,7 @@ Training results will be saved in the `runs/cartpole/` directory, including: If you want to observe the model's learning process during training, you can enable visualization rendering: ```bash -python scripts/train.py task=cartpole/skrl.ppo render=true +mxtrain task=cartpole/skrl.ppo render=true ``` ### 🎮 Interactive Rendering Control @@ -67,10 +67,10 @@ After training is complete, test the trained policy: ```bash # Automatically find best policy for testing (recommended) -python scripts/play.py env=cartpole +mxplay env=cartpole # Manually specify a checkpoint from a metadata-backed run -python scripts/play.py env=cartpole policy=/path/to/run/checkpoints/policy-file +mxplay env=cartpole policy=/path/to/run/checkpoints/policy-file ``` > **Tip**: The system automatically finds the latest run and its best policy under `runs/cartpole/`. A manually selected checkpoint must belong to a run containing `metadata.json` and `task_config.yaml`. diff --git a/docs/source/en/user_guide/tutorial/advanced/custom_training_backend.md b/docs/source/en/user_guide/tutorial/advanced/custom_training_backend.md index 0d35aaa8..efad55b3 100644 --- a/docs/source/en/user_guide/tutorial/advanced/custom_training_backend.md +++ b/docs/source/en/user_guide/tutorial/advanced/custom_training_backend.md @@ -162,7 +162,7 @@ MotrixLab loads installed entry points from this group before resolving a framew ## Step 5: Add Hydra Training Configuration -Registering the provider installs the structured Hydra schema for `MyPpoCfg`. Add the algorithm's source-of-truth values: +Registering the provider installs the structured Hydra schema for `MyPpoCfg`. Keep custom algorithm and Task YAML in your workspace's `configs/`; built-in algorithm defaults live in `motrix_rl/src/motrix_rl/configs/algo_base/`. Add the custom algorithm's source-of-truth values: ```yaml # configs/algo_base/myrl.ppo.yaml @@ -200,7 +200,7 @@ The Task's `rllib`, `algo`, and `train_backend` values must match the framework If the backend is registered on the startup path, use the shared CLI: ```bash -python scripts/train.py task=cartpole/myrl.ppo +mxtrain task=cartpole/myrl.ppo ``` For an external experiment package, use its Hydra config root and import the backend registration module before entering the training function. No separate Python Task registry is required. diff --git a/docs/source/en/user_guide/tutorial/advanced/export_onnx.md b/docs/source/en/user_guide/tutorial/advanced/export_onnx.md index b3da493b..41208e5d 100644 --- a/docs/source/en/user_guide/tutorial/advanced/export_onnx.md +++ b/docs/source/en/user_guide/tutorial/advanced/export_onnx.md @@ -24,6 +24,13 @@ of the default runtime environment installed by `sh install.sh` (see ## Export the Model +Activate the installed environment and run `mxexport onnx` (or `python -m motrix_rl.commands.export onnx`). +`mxexport` requires an explicit `onnx` or `deploy` subcommand; there is no default. +No `task` selection or registration import is needed: export restores the policy from the saved run without creating an environment. Install the RL provider used by the run; environment-provider discovery is a separate registry concern. +The command uses the current directory's `./configs` when present, or an explicit `--config-dir `; +project files take precedence, with missing files falling back to `motrix_rl.configs`, which includes `export_onnx.yaml`. +No repository checkout is required. Relative run and output paths are resolved from the current working directory. + Suppose training produced this run directory: ```text @@ -33,7 +40,7 @@ runs/cartpole/skrl/torch/ppo// Export it to an explicit output path: ```bash -python scripts/export_onnx.py \ +mxexport onnx \ run_dir=runs/cartpole/skrl/torch/ppo/ \ output=artifacts/cartpole.onnx ``` @@ -43,7 +50,7 @@ If a path contains spaces, quote the complete Hydra argument, for example `"run_ When `output` is omitted, the model is written next to the best checkpoint as `policy.onnx`: ```bash -python scripts/export_onnx.py \ +mxexport onnx \ run_dir=runs/cartpole/skrl/torch/ppo/ ``` @@ -105,7 +112,7 @@ In most cases, only `run_dir` and `output` are needed. Override these Hydra para For example, export with opset 17 and more validation samples: ```bash -python scripts/export_onnx.py \ +mxexport onnx \ run_dir= \ output=policy.onnx \ opset=17 \ diff --git a/docs/source/en/user_guide/tutorial/advanced/motrix_deploy.md b/docs/source/en/user_guide/tutorial/advanced/motrix_deploy.md index fa298139..7f105b73 100644 --- a/docs/source/en/user_guide/tutorial/advanced/motrix_deploy.md +++ b/docs/source/en/user_guide/tutorial/advanced/motrix_deploy.md @@ -4,7 +4,7 @@ Run an exported Go2 policy in MuJoCo before attempting hardware deployment. With a bounded headless check is: ```bash -motrix-deploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy runtime.viewer=false duration_s=2.0 +mxdeploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy runtime.viewer=false duration_s=2.0 ``` The following steps create that artifact, explain interactive control, and cover the existing Unitree hardware workflow. @@ -16,7 +16,7 @@ From the repository root: ```bash sh install.sh --all source .venv/bin/activate -python scripts/train.py task=go2-walk-flat/rslrl.ppo +mxtrain task=go2-walk-flat/rslrl.ppo ``` Training results are saved under `runs/go2-walk-flat/`. Skip training if you already have a metadata-backed run. @@ -24,13 +24,24 @@ Training results are saved under `runs/go2-walk-flat/`. Skip training if you alr ## 2. Export and inspect the artifact ```bash -python scripts/export_deploy.py env=go2-walk-flat -motrix-deploy inspect artifact=artifacts/go2-walk-flat.deploy +mxexport deploy env=go2-walk-flat output=artifacts/go2-walk-flat.deploy +mxdeploy inspect artifact=artifacts/go2-walk-flat.deploy ``` -Export selects the latest run and creates `artifacts/go2-walk-flat.deploy/`. To select a specific run, use -`run=` instead of `env`; `output=` changes the destination. Existing artifact directories are -not overwritten. Confirm that inspection reports `valid: true`. +`env=go2-walk-flat` selects the latest metadata-backed run. To select a specific run, use +`run_dir=` instead of `env`: exactly one of `env` and `run_dir` must be set. `output=` changes +the destination; when omitted, it defaults to `artifacts/.deploy`. Existing artifact directories are not +overwritten. Confirm that inspection reports `valid: true`. + +`mxexport` requires an explicit `onnx` or `deploy` subcommand. The equivalent module command is +`python -m motrix_rl.commands.export deploy`. Export uses the current directory's `./configs` when present, or an +explicit `--config-dir `; project files take precedence and missing files fall back to `motrix_rl.configs`. +Both `export_deploy.yaml` and `export_onnx.yaml` ship there, so export does not require a repository checkout. +Deployment export keeps its numerical-check options under `validation.*` (for example `validation.atol=3e-5`), +not the ONNX subcommand's `parity.*`. + +Export runs in `motrix-rl` and requires the training provider and an installed profile compiler for the environment. +The dependency is one-way: `motrix-rl` uses `motrix-deploy` to create artifacts; `motrix-deploy` has no RL dependency. The artifact directory is self-contained: @@ -55,7 +66,7 @@ string selector, not a nested configuration mapping or Hydra group. Use the headless command above for a bounded run, or open the viewer: ```bash -motrix-deploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy +mxdeploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy ``` Hold `W/S` to move forward/backward, `A/D` sideways, and `Q/E` to turn. Close the window, press Esc, or press Ctrl-C to stop. @@ -85,7 +96,7 @@ Raw ONNX use requires explicit task preprocessing and action settings; the artif With `motrix-deploy-motrixsim` installed, switch the backend to `motrixsim` to reuse the same artifact and scene recipe: ```bash -motrix-deploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy \ +mxdeploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy \ runtime.backend=motrixsim runtime.viewer=true ``` @@ -102,7 +113,7 @@ ready. Startup can physically stand the robot down before the remote Start gate. `enp5s0` with the actual interface: ```bash -motrix-deploy task=go2-walk-flat/hardware \ +mxdeploy task=go2-walk-flat/hardware \ artifact=artifacts/go2-walk-flat.deploy \ runtime.network_interface=enp5s0 ``` @@ -143,7 +154,7 @@ returns to the default pose, and closes with damping. ## Deploy the G1 WBT dance -The G1 whole-body-tracking dance task uses the same export → artifact → `motrix-deploy` pipeline. The artifact embeds +The G1 whole-body-tracking dance task uses the same export → artifact → `mxdeploy` pipeline. The artifact embeds the servo gains, action scales, preparation gains and timing, safety thresholds, and the motion clip itself (as a `payloads/motion.npz` payload with its checksum in the manifest), so deployment needs neither Torch nor an RL environment. Export needs the training provider and task environment packages, but does **not** train: @@ -151,18 +162,18 @@ Export needs the training provider and task environment packages, but does **not ```bash source .venv/bin/activate RUN=runs/g1-wbt-dance/motrix/torch/fastsac/ -python scripts/export_deploy.py run="$RUN" \ +mxexport deploy run_dir="$RUN" \ output=artifacts/g1-wbt-dance-software-pd.deploy validation.atol=3e-5 -motrix-deploy task=g1-wbt-dance/sim artifact=artifacts/g1-wbt-dance.deploy +mxdeploy task=g1-wbt-dance/sim artifact=artifacts/g1-wbt-dance.deploy ``` Headless validation and artifact inspection: ```bash -motrix-deploy task=g1-wbt-dance/sim artifact=artifacts/g1-wbt-dance.deploy \ +mxdeploy task=g1-wbt-dance/sim artifact=artifacts/g1-wbt-dance.deploy \ runtime.viewer=false -motrix-deploy inspect artifact=artifacts/g1-wbt-dance.deploy +mxdeploy inspect artifact=artifacts/g1-wbt-dance.deploy ``` The default duration comes from the embedded motion clip; shorter runs use `duration_s`, longer durations are rejected @@ -181,6 +192,49 @@ damping directly, and ending upright requires the motion clip itself to finish a ## Advanced usage +### Add a deployment profile compiler + +An environment package publishes one zero-argument registration callback in the +`motrix_deploy.profiles` group. Declare the provider in `pyproject.toml`: + +```toml +[project.entry-points."motrix_deploy.profiles"] +my-project = "my_project.deploy:register" +``` + +In `my_project/deploy/__init__.py`, register the environment-to-compiler mappings: + +```python +def register() -> None: + from motrix_deploy.profile import register_profile_compiler + from my_project.deploy.walk import build_profile + + register_profile_compiler("my-walk-flat", build_profile) +``` + +Implement `build_profile(env_name: str) -> DeploymentProfile` in `my_project/deploy/walk.py`, importing +`DeploymentProfile` from `motrix_deploy.profile`. The compiler builds the selected environment's robot, task, +control settings, training-space observation/action sizes, and task-owned payloads into that profile. +The registration callback only declares mappings: do not create environments or models or read motion data there. +One callback can register several Env IDs, and the same compiler can serve multiple IDs. + +Install the package, then export a matching run: + +```bash +mxexport deploy run_dir=/path/to/my-walk-flat/run output=artifacts/my-walk-flat.deploy +``` + +The first profile lookup, listing, or build loads all installed provider callbacks, once per process. +`registered_profile_compilers()` in `motrix_deploy.profile` therefore executes registration code before listing Env IDs; +only `build_deployment_profile(env_name)` invokes the selected environment's compiler. +Install the import dependencies of every provider: a provider with missing dependencies can prevent discovery even +when a different Env ID was requested. Duplicate provider names, duplicate compiler Env IDs, and provider loading or +registration failures are reported as errors. + +Profile discovery needs no application-side registration imports. Providers use the deployment API without an RL +dependency; `motrix-rl` consumes their profiles for export, not the reverse. Built-in compiler mappings are listed in the +[Motrix Environments README](https://github.com/Motphys/MotrixLab/blob/main/motrix_envs/README.md). + ### Load a task spec from YAML For a programmatic application, keep the complete Go2 task fields in `task.yaml` (the contents of @@ -211,7 +265,7 @@ Select a world with `runtime.deploy_env_id` and a robot with `runtime.robot_id`. artifact but switch its world: ```bash -motrix-deploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy \ +mxdeploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy \ runtime.deploy_env_id=rough ``` @@ -306,7 +360,7 @@ The installed simulation recipes default to MuJoCo. With `motrix-deploy-motrixsi installed, choose the native backend using the same recipe and artifact: ```bash -motrix-deploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy \ +mxdeploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy \ runtime.backend=motrixsim runtime.viewer=false runtime.realtime=false duration_s=1.0 ``` @@ -333,7 +387,7 @@ This illustrative command assumes you have authored `/absolute/path/to/my_app/co or copy a built-in recipe: ```bash -motrix-deploy \ +mxdeploy \ --config-path /absolute/path/to/my_app/config \ --config-name deploy \ artifact=artifacts/go2-walk-flat.deploy diff --git a/docs/source/en/user_guide/tutorial/basic_frame.md b/docs/source/en/user_guide/tutorial/basic_frame.md index bcb40ff7..56045048 100644 --- a/docs/source/en/user_guide/tutorial/basic_frame.md +++ b/docs/source/en/user_guide/tutorial/basic_frame.md @@ -47,7 +47,8 @@ MotrixLab is a UV workspace. The packages are grouped by ownership rather than t | `motrix_rl_builtin` | Policy training and updates | Built-in Motrix FastSAC provider | | `motrix_rl_skrl` | Policy training and updates | SKRL PPO provider for Torch (required) and optional JAX extra | | `motrix_rl_rslrl` | Policy training and updates | RSL-RL PPO provider for Torch (optional) | -| `configs/`, `scripts/` | Configuration and orchestration | Hydra algorithm configs and Task recipes; train / play / view / export entry points | +| `motrix_rl.configs`, `motrix_rl.commands` | Configuration and orchestration | Packaged train/play configs and algorithm defaults; installed `mxtrain` / `mxplay` entry points | +| `configs/`, `scripts/` | Workspace recipes and tools | Concrete Task recipes and view config; view / export tools and thin train/play wrappers | | `motrix_deploy` | Policy deployment | Framework-independent artifacts, runtime contracts, and deployment CLI | | `motrix_deploy_mujoco` | Policy deployment | MuJoCo deployment backend plugin | | `motrix_deploy_motrixsim` | Policy deployment | MotrixSim deployment backend plugin | @@ -58,13 +59,28 @@ A simulation backend (such as `motrix_env_motrixsim`) is isolated behind the `Si when using an environment you normally do not need to care which one it is. To select or integrate a backend, see [SimBackend: Decoupling from the Simulator](building_envs/sim_backend.md). +The configuration and command sources are laid out as follows: + +```text +motrix_rl/src/motrix_rl/ +├── commands/ # mxtrain / mxplay +└── configs/ + ├── train.yaml + ├── play.yaml + └── algo_base/ +configs/ +├── task/ # workspace training recipes +└── view.yaml +scripts/ # view/export tools; thin train/play wrappers +``` + ## One full training run ```bash -python scripts/train.py task=cartpole/skrl.ppo num_envs=1024 +mxtrain task=cartpole/skrl.ppo num_envs=1024 ``` -1. Hydra composes `configs/train.yaml`, `configs/algo_base/skrl.ppo.yaml`, and +1. Hydra composes `motrix_rl/src/motrix_rl/configs/train.yaml`, `motrix_rl/src/motrix_rl/configs/algo_base/skrl.ppo.yaml`, and `configs/task/cartpole/skrl.ppo.yaml`; `num_envs=1024` only overrides this run. 2. The runner resolves the SKRL PPO provider and automatically picks an available JAX/Torch backend. 3. The trainer creates the registered `cartpole` environment through the registry and starts optimizing. @@ -73,8 +89,8 @@ python scripts/train.py task=cartpole/skrl.ppo num_envs=1024 The same environment can have several Task recipes without touching the environment implementation: ```bash -python scripts/train.py task=cartpole/skrl.ppo -python scripts/train.py task=cartpole/rslrl.ppo +mxtrain task=cartpole/skrl.ppo +mxtrain task=cartpole/rslrl.ppo ``` ## Run an exported policy @@ -82,7 +98,7 @@ python scripts/train.py task=cartpole/rslrl.ppo Deployment uses an artifact rather than a training environment: ```bash -motrix-deploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy runtime.viewer=false duration_s=2.0 +mxdeploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy runtime.viewer=false duration_s=2.0 ``` This runs the exported policy headlessly in MuJoCo. See [Go2 deployment](advanced/motrix_deploy.md) for export, diff --git a/docs/source/en/user_guide/tutorial/building_envs/direct_env.md b/docs/source/en/user_guide/tutorial/building_envs/direct_env.md index 26cc94b7..811728f6 100644 --- a/docs/source/en/user_guide/tutorial/building_envs/direct_env.md +++ b/docs/source/en/user_guide/tutorial/building_envs/direct_env.md @@ -182,7 +182,12 @@ simulation parameters are covered in [Setting Up the Physics Scene](scene.md). -## Registration rules +## Environment registration + +DirectEnv and ManagerEnv use the same registration mechanism. Register the config and environment +under matching Env IDs, then publish a callback so installed applications can discover them. + +### Register configs and environments - **The config is registered before the environment class**: `@registry.env("name")` requires a config with the same name registered via `@registry.envcfg("name")`. @@ -192,3 +197,41 @@ simulation parameters are covered in [Setting Up the Physics Scene](scene.md). to the `"np"` data backend); never specify it manually. - The first docstring line and the `zh_CN:` line of the config serve as the environment's English and Chinese descriptions shown in the environment overview. + +### Publish an environment package + +Declare one provider callback for your environment package in `pyproject.toml`: + +```toml +[project.entry-points."motrix_env_core.envs"] +my-project = "my_project.envs:register" +``` + +Define the zero-argument callback in `my_project/envs/__init__.py`: + +```python +def register() -> None: + # Import configs before the environment modules that use their registrations. + from my_project.envs import my_robot_cfg # noqa: F401 + from my_project.envs import my_robot_env # noqa: F401 +``` + +Keep `@registry.envcfg("my-robot")` and `@registry.env("my-robot")` in those modules. +A provider callback can register multiple environments; Env IDs remain in the decorators rather +than being repeated as entry-point mappings. + +After installing the package, create the environment by name without a registration import: + +```python +from motrix_env_core import registry + +env = registry.make("my-robot", num_envs=1) +``` + +The first environment query loads all installed providers, once per process. Provider loading and +registration errors are reported to the caller; a provider with missing dependencies can prevent +discovery even when a different Env ID was requested. Install each provider's required dependencies. + +Environment packages own environment registration, not training presets. The consuming project +supplies its Task YAML files through its configuration directory or Hydra search path; see +[Task Configuration and CLI Overrides](../training/task_config.md). diff --git a/docs/source/en/user_guide/tutorial/building_envs/index.md b/docs/source/en/user_guide/tutorial/building_envs/index.md index 19638c73..7b2e8336 100644 --- a/docs/source/en/user_guide/tutorial/building_envs/index.md +++ b/docs/source/en/user_guide/tutorial/building_envs/index.md @@ -12,7 +12,8 @@ An environment consists of two parts, each registered under the environment name `@registry.env("name")`. Once registered, `scripts/view.py`, the trainers, and the replay flow all create the -environment by name through the same registry. +environment by name through the same registry. Installed environment packages are discovered automatically; +see [Environment registration](direct_env.md#environment-registration) to publish a package for either workflow. ## Lifecycle overview diff --git a/docs/source/en/user_guide/tutorial/training/index.md b/docs/source/en/user_guide/tutorial/training/index.md index 661cf45b..a7840bce 100644 --- a/docs/source/en/user_guide/tutorial/training/index.md +++ b/docs/source/en/user_guide/tutorial/training/index.md @@ -5,7 +5,7 @@ Once an environment is registered, training an RL task takes three steps: ```text Create a Task config pick the environment, RL framework, algorithm, hyperparameters ↓ -python scripts/train.py task=... Hydra composes the config and starts training +mxtrain task=... Hydra composes the config and starts training ↓ runs/{env}/... stores metadata, logs, and checkpoints ``` diff --git a/docs/source/en/user_guide/tutorial/training/runs_and_checkpoints.md b/docs/source/en/user_guide/tutorial/training/runs_and_checkpoints.md index 27d35285..a966abd4 100644 --- a/docs/source/en/user_guide/tutorial/training/runs_and_checkpoints.md +++ b/docs/source/en/user_guide/tutorial/training/runs_and_checkpoints.md @@ -93,7 +93,7 @@ The actual checkpoint files live under the `checkpoints/` subdirectory, and `man ``` - Paths in `manifest.json` are relative to the `checkpoints/` directory. -- **`best_policy`** (`kind: policy`): used for playback / export / evaluation; this is what `play.py` loads by default. +- **`best_policy`** (`kind: policy`): used for playback / export / evaluation; this is what `mxplay` loads by default. - **`latest_training_state`** (`kind: training_state`): used for resume, and should contain the full state needed to continue training, such as the optimizer, observation normalizer, replay buffer, and `global_step`. - The same physical file can be **registered as both artifacts**. For example, FastSAC's `latest.pt` (a complete state dict) can be used for both playback and resume, so `best_policy` and `latest_training_state` both point to it. @@ -106,16 +106,16 @@ The actual checkpoint files live under the `checkpoints/` subdirectory, and `man ```bash # Auto-discover and play the best policy of the latest run - python scripts/play.py env=g1-walk-flat + mxplay env=g1-walk-flat # Specify a checkpoint (must be able to locate metadata.json above it) - python scripts/play.py env=g1-walk-flat policy=/path/to/run/checkpoints/latest.pt + mxplay env=g1-walk-flat policy=/path/to/run/checkpoints/latest.pt ``` - **Resume**: set `resume=` to a run directory or checkpoint path, and the framework resolves `latest_training_state` from it to continue training. ```bash - python scripts/train.py task=g1-walk-flat/motrix.fastsac \ + mxtrain task=g1-walk-flat/motrix.fastsac \ resume=/path/to/run ``` diff --git a/docs/source/en/user_guide/tutorial/training/task_config.md b/docs/source/en/user_guide/tutorial/training/task_config.md index 63242a57..d9206dbb 100644 --- a/docs/source/en/user_guide/tutorial/training/task_config.md +++ b/docs/source/en/user_guide/tutorial/training/task_config.md @@ -53,7 +53,7 @@ algo: This configuration: -1. Loads the complete SKRL PPO base from `configs/algo_base/skrl.ppo.yaml`. +1. Loads the complete SKRL PPO base from `motrix_rl/src/motrix_rl/configs/algo_base/skrl.ppo.yaml`. 2. Selects the environment registered as `my-robot`. 3. Sets the number of parallel environments used for training and play. 4. Overrides the total training timesteps with `100000`. @@ -62,22 +62,28 @@ A Task only needs to contain values that differ from the algorithm base. It does ### Inspect and run the Task +`mxtrain` uses `./configs` in the current working directory when it exists; it does not search parent directories. Use `--config-dir /path/to/configs` to select another configuration directory. A Task must be selected explicitly; there is no default Cartpole Task. + +Install the environment package before running the Task; registry queries discover installed providers automatically. +See [Environment registration](../building_envs/direct_env.md#environment-registration) to package a custom environment; +Task YAML discovery remains separate. + Print the fully composed Hydra configuration before starting training: ```bash -python scripts/train.py --cfg job --resolve task=my-robot/skrl.ppo +mxtrain --cfg job --resolve task=my-robot/skrl.ppo ``` This command does not start training. After verifying `task.env`, `num_envs`, and `algo.trainer.timesteps`, start the run: ```bash -python scripts/train.py task=my-robot/skrl.ppo +mxtrain task=my-robot/skrl.ppo ``` List all Tasks available in the repository with: ```bash -python scripts/train.py --help +mxtrain --help ``` ## Adjust Task runtime settings @@ -146,7 +152,7 @@ algo: timesteps: 20000 ``` -See [`configs/algo_base/skrl.ppo.yaml`](../../../../../configs/algo_base/skrl.ppo.yaml) for the complete field set and per-field comments. +See [`motrix_rl/src/motrix_rl/configs/algo_base/skrl.ppo.yaml`](https://github.com/Motphys/MotrixLab/blob/main/motrix_rl/src/motrix_rl/configs/algo_base/skrl.ppo.yaml) for the complete field set and per-field comments. ### RSLRL PPO @@ -167,7 +173,7 @@ algo: entropy_coef: 0.005 ``` -See [`configs/algo_base/rslrl.ppo.yaml`](../../../../../configs/algo_base/rslrl.ppo.yaml) for the complete field set and per-field comments. +See [`motrix_rl/src/motrix_rl/configs/algo_base/rslrl.ppo.yaml`](https://github.com/Motphys/MotrixLab/blob/main/motrix_rl/src/motrix_rl/configs/algo_base/rslrl.ppo.yaml) for the complete field set and per-field comments. ### Motrix FastSAC @@ -182,7 +188,7 @@ FastSAC uses one algorithm identity for both execution topologies. Its main fiel - `weight_publish_interval` and `weight_poll_interval`: policy synchronization cadence. - `max_ingest_per_iter` and `idle_sleep_s`: learner ingestion and idle backoff. -See [`configs/algo_base/motrix.fastsac.yaml`](../../../../../configs/algo_base/motrix.fastsac.yaml) for the complete field set and per-field comments. +See [`motrix_rl/src/motrix_rl/configs/algo_base/motrix.fastsac.yaml`](https://github.com/Motphys/MotrixLab/blob/main/motrix_rl/src/motrix_rl/configs/algo_base/motrix.fastsac.yaml) for the complete field set and per-field comments. ## Override parameters from the CLI @@ -191,7 +197,7 @@ The MotrixLab CLI uses Hydra's `key=value` syntax. CLI values apply only to the ### Override runtime settings ```bash -python scripts/train.py \ +mxtrain \ task=my-robot/skrl.ppo \ num_envs=64 \ seed=7 \ @@ -202,7 +208,7 @@ python scripts/train.py \ Enable training rendering and play the policy afterward: ```bash -python scripts/train.py task=my-robot/skrl.ppo render=true play=true +mxtrain task=my-robot/skrl.ppo render=true play=true ``` ### Override algorithm settings @@ -210,7 +216,7 @@ python scripts/train.py task=my-robot/skrl.ppo render=true play=true SKRL PPO: ```bash -python scripts/train.py \ +mxtrain \ task=my-robot/skrl.ppo \ algo.agent.learning_rate=5e-4 \ algo.agent.learning_epochs=8 \ @@ -220,7 +226,7 @@ python scripts/train.py \ RSLRL PPO: ```bash -python scripts/train.py \ +mxtrain \ task=cartpole/rslrl.ppo \ algo.algorithm.learning_rate=5e-4 \ algo.algorithm.entropy_coef=0.005 \ @@ -230,7 +236,7 @@ python scripts/train.py \ FastSAC: ```bash -python scripts/train.py \ +mxtrain \ task=g1-walk-flat/motrix.fastsac \ algo.asynchronous=true \ algo.agent.actor_learning_rate=1e-4 \ @@ -244,20 +250,20 @@ python scripts/train.py \ Use lowercase `true` and `false` for booleans: ```bash -python scripts/train.py task=my-robot/skrl.ppo render=true +mxtrain task=my-robot/skrl.ppo render=true ``` Use `null` to clear nullable fields: ```bash -python scripts/train.py task=my-robot/skrl.ppo seed=null -python scripts/train.py task=my-robot/skrl.ppo algo.agent.learning_rate_scheduler=null +mxtrain task=my-robot/skrl.ppo seed=null +mxtrain task=my-robot/skrl.ppo algo.agent.learning_rate_scheduler=null ``` Quote lists so the shell does not interpret brackets: ```bash -python scripts/train.py \ +mxtrain \ task=my-robot/skrl.ppo \ 'algo.models.policy.hiddens=[128,64]' \ 'algo.models.value.hiddens=[128,64]' @@ -274,7 +280,7 @@ Training uses structured schemas. Existing fields do not need a `+` prefix. Add Combine CLI overrides with `--cfg job --resolve` to inspect the exact values that would be used: ```bash -python scripts/train.py \ +mxtrain \ --cfg job \ --resolve \ task=my-robot/skrl.ppo \ @@ -316,13 +322,13 @@ algo: Train with the JAX delta: ```bash -python scripts/train.py task=go2-walk-flat/skrl.ppo.jax +mxtrain task=go2-walk-flat/skrl.ppo.jax ``` Overriding the backend directly also selects the JAX trainer: ```bash -python scripts/train.py task=go2-walk-flat/skrl.ppo task.train_backend=jax +mxtrain task=go2-walk-flat/skrl.ppo task.train_backend=jax ``` This form does not load backend-specific values from `skrl.ppo.jax.yaml`. Select the `.jax` or `.torch` Task directly when a backend delta exists. @@ -334,12 +340,16 @@ After creating and tuning a basic Task, the following details explain how its fi ### Configuration directories ```text -configs/ +motrix_rl/src/motrix_rl/configs/ ├── train.yaml -├── algo_base/ -│ ├── skrl.ppo.yaml -│ ├── rslrl.ppo.yaml -│ └── motrix.fastsac.yaml +├── play.yaml +└── algo_base/ + ├── skrl.ppo.yaml + ├── rslrl.ppo.yaml + └── motrix.fastsac.yaml + +configs/ +├── view.yaml └── task/ └── / ├── ..yaml @@ -350,8 +360,8 @@ Each layer has a separate responsibility: | Layer | Location | Purpose | | ------------------- | -------------------------------------------------- | --------------------------------------------------------------------- | -| Entry configuration | `configs/train.yaml` | Defines shared runtime values and the default Task | -| Algorithm base | `configs/algo_base/..yaml` | Defines the complete field set and base values for an algorithm | +| Entry configuration | `motrix_rl/src/motrix_rl/configs/train.yaml` | Defines shared runtime values; `task: null` is a placeholder, and training requires an explicit Task | +| Algorithm base | `motrix_rl/src/motrix_rl/configs/algo_base/..yaml` | Defines the complete field set and base values for an algorithm | | Shared Task | `configs/task//..yaml` | Selects the environment and algorithm and stores task-specific tuning | | Backend delta | `configs/task//...yaml` | Stores backend-specific differences | | CLI override | `key=value` | Overrides the final value for one run | @@ -410,18 +420,18 @@ Hydra validates field names and types against the structured schema registered b ## Play and View overrides -`scripts/play.py` and `scripts/view.py` use the same `key=value` syntax: +`mxplay` restores the saved run configuration without requiring the training Task directory. It and `scripts/view.py` use the same `key=value` syntax: ```bash python scripts/view.py env=cartpole num_envs=4 -python scripts/play.py env=cartpole num_envs=1 -python scripts/play.py policy=/path/to/checkpoint.pt num_envs=1 +mxplay env=cartpole num_envs=1 +mxplay policy=/path/to/checkpoint.pt num_envs=1 ``` Play loads the `task_config.yaml` stored with a training run. To temporarily override algorithm settings, use `rl` as the algorithm root. Since `rl` starts as an empty mapping, add the path with `+`: ```bash -python scripts/play.py \ +mxplay \ env=cartpole \ '+rl.agent.learning_rate=1e-4' ``` diff --git a/docs/source/en/user_guide/tutorial/training/training_and_result.md b/docs/source/en/user_guide/tutorial/training/training_and_result.md index 39c52114..a5ee204f 100644 --- a/docs/source/en/user_guide/tutorial/training/training_and_result.md +++ b/docs/source/en/user_guide/tutorial/training/training_and_result.md @@ -12,16 +12,16 @@ A Task recipe selects an RL provider, but it does not install that provider. If ### Selecting a Task -The training entry point uses Hydra's `task=/.[.]` option. A Task selects the environment, RL provider, runtime settings, and algorithm hyperparameters as one reproducible recipe: +The training entry point requires Hydra's `task=/.[.]` option: the packaged configuration uses a `task: null` placeholder so `--help` works without a Task, but actual training requires an explicit Task. A Task selects the environment, RL provider, runtime settings, and algorithm hyperparameters as one reproducible recipe: ```bash -# Train the default Cartpole SKRL PPO Task -python scripts/train.py task=cartpole/skrl.ppo +# Select the Cartpole SKRL PPO Task explicitly +mxtrain task=cartpole/skrl.ppo # Select another framework or algorithm -python scripts/train.py task=cartpole/rslrl.ppo -python scripts/train.py task=g1-walk-flat/motrix.fastsac -python scripts/train.py task=g1-walk-flat/motrix.fastsac algo.asynchronous=false +mxtrain task=cartpole/rslrl.ppo +mxtrain task=g1-walk-flat/motrix.fastsac +mxtrain task=g1-walk-flat/motrix.fastsac algo.asynchronous=false ``` Built-in RL methods and their training backends: @@ -32,59 +32,59 @@ Built-in RL methods and their training backends: | `rslrl.ppo` | `torch` | RSLRL PPO | | `motrix.fastsac` | `torch` | FastSAC; `algo.asynchronous` selects the execution topology | -Run `python scripts/train.py --help` to list the Tasks available in the current checkout. See [Task Configuration and CLI Overrides](task_config.md) for the Task file layout and override rules. +Run these commands from the repository root: the CLI discovers `./configs` in the current working directory when it exists, without searching parent directories. Run `mxtrain --help` to list the Tasks available in the current checkout. See [Task Configuration and CLI Overrides](task_config.md) for the Task file layout and override rules. ### Selecting Training and Simulation Backends ```bash # Override the training backend (auto-selected when task.train_backend is null) -python scripts/train.py task=cartpole/skrl.ppo task.train_backend=jax -python scripts/train.py task=cartpole/skrl.ppo task.train_backend=torch +mxtrain task=cartpole/skrl.ppo task.train_backend=jax +mxtrain task=cartpole/skrl.ppo task.train_backend=torch # Specify the simulator injected into manager-based environments -python scripts/train.py task=g1-wbt-dance sim=motrixsim +mxtrain task=g1-wbt-dance/motrix.fastsac sim=motrixsim ``` ### Training Scale and Random Seed ```bash # Number of parallel environments -python scripts/train.py task=cartpole/skrl.ppo num_envs=1024 +mxtrain task=cartpole/skrl.ppo num_envs=1024 # Fixed random seed (reproducible) / choose a random seed at runtime -python scripts/train.py task=cartpole/skrl.ppo seed=42 -python scripts/train.py task=cartpole/skrl.ppo seed=null +mxtrain task=cartpole/skrl.ppo seed=42 +mxtrain task=cartpole/skrl.ppo seed=null ``` ```{note} -Hydra can override typed algorithm fields directly. For example: `algo.agent.learning_rate=5e-4`. Field paths depend on the selected Task; the complete defaults are in `configs/algo_base/`. +Hydra can override typed algorithm fields directly. For example: `algo.agent.learning_rate=5e-4`. Field paths depend on the selected Task; the complete defaults are in `motrix_rl/src/motrix_rl/configs/algo_base/`. ``` ### Auto-play and Resume ```bash # After training finishes successfully, play the best policy of this run -python scripts/train.py task=g1-walk-flat/motrix.fastsac play=true +mxtrain task=g1-walk-flat/motrix.fastsac play=true # Resume from a run directory or a checkpoint -python scripts/train.py task=g1-walk-flat/motrix.fastsac \ +mxtrain task=g1-walk-flat/motrix.fastsac \ resume=/path/to/run # Enable rendering to monitor the training process -python scripts/train.py task=cartpole/skrl.ppo render=true +mxtrain task=cartpole/skrl.ppo render=true ``` -Training and playback are separate operations: `play=true` replays the best policy after a successful training run, while `scripts/play.py` reopens a saved run later. Export a trained policy artifact with: +Training and playback are separate operations: `play=true` replays the best policy after a successful training run, while `mxplay` reopens a saved run later. Export a trained policy artifact with: ```bash -python scripts/export_onnx.py run_dir=/path/to/run output=/tmp/policy.onnx +mxexport onnx run_dir=/path/to/run output=/tmp/policy.onnx ``` ### Common Hydra Overrides | Override | Description | Default source | | -------------------- | ------------------------------------------------------------------------- | --------------------- | -| `task=...` | Environment, framework, algorithm, backend delta | `cartpole/skrl.ppo` | +| `task=...` | Environment, framework, algorithm, backend delta | Required for training | | `task.train_backend` | Training backend (`jax` / `torch`) | Selected Task | | `sim` | Simulator for manager environments; leave unset for np-style environments | `null` / auto-select | | `num_envs` | Number of parallel training environments | Selected Task | @@ -115,14 +115,14 @@ Playback (play) does not require re-specifying the RL method — the correct `rl ```bash # Auto-discover and play the best policy of the latest run (recommended) -python scripts/play.py env=cartpole +mxplay env=cartpole # Specify a checkpoint (must be able to locate metadata.json above it) -python scripts/play.py env=g1-walk-flat \ +mxplay env=g1-walk-flat \ policy=/path/to/run/checkpoints/latest.pt # Specify the number of playback environments -python scripts/play.py env=cartpole num_envs=100 +mxplay env=cartpole num_envs=100 ``` ```{note} diff --git a/docs/source/zh_CN/user_guide/envs/ball_balance.md b/docs/source/zh_CN/user_guide/envs/ball_balance.md index 9a67f564..afad97fd 100644 --- a/docs/source/zh_CN/user_guide/envs/ball_balance.md +++ b/docs/source/zh_CN/user_guide/envs/ball_balance.md @@ -25,13 +25,13 @@ python scripts/view.py env=microduck-ball-balance 使用内置的 Motrix FastSAC 配置训练(该任务目前只提供这一个训练配置,`num_envs=2048`,训练 20000 次迭代): ```bash -python scripts/train.py task=microduck-ball-balance/motrix.fastsac +mxtrain task=microduck-ball-balance/motrix.fastsac ``` 加载训练结果评估: ```bash -python scripts/play.py env=microduck-ball-balance +mxplay env=microduck-ball-balance ``` ## 动作空间 diff --git a/docs/source/zh_CN/user_guide/envs/basic/acrobot.md b/docs/source/zh_CN/user_guide/envs/basic/acrobot.md index efb592c5..f9a40b70 100644 --- a/docs/source/zh_CN/user_guide/envs/basic/acrobot.md +++ b/docs/source/zh_CN/user_guide/envs/basic/acrobot.md @@ -70,13 +70,13 @@ python scripts/view.py env=acrobot ```bash # 使用默认参数训练 -python scripts/train.py task=acrobot/skrl.ppo +mxtrain task=acrobot/skrl.ppo # 自定义并行环境数 -python scripts/train.py task=acrobot/skrl.ppo num_envs=1024 +mxtrain task=acrobot/skrl.ppo num_envs=1024 # 开启训练时渲染 -python scripts/train.py task=acrobot/skrl.ppo render=true +mxtrain task=acrobot/skrl.ppo render=true ``` ### 3. 查看训练进度 @@ -89,10 +89,10 @@ tensorboard --logdir runs/acrobot ```bash # 自动发现最佳策略(推荐) -python scripts/play.py env=acrobot +mxplay env=acrobot # 手动指定带 metadata 的 run 中的 checkpoint -python scripts/play.py env=acrobot policy=/path/to/run/checkpoints/policy-file +mxplay env=acrobot policy=/path/to/run/checkpoints/policy-file ``` > **提示**:策略会从 `runs/acrobot/` 中自动选择。使用 `policy=...` 时,checkpoint 所属 run 必须包含 `metadata.json`。 diff --git a/docs/source/zh_CN/user_guide/envs/basic/cartpole.md b/docs/source/zh_CN/user_guide/envs/basic/cartpole.md index 208cf286..e8e7ca90 100644 --- a/docs/source/zh_CN/user_guide/envs/basic/cartpole.md +++ b/docs/source/zh_CN/user_guide/envs/basic/cartpole.md @@ -94,7 +94,7 @@ python scripts/view.py env=cartpole ### 2. 开始训练 ```bash -python scripts/train.py task=cartpole/skrl.ppo +mxtrain task=cartpole/skrl.ppo ``` ### 3. 查看训练进度 @@ -106,7 +106,7 @@ tensorboard --logdir runs/cartpole ### 4. 测试训练结果 ```bash -python scripts/play.py env=cartpole +mxplay env=cartpole ``` --- diff --git a/docs/source/zh_CN/user_guide/envs/basic/pendulum.md b/docs/source/zh_CN/user_guide/envs/basic/pendulum.md index 758f4ce6..5cbf1cfa 100644 --- a/docs/source/zh_CN/user_guide/envs/basic/pendulum.md +++ b/docs/source/zh_CN/user_guide/envs/basic/pendulum.md @@ -71,13 +71,13 @@ python scripts/view.py env=pendulum ```bash # 默认参数训练 -python scripts/train.py task=pendulum/skrl.ppo +mxtrain task=pendulum/skrl.ppo # 自定义并行环境数 -python scripts/train.py task=pendulum/skrl.ppo num_envs=1024 +mxtrain task=pendulum/skrl.ppo num_envs=1024 # 开启训练时渲染 -python scripts/train.py task=pendulum/skrl.ppo render=true +mxtrain task=pendulum/skrl.ppo render=true ``` ### 3. 查看训练进度 @@ -90,10 +90,10 @@ tensorboard --logdir runs/pendulum ```bash # 自动寻找最新/最优策略(推荐) -python scripts/play.py env=pendulum +mxplay env=pendulum # 手动指定带 metadata 的 run 中的 checkpoint -python scripts/play.py env=pendulum policy=/path/to/run/checkpoints/policy-file +mxplay env=pendulum policy=/path/to/run/checkpoints/policy-file ``` > **提示**:策略默认在 `runs/pendulum/` 下自动发现。使用 `policy=...` 时,checkpoint 所属 run 必须包含 `metadata.json`。 diff --git a/docs/source/zh_CN/user_guide/envs/basic/stewart.md b/docs/source/zh_CN/user_guide/envs/basic/stewart.md index 5997fa1a..d924ddb1 100644 --- a/docs/source/zh_CN/user_guide/envs/basic/stewart.md +++ b/docs/source/zh_CN/user_guide/envs/basic/stewart.md @@ -163,8 +163,8 @@ python scripts/view.py env=stewart-disturb-xy ### 2. 开始训练 ```bash -python scripts/train.py task=stewart-static/skrl.ppo -python scripts/train.py task=stewart-disturb-xy/skrl.ppo +mxtrain task=stewart-static/skrl.ppo +mxtrain task=stewart-disturb-xy/skrl.ppo ``` ### 训练说明 @@ -183,8 +183,8 @@ tensorboard --logdir runs/stewart-disturb-xy ### 4. 测试训练结果 ```bash -python scripts/play.py env=stewart-static -python scripts/play.py env=stewart-disturb-xy +mxplay env=stewart-static +mxplay env=stewart-disturb-xy ``` --- diff --git a/docs/source/zh_CN/user_guide/envs/dm_control/bring_ball.md b/docs/source/zh_CN/user_guide/envs/dm_control/bring_ball.md index b29d2bc8..0b280696 100644 --- a/docs/source/zh_CN/user_guide/envs/dm_control/bring_ball.md +++ b/docs/source/zh_CN/user_guide/envs/dm_control/bring_ball.md @@ -145,7 +145,7 @@ python scripts/view.py env=dm-manipulator-bring-ball 建议显式指定训练后端(JAX / PyTorch 二选一): ```bash -python scripts/train.py task=dm-manipulator-bring-ball/skrl.ppo.torch +mxtrain task=dm-manipulator-bring-ball/skrl.ppo.torch ``` ### 3. 查看训练进度 @@ -157,7 +157,7 @@ tensorboard --logdir runs/dm-manipulator-bring-ball ### 4. 测试训练结果 ```bash -python scripts/play.py env=dm-manipulator-bring-ball +mxplay env=dm-manipulator-bring-ball ``` --- diff --git a/docs/source/zh_CN/user_guide/envs/dm_control/dm_cheetah.md b/docs/source/zh_CN/user_guide/envs/dm_control/dm_cheetah.md index 95892846..e9dcec31 100644 --- a/docs/source/zh_CN/user_guide/envs/dm_control/dm_cheetah.md +++ b/docs/source/zh_CN/user_guide/envs/dm_control/dm_cheetah.md @@ -108,7 +108,7 @@ python scripts/view.py env=dm-cheetah ### 2. 开始训练 ```bash -python scripts/train.py task=dm-cheetah/skrl.ppo +mxtrain task=dm-cheetah/skrl.ppo ``` ### 3. 查看训练进度 @@ -120,7 +120,7 @@ tensorboard --logdir runs/dm-cheetah ### 4. 测试训练结果 ```bash -python scripts/play.py env=dm-cheetah +mxplay env=dm-cheetah ``` --- diff --git a/docs/source/zh_CN/user_guide/envs/dm_control/dm_finger.md b/docs/source/zh_CN/user_guide/envs/dm_control/dm_finger.md index d2242d78..5ff9bfca 100644 --- a/docs/source/zh_CN/user_guide/envs/dm_control/dm_finger.md +++ b/docs/source/zh_CN/user_guide/envs/dm_control/dm_finger.md @@ -159,15 +159,15 @@ python scripts/view.py env=dm-finger-turn-hard 建议显式指定训练后端(JAX / PyTorch 二选一): ```bash -python scripts/train.py task=dm-finger-spin/skrl.ppo task.train_backend=torch +mxtrain task=dm-finger-spin/skrl.ppo task.train_backend=torch ``` ```bash -python scripts/train.py task=dm-finger-turn-easy/skrl.ppo task.train_backend=torch +mxtrain task=dm-finger-turn-easy/skrl.ppo task.train_backend=torch ``` ```bash -python scripts/train.py task=dm-finger-turn-hard/skrl.ppo task.train_backend=torch +mxtrain task=dm-finger-turn-hard/skrl.ppo task.train_backend=torch ``` ### 3. 查看训练进度 @@ -178,10 +178,10 @@ tensorboard --logdir runs/dm-finger-spin ### 4. 测试训练结果 -`scripts/play.py` 默认会在 `runs/{env-name}/` 下查找最新的 metadata-backed run,并加载其 `best_policy` artifact;也可以用 `policy=...` 显式指定 checkpoint: +`mxplay` 默认会在 `runs/{env-name}/` 下查找最新的 metadata-backed run,并加载其 `best_policy` artifact;也可以用 `policy=...` 显式指定 checkpoint: ```bash -python scripts/play.py env=dm-finger-turn-hard +mxplay env=dm-finger-turn-hard ``` --- diff --git a/docs/source/zh_CN/user_guide/envs/dm_control/dm_hopper.md b/docs/source/zh_CN/user_guide/envs/dm_control/dm_hopper.md index c91a80b9..a060f121 100644 --- a/docs/source/zh_CN/user_guide/envs/dm_control/dm_hopper.md +++ b/docs/source/zh_CN/user_guide/envs/dm_control/dm_hopper.md @@ -116,8 +116,8 @@ python scripts/view.py env=dm-hopper-hop ### 2. 开始训练 ```bash -python scripts/train.py task=dm-hopper-stand/skrl.ppo -python scripts/train.py task=dm-hopper-hop/skrl.ppo +mxtrain task=dm-hopper-stand/skrl.ppo +mxtrain task=dm-hopper-hop/skrl.ppo ``` ### 3. 查看训练进度 @@ -129,8 +129,8 @@ tensorboard --logdir runs/dm-hopper-hop ### 4. 测试训练结果 ```bash -python scripts/play.py env=dm-hopper-stand -python scripts/play.py env=dm-hopper-hop +mxplay env=dm-hopper-stand +mxplay env=dm-hopper-hop ``` --- diff --git a/docs/source/zh_CN/user_guide/envs/dm_control/dm_humanoid.md b/docs/source/zh_CN/user_guide/envs/dm_control/dm_humanoid.md index 6c271dbe..143a4204 100644 --- a/docs/source/zh_CN/user_guide/envs/dm_control/dm_humanoid.md +++ b/docs/source/zh_CN/user_guide/envs/dm_control/dm_humanoid.md @@ -193,9 +193,9 @@ python scripts/view.py env=dm-humanoid-run ### 2. 开始训练 ```bash -python scripts/train.py task=dm-humanoid-stand/skrl.ppo -python scripts/train.py task=dm-humanoid-walk/skrl.ppo -python scripts/train.py task=dm-humanoid-run/skrl.ppo +mxtrain task=dm-humanoid-stand/skrl.ppo +mxtrain task=dm-humanoid-walk/skrl.ppo +mxtrain task=dm-humanoid-run/skrl.ppo ``` ### 3. 查看训练进度 @@ -207,9 +207,9 @@ tensorboard --logdir runs/dm-humanoid-walk ### 4. 测试训练结果 ```bash -python scripts/play.py env=dm-humanoid-stand -python scripts/play.py env=dm-humanoid-walk -python scripts/play.py env=dm-humanoid-run +mxplay env=dm-humanoid-stand +mxplay env=dm-humanoid-walk +mxplay env=dm-humanoid-run ``` --- diff --git a/docs/source/zh_CN/user_guide/envs/dm_control/dm_lqr.md b/docs/source/zh_CN/user_guide/envs/dm_control/dm_lqr.md index 9b9c6865..7760b974 100644 --- a/docs/source/zh_CN/user_guide/envs/dm_control/dm_lqr.md +++ b/docs/source/zh_CN/user_guide/envs/dm_control/dm_lqr.md @@ -164,8 +164,8 @@ python scripts/view.py env=dm-lqr-6-2 ### 2. 开始训练 ```bash -python scripts/train.py task=dm-lqr-2-1/skrl.ppo -python scripts/train.py task=dm-lqr-6-2/skrl.ppo +mxtrain task=dm-lqr-2-1/skrl.ppo +mxtrain task=dm-lqr-6-2/skrl.ppo ``` ### 3. 查看训练曲线 @@ -178,8 +178,8 @@ tensorboard --logdir runs/dm-lqr-6-2 ### 4. 测试训练结果 ```bash -python scripts/play.py env=dm-lqr-2-1 -python scripts/play.py env=dm-lqr-6-2 +mxplay env=dm-lqr-2-1 +mxplay env=dm-lqr-6-2 ``` --- diff --git a/docs/source/zh_CN/user_guide/envs/dm_control/dm_point_mass.md b/docs/source/zh_CN/user_guide/envs/dm_control/dm_point_mass.md index dad63f73..7d9249c7 100644 --- a/docs/source/zh_CN/user_guide/envs/dm_control/dm_point_mass.md +++ b/docs/source/zh_CN/user_guide/envs/dm_control/dm_point_mass.md @@ -121,7 +121,7 @@ python scripts/view.py env=point_mass ### 2. 开始训练 ```bash -python scripts/train.py task=point_mass/skrl.ppo +mxtrain task=point_mass/skrl.ppo ``` ### 3. 查看训练进度 @@ -133,7 +133,7 @@ tensorboard --logdir runs/point_mass ### 4. 测试训练结果 ```bash -python scripts/play.py env=point_mass +mxplay env=point_mass ``` --- diff --git a/docs/source/zh_CN/user_guide/envs/dm_control/dm_quadruped.md b/docs/source/zh_CN/user_guide/envs/dm_control/dm_quadruped.md index ac00e0db..ebc26bcc 100644 --- a/docs/source/zh_CN/user_guide/envs/dm_control/dm_quadruped.md +++ b/docs/source/zh_CN/user_guide/envs/dm_control/dm_quadruped.md @@ -237,10 +237,10 @@ python scripts/view.py env=dm-quadruped-fetch ### 2. 开始训练 ```bash -python scripts/train.py task=dm-quadruped-walk/skrl.ppo -python scripts/train.py task=dm-quadruped-run/skrl.ppo -python scripts/train.py task=dm-quadruped-escape/skrl.ppo -python scripts/train.py task=dm-quadruped-fetch/skrl.ppo +mxtrain task=dm-quadruped-walk/skrl.ppo +mxtrain task=dm-quadruped-run/skrl.ppo +mxtrain task=dm-quadruped-escape/skrl.ppo +mxtrain task=dm-quadruped-fetch/skrl.ppo ``` ### 3. 查看训练进度 @@ -255,10 +255,10 @@ tensorboard --logdir runs/dm-quadruped-fetch ### 4. 测试训练结果 ```bash -python scripts/play.py env=dm-quadruped-walk -python scripts/play.py env=dm-quadruped-run -python scripts/play.py env=dm-quadruped-escape -python scripts/play.py env=dm-quadruped-fetch +mxplay env=dm-quadruped-walk +mxplay env=dm-quadruped-run +mxplay env=dm-quadruped-escape +mxplay env=dm-quadruped-fetch ``` --- diff --git a/docs/source/zh_CN/user_guide/envs/dm_control/dm_reacher.md b/docs/source/zh_CN/user_guide/envs/dm_control/dm_reacher.md index b2224852..ade1cc73 100644 --- a/docs/source/zh_CN/user_guide/envs/dm_control/dm_reacher.md +++ b/docs/source/zh_CN/user_guide/envs/dm_control/dm_reacher.md @@ -99,7 +99,7 @@ python scripts/view.py env=dm-reacher ### 2. 开始训练 ```bash -python scripts/train.py task=dm-reacher/skrl.ppo +mxtrain task=dm-reacher/skrl.ppo ``` ### 3. 查看训练进度 @@ -111,7 +111,7 @@ tensorboard --logdir runs/dm-reacher ### 4. 测试训练结果 ```bash -python scripts/play.py env=dm-reacher +mxplay env=dm-reacher ``` --- diff --git a/docs/source/zh_CN/user_guide/envs/dm_control/dm_walker.md b/docs/source/zh_CN/user_guide/envs/dm_control/dm_walker.md index c96bcd4d..86266d98 100644 --- a/docs/source/zh_CN/user_guide/envs/dm_control/dm_walker.md +++ b/docs/source/zh_CN/user_guide/envs/dm_control/dm_walker.md @@ -102,9 +102,9 @@ python scripts/view.py env=dm-runner ### 2. 开始训练 ```bash -python scripts/train.py task=dm-stander/skrl.ppo -python scripts/train.py task=dm-walker/skrl.ppo -python scripts/train.py task=dm-runner/skrl.ppo +mxtrain task=dm-stander/skrl.ppo +mxtrain task=dm-walker/skrl.ppo +mxtrain task=dm-runner/skrl.ppo ``` ### 3. 查看训练进度 @@ -116,9 +116,9 @@ tensorboard --logdir runs/dm-walker ### 4. 测试训练结果 ```bash -python scripts/play.py env=dm-stander -python scripts/play.py env=dm-walker -python scripts/play.py env=dm-runner +mxplay env=dm-stander +mxplay env=dm-walker +mxplay env=dm-runner ``` --- diff --git a/docs/source/zh_CN/user_guide/envs/humanoid_velocity_tracking.md b/docs/source/zh_CN/user_guide/envs/humanoid_velocity_tracking.md index 9232cc39..91fd4649 100644 --- a/docs/source/zh_CN/user_guide/envs/humanoid_velocity_tracking.md +++ b/docs/source/zh_CN/user_guide/envs/humanoid_velocity_tracking.md @@ -91,14 +91,14 @@ MotrixLab 已为下列机器人提供平地和程序化起伏高度场配置。 ```bash python scripts/view.py env=ENV_ID num_envs=1 -python scripts/train.py task=ENV_ID/TRAINING_CONFIG -python scripts/play.py env=ENV_ID num_envs=16 +mxtrain task=ENV_ID/TRAINING_CONFIG +mxplay env=ENV_ID num_envs=16 ``` 例如,使用 FastSAC 异步训练 K1 起伏地形任务: ```bash -python scripts/train.py task=k1-walk-rough/motrix.fastsac algo.asynchronous=true +mxtrain task=k1-walk-rough/motrix.fastsac algo.asynchronous=true ``` -`view.py` 使用随机动作,仅用于检查场景和模型;训练后的步态需要通过 `play.py` 回放策略查看。 +`view.py` 使用随机动作,仅用于检查场景和模型;训练后的步态需要通过 `mxplay` 回放策略查看。 diff --git a/docs/source/zh_CN/user_guide/envs/humanoid_velocity_tracking/adding_robot.md b/docs/source/zh_CN/user_guide/envs/humanoid_velocity_tracking/adding_robot.md index 4b989391..02e31ac7 100644 --- a/docs/source/zh_CN/user_guide/envs/humanoid_velocity_tracking/adding_robot.md +++ b/docs/source/zh_CN/user_guide/envs/humanoid_velocity_tracking/adding_robot.md @@ -125,9 +125,10 @@ registry.env("-walk-flat")(HumanoidVelocityTrackingEnv) registry.env("-walk-terrain")(HumanoidVelocityTrackingEnv) ``` -随后在 `motrix_envs/locomotion/humanoid/__init__.py` 中导入新的 `` 模块,确保 `import motrix_envs` 时配置和环境 -实现都会完成注册。`registry.env(...)` 的环境 ID 必须与对应 `@registry.envcfg(...)` 的配置 ID 完全一致。若只定义平地 -配置,则只注册平地环境 ID;起伏地形配置的派生方式见[“配置覆盖与调参”](config_tuning.md)。 +随后在 `motrix_envs/locomotion/humanoid/__init__.py` 中导入新的 `` 模块,让内置注册回调包含它。 +独立 package 的发布方式见[环境注册](../../tutorial/building_envs/direct_env.md#环境注册)。 +`registry.env(...)` 的环境 ID 必须与对应 `@registry.envcfg(...)` 的配置 ID 完全一致。若只定义平地配置,则只注册平地 +环境 ID;起伏地形配置的派生方式见[“配置覆盖与调参”](config_tuning.md)。 ## 5. 增加训练配置 @@ -158,8 +159,8 @@ algo: python scripts/view.py robot= python scripts/view.py env=-walk-flat python scripts/view.py env=-walk-terrain -python scripts/train.py task=-walk-flat/motrix.fastsac -python scripts/train.py task=-walk-flat/motrix.fastsac algo.asynchronous=false +mxtrain task=-walk-flat/motrix.fastsac +mxtrain task=-walk-flat/motrix.fastsac algo.asynchronous=false python -m pytest motrix_envs/tests/test_humanoid_walk.py -q python -m pytest motrix_rl/tests/test_task_configs.py -q ``` diff --git a/docs/source/zh_CN/user_guide/envs/manipulation/bounce_ball.md b/docs/source/zh_CN/user_guide/envs/manipulation/bounce_ball.md index fe1ff009..8af9a4aa 100644 --- a/docs/source/zh_CN/user_guide/envs/manipulation/bounce_ball.md +++ b/docs/source/zh_CN/user_guide/envs/manipulation/bounce_ball.md @@ -417,7 +417,7 @@ python scripts/view.py env=bounce_ball ### 2. 开始训练 ```bash -python scripts/train.py task=bounce_ball/skrl.ppo +mxtrain task=bounce_ball/skrl.ppo ``` ### 3. 查看训练进度 @@ -429,7 +429,7 @@ tensorboard --logdir runs/bounce_ball ### 4. 测试训练结果 ```bash -python scripts/play.py env=bounce_ball +mxplay env=bounce_ball ``` --- diff --git a/docs/source/zh_CN/user_guide/envs/manipulation/franka_lift_cube.md b/docs/source/zh_CN/user_guide/envs/manipulation/franka_lift_cube.md index c44e60f1..755edd4b 100644 --- a/docs/source/zh_CN/user_guide/envs/manipulation/franka_lift_cube.md +++ b/docs/source/zh_CN/user_guide/envs/manipulation/franka_lift_cube.md @@ -239,13 +239,13 @@ p = 1 / (1 + exp(-action)) ### 训练 ```bash -python scripts/train.py task=franka-lift-cube/skrl.ppo +mxtrain task=franka-lift-cube/skrl.ppo ``` ### 策略评估 ```bash -python scripts/play.py env=franka-lift-cube +mxplay env=franka-lift-cube ``` ### TensorBoard diff --git a/docs/source/zh_CN/user_guide/envs/manipulation/franka_open_cabinet.md b/docs/source/zh_CN/user_guide/envs/manipulation/franka_open_cabinet.md index 125cd222..6cca0742 100644 --- a/docs/source/zh_CN/user_guide/envs/manipulation/franka_open_cabinet.md +++ b/docs/source/zh_CN/user_guide/envs/manipulation/franka_open_cabinet.md @@ -234,13 +234,13 @@ Franka Emika Panda 是一个 7 自由度机械臂,由以下主要部分组成 ### 训练 ```bash -python scripts/train.py task=franka-open-cabinet/skrl.ppo +mxtrain task=franka-open-cabinet/skrl.ppo ``` ### 策略评估 ```bash -python scripts/play.py env=franka-open-cabinet +mxplay env=franka-open-cabinet ``` ### TensorBoard diff --git a/docs/source/zh_CN/user_guide/envs/manipulation/rm65_insert_peg.md b/docs/source/zh_CN/user_guide/envs/manipulation/rm65_insert_peg.md index f7040789..cb14231a 100644 --- a/docs/source/zh_CN/user_guide/envs/manipulation/rm65_insert_peg.md +++ b/docs/source/zh_CN/user_guide/envs/manipulation/rm65_insert_peg.md @@ -269,13 +269,13 @@ RM65 机器人在该环境中包含以下主要部分: ### 训练 ```bash -python scripts/train.py task=rm65_insert_peg/motrix.fastsac +mxtrain task=rm65_insert_peg/motrix.fastsac ``` ### 策略评估 ```bash -python scripts/play.py env=rm65_insert_peg num_envs=16 +mxplay env=rm65_insert_peg num_envs=16 ``` ### TensorBoard diff --git a/docs/source/zh_CN/user_guide/envs/manipulation/rm65_open_cabinet.md b/docs/source/zh_CN/user_guide/envs/manipulation/rm65_open_cabinet.md index a3d281f4..ccd5f27a 100644 --- a/docs/source/zh_CN/user_guide/envs/manipulation/rm65_open_cabinet.md +++ b/docs/source/zh_CN/user_guide/envs/manipulation/rm65_open_cabinet.md @@ -242,13 +242,13 @@ RM65 机器人在该环境中包含以下主要部分: ### 训练 ```bash -python scripts/train.py task=rm65-open-cabinet/skrl.ppo task.train_backend=torch +mxtrain task=rm65-open-cabinet/skrl.ppo task.train_backend=torch ``` ### 策略评估 ```bash -python scripts/play.py env=rm65-open-cabinet +mxplay env=rm65-open-cabinet ``` ### TensorBoard diff --git a/docs/source/zh_CN/user_guide/envs/manipulation/shadow_hand_repose.md b/docs/source/zh_CN/user_guide/envs/manipulation/shadow_hand_repose.md index 51b3f033..ecba08f8 100644 --- a/docs/source/zh_CN/user_guide/envs/manipulation/shadow_hand_repose.md +++ b/docs/source/zh_CN/user_guide/envs/manipulation/shadow_hand_repose.md @@ -297,13 +297,13 @@ actuator_ctrls = targets ### 训练 ```bash -python scripts/train.py task=shadow-hand-repose/skrl.ppo +mxtrain task=shadow-hand-repose/skrl.ppo ``` ### 策略评估 ```bash -python scripts/play.py env=shadow-hand-repose +mxplay env=shadow-hand-repose ``` ### 环境可视化 diff --git a/docs/source/zh_CN/user_guide/envs/quadruped_locomotion/go1_rough_terrain.md b/docs/source/zh_CN/user_guide/envs/quadruped_locomotion/go1_rough_terrain.md index 866e5781..db8fe4d7 100644 --- a/docs/source/zh_CN/user_guide/envs/quadruped_locomotion/go1_rough_terrain.md +++ b/docs/source/zh_CN/user_guide/envs/quadruped_locomotion/go1_rough_terrain.md @@ -25,6 +25,6 @@ $v_y$ 和 `yaw_rate` 固定为 0。 ```bash python scripts/view.py env=go1-stairs-terrain-walk -python scripts/train.py task=go1-stairs-terrain-walk/skrl.ppo -python scripts/play.py env=go1-stairs-terrain-walk +mxtrain task=go1-stairs-terrain-walk/skrl.ppo +mxplay env=go1-stairs-terrain-walk ``` diff --git a/docs/source/zh_CN/user_guide/envs/quadruped_velocity_tracking.md b/docs/source/zh_CN/user_guide/envs/quadruped_velocity_tracking.md index b09bd331..c8eaf10e 100644 --- a/docs/source/zh_CN/user_guide/envs/quadruped_velocity_tracking.md +++ b/docs/source/zh_CN/user_guide/envs/quadruped_velocity_tracking.md @@ -111,14 +111,14 @@ MotrixLab 已为下列机器人提供平地和程序化粗糙高度场配置。 ```bash python scripts/view.py env=ENV_ID num_envs=1 -python scripts/train.py task=ENV_ID/TRAINING_CONFIG -python scripts/play.py env=ENV_ID num_envs=16 +mxtrain task=ENV_ID/TRAINING_CONFIG +mxplay env=ENV_ID num_envs=16 ``` 例如,使用 RSL-RL PPO 训练 Go2 粗糙地形任务: ```bash -python scripts/train.py task=go2-walk-rough/rslrl.ppo +mxtrain task=go2-walk-rough/rslrl.ppo ``` -`view.py` 使用随机动作,仅用于检查场景和模型;训练后的步态需要通过 `play.py` 回放策略查看。 +`view.py` 使用随机动作,仅用于检查场景和模型;训练后的步态需要通过 `mxplay` 回放策略查看。 diff --git a/docs/source/zh_CN/user_guide/envs/quadruped_velocity_tracking/adding_robot.md b/docs/source/zh_CN/user_guide/envs/quadruped_velocity_tracking/adding_robot.md index 6fe876f8..ba54219e 100644 --- a/docs/source/zh_CN/user_guide/envs/quadruped_velocity_tracking/adding_robot.md +++ b/docs/source/zh_CN/user_guide/envs/quadruped_velocity_tracking/adding_robot.md @@ -163,7 +163,8 @@ registry.env("-walk-flat")(QuadrupedWalkTask) registry.env("-walk-rough")(QuadrupedWalkTask) ``` -随后从 `motrix_envs/locomotion/quadruped/__init__.py` 导入新模块,确保 `import motrix_envs` 时完成注册。 +随后从 `motrix_envs/locomotion/quadruped/__init__.py` 导入新模块,让内置注册回调包含它。 +独立 package 的发布方式见[环境注册](../../tutorial/building_envs/direct_env.md#环境注册)。 `registry.env(...)` 与 `@registry.envcfg(...)` 的 ID 必须完全一致;只提供平地配置时,只注册平地 Env ID。 ## 6. 增加训练配置 @@ -193,7 +194,7 @@ SKRL 配置选择 `/algo_base@algo: skrl.ppo`,并将 `task.rllib` 设为 `skrl python scripts/view.py robot= python scripts/view.py env=-walk-flat python scripts/view.py env=-walk-rough -python scripts/train.py task=-walk-flat/rslrl.ppo +mxtrain task=-walk-flat/rslrl.ppo ``` 预览时重点检查默认姿态、动作方向、四脚接触、足端位置参考系和粗糙地形出生高度。 diff --git a/docs/source/zh_CN/user_guide/envs/whole_body_tracking/adding_wbt_task.md b/docs/source/zh_CN/user_guide/envs/whole_body_tracking/adding_wbt_task.md index 2ec487ea..381a8501 100644 --- a/docs/source/zh_CN/user_guide/envs/whole_body_tracking/adding_wbt_task.md +++ b/docs/source/zh_CN/user_guide/envs/whole_body_tracking/adding_wbt_task.md @@ -81,8 +81,9 @@ range。 registry.env("g1-wbt-dance1-subject1")(ManagerEnv) ``` -该模块已经由 `motrix_envs.locomotion.wbt` package 导入,因此 `import motrix_envs` 会执行注册。`envcfg` 和 `env` -名称必须完全一致。 +该模块已经由 `motrix_envs.locomotion.wbt` package 导入,因此内置注册回调会包含它。 +独立 package 的发布方式见[环境注册](../../tutorial/building_envs/direct_env.md#环境注册)。 +`envcfg` 和 `env` 名称必须完全一致。 ## 5. 新增 Training Task @@ -126,7 +127,7 @@ algo: 先用小规模 smoke test 验证 Hydra composition、registry、tensor shape 和一步训练流程: ```bash -python scripts/train.py task=g1-wbt-dance1-subject1/motrix.fastsac \ +mxtrain task=g1-wbt-dance1-subject1/motrix.fastsac \ algo.asynchronous=true num_envs=64 algo.trainer.num_learning_iterations=100 ``` @@ -134,13 +135,13 @@ python scripts/train.py task=g1-wbt-dance1-subject1/motrix.fastsac \ `state.reward_terms` 与 `state.metrics` 能进入日志。随后使用默认规模训练: ```bash -python scripts/train.py task=g1-wbt-dance1-subject1/motrix.fastsac algo.asynchronous=true +mxtrain task=g1-wbt-dance1-subject1/motrix.fastsac algo.asynchronous=true ``` 生成 metadata-backed run 后回放策略: ```bash -python scripts/play.py env=g1-wbt-dance1-subject1 num_envs=16 +mxplay env=g1-wbt-dance1-subject1 num_envs=16 ``` Play 模式自动从第 0 帧开始,关闭 reset noise 和 adaptive sampler,并在 clip 结束后从头重播,无需单独注册 play 环境。 @@ -149,10 +150,10 @@ Play 模式自动从第 0 帧开始,关闭 reset noise 和 adaptive sampler, - Motion 通过 schema 加载和目标机器人运动学 replay,且 `fps == 1 / ctrl_dt`。 - `envcfg`、`env` 和 Training Task 使用相同 Env ID。 -- Package 导入路径会执行新配置与环境注册。 +- Registry 自动发现已安装的回调,并执行新配置与环境注册。 - Hydra 可以发现 `motrix.fastsac` Training Task,且 `algo.asynchronous` 能选择两种执行拓扑。 - 小规模训练的 observation、action、reward、terminated 和 truncated shape 正确。 - Reset 没有系统性 NaN、hard-limit 违规或立即 bad-tracking。 - Reward、termination 和 adaptive-sampling 指标能够写入日志。 -- `play.py` 可以发现 run,并从第 0 帧完整回放策略。 +- `mxplay` 可以发现 run,并从第 0 帧完整回放策略。 - 新 motion 包含来源与许可说明,并由 Git LFS 管理。 diff --git a/docs/source/zh_CN/user_guide/envs/whole_body_tracking/index.md b/docs/source/zh_CN/user_guide/envs/whole_body_tracking/index.md index 798f2749..71ce0612 100644 --- a/docs/source/zh_CN/user_guide/envs/whole_body_tracking/index.md +++ b/docs/source/zh_CN/user_guide/envs/whole_body_tracking/index.md @@ -159,20 +159,20 @@ python scripts/motion/replay.py \ 从“内置任务”表格中选择 Env ID 和对应的训练配置,并替换下列命令中的 `ENV_ID` 与 `TRAINING_CONFIG`: ```bash -python scripts/train.py task=ENV_ID/TRAINING_CONFIG +mxtrain task=ENV_ID/TRAINING_CONFIG ``` 例如,使用 FastSAC 异步训练 G1 舞蹈跟踪任务: ```bash -python scripts/train.py task=g1-wbt-dance/motrix.fastsac algo.asynchronous=true +mxtrain task=g1-wbt-dance/motrix.fastsac algo.asynchronous=true ``` 内置配置默认使用 2048 个并行环境,并按任务设置学习迭代次数。只验证 registry、shape 和训练入口时,可以使用不会 生成可用策略的小规模 smoke test: ```bash -python scripts/train.py task=g1-wbt-dance/motrix.fastsac \ +mxtrain task=g1-wbt-dance/motrix.fastsac \ algo.asynchronous=true num_envs=64 algo.trainer.num_learning_iterations=100 ``` @@ -183,9 +183,9 @@ python scripts/train.py task=g1-wbt-dance/motrix.fastsac \ ### 回放策略 ```bash -python scripts/play.py env=ENV_ID num_envs=16 +mxplay env=ENV_ID num_envs=16 ``` -`play.py` 自动选择该环境最新一次 metadata-backed run 的最佳策略。WBT 的 play 配置从 motion 第 0 帧开始,关闭 +`mxplay` 自动选择该环境最新一次 metadata-backed run 的最佳策略。WBT 的 play 配置从 motion 第 0 帧开始,关闭 reset noise 和自适应采样,并移除 10 秒训练时限;播放到 clip 末尾后从第 0 帧重新开始。训练产物和 checkpoint 选择规则见 [训练产物:runs 目录与 checkpoint 结构](../../tutorial/training/runs_and_checkpoints.md)。 diff --git a/docs/source/zh_CN/user_guide/getting_started/container_deployment.md b/docs/source/zh_CN/user_guide/getting_started/container_deployment.md index 756fb888..450acc97 100644 --- a/docs/source/zh_CN/user_guide/getting_started/container_deployment.md +++ b/docs/source/zh_CN/user_guide/getting_started/container_deployment.md @@ -98,7 +98,7 @@ docker build -t motphys/motrixlab:latest . docker run --gpus all \ -v $(pwd)/runs:/root/motrixlab/runs \ motphys/motrixlab:latest \ - scripts/train.py task=cartpole/skrl.ppo task.train_backend=jax num_envs=2048 + mxtrain task=cartpole/skrl.ppo task.train_backend=jax num_envs=2048 ``` ### 持久化训练结果 diff --git a/docs/source/zh_CN/user_guide/getting_started/hello_motrixlab.md b/docs/source/zh_CN/user_guide/getting_started/hello_motrixlab.md index 629f73b6..b8690f5d 100644 --- a/docs/source/zh_CN/user_guide/getting_started/hello_motrixlab.md +++ b/docs/source/zh_CN/user_guide/getting_started/hello_motrixlab.md @@ -19,7 +19,7 @@ python scripts/view.py env=cartpole 开始训练倒立摆平衡任务: ```bash -python scripts/train.py task=cartpole/skrl.ppo +mxtrain task=cartpole/skrl.ppo ``` 训练过程会自动: @@ -38,7 +38,7 @@ python scripts/train.py task=cartpole/skrl.ppo 如果您想要在训练过程中观察模型的学习过程,可以启用可视化渲染: ```bash -python scripts/train.py task=cartpole/skrl.ppo render=true +mxtrain task=cartpole/skrl.ppo render=true ``` ### 🎮 交互式渲染控制 @@ -67,10 +67,10 @@ tensorboard --logdir runs/cartpole ```bash # 自动寻找最佳策略测试(推荐) -python scripts/play.py env=cartpole +mxplay env=cartpole # 手动指定带 metadata 的 run 中的 checkpoint -python scripts/play.py env=cartpole policy=/path/to/run/checkpoints/policy-file +mxplay env=cartpole policy=/path/to/run/checkpoints/policy-file ``` > **提示**:系统会自动在 `runs/cartpole/` 下寻找最新 run 的最佳策略。手动指定的 checkpoint 必须属于包含 `metadata.json` 和 `task_config.yaml` 的 run。 diff --git a/docs/source/zh_CN/user_guide/tutorial/advanced/custom_training_backend.md b/docs/source/zh_CN/user_guide/tutorial/advanced/custom_training_backend.md index fdba6a73..1819faa5 100644 --- a/docs/source/zh_CN/user_guide/tutorial/advanced/custom_training_backend.md +++ b/docs/source/zh_CN/user_guide/tutorial/advanced/custom_training_backend.md @@ -163,7 +163,7 @@ MotrixLab 会在解析 framework 前,从这个 group 加载已安装的 entry ## Step 5:添加 Hydra 训练配置 -Provider 注册时会自动安装 `MyPpoCfg` 对应的结构化 schema。然后添加算法默认值: +Provider 注册时会自动安装 `MyPpoCfg` 对应的结构化 schema。自定义算法与 Task YAML 保留在 workspace 的 `configs/` 下;内置算法默认配置位于 `motrix_rl/src/motrix_rl/configs/algo_base/`。然后添加自定义算法默认值: ```yaml # configs/algo_base/myrl.ppo.yaml @@ -201,7 +201,7 @@ seed: 42 如果后端已经在启动路径中完成注册,可以直接用 CLI: ```bash -python scripts/train.py task=cartpole/myrl.ppo +mxtrain task=cartpole/myrl.ppo ``` 如果是外部实验包,使用自己的 Hydra config root,并在进入训练入口前导入 backend 注册模块。无需额外的 Python task registry。 diff --git a/docs/source/zh_CN/user_guide/tutorial/advanced/export_onnx.md b/docs/source/zh_CN/user_guide/tutorial/advanced/export_onnx.md index 076bc6e4..11961eca 100644 --- a/docs/source/zh_CN/user_guide/tutorial/advanced/export_onnx.md +++ b/docs/source/zh_CN/user_guide/tutorial/advanced/export_onnx.md @@ -23,6 +23,13 @@ SKRL/JAX checkpoint 暂不支持导出。导出命令接收完整的 run 目录 ## 导出模型 +激活已安装的环境后,运行 `mxexport onnx`(或 `python -m motrix_rl.commands.export onnx`)。 +`mxexport` 必须显式选择 `onnx` 或 `deploy` 子命令,没有默认子命令。 +不需要选择 `task` 或导入模块来触发注册:导出直接从已保存的 run 恢复策略,不创建环境。需要安装该 run 使用的 RL provider;环境 provider 发现属于独立的 registry 职责。 +命令会使用当前目录下存在的 `./configs`,也可以通过 `--config-dir ` 显式指定配置目录; +项目文件优先,缺失的文件回退到 `motrix_rl.configs`,其中包含 `export_onnx.yaml`。 +无需仓库 checkout,相对 run 路径和输出路径均相对于当前工作目录解析。 + 假设训练产生了以下 run 目录: ```text @@ -32,7 +39,7 @@ runs/cartpole/skrl/torch/ppo// 指定输出文件进行导出: ```bash -python scripts/export_onnx.py \ +mxexport onnx \ run_dir=runs/cartpole/skrl/torch/ppo/ \ output=artifacts/cartpole.onnx ``` @@ -42,7 +49,7 @@ python scripts/export_onnx.py \ 如果省略 `output`,模型会写入最佳 checkpoint 所在目录,文件名为 `policy.onnx`: ```bash -python scripts/export_onnx.py \ +mxexport onnx \ run_dir=runs/cartpole/skrl/torch/ppo/ ``` @@ -104,7 +111,7 @@ print(actions) 例如,导出为 opset 17,并增加检查样本数: ```bash -python scripts/export_onnx.py \ +mxexport onnx \ run_dir= \ output=policy.onnx \ opset=17 \ diff --git a/docs/source/zh_CN/user_guide/tutorial/advanced/motrix_deploy.md b/docs/source/zh_CN/user_guide/tutorial/advanced/motrix_deploy.md index 38586e0a..2a1730c8 100644 --- a/docs/source/zh_CN/user_guide/tutorial/advanced/motrix_deploy.md +++ b/docs/source/zh_CN/user_guide/tutorial/advanced/motrix_deploy.md @@ -3,7 +3,7 @@ 将导出的 Go2 策略先在 MuJoCo 中检查,再尝试真机部署。安装任务和 MuJoCo 插件后,可用以下命令进行有界无窗口检查: ```bash -motrix-deploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy runtime.viewer=false duration_s=2.0 +mxdeploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy runtime.viewer=false duration_s=2.0 ``` 下面介绍如何生成 artifact、交互控制,以及现有 Unitree 真机工作流。 @@ -15,7 +15,7 @@ motrix-deploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy run ```bash sh install.sh --all source .venv/bin/activate -python scripts/train.py task=go2-walk-flat/rslrl.ppo +mxtrain task=go2-walk-flat/rslrl.ppo ``` 训练结果保存在 `runs/go2-walk-flat/`。已有包含 metadata 的 run 时可跳过训练。 @@ -23,12 +23,23 @@ python scripts/train.py task=go2-walk-flat/rslrl.ppo ## 2. 导出并检查 artifact ```bash -python scripts/export_deploy.py env=go2-walk-flat -motrix-deploy inspect artifact=artifacts/go2-walk-flat.deploy +mxexport deploy env=go2-walk-flat output=artifacts/go2-walk-flat.deploy +mxdeploy inspect artifact=artifacts/go2-walk-flat.deploy ``` -导出会选择最近一次 run,并创建 `artifacts/go2-walk-flat.deploy/`。用 `run=` 替代 `env` 可选择指定 run, -`output=` 可更改输出位置。已有 artifact 目录不会被覆盖。确认检查结果为 `valid: true`。 +`env=go2-walk-flat` 选择最近一次包含 metadata 的 run。用 `run_dir=` 替代 `env` 可选择指定 run: +`env` 与 `run_dir` 必须且只能设置一个。`output=` 可更改输出位置,省略时默认为 +`artifacts/.deploy`。已有 artifact 目录不会被覆盖。确认检查结果为 `valid: true`。 + +`mxexport` 必须显式选择 `onnx` 或 `deploy` 子命令。等价模块命令是 +`python -m motrix_rl.commands.export deploy`。导出会使用当前目录下存在的 `./configs`,也可通过 +`--config-dir ` 显式指定;项目文件优先,缺失文件回退到 `motrix_rl.configs`。 +其中打包了 `export_deploy.yaml` 与 `export_onnx.yaml`,因此导出不需要仓库 checkout。 +部署导出的数值检查选项仍使用 `validation.*`(例如 `validation.atol=3e-5`), +而不是 ONNX 子命令的 `parity.*`。 + +导出由 `motrix-rl` 执行,需要训练 provider 和该环境已安装的 profile compiler。 +依赖是单向的:`motrix-rl` 使用 `motrix-deploy` 创建 artifact;`motrix-deploy` 不依赖 RL。 artifact 目录自包含: @@ -52,7 +63,7 @@ go2-walk-flat.deploy/ 使用开头的无窗口命令进行有界运行,或打开 viewer: ```bash -motrix-deploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy +mxdeploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy ``` 按住 `W/S` 前后移动,`A/D` 横移,`Q/E` 转向。关闭窗口、按 Esc 或 Ctrl-C 退出。 @@ -82,7 +93,7 @@ runtime 负责物理推进、调度与 viewer。[进阶的显式组装](#显式 安装 `motrix-deploy-motrixsim` 后,将 backend 切换为 `motrixsim`,即可复用同一 artifact 和场景配方: ```bash -motrix-deploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy \ +mxdeploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy \ runtime.backend=motrixsim runtime.viewer=true ``` @@ -98,7 +109,7 @@ python examples/deploy_to_sim.py --backend motrixsim 让机器人执行站下动作。先检查 artifact,再将 `enp5s0` 换成实际网卡名称: ```bash -motrix-deploy task=go2-walk-flat/hardware \ +mxdeploy task=go2-walk-flat/hardware \ artifact=artifacts/go2-walk-flat.deploy \ runtime.network_interface=enp5s0 ``` @@ -138,25 +149,25 @@ motrix-deploy-unitree joint-control enp5s0 FL_thigh_joint 0.9 \ ## 部署 G1 WBT 舞蹈 -G1 全身跟踪舞蹈任务使用同样的导出 → artifact → `motrix-deploy` 流程。artifact 内嵌伺服增益、action scale、 +G1 全身跟踪舞蹈任务使用同样的导出 → artifact → `mxdeploy` 流程。artifact 内嵌伺服增益、action scale、 preparation 增益与时序、安全阈值以及完整的 motion 片段(以 `payloads/motion.npz` payload 形式,校验和记录在 manifest 中),部署既不需要 Torch 也不需要 RL 环境。导出需要训练 provider 与任务环境包,但**不**执行训练: ```bash source .venv/bin/activate RUN=runs/g1-wbt-dance/motrix/torch/fastsac/ -python scripts/export_deploy.py run="$RUN" \ +mxexport deploy run_dir="$RUN" \ output=artifacts/g1-wbt-dance-software-pd.deploy validation.atol=3e-5 -motrix-deploy task=g1-wbt-dance/sim artifact=artifacts/g1-wbt-dance.deploy +mxdeploy task=g1-wbt-dance/sim artifact=artifacts/g1-wbt-dance.deploy ``` 无窗口验证与 artifact 检查: ```bash -motrix-deploy task=g1-wbt-dance/sim artifact=artifacts/g1-wbt-dance.deploy \ +mxdeploy task=g1-wbt-dance/sim artifact=artifacts/g1-wbt-dance.deploy \ runtime.viewer=false -motrix-deploy inspect artifact=artifacts/g1-wbt-dance.deploy +mxdeploy inspect artifact=artifacts/g1-wbt-dance.deploy ``` 默认时长来自内嵌的 motion 片段;更短的运行用 `duration_s` 缩短,超长时长会在打开 backend 前被拒绝, @@ -172,6 +183,48 @@ policy 能维持平衡:播放预算完成后 task 直接进入实测 damping ## 进阶说明 +### 添加部署 profile compiler + +环境 package 在 `motrix_deploy.profiles` 组中发布一个零参数注册回调。在 `pyproject.toml` 中声明 provider: + +```toml +[project.entry-points."motrix_deploy.profiles"] +my-project = "my_project.deploy:register" +``` + +在 `my_project/deploy/__init__.py` 中注册环境与 compiler 的映射: + +```python +def register() -> None: + from motrix_deploy.profile import register_profile_compiler + from my_project.deploy.walk import build_profile + + register_profile_compiler("my-walk-flat", build_profile) +``` + +在 `my_project/deploy/walk.py` 中实现 `build_profile(env_name: str) -> DeploymentProfile`, +从 `motrix_deploy.profile` 导入 `DeploymentProfile`。compiler 将所选环境的机器人、任务、控制设置、 +训练空间的 observation/action 维度,以及任务自有 payload 编译成 profile。 +注册回调只声明映射,不应在其中创建环境、构建模型或读取 motion 数据。 +一个回调可以注册多个 Env ID,同一个 compiler 也可以服务多个 ID。 + +安装 package 后,导出对应环境的 run: + +```bash +mxexport deploy run_dir=/path/to/my-walk-flat/run output=artifacts/my-walk-flat.deploy +``` + +首次 profile 查询、列举或构建会加载所有已安装的 provider 回调,每个进程执行一次。 +因此,`motrix_deploy.profile` 中的 `registered_profile_compilers()` 会先执行注册代码,再列出 Env ID; +只有 `build_deployment_profile(env_name)` 会调用所选环境的 compiler。 +请安装每个 provider 的导入依赖:即使请求的是另一个 Env ID,某个 provider 缺少依赖仍可能导致发现失败。 +重复 provider 名称、重复 compiler Env ID,以及 provider 加载或注册失败都会报错。 + +应用无需显式导入模块来触发 profile 注册。provider 使用部署 API,不需要依赖 RL; +`motrix-rl` 消费这些 profile 完成导出,而不是反向依赖。 +内置 compiler 映射见 +[Motrix Environments README](https://github.com/Motphys/MotrixLab/blob/main/motrix_envs/README.md)。 + ### 从 YAML 加载 task spec 程序化应用可在 `task.yaml` 中保存完整 Go2 任务字段(即 `task.config` 的内容,不包含 manifest envelope)。 @@ -200,7 +253,7 @@ spec = Go2WalkTaskSpec.model_validate(OmegaConf.to_container(cfg, resolve=True)) 保留同一 Go2 recipe 和 artifact,仅切换世界: ```bash -motrix-deploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy \ +mxdeploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy \ runtime.deploy_env_id=rough ``` @@ -293,7 +346,7 @@ with runtime: 选择原生 backend: ```bash -motrix-deploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy \ +mxdeploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy \ runtime.backend=motrixsim runtime.viewer=false runtime.realtime=false duration_s=1.0 ``` @@ -318,7 +371,7 @@ Headless 使用零速度指令;窗口模式在可用时使用 viewer keyboard `/absolute/path/to/my_app/config/deploy.yaml`,不是选择或复制内置 recipe: ```bash -motrix-deploy \ +mxdeploy \ --config-path /absolute/path/to/my_app/config \ --config-name deploy \ artifact=artifacts/go2-walk-flat.deploy diff --git a/docs/source/zh_CN/user_guide/tutorial/basic_frame.md b/docs/source/zh_CN/user_guide/tutorial/basic_frame.md index b5dfb8d4..9f4bdefa 100644 --- a/docs/source/zh_CN/user_guide/tutorial/basic_frame.md +++ b/docs/source/zh_CN/user_guide/tutorial/basic_frame.md @@ -42,7 +42,8 @@ MotrixLab 是一个 UV workspace。下面按所有权而不是按单一整体运 | `motrix_rl_builtin` | 策略的训练与更新 | 内置 Motrix FastSAC provider | | `motrix_rl_skrl` | 策略的训练与更新 | SKRL PPO Torch provider(必需)及可选 JAX extra | | `motrix_rl_rslrl` | 策略的训练与更新 | RSL-RL PPO Torch provider(可选) | -| `configs/`、`scripts/` | 配置与编排 | Hydra 算法配置与 Task 配方;train / play / view / export 入口 | +| `motrix_rl.configs`、`motrix_rl.commands` | 配置与编排 | 打包的 train/play 配置与算法默认值;安装后的 `mxtrain` / `mxplay` 入口 | +| `configs/`、`scripts/` | Workspace 配方与工具 | 具体 Task 配方与 view 配置;view / export 工具及 train/play 薄包装 | | `motrix_deploy` | 策略的部署 | 框架无关 artifact、运行时契约与部署 CLI | | `motrix_deploy_mujoco` | 策略的部署 | MuJoCo 部署 backend plugin | | `motrix_deploy_motrixsim` | 策略的部署 | MotrixSim 部署 backend plugin | @@ -52,13 +53,28 @@ MotrixLab 是一个 UV workspace。下面按所有权而不是按单一整体运 仿真后端(如 `motrix_env_motrixsim`)通过 `SimBackend` 接口隔离在环境框架之下,使用环境时通常 无需关心它;需要选择或接入仿真后端时,见[SimBackend:与仿真器解耦](building_envs/sim_backend.md)。 +配置与命令源码的目录如下: + +```text +motrix_rl/src/motrix_rl/ +├── commands/ # mxtrain / mxplay +└── configs/ + ├── train.yaml + ├── play.yaml + └── algo_base/ +configs/ +├── task/ # workspace 训练配方 +└── view.yaml +scripts/ # view/export 工具;train/play 薄包装 +``` + ## 一次训练的完整流程 ```bash -python scripts/train.py task=cartpole/skrl.ppo num_envs=1024 +mxtrain task=cartpole/skrl.ppo num_envs=1024 ``` -1. Hydra 组合 `configs/train.yaml`、`configs/algo_base/skrl.ppo.yaml` 和 +1. Hydra 组合 `motrix_rl/src/motrix_rl/configs/train.yaml`、`motrix_rl/src/motrix_rl/configs/algo_base/skrl.ppo.yaml` 和 `configs/task/cartpole/skrl.ppo.yaml`,`num_envs=1024` 只覆盖本次运行的值。 2. runner 解析 SKRL PPO provider,并从可用的 JAX/Torch 后端中自动选择。 3. trainer 通过环境注册表创建 `cartpole` 环境并开始优化。 @@ -67,8 +83,8 @@ python scripts/train.py task=cartpole/skrl.ppo num_envs=1024 同一环境可以拥有多份 Task 配方,不需要修改环境实现: ```bash -python scripts/train.py task=cartpole/skrl.ppo -python scripts/train.py task=cartpole/rslrl.ppo +mxtrain task=cartpole/skrl.ppo +mxtrain task=cartpole/rslrl.ppo ``` ## 运行导出的策略 @@ -76,7 +92,7 @@ python scripts/train.py task=cartpole/rslrl.ppo 部署使用 artifact,而不是训练环境: ```bash -motrix-deploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy runtime.viewer=false duration_s=2.0 +mxdeploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy runtime.viewer=false duration_s=2.0 ``` 上例在 MuJoCo 中无窗口运行导出的策略。导出、交互控制及真机操作见 [Go2 部署](advanced/motrix_deploy.md)。 diff --git a/docs/source/zh_CN/user_guide/tutorial/building_envs/direct_env.md b/docs/source/zh_CN/user_guide/tutorial/building_envs/direct_env.md index 0eb1d64e..0e2fb6e6 100644 --- a/docs/source/zh_CN/user_guide/tutorial/building_envs/direct_env.md +++ b/docs/source/zh_CN/user_guide/tutorial/building_envs/direct_env.md @@ -173,7 +173,12 @@ class CartPoleEnv(DirectEnv): -## 注册约定 +## 环境注册 + +DirectEnv 和 ManagerEnv 使用相同的注册机制:先用一致的 Env ID 注册配置与环境, +再发布注册回调,让已安装的应用能够发现它们。 + +### 注册配置与环境 - **配置先于环境类注册**:`@registry.env("name")` 要求同名配置已通过 `@registry.envcfg("name")` 注册,否则报错。 @@ -183,3 +188,38 @@ class CartPoleEnv(DirectEnv): 不要手动指定。 - 配置 docstring 的首行与 `zh_CN:` 行分别作为环境的英文、中文描述, 展示在环境概览中。 + +### 发布环境 package + +在环境 package 的 `pyproject.toml` 中声明一个 provider 回调: + +```toml +[project.entry-points."motrix_env_core.envs"] +my-project = "my_project.envs:register" +``` + +在 `my_project/envs/__init__.py` 中定义零参数回调: + +```python +def register() -> None: + # 先导入配置,再导入使用这些注册配置的环境模块。 + from my_project.envs import my_robot_cfg # noqa: F401 + from my_project.envs import my_robot_env # noqa: F401 +``` + +在上述模块中保留 `@registry.envcfg("my-robot")` 和 `@registry.env("my-robot")`。 +一个 provider 回调可以注册多个环境;Env ID 仍由装饰器声明,不需要在 entry-point 映射中重复列出。 + +安装 package 后,即可按名字创建环境,无需为了注册而显式导入模块: + +```python +from motrix_env_core import registry + +env = registry.make("my-robot", num_envs=1) +``` + +首次环境查询会加载所有已安装的 provider,每个进程执行一次。provider 加载或注册错误会报告给调用方; +即使请求的是另一个 Env ID,某个 provider 缺少依赖仍可能导致发现失败。请安装各 provider 所需的依赖。 + +环境 package 负责环境注册,不负责训练 preset。使用环境的项目通过自己的配置目录或 Hydra search path +提供 Task YAML,见[Task 配置与命令行参数覆盖](../training/task_config.md)。 diff --git a/docs/source/zh_CN/user_guide/tutorial/building_envs/index.md b/docs/source/zh_CN/user_guide/tutorial/building_envs/index.md index 36d72620..ae91adac 100644 --- a/docs/source/zh_CN/user_guide/tutorial/building_envs/index.md +++ b/docs/source/zh_CN/user_guide/tutorial/building_envs/index.md @@ -10,6 +10,7 @@ - **环境类**:实现任务逻辑,通过 `@registry.env("name")` 注册。 注册完成后,`scripts/view.py`、训练器与回放流程都通过同一个注册表按名字创建环境。 +已安装的环境 package 会被自动发现;两种工作流的 package 发布方式见[环境注册](direct_env.md#环境注册)。 ## 生命周期总览 diff --git a/docs/source/zh_CN/user_guide/tutorial/training/index.md b/docs/source/zh_CN/user_guide/tutorial/training/index.md index 7629fcf6..993e7811 100644 --- a/docs/source/zh_CN/user_guide/tutorial/training/index.md +++ b/docs/source/zh_CN/user_guide/tutorial/training/index.md @@ -5,7 +5,7 @@ ```text 创建 Task 配置 指定环境、RL 框架、算法与超参数 ↓ -python scripts/train.py task=... Hydra 组合配置并启动训练 +mxtrain task=... Hydra 组合配置并启动训练 ↓ runs/{env}/... 保存 metadata、日志与 checkpoint ``` diff --git a/docs/source/zh_CN/user_guide/tutorial/training/runs_and_checkpoints.md b/docs/source/zh_CN/user_guide/tutorial/training/runs_and_checkpoints.md index c38aee24..b35bec32 100644 --- a/docs/source/zh_CN/user_guide/tutorial/training/runs_and_checkpoints.md +++ b/docs/source/zh_CN/user_guide/tutorial/training/runs_and_checkpoints.md @@ -93,7 +93,7 @@ runs/g1-walk-flat/motrix/torch/fastsac/26-07-06_11-37-50-376526/ ``` - `manifest.json` 里的 `path` 相对于 `checkpoints/` 目录。 -- **`best_policy`**(`kind: policy`):用于回放 / 导出 / 评测,是 `play.py` 默认加载的产物。 +- **`best_policy`**(`kind: policy`):用于回放 / 导出 / 评测,是 `mxplay` 默认加载的产物。 - **`latest_training_state`**(`kind: training_state`):用于续训,应包含 optimizer、观测归一化器、replay buffer、`global_step` 等继续训练所需的完整状态。 - 同一个物理文件可以**同时登记为两个 artifact**。例如 FastSAC 的 `latest.pt`(完整 state dict)既能回放又能续训,因此 `best_policy` 与 `latest_training_state` 都指向它。 @@ -106,16 +106,16 @@ runs/g1-walk-flat/motrix/torch/fastsac/26-07-06_11-37-50-376526/ ```bash # 自动发现最新 run 的最佳策略 - python scripts/play.py env=g1-walk-flat + mxplay env=g1-walk-flat # 指定某个 checkpoint(需能向上找到 metadata.json) - python scripts/play.py env=g1-walk-flat policy=/path/to/run/checkpoints/latest.pt + mxplay env=g1-walk-flat policy=/path/to/run/checkpoints/latest.pt ``` - **续训(resume)**:将 `resume=` 设置为 run 目录或 checkpoint 路径,框架据此解析出 `latest_training_state` 继续训练。 ```bash - python scripts/train.py task=g1-walk-flat/motrix.fastsac \ + mxtrain task=g1-walk-flat/motrix.fastsac \ resume=/path/to/run ``` diff --git a/docs/source/zh_CN/user_guide/tutorial/training/task_config.md b/docs/source/zh_CN/user_guide/tutorial/training/task_config.md index f466d9df..ca2b5fa3 100644 --- a/docs/source/zh_CN/user_guide/tutorial/training/task_config.md +++ b/docs/source/zh_CN/user_guide/tutorial/training/task_config.md @@ -53,7 +53,7 @@ algo: 这个配置完成了四件事: -1. 从 `configs/algo_base/skrl.ppo.yaml` 加载完整的 SKRL PPO 基础配置。 +1. 从 `motrix_rl/src/motrix_rl/configs/algo_base/skrl.ppo.yaml` 加载完整的 SKRL PPO 基础配置。 2. 选择注册名为 `my-robot` 的环境。 3. 设置训练和播放使用的并行环境数量。 4. 将训练总步数覆盖为 `100000`。 @@ -62,22 +62,27 @@ Task 只需填写与算法基础配置不同的值,不需要复制所有算法 ### 检查最终配置 +`mxtrain` 默认使用当前工作目录下存在的 `./configs`,不会向上搜索父目录。使用 `--config-dir /path/to/configs` 可指定其他配置目录。必须显式选择 Task,没有默认的 Cartpole Task。 + +运行 Task 前先安装环境 package,registry 查询会自动发现已安装的 provider。 +自定义环境的打包方式见[环境注册](../building_envs/direct_env.md#环境注册);Task YAML 的发现与环境发现相互独立。 + 启动训练前,可以先输出 Hydra 组合后的完整配置: ```bash -python scripts/train.py --cfg job --resolve task=my-robot/skrl.ppo +mxtrain --cfg job --resolve task=my-robot/skrl.ppo ``` 该命令不会开始训练。确认 `task.env`、`num_envs` 和 `algo.trainer.timesteps` 符合预期后,再启动训练: ```bash -python scripts/train.py task=my-robot/skrl.ppo +mxtrain task=my-robot/skrl.ppo ``` 运行以下命令可以查看仓库中所有可选 Task: ```bash -python scripts/train.py --help +mxtrain --help ``` ## 调整 Task 的运行参数 @@ -146,7 +151,7 @@ algo: timesteps: 20000 ``` -完整字段和逐字段注释见 [`configs/algo_base/skrl.ppo.yaml`](../../../../../configs/algo_base/skrl.ppo.yaml)。 +完整字段和逐字段注释见 [`motrix_rl/src/motrix_rl/configs/algo_base/skrl.ppo.yaml`](https://github.com/Motphys/MotrixLab/blob/main/motrix_rl/src/motrix_rl/configs/algo_base/skrl.ppo.yaml)。 ### RSLRL PPO @@ -167,7 +172,7 @@ algo: entropy_coef: 0.005 ``` -完整字段和逐字段注释见 [`configs/algo_base/rslrl.ppo.yaml`](../../../../../configs/algo_base/rslrl.ppo.yaml)。 +完整字段和逐字段注释见 [`motrix_rl/src/motrix_rl/configs/algo_base/rslrl.ppo.yaml`](https://github.com/Motphys/MotrixLab/blob/main/motrix_rl/src/motrix_rl/configs/algo_base/rslrl.ppo.yaml)。 ### Motrix FastSAC @@ -182,7 +187,7 @@ FastSAC 使用同一个算法身份支持两种执行拓扑,主要字段和配 - `weight_publish_interval`、`weight_poll_interval`:策略权重同步频率。 - `max_ingest_per_iter`、`idle_sleep_s`:Learner 数据摄取和空闲退避参数。 -完整字段和逐字段注释见 [`configs/algo_base/motrix.fastsac.yaml`](../../../../../configs/algo_base/motrix.fastsac.yaml)。 +完整字段和逐字段注释见 [`motrix_rl/src/motrix_rl/configs/algo_base/motrix.fastsac.yaml`](https://github.com/Motphys/MotrixLab/blob/main/motrix_rl/src/motrix_rl/configs/algo_base/motrix.fastsac.yaml)。 ## 使用 CLI 临时覆盖参数 @@ -191,7 +196,7 @@ MotrixLab CLI 使用 Hydra 的 `key=value` 参数语法。CLI 覆盖只影响本 ### 覆盖运行参数 ```bash -python scripts/train.py \ +mxtrain \ task=my-robot/skrl.ppo \ num_envs=64 \ seed=7 \ @@ -202,7 +207,7 @@ python scripts/train.py \ 开启训练渲染,并在训练完成后播放: ```bash -python scripts/train.py task=my-robot/skrl.ppo render=true play=true +mxtrain task=my-robot/skrl.ppo render=true play=true ``` ### 覆盖算法参数 @@ -210,7 +215,7 @@ python scripts/train.py task=my-robot/skrl.ppo render=true play=true SKRL PPO: ```bash -python scripts/train.py \ +mxtrain \ task=my-robot/skrl.ppo \ algo.agent.learning_rate=5e-4 \ algo.agent.learning_epochs=8 \ @@ -220,7 +225,7 @@ python scripts/train.py \ RSLRL PPO: ```bash -python scripts/train.py \ +mxtrain \ task=cartpole/rslrl.ppo \ algo.algorithm.learning_rate=5e-4 \ algo.algorithm.entropy_coef=0.005 \ @@ -230,7 +235,7 @@ python scripts/train.py \ FastSAC: ```bash -python scripts/train.py \ +mxtrain \ task=g1-walk-flat/motrix.fastsac \ algo.asynchronous=true \ algo.agent.actor_learning_rate=1e-4 \ @@ -244,20 +249,20 @@ python scripts/train.py \ 布尔值使用小写 `true` 或 `false`: ```bash -python scripts/train.py task=my-robot/skrl.ppo render=true +mxtrain task=my-robot/skrl.ppo render=true ``` 使用 `null` 清空可空字段: ```bash -python scripts/train.py task=my-robot/skrl.ppo seed=null -python scripts/train.py task=my-robot/skrl.ppo algo.agent.learning_rate_scheduler=null +mxtrain task=my-robot/skrl.ppo seed=null +mxtrain task=my-robot/skrl.ppo algo.agent.learning_rate_scheduler=null ``` 列表建议使用引号,避免 shell 解释方括号: ```bash -python scripts/train.py \ +mxtrain \ task=my-robot/skrl.ppo \ 'algo.models.policy.hiddens=[128,64]' \ 'algo.models.value.hiddens=[128,64]' @@ -274,7 +279,7 @@ python scripts/train.py \ 将 CLI 覆盖和 `--cfg job --resolve` 组合,可以查看本次运行最终会使用的值: ```bash -python scripts/train.py \ +mxtrain \ --cfg job \ --resolve \ task=my-robot/skrl.ppo \ @@ -316,13 +321,13 @@ algo: 使用 JAX 差异配置训练: ```bash -python scripts/train.py task=go2-walk-flat/skrl.ppo.jax +mxtrain task=go2-walk-flat/skrl.ppo.jax ``` 直接覆盖后端也可以选择 JAX 训练器: ```bash -python scripts/train.py task=go2-walk-flat/skrl.ppo task.train_backend=jax +mxtrain task=go2-walk-flat/skrl.ppo task.train_backend=jax ``` 这种写法不会加载 `skrl.ppo.jax.yaml` 中的后端专用参数。当后端差异文件存在时,应直接选择带 `.jax` 或 `.torch` 后缀的 Task。 @@ -334,12 +339,16 @@ python scripts/train.py task=go2-walk-flat/skrl.ppo task.train_backend=jax ### 配置目录 ```text -configs/ +motrix_rl/src/motrix_rl/configs/ ├── train.yaml -├── algo_base/ -│ ├── skrl.ppo.yaml -│ ├── rslrl.ppo.yaml -│ └── motrix.fastsac.yaml +├── play.yaml +└── algo_base/ + ├── skrl.ppo.yaml + ├── rslrl.ppo.yaml + └── motrix.fastsac.yaml + +configs/ +├── view.yaml └── task/ └── / ├── ..yaml @@ -350,8 +359,8 @@ configs/ | 配置层 | 位置 | 用途 | | ------------ | -------------------------------------------------- | ---------------------------------- | -| 入口配置 | `configs/train.yaml` | 定义通用训练参数和默认 Task | -| 算法基础配置 | `configs/algo_base/..yaml` | 提供某种算法的完整字段和基础值 | +| 入口配置 | `motrix_rl/src/motrix_rl/configs/train.yaml` | 定义通用训练参数;`task: null` 为占位,训练时必须显式选择 Task | +| 算法基础配置 | `motrix_rl/src/motrix_rl/configs/algo_base/..yaml` | 提供某种算法的完整字段和基础值 | | 共享 Task | `configs/task//..yaml` | 选择环境和算法,并保存任务调优参数 | | 后端差异配置 | `configs/task//...yaml` | 保存后端独有的增量参数 | | CLI 覆盖 | `key=value` | 临时覆盖本次运行的最终配置 | @@ -410,18 +419,18 @@ Hydra 会根据 provider 注册的结构化 schema 检查字段名和类型。 ## Play 和 View 的参数覆盖 -`scripts/play.py` 和 `scripts/view.py` 同样使用 `key=value` 语法: +`mxplay` 恢复已保存的 run 配置,不需要训练 Task 目录。它和 `scripts/view.py` 同样使用 `key=value` 语法: ```bash python scripts/view.py env=cartpole num_envs=4 -python scripts/play.py env=cartpole num_envs=1 -python scripts/play.py policy=/path/to/checkpoint.pt num_envs=1 +mxplay env=cartpole num_envs=1 +mxplay policy=/path/to/checkpoint.pt num_envs=1 ``` Play 默认读取训练 run 中保存的 `task_config.yaml`。如需临时覆盖其中的算法参数,使用 `rl` 作为算法配置根节点。因为 `rl` 初始为空,需要用 `+` 添加路径: ```bash -python scripts/play.py \ +mxplay \ env=cartpole \ '+rl.agent.learning_rate=1e-4' ``` diff --git a/docs/source/zh_CN/user_guide/tutorial/training/training_and_result.md b/docs/source/zh_CN/user_guide/tutorial/training/training_and_result.md index 489a8288..e98dd3ef 100644 --- a/docs/source/zh_CN/user_guide/tutorial/training/training_and_result.md +++ b/docs/source/zh_CN/user_guide/tutorial/training/training_and_result.md @@ -12,16 +12,16 @@ Task 配方只选择 RL provider,不会替你安装 provider。如果 Task 需 ### 选择 Task -训练入口使用 Hydra 的 `task=<环境>/<框架>.<算法>[.<后端>]` 选项。一个 Task 会把环境、RL provider、运行参数和算法超参数组合成一份可复现的训练配方: +训练入口必须显式指定 Hydra 的 `task=<环境>/<框架>.<算法>[.<后端>]` 选项:打包配置使用 `task: null` 占位,使 `--help` 无需选择 Task,但实际训练必须显式选择 Task。一个 Task 会把环境、RL provider、运行参数和算法超参数组合成一份可复现的训练配方: ```bash -# 训练默认的 Cartpole SKRL PPO Task -python scripts/train.py task=cartpole/skrl.ppo +# 显式选择 Cartpole SKRL PPO Task +mxtrain task=cartpole/skrl.ppo # 选择其他框架或算法 -python scripts/train.py task=cartpole/rslrl.ppo -python scripts/train.py task=g1-walk-flat/motrix.fastsac -python scripts/train.py task=g1-walk-flat/motrix.fastsac algo.asynchronous=false +mxtrain task=cartpole/rslrl.ppo +mxtrain task=g1-walk-flat/motrix.fastsac +mxtrain task=g1-walk-flat/motrix.fastsac algo.asynchronous=false ``` 当前内置的 RL method 与训练后端: @@ -32,59 +32,59 @@ python scripts/train.py task=g1-walk-flat/motrix.fastsac algo.asynchronous=false | `rslrl.ppo` | `torch` | RSLRL PPO | | `motrix.fastsac` | `torch` | FastSAC;`algo.asynchronous` 选择同步或异步拓扑 | -运行 `python scripts/train.py --help` 可以查看当前代码中全部可选 Task。Task 文件结构和覆盖规则见 [Task 配置与命令行参数覆盖](task_config.md)。 +请在仓库根目录执行这些命令:CLI 会发现当前工作目录下存在的 `./configs`,不会向上查找父目录。运行 `mxtrain --help` 可以查看当前代码中全部可选 Task。Task 文件结构和覆盖规则见 [Task 配置与命令行参数覆盖](task_config.md)。 ### 选择训练后端与仿真后端 ```bash # 覆盖训练后端(task.train_backend 为 null 时自动选择) -python scripts/train.py task=cartpole/skrl.ppo task.train_backend=jax -python scripts/train.py task=cartpole/skrl.ppo task.train_backend=torch +mxtrain task=cartpole/skrl.ppo task.train_backend=jax +mxtrain task=cartpole/skrl.ppo task.train_backend=torch # 指定 manager 环境注入的仿真器 -python scripts/train.py task=g1-wbt-dance sim=motrixsim +mxtrain task=g1-wbt-dance/motrix.fastsac sim=motrixsim ``` ### 训练规模与随机种子 ```bash # 并行环境数量 -python scripts/train.py task=cartpole/skrl.ppo num_envs=1024 +mxtrain task=cartpole/skrl.ppo num_envs=1024 # 固定随机种子(复现)/ 运行时选择随机种子 -python scripts/train.py task=cartpole/skrl.ppo seed=42 -python scripts/train.py task=cartpole/skrl.ppo seed=null +mxtrain task=cartpole/skrl.ppo seed=42 +mxtrain task=cartpole/skrl.ppo seed=null ``` ```{note} -Hydra 可以直接覆盖已经声明的算法字段,例如 `algo.agent.learning_rate=5e-4`。字段路径取决于所选 Task,完整默认值位于 `configs/algo_base/`。 +Hydra 可以直接覆盖已经声明的算法字段,例如 `algo.agent.learning_rate=5e-4`。字段路径取决于所选 Task,完整默认值位于 `motrix_rl/src/motrix_rl/configs/algo_base/`。 ``` ### 训练后自动回放与续训 ```bash # 训练成功结束后,用本次 run 的最佳策略自动回放 -python scripts/train.py task=g1-walk-flat/motrix.fastsac play=true +mxtrain task=g1-walk-flat/motrix.fastsac play=true # 从某个 run 目录或 checkpoint 续训 -python scripts/train.py task=g1-walk-flat/motrix.fastsac \ +mxtrain task=g1-walk-flat/motrix.fastsac \ resume=/path/to/run # 启用渲染监控训练过程 -python scripts/train.py task=cartpole/skrl.ppo render=true +mxtrain task=cartpole/skrl.ppo render=true ``` -训练和回放是两个独立操作:`play=true` 会在训练成功后回放本次 run 的最佳策略;稍后可使用 `scripts/play.py` 重新打开已保存的 run。使用以下命令导出训练好的策略 artifact: +训练和回放是两个独立操作:`play=true` 会在训练成功后回放本次 run 的最佳策略;稍后可使用 `mxplay` 重新打开已保存的 run。使用以下命令导出训练好的策略 artifact: ```bash -python scripts/export_onnx.py run_dir=/path/to/run output=/tmp/policy.onnx +mxexport onnx run_dir=/path/to/run output=/tmp/policy.onnx ``` ### 常用 Hydra 覆盖项 | 覆盖项 | 说明 | 默认值来源 | | -------------------- | --------------------------------------- | ------------------- | -| `task=...` | 环境、框架、算法和后端差异配置 | `cartpole/skrl.ppo` | +| `task=...` | 环境、框架、算法和后端差异配置 | 训练时必填 | | `task.train_backend` | 训练后端(`jax` / `torch`) | 所选 Task | | `sim` | manager 环境的仿真器;np 环境保持未设置 | `null` / 自动选择 | | `num_envs` | 并行训练环境数量 | 所选 Task | @@ -115,14 +115,14 @@ tensorboard --logdir runs/cartpole ```bash # 自动发现最新 run 的最佳策略并回放(推荐) -python scripts/play.py env=cartpole +mxplay env=cartpole # 指定某个 checkpoint(需能向上找到 metadata.json) -python scripts/play.py env=g1-walk-flat \ +mxplay env=g1-walk-flat \ policy=/path/to/run/checkpoints/latest.pt # 指定回放环境数量 -python scripts/play.py env=cartpole num_envs=100 +mxplay env=cartpole num_envs=100 ``` ```{note} diff --git a/motrix_deploy/README.md b/motrix_deploy/README.md index 856f92b7..889a5991 100644 --- a/motrix_deploy/README.md +++ b/motrix_deploy/README.md @@ -1,13 +1,19 @@ # Motrix Deploy `motrix-deploy` runs exported policies without a training framework. It provides deployment artifacts, -robot/task contracts, shared control execution, runtime plugin discovery, and the `motrix-deploy` CLI. -Install the task and runtime plugins needed for your target. +robot/task contracts, shared control execution, runtime plugin discovery, and the `mxdeploy` CLI. +Install the task and runtime plugins needed for your target. `motrix-deploy` does not depend on `motrix-rl`: +training-run export belongs to `motrix-rl`, which depends on this package, while deployment runs independently. +Create artifacts with `mxexport deploy run_dir= output=artifacts/policy.deploy`; see the +[deployment tutorial](../docs/source/en/user_guide/tutorial/advanced/motrix_deploy.md) for export. +Environment packages provide profile compilers through package registration callbacks in `motrix_deploy.profiles`; +see [adding a profile compiler](../docs/source/en/user_guide/tutorial/advanced/motrix_deploy.md#add-a-deployment-profile-compiler) +for the provider recipe and discovery requirements. With the Go2 task and a simulation backend installed: ```bash -motrix-deploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy +mxdeploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy ``` ## Hardware API diff --git a/motrix_deploy/pyproject.toml b/motrix_deploy/pyproject.toml index 976f7a95..9ff4c2da 100644 --- a/motrix_deploy/pyproject.toml +++ b/motrix_deploy/pyproject.toml @@ -41,7 +41,7 @@ dependencies = [ ] [project.scripts] -motrix-deploy = "motrix_deploy.cli:main" +mxdeploy = "motrix_deploy.cli:main" [project.urls] Homepage = "https://github.com/Motphys/MotrixLab" diff --git a/motrix_deploy/src/motrix_deploy/cli.py b/motrix_deploy/src/motrix_deploy/cli.py index b0e75b5d..974b11f2 100644 --- a/motrix_deploy/src/motrix_deploy/cli.py +++ b/motrix_deploy/src/motrix_deploy/cli.py @@ -119,7 +119,7 @@ def main(argv: Sequence[str] | None = None) -> int | None: """Inspect an artifact or run a Hydra-configured deployment rollout.""" if argv is not None: original_argv = sys.argv - sys.argv = ["motrix-deploy", *argv] + sys.argv = ["mxdeploy", *argv] try: return main() finally: @@ -131,7 +131,7 @@ def main(argv: Sequence[str] | None = None) -> int | None: _hydra_inspect_main() return None if args == ["--version"]: - print(f"motrix-deploy {__version__}") + print(f"mxdeploy {__version__}") return 0 if not args: sys.argv.append("--help") diff --git a/motrix_deploy/src/motrix_deploy/profile.py b/motrix_deploy/src/motrix_deploy/profile.py index 7788015e..032a38c5 100644 --- a/motrix_deploy/src/motrix_deploy/profile.py +++ b/motrix_deploy/src/motrix_deploy/profile.py @@ -5,6 +5,7 @@ from collections.abc import Callable, Mapping from dataclasses import dataclass, field +from importlib import metadata from motrix_deploy.artifact.schema import ControlSpec, TaskSpec from motrix_deploy.contracts import RobotSpec @@ -26,45 +27,83 @@ class DeploymentProfile: ProfileCompiler = Callable[[str], DeploymentProfile] -_PROFILE_COMPILERS: dict[str, ProfileCompiler] = {} +PROFILE_COMPILER_ENTRY_POINT_GROUP = "motrix_deploy.profiles" -def register_profile_compiler(*env_names: str) -> Callable[[ProfileCompiler], ProfileCompiler]: - """Register one profile compiler for one or more environment names.""" - if not env_names: - raise ValueError("At least one environment name is required") - if any(not env_name for env_name in env_names): - raise ValueError("Environment names must be non-empty") - if len(set(env_names)) != len(env_names): - raise ValueError("Environment names must be unique") +_profile_compilers: dict[str, ProfileCompiler] = {} +_profile_compilers_discovered = False +_profile_compilers_discovering = False +_profile_entry_points: tuple[metadata.EntryPoint, ...] | None = None +_completed_profile_entry_points = 0 - def register(compiler: ProfileCompiler) -> ProfileCompiler: - duplicates = set(env_names) & set(_PROFILE_COMPILERS) - if duplicates: - names = ", ".join(sorted(duplicates)) - raise ValueError(f"Deployment profile compiler already registered for: {names}") - for env_name in env_names: - _PROFILE_COMPILERS[env_name] = compiler - return compiler - return register +def register_profile_compiler(env_name: str, compiler: ProfileCompiler) -> None: + """Register a compiler for an exact environment name without discovering providers.""" + if env_name in _profile_compilers: + raise ValueError(f"Deployment profile compiler for environment {env_name!r} is already registered") + if not callable(compiler): + raise TypeError(f"Deployment profile compiler for environment {env_name!r} must be callable") + _profile_compilers[env_name] = compiler + + +def _discover_profile_compilers() -> None: + """Invoke installed package registration callbacks once, on the first query. + + Provider names must be unique. Completed callbacks are skipped on retry if a + later load/callback fails. Failed callbacks retain registration side effects, + so providers own recovery from partial registration. Reentrant queries see + registrations completed so far without starting another discovery pass. + """ + global _profile_compilers_discovered, _profile_compilers_discovering + global _profile_entry_points, _completed_profile_entry_points + if _profile_compilers_discovered or _profile_compilers_discovering: + return + _profile_compilers_discovering = True + try: + if _profile_entry_points is None: + providers = tuple( + sorted( + metadata.entry_points(group=PROFILE_COMPILER_ENTRY_POINT_GROUP), + key=lambda ep: (ep.name, ep.value), + ) + ) + names = set() + for provider in providers: + if provider.name in names: + raise ValueError( + f"Duplicate deployment profile provider name {provider.name!r} in entry point group " + f"{PROFILE_COMPILER_ENTRY_POINT_GROUP!r}" + ) + names.add(provider.name) + _profile_entry_points = providers + for entry_point in _profile_entry_points[_completed_profile_entry_points:]: + register = entry_point.load() + if not callable(register): + raise TypeError(f"Deployment profile entry point {entry_point.name!r} must load a callable") + register() + _completed_profile_entry_points += 1 + _profile_compilers_discovered = True + finally: + _profile_compilers_discovering = False def build_deployment_profile(env_name: str) -> DeploymentProfile: - """Build a deployment profile using the compiler registered for an environment.""" + """Discover package providers, then invoke the compiler for this exact environment.""" + _discover_profile_compilers() try: - compiler = _PROFILE_COMPILERS[env_name] - except KeyError as error: - supported = ", ".join(sorted(_PROFILE_COMPILERS)) or "none" + compiler = _profile_compilers[env_name] + except KeyError: + supported = ", ".join(sorted(_profile_compilers)) or "none" raise ValueError( - f"No deployment profile compiler for environment {env_name!r}; supported environments: {supported}" - ) from error + f"No deployment profile compiler for environment {env_name!r}; registered environments: {supported}" + ) from None return compiler(env_name) def registered_profile_compilers() -> tuple[str, ...]: - """Return environment names with a compiler registered in this process.""" - return tuple(sorted(_PROFILE_COMPILERS)) + """Discover installed package providers and list registered compiler environment names.""" + _discover_profile_compilers() + return tuple(sorted(_profile_compilers)) __all__ = [ diff --git a/motrix_deploy/tests/test_profile.py b/motrix_deploy/tests/test_profile.py index 6ab8307c..25152bf2 100644 --- a/motrix_deploy/tests/test_profile.py +++ b/motrix_deploy/tests/test_profile.py @@ -1,12 +1,15 @@ # Copyright Motphys Technology Co., Ltd. 2025, 2026 # SPDX-License-Identifier: Apache-2.0 -"""Deployment profile compiler registry tests.""" +"""Installed package provider discovery and profile compiler registry contracts.""" +from collections.abc import Callable +from dataclasses import dataclass from typing import cast import pytest +from motrix_deploy import profile as profile_module from motrix_deploy.profile import ( DeploymentProfile, build_deployment_profile, @@ -15,21 +18,242 @@ ) -def test_profile_compiler_is_selected_by_environment_name() -> None: +@dataclass +class FakeEntryPoint: + name: str + callback: object + value: str = "test_provider:register" + load_error: Exception | None = None + loads: int = 0 + + def load(self) -> object: + self.loads += 1 + if self.load_error is not None: + raise self.load_error + return self.callback + + +@pytest.fixture(autouse=True) +def isolated_registry(monkeypatch: pytest.MonkeyPatch) -> None: + # Replace every registry/discovery state field, preserving any preexisting + # process state and preventing installed package providers from leaking in. + monkeypatch.setattr(profile_module, "_profile_compilers", {}) + monkeypatch.setattr(profile_module, "_profile_compilers_discovered", False) + monkeypatch.setattr(profile_module, "_profile_compilers_discovering", False) + monkeypatch.setattr(profile_module, "_profile_entry_points", None) + monkeypatch.setattr(profile_module, "_completed_profile_entry_points", 0) + monkeypatch.setattr(profile_module.metadata, "entry_points", lambda *, group: ()) + + +@pytest.fixture +def installed_providers(monkeypatch: pytest.MonkeyPatch) -> Callable[..., None]: + def install(*entries: FakeEntryPoint) -> None: + def entry_points(*, group: str) -> tuple[FakeEntryPoint, ...]: + assert group == "motrix_deploy.profiles" + return entries + + monkeypatch.setattr(profile_module.metadata, "entry_points", entry_points) + + return install + + +@pytest.mark.parametrize("first_query", ["list", "build"]) +def test_providers_register_multiple_mappings_and_only_selected_compiler_runs(installed_providers, first_query) -> None: expected = cast(DeploymentProfile, object()) + calls: list[str] = [] + callbacks: list[str] = [] - @register_profile_compiler("test-profile-a", "test-profile-b") def compile_profile(env_name: str) -> DeploymentProfile: - assert env_name in {"test-profile-a", "test-profile-b"} + calls.append(env_name) return expected - assert build_deployment_profile("test-profile-a") is expected - assert {"test-profile-a", "test-profile-b"} <= set(registered_profile_compilers()) + def unselected_compiler(env_name: str) -> DeploymentProfile: + pytest.fail(f"Unselected compiler executed for {env_name}") + + def register_first() -> None: + callbacks.append("first") + register_profile_compiler("test-profile", compile_profile) + register_profile_compiler("test-profile-variant", compile_profile) + + def register_other() -> None: + callbacks.append("other") + register_profile_compiler("test-profile-other", unselected_compiler) + + selected = FakeEntryPoint("package-a", register_first) + unrelated = FakeEntryPoint("package-b", register_other) + installed_providers(unrelated, selected) + + if first_query == "list": + assert registered_profile_compilers() == ("test-profile", "test-profile-other", "test-profile-variant") + assert calls == [] + assert build_deployment_profile("test-profile") is expected + assert calls == ["test-profile"] + assert callbacks == ["first", "other"] + assert registered_profile_compilers() == ("test-profile", "test-profile-other", "test-profile-variant") + assert build_deployment_profile("test-profile-variant") is expected + assert calls == ["test-profile", "test-profile-variant"] + assert selected.loads == unrelated.loads == 1 + assert callbacks == ["first", "other"] + + +def test_registration_does_not_discover_providers(installed_providers) -> None: + provider = FakeEntryPoint("not-loaded", None, load_error=ImportError("must stay lazy")) + installed_providers(provider) + + assert register_profile_compiler("manual-profile", lambda name: cast(DeploymentProfile, object())) is None + assert provider.loads == 0 + + +def test_reentrant_queries_see_current_registrations_without_rediscovery(installed_providers) -> None: + expected = cast(DeploymentProfile, object()) + calls: list[str] = [] + + def register() -> None: + calls.append("register") + register_profile_compiler("reentrant-profile", lambda name: expected) + assert registered_profile_compilers() == ("reentrant-profile",) + assert build_deployment_profile("reentrant-profile") is expected + + provider = FakeEntryPoint("reentrant-package", register) + installed_providers(provider) + + assert registered_profile_compilers() == ("reentrant-profile",) + assert registered_profile_compilers() == ("reentrant-profile",) + assert calls == ["register"] + assert provider.loads == 1 + + +@pytest.mark.parametrize("query", [registered_profile_compilers, lambda: build_deployment_profile("unknown")]) +def test_duplicate_provider_names_fail_before_any_provider_load(installed_providers, query) -> None: + unrelated = FakeEntryPoint("a-package", None) + first = FakeEntryPoint("duplicate-package", None, value="first:register") + second = FakeEntryPoint("duplicate-package", None, value="second:register") + installed_providers(unrelated, first, second) + + with pytest.raises(ValueError, match="Duplicate deployment profile provider name 'duplicate-package'"): + query() + + assert unrelated.loads == first.loads == second.loads == 0 + + +def test_duplicate_compiler_registration_does_not_overwrite() -> None: + expected = cast(DeploymentProfile, object()) + + def compiler(name: str) -> DeploymentProfile: + return expected + + register_profile_compiler("duplicate-profile", compiler) + + # Even registering the same callable again is a duplicate. + for duplicate in (compiler, lambda name: cast(DeploymentProfile, object())): + with pytest.raises(ValueError, match="'duplicate-profile' is already registered"): + register_profile_compiler("duplicate-profile", duplicate) + + assert build_deployment_profile("duplicate-profile") is expected + + +def test_duplicate_compilers_from_different_providers_fail(installed_providers) -> None: + def register() -> None: + register_profile_compiler("shared-profile", lambda name: cast(DeploymentProfile, object())) + + installed_providers(FakeEntryPoint("package-a", register), FakeEntryPoint("package-b", register)) + + with pytest.raises(ValueError, match="'shared-profile' is already registered"): + registered_profile_compilers() + + +@pytest.mark.parametrize("names, supported", [((), "none"), (("profile-z", "profile-a"), "profile-a, profile-z")]) +def test_unknown_profile_reports_registered_environments(installed_providers, names, supported) -> None: + def register() -> None: + for name in names: + register_profile_compiler(name, lambda name: cast(DeploymentProfile, object())) + + installed_providers(FakeEntryPoint("package-provider", register)) + + with pytest.raises(ValueError) as raised: + build_deployment_profile("profile") + + assert str(raised.value) == ( + f"No deployment profile compiler for environment 'profile'; registered environments: {supported}" + ) + + +@pytest.mark.parametrize("failure", ["load", "callback"]) +def test_discovery_retries_failed_provider_without_repeating_completed_callbacks(installed_providers, failure) -> None: + error = ImportError("provider dependency missing") + completed_calls: list[str] = [] + failing_calls: list[str] = [] + recovered = False + + def register_completed() -> None: + completed_calls.append("completed") + register_profile_compiler("completed-profile", lambda name: cast(DeploymentProfile, object())) + + def register_retrying() -> None: + failing_calls.append("retrying") + if failure == "callback" and not recovered: + # Match env discovery: failed callback side effects remain visible. + register_profile_compiler("partial-profile", lambda name: cast(DeploymentProfile, object())) + raise error + register_profile_compiler("recovered-profile", lambda name: cast(DeploymentProfile, object())) + + first = FakeEntryPoint("a-completed", register_completed) + second = FakeEntryPoint("b-retrying", register_retrying, load_error=error if failure == "load" else None) + installed_providers(second, first) + + with pytest.raises(ImportError) as raised: + registered_profile_compilers() + assert raised.value is error + assert completed_calls == ["completed"] + assert first.loads == second.loads == 1 + + recovered = True + second.load_error = None + names = registered_profile_compilers() + expected = ("completed-profile", "recovered-profile") + if failure == "callback": + expected = ("completed-profile", "partial-profile", "recovered-profile") + assert names == expected + assert registered_profile_compilers() == expected + assert completed_calls == ["completed"] + assert len(failing_calls) == (2 if failure == "callback" else 1) + assert first.loads == 1 + assert second.loads == 2 + + +def test_compiler_failure_propagates_without_rediscovering_provider(installed_providers) -> None: + error = RuntimeError("invalid training configuration") + + def compile_profile(env_name: str) -> DeploymentProfile: + raise error + + def register() -> None: + register_profile_compiler("broken-profile", compile_profile) + + provider = FakeEntryPoint("package-provider", register) + installed_providers(provider) + + for _ in range(2): + with pytest.raises(RuntimeError) as raised: + build_deployment_profile("broken-profile") + assert raised.value is error + assert provider.loads == 1 + + +def test_provider_entry_point_must_load_callable_and_can_retry(installed_providers) -> None: + provider = FakeEntryPoint("invalid-provider", object()) + installed_providers(provider) + + with pytest.raises(TypeError, match="'invalid-provider' must load a callable"): + registered_profile_compilers() + + provider.callback = lambda: None + assert registered_profile_compilers() == () + assert provider.loads == 2 - with pytest.raises(ValueError, match="already registered"): - register_profile_compiler("test-profile-a")(compile_profile) +def test_registered_compiler_must_be_callable() -> None: + with pytest.raises(TypeError, match="'invalid-profile' must be callable"): + register_profile_compiler("invalid-profile", cast(Callable[[str], DeploymentProfile], object())) -def test_unknown_profile_reports_registered_environments() -> None: - with pytest.raises(ValueError, match="No deployment profile compiler.*supported environments"): - build_deployment_profile("unknown-test-profile") + assert registered_profile_compilers() == () diff --git a/motrix_deploy_motrixsim/README.md b/motrix_deploy_motrixsim/README.md index 2e8bb8ca..f6ba701e 100644 --- a/motrix_deploy_motrixsim/README.md +++ b/motrix_deploy_motrixsim/README.md @@ -22,7 +22,7 @@ Artifact deployment reuses the installed simulation recipes rather than introduc backend-specific task copies: ```bash -motrix-deploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy \ +mxdeploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy \ runtime.backend=motrixsim runtime.viewer=false runtime.realtime=false duration_s=1 ``` diff --git a/motrix_deploy_mujoco/README.md b/motrix_deploy_mujoco/README.md index 1174b00f..00058aca 100644 --- a/motrix_deploy_mujoco/README.md +++ b/motrix_deploy_mujoco/README.md @@ -7,7 +7,7 @@ joint control, and an optional interactive viewer. With `motrix-deploy-tasks` and a matching exported artifact: ```bash -motrix-deploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy +mxdeploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy ``` For programmatic use, see [deploy_to_sim.py](../examples/deploy_to_sim.py). diff --git a/motrix_deploy_mujoco/tests/test_scene_session.py b/motrix_deploy_mujoco/tests/test_scene_session.py index eeaf35c2..67c9864b 100644 --- a/motrix_deploy_mujoco/tests/test_scene_session.py +++ b/motrix_deploy_mujoco/tests/test_scene_session.py @@ -164,7 +164,6 @@ def test_scene_init_pose_preserves_artifact_default_and_clips_servo_limits() -> @pytest.mark.integration def test_environment_scene_is_resolved_by_application_before_backend() -> None: - import motrix_envs # noqa: F401 registers configurations for this application test from motrix_env_core import registry source = registry.make_env_config("go2-walk-rough", mode="play").scene diff --git a/motrix_deploy_tasks/README.md b/motrix_deploy_tasks/README.md index 22413295..b974fc83 100644 --- a/motrix_deploy_tasks/README.md +++ b/motrix_deploy_tasks/README.md @@ -7,7 +7,7 @@ task. It does not provide a simulation or hardware backend. Install a backend plugin and use an artifact exported for the matching task: ```bash -motrix-deploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy +mxdeploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy ``` Use `go2-walk-rough/sim` for the rough scene, `runtime.backend=motrixsim` for native @@ -19,7 +19,7 @@ for export, configuration, and hardware safety procedures. ## G1 WBT dance: MuJoCo / MotrixSim sim2sim -Use the standard **export → artifact → `motrix-deploy`** pipeline. The `g1_wbt/v1` task +Use the standard **export → artifact → `mxdeploy`** pipeline. The `g1_wbt/v1` task embeds the actor's motion references, servo gains, action scales, preparation gains and timing, and hardware-observable termination thresholds in the artifact. Deployment requires neither Torch nor an RL environment; export needs the training provider and task @@ -28,19 +28,19 @@ environment packages, but does **not** train. ```bash source .venv/bin/activate RUN=runs/g1-wbt-dance/motrix/torch/fastsac/ -python scripts/export_deploy.py run="$RUN" \ +mxexport deploy run_dir="$RUN" \ output=artifacts/g1-wbt-dance-software-pd.deploy validation.atol=3e-5 -motrix-deploy task=g1-wbt-dance/sim \ +mxdeploy task=g1-wbt-dance/sim \ artifact=artifacts/g1-wbt-dance-software-pd.deploy ``` Headless validation and artifact inspection: ```bash -motrix-deploy task=g1-wbt-dance/sim \ +mxdeploy task=g1-wbt-dance/sim \ artifact=artifacts/g1-wbt-dance-software-pd.deploy runtime.viewer=false -motrix-deploy inspect artifact=artifacts/g1-wbt-dance-software-pd.deploy +mxdeploy inspect artifact=artifacts/g1-wbt-dance-software-pd.deploy ``` Notes: diff --git a/motrix_deploy_tasks/tests/test_training_parity.py b/motrix_deploy_tasks/tests/test_training_parity.py index 9be7bc33..f82a133f 100644 --- a/motrix_deploy_tasks/tests/test_training_parity.py +++ b/motrix_deploy_tasks/tests/test_training_parity.py @@ -10,12 +10,12 @@ import motrix_deploy_tasks from motrix_deploy.contracts import RobotState +from motrix_deploy.profile import build_deployment_profile from motrix_deploy.runtime import ControlContext from motrix_deploy_tasks.tasks.g1_wbt import G1WbtTaskSpec from motrix_deploy_tasks.tasks.go2_walk import Go2WalkPolicyProcessor, Go2WalkTaskSpec from motrix_env_core import registry from motrix_env_core.input import PlanarVelocityCommand -from motrix_envs.deploy import build_deployment_profile @pytest.mark.integration diff --git a/motrix_deploy_unitree/README.md b/motrix_deploy_unitree/README.md index f0a8739a..5c4a0302 100644 --- a/motrix_deploy_unitree/README.md +++ b/motrix_deploy_unitree/README.md @@ -26,7 +26,7 @@ motrix-deploy-unitree read-lowstate With `motrix-deploy-tasks` and a matching artifact installed: ```bash -motrix-deploy task=go2-walk-flat/hardware artifact=artifacts/go2-walk-flat.deploy \ +mxdeploy task=go2-walk-flat/hardware artifact=artifacts/go2-walk-flat.deploy \ runtime.network_interface= ``` diff --git a/motrix_env_core/README.md b/motrix_env_core/README.md index 7e4d5a29..a146bebe 100644 --- a/motrix_env_core/README.md +++ b/motrix_env_core/README.md @@ -17,8 +17,18 @@ the built-in MotrixLab environments are required. from motrix_env_core import EnvCfg, SceneCfg, configclass, registry ``` -Environment implementation packages register themselves by importing their configuration and -environment modules. The framework does not discover or import any concrete environment package. +## Environment discovery + +Registry queries automatically discover installed environment providers through the +`motrix_env_core.envs` entry-point group. Each package supplies a registration callback that uses the +existing environment decorators; applications need no registration imports. Environment discovery +is independent of training and Hydra Task configuration. + +The first query loads all installed providers, not just the provider for the requested Env ID. +Provider dependency and registration errors are reported to the caller. + +See [Environment registration](../docs/source/en/user_guide/tutorial/building_envs/direct_env.md#environment-registration) +for the package declaration and registration example shared by direct and manager environments. ## Simulator backends diff --git a/motrix_env_core/src/motrix_env_core/registry.py b/motrix_env_core/src/motrix_env_core/registry.py index 0ed50c63..aec3f7fa 100644 --- a/motrix_env_core/src/motrix_env_core/registry.py +++ b/motrix_env_core/src/motrix_env_core/registry.py @@ -90,7 +90,49 @@ def _construct_env( return env_cls(env_cfg, num_envs=num_envs) +ENV_ENTRY_POINT_GROUP = "motrix_env_core.envs" + _envs: dict[str, EnvMeta] = {} +_envs_discovered = False +_envs_discovering = False +_env_entry_points: tuple[EntryPoint, ...] | None = None +_completed_env_entry_points = 0 + + +def _discover_envs() -> None: + """Invoke installed environment registration callbacks on the first query. + + Provider names must be unique. Completed callbacks are not repeated if a + later load/callback fails; failures propagate and retry on the next query. + Failed callbacks retain any registration side effects, so providers own + recovery from partial registration. Reentrant queries see registrations + completed so far without starting another discovery pass. + """ + global _envs_discovered, _envs_discovering, _env_entry_points, _completed_env_entry_points + if _envs_discovered or _envs_discovering: + return + _envs_discovering = True + try: + if _env_entry_points is None: + providers = tuple(sorted(entry_points(group=ENV_ENTRY_POINT_GROUP), key=lambda ep: (ep.name, ep.value))) + names = set() + for provider in providers: + if provider.name in names: + raise ValueError( + f"Duplicate environment provider name {provider.name!r} in entry point group " + f"{ENV_ENTRY_POINT_GROUP!r}" + ) + names.add(provider.name) + _env_entry_points = providers + for entry_point in _env_entry_points[_completed_env_entry_points:]: + register = entry_point.load() + if not callable(register): + raise TypeError(f"Environment entry point {entry_point.name!r} must load a callable") + register() + _completed_env_entry_points += 1 + _envs_discovered = True + finally: + _envs_discovering = False @dataclass @@ -137,6 +179,7 @@ def _discover_robots() -> None: def _get_env_meta(name: str) -> EnvMeta: + _discover_envs() try: return _envs[name] except KeyError: @@ -152,7 +195,8 @@ def _resolve_backend(name: str, meta: EnvMeta) -> tuple[str, type[ABEnv]]: def contains(name: str) -> bool: - """Check if an environment configuration is registered.""" + """Discover installed environment plugins and check for a registered config.""" + _discover_envs() return name in _envs @@ -408,7 +452,8 @@ def make( def list_registered_envs() -> dict[str, dict[str, Any]]: - """List registered environments with backends and localized docstring summaries.""" + """Discover plugins and list environments with backends and localized summaries.""" + _discover_envs() result = {} for name, meta in _envs.items(): result[name] = { diff --git a/motrix_env_core/tests/test_registry.py b/motrix_env_core/tests/test_registry.py index 9f4a048d..8ba097eb 100644 --- a/motrix_env_core/tests/test_registry.py +++ b/motrix_env_core/tests/test_registry.py @@ -4,6 +4,7 @@ import subprocess import sys from collections.abc import Mapping +from dataclasses import dataclass import gymnasium as gym import numpy as np @@ -95,7 +96,12 @@ def _restore_global_registry(monkeypatch): registry. Without cleanup they leak into later tests (e.g. the robot-docs suite asserts the registered robot set matches its metadata) and into whole-repo runs. """ - # Core unit tests must not depend on installed robot distributions. + # Core unit tests must not discover installed environment/robot distributions. + monkeypatch.setattr(registry, "_envs", {}) + monkeypatch.setattr(registry, "_envs_discovered", False) + monkeypatch.setattr(registry, "_envs_discovering", False) + monkeypatch.setattr(registry, "_env_entry_points", None) + monkeypatch.setattr(registry, "_completed_env_entry_points", 0) monkeypatch.setattr(registry, "entry_points", lambda *, group: ()) monkeypatch.setattr(registry, "_robots", {}) monkeypatch.setattr(registry, "_robots_discovered", False) @@ -261,3 +267,167 @@ def reset(self, env_ids: np.ndarray) -> None: assert isinstance(env, DirectSimEnv) assert env.num_envs == 2 + + +@dataclass +class _EnvEntryPoint: + name: str + value: str + callback: object + loads: int = 0 + load_error: Exception | None = None + + def load(self): + self.loads += 1 + if self.load_error is not None: + raise self.load_error + return self.callback + + +@configclass +class _PluginEnvCfg(DirectEnvCfg): + scene: SceneCfg = SceneCfg() + + +class _PluginEnv(DirectEnv[_PluginEnvCfg]): + pass + + +def _register_plugin_env(name): + registry.envcfg(name)(_PluginEnvCfg) + registry.env(name)(_PluginEnv) + + +@pytest.mark.parametrize("query", ["contains", "config", "resolve", "make", "list"]) +def test_env_plugins_are_lazy_sorted_once_at_every_query_boundary(monkeypatch, query): + calls = [] + metadata_queries = [] + + def register(name): + calls.append(name) + _register_plugin_env(name) + + a = _EnvEntryPoint("a", "external_a:register", lambda: register("plugin-a")) + b = _EnvEntryPoint("b", "external_b:register", lambda: register("plugin-b")) + + def entry_points(*, group): + metadata_queries.append(group) + return [b, a] + + monkeypatch.setattr(registry, "entry_points", entry_points) + monkeypatch.setattr(registry, "_construct_env", lambda cls, cfg, *args: (cls, cfg)) + _register_plugin_env("manual") + assert metadata_queries == [] + if query == "contains": + assert registry.contains("plugin-a") + elif query == "config": + assert isinstance(registry.make_env_config("plugin-a"), _PluginEnvCfg) + elif query == "resolve": + assert registry.resolve("plugin-a", env_cfg=_PluginEnvCfg()).env_cls is _PluginEnv + elif query == "make": + cls, cfg = registry.make("plugin-a") + assert cls is _PluginEnv + assert isinstance(cfg, _PluginEnvCfg) + else: + assert "plugin-a" in registry.list_registered_envs() + first = registry.make_env_config("plugin-a") + assert first is not registry.make_env_config("plugin-a") + assert registry.resolve("manual").env_cls is _PluginEnv + assert set(registry.list_registered_envs()) == {"manual", "plugin-a", "plugin-b"} + assert registry.contains("manual") + assert metadata_queries == [registry.ENV_ENTRY_POINT_GROUP] + assert calls == ["plugin-a", "plugin-b"] + assert a.loads == b.loads == 1 + + +def test_env_plugin_registration_can_reenter_queries_and_decorators(monkeypatch): + def register(): + assert registry.list_registered_envs() == {} + assert not registry.contains("recursive") + _register_plugin_env("recursive") + assert registry.contains("recursive") + assert isinstance(registry.make_env_config("recursive"), _PluginEnvCfg) + assert registry.resolve("recursive").env_cls is _PluginEnv + assert "recursive" in registry.list_registered_envs() + + ep = _EnvEntryPoint("recursive", "external:register", register) + monkeypatch.setattr(registry, "entry_points", lambda *, group: [ep]) + assert registry.contains("recursive") + assert ep.loads == 1 + + +@pytest.mark.parametrize("failure", ["metadata", "load", "noncallable", "callback"]) +def test_env_plugin_errors_propagate_and_retry_even_for_manual_queries(monkeypatch, failure): + error = RuntimeError("external provider failed") + _register_plugin_env("manual") + + def fail(): + raise error + + ep = _EnvEntryPoint("broken", "external:register", fail) + if failure == "load": + ep.load_error = error + elif failure == "noncallable": + ep.callback = object() + + def entry_points(*, group): + if failure == "metadata": + raise error + return [ep] + + monkeypatch.setattr(registry, "entry_points", entry_points) + for _ in range(2): + if failure == "noncallable": + with pytest.raises(TypeError, match="Environment entry point 'broken' must load a callable"): + registry.contains("manual") + else: + with pytest.raises(RuntimeError) as caught: + registry.contains("manual") + assert caught.value is error + if failure != "metadata": + assert ep.loads == 2 + ep.load_error = None + ep.callback = lambda: _register_plugin_env("recovered") + monkeypatch.setattr(registry, "entry_points", lambda *, group: [ep]) + assert registry.contains("recovered") + assert registry.contains("manual") + + +def test_env_plugin_retry_does_not_repeat_completed_providers(monkeypatch): + def fail_after_registering(): + _register_plugin_env("partial") + raise RuntimeError("retry provider") + + first = _EnvEntryPoint("a", "external_a:register", lambda: _register_plugin_env("first")) + second = _EnvEntryPoint("b", "external_b:register", fail_after_registering) + monkeypatch.setattr(registry, "entry_points", lambda *, group: [first, second]) + with pytest.raises(RuntimeError, match="retry provider"): + registry.list_registered_envs() + # Side effects are retained, not rolled back; providers own partial recovery. + with pytest.raises(ValueError, match="Environment 'partial' is already registered"): + registry.list_registered_envs() + second.callback = lambda: _register_plugin_env("second") + assert set(registry.list_registered_envs()) == {"first", "partial", "second"} + assert first.loads == 1 + assert second.loads == 3 + + +@pytest.mark.parametrize("second_value", ["external_a:register", "external_b:register"]) +def test_env_duplicate_provider_names_fail_before_loading(monkeypatch, second_value): + first = _EnvEntryPoint("duplicate", "external_a:register", lambda: None) + second = _EnvEntryPoint("duplicate", second_value, lambda: None) + monkeypatch.setattr(registry, "entry_points", lambda *, group: [first, second]) + for _ in range(2): + with pytest.raises(ValueError, match="Duplicate environment provider name 'duplicate'"): + registry.list_registered_envs() + assert first.loads == second.loads == 0 + + +def test_env_plugin_registration_conflicts_are_not_silently_skipped(monkeypatch): + _register_plugin_env("manual") + ep = _EnvEntryPoint("external", "external:register", lambda: _register_plugin_env("manual")) + monkeypatch.setattr(registry, "entry_points", lambda *, group: [ep]) + with pytest.raises(ValueError, match="Environment 'manual' is already registered"): + registry.make_env_config("manual") + assert registry._envs["manual"].env_cfg_cls is _PluginEnvCfg + assert registry._envs["manual"].env_cls_dict == {"np": _PluginEnv} diff --git a/motrix_env_motrixsim/tests/test_motrixsim_backend.py b/motrix_env_motrixsim/tests/test_motrixsim_backend.py index f7f085fc..e569c59d 100644 --- a/motrix_env_motrixsim/tests/test_motrixsim_backend.py +++ b/motrix_env_motrixsim/tests/test_motrixsim_backend.py @@ -104,7 +104,6 @@ def test_motrixsim_runtime_reset_restores_default_state(): def test_body_joint_position_limits_follow_body_joint_dof_order(): - import motrix_envs # noqa: F401 from motrix_env_core import registry cfg = registry.make_env_config("g1-wbt-dance", mode="play") @@ -124,7 +123,6 @@ def test_body_joint_position_limits_follow_body_joint_dof_order(): def test_bodies_bake_init_snapshot(): - import motrix_envs # noqa: F401 from motrix_env_core import registry cfg = registry.make_env_config("g1-wbt-dance", mode="play") @@ -162,7 +160,6 @@ def test_bodies_stay_empty_for_scene_without_body_objs(): def test_dof_position_limits_follow_global_dof_position_order(): - import motrix_envs # noqa: F401 from motrix_env_core import registry cfg = registry.make_env_config("dm-humanoid-walk", mode="play") @@ -181,7 +178,6 @@ def test_dof_position_limits_follow_global_dof_position_order(): def test_geom_specs_include_only_declared_names_in_order(): - import motrix_envs # noqa: F401 from motrix_env_core import registry cfg = registry.make_env_config("dm-finger-turn-easy", mode="play") @@ -197,7 +193,6 @@ def test_geom_specs_include_only_declared_names_in_order(): def test_actuator_params_support_declared_names_or_full_model_order(): - import motrix_envs # noqa: F401 from motrix_env_core import registry cfg = registry.make_env_config("go2-walk-flat", mode="play") @@ -231,7 +226,6 @@ def test_actuator_params_support_declared_names_or_full_model_order(): def test_named_joint_queries_follow_declared_order(): - import motrix_envs # noqa: F401 from motrix_env_core import registry cfg = registry.make_env_config("dm-finger-spin", mode="play") @@ -253,7 +247,6 @@ def test_named_joint_queries_follow_declared_order(): def test_named_joint_reset_follows_declared_order_and_partial_selection(): - import motrix_envs # noqa: F401 from motrix_env_core import registry cfg = registry.make_env_config("dm-finger-spin", mode="play") @@ -280,7 +273,6 @@ def test_named_joint_reset_follows_declared_order_and_partial_selection(): def test_full_dof_reset_preserves_unsorted_environment_id_values(): - import motrix_envs # noqa: F401 from motrix_env_core import registry cfg = registry.make_env_config("dm-finger-spin", mode="play") @@ -303,7 +295,6 @@ def test_full_dof_reset_preserves_unsorted_environment_id_values(): def test_body_dof_reset_matches_body_query_layout(): - import motrix_envs # noqa: F401 from motrix_env_core import registry cfg = registry.make_env_config("g1-wbt-dance", mode="play") @@ -331,7 +322,6 @@ def test_body_dof_reset_matches_body_query_layout(): def test_reset_compilation_rejects_conflicting_targets(): - import motrix_envs # noqa: F401 from motrix_env_core import registry cfg = registry.make_env_config("dm-finger-spin", mode="play") @@ -347,7 +337,6 @@ def test_reset_compilation_rejects_conflicting_targets(): def test_body_state_reset_is_visible_to_link_queries(): - import motrix_envs # noqa: F401 from motrix_env_core import registry cfg = registry.make_env_config("dm-humanoid-walk", mode="play") @@ -387,7 +376,6 @@ def test_body_state_reset_is_visible_to_link_queries(): def test_named_joint_queries_reject_unknown_joints(): - import motrix_envs # noqa: F401 from motrix_env_core import registry finger_cfg = registry.make_env_config("dm-finger-spin", mode="play") @@ -421,7 +409,6 @@ def test_sim_cfg_preserves_unspecified_solver_options(): def test_ctrl_targets_write_routes_named_actuators_through_native_plan(): - import motrix_envs # noqa: F401 from motrix_env_core import registry cfg = registry.make_env_config("go2-walk-flat", mode="play") @@ -445,7 +432,6 @@ def test_ctrl_targets_write_routes_named_actuators_through_native_plan(): def test_partial_env_ids_leave_other_rows_untouched_with_native_writes(): - import motrix_envs # noqa: F401 from motrix_env_core import registry cfg = registry.make_env_config("dm-humanoid-walk", mode="play") @@ -470,7 +456,6 @@ def test_partial_env_ids_leave_other_rows_untouched_with_native_writes(): def test_ctrl_targets_reject_duplicated_actuators_with_stable_message(): - import motrix_envs # noqa: F401 from motrix_env_core import registry cfg = registry.make_env_config("go2-walk-flat", mode="play") @@ -486,7 +471,6 @@ def test_ctrl_targets_reject_duplicated_actuators_with_stable_message(): def test_mixed_dof_and_native_writes_execute_in_one_program(): - import motrix_envs # noqa: F401 from motrix_env_core import registry cfg = registry.make_env_config("dm-humanoid-walk", mode="play") diff --git a/motrix_envs/README.md b/motrix_envs/README.md index 15dbfdf9..55293837 100644 --- a/motrix_envs/README.md +++ b/motrix_envs/README.md @@ -3,10 +3,11 @@ `motrix-envs` contains the built-in MotrixLab reinforcement learning environments and their robot models, meshes, textures, and motion data. It depends on the standalone `motrix-env-core` framework package. -Importing `motrix_envs` registers all built-in environments in the core registry: +Install the package to make built-in environments available through automatic core registry discovery; +no registration import is needed: ```python -from motrix_envs import registry +from motrix_env_core import registry env = registry.make("cartpole", num_envs=1) ``` @@ -14,15 +15,24 @@ env = registry.make("cartpole", num_envs=1) The package includes the `basic`, `locomotion`, and `manipulation` environment families. Environment names used by training and playback commands remain unchanged. -Install the `deploy` extra and import `motrix_envs.deploy` to register the deployment profile compilers owned by the -built-in environments: +Install the `deploy` extra to use the built-in deployment profile compilers with `mxexport deploy`: -```python -from motrix_envs.deploy import build_deployment_profile +```bash +mxexport deploy env=go2-walk-rough output=artifacts/go2-walk-rough.deploy +``` -profile = build_deployment_profile("go2-walk-rough") +The installed package publishes one provider for its built-in profile compilers: + +```toml +[project.entry-points."motrix_deploy.profiles"] +builtin = "motrix_envs.deploy:register" ``` +Its callback registers profiles for `go2-walk-flat`, `go2-walk-rough`, and `g1-wbt-dance`. +No registration import is needed for profile discovery. See the +[deployment tutorial](../docs/source/en/user_guide/tutorial/advanced/motrix_deploy.md#add-a-deployment-profile-compiler) +for the plugin contract and an external-package example. + ## Framework API `motrix_envs.core` mirrors the complete public API exported by `motrix_env_core`: @@ -42,5 +52,15 @@ environment paths under `motrix_envs.basic`, `motrix_envs.locomotion`, and `motr ## Registration -The framework never imports or discovers concrete environment packages. Each implementation package owns its -registration side effects. For the built-in environments this happens when `motrix_envs` is imported. +The installed package exposes one callback for all built-in environments: + +```toml +[project.entry-points."motrix_env_core.envs"] +builtin = "motrix_envs:register" +``` + +The callback includes the environments under `basic`, `locomotion`, and `manipulation`. +Training/Hydra Task configs belong to the consuming project, not this package. See +[core discovery](../motrix_env_core/README.md#environment-discovery) for the shared discovery behavior and +[Environment registration](../docs/source/en/user_guide/tutorial/building_envs/direct_env.md#environment-registration) +for publishing an external environment package. diff --git a/motrix_envs/pyproject.toml b/motrix_envs/pyproject.toml index 5a63720d..5dea6ee0 100644 --- a/motrix_envs/pyproject.toml +++ b/motrix_envs/pyproject.toml @@ -19,5 +19,11 @@ dependencies = [ "omegaconf>=2.3,<2.4", ] +[project.entry-points."motrix_env_core.envs"] +builtin = "motrix_envs:register" + +[project.entry-points."motrix_deploy.profiles"] +builtin = "motrix_envs.deploy:register" + [project.optional-dependencies] deploy = ["motrix-deploy==0.4.0b1", "motrix-deploy-tasks==0.4.0b1"] diff --git a/motrix_envs/src/motrix_envs/__init__.py b/motrix_envs/src/motrix_envs/__init__.py index 3deeefd6..bfc1f2c2 100644 --- a/motrix_envs/src/motrix_envs/__init__.py +++ b/motrix_envs/src/motrix_envs/__init__.py @@ -20,6 +20,4 @@ def register() -> None: raise -register() - __all__ = ["core", "register", "registry"] diff --git a/motrix_envs/src/motrix_envs/deploy/__init__.py b/motrix_envs/src/motrix_envs/deploy/__init__.py index 9816b89b..f891b098 100644 --- a/motrix_envs/src/motrix_envs/deploy/__init__.py +++ b/motrix_envs/src/motrix_envs/deploy/__init__.py @@ -1,11 +1,15 @@ # Copyright Motphys Technology Co., Ltd. 2025, 2026 # SPDX-License-Identifier: Apache-2.0 -"""Deployment profile compilers for built-in MotrixLab environments.""" +"""Deployment profile provider for built-in environments.""" -# g1_wbt is imported for registration, not as a second public API facade. -from motrix_deploy.profile import build_deployment_profile, registered_profile_compilers -from motrix_envs.deploy import g1_wbt as _g1_wbt -from motrix_envs.deploy.go2_walk import build_go2_walk_profile -__all__ = ["build_deployment_profile", "build_go2_walk_profile", "registered_profile_compilers"] +def register() -> None: + """Register the built-in environment-to-profile compiler mappings.""" + from motrix_deploy.profile import register_profile_compiler + from motrix_envs.deploy.g1_wbt import build_g1_wbt_profile + from motrix_envs.deploy.go2_walk import build_go2_walk_profile + + register_profile_compiler("go2-walk-flat", build_go2_walk_profile) + register_profile_compiler("go2-walk-rough", build_go2_walk_profile) + register_profile_compiler("g1-wbt-dance", build_g1_wbt_profile) diff --git a/motrix_envs/src/motrix_envs/deploy/g1_wbt.py b/motrix_envs/src/motrix_envs/deploy/g1_wbt.py index 11459219..fea45c69 100644 --- a/motrix_envs/src/motrix_envs/deploy/g1_wbt.py +++ b/motrix_envs/src/motrix_envs/deploy/g1_wbt.py @@ -8,7 +8,7 @@ import numpy as np from motrix_deploy.artifact.schema import ControlSpec -from motrix_deploy.profile import DeploymentProfile, register_profile_compiler +from motrix_deploy.profile import DeploymentProfile from motrix_deploy_tasks.tasks.g1_wbt import G1WbtMotion, G1WbtTaskSpec from motrix_env_core import registry from motrix_envs.deploy.robot import build_robot_model, build_robot_spec, read_position_servo_gains @@ -17,7 +17,6 @@ from motrix_envs.motion.loader import MotrixMotion -@register_profile_compiler("g1-wbt-dance") def build_g1_wbt_profile(env_name: str) -> DeploymentProfile: """Embed actor reference channels and hardware-observable safety thresholds.""" cfg = registry.make_env_config(env_name) diff --git a/motrix_envs/src/motrix_envs/deploy/go2_walk.py b/motrix_envs/src/motrix_envs/deploy/go2_walk.py index 8110273f..b77aebc8 100644 --- a/motrix_envs/src/motrix_envs/deploy/go2_walk.py +++ b/motrix_envs/src/motrix_envs/deploy/go2_walk.py @@ -7,7 +7,7 @@ from motrix_robots.unitree import UnitreeGo2Robot from motrix_deploy.artifact import ControlSpec -from motrix_deploy.profile import DeploymentProfile, register_profile_compiler +from motrix_deploy.profile import DeploymentProfile from motrix_deploy_tasks.tasks.go2_walk import Go2WalkTaskSpec from motrix_env_core import registry from motrix_envs.deploy.robot import build_robot_model, build_robot_spec, read_position_servo_gains @@ -19,8 +19,6 @@ } -@register_profile_compiler("go2-walk-flat") -@register_profile_compiler("go2-walk-rough") def build_go2_walk_profile(env_name: str) -> DeploymentProfile: """Compile one registered Go2 walking environment into a deployment profile.""" env = registry.make(env_name, num_envs=1, mode="train") diff --git a/motrix_envs/tests/test_basic_direct_contract.py b/motrix_envs/tests/test_basic_direct_contract.py index 9f771ddb..37d2039a 100644 --- a/motrix_envs/tests/test_basic_direct_contract.py +++ b/motrix_envs/tests/test_basic_direct_contract.py @@ -18,7 +18,6 @@ import numpy as np import pytest -import motrix_envs # noqa: F401 registers built-in environments from motrix_env_core import registry from motrix_env_core.array.env import ArrayEnvState, NpObs diff --git a/motrix_envs/tests/test_basic_env_lifecycle.py b/motrix_envs/tests/test_basic_env_lifecycle.py index 7ddd4e8a..c489383b 100644 --- a/motrix_envs/tests/test_basic_env_lifecycle.py +++ b/motrix_envs/tests/test_basic_env_lifecycle.py @@ -4,7 +4,6 @@ import numpy as np import pytest -import motrix_envs # noqa: F401 registers built-in environments from motrix_env_core import registry from motrix_env_core.array.env import NpObs diff --git a/motrix_envs/tests/test_deploy_profile.py b/motrix_envs/tests/test_deploy_profile.py index 973b7783..f33eb29b 100644 --- a/motrix_envs/tests/test_deploy_profile.py +++ b/motrix_envs/tests/test_deploy_profile.py @@ -9,9 +9,9 @@ import pytest from motrix_robots.unitree import UnitreeGo2Robot +from motrix_deploy.profile import build_deployment_profile, registered_profile_compilers from motrix_deploy_tasks.tasks.go2_walk import Go2WalkTaskSpec from motrix_env_core import registry -from motrix_envs.deploy import build_deployment_profile, registered_profile_compilers from motrix_envs.deploy.robot import ( build_robot_model, build_robot_spec, diff --git a/motrix_envs/tests/test_manipulation_direct_contract.py b/motrix_envs/tests/test_manipulation_direct_contract.py index a1b427f8..78b8a0e7 100644 --- a/motrix_envs/tests/test_manipulation_direct_contract.py +++ b/motrix_envs/tests/test_manipulation_direct_contract.py @@ -18,7 +18,6 @@ import numpy as np import pytest -import motrix_envs # noqa: F401 registers built-in environments from motrix_env_core import registry from motrix_env_core.array.env import ArrayEnvState, NpObs diff --git a/motrix_envs/tests/test_package_boundary.py b/motrix_envs/tests/test_package_boundary.py index be4481e4..3a4e56c5 100644 --- a/motrix_envs/tests/test_package_boundary.py +++ b/motrix_envs/tests/test_package_boundary.py @@ -15,7 +15,7 @@ from motrix_envs.locomotion.wbt.k1 import K1WbtEnvCfg -def test_importing_builtins_registers_environments_in_core_registry(): +def test_core_registry_discovers_builtin_environments(): assert registry.contains("cartpole") assert registry.contains("g1-wbt-dance") diff --git a/motrix_envs/tests/test_quadruped_walk.py b/motrix_envs/tests/test_quadruped_walk.py index 85c8f2d6..86099d94 100644 --- a/motrix_envs/tests/test_quadruped_walk.py +++ b/motrix_envs/tests/test_quadruped_walk.py @@ -4,7 +4,6 @@ import numpy as np import pytest -import motrix_envs # noqa: F401 registers built-in environments from motrix_env_core import registry from motrix_env_core.config import configclass from motrix_env_core.config.scene import ( diff --git a/motrix_envs/tests/test_walk_randomization.py b/motrix_envs/tests/test_walk_randomization.py index a6f05642..8892e7fb 100644 --- a/motrix_envs/tests/test_walk_randomization.py +++ b/motrix_envs/tests/test_walk_randomization.py @@ -10,7 +10,6 @@ import numpy as np import pytest -import motrix_envs # noqa: F401 registers built-in environments from motrix_env_core.manager import ManagerEnv from motrix_env_core.mdp.action import JointPositionActionCfg, JointPositionActionState, JointPositionActionTerm from motrix_envs.locomotion.humanoid.g1 import make_g129dof_walk_flat_cfg diff --git a/motrix_envs/tests/test_wbt_numba.py b/motrix_envs/tests/test_wbt_numba.py index 5207ef2c..d658e3b0 100644 --- a/motrix_envs/tests/test_wbt_numba.py +++ b/motrix_envs/tests/test_wbt_numba.py @@ -9,7 +9,6 @@ import numpy as np import pytest -import motrix_envs # noqa: E402, F401 from motrix_env_core import registry # noqa: E402 from motrix_env_core.manager import ( # noqa: E402 ManagerContext, diff --git a/motrix_rl/README.md b/motrix_rl/README.md index c658e6dd..e2a4b626 100644 --- a/motrix_rl/README.md +++ b/motrix_rl/README.md @@ -67,22 +67,106 @@ through forwarding extras; do not use its exported requirements to install these [PyTorch wheel index](https://pytorch.org/get-started/locally/) in the consuming installer/project explicitly. In particular, a `rocm` extra's Torch version alone does not guarantee a ROCm build. GPU selection is explicit, not hardware auto-detection. -## Workspace training +## Usage -Environment-specific training presets live under `configs/task/`, while shared provider defaults live under -`configs/algo_base/`. From the workspace root, select a preset with Hydra's `task=/` syntax (run from the -activated workspace environment, `source .venv/bin/activate`): +Installing `motrix-rl` provides `mxtrain`, `mxplay`, and `mxexport`, backed by `motrix_rl.commands.train`, +`motrix_rl.commands.play`, and the `motrix_rl.commands.export` dispatcher. `mxexport` requires an explicit +`onnx` or `deploy` subcommand; there is no default subcommand. Their primary Hydra configurations ship in +`motrix_rl.configs`; the commands do not require a repository checkout or a particular working directory. +For training and playback, install an environment package and the selected provider too. +For the built-in environments, activate the installed workspace environment (`source .venv/bin/activate`) and run from +the workspace root: ```bash -python scripts/train.py task=cartpole/skrl.ppo -python scripts/train.py task=cartpole/rslrl.ppo -python scripts/train.py task=g1-walk-rough/motrix.fastsac +mxtrain task=cartpole/skrl.ppo +mxtrain task=cartpole/rslrl.ppo +mxtrain task=g1-walk-rough/motrix.fastsac +mxplay env=cartpole +mxplay env=cartpole policy=/path/to/run +mxexport onnx run_dir=/path/to/run output=artifacts/policy.onnx +mxexport deploy run_dir=/path/to/run output=artifacts/policy.deploy +mxexport deploy env=go2-walk-flat output=artifacts/go2-walk-flat.deploy ``` -Install the extra required by the selected provider before training: +`mxexport onnx` exports a saved run's best policy without selecting a `task` or creating an environment. Install the provider used by +that run; see the [ONNX export tutorial](../docs/source/en/user_guide/tutorial/advanced/export_onnx.md) for supported +configurations. `mxexport deploy` creates a self-contained deployment artifact; specify exactly one of `run_dir` or +`env` (the latter selects the latest metadata-backed run). Its numerical checks use `validation.*`, while ONNX export +uses `parity.*`. The `export_onnx.yaml` and `export_deploy.yaml` configurations ship in `motrix_rl.configs` and follow +the same project-first discovery described below, falling back to packaged files when the project does not provide them. + +Deployment profile compilers come from installed environment plugins, without hardcoded environment imports in the CLI. +See the [deployment tutorial](../docs/source/en/user_guide/tutorial/advanced/motrix_deploy.md#add-a-deployment-profile-compiler) +for the plugin contract. `motrix-rl` depends on `motrix-deploy` for artifact creation; `motrix-deploy` does not depend on RL. + +The equivalent module commands are `python -m motrix_rl.commands.train`, `python -m motrix_rl.commands.play`, and +`python -m motrix_rl.commands.export onnx` / `python -m motrix_rl.commands.export deploy`. The subcommands are implemented +in `motrix_rl.commands.export_onnx` and `motrix_rl.commands.export_deploy`; the repository's export scripts are thin +wrappers around those commands. In an already installed uv environment, use +`uv run --no-sync mxtrain ...` (or `mxplay` / `mxexport`) to avoid re-resolving GPU profiles. +Run artifacts are written under `runs/` relative to the working directory by default. + +Primary train/play configurations and shared provider defaults ship in `motrix_rl.configs`, with sources under +`motrix_rl/src/motrix_rl/configs/` (`algo_base/` for defaults). Concrete task presets belong to the consuming project: +the workspace keeps its built-in demos under root `configs/task/`, discovered through the current directory's +`./configs`, just like an external project. The packaged train configuration uses a `task: null` placeholder so bare +`--help` works without choosing a default environment. Actual training requires an explicit `task=...` selection; +mandatory `TrainConfig` fields reject execution without a task. Playback uses saved run configuration and needs no task +config directory. Environment packages do not own training configuration, and there is no built-in Hydra search-path plugin. + +Hydra configuration discovery and runtime environment discovery are separate. Core registry queries automatically load +installed environment providers from `motrix_env_core.envs`; train/play need no registration-module configuration or +side-effect imports. Environment packages remain independent of training/Hydra Task configs. See +[core discovery](../motrix_env_core/README.md#environment-discovery) for callback and dependency-error semantics. + +In the workspace, install the extra required by the selected provider before training: ```bash sh install.sh --skrl-jax # JAX (Linux only) sh install.sh --skrl-torch # PyTorch (default) sh install.sh --rslrl # RSLRL (PyTorch only) ``` + +## External project integration + +The CLI discovers `./configs` in the current working directory when it exists. It does not search parent directories, +and a missing default directory is ignored. An explicit `--config-dir ` overrides this default directory. +The selected project directory takes precedence over packaged defaults, including same-name `algo_base` configurations; +files not provided by the project fall back to the package. Hydra selects the first matching file, rather than merging +same-name files from multiple directories: + +```bash +mxtrain task=my-task/skrl.ppo +mxtrain --config-dir /path/to/project/configs task=my-task/skrl.ppo +``` + +Install the external environment package; train and play discover its registered environments automatically. +Playback uses the same discovery: `mxplay env=my-task`. See +[Environment registration](../docs/source/en/user_guide/tutorial/building_envs/direct_env.md#environment-registration) +for packaging an environment. + +An installed consuming project can expose packaged training presets through a Hydra `SearchPathPlugin` without coupling configuration +discovery to environment registration. For example, put the following in `src/hydra_plugins/my_project_searchpath.py`, +with task YAML files in `src/my_project/configs/task/`: + +```python +from hydra.core.config_search_path import ConfigSearchPath +from hydra.plugins.search_path_plugin import SearchPathPlugin + + +class MyProjectSearchPathPlugin(SearchPathPlugin): + def manipulate_search_path(self, search_path: ConfigSearchPath) -> None: + if not any( + item.provider == "main" and item.path == "pkg://motrix_rl.configs" + for item in search_path.get_path() + ): + return + search_path.append(provider="my_project", path="pkg://my_project.configs") +``` + +Make `my_project.configs` an importable package (include `__init__.py`) and include its YAML resources in the wheel. +Package both `my_project` and `hydra_plugins/my_project_searchpath.py` in the installed distribution. Keep `hydra_plugins` +a shared namespace package: **do not add `hydra_plugins/__init__.py`**. Hydra discovers installed plugin modules in that +namespace; no custom entry point or working-directory import convention is needed. Once installed, run +`mxtrain task=my-task/skrl.ppo` from any directory. Environment callbacks only register environments; they do not +install Hydra search paths. RL provider plugins still register separately through `motrix_rl.frameworks`. diff --git a/motrix_rl/pyproject.toml b/motrix_rl/pyproject.toml index 32118d8c..16941634 100644 --- a/motrix_rl/pyproject.toml +++ b/motrix_rl/pyproject.toml @@ -46,6 +46,11 @@ dependencies = [ "torch>=2.7,<3", ] +[project.scripts] +mxtrain = "motrix_rl.commands.train:main" +mxplay = "motrix_rl.commands.play:main" +mxexport = "motrix_rl.commands.export:main" + [project.optional-dependencies] cuda = [ "torch==2.7.0; sys_platform == 'linux' or sys_platform == 'win32'", diff --git a/motrix_rl/src/motrix_rl/cli.py b/motrix_rl/src/motrix_rl/cli.py index d7a45164..fc8e38c9 100644 --- a/motrix_rl/src/motrix_rl/cli.py +++ b/motrix_rl/src/motrix_rl/cli.py @@ -4,15 +4,45 @@ """Helpers for Hydra-based train/play/view command-line entry points.""" import logging +import sys +from collections.abc import Callable +from pathlib import Path from typing import TypeVar +from hydra.core.plugins import Plugins from omegaconf import DictConfig, OmegaConf +from motrix_rl.commands.searchpath import ProjectConfigSearchPath + logger = logging.getLogger(__name__) CfgT = TypeVar("CfgT") +def run_cli(command: Callable[[], None]) -> None: + """Use the launch directory's configs unless Hydra receives an explicit path. + + Pass the directory through Hydra's native --config-dir handling so package + configs remain available and Hydra controls composition and diagnostics. + """ + Plugins.instance().register(ProjectConfigSearchPath) + args = sys.argv[1:] + explicit_path = any( + arg in ("--config-dir", "--config-path", "-cp") or arg.startswith(("--config-dir=", "--config-path=", "-cp=")) + for arg in args + ) + config_dir = Path.cwd() / "configs" + if explicit_path or not config_dir.is_dir(): + command() + return + original_argv = sys.argv + sys.argv = [original_argv[0], "--config-dir", str(config_dir), *args] + try: + command() + finally: + sys.argv = original_argv + + def to_typed_config(cfg: DictConfig, expected_type: type[CfgT]) -> CfgT: """Convert a Hydra ``DictConfig`` to its registered dataclass schema. diff --git a/motrix_rl/src/motrix_rl/commands/__init__.py b/motrix_rl/src/motrix_rl/commands/__init__.py new file mode 100644 index 00000000..43b9aaf8 --- /dev/null +++ b/motrix_rl/src/motrix_rl/commands/__init__.py @@ -0,0 +1,4 @@ +# Copyright Motphys Technology Co., Ltd. 2025, 2026 +# SPDX-License-Identifier: Apache-2.0 + +"""Installed training and playback command-line entry points.""" diff --git a/motrix_rl/src/motrix_rl/commands/export.py b/motrix_rl/src/motrix_rl/commands/export.py new file mode 100644 index 00000000..9a104590 --- /dev/null +++ b/motrix_rl/src/motrix_rl/commands/export.py @@ -0,0 +1,33 @@ +# Copyright Motphys Technology Co., Ltd. 2025, 2026 +# SPDX-License-Identifier: Apache-2.0 + +"""Export training results as ONNX policies or deployment artifacts.""" + +import argparse +import sys + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("format", choices=("onnx", "deploy"), help="artifact format to export") + if sys.argv[1:2] in (["onnx"], ["deploy"]): + format_name = sys.argv[1] + else: + parser.parse_args(sys.argv[1:] or ["--help"]) + return + + if format_name == "onnx": + from motrix_rl.commands.export_onnx import main as export_main + else: + from motrix_rl.commands.export_deploy import main as export_main + + original_argv = sys.argv + sys.argv = [f"{original_argv[0]} {format_name}", *original_argv[2:]] + try: + export_main() + finally: + sys.argv = original_argv + + +if __name__ == "__main__": + main() diff --git a/motrix_rl/src/motrix_rl/commands/export_deploy.py b/motrix_rl/src/motrix_rl/commands/export_deploy.py new file mode 100644 index 00000000..154ef448 --- /dev/null +++ b/motrix_rl/src/motrix_rl/commands/export_deploy.py @@ -0,0 +1,70 @@ +# Copyright Motphys Technology Co., Ltd. 2025, 2026 +# SPDX-License-Identifier: Apache-2.0 + +"""Export a metadata-backed training run to a deployment artifact.""" + +import json +from pathlib import Path + +import hydra +from omegaconf import DictConfig + +from motrix_deploy.profile import build_deployment_profile +from motrix_rl.cli import run_cli, to_typed_config +from motrix_rl.config import DeploymentExportConfig +from motrix_rl.deploy import export_deploy_run +from motrix_rl.runs import latest_metadata_run, open_run_context + + +def run(cfg: DeploymentExportConfig) -> None: + """Export one training run using its environment deployment profile.""" + run_dir, env_name = _resolve_run(cfg) + output = Path(cfg.output) if cfg.output is not None else Path("artifacts") / f"{env_name}.deploy" + result = export_deploy_run( + run_dir, + output, + profile_builder=build_deployment_profile, + validation_seed=cfg.validation.seed, + validation_samples=cfg.validation.samples, + atol=cfg.validation.atol, + rtol=cfg.validation.rtol, + ) + print( + json.dumps( + { + "artifact": str(result.artifact.root), + "run": str(run_dir), + "validation_samples": result.validation_samples, + "max_abs_error": result.max_abs_error, + }, + indent=2, + sort_keys=True, + ) + ) + + +def _resolve_run(cfg: DeploymentExportConfig) -> tuple[Path, str]: + if (cfg.env is None) == (cfg.run_dir is None): + raise ValueError("Exactly one of env and run_dir must be configured") + if cfg.run_dir is not None: + context = open_run_context(cfg.run_dir) + return context.run_dir, context.metadata.env_name + assert cfg.env is not None + selected = latest_metadata_run(cfg.env) + if selected is None: + raise FileNotFoundError(f"No metadata-backed training runs found for environment {cfg.env!r}") + run_dir, metadata = selected + return run_dir, metadata.env_name + + +@hydra.main(version_base=None, config_path="pkg://motrix_rl.configs", config_name="export_deploy") +def _hydra_main(cfg: DictConfig) -> None: + run(to_typed_config(cfg, DeploymentExportConfig)) + + +def main() -> None: + run_cli(_hydra_main) + + +if __name__ == "__main__": + main() diff --git a/motrix_rl/src/motrix_rl/commands/export_onnx.py b/motrix_rl/src/motrix_rl/commands/export_onnx.py new file mode 100644 index 00000000..8261fd2f --- /dev/null +++ b/motrix_rl/src/motrix_rl/commands/export_onnx.py @@ -0,0 +1,61 @@ +# Copyright Motphys Technology Co., Ltd. 2025, 2026 +# SPDX-License-Identifier: Apache-2.0 + +"""Export a metadata-backed MotrixLab policy to ONNX.""" + +import json + +import hydra +from omegaconf import DictConfig + +from motrix_rl.cli import run_cli, to_typed_config +from motrix_rl.config import OnnxExportConfig +from motrix_rl.deploy import export_onnx + + +def run(cfg: OnnxExportConfig) -> None: + """Export one metadata-backed run and print its persisted report.""" + result = export_onnx( + cfg.run_dir, + cfg.output, + opset=cfg.opset, + validation_seed=cfg.parity.seed, + validation_samples=cfg.parity.samples, + atol=cfg.parity.atol, + rtol=cfg.parity.rtol, + ) + print( + json.dumps( + { + "output": str(result.path), + "input": { + "name": result.report.input_spec.name, + "shape": result.report.input_spec.shape, + "dtype": result.report.input_spec.dtype, + }, + "output_tensor": { + "name": result.report.output_spec.name, + "shape": result.report.output_spec.shape, + "dtype": result.report.output_spec.dtype, + }, + "validation_samples": result.report.parity.samples, + "max_abs_error": result.report.parity.max_abs_error, + "max_rel_error": result.report.parity.max_rel_error, + }, + indent=2, + sort_keys=True, + ) + ) + + +@hydra.main(version_base=None, config_path="pkg://motrix_rl.configs", config_name="export_onnx") +def _hydra_main(cfg: DictConfig) -> None: + run(to_typed_config(cfg, OnnxExportConfig)) + + +def main() -> None: + run_cli(_hydra_main) + + +if __name__ == "__main__": + main() diff --git a/motrix_rl/src/motrix_rl/commands/play.py b/motrix_rl/src/motrix_rl/commands/play.py new file mode 100644 index 00000000..9600af8a --- /dev/null +++ b/motrix_rl/src/motrix_rl/commands/play.py @@ -0,0 +1,134 @@ +# Copyright Motphys Technology Co., Ltd. 2025, 2026 +# SPDX-License-Identifier: Apache-2.0 + +import logging +from dataclasses import dataclass, replace +from pathlib import Path + +import hydra +from omegaconf import DictConfig + +from motrix_env_core.renderer import RenderConfig +from motrix_rl import checkpoints, runner, runs +from motrix_rl.cli import run_cli, to_typed_config +from motrix_rl.config import PlayConfig +from motrix_rl.runs import find_metadata_for_policy, latest_metadata_run + +logger = logging.getLogger(__name__) + +DEFAULT_ENV_NAME = "cartpole" +PLAY_RECORDING_FPS = 60 + + +@dataclass(frozen=True) +class PlayTarget: + run: runs.RunContext + policy_path: Path + + +def _latest_play_target(env_name: str, rllib: str | None = None) -> PlayTarget: + metadata_run = latest_metadata_run(env_name, rllib=rllib) + if metadata_run is not None: + run_dir, metadata = metadata_run + logger.info(f"Auto-discovered RL run: {run_dir}") + return PlayTarget( + run=runs.open_run_context(run_dir, metadata), + policy_path=checkpoints.best_policy(metadata, run_dir), + ) + + if rllib is not None: + raise FileNotFoundError( + f"No metadata-backed training runs found for framework '{rllib}' and environment '{env_name}'" + ) + raise FileNotFoundError(f"No metadata-backed training runs found for environment '{env_name}' in runs/{env_name}") + + +def _policy_play_target( + policy_path: str | Path, + requested_env_name: str | None, + sim: str | None = None, +) -> PlayTarget: + policy_path = Path(policy_path) + logger.info(f"Using specified policy: {policy_path}") + metadata_result = find_metadata_for_policy(policy_path) + if metadata_result is None: + raise FileNotFoundError( + f"No metadata.json found for policy {policy_path}. Pass a policy from a metadata-backed run." + ) + + run_dir, metadata = metadata_result + if requested_env_name is not None and requested_env_name != metadata.env_name: + raise ValueError(f"env={requested_env_name!r} does not match policy metadata env {metadata.env_name!r}") + if sim is not None: + metadata = replace(metadata, sim=sim) + logger.info(f"Using policy metadata: {metadata.rllib}.{metadata.algo} ({metadata.train_backend})") + return PlayTarget(run=runs.open_run_context(run_dir, metadata), policy_path=policy_path) + + +def _resolve_play_target(cfg: PlayConfig) -> PlayTarget: + if cfg.policy is not None: + return _policy_play_target(cfg.policy, cfg.env, sim=cfg.sim) + + rllib = cfg.rllib + if rllib is not None: + logger.warning("rllib= is deprecated for play and is only used to filter metadata-backed runs.") + logger.info(f"Using specified RL framework: {rllib}") + + target = _latest_play_target(cfg.env or DEFAULT_ENV_NAME, rllib=rllib) + logger.info(f"Auto-discovered best policy: {target.policy_path}") + if cfg.sim is not None: + metadata = replace(target.run.metadata, sim=cfg.sim) + target = replace(target, run=runs.open_run_context(target.run.run_dir, metadata)) + return target + + +def _play_render_config(target: PlayTarget, cfg: PlayConfig) -> RenderConfig: + if not cfg.record_video: + return RenderConfig() + if cfg.record_seconds <= 0.0: + raise ValueError(f"record_seconds must be positive, got {cfg.record_seconds}") + if cfg.record_width <= 0: + raise ValueError(f"record_width must be positive, got {cfg.record_width}") + if cfg.record_height <= 0: + raise ValueError(f"record_height must be positive, got {cfg.record_height}") + path = target.run.run_dir / "play_video.mp4" + num_frames = max(1, int(round(cfg.record_seconds * PLAY_RECORDING_FPS))) + return RenderConfig( + headless=True, + path=path, + fps=PLAY_RECORDING_FPS, + num_frames=num_frames, + width=cfg.record_width, + height=cfg.record_height, + ) + + +def run(cfg: PlayConfig) -> None: + try: + target = _resolve_play_target(cfg) + render = _play_render_config(target, cfg) + trainer = runner.create_run_handle( + target.run, + cfg_override=cfg.rl, + play_num_envs=cfg.num_envs, + seed=cfg.seed, + randomize_seed=cfg.rand_seed, + render=render, + ) + trainer.play(str(target.policy_path)) + except (FileNotFoundError, ValueError) as e: + logger.error(f"Error: {e}") + logger.error("Please train a model first or specify a metadata-backed policy path") + + +@hydra.main(version_base=None, config_path="pkg://motrix_rl.configs", config_name="play") +def _hydra_main(cfg: DictConfig) -> None: + run(to_typed_config(cfg, PlayConfig)) + + +def main() -> None: + run_cli(_hydra_main) + + +if __name__ == "__main__": + main() diff --git a/motrix_rl/src/motrix_rl/commands/searchpath.py b/motrix_rl/src/motrix_rl/commands/searchpath.py new file mode 100644 index 00000000..2e724f0a --- /dev/null +++ b/motrix_rl/src/motrix_rl/commands/searchpath.py @@ -0,0 +1,26 @@ +# Copyright Motphys Technology Co., Ltd. 2025, 2026 +# SPDX-License-Identifier: Apache-2.0 + +"""Give project configuration precedence over the packaged RL defaults.""" + +import argparse +from pathlib import Path + +from hydra.core.config_search_path import ConfigSearchPath +from hydra.plugins.search_path_plugin import SearchPathPlugin + + +class ProjectConfigSearchPath(SearchPathPlugin): + def manipulate_search_path(self, search_path: ConfigSearchPath) -> None: + if not any( + entry.provider == "main" and entry.path == "pkg://motrix_rl.configs" for entry in search_path.get_path() + ): + return + parser = argparse.ArgumentParser(add_help=False, allow_abbrev=False) + parser.add_argument("--config-dir") + args, _ = parser.parse_known_args() + if args.config_dir is not None: + # Hydra validates --config-dir itself after search-path plugins run. + # Its native entry is appended near the schema source; add an earlier + # project entry so same-name groups override the packaged defaults. + search_path.prepend(provider="motrix-project", path=Path(args.config_dir).resolve().as_uri()) diff --git a/motrix_rl/src/motrix_rl/commands/train.py b/motrix_rl/src/motrix_rl/commands/train.py new file mode 100644 index 00000000..eb881e41 --- /dev/null +++ b/motrix_rl/src/motrix_rl/commands/train.py @@ -0,0 +1,59 @@ +# Copyright Motphys Technology Co., Ltd. 2025, 2026 +# SPDX-License-Identifier: Apache-2.0 + +import logging +import os + +# FastSAC runs learner and collector processes concurrently. Let idle GNU +# OpenMP workers sleep instead of spinning and stealing CPU from the other +# process. These defaults must be set before importing Torch through motrix_rl; +# explicit user settings still take precedence. +os.environ.setdefault("OMP_WAIT_POLICY", "PASSIVE") +os.environ.setdefault("GOMP_SPINCOUNT", "0") + +import hydra # noqa: E402 +from omegaconf import DictConfig # noqa: E402 + +from motrix_env_core.renderer import RenderConfig # noqa: E402 +from motrix_rl import runner # noqa: E402 +from motrix_rl.cli import run_cli, to_typed_config # noqa: E402 +from motrix_rl.config import TrainConfig # noqa: E402 +from motrix_rl.plugins import load_plugins # noqa: E402 + +# Plugin schemas must be available before Hydra composes the selected task. +load_plugins() + +logger = logging.getLogger(__name__) + + +def run(cfg: TrainConfig) -> None: + render = RenderConfig() if cfg.render else None + + train_result = runner.train( + runner.TrainRequest( + config=cfg, + render=render, + ) + ) + + if cfg.play: + try: + train_result.play( + render=RenderConfig(), + ) + except FileNotFoundError as e: + logger.error(f"Unable to play after training: {e}") + return + + +@hydra.main(version_base=None, config_path="pkg://motrix_rl.configs", config_name="train") +def _hydra_main(cfg: DictConfig) -> None: + run(to_typed_config(cfg, TrainConfig)) + + +def main() -> None: + run_cli(_hydra_main) + + +if __name__ == "__main__": + main() diff --git a/motrix_rl/src/motrix_rl/config.py b/motrix_rl/src/motrix_rl/config.py index 07d204b5..e70ae89d 100644 --- a/motrix_rl/src/motrix_rl/config.py +++ b/motrix_rl/src/motrix_rl/config.py @@ -57,7 +57,7 @@ class DeploymentExportConfig: """Typed Hydra configuration for deployment artifact export.""" env: str | None = None - run: str | None = None + run_dir: str | None = None output: str | None = None validation: OnnxParityConfig = field( default_factory=lambda: OnnxParityConfig(atol=1e-5), @@ -90,7 +90,7 @@ class TaskConfig: @dataclass class TrainConfig(TaskConfig): - """Top-level schema for ``scripts/train.py``.""" + """Top-level schema for ``mxtrain``.""" render: bool = False play: bool = False @@ -100,7 +100,7 @@ class TrainConfig(TaskConfig): @dataclass class PlayConfig: - """Top-level schema for ``scripts/play.py``.""" + """Top-level schema for ``mxplay``.""" env: str | None = None sim: str | None = None diff --git a/motrix_rl/src/motrix_rl/configs/__init__.py b/motrix_rl/src/motrix_rl/configs/__init__.py new file mode 100644 index 00000000..1af1390b --- /dev/null +++ b/motrix_rl/src/motrix_rl/configs/__init__.py @@ -0,0 +1,4 @@ +# Copyright Motphys Technology Co., Ltd. 2025, 2026 +# SPDX-License-Identifier: Apache-2.0 + +"""Packaged Hydra configuration for the RL commands.""" diff --git a/configs/algo_base/motrix.fastsac.yaml b/motrix_rl/src/motrix_rl/configs/algo_base/motrix.fastsac.yaml similarity index 100% rename from configs/algo_base/motrix.fastsac.yaml rename to motrix_rl/src/motrix_rl/configs/algo_base/motrix.fastsac.yaml diff --git a/configs/algo_base/rslrl.ppo.yaml b/motrix_rl/src/motrix_rl/configs/algo_base/rslrl.ppo.yaml similarity index 100% rename from configs/algo_base/rslrl.ppo.yaml rename to motrix_rl/src/motrix_rl/configs/algo_base/rslrl.ppo.yaml diff --git a/configs/algo_base/skrl.ppo.yaml b/motrix_rl/src/motrix_rl/configs/algo_base/skrl.ppo.yaml similarity index 100% rename from configs/algo_base/skrl.ppo.yaml rename to motrix_rl/src/motrix_rl/configs/algo_base/skrl.ppo.yaml diff --git a/configs/deploy/export.yaml b/motrix_rl/src/motrix_rl/configs/export_deploy.yaml similarity index 94% rename from configs/deploy/export.yaml rename to motrix_rl/src/motrix_rl/configs/export_deploy.yaml index 40fe17c7..f882a789 100644 --- a/configs/deploy/export.yaml +++ b/motrix_rl/src/motrix_rl/configs/export_deploy.yaml @@ -11,7 +11,7 @@ hydra: chdir: false env: null -run: null +run_dir: null output: null validation: seed: 1 diff --git a/configs/export_onnx.yaml b/motrix_rl/src/motrix_rl/configs/export_onnx.yaml similarity index 100% rename from configs/export_onnx.yaml rename to motrix_rl/src/motrix_rl/configs/export_onnx.yaml diff --git a/configs/play.yaml b/motrix_rl/src/motrix_rl/configs/play.yaml similarity index 100% rename from configs/play.yaml rename to motrix_rl/src/motrix_rl/configs/play.yaml diff --git a/configs/train.yaml b/motrix_rl/src/motrix_rl/configs/train.yaml similarity index 84% rename from configs/train.yaml rename to motrix_rl/src/motrix_rl/configs/train.yaml index eb721669..28b43fe7 100644 --- a/configs/train.yaml +++ b/motrix_rl/src/motrix_rl/configs/train.yaml @@ -1,7 +1,9 @@ defaults: - train_schema - _self_ - - task: cartpole/skrl.ppo + # A null selection allows --help without a project task. The typed schema + # still requires a complete task recipe before training can start. + - task: null # Console logging only; don't drop a .log file in the working dir. - override hydra/job_logging: stdout diff --git a/motrix_rl/tests/test_rl_cli.py b/motrix_rl/tests/test_rl_cli.py new file mode 100644 index 00000000..cf4f3b70 --- /dev/null +++ b/motrix_rl/tests/test_rl_cli.py @@ -0,0 +1,76 @@ +# Copyright Motphys Technology Co., Ltd. 2025, 2026 +# SPDX-License-Identifier: Apache-2.0 + +"""Command boundary contracts independent of Hydra and simulator runtime.""" + +import sys + +import pytest + +from motrix_rl.cli import run_cli +from motrix_rl.commands.export import main as export_main + + +@pytest.mark.parametrize("format_name", ["onnx", "deploy"]) +def test_export_dispatch_preserves_hydra_arguments_and_restores_argv(monkeypatch, format_name): + import importlib + + module = importlib.import_module(f"motrix_rl.commands.export_{format_name}") + observed = [] + + def command(): + observed.append(sys.argv.copy()) + raise RuntimeError("export failed") + + monkeypatch.setattr(module, "main", command) + original = ["mxexport", format_name, "run_dir=example", "--config-dir", "/project/configs"] + monkeypatch.setattr(sys, "argv", original) + with pytest.raises(RuntimeError, match="export failed"): + export_main() + assert observed == [[f"mxexport {format_name}", *original[2:]]] + assert sys.argv is original + + +@pytest.mark.parametrize("args,code", [([], 0), (["--help"], 0), (["invalid"], 2)]) +def test_export_requires_an_explicit_format(monkeypatch, capsys, args, code): + monkeypatch.setattr(sys, "argv", ["mxexport", *args]) + with pytest.raises(SystemExit) as error: + export_main() + assert error.value.code == code + output = capsys.readouterr() + assert "onnx" in output.out + output.err + assert "deploy" in output.out + output.err + + +def test_cli_uses_local_configs_and_restores_arguments(monkeypatch, tmp_path): + (tmp_path / "configs").mkdir() + monkeypatch.chdir(tmp_path) + original = ["mxtrain", "task=example/ppo"] + monkeypatch.setattr(sys, "argv", original) + observed = [] + run_cli(lambda: observed.append(sys.argv.copy())) + assert observed == [["mxtrain", "--config-dir", str(tmp_path / "configs"), "task=example/ppo"]] + assert sys.argv is original + + +@pytest.mark.parametrize("args", [["--config-dir", "/other"], ["--config-dir=/other"], ["-cp", "/other"]]) +def test_explicit_hydra_directory_bypasses_local_default(monkeypatch, tmp_path, args): + (tmp_path / "configs").mkdir() + monkeypatch.chdir(tmp_path) + original = ["mxtrain", *args] + monkeypatch.setattr(sys, "argv", original) + observed = [] + run_cli(lambda: observed.append(sys.argv.copy())) + assert observed == [original] + + +def test_cli_does_not_search_parent_directories(monkeypatch, tmp_path): + (tmp_path / "configs").mkdir() + child = tmp_path / "child" + child.mkdir() + monkeypatch.chdir(child) + original = ["mxplay", "--cfg", "job"] + monkeypatch.setattr(sys, "argv", original) + observed = [] + run_cli(lambda: observed.append(sys.argv.copy())) + assert observed == [original] diff --git a/motrix_rl_builtin/tests/test_builtin_onnx_export.py b/motrix_rl_builtin/tests/test_builtin_onnx_export.py index f59790df..f955a1dd 100644 --- a/motrix_rl_builtin/tests/test_builtin_onnx_export.py +++ b/motrix_rl_builtin/tests/test_builtin_onnx_export.py @@ -7,7 +7,7 @@ import onnx import pytest import torch -from hydra import compose, initialize_config_dir +from hydra import compose, initialize_config_module from hydra.core.global_hydra import GlobalHydra from motrix_rl import checkpoints, runs @@ -29,8 +29,10 @@ def _train_config(task: str, overrides: list[str]) -> TrainConfig: from motrix_rl.plugins import load_plugins load_plugins() - with initialize_config_dir(version_base=None, config_dir=CONFIG_DIR): - config = compose(config_name="train", overrides=[f"task={task}", *overrides]) + with initialize_config_module(version_base=None, config_module="motrix_rl.configs"): + config = compose( + config_name="train", overrides=[f"hydra.searchpath=[file://{CONFIG_DIR}]", f"task={task}", *overrides] + ) return to_typed_config(config, TrainConfig) diff --git a/motrix_rl_builtin/tests/test_fastsac_ddp_equivalence.py b/motrix_rl_builtin/tests/test_fastsac_ddp_equivalence.py index 4fc56302..1016d0d7 100644 --- a/motrix_rl_builtin/tests/test_fastsac_ddp_equivalence.py +++ b/motrix_rl_builtin/tests/test_fastsac_ddp_equivalence.py @@ -29,7 +29,7 @@ def _make_agent(world_size: int, seed: int) -> object: from motrix_rl_builtin.fastsac.config import FastSacAgentCfg from omegaconf import OmegaConf - yaml_path = Path(__file__).resolve().parents[2] / "configs" / "algo_base" / "motrix.fastsac.yaml" + yaml_path = Path(__file__).resolve().parents[2] / "motrix_rl/src/motrix_rl/configs/algo_base/motrix.fastsac.yaml" base = OmegaConf.merge(OmegaConf.structured(FastSacAgentCfg), OmegaConf.load(yaml_path)["agent"]) cfg = OmegaConf.to_object( OmegaConf.merge( diff --git a/motrix_rl_builtin/tests/test_fastsac_live_metrics.py b/motrix_rl_builtin/tests/test_fastsac_live_metrics.py index 27212ad0..ae7bc5eb 100644 --- a/motrix_rl_builtin/tests/test_fastsac_live_metrics.py +++ b/motrix_rl_builtin/tests/test_fastsac_live_metrics.py @@ -108,8 +108,6 @@ def close(self) -> None: def _context(tmp_path, rl_cfg, metrics, *, num_envs, logging_interval, save_interval, resume_from=None): - import motrix_envs # noqa: F401 registers environments used by the trainer - (tmp_path / "checkpoints").mkdir(exist_ok=True) return TrainerContext( run=None, diff --git a/motrix_rl_builtin/tests/test_fastsac_sync_trainer.py b/motrix_rl_builtin/tests/test_fastsac_sync_trainer.py index 626901eb..49ebd59c 100644 --- a/motrix_rl_builtin/tests/test_fastsac_sync_trainer.py +++ b/motrix_rl_builtin/tests/test_fastsac_sync_trainer.py @@ -14,8 +14,6 @@ def test_sync_train_closes_tensorboard_writer_when_training_fails(tmp_path, monkeypatch): from motrix_rl_builtin.fastsac.sync.train import Trainer - import motrix_envs # noqa: F401 registers environments used by the trainer - writer = Mock() monkeypatch.setattr(torch.utils.tensorboard, "SummaryWriter", Mock(return_value=writer)) monkeypatch.setattr(Trainer, "_make_env", Mock()) diff --git a/motrix_rl_builtin/tests/test_fastsac_trainer_selection.py b/motrix_rl_builtin/tests/test_fastsac_trainer_selection.py index cc5644b2..e9a5347c 100644 --- a/motrix_rl_builtin/tests/test_fastsac_trainer_selection.py +++ b/motrix_rl_builtin/tests/test_fastsac_trainer_selection.py @@ -3,7 +3,7 @@ from pathlib import Path -from hydra import compose, initialize_config_dir +from hydra import compose, initialize_config_module from hydra.core.global_hydra import GlobalHydra from omegaconf import OmegaConf @@ -14,8 +14,12 @@ def _compose_task_option(env, rllib, algo, *, overrides): load_plugins() GlobalHydra.instance().clear() - with initialize_config_dir(version_base=None, config_dir=str(Path(__file__).resolve().parents[2] / "configs")): - cfg = compose(config_name="train", overrides=[f"task={env}/{rllib}.{algo}", *overrides]) + config_dir = Path(__file__).resolve().parents[2] / "configs" + with initialize_config_module(version_base=None, config_module="motrix_rl.configs"): + cfg = compose( + config_name="train", + overrides=[f"hydra.searchpath=[file://{config_dir}]", f"task={env}/{rllib}.{algo}", *overrides], + ) return OmegaConf.to_object(cfg) @@ -24,8 +28,6 @@ def test_motrix_fastsac_asynchronous_switches_trainer(tmp_path): from motrix_rl_builtin.fastsac.config import FastSacAsyncOptionsCfg from motrix_rl_builtin.fastsac.sync.train import Trainer as SyncTrainer - import motrix_envs # noqa: F401 registers environments used by the trainer - async_cfg = _compose_task_option( "g1-walk-flat", "motrix", diff --git a/motrix_rl_rslrl/tests/test_rslrl_onnx_export.py b/motrix_rl_rslrl/tests/test_rslrl_onnx_export.py index e8654420..7b33f90d 100644 --- a/motrix_rl_rslrl/tests/test_rslrl_onnx_export.py +++ b/motrix_rl_rslrl/tests/test_rslrl_onnx_export.py @@ -8,7 +8,7 @@ import onnx import pytest import torch -from hydra import compose, initialize_config_dir +from hydra import compose, initialize_config_module from hydra.core.global_hydra import GlobalHydra from tensordict import TensorDict @@ -31,8 +31,10 @@ def _train_config(task: str, overrides: list[str]) -> TrainConfig: from motrix_rl.plugins import load_plugins load_plugins() - with initialize_config_dir(version_base=None, config_dir=CONFIG_DIR): - config = compose(config_name="train", overrides=[f"task={task}", *overrides]) + with initialize_config_module(version_base=None, config_module="motrix_rl.configs"): + config = compose( + config_name="train", overrides=[f"hydra.searchpath=[file://{CONFIG_DIR}]", f"task={task}", *overrides] + ) return to_typed_config(config, TrainConfig) diff --git a/motrix_rl_skrl/src/motrix_rl_skrl/config.py b/motrix_rl_skrl/src/motrix_rl_skrl/config.py index 2cf9ff31..7864dab2 100644 --- a/motrix_rl_skrl/src/motrix_rl_skrl/config.py +++ b/motrix_rl_skrl/src/motrix_rl_skrl/config.py @@ -4,7 +4,7 @@ """SKRL Configuration Classes This module provides configuration classes for SKRL RL framework integration. -The configuration structure matches configs/algo_base/skrl.ppo.yaml with a hierarchical +The configuration structure matches motrix_rl.configs/algo_base/skrl.ppo.yaml with a hierarchical design following the same pattern as RslrlConfig. Configuration Hierarchy: @@ -27,7 +27,7 @@ class SkrlPolicyCfg: """Configuration for SKRL policy (GaussianMixin) model. - Corresponds to the policy section in configs/algo_base/skrl.ppo.yaml. + Corresponds to the policy section in motrix_rl.configs/algo_base/skrl.ppo.yaml. """ clip_actions: bool = MISSING @@ -143,7 +143,7 @@ def to_dict(self) -> dict: class SkrlValueCfg: """Configuration for SKRL value (DeterministicMixin) model. - Corresponds to the value section in configs/algo_base/skrl.ppo.yaml. + Corresponds to the value section in motrix_rl.configs/algo_base/skrl.ppo.yaml. """ clip_actions: bool = MISSING @@ -254,7 +254,7 @@ def to_dict(self) -> dict: class SkrlModelsCfg: """Configuration for SKRL models section. - Corresponds to the models section in configs/algo_base/skrl.ppo.yaml. + Corresponds to the models section in motrix_rl.configs/algo_base/skrl.ppo.yaml. """ separate: bool = MISSING @@ -274,7 +274,7 @@ def to_dict(self) -> dict: class SkrlMemoryCfg: """Configuration for SKRL memory. - Corresponds to the memory section in configs/algo_base/skrl.ppo.yaml. + Corresponds to the memory section in motrix_rl.configs/algo_base/skrl.ppo.yaml. """ class_name: str = MISSING @@ -291,7 +291,7 @@ def to_dict(self) -> dict: class SkrlAgentCfg: """Configuration for SKRL PPO agent. - Corresponds to the agent section in configs/algo_base/skrl.ppo.yaml. + Corresponds to the agent section in motrix_rl.configs/algo_base/skrl.ppo.yaml. Field names match PPO_DEFAULT_CONFIG from SKRL. """ @@ -359,7 +359,7 @@ def to_dict(self) -> dict: class SkrlTrainerCfg: """Configuration for SKRL sequential trainer. - Corresponds to the trainer section in configs/algo_base/skrl.ppo.yaml. + Corresponds to the trainer section in motrix_rl.configs/algo_base/skrl.ppo.yaml. """ timesteps: int = MISSING diff --git a/motrix_rl_skrl/tests/test_skrl_onnx_export.py b/motrix_rl_skrl/tests/test_skrl_onnx_export.py index 1c0c2199..3673670c 100644 --- a/motrix_rl_skrl/tests/test_skrl_onnx_export.py +++ b/motrix_rl_skrl/tests/test_skrl_onnx_export.py @@ -9,7 +9,7 @@ import onnxruntime as ort import pytest import torch -from hydra import compose, initialize_config_dir +from hydra import compose, initialize_config_module from hydra.core.global_hydra import GlobalHydra from motrix_rl import checkpoints, runs @@ -32,8 +32,10 @@ def _train_config(task: str, overrides: list[str]) -> TrainConfig: from motrix_rl.plugins import load_plugins load_plugins() - with initialize_config_dir(version_base=None, config_dir=CONFIG_DIR): - config = compose(config_name="train", overrides=[f"task={task}", *overrides]) + with initialize_config_module(version_base=None, config_module="motrix_rl.configs"): + config = compose( + config_name="train", overrides=[f"hydra.searchpath=[file://{CONFIG_DIR}]", f"task={task}", *overrides] + ) return to_typed_config(config, TrainConfig) diff --git a/scripts/export_deploy.py b/scripts/export_deploy.py index 8f63ab53..b59d8401 100644 --- a/scripts/export_deploy.py +++ b/scripts/export_deploy.py @@ -1,66 +1,9 @@ # Copyright Motphys Technology Co., Ltd. 2025, 2026 # SPDX-License-Identifier: Apache-2.0 -"""Export a metadata-backed training run to a deployment artifact.""" - -import json -from pathlib import Path - -import hydra -from omegaconf import DictConfig - -from motrix_envs.deploy import build_deployment_profile -from motrix_rl.cli import to_typed_config -from motrix_rl.config import DeploymentExportConfig -from motrix_rl.deploy import export_deploy_run -from motrix_rl.runs import latest_metadata_run, open_run_context - - -def run(cfg: DeploymentExportConfig) -> None: - """Export one training run using its environment deployment profile.""" - run_dir, env_name = _resolve_run(cfg) - output = Path(cfg.output) if cfg.output is not None else Path("artifacts") / f"{env_name}.deploy" - result = export_deploy_run( - run_dir, - output, - profile_builder=build_deployment_profile, - validation_seed=cfg.validation.seed, - validation_samples=cfg.validation.samples, - atol=cfg.validation.atol, - rtol=cfg.validation.rtol, - ) - print( - json.dumps( - { - "artifact": str(result.artifact.root), - "run": str(run_dir), - "validation_samples": result.validation_samples, - "max_abs_error": result.max_abs_error, - }, - indent=2, - sort_keys=True, - ) - ) - - -def _resolve_run(cfg: DeploymentExportConfig) -> tuple[Path, str]: - if (cfg.env is None) == (cfg.run is None): - raise ValueError("Exactly one of env and run must be configured") - if cfg.run is not None: - context = open_run_context(cfg.run) - return context.run_dir, context.metadata.env_name - assert cfg.env is not None - selected = latest_metadata_run(cfg.env) - if selected is None: - raise FileNotFoundError(f"No metadata-backed training runs found for environment {cfg.env!r}") - run_dir, metadata = selected - return run_dir, metadata.env_name - - -@hydra.main(version_base=None, config_path="../configs/deploy", config_name="export") -def main(cfg: DictConfig) -> None: - run(to_typed_config(cfg, DeploymentExportConfig)) +"""Compatibility wrapper; prefer ``mxexport deploy``.""" +from motrix_rl.commands.export_deploy import main if __name__ == "__main__": main() diff --git a/scripts/export_onnx.py b/scripts/export_onnx.py index 3e6abb76..622b89f3 100644 --- a/scripts/export_onnx.py +++ b/scripts/export_onnx.py @@ -1,57 +1,9 @@ # Copyright Motphys Technology Co., Ltd. 2025, 2026 # SPDX-License-Identifier: Apache-2.0 -"""Export a metadata-backed MotrixLab policy to ONNX.""" - -import json - -import hydra -from omegaconf import DictConfig - -from motrix_rl.cli import to_typed_config -from motrix_rl.config import OnnxExportConfig -from motrix_rl.deploy import export_onnx - - -def run(cfg: OnnxExportConfig) -> None: - """Export one metadata-backed run and print its persisted report.""" - result = export_onnx( - cfg.run_dir, - cfg.output, - opset=cfg.opset, - validation_seed=cfg.parity.seed, - validation_samples=cfg.parity.samples, - atol=cfg.parity.atol, - rtol=cfg.parity.rtol, - ) - print( - json.dumps( - { - "output": str(result.path), - "input": { - "name": result.report.input_spec.name, - "shape": result.report.input_spec.shape, - "dtype": result.report.input_spec.dtype, - }, - "output_tensor": { - "name": result.report.output_spec.name, - "shape": result.report.output_spec.shape, - "dtype": result.report.output_spec.dtype, - }, - "validation_samples": result.report.parity.samples, - "max_abs_error": result.report.parity.max_abs_error, - "max_rel_error": result.report.parity.max_rel_error, - }, - indent=2, - sort_keys=True, - ) - ) - - -@hydra.main(version_base=None, config_path="../configs", config_name="export_onnx") -def main(cfg: DictConfig) -> None: - run(to_typed_config(cfg, OnnxExportConfig)) +"""Compatibility wrapper; prefer the installed ``mxexport onnx`` command.""" +from motrix_rl.commands.export_onnx import main if __name__ == "__main__": main() diff --git a/scripts/gpu_utils.py b/scripts/gpu_utils.py deleted file mode 100644 index 44e87041..00000000 --- a/scripts/gpu_utils.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright Motphys Technology Co., Ltd. 2025, 2026 -# SPDX-License-Identifier: Apache-2.0 - -import jax.numpy as jnp -import pynvml - - -def monitor_gpu_utilization(stop_event, gpu_index=0, interval=1.0): - pynvml.nvmlInit() - handle = pynvml.nvmlDeviceGetHandleByIndex(gpu_index) - utilization_samples = [] - - while not stop_event.is_set(): - util = pynvml.nvmlDeviceGetUtilizationRates(handle) - utilization_samples.append(util.gpu) - stop_event.wait(interval) - - pynvml.nvmlShutdown() - - if utilization_samples: - data = jnp.array(utilization_samples) - print(f"GPU utilization statistics over {len(data)} samples:") - print(f" Mean: {jnp.mean(data):.2f}%") - print(f" Max : {jnp.max(data):.2f}%") - print(f" Min : {jnp.min(data):.2f}%") - print(f" Median : {jnp.median(data):.2f}%") - else: - print("No GPU utilization samples recorded.") diff --git a/scripts/play.py b/scripts/play.py index 538e29a9..fdaf036e 100644 --- a/scripts/play.py +++ b/scripts/play.py @@ -1,131 +1,9 @@ # Copyright Motphys Technology Co., Ltd. 2025, 2026 # SPDX-License-Identifier: Apache-2.0 -import logging -from dataclasses import dataclass, replace -from pathlib import Path - -import hydra -from omegaconf import DictConfig - -import motrix_envs # noqa: F401 registers built-in environments -from motrix_env_core.renderer import RenderConfig -from motrix_rl import checkpoints, runner, runs -from motrix_rl.cli import to_typed_config -from motrix_rl.config import PlayConfig -from motrix_rl.runs import find_metadata_for_policy, latest_metadata_run - -logger = logging.getLogger(__name__) - -DEFAULT_ENV_NAME = "cartpole" -PLAY_RECORDING_FPS = 60 - - -@dataclass(frozen=True) -class PlayTarget: - run: runs.RunContext - policy_path: Path - - -def _latest_play_target(env_name: str, rllib: str | None = None) -> PlayTarget: - metadata_run = latest_metadata_run(env_name, rllib=rllib) - if metadata_run is not None: - run_dir, metadata = metadata_run - logger.info(f"Auto-discovered RL run: {run_dir}") - return PlayTarget( - run=runs.open_run_context(run_dir, metadata), - policy_path=checkpoints.best_policy(metadata, run_dir), - ) - - if rllib is not None: - raise FileNotFoundError( - f"No metadata-backed training runs found for framework '{rllib}' and environment '{env_name}'" - ) - raise FileNotFoundError(f"No metadata-backed training runs found for environment '{env_name}' in runs/{env_name}") - - -def _policy_play_target( - policy_path: str | Path, - requested_env_name: str | None, - sim: str | None = None, -) -> PlayTarget: - policy_path = Path(policy_path) - logger.info(f"Using specified policy: {policy_path}") - metadata_result = find_metadata_for_policy(policy_path) - if metadata_result is None: - raise FileNotFoundError( - f"No metadata.json found for policy {policy_path}. Pass a policy from a metadata-backed run." - ) - - run_dir, metadata = metadata_result - if requested_env_name is not None and requested_env_name != metadata.env_name: - raise ValueError(f"env={requested_env_name!r} does not match policy metadata env {metadata.env_name!r}") - if sim is not None: - metadata = replace(metadata, sim=sim) - logger.info(f"Using policy metadata: {metadata.rllib}.{metadata.algo} ({metadata.train_backend})") - return PlayTarget(run=runs.open_run_context(run_dir, metadata), policy_path=policy_path) - - -def _resolve_play_target(cfg: PlayConfig) -> PlayTarget: - if cfg.policy is not None: - return _policy_play_target(cfg.policy, cfg.env, sim=cfg.sim) - - rllib = cfg.rllib - if rllib is not None: - logger.warning("rllib= is deprecated for play and is only used to filter metadata-backed runs.") - logger.info(f"Using specified RL framework: {rllib}") - - target = _latest_play_target(cfg.env or DEFAULT_ENV_NAME, rllib=rllib) - logger.info(f"Auto-discovered best policy: {target.policy_path}") - if cfg.sim is not None: - metadata = replace(target.run.metadata, sim=cfg.sim) - target = replace(target, run=runs.open_run_context(target.run.run_dir, metadata)) - return target - - -def _play_render_config(target: PlayTarget, cfg: PlayConfig) -> RenderConfig: - if not cfg.record_video: - return RenderConfig() - if cfg.record_seconds <= 0.0: - raise ValueError(f"record_seconds must be positive, got {cfg.record_seconds}") - if cfg.record_width <= 0: - raise ValueError(f"record_width must be positive, got {cfg.record_width}") - if cfg.record_height <= 0: - raise ValueError(f"record_height must be positive, got {cfg.record_height}") - path = target.run.run_dir / "play_video.mp4" - num_frames = max(1, int(round(cfg.record_seconds * PLAY_RECORDING_FPS))) - return RenderConfig( - headless=True, - path=path, - fps=PLAY_RECORDING_FPS, - num_frames=num_frames, - width=cfg.record_width, - height=cfg.record_height, - ) - - -def run(cfg: PlayConfig) -> None: - try: - target = _resolve_play_target(cfg) - render = _play_render_config(target, cfg) - trainer = runner.create_run_handle( - target.run, - cfg_override=cfg.rl, - play_num_envs=cfg.num_envs, - seed=cfg.seed, - randomize_seed=cfg.rand_seed, - render=render, - ) - trainer.play(str(target.policy_path)) - except (FileNotFoundError, ValueError) as e: - logger.error(f"Error: {e}") - logger.error("Please train a model first or specify a metadata-backed policy path") - - -@hydra.main(version_base=None, config_path="../configs", config_name="play") -def main(cfg: DictConfig) -> None: - run(to_typed_config(cfg, PlayConfig)) +"""Compatibility wrapper; prefer the installed ``mxplay`` command.""" +from motrix_rl.commands.play import main if __name__ == "__main__": main() diff --git a/scripts/test_all_demos.py b/scripts/test_all_demos.py index b187bebb..f083cfed 100755 --- a/scripts/test_all_demos.py +++ b/scripts/test_all_demos.py @@ -38,15 +38,18 @@ def run_command(env: str, backend: str, extra_args: list[str]) -> int: """Run a single training command.""" - backend_task = Path("configs/task") / env / f"skrl.ppo.{backend}.yaml" - task_option = f"{env}/skrl.ppo.{backend}" if backend_task.exists() else f"{env}/skrl.ppo" + config_dir = Path(__file__).resolve().parents[1] / "configs" + backend_task = config_dir / "task" / env / f"skrl.ppo.{backend}.yaml" + task_option = f"{env}/skrl.ppo.{backend}" if backend_task.is_file() else f"{env}/skrl.ppo" cmd = [ - "uv", - "run", - "scripts/train.py", + sys.executable, + "-m", + "motrix_rl.commands.train", + "--config-dir", + str(config_dir), f"task={task_option}", ] + extra_args - if not backend_task.exists(): + if not backend_task.is_file(): cmd.append(f"task.train_backend={backend}") print(f"\n{'=' * 80}") @@ -83,7 +86,7 @@ def main(): "--extra-args", nargs=argparse.REMAINDER, default=[], - help="Extra arguments to pass to train.py", + help="Extra arguments to pass to mxtrain", ) parser.add_argument( "--stop-on-error", diff --git a/scripts/train.py b/scripts/train.py index ef4ac33a..99391892 100644 --- a/scripts/train.py +++ b/scripts/train.py @@ -1,56 +1,9 @@ # Copyright Motphys Technology Co., Ltd. 2025, 2026 # SPDX-License-Identifier: Apache-2.0 -import logging -import os - -# FastSAC runs learner and collector processes concurrently. Let idle GNU -# OpenMP workers sleep instead of spinning and stealing CPU from the other -# process. These defaults must be set before importing Torch through motrix_rl; -# explicit user settings still take precedence. -os.environ.setdefault("OMP_WAIT_POLICY", "PASSIVE") -os.environ.setdefault("GOMP_SPINCOUNT", "0") - -import hydra # noqa: E402 -from omegaconf import DictConfig # noqa: E402 - -import motrix_envs # noqa: E402, F401 registers built-in environments -from motrix_env_core.renderer import RenderConfig # noqa: E402 -from motrix_rl import runner # noqa: E402 -from motrix_rl.cli import to_typed_config # noqa: E402 -from motrix_rl.config import TrainConfig # noqa: E402 -from motrix_rl.plugins import load_plugins # noqa: E402 - -# Plugin schemas must be available before Hydra composes the selected task. -load_plugins() - -logger = logging.getLogger(__name__) - - -def run(cfg: TrainConfig) -> None: - render = RenderConfig() if cfg.render else None - - train_result = runner.train( - runner.TrainRequest( - config=cfg, - render=render, - ) - ) - - if cfg.play: - try: - train_result.play( - render=RenderConfig(), - ) - except FileNotFoundError as e: - logger.error(f"Unable to play after training: {e}") - return - - -@hydra.main(version_base=None, config_path="../configs", config_name="train") -def main(cfg: DictConfig) -> None: - run(to_typed_config(cfg, TrainConfig)) +"""Compatibility wrapper; prefer the installed ``mxtrain`` command.""" +from motrix_rl.commands.train import main if __name__ == "__main__": main() diff --git a/scripts/view.py b/scripts/view.py index 053ea7f0..6ae0a66a 100644 --- a/scripts/view.py +++ b/scripts/view.py @@ -11,7 +11,6 @@ from motrixsim.render import RenderApp, RenderClosedError, RenderSettings from omegaconf import DictConfig -import motrix_envs # noqa: F401 registers built-in environments from motrix_env_core import registry from motrix_env_core.array.env import ArrayEnv from motrix_env_core.base import ABEnv diff --git a/test/test_all_envs.py b/test/test_all_envs.py index f20cb579..3c39a926 100644 --- a/test/test_all_envs.py +++ b/test/test_all_envs.py @@ -19,7 +19,6 @@ import pytest -import motrix_envs # noqa: F401 registers built-in environments from motrix_env_core import registry _RUNNER = """ @@ -27,7 +26,6 @@ import numpy as np -import motrix_envs # noqa: F401 registers built-in environments from motrix_env_core import registry env = registry.make(sys.argv[1], num_envs=int(sys.argv[2])) diff --git a/tests/test_cli_config.py b/tests/test_cli_config.py index aae30a87..833c235f 100644 --- a/tests/test_cli_config.py +++ b/tests/test_cli_config.py @@ -4,7 +4,7 @@ from pathlib import Path import pytest -from hydra import compose, initialize_config_dir +from hydra import compose, initialize_config_dir, initialize_config_module from hydra.core.global_hydra import GlobalHydra from motrix_rl import frameworks @@ -24,12 +24,19 @@ def _clear_hydra(): def _compose(config_name: str, overrides: list[str]): load_plugins() - with initialize_config_dir(version_base=None, config_dir=CONFIG_DIR): + initializer = ( + initialize_config_module(version_base=None, config_module="motrix_rl.configs") + if config_name in ("train", "play", "export_onnx") + else initialize_config_dir(version_base=None, config_dir=CONFIG_DIR) + ) + if config_name == "train": + overrides = [f"hydra.searchpath=[file://{CONFIG_DIR}]", *overrides] + with initializer: return compose(config_name=config_name, overrides=overrides) -def test_default_train_config_composes_to_selected_provider_type(): - cfg = _compose("train", []) +def test_selected_train_config_composes_to_selected_provider_type(): + cfg = _compose("train", ["task=cartpole/skrl.ppo"]) typed_cfg = to_typed_config(cfg, TrainConfig) @@ -58,7 +65,7 @@ def test_train_scalar_overrides(): def test_train_nested_algo_override(): - cfg = _compose("train", ["algo.agent.learning_rate=1e-3", "seed=7"]) + cfg = _compose("train", ["task=cartpole/skrl.ppo", "algo.agent.learning_rate=1e-3", "seed=7"]) assert cfg.algo.agent.learning_rate == 1e-3 assert cfg.seed == 7 diff --git a/tests/test_deploy_export_cli.py b/tests/test_deploy_export_cli.py index 2c5284ed..d9d51eae 100644 --- a/tests/test_deploy_export_cli.py +++ b/tests/test_deploy_export_cli.py @@ -3,25 +3,18 @@ """Hydra configuration and composition tests for deployment export.""" -import importlib.util import json from pathlib import Path from types import SimpleNamespace import pytest -from hydra import compose, initialize_config_dir +from hydra import compose, initialize_config_module from hydra.core.global_hydra import GlobalHydra from motrix_rl.cli import to_typed_config +from motrix_rl.commands import export_deploy from motrix_rl.config import DeploymentExportConfig, OnnxParityConfig -ROOT = Path(__file__).resolve().parents[1] -CONFIG_DIR = str(ROOT / "configs/deploy") -_SCRIPT_SPEC = importlib.util.spec_from_file_location("export_deploy_cli", ROOT / "scripts/export_deploy.py") -assert _SCRIPT_SPEC is not None and _SCRIPT_SPEC.loader is not None -export_deploy = importlib.util.module_from_spec(_SCRIPT_SPEC) -_SCRIPT_SPEC.loader.exec_module(export_deploy) - @pytest.fixture(autouse=True) def _clear_hydra(): @@ -31,9 +24,9 @@ def _clear_hydra(): def test_export_config_composes_hydra_overrides() -> None: - with initialize_config_dir(version_base=None, config_dir=CONFIG_DIR): + with initialize_config_module(version_base=None, config_module="motrix_rl.configs"): cfg = compose( - config_name="export", + config_name="export_deploy", overrides=[ "env=go2-walk-rough", "validation.samples=8", @@ -44,7 +37,7 @@ def test_export_config_composes_hydra_overrides() -> None: assert typed == DeploymentExportConfig( env="go2-walk-rough", - run=None, + run_dir=None, output=None, validation=OnnxParityConfig(seed=1, samples=8, atol=1e-5, rtol=1e-5), ) @@ -74,7 +67,7 @@ def _export(*args, **kwargs): lambda path: SimpleNamespace(run_dir=run_dir, metadata=SimpleNamespace(env_name="go2-walk-rough")), ) cfg = DeploymentExportConfig( - run=str(run_dir), + run_dir=str(run_dir), output=str(artifact_path), validation=OnnxParityConfig(seed=7, samples=4, atol=2e-5, rtol=3e-5), ) @@ -133,9 +126,9 @@ def test_export_cli_selects_latest_environment_run_and_default_output( "cfg", [ DeploymentExportConfig(), - DeploymentExportConfig(env="go2-walk-rough", run="runs/example"), + DeploymentExportConfig(env="go2-walk-rough", run_dir="runs/example"), ], ) def test_export_cli_requires_exactly_one_run_selector(cfg: DeploymentExportConfig) -> None: - with pytest.raises(ValueError, match="Exactly one of env and run"): + with pytest.raises(ValueError, match="Exactly one of env and run_dir"): export_deploy.run(cfg) diff --git a/tests/test_doc_generate_video_script.py b/tests/test_doc_generate_video_script.py index 30363769..ab7d56f7 100644 --- a/tests/test_doc_generate_video_script.py +++ b/tests/test_doc_generate_video_script.py @@ -59,12 +59,13 @@ def test_sim_override_replaces_metadata_sim_field(monkeypatch, tmp_path): def test_missing_policy_prints_train_command(monkeypatch, tmp_path, capsys): monkeypatch.setattr(generate_video, "RUNS_ROOT", tmp_path) + monkeypatch.chdir(tmp_path) assert generate_video.main(["cartpole"]) == 2 error = capsys.readouterr().err assert "No playable policy" in error - assert "uv run scripts/train.py task=cartpole/skrl.ppo" in error + assert "python -m motrix_rl.commands.train task=cartpole/skrl.ppo" in error def test_video_config_records_by_default_and_places_bare_output_in_docs_video_dir(): diff --git a/tests/test_env_provider_discovery.py b/tests/test_env_provider_discovery.py new file mode 100644 index 00000000..c0a272f3 --- /dev/null +++ b/tests/test_env_provider_discovery.py @@ -0,0 +1,124 @@ +# Copyright Motphys Technology Co., Ltd. 2025, 2026 +# SPDX-License-Identifier: Apache-2.0 + +"""Installed-style environment discovery across fresh and spawned interpreters.""" + +import json +import os +import subprocess +import sys +import textwrap +from pathlib import Path + +import pytest + + +@pytest.mark.integration +def test_external_environment_provider_is_discovered_in_fresh_and_spawned_python(tmp_path: Path) -> None: + # Real distribution metadata exercises importlib discovery without an installer + # or importing/registering the provider in the pytest process. + (tmp_path / "external_env_provider.py").write_text( + textwrap.dedent( + """\ + from motrix_env_core import registry + from motrix_env_core.base import EnvCfg + from motrix_env_core.config import configclass + from motrix_env_core.config.scene import SceneCfg + + calls = 0 + + @configclass + class ExternalEnvCfg(EnvCfg): + scene: SceneCfg = SceneCfg() + provider_value: str = "discovered externally" + + def register(): + global calls + calls += 1 + registry.register_env_config("external-provider-discovery-test", ExternalEnvCfg) + # Public queries are legal during a registration callback and + # must not recursively invoke the provider again. + assert registry.contains("external-provider-discovery-test") + """ + ), + encoding="utf-8", + ) + metadata = tmp_path / "external_env_provider-1.0.dist-info" + metadata.mkdir() + (metadata / "METADATA").write_text( + "Metadata-Version: 2.1\nName: external-env-provider\nVersion: 1.0\n", + encoding="utf-8", + ) + (metadata / "entry_points.txt").write_text( + "[motrix_env_core.envs]\nexternal-discovery-test = external_env_provider:register\n", + encoding="utf-8", + ) + script = tmp_path / "probe_discovery.py" + script.write_text( + textwrap.dedent( + """\ + import json + import multiprocessing + import os + import sys + + def probe(first_query): + assert "external_env_provider" not in sys.modules + from motrix_env_core import registry + assert "external_env_provider" not in sys.modules + name = "external-provider-discovery-test" + if first_query == "contains": + assert registry.contains(name) + else: + assert registry.make_env_config(name).provider_value == "discovered externally" + cfg = registry.make_env_config(name) + assert registry.contains(name) + assert registry.list_registered_envs()[name]["config_class"] == type(cfg).__name__ + assert cfg.provider_value == "discovered externally" + calls = sys.modules["external_env_provider"].calls + assert calls == 1 + return {"pid": os.getpid(), "value": cfg.provider_value, "calls": calls} + + def child(connection): + connection.send(probe("make_env_config")) + connection.close() + + if __name__ == "__main__": + parent = probe("contains") + context = multiprocessing.get_context("spawn") + receiver, sender = context.Pipe(duplex=False) + process = context.Process(target=child, args=(sender,)) + process.start() + sender.close() + try: + assert receiver.poll(45), "spawned child did not report discovery" + spawned = receiver.recv() + process.join(10) + assert process.exitcode == 0 + finally: + if process.is_alive(): + process.terminate() + process.join() + receiver.close() + print(json.dumps({"parent": parent, "spawned": spawned})) + """ + ), + encoding="utf-8", + ) + env = os.environ.copy() + env["PYTHONPATH"] = os.pathsep.join(filter(None, (str(tmp_path), env.get("PYTHONPATH")))) + result = subprocess.run( + [sys.executable, str(script)], + cwd=tmp_path, + env=env, + capture_output=True, + text=True, + timeout=90, + check=False, + ) + assert result.returncode == 0, result.stdout + result.stderr + report = json.loads(result.stdout.strip().splitlines()[-1]) + assert report["parent"]["pid"] != report["spawned"]["pid"] + for process_report in report.values(): + assert process_report["value"] == "discovered externally" + assert process_report["calls"] == 1 diff --git a/tests/test_play_script.py b/tests/test_play_script.py index 5dd0b2ad..4eae32ad 100644 --- a/tests/test_play_script.py +++ b/tests/test_play_script.py @@ -1,23 +1,14 @@ # Copyright Motphys Technology Co., Ltd. 2025, 2026 # SPDX-License-Identifier: Apache-2.0 -import importlib.util -import sys from pathlib import Path import pytest from motrix_rl import runs +from motrix_rl.commands import play from motrix_rl.config import PlayConfig -PLAY_SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "play.py" -PLAY_MODULE_SPEC = importlib.util.spec_from_file_location("motrix_test_play_script", PLAY_SCRIPT) -if PLAY_MODULE_SPEC is None or PLAY_MODULE_SPEC.loader is None: - raise ImportError(f"Unable to load {PLAY_SCRIPT}") -play = importlib.util.module_from_spec(PLAY_MODULE_SPEC) -sys.modules[PLAY_MODULE_SPEC.name] = play -PLAY_MODULE_SPEC.loader.exec_module(play) - def _metadata_run(tmp_path: Path, env_name: str = "go2", sim: str | None = None) -> Path: run_dir = tmp_path / "run" diff --git a/tests/test_plugin_export_integration.py b/tests/test_plugin_export_integration.py index f1b10681..6a4e6729 100644 --- a/tests/test_plugin_export_integration.py +++ b/tests/test_plugin_export_integration.py @@ -2,11 +2,14 @@ # SPDX-License-Identifier: Apache-2.0 +import json +import subprocess +import sys from pathlib import Path import pytest import torch -from hydra import compose, initialize_config_dir +from hydra import compose, initialize_config_module from hydra.core.global_hydra import GlobalHydra from motrix_rl import checkpoints, frameworks, runs @@ -14,8 +17,6 @@ from motrix_rl.config import TrainConfig from motrix_rl.deploy import export_onnx -CONFIG_DIR = str(Path(__file__).resolve().parents[1] / "configs") - @pytest.fixture(autouse=True) def _clear_hydra(): @@ -28,8 +29,11 @@ def _train_config(task: str, overrides: list[str]) -> TrainConfig: from motrix_rl.plugins import load_plugins load_plugins() - with initialize_config_dir(version_base=None, config_dir=CONFIG_DIR): - config = compose(config_name="train", overrides=[f"task={task}", *overrides]) + config_dir = Path(__file__).resolve().parents[1] / "configs" + with initialize_config_module(version_base=None, config_module="motrix_rl.configs"): + config = compose( + config_name="train", overrides=[f"hydra.searchpath=[file://{config_dir}]", f"task={task}", *overrides] + ) return to_typed_config(config, TrainConfig) @@ -87,3 +91,48 @@ def test_export_rejects_non_onnx_output_before_replacing_file(tmp_path: Path) -> with pytest.raises(ValueError, match="must end in .onnx"): export_onnx(run.run_dir, tmp_path / "policy.bin") assert not (tmp_path / "policy.bin").exists() + + +@pytest.mark.integration +def test_export_command_writes_onnx_and_reports_parity_outside_workspace(tmp_path: Path) -> None: + config = _train_config("cartpole/skrl.ppo", ["task.train_backend=torch", "algo.models.policy.hiddens=[2]"]) + run = _run_with_checkpoint( + tmp_path, + config, + { + "policy": { + "net.0.weight": torch.ones(2, 3), + "net.0.bias": torch.zeros(2), + "mean_layer.weight": torch.ones(1, 2), + "mean_layer.bias": torch.zeros(1), + }, + "observation_preprocessor": { + "running_mean": torch.zeros(3, dtype=torch.float64), + "running_variance": torch.ones(3, dtype=torch.float64), + "current_count": torch.tensor(1.0, dtype=torch.float64), + }, + }, + ) + output = tmp_path / "policy.onnx" + result = subprocess.run( + [ + sys.executable, + "-m", + "motrix_rl.commands.export", + "onnx", + f"run_dir={run.run_dir}", + f"output={output}", + "parity.samples=4", + ], + cwd=tmp_path, + capture_output=True, + text=True, + check=True, + ) + report = json.loads(result.stdout) + assert output.is_file() + assert report["output"] == str(output) + assert report["validation_samples"] == 4 + assert report["input"]["shape"][-1] == 3 + assert report["output_tensor"]["shape"][-1] == 1 + assert report["max_abs_error"] < 1e-4 diff --git a/tests/test_rl_commands.py b/tests/test_rl_commands.py new file mode 100644 index 00000000..6375090b --- /dev/null +++ b/tests/test_rl_commands.py @@ -0,0 +1,106 @@ +# Copyright Motphys Technology Co., Ltd. 2025, 2026 +# SPDX-License-Identifier: Apache-2.0 + +"""Installed command composition from project directories, without simulation.""" + +import subprocess +import sys +from importlib.resources import files +from pathlib import Path + +import pytest +import yaml + +pytestmark = pytest.mark.integration + + +def _task(config_dir: Path, seed: int) -> None: + task_dir = config_dir / "task" / "external" + task_dir.mkdir(parents=True) + (task_dir / "ppo.yaml").write_text( + "# @package _global_\n" + "defaults:\n - /algo_base@algo: skrl.ppo\n - _self_\n" + "task:\n env: external\n rllib: skrl\n algo: ppo\n" + f"num_envs: 2\nplay_num_envs: 1\nseed: {seed}\n", + encoding="utf-8", + ) + + +def _compose(cwd: Path, *args: str) -> dict: + result = subprocess.run( + [sys.executable, "-m", "motrix_rl.commands.train", "task=external/ppo", "--cfg", "job", *args], + cwd=cwd, + capture_output=True, + text=True, + check=True, + ) + return yaml.safe_load(result.stdout) + + +def test_project_task_composes_with_packaged_algorithm_and_explicit_override(tmp_path): + _task(tmp_path / "configs", seed=13) + _task(tmp_path / "other", seed=29) + + local = _compose(tmp_path) + explicit = _compose(tmp_path, "--config-dir", str(tmp_path / "other")) + + assert local["task"]["env"] == "external" + assert local["algo"]["agent"] + assert local["seed"] == 13 + assert explicit["seed"] == 29 + + +def test_project_algorithm_defaults_override_package_and_explicit_dir_wins(tmp_path): + base = yaml.safe_load(files("motrix_rl.configs").joinpath("algo_base/skrl.ppo.yaml").read_text()) + for directory, rate in ((tmp_path / "configs", 0.0123), (tmp_path / "other", 0.0456)): + _task(directory, seed=1) + algo_dir = directory / "algo_base" + algo_dir.mkdir() + base["agent"]["learning_rate"] = rate + (algo_dir / "skrl.ppo.yaml").write_text(yaml.safe_dump(base), encoding="utf-8") + + assert _compose(tmp_path)["algo"]["agent"]["learning_rate"] == 0.0123 + for args in (("--config-dir", str(tmp_path / "other")), (f"--config-dir={tmp_path / 'other'}",)): + assert _compose(tmp_path, *args)["algo"]["agent"]["learning_rate"] == 0.0456 + + +def test_train_help_does_not_require_task_but_training_does(tmp_path): + command = [sys.executable, "-m", "motrix_rl.commands.train"] + help_result = subprocess.run([*command, "--help"], cwd=tmp_path, capture_output=True, text=True) + assert help_result.returncode == 0, help_result.stderr + assert "num_envs" in help_result.stdout + + train_result = subprocess.run(command, cwd=tmp_path, capture_output=True, text=True) + assert train_result.returncode != 0 + assert "MissingMandatoryValue" in train_result.stderr + assert "task" in train_result.stderr + + +@pytest.mark.parametrize("format_name,validation", [("onnx", "parity"), ("deploy", "validation")]) +def test_export_help_and_project_config_override(tmp_path, format_name, validation): + command = [sys.executable, "-m", "motrix_rl.commands.export", format_name] + help_result = subprocess.run([*command, "--help"], cwd=tmp_path, capture_output=True, text=True) + assert help_result.returncode == 0, help_result.stderr + assert "run_dir" in help_result.stdout + + config_dir = tmp_path / "configs" + config_dir.mkdir() + (config_dir / f"export_{format_name}.yaml").write_text( + f"defaults:\n - export_{format_name}_schema\n - _self_\nrun_dir: example\n{validation}:\n samples: 7\n", + encoding="utf-8", + ) + result = subprocess.run([*command, "--cfg", "job"], cwd=tmp_path, capture_output=True, text=True, check=True) + config = yaml.safe_load(result.stdout) + assert config[validation]["samples"] == 7 + assert config["run_dir"] == "example" + + +def test_play_composes_without_project_config_directory(tmp_path): + result = subprocess.run( + [sys.executable, "-m", "motrix_rl.commands.play", "policy=/tmp/policy.pt", "--cfg", "job"], + cwd=tmp_path, + capture_output=True, + text=True, + check=True, + ) + assert yaml.safe_load(result.stdout)["policy"] == "/tmp/policy.pt" diff --git a/tests/test_task_configs.py b/tests/test_task_configs.py index 30dc377e..c685bdc0 100644 --- a/tests/test_task_configs.py +++ b/tests/test_task_configs.py @@ -14,7 +14,7 @@ import pytest import yaml -from hydra import compose, initialize_config_dir +from hydra import compose, initialize_config_dir, initialize_config_module from hydra.core.global_hydra import GlobalHydra from hydra.core.object_type import ObjectType from hydra.errors import MissingConfigException @@ -24,8 +24,9 @@ from motrix_rl import frameworks, runs from motrix_rl.config import TaskConfig -CONFIG_DIR = Path(__file__).resolve().parents[1] / "configs" -RSLRL_BASE_CONFIG = CONFIG_DIR / "algo_base" / "rslrl.ppo.yaml" +ROOT = Path(__file__).resolve().parents[1] +CONFIG_DIR = ROOT / "configs" +RSLRL_BASE_CONFIG = ROOT / "motrix_rl/src/motrix_rl/configs/algo_base/rslrl.ppo.yaml" def _discover_task_specs(): @@ -63,7 +64,7 @@ def _compose_task_option( rllib: str, algo: str, backend: str | None = None, - config_dir=CONFIG_DIR, + config_dir: Path | None = None, overrides: list[str] | None = None, ): option = f"{env}/{rllib}.{algo}" + (f".{backend}" if backend else "") @@ -71,8 +72,14 @@ def _compose_task_option( load_plugins() GlobalHydra.instance().clear() - with initialize_config_dir(version_base=None, config_dir=str(config_dir)): - cfg = compose(config_name="train", overrides=[f"task={option}", *(overrides or [])]) + initializer = ( + initialize_config_module(version_base=None, config_module="motrix_rl.configs") + if config_dir is None + else initialize_config_dir(version_base=None, config_dir=str(config_dir)) + ) + searchpath = [f"hydra.searchpath=[file://{CONFIG_DIR}]"] if config_dir is None else [] + with initializer: + cfg = compose(config_name="train", overrides=[*searchpath, f"task={option}", *(overrides or [])]) result = OmegaConf.to_object(cfg) if not isinstance(result, TaskConfig): raise TypeError(f"Expected TaskConfig, got {type(result).__name__}") diff --git a/wiki/design/architecture.md b/wiki/design/architecture.md index fac4603b..ca10e427 100644 --- a/wiki/design/architecture.md +++ b/wiki/design/architecture.md @@ -96,9 +96,12 @@ Wrapper 是唯一允许同时引用 `motrix_envs` 类型和 RL 框架类型的 系统通过两个注册表实现组件发现,避免硬编码的 import 链: -- **Env Registry(`motrix_env_core.registry`)**:将字符串名称映射到 `(EnvCfg factory, EnvCfg type, {backend: EnvClass})`,并通过 `sim` 参数选择 backend。导入 `motrix_envs` 时注册内置环境。 +- **Env Registry(`motrix_env_core.registry`)**:将字符串名称映射到 `(EnvCfg factory, EnvCfg type, {backend: EnvClass})`,并通过 `sim` 参数选择 backend。首次环境查询时发现 `motrix_env_core.envs` package entry points,调用各提供方的零参数注册函数;内置提供方为 `builtin = "motrix_envs:register"`。提供方以现有装饰器注册环境,每个进程完成一次发现,支持注册回调重入查询;插件冲突或加载失败明确报错。环境发现不依赖 RL 或 Hydra Task 配置。 - **Hydra task config**:通过 `task=/.[.]` 组合环境、算法与运行配置。 +环境发现按 provider 名称排序并在加载前拒绝同名提供方。回调重入查询只看到当前已注册的条目,不启动另一轮发现。 +后续提供方加载或注册失败时,已完成的回调不会重复执行;失败回调的部分注册副作用保留,恢复由提供方负责。 + 注册通过装饰器触发,发生在模块导入时: ```python diff --git a/wiki/design/motrix-deploy.md b/wiki/design/motrix-deploy.md index 641f0c8a..22682ac5 100644 --- a/wiki/design/motrix-deploy.md +++ b/wiki/design/motrix-deploy.md @@ -604,7 +604,7 @@ artifact 生成分为三个职责独立的阶段: deployment export 不得再按 `(rllib, train_backend, algo)` 实现 `export_rslrl_*`、`export_skrl_*` 等平行编排。 新增训练 backend 只增加对应的 ONNX adapter,不修改 profile compiler、artifact reader 或部署控制循环;新增 task/robot/action 语义时,在 `motrix_envs.deploy` 增加 profile compiler,在 `motrix_deploy_tasks` 增加 task 实现, -compiler 通过 import-time 注册接入 profile registry,并直接构造该 task 的具体 spec;task 类通过唯一的 +compiler 由 package 的零参数 provider 回调注册到 profile registry,构建 profile 时直接构造该 task 的具体 spec;task 类通过唯一的 `motrix_deploy.tasks` entry point 接入,如 `"go2_walk/v1" = "motrix_deploy_tasks.tasks.go2_walk:Go2WalkDeployTask"`,由类上的 `spec_type` 同时支持 manifest 解码和运行时构造,不另设 spec 插件。 @@ -876,9 +876,25 @@ CLI 读取 artifact 并实例化具体 `DeployTask`,不在创建 backend runti ## 10. Registry 与扩展机制 -只为确实需要按 artifact、环境或 runtime recipe 选择实现的组件保留 registry。`motrix_deploy.profile` 提供 -profile compiler registry;`motrix_envs.deploy` import 具体 compiler 时通过 -`register_profile_compiler()` 装饰器按环境名自动注册。task runtime 通过 `motrix_deploy.tasks` Python entry-point +只为确实需要按 artifact、环境或 runtime recipe 选择实现的组件保留发现机制。`motrix_deploy.profile` 通过 +`motrix_deploy.profiles` Python entry-point group 发现 package provider,每个 entry point 指向一个零参数注册回调, +名称标识 provider 而不是 Env ID。内置 provider 为 `builtin = "motrix_envs.deploy:register"`,注册 Go2 平地、 +Go2 起伏地形和 G1 dance 的 compiler;外部 package 通过自己的 provider 发布映射。 +回调调用 `register_profile_compiler(env_name, compiler)`,compiler 仍遵循 `(env_name: str) -> DeploymentProfile`, +不在注册阶段创建环境、构建模型或读取 motion。用户打包示例集中在[部署教程](../../docs/source/zh_CN/user_guide/tutorial/advanced/motrix_deploy.md#添加部署-profile-compiler)。 + +首次查询、列举或构建加载所有已安装 provider 并执行注册回调,每个进程完成一次发现。 +`registered_profile_compilers()` 列举的是执行注册后得到的 Env ID,不是 metadata-only 操作; +`build_deployment_profile(env_name)` 只调用该完整环境名对应的 compiler。所有 provider 都需要满足导入依赖, +任一 provider 缺少依赖或加载失败均可阻止发现,与请求的 Env ID 无关。未知环境、重复 provider 名称、 +重复 compiler 环境名及加载/回调失败明确报错,不按 import 顺序覆盖。提供方注册不需要 RL,依赖保持 +`motrix_rl -> motrix_deploy`;通用导出入口不硬编码导入 `motrix_envs`。 + +发现前按 provider 名称排序,并在加载任何回调前检查重名。回调重入查询只看到当前已注册的映射,不启动另一轮发现。 +后续加载或回调失败时,已经完成的 provider 回调不会在重试时重复执行;失败回调留下的部分注册由 provider 负责恢复, +core 不回滚其副作用。 + +task runtime 通过 `motrix_deploy.tasks` Python entry-point group 按版本化 task 标识发现。`available_tasks()` 只读取已安装插件元数据;`load_task_type()` 懒加载唯一 `DeployTask` 类,manifest codec 读取其 `spec_type` 而不实例化 task。`create_task()` 再将已解码的具体 `TaskSpec`、`RobotSpec`、已加载的 `Policy` 和可选 `steps` 传给该类构造完整 task controller。未知名称或重复插件报错,不按 import 顺序选择实现。 @@ -1072,7 +1088,7 @@ realtime scheduler,可通过 `runtime.realtime=false` 关闭实时节奏控制 已安装仿真 recipe 默认选择 MuJoCo;使用同一 task、artifact 和完整场景切换原生 backend: ```bash -motrix-deploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy \ +mxdeploy task=go2-walk-flat/sim artifact=artifacts/go2-walk-flat.deploy \ runtime.backend=motrixsim runtime.viewer=false runtime.realtime=false duration_s=1.0 ``` @@ -1111,20 +1127,20 @@ package 暴露统一入口: ```bash # 只检查 artifact,不创建设备 -motrix-deploy inspect artifact=path/to/go2_walk.deploy +mxdeploy inspect artifact=path/to/go2_walk.deploy # sim2sim 有界无窗口检查 -motrix-deploy task=go2-walk-flat/sim \ +mxdeploy task=go2-walk-flat/sim \ artifact=path/to/go2_walk.deploy \ runtime.viewer=false duration_s=2.0 # 实时可视化 -motrix-deploy task=go2-walk-flat/sim \ +mxdeploy task=go2-walk-flat/sim \ artifact=path/to/go2_walk.deploy \ runtime.viewer=true # 真机入口 -motrix-deploy task=go2-walk-flat/hardware \ +mxdeploy task=go2-walk-flat/hardware \ artifact=path/to/go2_walk.deploy \ runtime.network_interface=enp5s0 ``` @@ -1186,13 +1202,17 @@ artifact 的维度以 policy tensor shapes 为唯一来源。compiler 不恢复 ### 13.4 Deployment export 入口 -统一入口根据环境选择最新的 run 并生成默认 artifact 路径,不暴露训练框架专属子命令: +`motrix_rl` 提供 `mxexport onnx` 和 `mxexport deploy` 子命令;必须显式选择格式,不按训练框架拆分子命令。 +部署导出恰好指定 `run_dir` 或 `env`,后者选择环境的最新 metadata-backed run。默认输出为 `artifacts/.deploy`: ```bash -python scripts/export_deploy.py env=go2-walk-rough +mxexport deploy env=go2-walk-rough +mxexport deploy run_dir=/path/to/run output=artifacts/policy.deploy ``` -入口先调用统一 policy export,再依据 task metadata 选择 deployment profile,最后交给 artifact writer。CLI 和 +导出配置随 `motrix_rl` 发布,项目配置优先、包内配置兜底。依赖方向为 `motrix_rl -> motrix_deploy`;部署运行时 +消费已有 artifact,不反向依赖训练框架。入口通过已安装 provider 注册 compiler,再依据 run metadata 中的环境名构建 deployment profile, +调用统一 policy export,最后交给 artifact writer。CLI 和 公共 service 统一调用 metadata-driven `export_deploy_run`,不暴露同时绑定训练框架与任务的函数。不支持 policy export 时, 错误列出 `RlFramework` 当前支持的组合;缺少 deployment profile 时,错误单独指出 task/profile 不受支持,避免把 模型导出能力和任务部署能力混为一谈。 diff --git a/wiki/design/user-docs-envs-part.md b/wiki/design/user-docs-envs-part.md index e1919dd9..fc0ef7b2 100644 --- a/wiki/design/user-docs-envs-part.md +++ b/wiki/design/user-docs-envs-part.md @@ -200,7 +200,7 @@ overview 表格保留三个稳定字段: 生成流程为: -1. 导入 `motrix_envs`,触发全部内置 Environment 注册; +1. 导入 `motrix_env_core.registry`;首次环境查询通过 `motrix_env_core.envs` entry points 自动调用已安装提供方的注册函数; 2. 调用 `registry.list_registered_envs()` 获取 Env ID,以及从注册 provider docstring 自动提取的双语 description; 3. 校验每个已注册环境都具有英文首行和 `zh_CN:` 行; 4. 使用与 Hydra Task 测试一致的发现规则枚举 `configs/task//` 下的 `rllib.algo[.backend]` 选项;