Skip to content

Commit 085068d

Browse files
Merge pull request #4523 from AI-Hypercomputer:chengnuojin-train-eval-mesh
PiperOrigin-RevId: 952183577
2 parents c03f8bd + de63b4a commit 085068d

10 files changed

Lines changed: 116 additions & 40 deletions

File tree

src/maxtext/configs/base.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,9 @@ input_data_sharding_logical_axes: ['activation_embed_and_logits_batch', 'activat
616616
# Determines which physical axis plays the role of context parallelism for input data processing and load balancing
617617
# only supports "context" or "expert" (when custom_mesh_and_rule=ep-as-cp)
618618
context_sharding: "context"
619+
# Customized mesh and logical rules for evaluation (e.g. "ep-as-cp")
620+
custom_mesh_and_rule_for_eval: ""
621+
logical_axis_rules_for_eval: []
619622

620623
# sharding tolerance: float between 0.0 and 1.0 representing the allowed percentage of non-sharded parameters.
621624
sharding_tolerance: 0.02

src/maxtext/configs/types.py

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,6 +1064,10 @@ class HardwareAndMesh(BaseModel):
10641064
CustomRule.DEFAULT,
10651065
description="Customized mesh and logical rules for granularity.",
10661066
)
1067+
custom_mesh_and_rule_for_eval: CustomRule = Field(
1068+
CustomRule.DEFAULT,
1069+
description="Customized mesh and logical rules for evaluation.",
1070+
)
10671071
allow_split_physical_axes: bool = Field(False, description="Allow splitting physical axes for device mesh creation.")
10681072
enable_nnx: bool = Field(True, description="Whether to use NNX for model definition.")
10691073
optimize_mesh_for_tpu_v6e: bool = Field(False, description="Apply transformations to the mesh for TPU v6e.")
@@ -1080,6 +1084,9 @@ class LayoutAndSharding(BaseModel):
10801084
"""Configuration for data and model sharding rules."""
10811085

10821086
logical_axis_rules: Any = Field([], description="Rules for mapping logical axes to physical mesh axes.")
1087+
logical_axis_rules_for_eval: Any = Field(
1088+
[], description="Rules for mapping logical axes to physical mesh axes during evaluation."
1089+
)
10831090
data_sharding: Any = Field([], description="Sharding for input data.")
10841091
context_sharding: str = Field("context", description="Physical axis name for context parallelism.")
10851092
input_data_sharding_logical_axes: list[str] = Field(
@@ -2697,31 +2704,46 @@ def validate_num_moe_emb_chunks(self):
26972704
f"Got use_gmm_v2={self.use_gmm_v2}, use_ring_of_experts={self.use_ring_of_experts}."
26982705
)
26992706

2707+
@staticmethod
2708+
def _load_mesh_config_from_yaml(rule_value: str) -> dict:
2709+
"""Helper to load and parse custom mesh YAML configurations."""
2710+
custom_mesh_path = os.path.join(
2711+
os.path.dirname(os.path.abspath(__file__)),
2712+
"custom_mesh_and_rule",
2713+
f"{rule_value}.yml",
2714+
)
2715+
2716+
if not os.path.exists(custom_mesh_path):
2717+
# ValueError is more semantically correct for validation errors than NotImplementedError
2718+
raise ValueError(f"Custom mesh config file not found at {custom_mesh_path}")
2719+
2720+
# Explicitly setting encoding removes the need for the pylint disable comment
2721+
with open(custom_mesh_path, "r", encoding="utf-8") as f:
2722+
return yaml.safe_load(f) or {}
2723+
27002724
@model_validator(mode="after")
27012725
def set_derived_and_validate_values(self) -> "MaxTextConfig":
27022726
"""
27032727
Computes all derived values and runs all cross-field validations after initial parsing.
27042728
This logic is ported from the legacy pyconfig_deprecated.py system and adapted for Pydantic.
27052729
"""
2730+
# Handle primary custom mesh and rule
27062731
if self.custom_mesh_and_rule is not CustomRule.DEFAULT:
2707-
custom_mesh_path = os.path.join(
2708-
os.path.dirname(os.path.abspath(__file__)),
2709-
"custom_mesh_and_rule",
2710-
f"{self.custom_mesh_and_rule.value}.yml",
2711-
)
2712-
if os.path.exists(custom_mesh_path):
2713-
with open(custom_mesh_path, "r") as f: # pylint: disable=unspecified-encoding
2714-
custom_mesh_config = yaml.safe_load(f)
2715-
if "mesh_axes" in custom_mesh_config:
2716-
self.mesh_axes = custom_mesh_config["mesh_axes"]
2717-
if "logical_axis_rules" in custom_mesh_config:
2718-
self.logical_axis_rules = custom_mesh_config["logical_axis_rules"]
2719-
if "data_sharding" in custom_mesh_config:
2720-
self.data_sharding = custom_mesh_config["data_sharding"]
2721-
if "context_sharding" in custom_mesh_config:
2722-
self.context_sharding = custom_mesh_config["context_sharding"]
2723-
else:
2724-
raise NotImplementedError(f"Custom mesh config file not found at {custom_mesh_path}")
2732+
mesh_config = self._load_mesh_config_from_yaml(self.custom_mesh_and_rule.value)
2733+
2734+
# Use setattr to dynamically apply attributes, keeping code compact
2735+
for field in ("mesh_axes", "logical_axis_rules", "data_sharding", "context_sharding"):
2736+
if field in mesh_config:
2737+
setattr(self, field, mesh_config[field])
2738+
2739+
# Handle eval custom mesh and rule
2740+
if self.custom_mesh_and_rule_for_eval is CustomRule.DEFAULT:
2741+
# Fallback to primary rule if eval is DEFAULT
2742+
self.custom_mesh_and_rule_for_eval = self.custom_mesh_and_rule
2743+
self.logical_axis_rules_for_eval = self.logical_axis_rules
2744+
else:
2745+
eval_config = self._load_mesh_config_from_yaml(self.custom_mesh_and_rule_for_eval.value)
2746+
self.logical_axis_rules_for_eval = eval_config.get("logical_axis_rules", self.logical_axis_rules)
27252747

