How to use Isaac Lab

Notes · GPU-parallel robot learning · Jul 2026

This is the second half of a pair. The first half, How to use Isaac Sim, covers the simulator itself. This page covers the learning framework that sits on top of it. Isaac Lab is a BSD-3-Clause and Apache-2.0 dual-licensed framework from NVIDIA for training robot policies inside Isaac Sim, with thousands of copies of the robot stepping in lockstep on one GPU. Isaac Sim itself went open source at version 5.0.0 in August 2025 under Apache-2.0, so both halves of this stack are now readable. That is a recent change and a genuinely useful one, because when a behaviour surprises you the explanation is usually down in the simulator rather than up in the framework.

The one-sentence identity. Isaac Lab is a configuration layer that clones one USD stage into thousands of parallel environments and factors the reward, observation, reset and randomization logic into swappable terms, so a task becomes a dataclass rather than a simulation loop. Everything else in the framework exists to make that claim survive contact with real robots, real sensors and real training runs.

The lineage, stated once so the old names stop confusing you

Four different projects have been the recommended way to do GPU-parallel robot RL at NVIDIA in the last five years, and their names overlap badly. Search results still mix them freely. Here is the actual sequence.

Isaac Gym Preview            standalone `isaacgym` package, own viewer, no USD
  └─ IsaacGymEnvs            task suite for it
                             ──► Preview 4 is final. Page is titled "Isaac Gym - Now Deprecated".
                                 IsaacGymEnvs archived, last code push 2024-10-26.

Omniverse rewrite
  └─ OmniIsaacGymEnvs        task suite on Isaac Sim
                             ──► archived, last code push 2024-06-06.

Orbit                        github.com/NVIDIA-Omniverse/orbit
  └─ renamed in place        ──► github.com/isaac-sim/IsaacLab
                                 Same repository. Same GitHub id 567038244, created 2022-11-16.
                                 The old URL 301-redirects to the new one.

So Isaac Lab is not a successor to Orbit, it is Orbit with a new name and a new owner org. That single fact clears up most of the confusion, because it means Orbit tutorials describe the right architecture with the wrong import paths, while Isaac Gym Preview tutorials describe a different architecture entirely and should be treated as historical. NVIDIA's own page for Isaac Gym now says the software is legacy, that developers may continue to use it but that it is no longer supported, and points at Isaac Lab.

There were then two separate renames inside the Isaac Lab line, and people conflate those too. At v1.0.0 on 2024-06-26 the packages went from omni.isaac.orbit* to omni.isaac.lab*. At v2.0.0 on 2025-01-30 they went from omni.isaac.lab* to isaaclab*, and the source layout flattened from source/extensions/<dotted.name>/ to source/<flat_name>/. That second rename is the one that breaks almost every blog post you will find. It gets its own section below.

The hardware constraint, before you install anything

Isaac Lab renders through Isaac Sim, and Isaac Sim renders through the Omniverse RTX renderer. It therefore inherits Isaac Sim's hardware floor without restating most of it. The Isaac Sim requirements page carries this sentence verbatim, unchanged across the 4.5.0, 5.0.0, 5.1.0 and 6.0.1 versions of the page.

Verbatim, and unchanged for four releases: "GPUs without RT Cores (A100, H100) are not supported."

Read that carefully, because it is the least convenient sentence in the whole stack. It is not a performance warning. It is a support statement, and it names the two datacenter parts most people have access to. H20 is in the same category for the same reason. The official GPU ladder, identical in 5.0.0, 5.1.0 and 6.0.1, is the following.

TierGPUVRAM
MinimumGeForce RTX 408016 GB
GoodGeForce RTX 508016 GB
IdealRTX PRO 6000 Blackwell48 GB

The Isaac Lab installation page does not repeat the RT core rule. It asks only for 16 GB or more of VRAM and a recent driver, currently Linux 580.65.06, Windows 580.88 and DGX Spark 580.95.05 for the 2.3.x line. If you go looking for the constraint in the Isaac Lab docs alone you will not find it, conclude your cluster is fine, and lose an afternoon. Cite the Isaac Sim requirements page instead. That is where the rule lives.

Headless is not an escape hatch

The natural next thought is that --headless removes the renderer from the picture, so a compute-only card should be fine for a physics-only task with no cameras. It does not work that way. What --headless removes is the window and the Vulkan swapchain, which is why the Isaac Sim container documentation tells you that GUI mode needs a real local display and that remote or monitor-free hosts must use headless mode with livestreaming. The RTX renderer still initializes. An Isaac Sim maintainer put the architectural reason plainly in discussion #548, noting that Isaac Sim does not support plugging in an alternative Hydra renderer and that sensor simulation is tightly coupled to the RTX renderer, so swapping in a rasterizer would disable most of that functionality.

One nuance worth keeping straight, because the same maintainer thread contains both ideas. Running headless with viewport updates disabled is the correct way to get close to zero rendering overhead on an RTX GPU. It is a throughput tip. It is not a path to running on a card that has no RT cores. Do not let one become evidence for the other.

What this means if your training hardware is 2xH100

This is worth stating without softening, because it is the situation I am actually in. Two H100s are an excellent machine for training a policy and a completely unusable machine for Isaac Lab. Isaac Lab's parallelism model places the simulation and the policy update on the same CUDA device, one rank per GPU, and the multi-GPU documentation is explicit that scaling out means torch.distributed.run with one process per GPU plus the --distributed flag. There is no supported split where the simulation runs on an RTX card and the gradients land on a datacenter card. The device that renders is the device that learns.

