Skip to content

Commit 9fe2ba7

Browse files
Pins what the failure bound guarantees across parallel environments
Review on #7433 noted that with several environments the recorded failure count can pass max_num_failures. Measured on the shipped loop: the overshoot is exactly the number of attempts that end on the step which crosses the bound, minus one, so it is zero for a single environment and at most num_envs - 1. It cannot be driven to zero -- those attempts have already completed when the bound is read, and stopping a generator instead leaves env_loop waiting forever for an action from it, since the loop blocks until all num_envs actions are queued. So the field's docstring now states the guarantee for parallel runs, and two tests pin both sides of it: exact when attempts end on separate steps, bounded by num_envs - 1 when they end together. The test harness gains num_envs and attempts_per_step, defaulting to what it did before. No behaviour change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019SsU8ziGULaEBbtmvJSQZW
1 parent 3bcd74c commit 9fe2ba7

2 files changed

Lines changed: 42 additions & 10 deletions

File tree

source/isaaclab/isaaclab/envs/mimic_env_cfg.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ class DataGenConfig:
4848
4949
With the guarantee disabled, generation already stops after :attr:`generation_num_trials`
5050
attempts and this field is ignored, so setting it cannot cut a fixed-attempt run short.
51+
52+
The bound is read once per simulation step. Attempts that end on the step that crosses it are
53+
already complete, so a run over ``num_envs`` parallel environments can record up to
54+
``num_envs - 1`` failures beyond the bound; it is exact whenever attempts end on separate steps.
5155
"""
5256

5357
seed: int = 1

source/isaaclab_mimic/test/test_generation_failure_cap.py

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,20 @@ class _Fuse(Exception):
3131
"""Raised by the fake environment once the loop has run for longer than any test expects."""
3232

3333

34-
def _run(outcomes, *, max_num_failures, generation_num_trials=3, generation_guarantee=True):
34+
def _run(
35+
outcomes,
36+
*,
37+
max_num_failures,
38+
generation_num_trials=3,
39+
generation_guarantee=True,
40+
num_envs=1,
41+
attempts_per_step=1,
42+
):
3543
"""Run ``env_loop`` over scripted attempt outcomes; return how it ended and the final counters.
3644
45+
``attempts_per_step`` is how many of the ``num_envs`` generators finish an attempt on the same
46+
step, which is what decides whether the bound is read before or after the extra attempts land.
47+
3748
Returns:
3849
A tuple ``(how, num_success, num_failures, num_attempts)`` where ``how`` is ``"exited"`` if
3950
the loop returned on its own and ``"fuse"`` if it was still running after ``FUSE_STEPS``.
@@ -42,25 +53,28 @@ def _run(outcomes, *, max_num_failures, generation_num_trials=3, generation_guar
4253
loop = asyncio.get_event_loop()
4354
action_queue: asyncio.Queue = asyncio.Queue()
4455
reset_queue: asyncio.Queue = asyncio.Queue()
45-
action_queue.put_nowait((0, torch.zeros(7)))
56+
for env_id in range(num_envs):
57+
action_queue.put_nowait((env_id, torch.zeros(7)))
4658
scripted = iter(outcomes)
4759
steps = {"n": 0}
4860

4961
def step(actions):
5062
steps["n"] += 1
5163
if steps["n"] > FUSE_STEPS:
5264
raise _Fuse(f"env_loop still running after {FUSE_STEPS} steps")
53-
if next(scripted):
54-
generation.num_success += 1
55-
else:
56-
generation.num_failures += 1
57-
generation.num_attempts += 1
58-
action_queue.put_nowait((0, torch.zeros(7)))
65+
for _ in range(attempts_per_step):
66+
if next(scripted):
67+
generation.num_success += 1
68+
else:
69+
generation.num_failures += 1
70+
generation.num_attempts += 1
71+
for env_id in range(num_envs):
72+
action_queue.put_nowait((env_id, torch.zeros(7)))
5973

6074
env = SimpleNamespace(
61-
num_envs=1,
75+
num_envs=num_envs,
6276
device="cpu",
63-
action_space=SimpleNamespace(shape=(1, 7)),
77+
action_space=SimpleNamespace(shape=(num_envs, 7)),
6478
step=step,
6579
reset=lambda env_ids=None: None,
6680
close=lambda: None,
@@ -120,3 +134,17 @@ def test_attempt_based_termination_is_unchanged(max_num_failures):
120134
)
121135
assert how == "exited"
122136
assert attempts == 10
137+
138+
139+
def test_bound_is_exact_when_attempts_end_on_separate_steps():
140+
"""Four environments, one attempt landing per step: the bound is read between every attempt."""
141+
how, _, fail, _ = _run([False] * 100, max_num_failures=5, num_envs=4, attempts_per_step=1)
142+
assert how == "exited"
143+
assert fail == 5
144+
145+
146+
def test_attempts_ending_together_overshoot_the_bound_by_at_most_num_envs_minus_one():
147+
"""Attempts that end on the step that crosses the bound are already complete and still count."""
148+
how, _, fail, _ = _run([False] * 100, max_num_failures=5, num_envs=4, attempts_per_step=4)
149+
assert how == "exited"
150+
assert 5 < fail <= 5 + 4 - 1

0 commit comments

Comments
 (0)