Skip to content

Commit 4bdfa00

Browse files
authored
feat(adapter/nemo): add keep_mlf_checkpoint_on_train_end flag (#103)
This change will add a `keep_mlf_checkpoint_on_train_end` flag to MLFlashpointCheckpointCallback. It defaults to False, but when set to True, it skips the deletion of the final checkpoint at the end of training. This ensures the last checkpoint can be preserved for use in E2E testing.
1 parent cf7264e commit 4bdfa00

2 files changed

Lines changed: 66 additions & 2 deletions

File tree

src/ml_flashpoint/adapter/nemo/checkpoint_callback.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def __init__(
5555
every_n_steps: int,
5656
skip_every_n_steps: Optional[int] = None,
5757
enabled: bool = True,
58+
keep_mlf_checkpoint_on_train_end: bool = False,
5859
):
5960
"""
6061
Initializes and validates the callback.
@@ -68,12 +69,15 @@ def __init__(
6869
skip_every_n_steps (int, optional): The step frequency to skip checkpointing. This is suggested to be set to
6970
the interval used for long-term checkpointing by the alternative strategy. Defaults to 0 (no skipping).
7071
enabled (bool): Whether this callback should be enabled. Defaults to True.
72+
keep_mlf_checkpoint_on_train_end (bool): Whether to keep the ML Flashpoint checkpoint after training ends.
73+
Defaults to False.
7174
"""
7275
self.base_container = CheckpointContainerId(checkpoint_base_container)
7376
self.every_n_steps = every_n_steps
7477
self.skip_every_n_steps = skip_every_n_steps if skip_every_n_steps is not None else 0
7578
self._enabled = enabled
7679
self._replication_manager = None
80+
self._keep_mlf_checkpoint_on_train_end = keep_mlf_checkpoint_on_train_end
7781
self._validate()
7882

7983
@property
@@ -184,5 +188,11 @@ def on_train_end(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -
184188
self.replication_manager.shutdown()
185189

186190
if trainer.local_rank == 0:
187-
_LOGGER.info("Local rank 0: Performing final checkpoint cleanup...")
188-
trainer.strategy.checkpoint_io.remove_checkpoint(self.base_container.data)
191+
if not self._keep_mlf_checkpoint_on_train_end:
192+
_LOGGER.info("Local rank 0: Performing final checkpoint cleanup...")
193+
trainer.strategy.checkpoint_io.remove_checkpoint(self.base_container.data)
194+
else:
195+
_LOGGER.info(
196+
"Local rank 0: Skipping final checkpoint cleanup because keep_mlf_checkpoint_on_train_end=%s.",
197+
self._keep_mlf_checkpoint_on_train_end,
198+
)

tests/adapter/nemo/test_checkpoint_callback.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,3 +497,57 @@ def test_on_train_end_is_idempotent(mocker, tmp_path):
497497

498498
# Verify file deletion
499499
assert not base_container_path.exists(), "Base container directory should have been deleted"
500+
501+
502+
def test_on_train_end_skips_cleanup_when_flag_is_true(mocker, tmp_path):
503+
"""
504+
Tests that the final checkpoint cleanup is skipped when
505+
keep_mlf_checkpoint_on_train_end is set to True.
506+
507+
This ensures that for E2E tests or specific debugging scenarios,
508+
the last ML Flashpoint checkpoint remains on disk after training ends.
509+
"""
510+
511+
# Given
512+
trainer = mocker.MagicMock(spec=pl.Trainer)
513+
trainer.local_rank = 0
514+
chkpt_obj_manager = CheckpointObjectManager()
515+
516+
checkpoint_io = MLFlashpointCheckpointIO(
517+
flashpoint_base_path=str(tmp_path / "ckpt_base"),
518+
alt_checkpoint_io=mocker.MagicMock(),
519+
chkpt_obj_manager=chkpt_obj_manager,
520+
save_strategy=mocker.MagicMock(),
521+
load_strategy=mocker.MagicMock(),
522+
trainer=trainer,
523+
)
524+
checkpoint_io.maybe_finalize_save_checkpoint = mocker.MagicMock()
525+
mocker.spy(checkpoint_io, "remove_checkpoint")
526+
trainer.strategy.checkpoint_io = checkpoint_io
527+
528+
pl_module = mocker.MagicMock(spec=pl.LightningModule)
529+
530+
# Create a base container directory and a dummy file inside it
531+
base_container_path = tmp_path / "ckpt_base"
532+
base_container_path.mkdir()
533+
dummy_file = base_container_path / "dummy.txt"
534+
dummy_file.write_text("dummy")
535+
536+
base_container = CheckpointContainerId(str(base_container_path))
537+
callback = MLFlashpointCheckpointCallback(
538+
checkpoint_base_container=base_container, every_n_steps=1, keep_mlf_checkpoint_on_train_end=True
539+
)
540+
callback.replication_manager = mocker.MagicMock()
541+
542+
# When
543+
callback.on_train_end(trainer, pl_module)
544+
545+
# Then
546+
checkpoint_io.maybe_finalize_save_checkpoint.assert_called_once_with(blocking=True)
547+
trainer.strategy.barrier.assert_called_once_with("mlf_cleanup_barrier")
548+
callback.replication_manager.shutdown.assert_called_once()
549+
550+
checkpoint_io.remove_checkpoint.assert_not_called()
551+
552+
assert base_container_path.exists(), "Base container directory should NOT have been deleted"
553+
assert dummy_file.exists(), "Dummy file should NOT have been deleted"

0 commit comments

Comments
 (0)