Skip to content

[Code scan] Aggregate pretraining DDP validation metrics across ranks #38

Description

@njzjz

This issue is a result of a Codex global repository scan.

Summary

In pretraining DDP evaluation, each rank evaluates its validation shard, but rank 0 chooses best-checkpoint and early-stopping decisions from its local metrics. The metric reducer multiplies bsz by world size, but loss sums and sample counts are not all-reduced or gathered before checkpoint decisions.

Code references

if save_every > 0 and self.global_step % save_every == 0:
val_metrics = None
if self.valid_dataloader is not None:
val_metrics = self.evaluate(self.global_step, log=self.rank == 0)
if self.rank == 0:
self._save_checkpoint(epoch, self.global_step, 'checkpoint_last.ckpt')
if self.valid_dataloader is not None and val_metrics is not None:
curr_loss = val_metrics.get('loss', float('inf'))
if curr_loss < self.best_loss:
self.best_loss = curr_loss
self.no_improve_steps = 0
logger.info(
f"New best model at step {self.global_step} with loss {curr_loss:.4f}"
)
self._save_checkpoint(epoch, self.global_step, 'checkpoint_best.ckpt')
else:
self.no_improve_steps += 1
if (
self.patience >= 0
and self.no_improve_steps > self.patience
):
logger.info(
f"Early stopping triggered at step {self.global_step}"
)
stop_training = True
self._save_checkpoint(epoch, self.global_step)

def evaluate(self, step, log: bool = False):
self.model.eval()
logging_infos = []
with torch.no_grad():
for batch in self.valid_dataloader:
net_input, net_target = self.decorate_batch(batch)
loss, sample_size, logging_info = self.loss_fn(
self.model, net_input, net_target
)
logging_infos.append(logging_info)
metrics = self.reduce_metrics(
logging_infos,
writer=self.writer,
logger=logger if log else None,
step=step,
split="valid",
unit="Step",
)

def reduce_metrics(
self,
logging_outputs,
writer=None,
logger=None,
step=None,
split="train",
unit="Epoch",
epoch=None,
inner_step=None,
inner_total=None,
):
metrics_mean = self.loss_fn.reduce_metrics(logging_outputs, split=split)
if split.startswith("train") and self.fp16 and self.scaler is not None:
metrics_mean["loss_scale"] = self.scaler.loss_scale
if "bsz" in metrics_mean:
metrics_mean["bsz"] *= self.world_size

Impact

Best-checkpoint selection and early stopping can depend on only rank 0 validation samples, not the full validation set. This can select the wrong checkpoint or stop too early/late in distributed pretraining.

Suggested fix

All-reduce metric numerators and counts, or gather logging outputs from all ranks, before reduce_metrics and before checkpoint or early-stop decisions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions