Skip to content

Commit f0314c0

Browse files
ecnal-cienetcj401-amd
authored andcommitted
NNX: flip pure_nnx/enable_nnx/pure_nnx_decoder defaults to True
PR6-PR10 promoted every routed-to-Linen feature to NNX-native; PR#2885 lands NNX-native pipeline parallelism. This PR flips the three defaults in base.yml so NNX is the production path, and bundles the NNX-only fixes that surface once pure_nnx=True (DiLoCo merge/checkpoint, Zero-1 input shardings on flat nnx.State, MTP sown-Variable handling, generate_param_only_checkpoint NNX flow, maxengine Linen-parity removal).
1 parent 0b17bde commit f0314c0

28 files changed

Lines changed: 543 additions & 359 deletions

src/maxtext/checkpoint_conversion/to_maxtext.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -320,22 +320,20 @@ def get_maxtext_model_info(config):
320320
# Get abstract model structure (name, shape) without materializing the weights to save memory
321321
abstract_params_tree = maxtext_utils.get_abstract_param(maxtext_model_flax, config)["params"]
322322

323-
abstract_params_flat, _ = jax.tree_util.tree_flatten_with_path(abstract_params_tree)
324-
# Standardize abstract tree for later unflattening
325-
abstract_params_tree = jax.tree.map(
326-
lambda _: 0,
327-
abstract_params_tree,
328-
is_leaf=lambda x: isinstance(x, nn.LogicallyPartitioned),
323+
abstract_params_flat, abstract_params_treedef = jax.tree_util.tree_flatten_with_path(
324+
abstract_params_tree, is_leaf=lambda x: isinstance(x, nn.LogicallyPartitioned)
329325
)
330-
abstract_params_treedef = jax.tree_util.tree_structure(abstract_params_tree)
331326

332327
max_logging.log("MaxText abstract model and state initialized.")
333328

334329
# preprocess state
335330
maxtext_abstract_dict = {}
336331
for mt_target_idx, (path_tuple, abstract_leaf_value) in enumerate(abstract_params_flat):
337332
mt_param_key = "params-" + "-".join(param_key_parts_from_path(path_tuple))
338-
mt_target_shape = abstract_leaf_value.shape
333+
if isinstance(abstract_leaf_value, nn.LogicallyPartitioned):
334+
mt_target_shape = abstract_leaf_value.value.shape
335+
else:
336+
mt_target_shape = abstract_leaf_value.shape
339337
maxtext_abstract_dict[mt_param_key] = (mt_target_idx, mt_target_shape)
340338

341339
return maxtext_abstract_dict, abstract_params_treedef

src/maxtext/checkpoint_conversion/utils/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -968,7 +968,7 @@ def extract_nnx_weights(weights_dict: dict) -> dict[str, np.ndarray]:
968968
for path_tuple, leaf_value in leaves_with_paths:
969969
path_keys = param_key_parts_from_path(path_tuple)
970970
# Skip NNX RNG state variables (not model weights)
971-
if "to_nnx__rngs" in path_keys or any(k.endswith("_rngs") for k in path_keys):
971+
if "to_nnx__rngs" in path_keys or any(k == "rngs" or k.endswith("_rngs") for k in path_keys):
972972
continue
973973
maxtext_param_key = "params-" + "-".join(path_keys)
974974
if not isinstance(leaf_value, (jax.Array, np.ndarray)):

src/maxtext/common/checkpointing.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
1516
"""Create an Orbax CheckpointManager with specified (Async or not) Checkpointer."""
1617

1718
import time
@@ -357,11 +358,16 @@ def combine_sharding(sds, shardings):
357358
use_ocdbt=use_ocdbt,
358359
use_zarr3=use_zarr3,
359360
)
361+
# NNX checkpoints are saved as pure dicts (see maybe_save_checkpoint); the
362+
# restore target must match — a boxed nnx.State wouldn't.
363+
restore_target = abstract_unboxed_pre_state
364+
if isinstance(abstract_unboxed_pre_state, nnx.State):
365+
restore_target = abstract_unboxed_pre_state.to_pure_dict()
360366
# Provide sharding info to ensure restoration returns JAX arrays (not NumPy arrays).
361367
restore_args = jax.tree_util.tree_map(
362-
lambda x: ocp.type_handlers.ArrayRestoreArgs(sharding=x.sharding), abstract_unboxed_pre_state
368+
lambda x: ocp.type_handlers.ArrayRestoreArgs(sharding=x.sharding), restore_target
363369
)
364-
return ocp.Checkpointer(handler).restore(p, abstract_unboxed_pre_state, restore_args=restore_args)
370+
return ocp.Checkpointer(handler).restore(p, restore_target, restore_args=restore_args)
365371

