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(); the nested trainer.gym_config path supports the same extensions.

Example Configuration#

The configuration file (e.g., train_config.json or train_config.yaml) is located in embodichain_tasks/configs/agents/rl/push_cube or embodichain_tasks/configs/agents/rl/basic/cart_pole:

Example: train_config.json
 1{
 2    "trainer": {
 3        "exp_name": "push_cube_ppo",
 4        "gym_config": "embodichain_tasks/configs/agents/rl/push_cube/gym_config.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            "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
74        }
75    }
76}
Example: train_config.yaml (CartPole)
 1trainer:
 2  exp_name: cart_pole_ppo
 3  gym_config: embodichain_tasks/configs/agents/rl/basic/cart_pole/gym_config.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    learning_rate: 0.0001
64    n_epochs: 10
65    batch_size: 8192
66    gamma: 0.99
67    gae_lambda: 0.95
68    clip_coef: 0.2
69    ent_coef: 0.01
70    vf_coef: 0.5
71    max_grad_norm: 0.5

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
 17import argparse
 18import os
 19import time
 20from pathlib import Path
 21
 22import numpy as np
 23import torch
 24import wandb
 25from torch.utils.tensorboard import SummaryWriter
 26from copy import deepcopy
 27
 28from embodichain.learning.rl.models import build_policy, get_registered_policy_names
 29from embodichain.learning.rl.models import build_mlp_from_cfg
 30from embodichain.learning.rl.algo import build_algo, get_registered_algo_names
 31from embodichain.learning.rl.utils import dict_to_tensordict, flatten_dict_observation
 32from embodichain.learning.rl.utils.trainer import Trainer
 33from embodichain.utils import logger
 34from embodichain.lab.gym.utils.registration import (
 35    build_env,
 36    discover_task_packages,
 37    execute_init_hooks,
 38)
 39from embodichain.lab.gym.utils.gym_utils import config_to_cfg, get_manager_modules
 40from embodichain.utils.utility import load_config
 41from embodichain.utils.module_utils import find_function_from_modules
 42from embodichain.lab.sim import SimulationManagerCfg
 43from embodichain.lab.sim.cfg import RenderCfg
 44from embodichain.lab.gym.envs.managers.cfg import EventCfg
 45
 46
 47def parse_args():
 48    """Parse command line arguments."""
 49    parser = argparse.ArgumentParser()
 50    parser.add_argument(
 51        "--config",
 52        type=str,
 53        required=True,
 54        help="Path to training config file (.json, .yaml, or .yml).",
 55    )
 56    parser.add_argument(
 57        "--distributed",
 58        action=argparse.BooleanOptionalAction,
 59        default=None,
 60        help="Enable or disable multi-GPU distributed training",
 61    )
 62    return parser.parse_args()
 63
 64
 65def train_from_config(config_path: str, distributed: bool | None = None):
 66    """Run training from a config file path.
 67
 68    Args:
 69        config_path: Path to the training config file (.json, .yaml, or .yml).
 70        distributed: If True, run multi-GPU distributed training.
 71            If None, use trainer.distributed from config.
 72    """
 73    cfg_data = load_config(config_path)
 74
 75    trainer_cfg = cfg_data["trainer"]
 76    policy_block = cfg_data["policy"]
 77    algo_block = cfg_data["algorithm"]
 78
 79    # Resolve distributed flag
 80    if distributed is None:
 81        distributed = bool(trainer_cfg.get("distributed", False))
 82
 83    # Distributed setup
 84    rank = 0
 85    world_size = 1
 86    local_rank = 0
 87    if distributed:
 88        if not torch.distributed.is_available():
 89            raise RuntimeError(
 90                "Distributed training requested but torch.distributed is not available."
 91            )
 92        if not torch.cuda.is_available():
 93            raise RuntimeError(
 94                "Distributed training with NCCL backend requires CUDA, "
 95                "but torch.cuda.is_available() is False."
 96            )
 97        local_rank = int(os.environ.get("LOCAL_RANK", 0))
 98        if local_rank < 0 or local_rank >= torch.cuda.device_count():
 99            raise ValueError(
100                f"LOCAL_RANK {local_rank} is out of range "
101                f"(available GPUs: {torch.cuda.device_count()})."
102            )
103        torch.cuda.set_device(local_rank)
104        if not torch.distributed.is_initialized():
105            torch.distributed.init_process_group(backend="nccl")
106        rank = torch.distributed.get_rank()
107        world_size = torch.distributed.get_world_size()
108
109    # Runtime
110    exp_name = trainer_cfg.get("exp_name", "generic_exp")
111    seed = int(trainer_cfg.get("seed", 1))
112    device_str = trainer_cfg.get("device", "cpu")
113    if distributed:
114        device_str = f"cuda:{local_rank}"
115    iterations = int(trainer_cfg.get("iterations", 250))
116    buffer_size = int(
117        trainer_cfg.get("buffer_size", trainer_cfg.get("rollout_steps", 2048))
118    )
119    enable_eval = bool(trainer_cfg.get("enable_eval", False))
120    eval_freq = int(trainer_cfg.get("eval_freq", 10000))
121    save_freq = int(trainer_cfg.get("save_freq", 50000))
122    num_eval_episodes = int(trainer_cfg.get("num_eval_episodes", 5))
123    headless = bool(trainer_cfg.get("headless", True))
124    renderer = trainer_cfg.get("renderer", "hybrid")
125    gpu_id = int(trainer_cfg.get("gpu_id", 0))
126    num_envs = trainer_cfg.get("num_envs", None)
127    wandb_project_name = trainer_cfg.get("wandb_project_name", "embodichain-generic")
128
129    # Device
130    if not isinstance(device_str, str):
131        raise ValueError(
132            f"runtime.device must be a string such as 'cpu' or 'cuda:0'. Got: {device_str!r}"
133        )
134    try:
135        device = torch.device(device_str)
136    except RuntimeError as exc:
137        raise ValueError(
138            f"Failed to parse runtime.device='{device_str}': {exc}"
139        ) from exc
140
141    if device.type == "cuda":
142        if not torch.cuda.is_available():
143            raise ValueError(
144                "CUDA device requested but torch.cuda.is_available() is False."
145            )
146        index = (
147            device.index if device.index is not None else torch.cuda.current_device()
148        )
149        device_count = torch.cuda.device_count()
150        if index < 0 or index >= device_count:
151            raise ValueError(
152                f"CUDA device index {index} is out of range (available devices: {device_count})."
153            )
154        torch.cuda.set_device(index)
155        device = torch.device(f"cuda:{index}")
156    elif device.type != "cpu":
157        raise ValueError(f"Unsupported device type: {device}")
158    if rank == 0:
159        logger.log_info(f"Device: {device}")
160    if distributed and rank == 0:
161        logger.log_info(f"Distributed training: world_size={world_size}")
162
163    # Seeds
164    effective_seed = seed + rank
165    np.random.seed(effective_seed)
166    torch.manual_seed(effective_seed)
167    torch.backends.cudnn.deterministic = True
168    if device.type == "cuda":
169        torch.cuda.manual_seed_all(effective_seed)
170
171    # Outputs
172    if distributed:
173        run_stamp = time.strftime("%Y%m%d_%H%M%S") if rank == 0 else None
174        run_stamp_list = [run_stamp]
175        torch.distributed.broadcast_object_list(run_stamp_list, src=0)
176        run_stamp = run_stamp_list[0]
177    else:
178        run_stamp = time.strftime("%Y%m%d_%H%M%S")
179    run_base = os.path.join("outputs", f"{exp_name}_{run_stamp}")
180    log_dir = os.path.join(run_base, "logs")
181    checkpoint_dir = os.path.join(run_base, "checkpoints")
182    if rank == 0:
183        os.makedirs(log_dir, exist_ok=True)
184        os.makedirs(checkpoint_dir, exist_ok=True)
185    writer = SummaryWriter(f"{log_dir}/{exp_name}") if rank == 0 else None
186
187    # Initialize Weights & Biases (optional)
188    use_wandb = trainer_cfg.get("use_wandb", False)
189    if use_wandb and rank == 0:
190        wandb.init(project=wandb_project_name, name=exp_name, config=cfg_data)
191
192    gym_config_path = Path(trainer_cfg["gym_config"])
193    if rank == 0:
194        logger.log_info(f"Current working directory: {Path.cwd()}")
195
196    gym_config_data = load_config(str(gym_config_path))
197    gym_env_cfg = config_to_cfg(gym_config_data, manager_modules=get_manager_modules())
198    if num_envs is not None:
199        gym_env_cfg.num_envs = int(num_envs)
200
201    # Ensure sim configuration mirrors runtime overrides
202    if gym_env_cfg.sim_cfg is None:
203        gym_env_cfg.sim_cfg = SimulationManagerCfg()
204    if device.type == "cuda":
205        gpu_index = device.index
206        if gpu_index is None:
207            gpu_index = torch.cuda.current_device()
208        gym_env_cfg.sim_cfg.sim_device = torch.device(f"cuda:{gpu_index}")
209        if hasattr(gym_env_cfg.sim_cfg, "gpu_id"):
210            gym_env_cfg.sim_cfg.gpu_id = gpu_index
211    else:
212        gym_env_cfg.sim_cfg.sim_device = torch.device("cpu")
213    gym_env_cfg.sim_cfg.headless = headless
214    gym_env_cfg.sim_cfg.render_cfg = RenderCfg(renderer=renderer)
215    gym_env_cfg.sim_cfg.gpu_id = gpu_id
216    logger.log_info(
217        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})"
218    )
219
220    env = build_env(gym_config_data["id"], base_env_cfg=gym_env_cfg)
221    sample_obs, _ = env.reset()
222    sample_obs_td = dict_to_tensordict(sample_obs, device)
223    obs_dim = flatten_dict_observation(sample_obs_td).shape[-1]
224    flat_obs_space = env.flattened_observation_space
225
226    # Create evaluation environment only if enabled
227    eval_env = None
228    num_eval_envs = trainer_cfg.get("num_eval_envs", 4)
229    if enable_eval and rank == 0:
230        eval_gym_env_cfg = deepcopy(gym_env_cfg)
231        eval_gym_env_cfg.num_envs = num_eval_envs
232        eval_gym_env_cfg.sim_cfg.headless = True
233        eval_env = build_env(gym_config_data["id"], base_env_cfg=eval_gym_env_cfg)
234        logger.log_info(
235            f"Evaluation environment created (num_envs={num_eval_envs}, headless=True)"
236        )
237
238    # Build Policy via registry
239    policy_name = policy_block["name"]
240    env_action_dim = (
241        env.get_wrapper_attr("action_manager").total_action_dim
242        if env.get_wrapper_attr("action_manager") is not None
243        else len(env.get_wrapper_attr("active_joint_ids"))
244    )
245    action_dim = policy_block.get("action_dim", env_action_dim)
246    action_dim = int(action_dim)
247    if action_dim != env_action_dim:
248        raise ValueError(
249            f"Configured policy.action_dim={action_dim} does not match env action dim {env_action_dim}."
250        )
251    # Build Policy via registry (actor/critic must be explicitly defined in JSON when using actor_critic/actor_only)
252    if policy_name.lower() == "actor_critic":
253        actor_cfg = policy_block.get("actor")
254        critic_cfg = policy_block.get("critic")
255        if actor_cfg is None or critic_cfg is None:
256            raise ValueError(
257                "ActorCritic requires 'actor' and 'critic' definitions in JSON (policy.actor / policy.critic)."
258            )
259
260        actor = build_mlp_from_cfg(actor_cfg, obs_dim, action_dim)
261        critic = build_mlp_from_cfg(critic_cfg, obs_dim, 1)
262
263        policy = build_policy(
264            policy_block,
265            flat_obs_space,
266            env.action_space,
267            device,
268            actor=actor,
269            critic=critic,
270        )
271    elif policy_name.lower() == "actor_only":
272        actor_cfg = policy_block.get("actor")
273        if actor_cfg is None:
274            raise ValueError(
275                "ActorOnly requires 'actor' definition in JSON (policy.actor)."
276            )
277
278        actor = build_mlp_from_cfg(actor_cfg, obs_dim, action_dim)
279
280        policy = build_policy(
281            policy_block,
282            flat_obs_space,
283            env.action_space,
284            device,
285            actor=actor,
286        )
287    else:
288        policy = build_policy(
289            policy_block, env.observation_space, env.action_space, device
290        )
291
292    # Build Algorithm via factory
293    algo_name = algo_block["name"].lower()
294    algo_cfg = algo_block["cfg"]
295    algo = build_algo(
296        algo_name,
297        algo_cfg,
298        policy,
299        device,
300        distributed=distributed,
301    )
302
303    # Build Trainer
304    event_modules = [
305        "embodichain.lab.gym.envs.managers.randomization",
306        "embodichain.lab.gym.envs.managers.record",
307        "embodichain.lab.gym.envs.managers.events",
308    ]
309    events_dict = trainer_cfg.get("events", {})
310    train_event_cfg = {}
311    eval_event_cfg = {}
312    # Parse train events
313    for event_name, event_info in events_dict.get("train", {}).items():
314        event_func_str = event_info.get("func")
315        mode = event_info.get("mode", "interval")
316        params = event_info.get("params", {})
317        interval_step = event_info.get("interval_step", 1)
318        event_func = find_function_from_modules(
319            event_func_str, event_modules, raise_if_not_found=True
320        )
321        train_event_cfg[event_name] = EventCfg(
322            func=event_func,
323            mode=mode,
324            params=params,
325            interval_step=interval_step,
326        )
327    # Parse eval events (only if evaluation is enabled)
328    if enable_eval:
329        for event_name, event_info in events_dict.get("eval", {}).items():
330            event_func_str = event_info.get("func")
331            mode = event_info.get("mode", "interval")
332            params = event_info.get("params", {})
333            interval_step = event_info.get("interval_step", 1)
334            event_func = find_function_from_modules(
335                event_func_str, event_modules, raise_if_not_found=True
336            )
337            eval_event_cfg[event_name] = EventCfg(
338                func=event_func,
339                mode=mode,
340                params=params,
341                interval_step=interval_step,
342            )
343    trainer = Trainer(
344        policy=policy,
345        env=env,
346        algorithm=algo,
347        buffer_size=buffer_size,
348        batch_size=algo_cfg["batch_size"],
349        writer=writer,
350        eval_freq=eval_freq if enable_eval else 0,  # Disable eval if not enabled
351        save_freq=save_freq,
352        checkpoint_dir=checkpoint_dir,
353        exp_name=exp_name,
354        use_wandb=use_wandb,
355        eval_env=eval_env,  # None if enable_eval=False
356        event_cfg=train_event_cfg,
357        eval_event_cfg=eval_event_cfg if (enable_eval and rank == 0) else {},
358        num_eval_episodes=num_eval_episodes,
359        distributed=distributed,
360        rank=rank,
361        world_size=world_size,
362    )
363
364    if rank == 0:
365        logger.log_info("Generic training initialized")
366        logger.log_info(f"Task: {type(env).__name__}")
367        logger.log_info(
368            f"Policy: {policy_name} (available: {get_registered_policy_names()})"
369        )
370        logger.log_info(
371            f"Algorithm: {algo_name} (available: {get_registered_algo_names()})"
372        )
373
374    total_steps = int(iterations * buffer_size * env.num_envs * world_size)
375    if rank == 0:
376        logger.log_info(
377            f"Total steps: {total_steps} (iterations≈{iterations}, world_size={world_size})"
378        )
379
380    try:
381        trainer.train(total_steps)
382    except KeyboardInterrupt:
383        if rank == 0:
384            logger.log_info("Training interrupted by user")
385    finally:
386        trainer.save_checkpoint()
387        if writer is not None:
388            writer.close()
389        if use_wandb and rank == 0:
390            try:
391                wandb.finish()
392            except Exception:
393                pass
394
395        # Clean up environments to prevent resource leaks
396        try:
397            if env is not None:
398                env.close()
399        except Exception as e:
400            if rank == 0:
401                logger.log_warning(f"Failed to close training environment: {e}")
402
403        try:
404            if eval_env is not None:
405                eval_env.close()
406        except Exception as e:
407            if rank == 0:
408                logger.log_warning(f"Failed to close evaluation environment: {e}")
409
410        if distributed and torch.distributed.is_initialized():
411            torch.distributed.destroy_process_group()
412
413        if rank == 0:
414            logger.log_info("Training finished")
415
416
417def cli() -> None:
418    """Command-line interface for RL training.
419
420    Parses CLI arguments and launches training from a config file.
421
422    Task packages are discovered (and init hooks executed) before training so
423    that task environments registered in separate packages (e.g.
424    ``embodichain_tasks``) are available to ``build_env``. This mirrors the
425    ``run_env`` CLI.
426    """
427    args = parse_args()
428
429    # Discover all installed task packages and run init hooks (register custom
430    # manager modules / asset resolvers) before building any environment.
431    discover_task_packages()
432    execute_init_hooks()
433
434    train_from_config(args.config, distributed=args.distributed)
435
436
437if __name__ == "__main__":
438    cli()

