Skip to content

Train & Eval on entire dataset when pdbs < 1 - #5158

Draft
muskansh-google wants to merge 2 commits into
AI-Hypercomputer:mainfrom
muskansh-google:fractional_pdbs_bug
Draft

Train & Eval on entire dataset when pdbs < 1#5158
muskansh-google wants to merge 2 commits into
AI-Hypercomputer:mainfrom
muskansh-google:fractional_pdbs_bug

Conversation

@muskansh-google

@muskansh-google muskansh-google commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Description

This PR fixes a bug where data was silently dropped when running with fractional per-device batch sizes (per_device_batch_size < 1.0 or eval_per_device_batch_size < 1.0). In the past, the dataloader loaded at least one sample per device, but loss_fn truncated the input batch down to micro_batch_size_to_train_on or micro_batch_size_to_eval_on, discarding the remaining samples. With this change, the slicing logic is removed, and all loaded samples are processed across microbatches using gradient accumulation during training and forward-only metric accumulation during evaluation.

Why This Solution

Instead of discarding data, setting per_device_batch_size < 1.0 is treated as implicit microbatch accumulation:

  • For training, gradient accumulation is automatically computed over num_microbatches = global_batch_size_to_load // micro_batch_size_to_train_on. All loaded samples are processed across microbatches and accumulated into the gradients.
  • For evaluation, a dedicated forward-only scan loop (_fractional_batch_eval) processes each microbatch and aggregates metrics without performing unnecessary backpropagation.
  • No data is thrown away, batch size calculations are consistent, and throughput matches the loaded dataset volume.

Tests

Added unit tests to verify batch sizing calculations and microbatch accumulation:

  • tests/unit/pyconfig_test.py:
    • test_calculate_global_batch_sizes_unified: Verifies calculation logic across fractional batch sizes, gradient accumulation steps, and expansion factors.
    • test_fractional_per_device_batch_size_calculations: Verifies config initialization with fractional train and eval batch sizes.
  • tests/unit/gradient_accumulation_nnx_test.py:
    • test_fractional_per_device_batch_size_accumulates_all_samples: Verifies gradient accumulation across all 4 samples without data dropping when per_device_batch_size = 0.25.
    • test_fractional_eval_microbatch_reshaping_and_accumulation: Verifies microbatch reshaping and metric accumulation for eval without data dropping.

Commands to run the tests:

pytest tests/unit/pyconfig_test.py -k "calculate_global_batch_sizes_unified or fractional_per_device_batch_size"
pytest tests/unit/gradient_accumulation_nnx_test.py -k "fractional"

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request refactors batch size calculations and execution paths to properly support fractional batch sizes (where per-device batch size is less than 1.0) during both training and evaluation. Instead of decimating data, the code now leverages gradient accumulation or a new fractional evaluation helper to accumulate metrics across micro-batches. The review feedback suggests centralizing the duplicated batch size calculation helper function to a shared utility module and refactoring the hardcoded auxiliary loss keys in the fractional evaluation helper into a list constant to improve maintainability.

Comment on lines +3796 to +3819
def calculate_global_batch_sizes(
per_device_batch_size, expansion_factor, num_devices, grad_accum_steps=1
):
"""Helper to calculate global and micro batch sizes for training or evaluation.

Returns:
tuple: (global_batch_to_load, global_batch, micro_batch)
- global_batch_to_load: Batch size loaded by the dataloader across hosts
(scaled by expansion_factor for partial host loading).
- global_batch: Total effective batch size across all devices and
gradient accumulation steps (to train or eval on).
- micro_batch: Micro-batch size per step across all devices.
"""
micro_batch = int(num_devices * per_device_batch_size)
effective_pdbs = max(1.0, per_device_batch_size)
expansion = expansion_factor if expansion_factor > 0 else 1

global_batch = int(num_devices * effective_pdbs * grad_accum_steps)
global_batch_to_load = int(global_batch * expansion)
# Returns:
# 1. global_batch_to_load: Batch size loaded by dataloader (scaled by expansion factor for partial host loading).
# 2. global_batch: Total effective batch size across all devices and accumulation steps (train_on / eval_on).
# 3. micro_batch: Micro-batch size executed per step across all devices.
return global_batch_to_load, global_batch, micro_batch

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

This calculate_global_batch_sizes helper function duplicates the logic from pyconfig_deprecated.py:calculate_global_batch_sizes. To improve maintainability and prevent future inconsistencies, consider centralizing this logic.

You could move the standalone calculate_global_batch_sizes function from pyconfig_deprecated.py to a shared utility module (e.g., in maxtext/utils/) and import it here and in pyconfig_deprecated.py.

Comment on lines +798 to +817
def accumulate_eval(acc, micro_batch):
_, aux = single_eval_fn(micro_batch)
new_acc = {
"xent_sum": acc["xent_sum"] + aux["xent_sum"],
"total_weights": acc["total_weights"] + aux["total_weights"],
"z_loss": acc["z_loss"] + aux.get("z_loss", 0.0),
"moe_lb_loss": acc["moe_lb_loss"] + aux.get("moe_lb_loss", 0.0),
"indexer_loss": acc["indexer_loss"] + aux.get("indexer_loss", 0.0),
"mtp_loss": acc["mtp_loss"] + aux.get("mtp_loss", 0.0),
}
return new_acc, None

init_acc = {
"xent_sum": 0.0,
"total_weights": 0.0,
"z_loss": 0.0,
"moe_lb_loss": 0.0,
"indexer_loss": 0.0,
"mtp_loss": 0.0,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The init_acc dictionary and the accumulate_eval function hardcode the names of auxiliary losses to be accumulated (e.g., z_loss, moe_lb_loss). This makes the function brittle; if a new auxiliary loss is added to loss_fn, it will be silently ignored during fractional evaluation unless this function is also updated.

To improve maintainability, you could define the list of keys to accumulate in a single place. This makes it easier to add new losses in the future.

For example:

# At the top of the file or as a local constant
ACCUMULATED_EVAL_KEYS = [
    "xent_sum",
    "total_weights",
    "z_loss",
    "moe_lb_loss",
    "indexer_loss",
    "mtp_loss",
]

# ... inside _fractional_batch_eval ...
def accumulate_eval(acc, micro_batch):
  _, aux = single_eval_fn(micro_batch)
  new_acc = {k: acc[k] + aux.get(k, 0.0) for k in ACCUMULATED_EVAL_KEYS}
  return new_acc, None

init_acc = {k: 0.0 for k in ACCUMULATED_EVAL_KEYS}

This would make the accumulation logic more robust to future changes.

@muskansh-google muskansh-google changed the title Train & Eval on all samples when pdbs < 1 Train & Eval on entire dataset when pdbs < 1 Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant