Skip to content

Commit eda9b46

Browse files
committed
Replace the pretrained checkpoint functions with CheckpointBundle
The module answered every path question through 13 free functions that each took the same (workflow, task_name, physics_backend, render_backend) tuple, and the publish script unpacked it at 20 call sites. CheckpointBundle owns that identity and the checkpoints a task declares, so the policy is one checkpoint among them rather than a special case with auxiliaries hanging off it. find_cfgs and latest_file move to isaaclab.utils as public utilities. WORKFLOW_TRAINER and WORKFLOW_PLAYER are deleted: every workflow mapped to the same entrypoint and nothing read them. get_published_pretrained_checkpoint is unchanged, so its ten call sites are untouched.
1 parent 18689b2 commit eda9b46

14 files changed

Lines changed: 532 additions & 692 deletions

scripts/tools/test/test_train_and_publish_checkpoints.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def test_build_core_jobs_skips_unsupported_preset_without_normalizing_default(
4242
lambda _: {PresetTarget.PHYSICS: ["newton_kamino"]},
4343
)
4444
monkeypatch.setattr(
45-
"scripts.tools.train_and_publish_checkpoints.get_pretrained_checkpoint_backend_names",
45+
"scripts.tools.train_and_publish_checkpoints.CheckpointBundle.backend_names",
4646
lambda _: pytest.fail("preset-only tasks must not normalize their unsupported default backend"),
4747
)
4848
args = Namespace(physics_backends="physx,newtonmjwarp", render_backends="rtx,newton")
@@ -104,11 +104,11 @@ def test_select_physics_variants_does_not_fall_back_to_automatic_physx() -> None
104104
assert selections == []
105105

106106

107-
def test_legacy_job_experiment_name_preserves_task_name() -> None:
107+
def test_legacy_job_stem_preserves_task_name() -> None:
108108
"""Legacy jobs must keep separate experiment directories for each task."""
109109
job = CheckpointJob(workflow="rsl_rl", task_name="Isaac-Test")
110110

111-
assert job.experiment_name == "Isaac-Test"
111+
assert job.stem == "Isaac-Test"
112112

113113

114114
def test_legacy_collection_preserves_task_directory(tmp_path: Path) -> None:

scripts/tools/train_and_publish_checkpoints.py

Lines changed: 38 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@
5959
import fnmatch
6060
import json
6161
import os
62-
import posixpath
6362
import shutil
6463
import subprocess
6564
import sys
@@ -78,23 +77,12 @@
7877
import gymnasium as gym
7978

8079
from isaaclab.envs import DirectMARLEnvCfg
81-
from isaaclab.utils import Checkpoint
80+
from isaaclab.utils.assets import NUCLEUS_ASSET_ROOT_DIR
8281

8382
from isaaclab_rl.utils.pretrained_checkpoint import (
8483
WORKFLOW_EXPERIMENT_NAME_VARIABLE,
8584
WORKFLOWS,
86-
get_auxiliary_checkpoint_path,
87-
get_auxiliary_checkpoints,
88-
get_latest_job_run_path,
89-
get_pretrained_checkpoint_backend_names,
90-
get_pretrained_checkpoint_filename,
91-
get_pretrained_checkpoint_path,
92-
get_pretrained_checkpoint_publish_path,
93-
get_pretrained_checkpoint_review,
94-
get_pretrained_checkpoint_review_path,
95-
has_pretrained_checkpoint_job_finished,
96-
has_pretrained_checkpoint_job_run,
97-
has_pretrained_checkpoints_asset_root_dir,
85+
CheckpointBundle,
9886
)
9987

10088
import isaaclab_tasks # noqa: F401
@@ -108,19 +96,13 @@
10896

10997

11098
@dataclass(frozen=True)
111-
class CheckpointJob:
99+
class CheckpointJob(CheckpointBundle):
112100
"""One workflow, task, physics, and renderer training combination."""
113101

114-
workflow: str
115-
task_name: str
116-
physics_backend: str | None = None
117-
render_backend: str | None = None
118102
physics_selector: str | None = None
119103
render_selector: str | None = None
120104
agent: str | None = None
121105
algorithm: str | None = None
122-
auxiliary_checkpoints: tuple[Checkpoint, ...] = ()
123-
"""Run artifacts the task publishes beside its policy."""
124106

125107
@property
126108
def job_id(self) -> str:
@@ -129,20 +111,6 @@ def job_id(self) -> str:
129111
return f"{self.workflow}:{self.task_name}"
130112
return f"{self.workflow}:{self.task_name}:{self.physics_backend}:{self.render_backend}"
131113

