Skip to content

Commit b2d99ea

Browse files
Merge pull request AI-Hypercomputer#3526 from AI-Hypercomputer:feat/nnx-set-defaults-true
PiperOrigin-RevId: 940139961
2 parents cccfc67 + b1ac62a commit b2d99ea

29 files changed

Lines changed: 839 additions & 456 deletions

src/maxtext/checkpoint_conversion/to_maxtext.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -320,22 +320,23 @@ 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 = (
324+
jax.tree_util.tree_flatten_with_path(
325+
abstract_params_tree,
326+
is_leaf=lambda x: isinstance(x, nn.LogicallyPartitioned),
327+
)
329328
)
330-
abstract_params_treedef = jax.tree_util.tree_structure(abstract_params_tree)
331329

332330
max_logging.log("MaxText abstract model and state initialized.")
333331

334332
# preprocess state
335333
maxtext_abstract_dict = {}
336334
for mt_target_idx, (path_tuple, abstract_leaf_value) in enumerate(abstract_params_flat):
337335
mt_param_key = "params-" + "-".join(param_key_parts_from_path(path_tuple))
338-
mt_target_shape = abstract_leaf_value.shape
336+
if isinstance(abstract_leaf_value, nn.LogicallyPartitioned):
337+
mt_target_shape = abstract_leaf_value.value.shape
338+
else:
339+
mt_target_shape = abstract_leaf_value.shape
339340
maxtext_abstract_dict[mt_param_key] = (mt_target_idx, mt_target_shape)
340341

341342
return maxtext_abstract_dict, abstract_params_treedef

src/maxtext/checkpoint_conversion/utils/utils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -968,7 +968,9 @@ 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(
972+
k == "rngs" or k.endswith("_rngs") for k in path_keys
973+
):
972974
continue
973975
maxtext_param_key = "params-" + "-".join(path_keys)
974976
if not isinstance(leaf_value, (jax.Array, np.ndarray)):

src/maxtext/common/checkpointing.py

Lines changed: 28 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,19 @@ 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),
369+
restore_target,
370+
)
371+
return ocp.Checkpointer(handler).restore(
372+
p, restore_target, restore_args=restore_args
363373
)
364-
return ocp.Checkpointer(handler).restore(p, abstract_unboxed_pre_state, restore_args=restore_args)
365374

366375