So there are exactly three honest options, and picking one early saves you a lot of wasted setup.

  1. Train on workstation RTX hardware. One RTX 4090, 5090 or RTX PRO 6000 Blackwell running thousands of environments will out-train an H100 that cannot start the simulator at all. This is the path NVIDIA designs for.
  2. Rent datacenter GPUs that do have RT cores. L40S, L4, A10G and the RTX PRO 6000 Blackwell Server Edition all qualify. NVIDIA's own AWS deployment guide for Isaac Sim tells you to pick an instance type with RTX GPU support, and g6e is the L40S family. The distinction that matters is not workstation versus datacenter, it is RT cores versus no RT cores.
  3. Keep a separate engine-free simulator for the work that does not need pixels. Occupancy grids, range sensing, collision checking against a mesh and planner benchmarking are all geometry problems. A few hundred lines of NumPy or Warp on an H100 will run them faster than a photoreal renderer would, deterministically, and with no driver floor. Reserve Isaac Lab for the parts of the problem where the pixels or the contact dynamics are the point.

The third option is not a consolation prize. My aerial autonomy lab project runs on a Hopper machine with no ray tracing cores, no display engine and no Vulkan or EGL userspace at all, so Isaac Sim, Unreal and Unity camera output are all unavailable there and containers do not rescue it. Everything in that project that needed a simulator got a purpose-built one. The section near the end of this page on when a custom simulator wins is written from that experience rather than from principle.

Installing

The Python version is dictated by Isaac Sim, not by Isaac Lab, and getting this pairing wrong is the single most common install failure. The table below was derived from the README badge at each release tag plus the requires_python field on the isaacsim wheels, because NVIDIA does not publish a compatibility table.

Isaac LabReleasedIsaac SimPythonNamespace
v1.0.02024-06-264.03.10omni.isaac.lab*
v1.4.12025-01-304.2.03.10omni.isaac.lab*
v2.0.02025-01-304.5.03.10isaaclab*
v2.2.02025-08-075.0.03.11isaaclab*
v2.3.02025-10-285.1.03.11isaaclab*
v2.3.22026-02-025.1.03.11isaaclab*
v3.0.0-beta22026-06-176.0.03.12isaaclab* plus backends
v3.0.0-beta2.patch12026-07-026.0.13.12isaaclab* plus backends

The v3.0.0-beta2 release body pins Isaac Sim 6.0.0 with Python at least 3.12 and below 3.13 and PyTorch 2.10.0, while the README badge at the later patch tag says Isaac Sim 6.0.1. Both are correct for their own artifact, so do not merge them into one row. Support for Isaac Sim 4.2.0 and below has been dropped outright.

Two lines are live as of this writing, which is unusual and worth knowing before you clone. The GitHub default branch is release/3.0.0-beta2, so the README you land on describes 3.0, while the documentation site's main version describes the 2.3.x stable line. There is no final 3.0.0 release, only beta tags. Isaac Lab 3.0 is a multi-backend rewrite spanning PhysX, Newton and OVPhysX and its API is still moving, so everything below targets 2.3.x on Isaac Sim 5.1.0 with Python 3.11, with the 3.0 differences flagged where they matter.

The pip path

# 1. Python 3.11 exactly. Isaac Sim 5.x will not install on 3.10 or 3.12.
conda create -n env_isaaclab python=3.11
conda activate env_isaaclab
# venv works just as well:
#   python3.11 -m venv env_isaaclab && source env_isaaclab/bin/activate

pip install --upgrade pip

# 2. Isaac Sim, from NVIDIA's own index rather than PyPI.
pip install "isaacsim[all,extscache]==5.1.0" --extra-index-url https://pypi.nvidia.com

# 3. PyTorch, pinned. x86_64 Linux and Windows:
pip install -U torch==2.7.0 torchvision==0.22.0 --index-url https://download.pytorch.org/whl/cu128
# aarch64 Linux instead:
#   pip install -U torch==2.9.0 torchvision==0.24.0 --index-url https://download.pytorch.org/whl/cu130

# 4. Isaac Lab itself, from source. This is not optional, see the note below.
git clone https://github.com/isaac-sim/IsaacLab.git --branch main
cd IsaacLab
./isaaclab.sh --install        # Linux. isaaclab.bat --install on Windows.
# Limit the RL dependencies if you only want one:
#   ./isaaclab.sh --install rsl_rl

# 5. Smoke test. Prints every registered gym id.
./isaaclab.sh -p scripts/environments/list_envs.py

About step 4. The core isaaclab package is published on PyPI, first as 2.0.0 on 2025-01-31 and most recently as 2.3.2.post1 on 2026-02-11 with requires_python pinned to 3.11. But isaaclab_tasks, isaaclab_assets, isaaclab_rl and isaaclab_mimic are not on PyPI at all, and those are the packages that hold the 30-plus ready-to-train environments, the robot configurations and every train.py. A pip-only install gives you the library and none of the content. Clone the repository.

The isaaclab.sh helper

Almost every command in the Isaac Lab documentation goes through this script rather than through python directly, because it resolves the right interpreter, the right environment and the right extension paths. In 2.x it is a bash script of roughly 770 lines.

