2828import drjax
2929from flax import nnx
3030from flax import struct
31- from flax .training import train_state
3231import jax
3332import jax .numpy as jnp
3433from jaxtyping import Array , Int32 , Key , PyTree , UInt32
3534import optax
3635
3736from maxtext .configs import pyconfig
37+ from maxtext .common .train_state_nnx import TrainStateNNX
3838
3939Batch = Any
4040Params = PyTree
@@ -157,8 +157,10 @@ def add_diloco_dim(x):
157157 # For NNX, model params (Param variables only) live under abstract_state.model;
158158 # for Linen under abstract_state.params.
159159 if config .pure_nnx :
160- model_params = abstract_state .model .filter (nnx .Param )
161- model_params_sharding = state_mesh_shardings .model .filter (nnx .Param )
160+ _ , model_params , _ = nnx .split (abstract_state .model , nnx .Param , ...)
161+ model_params = model_params .to_pure_dict ()
162+ _ , model_params_sharding , _ = nnx .split (state_mesh_shardings .model , nnx .Param , ...)
163+ model_params_sharding = model_params_sharding .to_pure_dict ()
162164 else :
163165 model_params = abstract_state .params
164166 model_params_sharding = state_mesh_shardings .params
@@ -216,7 +218,11 @@ def init_diloco_state() -> tuple[DiLoCoTrainState, PyTree]:
216218 # Outer state retains a single copy of the model parameters and optimizer state.
217219 # For NNX, model params (Param variables only) live under state.model;
218220 # for Linen under state.params.
219- outer_params = state .model .filter (nnx .Param ) if config .pure_nnx else state .params
221+ if config .pure_nnx :
222+ _ , outer_params , _ = nnx .split (state .model , nnx .Param , ...)
223+ outer_params = outer_params .to_pure_dict ()
224+ else :
225+ outer_params = state .params
220226 outer_opt_state = outer_optimizer .init (outer_params )
221227 outer_opt_state_sharding = jax .tree_util .tree_map (lambda x : x .sharding , outer_opt_state )
222228 # For NNX, the step counter lives at state.optimizer.step; for Linen at state.step.
@@ -258,9 +264,11 @@ def synchronize(state):
258264 # state (since last synchronization).
259265 broadcast_outer_params = drjax .broadcast (state .params , mesh = mesh )
260266 # For NNX, model Param vars live under inner_state.model; for Linen under inner_state.params.
261- inner_model_params = (
262- nnx .filter_state (state .inner_state .model , nnx .Param ) if config .pure_nnx else state .inner_state .params
263- )
267+ if config .pure_nnx :
268+ _ , inner_model_params , _ = nnx .split (state .inner_state .model , nnx .Param , ...)
269+ inner_model_params = inner_model_params .to_pure_dict ()
270+ else :
271+ inner_model_params = state .inner_state .params
264272 model_delta = jax .tree .map (lambda x , y : y - x , inner_model_params , broadcast_outer_params )
265273 # Treat the average delta as the outer optimizer's gradient and apply to
266274 # the global (outer) model params.
@@ -273,15 +281,34 @@ def synchronize(state):
273281 if config .pure_nnx :
274282 # For NNX: merge new Param vars back with the non-Param model vars (e.g. RNG state).
275283 def replace_nnx_model_params (s , new_params ):
276- non_param_model = nnx .filter_state (s .model , nnx .Not (nnx .Param ))
277- new_model = nnx .merge_state (non_param_model , new_params )
278- # Assign via __setitem__ so nested States are stored as plain dicts (matching
279- # nnx.state()'s pytree structure). The dict-literal constructor keeps them as
280- # State objects, which makes jax.lax.cond see mismatched pytree structures.
281- result = type (s )({})
282- result ["model" ] = new_model
283- result ["optimizer" ] = s ["optimizer" ]
284- return result
284+ s_model = s ["model" ] if hasattr (s , "keys" ) else s .model
285+ s_opt = s ["optimizer" ] if hasattr (s , "keys" ) else s .optimizer
286+
287+ graphdef , _ , non_param_state = nnx .split (s_model , nnx .Param , ...)
288+ new_model = nnx .merge (graphdef , new_params , non_param_state )
289+
290+ if type (s_model ).__name__ == "State" :
291+ new_model = nnx .state (new_model )
292+ elif isinstance (s_model , dict ):
293+ new_model = nnx .to_pure_dict (new_model )
294+
295+ if hasattr (s , "keys" ):
296+ # Replace "model" leaves by path, keeping s's treedef. Picking by position
297+ # (leaves[N:]) breaks if a key sorts before "model"; reconstructing via
298+ # type(s)({...}) breaks the lax.cond match — nnx.State recursive-wraps.
299+ leaves_with_paths , treedef = jax .tree_util .tree_flatten_with_path (s )
300+ new_model_iter = iter (jax .tree_util .tree_leaves (new_model ))
301+
302+ def _is_model_leaf (path ):
303+ if not path :
304+ return False
305+ k = path [0 ]
306+ return getattr (k , "key" , None ) == "model" or getattr (k , "name" , None ) == "model"
307+
308+ new_leaves = [next (new_model_iter ) if _is_model_leaf (p ) else leaf for p , leaf in leaves_with_paths ]
309+ return jax .tree_util .tree_unflatten (treedef , new_leaves )
310+ else :
311+ return TrainStateNNX (new_model , s_opt )
285312
286313 new_inner_state = drjax .map_fn (
287314 lambda s : replace_nnx_model_params (s , new_outer_params ),
0 commit comments