Skip to content

Commit 991ccf0

Browse files
Keep invalid group-norm sentinels from becoming a finite 1.0 step (#8638)
## Summary - DeepSpeed encodes inf/NaN group norms as `-1`. Combining those with `vector_norm` squares the sentinel to `1.0`, so clipping and Adam treat a failed group as healthy and apply an unclipped step. - Treat non-finite or negative group norms as overflow (`inf`), skip the optimizer step, reset overflow each step, and advance the loss scaler once (including ZeRO-3 / SuperOffload / ZenFlow / FP16 fused+unfused / BF16). - Direct `unscale_and_clip_grads` callers map the sentinel to `inf` so they still zero gradients instead of using clip_coef=1. This is split out of the ZeRO-2 oversized-gradient / stream-lifetime work so the sentinel contract can land independently. ## Test plan - [x] CPU, rebased onto current master (`75eb961d1`, includes #8730): 17 passed in `tests/unit/v1/zero/test_invalid_grad_norm.py` (`combine_grad_norm_groups` / `get_global_norm` / `CheckOverflow`, ZeRO-1 clip, CPU-offload combine, ZeRO-3 / BF16 / SuperOffload overflow paths) - [x] GPU (2× H200, prior run): `TestZeROBFloat16Stage3InvalidNormRecovery`. Skipped on the rebase machine (2 GPUs required, 1 available) - [ ] CI unit tests for the touched runtime files, started by the rebase push --------- Signed-off-by: tunji-ruwase_snow <tunji.ruwase@snowflake.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent b90283d commit 991ccf0

10 files changed

Lines changed: 398 additions & 93 deletions

File tree

‎deepspeed/runtime/bf16_optimizer.py‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
from deepspeed.git_version_info import version
1515
from deepspeed.runtime.utils import (get_global_norm_of_tensors, clip_tensors_by_global_norm, DummyOptim,
1616
align_dense_tensors, all_gather_dp_groups, is_model_parallel_parameter,
17-
see_memory_usage, get_norm_with_moe_layers, is_optimized_parameter)
17+
see_memory_usage, get_norm_with_moe_layers, is_invalid_grad_norm,
18+
is_optimized_parameter)
1819
from deepspeed.utils import link_hp_params, lazy_init_hp_params_optimizer_state, fragment_address, groups
1920
from deepspeed.moe.utils import is_moe_param, is_moe_param_group
2021
from deepspeed.utils.bwc import bwc_tensor_model_parallel_rank
@@ -74,6 +75,7 @@ def __init__(self,
7475
self.custom_loss_scaler = False
7576
self.external_loss_scale = None
7677
self.torch_autocast_gradscaler = None
78+
self.overflow = False
7779

7880
self.immediate_grad_update = bfloat16_config.immediate_grad_update
7981

@@ -379,6 +381,7 @@ def step(self, closure=None):
379381
if closure is not None:
380382
raise NotImplementedError(f'{self.__class__} does not support closure.')
381383

384+
self.overflow = False
382385
non_expert_grads_for_norm, expert_grads_for_norm = self.get_grads_for_norm()
383386
non_expert_groups_norm = get_global_norm_of_tensors(input_tensors=non_expert_grads_for_norm,
384387
mpu=self.mpu,
@@ -392,7 +395,12 @@ def step(self, closure=None):
392395

393396
self._global_grad_norm = all_groups_norm
394397

395-
assert all_groups_norm > 0.
398+
if is_invalid_grad_norm(all_groups_norm):
399+
self.overflow = True
400+
self._global_grad_norm = float("inf")
401+
self.clear_hp_grads()
402+
self.clear_lp_grads()
403+
return
396404
if self.clip_grad > 0.:
397405
clip_tensors_by_global_norm(input_tensors=self.get_grads_for_norm(for_clipping=True),
398406
max_norm=self.clip_grad,

‎deepspeed/runtime/fp16/fused_optimizer.py‎

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
import torch
1111
from torch._utils import _flatten_dense_tensors, _unflatten_dense_tensors
1212
from deepspeed.runtime.base_optimizer import DeepSpeedOptimizer
13-
from deepspeed.runtime.utils import get_global_norm, get_flattened_grad_norm, CheckOverflow, get_weight_norm, get_norm_with_moe_layers, is_model_parallel_parameter, is_optimized_parameter
13+
from deepspeed.runtime.utils import (CheckOverflow, get_flattened_grad_norm, get_global_norm, get_norm_with_moe_layers,
14+
get_weight_norm, is_invalid_grad_norm, is_model_parallel_parameter,
15+
is_optimized_parameter)
1416
from deepspeed.runtime.fp16.loss_scaler import LossScaleConfig, LossScaleProfile
15-
from deepspeed.utils import logger, log_dist
17+
from deepspeed.utils import logger
1618
from deepspeed.utils.torch import required_torch_version
1719
from deepspeed.checkpoint.constants import OPTIMIZER_STATE_DICT, CLIP_GRAD
1820
from deepspeed.accelerator import get_accelerator
@@ -146,6 +148,18 @@ def zero_grad(self, set_to_none=True):
146148
p.grad.detach_()
147149
p.grad.zero_()
148150

151+
def _skip_invalid_grad_norm(self):
152+
self.overflow = True
153+
self._global_grad_norm = float("inf")
154+
if self.flatten_grad_norm_mask_list:
155+
self.has_executed_step = True
156+
if self.loss_scale_config.use_grad_scaling:
157+
self._update_scale(self.overflow)
158+
self.zero_grad(set_to_none=True)
159+
for group in self.fp32_groups_flat:
160+
group.grad = None
161+
return self.overflow
162+
149163
def step_fused_adam(self, closure=None):
150164
"""
151165
Not supporting closure.
@@ -162,17 +176,14 @@ def step_fused_adam(self, closure=None):
162176
norm_groups.append(get_weight_norm(grads_groups_flat[i], mpu=self.mpu))
163177

164178
self.overflow = self.overflow_checker.check_using_norm(norm_groups)
165-
if self.loss_scale_config.use_grad_scaling:
166-
prev_scale = self.loss_scale_config.cur_scale
167-
self._update_scale(self.overflow)
168-
169-
if self.overflow:
170-
if self.verbose:
171-
logger.info("[deepspeed] fp16 dynamic loss scale overflow! Skipping step. Attempted loss "
172-
"scale: {}, reducing to {}".format(prev_scale, self.loss_scale_config.cur_scale))
173-
return self.overflow
179+
if self.overflow:
180+
return self._skip_invalid_grad_norm()
174181

175182
scaled_grad_norm = get_global_norm(norm_list=norm_groups)
183+
if is_invalid_grad_norm(scaled_grad_norm):
184+
return self._skip_invalid_grad_norm()
185+
if self.loss_scale_config.use_grad_scaling:
186+
self._update_scale(False)
176187

177188
combined_scale = self.unscale_and_clip_grads(grads_groups_flat, scaled_grad_norm, apply_scale=False)
178189

@@ -260,22 +271,8 @@ def step(self, closure=None):
260271
if self.timers:
261272
self.timers(OVERFLOW_CHECK_TIMER).stop()
262273

263-
if self.loss_scale_config.use_grad_scaling:
264-
prev_scale = self.loss_scale_config.cur_scale
265-
self._update_scale(self.overflow)
266-
if self.overflow:
267-
if self.verbose:
268-
log_dist(
269-
"Overflow detected. Skipping step. Attempted loss "
270-
f"scale: {prev_scale}, reducing to {self.loss_scale_config.cur_scale}",
271-
ranks=[0])
272-
# Clear gradients
273-
for i, group in enumerate(self.fp16_groups):
274-
for p in group:
275-
p.grad = None
276-
if self.timers:
277-
self.timers.log(OVERFLOW_TIMERS)
278-
return self.overflow
274+
if self.overflow:
275+
return self._skip_invalid_grad_norm()
279276

280277
grads_groups_flat = []
281278
non_experts_grads_for_norm = []
@@ -330,8 +327,14 @@ def step(self, closure=None):
330327
norm_type=self.norm_type)
331328

332329
scaled_global_grad_norm = get_global_norm(norm_list=[all_groups_norm])
330+
if is_invalid_grad_norm(scaled_global_grad_norm):
331+
if self.timers:
332+
self.timers(COMPUTE_NORM_TIMER).stop()
333+
return self._skip_invalid_grad_norm()
333334
if self.timers:
334335
self.timers(COMPUTE_NORM_TIMER).stop()
336+
if self.loss_scale_config.use_grad_scaling:
337+
self._update_scale(False)
335338

336339
# Stash unscaled gradient norm
337340
self._global_grad_norm = scaled_global_grad_norm / self.loss_scale_config.cur_scale

‎deepspeed/runtime/fp16/unfused_optimizer.py‎

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
from torch._utils import _flatten_dense_tensors
1313

1414
from deepspeed.runtime.base_optimizer import DeepSpeedOptimizer
15-
from deepspeed.runtime.utils import get_global_norm, CheckOverflow, get_weight_norm, is_optimized_parameter
15+
from deepspeed.runtime.utils import (CheckOverflow, get_global_norm, get_weight_norm, is_invalid_grad_norm,
16+
is_optimized_parameter)
1617
from deepspeed.runtime.fp16.loss_scaler import LossScaleConfig, LossScaleProfile
1718
from deepspeed.utils import logger
1819
from deepspeed.utils.torch import required_torch_version
@@ -121,6 +122,16 @@ def zero_grad(self, set_to_none=True):
121122
p.grad.detach_()
122123
p.grad.zero_()
123124

125+
def _skip_invalid_grad_norm(self):
126+
self.overflow = True
127+
self._global_grad_norm = float("inf")
128+
self._update_scale(self.overflow)
129+
self.zero_grad(set_to_none=True)
130+
for group in self.fp32_groups:
131+
for param in group:
132+
param.grad = None
133+
return self.overflow
134+
124135
def step_fused_lamb(self, closure=None):
125136
"""
126137
Not supporting closure.
@@ -147,16 +158,13 @@ def step_fused_lamb(self, closure=None):
147158
expert_norm_groups.append(expert_norm_group_value)
148159

149160
self.overflow = self.overflow_checker.check_using_norm(norm_groups + expert_norm_groups)
150-
prev_scale = self.loss_scale_config.cur_scale
151-
152-
self._update_scale(self.overflow)
153161
if self.overflow:
154-
if self.verbose:
155-
logger.info("[deepspeed] fp16 dynamic loss scale overflow! Skipping step. Attempted loss "
156-
"scale: {}, reducing to {}".format(prev_scale, self.loss_scale_config.cur_scale))
157-
return self.overflow
162+
return self._skip_invalid_grad_norm()
158163

159164
self._global_grad_norm = get_global_norm(norm_list=norm_groups)
165+
if is_invalid_grad_norm(self._global_grad_norm):
166+
return self._skip_invalid_grad_norm()
167+
self._update_scale(False)
160168
combined_scale = self.unscale_and_clip_grads(self._global_grad_norm, apply_scale=False)
161169
self.optimizer.step(grads=grads_groups, output_params=self.fp16_groups, scale=combined_scale)
162170

@@ -195,14 +203,8 @@ def step(self, closure=None):
195203
return self.step_fused_lamb()
196204

197205
self.overflow = self.overflow_checker.check()
198-
prev_scale = self.loss_scale_config.cur_scale
199-
200-
self._update_scale(self.overflow)
201206
if self.overflow:
202-
if self.verbose:
203-
logger.info("[deepspeed] fp16 dynamic loss scale overflow! Skipping step. Attempted loss "
204-
"scale: {}, reducing to {}".format(prev_scale, self.loss_scale_config.cur_scale))
205-
return self.overflow
207+
return self._skip_invalid_grad_norm()
206208

207209
norm_groups = []
208210
for i, group in enumerate(self.fp16_groups):
@@ -220,6 +222,9 @@ def step(self, closure=None):
220222
fp32_param.grad = fp16_param.grad.to(fp32_param.dtype)
221223

222224
self._global_grad_norm = get_global_norm(norm_list=norm_groups)
225+
if is_invalid_grad_norm(self._global_grad_norm):
226+
return self._skip_invalid_grad_norm()
227+
self._update_scale(False)
223228
self.unscale_and_clip_grads(self._global_grad_norm)
224229

225230
self.optimizer.step()

‎deepspeed/runtime/superoffload/superoffload_stage3.py‎

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
# DeepSpeed Team
55

66
import time
7-
import torch
87
from typing import List
98

109
from deepspeed.runtime.superoffload.superoffload_utils import SuperOffloadCPUOptimizer, TaskKeys, ResultKeys, EventTypes
1110
from deepspeed.runtime.zero.partition_parameters import Parameter, Tensor
1211
from deepspeed.runtime.zero.stage3 import DeepSpeedZeroOptimizer_Stage3
12+
from deepspeed.runtime.utils import combine_grad_norm_groups, is_invalid_grad_norm
1313
from deepspeed.utils.nvtx import instrument_w_nvtx
1414
from deepspeed.utils import logger
1515
from deepspeed.accelerator import get_accelerator
@@ -39,6 +39,7 @@ def __init__(
3939

4040
self.sub_group_to_param_num = {}
4141
self.sub_group_grad_partition_counts = {}
42+
self._submitted_cpu_sub_groups = set()
4243
self.async_cpuadam_num = 0
4344
self.max_grad_numel = 0
4445

@@ -176,6 +177,7 @@ def partition_grads(self, params_to_release: List[Parameter], grad_partitions: L
176177
fp32_param.data,
177178
fp32_param.grad.data,
178179
lr=current_lr)
180+
self._submitted_cpu_sub_groups.add(i)
179181
self.async_cpuadam_num += 1
180182

181183
result = self.superoffload_cpu_optimizer.get_result()
@@ -200,13 +202,22 @@ def step(self, closure=None):
200202
self._pre_step()
201203
self._partition_all_parameters()
202204

203-
if self._overflow_check_and_loss_scale_update():
205+
self._overflow_check_and_loss_scale_update(update_scale=False)
206+
if self.overflow:
204207
if not self.clip_grad:
205208
self._handle_overflow_rollback()
209+
self._loss_scale_update_and_overflow_cleanup()
206210
return
207211

208212
norm_groups = self._get_norm_groups()
209-
scaled_global_grad_norm = torch.linalg.vector_norm(torch.stack(norm_groups))
213+
scaled_global_grad_norm = combine_grad_norm_groups(norm_groups)
214+
if is_invalid_grad_norm(scaled_global_grad_norm):
215+
self.overflow = True
216+
if not self.clip_grad:
217+
self._handle_overflow_rollback()
218+
self._loss_scale_update_and_overflow_cleanup()
219+
return
220+
self._loss_scale_update_and_overflow_cleanup()
210221
self._global_grad_norm = scaled_global_grad_norm / self.loss_scale
211222

212223
timer_names = set()
@@ -221,6 +232,10 @@ def step(self, closure=None):
221232
self.timers(OPTIMIZER_STEP_TIMER).stop()
222233
self._post_step(timer_names)
223234

235+
def _post_step(self, timer_names):
236+
super()._post_step(timer_names)
237+
self._submitted_cpu_sub_groups.clear()
238+
224239
def _step_without_clipping(self, scaled_global_grad_norm, timer_names):
225240
"""Fast path: async CPU steps already completed during backward."""
226241
for sub_group_id, group in enumerate(self.fp16_groups):
@@ -332,17 +347,15 @@ def _sync_cpu_optimizer_step(self,
332347

333348
def _handle_overflow_rollback(self):
334349
"""Handle gradient overflow by rolling back CPU optimizer states."""
335-
for sub_group_id, _ in enumerate(self.fp16_groups):
336-
if self.subgroup_to_device[sub_group_id] == 'cpu':
337-
param_group_id = self.sub_group_to_group_id[sub_group_id]
338-
fp32_param = self.fp32_partitioned_groups_flat[sub_group_id]
339-
340-
# Trigger rollback
341-
self._sync_cpu_optimizer_step(param_group_id,
342-
sub_group_id,
343-
fp32_param.data,
344-
fp32_param.grad.data,
345-
rollback=True)
350+
for sub_group_id in self._submitted_cpu_sub_groups:
351+
param_group_id = self.sub_group_to_group_id[sub_group_id]
352+
fp32_param = self.fp32_partitioned_groups_flat[sub_group_id]
353+
self._sync_cpu_optimizer_step(param_group_id,
354+
sub_group_id,
355+
fp32_param.data,
356+
fp32_param.grad.data,
357+
rollback=True)
358+
self._submitted_cpu_sub_groups.clear()
346359

347360
def _handle_gradient_clipping(self, scaled_global_grad_norm):
348361
"""Handle gradient clipping with CPU optimizer rollback and re-optimization."""

‎deepspeed/runtime/utils.py‎

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import os
1313
import psutil
1414
import gc
15-
from math import sqrt
15+
from math import isfinite, sqrt
1616

1717
from numpy import prod
1818

@@ -227,6 +227,12 @@ def has_inf_or_nan(x):
227227
return torch.isfinite(x.amax()).logical_and(torch.isfinite(x.amin())).logical_not()
228228

229229

230+
def is_invalid_grad_norm(norm):
231+
if torch.is_tensor(norm):
232+
return bool((~torch.isfinite(norm) | (norm < 0)).any())
233+
return not isfinite(float(norm)) or norm < 0
234+
235+
230236
class CheckOverflow(object):
231237
'''Checks for overflow in gradient across parallel process'''
232238

@@ -245,7 +251,7 @@ def __init__(self, param_groups=None, mpu=None, zero_reduce_scatter=False, deeps
245251

246252
def check_using_norm(self, norm_group, reduce_overflow=True):
247253
# TODO: I don't think reduce_overflow is needed if mpu is None
248-
overflow = -1 in norm_group
254+
overflow = any(is_invalid_grad_norm(norm) for norm in norm_group)
249255
overflow_gpu = get_accelerator().FloatTensor([overflow])
250256
if self.has_moe_params:
251257
# In this case, we need to do an all_reduce across
@@ -337,6 +343,8 @@ def _handle_overflow(cpu_sum, x, i):
337343
def get_global_norm(norm_list):
338344
""" Compute total from a list of norms
339345
"""
346+
if any(is_invalid_grad_norm(norm) for norm in norm_list):
347+
return float("inf")
340348
total_norm = 0.0
341349
for norm in norm_list:
342350
total_norm += norm**2.0
@@ -879,6 +887,24 @@ def mask_nan_or_inf_with_val_inplace(input, device=None, val=-1.):
879887
input.masked_fill_(inf_or_nan, err)
880888

881889

890+
def combine_grad_norm_groups(norm_groups, norm_type=2):
891+
"""L2-combine per-group gradient norms without laundering the -1 invalid sentinel.
892+
893+
Several DeepSpeed helpers replace inf/NaN group norms with -1. Combining those
894+
with ``vector_norm`` squares the sentinel to 1.0, so clipping treats a failed
895+
group as a healthy unit-norm group. Non-finite or negative group norms stay
896+
invalid (positive infinity) instead.
897+
"""
898+
norm_groups = list(norm_groups)
899+
device = next((norm.device for norm in norm_groups if torch.is_tensor(norm)), None)
900+
stacked = torch.stack([
901+
norm if torch.is_tensor(norm) else torch.tensor(norm, dtype=torch.float, device=device) for norm in norm_groups
902+
])
903+
invalid = (~torch.isfinite(stacked) | (stacked < 0)).any()
904+
combined = torch.linalg.vector_norm(stacked, ord=norm_type)
905+
return torch.where(invalid, torch.full_like(combined, float("inf")), combined)
906+
907+
882908
def get_global_norm_of_tensors(input_tensors, norm_type=2, mpu=None, moe_ep_group=None):
883909
"""Get norm of an iterable of tensors.
884910
@@ -1109,7 +1135,7 @@ def to_tensor(v):
11091135

11101136
# check if all norms are valid
11111137
group_norms = torch.stack([to_tensor(norm) for norm in group_norms])
1112-
if group_norms.eq(-1).any():
1138+
if is_invalid_grad_norm(group_norms):
11131139
return -1
11141140

11151141
# combine norms

0 commit comments

Comments
 (0)