Train & Eval on entire dataset when pdbs < 1 - #5158
Conversation
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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.
| 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, | ||
| } |
There was a problem hiding this comment.
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.
Description
This PR fixes a bug where data was silently dropped when running with fractional per-device batch sizes (
per_device_batch_size < 1.0oreval_per_device_batch_size < 1.0). In the past, the dataloader loaded at least one sample per device, butloss_fntruncated the input batch down tomicro_batch_size_to_train_onormicro_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.0is treated as implicit microbatch accumulation: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._fractional_batch_eval) processes each microbatch and aggregates metrics without performing unnecessary backpropagation.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 whenper_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: