Batch RL Training

uvms_rl is the Python training API installed by simlab. It runs many UVMS environments through the batched simulator core without launching ROS nodes. ros2_control owns the hardware integration path; RL tasks, rewards, resets, and experiment configs live in uvms_rl.

Quick Start

cd ~/ros_ws
colcon build --packages-select ros2_control_blue_reach_5 simlab
source install/setup.bash

ros2 run simlab uvms_rl_train_rsl \
  --config hover_vehicle_stage1_fixed \
  --backend gpu \
  --device cuda \
  --num-envs 2048

hover_vehicle_stage1_fixed is the first curriculum experiment. It trains a fixed-target hover policy through the GPU batch backend and writes logs and checkpoints under recordings/rl_runs unless --log-dir is supplied.

Python Training Loop

import numpy as np
from uvms_rl import UvmsBatchEnv
from uvms_rl.config import load_experiment


experiment = load_experiment("hover_vehicle")
env_cfg = experiment.config["env"]
task_cfg = experiment.config["task"]

env = UvmsBatchEnv(
    robot_count=env_cfg["robot_count"],
    control_dt=env_cfg["control_dt"],
    sim_dt=env_cfg["sim_dt"],
    max_episode_steps=env_cfg["max_episode_steps"],
    seed=env_cfg["seed"],
    backend=env_cfg.get("backend", "cpu"),
    dynamics_profile=env_cfg["dynamics_profile"],
    task=experiment.task_cls,
    task_config=task_cfg,
)

obs = env.reset()
actions = np.zeros((env.robot_count, env.action_dim), dtype=np.float32)
obs, rewards, dones, info = env.step(actions)

obs is the task observation used by the policy. Use env.sim_observations() only when you need the raw simulator state.

RSL-RL Adapter

Use RslRlUvmsEnv when training with RSL-RL. The adapter wraps UvmsBatchEnv as RSL-RL’s VecEnv interface and returns observations as a TensorDict with the policy observation group.

Install RSL-RL first if it is not already present:

python3 -m pip install rsl-rl-lib
from rsl_rl.runners import OnPolicyRunner
from uvms_rl.rsl_adapter import RslRlUvmsEnv


env = RslRlUvmsEnv.from_experiment("hover_vehicle")
runner = OnPolicyRunner(env, train_cfg, log_dir="runs/hover_vehicle", device=str(env.device))
runner.learn(num_learning_iterations=1000)

The packaged command trains from the experiment’s trainer.rsl_rl config:

ros2 run simlab uvms_rl_train_rsl --config hover_vehicle_stage1_fixed --backend gpu --device cuda

Training logs and checkpoints are written under recordings/rl_runs by default. Override the backend, number of environments, iteration count, or log directory from the command line:

ros2 run simlab uvms_rl_train_rsl \
  --config hover_vehicle_stage1_fixed \
  --backend gpu \
  --device cuda \
  --num-envs 2048 \
  --iterations 3 \
  --log-dir /tmp/uvms_rsl_hover_stage1

RSL-RL does not call reset before training, so the adapter resets once at construction. During training it performs same-step resets for completed environment rows: returned dones still mark the terminal transition, while the returned observations for those rows are already the next episode’s initial observations.

Timing Contract

The RL API separates the policy/control step from the internal simulation step. One env.step(action) applies one policy action, holds it constant, advances the simulator for one or more internal substeps, then returns one observation, reward, and done flag per environment.

control_dt = policy action interval
sim_dt     = internal simulator integration interval
substeps   = control_dt / sim_dt

For transfer to the ROS runtime, match control_dt to the action interval used by the controller path. The ROS runtime uses controller_manager.update_rate as the ros2_control loop frequency, so start with:

control_dt = 1.0 / controller_manager.update_rate

Set sim_dt to the simulator integration period. If the simulator integrates at the same rate as the controller, use sim_dt: control_dt. If it integrates faster, use the smaller simulator period and let uvms_rl substep.

