Skip to content

Commit 7957526

Browse files
Merge pull request #4326 from AI-Hypercomputer:sujinesh/avoid-skipped-checkpoint-work
PiperOrigin-RevId: 944756879
2 parents 4145a59 + 1cf2be5 commit 7957526

2 files changed

Lines changed: 156 additions & 33 deletions

File tree

src/maxtext/common/checkpointing.py

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,6 +1010,32 @@ def load_checkpoint_metadata(checkpoint_dir_path: str) -> dict[str, Any]:
10101010
return {}
10111011

10121012

1013+
def _uses_local_checkpoint_period(config):
1014+
return config.enable_emergency_checkpoint or config.enable_multi_tier_checkpointing
1015+
1016+
1017+
def _should_save_checkpoint_at_step(checkpoint_manager, step, config, force):
1018+
"""Returns whether MaxText should build and dispatch checkpoint args."""
1019+
if force:
1020+
return True
1021+
if config.enable_continuous_checkpointing:
1022+
base_checkpoint_due = bool(checkpoint_manager.should_save(step))
1023+
else:
1024+
base_checkpoint_due = step % config.checkpoint_period == 0
1025+
local_checkpoint_due = _uses_local_checkpoint_period(config) and step % config.local_checkpoint_period == 0
1026+
autocheckpoint_due = config.enable_autocheckpoint and checkpoint_manager.reached_preemption(step)
1027+
return base_checkpoint_due or local_checkpoint_due or autocheckpoint_due
1028+
1029+
1030+
def _handle_post_checkpoint_preemption(checkpoint_manager, step, force_ckpt_save):
1031+
"""Waits on final/preemption saves and raises if preempted."""
1032+
reached_preemption = checkpoint_manager.reached_preemption(step)
1033+
if force_ckpt_save or reached_preemption:
1034+
checkpoint_manager.wait_until_finished()
1035+
if reached_preemption:
1036+
raise exceptions.StopTraining("Job is preempted.")
1037+
1038+
10131039
def maybe_save_checkpoint(checkpoint_manager, state, config, data_iterator, step=None):
10141040
"""Save checkpoint if checkpointing is enabled."""
10151041
if checkpoint_manager is None:
@@ -1028,6 +1054,18 @@ def maybe_save_checkpoint(checkpoint_manager, state, config, data_iterator, step
10281054
# Linen TrainState has .step attribute
10291055
actual_step = int(state.step) - 1
10301056

1057+
# Determine if a checkpoint save should be forced, overriding the usual
1058+
# `config.checkpoint_period` logic.
1059+
# This occurs if this function was called:
1060+
# without an explicit 'step' (implying it's a checkpoint save for final step),
1061+
# AND the 'actual_step' is a valid step,
1062+
# AND it's not a step that would normally trigger a checkpoint save.
1063+
force_ckpt_save = step is None and actual_step != -1 and (actual_step % config.checkpoint_period != 0)
1064+
1065+
if not _should_save_checkpoint_at_step(checkpoint_manager, actual_step, config, force_ckpt_save):
1066+
_handle_post_checkpoint_preemption(checkpoint_manager, actual_step, force_ckpt_save)
1067+
return
1068+
10311069
if checkpoint_manager.latest_step() == actual_step:
10321070
max_logging.log(f"Checkpoint for step {actual_step} already exists, skipping save.")
10331071
return
@@ -1042,13 +1080,6 @@ def maybe_save_checkpoint(checkpoint_manager, state, config, data_iterator, step
10421080
else:
10431081
state = train_state_nnx.to_linen_checkpoint_dict(state.to_pure_dict())
10441082

1045-
# Determine if a checkpoint save should be forced, overriding the usual `config.checkpoint_period` logic.
1046-
# This occurs if this function was called:
1047-
# without an explicit 'step' (implying it's a checkpoint save for final step),
1048-
# AND the 'actual_step' is a valid step,
1049-
# AND it's not a step that would normally trigger a checkpoint save.
1050-
force_ckpt_save = step is None and actual_step != -1 and (actual_step % config.checkpoint_period != 0)
1051-
10521083
try:
10531084
checkpoint_saved = save_checkpoint(checkpoint_manager, actual_step, state, config, data_iterator, force_ckpt_save)
10541085
if checkpoint_saved:
@@ -1070,13 +1101,9 @@ def maybe_save_checkpoint(checkpoint_manager, state, config, data_iterator, step
10701101
except Exception as e:
10711102
raise exceptions.StopTraining(f"Checkpointing failed. {str(e)}") from e
10721103

1073-
# Wait for any pending checkpoint save to finish during preemption or final step save
1074-
if force_ckpt_save or checkpoint_manager.reached_preemption(actual_step):
1075-
checkpoint_manager.wait_until_finished()
1076-
1077-
# Raise exception upon preemption
1078-
if checkpoint_manager.reached_preemption(actual_step):
1079-
raise exceptions.StopTraining("Job is preempted.")
1104+
# Wait for any pending checkpoint save to finish during preemption or final
1105+
# step save, then raise upon preemption.
1106+
_handle_post_checkpoint_preemption(checkpoint_manager, actual_step, force_ckpt_save)
10801107

10811108

10821109
def save_checkpoint(checkpoint_manager, step, state, config=None, data_iterator=None, force=False):
@@ -1085,7 +1112,7 @@ def save_checkpoint(checkpoint_manager, step, state, config=None, data_iterator=
10851112
if (
10861113
force
10871114
or (step % config.checkpoint_period == 0 and not config.enable_continuous_checkpointing)
1088-
or (config.enable_emergency_checkpoint and step % config.local_checkpoint_period == 0)
1115+
or (_uses_local_checkpoint_period(config) and step % config.local_checkpoint_period == 0)
10891116
or (config.enable_autocheckpoint and checkpoint_manager.reached_preemption(step))
10901117
):
10911118
blocking_until_ready_start = time.time()

tests/unit/train_state_nnx_checkpoint_test.py

Lines changed: 114 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,22 @@ class TestMaybeSaveCheckpointStepAlignment(unittest.TestCase):
351351
def setUp(self):
352352
self.tx = optax.adam(1e-3)
353353

354+
def _config(self, **overrides):
355+
"""Builds a minimal checkpoint config for maybe_save_checkpoint tests."""
356+
values = {
357+
"pure_nnx": True,
358+
"checkpoint_period": 10,
359+
"async_checkpointing": False,
360+
"enable_diloco": False,
361+
"enable_continuous_checkpointing": False,
362+
"enable_emergency_checkpoint": False,
363+
"enable_multi_tier_checkpointing": False,
364+
"local_checkpoint_period": 0,
365+
"enable_autocheckpoint": False,
366+
}
367+
values.update(overrides)
368+
return SimpleNamespace(**values)
369+
354370
def _build_nnx_state(self, num_steps):
355371
"""Build an nnx.State flattened from TrainStateNNX after num_steps gradient applications."""
356372
model = MockModel(rngs=nnx.Rngs(0))
@@ -380,12 +396,7 @@ def _build_linen_state(self, num_steps):
380396
def _invoke_maybe_save(self, state, pure_nnx):
381397
"""Call maybe_save_checkpoint with save_checkpoint patched, return {step, state} captured."""
382398
# checkpoint_period=1 keeps force_ckpt_save False regardless of actual_step.
383-
config = SimpleNamespace(
384-
pure_nnx=pure_nnx,
385-
checkpoint_period=1,
386-
async_checkpointing=False,
387-
enable_diloco=False,
388-
)
399+
config = self._config(pure_nnx=pure_nnx, checkpoint_period=1)
389400
mgr = mock.MagicMock()
390401
mgr.reached_preemption.return_value = False
391402

@@ -451,12 +462,7 @@ def test_maybe_save_checkpoint_skips_if_already_saved(self):
451462
state = self._build_nnx_state(self.N_STEPS)
452463
actual_step = self.N_STEPS - 1
453464

454-
config = SimpleNamespace(
455-
pure_nnx=True,
456-
checkpoint_period=1,
457-
async_checkpointing=False,
458-
enable_diloco=False,
459-
)
465+
config = self._config(checkpoint_period=1)
460466
mgr = mock.MagicMock()
461467
mgr.reached_preemption.return_value = False
462468
# Mock latest_step to return the same actual_step
@@ -475,12 +481,7 @@ def test_maybe_save_checkpoint_saves_if_not_already_saved(self):
475481
state = self._build_nnx_state(self.N_STEPS)
476482
actual_step = self.N_STEPS - 1
477483

478-
config = SimpleNamespace(
479-
pure_nnx=True,
480-
checkpoint_period=1,
481-
async_checkpointing=False,
482-
enable_diloco=False,
483-
)
484+
config = self._config(checkpoint_period=1)
484485
mgr = mock.MagicMock()
485486
mgr.reached_preemption.return_value = False
486487
# Mock latest_step to return a different step (or None)
@@ -495,6 +496,101 @@ def test_maybe_save_checkpoint_saves_if_not_already_saved(self):
495496
# Assert that save_checkpoint WAS called!
496497
save_checkpoint_mock.assert_called_once()
497498

499+
def test_maybe_save_checkpoint_skips_non_checkpoint_step_before_state_work(
500+
self,
501+
):
502+
"""Non-checkpoint steps should not query latest_step or build save args."""
503+
state = mock.Mock()
504+
config = self._config()
505+
mgr = mock.MagicMock()
506+
mgr.reached_preemption.return_value = False
507+
508+
with mock.patch.object(checkpointing, "save_checkpoint") as save_checkpoint_mock:
509+
checkpointing.maybe_save_checkpoint(mgr, state, config, data_iterator=None, step=3)
510+
511+
mgr.latest_step.assert_not_called()
512+
mgr.reached_preemption.assert_called_once_with(3)
513+
mgr.wait_until_finished.assert_not_called()
514+
state.to_pure_dict.assert_not_called()
515+
save_checkpoint_mock.assert_not_called()
516+
517+
def test_maybe_save_checkpoint_handles_preemption_on_non_checkpoint_step(
518+
self,
519+
):
520+
"""Non-checkpoint steps must still honor preemption handling."""
521+
state = mock.Mock()
522+
config = self._config()
523+
mgr = mock.MagicMock()
524+
mgr.reached_preemption.return_value = True
525+
526+
with mock.patch.object(checkpointing, "save_checkpoint") as save_checkpoint_mock:
527+
with self.assertRaises(checkpointing.exceptions.StopTraining):
528+
checkpointing.maybe_save_checkpoint(mgr, state, config, data_iterator=None, step=3)
529+
530+
mgr.latest_step.assert_not_called()
531+
mgr.reached_preemption.assert_called_once_with(3)
532+
mgr.wait_until_finished.assert_called_once_with()
533+
state.to_pure_dict.assert_not_called()
534+
save_checkpoint_mock.assert_not_called()
535+
536+
def test_maybe_save_checkpoint_allows_local_checkpoint_period(self):
537+
"""Emergency and multi-tier local checkpoint periods dispatch save work."""
538+
for checkpoint_flag in (
539+
"enable_emergency_checkpoint",
540+
"enable_multi_tier_checkpointing",
541+
):
542+
with self.subTest(checkpoint_flag=checkpoint_flag):
543+
state = mock.Mock()
544+
state.to_pure_dict.return_value = {
545+
"model": {},
546+
"optimizer": {"step": 5},
547+
}
548+
config = self._config(
549+
checkpoint_period=100,
550+
local_checkpoint_period=5,
551+
**{checkpoint_flag: True},
552+
)
553+
mgr = mock.MagicMock()
554+
mgr.latest_step.return_value = None
555+
mgr.reached_preemption.return_value = False
556+
save_checkpoint_mock = mock.MagicMock(return_value=False)
557+
558+
with mock.patch.object(checkpointing, "save_checkpoint", save_checkpoint_mock):
559+
checkpointing.maybe_save_checkpoint(mgr, state, config, data_iterator=None, step=5)
560+
561+
mgr.latest_step.assert_called_once_with()
562+
mgr.reached_preemption.assert_called_once_with(5)
563+
mgr.wait_until_finished.assert_not_called()
564+
state.to_pure_dict.assert_called_once_with()
565+
save_checkpoint_mock.assert_called_once()
566+
567+
def test_maybe_save_checkpoint_allows_mtc_period_with_continuous_policy(
568+
self,
569+
):
570+
"""Continuous checkpointing should not suppress MTC local saves."""
571+
state = mock.Mock()
572+
state.to_pure_dict.return_value = {"model": {}, "optimizer": {"step": 5}}
573+
config = self._config(
574+
checkpoint_period=100,
575+
enable_continuous_checkpointing=True,
576+
enable_multi_tier_checkpointing=True,
577+
local_checkpoint_period=5,
578+
)
579+
mgr = mock.MagicMock()
580+
mgr.should_save.return_value = False
581+
mgr.latest_step.return_value = None
582+
mgr.reached_preemption.return_value = False
583+
save_checkpoint_mock = mock.MagicMock(return_value=False)
584+
585+
with mock.patch.object(checkpointing, "save_checkpoint", save_checkpoint_mock):
586+
checkpointing.maybe_save_checkpoint(mgr, state, config, data_iterator=None, step=5)
587+
588+
mgr.should_save.assert_called_once_with(5)
589+
mgr.latest_step.assert_called_once_with()
590+
mgr.reached_preemption.assert_called_once_with(5)
591+
state.to_pure_dict.assert_called_once_with()
592+
save_checkpoint_mock.assert_called_once()
593+
498594

499595
class TestLinenCheckpointFormatConverters(unittest.TestCase):
500596
"""to_linen_checkpoint_dict / from_linen_checkpoint_dict (NNX <-> Linen on-disk layout)."""

0 commit comments

Comments
 (0)