Skip to content

Commit 2d3e381

Browse files
Restore RL entrypoint backend state
Scope Torch and SKRL global backend settings to reusable in-process training and playback calls. Preserve attributes that were initially absent and add failure-path regressions for RSL-RL and SKRL.
1 parent 79accca commit 2d3e381

6 files changed

Lines changed: 175 additions & 14 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fixed
2+
^^^^^
3+
4+
* Fixed reusable reinforcement learning entrypoints to restore caller Torch and
5+
SKRL backend settings after in-process training and playback.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
2+
# All rights reserved.
3+
#
4+
# SPDX-License-Identifier: BSD-3-Clause
5+
6+
"""Scoped restoration for backend-global reinforcement learning settings."""
7+
8+
from __future__ import annotations
9+
10+
from collections.abc import Iterator
11+
from contextlib import ExitStack, contextmanager
12+
from types import ModuleType
13+
14+
_MISSING = object()
15+
16+
17+
@contextmanager
18+
def preserve_attribute(target: object, name: str) -> Iterator[None]:
19+
"""Restore an attribute after a scoped operation, including an initially missing attribute."""
20+
previous = getattr(target, name, _MISSING)
21+
try:
22+
yield
23+
finally:
24+
if previous is _MISSING:
25+
if hasattr(target, name):
26+
delattr(target, name)
27+
else:
28+
setattr(target, name, previous)
29+
30+
31+
@contextmanager
32+
def scoped_torch_backend_flags(torch_module: ModuleType) -> Iterator[None]:
33+
"""Temporarily configure the Torch backend flags used by RSL-RL training."""
34+
cuda_matmul = torch_module.backends.cuda.matmul
35+
cudnn = torch_module.backends.cudnn
36+
settings = (
37+
(cuda_matmul, "allow_tf32", True),
38+
(cudnn, "allow_tf32", True),
39+
(cudnn, "deterministic", False),
40+
(cudnn, "benchmark", False),
41+
)
42+
with ExitStack() as cleanup:
43+
for target, name, value in settings:
44+
cleanup.enter_context(preserve_attribute(target, name))
45+
setattr(target, name, value)
46+
yield

source/isaaclab_rl/isaaclab_rl/entrypoints/backends/play_skrl.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from isaaclab.utils.dict import print_dict
2828
from isaaclab.utils.seed import configure_seed
2929

30+
from isaaclab_rl.entrypoints._state import preserve_attribute
3031
from isaaclab_rl.entrypoints.common import CHECKPOINT_SELECTORS, resolve_checkpoint_selector
3132
from isaaclab_rl.utils.pretrained_checkpoint import get_published_pretrained_checkpoint
3233

@@ -108,7 +109,13 @@
108109

109110

110111
def main():
111-
"""Play with skrl agent."""
112+
"""Play with SKRL while restoring the caller's global settings."""
113+
with preserve_attribute(skrl.config.jax, "backend"):
114+
_main()
115+
116+
117+
def _main():
118+
"""Execute SKRL playback."""
112119
env_cfg, experiment_cfg = resolve_task_config(args_cli.task, agent_cfg_entry_point)
113120
with launch_simulation(env_cfg, args_cli):
114121
if args_cli.ml_framework.startswith("torch"):

source/isaaclab_rl/isaaclab_rl/entrypoints/backends/train_rsl_rl.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
from isaaclab.app import add_launcher_args
2222

