Skip to content

Commit f05a068

Browse files
committed
Raise the per-parameter norms to norm_type in clip_grad_norm_
The p-norm over every gradient is (sum_i ||g_i||_p ** p) ** (1/p), so combining the per-parameter norms means raising each to norm_type. clip_grad_norm_ squares them instead and then takes the 1/norm_type root, which is only the same thing when norm_type is 2: grads [3, -4] and [2], norm_type=1 torch.nn.utils.clip_grad_norm_ -> 9.0 deepspeed -> 53.0 (7**2 + 2**2) norm_type=3 torch.nn.utils.clip_grad_norm_ -> 4.626 deepspeed -> 2.894 The returned norm is wrong, and so is the clip coefficient derived from it, so the gradients are scaled by the wrong factor. This is a regression from #4915, which vectorized the accumulation. Before it the loop read `total_norm += param_norm.item()**norm_type`; the rewrite replaced that with `torch.stack(all_norms).square().sum()` and kept the 1/norm_type root. The four other norm-combining sites in this file all still raise to norm_type: get_flattened_grad_norm, get_weight_norm, get_global_norm_of_tensors and get_norm_with_moe_layers. norm_type is float()'d at the top of the function, so the default path becomes pow(2.0), which is bit-identical to square() on float32. Signed-off-by: Vineeth Sai <vineethsai4444@gmail.com>
1 parent 84fd92a commit f05a068

2 files changed

Lines changed: 32 additions & 1 deletion

File tree

deepspeed/runtime/utils.py

100755100644
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,10 @@ def clip_grad_norm_(parameters, max_norm, norm_type=2, mpu=None):
401401
param_norm = p.grad.data.detach().float().norm(norm_type)
402402
all_norms.append(param_norm)
403403
if len(all_norms) > 0:
404-
total_norm = torch.stack(all_norms).square().sum().float()
404+
# The p-norm over every gradient is (sum_i ||g_i||_p ** p) ** (1/p), and the
405+
# 1/norm_type root is taken below, so each per-parameter norm has to be raised
406+
# to norm_type here. Squaring only matches that for norm_type == 2.
407+
total_norm = torch.stack(all_norms).pow(norm_type).sum().float()
405408
else:
406409
total_norm = get_accelerator().FloatTensor([0.0])
407410
total_norm = total_norm.to(get_accelerator().current_device_name())

tests/unit/runtime/test_runtime_utils.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,34 @@ def test_params():
7373
assert torch.equal(params_expected[1].grad, params_actual[1].grad)
7474

7575

76+
class TestClipGradNormPNorm(DistributedTest):
77+
# world_size 1 so this runs wherever the suite runs; the bug is in the per-rank
78+
# recombination of the norms, which is independent of the group size.
79+
world_size = 1
80+
81+
@pytest.mark.parametrize("norm_type", [1, 2, 3])
82+
def test_matches_torch(self, norm_type):
83+
# The p-norm over all gradients is (sum_i ||g_i||_p ** p) ** (1/p). Squaring the
84+
# per-parameter norms computes that only for p == 2, which is the control here.
85+
def test_params():
86+
param1 = torch.nn.Parameter(torch.zeros(2))
87+
param1.grad = torch.Tensor([3.0, -4.0])
88+
param2 = torch.nn.Parameter(torch.zeros(1))
89+
param2.grad = torch.Tensor([2.0])
90+
return [param1, param2]
91+
92+
max_norm = 1.0
93+
params_expected = test_params()
94+
expected_norm = torch.nn.utils.clip_grad_norm_(params_expected, max_norm, norm_type=norm_type)
95+
96+
params_actual = test_params()
97+
actual_norm = ds_utils.clip_grad_norm_(params_actual, max_norm=max_norm, norm_type=norm_type)
98+
99+
assert torch.allclose(actual_norm.float().cpu(), expected_norm.float().cpu())
100+
for expected, actual in zip(params_expected, params_actual):
101+
assert torch.allclose(actual.grad, expected.grad)
102+
103+
76104
@pytest.mark.parametrize("check_using_norm", [(False), (True)])
77105
class TestCheckOverflow(DistributedTest):
78106
world_size = 2

0 commit comments

Comments
 (0)