Skip to content

Split logical rule for eval from for train#4523

Merged
copybara-service[bot] merged 1 commit into
mainfrom
chengnuojin-train-eval-mesh
Jul 22, 2026
Merged

Split logical rule for eval from for train#4523
copybara-service[bot] merged 1 commit into
mainfrom
chengnuojin-train-eval-mesh

Conversation

@NuojCheng

@NuojCheng NuojCheng commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Description

We might want different sharding logical rule for eval compared with train, since the global train batch size and eval batch size are different. This PR enables this feature by introducing two new flags:

  • custom_mesh_and_rule_for_eval
  • logical_axis_rules_for_eval

When custom_mesh_and_rule_for_eval is specified (non-default value), an alternative logical rule is used for the eval pipeline. A typical use case would be using fractional batch size for eval, so that CP/TP like shardings are enabled (only for eval). Same axis is used as other shardings, e.g. FSDP/EP, for the train step.

Currently, we still use the same mesh for both train and eval steps. A potential future optimization would be split the meshes for train and eval, which is in theory possible since train and eval functions are in different jit.It may required careful checks.

This PR only supports auto sharding. For explicit sharding, the logical rule split is way more complexed. I would defer the explicit sharding support to 2nd PR.

FIXES: b/535639231

Tests

Example 1: base rule for train, custom rule for eval

Run on v5p-8: we use FSDP+EP for train step, and EP-as-CP custom rule for eval,

smoke_train model_name=deepseek3-test per_device_batch_size=1 eval_per_device_batch_size=0.25 eval_interval=3 eval_steps=1 ici_expert_parallelism=4 custom_mesh_and_rule_for_eval=ep-as-cp debug_sharding=false profiler=xplane use_ring_of_experts=true  max_target_length=4096

log

xprof

Example 2: custom rule for train, another custom rule for eval

Run on v5p-8: we use EP-as-DP for train step, and EP-as-CP custom rule for eval,

smoke_train model_name=deepseek3-test per_device_batch_size=1 eval_per_device_batch_size=0.25 eval_interval=3 eval_steps=1 ici_expert_parallelism=4 custom_mesh_and_rule_for_eval=ep-as-cp debug_sharding=false profiler=xplane use_ring_of_experts=true  max_target_length=4096  custom_mesh_and_rule=ep-as-dp sharding_tolerance=0.5 override_model_config=true base_num_decoder_layers=7

log

xprof

Checklist

Before submitting this PR, please make sure (put X in square brackets):

  • I have performed a self-review of my code. For an optional AI review, add the gemini-review label.
  • I have necessary comments in my code, particularly in hard-to-understand areas.
  • I have run end-to-end tests tests and provided workload links above if applicable.
  • I have made or will make corresponding changes to the doc if needed, including adding new documentation pages to the relevant Table of Contents (toctree directive) as explained in our documentation.

@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.59259% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/maxtext/trainers/pre_train/train.py 50.00% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@NuojCheng
NuojCheng force-pushed the chengnuojin-train-eval-mesh branch 3 times, most recently from 1972c3b to 7e78b58 Compare July 17, 2026 22:17
@NuojCheng
NuojCheng marked this pull request as ready for review July 17, 2026 23:01
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@NuojCheng
NuojCheng force-pushed the chengnuojin-train-eval-mesh branch from 7e78b58 to 77bb651 Compare July 20, 2026 18:28
@github-actions

Copy link
Copy Markdown
Contributor

🤖 Hi @RissyRan, I've received your request, and I'm working on it now! You can track my progress in the logs for more details.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📋 Review Summary

This pull request introduces the ability to split logical axis rules between training and evaluation phases, allowing specialized sharding strategies (such as CP/TP) specifically for evaluation under fractional batch sizes. The overall architecture is very well thought out, but several correctness bugs in the fallback validation, active rule detection, and evaluation input sharding should be addressed to ensure seamless operation.

🔍 General Feedback

  • Input Sharding Consistency: Ensure that other data loaders or input processing utilities also correctly pick up the active logical_axis_rules_for_eval during evaluation to prevent layout mismatches.
  • Model-Level Rule Lookups: In addition to Qwen3, double check that other models (like DeepSeek or custom layer architectures) have been migrated to call get_logical_axis_rules() dynamically rather than accessing self.config.logical_axis_rules directly, to ensure they respect the eval-specific sharding rules.
  • Test Coverage: Consider adding a unit test validating that when custom_mesh_and_rule_for_eval is left as default, it successfully falls back and matches the main training logical rules as expected.