132-
@property
133-
def experiment_name(self) -> str:
134-
"""Return the experiment directory name used by the RL workflow."""
135-
if self.physics_backend is None and self.render_backend is None:
136-
return self.task_name
137-
filename = get_pretrained_checkpoint_filename(
138-
self.workflow,
139-
self.task_name,
140-
self.physics_backend,
141-
self.render_backend,
142-
)
143-
extension = os.path.splitext(filename)[1]
144-
return filename.removesuffix(extension)
145-
146114
@property
147115
def preset_args(self) -> list[str]:
148116
"""Return typed preset selectors for this job."""
@@ -293,7 +261,7 @@ def _resolve_physics_backend(task_name: str, physics_selector: str | None, defau
293261
if physics_selector is None:
294262
return default_backend
295263
env_cfg, _ = resolve_task_config(task_name, None, overrides=(f"physics={physics_selector}",))
296-
physics_backend, _ = get_pretrained_checkpoint_backend_names(env_cfg)
264+
physics_backend, _ = CheckpointBundle.backend_names(env_cfg)
297265
return physics_backend
298266

299267

@@ -343,7 +311,7 @@ def _build_core_jobs(args: argparse.Namespace) -> list[CheckpointJob]:
343311
workflow, agent, algorithm = _select_workflow(task_spec, env_cfg)
344312
default_physics = None
345313
if not physics_variants:
346-
default_physics, _ = get_pretrained_checkpoint_backend_names(env_cfg)
314+
default_physics, _ = CheckpointBundle.backend_names(env_cfg)
347315

348316
physics_selections = _select_physics_variants(
349317
task_spec.id,
@@ -356,16 +324,16 @@ def _build_core_jobs(args: argparse.Namespace) -> list[CheckpointJob]:
356324
physics_backend = _resolve_physics_backend(task_spec.id, physics_selector, default_physics)
357325
for render_backend, render_selector in render_selections:
358326
jobs.append(
359-
CheckpointJob(
360-
workflow=workflow,
361-
task_name=task_spec.id,
362-
physics_backend=physics_backend,
363-
render_backend=render_backend,
327+
CheckpointJob.from_env_cfg(
328+
workflow,
329+
task_spec.id,
330+
env_cfg,
331+
physics_backend,
332+
render_backend,
364333
physics_selector=physics_selector,
365334
render_selector=render_selector,
366335
agent=agent,
367336
algorithm=algorithm,
368-
auxiliary_checkpoints=tuple(get_auxiliary_checkpoints(env_cfg)),
369337
)
370338
)
371339
return jobs
@@ -411,7 +379,7 @@ def _training_command(job: CheckpointJob, args: argparse.Namespace, smoke: bool)
411379
if job.algorithm is not None:
412380
command.extend(["--algorithm", job.algorithm])
413381

414-
experiment_name = f"{job.experiment_name}_smoke" if smoke else job.experiment_name
382+
experiment_name = f"{job.stem}_smoke" if smoke else job.stem
415383
experiment_variable = WORKFLOW_EXPERIMENT_NAME_VARIABLE[job.workflow]
416384
if experiment_variable is not None:
417385
command.append(f"{experiment_variable}={experiment_name}")
@@ -471,30 +439,15 @@ def _run_command(command: list[str], dry_run: bool) -> int:
471439

472440
def _has_training_job_completed(job: CheckpointJob) -> bool:
473441
"""Return whether the latest run exited successfully with a checkpoint."""
474-
run_path = get_latest_job_run_path(
475-
job.workflow,
476-
job.task_name,
477-
job.physics_backend,
478-
job.render_backend,
479-
)
442+
run_path = job.latest_run
480443
if run_path is None or not os.path.isfile(os.path.join(run_path, _TRAINING_COMPLETE_FILENAME)):
481444
return False
482-
return has_pretrained_checkpoint_job_finished(
483-
job.workflow,
484-
job.task_name,
485-
job.physics_backend,
486-
job.render_backend,
487-
)
445+
return job.has_finished
488446

489447

490448
def _mark_training_job_completed(job: CheckpointJob) -> None:
491449
"""Record that the latest training subprocess exited successfully."""
492-
run_path = get_latest_job_run_path(
493-
job.workflow,
494-
job.task_name,
495-
job.physics_backend,
496-
job.render_backend,
497-
)
450+
run_path = job.latest_run
498451
if run_path is None:
499452
raise RuntimeError(f"Unable to determine the latest run for {job.job_id}")
500453
marker_path = os.path.join(run_path, _TRAINING_COMPLETE_FILENAME)
@@ -514,12 +467,7 @@ def train_job(job: CheckpointJob, args: argparse.Namespace, smoke: bool = False)
514467
return False
515468
if smoke or args.dry_run:
516469
return True
517-
if not has_pretrained_checkpoint_job_finished(
518-
job.workflow,
519-
job.task_name,
520-
job.physics_backend,
521-
job.render_backend,
522-
):
470+
if not job.has_finished:
523471
print(f"Training did not produce a checkpoint for {job.job_id}", file=sys.stderr)
524472
return False
525473
_mark_training_job_completed(job)
@@ -528,71 +476,45 @@ def train_job(job: CheckpointJob, args: argparse.Namespace, smoke: bool = False)
528476

529477
def collect_pretrained_checkpoint(job: CheckpointJob, output_dir: str, dry_run: bool = False) -> str | None:
530478
"""Copy the last or best checkpoint into the structured output directory."""
531-
destination = _get_collected_checkpoint_path(job, output_dir)
479+
destination = job.collected_path(output_dir)
532480
if dry_run:
533481
print(f"Would collect the completed checkpoint -> {destination}")
534482
return destination
535483

536-
source_path = get_pretrained_checkpoint_path(
537-
job.workflow,
538-
job.task_name,
539-
job.physics_backend,
540-
job.render_backend,
541-
)
484+
source_path = job.trained_path()
542485
if source_path is None or not os.path.isfile(source_path):
543486
print(f"No completed checkpoint to collect for {job.job_id}")
544487
return None
545488

546489
print(f"Collecting {source_path} -> {destination}")
547490
os.makedirs(os.path.dirname(destination), exist_ok=True)
548491
shutil.copy2(source_path, destination)
549-
run_path = get_latest_job_run_path(job.workflow, job.task_name, job.physics_backend, job.render_backend)
550-
for checkpoint in job.auxiliary_checkpoints:
551-
aux_source = checkpoint.find_in(run_path)
492+
for checkpoint in job.checkpoints:
493+
aux_source = job.trained_path(checkpoint)
552494
if aux_source is None:
553495
print(f"No {checkpoint.name} checkpoint matched {checkpoint.run_glob!r} for {job.job_id}")
554496
continue
555-
aux_destination = get_auxiliary_checkpoint_path(destination, job.workflow, checkpoint)
497+
aux_destination = job.collected_path(output_dir, checkpoint)
556498
print(f"Collecting {aux_source} -> {aux_destination}")
557499
shutil.copy2(aux_source, aux_destination)
558500
return destination
559501

560502

561503
def review_pretrained_checkpoint(job: CheckpointJob, args: argparse.Namespace) -> bool:
562504
"""Play and interactively review one checkpoint."""
563-
if not has_pretrained_checkpoint_job_run(
564-
job.workflow,
565-
job.task_name,
566-
job.physics_backend,
567-
job.render_backend,
568-
):
505+
if not job.has_run:
569506
print(f"Skipping review of {job.job_id}; it has not been trained")
570507
return False
571-
if not has_pretrained_checkpoint_job_finished(
572-
job.workflow,
573-
job.task_name,
574-
job.physics_backend,
575-
job.render_backend,
576-
):
508+
if not job.has_finished:
577509
print(f"Skipping review of {job.job_id}; training is incomplete")
578510
return False
579511

580-
review = get_pretrained_checkpoint_review(
581-
job.workflow,
582-
job.task_name,
583-
job.physics_backend,
584-
job.render_backend,
585-
)
512+
review = job.review
586513
if not args.force_review and review and review.get("reviewed"):
587514
print(f"Review already complete for {job.job_id}")
588515
return True
589516

590-
checkpoint_path = get_pretrained_checkpoint_path(
591-
job.workflow,
592-
job.task_name,
593-
job.physics_backend,
594-
job.render_backend,
595-
)
517+
checkpoint_path = job.trained_path()
596518
if checkpoint_path is None:
597519
print(f"Skipping review of {job.job_id}; no checkpoint was found")
598520
return False
@@ -610,12 +532,7 @@ def review_pretrained_checkpoint(job: CheckpointJob, args: argparse.Namespace) -
610532
if notes:
611533
review_data["notes"] = notes
612534

613-
review_path = get_pretrained_checkpoint_review_path(
614-
job.workflow,
615-
job.task_name,
616-
job.physics_backend,
617-
job.render_backend,
618-
)
535+
review_path = job.review_path
619536
if review_path is None:
620537
raise RuntimeError(f"Unable to determine review path for {job.job_id}")
621538
with open(review_path, "w", encoding="utf-8") as review_file:
@@ -625,46 +542,27 @@ def review_pretrained_checkpoint(job: CheckpointJob, args: argparse.Namespace) -
625542

626543
def publish_pretrained_checkpoint(job: CheckpointJob, args: argparse.Namespace) -> bool:
627544
"""Publish an accepted checkpoint to the configured Nucleus asset root."""
628-
if args.publish_root is None and not has_pretrained_checkpoints_asset_root_dir():
545+
if args.publish_root is None and not NUCLEUS_ASSET_ROOT_DIR:
629546
raise RuntimeError("A pretrained-checkpoint Nucleus asset root is not configured")
630-
local_path = _get_collected_checkpoint_path(job, args.output_dir)
547+
local_path = job.collected_path(args.output_dir)
631548
if not os.path.isfile(local_path):
632549
print(f"Skipping publish of {job.job_id}; no collected checkpoint was found")
633550
return False
634551

635552
if not args.force_publish:
636-
review = get_pretrained_checkpoint_review(
637-
job.workflow,
638-
job.task_name,
639-
job.physics_backend,
640-
job.render_backend,
641-
)
553+
review = job.review
642554
if not review or review.get("result") != "accepted":
643555
print(f"Skipping publish of {job.job_id}; it does not have an accepted review")
644556
return False
645557

646-
if args.publish_root is None:
647-
publish_path = get_pretrained_checkpoint_publish_path(
648-
job.workflow,
649-
job.task_name,
650-
job.physics_backend,
651-
job.render_backend,
652-
)
653-
else:
654-
filename = get_pretrained_checkpoint_filename(
655-
job.workflow,
656-
job.task_name,
657-
job.physics_backend,
658-
job.render_backend,
659-
)
660-
publish_path = posixpath.join(args.publish_root.rstrip("/"), job.workflow, filename)
558+
publish_path = job.published_path(root=args.publish_root)
661559
uploads = [(local_path, publish_path)]
662-
for checkpoint in job.auxiliary_checkpoints:
663-
local_auxiliary = get_auxiliary_checkpoint_path(local_path, job.workflow, checkpoint)
664-
if not os.path.isfile(local_auxiliary):
560+
for checkpoint in job.checkpoints:
561+
local_declared = job.collected_path(args.output_dir, checkpoint)
562+
if not os.path.isfile(local_declared):
665563
print(f"Skipping the {checkpoint.name} checkpoint for {job.job_id}; it was not collected")
666564
continue
667-
uploads.append((local_auxiliary, get_auxiliary_checkpoint_path(publish_path, job.workflow, checkpoint)))
565+
uploads.append((local_declared, job.published_path(checkpoint, root=args.publish_root)))
668566
for source, destination in uploads:
669567
print(f"Publishing {source} -> {destination}")
670568
if args.dry_run:
@@ -683,24 +581,10 @@ def publish_pretrained_checkpoint(job: CheckpointJob, args: argparse.Namespace)
683581

684582
def _summary_row(job: CheckpointJob, output_dir: str) -> list[str | bool]:
685583
"""Return one CSV summary row."""
686-
has_run = has_pretrained_checkpoint_job_run(
687-
job.workflow,
688-
job.task_name,
689-
job.physics_backend,
690-
job.render_backend,
691-
)
692-
has_finished = (
693-
_has_training_job_completed(job)
694-
if job.physics_backend is not None
695-
else has_pretrained_checkpoint_job_finished(job.workflow, job.task_name)
696-
)
697-
collected_path = _get_collected_checkpoint_path(job, output_dir)
698-
review = get_pretrained_checkpoint_review(
699-
job.workflow,
700-
job.task_name,
701-
job.physics_backend,
702-
job.render_backend,
703-
)
584+
has_run = job.has_run
585+
has_finished = _has_training_job_completed(job) if job.physics_backend is not None else job.has_finished
586+
collected_path = job.collected_path(output_dir)
587+
review = job.review
704588
return [
705589
job.workflow,
706590
job.task_name,
@@ -715,20 +599,6 @@ def _summary_row(job: CheckpointJob, output_dir: str) -> list[str | bool]:
715599
]
716600

717601

718-
def _get_collected_checkpoint_path(job: CheckpointJob, output_dir: str) -> str:
719-
"""Return the absolute path of a checkpoint in the collection directory."""
720-
filename = get_pretrained_checkpoint_filename(
721-
job.workflow,
722-
job.task_name,
723-
job.physics_backend,
724-
job.render_backend,
725-
)
726-
path_parts = [output_dir, job.workflow]
727-
if job.physics_backend is None:
728-
path_parts.append(job.task_name)
729-
return os.path.abspath(os.path.join(*path_parts, filename))
730-
731-
732602
def main(argv: list[str] | None = None) -> int:
733603
"""Run checkpoint management actions."""
734604
parser = _create_parser()

0 commit comments

Comments
 (0)