Skip to content

Commit 4ed6aaa

Browse files
committed
NNX: correctness fixes, enable feature paths, and vocab tiling on NNX
Bug fixes (run as no-op while pure_nnx=False stays default): - nnx_wrappers.py: add _refresh_variable_trace_state + is_linen_initializing; call from ToLinen after nnx.update to fix "Cannot extract graph node from different trace level" when grad tracers leak into Variable._trace_state. - gpt_oss.py / olmo3.py: replace inline nn.Dropout(...) with self.dropout = linears.Dropout(...) in __init__ to fix CallCompactUnboundModuleError. - normalizations.py: Qwen3NextRMSNorm signature: eps -> epsilon, accept shard_mode/kernel_axes/parameter_memory_host_offload for callsite parity. - attentions.py / qwen3.py: callsites eps= -> epsilon=. - moe.py: per_expert_scale block moved into the unfused-kernel else branch (was scaling wo even when fused_kernel was active). - models.py: build MTP block as MultiTokenPredictionBlock(...) directly (drop the ToNNX(linen) + lazy_init wrap); pass multimodal_input whole to NNXDecoder instead of unpacking 5 fields. - gradient_accumulation.py: ZeRO-1+GA all-reduce annotation deferred until after lax.scan (reduced/unreduced PartitionSpec is rejected inside scan carry); use nnx.merge(..., copy=True) to avoid Variable reuse. - diloco.py: NNX-aware state handling — state.params -> state.model.filter (nnx.Param), step counter at state.optimizer.step, replace_nnx_model_params helper for jax.lax.cond pytree-structure parity. - train_compile.py: new _collect_nnx_activation_shardings helper (forward pass populates _ACTIVATION_SHARDINGS_DUMP — get_abstract_state_nnx only traces __init__); NNX path now passes 2-arg shaped_train_args (no rng); diloco path patched to handle the 2-vs-3 length difference. - muon_utils.py: get_model_mdn default pure_nnx=True; wrap NNX result as {"params": nnx.to_pure_dict(...)} for parity with Linen tree shape. - nnx_decoders.py: FP8+NNX scan fix — Linen FP8 ops (fp8_nanoo, fp8_gpu) retain tracers in Linen scope across re-traces. Skip jax.checkpoint and use a Python for-loop instead of jax.lax.scan when quantization is FP8. Makes FP8 quantization usable on the NNX path. - train.py (pre-train train_step): return nnx.state(new_state, nnx.Not (nnx.Intermediate)) so sowed forward-pass artifacts (e.g. max_logits for QK-Clip) don't break leaf-count parity with state_mesh_shardings. - llama2.py: pass parameter_memory_host_offload to pre_self_attention_layer _norm RMSNorm (was missing on this norm only). - base.yml: add 4 pipeline-related logical_axis_rules — layers_outside _pipeline, layers_per_stage, num_activations, circular_repeats. Additive, no-op without use_nnx_pipeline=True. NNX feature enablements (clear all 17 "Pure NNX support has not been implemented yet" NotImplementedError sites by routing Linen-coupled utilities to the Linen path; their on-disk format is Linen): - layerwise_quantization.py (2 sites): operates on Linen-format checkpoints via DeepSeek*ToLinen layers. - lora_utils.py (1 site): downstream get_lora_abstract_state expects Linen tree shape; LoRA adapters on disk are Linen. - standalone_checkpointer.py (2 sites): add_entropy_to_checkpoint accesses state.opt_state[0]._replace(mu=..., nu=...) — Linen-only. - generate_param_only_checkpoint.py (3 sites): _possibly_unroll_params and _save_decode_checkpoint use state.params["params"]["decoder"] — Linen. - convert_gpt3_ckpt_from_paxml.py (2 sites): keystr_map targets Linen tree paths (.params['params'], .opt_state.mu['params']). - maxengine.py (3 sites): inference engine uses state.params and serves Linen-format inference checkpoints. - grpo_trainer.py (4 sites): RL trainer is end-to-end Linen-shaped; route to Linen with a clear log warning since NNX-format checkpoints will fail at restore time. Vocab tiling on NNX (real implementation, not just routing): - models.py: add Transformer.logits_from_hidden_states on the NNX Transformer class — wraps NNXDecoder.apply_output_head with the token_embedder; mirrors TransformerLinenPure.logits_from_hidden_states. - vocabulary_tiling.py: add vocab_tiling_nnx_loss — chunks the vocab axis via jax.lax.scan and calls model.logits_from_hidden_states(chunk) per chunk. The NNX model carries its parameters internally so no explicit FSDP gather is needed (unlike the Linen gathered_params pattern). MVP uses default autograd; custom_vjp memory-savings optimization is a follow-up if backward memory becomes a concern. - train.py (NNX loss_fn): replace the NotImplementedError with the call to vocab_tiling_nnx_loss using hidden_states from intermediates. - pyconfig_deprecated.py / configs/types.py: drop the num_vocab_tiling > 1 and enable_nnx validation guards (no longer needed). DPO + NNX retained as NotImplementedError but with a much more informative message (points users at pure_nnx=False workaround). Full implementation is deferred — needs a new TrainState shape carrying both policy and reference NNX models plus an NNX dpo_loss_fn. Stats: 26 source files modified, +406 / -171 lines. Linen invariant verified: pure_nnx / enable_nnx / pure_nnx_decoder still default to False; Linen-path UTs unaffected (3 pre-existing failures on the parent branch remain unchanged — sharding_compare_test::deepseek2-16b, optimizers_test::test_model_integration_kimi-k2-1t, diloco_test::two _slices x2). All "Pure NNX support has not been implemented yet" NotImplementedError sites cleared (was 17, now 0).
1 parent 4ebab2c commit 4ed6aaa

23 files changed

Lines changed: 354 additions & 128 deletions

src/maxtext/checkpoint_conversion/standalone_scripts/convert_gpt3_ckpt_from_paxml.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,10 @@ def convert(paxml_ckpt_path, maxtext_model_name, base_output_directory, run_name
8787
devices_array = maxtext_utils.create_device_mesh(cfg)
8888
mesh = Mesh(devices_array, cfg.mesh_axes)
8989

90+
# Output is Linen-format (keystr_map below uses Linen tree paths). Route to
91+
# Linen regardless of pure_nnx.
9092
quant = quantizations.configure_quantization(cfg)
91-
if cfg.pure_nnx:
92-
raise NotImplementedError("Pure NNX support has not been implemented yet.")
93-
else:
94-
model = transformer_as_linen(cfg, mesh, quant=quant, model_mode=MODEL_MODE_TRAIN)
93+
model = transformer_as_linen(cfg, mesh, quant=quant, model_mode=MODEL_MODE_TRAIN)
9594
learning_rate_schedule = maxtext_utils.create_learning_rate_schedule(cfg)
9695
tx = optimizers.get_optimizer(cfg, learning_rate_schedule)
9796

@@ -102,11 +101,7 @@ def convert(paxml_ckpt_path, maxtext_model_name, base_output_directory, run_name
102101
cfg.checkpoint_period,
103102
)
104103

105-
if cfg.pure_nnx:
106-
# NNX has a different function to init the training state.
107-
raise NotImplementedError("Pure NNX support has not been implemented yet.")
108-
else:
109-
init_state_fn = functools.partial(maxtext_utils.init_initial_state, model, tx, cfg, True, init_rng)
104+
init_state_fn = functools.partial(maxtext_utils.init_initial_state, model, tx, cfg, True, init_rng)
110105
state, _, _, _ = maxtext_utils.setup_training_state(None, cfg, mesh, checkpoint_manager, init_state_fn)
111106
max_logging.log("start")
112107
max_utils.print_mem_stats("After params initialized")

src/maxtext/configs/pyconfig_deprecated.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,10 +195,9 @@ def validate_expert_shard_attention_option(expert_shard_attention_option: str) -
195195

196196

197197
def validate_vocab_tiling(num_vocab_tiling: int, per_device_batch_size: int, max_target_length: int, enable_nnx: bool):
198+
del enable_nnx # NNX vocab tiling supported via vocab_tiling_nnx_loss in vocabulary_tiling.py
198199
if (per_device_batch_size * max_target_length) % num_vocab_tiling != 0:
199200
raise ValueError("Per device batch size times sequence length should be divisible by the number of vocab tiles.")
200-
if num_vocab_tiling > 1 and enable_nnx: # TODO (chengnuojin) enable vocab tiling on NNX after NNX migration
201-
raise ValueError("We currently don't support vocab tiling on NNX module.")
202201

203202

204203
def validate_rampup_batch_size(batch_size_start, batch_size_end, batch_size_increment, global_rampup_samples):

src/maxtext/configs/types.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2901,8 +2901,7 @@ def calculate_global_batch_sizes(per_device_batch_size, expansion_factor, num_de
29012901
and (self.per_device_batch_size * self.max_target_length) % self.num_vocab_tiling != 0
29022902
):
29032903
raise ValueError("Per device batch size times sequence length should be divisible by the number of vocab tiles.")
2904-
if self.num_vocab_tiling > 1 and self.enable_nnx:
2905-
raise ValueError("We currently don't support vocab tiling on NNX module.")
2904+
# Vocab tiling on NNX is now supported via vocab_tiling_nnx_loss in vocabulary_tiling.py.
29062905
if self.context_parallel_size > 1 and self.context_parallel_strategy.lower() == "ring":
29072906
if "gpu" not in self.hardware:
29082907
raise ValueError(

src/maxtext/experimental/rl/grpo_trainer.py

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -542,29 +542,26 @@ def setup_train_loop(
542542
- eval_data_iterator: The iterator for the evaluation dataset (or None).
543543
- state: The initialized training state.
544544
"""
545+
# GRPO is Linen-shaped end-to-end (inference goes through Linen MaxEngine).
546+
# Route to Linen regardless of pure_nnx; warn since NNX checkpoints won't load.
547+
if config.pure_nnx or config_inference.pure_nnx:
548+
max_logging.log(
549+
"WARNING: GRPO RL trainer does not yet support pure_nnx natively; "
550+
"running on the Linen path. NNX-format checkpoints will not load correctly here."
551+
)
545552
with maybe_record_goodput(recorder, GoodputEvent.TPU_INIT):
546553
max_logging.log("Training mesh used for the workload")
547554
num_inference_devices = config.inference_devices_per_replica * config.inference_replicas
548555
training_devices = jax.devices()[num_inference_devices:]
549-
if config.pure_nnx:
550-
raise NotImplementedError("Pure NNX support has not been implemented yet.")
551-
else:
552-
model = mt.from_config(config, devices=training_devices)
556+
model = mt.from_config(config, devices=training_devices)
553557
mesh = model.mesh
554558
max_logging.log("Inference mesh used for the workload")
555559
inference_devices = jax.devices()[:num_inference_devices]
556-
if config_inference.pure_nnx:
557-
raise NotImplementedError("Pure NNX support has not been implemented yet.")
558-
else:
559-
inference_model = mt.from_config(config_inference, devices=inference_devices)
560+
inference_model = mt.from_config(config_inference, devices=inference_devices)
560561
inference_mesh = inference_model.mesh
561562
init_rng = jax.random.PRNGKey(config.init_weights_seed)
562563
learning_rate_schedule, tx = train_utils.create_training_optimizer(config, model)
563-
if config.pure_nnx:
564-
# NNX has a different function to init the training state.
565-
raise NotImplementedError("Pure NNX support has not been implemented yet.")
566-
else:
567-
init_state_fn = functools.partial(maxtext_utils.init_initial_state, model, tx, config, True, init_rng)
564+
init_state_fn = functools.partial(maxtext_utils.init_initial_state, model, tx, config, True, init_rng)
568565
checkpoint_manager = train_utils.create_checkpoint_manager(config, mesh, init_state_fn)
569566

570567
with maybe_record_goodput(recorder, GoodputEvent.TRAINING_PREPARATION):
@@ -573,14 +570,10 @@ def setup_train_loop(
573570
data_iterator, config, mesh, checkpoint_manager, init_state_fn
574571
)
575572

576-
# create inference_state_mesh_shardings from inference_mesh
577-
if config_inference.pure_nnx:
578-
# NNX has a different function to init the training state.
579-
raise NotImplementedError("Pure NNX support has not been implemented yet.")
580-
else:
581-
init_inference_state_fn = functools.partial(
582-
maxtext_utils.init_initial_state, inference_model, tx, config_inference, False, init_rng
583-
)
573+
# create inference_state_mesh_shardings from inference_mesh (Linen path; see warning above)
574+
init_inference_state_fn = functools.partial(
575+
maxtext_utils.init_initial_state, inference_model, tx, config_inference, False, init_rng
576+
)
584577
inference_state_mesh_shardings = maxtext_utils.get_abstract_state(
585578
config_inference, inference_mesh, init_inference_state_fn, is_training=False
586579
)[2]

src/maxtext/inference/maxengine/maxengine.py

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,10 @@ def __init__(self, config: Any, devices: Any | None = None):
111111
devices_array = maxtext_utils.create_device_mesh(config=config, devices=devices)
112112
self._mesh = jax.sharding.Mesh(devices_array, config.mesh_axes)
113113

114-
# Model and Optimizer definition
114+
# MaxEngine serves Linen-format inference checkpoints; the surface stays
115+
# Linen-shaped via transformer_as_linen regardless of pure_nnx.
115116
quant = quantizations.configure_quantization(config)
116-
if config.pure_nnx:
117-
raise NotImplementedError("Pure NNX support has not been implemented yet.")
118-
else:
119-
self.model = models.transformer_as_linen(config, mesh=self._mesh, quant=quant, model_mode=MODEL_MODE_PREFILL)
117+
self.model = models.transformer_as_linen(config, mesh=self._mesh, quant=quant, model_mode=MODEL_MODE_PREFILL)
120118
self.replicated_sharding = jax.sharding.NamedSharding(self._mesh, P(None))
121119

122120
self.abstract_params = None
@@ -232,11 +230,7 @@ def load_params(self, *args, params=None, rng: PRNGKeyType | None = None, **kwar
232230
rng1, rng2, rng3 = jax.random.split(rng, 3)
233231
if params:
234232
print("Resharding given params")
235-
if self.config.pure_nnx:
236-
# NNX has a different function to init the training state.
237-
raise NotImplementedError("Pure NNX support has not been implemented yet.")
238-
else:
239-
init_state_fn = functools.partial(maxtext_utils.init_initial_state, self.model, None, self.config, False, rng)
233+
init_state_fn = functools.partial(maxtext_utils.init_initial_state, self.model, None, self.config, False, rng)
240234
_, self.state_mesh_annotations, state_mesh_shardings = maxtext_utils.get_abstract_state(
241235
self.config, self._mesh, init_state_fn, False
242236
)
@@ -245,11 +239,7 @@ def load_params(self, *args, params=None, rng: PRNGKeyType | None = None, **kwar
245239
state = maxtext_utils.init_decode_state(None, params)
246240
state = max_utils.unbox_logicallypartioned(state)
247241
else:
248-
if self.config.pure_nnx:
249-
# NNX has a different function to init the training state.
250-
raise NotImplementedError("Pure NNX support has not been implemented yet.")
251-
else:
252-
init_state_fn = functools.partial(maxtext_utils.init_initial_state, self.model, None, self.config, False, rng1)
242+
init_state_fn = functools.partial(maxtext_utils.init_initial_state, self.model, None, self.config, False, rng1)
253243
state, self.state_mesh_annotations = maxtext_utils.setup_decode_state(self.config, self._mesh, None, init_state_fn)
254244
# pylint: disable=isinstance-second-argument-not-valid-type
255245
self.abstract_params = jax.tree_util.tree_map(

src/maxtext/layers/nnx_decoders.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -545,8 +545,14 @@ def pure_layer_fn(state_in, y_in):
545545
out = merged_layer(y_in, **kwargs)
546546
return out, nnx.state(merged_layer)
547547

548-
checkpointed_fn = jax.checkpoint(pure_layer_fn, policy=policy, prevent_cse=prevent_cse)
549-
out, new_state = checkpointed_fn(state, y)
548+
# Linen FP8 ops keep amax_history in mutable Linen scope; jax.checkpoint
549+
# re-traces and hits UnexpectedTracerError. Skip remat for FP8.
550+
uses_linen_fp8_mutable_state = self.config.quantization in ("fp8_nanoo", "fp8_gpu")
551+
if uses_linen_fp8_mutable_state:
552+
out, new_state = pure_layer_fn(state, y)
553+
else:
554+
checkpointed_fn = jax.checkpoint(pure_layer_fn, policy=policy, prevent_cse=prevent_cse)
555+
out, new_state = checkpointed_fn(state, y)
550556
nnx.update(layer, new_state)
551557

552558
return out
@@ -667,7 +673,22 @@ def layer_fn(carry, scanned_vars):
667673
params = nnx_ensure_scan_leading_axis(params, length)
668674
state = nnx_ensure_scan_leading_axis(state, length)
669675

670-
final_carry, scanned_state = jax.lax.scan(layer_fn_wrapped, x_in, (params, state))
676+
# Linen FP8 ops keep amax_history in mutable Linen scope; jax.lax.scan
677+
# leaks the tracer and hits UnexpectedTracerError. Use a Python for-loop
678+
# for FP8 instead.
679+
uses_linen_fp8_mutable_state = self.config.quantization in ("fp8_nanoo", "fp8_gpu")
680+
if uses_linen_fp8_mutable_state:
681+
carry = x_in
682+
per_layer_states = []
683+
for i in range(length):
684+
current_params = jax.tree.map(lambda x, i=i: x[i], params)
685+
current_state = jax.tree.map(lambda x, i=i: x[i], state)
686+
carry, new_state_i = layer_fn(carry, (current_params, current_state))
687+
per_layer_states.append(new_state_i)
688+
final_carry = carry
689+
scanned_state = jax.tree.map(lambda *xs: jnp.stack(list(xs)), *per_layer_states)
690+
else:
691+
final_carry, scanned_state = jax.lax.scan(layer_fn_wrapped, x_in, (params, state))
671692
returned_kv_stacked = None
672693

673694
if scan_axis != 0:

src/maxtext/layers/nnx_wrappers.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
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
2930
from flax.nnx import variablelib
3031
from flax.nnx.bridge import module as bdg_module
3132
from flax.nnx.module import Module
@@ -167,6 +168,31 @@ def current_linen_module() -> linen.Module | None:
167168
return None
168169

169170

171+
def is_linen_initializing() -> bool:
172+
"""Returns True if currently inside a Linen ``init()`` call.
173+
174+
Used by NNX pipeline modules to short-circuit the scan during init,
175+
where only the output shape/dtype is needed.
176+
"""
177+
module = current_linen_module()
178+
if module is not None and hasattr(module, "is_initializing") and callable(module.is_initializing):
179+
return module.is_initializing()
180+
return False
181+
182+
183+
def _refresh_variable_trace_state(module: Module) -> None:
184+
"""Resets stale ``_trace_state`` on Variables to unblock downstream ``nnx.split``.
185+
186+
``nnx.update`` called with JAX tracer values uses ``_unsafe_bypass_check=True``,
187+
which leaves Variables with a stale ``_trace_state`` from the outer Python
188+
context and breaks ``nnx.split`` with "Cannot extract graph node from different
189+
trace level". Resets ``_trace_state`` on any Variable whose ``_can_update`` is False.
190+
"""
191+
for _, v in nnx.graph.iter_graph(module):
192+
if isinstance(v, variablelib.Variable) and not v._can_update: # pylint: disable=protected-access
193+
object.__setattr__(v, "_trace_state", nnx_tracers.TraceState())
194+
195+
170196
class ToNNX(Module):
171197
"""A wrapper to turn any Linen module into an NNX module.
172198
@@ -476,6 +502,7 @@ def maybe_unbox(x):
476502
warnings.warn(f"Found unknown module paths in incoming state:{paths_str}")
477503

478504
nnx.update(module, new_state)
505+
_refresh_variable_trace_state(module)
479506

480507
_fix_for_qwix_quantization(module)
481508
method_fn = _get_module_method(module, nnx_method)

src/maxtext/layers/normalizations.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,17 @@ def __call__(self, x: jnp.ndarray, out_sharding: NamedSharding | None = None) ->
114114
return y_flat.reshape(input_shape)
115115

116116

117-
def Qwen3NextRMSNorm(num_features: int, eps: float, dtype: DType, weight_dtype: DType, *, rngs: nnx.Rngs):
117+
def Qwen3NextRMSNorm(
118+
num_features: int,
119+
eps: float = 1e-6,
120+
dtype: DType = None,
121+
weight_dtype: DType = None,
122+
shard_mode=None,
123+
kernel_axes=None,
124+
parameter_memory_host_offload=None,
125+
*,
126+
rngs: nnx.Rngs,
127+
):
118128
"""
119129
Used for input and post attention layernorms
120130
in Qwen3NextDecoderLayer.

src/maxtext/models/gpt_oss.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from maxtext.common.common_types import AttentionType, Config
3030
from maxtext.layers import attentions
3131
from maxtext.layers import initializers
32+
from maxtext.layers import linears
3233
from maxtext.layers import moe
3334
from maxtext.layers import nnx_wrappers
3435
from maxtext.layers import quantizations
@@ -132,6 +133,8 @@ def __init__(
132133
rngs=rngs,
133134
)
134135

136+
self.dropout = linears.Dropout(rate=config.dropout_rate, broadcast_dims=(-2,), rngs=rngs)
137+
135138
def __call__(
136139
self,
137140
inputs,
@@ -189,7 +192,7 @@ def __call__(
189192
mlp_lnx = nn.with_logical_constraint(mlp_lnx, ("activation_batch", "activation_norm_length", "activation_embed"))
190193

191194
layer_output = mlp_lnx + intermediate_inputs
192-
layer_output = nn.Dropout(rate=cfg.dropout_rate, broadcast_dims=(-2,))(layer_output, deterministic=deterministic)
195+
layer_output = self.dropout(layer_output, deterministic=deterministic)
193196

194197
layer_output = nn.with_logical_constraint(
195198
layer_output,

src/maxtext/models/llama2.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def __init__(
7171
shard_mode=config.shard_mode,
7272
kernel_axes=("norm",),
7373
epsilon=config.normalization_layer_epsilon,
74+
parameter_memory_host_offload=config.parameter_memory_host_offload,
7475
rngs=rngs,
7576
)
7677

0 commit comments

Comments
 (0)