The Script Explained#

The training script performs the following steps:

  1. Parse Configuration: Loads the config file (.json, .yaml, or .yml) and extracts runtime/env/policy/algorithm blocks

  2. Setup: Initializes device, seeds, output directories, TensorBoard, and Weights & Biases

  3. Build Components: - Environment via build_env() factory - Policy via build_policy() registry - Algorithm via build_algo() factory

  4. Create Trainer: Instantiates the Trainer with all components

  5. Train: Runs the training loop until completion

Launching Training#

To start training, run:

python -m embodichain train-rl --config embodichain_tasks/configs/agents/rl/basic/cart_pole/train_config.yaml

JSON configs are also supported:

python -m embodichain train-rl --config embodichain_tasks/configs/agents/rl/push_cube/train_config.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:

  1. Rollout Phase: SyncCollector interacts with the environment and writes policy-side fields into a shared rollout TensorDict with uniform [N, T + 1] layout. EmbodiedEnv writes environment-side step fields such as reward, done, terminated, and truncated into the same rollout via set_rollout_buffer(). The final slot of transition-only fields is reserved as padding, while obs[:, -1] and value[:, -1] remain valid bootstrap data.

  2. 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 T steps before minibatch optimization.

  3. Update Phase: Algorithm updates the policy with update(rollout)

  4. Logging: Trainer logs training losses and aggregated metrics to TensorBoard and Weights & Biases

  5. Evaluation (periodic): Trainer evaluates the current policy

  6. 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 actor and critic modules 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_only policy. Set kl_coef=0 for from-scratch training (CartPole, dense reward); kl_coef=0.02 for VLA/LLM fine-tuning.

Adding a New Algorithm#

To add a new algorithm:

  1. Create a new algorithm class in embodichain/learning/rl/algo/

  2. Implement update(rollout) and consume the shared rollout TensorDict

  3. Register 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:

  1. Create a new policy class inheriting from the Policy abstract base class

  2. Register 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:

  1. 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
  1. Configure the environment in your config file with actions and extensions:

"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() using compute_task_state() template method

Best Practices#

  • Use EmbodiedEnv with Action Manager for RL Tasks: Inherit from EmbodiedEnv and configure actions in your config. The Action Manager handles action preprocessing (delta_qpos, qpos, qvel, qf, eef_pose) in a modular way.

  • Action Configuration: Use the actions field 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 rollout TensorDict. Collection is handled by SyncCollector plus environment-side rollout writes in EmbodiedEnv.

  • Reward Configuration: Use the RewardManager in your environment config to define reward components. Organize reward components in info["rewards"] dictionary and metrics in info["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. Override check_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#