"""
if config.using_pipeline_parallelism or config.eval_interval > 1:
return None
return config.logical_axis_rules

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Checking config.eval_interval > 1 is a brittle proxy for determining whether evaluation and training rule sets differ. If a user runs evaluation with eval_interval = 1, this check will evaluate to False and return config.logical_axis_rules (the training rules), overriding and ignoring the custom evaluation rules set in the active context manager during evaluation.

Comparing the rule lists directly or checking if evaluation rules are defined is much more robust and correct.

Suggested change
return config.logical_axis_rules
if config.using_pipeline_parallelism or (
config.logical_axis_rules_for_eval is not None
and config.logical_axis_rules_for_eval != config.logical_axis_rules
):

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I definitly agree that eval_interval > 1 is not the best way for selecting logical rules. I have tried using flax meta data to collect logical rules but it gets some unit test failures. I will be working on b/536927795 to deprecate this usage as a followup step.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you trying to distinguish if eval is turned on? If so, wondering if eval_per_device_batch_size a better indicator. I see default is 0:

eval_per_device_batch_size: 0.0
.

In MLPerf 6.0 runs, we set eval_interval=1. Or maybe you could check if eval_interval as -1:

eval_interval: -1 # the specific number of train step between eval_step

@NuojCheng NuojCheng Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the suggestion! Updated using eval_per_device_batch_size > 0 now.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic in

if getattr(self, "eval_per_device_batch_size", 0.0) == 0.0:
sets eval pdb same with pdb. I will use eval_interval != -1 instead.

Comment thread src/maxtext/models/qwen3.py
Comment on lines 731 to +733
if 0 < eval_steps <= eval_step_count:
break
with jax.set_mesh(mesh), nn_partitioning.axis_rules(logical_axis_rules):
with jax.set_mesh(mesh), nn_partitioning.axis_rules(logical_axis_rules_for_eval):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 During evaluation, the batch eval_batch is sharded on line 728 via sharding.get_input_data_sharding(config, mesh), which uses config.logical_axis_rules (the training rules) by default. Since the evaluation loop runs inside the context manager nn_partitioning.axis_rules(logical_axis_rules_for_eval), this mismatch will cause costly resharding/communication overhead or compilation failures.

To resolve this, update sharding.get_input_data_sharding to accept an optional rules=None parameter:

def get_input_data_sharding(config, mesh, rules=None):
  if rules is None:
    rules = config.logical_axis_rules
  # ... Use rules instead of config.logical_axis_rules

And then pass logical_axis_rules_for_eval here:

      eval_batch = jax.device_put(
          eval_batch, sharding.get_input_data_sharding(config, mesh, rules=logical_axis_rules_for_eval)
      )

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Updated

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like the Gemini PR review shows better quality now.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I agree!

Comment thread src/maxtext/configs/base.yml Outdated
Comment on lines +619 to +620
# Set as true a different logical rule is used for eval step
custom_mesh_and_rule_for_eval: ""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 The comment says # Set as true a different logical rule is used for eval step, but custom_mesh_and_rule_for_eval is a string identifier for custom rules (like "ep-as-cp") rather than a boolean. Clarify the comment to prevent user confusion.

Suggested change
# Set as true a different logical rule is used for eval step
custom_mesh_and_rule_for_eval: ""
# Customized mesh and logical rules for evaluation (e.g. "ep-as-cp")
custom_mesh_and_rule_for_eval: ""

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

Comment thread src/maxtext/configs/types.py Outdated
Comment on lines +2739 to +2746
# Handle eval custom mesh and rule
if self.custom_mesh_and_rule_for_eval is CustomRule.DEFAULT:
# Fallback to primary rule if eval is DEFAULT
self.custom_mesh_and_rule_for_eval = self.custom_mesh_and_rule
else:
eval_config = self._load_mesh_config_from_yaml(self.custom_mesh_and_rule_for_eval.value)
if "logical_axis_rules" in eval_config:
self.logical_axis_rules_for_eval = eval_config["logical_axis_rules"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 The fallback logic is incomplete when custom_mesh_and_rule_for_eval is DEFAULT. While self.custom_mesh_and_rule_for_eval is correctly updated to copy self.custom_mesh_and_rule, self.logical_axis_rules_for_eval is never updated and remains empty []. This will cause evaluation steps to use empty sharding rules instead of falling back to the training sharding rules, leading to compilation failures or incorrect device sharding.

Suggested change
# Handle eval custom mesh and rule
if self.custom_mesh_and_rule_for_eval is CustomRule.DEFAULT:
# Fallback to primary rule if eval is DEFAULT
self.custom_mesh_and_rule_for_eval = self.custom_mesh_and_rule
else:
eval_config = self._load_mesh_config_from_yaml(self.custom_mesh_and_rule_for_eval.value)
if "logical_axis_rules" in eval_config:
self.logical_axis_rules_for_eval = eval_config["logical_axis_rules"]
# Handle eval custom mesh and rule
if self.custom_mesh_and_rule_for_eval is CustomRule.DEFAULT:
# Fallback to primary rule if eval is DEFAULT
self.custom_mesh_and_rule_for_eval = self.custom_mesh_and_rule
self.logical_axis_rules_for_eval = self.logical_axis_rules
else:
eval_config = self._load_mesh_config_from_yaml(self.custom_mesh_and_rule_for_eval.value)
if "logical_axis_rules" in eval_config:
self.logical_axis_rules_for_eval = eval_config["logical_axis_rules"]
else:
self.logical_axis_rules_for_eval = self.logical_axis_rules

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

@NuojCheng
NuojCheng force-pushed the chengnuojin-train-eval-mesh branch from 77bb651 to bd59acc Compare July 20, 2026 21:59

@RissyRan RissyRan left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the change! A few minor comments for naming.

I didn't see you test feature for logical_axis_rules_for_eval. Do you forsee potential conflict when applying separately?

config = immutable_data["config"] # for helpers
logical_axis_rules = immutable_data["logical_axis_rules"]
logical_axis_rules_train = immutable_data["logical_axis_rules_train"]
logical_axis_rules_for_eval = immutable_data["logical_axis_rules_for_eval"]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we align the name from logical_axis_rules_for_eval to logical_axis_rules_eval?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the names to

  • logical_axis_rules_for_train
  • logical_axis_rules_for_eval
    for consistency. Good point!

Comment on lines 731 to +733
if 0 < eval_steps <= eval_step_count:
break
with jax.set_mesh(mesh), nn_partitioning.axis_rules(logical_axis_rules):
with jax.set_mesh(mesh), nn_partitioning.axis_rules(logical_axis_rules_for_eval):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like the Gemini PR review shows better quality now.

Comment thread src/maxtext/utils/train_utils.py Outdated
train_step_partial = functools.partial(train_step, model, config, state_mesh_shardings, params_shardings)
train_step = diloco.build_diloco_train_step(config, train_step_partial, mesh=mesh)
data_sharding = sharding.get_input_data_sharding(config, mesh)
train_data_sharding = sharding.get_input_data_sharding(config, mesh, rules=config.logical_axis_rules)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see some misalignment with logical_axis_rules_train and logical_axis_rules. Do you think we could let them aligned? i.e. we could use logical_axis_rules for training stage if that makes things simple?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree! Now they are

  • data_sharding_for_train
  • data_sharding_for_eval

"""
if config.using_pipeline_parallelism or config.eval_interval > 1:
return None
return config.logical_axis_rules

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you trying to distinguish if eval is turned on? If so, wondering if eval_per_device_batch_size a better indicator. I see default is 0:

eval_per_device_batch_size: 0.0
.

In MLPerf 6.0 runs, we set eval_interval=1. Or maybe you could check if eval_interval as -1:

eval_interval: -1 # the specific number of train step between eval_step

@NuojCheng
NuojCheng force-pushed the chengnuojin-train-eval-mesh branch from bd59acc to 94ca83b Compare July 21, 2026 00:25
@NuojCheng
NuojCheng force-pushed the chengnuojin-train-eval-mesh branch from 94ca83b to 583b6a6 Compare July 21, 2026 20:37
@NuojCheng
NuojCheng force-pushed the chengnuojin-train-eval-mesh branch from 583b6a6 to de63b4a Compare July 22, 2026 00:45
@NuojCheng

Copy link
Copy Markdown
Collaborator Author

Regarding to question

I didn't see you test feature for logical_axis_rules_for_eval. Do you forsee potential conflict when applying separately?

I added an integration test protecting this feature. Thank you for bringing it up!

@copybara-service
copybara-service Bot merged commit 085068d into main Jul 22, 2026
83 of 87 checks passed
@copybara-service
copybara-service Bot deleted the chengnuojin-train-eval-mesh branch July 22, 2026 17:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants