Files
2026-07-13 13:18:33 +08:00

210 lines
6.5 KiB
Python

# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
import math
from pydantic import field_validator, model_validator
from deepspeed.runtime.config_utils import DeepSpeedConfigModel
from .fp16.loss_scaler import (
INITIAL_LOSS_SCALE,
SCALE_WINDOW,
DELAYED_SHIFT,
CONSECUTIVE_HYSTERESIS,
MIN_LOSS_SCALE,
)
#########################################
# BFLOAT16 support
#########################################
# BFLOAT16 feature. By default, this feature is not enabled.
# Users can configure in ds_config.json as below example:
BFLOAT16_FORMAT = '''
BFLOAT16 parameters should be of the format:
"bf16": {
"enabled": true,
"immediate_grad_update": false,
"check_grad_overflow": true
}
'''
BFLOAT16 = "bf16"
BFLOAT16_OLD = "bfloat16" # keeping for backwards compatibility
def get_bfloat16_config(param_dict):
bf16_config_dict = param_dict.get(BFLOAT16, None)
if bf16_config_dict is None:
bf16_config_dict = param_dict.get(BFLOAT16_OLD, {})
return DeepSpeedBF16Config(**bf16_config_dict)
class DeepSpeedBF16Config(DeepSpeedConfigModel):
"""
For bfloat16 configuration
"""
enabled: bool = False
"""
Enable bfloat16 mixed-precision training/inference
"""
immediate_grad_update: bool = False
"""
Apply gradient updates immediately rather than delayed.
"""
check_grad_overflow: bool = True
"""
Detect gradient overflow/underflow before the optimizer step and skip the step
when detected. Defaults to True to match the fp16 default. See issue #5242 and
PR #6976 for context.
"""
bf16_master_weights_and_grads: bool = False
"""
Maintain master weights/gradients in bf16 precision for ZeRO optimizer.
"""
bf16_optimizer_states: bool = False
"""
Keep optimizer states in bf16 (only valid when bf16_master_weights_and_grads is enabled).
"""
#########################################
# FP16 support
#########################################
# FP16 feature. By default, this feature is not enabled.
# Users can configure in ds_config.json as below example:
FP16_FORMAT = '''
FP16 parameters should be of the format:
"fp16": {
"enabled": true,
"auto_cast": false,
"loss_scale": 0,
"initial_scale_power": 16,
"loss_scale_window": 1000,
"hysteresis": 2,
"consecutive_hysteresis": false,
"min_loss_scale": 1
}
'''
FP16 = "fp16"
def get_float16_config(param_dict):
fp16_config_dict = param_dict.get(FP16, {})
return DeepSpeedFP16Config(**fp16_config_dict)
class DeepSpeedFP16Config(DeepSpeedConfigModel):
"""
For float16 configuration
"""
enabled: bool = False
"""
Enable fp16 mixed-precision training/inference
"""
auto_cast: bool = False
"""
Automatically cast inputs to fp16
"""
loss_scale: float = 0
"""
Loss scaling value. Default value of 0 means dynamic loss scaling instead of static loss scale.
"""
@field_validator("loss_scale", mode="before")
@classmethod
def _validate_loss_scale(cls, v):
if isinstance(v, bool):
raise ValueError("fp16.loss_scale must be a number, not bool")
try:
v = float(v)
except (TypeError, ValueError):
raise ValueError("fp16.loss_scale must be a number")
if not math.isfinite(v):
raise ValueError("fp16.loss_scale must be a finite number (not inf/-inf/nan)")
if v < 0:
raise ValueError("fp16.loss_scale must be >= 0 (0 enables dynamic loss scaling)")
return v
initial_scale_power: int = 16
"""
For dynamic loss scaling, set initial loss scale to 2^{initial_scale_power}.
"""
loss_scale_window: int = 1000
"""
Iteration intervals for raising/lowering dynamic loss scale value.
"""
hysteresis: int = 2
"""
Delay shift in dynamic loss scaling.
"""
consecutive_hysteresis: bool = False
"""
Refill hysteresis if iteration does not overflow/underflow.
"""
min_loss_scale: int = 1
"""
Minimum dynamic loss scale value.
"""
fp16_master_weights_and_grads: bool = False
"""
Maintain master weights in optimizer state as fp16 instead of fp32 (valid with DeepSpeedCPUAdam only).
"""
@field_validator("loss_scale_window", "min_loss_scale", mode="before")
@classmethod
def _reject_non_integer_scale_params(cls, v, info):
# Pydantic coerces bool to int (True -> 1, False -> 0) and floats to int,
# so a bool or non-finite value would silently pass the positivity check
# in _validate_dynamic_loss_scale_params. Reject those here before coercion.
field = f"fp16.{info.field_name}"
if isinstance(v, bool):
raise ValueError(f"{field} must be an integer, not bool")
if isinstance(v, float) and not math.isfinite(v):
raise ValueError(f"{field} must be a finite number (not inf/-inf/nan)")
try:
int(v)
except (TypeError, ValueError):
raise ValueError(f"{field} must be an integer")
return v
@model_validator(mode="after")
def _validate_dynamic_loss_scale_params(self):
# loss_scale_window and min_loss_scale only take effect when dynamic loss
# scaling is active, i.e. fp16 is enabled and loss_scale == 0 (see
# DeepSpeedEngine.dynamic_loss_scale). Validating them otherwise would
# reject valid static-loss-scale configs that carry unused values.
if self.enabled and self.loss_scale == 0:
# loss_scale_window is used as `stable_interval % scale_window` in
# DynamicLossScaler.update_scale, so 0 raises ZeroDivisionError.
if self.loss_scale_window <= 0:
raise ValueError(
"fp16.loss_scale_window must be > 0 when dynamic loss scaling is enabled (loss_scale=0)")
# min_loss_scale is the loss-scale floor, which collapses if <= 0.
if self.min_loss_scale <= 0:
raise ValueError("fp16.min_loss_scale must be > 0 when dynamic loss scaling is enabled (loss_scale=0)")
return self
def initial_dynamic_scale(self):
return 2**self.initial_scale_power
def dynamic_loss_scale_args(self):
return {
INITIAL_LOSS_SCALE: 2**self.initial_scale_power,
SCALE_WINDOW: self.loss_scale_window,
DELAYED_SHIFT: self.hysteresis,
CONSECUTIVE_HYSTERESIS: self.consecutive_hysteresis,
MIN_LOSS_SCALE: self.min_loss_scale,
}