Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2300758
Skip wrench composer resets while inactive
StafaH Sep 28, 2026
f0fb3fb
Remove redundant solver reset from Newton physics step
StafaH Sep 28, 2026
bc225bf
Use elapsed time for Newton body COM acceleration
StafaH Sep 28, 2026
5bd37c1
Read back clamped joint defaults only when logged
StafaH Sep 28, 2026
e890cdf
Remove host syncs and redundant kernels from manager hot paths
StafaH Sep 28, 2026
92175d9
Trim sensor, marker, and renderer hot-path overhead
StafaH Sep 28, 2026
f6b9771
Merge branch 'perf/hot-path-7b-newton' into perf/camera-perf-07b-hot-…
StafaH Sep 28, 2026
5ccec99
Merge branch 'perf/hot-path-7b-managers' into perf/camera-perf-07b-ho…
StafaH Sep 28, 2026
c833086
Remove host syncs from task hot paths and RL wrappers
StafaH Sep 28, 2026
2792d5f
Merge branch 'perf/hot-path-7b-sensors' into perf/camera-perf-07b-hot…
StafaH Sep 28, 2026
0c5612a
Use elapsed time for OVPhysX rigid-object COM acceleration
StafaH Sep 28, 2026
8eccb93
Merge branch 'perf/hot-path-7b-tasks' into perf/camera-perf-07b-hot-path
StafaH Sep 28, 2026
7b5b894
Merge branch 'develop' of https://github.com/isaac-sim/IsaacLab into …
StafaH Sep 28, 2026
282a75b
Trim redundant hot-path regression tests
StafaH Sep 28, 2026
ec65628
Address hot-path review feedback and consolidate tests
StafaH Sep 29, 2026
a71c9af
Centralize sampling from named event ranges
StafaH Sep 29, 2026
e8a0e12
Require noise callbacks to preserve their inputs
StafaH Sep 30, 2026
cd13a32
Keep noise configuration docs concise
StafaH Sep 30, 2026
e81da34
Trim redundant host-sync comments
StafaH Sep 30, 2026
e300905
Merge branch 'develop' into perf/camera-perf-07b-hot-path
StafaH Sep 30, 2026
ea03b65
Remove sync-focused curriculum and velocity command tests
StafaH Sep 30, 2026
dc82bb9
Trim sync, RNG, and private-state test assertions
StafaH Sep 30, 2026
1ca362a
Restore contact force magnitude helper name
StafaH Sep 30, 2026
e9d4425
Fix hot-path PR test failures and benchmark cache permissions
StafaH Sep 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,8 @@ jobs:
run: |
set -uo pipefail
install -d -m 0777 "${GITHUB_WORKSPACE}/jit-cache/warp" "${GITHUB_WORKSPACE}/jit-cache/nv"
# Restored version directories and kernels must also be writable by the container user.
chmod -R a+rwX "${GITHUB_WORKSPACE}/jit-cache" || exit 1

