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
Example: train_config.yaml (CartPole)
Example: train_apg.yaml (PointMass)
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: Ordered flat Action Manager config (for example,
RelativeJointPositionActionwith a selected control part and scale)extensions: Task-specific parameters (e.g., success_threshold)
Example:
"env": {
"id": "PushCubeRL",
"cfg": {
"num_envs": 4,
"actions": {
"delta_qpos": {
"func": "RelativeJointPositionAction",
"params": { "part_name": "arm", "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 json
21import os
22import random
23import time
24from collections.abc import Sequence
25from copy import deepcopy
26from pathlib import Path
27
28import numpy as np
29import torch
30import wandb
31import yaml
32from torch.utils.tensorboard import SummaryWriter
33
34from embodichain.learning.rl.checkpoint import (
35 _atomic_write,
36 _resolve_resume,
37 _sync_errors,
38 _validate_saved_files,
39)
40from embodichain.learning.rl.policies import get_registered_policy_names
41from embodichain.learning.rl.policy_evaluation.manifest import (
42 write_run_manifest,
43)
44from embodichain.learning.rl.algo import (
45 RolloutKind,
46 build_algo,
47 get_registered_algo_names,
48)
49from embodichain.learning.rl.differentiable_trainer import (
50 DifferentiableTrainer,
51 DifferentiableTrainerCfg,
52)
53from embodichain.learning.rl.runtime import (
54 _close_environment,
55 _build_learning_environment,
56 _build_learning_policy,
57 build_gym_policy_runtime,
58 build_learning_policy_runtime,
59)
60from embodichain.learning.rl.routing import get_trainer_class
61from embodichain.learning.rl.utils.trainer import Trainer
62from embodichain.utils import logger, set_seed
63from embodichain.lab.gym.utils.registration import (
64 build_env,
65 discover_task_packages,
66 execute_init_hooks,
67)
68from embodichain.lab.gym.utils.profiler import EnvProfilerCfg
69from embodichain.utils.utility import load_config
70from embodichain.utils.module_utils import find_function_from_modules
71from embodichain.lab.gym.envs.managers.cfg import EventCfg
72
73_CAMERA_RECORDERS = {"record_camera_data", "record_camera_data_async"}
74
75
76def _seed_training_rng(seed: int, device: torch.device) -> None:
77 """Seed policy/trainer RNGs without changing deterministic-kernel settings."""
78 random.seed(seed)
79 np.random.seed(seed)
80 torch.manual_seed(seed)
81 if device.type == "cuda":
82 torch.cuda.manual_seed_all(seed)
83
84
85def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
86 """Parse command-line arguments.
87
88 Args:
89 argv: Arguments excluding the command name. Uses ``sys.argv`` when
90 omitted.
91
92 Returns:
93 Parsed training arguments.
94 """
95 parser = argparse.ArgumentParser(
96 prog="embodichain train-rl",
97 description="Train an RL agent from a JSON or YAML config.",
98 )
99 parser.add_argument(
100 "--config",
101 type=str,
102 required=True,
103 help="Path to training config file (.json, .yaml, or .yml).",
104 )
105 parser.add_argument(
106 "--distributed",
107 action=argparse.BooleanOptionalAction,
108 default=None,
109 help="Enable or disable multi-GPU distributed training",
110 )
111 parser.add_argument(
112 "--profile",
113 action="store_true",
114 default=False,
115 help=(
116 "Enable per-section time profiling of gym env reset/step "
117 "(report on env.close()). Requires trainer.gym_config."
118 ),
119 )
120 parser.add_argument(
121 "--profile_output",
122 type=str,
123 default=None,
124 help="Dump the profiling report as JSON on env.close() (requires --profile).",
125 )
126 parser.add_argument(
127 "--resume",
128 type=str,
129 default=None,
130 help="Resume a checkpoint file, or latest/best selected from --run.",
131 )
132 parser.add_argument(
133 "--run",
134 type=str,
135 default=None,
136 help="Source run directory for --resume latest/best.",
137 )
138 parser.add_argument(
139 "--resume-scheduler",
140 choices=("restore", "restart"),
141 default="restore",
142 help="Restore the saved LR curve, or restart it with the current config.",
143 )
144 args = parser.parse_args(argv)
145 if args.resume in {"latest", "best"} and args.run is None:
146 parser.error("--resume latest/best requires --run")
147 if args.run is not None and args.resume not in {"latest", "best"}:
148 parser.error("--run requires --resume latest/best")
149 if args.resume is None and args.resume_scheduler != "restore":
150 parser.error("--resume-scheduler requires --resume")
151 return args
152
153
154def _resolve_profile_output(
155 path: str | None,
156 *,
157 rank: int,
158 world_size: int,
159) -> str | None:
160 if path is None or world_size <= 1:
161 return path
162 output = Path(path)
163 return str(output.with_name(f"{output.stem}_rank{rank}{output.suffix}"))
164
165
166def _event_params(
167 event_info: dict,
168 *,
169 run_base: str | Path,
170 phase: str,
171) -> dict:
172 """Place default camera recordings under the current training run."""
173 params = dict(event_info.get("params", {}))
174 function_name = str(event_info.get("func", "")).rsplit(".", 1)[-1]
175 if function_name in _CAMERA_RECORDERS:
176 params.setdefault("save_path", str(Path(run_base) / "videos" / phase))
177 return params
178
179
180def _train_learning_env(
181 cfg_data: dict,
182 *,
183 config_path: str | Path,
184 distributed: bool | None,
185 profile: bool = False,
186 resume: str | Path | None = None,
187 run: str | Path | None = None,
188 resume_scheduler: str = "restore",
189) -> dict[str, object]:
190 """Train a lightweight registered environment through the unified CLI."""
191 if profile:
192 raise ValueError(
193 "--profile requires trainer.gym_config; learning_env is unsupported."
194 )
195 trainer_cfg = cfg_data["trainer"]
196 algorithm_block = cfg_data["algorithm"]
197 distributed = (
198 bool(trainer_cfg.get("distributed", False))
199 if distributed is None
200 else distributed
201 )
202 if distributed:
203 raise ValueError(
204 "Learning environments do not yet support distributed training."
205 )
206
207 discover_task_packages()
208 execute_init_hooks()
209 seed = int(trainer_cfg.get("seed", 1))
210 device = torch.device(trainer_cfg.get("device", "cpu"))
211 if device.type == "cuda" and not torch.cuda.is_available():
212 raise ValueError("CUDA was requested but is not available.")
213 if device.type == "cuda":
214 torch.cuda.set_device(device)
215 set_seed(
216 seed,
217 deterministic=bool(trainer_cfg.get("torch_deterministic", False)),
218 )
219
220 num_envs = int(trainer_cfg.get("num_envs", 64))
221 runtime = build_learning_policy_runtime(
222 cfg_data,
223 num_envs=num_envs,
224 device=device,
225 )
226 env = runtime.env
227 policy = runtime.policy
228 env_name = runtime.env_id
229
230 enable_eval = bool(trainer_cfg.get("enable_eval", False))
231 eval_env = None
232 if enable_eval:
233 _eval_name, eval_env = _build_learning_environment(
234 cfg_data,
235 num_envs=int(trainer_cfg.get("num_eval_envs", 16)),
236 device=device,
237 )
238
239 algorithm = build_algo(
240 algorithm_block["name"],
241 dict(algorithm_block.get("cfg", {})),
242 policy,
243 device,
244 )
245 trainer_class = get_trainer_class(algorithm)
246
247 exp_name = trainer_cfg.get("exp_name", f"{env_name}_{algorithm_block['name']}")
248 run_stamp = f"{time.strftime('%Y%m%d_%H%M%S')}_{time.time_ns() % 1_000_000_000:09d}"
249 run_base = Path("outputs") / f"{exp_name}_{run_stamp}"
250 log_dir = run_base / "logs" / exp_name
251 checkpoint_dir = run_base / "checkpoints"
252 checkpoint_dir.mkdir(parents=True, exist_ok=True)
253 writer = SummaryWriter(str(log_dir))
254 use_wandb = bool(trainer_cfg.get("use_wandb", False))
255 if use_wandb:
256 wandb.init(
257 project=trainer_cfg.get("wandb_project_name", "embodichain-generic"),
258 name=exp_name,
259 config=cfg_data,
260 )
261
262 eval_freq = int(trainer_cfg.get("eval_freq", 0)) if enable_eval else 0
263 eval_seed = int(trainer_cfg.get("eval_seed", seed + 10_000))
264 iterations = int(trainer_cfg.get("iterations", 250))
265 try:
266 if trainer_class is DifferentiableTrainer:
267 segment_length = int(trainer_cfg.get("segment_length", 16))
268 update_horizon = int(trainer_cfg.get("update_horizon", segment_length))
269 diff_cfg = DifferentiableTrainerCfg(
270 segment_length=segment_length,
271 update_horizon=update_horizon,
272 rollout_mode=str(trainer_cfg.get("rollout_mode", "segmented")),
273 gradient_accumulation_steps=int(
274 trainer_cfg.get("gradient_accumulation_steps", 1)
275 ),
276 deterministic_actions=bool(
277 trainer_cfg.get("deterministic_actions", False)
278 ),
279 clip_actions_to_space=bool(
280 trainer_cfg.get("clip_actions_to_space", False)
281 ),
282 action_adjoint_max_norm=float(
283 trainer_cfg.get("action_adjoint_max_norm", 0.0)
284 ),
285 normalize_observations=bool(
286 trainer_cfg.get("normalize_observations", False)
287 ),
288 rollout_seed=seed,
289 checkpoint_dir=str(checkpoint_dir),
290 experiment_name=exp_name,
291 save_frequency_updates=int(
292 trainer_cfg.get("save_frequency_updates", 0)
293 ),
294 eval_frequency_steps=eval_freq,
295 num_eval_episodes=int(trainer_cfg.get("num_eval_episodes", 5)),
296 eval_seed=eval_seed,
297 use_wandb=use_wandb,
298 best_eval_metric=trainer_cfg.get("best_eval_metric", "eval/avg_reward"),
299 best_eval_mode=trainer_cfg.get("best_eval_mode", "max"),
300 )
301 trainer = DifferentiableTrainer(
302 cfg=diff_cfg,
303 env=env,
304 policy=policy,
305 algorithm=algorithm,
306 writer=writer,
307 eval_env=eval_env,
308 )
309 default_steps = (
310 iterations
311 * update_horizon
312 * num_envs
313 * diff_cfg.gradient_accumulation_steps
314 )
315 else:
316 buffer_size = int(
317 trainer_cfg.get("buffer_size", trainer_cfg.get("rollout_steps", 256))
318 )
319 trainer = Trainer(
320 policy=policy,
321 env=env,
322 algorithm=algorithm,
323 buffer_size=buffer_size,
324 batch_size=int(algorithm.cfg.batch_size),
325 writer=writer,
326 eval_freq=eval_freq,
327 save_freq=int(trainer_cfg.get("save_freq", 0)),
328 checkpoint_dir=str(checkpoint_dir),
329 exp_name=exp_name,
330 use_wandb=use_wandb,
331 eval_env=eval_env,
332 num_eval_episodes=int(trainer_cfg.get("num_eval_episodes", 5)),
333 eval_seed=eval_seed,
334 best_eval_metric=trainer_cfg.get("best_eval_metric", "eval/avg_reward"),
335 best_eval_mode=trainer_cfg.get("best_eval_mode", "max"),
336 save_frequency_updates=int(
337 trainer_cfg.get("save_frequency_updates", 0)
338 ),
339 )
340 default_steps = iterations * buffer_size * num_envs
341 _configure_checkpoints(trainer, cfg_data, config_path, run_base)
342 _resume_training(trainer, resume, run, resume_scheduler)
343 if (
344 trainer_class is DifferentiableTrainer
345 and diff_cfg.rollout_mode == "complete"
346 and "total_timesteps" not in trainer_cfg
347 ):
348 trainer.train(total_updates=iterations)
349 else:
350 total_timesteps = int(trainer_cfg.get("total_timesteps", default_steps))
351 trainer.train(total_timesteps)
352 _save_final_checkpoint(trainer)
353 summary = trainer.get_summary()
354 finally:
355 writer.close()
356 if use_wandb:
357 wandb.finish()
358 env.close()
359 if eval_env is not None:
360 eval_env.close()
361 return summary
362
363
364def train_from_config(
365 config_path: str,
366 distributed: bool | None = None,
367 *,
368 profile: bool = False,
369 profile_output: str | None = None,
370 resume: str | Path | None = None,
371 run: str | Path | None = None,
372 resume_scheduler: str = "restore",
373) -> dict[str, object] | None:
374 """Run training from a config file path.
375
376 Args:
377 config_path: Path to the training config file (.json, .yaml, or .yml).
378 distributed: If True, run multi-GPU distributed training.
379 If None, use trainer.distributed from config.
380 profile: Enable gym ``EnvProfiler`` on the training environment.
381 profile_output: Optional JSON dump path for the profiling report.
382 resume: Checkpoint file, or latest/best selected from run.
383 run: Source run directory when resume selects latest/best.
384 resume_scheduler: Restore the saved LR curve or restart the configured
385 curve with optimizer momentum retained.
386
387 Returns:
388 The lightweight trainer summary, or ``None`` for simulator training.
389 """
390 if profile_output is not None and not profile:
391 raise ValueError("--profile_output requires --profile.")
392
393 if resume_scheduler not in {"restore", "restart"}:
394 raise ValueError("resume_scheduler must be 'restore' or 'restart'.")
395 if resume is None and resume_scheduler != "restore":
396 raise ValueError("resume_scheduler restart requires resume.")
397 if resume is None and run is not None:
398 raise ValueError("run requires resume latest/best.")
399 cfg_data = load_config(config_path)
400
401 trainer_cfg = cfg_data["trainer"]
402 if "learning_env" in trainer_cfg:
403 return _train_learning_env(
404 cfg_data,
405 config_path=config_path,
406 distributed=distributed,
407 profile=profile,
408 resume=resume,
409 run=run,
410 resume_scheduler=resume_scheduler,
411 )
412 policy_block = cfg_data["policy"]
413 algo_block = cfg_data["algorithm"]
414
415 if distributed is None:
416 distributed = bool(trainer_cfg.get("distributed", False))
417
418 rank = 0
419 world_size = 1
420 local_rank = 0
421 if distributed:
422 if not torch.distributed.is_available():
423 raise RuntimeError(
424 "Distributed training requested but torch.distributed is not available."
425 )
426 if not torch.cuda.is_available():
427 raise RuntimeError(
428 "Distributed training with NCCL backend requires CUDA, "
429 "but torch.cuda.is_available() is False."
430 )
431 local_rank = int(os.environ.get("LOCAL_RANK", 0))
432 if local_rank < 0 or local_rank >= torch.cuda.device_count():
433 raise ValueError(
434 f"LOCAL_RANK {local_rank} is out of range "
435 f"(available GPUs: {torch.cuda.device_count()})."
436 )
437 torch.cuda.set_device(local_rank)
438 if not torch.distributed.is_initialized():
439 torch.distributed.init_process_group(backend="nccl")
440 rank = torch.distributed.get_rank()
441 world_size = torch.distributed.get_world_size()
442
443 exp_name = trainer_cfg.get("exp_name", "generic_exp")
444 seed = int(trainer_cfg.get("seed", 1))
445 device_str = trainer_cfg.get("device", "cpu")
446 if distributed:
447 device_str = f"cuda:{local_rank}"
448 iterations = int(trainer_cfg.get("iterations", 250))
449 buffer_size = int(
450 trainer_cfg.get("buffer_size", trainer_cfg.get("rollout_steps", 2048))
451 )
452 enable_eval = bool(trainer_cfg.get("enable_eval", False))
453 eval_freq = int(trainer_cfg.get("eval_freq", 10000))
454 save_freq = int(
455 trainer_cfg.get(
456 "save_freq", 0 if "save_frequency_updates" in trainer_cfg else 50000
457 )
458 )
459 num_eval_episodes = int(trainer_cfg.get("num_eval_episodes", 5))
460 eval_seed = int(trainer_cfg.get("eval_seed", seed + 10_000))
461 headless = bool(trainer_cfg.get("headless", True))
462 renderer = trainer_cfg.get("renderer")
463 gpu_id = int(trainer_cfg.get("gpu_id", 0))
464 num_envs = trainer_cfg.get("num_envs", None)
465 wandb_project_name = trainer_cfg.get("wandb_project_name", "embodichain-generic")
466
467 if not isinstance(device_str, str):
468 raise ValueError(
469 f"runtime.device must be a string such as 'cpu' or 'cuda:0'. Got: {device_str!r}"
470 )
471 try:
472 device = torch.device(device_str)
473 except RuntimeError as exc:
474 raise ValueError(
475 f"Failed to parse runtime.device='{device_str}': {exc}"
476 ) from exc
477
478 if device.type == "cuda":
479 if not torch.cuda.is_available():
480 raise ValueError(
481 "CUDA device requested but torch.cuda.is_available() is False."
482 )
483 index = (
484 device.index if device.index is not None else torch.cuda.current_device()
485 )
486 device_count = torch.cuda.device_count()
487 if index < 0 or index >= device_count:
488 raise ValueError(
489 f"CUDA device index {index} is out of range (available devices: {device_count})."
490 )
491 torch.cuda.set_device(index)
492 device = torch.device(f"cuda:{index}")
493 elif device.type != "cpu":
494 raise ValueError(f"Unsupported device type: {device}")
495 if rank == 0:
496 logger.log_info(f"Device: {device}")
497 if distributed and rank == 0:
498 logger.log_info(f"Distributed training: world_size={world_size}")
499
500 # Seeds
501 effective_seed = seed + rank
502 _seed_training_rng(effective_seed, device)
503 torch.backends.cudnn.deterministic = True
504
505 # Outputs
506 if distributed:
507 run_stamp = (
508 f"{time.strftime('%Y%m%d_%H%M%S')}_{time.time_ns() % 1_000_000_000:09d}"
509 if rank == 0
510 else None
511 )
512 run_stamp_list = [run_stamp]
513 torch.distributed.broadcast_object_list(run_stamp_list, src=0)
514 run_stamp = run_stamp_list[0]
515 else:
516 run_stamp = (
517 f"{time.strftime('%Y%m%d_%H%M%S')}_{time.time_ns() % 1_000_000_000:09d}"
518 )
519 run_base = os.path.join("outputs", f"{exp_name}_{run_stamp}")
520 log_dir = os.path.join(run_base, "logs")
521 checkpoint_dir = os.path.join(run_base, "checkpoints")
522 if rank == 0:
523 os.makedirs(log_dir, exist_ok=True)
524 os.makedirs(checkpoint_dir, exist_ok=True)
525 writer = SummaryWriter(f"{log_dir}/{exp_name}") if rank == 0 else None
526
527 # Initialize Weights & Biases (optional)
528 use_wandb = trainer_cfg.get("use_wandb", False)
529 if use_wandb and rank == 0:
530 wandb.init(project=wandb_project_name, name=exp_name, config=cfg_data)
531
532 if rank == 0:
533 logger.log_info(f"Current working directory: {Path.cwd()}")
534
535 profiler = (
536 EnvProfilerCfg(
537 enable_time=True,
538 output_path=_resolve_profile_output(
539 profile_output,
540 rank=rank,
541 world_size=world_size,
542 ),
543 )
544 if profile
545 else None
546 )
547 # Resolve active event callables before constructing the first World.
548 event_modules = [
549 "embodichain.lab.gym.envs.managers.randomization",
550 "embodichain.lab.gym.envs.managers.record",
551 "embodichain.lab.gym.envs.managers.events",
552 ]
553 events_dict = trainer_cfg.get("events", {})
554 train_event_cfg = {}
555 eval_event_cfg = {}
556 # Parse train events
557 for event_name, event_info in events_dict.get("train", {}).items():
558 event_func_str = event_info.get("func")
559 mode = event_info.get("mode", "interval")
560 params = _event_params(event_info, run_base=run_base, phase="train")
561 interval_step = event_info.get("interval_step", 1)
562 event_func = find_function_from_modules(
563 event_func_str, event_modules, raise_if_not_found=True
564 )
565 train_event_cfg[event_name] = EventCfg(
566 func=event_func,
567 mode=mode,
568 params=params,
569 interval_step=interval_step,
570 is_global=event_info.get("is_global", False),
571 )
572 # Parse eval events (only if evaluation is enabled)
573 if enable_eval and rank == 0:
574 for event_name, event_info in events_dict.get("eval", {}).items():
575 event_func_str = event_info.get("func")
576 mode = event_info.get("mode", "interval")
577 params = _event_params(event_info, run_base=run_base, phase="eval")
578 interval_step = event_info.get("interval_step", 1)
579 event_func = find_function_from_modules(
580 event_func_str, event_modules, raise_if_not_found=True
581 )
582 eval_event_cfg[event_name] = EventCfg(
583 func=event_func,
584 mode=mode,
585 params=params,
586 interval_step=interval_step,
587 is_global=event_info.get("is_global", False),
588 )
589 runtime = build_gym_policy_runtime(
590 cfg_data,
591 device=device,
592 num_envs=num_envs,
593 headless=headless,
594 renderer=renderer,
595 gpu_id=gpu_id,
596 seed=effective_seed,
597 config_dir=Path(config_path).expanduser().resolve().parent,
598 profiler=profiler,
599 trainer_events=(*train_event_cfg.values(), *eval_event_cfg.values()),
600 )
601 env = runtime.env
602 policy = runtime.policy
603 gym_config_path = runtime.gym_config_path
604 gym_config_data = runtime.gym_config
605 gym_env_cfg = runtime.env_cfg
606 if gym_config_path is None or gym_config_data is None or gym_env_cfg is None:
607 raise RuntimeError("Simulator Policy runtime is missing task configuration")
608 if rank == 0:
609 logger.log_info(
610 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}, device={gym_env_cfg.sim_cfg.device})"
611 )
612
613 # Create evaluation environment only if enabled
614 eval_env = None
615 num_eval_envs = trainer_cfg.get("num_eval_envs", 4)
616 if enable_eval and rank == 0:
617 eval_gym_env_cfg = deepcopy(gym_env_cfg)
618 eval_gym_env_cfg.num_envs = int(num_eval_envs)
619 eval_gym_env_cfg.seed = eval_seed
620 eval_gym_env_cfg.sim_cfg.headless = True
621 eval_gym_env_cfg.profiler = None
622 eval_env = build_env(gym_config_data["id"], base_env_cfg=eval_gym_env_cfg)
623 logger.log_info(
624 f"Evaluation environment created (num_envs={num_eval_envs}, headless=True)"
625 )
626
627 # Environment construction intentionally uses task/evaluation seeds. Reset
628 # the trainer stream so policy initialization is independent of scene work.
629 _seed_training_rng(effective_seed, device)
630
631 policy_name = policy_block["name"]
632
633 # Build Algorithm via factory
634 algo_name = algo_block["name"].lower()
635 algo_cfg = algo_block["cfg"]
636 algo = build_algo(
637 algo_name,
638 algo_cfg,
639 policy,
640 device,
641 distributed=distributed,
642 )
643 if algo.rollout_kind is RolloutKind.DIFFERENTIABLE:
644 raise ValueError(
645 "Differentiable algorithms require trainer.learning_env; "
646 "simulator gym_config environments use standard rollouts."
647 )
648
649 # Build Trainer
650 trainer = Trainer(
651 policy=policy,
652 env=env,
653 algorithm=algo,
654 buffer_size=buffer_size,
655 batch_size=algo_cfg["batch_size"],
656 writer=writer,
657 eval_freq=eval_freq if enable_eval else 0, # Disable eval if not enabled
658 save_freq=save_freq,
659 save_frequency_updates=int(trainer_cfg.get("save_frequency_updates", 0)),
660 checkpoint_dir=checkpoint_dir,
661 exp_name=exp_name,
662 use_wandb=use_wandb,
663 eval_env=eval_env, # None if enable_eval=False
664 event_cfg=train_event_cfg,
665 eval_event_cfg=eval_event_cfg if (enable_eval and rank == 0) else {},
666 num_eval_episodes=num_eval_episodes,
667 distributed=distributed,
668 rank=rank,
669 world_size=world_size,
670 eval_seed=eval_seed,
671 best_eval_metric=trainer_cfg.get("best_eval_metric", "eval/avg_reward"),
672 best_eval_mode=trainer_cfg.get("best_eval_mode", "max"),
673 )
674
675 if rank == 0:
676 logger.log_info("Generic training initialized")
677 logger.log_info(f"Task: {type(env).__name__}")
678 logger.log_info(
679 f"Policy: {policy_name} (available: {get_registered_policy_names()})"
680 )
681 logger.log_info(
682 f"Algorithm: {algo_name} (available: {get_registered_algo_names()})"
683 )
684
685 total_steps = int(iterations * buffer_size * env.num_envs * world_size)
686 if rank == 0:
687 logger.log_info(
688 f"Total steps: {total_steps} (iterations≈{iterations}, world_size={world_size})"
689 )
690
691 try:
692 _configure_checkpoints(
693 trainer,
694 cfg_data,
695 config_path,
696 run_base,
697 gym_config=gym_config_path,
698 gym_data=gym_config_data,
699 )
700 _resume_training(trainer, resume, run, resume_scheduler)
701 trainer.train(total_steps)
702 _save_final_checkpoint(trainer)
703 except KeyboardInterrupt:
704 if rank == 0:
705 logger.log_info("Training interrupted by user")
706 finally:
707 if writer is not None:
708 writer.close()
709 if use_wandb and rank == 0:
710 try:
711 wandb.finish()
712 except Exception:
713 pass
714
715 # Clean up environments to prevent resource leaks
716 try:
717 if env is not None:
718 runtime.close()
719 except Exception as e:
720 if rank == 0:
721 logger.log_warning(f"Failed to close training environment: {e}")
722
723 try:
724 if eval_env is not None:
725 _close_environment(eval_env)
726 except Exception as e:
727 if rank == 0:
728 logger.log_warning(f"Failed to close evaluation environment: {e}")
729
730 if distributed and torch.distributed.is_initialized():
731 torch.distributed.destroy_process_group()
732
733 if rank == 0:
734 logger.log_info("Training finished")
735
736
737def _checkpoint_context(cfg_data: dict, gym_data: dict | None = None) -> dict:
738 policy = dict(cfg_data["policy"])
739 policy.pop("initial_log_std", None)
740 trainer = cfg_data["trainer"]
741 result = {"policy": policy, "learning_env": trainer.get("learning_env")}
742 if gym_data is not None:
743 result["gym"] = gym_data
744 result["best_eval_metric"] = trainer.get("best_eval_metric", "eval/avg_reward")
745 result["best_eval_mode"] = trainer.get("best_eval_mode", "max")
746 return result
747
748
749def _configure_checkpoints(
750 trainer,
751 cfg_data: dict,
752 config_path: str | Path,
753 run_base: str | Path,
754 *,
755 gym_config: str | Path | None = None,
756 gym_data: dict | None = None,
757) -> None:
758 trainer._checkpoint_context = deepcopy(_checkpoint_context(cfg_data, gym_data))
759 trainer._checkpoint_config = deepcopy(cfg_data)
760 if getattr(trainer, "rank", 0) != 0:
761 return
762 trainer._checkpoint_run = Path(run_base)
763 config_dir = Path(run_base) / "configs"
764 train_path = config_dir / f"train{Path(config_path).suffix.lower() or '.yaml'}"
765 gym_path = (
766 config_dir / f"gym{Path(gym_config or 'gym.yaml').suffix.lower() or '.yaml'}"
767 if gym_data is not None
768 else None
769 )
770 snapshots = {train_path: trainer._checkpoint_config}
771 if gym_path is not None:
772 snapshots[gym_path] = deepcopy(gym_data)
773
774 def update_manifest(summary: dict) -> None:
775 for path, data in snapshots.items():
776 contents = (
777 json.dumps(data, indent=2)
778 if path.suffix == ".json"
779 else yaml.safe_dump(data, sort_keys=False)
780 ).encode("utf-8")
781 _atomic_write(path, lambda stream: stream.write(contents))
782 snapshots.clear()
783 write_run_manifest(
784 run_base,
785 train_config=train_path,
786 gym_config=gym_path,
787 latest_checkpoint=summary["latest_checkpoint_path"],
788 best_checkpoint=summary["best_checkpoint_path"],
789 )
790
791 trainer._checkpoint_callback = update_manifest
792
793
794def _save_final_checkpoint(trainer) -> None:
795 """Write the final update only if periodic/best saving has not written it."""
796 if getattr(trainer, "_last_saved_update", None) != trainer.num_updates:
797 trainer.save_checkpoint()
798 return
799 error = None
800 if getattr(trainer, "rank", 0) == 0:
801 try:
802 _validate_saved_files(trainer)
803 except Exception as exc:
804 error = exc
805 _sync_errors(error, getattr(trainer, "distributed", False))
806
807
808def _resume_training(trainer, resume, run, scheduler: str) -> None:
809 error = None
810 path = None
811 try:
812 path = _resolve_resume(resume, run)
813 except Exception as exc:
814 error = exc
815 _sync_errors(error, getattr(trainer, "distributed", False))
816 if path is not None:
817 trainer.load_checkpoint(path, resume_scheduler=scheduler)
818 if getattr(trainer, "rank", 0) == 0:
819 logger.log_info(
820 f"Resumed step={trainer.global_step}, updates={trainer.num_updates}, "
821 f"learning_rate={trainer.algorithm.current_learning_rate():.6g}, "
822 f"scheduler={scheduler}"
823 )
824
825
826def cli(argv: Sequence[str] | None = None) -> None:
827 """Command-line interface for RL training.
828
829 Parses CLI arguments and launches training from a config file.
830
831 Task packages are discovered (and init hooks executed) before training so
832 that task environments registered in separate packages (e.g.
833 ``embodichain_tasks``) are available to ``build_env``. This mirrors the
834 ``run_task`` CLI.
835 """
836 args = parse_args(argv)
837
838 # Discover all installed task packages and run init hooks (register custom
839 # manager modules / asset resolvers) before building any environment.
840 discover_task_packages()
841 execute_init_hooks()
842
843 try:
844 train_from_config(
845 args.config,
846 distributed=args.distributed,
847 profile=args.profile,
848 profile_output=args.profile_output,
849 resume=args.resume,
850 run=args.run,
851 resume_scheduler=args.resume_scheduler,
852 )
853 finally:
854 from embodichain.lab.sim.sim_manager import SimulationManager
855
856 SimulationManager.flush_cleanup_queue()
857
858
859if __name__ == "__main__":
860 cli()
861
862
863__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
configs/: Training config and referenced gym config snapshots
evaluations/: Timestamped policy evaluation reports
run-manifest.json: Training configs and best/latest checkpoint index used by
eval-policy
A training run can be evaluated Headless or opened in its simulator task:
embodichain eval-policy outputs/<exp_name>_<timestamp>
embodichain eval-policy outputs/<exp_name>_<timestamp> --viewer
See Policy Evaluation for EmbodiChain .pt training
runs and the external Motion Profile example.
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.policies 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": "RelativeJointPositionAction",
"params": { "part_name": "arm", "scale": 0.1 }
}
},
"extensions": {
"success_threshold": 0.05
}
}
}
The EmbodiedEnv with Action Manager provides:
Action Composition: Configure ordered joint, EEF, and gripper terms via
actions; each term owns and directly applies its selected resources.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. For example:"delta_qpos": {"func": "RelativeJointPositionAction", "params": {"part_name": "arm", "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>_<timestamp>/logs/for TensorBoard logs.Checkpoints: Regular checkpoints are saved to
outputs/<exp_name>_<timestamp>/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
Policy Evaluation — Headless and Viewer evaluation of EmbodiChain
.ptcheckpoints and external Motion Profiles