148 lines
5.6 KiB
Python
148 lines
5.6 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# DeepSpeed Team
|
|
"""
|
|
Regression tests for ZeRO-1/2 + cpu_offload with multiple engine.backward()
|
|
calls per optimizer step (ga_steps=1, driven via set_gradient_accumulation_boundary).
|
|
"""
|
|
|
|
import pytest
|
|
import torch
|
|
import deepspeed
|
|
|
|
from unit.common import DistributedTest
|
|
from unit.simple_model import SimpleModel, random_dataloader
|
|
from deepspeed.accelerator import get_accelerator
|
|
|
|
|
|
def _base_config(zero_stage, gradient_accumulation_steps=1, cpu_offload=False):
|
|
config_dict = {
|
|
"train_batch_size": gradient_accumulation_steps,
|
|
"gradient_accumulation_steps": gradient_accumulation_steps,
|
|
"train_micro_batch_size_per_gpu": 1,
|
|
"steps_per_print": 1,
|
|
"zero_optimization": {
|
|
"stage": zero_stage,
|
|
},
|
|
"zero_force_ds_cpu_optimizer": False,
|
|
"optimizer": {
|
|
"type": "Adam",
|
|
"params": {
|
|
"lr": 1e-3,
|
|
},
|
|
},
|
|
}
|
|
if cpu_offload:
|
|
config_dict["zero_optimization"]["offload_optimizer"] = {"device": "cpu"}
|
|
if get_accelerator().is_bf16_supported():
|
|
config_dict["bf16"] = {"enabled": True}
|
|
elif get_accelerator().is_fp16_supported():
|
|
config_dict["fp16"] = {"enabled": True}
|
|
return config_dict
|
|
|
|
|
|
def _init_engine(config_dict, hidden_dim, seed=42):
|
|
torch.manual_seed(seed)
|
|
model = SimpleModel(hidden_dim, nlayers=2)
|
|
engine, _, _, _ = deepspeed.initialize(
|
|
model=model,
|
|
model_parameters=model.parameters(),
|
|
config=config_dict,
|
|
)
|
|
return engine
|
|
|
|
|
|
def _capture_params(engine):
|
|
return {name: p.detach().float().cpu().clone() for name, p in engine.module.named_parameters()}
|
|
|
|
|
|
def _assert_params_match(ref, test, label, tol=5e-5):
|
|
for name in ref:
|
|
max_diff = (ref[name] - test[name]).abs().max().item()
|
|
assert max_diff < tol, f"{label}: {name} differs by {max_diff:.3e}"
|
|
|
|
|
|
def _run_multi_backward(config_dict, hidden_dim, num_chunks, num_steps=1, seed=42):
|
|
engine = _init_engine(config_dict, hidden_dim, seed=seed)
|
|
data_loader = random_dataloader(
|
|
model=engine,
|
|
total_samples=num_chunks * num_steps,
|
|
hidden_dim=hidden_dim,
|
|
device=engine.device,
|
|
)
|
|
batches = list(data_loader)
|
|
for step_idx in range(num_steps):
|
|
step_batches = batches[step_idx * num_chunks:(step_idx + 1) * num_chunks]
|
|
for i, batch in enumerate(step_batches):
|
|
loss = engine(batch[0], batch[1])
|
|
engine.set_gradient_accumulation_boundary(i == num_chunks - 1)
|
|
engine.backward(loss)
|
|
engine.step()
|
|
params = _capture_params(engine)
|
|
engine.destroy()
|
|
return params
|
|
|
|
|
|
def _run_ga_microsteps(config_dict, hidden_dim, total_microsteps, seed=42):
|
|
engine = _init_engine(config_dict, hidden_dim, seed=seed)
|
|
data_loader = random_dataloader(
|
|
model=engine,
|
|
total_samples=total_microsteps,
|
|
hidden_dim=hidden_dim,
|
|
device=engine.device,
|
|
)
|
|
for batch in data_loader:
|
|
loss = engine(batch[0], batch[1])
|
|
engine.backward(loss)
|
|
engine.step()
|
|
params = _capture_params(engine)
|
|
engine.destroy()
|
|
return params
|
|
|
|
|
|
@pytest.mark.parametrize("zero_stage", [1, 2])
|
|
class TestZeroOffloadMultiBackward(DistributedTest):
|
|
world_size = 1
|
|
|
|
def test_multi_backward_matches_no_offload(self, zero_stage):
|
|
hidden_dim = 8
|
|
num_chunks = 4
|
|
ref = _run_multi_backward(_base_config(zero_stage, cpu_offload=False), hidden_dim, num_chunks)
|
|
test = _run_multi_backward(_base_config(zero_stage, cpu_offload=True), hidden_dim, num_chunks)
|
|
_assert_params_match(ref, test, label=f"ZeRO-{zero_stage} N=4")
|
|
|
|
def test_single_backward_unchanged(self, zero_stage):
|
|
hidden_dim = 8
|
|
ref = _run_multi_backward(_base_config(zero_stage, cpu_offload=False), hidden_dim, num_chunks=1)
|
|
test = _run_multi_backward(_base_config(zero_stage, cpu_offload=True), hidden_dim, num_chunks=1)
|
|
_assert_params_match(ref, test, label=f"ZeRO-{zero_stage} N=1")
|
|
|
|
def test_multi_backward_across_multiple_steps(self, zero_stage):
|
|
hidden_dim = 8
|
|
ref = _run_multi_backward(_base_config(zero_stage, cpu_offload=False), hidden_dim, num_chunks=3, num_steps=3)
|
|
test = _run_multi_backward(_base_config(zero_stage, cpu_offload=True), hidden_dim, num_chunks=3, num_steps=3)
|
|
_assert_params_match(ref, test, label=f"ZeRO-{zero_stage} 3x3")
|
|
|
|
def test_single_backward_allocates_no_cpu_accumulator(self, zero_stage):
|
|
hidden_dim = 8
|
|
engine = _init_engine(_base_config(zero_stage, cpu_offload=True), hidden_dim)
|
|
batch = next(
|
|
iter(random_dataloader(model=engine, total_samples=1, hidden_dim=hidden_dim, device=engine.device)))
|
|
loss = engine(batch[0], batch[1])
|
|
engine.set_gradient_accumulation_boundary(True)
|
|
engine.backward(loss)
|
|
engine.step()
|
|
populated = len(engine.optimizer.accumulated_grads_in_cpu)
|
|
engine.destroy()
|
|
assert populated == 0, f"ZeRO-{zero_stage}: ga=1+N=1 populated accumulated_grads_in_cpu ({populated} entries)"
|
|
|
|
def test_ga_greater_than_one_offload_unchanged(self, zero_stage):
|
|
hidden_dim = 8
|
|
ga = 4
|
|
ref = _run_ga_microsteps(_base_config(zero_stage, gradient_accumulation_steps=ga, cpu_offload=False),
|
|
hidden_dim,
|
|
total_microsteps=ga)
|
|
test = _run_ga_microsteps(_base_config(zero_stage, gradient_accumulation_steps=ga, cpu_offload=True),
|
|
hidden_dim,
|
|
total_microsteps=ga)
|
|
_assert_params_match(ref, test, label=f"ZeRO-{zero_stage} ga=4")
|