From dd61339b3b2f7b4549ca99b1a8654bc6b43a9e7c Mon Sep 17 00:00:00 2001 From: TATP-233 Date: Wed, 21 Oct 2026 22:15:55 +0800 Subject: [PATCH 1/3] docs(base): describe Genesis spawn device binding as a required cold path The comment block called itself "not a compatibility shim" while still containing the shim keyword, which defeats keyword scans for residual shim surfaces. Reword it to state plainly that per-process device binding in spawn collectors is a required cold path. Refs #2474, #2492 --- src/unilab/base/env_factory.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/unilab/base/env_factory.py b/src/unilab/base/env_factory.py index 02856d2c2..0ba8ff6ab 100644 --- a/src/unilab/base/env_factory.py +++ b/src/unilab/base/env_factory.py @@ -46,11 +46,10 @@ def make_registry_env( # carry the explicit cold-path id in the opaque override and bind # immediately before registry construction. Binding a non-zero id pins # CUDA_VISIBLE_DEVICES for this process, so forward the post-pin - # in-process index downstream. This binding is required, not a - # compatibility shim: spawn collectors are fresh interpreters that cannot - # inherit the parent process's binding, and only UniLab's - # ``bind_genesis_process_device`` sets the pinned-namespace flag its - # resolution helpers consult. + # in-process index downstream. This binding is a required cold path of + # every spawn collector: fresh interpreters cannot inherit the parent + # process's binding, and only UniLab's ``bind_genesis_process_device`` + # sets the pinned-namespace flag its resolution helpers consult. if sim_backend == "genesis" and env_cfg_override is not None: genesis_device_id = env_cfg_override.get("genesis_device_id") if genesis_device_id is not None: From d951a733396e708fa008b849d04d62ee000164e4 Mon Sep 17 00:00:00 2001 From: TATP-233 Date: Wed, 21 Oct 2026 22:15:56 +0800 Subject: [PATCH 2/3] refactor(envs): delete the orphaned suppressed hot state refresh hook The _suppress_hot_state_refresh getattr hook and its _enter/_exit_suppressed_hot_state_refresh pair were introduced in fc45ec22 for _DeviceResidentColdContractProxy in the motion-tracking torch_flashsac_env; that sole consumer was removed in 736025b3, leaving a silent getattr(..., False) hook with no in-repo implementation. Delete it outright instead of promoting an unused extension point. Refs #2474, #2492 --- src/unilab/envs/manager_based_rl_env.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/unilab/envs/manager_based_rl_env.py b/src/unilab/envs/manager_based_rl_env.py index a517e6e05..e4a0ff419 100644 --- a/src/unilab/envs/manager_based_rl_env.py +++ b/src/unilab/envs/manager_based_rl_env.py @@ -1035,23 +1035,8 @@ def _get_overlay() -> list[list[DebugPrimitive]] | None: return _get_overlay - def _enter_suppressed_hot_state_refresh(self) -> None: - """Allow a cold proxy to temporarily replace NumPy body-state reads.""" - raise NotImplementedError( - f"{type(self).__name__} does not support suppressed hot state refresh" - ) - - def _exit_suppressed_hot_state_refresh(self) -> None: - """Undo :meth:`_enter_suppressed_hot_state_refresh`.""" - raise NotImplementedError( - f"{type(self).__name__} does not support suppressed hot state refresh" - ) - def _load_managers(self) -> None: """Construct managers in the pinned community dependency order.""" - suppress_hot_state_refresh = bool(getattr(self, "_suppress_hot_state_refresh", False)) - if suppress_hot_state_refresh: - self._enter_suppressed_hot_state_refresh() self.event_manager = EventManager(self._cfg.events, self) self.command_manager = ( CommandManager(self._cfg.commands, self) if self._cfg.commands else NullCommandManager() @@ -1064,8 +1049,6 @@ def _load_managers(self) -> None: self, scale_by_dt=self._cfg.scale_rewards_by_dt, ) - if suppress_hot_state_refresh: - self._exit_suppressed_hot_state_refresh() self.curriculum_manager = ( CurriculumManager(self._cfg.curriculum, self) if self._cfg.curriculum From 52c6c6108d50f708ede1af31a112329775778699 Mon Sep 17 00:00:00 2001 From: TATP-233 Date: Wed, 21 Oct 2026 22:15:56 +0800 Subject: [PATCH 3/3] fix(envs): fail velocity command metrics before read-plan compilation UniformVelocityCommand._metric_velocities silently returned zero tensors when the scene's packed tensor read plan was not compiled yet, which would mask an ordering bug since metrics are only consumed by per-step command compute after plan compilation. Raise an explicit RuntimeError naming the required lifecycle order, and pin it with a test. Refs #2474, #2492 --- src/unilab/envs/mdp/commands/velocity_command.py | 13 ++++++++----- tests/envs/mdp/test_uniform_velocity_command.py | 9 +++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/unilab/envs/mdp/commands/velocity_command.py b/src/unilab/envs/mdp/commands/velocity_command.py index 50219654b..5a7c9dfc4 100644 --- a/src/unilab/envs/mdp/commands/velocity_command.py +++ b/src/unilab/envs/mdp/commands/velocity_command.py @@ -132,14 +132,17 @@ def tensor_sensor_names(self) -> tuple[str, ...]: def _metric_velocities(self) -> tuple[torch.Tensor, torch.Tensor]: read_plan = getattr(self._env.scene, "read_plan", None) + if read_plan is None: + raise RuntimeError( + "UniformVelocityCommand metrics require the scene's compiled packed " + "tensor read plan. The lifecycle order is manager construction, then " + "ManagerBasedRlEnv read-plan compilation, then per-step command " + "compute; consuming metrics before the plan exists is an ordering bug" + ) names = self.tensor_sensor_names - packed = read_plan.sensor_names.get(self.cfg.entity_name, ()) if read_plan else () + packed = read_plan.sensor_names.get(self.cfg.entity_name, ()) # The scene pack negotiates backend-local aliases (Go2 ``local_linvel``/ # ``gyro``) but the semantic metric remains the canonical IMU contract. - if read_plan is None: - # Manager construction and command reset before plan compilation do - # not consume metrics; defer to the compiled packed phase. - return torch.zeros((self.num_envs, 3)), torch.zeros((self.num_envs, 3)) aliases = self.packed_sensor_aliases resolved_names = tuple( next((local for local in aliases.get(name, (name,)) if local in packed), name) diff --git a/tests/envs/mdp/test_uniform_velocity_command.py b/tests/envs/mdp/test_uniform_velocity_command.py index 2f3dc9060..452c7fa81 100644 --- a/tests/envs/mdp/test_uniform_velocity_command.py +++ b/tests/envs/mdp/test_uniform_velocity_command.py @@ -232,6 +232,15 @@ def test_metrics_and_fixed_interval_resampling_follow_manager_schedule() -> None np.testing.assert_allclose(term.time_left, 0.2) +def test_metrics_fail_closed_before_read_plan_compilation() -> None: + env, _ = _env() + manager = _manager(env) + env.scene.read_plan = None + + with pytest.raises(RuntimeError, match="compiled packed tensor read plan"): + manager.compute(0.1) + + def test_heading_world_forward_and_standing_modes() -> None: all_mask = torch.ones(4, dtype=torch.bool)