23+
from isaaclab_rl.entrypoints._state import scoped_torch_backend_flags
2324
from isaaclab_rl.entrypoints.backends import cli_args_rsl_rl as cli_args
2425
from isaaclab_rl.entrypoints.common import (
2526
CHECKPOINT_SELECTORS,
@@ -96,8 +97,16 @@ def _parse_args(argv: list[str]) -> argparse.Namespace:
9697

9798

9899
def run(argv: list[str]) -> None:
99-
"""Train an RSL-RL agent."""
100+
"""Train an RSL-RL agent while restoring the caller's Torch backend settings."""
100101
import torch
102+
103+
args_cli = _parse_args(argv)
104+
with scoped_torch_backend_flags(torch):
105+
_run(args_cli)
106+
107+
108+
def _run(args_cli: argparse.Namespace) -> None:
109+
"""Execute RSL-RL training with parsed arguments."""
101110
from rsl_rl.runners import DistillationRunner, OnPolicyRunner
102111

103112
from isaaclab.app import launch_simulation
@@ -108,12 +117,6 @@ def run(argv: list[str]) -> None:
108117

109118
from isaaclab_tasks.utils import get_checkpoint_path, resolve_task_config
110119

111-
torch.backends.cuda.matmul.allow_tf32 = True
112-
torch.backends.cudnn.allow_tf32 = True
113-
torch.backends.cudnn.deterministic = False
114-
torch.backends.cudnn.benchmark = False
115-
116-
args_cli = _parse_args(argv)
117120
installed_version = _check_rsl_rl_version()
118121
env_cfg, agent_cfg = resolve_task_config(args_cli.task, args_cli.agent)
119122

source/isaaclab_rl/isaaclab_rl/entrypoints/backends/train_skrl.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
from isaaclab.app import add_launcher_args
2121

22+
from isaaclab_rl.entrypoints._state import preserve_attribute
2223
from isaaclab_rl.entrypoints.common import (
2324
CHECKPOINT_SELECTORS,
2425
add_common_train_args,
@@ -99,7 +100,19 @@ def _get_distributed_rank(args_cli: argparse.Namespace) -> int:
99100

100101

101102
def run(argv: list[str]) -> None:
102-
"""Train a skrl agent."""
103+
"""Train a skrl agent while restoring the caller's global SKRL settings."""
104+
import skrl
105+
106+
args_cli = _parse_args(argv)
107+
with contextlib.ExitStack() as cleanup:
108+
if args_cli.ml_framework.startswith("jax"):
109+
cleanup.enter_context(preserve_attribute(skrl.config.jax, "backend"))
110+
skrl.config.jax.backend = "jax" if args_cli.ml_framework == "jax" else "numpy"
111+
_run(args_cli)
112+
113+
114+
def _run(args_cli: argparse.Namespace) -> None:
115+
"""Execute SKRL training with parsed arguments."""
103116
import skrl
104117

105118
from isaaclab.app import launch_simulation
@@ -111,8 +124,6 @@ def run(argv: list[str]) -> None:
111124

112125
from isaaclab_tasks.utils import resolve_task_config
113126

114-
args_cli = _parse_args(argv)
115-
116127
if version.parse(skrl.__version__) < version.parse(SKRL_VERSION):
117128
skrl.logger.error(
118129
f"Unsupported skrl version: {skrl.__version__}. "
@@ -139,9 +150,6 @@ def run(argv: list[str]) -> None:
139150
agent_cfg["trainer"]["timesteps"] = args_cli.max_iterations * agent_cfg["agent"]["rollouts"]
140151
agent_cfg["trainer"]["close_environment_at_exit"] = False
141152

142-
if args_cli.ml_framework.startswith("jax"):
143-
skrl.config.jax.backend = "jax" if args_cli.ml_framework == "jax" else "numpy"
144-
145153
if args_cli.seed == -1:
146154
args_cli.seed = random.randint(0, 10000)
147155

source/isaaclab_rl/test/test_entrypoints.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@
88
from __future__ import annotations
99

1010
import importlib
11+
import runpy
1112
import sys
1213
import types
1314

15+
import pytest
16+
1417
from isaaclab_rl.entrypoints import PlaybackRequest, TrainingRequest, api, dispatch
1518

1619

@@ -140,3 +143,92 @@ def test_dispatch_fuses_option_like_kit_args(monkeypatch) -> None:
140143
def test_dispatch_requires_a_backend() -> None:
141144
"""Missing backend selection returns the conventional CLI error status."""
142145
assert dispatch.run_train_cli(["--task", "Isaac-Cartpole"]) == 2
146+
147+
148+
def _torch_backend_state() -> tuple[bool, bool, bool, bool]:
149+
import torch
150+
151+
return (
152+
torch.backends.cuda.matmul.allow_tf32,
153+
torch.backends.cudnn.allow_tf32,
154+
torch.backends.cudnn.deterministic,
155+
torch.backends.cudnn.benchmark,
156+
)
157+
158+
159+
def test_scoped_backend_state_restores_values_after_exception() -> None:
160+
"""Backend-global settings are restored when a scoped operation fails."""
161+
import torch
162+
163+
from isaaclab_rl.entrypoints._state import preserve_attribute, scoped_torch_backend_flags
164+
165+
original = _torch_backend_state()
166+
holder = types.SimpleNamespace(value="original")
167+
168+
with pytest.raises(RuntimeError, match="failed"):
169+
with scoped_torch_backend_flags(torch), preserve_attribute(holder, "value"):
170+
holder.value = "temporary"
171+
assert _torch_backend_state() == (True, True, False, False)
172+
raise RuntimeError("failed")
173+
174+
assert _torch_backend_state() == original
175+
assert holder.value == "original"
176+
177+
178+
def test_rejected_rsl_training_preserves_torch_backend_state(monkeypatch) -> None:
179+
"""A rejected in-process RSL-RL request does not mutate its caller."""
180+
import torch
181+
182+
caller_state = (False, False, True, True)
183+
settings = (
184+
(torch.backends.cuda.matmul, "allow_tf32"),
185+
(torch.backends.cudnn, "allow_tf32"),
186+
(torch.backends.cudnn, "deterministic"),
187+
(torch.backends.cudnn, "benchmark"),
188+
)
189+
for (target, name), value in zip(settings, caller_state):
190+
monkeypatch.setattr(target, name, value)
191+
192+
with pytest.raises(SystemExit):
193+
dispatch._run_backend("isaaclab_rl.entrypoints.backends.train_rsl_rl", ["--help"], run_as_script=False)
194+
195+
assert _torch_backend_state() == caller_state
196+
197+
198+
def test_skrl_training_restores_jax_backend(monkeypatch) -> None:
199+
"""SKRL training removes the JAX backend setting it created after an exception."""
200+
import skrl
201+
202+
from isaaclab_rl.entrypoints.backends import train_skrl
203+
204+
monkeypatch.delattr(skrl.config.jax, "backend", raising=False)
205+
monkeypatch.setattr(train_skrl, "_parse_args", lambda argv: types.SimpleNamespace(ml_framework="jax"))
206+
207+
def fail_after_mutation(_args_cli) -> None:
208+
assert skrl.config.jax.backend == "jax"
209+
skrl.config.jax.backend = "mutated"
210+
raise RuntimeError("failed")
211+
212+
monkeypatch.setattr(train_skrl, "_run", fail_after_mutation)
213+
with pytest.raises(RuntimeError, match="failed"):
214+
train_skrl.run([])
215+
216+
assert not hasattr(skrl.config.jax, "backend")
217+
218+
219+
def test_skrl_play_main_restores_jax_backend(monkeypatch) -> None:
220+
"""Direct SKRL play calls remove the JAX backend setting they created."""
221+
monkeypatch.setattr(sys, "argv", ["play_skrl.py"])
222+
namespace = runpy.run_module("isaaclab_rl.entrypoints.backends.play_skrl", run_name="test_play_skrl")
223+
skrl = namespace["skrl"]
224+
monkeypatch.delattr(skrl.config.jax, "backend", raising=False)
225+
226+
def fail_after_mutation() -> None:
227+
skrl.config.jax.backend = "mutated"
228+
raise RuntimeError("failed")
229+
230+
namespace["main"].__globals__["_main"] = fail_after_mutation
231+
with pytest.raises(RuntimeError, match="failed"):
232+
namespace["main"]()
233+
234+
assert not hasattr(skrl.config.jax, "backend")

0 commit comments

Comments
 (0)