./isaaclab.sh -h                 # --help     print this list
./isaaclab.sh -i [LIB]           # --install  install extensions and RL libraries, default all
./isaaclab.sh -f                 # --format   run pre-commit, format and lint
./isaaclab.sh -p <script.py>     # --python   run a script with the resolved interpreter
./isaaclab.sh -s                 # --sim      launch isaac-sim.sh directly
./isaaclab.sh -t                 # --test     run the pytest suite
./isaaclab.sh -o                 # --docker   delegate to docker/container.sh
./isaaclab.sh -v                 # --vscode   generate .vscode settings
./isaaclab.sh -d                 # --docs     build the sphinx docs
./isaaclab.sh -n                 # --new      scaffold a new project or task from a template
./isaaclab.sh -c [NAME]          # --conda    create a conda env, default env_isaaclab
./isaaclab.sh -u [NAME]          # --uv       create a uv env, default env_isaaclab

In 3.0 that script collapses to a 48-line shim whose last line is exec "$python_exe" -c "from isaaclab.cli import cli; cli()" "$@". The same flags survive, and two unified subcommands are added on top, so ./isaaclab.sh train --task=Isaac-Cartpole-v0 --rl_library=rsl_rl --headless replaces reaching into a per-library script directory. Treat that form as beta.

The container path

If you would rather not manage the Python environment, the repository ships a container driver. It needs Docker Engine 26.0.0 or newer, Docker Compose 2.25.0 or newer and the NVIDIA Container Toolkit, and the Isaac Lab directory has to live somewhere under /home.

./docker/container.py build   [base|ros2]
./docker/container.py start   [base|ros2]
./docker/container.py enter   [base|ros2]
./docker/container.py stop    [base|ros2]
./docker/container.py copy                 # pull logs, data and docs into docker/artifacts

# Or skip the build entirely with the prebuilt headless image.
# Source lives at /workspace/IsaacLab inside it.
docker pull nvcr.io/nvidia/isaac-lab:2.3.2

The import paths changed, and most tutorials are stale

This deserves its own section because it is the fastest way to tell whether a tutorial you found is current. I checked it at the tag level rather than trusting release prose. The GitHub contents API at tag v1.4.1 returns source/extensions/omni.isaac.lab, source/extensions/omni.isaac.lab_assets and source/extensions/omni.isaac.lab_tasks. At tag v2.0.0 it returns source/isaaclab, source/isaaclab_assets, source/isaaclab_mimic, source/isaaclab_rl and source/isaaclab_tasks. The directory nesting went away at the same time.

# -----------------------------------------------------------------
# ERA 1  Orbit, Isaac Lab v0.x up to v0.3.1. Dead.
#        layout: source/extensions/omni.isaac.orbit*/
# -----------------------------------------------------------------
from omni.isaac.orbit.envs import RLTaskEnvCfg
from omni.isaac.orbit.managers import RewardTermCfg
from omni.isaac.orbit_assets.cartpole import CARTPOLE_CFG
import omni.isaac.orbit_tasks

# -----------------------------------------------------------------
# ERA 2  Isaac Lab v1.0.0 through v1.4.1, 2024-06-26 to 2025-01-30. Dead.
#        layout: source/extensions/omni.isaac.lab*/
# -----------------------------------------------------------------
from omni.isaac.lab.envs import ManagerBasedRLEnvCfg
from omni.isaac.lab.managers import RewardTermCfg
from omni.isaac.lab_assets.cartpole import CARTPOLE_CFG
import omni.isaac.lab_tasks

# -----------------------------------------------------------------
# ERA 3  Isaac Lab v2.0.0 onward, 2025-01-30. Current.
#        layout: source/isaaclab*/     note the missing source/extensions/
# -----------------------------------------------------------------
import isaaclab.sim as sim_utils
from isaaclab.envs import ManagerBasedRLEnvCfg, DirectRLEnvCfg
from isaaclab.managers import RewardTermCfg
from isaaclab_assets.robots.cartpole import CARTPOLE_CFG
import isaaclab_tasks

Isaac Sim 4.5 renamed its own extensions in the same window, independently of the Isaac Lab rename, which is why two unrelated sets of broken imports tend to land in your terminal on the same afternoon.

# Isaac Sim 4.5 renames, unrelated to the Isaac Lab rename above.
# omni.isaac.kit.SimulationApp        -> isaacsim.SimulationApp
# omni.isaac.core.prims               -> isaacsim.core.prims
# omni.isaac.core.simulation_context  -> isaacsim.core.api.simulation_context
# omni.isaac.cloner                   -> isaacsim.core.cloner
# omni.importer.urdf                  -> isaacsim.asset.importer.urdf
# omni.importer.mjcf                  -> isaacsim.asset.importer.mjcf

# Class renames in the same release. The view classes dropped their suffix and
# the single-object classes gained a Single prefix, so the meaning of the bare
# name Articulation inverted:
#   ArticulationView -> Articulation
#   Articulation     -> SingleArticulation

One trap to know about, because it is an upstream documentation error rather than a misremembering on anyone's part. The official migration guide instructs you to replace from omni.isaac.lab_assets.anymal import ANYMAL_C_CFG with from isaaclab.robots.anymal import ANYMAL_C_CFG. That module does not exist. The real path is the assets package, as in from isaaclab_assets.robots.cartpole import CARTPOLE_CFG, which is what the repository's own task code uses. Follow the code, not the guide.

The environment model

Isaac Lab offers two ways to define a task, and picking the wrong one costs you a rewrite rather than a tweak. Both are gymnasium environments from the outside and both are configured with @configclass dataclasses.

WorkflowBase classChoose it when
Manager-based ManagerBasedEnv, ManagerBasedRLEnv You want reward, observation, reset and randomization terms to be independently swappable, several people are editing the same task, or you plan to run ablations by config rather than by branch.
Direct DirectRLEnv, DirectMARLEnv The logic does not decompose into terms, you want a single fused PyTorch or Warp kernel over the whole step, you are chasing the last slice of throughput, or you are porting a task from IsaacGymEnvs or OmniIsaacGymEnvs.