container=""
cleanup_container() {
Expand Down
Empty file.
39 changes: 39 additions & 0 deletions source/isaaclab/changelog.d/perf-hot-path-7b-managers.major.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
Added
^^^^^

* Added :func:`~isaaclab.utils.math.sample_uniform_from_ranges` to sample named components with
shared, bounded caching of device bounds. Core and Lift event terms now use this sampler instead
of maintaining separate tensor-cache helpers.

Changed
^^^^^^^

* **Breaking:** Required noise functions and models configured through
:class:`~isaaclab.utils.noise.NoiseCfg` and :class:`~isaaclab.utils.noise.NoiseModelCfg`
to leave their input unchanged. :class:`~isaaclab.managers.ObservationManager` now calls
them without a defensive input copy. Custom callbacks using in-place operations must
clone their input first (for example, ``data.clone().add_(bias)``) or use out-of-place
operations (``data + bias``). Returning the unchanged input or a view remains supported;
the manager still protects borrowed outputs during subsequent processing.

* Changed episode logging in :class:`~isaaclab.managers.CommandTerm`,
:class:`~isaaclab.managers.TerminationManager`, :class:`~isaaclab.managers.CurriculumManager`,
:class:`~isaaclab.envs.mdp.UniformPoseCommand`, :class:`~isaaclab.envs.mdp.UniformPose2dCommand` and
:class:`~isaaclab.envs.mdp.survival_success_rate` to report 0-d device tensors instead of Python floats,
matching :class:`~isaaclab.managers.RewardManager`, so resets no longer synchronize the stream. Code that
reads these ``extras["log"]`` entries as floats should call ``float()`` or ``.item()`` on them.
* Removed per-step host synchronizations from the heading control of
:class:`~isaaclab.envs.mdp.UniformVelocityCommand` and from
:class:`~isaaclab.envs.mdp.actions.task_space_actions.DifferentialInverseKinematicsAction`.
* Cached the device range tensors of the root-state, nodal-state, push and center-of-mass event terms by
value instead of uploading them on every call.
* Batched the per-term updates of :class:`~isaaclab.managers.RewardManager`, and skipped the defensive copy
before noise callbacks and after delay buffers in :class:`~isaaclab.managers.ObservationManager`.
* Shifted :class:`~isaaclab.utils.buffers.CircularBuffer` histories of small frames in two kernels
regardless of the history length.

Fixed
^^^^^

* Fixed :class:`~isaaclab.envs.mdp.reset_root_state_uniform` ignoring the ``pose_range`` and ``velocity_range``
passed at call time, including curriculum updates, in favor of the ranges present at construction.
6 changes: 6 additions & 0 deletions source/isaaclab/changelog.d/perf-hot-path-7b-newton.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Changed
^^^^^^^

* Changed :meth:`~isaaclab.utils.wrench_composer.WrenchComposer.reset` to return without launching work
when the composer is inactive, removing the per-step buffer clears for rigid objects and collections
that apply no external wrench.
16 changes: 16 additions & 0 deletions source/isaaclab/changelog.d/perf-hot-path-7b-sensors.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Changed
^^^^^^^

* Changed sensors with ``debug_vis=True`` to refresh their buffers lazily from the debug
visualization callback instead of on every :meth:`~isaaclab.sensors.SensorBase.update`.
Custom sensors must refresh outdated buffers (for example through ``data``) in their
``_debug_vis_callback`` before reading internal data.
* Skipped ray-caster drift sampling on reset when ``drift_range`` and ``ray_cast_drift_range``
are zero, and uploaded the ray-cast drift ranges to the device only when they change.
* Changed :meth:`~isaaclab.markers.VisualizationMarkers.visualize` to return without validating
or processing inputs when no visualizer backend is active, such as in headless runs.

Fixed
^^^^^

* Cleared previously sampled ray-caster drift on reset when its configured range was changed to zero.
6 changes: 3 additions & 3 deletions source/isaaclab/isaaclab/envs/manager_based_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ def step(self, action: torch.Tensor) -> tuple[VecEnvObs, dict]:
self.recorder_manager.record_pre_step()

# check if we need to do rendering within the physics loop
# note: uses cached property to avoid settings lookup every step
# note: evaluated once per step rather than once per physics substep
is_rendering = self.sim.is_rendering

# physics-owned decimation covers all substeps in one call
Expand All @@ -562,7 +562,7 @@ def step(self, action: torch.Tensor) -> tuple[VecEnvObs, dict]:
self.scene.update(dt=self.physics_dt * steps_per_call)

# post-step: step interval event
if "interval" in self.event_manager.available_modes:
if "interval" in self.event_manager.active_terms:
self.event_manager.apply(mode="interval", dt=self.step_dt)

# advance video recorders (after render, before obs)
Expand Down Expand Up @@ -631,7 +631,7 @@ def _reset_idx(self, env_ids: torch.Tensor | slice):
self.scene.reset(env_ids)

# apply events such as randomization for environments that need a reset
if "reset" in self.event_manager.available_modes:
if "reset" in self.event_manager.active_terms:
env_step_count = self._sim_step_counter // self.cfg.decimation
self.event_manager.apply(mode="reset", env_ids=env_ids, global_env_step_count=env_step_count)

Expand Down
6 changes: 3 additions & 3 deletions source/isaaclab/isaaclab/envs/manager_based_rl_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn:
self.recorder_manager.record_pre_step()

# check if we need to do rendering within the physics loop
# note: uses cached property to avoid settings lookup every step
# note: evaluated once per step rather than once per physics substep
is_rendering = self.sim.is_rendering

# physics-owned decimation covers all substeps in one call
Expand Down Expand Up @@ -269,7 +269,7 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn:
self.recorder_manager.record_post_reset(manual_reset_ids)

self.command_manager.compute(dt=self.step_dt)
if "interval" in self.event_manager.available_modes:
if "interval" in self.event_manager.active_terms:
self.event_manager.apply(mode="interval", dt=self.step_dt)
# -- advance video recorders (after render and resets, before final obs)
for recorder in self.video_recorders:
Expand Down Expand Up @@ -387,7 +387,7 @@ def _reset_idx(self, env_ids: torch.Tensor | slice):
# reset the internal buffers of the scene elements
self.scene.reset(env_ids)
# apply events such as randomizations for environments that need a reset
if "reset" in self.event_manager.available_modes:
if "reset" in self.event_manager.active_terms:
env_step_count = self._sim_step_counter // self.cfg.decimation
self.event_manager.apply(mode="reset", env_ids=env_ids, global_env_step_count=env_step_count)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,11 +210,9 @@ def apply_actions(self):
self._ik_controller.set_joint_pos_limits(limits[:, 0].clone(), limits[:, 1].clone())
self._limits_injected = True
# compute the delta in joint-space
if ee_quat_curr.norm() != 0:
jacobian = self._compute_frame_jacobian()
joint_pos_des = self._ik_controller.compute(ee_pos_curr, ee_quat_curr, jacobian, joint_pos)
else:
joint_pos_des = joint_pos.clone()
jacobian = self._compute_frame_jacobian()
joint_pos_des = self._ik_controller.compute(ee_pos_curr, ee_quat_curr, jacobian, joint_pos)
joint_pos_des = torch.where(ee_quat_curr.norm() != 0, joint_pos_des, joint_pos)
# set the joint position command
self._asset.set_joint_position_target_index(target=joint_pos_des, joint_ids=self._joint_ids)

Expand Down
6 changes: 2 additions & 4 deletions source/isaaclab/isaaclab/envs/mdp/commands/pose_2d_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,16 +98,14 @@ def _update_metrics(self):
if self._track_success:
self._succeeded |= self.metrics["error_pos"] < self.cfg.position_success_threshold

def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, float]:
def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, torch.Tensor]:
extras = super().reset(env_ids)
if self._track_success:
if env_ids is None:
env_ids = slice(None)
# Write the unified ``Metrics/success_rate`` directly to env extras so it shares
# a TensorBoard card with the same metric from other tasks.
self._env.extras.setdefault("log", {})["Metrics/success_rate"] = (
self._succeeded[env_ids].float().mean().item()
)
self._env.extras.setdefault("log", {})["Metrics/success_rate"] = self._succeeded[env_ids].float().mean()
index_fill_(self._succeeded, env_ids, False)
return extras

