Skip to content

Commit 6594704

Browse files
Fix benchmark timing validation
1 parent 979a9e0 commit 6594704

11 files changed

Lines changed: 153 additions & 9 deletions

File tree

scripts/benchmarks/rl_games/benchmark_rl_games_play.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ def _parse_args(argv: list[str]):
8686
parser.error("--num_frames must be greater than zero")
8787
if args_cli.warmup_steps < 0:
8888
parser.error("--warmup_steps must be non-negative")
89+
if args_cli.warmup_steps >= args_cli.num_frames:
90+
parser.error("--warmup_steps must be less than --num_frames")
8991
sys.argv = [sys.argv[0]] + remaining_args
9092

9193
return args_cli, remaining_args

scripts/benchmarks/rsl_rl/benchmark_rsl_rl_play.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ def _parse_args(argv: list[str]) -> tuple[argparse.Namespace, list[str]]:
8888
parser.error("--num_frames must be greater than zero")
8989
if args.warmup_steps < 0:
9090
parser.error("--warmup_steps must be non-negative")
91+
if args.warmup_steps >= args.num_frames:
92+
parser.error("--warmup_steps must be less than --num_frames")
9193
sys.argv = [sys.argv[0]] + remaining
9294
return args, remaining
9395

scripts/benchmarks/sb3/benchmark_sb3_play.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ def _parse_args(argv: list[str]):
9494
parser.error("--num_frames must be greater than zero")
9595
if args_cli.warmup_steps < 0:
9696
parser.error("--warmup_steps must be non-negative")
97+
if args_cli.warmup_steps >= args_cli.num_frames:
98+
parser.error("--warmup_steps must be less than --num_frames")
9799
sys.argv = [sys.argv[0]] + remaining_args
98100

99101
return args_cli, remaining_args

scripts/benchmarks/skrl/benchmark_skrl_play.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ def _parse_args(argv: list[str]):
107107
parser.error("--num_frames must be greater than zero")
108108
if args_cli.warmup_steps < 0:
109109
parser.error("--warmup_steps must be non-negative")
110+
if args_cli.warmup_steps >= args_cli.num_frames:
111+
parser.error("--warmup_steps must be less than --num_frames")
110112
sys.argv = [sys.argv[0]] + remaining_args
111113

112114
return args_cli, remaining_args

scripts/benchmarks/test/test_benchmark_smoke.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import importlib.util
99
import json
1010
import subprocess
11+
import sys
1112
from pathlib import Path
1213

1314
import pytest
@@ -48,26 +49,46 @@ def _load_adapter(library: str, workflow: str):
4849

4950
@pytest.mark.parametrize("library", ["rsl_rl", "rl_games", "skrl", "sb3"])
5051
@pytest.mark.parametrize(("workflow", "argument"), [("play", "--num_frames"), ("train", "--max_iterations")])
51-
def test_adapters_reject_non_positive_workloads(library: str, workflow: str, argument: str):
52+
def test_adapters_reject_non_positive_workloads(library: str, workflow: str, argument: str, monkeypatch, capsys):
5253
"""Benchmark adapters reject workloads that cannot produce timing samples."""
5354
module = _load_adapter(library, workflow)
55+
argv = ["--task", _TASK, argument, "0", "--headless"]
56+
monkeypatch.setattr(sys, "argv", ["benchmark", *argv])
5457

5558
with pytest.raises(SystemExit) as exc_info:
56-
module._parse_args(["--task", _TASK, argument, "0", "--headless"])
59+
module._parse_args(argv)
5760

5861
assert exc_info.value.code == 2
62+
assert f"{argument} must be greater than zero" in capsys.readouterr().err
5963

6064

6165
@pytest.mark.parametrize("library", ["rsl_rl", "rl_games", "skrl", "sb3"])
6266
@pytest.mark.parametrize("workflow", ["play", "train"])
63-
def test_adapters_reject_negative_warmup_steps(library: str, workflow: str):
67+
def test_adapters_reject_negative_warmup_steps(library: str, workflow: str, monkeypatch, capsys):
6468
"""Benchmark adapters reject a negative --warmup_steps (the cold-start exclusion is opt-in, N >= 0)."""
6569
module = _load_adapter(library, workflow)
70+
argv = ["--task", _TASK, "--warmup_steps", "-1", "--headless"]
71+
monkeypatch.setattr(sys, "argv", ["benchmark", *argv])
6672

6773
with pytest.raises(SystemExit) as exc_info:
68-
module._parse_args(["--task", _TASK, "--warmup_steps", "-1", "--headless"])
74+
module._parse_args(argv)
6975

