2121import math
2222import os
2323import pathlib
24+ from contextlib import nullcontext
2425from types import SimpleNamespace
2526
2627import torch
@@ -68,6 +69,35 @@ def _optim_steps_per_epoch(num_batches_per_epoch: int, grad_accumulation_steps:
6869 return - (- num_batches_per_epoch // grad_accumulation_steps )
6970
7071
72+ def _should_sync_grads (
73+ * ,
74+ pending_micro_batches : int ,
75+ grad_accumulation_steps : int ,
76+ batch_idx : int ,
77+ batches_per_epoch : int | None ,
78+ is_ddp : bool ,
79+ ) -> bool :
80+ """Return True when this micro-batch's backward should all-reduce gradients.
81+
82+ Under DDP with gradient accumulation only the micro-batch immediately
83+ followed by an ``optimizer.step()`` needs to synchronize: at that point the
84+ locally-accumulated ``.grad`` already holds the whole window's contribution,
85+ so a single all-reduce averages the complete window and the intervening
86+ micro-batches can run under ``no_sync()`` -- saving ``grad_accumulation_steps - 1``
87+ all-reduces per window. That step is either the window closer
88+ (``pending_micro_batches + 1 == grad_accumulation_steps``) or the epoch's
89+ final batch (which the trailing-flush step consumes). When the dataloader
90+ length is unknown we cannot identify the final batch, so we sync every step
91+ (correct, just no speedup). With a single process (no DDP) there is nothing
92+ to synchronize, so this is always True.
93+ """
94+ if not is_ddp or batches_per_epoch is None :
95+ return True
96+ closes_window = pending_micro_batches + 1 == grad_accumulation_steps
97+ is_last_batch = batch_idx == batches_per_epoch - 1
98+ return closes_window or is_last_batch
99+
100+
71101def _all_reduce_mean (value : torch .Tensor ) -> torch .Tensor :
72102 if dist .is_available () and dist .is_initialized ():
73103 dist .all_reduce (value , op = dist .ReduceOp .SUM )
@@ -506,6 +536,11 @@ def run_train_validation_loop(self):
506536 if self .dist_env .is_main :
507537 logger .info ("All %d epochs already completed; nothing to do." , self .num_epochs )
508538 return
539+ try :
540+ batches_per_epoch = len (self .train_dataloader )
541+ except TypeError :
542+ batches_per_epoch = None
543+ is_ddp = isinstance (self .trainer_module , DistributedDataParallel )
509544 for epoch_idx in range (start_epoch , self .num_epochs ):
510545 if hasattr (self .train_dataloader , "sampler" ) and hasattr (self .train_dataloader .sampler , "set_epoch" ):
511546 self .train_dataloader .sampler .set_epoch (epoch_idx )
@@ -525,16 +560,28 @@ def run_train_validation_loop(self):
525560 attention_mask = batch ["attention_mask" ],
526561 loss_mask = batch ["loss_mask" ],
527562 )
528- metrics = self .trainer_module (
529- input_ids = target_batch .input_ids ,
530- attention_mask = target_batch .attention_mask ,
531- loss_mask = target_batch .loss_mask ,
532- input_hidden_states = target_batch .input_hidden_states ,
533- target_hidden_states = target_batch .target_hidden_states ,
534- target_logits = target_batch .target_logits ,
563+ # Skip DDP's per-micro-batch all-reduce on every micro-batch
564+ # except the one an optimizer step immediately follows; that
565+ # step's all-reduce covers the whole locally-accumulated window.
566+ sync_grads = _should_sync_grads (
567+ pending_micro_batches = pending_micro_batches ,
568+ grad_accumulation_steps = self .grad_accumulation_steps ,
569+ batch_idx = batch_idx ,
570+ batches_per_epoch = batches_per_epoch ,
571+ is_ddp = is_ddp ,
535572 )
536- loss = metrics .loss / self .grad_accumulation_steps
537- loss .backward ()
573+ sync_ctx = nullcontext () if sync_grads else self .trainer_module .no_sync ()
574+ with sync_ctx :
575+ metrics = self .trainer_module (
576+ input_ids = target_batch .input_ids ,
577+ attention_mask = target_batch .attention_mask ,
578+ loss_mask = target_batch .loss_mask ,
579+ input_hidden_states = target_batch .input_hidden_states ,
580+ target_hidden_states = target_batch .target_hidden_states ,
581+ target_logits = target_batch .target_logits ,
582+ )
583+ loss = metrics .loss / self .grad_accumulation_steps
584+ loss .backward ()
538585
539586 running_loss += metrics .loss .detach ().item ()
540587 running_acc += metrics .accuracy .detach ().item ()
0 commit comments