The manager-based workflow is built out of a fixed set of managers, exported from isaaclab.managers. They are ActionManager, CommandManager, CurriculumManager, EventManager, ObservationManager, RewardManager, TerminationManager and RecorderManager, plus ManagerBase, ManagerTermBase and SceneEntityCfg. Each takes a matching term config, so RewardTermCfg, ObservationTermCfg and ObservationGroupCfg, EventTermCfg, TerminationTermCfg, CurriculumTermCfg, CommandTermCfg and RecorderTermCfg.

Domain randomization lives in the EventManager rather than in a manager of its own. An EventTermCfg carries a mode field whose value is "startup", "reset" or "interval", which is how you express the difference between perturbing masses once when the scene loads, resampling joint positions on every episode reset, and pushing the robot every few seconds during an episode. If you find older material referring to a randomization manager, this is what it became.

A real manager-based task configuration

This is the cartpole configuration from isaaclab_tasks, lightly trimmed. It is worth reading closely because every locomotion and manipulation task in the repository has exactly this shape, only longer.

cartpole_env_cfg.pyimport math

import isaaclab.sim as sim_utils
from isaaclab.assets import ArticulationCfg, AssetBaseCfg
from isaaclab.envs import ManagerBasedRLEnvCfg
from isaaclab.managers import EventTermCfg as EventTerm
from isaaclab.managers import ObservationGroupCfg as ObsGroup
from isaaclab.managers import ObservationTermCfg as ObsTerm
from isaaclab.managers import RewardTermCfg as RewTerm
from isaaclab.managers import SceneEntityCfg
from isaaclab.managers import TerminationTermCfg as DoneTerm
from isaaclab.scene import InteractiveSceneCfg
from isaaclab.utils import configclass

import isaaclab_tasks.manager_based.classic.cartpole.mdp as mdp

from isaaclab_assets.robots.cartpole import CARTPOLE_CFG  # isort:skip


@configclass
class CartpoleSceneCfg(InteractiveSceneCfg):
    """Everything that exists in the world, once. Isaac Lab clones it N times."""

    ground = AssetBaseCfg(
        prim_path="/World/ground",
        spawn=sim_utils.GroundPlaneCfg(size=(100.0, 100.0)),
    )
    # {ENV_REGEX_NS} expands to /World/envs/env_.* so one entry becomes N clones
    robot: ArticulationCfg = CARTPOLE_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot")
    dome_light = AssetBaseCfg(
        prim_path="/World/DomeLight",
        spawn=sim_utils.DomeLightCfg(color=(0.9, 0.9, 0.9), intensity=500.0),
    )


@configclass
class ActionsCfg:
    joint_effort = mdp.JointEffortActionCfg(
        asset_name="robot", joint_names=["slider_to_cart"], scale=100.0
    )


@configclass
class ObservationsCfg:
    @configclass
    class PolicyCfg(ObsGroup):
        # term order is preserved and defines the observation vector layout
        joint_pos_rel = ObsTerm(func=mdp.joint_pos_rel)
        joint_vel_rel = ObsTerm(func=mdp.joint_vel_rel)

        def __post_init__(self) -> None:
            self.enable_corruption = False
            self.concatenate_terms = True

    policy: PolicyCfg = PolicyCfg()


@configclass
class EventCfg:
    """Randomization. mode is one of startup, reset, interval."""

    reset_cart_position = EventTerm(
        func=mdp.reset_joints_by_offset,
        mode="reset",
        params={
            "asset_cfg": SceneEntityCfg("robot", joint_names=["slider_to_cart"]),
            "position_range": (-1.0, 1.0),
            "velocity_range": (-0.5, 0.5),
        },
    )
    reset_pole_position = EventTerm(
        func=mdp.reset_joints_by_offset,
        mode="reset",
        params={
            "asset_cfg": SceneEntityCfg("robot", joint_names=["cart_to_pole"]),
            "position_range": (-0.25 * math.pi, 0.25 * math.pi),
            "velocity_range": (-0.25 * math.pi, 0.25 * math.pi),
        },
    )


@configclass
class RewardsCfg:
    alive = RewTerm(func=mdp.is_alive, weight=1.0)
    terminating = RewTerm(func=mdp.is_terminated, weight=-2.0)
    pole_pos = RewTerm(
        func=mdp.joint_pos_target_l2,
        weight=-1.0,
        params={"asset_cfg": SceneEntityCfg("robot", joint_names=["cart_to_pole"]), "target": 0.0},
    )
    cart_vel = RewTerm(
        func=mdp.joint_vel_l1,
        weight=-0.01,
        params={"asset_cfg": SceneEntityCfg("robot", joint_names=["slider_to_cart"])},
    )


@configclass
class TerminationsCfg:
    time_out = DoneTerm(func=mdp.time_out, time_out=True)
    cart_out_of_bounds = DoneTerm(
        func=mdp.joint_pos_out_of_manual_limit,
        params={
            "asset_cfg": SceneEntityCfg("robot", joint_names=["slider_to_cart"]),
            "bounds": (-3.0, 3.0),
        },
    )