7076
assert exc_info.value.code == 2
77+
assert "--warmup_steps must be non-negative" in capsys.readouterr().err
78+
79+
80+
@pytest.mark.parametrize("library", ["rsl_rl", "rl_games", "skrl", "sb3"])
81+
def test_play_adapters_reject_warmup_that_exhausts_workload(library: str, monkeypatch, capsys):
82+
"""Play requires at least one measured environment step after warm-up."""
83+
module = _load_adapter(library, "play")
84+
argv = ["--task", _TASK, "--num_frames", "2", "--warmup_steps", "2", "--headless"]
85+
monkeypatch.setattr(sys, "argv", ["benchmark", *argv])
86+
87+
with pytest.raises(SystemExit) as exc_info:
88+
module._parse_args(argv)
89+
90+
assert exc_info.value.code == 2
91+
assert "--warmup_steps must be less than --num_frames" in capsys.readouterr().err
7192

7293

7394
@pytest.mark.parametrize(

source/isaaclab/isaaclab/test/benchmark/builders.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,12 +178,19 @@ def build_runtime(
178178
collection_fps_agg = _with_effective_mean(collection_fps_agg, collection_fps, effective_fps)
179179
total_fps_agg = _with_effective_mean(total_fps_agg, total_fps, effective_fps)
180180
iterations_per_s_agg = _with_effective_mean(iterations_per_s_agg, iter_per_s, effective_iterations_per_s)
181+
if environment_step_times_s is None and (simulation_step_times_s is not None or simulation_step_calls is not None):
182+
raise ValueError("environment_step_times_s is required with simulation timing")
181183
environment_step_timing = None
182184
if environment_step_times_s is not None:
183185
if frames_per_environment_step is None or frames_per_environment_step <= 0:
184186
raise ValueError("frames_per_environment_step must be greater than zero")
185187
environment_samples = list(environment_step_times_s)
186-
if not environment_samples or any(value <= 0 for value in environment_samples):
188+
if not environment_samples:
189+
raise ValueError(
190+
"environment_step_times_s must contain only positive samples; no samples remained after warm-up, "
191+
"so reduce warmup_steps or increase the workload"
192+
)
193+
if any(value <= 0 for value in environment_samples):
187194
raise ValueError("environment_step_times_s must contain only positive samples")
188195
total_environment_time_s = sum(environment_samples)
189196
environment_fps_samples = [frames_per_environment_step / value for value in environment_samples]

source/isaaclab/isaaclab/test/benchmark/schema.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,13 @@ def __post_init__(self) -> None:
246246
"""Validate that the timing fields form one consistent measurement mode."""
247247
if self.environment_step_calls <= 0:
248248
raise ValueError("environment_step_calls must be greater than zero")
249+
if not (
250+
math.isfinite(self.environment_step_time_s.mean)
251+
and self.environment_step_time_s.mean > 0.0
252+
and math.isfinite(self.environment_step_fps.mean)
253+
and self.environment_step_fps.mean > 0.0
254+
):
255+
raise ValueError("environment step time and FPS must be greater than zero")
249256

250257
breakdown = (
251258
self.simulation_step_time_s,

source/isaaclab/isaaclab/test/benchmark/stepping.py

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,35 @@ def sample_random_actions(env) -> torch.Tensor | dict[str, torch.Tensor]:
6060
return 2.0 * torch.rand(u.num_envs, u.single_action_space.shape[0], device=u.device) - 1.0
6161

6262

63+
def _find_cuda_devices(value) -> set[str]:
64+
"""Collect CUDA device names from nested tensor-like values."""
65+
import torch # noqa: PLC0415
66+
67+
devices: set[str] = set()
68+
69+
def collect(item) -> None:
70+
if isinstance(item, dict):
71+
for nested in item.values():
72+
collect(nested)
73+
return
74+
if isinstance(item, (list, tuple)):
75+
for nested in item:
76+
collect(nested)
77+
return
78+
device = item if isinstance(item, (str, torch.device)) else getattr(item, "device", None)
79+
if device is None:
80+
return
81+
try:
82+
device = torch.device(device)
83+
except (RuntimeError, TypeError):
84+
return
85+
if device.type == "cuda":
86+
devices.add(str(device))
87+
88+
collect(value)
89+
return devices
90+
91+
6392
class EnvironmentStepTimingRecorder(AbstractContextManager):
6493
"""Record host-return step time or an optional synchronized step breakdown.
6594
@@ -119,6 +148,7 @@ def __enter__(self) -> EnvironmentStepTimingRecorder:
119148
self._original_env_step = self._env.step
120149

121150
if self._measure_synchronized_step_breakdown:
151+
import torch # noqa: PLC0415
122152
import warp as wp # noqa: PLC0415
123153

124154
from isaaclab.utils.timer import Timer # noqa: PLC0415
@@ -130,34 +160,50 @@ def __enter__(self) -> EnvironmentStepTimingRecorder:
130160
self._simulation_step_calls = 0
131161
assert self._simulation_context is not None
132162
self._original_sim_step = self._simulation_context.step
163+
environment_cuda_devices = _find_cuda_devices(getattr(self._env.unwrapped, "device", None))
164+
active_cuda_devices = environment_cuda_devices
165+
166+
def synchronize_torch(devices: set[str]) -> None:
167+
for device in sorted(devices):
168+
torch.cuda.synchronize(device)
133169

134170
def timed_simulation_step(*args, **kwargs):
135171
if not self._inside_environment_step:
136172
return self._original_sim_step(*args, **kwargs)
173+
synchronize_torch(active_cuda_devices)
137174
wp.synchronize()
138175
timer = Timer()
176+
timer.start()
139177
try:
140-
with timer:
141-
return self._original_sim_step(*args, **kwargs)
178+
return self._original_sim_step(*args, **kwargs)
142179
finally:
180+
synchronize_torch(active_cuda_devices)
181+
timer.stop()
143182
self._simulation_total_time_s += timer.total_run_time
144183
self._simulation_step_calls += 1
145184

146185
self._simulation_context.step = timed_simulation_step
147186

148187
def timed_environment_step(*args, **kwargs):
188+
nonlocal active_cuda_devices
149189
recording = self._environment_step_index >= self._warmup_steps
150190
self._environment_step_index += 1
151191
simulation_start_time_s = self._simulation_total_time_s
152192
simulation_start_calls = self._simulation_step_calls
193+
previous_cuda_devices = active_cuda_devices
194+
active_cuda_devices = environment_cuda_devices | _find_cuda_devices(args) | _find_cuda_devices(kwargs)
195+
synchronize_torch(active_cuda_devices)
153196
wp.synchronize()
154197
timer = Timer()
198+
timer.start()
155199
self._inside_environment_step = True
156200
try:
157-
with timer:
158-
return self._original_env_step(*args, **kwargs)
201+
return self._original_env_step(*args, **kwargs)
159202
finally:
160203
self._inside_environment_step = False
204+
synchronize_torch(active_cuda_devices)
205+
timer.stop()
206+
active_cuda_devices = previous_cuda_devices
161207
if recording:
162208
self.step_times_s.append(timer.total_run_time)
163209
self.simulation_step_times_s.append(self._simulation_total_time_s - simulation_start_time_s)

source/isaaclab/test/benchmark/test_builders.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,25 @@ def test_build_runtime_rejects_non_positive_environment_step_times(environment_s
221221
)
222222

223223

224+
@pytest.mark.parametrize(
225+
("simulation_step_times_s", "simulation_step_calls"),
226+
[([0.5], None), (None, 1), ([0.5], 1)],
227+
)
228+
def test_build_runtime_rejects_simulation_timing_without_environment_timing(
229+
simulation_step_times_s, simulation_step_calls
230+
):
231+
with pytest.raises(ValueError, match="environment_step_times_s is required with simulation timing"):
232+
builders.build_runtime(
233+
startup_time_s=StartupTime(0.1, 0.2, 0.3),
234+
iteration_times_s=[1.0],
235+
collection_fps=[8.0],
236+
total_fps=[8.0],
237+
steps_per_iteration=8,
238+
simulation_step_times_s=simulation_step_times_s,
239+
simulation_step_calls=simulation_step_calls,
240+
)
241+
242+
224243
def test_build_training_bundle_round_trips(tmp_path):
225244
run = builders.build_run_identity(
226245
run_id="x",

source/isaaclab/test/benchmark/test_schema.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,23 @@ def test_environment_step_timing_rejects_incomplete_measurement_modes():
172172
dataclasses.replace(timing, simulation_step_time_s=None)
173173

174174

175+
@pytest.mark.parametrize("field", ["environment_step_time_s", "environment_step_fps"])
176+
def test_environment_step_timing_rejects_non_positive_environment_metrics(field):
177+
timing = EnvironmentStepTiming(
178+
environment_step_time_s=MeanStd(mean=0.1, std=0.01),
179+
environment_step_fps=MeanStd(mean=100.0, std=1.0),
180+
simulation_step_time_s=None,
181+
outside_simulation_step_time_s=None,
182+
outside_simulation_step_fraction=None,
183+
environment_step_calls=1,
184+
simulation_step_calls=None,
185+
measurement_mode="host_return",
186+
)
187+
188+
with pytest.raises(ValueError, match="environment step time and FPS must be greater than zero"):
189+
dataclasses.replace(timing, **{field: MeanStd(mean=-1.0, std=0.0)})
190+
191+
175192
def test_environment_step_timing_rejects_inconsistent_partition():
176193
timing = _runtime().environment_step_timing
177194
assert timing is not None

0 commit comments

Comments
 (0)