Skip to content

Commit a855496

Browse files
ooctipushujc7
andauthored
[3.0.0-beta2] Fix rl_games play checkpoint fallback (#6133)
Cherry-picks #6091 to release/3.0.0-beta2. Upstream merge commit: de78c80 Cherry-pick commit: e598169 Conflict resolution: - Kept the new checkpoint-path test under the beta2 test layout at `source/isaaclab_tasks/test/test_checkpoint_path.py`. Validation: - git diff --check upstream/release/3.0.0-beta2..HEAD - env PYTHONPATH=source/isaaclab_tasks:source/isaaclab:source/isaaclab_rl /home/zhengyuz/Projects/IsaacLab.wt/baseline-newton-fabric-repro/env_isaaclab/bin/python -m py_compile source/isaaclab_tasks/isaaclab_tasks/utils/parse_cfg.py scripts/reinforcement_learning/rl_games/play.py scripts/reinforcement_learning/rl_games/play_rl_games.py scripts/reinforcement_learning/sb3/play.py scripts/reinforcement_learning/sb3/play_sb3.py source/isaaclab_tasks/test/test_checkpoint_path.py - env PYTHONPATH=source/isaaclab_tasks:source/isaaclab:source/isaaclab_rl /home/zhengyuz/Projects/IsaacLab.wt/newton-fabric-body-binding/env_isaaclab/bin/python -m pytest -q source/isaaclab_tasks/test/test_checkpoint_path.py - env PYTHONPATH=source/isaaclab_tasks_experimental:source/isaaclab_tasks:source/isaaclab_rl:source/isaaclab:source/isaaclab_assets:source/isaaclab_mimic:source/isaaclab_newton:source/isaaclab_physx:source/isaaclab_ov:source/isaaclab_ovphysx:source/isaaclab_visualizers /home/zhengyuz/Projects/IsaacLab.wt/newton-fabric-body-binding/env_isaaclab/bin/python scripts/reinforcement_learning/rl_games/play.py --help - env PYTHONPATH=source/isaaclab_tasks_experimental:source/isaaclab_tasks:source/isaaclab_rl:source/isaaclab:source/isaaclab_assets:source/isaaclab_mimic:source/isaaclab_newton:source/isaaclab_physx:source/isaaclab_ov:source/isaaclab_ovphysx:source/isaaclab_visualizers /home/zhengyuz/Projects/IsaacLab.wt/newton-fabric-body-binding/env_isaaclab/bin/python scripts/reinforcement_learning/play.py --rl_library rl_games --help - pre-commit run --files docs/source/tutorials/03_envs/run_rl_training.rst scripts/reinforcement_learning/rl_games/play.py scripts/reinforcement_learning/rl_games/play_rl_games.py scripts/reinforcement_learning/sb3/play.py scripts/reinforcement_learning/sb3/play_sb3.py source/isaaclab_tasks/changelog.d/fix-rlgames-checkpoint-fallback.rst source/isaaclab_tasks/isaaclab_tasks/utils/parse_cfg.py source/isaaclab_tasks/test/test_checkpoint_path.py Co-authored-by: jichuanh <jichuanh@nvidia.com>
1 parent e3d9a56 commit a855496

8 files changed

Lines changed: 166 additions & 39 deletions

File tree

docs/source/tutorials/03_envs/run_rl_training.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ Once the training is complete, you can visualize the trained agent by executing
142142
.. code:: bash
143143
144144
# execute from the root directory of the repository
145-
./isaaclab.sh play --rl_library sb3 --task Isaac-Cartpole --num_envs 32 --use_last_checkpoint --viz kit
145+
./isaaclab.sh play --rl_library sb3 --task Isaac-Cartpole --num_envs 32 --viz kit
146146
147147
The above command will load the latest checkpoint from the ``logs/sb3/Isaac-Cartpole-v0``
148148
directory. You can also specify a specific checkpoint by passing the ``--checkpoint`` flag.

scripts/reinforcement_learning/rl_games/play.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,12 @@ def main():
114114
return
115115
elif args_cli.checkpoint is None:
116116
run_dir = agent_cfg["params"]["config"].get("full_experiment_name", ".*")
117-
if args_cli.use_last_checkpoint:
118-
checkpoint_file = ".*"
119-
else:
120-
checkpoint_file = f"{agent_cfg['params']['config']['name']}.pth"
121-
resume_path = get_checkpoint_path(log_root_path, run_dir, checkpoint_file, other_dirs=["nn"])
117+
# prefer the best-reward checkpoint (``<name>.pth``); fall back to the latest checkpoint when it has
118+
# not been written yet (e.g. short runs). With --use_last_checkpoint, skip the preference entirely.
119+
best_checkpoint = None if args_cli.use_last_checkpoint else f"{agent_cfg['params']['config']['name']}.pth"
120+
resume_path = get_checkpoint_path(
121+
log_root_path, run_dir, ".*", other_dirs=["nn"], preferred_checkpoint=best_checkpoint
122+
)
122123
else:
123124
resume_path = retrieve_file_path(args_cli.checkpoint)
124125
log_dir = os.path.dirname(os.path.dirname(resume_path))

scripts/reinforcement_learning/rl_games/play_rl_games.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,12 @@ def main():
104104
return
105105
elif args_cli.checkpoint is None:
106106
run_dir = agent_cfg["params"]["config"].get("full_experiment_name", ".*")
107-
if args_cli.use_last_checkpoint:
108-
checkpoint_file = ".*"
109-
else:
110-
checkpoint_file = f"{agent_cfg['params']['config']['name']}.pth"
111-
resume_path = get_checkpoint_path(log_root_path, run_dir, checkpoint_file, other_dirs=["nn"])
107+
# prefer the best-reward checkpoint (``<name>.pth``); fall back to the latest checkpoint when it has
108+
# not been written yet (e.g. short runs). With --use_last_checkpoint, skip the preference entirely.
109+
best_checkpoint = None if args_cli.use_last_checkpoint else f"{agent_cfg['params']['config']['name']}.pth"
110+
resume_path = get_checkpoint_path(
111+
log_root_path, run_dir, ".*", other_dirs=["nn"], preferred_checkpoint=best_checkpoint
112+
)
112113
else:
113114
resume_path = retrieve_file_path(args_cli.checkpoint)
114115
log_dir = os.path.dirname(os.path.dirname(resume_path))

scripts/reinforcement_learning/sb3/play.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,6 @@
6767
action="store_true",
6868
help="Use the pre-trained checkpoint from Nucleus.",
6969
)
70-
parser.add_argument(
71-
"--use_last_checkpoint",
72-
action="store_true",
73-
help="When no checkpoint provided, use the last saved model. Otherwise use the best saved model.",
74-
)
7570
parser.add_argument("--real-time", action="store_true", default=False, help="Run in real-time, if possible.")
7671
parser.add_argument(
7772
"--keep_all_info",
@@ -114,11 +109,11 @@ def main():
114109
print("[INFO] Unfortunately a pre-trained checkpoint is currently unavailable for this task.")
115110
return
116111
elif args_cli.checkpoint is None:
117-
if args_cli.use_last_checkpoint:
118-
checkpoint = "model_.*.zip"
119-
else:
120-
checkpoint = "model.zip"
121-
checkpoint_path = get_checkpoint_path(log_root_path, ".*", checkpoint, sort_alpha=False)
112+
# prefer the final model (``model.zip``); fall back to the latest periodic checkpoint when it has
113+
# not been written yet (e.g. short or interrupted runs)
114+
checkpoint_path = get_checkpoint_path(
115+
log_root_path, ".*", r"model_.*\.zip", sort_alpha=False, preferred_checkpoint=r"model\.zip"
116+
)
122117
else:
123118
checkpoint_path = args_cli.checkpoint
124119
log_dir = os.path.dirname(checkpoint_path)

scripts/reinforcement_learning/sb3/play_sb3.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,6 @@
5656
action="store_true",
5757
help="Use the pre-trained checkpoint from Nucleus.",
5858
)
59-
parser.add_argument(
60-
"--use_last_checkpoint",
61-
action="store_true",
62-
help="When no checkpoint provided, use the last saved model. Otherwise use the best saved model.",
63-
)
6459
parser.add_argument("--real-time", action="store_true", default=False, help="Run in real-time, if possible.")
6560
parser.add_argument(
6661
"--keep_all_info",
@@ -104,11 +99,11 @@ def main():
10499
print("[INFO] Unfortunately a pre-trained checkpoint is currently unavailable for this task.")
105100
return
106101
elif args_cli.checkpoint is None:
107-
if args_cli.use_last_checkpoint:
108-
checkpoint = "model_.*.zip"
109-
else:
110-
checkpoint = "model.zip"
111-
checkpoint_path = get_checkpoint_path(log_root_path, ".*", checkpoint, sort_alpha=False)
102+
# prefer the final model (``model.zip``); fall back to the latest periodic checkpoint when it has
103+
# not been written yet (e.g. short or interrupted runs)
104+
checkpoint_path = get_checkpoint_path(
105+
log_root_path, ".*", r"model_.*\.zip", sort_alpha=False, preferred_checkpoint=r"model\.zip"
106+
)
112107
else:
113108
checkpoint_path = args_cli.checkpoint
114109
log_dir = os.path.dirname(checkpoint_path)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Added
2+
^^^^^
3+
4+
* Added a ``preferred_checkpoint`` regex argument to :func:`~isaaclab_tasks.utils.parse_cfg.get_checkpoint_path`
5+
that is matched before ``checkpoint`` and wins when it matches, otherwise resolution falls back to ``checkpoint``.
6+
7+
Fixed
8+
^^^^^
9+
10+
* Fixed ``rl_games`` and ``sb3`` play failing to load a checkpoint on short runs where the preferred
11+
best/final checkpoint has not been written yet. They now prefer the best (``rl_games``) or final
12+
(``sb3``) checkpoint and fall back to the latest available checkpoint when it is missing. Numbered
13+
checkpoint filenames are now sorted naturally so epoch 10 is selected after epoch 9.

source/isaaclab_tasks/isaaclab_tasks/utils/parse_cfg.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,12 @@ def parse_env_cfg(
171171

172172

173173
def get_checkpoint_path(
174-
log_path: str, run_dir: str = ".*", checkpoint: str = ".*", other_dirs: list[str] = None, sort_alpha: bool = True
174+
log_path: str,
175+
run_dir: str = ".*",
176+
checkpoint: str = ".*",
177+
other_dirs: list[str] = None,
178+
sort_alpha: bool = True,
179+
preferred_checkpoint: str | None = None,
175180
) -> str:
176181
"""Get path to the model checkpoint in input directory.
177182
@@ -187,10 +192,15 @@ def get_checkpoint_path(
187192
recent directory created inside :attr:`log_path`.
188193
other_dirs: The intermediate directories between the run directory and the checkpoint file. Defaults to
189194
None, which implies that checkpoint file is directly under the run directory.
190-
checkpoint: The regex expression for the model checkpoint file. Defaults to the most recent
191-
torch-model saved in the :attr:`run_dir` directory.
195+
checkpoint: The regex expression for the model checkpoint file. Defaults to ``".*"``, which matches all
196+
checkpoints and selects the most recent one.
192197
sort_alpha: Whether to sort the runs by alphabetical order. Defaults to True.
193198
If False, the folders in :attr:`run_dir` are sorted by the last modified time.
199+
preferred_checkpoint: An optional regex expression that is matched before :attr:`checkpoint`. When it is
200+
provided and matches at least one file, that match is used; otherwise resolution falls back to
201+
:attr:`checkpoint`. This allows preferring a specific checkpoint (e.g. the best or final model) while
202+
still resolving the latest available checkpoint when the preferred file has not been written yet (e.g.
203+
for short runs). Defaults to None, which matches :attr:`checkpoint` directly.
194204
195205
Returns:
196206
The path to the model checkpoint.
@@ -219,13 +229,22 @@ def get_checkpoint_path(
219229
except IndexError:
220230
raise ValueError(f"No runs present in the directory: '{log_path}' match: '{run_dir}'.")
221231

222-
# list all model checkpoints in the directory
223-
model_checkpoints = [f for f in os.listdir(run_path) if re.match(checkpoint, f)]
232+
# prefer ``preferred_checkpoint`` when given and it matches; otherwise fall back to the general
233+
# ``checkpoint`` pattern (e.g. resolve the latest numbered checkpoint when the preferred best/final
234+
# checkpoint file has not been written yet)
235+
model_checkpoints = []
236+
if preferred_checkpoint is not None:
237+
model_checkpoints = [f for f in os.listdir(run_path) if re.match(preferred_checkpoint, f)]
238+
if len(model_checkpoints) == 0:
239+
model_checkpoints = [f for f in os.listdir(run_path) if re.match(checkpoint, f)]
224240
# check if any checkpoints are present
225241
if len(model_checkpoints) == 0:
226-
raise ValueError(f"No checkpoints in the directory: '{run_path}' match '{checkpoint}'.")
227-
# sort alphabetically while ensuring that *_10 comes after *_9
228-
model_checkpoints.sort(key=lambda m: f"{m:0>15}")
242+
patterns = f"'{checkpoint}'"
243+
if preferred_checkpoint is not None:
244+
patterns = f"'{preferred_checkpoint}' nor '{checkpoint}'"
245+
raise ValueError(f"No checkpoints in the directory: '{run_path}' match {patterns}.")
246+
# sort naturally so numbered checkpoints such as *_10 come after *_9 even with long filename prefixes
247+
model_checkpoints.sort(key=lambda m: [int(token) if token.isdigit() else token for token in re.split(r"(\d+)", m)])
229248
# get latest matched checkpoint file
230249
checkpoint_file = model_checkpoints[-1]
231250

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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+
import pytest
7+
8+
from isaaclab_tasks.utils import get_checkpoint_path
9+
10+
11+
def test_get_checkpoint_path_prefers_requested_checkpoint(tmp_path):
12+
checkpoint_dir = tmp_path / "cartpole_camera_direct" / "2026-06-07_02-41-41" / "nn"
13+
checkpoint_dir.mkdir(parents=True)
14+
expected_checkpoint = checkpoint_dir / "cartpole_camera_direct.pth"
15+
expected_checkpoint.touch()
16+
(checkpoint_dir / "last_cartpole_camera_direct_ep_10_rew_1.0.pth").touch()
17+
18+
checkpoint_path = get_checkpoint_path(
19+
str(tmp_path / "cartpole_camera_direct"), "2026-06-07_02-41-41", "cartpole_camera_direct.pth", other_dirs=["nn"]
20+
)
21+
22+
assert checkpoint_path == str(expected_checkpoint)
23+
24+
25+
def test_get_checkpoint_path_uses_latest_checkpoint_when_checkpoint_is_omitted(tmp_path):
26+
checkpoint_dir = tmp_path / "cartpole_camera_direct" / "2026-06-07_02-41-41" / "nn"
27+
checkpoint_dir.mkdir(parents=True)
28+
(checkpoint_dir / "last_cartpole_camera_direct_ep_5_rew_0.5.pth").touch()
29+
expected_checkpoint = checkpoint_dir / "last_cartpole_camera_direct_ep_10_rew_1.0.pth"
30+
expected_checkpoint.touch()
31+
32+
checkpoint_path = get_checkpoint_path(
33+
str(tmp_path / "cartpole_camera_direct"), "2026-06-07_02-41-41", other_dirs=["nn"]
34+
)
35+
36+
assert checkpoint_path == str(expected_checkpoint)
37+
38+
39+
def test_get_checkpoint_path_requested_checkpoint_stays_strict(tmp_path):
40+
checkpoint_dir = tmp_path / "cartpole_camera_direct" / "2026-06-07_02-41-41" / "nn"
41+
checkpoint_dir.mkdir(parents=True)
42+
(checkpoint_dir / "last_cartpole_camera_direct_ep_10_rew_1.0.pth").touch()
43+
44+
with pytest.raises(ValueError, match="cartpole_camera_direct.pth"):
45+
get_checkpoint_path(
46+
str(tmp_path / "cartpole_camera_direct"),
47+
"2026-06-07_02-41-41",
48+
"cartpole_camera_direct.pth",
49+
other_dirs=["nn"],
50+
)
51+
52+
53+
def test_get_checkpoint_path_prefers_preferred_over_checkpoint(tmp_path):
54+
# rl_games: the best checkpoint must win over numbered ones when given as the preferred pattern
55+
checkpoint_dir = tmp_path / "cartpole_camera_direct" / "2026-06-07_02-41-41" / "nn"
56+
checkpoint_dir.mkdir(parents=True)
57+
expected_checkpoint = checkpoint_dir / "cartpole_camera_direct.pth"
58+
expected_checkpoint.touch()
59+
(checkpoint_dir / "last_cartpole_camera_direct_ep_10_rew_1.0.pth").touch()
60+
61+
checkpoint_path = get_checkpoint_path(
62+
str(tmp_path / "cartpole_camera_direct"),
63+
"2026-06-07_02-41-41",
64+
".*",
65+
other_dirs=["nn"],
66+
preferred_checkpoint="cartpole_camera_direct.pth",
67+
)
68+
69+
assert checkpoint_path == str(expected_checkpoint)
70+
71+
72+
def test_get_checkpoint_path_falls_back_when_preferred_missing(tmp_path):
73+
# rl_games short run: the best checkpoint is never written, so resolution falls back to the latest numbered one
74+
checkpoint_dir = tmp_path / "cartpole_camera_direct" / "2026-06-07_02-41-41" / "nn"
75+
checkpoint_dir.mkdir(parents=True)
76+
(checkpoint_dir / "last_cartpole_camera_direct_ep_9_rew_0.9.pth").touch()
77+
expected_checkpoint = checkpoint_dir / "last_cartpole_camera_direct_ep_10_rew_1.0.pth"
78+
expected_checkpoint.touch()
79+
80+
checkpoint_path = get_checkpoint_path(
81+
str(tmp_path / "cartpole_camera_direct"),
82+
"2026-06-07_02-41-41",
83+
".*",
84+
other_dirs=["nn"],
85+
preferred_checkpoint="cartpole_camera_direct.pth",
86+
)
87+
88+
assert checkpoint_path == str(expected_checkpoint)
89+
90+
91+
def test_get_checkpoint_path_prefers_final_model_over_steps(tmp_path):
92+
# sb3: the final ``model.zip`` must win over numbered ``model_<n>_steps.zip`` snapshots
93+
run_dir = tmp_path / "2026-06-07_02-41-41"
94+
run_dir.mkdir(parents=True)
95+
expected_checkpoint = run_dir / "model.zip"
96+
expected_checkpoint.touch()
97+
(run_dir / "model_2000_steps.zip").touch()
98+
99+
checkpoint_path = get_checkpoint_path(
100+
str(tmp_path), ".*", r"model_.*\.zip", sort_alpha=False, preferred_checkpoint=r"model\.zip"
101+
)
102+
103+
assert checkpoint_path == str(expected_checkpoint)

0 commit comments

Comments
 (0)