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.
| Tier | GPU | VRAM |
|---|---|---|
| Minimum | GeForce RTX 4080 | 16 GB |
| Good | GeForce RTX 5080 | 16 GB |
| Ideal | RTX PRO 6000 Blackwell | 48 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.
- 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.
- 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.
- 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 Lab | Released | Isaac Sim | Python | Namespace |
|---|---|---|---|---|
| v1.0.0 | 2024-06-26 | 4.0 | 3.10 | omni.isaac.lab* |
| v1.4.1 | 2025-01-30 | 4.2.0 | 3.10 | omni.isaac.lab* |
| v2.0.0 | 2025-01-30 | 4.5.0 | 3.10 | isaaclab* |
| v2.2.0 | 2025-08-07 | 5.0.0 | 3.11 | isaaclab* |
| v2.3.0 | 2025-10-28 | 5.1.0 | 3.11 | isaaclab* |
| v2.3.2 | 2026-02-02 | 5.1.0 | 3.11 | isaaclab* |
| v3.0.0-beta2 | 2026-06-17 | 6.0.0 | 3.12 | isaaclab* plus backends |
| v3.0.0-beta2.patch1 | 2026-07-02 | 6.0.1 | 3.12 | isaaclab* 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.2The 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_tasksIsaac 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.
| Workflow | Base class | Choose 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.
| Library | Notes |
|---|---|
| rsl_rl | NVIDIA's own, PPO focused, the default for the locomotion tasks. Smallest surface area. |
| rl_games | Long lineage from IsaacGymEnvs, heavily tuned for massive vectorized envs. |
| skrl | Broadest algorithm coverage, PyTorch and JAX, and the only one wired for multi-agent tasks here. |
| Stable-Baselines3 | Familiar 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_camerasNote 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.decimationThree 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.
- 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.
- Mittal et al., Isaac Lab, 2025, linked from the repository README. The framework paper for the manager and direct workflows described above.
-
NVIDIA,
Isaac Lab documentation.
The
mainversion tracks the 2.3.x line. Read the multi-GPU page and the task workflows page before anything else. - 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.
-
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_CFGatisaaclab.robotsinstead ofisaaclab_assets.robots. - 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.
- NVIDIA, Isaac Gym, now deprecated. Worth one visit purely to confirm the lineage in NVIDIA's own words before you follow an old tutorial.
- 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.
- 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.