4949from maxtext .utils import model_creation_utils
5050from maxtext .utils import train_utils
5151
52+ _SCAN_AXIS_NAMES = ("layers_per_stage" , "layers" , "circular_repeats" )
53+
54+
55+ def get_scan_axis (config , sharding_or_spec , x = None ):
56+ """Find the scan axis from sharding spec or use default fallback if valid."""
57+ if hasattr (sharding_or_spec , "spec" ):
58+ spec = sharding_or_spec .spec
59+ else :
60+ spec = sharding_or_spec
61+
62+ for name in _SCAN_AXIS_NAMES :
63+ if name in spec :
64+ return spec .index (name )
65+ if x is not None and hasattr (x , "ndim" ) and x .ndim > config .param_scan_axis :
66+ return config .param_scan_axis
67+ return None
68+
5269
5370def _possibly_unroll_params (config , training_state , training_state_annotations , mesh ):
5471 """Unroll scanned input layers when force_unroll is set."""
@@ -106,28 +123,60 @@ def _possibly_unroll_params_nnx(config, state, state_mesh_shardings, mesh):
106123 and removes the original collection. Mirrors the same operation on
107124 `state_mesh_shardings` so downstream sharding stays correct.
108125 """
109- decoder_state = state .model .decoder
110- decoder_shardings = state_mesh_shardings .model .decoder
126+ param_only_state , other_state = nnx .split_state (state , nnx .Param , ...)
127+ param_paths = set (param_only_state .flat_state ().paths )
128+
129+ param_only_shardings_items = [(path , val ) for path , val in state_mesh_shardings .flat_state () if path in param_paths ]
130+ other_shardings_items = [(path , val ) for path , val in state_mesh_shardings .flat_state () if path not in param_paths ]
131+
132+ param_only_shardings = nnx .State .from_flat_path (param_only_shardings_items )
133+ other_shardings = nnx .State .from_flat_path (other_shardings_items )
134+
135+ decoder_state = param_only_state .model .decoder
136+ decoder_shardings = param_only_shardings .model .decoder
111137
112138 def unroll_layer_group (num_layers , layer_name = "layers" ):
113139 layers = decoder_state .get (layer_name , None )
114140 layers_shardings = decoder_shardings .get (layer_name , None )
115141 if layers is None or layers_shardings is None :
116142 raise ValueError (f"Missing { layer_name } in NNX state.model.decoder or state_mesh_shardings." )
117143
118- def drop_scan_axis (named_sharding ):
144+ def drop_scan_axis (named_sharding , x ):
119145 ps = named_sharding .spec
120- return jax .sharding .PartitionSpec (* (ps [0 : config .param_scan_axis ] + ps [config .param_scan_axis + 1 :]))
146+ val = x [...] if isinstance (x , nnx .Variable ) else x
147+ scan_axis = get_scan_axis (config , named_sharding , val )
148+ if scan_axis is not None :
149+ return jax .sharding .PartitionSpec (* (ps [0 :scan_axis ] + ps [scan_axis + 1 :]))
150+ return ps
121151
122152 new_layer_pspec = jax .tree_util .tree_map (
123- drop_scan_axis , layers_shardings , is_leaf = lambda x : isinstance (x , jax .sharding .NamedSharding )
153+ drop_scan_axis ,
154+ layers_shardings ,
155+ layers ,
156+ is_leaf = lambda x : isinstance (x , (jax .sharding .NamedSharding , nnx .Variable )),
124157 )
125158 new_layer_sharding = jax .tree_util .tree_map (lambda ps : jax .sharding .NamedSharding (mesh , ps ), new_layer_pspec )
126159
127160 for i in range (num_layers ):
128161
162+ # pylint: disable=cell-var-from-loop
129163 def slice_ith (input_layers ):
130- return jax .tree_util .tree_map (lambda x : jnp .take (x , i , axis = config .param_scan_axis ), input_layers )
164+ def _slice_leaf (x , sharding ):
165+ val = x [...] if isinstance (x , nnx .Variable ) else x
166+ scan_axis = get_scan_axis (config , sharding , val )
167+ if scan_axis is not None :
168+ sliced_val = jnp .take (val , i , axis = scan_axis )
169+ if isinstance (x , nnx .Variable ):
170+ return type (x )(sliced_val )
171+ return sliced_val
172+ return x
173+
174+ return jax .tree_util .tree_map (
175+ _slice_leaf ,
176+ input_layers ,
177+ layers_shardings ,
178+ is_leaf = lambda x : isinstance (x , (jax .sharding .NamedSharding , nnx .Variable )),
179+ )
131180
132181 # pylint: disable=not-callable
133182 new_layer = jax .jit (slice_ith , out_shardings = new_layer_sharding )(layers )
@@ -145,6 +194,13 @@ def slice_ith(input_layers):
145194 else :
146195 unroll_layer_group (config .num_decoder_layers , layer_name = "layers" )
147196
197+ # Merge modified parameter state and other variables back in-place
198+ merged_state = nnx .merge_state (param_only_state , other_state )
199+ state .update (merged_state )
200+
201+ merged_shardings = nnx .merge_state (param_only_shardings , other_shardings )
202+ state_mesh_shardings .update (merged_shardings )
203+
148204
149205def _read_train_checkpoint (config , checkpoint_manager , mesh ):
150206 """Read training checkpoint at path defined by load_full_state_path."""
@@ -173,7 +229,8 @@ def init_state_fn():
173229 if config .pure_nnx :
174230 # On NNX, state is a flat nnx.State; params live under state.model and the
175231 # legacy notations are unused (callers receive shardings directly).
176- num_params = max_utils .calculate_num_params_from_pytree (state .model )
232+ params , _ = nnx .split_state (state .model , nnx .Param , ...)
233+ num_params = max_utils .calculate_num_params_from_pytree (params )
177234 max_logging .log (f"In input checkpoint Number of model params={ num_params / 1e9 :.3f} billion" )
178235 return state , state_mesh_shardings
179236 num_params = max_utils .calculate_num_params_from_pytree (state .params )
@@ -283,16 +340,36 @@ def unroll_layer_group(num_layers, layer_name="layers"):
283340 if layers is None or layers_annotations is None :
284341 return # No LoRA on this layer group; nothing to unroll.
285342
286- def new_pspec (x ):
287- return jax .sharding .PartitionSpec (* (x [0 : config .param_scan_axis ] + x [config .param_scan_axis + 1 :]))
288-
289- new_layer_annotation = jax .tree_util .tree_map (new_pspec , layers_annotations )
343+ def drop_scan_axis (spec , x ):
344+ scan_axis = get_scan_axis (config , spec , x )
345+ if scan_axis is not None :
346+ return jax .sharding .PartitionSpec (* (spec [0 :scan_axis ] + spec [scan_axis + 1 :]))
347+ return spec
348+
349+ new_layer_annotation = jax .tree_util .tree_map (
350+ drop_scan_axis ,
351+ layers_annotations ,
352+ layers ,
353+ is_leaf = lambda x : isinstance (x , jax .sharding .PartitionSpec ),
354+ )
290355 new_layer_sharding = jax .tree_util .tree_map (lambda x : jax .sharding .NamedSharding (mesh , x ), new_layer_annotation )
291356
292357 for i in range (num_layers ):
293358
359+ # pylint: disable=cell-var-from-loop
294360 def slice_ith (input_layers ):
295- return jax .tree_util .tree_map (lambda x : jnp .take (x , i , axis = config .param_scan_axis ), input_layers )
361+ def _slice_leaf (x , spec ):
362+ scan_axis = get_scan_axis (config , spec , x )
363+ if scan_axis is not None :
364+ return jnp .take (x , i , axis = scan_axis )
365+ return x
366+
367+ return jax .tree_util .tree_map (
368+ _slice_leaf ,
369+ input_layers ,
370+ layers_annotations ,
371+ is_leaf = lambda x : isinstance (x , jax .sharding .PartitionSpec ),
372+ )
296373
297374 # pylint: disable=not-callable
298375 new_layer = jax .jit (slice_ith , out_shardings = new_layer_sharding )(layers )
0 commit comments