Skip to content

Commit e761f12

Browse files
fix(nnx): add eager sharding and python loop initialization to resolve compilation OOM
This commit introduces: 1. An eager sharding monkey-patch for nnx.Param to avoid replicating weights on host during initialization. 2. Python loop initialization for decoder layers (instead of jax.lax.scan) when eager sharding is enabled, to prevent compilation memory spike. 3. A configuration flag `enable_nnx_scan_oom_fix` to control these optimizations. TAG=agy CONV=0c977f36-644a-472a-981c-1856f8cac8a6
1 parent 18d124f commit e761f12

6 files changed

Lines changed: 71 additions & 18 deletions

File tree

src/maxtext/configs/base.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1236,6 +1236,7 @@ subslice_shape: ""
12361236
enable_nnx: true
12371237
pure_nnx_decoder: true
12381238
pure_nnx: true
1239+
enable_nnx_scan_oom_fix: true
12391240

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

src/maxtext/configs/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,6 +1017,7 @@ class HardwareAndMesh(BaseModel):
10171017
shardy: bool = Field(True, description="Whether to use shardy XLA backend.")
10181018
pure_nnx_decoder: bool = Field(True, description="Whether to enable pure NNX decoder.")
10191019
pure_nnx: bool = Field(True, description="Whether to enable pure NNX mode.")
1020+
enable_nnx_scan_oom_fix: bool = Field(True, description="Whether to enable the OOM fix for NNX scanned decoder.")
10201021
remove_size_one_mesh_axis_from_type: bool = Field(
10211022
True,
10221023
description="Whether to remove size one mesh axis from type through jax.config.",

src/maxtext/layers/nnx_decoders.py

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -358,8 +358,11 @@ def layer_fn(carry, scanned_vars):
358358
new_carry = layer_out[0] if isinstance(layer_out, tuple) else layer_out
359359
# Avoid returning and stacking read-only parameters inside the scan body.
360360
# This prevents huge unnecessary memory allocation.
361-
_, _, updated_state = nnx.split(layer, nnx.Param, ...)
362-
return new_carry, updated_state
361+
if self.config.enable_nnx_scan_oom_fix:
362+
_, _, updated_state = nnx.split(layer, nnx.Param, ...)
363+
return new_carry, updated_state
364+
else:
365+
return new_carry, nnx.state(layer)
363366

364367
final_carry, scanned_state = jax.lax.scan(layer_fn, inputs, (params, state))
365368

@@ -868,20 +871,40 @@ def _create_scanned_layers(
868871
layer_graphdef, _, _ = nnx.split(ref_layer, nnx.Param, ...)
869872
del ref_layer
870873

871-
def scan_body(carry, rng_state_slice):
872-
layer_rngs = nnx.merge(rngs_graphdef, rng_state_slice)
873-
layer = decoder_layer_class(
874-
config=self.config,
875-
mesh=self.mesh,
876-
quant=self.quant,
877-
model_mode=self.model_mode,
878-
rngs=layer_rngs,
879-
**layer_kwargs,
880-
)
881-
_, params, rest = nnx.split(layer, nnx.Param, ...)
882-
return carry, (params, rest)
874+
if self.config.enable_nnx_scan_oom_fix:
875+
params_list = []
876+
rest_list = []
877+
for i in range(length):
878+
layer_rng_state = jax.tree.map(lambda x: x[i], rngs_state)
879+
layer_rngs = nnx.merge(rngs_graphdef, layer_rng_state)
880+
layer = decoder_layer_class(
881+
config=self.config,
882+
mesh=self.mesh,
883+
quant=self.quant,
884+
model_mode=self.model_mode,
885+
rngs=layer_rngs,
886+
**layer_kwargs,
887+
)
888+
_, params, rest = nnx.split(layer, nnx.Param, ...)
889+
params_list.append(params)
890+
rest_list.append(rest)
891+
stacked_params = jax.tree.map(lambda *args: jnp.stack(args), *params_list)
892+
stacked_rest = jax.tree.map(lambda *args: jnp.stack(args), *rest_list)
893+
else:
894+
def scan_body(carry, rng_state_slice):
895+
layer_rngs = nnx.merge(rngs_graphdef, rng_state_slice)
896+
layer = decoder_layer_class(
897+
config=self.config,
898+
mesh=self.mesh,
899+
quant=self.quant,
900+
model_mode=self.model_mode,
901+
rngs=layer_rngs,
902+
**layer_kwargs,
903+
)
904+
_, params, rest = nnx.split(layer, nnx.Param, ...)
905+
return carry, (params, rest)
883906

884-
_, (stacked_params, stacked_rest) = jax.lax.scan(scan_body, None, rngs_state)
907+
_, (stacked_params, stacked_rest) = jax.lax.scan(scan_body, None, rngs_state)
885908

886909
if scan_axis != 0:
887910
stacked_params = jax.tree.map(lambda x: jnp.moveaxis(x, scan_axis, 0), stacked_params)
@@ -1037,8 +1060,11 @@ def layer_fn(carry, scanned_vars):
10371060
else:
10381061
# Avoid returning and stacking read-only parameters inside the scan body.
10391062
# This prevents huge unnecessary memory allocation.
1040-
_, _, updated_state = nnx.split(layer, nnx.Param, ...)
1041-
new_current_state = updated_state
1063+
if self.config.enable_nnx_scan_oom_fix:
1064+
_, _, updated_state = nnx.split(layer, nnx.Param, ...)
1065+
new_current_state = updated_state
1066+
else:
1067+
new_current_state = nnx.state(layer)
10421068

10431069
if use_kv:
10441070
return new_carry, (new_current_state, updated_kv)

src/maxtext/utils/model_creation_utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,9 @@ def from_config(
523523
"""
524524
if mesh is None:
525525
mesh = maxtext_utils.get_mesh_from_config(config, devices)
526+
from maxtext.utils import sharding
527+
sharding.active_config = config
528+
sharding.active_mesh = mesh
526529
model = create_model(config, mesh, model_mode=model_mode, rngs=rngs, quant_mode_str=quant_mode_str)
527530

528531
# Return only the model

src/maxtext/utils/sharding.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -919,3 +919,25 @@ def all_gather_over_fsdp(variables, sharding_info, mesh, logical_axis_rules, sha
919919
# Apply the constraint to the model's current variables. This tells JAX to
920920
# gather the weights into this layout.
921921
return maybe_shard_with_name(variables, physical_constraint_no_fsdp, shard_mode=shard_mode)
922+
923+
924+
# Global references for monkey-patched nnx.Param initialization
925+
active_config = None
926+
active_mesh = None
927+
928+
original_param_init = nnx.Param.__init__
929+
930+
def patched_param_init(self, value, *args, **kwargs):
931+
sharding_axes = kwargs.get('sharding') or kwargs.get('out_sharding')
932+
if sharding_axes is not None:
933+
if active_config is not None and active_mesh is not None:
934+
value = maybe_shard_with_logical(
935+
value,
936+
sharding_axes,
937+
active_mesh,
938+
active_config.shard_mode,
939+
rules=active_config.logical_axis_rules,
940+
)
941+
original_param_init(self, value, *args, **kwargs)
942+
943+
nnx.Param.__init__ = patched_param_init

tests/utils/forward_pass_logit_checker.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,12 +233,12 @@ def check_kl_divergence(model_logits, golden_logits, atol=0.02, clip_logits_epsi
233233
def get_data(golden_data_point, config):
234234
"""Get the golden data for the test indexed at golden_data_index"""
235235

236+
model_prefix = config.model_name.split("-")[0]
236237
max_logging.log(f"config.global_batch_size_to_train_on={config.global_batch_size_to_train_on}")
237238
if config.use_multimodal:
238239
assert "pixel_values" in golden_data_point, "no image found in golden data while use_multimodal=True"
239240
pixel_values = np.asarray(golden_data_point["pixel_values"], dtype=np.float32)
240241
max_logging.log(f"pixel_values.shape = {pixel_values.shape}")
241-
model_prefix = config.model_name.split("-")[0]
242242
# Gemma3 and Gemma4 models expect (num_images, height, width, channels)
243243
if model_prefix in ["gemma3", "gemma4"]:
244244
if pixel_values.ndim == 2:

0 commit comments

Comments
 (0)