feat(env): config to skip the discarded scene rebuild on reset Libero (#4272)

* perf(libero): skip the discarded scene rebuild on reset

LIBERO's OffScreenRenderEnv defaults to hard_reset=True, so every reset() frees
the MjSim, re-serialises the scene with model.get_xml(), recompiles it with
MjSim.from_xml_string(), constructs a fresh offscreen GL context and re-wires
every observable.

When init states are in use, LiberoEnv.reset() immediately calls
set_init_state(), which overwrites the whole sim state -- so all of that work is
discarded. This passes hard_reset=not init_states instead. Without init states
the randomisation reset() performs is the only thing placing the objects, so the
hard reset is kept.

Measured on an RTX 3060 Ti (EGL, robosuite 1.4.0, mujoco 3.2.7, 256x256 x2
cameras), through LiberoEnv.reset(), fresh env per arm:

  suite            hard      soft     saved
  libero_spatial   1697 ms   233 ms   1464 ms
  libero_object    1394 ms   172 ms   1222 ms
  libero_goal      1177 ms   164 ms   1013 ms
  libero_10        1528 ms   207 ms   1321 ms

Equivalence
-----------
Immediately after set_init_state, qpos, qvel, ctrl and act are bit-identical
between the two paths on every suite tested.

After the 10 settle steps that reset() runs, 9 of 41 qpos entries differ:
robot0_joint1..7 (<= 2.4e-5 rad) and gripper0_finger_joint1/2 (<= 2.1e-4 rad).
No object joint differs on any suite. The drift is driven by the gripper
component of the settle action ([0,0,0,0,0,0,-1]); replacing it with zeros keeps
the two paths bit-identical for 12 further steps, and with num_steps_wait=0 there
is no divergence at all.

Wrist-camera pixels can differ by up to ~87/255, because a sub-millimetre finger
displacement crosses rasterisation boundaries at 256x256. The pixel metric badly
overstates the physical difference here; 2.1e-4 rad is 0.012 degrees.

So this is not bit-identical end to end, and reviewers should decide whether
0.012 degrees of gripper drift is acceptable for the benchmark. It does not
change object placement, which is what the fixed init states exist to control.


* refactor(env): config param libero + docs

---------

Co-authored-by: Dimitar Dimitrov <dvdimitrov13@gmail.com>
This commit is contained in:
Steven Palma
2026-07-31 16:28:12 +02:00
committed by GitHub
parent 732a12108e
commit 2d8f5f314e
4 changed files with 39 additions and 0 deletions
+14
View File
@@ -92,6 +92,20 @@ LIBERO supports two control modes — `relative` (default) and `absolute`. Diffe
--env.control_mode=relative # or "absolute" --env.control_mode=relative # or "absolute"
``` ```
### Reset performance
By default, LeRobot preserves LIBERO's hard-reset behavior. With fixed initial
states enabled, you can opt into soft resets to skip rebuilding the simulator
model and renderer on every episode:
```bash
--env.init_states=true --env.hard_reset=false
```
Soft resets are faster but are not bit-identical to hard resets after the
environment's settling steps, so camera observations and policy results may
differ slightly. Use hard resets when reproducing benchmark results.
### Policy inputs and outputs ### Policy inputs and outputs
**Observations:** **Observations:**
+14
View File
@@ -134,6 +134,20 @@ LIBERO-plus supports two control modes — `relative` (default) and `absolute`.
--env.control_mode=relative # or "absolute" --env.control_mode=relative # or "absolute"
``` ```
### Reset performance
By default, LeRobot preserves LIBERO's hard-reset behavior. With fixed initial
states enabled, you can opt into soft resets to skip rebuilding the simulator
model and renderer on every episode:
```bash
--env.init_states=true --env.hard_reset=false
```
Soft resets are faster but are not bit-identical to hard resets after the
environment's settling steps, so camera observations and policy results may
differ slightly. Use hard resets when reproducing benchmark results.
### Policy inputs and outputs ### Policy inputs and outputs
**Observations:** **Observations:**
+4
View File
@@ -328,6 +328,7 @@ class LiberoEnv(EnvConfig):
render_mode: str = "rgb_array" render_mode: str = "rgb_array"
camera_name: str = "agentview_image,robot0_eye_in_hand_image" camera_name: str = "agentview_image,robot0_eye_in_hand_image"
init_states: bool = True init_states: bool = True
hard_reset: bool = True
camera_name_mapping: dict[str, str] | None = None camera_name_mapping: dict[str, str] | None = None
observation_height: int = 360 observation_height: int = 360
observation_width: int = 360 observation_width: int = 360
@@ -356,6 +357,8 @@ class LiberoEnv(EnvConfig):
def __post_init__(self): def __post_init__(self):
if self.fps <= 0: if self.fps <= 0:
raise ValueError(f"fps must be positive, got {self.fps}") raise ValueError(f"fps must be positive, got {self.fps}")
if not self.hard_reset and not self.init_states:
raise ValueError("hard_reset=False requires init_states=True")
if self.obs_type == "pixels": if self.obs_type == "pixels":
self.features[LIBERO_KEY_PIXELS_AGENTVIEW] = PolicyFeature( self.features[LIBERO_KEY_PIXELS_AGENTVIEW] = PolicyFeature(
@@ -416,6 +419,7 @@ class LiberoEnv(EnvConfig):
"observation_height": self.observation_height, "observation_height": self.observation_height,
"observation_width": self.observation_width, "observation_width": self.observation_width,
"control_freq": self.fps, "control_freq": self.fps,
"hard_reset": self.hard_reset,
} }
if self.task_ids is not None: if self.task_ids is not None:
kwargs["task_ids"] = self.task_ids kwargs["task_ids"] = self.task_ids
+7
View File
@@ -128,10 +128,13 @@ class LiberoEnv(gym.Env):
control_freq: int = 20, control_freq: int = 20,
control_mode: str = "relative", control_mode: str = "relative",
is_libero_plus: bool = False, is_libero_plus: bool = False,
hard_reset: bool = True,
): ):
super().__init__() super().__init__()
if control_freq <= 0: if control_freq <= 0:
raise ValueError(f"control_freq must be positive, got {control_freq}") raise ValueError(f"control_freq must be positive, got {control_freq}")
if not hard_reset and not init_states:
raise ValueError("hard_reset=False requires init_states=True")
self.task_id = task_id self.task_id = task_id
self.is_libero_plus = is_libero_plus self.is_libero_plus = is_libero_plus
self.obs_type = obs_type self.obs_type = obs_type
@@ -158,6 +161,7 @@ class LiberoEnv(gym.Env):
self.camera_name_mapping = camera_name_mapping self.camera_name_mapping = camera_name_mapping
self.num_steps_wait = num_steps_wait self.num_steps_wait = num_steps_wait
self.control_freq = control_freq self.control_freq = control_freq
self.hard_reset = hard_reset
self.episode_index = episode_index self.episode_index = episode_index
self.episode_length = episode_length self.episode_length = episode_length
# Load once and keep # Load once and keep
@@ -265,6 +269,9 @@ class LiberoEnv(gym.Env):
camera_heights=self.observation_height, camera_heights=self.observation_height,
camera_widths=self.observation_width, camera_widths=self.observation_width,
control_freq=self.control_freq, control_freq=self.control_freq,
# Soft resets skip LIBERO's model and renderer rebuild. They are opt-in
# because settle steps can make their observations differ from hard resets.
hard_reset=self.hard_reset,
) )
env.reset() env.reset()
self._env = env self._env = env