> This page is for version Release 3.0 (default).
> For other versions, use one of these documentation indexes:
> - Release 3.0 (default): https://docs.nvidia.com/isaaclab/3.0/llms.txt
> - Release 2.3: https://docs.nvidia.com/isaaclab/2.3/llms.txt
> - Release 2.2: https://docs.nvidia.com/isaaclab/2.2/llms.txt
> - Release 2.1: https://docs.nvidia.com/isaaclab/2.1/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.nvidia.com/isaaclab/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nvidia.com/isaaclab/_mcp/server.

# Warp Environments

> **Note**
>
> The warp environment infrastructure lives in `isaaclab_experimental` and
> `isaaclab_tasks_experimental`. It's an experimental feature.

Experimental Warp environments compute observations, rewards, resets, and actions
in Warp kernels. CUDA graph capture reduces Python overhead during execution.

Here, **stable** means the default Torch environments in `isaaclab` and
`isaaclab_tasks`. Pass `--frontend warp` to use their experimental Warp counterparts.

## Quick Start

```bash
# Direct workflow: stable task, warp env implementation
uv run isaaclab train --rl_library rsl_rl \
    --task Isaac-Cartpole-Direct --frontend warp presets=newton_mjwarp --num_envs 4096
uv run isaaclab play --rl_library rsl_rl \
    --task Isaac-Cartpole-Direct --frontend warp presets=newton_mjwarp \
    --num_envs 32 --checkpoint latest --visualizer newton_gl

# Manager-based workflow: stable task on the warp runtime
uv run isaaclab train --rl_library rsl_rl \
    --task Isaac-Velocity-Flat-AnymalD --frontend warp presets=newton_mjwarp --num_envs 4096
uv run isaaclab play --rl_library rsl_rl \
    --task Isaac-Velocity-Flat-AnymalD --frontend warp presets=newton_mjwarp \
    --num_envs 32 --checkpoint latest --visualizer newton_gl
```

All RL libraries with warp-compatible wrappers are supported: RSL-RL, RL Games, SKRL, and
Stable-Baselines3.

> **Note**
>
> `--video` is rejected on the warp path, for both `train` and `play`: video
> recording requires the standard torch frontend. To record a rollout, replay the same
> checkpoint with `--frontend torch`; see [Recording video](/isaaclab/3.0/isaac-lab/recording-video).

## Workflows

Two environment workflows are supported:

**Direct workflow** — `DirectRLEnvWarp` base class. You implement the step loop, observations,
rewards, and resets directly in your env class using Warp kernels.

**Manager-based workflow** — `ManagerBasedRLEnvWarp` base class. You define MDP terms as
standalone Warp-kernel functions and compose them via configuration.

## Available Environments

### Direct Warp Environments

Direct tasks share the stable task configuration. `--frontend warp` resolves
the stable `<task>_direct_env:<Name>Env` entry point to the mirrored
`<task>_warp_env:<Name>WarpEnv` implementation and swaps only the environment
class. Registrations can use `warp_entry_point` as an optional override when
the implementation cannot follow this convention. Stable tasks with a Warp
implementation:

* `Isaac-Cartpole-Direct` — Cartpole balance
* `Isaac-Ant-Direct` — Ant locomotion
* `Isaac-Humanoid-Direct` — Humanoid locomotion
* `Isaac-Reorient-Cube-Allegro-Direct` — Allegro hand cube reorient

Manager-Based Warp Execution (`--frontend warp`)

### Manager-Based Warp Execution (`--frontend warp`)

Manager-based tasks do not need a parallel warp registration: the shared RL
CLI exposes `--frontend {torch,warp}`, and `--frontend warp` adapts the
*stable* task configuration onto the warp runtime at build time (swapping each
MDP term for its warp twin). Select the Newton solver explicitly with
`presets=newton_mjwarp`. Stable tasks with full twin coverage:

* `Isaac-Cartpole`
* `Isaac-Ant`
* `Isaac-Humanoid`
* `Isaac-Reach-Franka`
* `Isaac-Reach-UR10`
* `Isaac-Velocity-Flat-AnymalD`
* `Isaac-Velocity-Flat-Cassie`
* `Isaac-Velocity-Flat-G1`
* `Isaac-Velocity-Flat-H1`
* `Isaac-Velocity-Flat-UnitreeGo2`

The following contributed tasks also have full twin coverage:

* `IsaacContrib-Velocity-Flat-AnymalB`
* `IsaacContrib-Velocity-Flat-AnymalC`
* `IsaacContrib-Velocity-Flat-UnitreeA1`
* `IsaacContrib-Velocity-Flat-UnitreeGo1`

