Skip to content

Commit da3be38

Browse files
authored
Avoid per-microbatch D2H caused by isfinite (#3873)
Stack from [ghstack](https://github.com/ezyang/ghstack/tree/0.15.0) (oldest at bottom): * __->__ #3873 #3863 introduces a good feature to raise if loss is NaN but also introduces per-micro-batch D2H sync. This PR fixes the issue with the penalty that the actual batch that causes the NaN is unkown (within a certain range). #2728 is a potential solution to remove the penalty. Another mitigation from @mlazos: you could also copy N loss values on GPU to keep a record of which of the N steps had the NaN.
1 parent 957bf38 commit da3be38

2 files changed

Lines changed: 100 additions & 9 deletions

File tree

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
import unittest
8+
from unittest.mock import MagicMock, patch
9+
10+
import torch
11+
12+
from torchtitan.components.loss import IGNORE_INDEX
13+
from torchtitan.trainer import Trainer
14+
15+
16+
class TestInvalidLoss(unittest.TestCase):
17+
"""Trainer.train_step crashes on a non-finite loss, aligned with logging.
18+
19+
The finiteness check reuses the device-to-host copy that logging already
20+
performs, so it only runs on steps where metrics are logged.
21+
"""
22+
23+
def _make_trainer(self, loss_value: float, should_log: bool) -> Trainer:
24+
# Build a bare Trainer and inject only the collaborators train_step
25+
# touches on the non-distributed (single-rank) path.
26+
trainer = object.__new__(Trainer)
27+
28+
trainer.optimizers = MagicMock()
29+
trainer.lr_schedulers = MagicMock()
30+
trainer.lr_schedulers.get_metrics.return_value = {}
31+
trainer.checkpointer = MagicMock()
32+
trainer.model_parts = []
33+
trainer.config = MagicMock()
34+
trainer.config.training.max_norm = 1.0
35+
trainer.device = torch.device("cpu")
36+
trainer.gradient_accumulation_steps = 1
37+
trainer.step = 1
38+
trainer.ntokens_seen = 0
39+
40+
parallel_dims = MagicMock()
41+
parallel_dims.dp_enabled = False
42+
parallel_dims.dp_cp_enabled = False
43+
parallel_dims.ep_enabled = False
44+
parallel_dims.get_optional_mesh.return_value = None
45+
trainer.parallel_dims = parallel_dims
46+
47+
trainer.metrics_processor = MagicMock()
48+
trainer.metrics_processor.should_log.return_value = should_log
49+
50+
# Shadow the bound method so forward/backward returns a canned loss.
51+
trainer.forward_backward_step = MagicMock(return_value=torch.tensor(loss_value))
52+
return trainer
53+
54+
def _data_iterator(self):
55+
labels = torch.tensor([1, 2, IGNORE_INDEX])
56+
input_dict = {"input": torch.tensor([1, 2, 3])}
57+
while True:
58+
yield input_dict, labels
59+
60+
def _run_step(self, loss_value: float, should_log: bool) -> None:
61+
trainer = self._make_trainer(loss_value, should_log)
62+
# sl.* are logging side effects; clip_grad_norm_ needs real params.
63+
with patch("torchtitan.trainer.sl", MagicMock()), patch(
64+
"torchtitan.trainer.dist_utils.clip_grad_norm_",
65+
return_value=torch.tensor(1.0),
66+
):
67+
trainer.train_step(self._data_iterator())
68+
69+
def test_nan_loss_raises_on_log_step(self):
70+
with self.assertRaises(RuntimeError) as ctx:
71+
self._run_step(float("nan"), should_log=True)
72+
self.assertIn("not finite", str(ctx.exception))
73+
74+
def test_inf_loss_raises_on_log_step(self):
75+
with self.assertRaises(RuntimeError) as ctx:
76+
self._run_step(float("inf"), should_log=True)
77+
self.assertIn("not finite", str(ctx.exception))
78+
79+
def test_finite_loss_does_not_raise(self):
80+
self._run_step(1.5, should_log=True)
81+
82+
def test_nan_loss_ignored_when_not_logging(self):
83+
# Detection is gated on logging, so a non-log step must not raise even
84+
# when the loss is NaN.
85+
self._run_step(float("nan"), should_log=False)
86+
87+
88+
if __name__ == "__main__":
89+
unittest.main()

torchtitan/trainer.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import dataclasses
88
import json
9+
import math
910
import os
1011
import time
1112
from collections.abc import Iterable, Iterator
@@ -799,15 +800,6 @@ def train_step(
799800
labels=labels,
800801
global_valid_tokens=global_valid_tokens,
801802
)
802-
803-
if not loss.isfinite():
804-
raise RuntimeError(
805-
f"Loss is not finite (loss={loss.item()}) at step "
806-
f"{self.step}. Dumping the offending microbatch for "
807-
f"debugging -- input_dict: {input_dict}, labels: {labels}, "
808-
f"global_valid_tokens: {global_valid_tokens}"
809-
)
810-
811803
accumulated_losses.append(loss.detach())
812804

813805
with sl.log_trace_span("optim"):
@@ -861,6 +853,16 @@ def train_step(
861853
global_avg_loss = global_max_loss = float(loss.detach().item())
862854
global_ntokens_seen = self.ntokens_seen
863855

856+
# Crash on invalid loss. global_avg_loss is a SUM reduction, so a infinite
857+
# loss on any rank propagates here. This reuses the D2H copy already done
858+
# for logging, so it adds no extra sync.
859+
# TODO: make this step work even logging is off.
860+
if not math.isfinite(global_avg_loss):
861+
raise RuntimeError(
862+
f"Loss is not finite (global_avg_loss={global_avg_loss}) at "
863+
f"step {self.step}. Stopping training."
864+
)
865+
864866
extra_metrics = {
865867
"n_tokens_seen": global_ntokens_seen,
866868
**lr_metrics,

0 commit comments

Comments
 (0)