Skip to content

Commit c7b9871

Browse files
Merge pull request #4205 from AI-Hypercomputer:xibin/nnx_sharding
PiperOrigin-RevId: 937382038
2 parents cf9b093 + 99d7adc commit c7b9871

8 files changed

Lines changed: 441 additions & 230 deletions

File tree

src/maxtext/inference/maxengine/maxengine.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
from maxtext.utils import max_logging
4848
from maxtext.utils import max_utils
4949
from maxtext.utils import maxtext_utils
50+
from maxtext.utils import sharding
5051
from maxtext.utils import maxtext_utils_nnx
5152
from maxtext.utils import model_creation_utils
5253
from maxtext.common.gcloud_stub import jetstream, is_decoupled
@@ -398,15 +399,19 @@ def _load_params_nnx(self, params, rng):
398399
# axis metadata but no physical .sharding. Resolve logical to physical here so
399400
# device_put actually reshards instead of being a no-op.
400401
with nn_partitioning.axis_rules(self.config.logical_axis_rules):
401-
target_shardings = maxtext_utils.get_nnx_named_sharding_with_scan_axis(params_abs, self._mesh)
402+
target_shardings = sharding.nnx_construct_named_sharding(
403+
params_abs, self._mesh
404+
)
402405
params_state = jax.device_put(params, target_shardings)
403406
# We only need a concrete `rest` (RNG vars) for nnx.merge. create_nnx_sharded_model
404407
# builds the model with a jitted out_shardings so params are produced already
405408
# sharded, avoiding a single-device allocation of the full model (an OOM risk for
406409
# large models). self.model is abstract with no .sharding, so pass an explicit one.
407410
_, full_abs = nnx.split(self.model)
408411
with nn_partitioning.axis_rules(self.config.logical_axis_rules):
409-
full_sharding = maxtext_utils.get_nnx_named_sharding_with_scan_axis(full_abs, self._mesh)
412+
full_sharding = sharding.nnx_construct_named_sharding(
413+
full_abs, self._mesh
414+
)
410415
concrete_model = maxtext_utils_nnx.create_nnx_sharded_model(
411416
self.model, self._create_model_fn, mesh=self._mesh, named_sharding=full_sharding
412417
)

src/maxtext/utils/maxtext_utils.py

Lines changed: 23 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -19,41 +19,36 @@
1919
import os
2020
from typing import Sequence
2121

22-
from flax import nnx, linen as nn
23-
from flax.core.spmd import composite_rules, from_sharding_rules, get_logical_axis_rules
22+
from flax import linen as nn, nnx
2423
from flax.linen import partitioning as nn_partitioning
2524
from flax.training.train_state import TrainState
26-
27-
import numpy as np
28-
2925
import jax
30-
import jax.numpy as jnp
31-
from jax.sharding import AxisType, Mesh, NamedSharding, PartitionSpec
3226
from jax.experimental import mesh_utils
3327
from jax.experimental.serialize_executable import deserialize_and_load
34-
35-
import optax
36-
import orbax.checkpoint.experimental.emergency.checkpoint_manager as emergency_checkpoint_manager
37-
import orbax.checkpoint.experimental.emergency.replicator_checkpoint_manager as emergency_replicator_checkpoint_manager
38-
39-
from maxtext.configs import pyconfig
28+
import jax.numpy as jnp
29+
from jax.sharding import AxisType, Mesh, NamedSharding
30+
from maxtext.common import checkpointing
4031
from maxtext.common.common_types import (
4132
AttentionType,
4233
DecoderBlockType,
43-
MODEL_MODE_PREFILL,
4434
MODEL_MODE_AUTOREGRESSIVE,
35+
MODEL_MODE_PREFILL,
4536
ReorderStrategy,
4637
ShardMode,
4738
)
39+
from maxtext.configs import pyconfig
4840
from maxtext.configs import types
49-
from maxtext.common import checkpointing
5041
from maxtext.multimodal import processor as mm_processor
42+
from maxtext.utils import elastic_utils
5143
from maxtext.utils import gcs_utils
5244
from maxtext.utils import max_logging
5345
from maxtext.utils import max_utils
54-
from maxtext.utils import sharding
55-
from maxtext.utils import elastic_utils
5646
from maxtext.utils import maxtext_utils_nnx
47+
from maxtext.utils import sharding
48+
import numpy as np
49+
import optax
50+
import orbax.checkpoint.experimental.emergency.checkpoint_manager as emergency_checkpoint_manager
51+
import orbax.checkpoint.experimental.emergency.replicator_checkpoint_manager as emergency_replicator_checkpoint_manager
5752

