Fix curriculum auto-resume stage handoff
Browse files- experiments/exp_base.py +17 -0
- tests/test_resume_checkpoint_logic.py +40 -0
experiments/exp_base.py
CHANGED
|
@@ -9,6 +9,7 @@ from abc import ABC, abstractmethod
|
|
| 9 |
from typing import Optional, Union, Literal, List, Dict
|
| 10 |
import pathlib
|
| 11 |
import os
|
|
|
|
| 12 |
from datetime import timedelta
|
| 13 |
|
| 14 |
import hydra
|
|
@@ -72,6 +73,13 @@ _DEMEMWM_FRAME_MEMORY_MARKERS = (
|
|
| 72 |
)
|
| 73 |
|
| 74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
def _short_list(items, limit=8):
|
| 76 |
items = list(items)
|
| 77 |
suffix = "" if len(items) <= limit else f", ... (+{len(items) - limit} more)"
|
|
@@ -483,9 +491,18 @@ class BaseLightningExperiment(BaseExperiment):
|
|
| 483 |
return None if self.customized_load else self.ckpt_path
|
| 484 |
|
| 485 |
def _fit_training_curriculum(self, stages, ckpt_path) -> None:
|
|
|
|
| 486 |
for stage_idx, stage in enumerate(stages):
|
| 487 |
until_step = self._curriculum_until_step(stage, stage_idx)
|
| 488 |
self._apply_curriculum_stage(stage, stage_idx, until_step)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 489 |
trainer = self._build_training_trainer(max_steps=until_step)
|
| 490 |
trainer.fit(
|
| 491 |
self.algo,
|
|
|
|
| 9 |
from typing import Optional, Union, Literal, List, Dict
|
| 10 |
import pathlib
|
| 11 |
import os
|
| 12 |
+
import re
|
| 13 |
from datetime import timedelta
|
| 14 |
|
| 15 |
import hydra
|
|
|
|
| 73 |
)
|
| 74 |
|
| 75 |
|
| 76 |
+
def _checkpoint_step_from_path(ckpt_path) -> Optional[int]:
|
| 77 |
+
if ckpt_path is None:
|
| 78 |
+
return None
|
| 79 |
+
step_match = re.search(r"step[=_-]?(\d+)", pathlib.Path(str(ckpt_path)).stem)
|
| 80 |
+
return int(step_match.group(1)) if step_match else None
|
| 81 |
+
|
| 82 |
+
|
| 83 |
def _short_list(items, limit=8):
|
| 84 |
items = list(items)
|
| 85 |
suffix = "" if len(items) <= limit else f", ... (+{len(items) - limit} more)"
|
|
|
|
| 491 |
return None if self.customized_load else self.ckpt_path
|
| 492 |
|
| 493 |
def _fit_training_curriculum(self, stages, ckpt_path) -> None:
|
| 494 |
+
resume_step = _checkpoint_step_from_path(ckpt_path) if self.auto_resuming else None
|
| 495 |
for stage_idx, stage in enumerate(stages):
|
| 496 |
until_step = self._curriculum_until_step(stage, stage_idx)
|
| 497 |
self._apply_curriculum_stage(stage, stage_idx, until_step)
|
| 498 |
+
if resume_step is not None and resume_step >= until_step:
|
| 499 |
+
if is_rank_zero:
|
| 500 |
+
print(
|
| 501 |
+
cyan("Skipping curriculum stage:"),
|
| 502 |
+
f"{stage.get('name', stage_idx + 1)} until step {until_step}; "
|
| 503 |
+
f"resume checkpoint is already at step {resume_step}",
|
| 504 |
+
)
|
| 505 |
+
continue
|
| 506 |
trainer = self._build_training_trainer(max_steps=until_step)
|
| 507 |
trainer.fit(
|
| 508 |
self.algo,
|
tests/test_resume_checkpoint_logic.py
CHANGED
|
@@ -184,6 +184,46 @@ class ResumeCheckpointLogicTests(unittest.TestCase):
|
|
| 184 |
self.assertEqual(DummyTrainer.save_checkpoint_calls, [Path("/tmp/curriculum_stage.ckpt")])
|
| 185 |
self.assertEqual(float(experiment.root_cfg.algorithm.memory_selection.pose_similarity_radius), 8.0)
|
| 186 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
def test_validation_load_checkpoint_takes_priority_over_separate_load(self):
|
| 188 |
DummyTrainer.validate_calls = []
|
| 189 |
experiment = DummyExperiment(_root_cfg(auto_resuming=False), logger=None, ckpt_path="/tmp/trained.ckpt")
|
|
|
|
| 184 |
self.assertEqual(DummyTrainer.save_checkpoint_calls, [Path("/tmp/curriculum_stage.ckpt")])
|
| 185 |
self.assertEqual(float(experiment.root_cfg.algorithm.memory_selection.pose_similarity_radius), 8.0)
|
| 186 |
|
| 187 |
+
def test_auto_resume_curriculum_skips_completed_stages(self):
|
| 188 |
+
DummyTrainer.init_calls = []
|
| 189 |
+
DummyTrainer.fit_calls = []
|
| 190 |
+
DummyTrainer.save_checkpoint_calls = []
|
| 191 |
+
cfg = _root_cfg(auto_resuming=True)
|
| 192 |
+
cfg.experiment.training.curriculum = {
|
| 193 |
+
"enabled": True,
|
| 194 |
+
"stages": [
|
| 195 |
+
{"name": "near", "until_step": 3, "dataset": {"wo_updown": True}},
|
| 196 |
+
{"name": "full", "until_step": 5, "dataset": {"wo_updown": False}},
|
| 197 |
+
],
|
| 198 |
+
}
|
| 199 |
+
experiment = DummyExperiment(cfg, logger=None, ckpt_path="/tmp/epoch1_step4.ckpt")
|
| 200 |
+
experiment.algo = object()
|
| 201 |
+
seen_dataset = []
|
| 202 |
+
|
| 203 |
+
def build_training_loader():
|
| 204 |
+
seen_dataset.append(bool(experiment.root_cfg.dataset.wo_updown))
|
| 205 |
+
return None
|
| 206 |
+
|
| 207 |
+
experiment._build_training_loader = build_training_loader
|
| 208 |
+
|
| 209 |
+
with patch.object(exp_base.pl, "Trainer", DummyTrainer), patch.object(
|
| 210 |
+
DummyExperiment,
|
| 211 |
+
"_curriculum_checkpoint_path",
|
| 212 |
+
return_value=Path("/tmp/curriculum_stage.ckpt"),
|
| 213 |
+
):
|
| 214 |
+
experiment.training()
|
| 215 |
+
|
| 216 |
+
self.assertEqual(seen_dataset, [False])
|
| 217 |
+
self.assertEqual(
|
| 218 |
+
[call["kwargs"]["ckpt_path"] for call in DummyTrainer.fit_calls],
|
| 219 |
+
["/tmp/epoch1_step4.ckpt"],
|
| 220 |
+
)
|
| 221 |
+
self.assertEqual(
|
| 222 |
+
[call["kwargs"]["max_steps"] for call in DummyTrainer.init_calls],
|
| 223 |
+
[5],
|
| 224 |
+
)
|
| 225 |
+
self.assertEqual(DummyTrainer.save_checkpoint_calls, [])
|
| 226 |
+
|
| 227 |
def test_validation_load_checkpoint_takes_priority_over_separate_load(self):
|
| 228 |
DummyTrainer.validate_calls = []
|
| 229 |
experiment = DummyExperiment(_root_cfg(auto_resuming=False), logger=None, ckpt_path="/tmp/trained.ckpt")
|