chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,569 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# DeepSpeed Team
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import deepspeed.comm as dist
|
||||
import deepspeed
|
||||
from copy import deepcopy
|
||||
from torch import nn
|
||||
|
||||
from unit.common import DistributedTest, preferred_dtype
|
||||
from deepspeed.accelerator import get_accelerator
|
||||
from deepspeed.utils import groups
|
||||
from deepspeed.module_inject.layers import (LinearAllreduce, LinearLayer, SubParamLinearLayer, fused_LinearLayer)
|
||||
from deepspeed.module_inject.autotp_config import AutoTPConfig
|
||||
from deepspeed.module_inject.auto_tp import AutoTP
|
||||
|
||||
|
||||
def skip_on_device():
|
||||
if get_accelerator().device_name() == 'xpu':
|
||||
pytest.skip("XPU requires a higher version for test")
|
||||
|
||||
|
||||
class SequentialLinearModel(torch.nn.Module):
|
||||
|
||||
def __init__(self, hidden_dim, nlayers=1):
|
||||
super(SequentialLinearModel, self).__init__()
|
||||
self.linears = torch.nn.ModuleList([torch.nn.Linear(hidden_dim, hidden_dim) for _ in range(nlayers)])
|
||||
|
||||
def forward(self, x):
|
||||
for layer in self.linears:
|
||||
x = layer(x)
|
||||
return x
|
||||
|
||||
|
||||
class CustomLinearModule(torch.nn.Module):
|
||||
|
||||
def __init__(self, hidden_dim):
|
||||
super(CustomLinearModule, self).__init__()
|
||||
self.weight = torch.nn.Parameter(torch.empty(hidden_dim, hidden_dim))
|
||||
self.bias = torch.nn.Parameter(torch.empty(hidden_dim))
|
||||
torch.nn.init.uniform_(self.weight, -0.02, 0.02)
|
||||
torch.nn.init.uniform_(self.bias, -0.02, 0.02)
|
||||
|
||||
def forward(self, x):
|
||||
return torch.matmul(x, self.weight.transpose(-1, -2)) + self.bias
|
||||
|
||||
|
||||
class CustomLinearModel(torch.nn.Module):
|
||||
|
||||
def __init__(self, hidden_dim):
|
||||
super(CustomLinearModel, self).__init__()
|
||||
self.custom = CustomLinearModule(hidden_dim)
|
||||
|
||||
def forward(self, x):
|
||||
return self.custom(x)
|
||||
|
||||
|
||||
class QKVLinearModule(torch.nn.Module):
|
||||
|
||||
def __init__(self, hidden_dim):
|
||||
super(QKVLinearModule, self).__init__()
|
||||
self.qkv_proj = torch.nn.Linear(hidden_dim, hidden_dim * 3)
|
||||
|
||||
def forward(self, x):
|
||||
return self.qkv_proj(x)
|
||||
|
||||
|
||||
class QKVLinearModel(torch.nn.Module):
|
||||
|
||||
def __init__(self, hidden_dim):
|
||||
super(QKVLinearModel, self).__init__()
|
||||
self.self_attn = QKVLinearModule(hidden_dim)
|
||||
|
||||
def forward(self, x):
|
||||
return self.self_attn(x)
|
||||
|
||||
|
||||
class DeepAttention(torch.nn.Module):
|
||||
"""Mimics HF attention module with separate projection layers."""
|
||||
|
||||
def __init__(self, hidden_dim):
|
||||
super().__init__()
|
||||
self.q_proj = torch.nn.Linear(hidden_dim, hidden_dim)
|
||||
self.o_proj = torch.nn.Linear(hidden_dim, hidden_dim)
|
||||
|
||||
def forward(self, x):
|
||||
return self.o_proj(self.q_proj(x))
|
||||
|
||||
|
||||
class DeepBlock(torch.nn.Module):
|
||||
"""Mimics a single HF transformer block."""
|
||||
|
||||
def __init__(self, hidden_dim):
|
||||
super().__init__()
|
||||
self.self_attn = DeepAttention(hidden_dim)
|
||||
|
||||
def forward(self, x):
|
||||
return self.self_attn(x)
|
||||
|
||||
|
||||
class DeepModel(torch.nn.Module):
|
||||
"""Mimics HF transformer structure: model.layers.[N].self_attn.{q,o}_proj.
|
||||
|
||||
This creates a 4-level-deep module hierarchy to test that _replace_module
|
||||
correctly propagates the full module path during recursion.
|
||||
"""
|
||||
|
||||
def __init__(self, hidden_dim, nlayers=2):
|
||||
super().__init__()
|
||||
self.layers = torch.nn.ModuleList([DeepBlock(hidden_dim) for _ in range(nlayers)])
|
||||
|
||||
def forward(self, x):
|
||||
for layer in self.layers:
|
||||
x = layer(x)
|
||||
return x
|
||||
|
||||
|
||||
def init_tp_engine(tp_size, partition_config=None):
|
||||
config_dict = {
|
||||
"train_micro_batch_size_per_gpu": 1,
|
||||
"optimizer": {
|
||||
"type": "Adam",
|
||||
"params": {
|
||||
"lr": 1e-6
|
||||
}
|
||||
},
|
||||
"tensor_parallel": {
|
||||
"autotp_size": tp_size,
|
||||
},
|
||||
"zero_optimization": {
|
||||
"stage": 0,
|
||||
}
|
||||
}
|
||||
if partition_config is not None:
|
||||
config_dict["tensor_parallel"]["partition_config"] = partition_config
|
||||
else:
|
||||
config_dict["tensor_parallel"]["partition_config"] = {
|
||||
"use_default_specs": False,
|
||||
"layer_specs": [{
|
||||
"patterns": [".*\\.weight$"],
|
||||
"partition_type": "skip",
|
||||
}],
|
||||
}
|
||||
if preferred_dtype() is torch.float16:
|
||||
config_dict["fp16"] = {"enabled": True}
|
||||
elif preferred_dtype() is torch.bfloat16:
|
||||
config_dict["bf16"] = {"enabled": True}
|
||||
|
||||
model = SequentialLinearModel(hidden_dim=8)
|
||||
deepspeed.initialize(model=model, model_parameters=model.parameters(), config=config_dict)
|
||||
|
||||
|
||||
def apply_autotp_with_partition_config(model, tp_size, partition_config):
|
||||
groups._init_tp_mesh_device(tensor_model_parallel_size=tp_size)
|
||||
autotp_config = AutoTPConfig.from_dict(partition_config)
|
||||
autotp = AutoTP(module=model,
|
||||
all_reduce_linears=[],
|
||||
prefix="",
|
||||
state_dict=None,
|
||||
linear_layer_setting=None,
|
||||
orig_layer_impl=None,
|
||||
keep_module_on_host=False,
|
||||
partition_config=autotp_config)
|
||||
autotp.set_tensor_parallel_config(tp_size, groups.get_tensor_model_parallel_group())
|
||||
autotp.update_linear_policies()
|
||||
autotp._replace_module(model)
|
||||
return model
|
||||
|
||||
|
||||
def gather_subparam_output(output, subparam_sizes, mp_group):
|
||||
tp_world_size = dist.get_world_size(group=mp_group)
|
||||
local_sizes = [size // tp_world_size for size in subparam_sizes]
|
||||
output_chunks = torch.split(output, local_sizes, dim=-1)
|
||||
gathered_chunks = []
|
||||
for chunk in output_chunks:
|
||||
chunk = chunk.contiguous()
|
||||
gathered = [torch.empty_like(chunk) for _ in range(tp_world_size)]
|
||||
dist.all_gather(gathered, chunk, group=mp_group)
|
||||
gathered_chunks.append(torch.cat(gathered, dim=-1))
|
||||
return torch.cat(gathered_chunks, dim=-1)
|
||||
|
||||
|
||||
def assert_close_for_preferred_dtype(actual, expected):
|
||||
atol = 1e-3
|
||||
rtol = 2e-2
|
||||
if preferred_dtype() is torch.float32:
|
||||
atol = 1e-5
|
||||
rtol = 1e-5
|
||||
torch.testing.assert_close(actual, expected, atol=atol, rtol=rtol)
|
||||
|
||||
|
||||
class TestAutoTPCustomPatterns(DistributedTest):
|
||||
world_size = 2
|
||||
reuse_dist_env = False
|
||||
|
||||
def test_custom_pattern_replacement(self):
|
||||
skip_on_device()
|
||||
partition_config = {
|
||||
"use_default_specs":
|
||||
False,
|
||||
"layer_specs": [
|
||||
{
|
||||
"patterns": [".*linears\\.0\\.weight$"],
|
||||
"partition_type": "row",
|
||||
},
|
||||
{
|
||||
"patterns": [".*linears\\.1\\.weight$"],
|
||||
"partition_type": "column",
|
||||
},
|
||||
{
|
||||
"patterns": [".*linears\\.2\\.weight$"],
|
||||
"partition_type": "skip",
|
||||
},
|
||||
],
|
||||
}
|
||||
model = SequentialLinearModel(hidden_dim=16, nlayers=3)
|
||||
model = apply_autotp_with_partition_config(model, tp_size=2, partition_config=partition_config)
|
||||
|
||||
assert isinstance(model.linears[0], LinearAllreduce)
|
||||
assert isinstance(model.linears[1], LinearLayer)
|
||||
assert isinstance(model.linears[2], nn.Linear)
|
||||
|
||||
def test_custom_patterns_applied_via_config(self):
|
||||
skip_on_device()
|
||||
partition_config = {
|
||||
"use_default_specs":
|
||||
False,
|
||||
"layer_specs": [
|
||||
{
|
||||
"patterns": [".*linears\\.0\\.weight$"],
|
||||
"partition_type": "row",
|
||||
},
|
||||
{
|
||||
"patterns": [".*linears\\.1\\.weight$"],
|
||||
"partition_type": "column",
|
||||
},
|
||||
{
|
||||
"patterns": [".*linears\\.2\\.weight$"],
|
||||
"partition_type": "skip",
|
||||
},
|
||||
],
|
||||
}
|
||||
config_dict = {
|
||||
"train_micro_batch_size_per_gpu": 1,
|
||||
"optimizer": {
|
||||
"type": "Adam",
|
||||
"params": {
|
||||
"lr": 1e-6
|
||||
}
|
||||
},
|
||||
"tensor_parallel": {
|
||||
"autotp_size": 2,
|
||||
"partition_config": partition_config,
|
||||
},
|
||||
"zero_optimization": {
|
||||
"stage": 0,
|
||||
}
|
||||
}
|
||||
if preferred_dtype() is torch.float16:
|
||||
config_dict["fp16"] = {"enabled": True}
|
||||
elif preferred_dtype() is torch.bfloat16:
|
||||
config_dict["bf16"] = {"enabled": True}
|
||||
|
||||
model = SequentialLinearModel(hidden_dim=16, nlayers=3)
|
||||
engine, _, _, _ = deepspeed.initialize(model=model, model_parameters=model.parameters(), config=config_dict)
|
||||
assert isinstance(engine.module.linears[0], LinearAllreduce)
|
||||
assert isinstance(engine.module.linears[1], LinearLayer)
|
||||
assert isinstance(engine.module.linears[2], nn.Linear)
|
||||
|
||||
def test_use_default_specs_false_skips_unmatched_layers(self):
|
||||
skip_on_device()
|
||||
# Verify unmatched layers remain unsharded when defaults are disabled.
|
||||
partition_config = {
|
||||
"use_default_specs":
|
||||
False,
|
||||
"layer_specs": [
|
||||
{
|
||||
"patterns": [".*linears\\.0\\.weight$"],
|
||||
"partition_type": "row",
|
||||
},
|
||||
{
|
||||
"patterns": [".*linears\\.1\\.weight$"],
|
||||
"partition_type": "column",
|
||||
},
|
||||
],
|
||||
}
|
||||
config_dict = {
|
||||
"train_micro_batch_size_per_gpu": 1,
|
||||
"optimizer": {
|
||||
"type": "Adam",
|
||||
"params": {
|
||||
"lr": 1e-6
|
||||
}
|
||||
},
|
||||
"tensor_parallel": {
|
||||
"autotp_size": 2,
|
||||
"partition_config": partition_config,
|
||||
},
|
||||
"zero_optimization": {
|
||||
"stage": 0,
|
||||
}
|
||||
}
|
||||
if preferred_dtype() is torch.float16:
|
||||
config_dict["fp16"] = {"enabled": True}
|
||||
elif preferred_dtype() is torch.bfloat16:
|
||||
config_dict["bf16"] = {"enabled": True}
|
||||
|
||||
model = SequentialLinearModel(hidden_dim=16, nlayers=3)
|
||||
engine, _, _, _ = deepspeed.initialize(model=model, model_parameters=model.parameters(), config=config_dict)
|
||||
assert isinstance(engine.module.linears[0], LinearAllreduce)
|
||||
assert isinstance(engine.module.linears[1], LinearLayer)
|
||||
assert isinstance(engine.module.linears[2], nn.Linear)
|
||||
|
||||
def test_custom_module_replacement_with_patterns(self):
|
||||
skip_on_device()
|
||||
# Verify custom linear-like modules are partitioned via patterns.
|
||||
partition_config = {
|
||||
"use_default_specs": False,
|
||||
"layer_specs": [
|
||||
{
|
||||
"patterns": [".*custom\\.weight$"],
|
||||
"partition_type": "column",
|
||||
},
|
||||
],
|
||||
}
|
||||
config_dict = {
|
||||
"train_micro_batch_size_per_gpu": 1,
|
||||
"optimizer": {
|
||||
"type": "Adam",
|
||||
"params": {
|
||||
"lr": 1e-6
|
||||
}
|
||||
},
|
||||
"tensor_parallel": {
|
||||
"autotp_size": 2,
|
||||
"partition_config": partition_config,
|
||||
},
|
||||
"zero_optimization": {
|
||||
"stage": 0,
|
||||
}
|
||||
}
|
||||
if preferred_dtype() is torch.float16:
|
||||
config_dict["fp16"] = {"enabled": True}
|
||||
elif preferred_dtype() is torch.bfloat16:
|
||||
config_dict["bf16"] = {"enabled": True}
|
||||
|
||||
model = CustomLinearModel(hidden_dim=16)
|
||||
engine, _, _, _ = deepspeed.initialize(model=model, model_parameters=model.parameters(), config=config_dict)
|
||||
assert isinstance(engine.module.custom, LinearLayer)
|
||||
|
||||
def test_custom_pattern_disables_fused_qkv_heuristic(self):
|
||||
skip_on_device()
|
||||
# Use a qkv_proj name that would trigger the fused-QKV heuristic, then
|
||||
# verify custom patterns override that path and preserve correctness.
|
||||
torch.manual_seed(1234)
|
||||
hidden_dim = 16
|
||||
qkv_sizes = (hidden_dim, hidden_dim, hidden_dim)
|
||||
partition_config = {
|
||||
"use_default_specs":
|
||||
False,
|
||||
"layer_specs": [
|
||||
{
|
||||
"patterns": [".*self_attn\\.qkv_proj\\.weight$"],
|
||||
"partition_type": "column",
|
||||
"shape": [list(qkv_sizes), -1],
|
||||
"partition_dim": 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
config_dict = {
|
||||
"train_micro_batch_size_per_gpu": 1,
|
||||
"optimizer": {
|
||||
"type": "Adam",
|
||||
"params": {
|
||||
"lr": 1e-6
|
||||
}
|
||||
},
|
||||
"tensor_parallel": {
|
||||
"autotp_size": 2,
|
||||
"partition_config": partition_config,
|
||||
},
|
||||
"zero_optimization": {
|
||||
"stage": 0,
|
||||
}
|
||||
}
|
||||
if preferred_dtype() is torch.float16:
|
||||
config_dict["fp16"] = {"enabled": True}
|
||||
elif preferred_dtype() is torch.bfloat16:
|
||||
config_dict["bf16"] = {"enabled": True}
|
||||
|
||||
model = QKVLinearModel(hidden_dim=hidden_dim)
|
||||
baseline = deepcopy(model).to(get_accelerator().current_device(), dtype=preferred_dtype())
|
||||
engine, _, _, _ = deepspeed.initialize(model=model, model_parameters=model.parameters(), config=config_dict)
|
||||
qkv_layer = engine.module.self_attn.qkv_proj
|
||||
# Custom pattern should force SubParamLinearLayer (shape-based path),
|
||||
# and avoid the legacy fused-QKV heuristic despite the qkv_proj name.
|
||||
assert isinstance(qkv_layer, SubParamLinearLayer)
|
||||
assert not isinstance(qkv_layer, fused_LinearLayer)
|
||||
|
||||
assert qkv_layer.partition_dim == 0
|
||||
assert qkv_layer._subparam_sizes == qkv_sizes
|
||||
assert qkv_layer._orig_weight_shape == (hidden_dim * 3, hidden_dim)
|
||||
|
||||
qkv_layer.gather_params([qkv_layer.weight, qkv_layer.bias])
|
||||
torch.testing.assert_close(qkv_layer.weight, baseline.self_attn.qkv_proj.weight)
|
||||
if qkv_layer.bias is not None:
|
||||
torch.testing.assert_close(qkv_layer.bias, baseline.self_attn.qkv_proj.bias)
|
||||
|
||||
torch.manual_seed(4321)
|
||||
inputs = torch.randn(2, hidden_dim, dtype=preferred_dtype(), device=get_accelerator().current_device())
|
||||
full_output = baseline(inputs)
|
||||
tp_output = engine.module(inputs)
|
||||
assert_close_for_preferred_dtype(tp_output, full_output)
|
||||
|
||||
def test_first_match_precedence(self):
|
||||
skip_on_device()
|
||||
partition_config = {
|
||||
"use_default_specs":
|
||||
False,
|
||||
"layer_specs": [
|
||||
{
|
||||
"patterns": [".*linears\\.0\\.weight$"],
|
||||
"partition_type": "skip",
|
||||
},
|
||||
{
|
||||
"patterns": [".*linears\\.0\\.weight$"],
|
||||
"partition_type": "column",
|
||||
},
|
||||
],
|
||||
}
|
||||
model = SequentialLinearModel(hidden_dim=16, nlayers=1)
|
||||
model = apply_autotp_with_partition_config(model, tp_size=2, partition_config=partition_config)
|
||||
|
||||
assert isinstance(model.linears[0], nn.Linear)
|
||||
|
||||
def test_deep_model_full_path_propagation(self):
|
||||
"""Verify _replace_module propagates accumulated paths through deep hierarchies.
|
||||
|
||||
Uses a 4-level-deep model (layers.N.self_attn.{q,o}_proj) with patterns
|
||||
that require intermediate path components (layers.N). Without correct
|
||||
full_name propagation, the recursive path is truncated and patterns
|
||||
that include intermediate levels will silently fail to match.
|
||||
"""
|
||||
skip_on_device()
|
||||
partition_config = {
|
||||
"use_default_specs":
|
||||
False,
|
||||
"layer_specs": [
|
||||
{
|
||||
"patterns": [r".*layers\.\d+\.self_attn\.q_proj\.weight$"],
|
||||
"partition_type": "column",
|
||||
},
|
||||
{
|
||||
"patterns": [r".*layers\.\d+\.self_attn\.o_proj\.weight$"],
|
||||
"partition_type": "row",
|
||||
},
|
||||
],
|
||||
}
|
||||
model = DeepModel(hidden_dim=16, nlayers=2)
|
||||
model = apply_autotp_with_partition_config(model, tp_size=2, partition_config=partition_config)
|
||||
|
||||
# All 4 projections (2 layers x {q_proj, o_proj}) must be replaced.
|
||||
# Before the full_name fix, 0 modules were replaced because the mangled
|
||||
# path "self_attn.q_proj.weight" could not match "layers.N.self_attn...".
|
||||
for i in range(2):
|
||||
assert isinstance(model.layers[i].self_attn.q_proj, LinearLayer), \
|
||||
f"layers.{i}.self_attn.q_proj was not replaced (path propagation bug?)"
|
||||
assert isinstance(model.layers[i].self_attn.o_proj, LinearAllreduce), \
|
||||
f"layers.{i}.self_attn.o_proj was not replaced (path propagation bug?)"
|
||||
|
||||
|
||||
def test_invalid_custom_shape_rejected():
|
||||
bad_config = {
|
||||
"layer_specs": [{
|
||||
"patterns": [".*"],
|
||||
"partition_type": "column",
|
||||
"shape": [2, [1, 1]],
|
||||
}]
|
||||
}
|
||||
with pytest.raises(ValueError, match="nested tuple only allowed at partition_dim"):
|
||||
AutoTPConfig.from_dict(bad_config)
|
||||
|
||||
|
||||
class TestAutoTPFusedWeights(DistributedTest):
|
||||
world_size = 2
|
||||
reuse_dist_env = False
|
||||
|
||||
def test_gate_up_fused_weight_partition(self):
|
||||
skip_on_device()
|
||||
init_tp_engine(tp_size=2)
|
||||
|
||||
hidden_dim = 8
|
||||
torch.manual_seed(42)
|
||||
linear = nn.Linear(hidden_dim,
|
||||
hidden_dim * 2,
|
||||
bias=True,
|
||||
dtype=preferred_dtype(),
|
||||
device=get_accelerator().current_device())
|
||||
full_weight = deepcopy(linear.weight.data)
|
||||
full_bias = deepcopy(linear.bias.data)
|
||||
|
||||
layer = SubParamLinearLayer(deepcopy(linear),
|
||||
groups.get_tensor_model_parallel_group(),
|
||||
shape=(2, -1),
|
||||
partition_dim=0,
|
||||
name="mlp.gate_up_proj")
|
||||
assert layer._subparam_sizes == (hidden_dim, hidden_dim)
|
||||
assert layer.weight.shape == (hidden_dim, hidden_dim)
|
||||
|
||||
layer.gather_params([layer.weight, layer.bias])
|
||||
torch.testing.assert_close(layer.weight.data, full_weight)
|
||||
torch.testing.assert_close(layer.bias.data, full_bias)
|
||||
|
||||
def test_gqa_uneven_qkv_fused_weight_partition(self):
|
||||
skip_on_device()
|
||||
init_tp_engine(tp_size=2)
|
||||
|
||||
hidden_dim = 8
|
||||
q_size, k_size, v_size = 8, 4, 4
|
||||
torch.manual_seed(123)
|
||||
linear = nn.Linear(hidden_dim,
|
||||
q_size + k_size + v_size,
|
||||
bias=True,
|
||||
dtype=preferred_dtype(),
|
||||
device=get_accelerator().current_device())
|
||||
full_weight = deepcopy(linear.weight.data)
|
||||
full_bias = deepcopy(linear.bias.data)
|
||||
|
||||
layer = SubParamLinearLayer(deepcopy(linear),
|
||||
groups.get_tensor_model_parallel_group(),
|
||||
shape=((q_size, k_size, v_size), -1),
|
||||
partition_dim=0,
|
||||
name="self_attn.qkv_proj")
|
||||
assert layer._subparam_sizes == (q_size, k_size, v_size)
|
||||
assert layer.weight.shape == ((q_size + k_size + v_size) // 2, hidden_dim)
|
||||
|
||||
layer.gather_params([layer.weight, layer.bias])
|
||||
torch.testing.assert_close(layer.weight.data, full_weight)
|
||||
torch.testing.assert_close(layer.bias.data, full_bias)
|
||||
|
||||
def test_gqa_uneven_qkv_fused_forward(self):
|
||||
skip_on_device()
|
||||
groups._init_tp_mesh_device(tensor_model_parallel_size=2)
|
||||
|
||||
hidden_dim = 8
|
||||
q_size, k_size, v_size = 8, 4, 4
|
||||
torch.manual_seed(321)
|
||||
linear = nn.Linear(hidden_dim,
|
||||
q_size + k_size + v_size,
|
||||
bias=True,
|
||||
dtype=preferred_dtype(),
|
||||
device=get_accelerator().current_device())
|
||||
layer = SubParamLinearLayer(deepcopy(linear),
|
||||
groups.get_tensor_model_parallel_group(),
|
||||
shape=((q_size, k_size, v_size), -1),
|
||||
partition_dim=0,
|
||||
name="self_attn.qkv_proj")
|
||||
|
||||
torch.manual_seed(42)
|
||||
inputs = torch.randn(2, hidden_dim, dtype=preferred_dtype(), device=get_accelerator().current_device())
|
||||
full_output = linear(inputs)
|
||||
tp_output = layer(inputs)
|
||||
|
||||
gathered_output = gather_subparam_output(tp_output, (q_size, k_size, v_size),
|
||||
groups.get_tensor_model_parallel_group())
|
||||
assert_close_for_preferred_dtype(gathered_output, full_output)
|
||||
@@ -0,0 +1,822 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# DeepSpeed Team
|
||||
|
||||
import pytest
|
||||
import deepspeed.comm as dist
|
||||
import torch
|
||||
import math
|
||||
from copy import deepcopy
|
||||
|
||||
from unit.common import DistributedTest, preferred_dtype
|
||||
import deepspeed
|
||||
from deepspeed.accelerator import get_accelerator
|
||||
from unit.simple_model import SimpleModel, random_dataloader
|
||||
from deepspeed.utils import groups
|
||||
from contextlib import contextmanager
|
||||
from torch import nn
|
||||
from deepspeed.module_inject.layers import LinearAllreduce, LinearLayer, set_autotp_mode, is_autotp_training_mode
|
||||
from unit.checkpoint.common import compare_lr_scheduler_states, compare_optimizer_states
|
||||
import os
|
||||
from deepspeed.runtime.utils import is_model_parallel_parameter
|
||||
|
||||
|
||||
def skip_on_device():
|
||||
if get_accelerator().device_name() == 'xpu':
|
||||
pytest.skip("XPU requires a higher version for test")
|
||||
|
||||
|
||||
def reset_tp_model_init_state():
|
||||
deepspeed._TP_MODEL_INIT_ARGS = None
|
||||
set_autotp_mode(training=False)
|
||||
|
||||
|
||||
def _reset_tp_groups(monkeypatch):
|
||||
monkeypatch.setattr(groups, "_DATA_PARALLEL_GROUP", None)
|
||||
monkeypatch.setattr(groups, "_MODEL_PARALLEL_GROUP", None)
|
||||
monkeypatch.setattr(groups, "_TENSOR_MODEL_PARALLEL_GROUP", None)
|
||||
|
||||
|
||||
def _patch_tp_group_creation(monkeypatch, *, rank=2, initialize_mesh_device=None):
|
||||
new_group_calls = []
|
||||
|
||||
def fake_new_group(ranks):
|
||||
ranks = tuple(ranks)
|
||||
new_group_calls.append(ranks)
|
||||
return ranks
|
||||
|
||||
_reset_tp_groups(monkeypatch)
|
||||
monkeypatch.setattr(groups.dist, "get_world_size", lambda group=None: 4)
|
||||
monkeypatch.setattr(groups.dist, "get_rank", lambda group=None: rank)
|
||||
monkeypatch.setattr(groups.dist, "new_group", fake_new_group)
|
||||
monkeypatch.setattr(groups, "log_dist", lambda *args, **kwargs: None)
|
||||
if initialize_mesh_device is not None:
|
||||
monkeypatch.setattr(groups.dist, "initialize_mesh_device", initialize_mesh_device)
|
||||
|
||||
return new_group_calls
|
||||
|
||||
|
||||
def test_init_tp_mesh_device_debug_detail_uses_explicit_groups(monkeypatch):
|
||||
|
||||
def fail_initialize_mesh_device(*args, **kwargs):
|
||||
raise AssertionError("DeviceMesh should be skipped when TORCH_DISTRIBUTED_DEBUG=DETAIL")
|
||||
|
||||
new_group_calls = _patch_tp_group_creation(monkeypatch, initialize_mesh_device=fail_initialize_mesh_device)
|
||||
monkeypatch.setenv("TORCH_DISTRIBUTED_DEBUG", "DETAIL")
|
||||
|
||||
data_parallel_group, tensor_parallel_group = groups._init_tp_mesh_device(tensor_model_parallel_size=2)
|
||||
|
||||
assert new_group_calls == [(0, 2), (1, 3), (0, 1), (2, 3)]
|
||||
assert data_parallel_group == (0, 2)
|
||||
assert tensor_parallel_group == (2, 3)
|
||||
assert groups.get_data_parallel_group() == (0, 2)
|
||||
assert groups.get_tensor_model_parallel_group() == (2, 3)
|
||||
|
||||
|
||||
def test_init_tp_mesh_device_split_error_falls_back_to_explicit_groups(monkeypatch):
|
||||
|
||||
def raise_split_error(*args, **kwargs):
|
||||
raise RuntimeError(groups._DEVICE_MESH_SPLIT_UNSUPPORTED)
|
||||
|
||||
new_group_calls = _patch_tp_group_creation(monkeypatch, initialize_mesh_device=raise_split_error)
|
||||
monkeypatch.delenv("TORCH_DISTRIBUTED_DEBUG", raising=False)
|
||||
|
||||
data_parallel_group, tensor_parallel_group = groups._init_tp_mesh_device(tensor_model_parallel_size=2)
|
||||
|
||||
assert new_group_calls == [(0, 2), (1, 3), (0, 1), (2, 3)]
|
||||
assert data_parallel_group == (0, 2)
|
||||
assert tensor_parallel_group == (2, 3)
|
||||
assert groups.get_data_parallel_group() == (0, 2)
|
||||
assert groups.get_tensor_model_parallel_group() == (2, 3)
|
||||
|
||||
|
||||
class DummyMPU:
|
||||
|
||||
def __init__(self, tp_world_size=1):
|
||||
self.rank = dist.get_rank()
|
||||
self.world_size = dist.get_world_size()
|
||||
self.tp_world_size = tp_world_size
|
||||
self.dp_group = dist.get_world_group()
|
||||
self.tp_group = dist.get_world_group()
|
||||
|
||||
def get_model_parallel_rank(self):
|
||||
return self.rank % self.tp_world_size
|
||||
|
||||
def get_model_parallel_world_size(self):
|
||||
return self.tp_world_size
|
||||
|
||||
def get_data_parallel_rank(self):
|
||||
return self.rank // self.tp_world_size
|
||||
|
||||
def get_data_parallel_world_size(self):
|
||||
return self.world_size // self.tp_world_size
|
||||
|
||||
def get_data_parallel_group(self):
|
||||
return self.dp_group
|
||||
|
||||
def get_model_parallel_group(self):
|
||||
return self.tp_group
|
||||
|
||||
|
||||
class SequentialLinearModel(torch.nn.Module):
|
||||
|
||||
def __init__(self, hidden_dim, empty_grad=False, nlayers=1):
|
||||
super(SequentialLinearModel, self).__init__()
|
||||
self.linears = torch.nn.ModuleList([torch.nn.Linear(hidden_dim, hidden_dim) for _ in range(nlayers)])
|
||||
if empty_grad:
|
||||
self.linear2 = torch.nn.Linear(hidden_dim, hidden_dim)
|
||||
self.cross_entropy_loss = torch.nn.CrossEntropyLoss()
|
||||
self.empty_grad = empty_grad
|
||||
|
||||
def forward(self, x, y):
|
||||
if len(self.linears) == 1:
|
||||
x = self.linears[0](x)
|
||||
else:
|
||||
for i, l in enumerate(self.linears):
|
||||
x = self.linears[i](x)
|
||||
return self.cross_entropy_loss(x, y)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def should_assert_with_msg(expected_message):
|
||||
try:
|
||||
yield
|
||||
except AssertionError as e:
|
||||
if dist.get_rank() == 0:
|
||||
print(expected_message)
|
||||
print(str(e))
|
||||
if str(e) == expected_message:
|
||||
pass
|
||||
else:
|
||||
raise e
|
||||
else:
|
||||
raise AssertionError(f"Expected AssertionError with message '{expected_message}' "
|
||||
"but no exception was raised.")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tp_size", [2, 4])
|
||||
class TestTpParallelStates(DistributedTest):
|
||||
world_size = 4
|
||||
|
||||
def test(self, tp_size: int):
|
||||
skip_on_device()
|
||||
dp_size = 4 / tp_size
|
||||
hidden_dim = 128
|
||||
config_dict = {
|
||||
"train_micro_batch_size_per_gpu": 1,
|
||||
"tensor_parallel": {
|
||||
"autotp_size": tp_size,
|
||||
"partition_config": {
|
||||
"use_default_specs": False,
|
||||
"layer_specs": [{
|
||||
"patterns": [".*\\.weight$"],
|
||||
"partition_type": "skip",
|
||||
}],
|
||||
}
|
||||
},
|
||||
"zero_optimization": {
|
||||
"stage": 0
|
||||
}
|
||||
}
|
||||
model = SimpleModel(hidden_dim=hidden_dim)
|
||||
model, _, _, _ = deepspeed.initialize(model=model, model_parameters=model.parameters(), config=config_dict)
|
||||
assert groups.get_tensor_model_parallel_world_size() == tp_size
|
||||
assert groups.get_data_parallel_world_size() == dp_size
|
||||
|
||||
|
||||
class TestTpModelInitCompatibility(DistributedTest):
|
||||
world_size = 4
|
||||
reuse_dist_env = False
|
||||
|
||||
def test_tp_model_init_merges_config(self):
|
||||
skip_on_device()
|
||||
reset_tp_model_init_state()
|
||||
model = SimpleModel(hidden_dim=8)
|
||||
deepspeed.tp_model_init(model, tp_size=1, dtype=preferred_dtype())
|
||||
config_dict = {
|
||||
"train_micro_batch_size_per_gpu": 1,
|
||||
"zero_optimization": {
|
||||
"stage": 0,
|
||||
}
|
||||
}
|
||||
engine, _, _, _ = deepspeed.initialize(model=model,
|
||||
model_parameters=model.parameters(),
|
||||
config=config_dict,
|
||||
mpu=DummyMPU())
|
||||
assert engine.autotp_size() == 1
|
||||
assert is_autotp_training_mode()
|
||||
|
||||
def test_tp_model_init_config_autotp_size_mismatch(self):
|
||||
skip_on_device()
|
||||
reset_tp_model_init_state()
|
||||
model = SimpleModel(hidden_dim=8)
|
||||
deepspeed.tp_model_init(model, tp_size=1, dtype=preferred_dtype())
|
||||
config_dict = {
|
||||
"train_micro_batch_size_per_gpu": 1,
|
||||
"tensor_parallel": {
|
||||
"autotp_size": 2,
|
||||
},
|
||||
"zero_optimization": {
|
||||
"stage": 0,
|
||||
}
|
||||
}
|
||||
with pytest.raises(ValueError, match="tensor_parallel.autotp_size"):
|
||||
deepspeed.initialize(model=model, model_parameters=model.parameters(), config=config_dict, mpu=DummyMPU())
|
||||
|
||||
def test_tp_model_init_autocreates_tp_group(self):
|
||||
skip_on_device()
|
||||
reset_tp_model_init_state()
|
||||
# Verify tp_model_init creates TP groups when no mpu is provided.
|
||||
model = SimpleModel(hidden_dim=8)
|
||||
tp_size = 2
|
||||
deepspeed.tp_model_init(model, tp_size=tp_size, dtype=preferred_dtype())
|
||||
config_dict = {
|
||||
"train_micro_batch_size_per_gpu": 1,
|
||||
"tensor_parallel": {
|
||||
"partition_config": {
|
||||
"use_default_specs": False,
|
||||
"layer_specs": [{
|
||||
"patterns": [".*\\.weight$"],
|
||||
"partition_type": "skip",
|
||||
}],
|
||||
}
|
||||
},
|
||||
"zero_optimization": {
|
||||
"stage": 0,
|
||||
}
|
||||
}
|
||||
engine, _, _, _ = deepspeed.initialize(model=model, model_parameters=model.parameters(), config=config_dict)
|
||||
assert engine.autotp_size() == tp_size
|
||||
assert groups.get_tensor_model_parallel_world_size() == tp_size
|
||||
assert groups.get_data_parallel_world_size() == dist.get_world_size() // tp_size
|
||||
|
||||
def test_tp_model_init_tp_group_rejects_mpu(self):
|
||||
skip_on_device()
|
||||
reset_tp_model_init_state()
|
||||
model = SimpleModel(hidden_dim=8)
|
||||
tp_group = dist.new_group(ranks=[0])
|
||||
deepspeed.tp_model_init(model, tp_size=1, dtype=preferred_dtype(), tp_group=tp_group)
|
||||
config_dict = {
|
||||
"train_micro_batch_size_per_gpu": 1,
|
||||
"zero_optimization": {
|
||||
"stage": 0,
|
||||
}
|
||||
}
|
||||
with pytest.raises(ValueError, match="tp_model_init provided tp_group"):
|
||||
deepspeed.initialize(model=model, model_parameters=model.parameters(), config=config_dict, mpu=DummyMPU())
|
||||
|
||||
def test_tp_model_init_dtype_mismatch(self):
|
||||
skip_on_device()
|
||||
reset_tp_model_init_state()
|
||||
model = SimpleModel(hidden_dim=8)
|
||||
deepspeed.tp_model_init(model, tp_size=1, dtype=torch.float16)
|
||||
config_dict = {
|
||||
"train_micro_batch_size_per_gpu": 1,
|
||||
"bf16": {
|
||||
"enabled": True,
|
||||
},
|
||||
"zero_optimization": {
|
||||
"stage": 0,
|
||||
}
|
||||
}
|
||||
with pytest.raises(ValueError, match="Conflicting dtype"):
|
||||
deepspeed.initialize(model=model, model_parameters=model.parameters(), config=config_dict, mpu=DummyMPU())
|
||||
|
||||
@pytest.mark.sequential
|
||||
@pytest.mark.parametrize("tp_size", [2, 4])
|
||||
@pytest.mark.parametrize("tp_overlap_comm", [True, False])
|
||||
def test_tp_model_init_row_parallel(self, tp_size: int, tp_overlap_comm: bool):
|
||||
run_tp_layer_fwd_bwd(tp_size, tp_overlap_comm, column_parallel=False, use_tp_model_init=True)
|
||||
|
||||
@pytest.mark.sequential
|
||||
@pytest.mark.parametrize("tp_size", [2, 4])
|
||||
@pytest.mark.parametrize("tp_overlap_comm", [True, False])
|
||||
def test_tp_model_init_column_parallel(self, tp_size: int, tp_overlap_comm: bool):
|
||||
run_tp_layer_fwd_bwd(tp_size, tp_overlap_comm, column_parallel=True, use_tp_model_init=True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tp_size", [2, 4])
|
||||
class TestTpDataloaderCorrectness(DistributedTest):
|
||||
world_size = 4
|
||||
reuse_dist_env = False
|
||||
|
||||
def test(self, tp_size: int):
|
||||
skip_on_device()
|
||||
hidden_dim = 128
|
||||
config_dict = {
|
||||
"train_micro_batch_size_per_gpu": 1,
|
||||
"steps_per_print": 1,
|
||||
"optimizer": {
|
||||
"type": "Adam",
|
||||
"params": {
|
||||
"lr": 1e-6
|
||||
}
|
||||
},
|
||||
"tensor_parallel": {
|
||||
"autotp_size": tp_size,
|
||||
"partition_config": {
|
||||
"use_default_specs": False,
|
||||
"layer_specs": [{
|
||||
"patterns": [".*\\.weight$"],
|
||||
"partition_type": "skip",
|
||||
}],
|
||||
}
|
||||
},
|
||||
"zero_optimization": {
|
||||
"stage": 0,
|
||||
}
|
||||
}
|
||||
if preferred_dtype() is torch.float16:
|
||||
config_dict["fp16"] = {"enabled": True}
|
||||
elif preferred_dtype() is torch.bfloat16:
|
||||
config_dict["bf16"] = {"enabled": True}
|
||||
|
||||
model = SimpleModel(hidden_dim=hidden_dim)
|
||||
model, _, _, _ = deepspeed.initialize(model=model, model_parameters=model.parameters(), config=config_dict)
|
||||
torch.manual_seed(42)
|
||||
|
||||
data_loader = random_dataloader(model=model,
|
||||
total_samples=3,
|
||||
hidden_dim=hidden_dim,
|
||||
device=model.device,
|
||||
dtype=preferred_dtype())
|
||||
dist.barrier()
|
||||
with should_assert_with_msg(
|
||||
"Data inconsistency within the TP group. Please check the Dataloader implementation to ensure consistency."
|
||||
):
|
||||
for batch in data_loader:
|
||||
# batch[0].requires_grad = requires_grad
|
||||
batch[0] += dist.get_rank()
|
||||
model(batch[0], batch[1])
|
||||
|
||||
model = SimpleModel(hidden_dim=hidden_dim)
|
||||
model, _, _, _ = deepspeed.initialize(model=model, model_parameters=model.parameters(), config=config_dict)
|
||||
data_loader = random_dataloader(model=model,
|
||||
total_samples=3,
|
||||
hidden_dim=hidden_dim,
|
||||
device=model.device,
|
||||
dtype=preferred_dtype())
|
||||
for batch in data_loader:
|
||||
dist.broadcast(batch[0],
|
||||
src=groups.get_tensor_model_parallel_src_rank(),
|
||||
group=groups.get_tensor_model_parallel_group())
|
||||
dist.broadcast(batch[1],
|
||||
src=groups.get_tensor_model_parallel_src_rank(),
|
||||
group=groups.get_tensor_model_parallel_group())
|
||||
model(batch[0], batch[1])
|
||||
|
||||
|
||||
def process_linear_layer(hidden_dim, input):
|
||||
torch.manual_seed(42)
|
||||
torch_linear = nn.Linear(hidden_dim,
|
||||
hidden_dim,
|
||||
dtype=preferred_dtype(),
|
||||
device=get_accelerator().current_device())
|
||||
torch_out = torch_linear(input)
|
||||
torch_loss = torch_out.sum()
|
||||
torch_loss.backward()
|
||||
return torch_linear, torch_out
|
||||
|
||||
|
||||
def run_tp_layer_fwd_bwd(tp_size, tp_overlap_comm, column_parallel, use_tp_model_init=False):
|
||||
skip_on_device()
|
||||
hidden_dim = 128
|
||||
batch_size_per_device = 1
|
||||
config_dict = {
|
||||
"train_micro_batch_size_per_gpu": 1,
|
||||
"steps_per_print": 1,
|
||||
"optimizer": {
|
||||
"type": "Adam",
|
||||
"params": {
|
||||
"lr": 1e-6
|
||||
}
|
||||
},
|
||||
"tensor_parallel": {
|
||||
"autotp_size": tp_size,
|
||||
"tp_overlap_comm": tp_overlap_comm
|
||||
},
|
||||
"zero_optimization": {
|
||||
"stage": 0,
|
||||
}
|
||||
}
|
||||
partition_type = "column" if column_parallel else "row"
|
||||
config_dict["tensor_parallel"]["partition_config"] = {
|
||||
"use_default_specs": False,
|
||||
"layer_specs": [{
|
||||
"patterns": [".*\\.weight$"],
|
||||
"partition_type": partition_type,
|
||||
}],
|
||||
}
|
||||
if preferred_dtype() is torch.float16:
|
||||
config_dict["fp16"] = {"enabled": True}
|
||||
elif preferred_dtype() is torch.bfloat16:
|
||||
config_dict["bf16"] = {"enabled": True}
|
||||
|
||||
model = SequentialLinearModel(hidden_dim=hidden_dim)
|
||||
if use_tp_model_init:
|
||||
reset_tp_model_init_state()
|
||||
deepspeed.tp_model_init(model, tp_size=tp_size, dtype=preferred_dtype())
|
||||
mpu = DummyMPU(tp_world_size=tp_size)
|
||||
model, _, _, _ = deepspeed.initialize(model=model,
|
||||
model_parameters=model.parameters(),
|
||||
config=config_dict,
|
||||
mpu=mpu)
|
||||
else:
|
||||
model, _, _, _ = deepspeed.initialize(model=model, model_parameters=model.parameters(), config=config_dict)
|
||||
|
||||
input = torch.randn(batch_size_per_device,
|
||||
hidden_dim,
|
||||
dtype=preferred_dtype(),
|
||||
requires_grad=True,
|
||||
device=get_accelerator().current_device())
|
||||
dist.broadcast(input, groups.get_tensor_model_parallel_src_rank(), group=groups.get_tensor_model_parallel_group())
|
||||
|
||||
# Note: correctness checks below use standalone TP wrappers and do not
|
||||
# rely on the model's AutoTP-partitioned parameters.
|
||||
torch_linear, torch_out = process_linear_layer(hidden_dim, input)
|
||||
if column_parallel:
|
||||
linear = LinearLayer(deepcopy(torch_linear), groups.get_tensor_model_parallel_group())
|
||||
out = linear(input.to(get_accelerator().current_device()))
|
||||
loss = out.sum()
|
||||
loss.backward()
|
||||
|
||||
cur_device_out = torch.chunk(torch_out, tp_size, dim=-1)[groups.get_tensor_model_parallel_rank()]
|
||||
torch_grad = torch.chunk(torch_linear.weight.grad, tp_size, dim=0)[groups.get_tensor_model_parallel_rank()]
|
||||
torch_bias_grad = torch.chunk(torch_linear.bias.grad, tp_size, dim=0)[groups.get_tensor_model_parallel_rank()]
|
||||
|
||||
torch.testing.assert_close(linear.bias.grad,
|
||||
torch_bias_grad.to(get_accelerator().current_device()),
|
||||
atol=1e-3,
|
||||
rtol=1e-3)
|
||||
torch.testing.assert_close(linear.weight.grad,
|
||||
torch_grad.to(get_accelerator().current_device()),
|
||||
atol=1e-3,
|
||||
rtol=1e-3)
|
||||
torch.testing.assert_close(cur_device_out.to(get_accelerator().current_device()).contiguous(),
|
||||
out.contiguous(),
|
||||
atol=1e-2,
|
||||
rtol=1e-2)
|
||||
else:
|
||||
linear = LinearAllreduce(deepcopy(torch_linear), groups.get_tensor_model_parallel_group())
|
||||
input_ = torch.chunk(input, tp_size, dim=-1)[groups.get_tensor_model_parallel_rank()]
|
||||
out = linear(input_.to(get_accelerator().current_device()))
|
||||
loss = out.sum()
|
||||
loss.backward()
|
||||
|
||||
torch_grad = torch.chunk(torch_linear.weight.grad, tp_size, dim=1)[groups.get_tensor_model_parallel_rank()]
|
||||
torch_bias_grad = torch_linear.bias.grad
|
||||
torch.testing.assert_close(linear.bias.grad,
|
||||
torch_bias_grad.to(get_accelerator().current_device()),
|
||||
atol=1e-3,
|
||||
rtol=1e-3)
|
||||
torch.testing.assert_close(linear.weight.grad,
|
||||
torch_grad.to(get_accelerator().current_device()),
|
||||
atol=1e-3,
|
||||
rtol=1e-3)
|
||||
torch.testing.assert_close(out, torch_out.to(get_accelerator().current_device()), atol=1e-2, rtol=1e-2)
|
||||
|
||||
|
||||
@pytest.mark.sequential
|
||||
@pytest.mark.parametrize("tp_size", [2, 4])
|
||||
@pytest.mark.parametrize("tp_overlap_comm", [True, False])
|
||||
class TestTpLayerFwdBwd(DistributedTest):
|
||||
world_size = 4
|
||||
reuse_dist_env = False
|
||||
|
||||
def testRowParallel(self, tp_size: int, tp_overlap_comm: bool):
|
||||
run_tp_layer_fwd_bwd(tp_size, tp_overlap_comm, column_parallel=False)
|
||||
|
||||
def testColumnParallel(self, tp_size: int, tp_overlap_comm: bool):
|
||||
run_tp_layer_fwd_bwd(tp_size, tp_overlap_comm, column_parallel=True)
|
||||
|
||||
|
||||
# @pytest.mark.sequential
|
||||
class TestParamsGather(DistributedTest):
|
||||
world_size = 4
|
||||
reuse_dist_env = False
|
||||
|
||||
@pytest.mark.parametrize("layer_type", ["linear", "linearallreduce"])
|
||||
def test(self, layer_type):
|
||||
skip_on_device()
|
||||
tp_size = 4
|
||||
hidden_dim = 128
|
||||
config_dict = {
|
||||
"train_micro_batch_size_per_gpu": 1,
|
||||
"optimizer": {
|
||||
"type": "Adam",
|
||||
"params": {
|
||||
"lr": 1e-6
|
||||
}
|
||||
},
|
||||
"tensor_parallel": {
|
||||
"autotp_size": tp_size,
|
||||
"partition_config": {
|
||||
"use_default_specs": False,
|
||||
"layer_specs": [{
|
||||
"patterns": [".*\\.weight$"],
|
||||
"partition_type": "skip",
|
||||
}],
|
||||
}
|
||||
},
|
||||
"zero_optimization": {
|
||||
"stage": 0,
|
||||
}
|
||||
}
|
||||
if preferred_dtype() is torch.float16:
|
||||
config_dict["fp16"] = {"enabled": True}
|
||||
elif preferred_dtype() is torch.bfloat16:
|
||||
config_dict["bf16"] = {"enabled": True}
|
||||
|
||||
torch.manual_seed(42)
|
||||
model = SequentialLinearModel(hidden_dim=hidden_dim)
|
||||
model, _, _, _ = deepspeed.initialize(model=model, model_parameters=model.parameters(), config=config_dict)
|
||||
|
||||
torch_linear = nn.Linear(hidden_dim, hidden_dim, dtype=preferred_dtype(), device="cpu")
|
||||
total_params = sum(p.numel() for p in torch_linear.parameters())
|
||||
tp_layer = None
|
||||
if layer_type == "linear":
|
||||
tp_layer = LinearLayer(deepcopy(torch_linear), groups.get_tensor_model_parallel_group())
|
||||
elif layer_type == "linearallreduce":
|
||||
tp_layer = LinearAllreduce(deepcopy(torch_linear), groups.get_tensor_model_parallel_group())
|
||||
else:
|
||||
raise ValueError(f"Invalid linear type: {config_dict['linear_type']}")
|
||||
|
||||
tp_params = sum(p.numel() for p in tp_layer.parameters())
|
||||
|
||||
expected_tp_params = 0
|
||||
# compute expected TP params:
|
||||
# - column-parallel (LinearLayer): weight & bias both split => total // tp_size
|
||||
# - row-parallel (LinearAllreduce): weight split, bias (1d tensors) replicated
|
||||
if layer_type == "linearallreduce":
|
||||
weight_params = torch_linear.weight.numel()
|
||||
bias_params = torch_linear.bias.numel()
|
||||
expected_tp_params = weight_params // tp_size + bias_params
|
||||
else:
|
||||
expected_tp_params = total_params // tp_size
|
||||
assert expected_tp_params == tp_params, (
|
||||
f"{layer_type}: expected {expected_tp_params} tp params, got {tp_params}")
|
||||
|
||||
for name, param in tp_layer.named_parameters(recurse=False):
|
||||
if is_model_parallel_parameter(param):
|
||||
param.gather_params([param])
|
||||
|
||||
torch_linear = torch_linear.to(get_accelerator().current_device())
|
||||
is_same_weights = all(
|
||||
torch.equal(param1, param2) for param1, param2 in zip(tp_layer.parameters(), torch_linear.parameters()))
|
||||
|
||||
assert is_same_weights
|
||||
|
||||
params1 = sum(p.numel() for p in tp_layer.parameters())
|
||||
assert total_params == params1
|
||||
|
||||
for name, param in tp_layer.named_parameters(recurse=False):
|
||||
if is_model_parallel_parameter(param):
|
||||
param._tp_partition([param])
|
||||
|
||||
tp_params2 = sum(p.numel() for p in tp_layer.parameters())
|
||||
|
||||
assert expected_tp_params == tp_params2
|
||||
|
||||
|
||||
def dummy_init_engine(config):
|
||||
# This is a dummy initialization function for the DeepSpeed engine.
|
||||
# We only need to use the config to initialize the distributed settings for the test.
|
||||
# Add default partition_config for simple test models if not provided
|
||||
if "tensor_parallel" in config and "partition_config" not in config["tensor_parallel"]:
|
||||
config["tensor_parallel"]["partition_config"] = {
|
||||
"use_default_specs": False,
|
||||
"layer_specs": [{
|
||||
"patterns": [".*\\.weight$"],
|
||||
"partition_type": "skip",
|
||||
}],
|
||||
}
|
||||
model = SequentialLinearModel(hidden_dim=8)
|
||||
model, _, _, _ = deepspeed.initialize(model=model, model_parameters=model.parameters(), config=config)
|
||||
|
||||
|
||||
def prepare_tp_model(hidden_dim, nlayers, linear_indices, allreduce_indices, group, return_global_copy=False):
|
||||
model = SequentialLinearModel(hidden_dim=hidden_dim, nlayers=nlayers).to(preferred_dtype())
|
||||
base_model = None
|
||||
if return_global_copy:
|
||||
base_model = deepcopy(model)
|
||||
for i in linear_indices:
|
||||
layer = LinearLayer(model.linears[i], group)
|
||||
model.linears[i] = layer
|
||||
|
||||
for i in allreduce_indices:
|
||||
layer = LinearAllreduce(model.linears[i], group)
|
||||
model.linears[i] = layer
|
||||
|
||||
return model, base_model
|
||||
|
||||
|
||||
@pytest.mark.parametrize("zero_stage", [0, 1, 2])
|
||||
@pytest.mark.parametrize("tp_size", [2, 4])
|
||||
class TestSave(DistributedTest):
|
||||
|
||||
world_size = 4
|
||||
reuse_dist_env = False
|
||||
|
||||
def test_save_original_weight(self, tp_size: int, zero_stage: int):
|
||||
skip_on_device()
|
||||
hidden_dim = 64
|
||||
config_dict = {
|
||||
"train_micro_batch_size_per_gpu": 1,
|
||||
"steps_per_print": 1,
|
||||
"optimizer": {
|
||||
"type": "Adam",
|
||||
"params": {
|
||||
"lr": 1e-6
|
||||
}
|
||||
},
|
||||
"tensor_parallel": {
|
||||
"autotp_size": tp_size
|
||||
},
|
||||
"zero_optimization": {
|
||||
"stage": zero_stage,
|
||||
}
|
||||
}
|
||||
if preferred_dtype() is torch.float16:
|
||||
config_dict["fp16"] = {"enabled": True}
|
||||
elif preferred_dtype() is torch.bfloat16:
|
||||
config_dict["bf16"] = {"enabled": True}
|
||||
dummy_init_engine(config_dict)
|
||||
torch.manual_seed(42)
|
||||
|
||||
model, base_model = prepare_tp_model(hidden_dim,
|
||||
8, [2, 5], [3, 6],
|
||||
groups.get_tensor_model_parallel_group(),
|
||||
return_global_copy=True)
|
||||
model, _, _, _ = deepspeed.initialize(model=model, model_parameters=model.parameters(), config=config_dict)
|
||||
|
||||
cur_params_numel = sum(p.numel() for p in model.parameters())
|
||||
base_params_numel = sum(p.numel() for p in base_model.parameters())
|
||||
assert cur_params_numel < base_params_numel
|
||||
|
||||
tp_state_dict = model._consolidated_16bit_state_dict()
|
||||
|
||||
def compare_state_dicts(state_dict1, state_dict2):
|
||||
if state_dict1.keys() != state_dict2.keys():
|
||||
print("The state_dicts have different keys!")
|
||||
return False
|
||||
|
||||
for key in state_dict1:
|
||||
if not torch.allclose(state_dict1[key], state_dict2[key], atol=1e-3):
|
||||
assert state_dict1[key].device == "cpu"
|
||||
print(f"Parameters for {key} are different!")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
base_state_dict = base_model.state_dict()
|
||||
if dist.get_rank() == 0:
|
||||
# we should consider the case when zero3 is used in the future.
|
||||
assert compare_state_dicts(base_state_dict, tp_state_dict), "State_dict is not the same!"
|
||||
else:
|
||||
assert tp_state_dict is None, "noly rank0 should have the state_dict"
|
||||
|
||||
def test_ckpt_save(self, tmpdir, tp_size: int, zero_stage: int):
|
||||
skip_on_device()
|
||||
hidden_dim = 64
|
||||
config_dict = {
|
||||
"train_micro_batch_size_per_gpu": 1,
|
||||
"steps_per_print": 1,
|
||||
"optimizer": {
|
||||
"type": "Adam",
|
||||
"params": {
|
||||
"lr": 1e-3
|
||||
}
|
||||
},
|
||||
"zero_optimization": {
|
||||
"stage": zero_stage,
|
||||
},
|
||||
"tensor_parallel": {
|
||||
"autotp_size": tp_size
|
||||
},
|
||||
"scheduler": {
|
||||
"type": "WarmupLR",
|
||||
"params": {
|
||||
"warmup_min_lr": 0,
|
||||
"warmup_max_lr": 0.001,
|
||||
"warmup_num_steps": 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if preferred_dtype() is torch.float16:
|
||||
config_dict["fp16"] = {"enabled": True}
|
||||
elif preferred_dtype() is torch.bfloat16:
|
||||
config_dict["bf16"] = {"enabled": True}
|
||||
|
||||
dummy_init_engine(config_dict)
|
||||
|
||||
trained_model, _ = prepare_tp_model(hidden_dim, 8, [2, 5], [3, 6], groups.get_tensor_model_parallel_group())
|
||||
loaded_model, _ = prepare_tp_model(hidden_dim, 8, [2, 5], [3, 6], groups.get_tensor_model_parallel_group())
|
||||
|
||||
trained_model, _, _, _ = deepspeed.initialize(model=trained_model,
|
||||
model_parameters=trained_model.parameters(),
|
||||
config=config_dict)
|
||||
torch.manual_seed(42)
|
||||
|
||||
data_loader = random_dataloader(model=trained_model,
|
||||
total_samples=3,
|
||||
hidden_dim=hidden_dim,
|
||||
device=trained_model.device,
|
||||
dtype=preferred_dtype())
|
||||
ckpt_path = os.path.join(tmpdir, 'tp_saved_checkpoint')
|
||||
for i, batch in enumerate(data_loader):
|
||||
batch[0].requires_grad = True
|
||||
loss = trained_model(batch[0], batch[1])
|
||||
loss = loss
|
||||
trained_model.backward(loss)
|
||||
trained_model.step()
|
||||
trained_model.save_checkpoint(ckpt_path)
|
||||
|
||||
loaded_model, _, _, _ = deepspeed.initialize(model=loaded_model,
|
||||
model_parameters=loaded_model.parameters(),
|
||||
config=config_dict)
|
||||
loaded_model.load_checkpoint(ckpt_path, load_optimizer_states=True, load_lr_scheduler_states=True)
|
||||
compare_optimizer_states(trained_model, loaded_model, hidden_dim, fp16=(preferred_dtype() == torch.float16))
|
||||
compare_lr_scheduler_states(trained_model, loaded_model)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("zero_stage", [0, 1, 2])
|
||||
@pytest.mark.parametrize("tp_size", [2, 4])
|
||||
class TestTpGradNorm(DistributedTest):
|
||||
|
||||
world_size = 4
|
||||
reuse_dist_env = False
|
||||
|
||||
def test(self, tp_size: int, zero_stage: int):
|
||||
skip_on_device()
|
||||
hidden_dim = 64
|
||||
config_dict = {
|
||||
"train_micro_batch_size_per_gpu": 1,
|
||||
"steps_per_print": 1,
|
||||
"optimizer": {
|
||||
"type": "Adam",
|
||||
"params": {
|
||||
"lr": 1e-6
|
||||
}
|
||||
},
|
||||
"tensor_parallel": {
|
||||
"autotp_size": tp_size
|
||||
},
|
||||
"zero_optimization": {
|
||||
"stage": zero_stage,
|
||||
}
|
||||
}
|
||||
if preferred_dtype() is torch.float16:
|
||||
config_dict["fp16"] = {"enabled": True}
|
||||
elif preferred_dtype() is torch.bfloat16:
|
||||
if zero_stage == 0:
|
||||
pytest.skip(
|
||||
"This test has an overflow data and needs to implement an overflow skip mechanism in BF16_optimizer"
|
||||
)
|
||||
config_dict["bf16"] = {"enabled": True}
|
||||
|
||||
torch.manual_seed(42)
|
||||
|
||||
dummy_init_engine(config=config_dict)
|
||||
tp_model, base_model = prepare_tp_model(hidden_dim,
|
||||
8, [2, 5], [3, 6],
|
||||
groups.get_tensor_model_parallel_group(),
|
||||
return_global_copy=True)
|
||||
|
||||
base_model, base_optimizer, _, _ = deepspeed.initialize(model=base_model,
|
||||
model_parameters=base_model.parameters(),
|
||||
config=config_dict)
|
||||
data_loader = random_dataloader(model=base_model,
|
||||
total_samples=20,
|
||||
hidden_dim=hidden_dim,
|
||||
device=base_model.device,
|
||||
dtype=preferred_dtype())
|
||||
|
||||
for i, batch in enumerate(data_loader):
|
||||
batch[0].requires_grad = True
|
||||
loss = base_model(batch[0], batch[1])
|
||||
loss = loss
|
||||
base_model.backward(loss)
|
||||
base_model.step()
|
||||
|
||||
base_norm = base_optimizer._global_grad_norm
|
||||
|
||||
base_model.destroy()
|
||||
|
||||
tp_model, tp_optimizer, _, _ = deepspeed.initialize(model=tp_model,
|
||||
model_parameters=tp_model.parameters(),
|
||||
config=config_dict)
|
||||
for i, batch in enumerate(data_loader):
|
||||
batch[0].requires_grad = True
|
||||
loss = tp_model(batch[0], batch[1])
|
||||
loss = loss
|
||||
tp_model.backward(loss)
|
||||
tp_model.step()
|
||||
|
||||
tp_norm = tp_optimizer._global_grad_norm
|
||||
|
||||
assert math.isclose(base_norm, tp_norm, abs_tol=1e-3), f"base_norm: {base_norm}, tp_norm: {tp_norm}"
|
||||
tp_params_numel = sum(p.numel() for p in tp_model.parameters())
|
||||
base_params_numel = sum(p.numel() for p in base_model.parameters())
|
||||
assert tp_params_numel < base_params_numel, f"tp_params_numel: {tp_params_numel}, base_params_numel: {base_params_numel}"
|
||||
@@ -0,0 +1,177 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# DeepSpeed Team
|
||||
|
||||
import os
|
||||
import torch
|
||||
import deepspeed
|
||||
import pytest
|
||||
import random
|
||||
import numpy as np
|
||||
import deepspeed.comm as dist
|
||||
from deepspeed.accelerator import get_accelerator
|
||||
from unit.common import DistributedTest, DistributedFixture
|
||||
from unit.megatron_model import get_gpt2_model, get_megatron_version
|
||||
from deepspeed.utils.torch import required_torch_version
|
||||
|
||||
pytestmark = pytest.mark.skipif(not required_torch_version(min_version=1.5, max_version=1.13),
|
||||
reason='Megatron-LM package requires Pytorch version >=1.5 and <=1.13')
|
||||
|
||||
|
||||
# TODO: integrated testing of TP and ZeRO 1/2/3
|
||||
def get_deepspeed_model(model):
|
||||
ds_config_dict = {
|
||||
"train_micro_batch_size_per_gpu": 1,
|
||||
"optimizer": {
|
||||
"type": "Lamb",
|
||||
"params": {
|
||||
"lr": 0.00015
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
from megatron import mpu
|
||||
model, _, _, _ = deepspeed.initialize(model=model,
|
||||
mpu=mpu,
|
||||
model_parameters=model.parameters(),
|
||||
config=ds_config_dict)
|
||||
return model
|
||||
|
||||
|
||||
class ConfigurableMP(DistributedTest):
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_random(self, seed=1234):
|
||||
random.seed(seed)
|
||||
np.random.seed(seed)
|
||||
torch.manual_seed(seed)
|
||||
get_accelerator().manual_seed_all(seed)
|
||||
|
||||
@pytest.fixture
|
||||
def inputs(self, bs=1, seq_len=20):
|
||||
input_ids = torch.randint(low=0, high=1000, size=(bs, seq_len))
|
||||
position_ids = torch.randint(low=0, high=2, size=(bs, seq_len))
|
||||
attention_mask = torch.randint(low=0, high=2, size=(bs, seq_len), dtype=torch.bool)
|
||||
return [input_ids, position_ids, attention_mask]
|
||||
|
||||
|
||||
class TestConfigurableMP(ConfigurableMP):
|
||||
|
||||
@pytest.mark.world_size(1)
|
||||
@pytest.mark.skip(reason="megatron-lm is currently broken so this test cannot be run.")
|
||||
def test_gpt2_basic(self, tmpdir, inputs):
|
||||
args_defaults = {
|
||||
'num_layers': 2,
|
||||
'hidden_size': 128,
|
||||
'num_attention_heads': 8,
|
||||
'max_position_embeddings': 128,
|
||||
}
|
||||
|
||||
model = get_gpt2_model(args_defaults)
|
||||
model = get_deepspeed_model(model)
|
||||
|
||||
model.eval()
|
||||
device_name = get_accelerator().device_name()
|
||||
baseline = model(inputs[0].to(device_name), inputs[1].to(device_name), inputs[2].to(device_name))
|
||||
|
||||
tag = 'mp_1'
|
||||
state_dict = {}
|
||||
state_dict['checkpoint_version'] = get_megatron_version()
|
||||
model.save_checkpoint(tmpdir, tag=tag, client_state=state_dict)
|
||||
dist.barrier()
|
||||
model.load_checkpoint(tmpdir, tag=tag, load_optimizer_states=False, load_lr_scheduler_states=False)
|
||||
|
||||
test = model(inputs[0], inputs[1], inputs[2])
|
||||
assert torch.allclose(baseline, test,
|
||||
atol=1e-07), f"Baseline output {baseline} is not equal to save-then-load output {test}"
|
||||
|
||||
@pytest.mark.world_size(2)
|
||||
@pytest.mark.skip(reason="megatron-lm is currently broken so this test cannot be run.")
|
||||
def test_gpt2_mp2_no_resize(self, tmpdir, inputs):
|
||||
args_defaults = {
|
||||
'num_layers': 2,
|
||||
'hidden_size': 128,
|
||||
'num_attention_heads': 8,
|
||||
'max_position_embeddings': 128,
|
||||
}
|
||||
|
||||
model = get_gpt2_model(args_defaults, mp_size=2)
|
||||
model = get_deepspeed_model(model)
|
||||
|
||||
model.eval()
|
||||
|
||||
device_name = get_accelerator().device_name()
|
||||
baseline = model(inputs[0].to(device_name), inputs[1].to(device_name), inputs[2].to(device_name))
|
||||
|
||||
tag = 'mp_2'
|
||||
state_dict = {}
|
||||
state_dict['checkpoint_version'] = get_megatron_version()
|
||||
model.save_checkpoint(tmpdir, tag=tag, client_state=state_dict)
|
||||
dist.barrier()
|
||||
model.load_checkpoint(tmpdir, tag=tag, load_optimizer_states=False, load_lr_scheduler_states=False)
|
||||
|
||||
device_name = get_accelerator().device_name()
|
||||
test = model(inputs[0].to(device_name), inputs[1].to(device_name), inputs[2].to(device_name))
|
||||
assert torch.allclose(baseline, test, rtol=1.0,
|
||||
atol=1e-07), f"Baseline output {baseline} is not equal to save-then-load output {test}"
|
||||
|
||||
|
||||
# This fixture provides the baseline model with mp=2 to TestConfigurableMPResize
|
||||
class baseline_mp2(DistributedFixture):
|
||||
world_size = 2
|
||||
|
||||
def run(self, inputs, class_tmpdir):
|
||||
args_defaults = {
|
||||
'num_layers': 2,
|
||||
'hidden_size': 128,
|
||||
'num_attention_heads': 8,
|
||||
'max_position_embeddings': 128,
|
||||
}
|
||||
|
||||
model = get_gpt2_model(args_defaults, mp_size=self.world_size)
|
||||
model = get_deepspeed_model(model)
|
||||
|
||||
model.eval()
|
||||
|
||||
with torch.no_grad():
|
||||
device_name = get_accelerator().device_name()
|
||||
baseline = model(inputs[0].to(device_name), inputs[1].to(device_name), inputs[2].to(device_name))
|
||||
if dist.get_rank() == 0:
|
||||
save_path = os.path.join(class_tmpdir, "output.pt")
|
||||
torch.save(baseline.cpu(), save_path)
|
||||
|
||||
state_dict = {}
|
||||
state_dict['checkpoint_version'] = get_megatron_version()
|
||||
model.save_checkpoint(class_tmpdir, client_state=state_dict)
|
||||
|
||||
|
||||
class TestConfigurableResizeMP(ConfigurableMP):
|
||||
world_size = [1, 4]
|
||||
|
||||
@pytest.mark.skip(reason="megatron-lm is currently broken so this test cannot be run.")
|
||||
def test(self, baseline_mp2, inputs, class_tmpdir):
|
||||
args_defaults = {
|
||||
'num_layers': 2,
|
||||
'hidden_size': 128,
|
||||
'num_attention_heads': 8,
|
||||
'max_position_embeddings': 128,
|
||||
}
|
||||
|
||||
world_size = os.environ["WORLD_SIZE"]
|
||||
model = get_gpt2_model(args_defaults, mp_size=world_size)
|
||||
model = get_deepspeed_model(model)
|
||||
|
||||
model.eval()
|
||||
|
||||
with torch.no_grad():
|
||||
model.load_checkpoint(class_tmpdir, load_optimizer_states=False, load_lr_scheduler_states=False)
|
||||
device_name = get_accelerator().device_name()
|
||||
test = model(inputs[0].to(device_name), inputs[1].to(device_name), inputs[2].to(device_name))
|
||||
if dist.get_rank() == 0:
|
||||
load_path = os.path.join(class_tmpdir, "output.pt")
|
||||
baseline = torch.load(load_path, weights_only=False)
|
||||
test = test.cpu()
|
||||
assert torch.allclose(
|
||||
baseline, test,
|
||||
atol=1e-03), f"Baseline output {baseline} is not equal to save-then-load output {test}"
|
||||
@@ -0,0 +1,267 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# DeepSpeed Team
|
||||
|
||||
import os
|
||||
import torch
|
||||
import deepspeed
|
||||
import pytest
|
||||
import random
|
||||
import numpy as np
|
||||
import deepspeed.comm as dist
|
||||
from unit.common import DistributedTest, DistributedFixture
|
||||
from unit.megatron_model import get_megatron_version
|
||||
from unit.megatron_model import MockGPT2ModelPipe as GPT2ModelPipe
|
||||
from deepspeed.utils import RepeatingLoader
|
||||
from deepspeed.accelerator import get_accelerator
|
||||
from deepspeed.utils.torch import required_torch_version
|
||||
|
||||
pytestmark = pytest.mark.skipif(not required_torch_version(min_version=1.5, max_version=1.13),
|
||||
reason='Megatron-LM package requires Pytorch version >=1.5 and <=1.13')
|
||||
|
||||
|
||||
def get_deepspeed_model(model):
|
||||
ds_config_dict = {
|
||||
"train_micro_batch_size_per_gpu": 1,
|
||||
"optimizer": {
|
||||
"type": "Lamb",
|
||||
"params": {
|
||||
"lr": 0.00015
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
model, _, _, _ = deepspeed.initialize(model=model, model_parameters=model.parameters(), config=ds_config_dict)
|
||||
return model.to(get_accelerator().device_name())
|
||||
|
||||
|
||||
def get_topology(mp, pp, world_size):
|
||||
assert world_size % (pp * mp) == 0
|
||||
dp = world_size // (pp * mp)
|
||||
|
||||
from deepspeed.runtime.pipe.topology import PipeModelDataParallelTopology
|
||||
topo = PipeModelDataParallelTopology(num_pp=pp, num_mp=mp, num_dp=dp)
|
||||
|
||||
return topo
|
||||
|
||||
|
||||
class ConfigurablePP(DistributedTest):
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_random(self, seed=1234):
|
||||
random.seed(seed)
|
||||
np.random.seed(seed)
|
||||
torch.manual_seed(seed)
|
||||
get_accelerator().manual_seed_all(seed)
|
||||
|
||||
@pytest.fixture
|
||||
def inputs(self, bs=1, seq_len=1, hidden_size=128):
|
||||
hidden_states = torch.randn(bs, seq_len, hidden_size)
|
||||
attention_mask = torch.randint(low=0, high=2, size=(bs, seq_len), dtype=torch.bool)
|
||||
return (hidden_states, attention_mask)
|
||||
|
||||
|
||||
class TestConfigurablePP(ConfigurablePP):
|
||||
mp_size = 2
|
||||
pp_size = 2
|
||||
world_size = 4 # mp_size * pp_size
|
||||
|
||||
@pytest.mark.skip(reason="megatron-lm is currently broken so this test cannot be run.")
|
||||
def test_pp_basic(self, inputs, tmpdir):
|
||||
# basic test case, mp_size=2, pp_size=2, verify ckpt saving/loading.
|
||||
args_defaults = {
|
||||
'num_layers': 8,
|
||||
'hidden_size': 128,
|
||||
'num_attention_heads': 8,
|
||||
'max_position_embeddings': 128,
|
||||
}
|
||||
mp_size = self.mp_size
|
||||
pp_size = self.pp_size
|
||||
world_size = self.world_size
|
||||
|
||||
topo = get_topology(mp_size, pp_size, world_size)
|
||||
gpt2_pipe_model = GPT2ModelPipe(num_layers=8,
|
||||
num_stages=pp_size,
|
||||
mp_size=mp_size,
|
||||
args_others=args_defaults,
|
||||
topo=topo)
|
||||
model = get_deepspeed_model(gpt2_pipe_model)
|
||||
|
||||
tag = 'pp_basic'
|
||||
state_dict = {}
|
||||
state_dict['checkpoint_version'] = get_megatron_version()
|
||||
model.save_checkpoint(tmpdir, tag=tag, client_state=state_dict)
|
||||
|
||||
if model.is_first_stage() or model.is_last_stage():
|
||||
loader = RepeatingLoader([(inputs[0], 0)])
|
||||
data_iter = iter(loader)
|
||||
else:
|
||||
data_iter = None
|
||||
|
||||
baseline = model.eval_batch(data_iter=data_iter, compute_loss=False, reduce_output=None)
|
||||
|
||||
dist.barrier()
|
||||
model.load_checkpoint(tmpdir, tag=tag, load_optimizer_states=False, load_lr_scheduler_states=False)
|
||||
dist.barrier()
|
||||
|
||||
test = model.eval_batch(data_iter=data_iter, compute_loss=False, reduce_output=None)
|
||||
|
||||
if test is not None:
|
||||
assert len(baseline) == len(test)
|
||||
# Compare outputs of each microbatch
|
||||
for mb in range(len(baseline)):
|
||||
for b, t in zip(baseline[mb], test[mb]):
|
||||
if b.is_floating_point(): # don't compare masks
|
||||
assert torch.allclose(
|
||||
b, t,
|
||||
atol=1e-07), f"Baseline output {baseline} is not equal to save-then-load output {test}"
|
||||
|
||||
|
||||
# Fixture for defining the checkpoint path since all tests in
|
||||
# TestConfigurableResizePP will use the same tmpdir
|
||||
@pytest.fixture
|
||||
def checkpoint_tag(mp_size, pp_size, mp_resize, pp_resize):
|
||||
return f"{mp_size}-{pp_size}-{mp_resize}-{pp_resize}"
|
||||
|
||||
|
||||
# Base class for creating / saving model output for baseline models. This is
|
||||
# not meant to be used directly as a fixture to any classes
|
||||
class _baseline(DistributedFixture):
|
||||
world_size = None
|
||||
|
||||
def run(self, inputs, class_tmpdir, checkpoint_tag, mp_size, pp_size):
|
||||
assert int(os.environ["WORLD_SIZE"]) == (pp_size *
|
||||
mp_size), "world size does not match provided pp_size and mp_size"
|
||||
args_defaults = {
|
||||
'num_layers': 8,
|
||||
'hidden_size': 128,
|
||||
'num_attention_heads': 8,
|
||||
'max_position_embeddings': 128,
|
||||
}
|
||||
|
||||
topo = get_topology(mp_size, pp_size, mp_size * pp_size)
|
||||
gpt2_pipe_model = GPT2ModelPipe(num_layers=8,
|
||||
num_stages=pp_size,
|
||||
mp_size=mp_size,
|
||||
args_others=args_defaults,
|
||||
topo=topo)
|
||||
model = get_deepspeed_model(gpt2_pipe_model)
|
||||
|
||||
with torch.no_grad():
|
||||
inputs = [x.to(get_accelerator().device_name()) for x in inputs]
|
||||
if model.is_first_stage() or model.is_last_stage():
|
||||
loader = RepeatingLoader([(inputs[0], 0)])
|
||||
data_iter = iter(loader)
|
||||
else:
|
||||
data_iter = None
|
||||
|
||||
baseline = model.eval_batch(data_iter=data_iter, compute_loss=False, reduce_output=None)
|
||||
|
||||
if baseline is not None:
|
||||
# baseline should be [[hidden, True]]]
|
||||
assert len(baseline) == 1
|
||||
assert len(baseline[0]) == 1
|
||||
assert torch.is_tensor(baseline[0][0])
|
||||
save_path = os.path.join(class_tmpdir, f"output-{checkpoint_tag}.pt")
|
||||
torch.save(baseline[0][0].cpu(), save_path)
|
||||
|
||||
state_dict = {}
|
||||
state_dict['checkpoint_version'] = get_megatron_version()
|
||||
model.save_checkpoint(class_tmpdir, tag=checkpoint_tag, client_state=state_dict)
|
||||
|
||||
|
||||
# This may look odd, but there is a limitation with DistributedFixture that
|
||||
# doesn't allow us to reuse a fixture with different worldsizes. This could be
|
||||
# implemented in conftest.py::pytest_fixture_setup and common.py::DistributedFixture
|
||||
class baseline_ws1(_baseline):
|
||||
world_size = 1
|
||||
|
||||
|
||||
class baseline_ws2(_baseline):
|
||||
world_size = 2
|
||||
|
||||
|
||||
class baseline_ws4(_baseline):
|
||||
world_size = 4
|
||||
|
||||
|
||||
class TestConfigurableResizePP(ConfigurablePP):
|
||||
|
||||
def _test(self, inputs, class_tmpdir, checkpoint_tag, mp_size, pp_size, mp_resize, pp_resize):
|
||||
args_defaults = {
|
||||
'num_layers': 8,
|
||||
'hidden_size': 128,
|
||||
'num_attention_heads': 8,
|
||||
'max_position_embeddings': 128,
|
||||
}
|
||||
|
||||
topo = get_topology(mp_resize, pp_resize, mp_resize * pp_resize)
|
||||
gpt2_pipe_model = GPT2ModelPipe(num_layers=8,
|
||||
num_stages=pp_resize,
|
||||
mp_size=mp_resize,
|
||||
args_others=args_defaults,
|
||||
topo=topo)
|
||||
model = get_deepspeed_model(gpt2_pipe_model)
|
||||
|
||||
with torch.no_grad():
|
||||
model.load_checkpoint(class_tmpdir,
|
||||
tag=checkpoint_tag,
|
||||
load_optimizer_states=False,
|
||||
load_lr_scheduler_states=False)
|
||||
inputs = [x.to(get_accelerator().device_name()) for x in inputs]
|
||||
if model.is_first_stage() or model.is_last_stage():
|
||||
loader = RepeatingLoader([(inputs[0], 0)])
|
||||
data_iter = iter(loader)
|
||||
else:
|
||||
data_iter = None
|
||||
|
||||
test = model.eval_batch(data_iter=data_iter, compute_loss=False, reduce_output=None)
|
||||
|
||||
if test is not None:
|
||||
# test should be [[hidden, True]]]
|
||||
assert len(test) == 1
|
||||
assert len(test[0]) == 1
|
||||
assert torch.is_tensor(test[0][0])
|
||||
test = test[0][0].cpu()
|
||||
load_path = os.path.join(class_tmpdir, f"output-{checkpoint_tag}.pt")
|
||||
baseline = torch.load(load_path, weights_only=False)
|
||||
assert torch.allclose(
|
||||
baseline, test,
|
||||
atol=1e-03), f"Baseline output {baseline} is not equal to save-then-load output {test}"
|
||||
|
||||
# These tests are divided by baseline model worldsize and test model worldsize
|
||||
@pytest.mark.world_size(1)
|
||||
@pytest.mark.parametrize("mp_size, pp_size, mp_resize, pp_resize", [(1, 2, 1, 1)])
|
||||
@pytest.mark.skip(reason="megatron-lm is currently broken so this test cannot be run.")
|
||||
def test_world_size_2to1(self, inputs, class_tmpdir, checkpoint_tag, baseline_ws2, mp_size, pp_size, mp_resize,
|
||||
pp_resize):
|
||||
self._test(inputs, class_tmpdir, checkpoint_tag, mp_size, pp_size, mp_resize, pp_resize)
|
||||
|
||||
@pytest.mark.world_size(1)
|
||||
@pytest.mark.parametrize("mp_size, pp_size, mp_resize, pp_resize", [(2, 2, 1, 1)])
|
||||
@pytest.mark.skip(reason="megatron-lm is currently broken so this test cannot be run.")
|
||||
def test_world_size_4to1(self, inputs, class_tmpdir, checkpoint_tag, baseline_ws4, mp_size, pp_size, mp_resize,
|
||||
pp_resize):
|
||||
self._test(inputs, class_tmpdir, checkpoint_tag, mp_size, pp_size, mp_resize, pp_resize)
|
||||
|
||||
@pytest.mark.world_size(2)
|
||||
@pytest.mark.parametrize("mp_size, pp_size, mp_resize, pp_resize", [(2, 2, 2, 1)])
|
||||
@pytest.mark.skip(reason="megatron-lm is currently broken so this test cannot be run.")
|
||||
def test_world_size_4to2(self, inputs, class_tmpdir, checkpoint_tag, baseline_ws4, mp_size, pp_size, mp_resize,
|
||||
pp_resize):
|
||||
self._test(inputs, class_tmpdir, checkpoint_tag, mp_size, pp_size, mp_resize, pp_resize)
|
||||
|
||||
@pytest.mark.world_size(4)
|
||||
@pytest.mark.parametrize("mp_size, pp_size, mp_resize, pp_resize", [(1, 1, 2, 2)])
|
||||
@pytest.mark.skip(reason="megatron-lm is currently broken so this test cannot be run.")
|
||||
def test_world_size_1to4(self, inputs, class_tmpdir, checkpoint_tag, baseline_ws1, mp_size, pp_size, mp_resize,
|
||||
pp_resize):
|
||||
self._test(inputs, class_tmpdir, checkpoint_tag, mp_size, pp_size, mp_resize, pp_resize)
|
||||
|
||||
@pytest.mark.world_size(4)
|
||||
@pytest.mark.parametrize("mp_size, pp_size, mp_resize, pp_resize", [(1, 2, 1, 4), (2, 1, 2, 2)])
|
||||
@pytest.mark.skip(reason="megatron-lm is currently broken so this test cannot be run.")
|
||||
def test_world_size_2to4(self, inputs, class_tmpdir, checkpoint_tag, baseline_ws2, mp_size, pp_size, mp_resize,
|
||||
pp_resize):
|
||||
self._test(inputs, class_tmpdir, checkpoint_tag, mp_size, pp_size, mp_resize, pp_resize)
|
||||
@@ -0,0 +1,274 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# DeepSpeed Team
|
||||
|
||||
import torch
|
||||
import deepspeed.comm as dist
|
||||
import deepspeed
|
||||
from deepspeed.accelerator import get_accelerator
|
||||
from deepspeed.utils import groups
|
||||
from deepspeed.runtime.utils import is_model_parallel_parameter
|
||||
from unit.common import DistributedTest, preferred_dtype
|
||||
|
||||
|
||||
def skip_on_device():
|
||||
return
|
||||
|
||||
|
||||
class TestTPPlanEndToEnd(DistributedTest):
|
||||
world_size = 2
|
||||
|
||||
class SimpleHFModel(torch.nn.Module):
|
||||
|
||||
class Block(torch.nn.Module):
|
||||
|
||||
def __init__(self, hidden_size):
|
||||
super().__init__()
|
||||
self.q_proj = torch.nn.Linear(hidden_size, hidden_size * 2)
|
||||
self.o_proj = torch.nn.Linear(hidden_size * 2, hidden_size)
|
||||
|
||||
def forward(self, x):
|
||||
return self.o_proj(self.q_proj(x))
|
||||
|
||||
def __init__(self, hidden_size=64):
|
||||
super().__init__()
|
||||
self.hidden_size = hidden_size
|
||||
self.config = type(
|
||||
"Config",
|
||||
(),
|
||||
{"base_model_tp_plan": {
|
||||
"*.q_proj": "colwise",
|
||||
"*.o_proj": "rowwise",
|
||||
}},
|
||||
)()
|
||||
self.layers = torch.nn.ModuleList([self.Block(hidden_size)])
|
||||
|
||||
def forward(self, x):
|
||||
return self.layers[0](x)
|
||||
|
||||
def _setup_baseline_linears(self, model):
|
||||
torch_q = torch.nn.Linear(model.hidden_size, model.hidden_size * 2)
|
||||
torch_o = torch.nn.Linear(model.hidden_size * 2, model.hidden_size)
|
||||
torch_q.load_state_dict(model.layers[0].q_proj.state_dict(), strict=True)
|
||||
torch_o.load_state_dict(model.layers[0].o_proj.state_dict(), strict=True)
|
||||
|
||||
if preferred_dtype() == torch.float16:
|
||||
torch_q = torch_q.half()
|
||||
torch_o = torch_o.half()
|
||||
elif preferred_dtype() == torch.bfloat16:
|
||||
torch_q = torch_q.bfloat16()
|
||||
torch_o = torch_o.bfloat16()
|
||||
|
||||
device = get_accelerator().current_device_name()
|
||||
torch_q = torch_q.to(device)
|
||||
torch_o = torch_o.to(device)
|
||||
return torch_q, torch_o
|
||||
|
||||
def _compare_tp_gradients(self, model, torch_q, torch_o, input_tensor, engine):
|
||||
|
||||
def _get_grad(param):
|
||||
if param.grad is not None:
|
||||
return param.grad
|
||||
return getattr(param, "grad_accum", None)
|
||||
|
||||
torch_q.zero_grad(set_to_none=True)
|
||||
torch_o.zero_grad(set_to_none=True)
|
||||
torch_q_out = torch_q(input_tensor)
|
||||
torch_o_out = torch_o(torch_q_out)
|
||||
torch_loss = torch_o_out.sum()
|
||||
torch_loss.backward()
|
||||
|
||||
output = engine(input_tensor)
|
||||
loss = output.sum()
|
||||
engine.backward(loss)
|
||||
|
||||
tp_rank = groups.get_tensor_model_parallel_rank()
|
||||
tp_size = engine.autotp_size()
|
||||
q_proj = model.layers[0].q_proj
|
||||
o_proj = model.layers[0].o_proj
|
||||
|
||||
torch_q_grad = torch.chunk(torch_q.weight.grad, tp_size, dim=0)[tp_rank]
|
||||
torch_q_bias_grad = torch.chunk(torch_q.bias.grad, tp_size, dim=0)[tp_rank]
|
||||
torch_o_grad = torch.chunk(torch_o.weight.grad, tp_size, dim=1)[tp_rank]
|
||||
|
||||
q_weight_grad = _get_grad(q_proj.weight)
|
||||
q_bias_grad = _get_grad(q_proj.bias) if q_proj.bias is not None else None
|
||||
o_weight_grad = _get_grad(o_proj.weight)
|
||||
|
||||
torch.testing.assert_close(q_weight_grad, torch_q_grad, atol=2e-2, rtol=2e-2)
|
||||
if q_bias_grad is not None:
|
||||
torch.testing.assert_close(q_bias_grad, torch_q_bias_grad, atol=2e-2, rtol=2e-2)
|
||||
torch.testing.assert_close(o_weight_grad, torch_o_grad, atol=2e-2, rtol=2e-2)
|
||||
|
||||
def _gather_and_compare_params(self, model, torch_q, torch_o, compare_values=True):
|
||||
q_proj = model.layers[0].q_proj
|
||||
o_proj = model.layers[0].o_proj
|
||||
|
||||
original_shards = []
|
||||
for _, param in q_proj.named_parameters(recurse=False):
|
||||
if is_model_parallel_parameter(param):
|
||||
original_shards.append((param, param.data.detach().clone()))
|
||||
|
||||
for _, param in o_proj.named_parameters(recurse=False):
|
||||
if is_model_parallel_parameter(param):
|
||||
original_shards.append((param, param.data.detach().clone()))
|
||||
|
||||
for param, _ in original_shards:
|
||||
param.gather_params([param])
|
||||
|
||||
if compare_values:
|
||||
torch.testing.assert_close(q_proj.weight, torch_q.weight, atol=2e-2, rtol=2e-2)
|
||||
if q_proj.bias is not None:
|
||||
torch.testing.assert_close(q_proj.bias, torch_q.bias, atol=2e-2, rtol=2e-2)
|
||||
torch.testing.assert_close(o_proj.weight, torch_o.weight, atol=2e-2, rtol=2e-2)
|
||||
if o_proj.bias is not None:
|
||||
torch.testing.assert_close(o_proj.bias, torch_o.bias, atol=2e-2, rtol=2e-2)
|
||||
|
||||
for param, original in original_shards:
|
||||
param._tp_partition([param])
|
||||
torch.testing.assert_close(param.data, original, atol=2e-2, rtol=2e-2)
|
||||
|
||||
def test_tp_plan_basic_training(self):
|
||||
skip_on_device()
|
||||
|
||||
model = self.SimpleHFModel()
|
||||
if preferred_dtype() == torch.float16:
|
||||
model = model.half()
|
||||
elif preferred_dtype() == torch.bfloat16:
|
||||
model = model.bfloat16()
|
||||
|
||||
torch_q, torch_o = self._setup_baseline_linears(model)
|
||||
|
||||
ds_config = {
|
||||
"train_micro_batch_size_per_gpu": 1,
|
||||
"tensor_parallel": {
|
||||
"autotp_size": 2
|
||||
},
|
||||
"optimizer": {
|
||||
"type": "AdamW",
|
||||
"params": {
|
||||
"lr": 1e-4
|
||||
}
|
||||
},
|
||||
"zero_optimization": {
|
||||
"stage": 0
|
||||
},
|
||||
"steps_per_print": 1,
|
||||
}
|
||||
|
||||
if preferred_dtype() == torch.float16:
|
||||
ds_config["fp16"] = {"enabled": True}
|
||||
elif preferred_dtype() == torch.bfloat16:
|
||||
ds_config["bf16"] = {"enabled": True}
|
||||
|
||||
engine, _, _, _ = deepspeed.initialize(model=model, model_parameters=model.parameters(), config=ds_config)
|
||||
|
||||
assert engine.autotp_size() == 2
|
||||
|
||||
input_tensor = torch.randn(2, 4, 64, dtype=preferred_dtype()).to(get_accelerator().current_device_name())
|
||||
dist.broadcast(
|
||||
input_tensor,
|
||||
src=groups.get_tensor_model_parallel_src_rank(),
|
||||
group=groups.get_tensor_model_parallel_group(),
|
||||
)
|
||||
if preferred_dtype() == torch.float16:
|
||||
torch_q = torch_q.half()
|
||||
torch_o = torch_o.half()
|
||||
elif preferred_dtype() == torch.bfloat16:
|
||||
torch_q = torch_q.bfloat16()
|
||||
torch_o = torch_o.bfloat16()
|
||||
|
||||
self._compare_tp_gradients(model, torch_q, torch_o, input_tensor, engine)
|
||||
|
||||
def test_tp_plan_with_zero1(self):
|
||||
skip_on_device()
|
||||
|
||||
model = self.SimpleHFModel()
|
||||
torch_q, torch_o = self._setup_baseline_linears(model)
|
||||
|
||||
ds_config = {
|
||||
"train_micro_batch_size_per_gpu": 1,
|
||||
"tensor_parallel": {
|
||||
"autotp_size": 2
|
||||
},
|
||||
"optimizer": {
|
||||
"type": "AdamW",
|
||||
"params": {
|
||||
"lr": 1e-4
|
||||
}
|
||||
},
|
||||
"zero_optimization": {
|
||||
"stage": 1
|
||||
},
|
||||
"steps_per_print": 1,
|
||||
}
|
||||
|
||||
if preferred_dtype() == torch.float16:
|
||||
ds_config["fp16"] = {"enabled": True}
|
||||
elif preferred_dtype() == torch.bfloat16:
|
||||
ds_config["bf16"] = {"enabled": True}
|
||||
|
||||
engine, _, _, _ = deepspeed.initialize(model=model, model_parameters=model.parameters(), config=ds_config)
|
||||
|
||||
assert engine.autotp_size() == 2
|
||||
|
||||
for _ in range(1):
|
||||
input_tensor = torch.randn(2, 4, 64, dtype=preferred_dtype()).to(get_accelerator().current_device_name())
|
||||
dist.broadcast(
|
||||
input_tensor,
|
||||
src=groups.get_tensor_model_parallel_src_rank(),
|
||||
group=groups.get_tensor_model_parallel_group(),
|
||||
)
|
||||
self._gather_and_compare_params(model, torch_q, torch_o, compare_values=False)
|
||||
output = engine(input_tensor)
|
||||
loss = output.mean()
|
||||
engine.backward(loss)
|
||||
engine.step()
|
||||
|
||||
for p in engine.parameters():
|
||||
assert not torch.isnan(p).any()
|
||||
|
||||
def test_tp_plan_with_zero2(self):
|
||||
skip_on_device()
|
||||
|
||||
model = self.SimpleHFModel()
|
||||
torch_q, torch_o = self._setup_baseline_linears(model)
|
||||
|
||||
ds_config = {
|
||||
"train_micro_batch_size_per_gpu": 1,
|
||||
"tensor_parallel": {
|
||||
"autotp_size": 2
|
||||
},
|
||||
"optimizer": {
|
||||
"type": "AdamW",
|
||||
"params": {
|
||||
"lr": 1e-4
|
||||
}
|
||||
},
|
||||
"zero_optimization": {
|
||||
"stage": 2
|
||||
},
|
||||
"steps_per_print": 1,
|
||||
}
|
||||
|
||||
if preferred_dtype() == torch.float16:
|
||||
ds_config["fp16"] = {"enabled": True}
|
||||
elif preferred_dtype() == torch.bfloat16:
|
||||
ds_config["bf16"] = {"enabled": True}
|
||||
|
||||
engine, _, _, _ = deepspeed.initialize(model=model, model_parameters=model.parameters(), config=ds_config)
|
||||
|
||||
assert engine.autotp_size() == 2
|
||||
|
||||
input_tensor = torch.randn(2, 4, 64, dtype=preferred_dtype()).to(get_accelerator().current_device_name())
|
||||
dist.broadcast(
|
||||
input_tensor,
|
||||
src=groups.get_tensor_model_parallel_src_rank(),
|
||||
group=groups.get_tensor_model_parallel_group(),
|
||||
)
|
||||
self._gather_and_compare_params(model, torch_q, torch_o, compare_values=False)
|
||||
output = engine(input_tensor)
|
||||
loss = output.mean()
|
||||
engine.backward(loss)
|
||||
engine.step()
|
||||
@@ -0,0 +1,174 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# DeepSpeed Team
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import deepspeed.comm as dist
|
||||
import deepspeed
|
||||
from deepspeed.accelerator import get_accelerator
|
||||
from deepspeed.utils import groups
|
||||
from unit.common import DistributedTest
|
||||
|
||||
|
||||
def skip_on_device():
|
||||
if get_accelerator().device_name() == "xpu":
|
||||
pytest.skip("XPU requires a higher version for test")
|
||||
|
||||
|
||||
class TestTPPlanRealHFModels(DistributedTest):
|
||||
"""End-to-end tests using real HuggingFace models"""
|
||||
|
||||
world_size = 2
|
||||
|
||||
def test_qwen2_tp_plan_with_zero2(self):
|
||||
"""Test Qwen2 model + tp_plan + ZeRO2"""
|
||||
skip_on_device()
|
||||
|
||||
try:
|
||||
from transformers import AutoModelForCausalLM, AutoConfig
|
||||
except ImportError:
|
||||
pytest.skip("transformers not installed")
|
||||
|
||||
# Create small Qwen2 config
|
||||
config = AutoConfig.from_pretrained(
|
||||
"Qwen/Qwen2-7B",
|
||||
vocab_size=1000,
|
||||
hidden_size=128,
|
||||
intermediate_size=256,
|
||||
num_hidden_layers=2,
|
||||
num_attention_heads=4,
|
||||
num_key_value_heads=4,
|
||||
)
|
||||
|
||||
model = AutoModelForCausalLM.from_config(config)
|
||||
|
||||
ds_config = {
|
||||
"train_micro_batch_size_per_gpu": 1,
|
||||
"tensor_parallel": {
|
||||
"autotp_size": 2
|
||||
},
|
||||
"optimizer": {
|
||||
"type": "AdamW",
|
||||
"params": {
|
||||
"lr": 1e-4
|
||||
}
|
||||
},
|
||||
"zero_optimization": {
|
||||
"stage": 2
|
||||
},
|
||||
"bf16": {
|
||||
"enabled": True
|
||||
},
|
||||
"steps_per_print": 1,
|
||||
}
|
||||
|
||||
engine, _, _, _ = deepspeed.initialize(model=model, model_parameters=model.parameters(), config=ds_config)
|
||||
|
||||
assert engine.autotp_size() == 2
|
||||
|
||||
# Train for a few steps
|
||||
for _ in range(3):
|
||||
input_ids = torch.randint(0, 1000, (1, 16)).to(get_accelerator().current_device_name())
|
||||
dist.broadcast(
|
||||
input_ids,
|
||||
src=groups.get_tensor_model_parallel_src_rank(),
|
||||
group=groups.get_tensor_model_parallel_group(),
|
||||
)
|
||||
outputs = engine(input_ids, labels=input_ids)
|
||||
engine.backward(outputs.loss)
|
||||
engine.step()
|
||||
|
||||
assert not torch.isnan(outputs.loss)
|
||||
|
||||
def test_custom_model_with_custom_tp_plan(self):
|
||||
"""Test custom model + custom tp_plan"""
|
||||
skip_on_device()
|
||||
|
||||
class CustomTransformerModel(torch.nn.Module):
|
||||
|
||||
def __init__(self, hidden_size=64):
|
||||
super().__init__()
|
||||
self.config = type(
|
||||
"Config",
|
||||
(),
|
||||
{
|
||||
"base_model_tp_plan": {
|
||||
"encoder.*.attention.query": "colwise",
|
||||
"encoder.*.attention.key": "colwise",
|
||||
"encoder.*.attention.value": "colwise",
|
||||
"encoder.*.attention.output": "rowwise",
|
||||
"encoder.*.ffn.intermediate": "colwise",
|
||||
"encoder.*.ffn.output": "rowwise",
|
||||
}
|
||||
},
|
||||
)()
|
||||
|
||||
# Simple encoder layers
|
||||
self.encoder = torch.nn.ModuleList([
|
||||
torch.nn.ModuleDict({
|
||||
"attention":
|
||||
torch.nn.ModuleDict({
|
||||
"query": torch.nn.Linear(hidden_size, hidden_size),
|
||||
"key": torch.nn.Linear(hidden_size, hidden_size),
|
||||
"value": torch.nn.Linear(hidden_size, hidden_size),
|
||||
"output": torch.nn.Linear(hidden_size, hidden_size),
|
||||
}),
|
||||
"ffn":
|
||||
torch.nn.ModuleDict({
|
||||
"intermediate": torch.nn.Linear(hidden_size, hidden_size * 4),
|
||||
"output": torch.nn.Linear(hidden_size * 4, hidden_size),
|
||||
}),
|
||||
}) for _ in range(2)
|
||||
])
|
||||
|
||||
def forward(self, x):
|
||||
for layer in self.encoder:
|
||||
# Simplified attention
|
||||
q = layer.attention.query(x)
|
||||
k = layer.attention.key(x)
|
||||
v = layer.attention.value(x)
|
||||
attn_out = layer.attention.output(q + k + v)
|
||||
|
||||
# FFN
|
||||
intermediate = torch.relu(layer.ffn.intermediate(attn_out))
|
||||
x = layer.ffn.output(intermediate)
|
||||
return x
|
||||
|
||||
model = CustomTransformerModel(hidden_size=64)
|
||||
|
||||
ds_config = {
|
||||
"train_micro_batch_size_per_gpu": 1,
|
||||
"tensor_parallel": {
|
||||
"autotp_size": 2
|
||||
},
|
||||
"optimizer": {
|
||||
"type": "AdamW",
|
||||
"params": {
|
||||
"lr": 1e-4
|
||||
}
|
||||
},
|
||||
"zero_optimization": {
|
||||
"stage": 0
|
||||
},
|
||||
"bf16": {
|
||||
"enabled": True
|
||||
},
|
||||
}
|
||||
|
||||
engine, _, _, _ = deepspeed.initialize(model=model, model_parameters=model.parameters(), config=ds_config)
|
||||
|
||||
assert engine.autotp_size() == 2
|
||||
|
||||
# Training step
|
||||
input_tensor = torch.randn(2, 4, 64, dtype=torch.bfloat16).to(get_accelerator().current_device_name())
|
||||
dist.broadcast(
|
||||
input_tensor,
|
||||
src=groups.get_tensor_model_parallel_src_rank(),
|
||||
group=groups.get_tensor_model_parallel_group(),
|
||||
)
|
||||
output = engine(input_tensor)
|
||||
loss = output.mean()
|
||||
engine.backward(loss)
|
||||
engine.step()
|
||||
Reference in New Issue
Block a user