Example for a 150 Hz controller and 600 Hz simulator:

env:
  control_dt: 0.006666666666666667
  sim_dt: 0.001666666666666667

The packaged hover experiments set sim_dt equal to control_dt for a simple one-substep rollout. For transfer experiments, set both values explicitly to the controller and simulator rates you want to match.

Data Contract

The first dimension is the environment batch size.

task observations: [robot_count, task.policy_observation_dim]
raw sim state:     [robot_count, 22]
actions:           [robot_count, 11]
rewards:           [robot_count]
dones:             [robot_count]

Raw simulator state layout:

x, y, z, roll, pitch, yaw,
u, v, w, p, q, r,
arm_q1..arm_q5,
arm_qd1..arm_qd5

The vehicle pose is the dynamics NED state. z is positive depth/down, so underwater hover targets should use positive target_z values.

Action layout:

vehicle_wrench: force.x, force.y, force.z, torque.x, torque.y, torque.z
arm_torque: arm_joint_1..arm_joint_5

Add an Experiment

Add experiments as folders under uvms-simlab/uvms_rl/experiments. An experiment should own its config. Reusable task implementations should live in uvms-simlab/uvms_rl/tasks so multiple curriculum stages can share the same reward/reset logic without importing from each other.

uvms_rl/experiments/my_experiment/
|-- config.yaml
`-- task.py
  1. Create a reusable task implementation, for example uvms_rl/tasks/my_task.py:

from uvms_rl.task_base import TaskBase


class Task(TaskBase):

    @property
    def policy_observation_dim(self) -> int:
        return 33

    def reset(self, env):
        # Return initial raw simulator state with shape [N, 22].
        ...

    def reset_indices(self, env, indices):
        # Return reset raw simulator states with shape [len(indices), 22].
        # Required when using RSL-RL same-step resets.
        ...

    def policy_observation(self, env, sim_obs, actions):
        # Return policy input with shape [N, policy_observation_dim].
        ...

    def reward_done(self, env, sim_obs, actions):
        # Return rewards [N], dones [N], and metric dict.
        ...
  1. Point the experiment folder at that task with a tiny uvms_rl/experiments/my_experiment/task.py:

from uvms_rl.tasks.my_task import Task
  1. Add uvms_rl/experiments/my_experiment/config.yaml:

env:
  backend: cpu
  dynamics_profile: dory_alpha
  robot_count: 1024
  control_dt: 0.006666666666666667
  sim_dt: 0.001666666666666667
  max_episode_steps: 500
  seed: 7

task:
  target_x: [-2.0, 2.0]
  1. Rebuild and run:

cd ~/ros_ws
colcon build --packages-select simlab
source install/setup.bash
ros2 run simlab uvms_rl_train_rsl --config my_experiment --backend cpu --iterations 1

Backend Boundary

The task API is independent of the dynamics backend. CPU dynamics are the portable baseline. GPU dynamics use the CUDA batch core for high-throughput rollouts under the same task, reward, reset, and trainer code.

Experiment configs should name the backend explicitly:

env:
  backend: cpu
  dynamics_profile: dory_alpha

Use backend: gpu only on machines where ros2_control_blue_reach_5 was built with CUDA dynamics enabled. It does not silently fall back to CPU; if the GPU extension or PyTorch CUDA support is missing, construction fails so the training run cannot accidentally use the wrong backend.

dynamics_profile is also required. It names the same whole-robot dynamics profile used by the hardware simulator replay path. Missing profiles fail at environment construction time instead of silently falling back to hidden defaults.

Before training against the GPU backend, validate the installed GPU dynamics once after building:

ros2 run ros2_control_blue_reach_5 uvms_gpu_dynamics_correctness
ros2 run ros2_control_blue_reach_5 uvms_gpu_dynamics_benchmark

After those pass, switch the experiment config to backend: gpu and use the same task and trainer code.