mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-26 19:26:16 +00:00
fix(vlabench): retry unstable reset, graceful PhysicsError on step
VLABench's LM4ManipDMEnv.reset() runs 20 warm-up `step()` calls while toggling gravity / fluid coefficients to settle the scene. For some random layout samples MuJoCo's integrator diverges and raises `mjWARN_BADQACC` -> `dm_control.rl.control.PhysicsError` before we ever get to return from reset. In a multi-task smoke eval that kills the whole run at the next task's env construction. - `_ensure_env`: catch PhysicsError and retry `load_env()` up to five times with a fresh random layout. Almost always converges; if it really can't, re-raise the last exception. - `step`: wrap `self._env.step(ctrl)` in a PhysicsError handler. On divergence, terminate the episode gracefully (is_success=False, terminated=True, info['physics_error']=True) and drop the stale inner env so the next reset rebuilds it. Keeps the rest of the multi-task eval moving instead of propagating a hard crash. Made-with: Cursor
This commit is contained in:
@@ -25,6 +25,7 @@ with long-horizon reasoning, built on MuJoCo/dm_control.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from collections.abc import Callable, Sequence
|
from collections.abc import Callable, Sequence
|
||||||
from functools import partial
|
from functools import partial
|
||||||
@@ -181,30 +182,54 @@ class VLABenchEnv(gym.Env):
|
|||||||
low=ACTION_LOW, high=ACTION_HIGH, shape=(ACTION_DIM,), dtype=np.float32
|
low=ACTION_LOW, high=ACTION_HIGH, shape=(ACTION_DIM,), dtype=np.float32
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Max attempts to rebuild the underlying env when MuJoCo throws
|
||||||
|
# `PhysicsError` (e.g. mjWARN_BADQACC) during VLABench's 20-step
|
||||||
|
# reset warm-up. Some random task/layout samples land in unstable
|
||||||
|
# initial configurations; re-sampling the layout almost always
|
||||||
|
# gives a stable one.
|
||||||
|
_ENSURE_ENV_MAX_ATTEMPTS = 5
|
||||||
|
|
||||||
def _ensure_env(self) -> None:
|
def _ensure_env(self) -> None:
|
||||||
"""Create the underlying VLABench env on first use.
|
"""Create the underlying VLABench env on first use.
|
||||||
|
|
||||||
Called inside the worker subprocess after fork(), so each worker gets
|
Called inside the worker subprocess after fork(), so each worker gets
|
||||||
its own clean rendering context rather than inheriting a stale one from
|
its own clean rendering context rather than inheriting a stale one from
|
||||||
the parent process (which causes crashes with AsyncVectorEnv).
|
the parent process (which causes crashes with AsyncVectorEnv).
|
||||||
|
|
||||||
|
Retries on `PhysicsError`: VLABench's `LM4ManipDMEnv.reset()` runs 20
|
||||||
|
warm-up `step()` calls while toggling gravity/fluids to let the scene
|
||||||
|
settle; for some random layouts MuJoCo's integrator diverges and
|
||||||
|
raises `mjWARN_BADQACC`. Re-sampling the layout almost always yields
|
||||||
|
a stable one, so we retry a few times before giving up.
|
||||||
"""
|
"""
|
||||||
if self._env is not None:
|
if self._env is not None:
|
||||||
return
|
return
|
||||||
|
|
||||||
import VLABench.robots # noqa: F401 # type: ignore[import-untyped]
|
import VLABench.robots # noqa: F401 # type: ignore[import-untyped]
|
||||||
import VLABench.tasks # noqa: F401 # type: ignore[import-untyped]
|
import VLABench.tasks # noqa: F401 # type: ignore[import-untyped]
|
||||||
|
from dm_control.rl.control import PhysicsError # type: ignore[import-untyped]
|
||||||
from VLABench.envs import load_env # type: ignore[import-untyped]
|
from VLABench.envs import load_env # type: ignore[import-untyped]
|
||||||
|
|
||||||
h, w = self.render_resolution
|
h, w = self.render_resolution
|
||||||
env = load_env(
|
last_exc: PhysicsError | None = None
|
||||||
task=self.task,
|
for attempt in range(1, self._ENSURE_ENV_MAX_ATTEMPTS + 1):
|
||||||
robot=self.robot,
|
try:
|
||||||
render_resolution=(h, w),
|
env = load_env(task=self.task, robot=self.robot, render_resolution=(h, w))
|
||||||
)
|
|
||||||
self._env = env
|
self._env = env
|
||||||
|
break
|
||||||
|
except PhysicsError as exc:
|
||||||
|
last_exc = exc
|
||||||
|
print(
|
||||||
|
f"[vlabench] PhysicsError on attempt {attempt}/"
|
||||||
|
f"{self._ENSURE_ENV_MAX_ATTEMPTS} while building task "
|
||||||
|
f"'{self.task}': {exc}. Retrying with fresh layout…"
|
||||||
|
)
|
||||||
|
if self._env is None:
|
||||||
|
assert last_exc is not None
|
||||||
|
raise last_exc
|
||||||
|
|
||||||
# Extract task description from the dm_control task
|
# Extract task description from the dm_control task
|
||||||
task_obj = env.task
|
task_obj = self._env.task
|
||||||
if hasattr(task_obj, "task_description"):
|
if hasattr(task_obj, "task_description"):
|
||||||
self.task_description = task_obj.task_description
|
self.task_description = task_obj.task_description
|
||||||
elif hasattr(task_obj, "language_instruction"):
|
elif hasattr(task_obj, "language_instruction"):
|
||||||
@@ -374,6 +399,8 @@ class VLABenchEnv(gym.Env):
|
|||||||
return observation, info
|
return observation, info
|
||||||
|
|
||||||
def step(self, action: np.ndarray) -> tuple[RobotObservation, float, bool, bool, dict[str, Any]]:
|
def step(self, action: np.ndarray) -> tuple[RobotObservation, float, bool, bool, dict[str, Any]]:
|
||||||
|
from dm_control.rl.control import PhysicsError # type: ignore[import-untyped]
|
||||||
|
|
||||||
self._ensure_env()
|
self._ensure_env()
|
||||||
assert self._env is not None
|
assert self._env is not None
|
||||||
|
|
||||||
@@ -391,7 +418,20 @@ class VLABenchEnv(gym.Env):
|
|||||||
physics = self._env.physics
|
physics = self._env.physics
|
||||||
ctrl_dim = int(physics.data.ctrl.shape[0])
|
ctrl_dim = int(physics.data.ctrl.shape[0])
|
||||||
ctrl = self._build_ctrl_from_action(action, ctrl_dim)
|
ctrl = self._build_ctrl_from_action(action, ctrl_dim)
|
||||||
|
try:
|
||||||
timestep = self._env.step(ctrl)
|
timestep = self._env.step(ctrl)
|
||||||
|
except PhysicsError as exc:
|
||||||
|
# Physics integrator diverged (e.g. mjWARN_BADQACC). Treat it as
|
||||||
|
# a graceful failed termination rather than a hard crash — the
|
||||||
|
# rest of the multi-task eval should still run.
|
||||||
|
print(f"[vlabench] PhysicsError during step on task '{self.task}': {exc}. Terminating episode.")
|
||||||
|
observation = self._get_obs()
|
||||||
|
info = {"task": self.task, "is_success": False, "physics_error": True}
|
||||||
|
# Drop the stale env so the next reset() rebuilds it cleanly.
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
self._env.close()
|
||||||
|
self._env = None
|
||||||
|
return observation, 0.0, True, False, info
|
||||||
|
|
||||||
# Extract reward from dm_control timestep
|
# Extract reward from dm_control timestep
|
||||||
reward = float(timestep.reward) if timestep.reward is not None else 0.0
|
reward = float(timestep.reward) if timestep.reward is not None else 0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user