@configclass
class CartpoleEnvCfg(ManagerBasedRLEnvCfg):
    scene: CartpoleSceneCfg = CartpoleSceneCfg(num_envs=4096, env_spacing=4.0, clone_in_fabric=True)
    observations: ObservationsCfg = ObservationsCfg()
    actions: ActionsCfg = ActionsCfg()
    events: EventCfg = EventCfg()
    rewards: RewardsCfg = RewardsCfg()
    terminations: TerminationsCfg = TerminationsCfg()

    def __post_init__(self) -> None:
        self.decimation = 2                    # one policy step per 2 physics steps
        self.episode_length_s = 5
        self.viewer.eye = (8.0, 0.0, 5.0)
        self.sim.dt = 1 / 120                  # 120 Hz physics, so 60 Hz control
        self.sim.render_interval = self.decimation

Three details in that file matter more than they look. The {ENV_REGEX_NS} token is what turns one authored robot into num_envs clones without you writing a loop. The decimation and sim.dt pair is your real control rate, and people routinely report a control frequency they never actually ran because they read dt and forgot the decimation. And clone_in_fabric=True is a Fabric-backed cloning path that does not exist in Isaac Lab 1.x samples, so its presence is another quick freshness test on code you find.

The direct workflow, for comparison

cartpole_env.pyfrom __future__ import annotations

import torch

import isaaclab.sim as sim_utils
from isaaclab.assets import Articulation, ArticulationCfg
from isaaclab.envs import DirectRLEnv, DirectRLEnvCfg
from isaaclab.scene import InteractiveSceneCfg
from isaaclab.sim import SimulationCfg
from isaaclab.utils import configclass

from isaaclab_assets.robots.cartpole import CARTPOLE_CFG


@configclass
class CartpoleEnvCfg(DirectRLEnvCfg):
    # No managers. Spaces are declared as plain numbers.
    decimation = 2
    episode_length_s = 5.0
    action_scale = 100.0
    action_space = 1
    observation_space = 4
    state_space = 0

    sim: SimulationCfg = SimulationCfg(dt=1 / 120, render_interval=decimation)
    robot_cfg: ArticulationCfg = CARTPOLE_CFG.replace(prim_path="/World/envs/env_.*/Robot")
    scene: InteractiveSceneCfg = InteractiveSceneCfg(
        num_envs=4096, env_spacing=4.0, replicate_physics=True, clone_in_fabric=True
    )


class CartpoleEnv(DirectRLEnv):
    cfg: CartpoleEnvCfg

    def __init__(self, cfg: CartpoleEnvCfg, render_mode: str | None = None, **kwargs):
        super().__init__(cfg, render_mode, **kwargs)
        self._cart_dof_idx, _ = self.cartpole.find_joints("slider_to_cart")
        self._pole_dof_idx, _ = self.cartpole.find_joints("cart_to_pole")
        self.joint_pos = self.cartpole.data.joint_pos
        self.joint_vel = self.cartpole.data.joint_vel

    def _setup_scene(self):
        self.cartpole = Articulation(self.cfg.robot_cfg)
        # then add the ground plane, clone the envs, register the articulation

    # You now write, by hand, everything the managers were doing:
    #   _pre_physics_step(actions)   _apply_action()
    #   _get_observations()          _get_rewards()
    #   _get_dones()                 _reset_idx(env_ids)

The trade is visible in the file length. The direct environment is one class you can read top to bottom and profile as a unit. The manager-based environment is six small classes you can recombine without touching a step function. For a task you expect to iterate on for months, the second is usually the better bet. For a task where the reward is one fused kernel over a large state tensor, the first is.

Registering your own task

# my_tasks/__init__.py  -  this is what makes --task=My-Drone-Nav-v0 resolvable
import gymnasium as gym

from . import agents
from .drone_nav_env_cfg import DroneNavEnvCfg

gym.register(
    id="My-Drone-Nav-v0",
    entry_point="isaaclab.envs:ManagerBasedRLEnv",     # manager-based workflow
    disable_env_checker=True,
    kwargs={
        "env_cfg_entry_point": DroneNavEnvCfg,
        "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:DroneNavPPORunnerCfg",
        "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml",
        "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml",
    },
)

# For the direct workflow the entry point names your own class instead:
#   entry_point="my_tasks.drone_nav_env:DroneNavEnv"

You do not have to write that file by hand. ./isaaclab.sh --new scaffolds a project, asking whether it is external or internal, where it lives, which workflow you want and which RL library and algorithm to wire up. A pip-only install cannot use the internal option, which is one more reason to clone.

Launching training

Isaac Lab does not implement an RL algorithm. It ships thin adapters to four external libraries and one entry-point script per library, under scripts/reinforcement_learning/. The choice between them is mostly about which ecosystem you already trust.

LibraryNotes
rsl_rlNVIDIA's own, PPO focused, the default for the locomotion tasks. Smallest surface area.
rl_gamesLong lineage from IsaacGymEnvs, heavily tuned for massive vectorized envs.
skrlBroadest algorithm coverage, PyTorch and JAX, and the only one wired for multi-agent tasks here.
Stable-Baselines3Familiar API, slowest of the four in this setting, and no distributed support.
# rsl_rl, the usual starting point
./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py \
    --task Isaac-Velocity-Rough-Anymal-C-v0 --headless

# replay a checkpoint with a small number of visible envs
./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/play.py \
    --task Isaac-Velocity-Rough-Anymal-C-v0 --num_envs 32 \
    --load_run run_folder_name --checkpoint /PATH/TO/model.pt

# rl_games
./isaaclab.sh -p scripts/reinforcement_learning/rl_games/train.py \
    --task Isaac-Ant-v0 --headless