366372

367373
def create_orbax_checkpoint_manager(
@@ -1028,7 +1034,8 @@ def maybe_save_checkpoint(checkpoint_manager, state, config, data_iterator, step
10281034
actual_step = int(step)
10291035
else:
10301036
if config.pure_nnx:
1031-
actual_step = int(state.optimizer.step) - 1
1037+
# Under DiLoCo the step lives on the DiLoCoTrainState; otherwise on the optimizer.
1038+
actual_step = int(state.step if config.enable_diloco else state.optimizer.step) - 1
10321039
else:
10331040
# Linen TrainState has .step attribute
10341041
actual_step = int(state.step) - 1
@@ -1039,7 +1046,13 @@ def maybe_save_checkpoint(checkpoint_manager, state, config, data_iterator, step
10391046

10401047
if config.pure_nnx:
10411048
# Save in the Linen on-disk layout so pure_nnx and Linen checkpoints are interchangeable.
1042-
state = train_state_nnx.to_linen_checkpoint_dict(state.to_pure_dict())
1049+
if config.enable_diloco:
1050+
# DiLoCoTrainState: persist the synchronized global model (outer params).
1051+
# The per-replica inner optimizer / outer-momentum state is not checkpointed.
1052+
step_value = state.step.get_value() if hasattr(state.step, "get_value") else state.step
1053+
state = train_state_nnx.to_linen_checkpoint_dict({"model": state.params, "optimizer": {"step": step_value}})
1054+
else:
1055+
state = train_state_nnx.to_linen_checkpoint_dict(state.to_pure_dict())
10431056

10441057
# Determine if a checkpoint save should be forced, overriding the usual `config.checkpoint_period` logic.
10451058
# This occurs if this function was called:

src/maxtext/configs/base.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1236,9 +1236,9 @@ position_id_per_seconds: 25
12361236
subslice_shape: ""
12371237

12381238
# NNX
1239-
enable_nnx: false
1240-
pure_nnx_decoder: false
1241-
pure_nnx: false
1239+
enable_nnx: true
1240+
pure_nnx_decoder: true
1241+
pure_nnx: true
12421242

12431243
################################## Qwen3-Next Specific Configs ##################################
12441244
# Kernel size for the 1D convolution in the Gated Delta Net

src/maxtext/layers/nnx_decoders.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
from maxtext.layers.quantizations import AqtQuantization as Quant
4646
from maxtext.models import (
4747
deepseek,
48+
deepseek4,
4849
deepseek_batchsplit,
4950
deepseek_batchsplit_fp8,
5051
gemma,
@@ -1121,6 +1122,7 @@ def get_deepseek():
11211122
DecoderBlockType.SIMPLE: [simple_layer.SimpleDecoderLayer],
11221123
DecoderBlockType.SIMPLE_MLP: [simple_layer.SimpleMlpDecoderLayer],
11231124
DecoderBlockType.DEEPSEEK: get_deepseek(),
1125+
DecoderBlockType.DEEPSEEK4: get_scannable(deepseek4.DeepSeek4DecoderLayer, deepseek4.DeepSeek4ScannableBlock),
11241126
DecoderBlockType.GPT_OSS: get_scannable(gpt_oss.GptOssDecoderLayer, gpt_oss.GptOssScannableBlock),
11251127
DecoderBlockType.QWEN3_NEXT: get_scannable(qwen3.Qwen3NextDecoderLayer, qwen3.Qwen3NextScannableBlock),
11261128
DecoderBlockType.QWEN3_5: get_scannable(qwen3_5.Qwen3_5DecoderLayer, qwen3_5.Qwen3_5ScannableBlock),

src/maxtext/layers/nnx_wrappers.py

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
from flax.core import FrozenDict
2727
from flax.core import meta
2828
from flax.nnx import graph
29-
from flax.nnx import tracers as nnx_tracers
3029
from flax.nnx import variablelib
3130
from flax.nnx.bridge import module as bdg_module
3231
from flax.nnx.module import Module
@@ -183,19 +182,6 @@ def is_linen_initializing() -> bool:
183182
return False
184183

185184

186-
def _refresh_variable_trace_state(module: Module) -> None:
187-
"""Resets stale ``_trace_state`` on Variables to unblock downstream ``nnx.split``.
188-
189-
``nnx.update`` called with JAX tracer values uses ``_unsafe_bypass_check=True``,
190-
which leaves Variables with a stale ``_trace_state`` from the outer Python
191-
context and breaks ``nnx.split`` with "Cannot extract graph node from different
192-
trace level". Resets ``_trace_state`` on any Variable whose ``_can_update`` is False.
193-
"""
194-
for _, v in nnx.graph.iter_graph(module):
195-
if isinstance(v, variablelib.Variable) and not v._can_update: # pylint: disable=protected-access
196-
object.__setattr__(v, "_trace_state", nnx_tracers.TraceState())
197-
198-
199185
class ToNNX(Module):
200186
"""A wrapper to turn any Linen module into an NNX module.
201187
@@ -542,9 +528,17 @@ def maybe_unbox(x):
542528
f"Found unknown module paths in incoming state:{paths_str}. Intermediate modules have been reconstructed."
543529
)
544530

531+
# Filter out unknown paths so we don't try to assign them to static attributes
532+
filtered_state_flat = {k: v for k, v in new_state_flat.items() if k not in unknown_state_flat}
533+
new_state = nnx.State(nnx.traversals.unflatten_mapping(filtered_state_flat))
534+
535+
# Rebind the module to the current trace via split / update / merge.
536+
# nnx.update directly on the live module can leave stale tracers.
537+
graphdef, full_state = nnx.split(module)
538+
nnx.update(full_state, new_state)
539+
module = nnx.merge(graphdef, full_state)
540+
545541
_fix_for_qwix_quantization(module)
546-
nnx.update(module, new_state)
547-
_refresh_variable_trace_state(module)
548542
method_fn = _get_module_method(module, nnx_method)
549543
out = method_fn(module, *args, **kwargs)
550544
self._update_variables(module)

src/maxtext/layers/quantizations.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,8 +1031,7 @@ def generate_quantizer_set(
10311031
fp8_recipe: recipe.Recipe | None = default_recipe,
10321032
n_groups: int | None = None,
10331033
):
1034-
"""Generates a set of quantizers for TransformerEngine."""
1035-
1034+
"""Route quantizer-set generation through the OVERWRITE_WITH_GRADIENT collection."""
10361035
OVERWRITE_WITH_GRADIENT = "_overwrite_with_gradient"
10371036
return super().generate_quantizer_set( # pytype: disable=wrong-keyword-args
10381037
postfix=postfix,

src/maxtext/trainers/diloco/diloco.py

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@
2828
import drjax
2929
from flax import nnx
3030
from flax import struct
31-
from flax.training import train_state
3231
import jax
3332
import jax.numpy as jnp
3433
from jaxtyping import Array, Int32, Key, PyTree, UInt32
3534
import optax
3635

3736
from maxtext.configs import pyconfig
37+
from maxtext.common.train_state_nnx import TrainStateNNX
3838

3939
Batch = Any
4040
Params = 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),

src/maxtext/trainers/pre_train/train.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@
8585
def get_first_step(model, state):
8686
if isinstance(model, nn.Module):
8787
return int(state.step)
88+
if hasattr(state, "inner_state"): # DiLoCoTrainState (NNX DiLoCo): step is the optimizer step var
89+
return int(state.step.get_value())
8890
return int(state.optimizer.step.get_value())
8991

9092

@@ -689,7 +691,8 @@ def training_loop_iteration(
689691

690692
with jax.profiler.StepTraceAnnotation("train", step_num=step):
691693
example_batch = data_loader.load_next_batch(rampup_manager=rampup_manager)
692-
if isinstance(model, nn.Module):
694+
# DiLoCo's inner step takes the rng like the Linen step does.
695+
if isinstance(model, nn.Module) or config.enable_diloco:
693696
# pylint: disable=not-callable
694697
step_rng_args = (jax.jit(jax.random.fold_in)(init_rng, step),)
695698
else:
@@ -776,10 +779,18 @@ def train_loop(config, recorder, state=None):
776779

777780
if isinstance(model, nn.Module):
778781
jit_model = model
782+
elif config.enable_diloco:
783+
# state is the DiLoCoTrainState; `model` is already the TrainStateNNX graphdef the inner step needs.
784+
jit_model = model
779785
else:
780786
jit_model, state = nnx.split(state)
781787

782-
params_shardings, state_mesh_shardings = sharding.maybe_update_params_sharding_with_opt(config, state_mesh_shardings)
788+
if config.pure_nnx and config.enable_diloco:
789+
# DiLoCoTrainState.params already holds the param shardings the inner step needs;
790+
# the Zero-1 opt overlay doesn't apply through the diloco wrapper.
791+
params_shardings = state_mesh_shardings.params
792+
else:
793+
params_shardings, state_mesh_shardings = sharding.maybe_update_params_sharding_with_opt(config, state_mesh_shardings)
783794

784795
p_train_step, p_eval_step = train_utils.jit_train_and_eval_step(
785796
config,
@@ -801,7 +812,8 @@ def train_loop(config, recorder, state=None):
801812
elif config.shard_optimizer_over_data:
802813
# NNX: reshard state so params match the data-sharded in_shardings (Zero-1 layout)
803814
state = jax.device_put(state, state_mesh_shardings)
804-
if isinstance(model, nn.Module):
815+
if isinstance(model, nn.Module) or config.enable_diloco:
816+
# The DiLoCo train step takes (state, batch, rng), like the Linen step.
805817
lower_args = (state, shaped_batch, init_rng)
806818
else:
807819
lower_args = (state, shaped_batch)
@@ -817,6 +829,8 @@ def train_loop(config, recorder, state=None):
817829
# Write train config params, num model params, and XLA flags to tensorboard
818830
if isinstance(model, nn.Module):
819831
setup_params = state.params
832+
elif config.enable_diloco:
833+
setup_params = state.params # DiLoCoTrainState.params: the outer (global) params
820834
else:
821835
_, setup_params, _ = nnx.split(state.model, nnx.Param, ...)
822836
metric_logger_instance.write_setup_info_to_tensorboard(setup_params)

src/maxtext/trainers/pre_train/train_compile.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -265,12 +265,9 @@ def is_oom(argv: Sequence[str]) -> bool:
265265
# Update params_shardings when shard_optimizer_over_data is enabled (Zero-1)
266266
params_shardings, state_mesh_shardings = sharding.maybe_update_params_sharding_with_opt(config, state_mesh_shardings)
267267

268-
# When ZeRO-1 is enabled, we need to use the original params_shardings for input shardings
269-
# but keep the updated state_mesh_shardings for the optimizer state
270-
if config.shard_optimizer_over_data:
271-
input_state_mesh_shardings = state_mesh_shardings.replace(params=params_shardings)
272-
else:
273-
input_state_mesh_shardings = state_mesh_shardings
268+
input_state_mesh_shardings = sharding.build_zero1_input_state_mesh_shardings(
269+
config, state_mesh_shardings, params_shardings
270+
)
274271

275272
# Get data sharding
276273
data_sharding = sharding.get_input_data_sharding(config, topology_mesh)
@@ -347,12 +344,9 @@ def main(argv: Sequence[str]) -> None:
347344
# Update params_shardings when shard_optimizer_over_data is enabled (Zero-1)
348345
params_shardings, state_mesh_shardings = sharding.maybe_update_params_sharding_with_opt(config, state_mesh_shardings)
349346

350-
# When ZeRO-1 is enabled, we need to use the original params_shardings for input shardings
351-
# but keep the updated state_mesh_shardings for the optimizer state
352-
if config.shard_optimizer_over_data:
353-
input_state_mesh_shardings = state_mesh_shardings.replace(params=params_shardings)
354-
else:
355-
input_state_mesh_shardings = state_mesh_shardings
347+
input_state_mesh_shardings = sharding.build_zero1_input_state_mesh_shardings(
348+
config, state_mesh_shardings, params_shardings
349+
)
356350

357351
# Get data sharding
358352
data_sharding = sharding.get_input_data_sharding(config, topology_mesh)

0 commit comments

Comments
 (0)