|
| 1 | +# Copyright 2026 Google LLC |
| 2 | +# |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +# you may not use this file except in compliance with the License. |
| 5 | +# You may obtain a copy of the License at |
| 6 | +# |
| 7 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +# |
| 9 | +# Unless required by applicable law or agreed to in writing, software |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +# See the License for the specific language governing permissions and |
| 13 | +# limitations under the License. |
| 14 | + |
| 15 | +"""Checkpointing utilities for MaxText training engine.""" |
| 16 | + |
| 17 | +from typing import Any |
| 18 | +from absl import logging |
| 19 | +from flax import nnx |
| 20 | +import jax |
| 21 | +from maxtext.configs import pyconfig |
| 22 | +import orbax.checkpoint as ocp |
| 23 | + |
| 24 | + |
| 25 | +class CheckpointManager: |
| 26 | + """CheckpointManager wrapper for MaxText training engine.""" |
| 27 | + |
| 28 | + def __init__( |
| 29 | + self, |
| 30 | + checkpoint_dir: str, |
| 31 | + config: pyconfig.HyperParameters, |
| 32 | + ) -> None: |
| 33 | + """Initializes the CheckpointManager. |
| 34 | +
|
| 35 | + Args: |
| 36 | + checkpoint_dir: The root directory for saving checkpoints. |
| 37 | + config: The training configuration. |
| 38 | + """ |
| 39 | + self._checkpoint_manager: ocp.CheckpointManager | None = None |
| 40 | + if checkpoint_dir: |
| 41 | + self._checkpoint_manager = ocp.CheckpointManager( |
| 42 | + directory=checkpoint_dir, |
| 43 | + options=ocp.CheckpointManagerOptions( |
| 44 | + save_interval_steps=getattr(config, "checkpoint_period", 1), |
| 45 | + max_to_keep=getattr(config, "max_num_checkpoints_to_keep", None), |
| 46 | + enable_async_checkpointing=getattr( |
| 47 | + config, "async_checkpointing", True |
| 48 | + ), |
| 49 | + ), |
| 50 | + ) |
| 51 | + |
| 52 | + def get_latest_step(self) -> int | None: |
| 53 | + """Returns the latest checkpoint step.""" |
| 54 | + if self._checkpoint_manager: |
| 55 | + return self._checkpoint_manager.latest_step() |
| 56 | + return None |
| 57 | + |
| 58 | + def save_checkpoint( |
| 59 | + self, |
| 60 | + step: int, |
| 61 | + model: nnx.Module, |
| 62 | + optimizer: nnx.optimizer.Optimizer | None, |
| 63 | + custom_metadata: Any, |
| 64 | + ) -> bool: |
| 65 | + """Saves the params for the given step. |
| 66 | +
|
| 67 | + Args: |
| 68 | + step: The step to save the params for. |
| 69 | + model: The model to save. |
| 70 | + optimizer: The optimizer to save. |
| 71 | + custom_metadata: Custom metadata to save with the checkpoint. |
| 72 | +
|
| 73 | + Returns: |
| 74 | + Whether the checkpoint was saved. |
| 75 | + """ |
| 76 | + if self._checkpoint_manager is None: |
| 77 | + logging.info("Checkpointing is disabled, skipping save.") |
| 78 | + return False |
| 79 | + |
| 80 | + # Check if the checkpoint already exists at the current step. |
| 81 | + if self.get_latest_step() == step: |
| 82 | + logging.info( |
| 83 | + "Checkpoint already saved at step %d, skipping save.", |
| 84 | + step, |
| 85 | + ) |
| 86 | + return False |
| 87 | + |
| 88 | + params = nnx.state(model) |
| 89 | + jax.block_until_ready(params) |
| 90 | + model_cp_args = ocp.args.PyTreeSave( |
| 91 | + item=params, |
| 92 | + save_args=jax.tree.map(lambda _: ocp.SaveArgs(), params), |
| 93 | + ) |
| 94 | + save_args = {"model_params": model_cp_args} |
| 95 | + if optimizer is not None: |
| 96 | + optimizer_state = nnx.state(optimizer, nnx.optimizer.OptState) |
| 97 | + jax.block_until_ready(optimizer_state) |
| 98 | + optimizer_cp_args = ocp.args.PyTreeSave( |
| 99 | + item=optimizer_state, |
| 100 | + save_args=jax.tree.map(lambda _: ocp.SaveArgs(), optimizer_state), |
| 101 | + ) |
| 102 | + save_args["optimizer_state"] = optimizer_cp_args |
| 103 | + |
| 104 | + return self._checkpoint_manager.save( |
| 105 | + step=step, |
| 106 | + args=ocp.args.Composite(**save_args), |
| 107 | + custom_metadata=custom_metadata, |
| 108 | + ) |
| 109 | + |
| 110 | + def restore_checkpoint( |
| 111 | + self, |
| 112 | + model: nnx.Module, |
| 113 | + optimizer: nnx.optimizer.Optimizer | None, |
| 114 | + step: int | None = None, |
| 115 | + ) -> Any: |
| 116 | + """Restores items from the checkpoint at the given step. |
| 117 | +
|
| 118 | + Args: |
| 119 | + model: The model to restore the params to. |
| 120 | + optimizer: The optimizer to restore the state to. |
| 121 | + step: Optional step index to restore from. |
| 122 | +
|
| 123 | + Returns: |
| 124 | + The metadata of the restored checkpoint, or None if no checkpoint was |
| 125 | + restored. |
| 126 | + """ |
| 127 | + if self._checkpoint_manager is None: |
| 128 | + logging.info("Checkpointing is disabled, skipping restore.") |
| 129 | + return None, {} |
| 130 | + |
| 131 | + if step is None: |
| 132 | + step = self.get_latest_step() |
| 133 | + if step is None: |
| 134 | + logging.info("No checkpoint found, skipping restore.") |
| 135 | + return None, {} |
| 136 | + |
| 137 | + metadata = self._checkpoint_manager.metadata(step) |
| 138 | + |
| 139 | + abstract_params = nnx.state(model) |
| 140 | + model_args = ocp.args.PyTreeRestore( |
| 141 | + item=abstract_params, |
| 142 | + restore_args=ocp.checkpoint_utils.construct_restore_args( |
| 143 | + target=abstract_params |
| 144 | + ), |
| 145 | + ) |
| 146 | + if optimizer is not None and "optimizer_state" in metadata.item_metadata: |
| 147 | + optimizer_state = nnx.state(optimizer, nnx.optimizer.OptState) |
| 148 | + optimizer_args = ocp.args.PyTreeRestore( |
| 149 | + item=optimizer_state, |
| 150 | + restore_args=ocp.checkpoint_utils.construct_restore_args( |
| 151 | + target=optimizer_state |
| 152 | + ), |
| 153 | + ) |
| 154 | + restore_args = { |
| 155 | + "model_params": model_args, |
| 156 | + "optimizer_state": optimizer_args, |
| 157 | + } |
| 158 | + else: |
| 159 | + restore_args = {"model_params": model_args} |
| 160 | + |
| 161 | + restored_items = self._checkpoint_manager.restore( |
| 162 | + step=step, |
| 163 | + args=ocp.args.Composite(**restore_args), |
| 164 | + ) |
| 165 | + if "model_params" in restored_items: |
| 166 | + nnx.update(model, restored_items["model_params"]) |
| 167 | + if optimizer is not None and "optimizer_state" in restored_items: |
| 168 | + nnx.update(optimizer, restored_items["optimizer_state"]) |
| 169 | + |
| 170 | + return step, metadata |
| 171 | + |
| 172 | + def close(self) -> None: |
| 173 | + """Closes the checkpoint manager.""" |
| 174 | + if self._checkpoint_manager: |
| 175 | + self._checkpoint_manager.close() |
0 commit comments