5853
OVERWRITE_WITH_GRADIENT = "_overwrite_with_gradient"
5954

@@ -1612,88 +1607,6 @@ def move(path, x):
16121607
)
16131608

16141609

1615-
def get_nnx_named_sharding_with_scan_axis(abs_var_state: nnx.State, mesh) -> nnx.State:
1616-
"""Compute NamedSharding for each NNX variable, correctly handling the scan (stacked layers) axis.
1617-
1618-
Unlike flax.nnx.spmd.get_var_pspec (used inside nnx.get_abstract_model), this function also
1619-
inserts the partition_name axis at the correct scan_axis position for parameters created by
1620-
_create_scanned_layers. Without this, scanned parameters get a 2D partition spec applied to a
1621-
3D tensor, placing sharding on the stacked-layers dimension instead of the embedding dimension.
1622-
1623-
Args:
1624-
abs_var_state: NNX abstract variable state from nnx.split(nnx.eval_shape(...)).
1625-
mesh: JAX physical mesh.
1626-
1627-
Returns:
1628-
Same tree structure as abs_var_state but each Variable's value replaced with NamedSharding.
1629-
"""
1630-
1631-
def _make_named_sharding(v):
1632-
val = v.get_value()
1633-
if not hasattr(val, "shape"):
1634-
# `val` is either truly leafless (e.g. optax MaskedNode) or a composite
1635-
# pytree of tensors (e.g. AQT QTensor on serve-mode quantized variables —
1636-
# a `qvalue` int8 array + a list of `scale` bf16 arrays). For the latter
1637-
# we must emit a parallel tree of NamedSharding leaves so the downstream
1638-
# `jax.tree.map(lambda a, s: ShapeDtypeStruct(..., sharding=s), abs, names)`
1639-
# finds a real Sharding at every position. Replicated sharding is a safe
1640-
# default — AQT serve-mode QTensors are normally small (per-channel scale
1641-
# factors and packed int8 weights) and don't need axis-aware sharding.
1642-
if jax.tree_util.tree_leaves(val):
1643-
replicated = NamedSharding(mesh, PartitionSpec())
1644-
return v.replace(jax.tree.map(lambda _: replicated, val))
1645-
return v
1646-
metadata = v.get_metadata()
1647-
out_sharding = metadata.get("out_sharding") or metadata.get("sharding_names") or metadata.get("sharding")
1648-
if not out_sharding:
1649-
pspec = PartitionSpec()
1650-
else:
1651-
# Insert the scan axis for parameters created by _create_scanned_layers.
1652-
# _add_scan_metadata stores the axis name in nnx.PARTITION_NAME and the
1653-
# axis index in "param_scan_axis". flax.nnx.spmd.get_var_pspec ignores these.
1654-
if nnx.PARTITION_NAME in metadata:
1655-
partition_name = metadata[nnx.PARTITION_NAME]
1656-
# Always use param_scan_axis from metadata. OptVariable (optimizer state) inherits
1657-
# param_scan_axis=1 from the model Param via to_opt_state(), so we must not hardcode
1658-
# scan_axis=0 for non-Param types. stacked_rest non-Param variables have
1659-
# param_scan_axis=0 set explicitly by _add_scan_metadata, so this is always correct.
1660-
scan_axis = metadata.get("param_scan_axis", 0)
1661-
out_sharding = [out_sharding] if isinstance(out_sharding, str) else list(out_sharding)
1662-
# Guard against double-insertion: Flax 0.12.6 _remap_sharding_metadata renames
1663-
# 'sharding' -> 'out_sharding', so _add_scan_metadata may have already inserted
1664-
# the scan axis. Only insert if not already present.
1665-
if partition_name not in out_sharding:
1666-
out_sharding.insert(scan_axis, partition_name)
1667-
out_sharding = tuple(out_sharding)
1668-
# Convert logical axis names to physical mesh axes using current context rules.
1669-
context_rules = get_logical_axis_rules()
1670-
local_rules = metadata.get("sharding_rules", ())
1671-
if context_rules or local_rules:
1672-
rules = composite_rules(context_rules, local_rules)
1673-
raw_sharding = from_sharding_rules(out_sharding, rules)
1674-
mesh_axis_names = mesh.axis_names if mesh is not None else ()
1675-
1676-
# from_sharding_rules leaves a logical name with no matching rule unchanged, so a
1677-
# name missing from logical_axis_rules (e.g. concat_embed on the MTP kernel)
1678-
# reaches NamedSharding and is rejected as an unknown mesh axis. Map any such
1679-
# leftover name to None (replicated), matching Linen, whose logical_to_mesh_axes
1680-
# replicates unmatched names.
1681-
def _sanitize(x):
1682-
if isinstance(x, list):
1683-
x = tuple(x)
1684-
if x is None or (isinstance(x, str) and x in mesh_axis_names) or isinstance(x, tuple):
1685-
return x
1686-
return None
1687-
1688-
sanitized_sharding = [_sanitize(x) for x in raw_sharding]
1689-
pspec = PartitionSpec(*sanitized_sharding)
1690-
else:
1691-
pspec = PartitionSpec(*out_sharding)
1692-
return v.replace(NamedSharding(mesh, pspec))
1693-
1694-
return jax.tree.map(_make_named_sharding, abs_var_state, is_leaf=lambda x: isinstance(x, nnx.Variable))
1695-
1696-
16971610
def get_abstract_state_nnx(config, mesh, nnx_init_trainstate_fn, is_training=True):
16981611
"""Calculates the abstract sharded state and memory placement for an NNX TrainState.
16991612
@@ -1724,26 +1637,29 @@ def get_abstract_state_nnx(config, mesh, nnx_init_trainstate_fn, is_training=Tru
17241637

17251638
with nn_partitioning.axis_rules(config.logical_axis_rules):
17261639
# Use nnx.eval_shape + nnx.split instead of nnx.get_abstract_model, so we can apply
1727-
# get_nnx_named_sharding_with_scan_axis which correctly inserts the stacked-layers
1640+
# nnx_construct_named_sharding which correctly inserts the stacked-layers
17281641
# axis into the partition spec. nnx.get_abstract_model uses get_var_pspec internally
17291642
# which ignores nnx.PARTITION_NAME / param_scan_axis metadata set by _create_scanned_layers,
17301643
# causing the 2D partition spec to be misapplied to the 3D stacked parameter tensor.
17311644
# Do NOT wrap nnx.eval_shape in jax.set_mesh: Flax 0.12.6's _to_variable calls
17321645
# var.shape for every variable when a global mesh is active, but masked optimizer
17331646
# state variables (e.g. from trainable_parameters_mask) have value=MaskedNode()
1734-
# which has no .shape and would raise AttributeError. We handle sharding
1735-
# ourselves via get_nnx_named_sharding_with_scan_axis, so auto-assignment is not
1736-
# needed here.
1647+
# which has no .shape and would raise AttributeError. We handle sharding
1648+
# ourselves via nnx_construct_named_sharding, so auto-assignment is not needed here.
17371649
abs_model = nnx.eval_shape(nnx_init_trainstate_fn)
17381650
_, abs_var_state = nnx.split(abs_model)
1739-
named_sharding_state = get_nnx_named_sharding_with_scan_axis(abs_var_state, mesh)
1651+
named_sharding_state = sharding.nnx_construct_named_sharding(
1652+
abs_var_state, mesh
1653+
)
17401654
abstract_state = jax.tree.map(
17411655
lambda a, s: jax.ShapeDtypeStruct(a.shape, a.dtype, sharding=s),
17421656
abs_var_state,
17431657
named_sharding_state,
17441658
)
17451659

1746-
state_mesh_shardings = maxtext_utils_nnx.get_named_sharding_nnx(abstract_state)
1660+
state_mesh_shardings = maxtext_utils_nnx.nnx_extract_named_sharding(
1661+
abstract_state
1662+
)
17471663

17481664
if is_training and config.shard_optimizer_over_data:
17491665
# Add data to sharding for optimizer state
@@ -1849,10 +1765,10 @@ def _nnx_cache_partition_specs(abstract_model, config, mesh):
18491765
way it does for the Linen helpers below.
18501766
"""
18511767
_, cache_state, _ = nnx.split(abstract_model, nnx.Cache, ...)
1852-
# get_nnx_named_sharding_with_scan_axis reads logical axis rules from the
1768+
# nnx_construct_named_sharding reads logical axis rules from the
18531769
# active flax partitioning context, so wrap.
18541770
with nn_partitioning.axis_rules(config.logical_axis_rules):
1855-
named_state = get_nnx_named_sharding_with_scan_axis(cache_state, mesh)
1771+
named_state = sharding.nnx_construct_named_sharding(cache_state, mesh)
18561772
return jax.tree.map(lambda s: s.spec, named_state.to_pure_dict())
18571773

