Reinforcement Learning Training#
This tutorial shows you how to train reinforcement learning agents using EmbodiChain’s RL framework. You’ll learn how to configure training via JSON or YAML, set up environments, policies, and algorithms, and launch training sessions.
Overview#
The RL framework provides a modular, extensible stack for robotics tasks:
Trainer: Orchestrates the training loop (calls algorithm for data collection and updates, handles logging/eval/save)
Algorithm: Controls data collection process (interacts with environment, fills buffer, computes advantages/returns) and updates the policy (e.g., PPO)
Policy: Neural network models implementing a unified interface (get_action/get_value/evaluate_actions)
Buffer: On-policy rollout storage and minibatch iterator (managed by algorithm)
Env Factory: Build environments from a JSON or YAML config via registry
Architecture#
The framework follows a clean separation of concerns:
Trainer: Orchestrates the training loop (calls algorithm for data collection and updates, handles logging/eval/save)
Algorithm: Controls data collection process (interacts with environment, fills buffer, computes advantages/returns) and updates the policy (e.g., PPO)
Policy: Neural network models implementing a unified interface
Buffer: On-policy rollout storage and minibatch iterator (managed by algorithm)
Env Factory: Build environments from a JSON or YAML config via registry
The core components and their relationships:
Trainer → Policy, Env, Algorithm (via callbacks for statistics)
Algorithm → Policy, RolloutBuffer (algorithm manages its own buffer)
Configuration via JSON or YAML#
Training is configured via a JSON or YAML file that defines runtime settings, environment, policy, and algorithm parameters. EmbodiChain loads either format with load_config(). Two environment sources are supported:
trainer.gym_config: simulatorEmbodiedEnvtasks such as CartPole and PushCubetrainer.learning_env: lightweight registered tensor environments such as PointMass
algorithm.name selects the update rule. Standard algorithms (PPO/GRPO) use
Trainer and SyncCollector. Differentiable algorithms (APG) use
DifferentiableTrainer and DifferentiableCollector. Both paths share
evaluate_episodes() for deterministic evaluation metrics under eval/*.
Example Configuration#
Training configurations are task-local: PushCube uses embodichain_tasks/configs/tasks/manipulation/push_cube/agents and CartPole uses embodichain_tasks/configs/tasks/classic_control/cart_pole/agents. PointMass APG/PPO configs live under embodichain_tasks/configs/tasks/classic_control/point_mass/agents:
Example: train_config.json
1{
2 "trainer": {
3 "exp_name": "push_cube_ppo",
4 "gym_config": "embodichain_tasks/configs/tasks/manipulation/push_cube/env.json",
5 "seed": 42,
6 "device": "cuda:0",
7 "headless": true,
8 "gpu_id": 0,
9 "num_envs": 64,
10 "iterations": 1000,
11 "buffer_size": 1024,
12 "enable_eval": true,
13 "num_eval_envs": 16,
14 "num_eval_episodes": 3,
15 "eval_freq": 100,
16 "save_freq": 100,
17 "use_wandb": true,
18 "wandb_project_name": "embodichain-push_cube",
19 "events": {
20 "eval": {
21 "record_camera": {
22 "func": "record_camera_data_async",
23 "mode": "interval",
24 "interval_step": 1,
25 "params": {
26 "name": "main_cam",
27 "resolution": [640, 480],
28 "eye": [-1.4, 1.4, 2.0],
29 "target": [0, 0, 0],
30 "up": [0, 0, 1],
31 "intrinsics": [600, 600, 320, 240],
32 "save_path": "./outputs/videos_ppo1/eval"
33 }
34 }
35 }
36 },
37 "renderer": "hybrid"
38 },
39 "policy": {
40 "name": "actor_critic",
41 "actor": {
42 "type": "mlp",
43 "network_cfg": {
44 "hidden_sizes": [
45 256,
46 256
47 ],
48 "activation": "relu"
49 }
50 },
51 "critic": {
52 "type": "mlp",
53 "network_cfg": {
54 "hidden_sizes": [
55 256,
56 256
57 ],
58 "activation": "relu"
59 }
60 }
61 },
62 "algorithm": {
63 "name": "ppo",
64 "cfg": {
65 "optimizer": {
66 "name": "adam",
67 "learning_rate": 0.0001
68 },
69 "n_epochs": 10,
70 "batch_size": 8192,
71 "gamma": 0.99,
72 "gae_lambda": 0.95,
73 "clip_coef": 0.2,
74 "ent_coef": 0.01,
75 "vf_coef": 0.5,
76 "max_grad_norm": 0.5
77 }
78 }
79}
Example: train_config.yaml (CartPole)
1trainer:
2 exp_name: cart_pole_ppo
3 gym_config: embodichain_tasks/configs/tasks/classic_control/cart_pole/env.yaml
4 seed: 42
5 device: cuda:0
6 headless: true
7 num_envs: 64
8 iterations: 1000
9 buffer_size: 1024
10 eval_freq: 200
11 save_freq: 200
12 use_wandb: false
13 wandb_project_name: embodichain-cart_pole
14 events:
15 eval:
16 record_camera:
17 func: record_camera_data_async
18 mode: interval
19 interval_step: 1
20 params:
21 name: main_cam
22 resolution:
23 - 640
24 - 480
25 eye:
26 - -1.4
27 - 1.4
28 - 2.5
29 target:
30 - 0
31 - 0
32 - 0.7
33 up:
34 - 0
35 - 0
36 - 1
37 intrinsics:
38 - 600
39 - 600
40 - 320
41 - 240
42 save_path: ./outputs/videos/eval
43 renderer: fast-rt
44policy:
45 name: actor_critic
46 actor:
47 type: mlp
48 network_cfg:
49 hidden_sizes:
50 - 256
51 - 256
52 activation: relu
53 critic:
54 type: mlp
55 network_cfg:
56 hidden_sizes:
57 - 256
58 - 256
59 activation: relu
60algorithm:
61 name: ppo
62 cfg:
63 optimizer:
64 name: adam
65 learning_rate: 0.0001
66 n_epochs: 10
67 batch_size: 8192
68 gamma: 0.99
69 gae_lambda: 0.95
70 clip_coef: 0.2
71 ent_coef: 0.01
72 vf_coef: 0.5
73 max_grad_norm: 0.5
Example: train_apg.yaml (PointMass)
1trainer:
2 exp_name: point_mass_apg
3 learning_env:
4 name: PointMassRL
5 cfg:
6 max_episode_steps: 100
7 success_threshold: 0.03
8 seed: 42
9 device: cuda:0
10 num_envs: 64
11 iterations: 500
12 segment_length: 100
13 update_horizon: 100
14 enable_eval: true
15 eval_freq: 32000
16 num_eval_envs: 32
17 num_eval_episodes: 128
18 eval_seed: 10042
19 best_eval_metric: eval/success_rate
20 best_eval_mode: max
21 save_frequency_updates: 50
22 use_wandb: false
23 wandb_project_name: embodichain-point-mass
24
25policy:
26 name: actor_only
27 initial_log_std: -2.0
28 actor:
29 type: mlp
30 network_cfg:
31 hidden_sizes: [64, 64]
32 activation: tanh
33 last_activation: tanh
34 orthogonal_init: [1.414, 1.414, 0.01]
35
36algorithm:
37 name: apg
38 cfg:
39 optimizer:
40 name: adam
41 learning_rate: 0.00025
42 gamma: 0.99
43 max_grad_norm: 0.5
44 ent_coef: 0.0
PointMass uses one differentiable PyTorch dynamics implementation for both APG and PPO. Launch either config with the same CLI:
embodichain train-rl --config embodichain_tasks/configs/tasks/classic_control/point_mass/agents/apg.yaml
embodichain train-rl --config embodichain_tasks/configs/tasks/classic_control/point_mass/agents/ppo.yaml
Configuration Sections#
Runtime Settings#
The trainer section controls experiment setup:
exp_name: Experiment name (used for output directories)
seed: Random seed for reproducibility
device: Runtime device string, e.g.
"cpu"or"cuda:0"headless: Whether to run simulation in headless mode
iterations: Number of training iterations
buffer_size: Steps collected per rollout (e.g., 1024)
eval_freq: Frequency of evaluation (in steps)
save_freq: Frequency of checkpoint saving (in steps)
use_wandb: Whether to enable Weights & Biases logging (set in the config file)
wandb_project_name: Weights & Biases project name
Environment Configuration#
The env section defines the task environment:
id: Environment registry ID (e.g., “PushCubeRL”)
cfg: Environment-specific configuration parameters
For RL environments, use the actions field for action preprocessing and extensions for task-specific parameters:
actions: Action Manager config (e.g., DeltaQposTerm with scale)
extensions: Task-specific parameters (e.g., success_threshold)
Example:
"env": {
"id": "PushCubeRL",
"cfg": {
"num_envs": 4,
"actions": {
"delta_qpos": {
"func": "DeltaQposTerm",
"params": { "scale": 0.1 }
}
},
"extensions": {
"success_threshold": 0.1
}
}
}
Policy Configuration#
The policy section defines the neural network policy:
name: Policy name (e.g., “actor_critic”, “vla”)
action_dim: Optional policy output action dimension. If omitted, it is inferred from
env.action_space.actor: Actor network configuration (required for actor_critic)
critic: Critic network configuration (required for actor_critic)
Example:
"policy": {
"name": "actor_critic",
"actor": {
"type": "mlp",
"network_cfg": {
"hidden_sizes": [256, 256],
"activation": "relu"
}
},
"critic": {
"type": "mlp",
"network_cfg": {
"hidden_sizes": [256, 256],
"activation": "relu"
}
}
}
Algorithm Configuration#
The algorithm section defines the RL algorithm:
name: Algorithm name (e.g., “ppo”, “grpo”)
cfg: Algorithm-specific hyperparameters
PPO example:
"algorithm": {
"name": "ppo",
"cfg": {
"learning_rate": 0.0001,
"n_epochs": 10,
"batch_size": 64,
"gamma": 0.99,
"gae_lambda": 0.95,
"clip_coef": 0.2,
"ent_coef": 0.01,
"vf_coef": 0.5,
"max_grad_norm": 0.5
}
}
GRPO example (for Embodied AI / from-scratch training, e.g. CartPole):
"algorithm": {
"name": "grpo",
"cfg": {
"learning_rate": 0.0001,
"n_epochs": 10,
"batch_size": 8192,
"gamma": 0.99,
"clip_coef": 0.2,
"ent_coef": 0.001,
"kl_coef": 0,
"group_size": 4,
"eps": 1e-8,
"reset_every_rollout": true,
"max_grad_norm": 0.5,
"truncate_at_first_done": true
}
}
For GRPO: use actor_only policy. Set kl_coef=0 for from-scratch training; kl_coef=0.02 for VLA/LLM fine-tuning.
Training Script#
The training script (train.py) is located in embodichain/learning/rl/:
Code for train.py
1# ----------------------------------------------------------------------------
2# Copyright (c) 2021-2026 DexForce Technology Co., Ltd.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15# ----------------------------------------------------------------------------
16
17from __future__ import annotations
18
19import argparse
20import os
21import random
22import time
23from collections.abc import Sequence
24from pathlib import Path
25
26import numpy as np
27import torch
28import wandb
29from torch.utils.tensorboard import SummaryWriter
30from copy import deepcopy
31
32from embodichain.learning.rl.models import build_policy, get_registered_policy_names
33from embodichain.learning.rl.models import build_mlp_from_cfg
34from embodichain.learning.rl.algo import (
35 RolloutKind,
36 build_algo,
37 get_registered_algo_names,
38)
39from embodichain.learning.rl.differentiable_trainer import (
40 DifferentiableTrainer,
41 DifferentiableTrainerCfg,
42)
43from embodichain.learning.rl.env import build_learning_env
44from embodichain.learning.rl.routing import get_trainer_class
45from embodichain.learning.rl.utils import dict_to_tensordict, flatten_dict_observation
46from embodichain.learning.rl.utils.trainer import Trainer
47from embodichain.utils import logger
48from embodichain.lab.gym.utils.registration import (
49 build_env,
50 discover_task_packages,
51 execute_init_hooks,
52)
53from embodichain.lab.gym.utils.gym_utils import config_to_cfg, get_manager_modules
54from embodichain.lab.gym.utils.profiler import EnvProfilerCfg
55from embodichain.utils.utility import load_config
56from embodichain.utils.module_utils import find_function_from_modules
57from embodichain.lab.sim import SimulationManagerCfg
58from embodichain.lab.sim.cfg import RenderCfg
59from embodichain.lab.gym.envs.managers.cfg import EventCfg
60
61
62def _seed_training_rng(seed: int, device: torch.device) -> None:
63 """Seed policy/trainer RNGs without changing deterministic-kernel settings."""
64 random.seed(seed)
65 np.random.seed(seed)
66 torch.manual_seed(seed)
67 if device.type == "cuda":
68 torch.cuda.manual_seed_all(seed)
69
70
71def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
72 """Parse command-line arguments.
73
74 Args:
75 argv: Arguments excluding the command name. Uses ``sys.argv`` when
76 omitted.
77
78 Returns:
79 Parsed training arguments.
80 """
81 parser = argparse.ArgumentParser(
82 prog="embodichain train-rl",
83 description="Train an RL agent from a JSON or YAML config.",
84 )
85 parser.add_argument(
86 "--config",
87 type=str,
88 required=True,
89 help="Path to training config file (.json, .yaml, or .yml).",
90 )
91 parser.add_argument(
92 "--distributed",
93 action=argparse.BooleanOptionalAction,
94 default=None,
95 help="Enable or disable multi-GPU distributed training",
96 )
97 parser.add_argument(
98 "--profile",
99 action="store_true",
100 default=False,
101 help=(
102 "Enable per-section time profiling of gym env reset/step "
103 "(report on env.close()). Requires trainer.gym_config."
104 ),
105 )
106 parser.add_argument(
107 "--profile_output",
108 type=str,
109 default=None,
110 help="Dump the profiling report as JSON on env.close() (requires --profile).",
111 )
112 return parser.parse_args(argv)
113
114
115def _resolve_profile_output(
116 path: str | None,
117 *,
118 rank: int,
119 world_size: int,
120) -> str | None:
121 if path is None or world_size <= 1:
122 return path
123 output = Path(path)
124 return str(output.with_name(f"{output.stem}_rank{rank}{output.suffix}"))
125
126
127def _build_learning_policy(
128 policy_block: dict,
129 env,
130 device: torch.device,
131):
132 obs_dim = int(env.single_observation_space.shape[-1])
133 action_dim = int(env.single_action_space.shape[-1])
134 policy_name = policy_block["name"].lower()
135 actor_cfg = policy_block.get("actor")
136 critic_cfg = policy_block.get("critic")
137 actor = (
138 build_mlp_from_cfg(actor_cfg, obs_dim, action_dim)
139 if actor_cfg is not None
140 else None
141 )
142 critic = (
143 build_mlp_from_cfg(critic_cfg, obs_dim, 1) if critic_cfg is not None else None
144 )
145 policy = build_policy(
146 policy_block,
147 env.single_observation_space,
148 env.single_action_space,
149 device,
150 actor=actor,
151 critic=critic,
152 )
153 if "initial_log_std" in policy_block and hasattr(policy, "log_std"):
154 with torch.no_grad():
155 policy.log_std.fill_(float(policy_block["initial_log_std"]))
156 return policy
157
158
159def _train_learning_env(
160 cfg_data: dict,
161 *,
162 distributed: bool | None,
163 profile: bool = False,
164):
165 """Train a lightweight registered environment through the unified CLI."""
166 if profile:
167 raise ValueError(
168 "--profile requires trainer.gym_config; learning_env is unsupported."
169 )
170 trainer_cfg = cfg_data["trainer"]
171 policy_block = cfg_data["policy"]
172 algorithm_block = cfg_data["algorithm"]
173 distributed = (
174 bool(trainer_cfg.get("distributed", False))
175 if distributed is None
176 else distributed
177 )
178 if distributed:
179 raise ValueError(
180 "Learning environments do not yet support distributed training."
181 )
182
183 discover_task_packages()
184 execute_init_hooks()
185 seed = int(trainer_cfg.get("seed", 1))
186 device = torch.device(trainer_cfg.get("device", "cpu"))
187 if device.type == "cuda" and not torch.cuda.is_available():
188 raise ValueError("CUDA was requested but is not available.")
189 if device.type == "cuda":
190 torch.cuda.set_device(device)
191 torch.cuda.manual_seed_all(seed)
192 np.random.seed(seed)
193 torch.manual_seed(seed)
194
195 env_block = trainer_cfg["learning_env"]
196 if isinstance(env_block, str):
197 env_name = env_block
198 env_cfg = {}
199 else:
200 env_name = env_block["name"]
201 env_cfg = dict(env_block.get("cfg", {}))
202 num_envs = int(trainer_cfg.get("num_envs", 64))
203 env = build_learning_env(
204 env_name,
205 num_envs=num_envs,
206 device=device,
207 **env_cfg,
208 )
209
210 enable_eval = bool(trainer_cfg.get("enable_eval", False))
211 eval_env = None
212 if enable_eval:
213 eval_env = build_learning_env(
214 env_name,
215 num_envs=int(trainer_cfg.get("num_eval_envs", 16)),
216 device=device,
217 **env_cfg,
218 )
219
220 policy = _build_learning_policy(policy_block, env, device)
221 algorithm = build_algo(
222 algorithm_block["name"],
223 dict(algorithm_block.get("cfg", {})),
224 policy,
225 device,
226 )
227 trainer_class = get_trainer_class(algorithm)
228
229 exp_name = trainer_cfg.get("exp_name", f"{env_name}_{algorithm_block['name']}")
230 run_stamp = time.strftime("%Y%m%d_%H%M%S")
231 run_base = Path("outputs") / f"{exp_name}_{run_stamp}"
232 log_dir = run_base / "logs" / exp_name
233 checkpoint_dir = run_base / "checkpoints"
234 checkpoint_dir.mkdir(parents=True, exist_ok=True)
235 writer = SummaryWriter(str(log_dir))
236 use_wandb = bool(trainer_cfg.get("use_wandb", False))
237 if use_wandb:
238 wandb.init(
239 project=trainer_cfg.get("wandb_project_name", "embodichain-generic"),
240 name=exp_name,
241 config=cfg_data,
242 )
243
244 eval_freq = int(trainer_cfg.get("eval_freq", 0)) if enable_eval else 0
245 eval_seed = int(trainer_cfg.get("eval_seed", seed + 10_000))
246 iterations = int(trainer_cfg.get("iterations", 250))
247 try:
248 if trainer_class is DifferentiableTrainer:
249 segment_length = int(trainer_cfg.get("segment_length", 16))
250 update_horizon = int(trainer_cfg.get("update_horizon", segment_length))
251 diff_cfg = DifferentiableTrainerCfg(
252 segment_length=segment_length,
253 update_horizon=update_horizon,
254 deterministic_actions=bool(
255 trainer_cfg.get("deterministic_actions", False)
256 ),
257 checkpoint_dir=str(checkpoint_dir),
258 experiment_name=exp_name,
259 save_frequency_updates=int(
260 trainer_cfg.get("save_frequency_updates", 0)
261 ),
262 eval_frequency_steps=eval_freq,
263 num_eval_episodes=int(trainer_cfg.get("num_eval_episodes", 5)),
264 eval_seed=eval_seed,
265 use_wandb=use_wandb,
266 best_eval_metric=trainer_cfg.get("best_eval_metric", "eval/avg_reward"),
267 best_eval_mode=trainer_cfg.get("best_eval_mode", "max"),
268 )
269 trainer = DifferentiableTrainer(
270 cfg=diff_cfg,
271 env=env,
272 policy=policy,
273 algorithm=algorithm,
274 writer=writer,
275 eval_env=eval_env,
276 )
277 default_steps = iterations * update_horizon * num_envs
278 else:
279 buffer_size = int(
280 trainer_cfg.get("buffer_size", trainer_cfg.get("rollout_steps", 256))
281 )
282 trainer = Trainer(
283 policy=policy,
284 env=env,
285 algorithm=algorithm,
286 buffer_size=buffer_size,
287 batch_size=int(algorithm.cfg.batch_size),
288 writer=writer,
289 eval_freq=eval_freq,
290 save_freq=int(trainer_cfg.get("save_freq", 0)),
291 checkpoint_dir=str(checkpoint_dir),
292 exp_name=exp_name,
293 use_wandb=use_wandb,
294 eval_env=eval_env,
295 num_eval_episodes=int(trainer_cfg.get("num_eval_episodes", 5)),
296 eval_seed=eval_seed,
297 best_eval_metric=trainer_cfg.get("best_eval_metric", "eval/avg_reward"),
298 best_eval_mode=trainer_cfg.get("best_eval_mode", "max"),
299 )
300 default_steps = iterations * buffer_size * num_envs
301 total_timesteps = int(trainer_cfg.get("total_timesteps", default_steps))
302 trainer.train(total_timesteps)
303 trainer.save_checkpoint()
304 return trainer.get_summary()
305 finally:
306 writer.close()
307 if use_wandb:
308 wandb.finish()
309 env.close()
310 if eval_env is not None:
311 eval_env.close()
312
313
314def train_from_config(
315 config_path: str,
316 distributed: bool | None = None,
317 *,
318 profile: bool = False,
319 profile_output: str | None = None,
320):
321 """Run training from a config file path.
322
323 Args:
324 config_path: Path to the training config file (.json, .yaml, or .yml).
325 distributed: If True, run multi-GPU distributed training.
326 If None, use trainer.distributed from config.
327 profile: Enable gym ``EnvProfiler`` on the training environment.
328 profile_output: Optional JSON dump path for the profiling report.
329 """
330 if profile_output is not None and not profile:
331 raise ValueError("--profile_output requires --profile.")
332
333 cfg_data = load_config(config_path)
334
335 trainer_cfg = cfg_data["trainer"]
336 if "learning_env" in trainer_cfg:
337 return _train_learning_env(
338 cfg_data,
339 distributed=distributed,
340 profile=profile,
341 )
342 policy_block = cfg_data["policy"]
343 algo_block = cfg_data["algorithm"]
344
345 if distributed is None:
346 distributed = bool(trainer_cfg.get("distributed", False))
347
348 rank = 0
349 world_size = 1
350 local_rank = 0
351 if distributed:
352 if not torch.distributed.is_available():
353 raise RuntimeError(
354 "Distributed training requested but torch.distributed is not available."
355 )
356 if not torch.cuda.is_available():
357 raise RuntimeError(
358 "Distributed training with NCCL backend requires CUDA, "
359 "but torch.cuda.is_available() is False."
360 )
361 local_rank = int(os.environ.get("LOCAL_RANK", 0))
362 if local_rank < 0 or local_rank >= torch.cuda.device_count():
363 raise ValueError(
364 f"LOCAL_RANK {local_rank} is out of range "
365 f"(available GPUs: {torch.cuda.device_count()})."
366 )
367 torch.cuda.set_device(local_rank)
368 if not torch.distributed.is_initialized():
369 torch.distributed.init_process_group(backend="nccl")
370 rank = torch.distributed.get_rank()
371 world_size = torch.distributed.get_world_size()
372
373 exp_name = trainer_cfg.get("exp_name", "generic_exp")
374 seed = int(trainer_cfg.get("seed", 1))
375 device_str = trainer_cfg.get("device", "cpu")
376 if distributed:
377 device_str = f"cuda:{local_rank}"
378 iterations = int(trainer_cfg.get("iterations", 250))
379 buffer_size = int(
380 trainer_cfg.get("buffer_size", trainer_cfg.get("rollout_steps", 2048))
381 )
382 enable_eval = bool(trainer_cfg.get("enable_eval", False))
383 eval_freq = int(trainer_cfg.get("eval_freq", 10000))
384 save_freq = int(trainer_cfg.get("save_freq", 50000))
385 num_eval_episodes = int(trainer_cfg.get("num_eval_episodes", 5))
386 eval_seed = int(trainer_cfg.get("eval_seed", seed + 10_000))
387 headless = bool(trainer_cfg.get("headless", True))
388 renderer = trainer_cfg.get("renderer", "hybrid")
389 gpu_id = int(trainer_cfg.get("gpu_id", 0))
390 num_envs = trainer_cfg.get("num_envs", None)
391 wandb_project_name = trainer_cfg.get("wandb_project_name", "embodichain-generic")
392
393 if not isinstance(device_str, str):
394 raise ValueError(
395 f"runtime.device must be a string such as 'cpu' or 'cuda:0'. Got: {device_str!r}"
396 )
397 try:
398 device = torch.device(device_str)
399 except RuntimeError as exc:
400 raise ValueError(
401 f"Failed to parse runtime.device='{device_str}': {exc}"
402 ) from exc
403
404 if device.type == "cuda":
405 if not torch.cuda.is_available():
406 raise ValueError(
407 "CUDA device requested but torch.cuda.is_available() is False."
408 )
409 index = (
410 device.index if device.index is not None else torch.cuda.current_device()
411 )
412 device_count = torch.cuda.device_count()
413 if index < 0 or index >= device_count:
414 raise ValueError(
415 f"CUDA device index {index} is out of range (available devices: {device_count})."
416 )
417 torch.cuda.set_device(index)
418 device = torch.device(f"cuda:{index}")
419 elif device.type != "cpu":
420 raise ValueError(f"Unsupported device type: {device}")
421 if rank == 0:
422 logger.log_info(f"Device: {device}")
423 if distributed and rank == 0:
424 logger.log_info(f"Distributed training: world_size={world_size}")
425
426 # Seeds
427 effective_seed = seed + rank
428 _seed_training_rng(effective_seed, device)
429 torch.backends.cudnn.deterministic = True
430
431 # Outputs
432 if distributed:
433 run_stamp = time.strftime("%Y%m%d_%H%M%S") if rank == 0 else None
434 run_stamp_list = [run_stamp]
435 torch.distributed.broadcast_object_list(run_stamp_list, src=0)
436 run_stamp = run_stamp_list[0]
437 else:
438 run_stamp = time.strftime("%Y%m%d_%H%M%S")
439 run_base = os.path.join("outputs", f"{exp_name}_{run_stamp}")
440 log_dir = os.path.join(run_base, "logs")
441 checkpoint_dir = os.path.join(run_base, "checkpoints")
442 if rank == 0:
443 os.makedirs(log_dir, exist_ok=True)
444 os.makedirs(checkpoint_dir, exist_ok=True)
445 writer = SummaryWriter(f"{log_dir}/{exp_name}") if rank == 0 else None
446
447 # Initialize Weights & Biases (optional)
448 use_wandb = trainer_cfg.get("use_wandb", False)
449 if use_wandb and rank == 0:
450 wandb.init(project=wandb_project_name, name=exp_name, config=cfg_data)
451
452 gym_config_path = Path(trainer_cfg["gym_config"])
453 if rank == 0:
454 logger.log_info(f"Current working directory: {Path.cwd()}")
455
456 gym_config_data = load_config(str(gym_config_path))
457 gym_env_cfg = config_to_cfg(gym_config_data, manager_modules=get_manager_modules())
458 gym_env_cfg.seed = effective_seed
459 if num_envs is not None:
460 gym_env_cfg.num_envs = int(num_envs)
461
462 # Ensure sim configuration mirrors runtime overrides
463 if gym_env_cfg.sim_cfg is None:
464 gym_env_cfg.sim_cfg = SimulationManagerCfg()
465 if device.type == "cuda":
466 gpu_index = device.index
467 if gpu_index is None:
468 gpu_index = torch.cuda.current_device()
469 gym_env_cfg.sim_cfg.sim_device = torch.device(f"cuda:{gpu_index}")
470 if hasattr(gym_env_cfg.sim_cfg, "gpu_id"):
471 gym_env_cfg.sim_cfg.gpu_id = gpu_index
472 else:
473 gym_env_cfg.sim_cfg.sim_device = torch.device("cpu")
474 gym_env_cfg.sim_cfg.headless = headless
475 gym_env_cfg.sim_cfg.render_cfg = RenderCfg(renderer=renderer)
476 gym_env_cfg.sim_cfg.gpu_id = gpu_id
477 if profile:
478 gym_env_cfg.profiler = EnvProfilerCfg(
479 enable_time=True,
480 output_path=_resolve_profile_output(
481 profile_output,
482 rank=rank,
483 world_size=world_size,
484 ),
485 )
486 if rank == 0:
487 logger.log_info(
488 f"Loaded gym_config from {gym_config_path} (env_id={gym_config_data['id']}, num_envs={gym_env_cfg.num_envs}, headless={gym_env_cfg.sim_cfg.headless}, renderer={gym_env_cfg.sim_cfg.render_cfg.renderer}, sim_device={gym_env_cfg.sim_cfg.sim_device})"
489 )
490
491 env = build_env(gym_config_data["id"], base_env_cfg=gym_env_cfg)
492 sample_obs, _ = env.reset(seed=effective_seed)
493 sample_obs_td = dict_to_tensordict(sample_obs, device)
494 obs_dim = flatten_dict_observation(sample_obs_td).shape[-1]
495 flat_obs_space = env.flattened_observation_space
496
497 # Create evaluation environment only if enabled
498 eval_env = None
499 num_eval_envs = trainer_cfg.get("num_eval_envs", 4)
500 if enable_eval and rank == 0:
501 eval_gym_env_cfg = deepcopy(gym_env_cfg)
502 eval_gym_env_cfg.num_envs = num_eval_envs
503 eval_gym_env_cfg.seed = eval_seed
504 eval_gym_env_cfg.sim_cfg.headless = True
505 eval_gym_env_cfg.profiler = None
506 eval_env = build_env(gym_config_data["id"], base_env_cfg=eval_gym_env_cfg)
507 logger.log_info(
508 f"Evaluation environment created (num_envs={num_eval_envs}, headless=True)"
509 )
510
511 # Environment construction intentionally uses task/evaluation seeds. Reset
512 # the trainer stream so policy initialization is independent of scene work.
513 _seed_training_rng(effective_seed, device)
514
515 # Build Policy via registry
516 policy_name = policy_block["name"]
517 env_action_dim = (
518 env.get_wrapper_attr("action_manager").total_action_dim
519 if env.get_wrapper_attr("action_manager") is not None
520 else len(env.get_wrapper_attr("active_joint_ids"))
521 )
522 action_dim = policy_block.get("action_dim", env_action_dim)
523 action_dim = int(action_dim)
524 if action_dim != env_action_dim:
525 raise ValueError(
526 f"Configured policy.action_dim={action_dim} does not match env action dim {env_action_dim}."
527 )
528 # Build Policy via registry (actor/critic must be explicitly defined in JSON when using actor_critic/actor_only)
529 if policy_name.lower() == "actor_critic":
530 actor_cfg = policy_block.get("actor")
531 critic_cfg = policy_block.get("critic")
532 if actor_cfg is None or critic_cfg is None:
533 raise ValueError(
534 "ActorCritic requires 'actor' and 'critic' definitions in JSON (policy.actor / policy.critic)."
535 )
536
537 actor = build_mlp_from_cfg(actor_cfg, obs_dim, action_dim)
538 critic = build_mlp_from_cfg(critic_cfg, obs_dim, 1)
539
540 policy = build_policy(
541 policy_block,
542 flat_obs_space,
543 env.action_space,
544 device,
545 actor=actor,
546 critic=critic,
547 )
548 elif policy_name.lower() == "actor_only":
549 actor_cfg = policy_block.get("actor")
550 if actor_cfg is None:
551 raise ValueError(
552 "ActorOnly requires 'actor' definition in JSON (policy.actor)."
553 )
554
555 actor = build_mlp_from_cfg(actor_cfg, obs_dim, action_dim)
556
557 policy = build_policy(
558 policy_block,
559 flat_obs_space,
560 env.action_space,
561 device,
562 actor=actor,
563 )
564 else:
565 policy = build_policy(
566 policy_block, env.observation_space, env.action_space, device
567 )
568
569 # Build Algorithm via factory
570 algo_name = algo_block["name"].lower()
571 algo_cfg = algo_block["cfg"]
572 algo = build_algo(
573 algo_name,
574 algo_cfg,
575 policy,
576 device,
577 distributed=distributed,
578 )
579 if algo.rollout_kind is RolloutKind.DIFFERENTIABLE:
580 raise ValueError(
581 "Differentiable algorithms require trainer.learning_env; "
582 "simulator gym_config environments use standard rollouts."
583 )
584
585 # Build Trainer
586 event_modules = [
587 "embodichain.lab.gym.envs.managers.randomization",
588 "embodichain.lab.gym.envs.managers.record",
589 "embodichain.lab.gym.envs.managers.events",
590 ]
591 events_dict = trainer_cfg.get("events", {})
592 train_event_cfg = {}
593 eval_event_cfg = {}
594 # Parse train events
595 for event_name, event_info in events_dict.get("train", {}).items():
596 event_func_str = event_info.get("func")
597 mode = event_info.get("mode", "interval")
598 params = event_info.get("params", {})
599 interval_step = event_info.get("interval_step", 1)
600 event_func = find_function_from_modules(
601 event_func_str, event_modules, raise_if_not_found=True
602 )
603 train_event_cfg[event_name] = EventCfg(
604 func=event_func,
605 mode=mode,
606 params=params,
607 interval_step=interval_step,
608 is_global=event_info.get("is_global", False),
609 )
610 # Parse eval events (only if evaluation is enabled)
611 if enable_eval:
612 for event_name, event_info in events_dict.get("eval", {}).items():
613 event_func_str = event_info.get("func")
614 mode = event_info.get("mode", "interval")
615 params = event_info.get("params", {})
616 interval_step = event_info.get("interval_step", 1)
617 event_func = find_function_from_modules(
618 event_func_str, event_modules, raise_if_not_found=True
619 )
620 eval_event_cfg[event_name] = EventCfg(
621 func=event_func,
622 mode=mode,
623 params=params,
624 interval_step=interval_step,
625 is_global=event_info.get("is_global", False),
626 )
627 trainer = Trainer(
628 policy=policy,
629 env=env,
630 algorithm=algo,
631 buffer_size=buffer_size,
632 batch_size=algo_cfg["batch_size"],
633 writer=writer,
634 eval_freq=eval_freq if enable_eval else 0, # Disable eval if not enabled
635 save_freq=save_freq,
636 checkpoint_dir=checkpoint_dir,
637 exp_name=exp_name,
638 use_wandb=use_wandb,
639 eval_env=eval_env, # None if enable_eval=False
640 event_cfg=train_event_cfg,
641 eval_event_cfg=eval_event_cfg if (enable_eval and rank == 0) else {},
642 num_eval_episodes=num_eval_episodes,
643 distributed=distributed,
644 rank=rank,
645 world_size=world_size,
646 eval_seed=eval_seed,
647 best_eval_metric=trainer_cfg.get("best_eval_metric", "eval/avg_reward"),
648 best_eval_mode=trainer_cfg.get("best_eval_mode", "max"),
649 )
650
651 if rank == 0:
652 logger.log_info("Generic training initialized")
653 logger.log_info(f"Task: {type(env).__name__}")
654 logger.log_info(
655 f"Policy: {policy_name} (available: {get_registered_policy_names()})"
656 )
657 logger.log_info(
658 f"Algorithm: {algo_name} (available: {get_registered_algo_names()})"
659 )
660
661 total_steps = int(iterations * buffer_size * env.num_envs * world_size)
662 if rank == 0:
663 logger.log_info(
664 f"Total steps: {total_steps} (iterations≈{iterations}, world_size={world_size})"
665 )
666
667 try:
668 trainer.train(total_steps)
669 except KeyboardInterrupt:
670 if rank == 0:
671 logger.log_info("Training interrupted by user")
672 finally:
673 trainer.save_checkpoint()
674 if writer is not None:
675 writer.close()
676 if use_wandb and rank == 0:
677 try:
678 wandb.finish()
679 except Exception:
680 pass
681
682 # Clean up environments to prevent resource leaks
683 try:
684 if env is not None:
685 env.close()
686 except Exception as e:
687 if rank == 0:
688 logger.log_warning(f"Failed to close training environment: {e}")
689
690 try:
691 if eval_env is not None:
692 eval_env.close()
693 except Exception as e:
694 if rank == 0:
695 logger.log_warning(f"Failed to close evaluation environment: {e}")
696
697 if distributed and torch.distributed.is_initialized():
698 torch.distributed.destroy_process_group()
699
700 if rank == 0:
701 logger.log_info("Training finished")
702
703
704def cli(argv: Sequence[str] | None = None) -> None:
705 """Command-line interface for RL training.
706
707 Parses CLI arguments and launches training from a config file.
708
709 Task packages are discovered (and init hooks executed) before training so
710 that task environments registered in separate packages (e.g.
711 ``embodichain_tasks``) are available to ``build_env``. This mirrors the
712 ``run_env`` CLI.
713 """
714 args = parse_args(argv)
715
716 # Discover all installed task packages and run init hooks (register custom
717 # manager modules / asset resolvers) before building any environment.
718 discover_task_packages()
719 execute_init_hooks()
720
721 train_from_config(
722 args.config,
723 distributed=args.distributed,
724 profile=args.profile,
725 profile_output=args.profile_output,
726 )
727
728
729if __name__ == "__main__":
730 cli()
731
732
733__all__ = ["cli", "parse_args", "train_from_config"]
The Script Explained#
The training script performs the following steps:
Parse Configuration: Loads the config file (
.json,.yaml, or.yml) and extracts runtime/env/policy/algorithm blocksSetup: Initializes device, seeds, output directories, TensorBoard, and Weights & Biases
Build Components: - Environment via
build_env()factory - Policy viabuild_policy()registry - Algorithm viabuild_algo()factoryCreate Trainer: Instantiates the
Trainerwith all componentsTrain: Runs the training loop until completion
Launching Training#
To start training, run:
embodichain train-rl --config embodichain_tasks/configs/tasks/classic_control/cart_pole/agents/ppo.yaml
JSON configs are also supported:
embodichain train-rl --config embodichain_tasks/configs/tasks/manipulation/push_cube/agents/ppo.json
Outputs#
All outputs are written to ./outputs/<exp_name>_<timestamp>/:
logs/: TensorBoard logs
checkpoints/: Model checkpoints
Training Process#
The training process follows this sequence:
Rollout Phase:
SyncCollectorinteracts with the environment and writes policy-side fields into a shared rolloutTensorDictwith uniform[N, T + 1]layout.EmbodiedEnvwrites environment-side step fields such asreward,done,terminated, andtruncatedinto the same rollout viaset_rollout_buffer(). The final slot of transition-only fields is reserved as padding, whileobs[:, -1]andvalue[:, -1]remain valid bootstrap data.Advantage/Return Computation: Algorithm computes advantages and returns from the collected rollout (e.g. GAE for PPO, step-wise group normalization for GRPO) and converts it to a transition-aligned view over the valid first
Tsteps before minibatch optimization.Update Phase: Algorithm updates the policy with
update(rollout)Logging: Trainer logs training losses and aggregated metrics to TensorBoard and Weights & Biases
Evaluation (periodic): Trainer evaluates the current policy
Checkpointing (periodic): Trainer saves model checkpoints
Policy Interface#
All policies must inherit from the Policy abstract base class:
from abc import ABC, abstractmethod
import torch.nn as nn
class Policy(nn.Module, ABC):
device: torch.device
def get_action(self, tensordict, deterministic: bool = False):
"""Samples action, sample_log_prob, and value into the TensorDict."""
...
@abstractmethod
def forward(self, tensordict, deterministic: bool = False):
"""Writes action, sample_log_prob, and value into the TensorDict."""
raise NotImplementedError
@abstractmethod
def get_value(self, tensordict):
"""Writes value estimate into the TensorDict."""
raise NotImplementedError
@abstractmethod
def evaluate_actions(self, tensordict):
"""Returns a new TensorDict with log_prob, entropy, and value."""
raise NotImplementedError
Available Policies#
ActorCritic: MLP-based Gaussian policy with learnable log_std. Requires external
actorandcriticmodules to be provided (defined in the training config file). Used with PPO.ActorOnly: Actor-only policy without Critic. Used with GRPO (group-relative advantage estimation).
VLAPlaceholderPolicy: Placeholder for Vision-Language-Action policies
Algorithms#
Available Algorithms#
PPO: Proximal Policy Optimization with GAE
GRPO: Group Relative Policy Optimization (no Critic, step-wise returns, masked group normalization). Use
actor_onlypolicy. Setkl_coef=0for from-scratch training (CartPole, dense reward);kl_coef=0.02for VLA/LLM fine-tuning.
Adding a New Algorithm#
To add a new algorithm:
Create a new algorithm class in
embodichain/learning/rl/algo/Implement
update(rollout)and consume the shared rolloutTensorDictRegister in
algo/__init__.py:
from tensordict import TensorDict
from embodichain.learning.rl.algo import BaseAlgorithm, register_algo
@register_algo("my_algo")
class MyAlgorithm(BaseAlgorithm):
def __init__(self, cfg, policy):
self.cfg = cfg
self.policy = policy
self.device = torch.device(cfg.device)
def update(self, rollout: TensorDict):
"""Update the policy using a collected rollout."""
# compute advantages / returns from rollout
# optimize policy parameters
return {"loss": 0.0}
Adding a New Policy#
To add a new policy:
Create a new policy class inheriting from the
Policyabstract base classRegister in
models/__init__.py:
from embodichain.learning.rl.models import register_policy, Policy
@register_policy("my_policy")
class MyPolicy(Policy):
def __init__(self, obs_dim, action_dim, device, config):
super().__init__()
self.device = device
# Initialize your networks here
def get_action(self, tensordict, deterministic=False):
...
def forward(self, tensordict, deterministic=False):
...
def get_value(self, tensordict):
...
def evaluate_actions(self, tensordict):
...
Current built-in MLP policies use flattened observations in the training path. If your policy requires structured or multi-modal inputs, keep the richer obs_space interface and define a matching rollout/collector schema.
Adding a New Environment#
To add a new RL environment:
Create an environment class inheriting from
EmbodiedEnv(with Action Manager configured for action preprocessing and standardized info structure):
from embodichain.lab.gym.envs import EmbodiedEnv, EmbodiedEnvCfg
from embodichain.lab.gym.utils.registration import register_env
import torch
@register_env("MyTaskRL", override=True)
class MyTaskEnv(EmbodiedEnv):
def __init__(self, cfg: EmbodiedEnvCfg = None, **kwargs):
super().__init__(cfg, **kwargs)
def compute_task_state(self, **kwargs):
"""Compute success/failure conditions and metrics."""
is_success = ... # Define success condition
is_fail = torch.zeros_like(is_success)
metrics = {"distance": ..., "error": ...}
return is_success, is_fail, metrics
Configure the environment in your config file with
actionsandextensions:
"env": {
"id": "MyTaskRL",
"cfg": {
"num_envs": 4,
"actions": {
"delta_qpos": {
"func": "DeltaQposTerm",
"params": { "scale": 0.1 }
}
},
"extensions": {
"success_threshold": 0.05
}
}
}
The EmbodiedEnv with Action Manager provides:
Action Preprocessing: Configurable via
actions(DeltaQposTerm, QposTerm, EefPoseTerm, etc.)Standardized Info: Implements
get_info()usingcompute_task_state()template method
Best Practices#
Use EmbodiedEnv with Action Manager for RL Tasks: Inherit from
EmbodiedEnvand configureactionsin your config. The Action Manager handles action preprocessing (delta_qpos, qpos, qvel, qf, eef_pose) in a modular way.Action Configuration: Use the
actionsfield in your config file. Example:"delta_qpos": {"func": "DeltaQposTerm", "params": {"scale": 0.1}}.Device Management: Device is single-sourced from
runtime.cuda. All components (trainer/algorithm/policy/env) share the same device.Observation Format: Environments should provide consistent observation shape/types (torch.float32) and a single
done = terminated | truncated.Algorithm Interface: Algorithms implement
update(rollout)and consume a shared rolloutTensorDict. Collection is handled bySyncCollectorplus environment-side rollout writes inEmbodiedEnv.Reward Configuration: Use the
RewardManagerin your environment config to define reward components. Organize reward components ininfo["rewards"]dictionary and metrics ininfo["metrics"]dictionary. The trainer performs dense per-step logging directly from environment info.Template Methods: Override
compute_task_state()to define success/failure conditions and metrics. Overridecheck_truncated()for custom truncation logic.Configuration: Use JSON for all hyperparameters. This makes experiments reproducible and easy to track.
Logging: Metrics are automatically logged to TensorBoard and Weights & Biases. Check
outputs/<exp_name>/logs/for TensorBoard logs.Checkpoints: Regular checkpoints are saved to
outputs/<exp_name>/checkpoints/. Use these to resume training or evaluate policies.
See Also#
Reinforcement Learning — RL module architecture and component reference
Embodied Environments — EmbodiedEnv configuration and Action Manager
Creating a Basic Environment — Creating basic Gymnasium environments
Creating a Modular Environment — Advanced modular environments with managers
Supported Tasks — List of available RL task environments