Repository navigation
Partial reset advances actuator delay buffers in environments that were not reset #1216
Description
Calling env.reset(env_ids=[0]) advances the actuator delay buffer of environment 1, although environment 1's simulation time does not advance. With a nonconstant command history, its delayed control output changes immediately as well.
Environment
- mjlab 1.6.0
- MuJoCo 3.11.0 and MuJoCo-Warp 3.11.0
- PyTorch 2.14.0+cu132
- NVIDIA RTX 4090, Linux
Standalone reproducer
The script below uses an inline MJCF slider and the standard XmlActuatorCfg; it needs no external robot assets or application code. Run it with a GPU-enabled mjlab installation. The final assertion is expected to fail on the affected implementation.
"""Standalone upstream reproducer: no manipulation imports or robot assets.
Run with a GPU-enabled mjlab installation. The final assertion is expected
to fail on the affected implementation, after printing the observed changes.
"""
from importlib.metadata import version
import json
import mujoco
import torch
from mjlab.actuator import XmlActuatorCfg
from mjlab.entity import EntityArticulationInfoCfg, EntityCfg
from mjlab.envs import ManagerBasedRlEnv, ManagerBasedRlEnvCfg
from mjlab.scene import SceneCfg
def spec():
return mujoco.MjSpec.from_string('''
<mujoco>
<worldbody>
<body name="base">
<body name="slider" pos="0 0 1">
<joint name="slide" type="slide" axis="1 0 0"/>
<geom type="sphere" size="0.05" mass="1"/>
</body>
</body>
</worldbody>
<actuator><position name="servo" joint="slide" kp="10"/></actuator>
</mujoco>
''')
cfg = ManagerBasedRlEnvCfg(
scene=SceneCfg(num_envs=2, entities={
'robot': EntityCfg(
spec_fn=spec,
articulation=EntityArticulationInfoCfg(actuators=(
XmlActuatorCfg(target_names_expr=('slide',), delay_min_lag=3, delay_max_lag=3),
)),
),
}),
decimation=1,
auto_reset=False,
episode_length_s=10.,
)
cfg.sim.mujoco.timestep = .001
env = ManagerBasedRlEnv(cfg, device='cuda:0')
try:
env.reset()
robot = env.scene['robot']
# Populate a nonconstant history so the effect is visible in ctrl too.
for i in range(10):
robot.set_joint_position_target(torch.full((2, 1), i / 100., device=env.device))
env.step(torch.zeros((2, 0), device=env.device))
delay = robot.actuators[0]._delay_buffer
before_count = delay._step_count.clone()
before_time = env.sim.data.time.clone()
before_ctrl = env.sim.data.ctrl.clone()
before_history = delay._buffer.buffer.clone()
env.reset(env_ids=torch.tensor([0], device=env.device))
result = {
'versions': {name: version(name) for name in ['mjlab', 'mujoco', 'mujoco-warp', 'torch']},
'unreset_env': 1,
'count_before': int(before_count[1]),
'count_after': int(delay._step_count[1]),
'time_before': float(before_time[1]),
'time_after': float(env.sim.data.time[1]),
'ctrl_before': before_ctrl[1].tolist(),
'ctrl_after': env.sim.data.ctrl[1].tolist(),
'history_unchanged': torch.equal(before_history[1], delay._buffer.buffer[1]),
}
print('RESULT ' + json.dumps(result, indent=2), flush=True)
assert int(delay._step_count[1]) == int(before_count[1]), 'Partial reset advanced an untouched actuator delay buffer'
finally:
env.close()Actual behavior
For the untouched environment 1, the script reports:
delay step count: 11 -> 12
simulation time: 0.010 -> 0.010 seconds
actuator ctrl: 0.060 -> 0.070
history unchanged: False
Only environment 0 was reset. Environment 1's delay history and control output changed without a corresponding physics step.
Expected behavior
A partial reset should initialize the selected environments without advancing the delay history, lag-update schedule, or control output of untouched environments. Normal physics steps should continue to advance each actuator's delay buffer exactly once.
Likely cause
ManagerBasedRlEnv.reset() calls scene.write_data_to_sim() without restricting the environment IDs. For XML actuators, this reaches Entity._apply_actuator_controls() -> Actuator.apply_delay() -> delay_command(), which appends to the buffer and advances the lag-update schedule for the entire batch.
Consequently, one environment's effective actuator delay depends on resets elsewhere in the batch. In our application, each extra append represents one 1 ms physics sample, not an entire control step.
The automatic-reset branch in step() also calls scene.write_data_to_sim() after _reset_idx(reset_env_ids) and should be checked. The runtime reproducer above specifically tests explicit partial reset with auto_reset=False.
Source: ManagerBasedRlEnv reset and step.
Suggested regression coverage
- Resetting one environment preserves another environment's logical delay history, counters, sampled lag, and control output.
- An untouched environment follows the same trajectory with and without unrelated resets elsewhere in the batch.
- Cover explicit partial reset, automatic partial reset, and full reset initialization.
Related: #364 discusses observation-delay buffers; this report concerns actuator command-delay buffers.
Activity
lukehugh commentedon Oct 8, 2026
I quantified the downstream impact using mjlab's bundled cartpole balance task. Over 10 s, only environment 0 was reset; the other 24 environments received identical initial states and scripted actions. No external assets or trained policy were used.
On #1218 (
0bf9a0a), I compared no unrelated resets, scoped reset writes, and the affected whole-batch write reproduced by discarding resetenv_idsatScene.write_data_to_sim. Initialization was identical.The 80 ms cases use timestep=10 ms, decimation=5; the 125 ms cases use 1 ms, decimation=50. Automatic resets use environment 0's timeout counter. Observation corruption is disabled. These are open-loop differences, not PPO performance losses: unstable cartpole dynamics amplify small errors. Reward changes have both signs; termination flags were unchanged.
With #1218, all eight scenarios matched baseline state, observations, rewards, and physics-substep controls exactly. No-delay and 30 ms held-action cases were negative controls. In affected 80/125 ms cases, all 24 untouched environments had changed returns, even though immediate reset snapshots and end-of-control-step
ctrlmatched. The error occurs inside the decimation loop.This is important for our RM75_6F training with a 125 ms command delay and 1 ms physics timestep. Pending commands are part of the robot's dynamics. Unrelated resets make those dynamics depend on other environments in the batch, introducing a mismatch with single-robot deployment and affecting safety evaluation of delayed responses.
The reproducer below runs on #1218 and saves trajectories and JSON metrics. Validated on RTX 4090, mjlab 1.6.0, MuJoCo 3.11.0, PyTorch 2.14.0+cu132.
To run just the 80 ms explicit-reset case:
Reproducer: cartpole_benchmark.py (run on PR #1218)