Skip to content

Commit ded2349

Browse files
aryanputtatohtana
andauthored
Validate fp16 dynamic loss scaling parameters are positive (#8050)
## What `fp16.loss_scale_window` and `fp16.min_loss_scale` drive dynamic loss scaling but are not validated, so invalid values initialize silently and fail later during training: - **`loss_scale_window`** is used as `stable_interval % self.scale_window` in `DynamicLossScaler.update_scale` (`deepspeed/runtime/fp16/loss_scaler.py`), so a value of `0` raises `ZeroDivisionError` mid-training. - **`min_loss_scale`** is the loss-scale floor (`max(cur_scale / scale_factor, min_scale)`); a value `<= 0` collapses dynamic loss scaling. This is the same class of silent-misconfiguration bug as `fp16.loss_scale` accepting `inf`, fixed in #7889. ## Change Add a single Pydantic `mode="before"` field validator on `DeepSpeedFP16Config` covering both fields. It rejects `bool`, non-numeric, non-finite (`inf`/`-inf`/`nan`), and non-positive values, raising a clear `ValidationError` (e.g. `fp16.loss_scale_window must be > 0`). Following the #7889 review, `mode="before"` runs prior to type coercion (so `True` is rejected), and `float()` is wrapped in `try/except` so `[]`/`{}` surface a clear `ValidationError` rather than a raw `TypeError`. ## Tests Adds `tests/unit/runtime/test_precision_config_dynamic_scale.py`, parametrized over both fields: - invalid: `0, -1, inf, nan, True, [], {}` -> `ValidationError` - valid: `1, 1000, "2"` -> accepted ```bash pytest -q tests/unit/runtime/test_precision_config_dynamic_scale.py ``` The validator logic was verified against the full matrix locally; the import-level test runs under CI. --------- Signed-off-by: Aryan <aryansputta@gmail.com> Co-authored-by: Masahiro Tanaka <81312776+tohtana@users.noreply.github.com>
1 parent ad026a1 commit ded2349

2 files changed

Lines changed: 93 additions & 1 deletion

File tree

deepspeed/runtime/precision_config.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# DeepSpeed Team
55

66
import math
7-
from pydantic import field_validator
7+
from pydantic import field_validator, model_validator
88
from deepspeed.runtime.config_utils import DeepSpeedConfigModel
99
from .fp16.loss_scaler import (
1010
INITIAL_LOSS_SCALE,
@@ -160,6 +160,40 @@ def _validate_loss_scale(cls, v):
160160
Maintain master weights in optimizer state as fp16 instead of fp32 (valid with DeepSpeedCPUAdam only).
161161
"""
162162

163+
@field_validator("loss_scale_window", "min_loss_scale", mode="before")
164+
@classmethod
165+
def _reject_non_integer_scale_params(cls, v, info):
166+
# Pydantic coerces bool to int (True -> 1, False -> 0) and floats to int,
167+
# so a bool or non-finite value would silently pass the positivity check
168+
# in _validate_dynamic_loss_scale_params. Reject those here before coercion.
169+
field = f"fp16.{info.field_name}"
170+
if isinstance(v, bool):
171+
raise ValueError(f"{field} must be an integer, not bool")
172+
if isinstance(v, float) and not math.isfinite(v):
173+
raise ValueError(f"{field} must be a finite number (not inf/-inf/nan)")
174+
try:
175+
int(v)
176+
except (TypeError, ValueError):
177+
raise ValueError(f"{field} must be an integer")
178+
return v
179+
180+
@model_validator(mode="after")
181+
def _validate_dynamic_loss_scale_params(self):
182+
# loss_scale_window and min_loss_scale only take effect when dynamic loss
183+
# scaling is active, i.e. fp16 is enabled and loss_scale == 0 (see
184+
# DeepSpeedEngine.dynamic_loss_scale). Validating them otherwise would
185+
# reject valid static-loss-scale configs that carry unused values.
186+
if self.enabled and self.loss_scale == 0:
187+
# loss_scale_window is used as `stable_interval % scale_window` in
188+
# DynamicLossScaler.update_scale, so 0 raises ZeroDivisionError.
189+
if self.loss_scale_window <= 0:
190+
raise ValueError(
191+
"fp16.loss_scale_window must be > 0 when dynamic loss scaling is enabled (loss_scale=0)")
192+
# min_loss_scale is the loss-scale floor, which collapses if <= 0.
193+
if self.min_loss_scale <= 0:
194+
raise ValueError("fp16.min_loss_scale must be > 0 when dynamic loss scaling is enabled (loss_scale=0)")
195+
return self
196+
163197
def initial_dynamic_scale(self):
164198
return 2**self.initial_scale_power
165199

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
# DeepSpeed Team
5+
6+
import pytest
7+
from pydantic import ValidationError
8+
9+
from deepspeed.runtime.precision_config import DeepSpeedFP16Config
10+
11+
12+
@pytest.mark.parametrize("field", ["loss_scale_window", "min_loss_scale"])
13+
@pytest.mark.parametrize("value", [0, -1])
14+
def test_fp16_dynamic_scale_rejects_nonpositive_when_dynamic(field, value):
15+
# Dynamic loss scaling is active when fp16 is enabled and loss_scale == 0.
16+
with pytest.raises(ValidationError):
17+
DeepSpeedFP16Config(enabled=True, loss_scale=0, **{field: value})
18+
19+
20+
@pytest.mark.parametrize("field", ["loss_scale_window", "min_loss_scale"])
21+
@pytest.mark.parametrize("value", [1, 1000])
22+
def test_fp16_dynamic_scale_accepts_positive_when_dynamic(field, value):
23+
cfg = DeepSpeedFP16Config(enabled=True, loss_scale=0, **{field: value})
24+
assert getattr(cfg, field) > 0
25+
26+
27+
@pytest.mark.parametrize("field", ["loss_scale_window", "min_loss_scale"])
28+
@pytest.mark.parametrize("value", [0, -1])
29+
def test_fp16_dynamic_scale_ignored_with_static_loss_scale(field, value):
30+
# With a static loss scale (loss_scale > 0) these fields are unused, so a
31+
# non-positive value must not fail config construction (compatibility).
32+
cfg = DeepSpeedFP16Config(enabled=True, loss_scale=128, **{field: value})
33+
assert getattr(cfg, field) == value
34+
35+
36+
@pytest.mark.parametrize("field", ["loss_scale_window", "min_loss_scale"])
37+
@pytest.mark.parametrize("value", [0, -1])
38+
def test_fp16_dynamic_scale_ignored_when_fp16_disabled(field, value):
39+
# When fp16 is disabled the dynamic scaling fields are unused.
40+
cfg = DeepSpeedFP16Config(enabled=False, loss_scale=0, **{field: value})
41+
assert getattr(cfg, field) == value
42+
43+
44+
@pytest.mark.parametrize("field", ["loss_scale_window", "min_loss_scale"])
45+
@pytest.mark.parametrize("value", [True, False])
46+
def test_fp16_dynamic_scale_rejects_bool(field, value):
47+
# Pydantic coerces bool to int (True -> 1), which would otherwise slip past
48+
# the positivity check. Bools must be rejected before coercion.
49+
with pytest.raises(ValidationError):
50+
DeepSpeedFP16Config(enabled=True, loss_scale=0, **{field: value})
51+
52+
53+
@pytest.mark.parametrize("field", ["loss_scale_window", "min_loss_scale"])
54+
@pytest.mark.parametrize("value", [float("inf"), float("nan"), "abc", None])
55+
def test_fp16_dynamic_scale_rejects_non_integer(field, value):
56+
# Non-finite and non-numeric values must be rejected rather than coerced.
57+
with pytest.raises(ValidationError):
58+
DeepSpeedFP16Config(enabled=True, loss_scale=0, **{field: value})

0 commit comments

Comments
 (0)