367376
def create_orbax_checkpoint_manager(
@@ -1028,7 +1037,10 @@ def maybe_save_checkpoint(checkpoint_manager, state, config, data_iterator, step
10281037
actual_step = int(step)
10291038
else:
10301039
if config.pure_nnx:
1031-
actual_step = int(state.optimizer.step) - 1
1040+
# Under DiLoCo the step lives on the DiLoCoTrainState; otherwise on the optimizer.
1041+
actual_step = (
1042+
int(state.step if config.enable_diloco else state.optimizer.step) - 1
1043+
)
10321044
else:
10331045
# Linen TrainState has .step attribute
10341046
actual_step = int(state.step) - 1
@@ -1039,7 +1051,19 @@ def maybe_save_checkpoint(checkpoint_manager, state, config, data_iterator, step
10391051

10401052
if config.pure_nnx:
10411053
# 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())
1054+
if config.enable_diloco:
1055+
# DiLoCoTrainState: persist the synchronized global model (outer params).
1056+
# The per-replica inner optimizer / outer-momentum state is not checkpointed.
1057+
step_value = (
1058+
state.step.get_value()
1059+
if hasattr(state.step, "get_value")
1060+
else state.step
1061+
)
1062+
state = train_state_nnx.to_linen_checkpoint_dict(
1063+
{"model": state.params, "optimizer": {"step": step_value}}
1064+
)
1065+
else:
1066+
state = train_state_nnx.to_linen_checkpoint_dict(state.to_pure_dict())
10431067

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

src/maxtext/configs/base.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,7 @@ shard_mode: "auto" # can be either auto or explicit
494494
custom_mesh_and_rule: "" # replace default mesh and logical rule by specifying yml name under config/mesh_and_rule/.
495495
mesh_axes: ['diloco', 'data', 'stage', 'fsdp', 'fsdp_transpose', 'context', 'context_autoregressive', 'tensor', 'tensor_transpose', 'tensor_sequence', 'expert', 'autoregressive']
496496
logical_axis_rules: [
497+
['circular_repeats', []],
497498
# ==========================================
498499
# Vocabulary Embedding
499500
# ==========================================
@@ -1235,9 +1236,9 @@ position_id_per_seconds: 25
12351236
subslice_shape: ""
12361237

12371238
# NNX
1238-
enable_nnx: false
1239-
pure_nnx_decoder: false
1240-
pure_nnx: false
1239+
enable_nnx: true
1240+
pure_nnx_decoder: true
1241+
pure_nnx: true
12411242

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

src/maxtext/layers/nnx_decoders.py

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,33 +18,34 @@
1818

1919
import functools
2020
import inspect
21-
import warnings
2221
from typing import Any
22+
import warnings
2323

24-
import jax
25-
import jax.numpy as jnp
2624
from flax import linen as nn
2725
from flax import nnx
28-
from maxtext.layers import nnx_wrappers
26+
import jax
2927
from jax.ad_checkpoint import checkpoint_name
28+
import jax.numpy as jnp
3029
from jax.sharding import Mesh
31-
3230
from maxtext.common.common_types import (
31+
Config,
32+
DecoderBlockType,
3333
MODEL_MODE_AUTOREGRESSIVE,
3434
MODEL_MODE_PREFILL,
3535
MODEL_MODE_TRAIN,
36-
Config,
37-
DecoderBlockType,
3836
MultimodalInput,
3937
ShardMode,
4038
)
4139
from maxtext.layers import initializers, linears, mhc, normalizations, quantizations
40+
from maxtext.layers import nnx_wrappers
4241
from maxtext.layers.attentions import Attention
4342
from maxtext.layers.embeddings import Embed, PositionalEmbedding, attend_on_embedding
4443
from maxtext.layers.normalizations import RMSNorm
44+
from maxtext.layers.pipeline import create_nnx_pipeline
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,
@@ -68,7 +69,6 @@
6869
from maxtext.multimodal import utils as mm_utils
6970
from maxtext.utils import max_logging, max_utils, maxtext_utils, maxtext_utils_nnx, sharding
7071
from maxtext.utils.sharding import create_sharding
71-
from maxtext.layers.pipeline import create_nnx_pipeline
7272

7373
# ------------------------------------------------------------------------------
7474
# The network: Decoder Definitions
@@ -1111,21 +1111,38 @@ def get_deepseek():
11111111
DecoderBlockType.GEMMA: [gemma.GemmaDecoderLayer],
11121112
DecoderBlockType.GEMMA2: [gemma2.Gemma2DecoderLayer],
11131113
DecoderBlockType.GEMMA3: [gemma3.Gemma3DecoderLayer],
1114-
DecoderBlockType.GEMMA4: get_scannable(gemma4.Gemma4DecoderLayer, gemma4.Gemma4ScannableBlock),
1114+
DecoderBlockType.GEMMA4: get_scannable(
1115+
gemma4.Gemma4DecoderLayer, gemma4.Gemma4ScannableBlock
1116+
),
11151117
DecoderBlockType.GEMMA4_SMALL: [gemma4_small.Gemma4SmallDecoderLayer],
11161118
DecoderBlockType.GPT3: [gpt3.Gpt3DecoderLayer],
11171119
DecoderBlockType.QWEN2: [qwen2.Qwen2DecoderLayer],
11181120
DecoderBlockType.QWEN3: [qwen3.Qwen3DecoderLayer],
11191121
DecoderBlockType.QWEN3_MOE: [qwen3.Qwen3MoeDecoderLayer],
1120-
DecoderBlockType.QWEN3_CUSTOM_MOE: [qwen3_custom.Qwen3CustomMoeDecoderLayer],
1122+
DecoderBlockType.QWEN3_CUSTOM_MOE: [
1123+
qwen3_custom.Qwen3CustomMoeDecoderLayer
1124+
],
11211125
DecoderBlockType.SIMPLE: [simple_layer.SimpleDecoderLayer],
11221126
DecoderBlockType.SIMPLE_MLP: [simple_layer.SimpleMlpDecoderLayer],
11231127
DecoderBlockType.DEEPSEEK: get_deepseek(),
1124-
DecoderBlockType.GPT_OSS: get_scannable(gpt_oss.GptOssDecoderLayer, gpt_oss.GptOssScannableBlock),
1125-
DecoderBlockType.QWEN3_NEXT: get_scannable(qwen3.Qwen3NextDecoderLayer, qwen3.Qwen3NextScannableBlock),
1126-
DecoderBlockType.QWEN3_5: get_scannable(qwen3_5.Qwen3_5DecoderLayer, qwen3_5.Qwen3_5ScannableBlock),
1127-
DecoderBlockType.LLAMA4: get_scannable(llama4.Llama4DecoderLayer, llama4.Llama4ScannableBlock),
1128-
DecoderBlockType.OLMO3: get_scannable(olmo3.Olmo3DecoderLayer, olmo3.Olmo3ScannableBlock),
1128+
DecoderBlockType.DEEPSEEK4: get_scannable(
1129+
deepseek4.DeepSeek4DecoderLayer, deepseek4.DeepSeek4ScannableBlock
1130+
),
1131+
DecoderBlockType.GPT_OSS: get_scannable(
1132+
gpt_oss.GptOssDecoderLayer, gpt_oss.GptOssScannableBlock
1133+
),
1134+
DecoderBlockType.QWEN3_NEXT: get_scannable(
1135+
qwen3.Qwen3NextDecoderLayer, qwen3.Qwen3NextScannableBlock
1136+
),
1137+
DecoderBlockType.QWEN3_5: get_scannable(
1138+
qwen3_5.Qwen3_5DecoderLayer, qwen3_5.Qwen3_5ScannableBlock
1139+
),
1140+
DecoderBlockType.LLAMA4: get_scannable(
1141+
llama4.Llama4DecoderLayer, llama4.Llama4ScannableBlock
1142+
),
1143+
DecoderBlockType.OLMO3: get_scannable(
1144+
olmo3.Olmo3DecoderLayer, olmo3.Olmo3ScannableBlock
1145+
),
11291146
}
11301147

11311148
if cfg.decoder_block not in layer_map:

src/maxtext/layers/nnx_wrappers.py

Lines changed: 14 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,21 @@ 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 = {
533+
k: v for k, v in new_state_flat.items() if k not in unknown_state_flat
534+
}
535+
new_state = nnx.State(
536+
nnx.traversals.unflatten_mapping(filtered_state_flat)
537+
)
538+
539+
# Rebind the module to the current trace via split / update / merge.
540+
# nnx.update directly on the live module can leave stale tracers.
541+
graphdef, full_state = nnx.split(module)
542+
nnx.update(full_state, new_state)
543+
module = nnx.merge(graphdef, full_state)
544+
545545
_fix_for_qwix_quantization(module)
546-
nnx.update(module, new_state)
547-
_refresh_variable_trace_state(module)
548546
method_fn = _get_module_method(module, nnx_method)
549547
out = method_fn(module, *args, **kwargs)
550548
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: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,12 @@
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
35-
import optax
36-
34+
from maxtext.common.train_state_nnx import TrainStateNNX
3735
from maxtext.configs import pyconfig
36+
import optax
3837

3938
Batch = Any
4039
Params = PyTree
@@ -157,8 +156,12 @@ def add_diloco_dim(x):
157156
# For NNX, model params (Param variables only) live under abstract_state.model;
158157
# for Linen under abstract_state.params.
159158
if config.pure_nnx:
160-
model_params = abstract_state.model.filter(nnx.Param)
161-
model_params_sharding = state_mesh_shardings.model.filter(nnx.Param)
159+
_, model_params, _ = nnx.split(abstract_state.model, nnx.Param, ...)
160+
model_params = model_params.to_pure_dict()
161+
_, model_params_sharding, _ = nnx.split(
162+
state_mesh_shardings.model, nnx.Param, ...
163+
)
164+
model_params_sharding = model_params_sharding.to_pure_dict()
162165
else:
163166
model_params = abstract_state.params
164167
model_params_sharding = state_mesh_shardings.params
@@ -216,7 +219,11 @@ def init_diloco_state() -> tuple[DiLoCoTrainState, PyTree]:
216219
# Outer state retains a single copy of the model parameters and optimizer state.
217220
# For NNX, model params (Param variables only) live under state.model;
218221
# for Linen under state.params.
219-
outer_params = state.model.filter(nnx.Param) if config.pure_nnx else state.params
222+
if config.pure_nnx:
223+
_, outer_params, _ = nnx.split(state.model, nnx.Param, ...)
224+
outer_params = outer_params.to_pure_dict()
225+
else:
226+
outer_params = state.params
220227
outer_opt_state = outer_optimizer.init(outer_params)
221228
outer_opt_state_sharding = jax.tree_util.tree_map(lambda x: x.sharding, outer_opt_state)
222229
# For NNX, the step counter lives at state.optimizer.step; for Linen at state.step.
@@ -258,9 +265,13 @@ def synchronize(state):
258265
# state (since last synchronization).
259266
broadcast_outer_params = drjax.broadcast(state.params, mesh=mesh)
260267
# 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-
)
268+
if config.pure_nnx:
269+
_, inner_model_params, _ = nnx.split(
270+
state.inner_state.model, nnx.Param, ...
271+
)
272+
inner_model_params = inner_model_params.to_pure_dict()
273+
else:
274+
inner_model_params = state.inner_state.params
264275
model_delta = jax.tree.map(lambda x, y: y - x, inner_model_params, broadcast_outer_params)
265276
# Treat the average delta as the outer optimizer's gradient and apply to
266277
# the global (outer) model params.
@@ -273,15 +284,40 @@ def synchronize(state):
273284
if config.pure_nnx:
274285
# For NNX: merge new Param vars back with the non-Param model vars (e.g. RNG state).
275286
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
287+
s_model = s["model"] if hasattr(s, "keys") else s.model
288+
s_opt = s["optimizer"] if hasattr(s, "keys") else s.optimizer
289+
290+
graphdef, _, non_param_state = nnx.split(s_model, nnx.Param, ...)
291+
new_model = nnx.merge(graphdef, new_params, non_param_state)
292+
293+
if type(s_model).__name__ == "State":
294+
new_model = nnx.state(new_model)
295+
elif isinstance(s_model, dict):
296+
new_model = nnx.to_pure_dict(new_model)
297+
298+
if hasattr(s, "keys"):
299+
# Replace "model" leaves by path, keeping s's treedef. Picking by position
300+
# (leaves[N:]) breaks if a key sorts before "model"; reconstructing via
301+
# type(s)({...}) breaks the lax.cond match — nnx.State recursive-wraps.
302+
leaves_with_paths, treedef = jax.tree_util.tree_flatten_with_path(s)
303+
new_model_iter = iter(jax.tree_util.tree_leaves(new_model))
304+
305+
def _is_model_leaf(path):
306+
if not path:
307+
return False
308+
k = path[0]
309+
return (
310+
getattr(k, "key", None) == "model"
311+
or getattr(k, "name", None) == "model"
312+
)
313+
314+
new_leaves = [
315+
next(new_model_iter) if _is_model_leaf(p) else leaf
316+
for p, leaf in leaves_with_paths
317+
]
318+
return jax.tree_util.tree_unflatten(treedef, new_leaves)
319+
else:
320+
return TrainStateNNX(new_model, s_opt)
285321

286322
new_inner_state = drjax.map_fn(
287323
lambda s: replace_nnx_model_params(s, new_outer_params),

0 commit comments

Comments
 (0)