1515
1616"""Create an Orbax CheckpointManager with specified (Async or not) Checkpointer."""
1717
18+ import importlib
1819import time
1920from typing import Any
2021
@@ -158,33 +159,58 @@ def _load_linen_checkpoint_into_nnx(
158159 restore_args = ocp .checkpoint_utils .construct_restore_args (linen_abstract )
159160 restored = ocp .args .PyTreeRestore (item = linen_abstract , restore_args = restore_args , partial_restore = True )
160161 restored = ckptr .restore (epath .Path (path ), args = restored )
161- _raise_on_weight_mismatch (* _expected_and_restored_params (abstract_nnx_state , restored ))
162- return _linen_items_to_nnx (restored , abstract_nnx_state )
162+ return _restored_linen_to_nnx (restored , abstract_nnx_state )
163163
164164
165- def _restore_emergency_linen_checkpoint_into_nnx (
166- checkpoint_manager ,
167- step ,
168- abstract_nnx_state ,
169- map_to_pspec ,
170- ):
171- """Restores an emergency Linen-layout checkpoint into an NNX state.
165+ def _restored_linen_to_nnx (restored_linen , abstract_nnx_state ):
166+ """Reshapes a restored Linen-layout tree into the NNX state.
172167
173- The `nnx_aux` subtree is stored inside `items`, so an emergency checkpoint
174- carries it too; it's restored when present and otherwise kept at its fresh
175- init value. A genuinely-missing weight raises.
168+ Raises if the checkpoint is missing a weight. Every NNX restore path ends here: the load
169+ itself is the Linen one, since pure_nnx reads and writes the Linen on-disk layout.
176170 """
177- max_logging .log (f"Restoring emergency Linen-layout checkpoint into NNX state at step { step } " )
178- linen_abstract = train_state_nnx .to_checkpoint_dict (abstract_nnx_state )
179- restore_args = jax .tree_util .tree_map (map_to_pspec , linen_abstract )
180- checkpoint_args = ocp .args .PyTreeRestore (
181- item = linen_abstract ,
182- restore_args = restore_args ,
183- partial_restore = True ,
184- )
185- restored = checkpoint_manager .restore (step , args = Composite (state = checkpoint_args )).state
186- _raise_on_weight_mismatch (* _expected_and_restored_params (abstract_nnx_state , restored ))
187- return _linen_items_to_nnx (restored , abstract_nnx_state )
171+ _raise_on_weight_mismatch (* _expected_and_restored_params (abstract_nnx_state , restored_linen ))
172+ return _linen_items_to_nnx (restored_linen , abstract_nnx_state )
173+
174+
175+ def _abstract_params (abstract_unboxed_pre_state ):
176+ """Returns the state's weights: the NNX Param subtree, or Linen's `params` collection."""
177+ if isinstance (abstract_unboxed_pre_state , nnx .State ):
178+ return nnx .split_state (abstract_unboxed_pre_state .model , nnx .Param , ...)[0 ]
179+ return abstract_unboxed_pre_state .params
180+
181+
182+ def _bare_weights (tree ):
183+ """Strips the Flax `params` collection wrapper so weights compare at the same depth.
184+
185+ A Linen params tree is the collection, an NNX one the bare weights; the dynamic
186+ safetensors loader always returns the collection.
187+ """
188+ return tree ["params" ] if isinstance (tree , dict ) and set (tree ) == {"params" } else tree
189+
190+
191+ def _resolve_conversion_fn (checkpoint_conversion_fn ):
192+ """Returns `checkpoint_conversion_fn` as a callable.
193+
194+ Config carries it as a dotted string ("my_pkg.my_module.my_fn"), so it has to be imported
195+ before it can be called. A callable is used as is.
196+ """
197+ if checkpoint_conversion_fn is None :
198+ raise ValueError (
199+ "source_checkpoint_layout='safetensors' needs `checkpoint_conversion_fn` to map the "
200+ "checkpoint's weights onto the model's, e.g. checkpoint_conversion_fn=my_pkg.my_module.my_fn."
201+ )
202+ if callable (checkpoint_conversion_fn ):
203+ return checkpoint_conversion_fn
204+ module_name , _ , fn_name = str (checkpoint_conversion_fn ).rpartition ("." )
205+ if not module_name :
206+ raise ValueError (f"`checkpoint_conversion_fn` must be a dotted path to a function, got { checkpoint_conversion_fn !r} ." )
207+ try :
208+ fn = getattr (importlib .import_module (module_name ), fn_name , None )
209+ except ImportError as e :
210+ raise ValueError (f"Could not import `checkpoint_conversion_fn` { checkpoint_conversion_fn !r} : { e } " ) from e
211+ if not callable (fn ):
212+ raise ValueError (f"`checkpoint_conversion_fn` { checkpoint_conversion_fn !r} is not a function." )
213+ return fn
188214
189215
190216def _load_full_state_from_path (
@@ -227,6 +253,8 @@ def _load_full_state_from_path(
227253 with context :
228254 return ocp_v1 .load_pytree (path , abstract_unboxed_pre_state )
229255 elif source_checkpoint_layout == "safetensors" :
256+ # Resolved first, so a bad config fails before the weights are read.
257+ conversion_fn = _resolve_conversion_fn (checkpoint_conversion_fn )
230258 context = ocp_v1 .Context (checkpoint_layout = ocp_v1 .options .CheckpointLayout .SAFETENSORS )
231259 with context :
232260 metadata = ocp_v1 .pytree_metadata (path )
@@ -238,7 +266,11 @@ def combine_sharding(sds, shardings):
238266
239267 sharded_abstract_state = jax .tree .map (combine_sharding , simple_abstract_state , shardings )
240268 pre_transformed_state = ocp_v1 .load_pytree (path , sharded_abstract_state )
241- state = checkpoint_conversion_fn (pre_transformed_state )
269+ state = conversion_fn (pre_transformed_state )
270+ # The conversion fn returns MaxText's on-disk (Linen) layout, which is what pure_nnx reads,
271+ # so NNX needs the same reshape as every other restore. An NNX state passes through.
272+ if isinstance (abstract_unboxed_pre_state , nnx .State ) and not isinstance (state , nnx .State ):
273+ state = _restored_linen_to_nnx (state , abstract_unboxed_pre_state )
242274 return state
243275 else :
244276 raise ocp_v1 .errors .InvalidLayoutError (f"Unknown checkpoint layout: { source_checkpoint_layout } " )
@@ -547,49 +579,12 @@ def map_to_pspec(data):
547579 )
548580 ocp .type_handlers .register_type_handler (jax .Array , array_handler , override = True )
549581
550- # pure_nnx saves in the Linen on-disk layout; restore that layout (weights +
551- # opt_state + step + nnx_aux), restoring the grain iterator in place when
552- # present, then reshape it back into the NNX state.
553- # (Emergency managers use their own restore path below.)
554- if isinstance (abstract_unboxed_pre_state , nnx .State ) and not isinstance (
555- checkpoint_manager ,
556- (EmergencyCheckpointManager , EmergencyReplicatorCheckpointManager ),
557- ):
558- linen_abstract = train_state_nnx .to_checkpoint_dict (abstract_unboxed_pre_state )
559- restore_args = jax .tree_util .tree_map (map_to_pspec , linen_abstract )
560- checkpoint_args = ocp .args .PyTreeRestore (item = linen_abstract , restore_args = restore_args , partial_restore = True )
561- if (
562- dataset_type == "grain"
563- and data_iterator
564- and not isinstance (data_iterator , PlaceHolderDataIterator )
565- and (checkpoint_manager .directory / str (step ) / "iter" ).exists ()
566- ):
567- restored , _ = grain_utility .restore_grain_iterator (
568- checkpoint_manager , step , data_iterator , checkpoint_args , expansion_factor_real_data
569- )
570- else :
571- restored = checkpoint_manager .restore (step , args = Composite (items = checkpoint_args ))
572- _raise_on_weight_mismatch (* _expected_and_restored_params (abstract_unboxed_pre_state , restored ["items" ]))
573- restored_nnx = _linen_items_to_nnx (restored ["items" ], abstract_unboxed_pre_state )
574- return ({"items" : restored_nnx }, None )
575-
576- if isinstance (abstract_unboxed_pre_state , nnx .State ) and isinstance (
577- checkpoint_manager ,
578- (EmergencyCheckpointManager , EmergencyReplicatorCheckpointManager ),
579- ):
580- restored = _restore_emergency_linen_checkpoint_into_nnx (
581- checkpoint_manager ,
582- step ,
583- abstract_unboxed_pre_state ,
584- map_to_pspec ,
585- )
586- return (
587- restored ,
588- None ,
589- )
590-
591- # Only Linen TrainState reaches here; the NNX cases returned above.
592- restore_target = abstract_unboxed_pre_state
582+ # pure_nnx saves in the Linen on-disk layout, so both paths restore the same tree through
583+ # the same manager calls below: convert the NNX abstract going in, reshape it coming out.
584+ is_nnx = isinstance (abstract_unboxed_pre_state , nnx .State )
585+ restore_target = (
586+ train_state_nnx .to_checkpoint_dict (abstract_unboxed_pre_state ) if is_nnx else abstract_unboxed_pre_state
587+ )
593588 restore_args = jax .tree_util .tree_map (map_to_pspec , restore_target )
594589 checkpoint_args = ocp .args .PyTreeRestore (
595590 item = restore_target ,
@@ -609,6 +604,8 @@ def map_to_pspec(data):
609604 ),
610605 ):
611606 restored = checkpoint_manager .restore (step , args = Composite (state = checkpoint_args )).state
607+ if is_nnx :
608+ restored = _restored_linen_to_nnx (restored , abstract_unboxed_pre_state )
612609 return (
613610 restored ,
614611 None ,
@@ -625,30 +622,44 @@ def map_to_pspec(data):
625622 and not isinstance (data_iterator , PlaceHolderDataIterator )
626623 and (checkpoint_manager .directory / str (step ) / "iter" ).exists ()
627624 ):
628- return grain_utility .restore_grain_iterator (
625+ restored , iterator = grain_utility .restore_grain_iterator (
629626 checkpoint_manager ,
630627 step ,
631628 data_iterator ,
632629 checkpoint_args ,
633630 expansion_factor_real_data ,
634631 )
632+ if is_nnx :
633+ restored = {"items" : _restored_linen_to_nnx (restored ["items" ], abstract_unboxed_pre_state )}
634+ return (restored , iterator )
635635 # Case 3: Default/Fallback case.
636636 # This case acts as a wildcard ('_') and matches if none of the preceding cases were met.
637637 case _:
638638 restored = checkpoint_manager .restore (step , args = Composite (items = checkpoint_args ))
639+ if is_nnx :
640+ restored = {"items" : _restored_linen_to_nnx (restored ["items" ], abstract_unboxed_pre_state )}
639641 return (restored , None )
640642
641643 if source_checkpoint_layout == "safetensors_dynamic" :
642644 path = load_parameters_from_path or load_full_state_from_path
643645 max_logging .log (f"Dynamic On-the-Fly Formatting: Loading SafeTensors from { path } " )
644646
645- return load_safetensors_dynamic_state (path , abstract_unboxed_pre_state , maxtext_config )
647+ # Weights-only for both paths, so the loader gets the weights rather than the whole state:
648+ # the HF param mappings name weights, and an NNX state hides them under `model`.
649+ params = _abstract_params (abstract_unboxed_pre_state )
650+ restored , restored_params = load_safetensors_dynamic_state (path , params , maxtext_config )
651+ is_nnx = isinstance (params , nnx .State )
652+ # A weight no HF mapping covered comes back unmaterialized and would reach the model as an
653+ # untrained init value. Same check the Orbax weights-only load makes.
654+ _raise_on_weight_mismatch (_bare_weights (params .to_pure_dict () if is_nnx else params ), _bare_weights (restored_params ))
655+ if is_nnx :
656+ # The loader returns the Linen `params` collection; NNX holds bare weights, so unwrap it
657+ # back into the params state, the shape load_params_from_path returns.
658+ nnx .replace_by_pure_dict (params , restored_params ["params" ])
659+ return restored , params
660+ return restored , restored_params
646661 elif load_parameters_from_path != "" :
647- if isinstance (abstract_unboxed_pre_state , nnx .State ):
648- _ , params , _ = nnx .split (abstract_unboxed_pre_state .model , nnx .Param , ...)
649- else :
650- params = abstract_unboxed_pre_state .params
651-
662+ params = _abstract_params (abstract_unboxed_pre_state )
652663 restored_params = load_params_from_path (
653664 load_parameters_from_path ,
654665 params ,
0 commit comments