# skrl, including a multi-agent task
./isaaclab.sh -p scripts/reinforcement_learning/skrl/train.py \
    --task Isaac-Shadow-Hand-Over-Direct-v0 --headless --algorithm MAPPO

# record video from a headless run
./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py \
    --task Isaac-Reach-Franka-v0 --headless \
    --video --video_length 200 --video_interval 2000

# watch it
./isaaclab.sh -p -m tensorboard.main --logdir=logs

It helps to know where each flag comes from, because the documentation lists them in two different places. AppLauncher contributes the flags every Isaac Lab script accepts, which are --headless, --enable_cameras, --device, --livestream, --xr, --experience, --rendering_mode, --kit_args, --cpu, --verbose and --info. The individual train.py scripts add --task, --num_envs, --seed, --max_iterations, --distributed, --agent, --video, --video_length and --video_interval. The play.py scripts add --checkpoint and, for rsl_rl, --load_run.

Two of those flags are load-bearing in a way the names do not reveal. --num_envs overrides the number in the config, so it is how you drop from 4096 to 32 for a debug run without editing anything. And --enable_cameras is mandatory for any task with a camera sensor, including in headless mode. Camera tasks do not render without it and they do not warn you loudly.

Scaling out

# One node, two GPUs. Both parts are required: torch.distributed.run
# spawns the ranks, and --distributed tells Isaac Lab it is one of them.
python -m torch.distributed.run --nnodes=1 --nproc_per_node=2 \
    scripts/reinforcement_learning/rsl_rl/train.py \
    --task=Isaac-Cartpole-v0 --headless --distributed

# Two nodes, two GPUs each. Master is node_rank 0, each worker increments.
python -m torch.distributed.run --nproc_per_node=2 --nnodes=2 --node_rank=0 \
    --master_addr=<ip_of_master> --master_port=5555 \
    scripts/reinforcement_learning/rl_games/train.py \
    --task=Isaac-Cartpole-v0 --headless --distributed

# skrl on JAX uses a different launcher entirely
python -m skrl.utils.distributed.jax --nnodes=1 --nproc_per_node=2 \
    scripts/reinforcement_learning/skrl/train.py \
    --task=Isaac-Cartpole-v0 --headless --distributed --ml_framework jax

# Distributed training is available for rl_games, rsl_rl and skrl only.
# Stable-Baselines3 has no distributed path here.
#
# And every one of these GPUs needs RT cores. A multi-node A100 or H100
# cluster will not run any of the commands above.

Tiled rendering

If your task uses cameras, this is the feature that decides whether it is feasible at all. The naive implementation of vision-based RL over N parallel environments creates N render products and reads back N images every step, which means N device synchronizations per frame. Tiled rendering instead concatenates every clone of a camera into one large render product and slices the result. The documentation states the reason directly, that only a single call is used to synchronize the device data instead of one call per camera, and that this is a big part of what makes the tiled rendering API more efficient for working with vision data.

import isaaclab.sim as sim_utils
from isaaclab.sensors import TiledCameraCfg

tiled_camera: TiledCameraCfg = TiledCameraCfg(
    prim_path="/World/envs/env_.*/Camera",          # the regex matches every clone
    offset=TiledCameraCfg.OffsetCfg(
        pos=(-7.0, 0.0, 3.0),
        rot=(0.9945, 0.0, 0.1045, 0.0),
        convention="world",
    ),
    data_types=["rgb"],
    spawn=sim_utils.PinholeCameraCfg(
        focal_length=24.0,
        focus_distance=400.0,
        horizontal_aperture=20.955,
        clipping_range=(0.1, 20.0),
    ),
    width=80,
    height=80,
)

# Available data_types:
#   rgb, rgba, distance_to_camera, distance_to_image_plane, depth, normals,
#   motion_vectors, semantic_segmentation,
#   instance_segmentation_fast, instance_id_segmentation_fast

# Launch camera tasks with --enable_cameras or nothing renders:
#   ./isaaclab.sh -p scripts/reinforcement_learning/rl_games/train.py \
#       --task=Isaac-Cartpole-RGB-Camera-Direct-v0 --headless --enable_cameras

Note the resolution in that config. Eighty by eighty pixels is not a typo, and it is the honest scale at which vision-based RL over thousands of environments currently runs. The binding constraint is bandwidth rather than shading. NVIDIA frames it as a single 60 fps camera recording every frame moving roughly 120 MB per second, which then multiplies by cameras times environments, and recommends running on the order of 512 cameras in the scene on an RTX 4090 or similar when the images feed an image-processing network. I have not seen first-party frames-per-second numbers published for this, so treat any benchmark table you find elsewhere with suspicion unless it names its exact build.

In Isaac Lab 3.0 beta, TiledCamera is folded into Camera. The release notes add that existing tiled-camera aliases remain as a compatibility surface where available, so this is a migration rather than a hard break, but new code should be written against Camera and CameraCfg.

A worked framing, a drone in an unknown map

Concretely, suppose the goal is what my aerial autonomy lab is about. A quadrotor with no GPS, building a map as it goes, planning with RRT* toward a goal it cannot see. What part of that would you actually put in Isaac Lab, and what part would you not?

The first thing to be clear about is that RRT* is not a learning problem. It is a sampling-based planner with well understood asymptotic optimality properties, covered in the motion planning note, and training a network to imitate it is a research project rather than an engineering shortcut. The planner stays hand-written. What is worth learning is the layer beneath it, the policy that turns a reference path into motor commands while staying alive around obstacles the planner did not know about when it committed. That layer is contact-adjacent, dynamics heavy, and hard to hand-tune, which is exactly the profile a simulator like this exists for.

