Skip to content

Commit 500997a

Browse files
Log the skipped optimizer step on gradient overflow (#8417)
ZeRO returns early from `step()` on non-finite gradients. `DynamicLossScaler` reports that from inside `update_scale`, but `CreateLossScaler` only returns it for `fp16 + dynamic_scaling` — bf16 and fp32 always get the static `LossScaler`, whose `update_scale` is an inherited no-op. The skip is counted in `engine.skipped_steps`, but that is only surfaced by `_report_progress`, gated on `steps_per_print`, and `STEPS_PER_PRINT_DEFAULT` is `None`. So on a default bf16 config the loop advances, the LR scheduler steps and metrics are emitted while no weights are updated, with no output at all. Fixes #8415 --------- Signed-off-by: Abhishek Kulkarni <abhishek.kulkarni@snowflake.com> Co-authored-by: Olatunji Ruwase <tunji.ruwase@snowflake.com>
1 parent 3ca100d commit 500997a

2 files changed

Lines changed: 62 additions & 2 deletions

File tree

‎deepspeed/runtime/fp16/loss_scaler.py‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@
2121
Commit: 93ab4bea59dc5cbf97c079d313741866af4deac9
2222
"""
2323

24+
import logging
25+
2426
import torch
2527
from dataclasses import dataclass
2628
from typing import Optional
2729
from enum import Enum
2830
from deepspeed.runtime.config_utils import DeepSpeedConfigObject
2931
from deepspeed import comm as dist
30-
from deepspeed.utils import logger
32+
from deepspeed.utils import log_dist, logger
3133
from deepspeed.runtime.utils import has_inf_or_nan
3234

3335
INITIAL_LOSS_SCALE = 'init_scale'
@@ -147,7 +149,10 @@ def scale_gradient(self, module, grad_in, grad_out):
147149
return tuple(self.loss_scale * g for g in grad_in)
148150

149151
def update_scale(self, overflow):
150-
pass
152+
if overflow:
153+
log_dist("[deepspeed] OVERFLOW! Skipping step. Gradients are non-finite, weights were not updated.",
154+
ranks=[0],
155+
level=logging.WARNING)
151156

152157
def scale_loss(self, loss):
153158
""" Scales the loss by the current loss scale.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
# DeepSpeed Team
5+
6+
import logging
7+
8+
import pytest
9+
import torch
10+
11+
from deepspeed.runtime.fp16.loss_scaler import CreateLossScaler, DynamicLossScaler, LossScaler
12+
13+
14+
@pytest.fixture
15+
def warnings(monkeypatch):
16+
recorded = []
17+
monkeypatch.setattr("deepspeed.utils.logging.logger.log", lambda level, msg, *a, **kw: recorded.append(
18+
(level, msg)))
19+
return recorded
20+
21+
22+
def test_static_scaler_reports_the_skipped_step(warnings):
23+
LossScaler(scale=1.0).update_scale(overflow=True)
24+
25+
assert len(warnings) == 1
26+
level, msg = warnings[0]
27+
assert level == logging.WARNING
28+
assert "OVERFLOW" in msg and "weights were not updated" in msg
29+
30+
31+
def test_static_scaler_is_quiet_without_overflow(warnings):
32+
LossScaler(scale=1.0).update_scale(overflow=False)
33+
34+
assert warnings == []
35+
36+
37+
def test_dynamic_scaler_is_not_double_reported(warnings, monkeypatch):
38+
"""DynamicLossScaler overrides update_scale and logs the skip itself."""
39+
monkeypatch.setattr("deepspeed.comm.get_rank", lambda: 0)
40+
scaler = DynamicLossScaler(init_scale=2**16,
41+
scale_window=1000,
42+
min_scale=1,
43+
delayed_shift=1,
44+
consecutive_hysteresis=False)
45+
scaler.update_scale(overflow=True)
46+
47+
assert not [msg for _, msg in warnings if "weights were not updated" in msg]
48+
49+
50+
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float32, torch.half])
51+
def test_non_dynamic_configs_get_the_reporting_scaler(dtype):
52+
"""bf16 and fp32 never get a DynamicLossScaler, so the static path is the only one."""
53+
scaler = CreateLossScaler(dtype=dtype, static_loss_scale=1.0, dynamic_scaling=False, dynamic_loss_args=None)
54+
55+
assert not isinstance(scaler, DynamicLossScaler)

0 commit comments

Comments
 (0)