Expand Down
6 changes: 2 additions & 4 deletions source/isaaclab/isaaclab/envs/mdp/commands/pose_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,16 +151,14 @@ def _compute_success(self, position_error: torch.Tensor, orientation_error: torc
success[:] = False
return success

def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, float]:
def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, torch.Tensor]:
extras = super().reset(env_ids)
if self._track_success:
if env_ids is None:
env_ids = slice(None)
# Write the unified ``Metrics/success_rate`` directly to env extras so it shares
# a TensorBoard card with the same metric from other tasks.
self._env.extras.setdefault("log", {})["Metrics/success_rate"] = (
self._succeeded[env_ids].float().mean().item()
)
self._env.extras.setdefault("log", {})["Metrics/success_rate"] = self._succeeded[env_ids].float().mean()
index_fill_(self._succeeded, env_ids, False)
return extras

Expand Down
12 changes: 4 additions & 8 deletions source/isaaclab/isaaclab/envs/mdp/commands/velocity_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ def _update_metrics(self):
self._error_yaw_sum += error_yaw
self._step_count += 1.0

def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, float]:
def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, torch.Tensor]:
# Finalize the just-ended episode's metrics into ``self.metrics`` BEFORE the base
# class reads them. ``success_rate`` is per-env binary: the *episode-mean* error
# is below both thresholds. Then super().reset() logs and zeros ``self.metrics``;
Expand Down Expand Up @@ -181,17 +181,13 @@ def _update_command(self):
"""
# Compute angular velocity from heading direction
if self.cfg.heading_command:
# resolve indices of heading envs
env_ids = self.is_heading_env.nonzero(as_tuple=False).flatten()
Comment thread
StafaH marked this conversation as resolved.
# compute angular velocity
heading_error = math_utils.wrap_to_pi(
self.heading_target[env_ids] - self.robot.data.heading_w.torch[env_ids]
)
self.vel_command_b[env_ids, 2] = torch.clip(
heading_error = math_utils.wrap_to_pi(self.heading_target - self.robot.data.heading_w.torch)
heading_ang_vel = torch.clip(
self.cfg.heading_control_stiffness * heading_error,
min=self.cfg.ranges.ang_vel_z[0],
max=self.cfg.ranges.ang_vel_z[1],
)
self.vel_command_b[:, 2] = torch.where(self.is_heading_env, heading_ang_vel, self.vel_command_b[:, 2])
# Enforce standing (i.e., zero velocity command) for standing envs
index_fill_(self.vel_command_b, self.is_standing_env, 0.0)

Expand Down
70 changes: 23 additions & 47 deletions source/isaaclab/isaaclab/envs/mdp/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,11 +522,9 @@ def __call__(
env_rows = env_ids[:, None] if not isinstance(env_ids, slice) and not isinstance(body_ids, slice) else env_ids

# sample random CoM values
range_list = [com_range.get(key, (0.0, 0.0)) for key in ["x", "y", "z"]]
ranges = torch.tensor(range_list, device=self.asset.device)
num_envs = len(range(env.num_envs)[env_ids]) if isinstance(env_ids, slice) else len(env_ids)
rand_samples = math_utils.sample_uniform(
ranges[:, 0], ranges[:, 1], (num_envs, 3), device=self.asset.device
rand_samples = math_utils.sample_uniform_from_ranges(
com_range, ("x", "y", "z"), num_envs, device=self.asset.device
).unsqueeze(1)

# start from defaults and add random offsets
Expand Down Expand Up @@ -1257,9 +1255,9 @@ def push_by_setting_velocity(
# velocities
vel_w = asset.data.root_vel_w.torch[env_ids]
# sample random velocities
range_list = [velocity_range.get(key, (0.0, 0.0)) for key in ["x", "y", "z", "roll", "pitch", "yaw"]]
ranges = torch.tensor(range_list, device=asset.device)
vel_w += math_utils.sample_uniform(ranges[:, 0], ranges[:, 1], vel_w.shape, device=asset.device)
vel_w += math_utils.sample_uniform_from_ranges(
velocity_range, ("x", "y", "z", "roll", "pitch", "yaw"), vel_w.shape[:-1], device=asset.device
)
# set the velocities into the physics simulation
asset.write_root_velocity_to_sim_index(root_velocity=vel_w, env_ids=env_ids)

Expand All @@ -1277,20 +1275,8 @@ class reset_root_state_uniform(ManagerTermBase):
The term takes a dictionary of pose and velocity ranges for each axis and rotation. The keys of the
dictionary are ``x``, ``y``, ``z``, ``roll``, ``pitch``, and ``yaw``. The values are tuples of the form
``(min, max)``. If the dictionary does not contain a key, the position or velocity is set to zero for that axis.

The range dictionaries are materialized as device tensors once at construction.
"""