RRT* global planner            hand written, runs at ~1 Hz on a fresh occupancy grid
        │  waypoints
        ▼
learned local policy           Isaac Lab, 50 to 100 Hz, trained over thousands of clones
        │  body rates / thrust
        ▼
low level attitude control     hand written, runs on the flight controller

Structurally the task looks like the cartpole config, with a different scene and terms that are yours rather than the library's. The sketch below is my own composition rather than repository code, and the mdp functions it references are ones you would write. That is normal. Term functions are plain callables taking the environment and returning a tensor, and the library only supplies the common ones. Check the exact keyword names against the API reference for the version you install, since the term configs gained and lost fields between 2.0 and 2.3.

drone_nav_env_cfg.pyimport isaaclab.sim as sim_utils
from isaaclab.envs import ManagerBasedRLEnvCfg
from isaaclab.managers import EventTermCfg as EventTerm
from isaaclab.managers import ObservationGroupCfg as ObsGroup
from isaaclab.managers import ObservationTermCfg as ObsTerm
from isaaclab.managers import RewardTermCfg as RewTerm
from isaaclab.managers import SceneEntityCfg
from isaaclab.managers import TerminationTermCfg as DoneTerm
from isaaclab.scene import InteractiveSceneCfg
from isaaclab.sensors import TiledCameraCfg
from isaaclab.utils import configclass

import my_tasks.drone_nav.mdp as mdp        # your own term functions


@configclass
class DroneNavSceneCfg(InteractiveSceneCfg):
    # MY_QUADROTOR_CFG is an ArticulationCfg you author yourself, either from a
    # USD file with sim_utils.UsdFileCfg or straight from URDF with UrdfFileCfg.
    robot = MY_QUADROTOR_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot")

    # depth only. RGB is not what the local policy needs and costs bandwidth.
    depth_cam = TiledCameraCfg(
        prim_path="{ENV_REGEX_NS}/Robot/base/front_cam",
        data_types=["distance_to_image_plane"],
        spawn=sim_utils.PinholeCameraCfg(
            focal_length=12.0, clipping_range=(0.1, 12.0)
        ),
        width=64,
        height=48,
    )


@configclass
class ObservationsCfg:
    @configclass
    class PolicyCfg(ObsGroup):
        # what the drone can actually know without GPS
        lin_vel_b = ObsTerm(func=mdp.base_lin_vel_body)
        ang_vel_b = ObsTerm(func=mdp.base_ang_vel_body)
        projected_gravity = ObsTerm(func=mdp.projected_gravity)
        next_waypoint_b = ObsTerm(func=mdp.waypoint_in_body_frame)   # from the planner
        depth = ObsTerm(func=mdp.depth_image, params={"sensor_cfg": SceneEntityCfg("depth_cam")})
        last_action = ObsTerm(func=mdp.last_action)

        def __post_init__(self) -> None:
            self.enable_corruption = True     # sensor noise on, this is the point
            self.concatenate_terms = False    # keep the image separate from the vector

    policy: PolicyCfg = PolicyCfg()


@configclass
class EventCfg:
    # startup: things that differ between one built drone and another
    randomize_mass = EventTerm(
        func=mdp.randomize_rigid_body_mass,
        mode="startup",
        params={"asset_cfg": SceneEntityCfg("robot", body_names="base"),
                "mass_distribution_params": (0.9, 1.1), "operation": "scale"},
    )
    # reset: a different obstacle field and start pose every episode
    resample_clutter = EventTerm(func=mdp.resample_obstacles, mode="reset")
    # interval: wind gusts partway through an episode
    gust = EventTerm(
        func=mdp.push_by_setting_velocity,
        mode="interval",
        interval_range_s=(2.0, 6.0),
        params={"velocity_range": {"x": (-1.0, 1.0), "y": (-1.0, 1.0)}},
    )


@configclass
class RewardsCfg:
    progress = RewTerm(func=mdp.progress_toward_waypoint, weight=1.0)
    clearance = RewTerm(func=mdp.min_obstacle_distance_penalty, weight=-0.5)
    action_rate = RewTerm(func=mdp.action_rate_l2, weight=-0.01)
    upright = RewTerm(func=mdp.flat_orientation_l2, weight=-0.2)


@configclass
class TerminationsCfg:
    time_out = DoneTerm(func=mdp.time_out, time_out=True)
    collision = DoneTerm(func=mdp.illegal_contact,
                         params={"sensor_cfg": SceneEntityCfg("contact_forces"),
                                 "threshold": 1.0})


@configclass
class DroneNavEnvCfg(ManagerBasedRLEnvCfg):
    # 4096 is the cartpole number. A depth camera per env will not reach it.
    scene: DroneNavSceneCfg = DroneNavSceneCfg(num_envs=512, env_spacing=8.0)
    observations: ObservationsCfg = ObservationsCfg()
    events: EventCfg = EventCfg()
    rewards: RewardsCfg = RewardsCfg()
    terminations: TerminationsCfg = TerminationsCfg()

    def __post_init__(self) -> None:
        self.decimation = 4
        self.episode_length_s = 20.0
        self.sim.dt = 1 / 200                       # 200 Hz physics, 50 Hz control
        self.sim.render_interval = self.decimation

