Skip to content

Commit 1cf2be5

Browse files
committed
Avoid checkpoint work on skipped steps
1 parent ecf666f commit 1cf2be5

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
@@ -1082,6 +1082,32 @@ def load_checkpoint_metadata(checkpoint_dir_path: str) -> dict[str, Any]:
10821082
return {}
10831083

10841084

1085+
def _uses_local_checkpoint_period(config):
1086+
return config.enable_emergency_checkpoint or config.enable_multi_tier_checkpointing
1087+
1088+
1089+
def _should_save_checkpoint_at_step(checkpoint_manager, step, config, force):
1090+
"""Returns whether MaxText should build and dispatch checkpoint args."""
1091+
if force:
1092+
return True
1093+
if config.enable_continuous_checkpointing:
1094+
base_checkpoint_due = bool(checkpoint_manager.should_save(step))
1095+
else:
1096+
base_checkpoint_due = step % config.checkpoint_period == 0
1097+
local_checkpoint_due = _uses_local_checkpoint_period(config) and step % config.local_checkpoint_period == 0
1098+
autocheckpoint_due = config.enable_autocheckpoint and checkpoint_manager.reached_preemption(step)
1099+
return base_checkpoint_due or local_checkpoint_due or autocheckpoint_due
1100+
1101+
1102+
def _handle_post_checkpoint_preemption(checkpoint_manager, step, force_ckpt_save):
1103+
"""Waits on final/preemption saves and raises if preempted."""
1104+
reached_preemption = checkpoint_manager.reached_preemption(step)
1105+
if force_ckpt_save or reached_preemption:
1106+
checkpoint_manager.wait_until_finished()
1107+
if reached_preemption:
1108+
raise exceptions.StopTraining("Job is preempted.")
1109+
1110+
10851111
def maybe_save_checkpoint(checkpoint_manager, state, config, data_iterator, step=None):
10861112
"""Save checkpoint if checkpointing is enabled."""
10871113
if checkpoint_manager is None:
@@ -1100,6 +1126,18 @@ def maybe_save_checkpoint(checkpoint_manager, state, config, data_iterator, step
11001126
# Linen TrainState has .step attribute
11011127
actual_step = int(state.step) - 1
11021128

1129+
# Determine if a checkpoint save should be forced, overriding the usual
1130+
# `config.checkpoint_period` logic.
1131+
# This occurs if this function was called:
1132+
# without an explicit 'step' (implying it's a checkpoint save for final step),
1133+
# AND the 'actual_step' is a valid step,
1134+
# AND it's not a step that would normally trigger a checkpoint save.
1135+
force_ckpt_save = step is None and actual_step != -1 and (actual_step % config.checkpoint_period != 0)
1136+
1137+
if not _should_save_checkpoint_at_step(checkpoint_manager, actual_step, config, force_ckpt_save):
1138+
_handle_post_checkpoint_preemption(checkpoint_manager, actual_step, force_ckpt_save)
1139+
return
1140+
11031141
if checkpoint_manager.latest_step() == actual_step:
11041142
max_logging.log(f"Checkpoint for step {actual_step} already exists, skipping save.")
11051143
return
@@ -1114,13 +1152,6 @@ def maybe_save_checkpoint(checkpoint_manager, state, config, data_iterator, step
11141152
else:
11151153
state = train_state_nnx.to_linen_checkpoint_dict(state.to_pure_dict())
11161154

1117-
# Determine if a checkpoint save should be forced, overriding the usual `config.checkpoint_period` logic.
1118-
# This occurs if this function was called:
1119-
# without an explicit 'step' (implying it's a checkpoint save for final step),
1120-
# AND the 'actual_step' is a valid step,
1121-
# AND it's not a step that would normally trigger a checkpoint save.
1122-
force_ckpt_save = step is None and actual_step != -1 and (actual_step % config.checkpoint_period != 0)
1123-
11241155
try:
11251156
checkpoint_saved = save_checkpoint(checkpoint_manager, actual_step, state, config, data_iterator, force_ckpt_save)
11261157
if checkpoint_saved:
@@ -1142,13 +1173,9 @@ def maybe_save_checkpoint(checkpoint_manager, state, config, data_iterator, step
11421173
except Exception as e:
11431174
raise exceptions.StopTraining(f"Checkpointing failed. {str(e)}") from e
11441175

1145-
# Wait for any pending checkpoint save to finish during preemption or final step save
1146-
if force_ckpt_save or checkpoint_manager.reached_preemption(actual_step):
1147-
checkpoint_manager.wait_until_finished()
1148-
1149-
# Raise exception upon preemption
1150-
if checkpoint_manager.reached_preemption(actual_step):
1151-
raise exceptions.StopTraining("Job is preempted.")
1176+
# Wait for any pending checkpoint save to finish during preemption or final
1177+
# step save, then raise upon preemption.
1178+
_handle_post_checkpoint_preemption(checkpoint_manager, actual_step, force_ckpt_save)
11521179

11531180

11541181
def save_checkpoint(checkpoint_manager, step, state, config=None, data_iterator=None, force=False):
@@ -1157,7 +1184,7 @@ def save_checkpoint(checkpoint_manager, step, state, config=None, data_iterator=
11571184
if (
11581185
force
11591186
or (step % config.checkpoint_period == 0 and not config.enable_continuous_checkpointing)
1160-
or (config.enable_emergency_checkpoint and step % config.local_checkpoint_period == 0)
1187+
or (_uses_local_checkpoint_period(config) and step % config.local_checkpoint_period == 0)
11611188
or (config.enable_autocheckpoint and checkpoint_manager.reached_preemption(step))
11621189
):
11631190
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)