A missing twin is a hard error listing the affected terms, so a partially
covered task fails at build time rather than silently changing behavior.
Rough-terrain velocity configurations that use `height_scan` cannot currently be
adapted: that observation term has no Warp twin. Check the terms used by a task rather
than assuming that every terrain configuration has the same limitation.

## Performance Comparison

These historical measurements compare Torch manager-based and CUDA-graph-captured
Warp environments on Newton, over 300 iterations with 4096 environments. They do
not represent the latest revision or every supported task.

For a current comparison, use the same hardware and Isaac Lab, Newton, and Warp
revisions for both frontends. Shared-library improvements can benefit both.

> **Note**
>
> The warp migration is an ongoing effort. Several components (e.g. scene write, actuator models)
> have not yet been migrated to Warp kernels and still run through torch. Further performance
> improvements are expected as these components are migrated.

| Env                                | Type    | Stable Step (us) | Warp Step (us) | Change  |
| ---------------------------------- | ------- | ---------------- | -------------- | ------- |
| Isaac-Cartpole-Direct              | Direct  | 5,274            | 4,331          | -17.88% |
| Isaac-Ant-Direct                   | Direct  | 6,368            | 3,128          | -50.88% |
| Isaac-Humanoid-Direct              | Direct  | 13,937           | 10,783         | -22.63% |
| Isaac-Reorient-Cube-Allegro-Direct | Direct  | 82,950           | 74,570         | -10.10% |
| Isaac-Cartpole                     | Manager | 7,971            | 3,642          | -54.31% |
| Isaac-Ant                          | Manager | 9,781            | 4,672          | -52.23% |
| Isaac-Humanoid                     | Manager | 17,653           | 12,505         | -29.16% |
| Isaac-Reach-Franka                 | Manager | 11,458           | 7,813          | -31.83% |
| Isaac-Velocity-Flat-AnymalD        | Manager | 32,294           | 23,977         | -25.75% |
| Isaac-Velocity-Flat-Cassie         | Manager | 17,320           | 10,706         | -38.19% |
| Isaac-Velocity-Flat-G1             | Manager | 34,487           | 27,300         | -20.84% |
| Isaac-Velocity-Flat-H1             | Manager | 22,202           | 15,864         | -28.55% |
| Isaac-Velocity-Flat-UnitreeGo2     | Manager | 15,221           | 9,966          | -34.52% |

## Which Workflows Benefit Most

The savings come from eliminating Python / torch overhead in the env's step loop, so envs
gain in proportion to how much of their step time was previously dominated by per-kernel CPU
overhead. Reading the table above:

* **Manager-based classic RL** (Cartpole, Ant) — biggest gains (-52% to -54%). Many small
  reward / observation terms with low compute per term, so per-launch CPU overhead dominated
  the stable baseline.
* **Manager-based locomotion** (AnymalD, G1, H1, Cassie, UnitreeGo2) — consistent -21% to -38%
  range. The MDP has more terms but the underlying physics step is heavier, so the relative
  Python savings shrink.
* **Direct workflow** — gains scale with how much the env's step body was Python (Ant -51%,
  Cartpole -18%, Allegro hand -10%). Direct envs that already wrote most of their work as
  GPU kernels see modest gains; ones with substantial Python state machinery see large ones.
* **Compute-heavy / scene-write-heavy envs** (Allegro hand, large humanoids) — see smaller
  relative gains because the warp-side savings are amortised over a heavier step. Components
  that still go through torch (scene write, actuator models) currently bound the floor; this
  is expected to improve as remaining components migrate to warp.

If your env's step time is dominated by physics or scene I/O, expect modest gains. If it has
many small MDP terms or a lot of Python in the step loop, expect large ones. Use the
benchmarking workflow below to measure on your task before committing to a migration.

## Limitations