18581774

src/maxtext/utils/maxtext_utils_nnx.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
""" Utils for MaxText NNX. """
14+
"""Utils for MaxText NNX."""
1515

1616
from functools import partial
1717
from typing import Callable
@@ -51,14 +51,18 @@ def create_nnx_rngs(
5151
return nnx.Rngs(params=rng_key) # disable dropout RNG and aqt for inference
5252

5353

54-
def get_named_sharding_nnx(abstract_state: nnx.State) -> nnx.State:
54+
def nnx_extract_named_sharding(abstract_state: nnx.State) -> nnx.State:
5555
"""Get named sharding from NNX abstract state.
5656
5757
Args:
58-
abstract_state: NNX model abstract state created from nnx.get_abstract_model.
58+
abstract_state: NNX model abstract state created from
59+
nnx.get_abstract_model.
5960
6061
Returns:
61-
named sharding structure
62+
A tree of raw NamedSharding objects (stripping out any nnx.Variable / Param
63+
wrappers). This clean structure is expected by JAX compiler APIs (like JIT
64+
out_shardings). Contrast with sharding.nnx_construct_named_sharding, which
65+
retains wrappers for abstract tree zipping compatibility.
6266
"""
6367
# Don't use nnx.get_named_sharding() because it constructs new shardings. Instead, we
6468
# get the existing sharding from the abstract_state.
@@ -156,7 +160,7 @@ def create_nnx_sharded_model(
156160
if named_sharding is None:
157161
# The state leaf is of type jax.ShapeDtypeStruct(shape, dtype, sharding)
158162
# we get the sharding directly from it.
159-
named_sharding = get_named_sharding_nnx(abstract_state)
163+
named_sharding = nnx_extract_named_sharding(abstract_state)
160164

161165
if mesh is None:
162166
mesh = abstract_model.mesh

src/maxtext/utils/model_creation_utils.py

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
from maxtext.layers import quantizations
5252
from maxtext.models import models
5353
from maxtext.utils import max_logging
54-
from maxtext.utils import max_utils, maxtext_utils, maxtext_utils_nnx
54+
from maxtext.utils import max_utils, maxtext_utils, maxtext_utils_nnx, sharding
5555
import numpy as np
5656
from orbax import checkpoint as ocp
5757

@@ -112,13 +112,13 @@ def _zero_pad_axis(arr, axis, extra):
112112
if extra == 0:
113113
return arr
114114

115-
sharding = getattr(arr, "sharding", None)
115+
arr_sharding = getattr(arr, "sharding", None)
116116
pad_width = [(0, 0)] * arr.ndim
117117

118-
if isinstance(sharding, jax.sharding.NamedSharding):
119-
spec = sharding.spec
118+
if isinstance(arr_sharding, jax.sharding.NamedSharding):
119+
spec = arr_sharding.spec
120120
partition = spec[axis] if axis < len(spec) else None
121-
shards_along_axis = _partition_size(partition, sharding.mesh)
121+
shards_along_axis = _partition_size(partition, arr_sharding.mesh)
122122
if shards_along_axis > 1:
123123
if extra % shards_along_axis != 0:
124124
raise ValueError(
@@ -131,7 +131,13 @@ def _zero_pad_axis(arr, axis, extra):
131131
def _pad_local(x):
132132
return jnp.pad(x, pad_width)
133133

134-
return jax.shard_map(_pad_local, mesh=sharding.mesh, in_specs=spec, out_specs=spec, check_vma=False)(arr)
134+
return jax.shard_map(
135+
_pad_local,
136+
mesh=arr_sharding.mesh,
137+
in_specs=spec,
138+
out_specs=spec,
139+
check_vma=False,
140+
)(arr)
135141

136142
pad_width[axis] = (0, extra)
137143
return jnp.pad(arr, pad_width)
@@ -257,11 +263,11 @@ def _maybe_fuse(path, ckpt_node):
257263

258264
# Determine the number of shards (TP degree) along the concatenated axis
259265
n_shards = 1
260-
sharding = getattr(wi_model, "sharding", None)
261-
if isinstance(sharding, jax.sharding.NamedSharding):
262-
spec = sharding.spec
266+
wi_sharding = getattr(wi_model, "sharding", None)
267+
if isinstance(wi_sharding, jax.sharding.NamedSharding):
268+
spec = wi_sharding.spec
263269
partition = spec[axis] if axis < len(spec) else None
264-
n_shards = _partition_size(partition, sharding.mesh)
270+
n_shards = _partition_size(partition, wi_sharding.mesh)
265271

266272
# Target size for a single half (wi_0 or wi_1) AFTER padding
267273
target_half_dim = wi_model.shape[-1] // 2
@@ -325,13 +331,13 @@ def _stored_shape_evenly_shardable(restore_arg, stored_shape):
325331
(each device receives only its local slice), avoiding the multi-GB replicated
326332
fanout that fully-replicated loading produces for large MoE weights.
327333
"""
328-
sharding = restore_arg.sharding
329-
if not isinstance(sharding, jax.sharding.NamedSharding):
334+
restore_sharding = restore_arg.sharding
335+
if not isinstance(restore_sharding, jax.sharding.NamedSharding):
330336
return False
331-
spec = sharding.spec
337+
spec = restore_sharding.spec
332338
for axis_idx, dim in enumerate(stored_shape):
333339
partition = spec[axis_idx] if axis_idx < len(spec) else None
334-
if dim % _partition_size(partition, sharding.mesh) != 0:
340+
if dim % _partition_size(partition, restore_sharding.mesh) != 0:
335341
return False
336342
return True
337343

@@ -581,7 +587,9 @@ def create_nnx_abstract_model(
581587
# wrap is unnecessary here.
582588
abs_model = nnx.eval_shape(_create_model)
583589
graphdef, abs_var_state = nnx.split(abs_model)
584-
named_sharding_state = maxtext_utils.get_nnx_named_sharding_with_scan_axis(abs_var_state, mesh)
590+
named_sharding_state = sharding.nnx_construct_named_sharding(
591+
abs_var_state, mesh
592+
)
585593
abstract_state = jax.tree.map(
586594
lambda a, s: jax.ShapeDtypeStruct(a.shape, a.dtype, sharding=s),
587595
abs_var_state,

0 commit comments

Comments
 (0)