1616"""Create an Orbax CheckpointManager with specified (Async or not) Checkpointer."""
1717
1818import datetime
19+ import importlib
1920import time
2021from typing import Any
2122
@@ -157,33 +158,58 @@ def _load_linen_checkpoint_into_nnx(
157158 restore_args = ocp .checkpoint_utils .construct_restore_args (linen_abstract )
158159 restored = ocp .args .PyTreeRestore (item = linen_abstract , restore_args = restore_args , partial_restore = True )
159160 restored = ckptr .restore (epath .Path (path ), args = restored )
160- _raise_on_weight_mismatch (* _expected_and_restored_params (abstract_nnx_state , restored ))
161- return _linen_items_to_nnx (restored , abstract_nnx_state )
161+ return _restored_linen_to_nnx (restored , abstract_nnx_state )
162162
163163
164- def _restore_emergency_linen_checkpoint_into_nnx (
165- checkpoint_manager ,
166- step ,
167- abstract_nnx_state ,
168- map_to_pspec ,
169- ):
170- """Restores an emergency Linen-layout checkpoint into an NNX state.
164+ def _restored_linen_to_nnx (restored_linen , abstract_nnx_state ):
165+ """Reshapes a restored Linen-layout tree into the NNX state.
171166
172- The `nnx_aux` subtree is stored inside `items`, so an emergency checkpoint
173- carries it too; it's restored when present and otherwise kept at its fresh
174- init value. A genuinely-missing weight raises.
167+ Raises if the checkpoint is missing a weight. Every NNX restore path ends here: the load
168+ itself is the Linen one, since pure_nnx reads and writes the Linen on-disk layout.
175169 """
176- max_logging .log (f"Restoring emergency Linen-layout checkpoint into NNX state at step { step } " )
177- linen_abstract = train_state_nnx .to_checkpoint_dict (abstract_nnx_state )
178- restore_args = jax .tree_util .tree_map (map_to_pspec , linen_abstract )
179- checkpoint_args = ocp .args .PyTreeRestore (
180- item = linen_abstract ,
181- restore_args = restore_args ,
182- partial_restore = True ,
183- )
184- restored = checkpoint_manager .restore (step , args = Composite (state = checkpoint_args )).state
185- _raise_on_weight_mismatch (* _expected_and_restored_params (abstract_nnx_state , restored ))
186- return _linen_items_to_nnx (restored , abstract_nnx_state )
170+ _raise_on_weight_mismatch (* _expected_and_restored_params (abstract_nnx_state , restored_linen ))
171+ return _linen_items_to_nnx (restored_linen , abstract_nnx_state )
172+
173+
174+ def _abstract_params (abstract_unboxed_pre_state ):
175+ """Returns the state's weights: the NNX Param subtree, or Linen's `params` collection."""
176+ if isinstance (abstract_unboxed_pre_state , nnx .State ):
177+ return nnx .split_state (abstract_unboxed_pre_state .model , nnx .Param , ...)[0 ]
178+ return abstract_unboxed_pre_state .params
179+
180+
181+ def _bare_weights (tree ):
182+ """Strips the Flax `params` collection wrapper so weights compare at the same depth.
183+
184+ A Linen params tree is the collection, an NNX one the bare weights; the dynamic
185+ safetensors loader always returns the collection.
186+ """
187+ return tree ["params" ] if isinstance (tree , dict ) and len (tree ) == 1 and "params" in tree else tree
188+
189+
190+ def _resolve_conversion_fn (checkpoint_conversion_fn ):
191+ """Returns `checkpoint_conversion_fn` as a callable.
192+
193+ Config carries it as a dotted string ("my_pkg.my_module.my_fn"), so it has to be imported
194+ before it can be called. A callable is used as is.
195+ """
196+ if checkpoint_conversion_fn is None :
197+ raise ValueError (
198+ "source_checkpoint_layout='safetensors' needs `checkpoint_conversion_fn` to map the "
199+ "checkpoint's weights onto the model's, e.g. checkpoint_conversion_fn=my_pkg.my_module.my_fn."
200+ )
201+ if callable (checkpoint_conversion_fn ):
202+ return checkpoint_conversion_fn
203+ module_name , _ , fn_name = str (checkpoint_conversion_fn ).rpartition ("." )
204+ if not module_name :
205+ raise ValueError (f"`checkpoint_conversion_fn` must be a dotted path to a function, got { checkpoint_conversion_fn !r} ." )
206+ try :
207+ fn = getattr (importlib .import_module (module_name ), fn_name , None )
208+ except ImportError as e :
209+ raise ValueError (f"Could not import `checkpoint_conversion_fn` { checkpoint_conversion_fn !r} : { e } " ) from e
210+ if not callable (fn ):
211+ raise ValueError (f"`checkpoint_conversion_fn` { checkpoint_conversion_fn !r} is not a function." )
212+ return fn
187213
188214
189215def _load_full_state_from_path (
@@ -226,6 +252,8 @@ def _load_full_state_from_path(
226252 with context :
227253 return ocp_v1 .load_pytree (path , abstract_unboxed_pre_state )
228254 elif source_checkpoint_layout == "safetensors" :
255+ # Resolved first, so a bad config fails before the weights are read.
256+ conversion_fn = _resolve_conversion_fn (checkpoint_conversion_fn )
229257 context = ocp_v1 .Context (checkpoint_layout = ocp_v1 .options .CheckpointLayout .SAFETENSORS )
230258 with context :
231259 metadata = ocp_v1 .pytree_metadata (path )
@@ -237,7 +265,11 @@ def combine_sharding(sds, shardings):
237265
238266 sharded_abstract_state = jax .tree .map (combine_sharding , simple_abstract_state , shardings )
239267 pre_transformed_state = ocp_v1 .load_pytree (path , sharded_abstract_state )
240- state = checkpoint_conversion_fn (pre_transformed_state )
268+ state = conversion_fn (pre_transformed_state )
269+ # The conversion fn returns MaxText's on-disk (Linen) layout, which is what pure_nnx reads,
270+ # so NNX needs the same reshape as every other restore. An NNX state passes through.
271+ if isinstance (abstract_unboxed_pre_state , nnx .State ) and not isinstance (state , nnx .State ):
272+ state = _restored_linen_to_nnx (state , abstract_unboxed_pre_state )
241273 return state
242274 else :
243275 raise ocp_v1 .errors .InvalidLayoutError (f"Unknown checkpoint layout: { source_checkpoint_layout } " )
@@ -406,6 +438,11 @@ def load_state_if_possible(
406438 set.
407439 """
408440
441+ # pure_nnx saves in the Linen on-disk layout, so every branch below loads the same tree Linen
442+ # does: the NNX abstract is converted to that layout going in, and what comes back is reshaped
443+ # into the NNX state on the way out.
444+ is_nnx = isinstance (abstract_unboxed_pre_state , nnx .State )
445+
409446 if checkpoint_manager is not None :
410447 max_logging .log ("checkpoint manager exists so trying to load this run's existing checkpoint" )
411448
@@ -437,49 +474,9 @@ def map_to_pspec(data):
437474 )
438475 ocp .type_handlers .register_type_handler (jax .Array , array_handler , override = True )
439476
440- # pure_nnx saves in the Linen on-disk layout; restore that layout (weights +
441- # opt_state + step + nnx_aux), restoring the grain iterator in place when
442- # present, then reshape it back into the NNX state.
443- # (Emergency managers use their own restore path below.)
444- if isinstance (abstract_unboxed_pre_state , nnx .State ) and not isinstance (
445- checkpoint_manager ,
446- (EmergencyCheckpointManager , EmergencyReplicatorCheckpointManager ),
447- ):
448- linen_abstract = train_state_nnx .to_checkpoint_dict (abstract_unboxed_pre_state )
449- restore_args = jax .tree_util .tree_map (map_to_pspec , linen_abstract )
450- checkpoint_args = ocp .args .PyTreeRestore (item = linen_abstract , restore_args = restore_args , partial_restore = True )
451- if (
452- dataset_type == "grain"
453- and data_iterator
454- and not isinstance (data_iterator , PlaceHolderDataIterator )
455- and (checkpoint_manager .directory / str (step ) / "iter" ).exists ()
456- ):
457- restored , _ = grain_utility .restore_grain_iterator (
458- checkpoint_manager , step , data_iterator , checkpoint_args , expansion_factor_real_data
459- )
460- else :
461- restored = checkpoint_manager .restore (step , args = Composite (items = checkpoint_args ))
462- _raise_on_weight_mismatch (* _expected_and_restored_params (abstract_unboxed_pre_state , restored ["items" ]))
463- restored_nnx = _linen_items_to_nnx (restored ["items" ], abstract_unboxed_pre_state )
464- return ({"items" : restored_nnx }, None )
465-
466- if isinstance (abstract_unboxed_pre_state , nnx .State ) and isinstance (
467- checkpoint_manager ,
468- (EmergencyCheckpointManager , EmergencyReplicatorCheckpointManager ),
469- ):
470- restored = _restore_emergency_linen_checkpoint_into_nnx (
471- checkpoint_manager ,
472- step ,
473- abstract_unboxed_pre_state ,
474- map_to_pspec ,
475- )
476- return (
477- restored ,
478- None ,
479- )
480-
481- # Only Linen TrainState reaches here; the NNX cases returned above.
482- restore_target = abstract_unboxed_pre_state
477+ restore_target = (
478+ train_state_nnx .to_checkpoint_dict (abstract_unboxed_pre_state ) if is_nnx else abstract_unboxed_pre_state
479+ )
483480 restore_args = jax .tree_util .tree_map (map_to_pspec , restore_target )
484481 checkpoint_args = ocp .args .PyTreeRestore (
485482 item = restore_target ,
@@ -499,6 +496,8 @@ def map_to_pspec(data):
499496 ),
500497 ):
501498 restored = checkpoint_manager .restore (step , args = Composite (state = checkpoint_args )).state
499+ if is_nnx :
500+ restored = _restored_linen_to_nnx (restored , abstract_unboxed_pre_state )
502501 return (
503502 restored ,
504503 None ,
@@ -515,30 +514,43 @@ def map_to_pspec(data):
515514 and not isinstance (data_iterator , PlaceHolderDataIterator )
516515 and (checkpoint_manager .directory / str (step ) / "iter" ).exists ()
517516 ):
518- return grain_utility .restore_grain_iterator (
517+ restored , iterator = grain_utility .restore_grain_iterator (
519518 checkpoint_manager ,
520519 step ,
521520 data_iterator ,
522521 checkpoint_args ,
523522 expansion_factor_real_data ,
524523 )
524+ if is_nnx :
525+ restored = {"items" : _restored_linen_to_nnx (restored ["items" ], abstract_unboxed_pre_state )}
526+ return (restored , iterator )
525527 # Case 3: Default/Fallback case.
526528 # This case acts as a wildcard ('_') and matches if none of the preceding cases were met.
527529 case _:
528530 restored = checkpoint_manager .restore (step , args = Composite (items = checkpoint_args ))
531+ if is_nnx :
532+ restored = {"items" : _restored_linen_to_nnx (restored ["items" ], abstract_unboxed_pre_state )}
529533 return (restored , None )
530534
531535 if source_checkpoint_layout == "safetensors_dynamic" :
532536 path = load_parameters_from_path or load_full_state_from_path
533537 max_logging .log (f"Dynamic On-the-Fly Formatting: Loading SafeTensors from { path } " )
534538
535- return load_safetensors_dynamic_state (path , abstract_unboxed_pre_state , maxtext_config )
539+ # Weights-only for both paths, so the loader gets the weights rather than the whole state:
540+ # the HF param mappings name weights, and an NNX state hides them under `model`.
541+ params = _abstract_params (abstract_unboxed_pre_state )
542+ restored , restored_params = load_safetensors_dynamic_state (path , params , maxtext_config )
543+ # A weight no HF mapping covered comes back unmaterialized and would reach the model as an
544+ # untrained init value. Same check the Orbax weights-only load makes.
545+ _raise_on_weight_mismatch (_bare_weights (params .to_pure_dict () if is_nnx else params ), _bare_weights (restored_params ))
546+ if is_nnx :
547+ # The loader returns the Linen `params` collection; NNX holds bare weights, so unwrap it
548+ # back into the params state, the shape load_params_from_path returns.
549+ nnx .replace_by_pure_dict (params , restored_params ["params" ])
550+ return restored , params
551+ return restored , restored_params
536552 elif load_parameters_from_path != "" :
537- if isinstance (abstract_unboxed_pre_state , nnx .State ):
538- _ , params , _ = nnx .split (abstract_unboxed_pre_state .model , nnx .Param , ...)
539- else :
540- params = abstract_unboxed_pre_state .params
541-
553+ params = _abstract_params (abstract_unboxed_pre_state )
542554 restored_params = load_params_from_path (
543555 load_parameters_from_path ,
544556 params ,
0 commit comments