Three observations about that sketch, all of which cost me more thought than the code did.

  • The environment count drops by an order of magnitude. Cartpole runs 4096 environments because its observation is four floats. A 64 by 48 depth image per environment is a different regime, and the tiled rendering guidance about roughly 512 cameras on a 4090-class card is the number to plan around.
  • The planner stays outside the environment. The policy sees the next waypoint in body frame and nothing about how it was produced, which means you can swap RRT* for anything else later without retraining. This is the same separation that makes the imitation versus RL note land on sim-to-real as the real difficulty. The learned part should be the smallest part that has to be learned.
  • The randomization is the product. The three event modes above are doing the actual work of making the policy survive a real drone. Mass and thrust coefficient at startup, obstacle layout and start pose at reset, gusts at interval. A policy trained without them will fly beautifully in the viewport and badly outdoors.

When a custom simulator beats Isaac Lab

Isaac Lab is the right tool when the physics of contact, the appearance of the world, or the sheer number of parallel robots is the hard part. It is the wrong tool more often than its marketing suggests, and being clear about when saves real time.

  • Determinism across builds. A planner benchmark needs to give the same answer next year. Isaac Sim pins a Kit SDK per release, ships GPU-dependent solver behaviour, and changes defaults between versions. Isaac Sim 5.0 turned on fixed time stepping in the full experience by default, which changed how animations played back relative to 4.5 and needed three separate flags to undo. A geometry-only simulator you wrote is a few hundred lines with a fixed seed and no driver dependency, and it will reproduce.
  • Throughput for geometry-only sensing. If the sensor model is a ray cast against a mesh and the dynamics are a double integrator, rendering is pure overhead. Batched ray casts in Warp or a plain occupancy grid lookup will beat the full RTX pipeline by a wide margin for the same physics fidelity, and they run on any CUDA device including the ones Isaac Sim refuses.
  • Portability, which for me is the deciding one. Isaac Lab has a hard floor of an RT-core GPU, a 16 GB VRAM recommendation, a specific driver branch, a specific Python version and an operating system list that dropped Ubuntu 20.04 and Windows 10 along the way. A NumPy and Warp simulator runs on a laptop, on a CI runner with no GPU at all, and on the H100 box. Anything that has to run in continuous integration should not depend on a renderer.
  • Not tying published numbers to one Omniverse build. If a result in a write-up is only reproducible on Isaac Sim 5.1.0 with driver 580.65.06 on an RTX 4090, then the build is part of the claim and has to be stated as such. That is a fine cost for a locomotion policy where no alternative exists. It is a bad trade for a planner comparison that a self-contained script could have produced.

The practical shape this settles into is a two-simulator setup rather than a choice. Geometry, planning, estimation and anything that runs in CI go in a small deterministic simulator that runs anywhere. Contact-rich control, vision in the loop and large-scale domain randomization go in Isaac Lab on hardware that can render. The interface between them is the waypoint and the state estimate, which are cheap to keep identical on both sides.

Papers and further reading

The ideas behind this framework come from a small set of sources, and each rewards a direct read. Where this site derives the same idea in depth, the companion link points there.

  1. Makoviychuk et al., Isaac Gym, High Performance GPU-Based Physics Simulation For Robot Learning, 2021. The paper that established the end-to-end GPU pipeline Isaac Lab inherited, where observations, actions and physics state never leave device memory. The robot kinematics and control coursework derives the simulation side of the same argument.
  2. Mittal et al., Isaac Lab, 2025, linked from the repository README. The framework paper for the manager and direct workflows described above.
  3. NVIDIA, Isaac Lab documentation. The main version tracks the 2.3.x line. Read the multi-GPU page and the task workflows page before anything else.
  4. NVIDIA, Isaac Sim 5.1.0 system requirements. The source of the RT core rule, the GPU ladder and the driver floors. Version-pinned on purpose, since the unpinned path serves whatever is newest and the numbers drift. The companion Isaac Sim guide covers the rest of the installation surface.
  5. NVIDIA, Migrating from Isaac Lab 1.x. Useful for the rename tables and wrong in one place, as noted above, where it points ANYMAL_C_CFG at isaaclab.robots instead of isaaclab_assets.robots.
  6. Schulman et al., Proximal Policy Optimization Algorithms, 2017. The algorithm behind almost every example task shipped with Isaac Lab. The PPO derivation on this site works through the clipped objective, and the deep reinforcement learning coursework covers where it sits among the alternatives.
  7. NVIDIA, Isaac Gym, now deprecated. Worth one visit purely to confirm the lineage in NVIDIA's own words before you follow an old tutorial.
  8. Isaac Sim maintainers, discussion #548 on alternative renderers. The clearest statement that the RTX renderer is not swappable and that sensor simulation is coupled to it, which is why headless mode does not rescue a non-RTX GPU.
  9. PyTorch and Ray walkthroughs on this site. Isaac Lab's tensors, policies and learning libraries are all PyTorch, and Ray is what the repository leans on for sweeps and multi-run tooling.

Final takeaway

The interesting decision with Isaac Lab is not how to configure a task. It is whether the problem you have is one where photoreal rendering and contact-rich physics are load bearing, because the price of admission is a specific class of GPU and a version-pinned software stack that you will be quoting alongside your results for as long as you cite them.

Key takeaway: Isaac Lab makes a task a dataclass, which is a genuinely good idea, and it does it on top of a renderer that will not start on an A100 or an H100. Decide the hardware question before the API question. If your training box is a datacenter GPU, the honest options are workstation RTX cards, RT-core datacenter parts like the L40S, or a small deterministic simulator of your own for everything that does not need pixels. The last of those is not a workaround, it is the right answer for planners, estimators and anything that has to reproduce a year from now.