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

466 lines
20 KiB
Python

# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
import torch
import deepspeed
import pytest
import gc
import random
from unit.common import DistributedTest
from unit.simple_model import SimplePRMoEModel, SimpleMoEModel, sequence_dataloader
import deepspeed.comm as dist
import deepspeed.moe.sharded_moe as sharded_moe
from deepspeed import get_accelerator
from deepspeed.moe.layer import MoE
from deepspeed.moe.sharded_moe import top1gating, top2gating, topkgating
from deepspeed.moe.utils import split_params_into_different_moe_groups_for_optimizer, is_moe_param
from deepspeed.utils.torch import required_torch_version
@pytest.mark.parametrize("zero_stage", [0, 1, 2])
class TestSimpleMoE(DistributedTest):
world_size = 2
def test(self, zero_stage):
if not required_torch_version(min_version=1.8):
pytest.skip("DeepSpeed MoE tests need torch 1.8 or higher to run correctly")
config_dict = {
"train_micro_batch_size_per_gpu": 1,
"steps_per_print": 1,
"optimizer": {
"type": "Adam",
"params": {
"lr": 0.00015
}
},
"fp16": {
"enabled": True
},
"zero_optimization": {
"stage": zero_stage
}
}
# should automatically create moe param groups in deepspeed backend
hidden_dim = 16
model = SimpleMoEModel(hidden_dim=hidden_dim, ep_size=1)
model, optimizer, _, _ = deepspeed.initialize(config=config_dict, model=model)
data_loader = sequence_dataloader(model=model,
total_samples=50,
hidden_dim=hidden_dim,
device=model.device,
dtype=torch.float16)
for n, batch in enumerate(data_loader):
loss = model(batch[0], batch[1])
model.backward(loss)
model.step()
@pytest.mark.parametrize("ep_size", [2, 4])
@pytest.mark.parametrize("zero_stage", [0, 1, 2])
@pytest.mark.parametrize("use_residual", [True, False])
class TestMoE(DistributedTest):
world_size = 4
def test(self, ep_size, zero_stage, use_residual):
if not required_torch_version(min_version=1.8):
pytest.skip("DeepSpeed MoE tests need torch 1.8 or higher to run correctly")
config_dict = {
"train_micro_batch_size_per_gpu": 1,
"steps_per_print": 1,
"fp16": {
"enabled": True
},
"zero_optimization": {
"stage": zero_stage
}
}
hidden_dim = 16
# E+D -- ep_size = 2
# E only -- ep_size = 4
model = SimpleMoEModel(hidden_dim, ep_size=ep_size, use_residual=use_residual)
param_group = {'params': [p for p in model.parameters()], 'name': 'random-unique-name'}
params = split_params_into_different_moe_groups_for_optimizer(param_group)
optimizer = torch.optim.AdamW(params=params)
model, optimizer, _, _ = deepspeed.initialize(config=config_dict,
model=model,
optimizer=optimizer,
dist_init_required=False)
#dist_init_required=False -- parameterize to True/False?
data_loader = sequence_dataloader(model=model,
total_samples=50,
hidden_dim=hidden_dim,
device=model.device,
dtype=torch.float16)
def strict_average_tensor(tensor, communication_data_type: torch.dtype):
process_group = optimizer.dp_process_group
curr_size = 0
pg_offsets = []
ipg_bucket = optimizer.ipg_buckets[communication_data_type]
for i, param_idx, param_id in ipg_bucket.params:
param = optimizer.bit16_groups[i][param_idx]
process_group = optimizer.dp_process_group
if ipg_bucket.has_moe_params:
process_group = optimizer.expert_dp_process_group[param.group_name] if is_moe_param(
param) else optimizer.dp_process_group
partition_ids = optimizer.param_to_partition_ids[i][param_id]
# Get all partition ids + their offsets
partition_offsets = []
for partition_id in partition_ids:
offset = optimizer.grad_start_offset[i][partition_id][param_id]
partition_offsets.append(offset)
partition_offsets.sort()
# Calculate rank and offsets for grad slices
for idx, offset in enumerate(partition_offsets):
# Calculate numel for grad slice depending on partition location
if idx == len(partition_offsets) - 1:
# Last partition_id uses its own offset
numel = param.numel() - offset
else:
# Set numel to next partition's offset
numel = partition_offsets[idx + 1] - offset
pg_offsets.append((curr_size, process_group))
curr_size += numel
def strict_narrow(dim, start, length):
lo, hi = 0, len(pg_offsets) - 1
while lo < hi:
mi = lo + (hi - lo) // 2
if pg_offsets[mi][0] >= start:
hi = mi
else:
lo = mi + 1
curr_slice, reduce_process_group = lo, pg_offsets[lo][1]
while curr_slice < len(pg_offsets) and start + length > pg_offsets[curr_slice][0]:
assert reduce_process_group == pg_offsets[curr_slice][
1], "reduce process_group does not match the parameter's process_group"
curr_slice += 1
return orig_narrow(dim, start, length) # real call
orig_narrow, tensor.narrow = tensor.narrow, strict_narrow
type(optimizer).average_tensor(optimizer, tensor, communication_data_type) # real call
tensor.narrow = orig_narrow
if "average_tensor" in dir(optimizer):
optimizer.average_tensor = strict_average_tensor
for n, batch in enumerate(data_loader):
loss = model(batch[0], batch[1])
model.backward(loss)
model.step()
gc.collect() # Must do this or we get a memory leak in this test
@pytest.mark.parametrize("ep_size, use_residual", [(2, True), (2, False)])
class TestPRMoE(DistributedTest):
world_size = 4
def test(self, ep_size, use_residual):
if not required_torch_version(min_version=1.8):
pytest.skip("DeepSpeed MoE tests need torch 1.8 or higher to run correctly")
config_dict = {"train_batch_size": 8, "steps_per_print": 1, "fp16": {"enabled": True}}
hidden_dim = 16
# E+D -- ep_size = 2
# E only -- ep_size = 4
model = SimplePRMoEModel(hidden_dim, ep_size=ep_size, use_residual=use_residual)
optimizer = torch.optim.AdamW(params=model.parameters())
model, _, _, _ = deepspeed.initialize(config=config_dict,
model=model,
optimizer=optimizer,
dist_init_required=False)
data_loader = sequence_dataloader(model=model,
total_samples=50,
hidden_dim=hidden_dim,
device=model.device,
dtype=torch.float16)
for n, batch in enumerate(data_loader):
loss = model(batch[0], batch[1])
model.backward(loss)
model.step()
class TestTopk(DistributedTest):
world_size = 2
def test(self):
device = get_accelerator().current_device_name()
if dist.get_rank() == 0:
logits = torch.rand(2, 2, device=device)
elif dist.get_rank() == 1:
logits = torch.rand(10, 2, device=device)
output = top1gating(logits=logits,
capacity_factor=1,
min_capacity=0,
used_token=None,
noisy_gate_policy=None,
drop_tokens=False,
use_rts=True,
use_tutel=False)
class TestMoESingleton(DistributedTest):
world_size = 2
@pytest.mark.parametrize("ep_size, expected_calls", [(1, 0), (2, 2)], ids=["single", "multi"])
def test_all_to_all(self, monkeypatch, ep_size, expected_calls):
if not required_torch_version(min_version=1.8):
pytest.skip("DeepSpeed MoE tests need torch 1.8 or higher to run correctly")
config_dict = {"train_micro_batch_size_per_gpu": 1, "steps_per_print": 1}
hidden_dim = 8
expert = torch.nn.Sequential(torch.nn.Linear(hidden_dim, hidden_dim), torch.nn.Linear(hidden_dim, hidden_dim))
model = MoE(hidden_size=hidden_dim, expert=expert, num_experts=2, ep_size=ep_size, k=1, min_capacity=0)
optimizer = torch.optim.AdamW(params=model.parameters())
model, _, _, _ = deepspeed.initialize(config=config_dict,
model=model,
optimizer=optimizer,
dist_init_required=False)
all_to_all_calls = []
def counted_all_to_all(group, input):
all_to_all_calls.append((group, input.shape))
return input
monkeypatch.setattr(sharded_moe._AllToAll, "apply", counted_all_to_all)
x = torch.randn(1, 4, hidden_dim, device=model.device, requires_grad=True)
output, l_aux, _ = model(x)
assert len(all_to_all_calls) == expected_calls
loss = output.float().sum() + l_aux.float()
model.backward(loss)
assert len(all_to_all_calls) == expected_calls
assert x.grad is not None
assert any(param.grad is not None for param in model.module.parameters())
@pytest.mark.parametrize("gate_fn, capacity_args", [(top1gating, (1, 0)), (top2gating, (1, 0)),
(topkgating, (3, 1, 0))],
ids=["top1", "top2", "topk"])
@pytest.mark.parametrize("ep_world_size, expected_calls", [(1, 0), (2, 1)], ids=["single", "multi"])
def test_capacity(self, monkeypatch, gate_fn, capacity_args, ep_world_size, expected_calls):
if not required_torch_version(min_version=1.8):
pytest.skip("DeepSpeed MoE tests need torch 1.8 or higher to run correctly")
ep_group = None
if ep_world_size == 1:
for rank in range(dist.get_world_size()):
group = dist.new_group([rank])
if rank == dist.get_rank():
ep_group = group
else:
ep_group = dist.new_group(list(range(dist.get_world_size())))
all_reduce_calls = []
original_all_reduce = sharded_moe.dist.all_reduce
def counted_all_reduce(tensor, op=dist.ReduceOp.SUM, group=None):
all_reduce_calls.append((tensor, op, group))
return original_all_reduce(tensor, op=op, group=group)
monkeypatch.setattr(sharded_moe.dist, "all_reduce", counted_all_reduce)
device = get_accelerator().current_device_name()
logits = torch.randn(8, 4, device=device)
gate_fn(logits, *capacity_args, drop_tokens=False, ep_group=ep_group)
assert len(all_reduce_calls) == expected_calls
if all_reduce_calls:
_, op, group = all_reduce_calls[0]
assert op == dist.ReduceOp.MAX
assert group is ep_group
def test_no_ep_group(self, monkeypatch):
if not required_torch_version(min_version=1.8):
pytest.skip("DeepSpeed MoE tests need torch 1.8 or higher to run correctly")
def fail_collective(*args, **kwargs):
raise AssertionError("ep_group=None should not enter expert-parallel collective code")
monkeypatch.setattr(sharded_moe.dist, "get_world_size", fail_collective)
monkeypatch.setattr(sharded_moe.dist, "all_reduce", fail_collective)
device = get_accelerator().current_device_name()
logits = torch.randn(8, 4, device=device)
top2gating(logits, 1, 0, drop_tokens=False, ep_group=None, top2_2nd_expert_sampling=False)
class TestTopkGate(DistributedTest):
def test(self):
def check_equal(logits, cap, sparse_truth, res):
m, n = logits.shape
dispatch_mask_truth = torch.zeros(m, n, cap)
i, j, k = sparse_truth.t()
dispatch_mask_truth[i, j, k] = 1
assert (torch.equal(dispatch_mask_truth, res))
#s=4 e=4 topk=2 cap=2(s*topk/e)
logits = torch.tensor([[0.11, 0.2, 0.1, 0.3], [0.3, 0.4, 0.11, 0.1], [0.11, 0.1, 0.6, 0.5],
[0.1, 0.11, 0.7, 0.8]])
logits *= dist.get_rank() + 1
probs_dispatch_res = topkgating(logits, 2, 1, min_capacity=1, drop_policy='probs')[2]
probs_sec_sparse = torch.tensor([[0, 1, 0], [1, 0, 0], [1, 1, 1], [2, 2, 0], [2, 3, 0], [3, 2, 1], [3, 3, 1]])
check_equal(logits, 2, probs_sec_sparse, probs_dispatch_res)
position_sec_sparse = torch.tensor([[0, 1, 0], [0, 3, 0], [1, 0, 0], [1, 1, 1], [2, 2, 0], [2, 3, 1],
[3, 2, 1]])
position_dispatch_res = topkgating(logits, 2, 1, min_capacity=1, drop_policy='position')[2]
check_equal(logits, 2, position_sec_sparse, position_dispatch_res)
#s=4 e=6 topk=3 cap=2(s*topk/e)
logits2 = torch.tensor([[0.5858, 0.4801, 0.6269, 0.5397, 0.9722, 0.7034],
[0.5445, 0.6332, 0.4519, 0.6308, 0.0519, 0.6450],
[0.4874, 0.8110, 0.7467, 0.8474, 0.0277, 0.3068],
[0.8570, 0.6714, 0.5310, 0.3274, 0.4836, 0.9892]])
logits2 *= dist.get_rank() + 1
#top3 full mask #prob_mask #postion_mask
#0 0 1 0 1 1 #0 0 1 0 1 0 #0 0 1 0 1 1
#0 1 0 1 0 1 #0 1 0 1 0 1 #0 1 0 1 0 1
#0 1 1 1 0 0 #0 1 1 1 0 0 #0 1 1 1 0 0
#1 1 0 0 0 1 #1 0 0 0 0 1 #1 0 0 0 0 0
probs_dispatch_res = topkgating(logits2, 3, 1, min_capacity=1, drop_policy='probs')[2]
probs_sec_sparse = torch.tensor([[0, 2, 0], [0, 4, 0], [1, 1, 0], [1, 3, 0], [1, 5, 0], [2, 1, 1], [2, 2, 1],
[2, 3, 1], [3, 0, 0], [3, 5, 1]])
check_equal(logits2, 2, probs_sec_sparse, probs_dispatch_res)
position_sec_sparse = torch.tensor([[0, 2, 0], [0, 4, 0], [0, 5, 0], [1, 1, 0], [1, 3, 0], [1, 5, 1],
[2, 1, 1], [2, 2, 1], [2, 3, 1], [3, 0, 0]])
position_dispatch_res = topkgating(logits2, 3, 1, min_capacity=1, drop_policy='position')[2]
check_equal(logits2, 2, position_sec_sparse, position_dispatch_res)
#s=4 e=4 topk=2 drop_tokens=False
logits3 = torch.tensor([[0.95, 0.85, 0.90, 0.80], [0.70, 0.65, 0.75, 0.60], [0.50, 0.55, 0.45, 0.40],
[0.35, 0.30, 0.25, 0.20]])
logits3 *= dist.get_rank() + 1
dispatch_res = topkgating(logits3, 2, 1, min_capacity=1, drop_tokens=False)[2]
sec_sparse = torch.tensor([[0, 0, 0], [0, 2, 0], [1, 0, 1], [1, 2, 1], [2, 0, 2], [2, 1, 0], [3, 0, 3],
[3, 1, 1]])
check_equal(logits3, 4, sec_sparse, dispatch_res)
class TestExpertWeightGradWithZero(DistributedTest):
world_size = 2
@pytest.mark.parametrize("zero_stage", [0, 1, 2])
def test(self, zero_stage):
if not required_torch_version(min_version=1.8):
pytest.skip("DeepSpeed MoE tests need torch 1.8 or higher to run correctly")
def seed_everything(seed=11):
random.seed(seed)
torch.manual_seed(seed)
get_accelerator().manual_seed(seed)
get_accelerator().manual_seed_all(seed)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
def get_state_dict_ep2(state_dict):
"""
convert state_dict from EP=1 to EP=2
"""
rank = int(deepspeed.comm.get_rank())
ep_state_dict = dict()
dst_sub_key = "deepspeed_moe.experts.deepspeed_experts.0"
src_sub_key = f"deepspeed_moe.experts.deepspeed_experts.{rank}"
for moe_layer in ["moe_1", "moe_2"]:
for mlp_in_moe in [0, 1]:
dst_key = f"{moe_layer}.{dst_sub_key}.{mlp_in_moe}"
src_key = f"{moe_layer}.{src_sub_key}.{mlp_in_moe}"
ep_state_dict[f"{dst_key}.weight"] = state_dict[f"{src_key}.weight"].detach().clone()
ep_state_dict[f"{dst_key}.bias"] = state_dict[f"{src_key}.bias"].detach().clone()
for key in state_dict.keys():
if "deepspeed_moe.experts.deepspeed_experts" not in key:
ep_state_dict[key] = state_dict[key].detach().clone()
return ep_state_dict
def get_models(hidden_dim):
model_ep1 = SimpleMoEModel(hidden_dim=hidden_dim, num_experts=2, ep_size=1, use_rts=False)
model_ep2 = SimpleMoEModel(hidden_dim=hidden_dim, num_experts=2, ep_size=2, use_rts=False)
state_dict_ep1 = model_ep1.state_dict()
state_dict_ep2 = get_state_dict_ep2(state_dict_ep1)
model_ep2.load_state_dict(state_dict_ep2)
model_ep1, _, _, _ = deepspeed.initialize(config=config_dict, model=model_ep1)
model_ep2, _, _, _ = deepspeed.initialize(config=config_dict, model=model_ep2)
return model_ep1, model_ep2
def extract_expert_grad(model, expert_id):
def _get_weight_bias(experts):
return ([deepspeed.utils.safe_get_full_grad(expert[0].weight)
for expert in experts][expert_id].detach().clone(),
[deepspeed.utils.safe_get_full_grad(expert[0].bias)
for expert in experts][expert_id].detach().clone(),
[deepspeed.utils.safe_get_full_grad(expert[1].weight)
for expert in experts][expert_id].detach().clone(),
[deepspeed.utils.safe_get_full_grad(expert[1].bias)
for expert in experts][expert_id].detach().clone())
return (*_get_weight_bias(model.moe_1.deepspeed_moe.experts.deepspeed_experts),
*_get_weight_bias(model.moe_2.deepspeed_moe.experts.deepspeed_experts))
seed_everything()
config_dict = {
"train_micro_batch_size_per_gpu": 1,
"steps_per_print": 1,
"optimizer": {
"type": "Adam",
"params": {
"lr": 0.1,
}
},
"zero_optimization": {
"stage": zero_stage
}
}
hidden_dim = 4
total_samples = 2
rank = deepspeed.comm.get_rank()
model_ep1, model_ep2 = get_models(hidden_dim)
data_loader = sequence_dataloader(model=model_ep1,
total_samples=total_samples,
hidden_dim=hidden_dim,
device=model_ep1.device,
dtype=torch.float32)
expert_weight_grad_ep1 = []
expert_weight_grad_ep2 = []
for batch in data_loader:
loss_ep1 = model_ep1(batch[0], batch[1])
loss_ep2 = model_ep2(batch[0], batch[1])
model_ep1.backward(loss_ep1)
model_ep2.backward(loss_ep2)
expert_weight_grad_ep1.extend(extract_expert_grad(model_ep1, rank))
expert_weight_grad_ep2.extend(extract_expert_grad(model_ep2, 0))
model_ep1.step()
model_ep2.step()
assert len(expert_weight_grad_ep1) == len(expert_weight_grad_ep2)
for grad_from_ep1, grad_from_ep2 in zip(expert_weight_grad_ep1, expert_weight_grad_ep2):
assert torch.allclose(grad_from_ep1, grad_from_ep2, atol=0, rtol=1e-4)