The warp env path is experimental and has the following known constraints. These are
specific to warp envs; for Newton physics features and related guides see
[Newton](/isaaclab/3.0/concepts/physics-backends#newton).

**Physics backend**

* **Newton only.** PhysX is not supported under the warp env path. Asset and sensor
  `class_type` fields resolve to `isaaclab_physx.*` classes that depend on
  `omni.physics.tensors` (a Kit module the warp runtime does not initialise), and several
  warp APIs (env-mask reset, CUDA graph capture) require the Newton articulation. Configure
  the cfg with a Newton physics block (or the typed selector `physics=newton_mjwarp`,
  which fails loudly if the task has no Newton physics preset).

**MDP coverage**

* Only the terms listed under [Available Warp MDP Terms](/isaaclab/3.0/experimental-features/warp-environments#available-warp-mdp-terms) are
  implemented. Stable envs that depend on un-migrated terms cannot be run on the warp path
  until those terms are ported.
* Some scene-side operations (asset write, actuator models, certain sensor types) still go
  through torch. They participate in the step but are not yet captured into the graph; they
  set the lower bound on observed step time.
* Sensors that depend on the Kit RTX renderer (camera-based observations) cannot be combined
  with the warp env path — they need Kit, which the warp runtime does not initialise.

**API differences vs stable**

* Reset events use a boolean `env_mask` (`wp.array(dtype=wp.bool)`) instead of an
  `env_ids` list. This is required for capture safety: variable-length indexing changes
  graph topology and breaks replay.
* All buffers must be pre-allocated in `__init__`. There is no dynamic allocation inside
  the captured step loop, so observation / reward / termination output dimensions must be
  known at env init.
* Term functions write into a pre-allocated `out` buffer rather than returning a tensor.
  See [Migrating Existing Environments](/isaaclab/3.0/experimental-features/warp-environments#migrating-existing-environments) for the kernel + launch pattern.
* Code inside the captured step loop must follow capture-safety rules (no
  `wp.to_torch`, no torch arithmetic, no lazy-evaluated properties, no Python branching
  on GPU data). See [Capture Safety](/isaaclab/3.0/experimental-features/warp-environments#capture-safety) for the
  full set of rules.

## Benchmarking Your Environment

The performance table above was produced with `isaaclab benchmark training`,
which runs a fixed iteration count and reports step-time statistics. Use the same command
to estimate the gain for your own task before committing to a migration.

**Single-task A/B**

Run the same stable task id twice, differing only in `--frontend`. Pass
`presets=newton_mjwarp` to *both* runs so the physics backend is identical and the
measured difference isolates the frontend (manager pipeline + CUDA graph capture):

```bash
# Torch frontend (stable managers)
uv run isaaclab benchmark training \
    --rl_library rsl_rl \
    --task <Task-Name> \
    --num_envs 4096 \
    --max_iterations 500 \
    --benchmark_formatter summary \
    --output_path benchmarks/torch \
    presets=newton_mjwarp

# Warp frontend (warp managers, CUDA-graph captured) — same task id
uv run isaaclab benchmark training \
    --rl_library rsl_rl \
    --task <Task-Name> \
    --frontend warp \
    --num_envs 4096 \
    --max_iterations 500 \
    --benchmark_formatter summary \
    --output_path benchmarks/warp \
    presets=newton_mjwarp
```

The `summary` formatter prints step time (min / mean / max) and total throughput. Compare
"step time" between the two runs to estimate the gain per env step.

**Sweep across all available tasks**

Run `isaaclab benchmark training` for each task in the stable set (cartpole, ant, humanoid,
locomotion, manipulation) and again with `--frontend warp`, then diff the two output
directories.

**What to look at in the output**

* *Step time (min / mean / max)*: the headline number — what each env step costs.
* *Iteration time*: includes policy update; useful for end-to-end training throughput.
* *Capture overhead*: for warp runs, the first few iterations include CUDA graph capture
  cost; exclude those when comparing steady-state numbers.

**Estimating before you migrate**

If you can't run the warp variant yet (e.g. the task isn't ported), measure the stable
step time and look at where it's spent:

* `num_envs * step_time` dominated by physics → expect modest warp gains.
* `step_time` dominated by `manager.compute_*` calls → expect large gains, since those
  are exactly what the warp managers replace with captured kernel launches.

Use `--num_steps` on `runtime.py` for a no-policy step-time microbenchmark
when you want to isolate env overhead from policy compute.

## Migrating Existing Environments

The following sections cover project layout, Warp kernels, CUDA graph capture,
and parity testing for new or migrated environments.

### Design Rationale

The warp environment path is built around [CUDA graph capture](https://docs.nvidia.com/cuda/cuda-programming-guide/04-special-topics/cuda-graphs.html).
A CUDA graph records a sequence of GPU operations (kernel launches, memory copies) during a
capture phase, then replays the entire sequence with a single launch. This eliminates per-kernel
CPU overhead — the parameter validation, kernel selection, and buffer setup that normally costs
20–200 μs per operation is performed once during graph instantiation and reused on every replay
(\~10 μs total). All CPU-side code (Python logic, torch dispatching) executed during capture is
completely bypassed during replay. See the [Warp concurrency documentation](https://nvidia.github.io/warp/stable/deep_dive/concurrency.html) for Warp's graph capture API
(`wp.ScopedCapture`).

All design decisions in the warp infrastructure follow from this constraint: every operation in the
step loop must be a GPU kernel launch with stable memory pointers so that the captured graph can
be replayed without modification.

Key consequences:

* All buffers are **pre-allocated** — no dynamic allocation inside the step loop
* Data flows through **persistent `wp.array` pointers** — never replaced, only overwritten
* MDP terms are **pure `@wp.kernel` functions** — no Python branching on GPU data
* Reset uses **boolean masks** (`env_mask`) instead of index lists (`env_ids`) to avoid
  variable-length indexing that changes graph topology

### Project Structure

Warp-specific implementations that diverge from the torch-based managers and env classes live in the `_experimental` packages:

* `isaaclab_experimental` — warp managers, base env classes, warp MDP terms
* `isaaclab_tasks_experimental` — warp task configs and task-specific MDP terms

Any new warp implementation that differs from the torch-based managers or env classes belongs in these packages.
Warp task configs reference Newton physics directly (no `PresetCfg`) since the warp path
is Newton-only.

### Writing Warp MDP Terms

#### Imports

Warp task configs import from the experimental packages:

```python
# Warp
from isaaclab_experimental.managers import ObservationTermCfg, RewardTermCfg, SceneEntityCfg
import isaaclab_experimental.envs.mdp as mdp
```

The term config classes have the same interface — only the import path changes.

#### Common Pattern

All warp MDP terms (observations, rewards, terminations, events, actions) follow the same
**kernel + launch** pattern. Torch terms use torch tensors and return results; warp terms
write into pre-allocated `wp.array` output buffers via `@wp.kernel` functions:

```python
# Torch — returns a tensor
def lin_vel_z_l2(env, asset_cfg) -> torch.Tensor:
    return torch.square(asset.data.root_lin_vel_b[:, 2])

# Warp — writes into pre-allocated output
@wp.kernel
def _lin_vel_z_l2_kernel(vel: wp.array(...), out: wp.array(dtype=wp.float32)):
    i = wp.tid()
    out[i] = vel[i][2] * vel[i][2]

def lin_vel_z_l2(env, out, asset_cfg) -> None:
    wp.launch(_lin_vel_z_l2_kernel, dim=env.num_envs, inputs=[..., out])
```

The output buffer shapes differ by term type:

* **Observations**: `(num_envs, D)` where D is the observation dimension
* **Rewards**: `(num_envs,)`
* **Terminations**: `(num_envs,)` with dtype `bool`
* **Events**: `(num_envs,)` mask — events don't produce output, they modify sim state

#### Observation Terms

Since warp terms write into pre-allocated buffers, the observation manager must know each
term's output dimension at initialization to allocate the correct `(num_envs, D)` output
array. This is resolved via a fallback chain (see
`ObservationManager._infer_term_dim_scalar` in
`isaaclab_experimental/managers/observation_manager.py`):

> **Warning**
>
> The IO descriptor decorators are deprecated in Isaac Lab 3.0 and will be
> removed in Isaac Lab 3.2. Their output-dimension metadata remains available
> temporarily for Warp-first environments while a replacement runtime
> configuration is developed.

1. **Explicit `out_dim` in decorator** (preferred):

   ```python
   @generic_io_descriptor_warp(out_dim=3, observation_type="RootState")
   def base_lin_vel(env, out, asset_cfg) -> None: ...
   ```

   `out_dim` can be an integer, or a string that resolves at initialization:

   * `"joint"` — number of selected joints from `asset_cfg`
   * `"body:N"` — N components per selected body from `asset_cfg`
   * `"command"` — dimension from command manager
   * `"action"` — dimension from action manager

2. **`axes` metadata**: Dimension equals the number of axes listed:

   ```python
   @generic_io_descriptor_warp(axes=["X", "Y", "Z"], observation_type="RootState")
   def projected_gravity(env, out, asset_cfg) -> None: ...
   # → dimension = 3
   ```

3. **Legacy params**: `term_dim`, `out_dim`, or `obs_dim` keys in `term_cfg.params`.

4. **Asset config fallback**: Count of `asset_cfg.joint_ids` (or `joint_ids_wp`) for
   joint-level terms.

#### Event Terms

Events use `env_mask` (boolean `wp.array`) instead of `env_ids`, and each kernel
checks the mask to skip non-selected environments:

```python
def reset_joints_by_offset(env, env_mask, ...):
    wp.launch(_kernel, dim=env.num_envs, inputs=[env_mask, ...])

@wp.kernel
def _kernel(env_mask: wp.array(dtype=wp.bool), ...):
    i = wp.tid()
    if not env_mask[i]:
        return
    # ... modify state for selected envs only
```

* RNG uses per-env `env.rng_state_wp` (`wp.uint32`) instead of `torch.rand`
* **Startup/prestartup** events use the torch convention `(env, env_ids, **params)`
* **Reset/interval** events use the warp convention `(env, env_mask, **params)`

#### Action Terms

Actions follow a **two-stage execution**: `process_actions` (called once per env step) scales
and clips raw actions, and `apply_actions` (called once per sim step) writes targets to the
asset. Both stages use warp kernels with pre-allocated `_raw_actions` and `_processed_actions`
buffers.

#### Capture Safety

When writing terms that run inside the captured step loop, keep in mind:

* **No `wp.to_torch`** or torch arithmetic — stay in warp throughout
* **No lazy-evaluated properties** — use sim-bound (Tier 1) data directly; if a derived
  quantity is needed, compute it inline in the kernel
* **No dynamic allocation** — all buffers must be pre-allocated in `__init__`

### Parity Testing

Two levels of parity testing are used to validate warp terms:

**1. Implementation parity (torch vs warp)** — verifies that the warp kernel produces the
same result as the torch implementation. This is optional for terms that have no torch
counterpart (e.g. new terms written directly in warp).

```python
import isaaclab.envs.mdp.observations as torch_obs
import isaaclab_experimental.envs.mdp.observations as warp_obs

# Torch baseline
expected = torch_obs.joint_pos(torch_env, asset_cfg=cfg)

# Warp (uncaptured)
out = wp.zeros((num_envs, num_joints), dtype=wp.float32, device=device)
warp_obs.joint_pos(warp_env, out, asset_cfg=cfg)
actual = wp.to_torch(out)

torch.testing.assert_close(actual, expected)
```

**2. Capture parity (warp vs warp-captured)** — verifies that the term produces identical
results when replayed from a CUDA graph vs launched directly. A mismatch here indicates capture-unsafe
code (e.g. stale pointers, dynamic allocation, or lazy property access that doesn't replay).
This test should always be run, even for terms without a torch counterpart.

```python
# Warp uncaptured
out_uncaptured = wp.zeros((num_envs, num_joints), dtype=wp.float32, device=device)
warp_obs.joint_pos(warp_env, out_uncaptured, asset_cfg=cfg)

# Warp captured (graph replay)
out_captured = wp.zeros((num_envs, num_joints), dtype=wp.float32, device=device)
with wp.ScopedCapture() as cap:
    warp_obs.joint_pos(warp_env, out_captured, asset_cfg=cfg)
wp.capture_launch(cap.graph)

torch.testing.assert_close(wp.to_torch(out_captured), wp.to_torch(out_uncaptured))
```

See `source/isaaclab_experimental/test/envs/mdp/` for complete parity test examples.

### Available Warp MDP Terms

| Category          | Available Terms                                                                                                                                                                                                                                                                                                                        |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Observations (11) | \| `base_pos_z` \| `base_lin_vel` \| `base_ang_vel` \| `projected_gravity` \| `joint_pos` \| `joint_pos_rel` \| `joint_pos_limit_normalized` \| `joint_vel` \| `joint_vel_rel` \| `last_action` \| `generated_commands`                                                                                                                |
| Rewards (16)      | \| `is_alive` \| `is_terminated` \| `lin_vel_z_l2` \| `ang_vel_xy_l2` \| `flat_orientation_l2` \| `joint_torques_l2` \| `joint_vel_l1` \| `joint_vel_l2` \| `joint_acc_l2` \| `joint_deviation_l1` \| `joint_pos_limits` \| `action_rate_l2` \| `action_l2` \| `undesired_contacts` \| `track_lin_vel_xy_exp` \| `track_ang_vel_z_exp` |
| Events (6)        | \| `reset_joints_by_offset` \| `reset_joints_by_scale` \| `reset_root_state_uniform` \| `push_by_setting_velocity` \| `apply_external_force_torque` \| `randomize_rigid_body_com`                                                                                                                                                      |
| Terminations (4)  | \| `time_out` \| `root_height_below_minimum` \| `joint_pos_out_of_manual_limit` \| `illegal_contact`                                                                                                                                                                                                                                   |
| Actions (2)       | \| `JointPositionAction` \| `JointEffortAction`                                                                                                                                                                                                                                                                                        |

Terms not listed here remain in torch only. When using an env that requires unlisted terms,
those terms must be implemented in warp first.