27262748
# A. SET RUN NAME AND PATHS
27272749
# If run_name is not set, generate one from the JOBSET_NAME environment variable (if available)

src/maxtext/layers/attention_op.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575
from maxtext.layers.initializers import variable_to_logically_partitioned
7676
from maxtext.layers.quantizations import AqtQuantization as Quant
7777
from maxtext.utils import max_utils
78-
from maxtext.utils.sharding import logical_to_mesh_axes, maybe_shard_with_pspec
78+
from maxtext.utils.sharding import logical_to_mesh_axes, maybe_shard_with_pspec, get_logical_axis_rules_from_config
7979
import numpy as np
8080
# pylint: disable=line-too-long, g-doc-args, g-doc-return-or-yield, bad-continuation, g-inconsistent-quotes
8181
# pytype: disable=attribute-error
@@ -628,7 +628,7 @@ def maybe_create_nnx(einsum, *args):
628628
self.AqtEinsum_3 = jnp.einsum
629629

630630
def _logical_to_mesh_axes(self, logical_name):
631-
logical_rules = None if self.config.using_pipeline_parallelism else self.config.logical_axis_rules
631+
logical_rules = get_logical_axis_rules_from_config(self.config)
632632
return logical_to_mesh_axes(logical_name, mesh=self.mesh, rules=logical_rules)
633633

634634
def check_attention_inputs(self, query: Array, key: Array | KVTensor, value: Array | KVTensor) -> None:

src/maxtext/layers/moe.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
from maxtext.utils import max_utils
4444
from maxtext.utils import maxtext_utils
4545
from maxtext.utils.sharding import create_sharding, maybe_shard_with_logical, maybe_shard_with_pspec
46-
from maxtext.utils.sharding import logical_to_mesh_axes, remove_expert_from_partition_spec
46+
from maxtext.utils.sharding import logical_to_mesh_axes, remove_expert_from_partition_spec, get_logical_axis_rules_from_config
4747
import numpy as np
4848
import qwix
4949
from qwix.contrib.sparsity import sparsity_module
@@ -644,7 +644,7 @@ def _maybe_shard_with_logical(self, inputs, logical_name):
644644
)
645645

646646
def _logical_to_mesh_axes(self, logical_name):
647-
logical_rules = None if self.config.using_pipeline_parallelism else self.config.logical_axis_rules
647+
logical_rules = get_logical_axis_rules_from_config(self.config)
648648
return logical_to_mesh_axes(logical_name, mesh=self.mesh, rules=logical_rules)
649649

650650
def _maybe_shard_with_pspec(self, inputs, pspec: jax.sharding.PartitionSpec | None):

src/maxtext/models/deepseek.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
from maxtext.utils import max_utils
4343
from maxtext.utils.sharding import create_sharding
4444
from maxtext.utils.sharding import maybe_shard_with_logical
45+
from maxtext.utils.sharding import get_logical_axis_rules_from_config
4546

4647
import transformers
4748

@@ -189,7 +190,7 @@ def with_logical_constraint(self, x):
189190
shard_mode=self.config.shard_mode,
190191
debug_sharding=self.config.debug_sharding,
191192
extra_stack_level=1,
192-
rules=self.config.logical_axis_rules,
193+
rules=get_logical_axis_rules_from_config(self.config),
193194
)
194195

195196
def dropout_op(self, x, deterministic):

src/maxtext/models/qwen3.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333

