Skip to content

Commit f10cb80

Browse files
committed
Raise the per-tensor norms to norm_type when combining them
The p-norm over a set of tensors is (sum_i ||g_i||_p ** p) ** (1/p), so combining per-tensor norms means raising each to norm_type. Three sites take the 1/norm_type root but hardcode the exponent at 2, so they are only correct for norm_type == 2: runtime/utils.py clip_grad_norm_ .square().sum() zero/stage_1_and_2.py get_grad_norm_direct .square().sum() zero/stage3.py get_grad_norm_direct .norm(2), then pow(..., 2) stage3 is doubly wrong: it takes an L2 norm per tensor whatever norm_type says, then sums the squares, then takes the 1/norm_type root. Measured against the p-norm of the concatenated gradients, for grads [3, -4] and [2]: p=1 p=2 p=3 truth 9.000 5.385 4.626 clip_grad_ 53.000 5.385 2.894 zero 1/2 53.000 5.385 2.894 zero 3 29.000 5.385 3.072 The returned norm is wrong, and so is the clip coefficient derived from it, so the gradients get scaled by the wrong factor. clip_grad_norm_ is a regression from #4915, which vectorized the accumulation: the loop there read `total_norm += param_norm.item()**norm_type` before it, and the rewrite replaced that with `.square()` while keeping the 1/norm_type root. The four norm-combining sites that were not touched by that commit 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 in every one of these functions, 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 f10cb80

5 files changed

Lines changed: 72 additions & 4 deletions

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())

deepspeed/runtime/zero/stage3.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2283,15 +2283,17 @@ def get_grad_norm_direct(self, gradients, params, norm_type=2):
22832283
for g, p in zip(gradients, params):
22842284
if is_model_parallel_parameter(p) or (self.model_parallel_rank == 0):
22852285
grad_norms.append(
2286-
g.to(get_accelerator().device_name(), non_blocking=True).to(get_norm_dtype()).norm(2))
2286+
g.to(get_accelerator().device_name(), non_blocking=True).to(get_norm_dtype()).norm(norm_type))
22872287

22882288
# Sum across all model parallel GPUs.
22892289
if len(grad_norms) == 0:
22902290
# FIX https://github.com/deepspeedai/DeepSpeed/issues/3564
22912291
total_norm_cuda = torch.tensor(0, dtype=gradients[0].dtype).to(get_accelerator().device_name()).to(
22922292
get_norm_dtype())
22932293
else:
2294-
total_norm_cuda = torch.sum(torch.pow(torch.stack(grad_norms), 2))
2294+
# Each entry is ||g||_norm_type and the 1/norm_type root is taken below, so
2295+
# both the per-tensor norm above and this exponent follow norm_type.
2296+
total_norm_cuda = torch.sum(torch.pow(torch.stack(grad_norms), norm_type))
22952297

22962298
dist.all_reduce(total_norm_cuda, op=dist.ReduceOp.SUM, group=process_group)
22972299

deepspeed/runtime/zero/stage_1_and_2.py

100755100644
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2090,7 +2090,9 @@ def get_grad_norm_direct(self, gradients, params, norm_type=2):
20902090
torch.linalg.vector_norm(g.data.to(get_norm_dtype()).detach(),
20912091
ord=norm_type).to(get_accelerator().current_device_name()))
20922092
if len(all_norms) > 0:
2093-
total_norm = torch.stack(all_norms).square().sum().float()
2093+
# vector_norm above already gives each ||g||_norm_type, and the 1/norm_type
2094+
# root is taken below, so the exponent here has to be norm_type too.
2095+
total_norm = torch.stack(all_norms).pow(norm_type).sum().float()
20942096
else:
20952097
total_norm = torch.tensor(0.0, dtype=torch.float32).to(self.device)
20962098
# Sum across all model parallel Device.

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

tests/unit/runtime/zero/test_zero_grad_clip.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from deepspeed.utils import safe_get_local_grad, safe_set_local_grad
1111
from deepspeed.accelerator import get_accelerator
1212
from unit.simple_model import SimpleModel
13+
from unit.common import DistributedTest
1314
import os
1415

1516

@@ -48,6 +49,38 @@ def get_config(precision, clip_value, offload_device="cpu"):
4849
return config
4950

5051

52+
@pytest.mark.parametrize("zero_stage", [1, 2, 3])
53+
@pytest.mark.parametrize("norm_type", [1, 2, 3])
54+
class TestZeroGradNormPNorm(DistributedTest):
55+
world_size = 1
56+
57+
def test_matches_flat_norm(self, zero_stage, norm_type):
58+
# get_grad_norm_direct returns the norm of the gradients viewed as a single vector,
59+
# so on one rank with no model parallelism it must equal the p-norm of the
60+
# concatenation. norm_type 2 is the control: it is right on both sides.
61+
config = {
62+
"train_batch_size": 1,
63+
"optimizer": {
64+
"type": "Adam",
65+
"params": {
66+
"lr": 1e-4
67+
}
68+
},
69+
"zero_optimization": {
70+
"stage": zero_stage
71+
},
72+
}
73+
model = SimpleModel(hidden_dim=4, nlayers=2)
74+
engine, optimizer, _, _ = deepspeed.initialize(model=model, model_parameters=model.parameters(), config=config)
75+
76+
gradients = [torch.Tensor([3.0, -4.0]), torch.Tensor([2.0])]
77+
params = list(model.parameters())[:len(gradients)]
78+
expected = torch.cat([g.reshape(-1) for g in gradients]).norm(float(norm_type))
79+
80+
actual = optimizer.get_grad_norm_direct(gradients, params, norm_type=norm_type)
81+
assert torch.allclose(torch.as_tensor(actual).float().cpu(), expected.float().cpu())
82+
83+
5184
@pytest.mark.parametrize("precision,clip_value,offload_device", [
5285
("fp16", 0.5, "cpu"),
5386
("bf16", 0.05, "cpu"),

0 commit comments

Comments
 (0)