def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv):
super().__init__(cfg, env)
keys = ("x", "y", "z", "roll", "pitch", "yaw")
pose_range = cfg.params.get("pose_range", {})
velocity_range = cfg.params.get("velocity_range", {})
self._pose_ranges = torch.tensor([tuple(pose_range.get(key, (0.0, 0.0))) for key in keys], device=env.device)
self._velocity_ranges = torch.tensor(
[tuple(velocity_range.get(key, (0.0, 0.0))) for key in keys], device=env.device
)

def __call__(
self,
env: ManagerBasedEnv,
Expand All @@ -1304,18 +1290,16 @@ def __call__(
default_root_pose = asset.data.default_root_pose.torch[env_ids]
default_root_vel = asset.data.default_root_vel.torch[env_ids]

ranges = self._pose_ranges
rand_samples = math_utils.sample_uniform(
ranges[:, 0], ranges[:, 1], (default_root_pose.shape[0], 6), device=asset.device
rand_samples = math_utils.sample_uniform_from_ranges(
pose_range, ("x", "y", "z", "roll", "pitch", "yaw"), default_root_pose.shape[0], device=asset.device
)

positions = default_root_pose[:, 0:3] + env.scene.env_origins[env_ids] + rand_samples[:, 0:3]
orientations_delta = math_utils.quat_from_euler_xyz(rand_samples[:, 3], rand_samples[:, 4], rand_samples[:, 5])
orientations = math_utils.quat_mul(default_root_pose[:, 3:7], orientations_delta)
# velocities
ranges = self._velocity_ranges
rand_samples = math_utils.sample_uniform(
ranges[:, 0], ranges[:, 1], (default_root_pose.shape[0], 6), device=asset.device
rand_samples = math_utils.sample_uniform_from_ranges(
velocity_range, ("x", "y", "z", "roll", "pitch", "yaw"), default_root_pose.shape[0], device=asset.device
)

velocities = default_root_vel + rand_samples
Expand Down Expand Up @@ -1356,20 +1340,16 @@ def reset_root_state_with_random_orientation(
default_root_vel = asset.data.default_root_vel.torch[env_ids].clone()

# poses
range_list = [pose_range.get(key, (0.0, 0.0)) for key in ["x", "y", "z"]]
ranges = torch.tensor(range_list, device=asset.device)
rand_samples = math_utils.sample_uniform(
ranges[:, 0], ranges[:, 1], (default_root_pose.shape[0], 3), device=asset.device
rand_samples = math_utils.sample_uniform_from_ranges(
pose_range, ("x", "y", "z"), default_root_pose.shape[0], device=asset.device
)

positions = default_root_pose[:, 0:3] + env.scene.env_origins[env_ids] + rand_samples
orientations = math_utils.random_orientation(default_root_pose.shape[0], device=asset.device)

# velocities
range_list = [velocity_range.get(key, (0.0, 0.0)) for key in ["x", "y", "z", "roll", "pitch", "yaw"]]
ranges = torch.tensor(range_list, device=asset.device)
rand_samples = math_utils.sample_uniform(
ranges[:, 0], ranges[:, 1], (default_root_pose.shape[0], 6), device=asset.device
rand_samples = math_utils.sample_uniform_from_ranges(
velocity_range, ("x", "y", "z", "roll", "pitch", "yaw"), default_root_pose.shape[0], device=asset.device
)

velocities = default_root_vel + rand_samples
Expand Down Expand Up @@ -1426,17 +1406,17 @@ def reset_root_state_from_terrain(
positions = valid_positions[terrain.terrain_levels[env_ids], terrain.terrain_types[env_ids], ids]
positions += asset.data.default_root_pose.torch[env_ids, :3]
# sample random orientations
range_list = [pose_range.get(key, (0.0, 0.0)) for key in ["roll", "pitch", "yaw"]]
ranges = torch.tensor(range_list, device=asset.device)
rand_samples = math_utils.sample_uniform(ranges[:, 0], ranges[:, 1], (num_envs, 3), device=asset.device)
rand_samples = math_utils.sample_uniform_from_ranges(
pose_range, ("roll", "pitch", "yaw"), num_envs, device=asset.device
)

# convert to quaternions
orientations = math_utils.quat_from_euler_xyz(rand_samples[:, 0], rand_samples[:, 1], rand_samples[:, 2])

# sample random velocities
range_list = [velocity_range.get(key, (0.0, 0.0)) for key in ["x", "y", "z", "roll", "pitch", "yaw"]]
ranges = torch.tensor(range_list, device=asset.device)
rand_samples = math_utils.sample_uniform(ranges[:, 0], ranges[:, 1], (num_envs, 6), device=asset.device)
rand_samples = math_utils.sample_uniform_from_ranges(
velocity_range, ("x", "y", "z", "roll", "pitch", "yaw"), num_envs, device=asset.device
)

velocities = asset.data.default_root_vel.torch[env_ids] + rand_samples

Expand Down Expand Up @@ -1699,19 +1679,15 @@ def reset_nodal_state_uniform(
nodal_state = asset.data.default_nodal_state_w.torch[env_ids].clone()

# position
range_list = [position_range.get(key, (0.0, 0.0)) for key in ["x", "y", "z"]]
ranges = torch.tensor(range_list, device=asset.device)
rand_samples = math_utils.sample_uniform(
ranges[:, 0], ranges[:, 1], (nodal_state.shape[0], 1, 3), device=asset.device
rand_samples = math_utils.sample_uniform_from_ranges(
position_range, ("x", "y", "z"), (nodal_state.shape[0], 1), device=asset.device
)

nodal_state[..., :3] += rand_samples

# velocities
range_list = [velocity_range.get(key, (0.0, 0.0)) for key in ["x", "y", "z"]]
ranges = torch.tensor(range_list, device=asset.device)
rand_samples = math_utils.sample_uniform(
ranges[:, 0], ranges[:, 1], (nodal_state.shape[0], 1, 3), device=asset.device
rand_samples = math_utils.sample_uniform_from_ranges(
velocity_range, ("x", "y", "z"), (nodal_state.shape[0], 1), device=asset.device
)

nodal_state[..., 3:] += rand_samples
Expand Down
2 changes: 1 addition & 1 deletion source/isaaclab/isaaclab/envs/mdp/rewards.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ class survival_success_rate(ManagerTermBase):

def reset(self, env_ids: torch.Tensor) -> None:
survived = self._env.termination_manager.time_outs[env_ids]
self._env.extras.setdefault("log", {})["Metrics/success_rate"] = survived.float().mean().item()
self._env.extras.setdefault("log", {})["Metrics/success_rate"] = survived.float().mean()

def __call__(self, env: ManagerBasedRLEnv) -> torch.Tensor:
return torch.zeros(env.num_envs, device=env.device)
Expand Down
Loading
Loading