1515
1616"""Create an Orbax CheckpointManager with specified (Async or not) Checkpointer."""
1717
18- import asyncio
1918import time
20- from typing import Any , Optional
19+ from typing import Any
2120
2221from absl import flags
2322import datetime
2928from maxtext .input_pipeline .multihost_dataloading import MultiHostDataLoadIterator
3029from maxtext .input_pipeline .multihost_dataloading import RemoteIteratorWrapper
3130from maxtext .input_pipeline .synthetic_data_processing import PlaceHolderDataIterator
31+ from maxtext .common import grain_utility
3232from maxtext .common import train_state_nnx
3333from maxtext .utils import exceptions
3434from maxtext .utils import max_logging
3535from maxtext .utils import gcs_utils
3636from maxtext .utils import elastic_utils
3737from maxtext .checkpoint_conversion .utils .load_dynamic import load_safetensors_dynamic_state
3838
39- import numpy as np
4039import orbax .checkpoint as ocp
4140from orbax .checkpoint import v1 as ocp_v1
4241from orbax .checkpoint ._src .arrays import sharding as sharding_utils
4544import orbax .checkpoint .experimental .emergency .checkpoint_manager as emergency_checkpoint_manager
4645import orbax .checkpoint .experimental .emergency .replicator_checkpoint_manager as emergency_replicator_checkpoint_manager
4746# pylint: disable=too-many-positional-arguments
48- import dataclasses
49- import json
5047
51- import grain
52- from grain .python import PyGrainCheckpointHandler
5348from grain .experimental import ElasticIterator
5449
5550CheckpointManager = ocp .CheckpointManager
6257EmergencyReplicatorCheckpointManager = emergency_replicator_checkpoint_manager .ReplicatorCheckpointManager
6358
6459
65- class GrainCheckpointHandler (PyGrainCheckpointHandler , ocp .CheckpointHandler ):
66- """A CheckpointHandler that allows specifying process_index and process_count."""
67-
68- def save (
69- self ,
70- directory : epath .Path ,
71- # `item` is for backwards compatibility with older Orbax API, see
72- # https://orbax.readthedocs.io/en/latest/guides/checkpoint/api_refactor.html.
73- item : Optional [Any ] = None ,
74- args : Any = None ,
75- ):
76- """Saves the given iterator to the checkpoint in `directory`."""
77- item = item or args .item # pytype:disable=attribute-error
78-
79- # RemoteIteratorWrapper handles checkpointing via colocated python
80- if isinstance (item , RemoteIteratorWrapper ):
81- step = int (directory .parent .name )
82- item .save_state (step )
83- return
84-
85- # ElasticIterator state is a single global scalar shared by all shards,
86- # so we write one fixed `process_0.json` from process 0 only. This file
87- # layout survives changes in `jax.process_count()`.
88- if isinstance (item , ElasticIterator ):
89- if jax .process_index () == 0 :
90- directory .mkdir (parents = True , exist_ok = True )
91- filename = directory / "process_0.json"
92- filename .write_text (json .dumps (item .get_state (), indent = 4 ))
93- return
94-
95- def save_single_process (item , process_index , process_count ):
96- filename = directory / f"process_{ process_index } -of-{ process_count } .json"
97- if isinstance (item , grain .DatasetIterator ):
98- state = json .dumps (item .get_state (), indent = 4 )
99- else :
100- state = item .get_state ().decode ()
101- filename .write_text (state )
102-
103- if isinstance (item , list ):
104- for local_iterator , process_index , process_count in item :
105- save_single_process (local_iterator , process_index , process_count )
106- else :
107- process_index , process_count = jax .process_index (), jax .process_count ()
108- save_single_process (item , process_index , process_count )
109-
110- def restore (
111- self ,
112- directory : epath .Path ,
113- item : Optional [Any ] = None ,
114- args : Any = None ,
115- ) -> Any :
116- """Restores the given iterator from the checkpoint in `directory`."""
117- item = item or args .item
118- process_index = getattr (args , "process_index" , None )
119- process_count = getattr (args , "process_count" , None )
120-
121- # In Pathways + colocated_python environment, RemoteIteratorWrapper handles checkpointing
122- if isinstance (item , RemoteIteratorWrapper ):
123- step = int (directory .parent .name )
124- item .restore_state (step )
125- return item
126-
127- # McJax and Pathways through controller cases
128- # ElasticIterator: every process reads the same shared `process_0.json`.
129- if isinstance (item , ElasticIterator ):
130- filename = directory / "process_0.json"
131- if not filename .exists ():
132- raise ValueError (f"File { filename } does not exist." )
133- item .set_state (json .loads (filename .read_text ()))
134- return item
135-
136- def restore_single_process (item , process_index , process_count ):
137- filename = directory / f"process_{ process_index } -of-{ process_count } .json"
138- if not filename .exists ():
139- raise ValueError (f"File { filename } does not exist." )
140- state = filename .read_text ()
141- if isinstance (item , grain .DatasetIterator ):
142- state = json .loads (state )
143- else :
144- state = state .encode ()
145- item .set_state (state )
146- return item
147-
148- if isinstance (item , list ):
149- restored_items = []
150- for data_iter , process_idx in zip (item , process_index ): # pyrefly: ignore[bad-argument-type]
151- restored_items .append (restore_single_process (data_iter , process_idx , process_count ))
152- return restored_items
153- else :
154- if process_index is None or process_count is None :
155- process_index , process_count = jax .process_index (), jax .process_count ()
156- return restore_single_process (item , process_index , process_count )
157-
158-
159- @ocp .args .register_with_handler (GrainCheckpointHandler , for_save = True )
160- @dataclasses .dataclass
161- class GrainCheckpointSave (ocp .args .CheckpointArgs ):
162- item : Any
163-
164-
165- @ocp .args .register_with_handler (GrainCheckpointHandler , for_restore = True )
166- @dataclasses .dataclass
167- class GrainCheckpointRestore (ocp .args .CheckpointArgs ):
168- item : Any
169- process_index : Optional [int | list [int ]] = None
170- process_count : Optional [int ] = None
171-
172-
173- class GrainCheckpointable (ocp_v1 .StatefulCheckpointable ):
174- """Adapts `GrainCheckpointHandler` to Orbax v1's `StatefulCheckpointable`."""
175-
176- def __init__ (
177- self ,
178- * ,
179- save_args : GrainCheckpointSave | None = None ,
180- restore_args : GrainCheckpointRestore | None = None ,
181- ):
182- self ._handler = GrainCheckpointHandler ()
183- self ._save_args = save_args
184- self ._restore_args = restore_args
185-
186- async def save (self , directory ):
187- """Saves the Grain iterator state to the given directory."""
188- # `GrainCheckpointHandler.save` snapshots iterator state (`get_state`) AND
189- # writes it; both must happen in this (blocking) save phase, NOT in the
190- # returned background coroutine.
191- path = await directory .await_creation ()
192- self ._handler .save (path , args = self ._save_args )
193-
194- async def _committed (): # nothing left for the background commit phase
195- return None
196-
197- return _committed ()
198-
199- async def load (self , directory : epath .Path ):
200- """Loads the Grain iterator state from the given directory."""
201- handler , args = self ._handler , self ._restore_args
202-
203- # This will be ran to completion so asynchronous portion is just for
204- # compatibility with Orbax v1 API.
205- async def _background_load ():
206- await asyncio .to_thread (handler .restore , directory , args = args )
207-
208- return _background_load ()
209-
210-
21160def _weight_mismatches (want , have , path = ()):
21261 """Returns `(path, problem)` for each weight in `want` that `have` didn't restore faithfully.
21362
@@ -462,7 +311,7 @@ def create_orbax_checkpoint_manager(
462311
463312 if dataset_type is not None and dataset_type == "grain" :
464313 item_names += ("iter" ,)
465- item_handlers ["iter" ] = GrainCheckpointHandler () # pyrefly: ignore[bad-assignment]
314+ item_handlers ["iter" ] = grain_utility . GrainCheckpointHandler () # pyrefly: ignore[bad-assignment]
466315
467316 # local storage checkpoint needs parent directory created
468317 p = gcs_utils .mkdir_and_check_permissions (checkpoint_dir )
@@ -623,137 +472,6 @@ def print_save_message(step, async_checkpointing):
623472 max_logging .log (f"Saved a checkpoint at step { step } ." )
624473
625474
626- def _find_idx (array : np .ndarray , replica_axis_idx : int ):
627- """Returns the index along given dimension that the current host belongs to."""
628- idx = None
629- for idx , val in np .ndenumerate (array ):
630- if val .process_index == jax .process_index ():
631- break
632- return idx [replica_axis_idx ]
633-
634-
635- def _replica_devices (device_array : np .ndarray , replica_axis_idx : int ):
636- """Returns the devices from the replica that current host belongs to.
637-
638- Replicas are assumed to be restricted to the first axis.
639-
640- Args:
641- device_array: devices of the mesh that can be obtained by mesh.devices()
642- replica_axis_idx: axis dimension along which replica is taken
643-
644- Returns:
645- devices inside the replica that current host is in
646- """
647- idx = _find_idx (device_array , replica_axis_idx )
648- replica_result = np .take (device_array , idx , axis = replica_axis_idx )
649- return np .expand_dims (replica_result , axis = replica_axis_idx )
650-
651-
652- def _prepare_scaled_down_grain_restore_args (
653- data_iterator : list , process_count_jax : int , process_count_stored : int , directory : epath .Path
654- ) -> GrainCheckpointRestore :
655- """
656- Prepares the restore arguments for a scaled-up (list) data iterator.
657-
658- This is used when restoring a checkpoint saved with more processes than
659- the current run (e.g., 64 files onto 32 JAX processes).
660- """
661- # 1. Validation Assertions
662- assert isinstance (data_iterator , list ), (
663- f"{ process_count_stored } processes found in Grain checkpoint directory { directory } , but only "
664- f"{ process_count_jax } jax processes in this run, please set expansion_factor_real_data accordingly."
665- )
666-
667- scaling_factor = len (data_iterator )
668- expected_process_count = process_count_stored / process_count_jax
669- assert scaling_factor == expected_process_count , (
670- f"Found { process_count_stored } processes in checkpoint and { process_count_jax } "
671- f"JAX processes, implying a scaling factor of { expected_process_count } . "
672- f"However, the data_iterator list has { scaling_factor } items."
673- )
674-
675- # 2. Prepare Arguments
676- local_iterator_list = [x .local_iterator for x in data_iterator ]
677- # Each JAX process calculates the global indices it's responsible for.
678- # e.g., process 0 with scaling_factor=2 handles checkpoints from processes [0, 32]
679- # e.g., process 1 with scaling_factor=2 handles checkpoints from processes [1, 33]
680- process_index_list = [jax .process_index () + i * process_count_jax for i in range (scaling_factor )]
681-
682- return GrainCheckpointRestore (local_iterator_list , process_index = process_index_list , process_count = process_count_stored )
683-
684-
685- def _restore_grain_iterator (
686- checkpoint_manager ,
687- step : int ,
688- data_iterator ,
689- checkpoint_args ,
690- expansion_factor_real_data : int , # This must be defined in the outer scope
691- ) -> tuple [Any , None ]:
692- """
693- Handles the complex logic for restoring a Grain data iterator checkpoint.
694- This function dispatches to the correct restore strategy based on
695- the number of stored checkpoint files vs. current JAX processes.
696- """
697- if isinstance (data_iterator , RemoteIteratorWrapper ):
698- grain_restore_args = GrainCheckpointRestore (item = data_iterator )
699- restored_state = checkpoint_manager .restore (step , args = Composite (items = checkpoint_args , iter = grain_restore_args ))
700- return (restored_state , None )
701-
702- # ElasticIterator: one shared `process_0.json` regardless of shard count.
703- if not isinstance (data_iterator , list ) and isinstance (data_iterator .local_iterator , ElasticIterator ):
704- grain_restore_args = GrainCheckpointRestore (item = data_iterator .local_iterator )
705- restored_state = checkpoint_manager .restore (step , args = Composite (items = checkpoint_args , iter = grain_restore_args ))
706- return (restored_state , None )
707-
708- directory = checkpoint_manager .directory / str (step ) / "iter"
709- process_count_jax = jax .process_count ()
710-
711- # Count the number of checkpoint files
712- process_count_stored = len (list (directory .glob ("process_*-of-*.json" )))
713-
714- grain_restore_args = None
715-
716- if process_count_stored > process_count_jax :
717- # Scaling down from a larger number of hosts. (e.g., 128 files -> 64 processes)
718- # In this case, each host restores a list of data iterators.
719- grain_restore_args = _prepare_scaled_down_grain_restore_args (
720- data_iterator , process_count_jax , process_count_stored , directory
721- )
722-
723- elif process_count_stored == process_count_jax :
724- # Normal case: number of hosts is the same. (e.g., 64 files -> 64 processes)
725- assert not isinstance (data_iterator , list ), (
726- f"{ process_count_stored } processes found in Grain checkpoint directory { directory } , matching the number of "
727- "jax process, please do not set expansion_factor_real_data."
728- )
729- grain_restore_args = GrainCheckpointRestore (data_iterator .local_iterator )
730-
731- elif expansion_factor_real_data > 1 and process_count_stored == process_count_jax // expansion_factor_real_data :
732- # Scaling up to a larger number of hosts.(e.g., 32 files -> 64 processes)
733- # In this case, a subset of hosts restore the data iterator.
734- assert not isinstance (
735- data_iterator , list
736- ), "when expansion_factor_real_data > 1, the data iterator should not be a list."
737- grain_restore_args = GrainCheckpointRestore (
738- data_iterator .local_iterator , process_index = jax .process_index (), process_count = process_count_stored
739- )
740-
741- else :
742- # Case 4: Mismatch
743- raise ValueError (
744- f"Error restoring Grain checkpoint in { directory } : "
745- f"The number of stored checkpoint files ({ process_count_stored } ) "
746- f"is incompatible with the number of JAX processes ({ process_count_jax } ). "
747- "If you are resuming training with a different number of chips, see instructions in "
748- "https://github.com/AI-Hypercomputer/maxtext/blob/main/docs/guides/data_input_pipeline/"
749- "data_input_grain.md#using-grain"
750- )
751-
752- # Call restore once with the composed arguments
753- restored_state = checkpoint_manager .restore (step , args = Composite (items = checkpoint_args , iter = grain_restore_args ))
754- return (restored_state , None )
755-
756-
757475def load_state_if_possible (
758476 checkpoint_manager : CheckpointManager | None ,
759477 data_iterator : MultiHostDataLoadIterator | list [MultiHostDataLoadIterator ] | None ,
@@ -811,7 +529,7 @@ def map_to_pspec(data):
811529 pspec = data .sharding .spec
812530 mesh = data .sharding .mesh
813531 replica_axis_index = 0
814- replica_devices = _replica_devices (mesh .devices , replica_axis_index )
532+ replica_devices = grain_utility . replica_devices (mesh .devices , replica_axis_index )
815533 replica_mesh = jax .sharding .Mesh (replica_devices , mesh .axis_names )
816534 single_replica_sharding = jax .sharding .NamedSharding (replica_mesh , pspec )
817535
@@ -846,7 +564,7 @@ def map_to_pspec(data):
846564 and not isinstance (data_iterator , PlaceHolderDataIterator )
847565 and (checkpoint_manager .directory / str (step ) / "iter" ).exists ()
848566 ):
849- restored , _ = _restore_grain_iterator (
567+ restored , _ = grain_utility . restore_grain_iterator (
850568 checkpoint_manager , step , data_iterator , checkpoint_args , expansion_factor_real_data
851569 )
852570 else :
@@ -907,7 +625,7 @@ def map_to_pspec(data):
907625 and not isinstance (data_iterator , PlaceHolderDataIterator )
908626 and (checkpoint_manager .directory / str (step ) / "iter" ).exists ()
909627 ):
910- return _restore_grain_iterator (
628+ return grain_utility . restore_grain_iterator (
911629 checkpoint_manager ,
912630 step ,
913631 data_iterator ,
@@ -1189,12 +907,14 @@ def save_checkpoint(checkpoint_manager, step, state, config=None, data_iterator=
1189907 if config and config .dataset_type == "grain" and not isinstance (data_iterator , PlaceHolderDataIterator ):
1190908 if isinstance (data_iterator , RemoteIteratorWrapper ):
1191909 # Pass the wrapper directly; GrainCheckpointHandler will call save_state with the step
1192- save_args_composite ["iter" ] = GrainCheckpointSave (item = data_iterator ) # pyrefly: ignore[bad-assignment]
910+ save_args_composite ["iter" ] = grain_utility .GrainCheckpointSave (
911+ item = data_iterator
912+ ) # pyrefly: ignore[bad-assignment]
1193913 elif not isinstance (data_iterator , list ) and isinstance (
1194914 data_iterator .local_iterator , ElasticIterator
1195915 ): # pyrefly: ignore[missing-attribute]
1196916 # ElasticIterator checkpoints a single global scalar shared by all shards.
1197- save_args_composite ["iter" ] = GrainCheckpointSave (
917+ save_args_composite ["iter" ] = grain_utility . GrainCheckpointSave (
1198918 item = data_iterator .local_iterator
1199919 ) # pyrefly: ignore[bad-assignment]
1200920 else :
@@ -1209,7 +929,9 @@ def save_checkpoint(checkpoint_manager, step, state, config=None, data_iterator=
1209929 grain_iters_to_save .append (
1210930 (data_iter .local_iterator , process_index , process_count_total )
1211931 ) # pyrefly: ignore[missing-attribute]
1212- save_args_composite ["iter" ] = GrainCheckpointSave (item = grain_iters_to_save ) # pyrefly: ignore[bad-assignment]
932+ save_args_composite ["iter" ] = grain_utility .GrainCheckpointSave (
933+ item = grain_iters_to_save
934+ ) # pyrefly: ignore[bad-assignment]
1213935
1214936 custom_metadata = {}
1215937 if config :
0 commit comments