1435 lines
57 KiB
Python
1435 lines
57 KiB
Python
# Copyright (c) Microsoft Corporation.
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
# DeepSpeed Team
|
|
|
|
import pytest
|
|
import torch
|
|
import deepspeed.comm as dist
|
|
import deepspeed
|
|
from torch.nn.parallel import DistributedDataParallel as DDP
|
|
|
|
from unit.common import DistributedTest, preferred_dtype, allclose_on_all_ranks
|
|
from unit.simple_model import SimpleModel, random_dataloader
|
|
from deepspeed.accelerator import get_accelerator
|
|
from deepspeed.utils import safe_get_full_grad
|
|
|
|
|
|
class SimpleNonScalarModel(torch.nn.Module):
|
|
"""Model that returns non-scalar output for testing tensor.backward(grad)"""
|
|
|
|
def __init__(self, hidden_dim):
|
|
super().__init__()
|
|
self.linear1 = torch.nn.Linear(hidden_dim, hidden_dim)
|
|
self.linear2 = torch.nn.Linear(hidden_dim, hidden_dim)
|
|
|
|
def forward(self, x):
|
|
# Returns non-scalar output
|
|
x = self.linear1(x)
|
|
x = self.linear2(x)
|
|
return x
|
|
|
|
|
|
class SimpleOutputModel(torch.nn.Module):
|
|
"""Model that returns output without computing loss"""
|
|
|
|
def __init__(self, hidden_dim):
|
|
super().__init__()
|
|
self.linear1 = torch.nn.Linear(hidden_dim, hidden_dim)
|
|
self.linear2 = torch.nn.Linear(hidden_dim, hidden_dim)
|
|
|
|
def forward(self, x):
|
|
x = self.linear1(x)
|
|
x = self.linear2(x)
|
|
return x
|
|
|
|
|
|
def get_config_dict(zero_stage, gradient_accumulation_steps=1):
|
|
"""Helper to create config dict with common settings"""
|
|
config_dict = {
|
|
"train_micro_batch_size_per_gpu": 2,
|
|
"gradient_accumulation_steps": gradient_accumulation_steps,
|
|
"steps_per_print": 1,
|
|
"zero_optimization": {
|
|
"stage": zero_stage,
|
|
},
|
|
"optimizer": {
|
|
"type": "Adam",
|
|
"params": {
|
|
"lr": 1e-3
|
|
}
|
|
},
|
|
}
|
|
|
|
if zero_stage == 3:
|
|
# For ZeRO-3, force partitioning of all parameters
|
|
config_dict["zero_optimization"]["stage3_param_persistence_threshold"] = 0
|
|
|
|
if get_accelerator().is_bf16_supported():
|
|
config_dict["bf16"] = {"enabled": True}
|
|
elif get_accelerator().is_fp16_supported():
|
|
config_dict["fp16"] = {"enabled": True, "initial_scale_power": 8}
|
|
|
|
return config_dict
|
|
|
|
|
|
def collect_gradients_safe(model):
|
|
"""Collect gradients from model parameters using safe_get_full_grad API"""
|
|
grads = {}
|
|
for name, param in model.named_parameters():
|
|
if param.requires_grad:
|
|
grad = safe_get_full_grad(param)
|
|
if grad is not None:
|
|
# Remove 'module.' prefix if present (DeepSpeed wraps the model)
|
|
clean_name = name.replace('module.', '')
|
|
grads[clean_name] = grad.detach().clone().cpu()
|
|
return grads
|
|
|
|
|
|
def initialize_distributed():
|
|
deepspeed.init_distributed(dist_backend=get_accelerator().communication_backend_name())
|
|
device = get_accelerator().current_device_name()
|
|
rank = get_accelerator().current_device()
|
|
dtype = preferred_dtype()
|
|
return device, rank, dtype
|
|
|
|
|
|
def create_ddp_model(model_class, device, rank, dtype, seed=42, lr=1e-3, **model_kwargs):
|
|
torch.manual_seed(seed)
|
|
model = model_class(**model_kwargs)
|
|
model = model.to(device=device, dtype=dtype)
|
|
model = DDP(model, device_ids=[rank], output_device=rank)
|
|
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
|
|
return model, optimizer
|
|
|
|
|
|
def create_deepspeed_engine(model_class, zero_stage, seed=42, gradient_accumulation_steps=1, **model_kwargs):
|
|
torch.manual_seed(seed)
|
|
model = model_class(**model_kwargs)
|
|
|
|
config = get_config_dict(zero_stage, gradient_accumulation_steps=gradient_accumulation_steps)
|
|
engine, _, _, _ = deepspeed.initialize(config=config, model=model, model_parameters=model.parameters())
|
|
return engine
|
|
|
|
|
|
def create_deepspeed_engine_from_model(model, zero_stage, gradient_accumulation_steps=1):
|
|
config = get_config_dict(zero_stage, gradient_accumulation_steps=gradient_accumulation_steps)
|
|
engine, _, _, _ = deepspeed.initialize(config=config, model=model, model_parameters=model.parameters())
|
|
return engine
|
|
|
|
|
|
def setup_models_and_engines(model_class, zero_stage, seed=42, lr=1e-3, gradient_accumulation_steps=1, **model_kwargs):
|
|
# Initialize distributed environment
|
|
device, rank, dtype = initialize_distributed()
|
|
|
|
# Create DDP model
|
|
model_ddp, optimizer_ddp = create_ddp_model(model_class, device, rank, dtype, seed=seed, lr=lr, **model_kwargs)
|
|
|
|
# Create DeepSpeed engine
|
|
model_engine = create_deepspeed_engine(model_class,
|
|
zero_stage,
|
|
seed=seed,
|
|
gradient_accumulation_steps=gradient_accumulation_steps,
|
|
**model_kwargs)
|
|
|
|
return model_ddp, optimizer_ddp, model_engine, device, dtype
|
|
|
|
|
|
def collect_ddp_gradients(model_ddp):
|
|
"""Collect gradients from DDP model"""
|
|
grads = {}
|
|
for name, param in model_ddp.named_parameters():
|
|
if param.grad is not None:
|
|
clean_name = name.replace('module.', '')
|
|
grads[clean_name] = param.grad.detach().clone().cpu()
|
|
return grads
|
|
|
|
|
|
def compare_gradients(grads_ddp, grads_ds, step_info=""):
|
|
"""Compare gradients between DDP and DeepSpeed.
|
|
|
|
Uses PyTorch's default tolerances for the tensor dtype (e.g., for bfloat16:
|
|
rtol=1.6e-2, atol=1e-5). The 2-layer model keeps differences small enough
|
|
to pass with default tolerances even after multiple optimizer steps.
|
|
"""
|
|
step_suffix = f" at {step_info}" if step_info else ""
|
|
assert len(grads_ddp) == len(grads_ds), \
|
|
f"Different number of parameters with gradients{step_suffix}: DDP={len(grads_ddp)}, DeepSpeed={len(grads_ds)}"
|
|
|
|
for name in grads_ddp.keys():
|
|
assert name in grads_ds, f"Parameter {name} missing in DeepSpeed gradients{step_suffix}"
|
|
grad_ddp = grads_ddp[name]
|
|
grad_ds = grads_ds[name]
|
|
# If dtypes differ, convert ds to match ddp's dtype
|
|
if grad_ds.dtype != grad_ddp.dtype:
|
|
grad_ds = grad_ds.to(grad_ddp.dtype)
|
|
# Use PyTorch's default tolerances for the dtype
|
|
allclose_on_all_ranks(grad_ddp, grad_ds, assert_message=f"Gradients differ for parameter {name}{step_suffix}")
|
|
|
|
|
|
def collect_ddp_parameters(model_ddp):
|
|
"""Collect parameters from DDP model"""
|
|
params = {}
|
|
for name, param in model_ddp.named_parameters():
|
|
clean_name = name.replace('module.', '')
|
|
params[clean_name] = param.detach().clone().cpu()
|
|
return params
|
|
|
|
|
|
def collect_deepspeed_parameters(model_engine, zero_stage):
|
|
"""Collect parameters from DeepSpeed engine (handles ZeRO-3 gathering)"""
|
|
params = {}
|
|
for name, param in model_engine.named_parameters():
|
|
clean_name = name.replace('module.', '')
|
|
if zero_stage == 3:
|
|
with deepspeed.zero.GatheredParameters([param], modifier_rank=None):
|
|
params[clean_name] = param.detach().clone().cpu()
|
|
else:
|
|
params[clean_name] = param.detach().clone().cpu()
|
|
return params
|
|
|
|
|
|
def compare_parameters(params_ddp, params_ds, step_info=""):
|
|
"""Compare parameters between DDP and DeepSpeed"""
|
|
step_suffix = f" at {step_info}" if step_info else ""
|
|
assert len(params_ddp) == len(params_ds), \
|
|
f"Parameter count mismatch{step_suffix}: DDP={len(params_ddp)}, DeepSpeed={len(params_ds)}"
|
|
|
|
for name in params_ddp.keys():
|
|
assert name in params_ds, f"Parameter {name} missing in DeepSpeed model{step_suffix}"
|
|
# Convert to fp32 for comparison in case of dtype mismatch
|
|
params_ddp_fp32 = params_ddp[name].float()
|
|
params_ds_fp32 = params_ds[name].float()
|
|
allclose_on_all_ranks(params_ddp_fp32,
|
|
params_ds_fp32,
|
|
assert_message=f"Parameter {name} mismatch{step_suffix}")
|
|
|
|
|
|
@pytest.mark.parametrize("zero_stage", [1, 2, 3])
|
|
class TestZeroUserBackwardBasic(DistributedTest):
|
|
"""Test basic functionality of user backward (loss.backward()) by comparing with PyTorch DDP"""
|
|
world_size = 2
|
|
|
|
def test_loss_backward_matches_ddp(self, zero_stage):
|
|
"""Test that DeepSpeed loss.backward() produces same gradients as PyTorch DDP"""
|
|
hidden_dim = 4
|
|
|
|
# Create DDP and DeepSpeed models
|
|
model_ddp, optimizer_ddp, model_engine, device, dtype = setup_models_and_engines(model_class=SimpleModel,
|
|
zero_stage=zero_stage,
|
|
hidden_dim=hidden_dim,
|
|
nlayers=2)
|
|
|
|
# Create data
|
|
data_loader = random_dataloader(model=model_engine, total_samples=8, hidden_dim=hidden_dim, device=device)
|
|
|
|
# Run one training step with both models
|
|
batch = next(iter(data_loader))
|
|
|
|
# DDP: forward and backward
|
|
optimizer_ddp.zero_grad()
|
|
loss_ddp = model_ddp(batch[0], batch[1])
|
|
loss_ddp.backward()
|
|
grads_ddp = collect_ddp_gradients(model_ddp)
|
|
|
|
# DeepSpeed: forward and backward
|
|
loss_ds = model_engine(batch[0], batch[1])
|
|
loss_ds.backward()
|
|
grads_ds = collect_gradients_safe(model_engine)
|
|
|
|
# Compare gradients
|
|
compare_gradients(grads_ddp, grads_ds)
|
|
|
|
model_engine.destroy()
|
|
|
|
|
|
@pytest.mark.parametrize("zero_stage", [1, 2, 3])
|
|
class TestZeroUserBackwardNonScalar(DistributedTest):
|
|
"""Test non-scalar backward support"""
|
|
world_size = 2
|
|
|
|
def test_non_scalar_backward(self, zero_stage):
|
|
"""Test that tensor.backward(grad) works correctly by comparing with PyTorch DDP"""
|
|
hidden_dim = 4
|
|
batch_size = 2
|
|
|
|
# Create DDP and DeepSpeed models
|
|
model_ddp, optimizer_ddp, model_engine, device, dtype = setup_models_and_engines(
|
|
model_class=SimpleNonScalarModel, zero_stage=zero_stage, hidden_dim=hidden_dim)
|
|
|
|
# Create input data
|
|
torch.manual_seed(123)
|
|
x = torch.randn(batch_size, hidden_dim, device=device, dtype=dtype)
|
|
|
|
# DDP: forward and non-scalar backward
|
|
optimizer_ddp.zero_grad()
|
|
output_ddp = model_ddp(x)
|
|
grad_output = torch.ones_like(output_ddp)
|
|
output_ddp.backward(grad_output)
|
|
ddp_grads = collect_ddp_gradients(model_ddp)
|
|
|
|
# DeepSpeed: forward and non-scalar backward
|
|
output_deepspeed = model_engine(x)
|
|
grad_output_ds = torch.ones_like(output_deepspeed)
|
|
output_deepspeed.backward(grad_output_ds)
|
|
deepspeed_grads = collect_gradients_safe(model_engine)
|
|
|
|
# Compare gradients
|
|
compare_gradients(ddp_grads, deepspeed_grads, "after non-scalar backward")
|
|
|
|
# Run optimizer step
|
|
optimizer_ddp.step()
|
|
model_engine.step()
|
|
|
|
# Collect and compare parameters after step
|
|
ddp_params = collect_ddp_parameters(model_ddp)
|
|
deepspeed_params = collect_deepspeed_parameters(model_engine, zero_stage)
|
|
compare_parameters(ddp_params, deepspeed_params, "after non-scalar backward")
|
|
|
|
model_engine.destroy()
|
|
|
|
|
|
@pytest.mark.parametrize("zero_stage", [1, 2, 3])
|
|
class TestZeroUserBackwardGradAccumulation(DistributedTest):
|
|
"""Test gradient accumulation with user backward"""
|
|
world_size = 2
|
|
|
|
def test_grad_accumulation(self, zero_stage):
|
|
"""Test that gradient accumulation works correctly with loss.backward() by comparing with DDP"""
|
|
hidden_dim = 4
|
|
gradient_accumulation_steps = 4
|
|
|
|
# Create DDP and DeepSpeed models with gradient accumulation
|
|
model_ddp, optimizer_ddp, model_engine, device, _ = setup_models_and_engines(
|
|
model_class=SimpleModel,
|
|
zero_stage=zero_stage,
|
|
gradient_accumulation_steps=gradient_accumulation_steps,
|
|
hidden_dim=hidden_dim,
|
|
nlayers=2)
|
|
|
|
# Create data
|
|
data_loader = random_dataloader(model=model_engine, total_samples=16, hidden_dim=hidden_dim, device=device)
|
|
|
|
# Run training with gradient accumulation
|
|
for i, batch in enumerate(data_loader):
|
|
# DDP: Manual gradient accumulation
|
|
loss_ddp = model_ddp(batch[0], batch[1])
|
|
(loss_ddp / gradient_accumulation_steps).backward()
|
|
|
|
# DeepSpeed: Built-in gradient accumulation
|
|
loss_ds = model_engine(batch[0], batch[1])
|
|
loss_ds.backward()
|
|
|
|
# Compare gradients at accumulation boundary
|
|
if model_engine.is_gradient_accumulation_boundary():
|
|
grads_ddp = collect_ddp_gradients(model_ddp)
|
|
grads_ds = collect_gradients_safe(model_engine)
|
|
compare_gradients(grads_ddp, grads_ds, f"step {i}")
|
|
|
|
# Step both optimizers
|
|
optimizer_ddp.step()
|
|
optimizer_ddp.zero_grad()
|
|
|
|
# Step DeepSpeed (handles gradient accumulation internally)
|
|
model_engine.step()
|
|
|
|
model_engine.destroy()
|
|
|
|
def test_grad_accumulation_scale_wrt_gas_false(self, zero_stage):
|
|
"""Test that scale_wrt_gas=False disables gradient scaling by accumulation steps.
|
|
|
|
When scale_wrt_gas=False is passed to engine.backward(), gradients should NOT be
|
|
scaled by gradient_accumulation_steps. This is useful when users want to handle
|
|
gradient scaling themselves (e.g., using Hugging Face Accelerate).
|
|
"""
|
|
hidden_dim = 4
|
|
gradient_accumulation_steps = 4
|
|
|
|
# Create DDP and DeepSpeed models with gradient accumulation
|
|
model_ddp, optimizer_ddp, model_engine, device, _ = setup_models_and_engines(
|
|
model_class=SimpleModel,
|
|
zero_stage=zero_stage,
|
|
gradient_accumulation_steps=gradient_accumulation_steps,
|
|
hidden_dim=hidden_dim,
|
|
nlayers=2)
|
|
|
|
# Create data
|
|
data_loader = random_dataloader(model=model_engine, total_samples=16, hidden_dim=hidden_dim, device=device)
|
|
|
|
# Run training with gradient accumulation but WITHOUT scaling by GAS
|
|
for i, batch in enumerate(data_loader):
|
|
# DDP: Do NOT divide by GAS (since we're testing scale_wrt_gas=False)
|
|
loss_ddp = model_ddp(batch[0], batch[1])
|
|
loss_ddp.backward()
|
|
|
|
# DeepSpeed: Use scale_wrt_gas=False to disable gradient scaling
|
|
loss_ds = model_engine(batch[0], batch[1])
|
|
model_engine.backward(loss_ds, scale_wrt_gas=False)
|
|
|
|
# Compare gradients at accumulation boundary
|
|
if model_engine.is_gradient_accumulation_boundary():
|
|
grads_ddp = collect_ddp_gradients(model_ddp)
|
|
grads_ds = collect_gradients_safe(model_engine)
|
|
compare_gradients(grads_ddp, grads_ds, f"step {i} with scale_wrt_gas=False")
|
|
|
|
# Step both optimizers
|
|
optimizer_ddp.step()
|
|
optimizer_ddp.zero_grad()
|
|
|
|
# Step DeepSpeed (handles gradient accumulation internally)
|
|
model_engine.step()
|
|
|
|
model_engine.destroy()
|
|
|
|
|
|
@pytest.mark.parametrize("zero_stage", [1, 2, 3])
|
|
class TestZeroUserBackwardMultipleEngines(DistributedTest):
|
|
"""Test multiple DeepSpeed engines with combined loss without manual _backward_epilogue()"""
|
|
world_size = 2
|
|
|
|
def test_multiple_engines_combined_loss(self, zero_stage):
|
|
"""Test that multiple engines work with combined loss.backward() without manual _backward_epilogue()
|
|
|
|
This test compares the behavior with PyTorch DDP baseline to ensure correctness.
|
|
"""
|
|
hidden_dim = 4
|
|
batch_size = 2
|
|
num_models = 3
|
|
lr = 1e-3
|
|
|
|
# Initialize distributed
|
|
device, rank, dtype = initialize_distributed()
|
|
|
|
# Create DDP baseline models
|
|
ddp_models = []
|
|
ddp_optimizers = []
|
|
for i in range(num_models):
|
|
model, optimizer = create_ddp_model(SimpleModel,
|
|
device,
|
|
rank,
|
|
dtype,
|
|
seed=42 + i,
|
|
lr=lr,
|
|
hidden_dim=hidden_dim,
|
|
nlayers=2)
|
|
ddp_models.append(model)
|
|
ddp_optimizers.append(optimizer)
|
|
|
|
# Create multiple DeepSpeed engines with identical initialization
|
|
model_engines = []
|
|
for i in range(num_models):
|
|
engine = create_deepspeed_engine(SimpleModel, zero_stage, seed=42 + i, hidden_dim=hidden_dim, nlayers=2)
|
|
model_engines.append(engine)
|
|
|
|
# Create same input for all models
|
|
torch.manual_seed(123)
|
|
x = torch.randn(batch_size, hidden_dim, device=device, dtype=dtype)
|
|
y = torch.randint(0, hidden_dim, (batch_size, ), device=device)
|
|
|
|
# DDP baseline: compute losses and combined backward
|
|
for optimizer in ddp_optimizers:
|
|
optimizer.zero_grad()
|
|
|
|
ddp_losses = []
|
|
for model in ddp_models:
|
|
loss = model(x, y)
|
|
ddp_losses.append(loss)
|
|
|
|
ddp_combined_loss = sum(l / (i + 1) for i, l in enumerate(ddp_losses))
|
|
ddp_combined_loss.backward()
|
|
|
|
# Collect DDP gradients for each model
|
|
ddp_grads_per_model = [collect_ddp_gradients(model) for model in ddp_models]
|
|
|
|
# DeepSpeed: compute losses and combined backward WITHOUT manual _backward_epilogue()
|
|
ds_losses = [engine(x, y) for engine in model_engines]
|
|
ds_combined_loss = sum(l / (i + 1) for i, l in enumerate(ds_losses))
|
|
ds_combined_loss.backward()
|
|
|
|
# Collect DeepSpeed gradients for each engine and compare with DDP
|
|
for engine_idx, engine in enumerate(model_engines):
|
|
ds_grads = collect_gradients_safe(engine)
|
|
ddp_grads = ddp_grads_per_model[engine_idx]
|
|
assert len(ds_grads) > 0, f"Engine {engine_idx} has no gradients after combined_loss.backward()"
|
|
compare_gradients(ddp_grads, ds_grads, f"Engine {engine_idx}")
|
|
|
|
# Step all DDP models
|
|
for optimizer in ddp_optimizers:
|
|
optimizer.step()
|
|
optimizer.zero_grad()
|
|
|
|
# Step all DeepSpeed engines
|
|
for engine in model_engines:
|
|
engine.step()
|
|
engine.optimizer.zero_grad()
|
|
|
|
# Run another iteration to ensure everything still works
|
|
torch.manual_seed(456)
|
|
x2 = torch.randn(batch_size, hidden_dim, device=device, dtype=dtype)
|
|
y2 = torch.randint(0, hidden_dim, (batch_size, ), device=device)
|
|
|
|
# DDP second iteration
|
|
ddp_losses2 = [model(x2, y2) for model in ddp_models]
|
|
ddp_combined_loss2 = sum(l / (i + 1) for i, l in enumerate(ddp_losses2))
|
|
ddp_combined_loss2.backward()
|
|
ddp_grads_per_model2 = [collect_ddp_gradients(model) for model in ddp_models]
|
|
|
|
# DeepSpeed second iteration
|
|
ds_losses2 = [engine(x2, y2) for engine in model_engines]
|
|
ds_combined_loss2 = sum(l / (i + 1) for i, l in enumerate(ds_losses2))
|
|
ds_combined_loss2.backward()
|
|
|
|
# Verify gradients again and compare with DDP
|
|
for engine_idx, engine in enumerate(model_engines):
|
|
ds_grads = collect_gradients_safe(engine)
|
|
ddp_grads = ddp_grads_per_model2[engine_idx]
|
|
assert len(ds_grads) > 0, f"Engine {engine_idx} has no gradients in second iteration"
|
|
compare_gradients(ddp_grads, ds_grads, f"Engine {engine_idx} (iter 2)")
|
|
|
|
# Step both
|
|
for optimizer in ddp_optimizers:
|
|
optimizer.step()
|
|
|
|
for engine in model_engines:
|
|
engine.step()
|
|
|
|
# Cleanup
|
|
for engine in model_engines:
|
|
engine.destroy()
|
|
|
|
|
|
@pytest.mark.parametrize("zero_stage", [1, 2, 3])
|
|
class TestZeroUserBackwardSeparateLoss(DistributedTest):
|
|
"""Test using separate loss functions"""
|
|
world_size = 2
|
|
|
|
def test_separate_loss_function(self, zero_stage):
|
|
"""Test that separate loss function works correctly by comparing with PyTorch DDP"""
|
|
hidden_dim = 4
|
|
batch_size = 2
|
|
|
|
# Create DDP and DeepSpeed models
|
|
model_ddp, optimizer_ddp, model_engine, device, dtype = setup_models_and_engines(model_class=SimpleOutputModel,
|
|
zero_stage=zero_stage,
|
|
hidden_dim=hidden_dim)
|
|
|
|
# Define loss function separately
|
|
loss_fn = torch.nn.CrossEntropyLoss()
|
|
|
|
# Create input data
|
|
torch.manual_seed(456)
|
|
x = torch.randn(batch_size, hidden_dim, device=device, dtype=dtype)
|
|
y = torch.randint(0, hidden_dim, (batch_size, ), device=device)
|
|
|
|
# DDP: forward, loss, backward
|
|
optimizer_ddp.zero_grad()
|
|
output_ddp = model_ddp(x)
|
|
loss_ddp = loss_fn(output_ddp, y)
|
|
loss_ddp.backward()
|
|
grads_ddp = collect_ddp_gradients(model_ddp)
|
|
|
|
# DeepSpeed: forward, loss, backward
|
|
output_ds = model_engine(x)
|
|
loss_ds = loss_fn(output_ds, y)
|
|
loss_ds.backward()
|
|
grads_ds = collect_gradients_safe(model_engine)
|
|
|
|
# Compare gradients
|
|
compare_gradients(grads_ddp, grads_ds)
|
|
|
|
model_engine.destroy()
|
|
|
|
|
|
class LeafModuleModel(torch.nn.Module):
|
|
"""Model with ModuleList that uses all parameters - for testing leaf module compatibility"""
|
|
|
|
def __init__(self, hidden_dim):
|
|
super().__init__()
|
|
# ModuleList where all branches are used in forward pass
|
|
self.branches = torch.nn.ModuleList([
|
|
torch.nn.Linear(hidden_dim, hidden_dim),
|
|
torch.nn.Linear(hidden_dim, hidden_dim),
|
|
])
|
|
self.final_layer = torch.nn.Linear(hidden_dim, hidden_dim)
|
|
|
|
def forward(self, x, y):
|
|
# Use all branches - add their outputs together
|
|
x = self.branches[0](x) + self.branches[1](x)
|
|
x = self.final_layer(x)
|
|
loss = torch.nn.functional.cross_entropy(x, y)
|
|
return loss
|
|
|
|
|
|
class LeafNonScalarModel(torch.nn.Module):
|
|
"""Leaf module model that returns non-scalar output"""
|
|
|
|
def __init__(self, hidden_dim):
|
|
super().__init__()
|
|
self.branches = torch.nn.ModuleList([
|
|
torch.nn.Linear(hidden_dim, hidden_dim),
|
|
torch.nn.Linear(hidden_dim, hidden_dim),
|
|
])
|
|
|
|
def forward(self, x):
|
|
# Use all branches - returns non-scalar output
|
|
return self.branches[0](x) + self.branches[1](x)
|
|
|
|
|
|
@pytest.mark.parametrize("zero_stage", [3])
|
|
class TestZeroUserBackwardLeafModule(DistributedTest):
|
|
"""Test leaf module behavior during backward passes in ZeRO Stage 3"""
|
|
world_size = 2
|
|
|
|
def test_leaf_module_backward(self, zero_stage):
|
|
"""Test that leaf modules work correctly with user backward by comparing with PyTorch DDP
|
|
|
|
This test validates that the leaf_module_count and backward hooks are correctly
|
|
handled in create_reduce_and_remove_grad_hooks.
|
|
"""
|
|
from deepspeed.utils import set_z3_leaf_modules, z3_leaf_module
|
|
|
|
hidden_dim = 4
|
|
batch_size = 2
|
|
lr = 1e-3
|
|
|
|
# Initialize distributed environment
|
|
device, rank, dtype = initialize_distributed()
|
|
|
|
# Create DDP model
|
|
model_ddp, optimizer_ddp = create_ddp_model(LeafModuleModel,
|
|
device,
|
|
rank,
|
|
dtype,
|
|
seed=42,
|
|
lr=lr,
|
|
hidden_dim=hidden_dim)
|
|
|
|
# Create DeepSpeed model and mark leaf modules BEFORE initialization
|
|
torch.manual_seed(42)
|
|
model_deepspeed = LeafModuleModel(hidden_dim=hidden_dim)
|
|
leaf_modules = set_z3_leaf_modules(model_deepspeed, [torch.nn.ModuleList])
|
|
assert len(leaf_modules) == 1, "Expected exactly one ModuleList to be marked as leaf"
|
|
assert z3_leaf_module(model_deepspeed.branches), "ModuleList should be marked as leaf module"
|
|
|
|
# Initialize DeepSpeed engine from the prepared model
|
|
model_engine = create_deepspeed_engine_from_model(model_deepspeed, zero_stage)
|
|
|
|
# Verify leaf_module_count was set correctly
|
|
assert len(model_engine.optimizer.leaf_parameters) == 1, \
|
|
"Expected 1 leaf module in optimizer.leaf_parameters"
|
|
|
|
# Create input data
|
|
torch.manual_seed(123)
|
|
x = torch.randn(batch_size, hidden_dim, device=device, dtype=dtype)
|
|
y = torch.randint(0, hidden_dim, (batch_size, ), device=device)
|
|
|
|
# DDP: forward and backward
|
|
optimizer_ddp.zero_grad()
|
|
loss_ddp = model_ddp(x, y)
|
|
loss_ddp.backward()
|
|
ddp_grads = collect_ddp_gradients(model_ddp)
|
|
|
|
# DeepSpeed: forward and backward with leaf module
|
|
loss_deepspeed = model_engine(x, y)
|
|
loss_deepspeed.backward()
|
|
deepspeed_grads = collect_gradients_safe(model_engine)
|
|
|
|
# Compare gradients
|
|
compare_gradients(ddp_grads, deepspeed_grads, "with leaf modules")
|
|
|
|
model_engine.destroy()
|
|
|
|
def test_leaf_module_non_scalar_backward(self, zero_stage):
|
|
"""Test that leaf modules work correctly with non-scalar backward (tensor.backward(grad))
|
|
|
|
This specifically tests the interaction between leaf modules and non-scalar backward.
|
|
"""
|
|
from deepspeed.utils import set_z3_leaf_modules, z3_leaf_module
|
|
|
|
hidden_dim = 4
|
|
batch_size = 2
|
|
lr = 1e-3
|
|
|
|
# Initialize distributed environment
|
|
device, rank, dtype = initialize_distributed()
|
|
|
|
# Create DDP model
|
|
model_ddp, optimizer_ddp = create_ddp_model(LeafNonScalarModel,
|
|
device,
|
|
rank,
|
|
dtype,
|
|
seed=42,
|
|
lr=lr,
|
|
hidden_dim=hidden_dim)
|
|
|
|
# Create DeepSpeed model and mark leaf modules BEFORE initialization
|
|
torch.manual_seed(42)
|
|
model_deepspeed = LeafNonScalarModel(hidden_dim=hidden_dim)
|
|
leaf_modules = set_z3_leaf_modules(model_deepspeed, [torch.nn.ModuleList])
|
|
assert len(leaf_modules) == 1, "Expected exactly one ModuleList to be marked as leaf"
|
|
assert z3_leaf_module(model_deepspeed.branches), "ModuleList should be marked as leaf module"
|
|
|
|
# Initialize DeepSpeed engine from the prepared model
|
|
model_engine = create_deepspeed_engine_from_model(model_deepspeed, zero_stage)
|
|
|
|
# Verify leaf_module_count was set correctly
|
|
assert len(model_engine.optimizer.leaf_parameters) == 1, \
|
|
"Expected 1 leaf module in optimizer.leaf_parameters"
|
|
|
|
# Create input data
|
|
torch.manual_seed(123)
|
|
x = torch.randn(batch_size, hidden_dim, device=device, dtype=dtype)
|
|
|
|
# DDP: forward and non-scalar backward
|
|
optimizer_ddp.zero_grad()
|
|
output_ddp = model_ddp(x)
|
|
grad_output = torch.ones_like(output_ddp)
|
|
output_ddp.backward(grad_output)
|
|
ddp_grads = collect_ddp_gradients(model_ddp)
|
|
|
|
# DeepSpeed: forward and non-scalar backward with leaf module
|
|
output_deepspeed = model_engine(x)
|
|
grad_output_ds = torch.ones_like(output_deepspeed)
|
|
output_deepspeed.backward(grad_output_ds)
|
|
deepspeed_grads = collect_gradients_safe(model_engine)
|
|
|
|
# Compare gradients
|
|
compare_gradients(ddp_grads, deepspeed_grads, "in leaf module non-scalar backward")
|
|
|
|
model_engine.destroy()
|
|
|
|
|
|
@pytest.mark.sequential
|
|
class TestZeroUserBackwardScaleErrorDetection(DistributedTest):
|
|
"""Test error detection for missing scale() with fp16 in single-process setup"""
|
|
world_size = 1 # Use single process to avoid distributed deadlock issues
|
|
|
|
def test_error_when_backward_without_scale_sequential(self):
|
|
"""Test that error is raised when calling backward() without scale() with fp16"""
|
|
if not get_accelerator().is_fp16_supported():
|
|
pytest.skip("Test requires fp16 support")
|
|
|
|
hidden_dim = 4
|
|
zero_stage = 1 # Use ZeRO stage 1 for simplicity
|
|
|
|
# Initialize distributed
|
|
device, _, _ = initialize_distributed()
|
|
|
|
# Create engine with fp16 - requires scaling
|
|
torch.manual_seed(42)
|
|
model = SimpleModel(hidden_dim=hidden_dim, nlayers=2)
|
|
|
|
config = {
|
|
"train_micro_batch_size_per_gpu": 2,
|
|
"gradient_accumulation_steps": 1,
|
|
"steps_per_print": 1,
|
|
"zero_optimization": {
|
|
"stage": zero_stage,
|
|
},
|
|
"optimizer": {
|
|
"type": "Adam",
|
|
"params": {
|
|
"lr": 1e-3
|
|
}
|
|
},
|
|
"fp16": {
|
|
"enabled": True,
|
|
"initial_scale_power": 8
|
|
}
|
|
}
|
|
|
|
model_engine, _, _, _ = deepspeed.initialize(config=config, model=model, model_parameters=model.parameters())
|
|
|
|
# Verify needs_scaler is True
|
|
from deepspeed.runtime.base_optimizer import ZeROOptimizer
|
|
assert isinstance(model_engine.optimizer, ZeROOptimizer)
|
|
assert model_engine.optimizer.needs_scaler(), "fp16 should require scaling"
|
|
|
|
# Create data
|
|
data_loader = random_dataloader(model=model_engine,
|
|
total_samples=8,
|
|
hidden_dim=hidden_dim,
|
|
device=device,
|
|
dtype=torch.float16)
|
|
batch = next(iter(data_loader))
|
|
|
|
loss = model_engine(batch[0], batch[1])
|
|
|
|
# Calling backward() without scale() should raise RuntimeError
|
|
with pytest.raises(RuntimeError, match="Loss scaling is required"):
|
|
loss.backward()
|
|
|
|
model_engine.destroy()
|
|
|
|
|
|
@pytest.mark.parametrize("zero_stage", [1, 3])
|
|
class TestZeroUserBackwardWithScale(DistributedTest):
|
|
"""Test engine.scale() method for manual backward passes with loss scaling"""
|
|
world_size = 2
|
|
|
|
def test_scale_backward_matches_engine_backward(self, zero_stage):
|
|
"""Test that engine.scale(loss).backward() produces same gradients as engine.backward(loss)"""
|
|
hidden_dim = 4
|
|
|
|
# Create DeepSpeed engines with same seed
|
|
model_engine1 = create_deepspeed_engine(model_class=SimpleModel,
|
|
zero_stage=zero_stage,
|
|
seed=42,
|
|
hidden_dim=hidden_dim,
|
|
nlayers=2)
|
|
model_engine2 = create_deepspeed_engine(model_class=SimpleModel,
|
|
zero_stage=zero_stage,
|
|
seed=42,
|
|
hidden_dim=hidden_dim,
|
|
nlayers=2)
|
|
|
|
# Create data
|
|
device = get_accelerator().current_device_name()
|
|
data_loader = random_dataloader(model=model_engine1, total_samples=8, hidden_dim=hidden_dim, device=device)
|
|
batch = next(iter(data_loader))
|
|
|
|
# Model 1: use engine.backward(loss)
|
|
loss1 = model_engine1(batch[0], batch[1])
|
|
model_engine1.backward(loss1)
|
|
grads1 = collect_gradients_safe(model_engine1)
|
|
|
|
# Model 2: use engine.scale(loss).backward()
|
|
loss2 = model_engine2(batch[0], batch[1])
|
|
scaled_loss = model_engine2.scale(loss2)
|
|
scaled_loss.backward()
|
|
grads2 = collect_gradients_safe(model_engine2)
|
|
|
|
# Compare gradients - they should be identical
|
|
compare_gradients(grads1, grads2, "comparing engine.backward vs engine.scale().backward()")
|
|
|
|
model_engine1.destroy()
|
|
model_engine2.destroy()
|
|
|
|
def test_scale_backward_matches_ddp(self, zero_stage):
|
|
"""Test that engine.scale(loss).backward() produces same gradients as DDP"""
|
|
hidden_dim = 4
|
|
|
|
# Create DDP and DeepSpeed models
|
|
model_ddp, optimizer_ddp, model_engine, device, dtype = setup_models_and_engines(model_class=SimpleModel,
|
|
zero_stage=zero_stage,
|
|
hidden_dim=hidden_dim,
|
|
nlayers=2)
|
|
|
|
# Create data
|
|
data_loader = random_dataloader(model=model_engine, total_samples=8, hidden_dim=hidden_dim, device=device)
|
|
batch = next(iter(data_loader))
|
|
|
|
# DDP: forward and backward
|
|
optimizer_ddp.zero_grad()
|
|
loss_ddp = model_ddp(batch[0], batch[1])
|
|
loss_ddp.backward()
|
|
grads_ddp = collect_ddp_gradients(model_ddp)
|
|
|
|
# DeepSpeed: forward and scale + backward
|
|
loss_ds = model_engine(batch[0], batch[1])
|
|
scaled_loss = model_engine.scale(loss_ds)
|
|
scaled_loss.backward()
|
|
grads_ds = collect_gradients_safe(model_engine)
|
|
|
|
# Compare gradients
|
|
compare_gradients(grads_ddp, grads_ds, "comparing DDP vs engine.scale().backward()")
|
|
|
|
model_engine.destroy()
|
|
|
|
def test_scale_with_gradient_accumulation(self, zero_stage):
|
|
"""Test that engine.scale() works correctly with gradient accumulation"""
|
|
hidden_dim = 4
|
|
gradient_accumulation_steps = 4
|
|
|
|
# Create models with gradient accumulation
|
|
model_ddp, optimizer_ddp, model_engine, device, _ = setup_models_and_engines(
|
|
model_class=SimpleModel,
|
|
zero_stage=zero_stage,
|
|
gradient_accumulation_steps=gradient_accumulation_steps,
|
|
hidden_dim=hidden_dim,
|
|
nlayers=2)
|
|
|
|
# Create data
|
|
data_loader = random_dataloader(model=model_engine, total_samples=16, hidden_dim=hidden_dim, device=device)
|
|
|
|
# Run gradient accumulation steps
|
|
for i, batch in enumerate(data_loader):
|
|
# DDP: manual gradient accumulation
|
|
loss_ddp = model_ddp(batch[0], batch[1])
|
|
# Scale by GAS for DDP to match DeepSpeed behavior
|
|
(loss_ddp / gradient_accumulation_steps).backward()
|
|
|
|
# DeepSpeed: use scale() with built-in gradient accumulation
|
|
# Note: scale() only applies loss scaler, NOT GAS. DeepSpeed handles GAS internally
|
|
# via engine.step(), so we do NOT manually divide by GAS here.
|
|
loss_ds = model_engine(batch[0], batch[1])
|
|
scaled_loss = model_engine.scale(loss_ds)
|
|
scaled_loss.backward()
|
|
|
|
# Compare gradients at accumulation boundary
|
|
if model_engine.is_gradient_accumulation_boundary():
|
|
grads_ddp = collect_ddp_gradients(model_ddp)
|
|
grads_ds = collect_gradients_safe(model_engine)
|
|
compare_gradients(grads_ddp, grads_ds, f"step {i}")
|
|
|
|
# Step both optimizers
|
|
optimizer_ddp.step()
|
|
optimizer_ddp.zero_grad()
|
|
|
|
# Step DeepSpeed (handles gradient accumulation internally)
|
|
model_engine.step()
|
|
|
|
model_engine.destroy()
|
|
|
|
def test_needs_scaler_with_fp16(self, zero_stage):
|
|
"""Test that needs_scaler() correctly identifies when scaling is required with fp16"""
|
|
if not get_accelerator().is_fp16_supported():
|
|
pytest.skip("Test requires fp16 support for gradient scaling")
|
|
|
|
hidden_dim = 4
|
|
|
|
# Initialize distributed first
|
|
device, _, _ = initialize_distributed()
|
|
|
|
# Create engine with fp16 explicitly to test gradient scaling requirement
|
|
torch.manual_seed(42)
|
|
model = SimpleModel(hidden_dim=hidden_dim, nlayers=2)
|
|
|
|
config = {
|
|
"train_micro_batch_size_per_gpu": 2,
|
|
"gradient_accumulation_steps": 1,
|
|
"steps_per_print": 1,
|
|
"zero_optimization": {
|
|
"stage": zero_stage,
|
|
},
|
|
"optimizer": {
|
|
"type": "Adam",
|
|
"params": {
|
|
"lr": 1e-3
|
|
}
|
|
},
|
|
# Explicitly enable fp16 to test gradient scaling requirement
|
|
"fp16": {
|
|
"enabled": True,
|
|
"initial_scale_power": 8
|
|
}
|
|
}
|
|
|
|
if zero_stage == 3:
|
|
config["zero_optimization"]["stage3_param_persistence_threshold"] = 0
|
|
|
|
model_engine, _, _, _ = deepspeed.initialize(config=config, model=model, model_parameters=model.parameters())
|
|
|
|
# Verify that the optimizer correctly reports it needs scaling with fp16
|
|
from deepspeed.runtime.base_optimizer import ZeROOptimizer
|
|
assert isinstance(model_engine.optimizer, ZeROOptimizer), "Optimizer should be ZeROOptimizer"
|
|
assert model_engine.optimizer.needs_scaler(), "fp16 configuration should require gradient scaling"
|
|
|
|
# Verify scale() method works correctly
|
|
data_loader = random_dataloader(model=model_engine,
|
|
total_samples=8,
|
|
hidden_dim=hidden_dim,
|
|
device=device,
|
|
dtype=torch.float16)
|
|
batch = next(iter(data_loader))
|
|
loss = model_engine(batch[0], batch[1])
|
|
|
|
# Should be able to use scale() method and get a valid scaled tensor
|
|
scaled_loss = model_engine.scale(loss)
|
|
assert scaled_loss is not None, "scale() should return a scaled loss tensor"
|
|
assert scaled_loss.requires_grad, "scaled loss should require grad"
|
|
|
|
model_engine.destroy()
|
|
|
|
def test_needs_scaler_with_bf16(self, zero_stage):
|
|
"""Test that needs_scaler() correctly identifies that bf16 does NOT require scaling"""
|
|
if not get_accelerator().is_bf16_supported():
|
|
pytest.skip("Test requires bf16 support")
|
|
|
|
hidden_dim = 4
|
|
|
|
# Initialize distributed first
|
|
device, _, _ = initialize_distributed()
|
|
|
|
# Create engine with bf16 to verify scaling is NOT required
|
|
torch.manual_seed(42)
|
|
model = SimpleModel(hidden_dim=hidden_dim, nlayers=2)
|
|
|
|
config = {
|
|
"train_micro_batch_size_per_gpu": 2,
|
|
"gradient_accumulation_steps": 1,
|
|
"steps_per_print": 1,
|
|
"zero_optimization": {
|
|
"stage": zero_stage,
|
|
},
|
|
"optimizer": {
|
|
"type": "Adam",
|
|
"params": {
|
|
"lr": 1e-3
|
|
}
|
|
},
|
|
# Use bf16 which does NOT require gradient scaling
|
|
"bf16": {
|
|
"enabled": True
|
|
}
|
|
}
|
|
|
|
if zero_stage == 3:
|
|
config["zero_optimization"]["stage3_param_persistence_threshold"] = 0
|
|
|
|
model_engine, _, _, _ = deepspeed.initialize(config=config, model=model, model_parameters=model.parameters())
|
|
|
|
# Verify that the optimizer correctly reports it does NOT need scaling with bf16
|
|
from deepspeed.runtime.base_optimizer import ZeROOptimizer
|
|
assert isinstance(model_engine.optimizer, ZeROOptimizer), "Optimizer should be ZeROOptimizer"
|
|
assert not model_engine.optimizer.needs_scaler(), "bf16 configuration should NOT require gradient scaling"
|
|
|
|
# Verify that loss.backward() can be called directly without scale() for bf16
|
|
data_loader = random_dataloader(model=model_engine,
|
|
total_samples=8,
|
|
hidden_dim=hidden_dim,
|
|
device=device,
|
|
dtype=torch.bfloat16)
|
|
batch = next(iter(data_loader))
|
|
loss = model_engine(batch[0], batch[1])
|
|
|
|
# With bf16, should be able to call backward directly (no scaling required)
|
|
loss.backward()
|
|
|
|
# Collect gradients to verify backward completed successfully
|
|
grads = collect_gradients_safe(model_engine)
|
|
assert len(grads) > 0, "Expected gradients to be computed"
|
|
|
|
model_engine.destroy()
|
|
|
|
def test_error_when_backward_without_scale_fp16(self, zero_stage):
|
|
"""Test that calling backward() without scale() raises an error with fp16"""
|
|
if not get_accelerator().is_fp16_supported():
|
|
pytest.skip("Test requires fp16 support for gradient scaling")
|
|
|
|
hidden_dim = 4
|
|
|
|
# Initialize distributed first
|
|
device, _, _ = initialize_distributed()
|
|
|
|
# Create engine with fp16
|
|
torch.manual_seed(42)
|
|
model = SimpleModel(hidden_dim=hidden_dim, nlayers=2)
|
|
|
|
config = {
|
|
"train_micro_batch_size_per_gpu": 2,
|
|
"gradient_accumulation_steps": 1,
|
|
"steps_per_print": 1,
|
|
"zero_optimization": {
|
|
"stage": zero_stage,
|
|
},
|
|
"optimizer": {
|
|
"type": "Adam",
|
|
"params": {
|
|
"lr": 1e-3
|
|
}
|
|
},
|
|
"fp16": {
|
|
"enabled": True,
|
|
"initial_scale_power": 8
|
|
}
|
|
}
|
|
|
|
if zero_stage == 3:
|
|
config["zero_optimization"]["stage3_param_persistence_threshold"] = 0
|
|
|
|
model_engine, _, _, _ = deepspeed.initialize(config=config, model=model, model_parameters=model.parameters())
|
|
|
|
# Verify needs_scaler is True
|
|
assert model_engine.optimizer.needs_scaler(), "fp16 should require scaling"
|
|
|
|
# Create data
|
|
data_loader = random_dataloader(model=model_engine,
|
|
total_samples=8,
|
|
hidden_dim=hidden_dim,
|
|
device=device,
|
|
dtype=torch.float16)
|
|
batch = next(iter(data_loader))
|
|
|
|
loss = model_engine(batch[0], batch[1])
|
|
|
|
# Try to call backward without scale - should raise RuntimeError
|
|
error_raised = False
|
|
try:
|
|
loss.backward()
|
|
except RuntimeError as e:
|
|
if "Loss scaling is required" in str(e):
|
|
error_raised = True
|
|
else:
|
|
raise # Re-raise if it's a different error
|
|
|
|
# If the test completes (doesn't hang), verify error was raised
|
|
if error_raised:
|
|
# Success - error was properly detected
|
|
pass
|
|
else:
|
|
# If no error was raised, this is a problem (or it hung and timed out)
|
|
pytest.fail("Expected RuntimeError about loss scaling, but backward completed without error")
|
|
|
|
model_engine.destroy()
|
|
|
|
def test_scale_validates_scalar_loss(self, zero_stage):
|
|
"""Test that scale() validates the input is a scalar loss tensor"""
|
|
hidden_dim = 4
|
|
|
|
model_engine = create_deepspeed_engine(model_class=SimpleNonScalarModel,
|
|
zero_stage=zero_stage,
|
|
seed=42,
|
|
hidden_dim=hidden_dim)
|
|
|
|
device = get_accelerator().current_device_name()
|
|
dtype = preferred_dtype()
|
|
torch.manual_seed(123)
|
|
x = torch.randn(2, hidden_dim, device=device, dtype=dtype)
|
|
|
|
# Forward to get non-scalar output
|
|
output = model_engine(x)
|
|
|
|
# Trying to scale a non-scalar tensor should raise an assertion error
|
|
with pytest.raises(AssertionError, match="scalar tensor"):
|
|
model_engine.scale(output)
|
|
|
|
model_engine.destroy()
|
|
|
|
def test_scale_with_torch_autocast(self, zero_stage):
|
|
"""Test that scale() works correctly with torch.autocast and fp16"""
|
|
if not get_accelerator().is_fp16_supported():
|
|
pytest.skip("FP16 not supported on this accelerator")
|
|
|
|
hidden_dim = 4
|
|
|
|
# Initialize distributed first
|
|
device, _, _ = initialize_distributed()
|
|
|
|
# Create engine with fp16 config to test gradient scaling
|
|
torch.manual_seed(42)
|
|
model = SimpleModel(hidden_dim=hidden_dim, nlayers=2)
|
|
|
|
config = {
|
|
"train_micro_batch_size_per_gpu": 2,
|
|
"gradient_accumulation_steps": 1,
|
|
"steps_per_print": 1,
|
|
"zero_optimization": {
|
|
"stage": zero_stage,
|
|
},
|
|
"optimizer": {
|
|
"type": "Adam",
|
|
"params": {
|
|
"lr": 1e-3
|
|
}
|
|
},
|
|
# Enable fp16 to test gradient scaling (bf16 doesn't use gradient scaling)
|
|
"fp16": {
|
|
"enabled": True,
|
|
"initial_scale_power": 8
|
|
}
|
|
}
|
|
|
|
if zero_stage == 3:
|
|
config["zero_optimization"]["stage3_param_persistence_threshold"] = 0
|
|
|
|
model_engine, _, _, _ = deepspeed.initialize(config=config, model=model, model_parameters=model.parameters())
|
|
|
|
# Create data with fp16 dtype to match the config
|
|
data_loader = random_dataloader(model=model_engine,
|
|
total_samples=8,
|
|
hidden_dim=hidden_dim,
|
|
device=device,
|
|
dtype=torch.float16)
|
|
batch = next(iter(data_loader))
|
|
|
|
# Forward and use scale()
|
|
loss = model_engine(batch[0], batch[1])
|
|
scaled_loss = model_engine.scale(loss)
|
|
|
|
# Should be able to call backward
|
|
scaled_loss.backward()
|
|
|
|
# Collect gradients to verify they exist
|
|
grads = collect_gradients_safe(model_engine)
|
|
assert len(grads) > 0, "Expected gradients to be computed"
|
|
|
|
model_engine.destroy()
|
|
|
|
|
|
class NonCheckpointedModel(torch.nn.Module):
|
|
"""Model without gradient checkpointing, used as reference for comparison."""
|
|
|
|
def __init__(self, hidden_dim):
|
|
super().__init__()
|
|
self.linear1 = torch.nn.Linear(hidden_dim, hidden_dim)
|
|
self.linear2 = torch.nn.Linear(hidden_dim, hidden_dim)
|
|
|
|
def forward(self, x):
|
|
x = self.linear1(x)
|
|
x = torch.nn.functional.relu(x)
|
|
x = self.linear2(x)
|
|
return x
|
|
|
|
|
|
class CheckpointedModel(torch.nn.Module):
|
|
"""Model that uses gradient checkpointing with configurable use_reentrant setting.
|
|
|
|
This model is designed to test the interaction between ZeRO-3 and gradient
|
|
checkpointing with both reentrant (use_reentrant=True) and non-reentrant
|
|
(use_reentrant=False) modes.
|
|
|
|
Uses 2 layers to minimize numerical divergence from bfloat16 precision
|
|
accumulation over multiple optimizer steps.
|
|
"""
|
|
|
|
def __init__(self, hidden_dim, use_reentrant=True):
|
|
super().__init__()
|
|
self.use_reentrant = use_reentrant
|
|
self.linear1 = torch.nn.Linear(hidden_dim, hidden_dim)
|
|
self.linear2 = torch.nn.Linear(hidden_dim, hidden_dim)
|
|
|
|
def _checkpointed_block(self, x):
|
|
"""Block that will be checkpointed"""
|
|
x = self.linear1(x)
|
|
x = torch.nn.functional.relu(x)
|
|
return x
|
|
|
|
def forward(self, x):
|
|
# Use gradient checkpointing on the first block
|
|
if self.training:
|
|
from torch.utils.checkpoint import checkpoint
|
|
x = checkpoint(self._checkpointed_block, x, use_reentrant=self.use_reentrant)
|
|
else:
|
|
x = self._checkpointed_block(x)
|
|
x = self.linear2(x)
|
|
return x
|
|
|
|
|
|
@pytest.mark.parametrize("zero_stage", [1, 2, 3])
|
|
@pytest.mark.parametrize("use_reentrant", [True, False])
|
|
class TestZeroUserBackwardWithCheckpointing(DistributedTest):
|
|
"""Test ZeRO with gradient checkpointing and non-scalar backward.
|
|
|
|
This test class validates the interaction between:
|
|
1. ZeRO parameter partitioning (stages 1 and 3)
|
|
2. Gradient checkpointing (both reentrant and non-reentrant modes)
|
|
3. Non-scalar backward (tensor.backward(gradient=...))
|
|
|
|
Both use_reentrant=True and use_reentrant=False are supported with ZeRO.
|
|
Note: When using use_reentrant=True, input tensors should have requires_grad=True
|
|
for proper gradient computation through the checkpointed region.
|
|
"""
|
|
world_size = 2
|
|
|
|
def test_checkpointed_non_scalar_backward(self, zero_stage, use_reentrant):
|
|
"""Test that gradient checkpointing works with ZeRO and non-scalar backward.
|
|
|
|
Verifies that tensor.backward(gradient=...) works correctly with ZeRO
|
|
and gradient checkpointing in both reentrant and non-reentrant modes.
|
|
"""
|
|
hidden_dim = 8
|
|
batch_size = 2
|
|
|
|
# Initialize distributed environment
|
|
device, rank, dtype = initialize_distributed()
|
|
|
|
# Create DDP model for reference (no checkpointing issues with DDP)
|
|
torch.manual_seed(42)
|
|
model_ddp = CheckpointedModel(hidden_dim=hidden_dim, use_reentrant=use_reentrant)
|
|
model_ddp = model_ddp.to(device=device, dtype=dtype)
|
|
model_ddp = DDP(model_ddp, device_ids=[rank], output_device=rank)
|
|
optimizer_ddp = torch.optim.Adam(model_ddp.parameters(), lr=1e-3)
|
|
|
|
# Create DeepSpeed model with ZeRO-3
|
|
torch.manual_seed(42)
|
|
model_ds = CheckpointedModel(hidden_dim=hidden_dim, use_reentrant=use_reentrant)
|
|
|
|
config = get_config_dict(zero_stage)
|
|
model_engine, _, _, _ = deepspeed.initialize(config=config,
|
|
model=model_ds,
|
|
model_parameters=model_ds.parameters())
|
|
|
|
# Create input data - use separate tensors for DDP and DeepSpeed to avoid
|
|
# memory sharing issues during parallel test execution
|
|
torch.manual_seed(123)
|
|
x_ddp = torch.randn(batch_size, hidden_dim, device=device, dtype=dtype, requires_grad=True)
|
|
|
|
# DDP: forward and non-scalar backward
|
|
optimizer_ddp.zero_grad()
|
|
output_ddp = model_ddp(x_ddp)
|
|
grad_output = torch.ones_like(output_ddp)
|
|
output_ddp.backward(grad_output)
|
|
get_accelerator().synchronize() # Ensure CUDA ops complete
|
|
dist.barrier() # Ensure all ranks complete gradient sync
|
|
ddp_grads = collect_ddp_gradients(model_ddp)
|
|
|
|
# DeepSpeed with ZeRO-3: forward and non-scalar backward
|
|
# This is the pattern used in disaggregated training
|
|
# Create fresh tensor with same seed for reproducibility
|
|
torch.manual_seed(123)
|
|
x_ds = torch.randn(batch_size, hidden_dim, device=device, dtype=dtype, requires_grad=True)
|
|
output_ds = model_engine(x_ds)
|
|
grad_output_ds = torch.ones_like(output_ds)
|
|
|
|
# Non-scalar backward with gradient checkpointing
|
|
output_ds.backward(grad_output_ds)
|
|
|
|
# Synchronize device before collecting gradients. ZeRO-3 uses async operations
|
|
# on separate streams for gradient reduction. With use_reentrant=True checkpointing,
|
|
# we need to ensure all operations complete before reading gradient data.
|
|
get_accelerator().synchronize()
|
|
dist.barrier() # Ensure all ranks complete backward before collecting gradients
|
|
|
|
# Collect and verify gradients
|
|
ds_grads = collect_gradients_safe(model_engine)
|
|
|
|
# Verify gradients were computed
|
|
assert len(ds_grads) > 0, \
|
|
f"No gradients computed with use_reentrant={use_reentrant} and ZeRO-3"
|
|
|
|
# Compare gradients with DDP reference
|
|
compare_gradients(ddp_grads, ds_grads, f"with checkpointing use_reentrant={use_reentrant}")
|
|
|
|
# Run optimizer step to verify full training loop works
|
|
model_engine.step()
|
|
|
|
model_engine.destroy()
|
|
|
|
def test_checkpointed_scalar_backward(self, zero_stage, use_reentrant):
|
|
"""Test that gradient checkpointing works with ZeRO and scalar backward.
|
|
|
|
Verifies that scalar loss.backward() works correctly with ZeRO and
|
|
gradient checkpointing in both reentrant and non-reentrant modes.
|
|
"""
|
|
hidden_dim = 8
|
|
batch_size = 2
|
|
|
|
# Initialize distributed environment
|
|
device, rank, dtype = initialize_distributed()
|
|
|
|
# Create DDP model for reference
|
|
torch.manual_seed(42)
|
|
model_ddp = CheckpointedModel(hidden_dim=hidden_dim, use_reentrant=use_reentrant)
|
|
model_ddp = model_ddp.to(device=device, dtype=dtype)
|
|
model_ddp = DDP(model_ddp, device_ids=[rank], output_device=rank)
|
|
optimizer_ddp = torch.optim.Adam(model_ddp.parameters(), lr=1e-3)
|
|
|
|
# Create DeepSpeed model with ZeRO-3
|
|
torch.manual_seed(42)
|
|
model_ds = CheckpointedModel(hidden_dim=hidden_dim, use_reentrant=use_reentrant)
|
|
|
|
config = get_config_dict(zero_stage)
|
|
model_engine, _, _, _ = deepspeed.initialize(config=config,
|
|
model=model_ds,
|
|
model_parameters=model_ds.parameters())
|
|
|
|
# Create input data - use separate tensors for DDP and DeepSpeed to avoid
|
|
# memory sharing issues during parallel test execution
|
|
torch.manual_seed(123)
|
|
x_ddp = torch.randn(batch_size, hidden_dim, device=device, dtype=dtype, requires_grad=True)
|
|
y = torch.randint(0, hidden_dim, (batch_size, ), device=device)
|
|
|
|
# DDP: forward with scalar loss and backward
|
|
optimizer_ddp.zero_grad()
|
|
output_ddp = model_ddp(x_ddp)
|
|
loss_ddp = torch.nn.functional.cross_entropy(output_ddp, y)
|
|
loss_ddp.backward()
|
|
get_accelerator().synchronize() # Ensure CUDA ops complete
|
|
dist.barrier() # Ensure all ranks complete gradient sync
|
|
ddp_grads = collect_ddp_gradients(model_ddp)
|
|
|
|
# DeepSpeed with ZeRO-3: forward with scalar loss and backward
|
|
# Create fresh tensor with same seed for reproducibility
|
|
torch.manual_seed(123)
|
|
x_ds = torch.randn(batch_size, hidden_dim, device=device, dtype=dtype, requires_grad=True)
|
|
output_ds = model_engine(x_ds)
|
|
loss_ds = torch.nn.functional.cross_entropy(output_ds, y)
|
|
|
|
loss_ds.backward()
|
|
|
|
# Synchronize device before collecting gradients. ZeRO-3 uses async operations
|
|
# on separate streams for gradient reduction. With use_reentrant=True checkpointing,
|
|
# we need to ensure all operations complete before reading gradient data.
|
|
get_accelerator().synchronize()
|
|
dist.barrier() # Ensure all ranks complete backward before collecting gradients
|
|
|
|
# Collect and verify gradients
|
|
ds_grads = collect_gradients_safe(model_engine)
|
|
|
|
# Verify gradients were computed
|
|
assert len(ds_grads) > 0, \
|
|
f"No gradients computed with scalar loss, use_reentrant={use_reentrant}"
|
|
|
|
# Compare gradients with DDP reference
|
|
compare_gradients(ddp_grads, ds_grads, f"scalar loss with checkpointing use_reentrant={use_reentrant}")
|
|
|
|
model_engine.destroy()
|
|
|
|
def test_checkpointed_multiple_backward(self, zero_stage, use_reentrant):
|
|
"""Test multiple backward passes with checkpointing and ZeRO.
|
|
|
|
Verifies that consecutive training iterations work correctly with
|
|
gradient checkpointing. Compares gradients with DDP at all iterations
|
|
to verify correctness. Uses PyTorch Adam for both to ensure fair comparison.
|
|
"""
|
|
hidden_dim = 8
|
|
batch_size = 2
|
|
num_iterations = 3
|
|
|
|
# Initialize distributed environment
|
|
device, rank, dtype = initialize_distributed()
|
|
|
|
# Create DDP model for reference with PyTorch Adam
|
|
torch.manual_seed(42)
|
|
model_ddp = CheckpointedModel(hidden_dim=hidden_dim, use_reentrant=use_reentrant)
|
|
model_ddp = model_ddp.to(device=device, dtype=dtype)
|
|
model_ddp = DDP(model_ddp, device_ids=[rank], output_device=rank)
|
|
optimizer_ddp = torch.optim.Adam(model_ddp.parameters(), lr=1e-3)
|
|
|
|
# Create DeepSpeed model WITH checkpointing, using PyTorch Adam
|
|
torch.manual_seed(42)
|
|
model_ds = CheckpointedModel(hidden_dim=hidden_dim, use_reentrant=use_reentrant)
|
|
optimizer_ds = torch.optim.Adam(model_ds.parameters(), lr=1e-3)
|
|
config = get_config_dict(zero_stage)
|
|
model_engine, _, _, _ = deepspeed.initialize(config=config,
|
|
model=model_ds,
|
|
model_parameters=model_ds.parameters(),
|
|
optimizer=optimizer_ds)
|
|
|
|
for iteration in range(num_iterations):
|
|
# Use same random seed for both models
|
|
torch.manual_seed(123 + iteration)
|
|
x = torch.randn(batch_size, hidden_dim, device=device, dtype=dtype, requires_grad=True)
|
|
|
|
# DDP: forward and backward
|
|
optimizer_ddp.zero_grad()
|
|
x_ddp = x.clone().detach().requires_grad_(True)
|
|
output_ddp = model_ddp(x_ddp)
|
|
output_ddp.backward(torch.ones_like(output_ddp))
|
|
get_accelerator().synchronize()
|
|
dist.barrier()
|
|
ddp_grads = collect_ddp_gradients(model_ddp)
|
|
|
|
# DeepSpeed: forward and backward
|
|
x_ds = x.clone().detach().requires_grad_(True)
|
|
output_ds = model_engine(x_ds)
|
|
output_ds.backward(torch.ones_like(output_ds))
|
|
get_accelerator().synchronize()
|
|
dist.barrier()
|
|
ds_grads = collect_gradients_safe(model_engine)
|
|
|
|
# Verify gradients were computed
|
|
assert len(ds_grads) > 0, \
|
|
f"No gradients at iteration {iteration} with use_reentrant={use_reentrant}"
|
|
|
|
# Compare gradients with DDP - using same optimizer so should match closely
|
|
# Small differences at later iterations are expected due to bfloat16 precision
|
|
compare_gradients(ddp_grads, ds_grads, f"iteration {iteration} with use_reentrant={use_reentrant}")
|
|
|
|
# Run optimizer steps on both models
|
|
optimizer_ddp.step()
|
|
model_engine.step()
|
|
|
|
model_engine.destroy()
|