Skip to content

Commit 682d884

Browse files
Merge pull request #4369 from AI-Hypercomputer:fix/nnx-unroll-scan-axis
PiperOrigin-RevId: 945408994
2 parents 8a41c4d + c6128dd commit 682d884

1 file changed

Lines changed: 89 additions & 12 deletions

File tree

src/maxtext/utils/generate_param_only_checkpoint.py

Lines changed: 89 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,23 @@
4949
from maxtext.utils import model_creation_utils
5050
from maxtext.utils import train_utils
5151

52+
_SCAN_AXIS_NAMES = ("layers_per_stage", "layers", "circular_repeats")
53+
54+
55+
def get_scan_axis(config, sharding_or_spec, x=None):
56+
"""Find the scan axis from sharding spec or use default fallback if valid."""
57+
if hasattr(sharding_or_spec, "spec"):
58+
spec = sharding_or_spec.spec
59+
else:
60+
spec = sharding_or_spec
61+
62+
for name in _SCAN_AXIS_NAMES:
63+
if name in spec:
64+
return spec.index(name)
65+
if x is not None and hasattr(x, "ndim") and x.ndim > config.param_scan_axis:
66+
return config.param_scan_axis
67+
return None
68+
5269

5370
def _possibly_unroll_params(config, training_state, training_state_annotations, mesh):
5471
"""Unroll scanned input layers when force_unroll is set."""
@@ -106,28 +123,60 @@ def _possibly_unroll_params_nnx(config, state, state_mesh_shardings, mesh):
106123
and removes the original collection. Mirrors the same operation on
107124
`state_mesh_shardings` so downstream sharding stays correct.
108125
"""
109-
decoder_state = state.model.decoder
110-
decoder_shardings = state_mesh_shardings.model.decoder
126+
param_only_state, other_state = nnx.split_state(state, nnx.Param, ...)
127+
param_paths = set(param_only_state.flat_state().paths)
128+
129+
param_only_shardings_items = [(path, val) for path, val in state_mesh_shardings.flat_state() if path in param_paths]
130+
other_shardings_items = [(path, val) for path, val in state_mesh_shardings.flat_state() if path not in param_paths]
131+
132+
param_only_shardings = nnx.State.from_flat_path(param_only_shardings_items)
133+
other_shardings = nnx.State.from_flat_path(other_shardings_items)
134+
135+
decoder_state = param_only_state.model.decoder
136+
decoder_shardings = param_only_shardings.model.decoder
111137

112138
def unroll_layer_group(num_layers, layer_name="layers"):
113139
layers = decoder_state.get(layer_name, None)
114140
layers_shardings = decoder_shardings.get(layer_name, None)
115141
if layers is None or layers_shardings is None:
116142
raise ValueError(f"Missing {layer_name} in NNX state.model.decoder or state_mesh_shardings.")
117143

118-
def drop_scan_axis(named_sharding):
144+
def drop_scan_axis(named_sharding, x):
119145
ps = named_sharding.spec
120-
return jax.sharding.PartitionSpec(*(ps[0 : config.param_scan_axis] + ps[config.param_scan_axis + 1 :]))
146+
val = x[...] if isinstance(x, nnx.Variable) else x
147+
scan_axis = get_scan_axis(config, named_sharding, val)
148+
if scan_axis is not None:
149+
return jax.sharding.PartitionSpec(*(ps[0:scan_axis] + ps[scan_axis + 1 :]))
150+
return ps
121151

122152
new_layer_pspec = jax.tree_util.tree_map(
123-
drop_scan_axis, layers_shardings, is_leaf=lambda x: isinstance(x, jax.sharding.NamedSharding)
153+
drop_scan_axis,
154+
layers_shardings,
155+
layers,
156+
is_leaf=lambda x: isinstance(x, (jax.sharding.NamedSharding, nnx.Variable)),
124157
)
125158
new_layer_sharding = jax.tree_util.tree_map(lambda ps: jax.sharding.NamedSharding(mesh, ps), new_layer_pspec)
126159

127160
for i in range(num_layers):
128161

162+
# pylint: disable=cell-var-from-loop
129163
def slice_ith(input_layers):
130-
return jax.tree_util.tree_map(lambda x: jnp.take(x, i, axis=config.param_scan_axis), input_layers)
164+
def _slice_leaf(x, sharding):
165+
val = x[...] if isinstance(x, nnx.Variable) else x
166+
scan_axis = get_scan_axis(config, sharding, val)
167+
if scan_axis is not None:
168+
sliced_val = jnp.take(val, i, axis=scan_axis)
169+
if isinstance(x, nnx.Variable):
170+
return type(x)(sliced_val)
171+
return sliced_val
172+
return x
173+
174+
return jax.tree_util.tree_map(
175+
_slice_leaf,
176+
input_layers,
177+
layers_shardings,
178+
is_leaf=lambda x: isinstance(x, (jax.sharding.NamedSharding, nnx.Variable)),
179+
)
131180

132181
# pylint: disable=not-callable
133182
new_layer = jax.jit(slice_ith, out_shardings=new_layer_sharding)(layers)
@@ -145,6 +194,13 @@ def slice_ith(input_layers):
145194
else:
146195
unroll_layer_group(config.num_decoder_layers, layer_name="layers")
147196

197+
# Merge modified parameter state and other variables back in-place
198+
merged_state = nnx.merge_state(param_only_state, other_state)
199+
state.update(merged_state)
200+
201+
merged_shardings = nnx.merge_state(param_only_shardings, other_shardings)
202+
state_mesh_shardings.update(merged_shardings)
203+
148204

149205
def _read_train_checkpoint(config, checkpoint_manager, mesh):
150206
"""Read training checkpoint at path defined by load_full_state_path."""
@@ -173,7 +229,8 @@ def init_state_fn():
173229
if config.pure_nnx:
174230
# On NNX, state is a flat nnx.State; params live under state.model and the
175231
# legacy notations are unused (callers receive shardings directly).
176-
num_params = max_utils.calculate_num_params_from_pytree(state.model)
232+
params, _ = nnx.split_state(state.model, nnx.Param, ...)
233+
num_params = max_utils.calculate_num_params_from_pytree(params)
177234
max_logging.log(f"In input checkpoint Number of model params={num_params/1e9:.3f} billion")
178235
return state, state_mesh_shardings
179236
num_params = max_utils.calculate_num_params_from_pytree(state.params)
@@ -283,16 +340,36 @@ def unroll_layer_group(num_layers, layer_name="layers"):
283340
if layers is None or layers_annotations is None:
284341
return # No LoRA on this layer group; nothing to unroll.
285342

286-
def new_pspec(x):
287-
return jax.sharding.PartitionSpec(*(x[0 : config.param_scan_axis] + x[config.param_scan_axis + 1 :]))
288-
289-
new_layer_annotation = jax.tree_util.tree_map(new_pspec, layers_annotations)
343+
def drop_scan_axis(spec, x):
344+
scan_axis = get_scan_axis(config, spec, x)
345+
if scan_axis is not None:
346+
return jax.sharding.PartitionSpec(*(spec[0:scan_axis] + spec[scan_axis + 1 :]))
347+
return spec
348+
349+
new_layer_annotation = jax.tree_util.tree_map(
350+
drop_scan_axis,
351+
layers_annotations,
352+
layers,
353+
is_leaf=lambda x: isinstance(x, jax.sharding.PartitionSpec),
354+
)
290355
new_layer_sharding = jax.tree_util.tree_map(lambda x: jax.sharding.NamedSharding(mesh, x), new_layer_annotation)
291356

292357
for i in range(num_layers):
293358

359+
# pylint: disable=cell-var-from-loop
294360
def slice_ith(input_layers):
295-
return jax.tree_util.tree_map(lambda x: jnp.take(x, i, axis=config.param_scan_axis), input_layers)
361+
def _slice_leaf(x, spec):
362+
scan_axis = get_scan_axis(config, spec, x)
363+
if scan_axis is not None:
364+
return jnp.take(x, i, axis=scan_axis)
365+
return x
366+
367+
return jax.tree_util.tree_map(
368+
_slice_leaf,
369+
input_layers,
370+
layers_annotations,
371+
is_leaf=lambda x: isinstance(x, jax.sharding.PartitionSpec),
372+
)
296373

297374
# pylint: disable=not-callable
298375
new_layer = jax.jit(slice_ith, out_shardings=new_layer_sharding)(layers)

0 commit comments

Comments
 (0)