3434
from maxtext.common.common_types import AttentionType, Config, DType, Array, BATCH, EMBED, MODEL_MODE_TRAIN, LENGTH, MODEL_MODE_AUTOREGRESSIVE
3535
from maxtext.common.common_types import KV_BATCH, KV_HEAD
36-
from maxtext.utils.sharding import logical_to_mesh_axes
36+
from maxtext.utils.sharding import logical_to_mesh_axes, get_logical_axis_rules
3737
from maxtext.layers import attentions
3838
from maxtext.layers import initializers as max_initializers
3939
from maxtext.layers import moe
@@ -612,7 +612,7 @@ def __call__(
612612
# mixed_qkvz: (B, S, H_k, 2*D_k + 2*D_v*V_per_K)
613613
mixed_qkvz = qkvz.reshape(new_shape_qkvz)
614614
if self.mesh is not None:
615-
logical_rules = None if self.config.using_pipeline_parallelism else self.config.logical_axis_rules
615+
logical_rules = get_logical_axis_rules()
616616
qkvz_pspec = logical_to_mesh_axes((KV_BATCH, None, KV_HEAD, None), mesh=self.mesh, rules=logical_rules)
617617
qkvz_sharding = jax.sharding.NamedSharding(self.mesh, qkvz_pspec)
618618
mixed_qkvz = jax.lax.with_sharding_constraint(mixed_qkvz, qkvz_sharding)
@@ -845,7 +845,7 @@ def extract_state(c_in, v_len):
845845
compute_dtype=cfg.dtype,
846846
)
847847
elif self.mesh is not None:
848-
logical_rules = self.config.logical_axis_rules
848+
logical_rules = get_logical_axis_rules()
849849
recurrent_state_arg = (
850850
recurrent_state
851851
if recurrent_state is not None

src/maxtext/trainers/pre_train/train.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -667,7 +667,8 @@ def training_loop_iteration(
667667

668668
# Unpack immutable_data
669669
config = immutable_data["config"] # for helpers
670-
logical_axis_rules = immutable_data["logical_axis_rules"]
670+
logical_axis_rules_for_train = immutable_data["logical_axis_rules_for_train"]
671+
logical_axis_rules_for_eval = immutable_data["logical_axis_rules_for_eval"]
671672
shard_optimizer_over_data = immutable_data["shard_optimizer_over_data"]
672673
shard_mode = immutable_data["shard_mode"]
673674
eval_interval = immutable_data["eval_interval"]
@@ -696,7 +697,7 @@ def training_loop_iteration(
696697
else:
697698
step_rng_args = ()
698699
with maybe_record_goodput(recorder, GoodputEvent.STEP, step):
699-
with jax.set_mesh(mesh), nn_partitioning.axis_rules(logical_axis_rules):
700+
with jax.set_mesh(mesh), nn_partitioning.axis_rules(logical_axis_rules_for_train):
700701
if shard_optimizer_over_data and isinstance(model, nn.Module):
701702
state = sharding.maybe_shard_with_name(state, state_mesh_shardings, shard_mode)
702703
state, metrics = p_train_step(state, example_batch, *step_rng_args)
@@ -728,10 +729,12 @@ def training_loop_iteration(
728729
# pylint: disable=not-callable
729730
for eval_batch in eval_data_iterator:
730731
# Shard input eval data
731-
eval_batch = jax.device_put(eval_batch, sharding.get_input_data_sharding(config, mesh))
732+
eval_batch = jax.device_put(
733+
eval_batch, sharding.get_input_data_sharding(config, mesh, rules=config.logical_axis_rules_for_eval)
734+
)
732735
if 0 < eval_steps <= eval_step_count:
733736
break
734-
with jax.set_mesh(mesh), nn_partitioning.axis_rules(logical_axis_rules):
737+
with jax.set_mesh(mesh), nn_partitioning.axis_rules(logical_axis_rules_for_eval):
735738
eval_metrics = p_eval_step(state, eval_batch, *step_rng_args)
736739
eval_step_time_delta = datetime.datetime.now() - last_eval_step_completion
737740
last_eval_step_completion = datetime.datetime.now()
@@ -861,7 +864,8 @@ def train_loop(config, recorder, state=None):
861864

862865
immutable_data = {
863866
"config": config,
864-
"logical_axis_rules": config.logical_axis_rules,
867+
"logical_axis_rules_for_train": config.logical_axis_rules,
868+
"logical_axis_rules_for_eval": config.logical_axis_rules_for_eval,
865869
"shard_optimizer_over_data": config.shard_optimizer_over_data,
866870
"shard_mode": config.shard_mode,
867871
"steps": config.steps,

src/maxtext/utils/sharding.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from pathlib import Path
2121

2222
from flax import linen as nn, nnx
23-
from flax.core.spmd import get_logical_axis_rules
23+
from flax.core.spmd import get_logical_axis_rules as flax_get_logical_axis_rules
2424
import jax
2525
from jax.core import Tracer
2626
from jax.sharding import NamedSharding, PartitionSpec as P, reshard
@@ -40,17 +40,33 @@ def clear_input_shardings_dump():
4040
_ACTIVATION_SHARDINGS_DUMP.clear()
4141

4242

43-
def get_input_data_sharding(config, mesh):
43+
def get_input_data_sharding(config, mesh, rules=None):
4444
"""Get the input data sharding for the model"""
45+
if rules is None:
46+
rules = config.logical_axis_rules
4547
if config.enable_diloco:
46-
data_sharding = create_sharding(
47-
mesh, ["diloco"] + config.input_data_sharding_logical_axes, rules=config.logical_axis_rules
48-
)
48+
data_sharding = create_sharding(mesh, ["diloco"] + config.input_data_sharding_logical_axes, rules=rules)
4949
else:
50-
data_sharding = create_sharding(mesh, config.input_data_sharding_logical_axes, rules=config.logical_axis_rules)
50+
data_sharding = create_sharding(mesh, config.input_data_sharding_logical_axes, rules=rules)
5151
return data_sharding
5252

5353

54+
def get_logical_axis_rules():
55+
"""Get the logical axis rules for the flax model"""
56+
return flax_get_logical_axis_rules()
57+
58+
59+
def get_logical_axis_rules_from_config(config):
60+
"""Get the logical axis rules from the config.
61+
When we use pipeline parallelism or in eval step, we may use
62+
a different logical axis rule than the one in config.
63+
Plan to deprecate (b/536927795) and use get_logical_axis_rules instead.
64+
"""
65+
if config.using_pipeline_parallelism or config.eval_interval != -1:
66+
return None
67+
return config.logical_axis_rules
68+
69+
5470
def _get_sharding_desc(inputs, extra_stack_level):
5571
"""Get the inputs sharding description using inspect module"""
5672
frame = inspect.currentframe()

src/maxtext/utils/train_utils.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,13 +191,14 @@ def jit_train_and_eval_step(
191191
if config.enable_diloco:
192192
train_step_partial = functools.partial(train_step, model, config, state_mesh_shardings, params_shardings)
193193
train_step = diloco.build_diloco_train_step(config, train_step_partial, mesh=mesh)
194-
data_sharding = sharding.get_input_data_sharding(config, mesh)
194+
data_sharding_for_train = sharding.get_input_data_sharding(config, mesh, rules=config.logical_axis_rules)
195+
data_sharding_for_eval = sharding.get_input_data_sharding(config, mesh, rules=config.logical_axis_rules_for_eval)
195196
p_train_step = jit_train_step(
196-
config, model, state, state_mesh_shardings, data_sharding, train_step, params_shardings, mesh=mesh
197+
config, model, state, state_mesh_shardings, data_sharding_for_train, train_step, params_shardings, mesh=mesh
197198
)
198199
p_eval_step = None
199200
if eval_data_iterator:
200-
p_eval_step = jit_eval_step(config, model, state_mesh_shardings, data_sharding, eval_step)
201+
p_eval_step = jit_eval_step(config, model, state_mesh_shardings, data_sharding_for_eval, eval_step)
201202

202203
return p_train_step, p_eval_step
203204

tests/integration/train_tests.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,35 @@ def test_gpu_ring_attention_with_packing(self):
677677
)
678678
train_main(thd_ring_attention)
679679

680+
@pytest.mark.integration_test
681+
@pytest.mark.tpu_only
682+
def test_fractional_eval_batch_size(self):
683+
frac_eval = [ # tests Zero-1 optimizer sharding with gradient accumulation
684+
None,
685+
get_test_config_path(),
686+
f"base_output_directory={self._base_output_directory}",
687+
"run_name=runner_test",
688+
f"dataset_path={self.dataset_path}",
689+
"steps=5",
690+
"enable_checkpointing=False",
691+
"enable_goodput_recording=False",
692+
"dataset_type=synthetic",
693+
"remat_policy=minimal",
694+
"per_device_batch_size=1",
695+
"ici_expert_parallelism=-1",
696+
"ici_fsdp_parallelism=1",
697+
"use_ring_of_experts=True",
698+
"model_name=deepseek3-test",
699+
"eval_per_device_batch_size=0.25",
700+
"eval_interval=3",
701+
"eval_steps=1",
702+
"custom_mesh_and_rule_for_eval=ep-as-cp",
703+
"override_model_config=true",
704+
"base_num_decoder_layers=7",
705+
rf"tokenizer_path={os.path.join(MAXTEXT_ASSETS_ROOT, 'tokenizers', 'tokenizer.llama2')}",
706+
]
707+
train_main(frac_eval)
708+
680709

681710
if __name__ == "__main__":
682711
absltest.main()

0 commit comments

Comments
 (0)