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

135 lines
4.8 KiB
Python

# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
import torch
import pytest
import deepspeed
from deepspeed.runtime.zero.stage3 import DeepSpeedZeroOptimizer_Stage3
from deepspeed.utils import safe_get_local_grad, safe_set_local_grad
from deepspeed.accelerator import get_accelerator
from unit.simple_model import SimpleModel
import os
def get_config(precision, clip_value, offload_device="cpu"):
config = {
"train_batch_size": 8,
"steps_per_print": 1,
"optimizer": {
"type": "Adam",
"params": {
"lr": 1e-4
}
},
"zero_optimization": {
"stage": 3,
"offload_optimizer": {
"device": offload_device
},
"contiguous_gradients": True,
"overlap_comm": False,
},
"gradient_clipping": 1.0,
}
if precision == "fp16":
config["fp16"] = {
"enabled": True,
"loss_scale": 1024,
"initial_scale_power": 10,
}
elif precision == "bf16":
config["bf16"] = {
"enabled": True,
}
return config
@pytest.mark.parametrize("precision,clip_value,offload_device", [
("fp16", 0.5, "cpu"),
("bf16", 0.05, "cpu"),
("fp16", 0.5, "none"),
("bf16", 0.05, "none"),
])
class TestZeroGradClip():
world_size = 1
def test_grad_clip_and_norm_update(self, precision, clip_value, offload_device):
"""Test custom gradient clipping with configurations and to check if the norm_groups are updated correctly"""
config_dict = get_config(precision, clip_value, offload_device)
model = SimpleModel(hidden_dim=10)
# Set up distributed environment variables
os.environ['LOCAL_RANK'] = '0'
os.environ['RANK'] = '0'
os.environ['WORLD_SIZE'] = '1'
os.environ['MASTER_ADDR'] = '127.0.0.1'
os.environ['MASTER_PORT'] = '29500'
try:
model_engine, optimizer, _, _ = deepspeed.initialize(args=None,
model=model,
config=config_dict,
model_parameters=model.parameters(),
dist_init_required=True)
except Exception as e:
pytest.skip("Could not initialize deepspeed")
assert isinstance(optimizer, DeepSpeedZeroOptimizer_Stage3)
torch.manual_seed(1670)
inputs = torch.randn(8, 10, device=model_engine.device)
targets = torch.randn(8, 10, device=model_engine.device)
if model_engine.fp16_enabled() and get_accelerator().is_fp16_supported():
inputs = inputs.half()
targets = targets.half()
elif model_engine.bfloat16_enabled() and get_accelerator().is_bf16_supported():
inputs = inputs.bfloat16()
targets = targets.bfloat16()
else:
pytest.skip("Unsupported precision")
loss = model_engine(inputs, targets)
model_engine.backward(loss)
pre_clip_norm_groups = optimizer._get_norm_groups()
pre_clip_global_norm = torch.linalg.vector_norm(torch.stack(pre_clip_norm_groups))
modified_count = 0
for param in model_engine.parameters():
if not hasattr(param, 'ds_id'):
continue
grad = safe_get_local_grad(param)
if grad is not None:
pre_clip_norm = grad.norm().item()
clamped_grad = torch.clamp(grad, -clip_value, clip_value)
post_clip_norm = clamped_grad.norm().item()
if pre_clip_norm > clip_value:
# Checks if the post-clip norm is less than the pre-clip norm
assert post_clip_norm < pre_clip_norm, f"Post-clip norm should be < pre-clip norm for param {param.ds_id}"
safe_set_local_grad(param, clamped_grad)
modified_count += 1
# Get post-clip state
post_clip_norm_groups = optimizer._get_norm_groups()
post_clip_global_norm = torch.linalg.vector_norm(torch.stack(post_clip_norm_groups))
assert modified_count > 0, "No parameters were modified during clipping"
assert post_clip_global_norm.item() < pre_clip_global_norm.item(
), f"Post-clip norm {post_clip_global_norm.item():.6f} should be < pre-clip norm {pre_clip_global_norm.item():.6f}"
model_engine.step()
final_norm = optimizer._global_grad_norm
if pre_clip_global_norm.item() > clip_value:
assert post_clip_global_norm.item() < pre_clip_global_norm.item(
), "Global norm should be reduced after clipping when pre-clip norm > clip_value"