chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:18:33 +08:00
commit 4ececc111a
2017 changed files with 331736 additions and 0 deletions
@@ -0,0 +1,4 @@
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
@@ -0,0 +1,4 @@
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
@@ -0,0 +1,120 @@
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
from typing import List
import pytest
import torch
from deepspeed.accelerator import get_accelerator
from deepspeed.inference.v2.model_implementations.flat_model_helpers import (
flatten_inference_model,
restore_inference_model,
)
from deepspeed.inference.v2.model_implementations.layer_container_base import LayerContainer
from .utils import SimpleParam, DummyInferenceModel
class TransformerLayerContainer(LayerContainer):
"""
Stub layer container
"""
PARAM_MAPPING = {
"param_1": "param_1.param",
"param_2": "param_2.param",
}
param_1: SimpleParam
param_2: SimpleParam
class NonTransformerContainer(LayerContainer):
"""
Stub layer container
"""
PARAM_MAPPING = {
"param_1": "param_1.param",
"param_2": "param_2.param",
"param_3": "param_3.param",
}
param_1: SimpleParam
param_2: SimpleParam
param_3: SimpleParam
@pytest.mark.inference_v2
def test_contiguify_roundtrip():
"""
Validate that contiguify round trips and reconstructions are correct.
"""
model = DummyInferenceModel()
n_layers = 2
transformer_params = []
transformer_containers = []
# Create parameters and populate them into the containers
for i in range(n_layers):
transformer_containers.append(TransformerLayerContainer(model))
layer_params = []
for j in range(2):
layer_params.append(torch.rand(16, 16))
transformer_containers[i].set_dependency(f"param_{j+1}", layer_params[j])
layer_params = [p.to(get_accelerator().current_device()) for p in layer_params]
transformer_params.append(layer_params)
assert transformer_containers[i].is_populated == True
non_transformer_params = []
non_transformer_container = NonTransformerContainer(model)
for i in range(3):
non_transformer_params.append(torch.rand(16, 16).permute(1, 0))
non_transformer_container.set_dependency(f"param_{i+1}", non_transformer_params[i])
non_transformer_params = [p.to(get_accelerator().current_device()) for p in non_transformer_params]
def validate_containers(t_containers: List[LayerContainer], n_t_containers: LayerContainer,
t_params: List[List[torch.Tensor]], n_t_params: List[torch.Tensor]):
"""
Validate params match what is on the containers.
"""
for i in range(n_layers):
l_c = t_containers[i]
assert l_c.is_initialized == True
assert torch.equal(l_c.param_1, t_params[i][0])
assert torch.equal(l_c.param_2, t_params[i][1])
assert n_t_containers.is_initialized == True
assert torch.equal(n_t_containers.param_1, n_t_params[0])
assert torch.equal(n_t_containers.param_2, n_t_params[1])
assert torch.equal(n_t_containers.param_3, n_t_params[2])
assert not n_t_containers.param_1.is_contiguous()
assert not n_t_containers.param_2.is_contiguous()
assert not n_t_containers.param_3.is_contiguous()
buffer, metadata = flatten_inference_model(transformer_containers, non_transformer_container, "NoOpPolicy")
# Validate containers before contiguify
validate_containers(transformer_containers, non_transformer_container, transformer_params, non_transformer_params)
# Validate restore pass
transformer_containers_r = []
for i in range(n_layers):
transformer_containers_r.append(TransformerLayerContainer(model))
non_transformer_container_r = NonTransformerContainer(model)
restore_inference_model(buffer, metadata, transformer_containers_r, non_transformer_container_r)
validate_containers(transformer_containers_r, non_transformer_container_r, transformer_params,
non_transformer_params)
@@ -0,0 +1,48 @@
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
import pytest
import torch
from deepspeed.inference.v2.inference_parameter import InferenceParameter
from deepspeed.inference.v2.model_implementations.layer_container_base import LayerContainer
from .utils import SimpleParam, DummyInferenceModel
class ParentLayer(LayerContainer):
"""
A layer that has a dependency on a simple parameter.
"""
param_1: SimpleParam
class ChildLayer(ParentLayer):
"""
A layer that inherits from another layer.
"""
param_2: SimpleParam
@pytest.mark.inference_v2
def test_layer_inheritance():
inference_model = DummyInferenceModel()
multi_param_layer = ChildLayer(inference_model)
assert multi_param_layer.n_params == 2
assert multi_param_layer.is_initialized is False
multi_param_layer.param_1.param = torch.ones(16, 16)
assert multi_param_layer.is_initialized is False
multi_param_layer.param_2.param = torch.full((16, 16), 2.0)
assert multi_param_layer.is_populated is True
assert isinstance(multi_param_layer.param_1, InferenceParameter)
assert isinstance(multi_param_layer.param_2, InferenceParameter)
@@ -0,0 +1,168 @@
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
import pytest
import torch
from deepspeed.inference.v2.allocator import on_device
from deepspeed.inference.v2.inference_parameter import InferenceParameter
from deepspeed.inference.v2.model_implementations.parameter_base import ParameterBase, ParamList
from deepspeed.inference.v2.model_implementations.layer_container_base import LayerContainer
class MultiDependencyContainer(ParameterBase):
dependency_1: torch.Tensor
dependency_2: torch.Tensor
@on_device
def finalize(self) -> torch.Tensor:
param = torch.cat([self.dependency_1, self.dependency_2])
return InferenceParameter.initialize(param)
class ListDependencyContainer(ParameterBase):
dependencies: ParamList("list_items") # noqa: F821
@on_device
def finalize(self) -> torch.Tensor:
param = torch.cat(tuple(self.dependencies))
return InferenceParameter.initialize(param)
class MappingLayer(LayerContainer):
PARAM_MAPPING = {
"model.val.item.d_1": "multi_depend.dependency_1",
"model.val.item.d_2": "multi_depend.dependency_2",
"model.list_vals.*.d": "list_depend.dependencies"
}
multi_depend: MultiDependencyContainer
list_depend: ListDependencyContainer
class SubMappingLayer(MappingLayer):
PARAM_MAPPING = {
"model.val.item2.d_1": "multi_depend2.dependency_1",
"model.val.item2.d_2": "multi_depend2.dependency_2",
}
multi_depend2: MultiDependencyContainer
class DoubleMappingLayer(LayerContainer):
PARAM_MAPPING = {
"model.val.item.d_1": ["multi_depend.dependency_1", "multi_depend.dependency_2"],
}
multi_depend: MultiDependencyContainer
class InferenceModel:
@property
def list_items(self) -> int:
return 16
@pytest.mark.inference_v2
def test_mapping_syntax():
model = InferenceModel()
mapping_layer = MappingLayer(model)
mapping_layer.set_dependency("model.val.item.d_1", torch.ones(1))
mapping_layer.set_dependency("model.val.item.d_2", torch.ones(1) * 2)
assert isinstance(mapping_layer.multi_depend, torch.Tensor)
for i in range(16):
mapping_layer.set_dependency(f"model.list_vals.{i}.d", torch.ones(1) * i)
if i != 16 - 1:
assert mapping_layer.is_populated == False
assert isinstance(mapping_layer.list_depend, InferenceParameter)
assert mapping_layer.is_populated == True
@pytest.mark.inference_v2
def test_sub_mapping_syntax():
model = InferenceModel()
mapping_layer = SubMappingLayer(model)
mapping_layer.set_dependency("model.val.item.d_1", torch.ones(1))
mapping_layer.set_dependency("model.val.item.d_2", torch.ones(1) * 2)
assert isinstance(mapping_layer.multi_depend, InferenceParameter)
mapping_layer.set_dependency("model.val.item2.d_1", torch.ones(1))
mapping_layer.set_dependency("model.val.item2.d_2", torch.ones(1) * 2)
assert isinstance(mapping_layer.multi_depend2, InferenceParameter)
# We want to check into double digits to make sure that this isn't specific
# to single difit indexing.
for i in range(16):
mapping_layer.set_dependency(f"model.list_vals.{i}.d", torch.ones(1) * i)
if i != 16 - 1:
assert mapping_layer.is_populated == False
assert isinstance(mapping_layer.list_depend, InferenceParameter)
assert mapping_layer.is_populated == True
@pytest.mark.inference_v2
def test_double_mapping_syntax():
model = InferenceModel()
mapping_layer = DoubleMappingLayer(model)
mapping_layer.set_dependency("model.val.item.d_1", torch.ones(1))
# The single parameter setting should immediately make the parameter finalized
# and the whole layer initialized.
assert isinstance(mapping_layer.multi_depend, InferenceParameter)
assert mapping_layer.is_populated == True
@pytest.mark.inference_v2
def test_insufficient_mapping_syntax():
"""
In the above example, we don't have a mapping for `multi_depend2.dependency_2`.
"""
with pytest.raises(ValueError):
class InsuffienctMappingLayer(LayerContainer):
PARAM_MAPPING = {
"model.val.item.d_1": "multi_depend1.dependency_1",
"model.val.item.d_2": "multi_depend1.dependency_2",
"model.val.item2.d_1": "multi_depend2.dependency_1",
}
multi_depend1: MultiDependencyContainer
multi_depend2: MultiDependencyContainer
@pytest.mark.inference_v2
def test_unknown_target_mapping_syntax():
"""
In the above example, `multi_depend_unknown` does not exist
"""
with pytest.raises(ValueError):
class UnknownTargetMappingLayer(LayerContainer):
PARAM_MAPPING = {
"model.val.item.d_1": "multi_depend1.dependency_1",
"model.val.item.d_2": "multi_depend1.dependency_2",
"model.val.item2.d_1": "multi_depend_unknown.dependency_1",
}
multi_depend: MultiDependencyContainer
@@ -0,0 +1,79 @@
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
import pytest
import torch
from deepspeed.inference.v2.inference_parameter import InferenceParameter
from deepspeed.inference.v2.model_implementations.layer_container_base import LayerContainer
from .utils import validate_device, SimpleParam, ListParam, DummyInferenceModel
class MultiParameterLayer(LayerContainer):
"""
Two dependencies, both of which are simple parameters.
"""
param_1: SimpleParam
param_2: SimpleParam
class MixedMultiParameterLayer(LayerContainer):
"""
Two dependencies, one of which is a simple parameter, the other is a list parameter.
"""
param_1: SimpleParam
param_2: ListParam
@pytest.mark.inference_v2
def test_multi_parameter_layer():
inference_model = DummyInferenceModel()
multi_param_layer = MultiParameterLayer(inference_model)
assert multi_param_layer.n_params == 2
assert multi_param_layer.is_populated is False
multi_param_layer.param_1.param = torch.ones(16, 16)
assert multi_param_layer.is_populated is False
multi_param_layer.param_2.param = torch.full((16, 16), 2.0)
assert multi_param_layer.is_populated is True
assert isinstance(multi_param_layer.param_1, InferenceParameter)
assert isinstance(multi_param_layer.param_2, InferenceParameter)
@pytest.mark.inference_v2
def test_mixed_multi_parameter_layer():
inference_model = DummyInferenceModel()
mixed_multi_param_layer = MixedMultiParameterLayer(inference_model)
assert mixed_multi_param_layer.n_params == 2
assert mixed_multi_param_layer.is_populated is False
mixed_multi_param_layer.param_2.params[1] = torch.full((16, 16), 2.0)
assert mixed_multi_param_layer.is_populated is False
assert not isinstance(mixed_multi_param_layer.param_2, InferenceParameter)
mixed_multi_param_layer.param_1.param = torch.ones(16, 16)
assert mixed_multi_param_layer.is_populated is False
assert isinstance(mixed_multi_param_layer.param_1, InferenceParameter)
validate_device(mixed_multi_param_layer.param_1)
mixed_multi_param_layer.param_2.params[0] = torch.full((16, 16), 2.0)
assert mixed_multi_param_layer.is_populated is True
assert isinstance(mixed_multi_param_layer.param_2, InferenceParameter)
validate_device(mixed_multi_param_layer.param_2)
@@ -0,0 +1,105 @@
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
import pytest
import torch
from deepspeed.inference.v2.allocator import on_device
from deepspeed.inference.v2.inference_parameter import InferenceParameter
from deepspeed.inference.v2.model_implementations.parameter_base import ParameterBase, ParamList
from deepspeed.inference.v2.model_implementations.layer_container_base import LayerContainer
from deepspeed.inference.v2.model_implementations.common_parameters import *
from .utils import validate_device
class SimpleMoELayer(LayerContainer):
moe_mlp_1: UnfusedMoEMLP1Parameter
class DummyInferenceModel:
def __init__(self, experts_per_rank: int) -> None:
self._num_experts = experts_per_rank
@property
def n_experts(self) -> int:
return self._num_experts
@on_device
def transform_moe_mlp_1_param(self, param: torch.Tensor) -> torch.Tensor:
return InferenceParameter.initialize(param)
@pytest.mark.inference_v2
def test_simple_moe_layer():
inference_model = DummyInferenceModel(experts_per_rank=2)
simple_moe_layer = SimpleMoELayer(inference_model)
assert simple_moe_layer.moe_mlp_1.experts[0] is None
assert simple_moe_layer.moe_mlp_1.experts[1] is None
# Set the first expert
simple_moe_layer.moe_mlp_1.experts[0] = torch.zeros(16, 16)
assert simple_moe_layer.moe_mlp_1.experts[0] is not None
assert simple_moe_layer.moe_mlp_1.experts[1] is None
assert not simple_moe_layer.is_initialized
# Set the second expert
simple_moe_layer.moe_mlp_1.experts[1] = torch.ones(16, 16)
# We have all the experts, so the layer should be initialized
assert simple_moe_layer.is_initialized
assert isinstance(simple_moe_layer.moe_mlp_1, torch.Tensor)
validate_device(simple_moe_layer.moe_mlp_1)
"""
Check that we can mix the number of elements in lists in the same context and have that
be tracked correctly.
"""
class CustomListParam1(ParameterBase):
deps: ParamList("attr_1")
class CustomListParam2(ParameterBase):
deps: ParamList("attr_2")
class MixedLayer(LayerContainer):
list_1: CustomListParam1
list_2: CustomListParam2
class MixedInferenceModel:
@property
def attr_1(self) -> int:
return 1
@property
def attr_2(self) -> int:
return 2
@pytest.mark.inference_v2
def test_mixed_param_lists():
model = MixedInferenceModel()
layer = MixedLayer(model)
assert layer.list_1.deps.n_params == 1
assert layer.list_2.deps.n_params == 2
@@ -0,0 +1,60 @@
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
import torch
from deepspeed.accelerator import get_accelerator
from deepspeed.inference.v2.allocator import on_device
from deepspeed.inference.v2.inference_parameter import InferenceParameter
from deepspeed.inference.v2.model_implementations.parameter_base import ParameterBase, ParametrizedList
class SimpleParam(ParameterBase):
"""
Parameter with single dependency.
"""
param: torch.Tensor
@on_device
def finalize(self) -> torch.Tensor:
return self.inference_model.transform(self.param)
class SimpleParametrizedList(ParametrizedList):
"""
Parameter list based on `num_dependencies` attribute.
"""
count_attr: str = "num_dependencies"
class ListParam(ParameterBase):
"""
Parameter with list dependency.
NOTE: This uses the tuple workaround for the `ParametrizedList` class
as described in the docstring of `ParametrizedList`.
"""
params: SimpleParametrizedList
@on_device
def finalize(self) -> torch.Tensor:
return self.inference_model.transform(torch.cat(tuple(self.params)))
class DummyInferenceModel:
@property
def num_dependencies(self) -> int:
return 2
def transform(self, param: torch.Tensor) -> torch.Tensor:
return InferenceParameter.initialize(param)
def validate_device(tensor: torch.Tensor):
assert tensor.device == torch.device(get_accelerator().current_device())
@@ -0,0 +1,4 @@
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
@@ -0,0 +1,129 @@
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
import pytest
import torch
from deepspeed.accelerator import get_accelerator
from deepspeed.inference.v2.model_implementations.sharding import *
# None of the logic should be dependent on head size.
HEAD_SIZE = 64
def fill_with_head_ids(head_size: int, n_heads: int) -> torch.Tensor:
"""
Fills a tensor with the associated head ids. All columns should have the same value.
"""
head_ids = torch.arange(n_heads, dtype=torch.half, device=get_accelerator().current_device())
head_ids = head_ids.repeat_interleave(head_size).repeat(head_size * n_heads).reshape(n_heads * head_size, -1)
return head_ids
@pytest.mark.inference_v2
@pytest.mark.parametrize("n_heads, n_shards", [(1, 1), (8, 4), (32, 8)])
def test_mha_even_sharding(n_heads: int, n_shards: int):
"""
Even head sharding for MHA.
Args:
n_heads (int): The number QKV heads.
n_shards (int): The number of shards to test for.
"""
param = fill_with_head_ids(HEAD_SIZE, n_heads)
n_local_heads = n_heads // n_shards
sharded_shape = (HEAD_SIZE * n_heads, HEAD_SIZE * n_local_heads)
for shard_rank in range(n_shards):
sharded_param = shard_attn_out_param(param, shard_rank, n_shards, HEAD_SIZE)
n_heads_local_q, _ = get_local_heads(shard_rank, n_shards, n_heads)
assert sharded_param.shape[-1] == HEAD_SIZE * n_heads_local_q
assert sharded_param.shape == sharded_shape
heads = torch.chunk(sharded_param, n_local_heads, dim=1)
for i, head in enumerate(heads):
assert torch.all(head == i + shard_rank * n_local_heads)
@pytest.mark.inference_v2
@pytest.mark.parametrize("n_heads, n_shards", [(3, 2), (20, 8)])
def test_mha_unbalanced_sharding(n_heads: int, n_shards: int):
"""
Unbalanced head sharding for MHA.
Args:
n_heads (int): The number QKV heads.
n_shards (int): The number of shards to test for.
"""
param = fill_with_head_ids(HEAD_SIZE, n_heads)
max_heads = 0
min_heads = n_heads
seen_heads = set()
total_heads = 0
for shard_rank in range(n_shards):
sharded_param = shard_attn_out_param(param, shard_rank, n_shards, HEAD_SIZE)
n_heads_local_q, _ = get_local_heads(shard_rank, n_shards, n_heads)
assert sharded_param.shape[-1] == HEAD_SIZE * n_heads_local_q
n_local_heads = sharded_param.shape[1] // HEAD_SIZE
total_heads += n_local_heads
max_heads = max(max_heads, n_local_heads)
min_heads = min(min_heads, n_local_heads)
for i in range(n_local_heads):
head_ids = torch.unique_consecutive(sharded_param[:, i * HEAD_SIZE:(i + 1) * HEAD_SIZE])
assert len(head_ids) == 1
seen_heads.add(head_ids.item())
assert max_heads == min_heads + 1
assert total_heads == n_heads
assert len(seen_heads) == n_heads
@pytest.mark.inference_v2
@pytest.mark.parametrize("n_heads_q, n_heads_kv, n_shards", [(20, 4, 8)])
def test_gqa_uneven_sharding(n_heads_q: int, n_heads_kv: int, n_shards: int):
"""
We only test the uneven GQA test case because even GQA shards the attention output
in the exact same manner as MHA.
Args:
n_heads_q (int): The number of query heads.
n_heads_kv (int): The number of key/value heads.
n_shards (int): The number of shards to test for.
"""
param = fill_with_head_ids(HEAD_SIZE, n_heads_q)
min_heads = n_heads_q
max_heads = 0
seen_heads = set()
total_heads = 0
for shard_rank in range(n_shards):
sharded_param = shard_attn_out_param(param, shard_rank, n_shards, HEAD_SIZE, n_heads_q, n_heads_kv)
n_heads_local_q, _ = get_local_heads(shard_rank, n_shards, n_heads_q, n_heads_kv)
assert sharded_param.shape[-1] == HEAD_SIZE * n_heads_local_q
n_local_heads = sharded_param.shape[1] // HEAD_SIZE
total_heads += n_local_heads
max_heads = max(max_heads, n_local_heads)
min_heads = min(min_heads, n_local_heads)
for i in range(n_local_heads):
head_id = torch.unique_consecutive(sharded_param[:, i * HEAD_SIZE:(i + 1) * HEAD_SIZE])
assert len(head_id) == 1
seen_heads.add(head_id.item())
assert max_heads == min_heads + 1
assert total_heads == n_heads_q
assert len(seen_heads) == n_heads_q
@@ -0,0 +1,116 @@
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
import pytest
import torch
from deepspeed.accelerator import get_accelerator
from deepspeed.inference.v2.model_implementations.sharding import *
def round_up_to_256(x: int) -> int:
"""
Round up to the nearest multiple of 256.
"""
return x + (256 - x % 256)
def make_params(model_dim: int, ffn_multiplier: int, n_experts: int, gated: bool = False) -> torch.Tensor:
"""
"""
if gated:
mlp_1_intermediate = round_up_to_256(int(model_dim * ffn_multiplier * 4 / 3))
mlp_2_intermediate = mlp_1_intermediate // 2
else:
mlp_1_intermediate = ffn_multiplier * model_dim
mlp_2_intermediate = ffn_multiplier * model_dim
mlp_1_shared_dim = torch.arange(mlp_1_intermediate, dtype=torch.float32, device=get_accelerator().current_device())
mlp_1_w = mlp_1_shared_dim.repeat_interleave(model_dim).reshape(mlp_1_intermediate, model_dim)
mlp_1_b = mlp_1_shared_dim
mlp_2_shared_dim = torch.arange(mlp_2_intermediate, dtype=torch.float32, device=get_accelerator().current_device())
mlp_2_w = mlp_2_shared_dim.repeat(model_dim).reshape(model_dim, mlp_2_intermediate)
mlp_2_b = torch.ones(model_dim, dtype=torch.float32, device=get_accelerator().current_device())
if n_experts > 1:
mlp_1_w = mlp_1_w.expand(n_experts, -1, -1)
mlp_1_b = mlp_1_b.expand(n_experts, -1)
mlp_2_w = mlp_2_w.expand(n_experts, -1, -1)
mlp_2_b = mlp_2_b.expand(n_experts, -1)
return (mlp_1_w, mlp_1_b, mlp_2_w, mlp_2_b)
@pytest.mark.inference_v2
@pytest.mark.parametrize("model_dim, ffn_multiplier, n_shards", [(1024, 4, 1), (1024, 4, 8), (1024, 4, 6)])
@pytest.mark.parametrize("n_experts", [1, 16])
def test_even_ffn_sharding(model_dim: int, ffn_multiplier: int, n_shards: int, n_experts: int):
"""
FFN sharding tends to be much simpler than attention sharding since it works on larger granularities.
While the test case of (1024, 4, 6) is not a use case we're likely to see, this does ensure that
the sharding logic will round correctly for the alignments we care about.
"""
mlp_1_w, mlp_1_b, mlp_2_w, mlp_2_b = make_params(model_dim, ffn_multiplier, n_experts)
total_ffn_dim = model_dim * ffn_multiplier
mapped_neurons = 0
is_moe = n_experts > 1
for shard_rank in range(n_shards):
shard_1_w = shard_mlp_1_param(mlp_1_w, shard_rank, n_shards, is_moe=is_moe)
shard_1_b = shard_mlp_1_param(mlp_1_b, shard_rank, n_shards, is_moe=is_moe)
shard_2_w = shard_mlp_2_param(mlp_2_w, shard_rank, n_shards, is_moe=is_moe)
shard_2_b = shard_mlp_2_param(mlp_2_b, shard_rank, n_shards, is_moe=is_moe)
assert shard_1_w.shape[-2] == shard_2_w.shape[-1]
assert shard_1_w.shape[-2] % DEFAULT_SHARD_GRANULARITY == 0
assert shard_1_w.shape[-2] == shard_1_b.shape[-1]
mapped_neurons += shard_1_w.shape[-2]
if shard_rank != 0:
assert shard_2_b is None
else:
assert shard_2_b.shape[-1] == model_dim
assert mapped_neurons == total_ffn_dim
@pytest.mark.inference_v2
@pytest.mark.parametrize("model_dim, ffn_multiplier, n_shards", [(1024, 4, 1), (1024, 4, 8), (1024, 4, 6)])
@pytest.mark.parametrize("n_experts", [1, 16])
def test_gated_ffn_sharding(model_dim: int, ffn_multiplier: int, n_shards: int, n_experts: int):
"""
Test the same cases assuming a gated regime.
"""
mlp_1_w, mlp_1_b, mlp_2_w, mlp_2_b = make_params(model_dim, ffn_multiplier, n_experts, gated=True)
total_ffn_dim = round_up_to_256(int(model_dim * ffn_multiplier * 4 / 3))
mapped_neurons = 0
is_moe = n_experts > 1
for shard_rank in range(n_shards):
shard_1_w = shard_mlp_1_param(mlp_1_w, shard_rank, n_shards, gated=True, is_moe=is_moe)
shard_1_b = shard_mlp_1_param(mlp_1_b, shard_rank, n_shards, gated=True, is_moe=is_moe)
shard_2_w = shard_mlp_2_param(mlp_2_w, shard_rank, n_shards, is_moe=is_moe)
shard_2_b = shard_mlp_2_param(mlp_2_b, shard_rank, n_shards, is_moe=is_moe)
assert shard_1_w.shape[-2] == shard_2_w.shape[-1] * 2
assert shard_1_w.shape[-2] % DEFAULT_SHARD_GRANULARITY == 0
assert shard_1_w.shape[-2] == shard_1_b.shape[-1]
mapped_neurons += shard_1_w.shape[-2]
if shard_rank != 0:
assert shard_2_b is None
else:
assert shard_2_b.shape[-1] == model_dim
assert mapped_neurons == total_ffn_dim
@@ -0,0 +1,251 @@
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
from typing import Optional
import pytest
import torch
from deepspeed.accelerator import get_accelerator
from deepspeed.inference.v2.model_implementations.sharding import *
def fill_with_head_ids(head_size: int, n_heads_q: int, n_heads_kv: Optional[int] = None) -> torch.Tensor:
"""
"""
head_ids_q = torch.arange(n_heads_q, dtype=torch.half, device=get_accelerator().current_device())
head_vals_q = head_ids_q.repeat_interleave(head_size * head_size * n_heads_q).reshape(n_heads_q * head_size, -1)
if n_heads_kv is None:
return torch.cat([head_vals_q, head_vals_q, head_vals_q], dim=0)
head_ids_k = torch.arange(n_heads_kv, dtype=torch.half, device=get_accelerator().current_device())
head_vals_k = head_ids_k.repeat_interleave(head_size * head_size * n_heads_q).reshape(n_heads_kv * head_size, -1)
return torch.cat([head_vals_q, head_vals_k, head_vals_k], dim=0)
def validate_inferred_shape(shard: torch.Tensor, head_size: int, n_local_q_heads: int, n_local_kv_heads: int):
"""
Validate that the leading dim of the shard is of the expected size and aligns with the sharding
logic for the attention computation itself.
"""
inferred_leading_dim = head_size * (n_local_q_heads + 2 * n_local_kv_heads)
assert shard.shape[0] == inferred_leading_dim
@pytest.mark.inference_v2
@pytest.mark.parametrize("head_size", [64])
@pytest.mark.parametrize("n_heads,n_shards", [(1, 1), (32, 1), (32, 8)])
def test_even_mha_sharding(head_size: int, n_heads: int, n_shards: int):
"""
Test for MHA sharding. In these scenarios, we expect that each of the shards
should be the same size.
"""
param = fill_with_head_ids(head_size, n_heads)
heads_per_shard = n_heads // n_shards
for shard_rank in range(n_shards):
shard = shard_qkv_param(param, shard_rank, n_shards, head_size, n_heads, n_heads)
n_local_q_heads, n_local_kv_heads = get_local_heads(shard_rank, n_shards, n_heads, n_heads)
validate_inferred_shape(shard, head_size, n_local_q_heads, n_local_kv_heads)
assert shard.shape == (3 * head_size * heads_per_shard, head_size * n_heads)
heads = shard.chunk(heads_per_shard * 3, dim=0)
for i in range(heads_per_shard):
assert torch.all(heads[i] == i + shard_rank * heads_per_shard)
assert torch.all(heads[i + heads_per_shard] == i + shard_rank * heads_per_shard)
assert torch.all(heads[i + heads_per_shard * 2] == i + shard_rank * heads_per_shard)
@pytest.mark.inference_v2
@pytest.mark.parametrize("head_size", [64])
@pytest.mark.parametrize("n_heads, n_shards", [(3, 2), (20, 8)])
def test_unbalanced_mha_sharding(head_size: int, n_heads: int, n_shards: int):
"""
Test MHA sharding when the distribution of heads will not be equal across all ranks.
"""
param = fill_with_head_ids(head_size, n_heads)
max_heads = 0
min_heads = n_heads
total_heads = 0
seen_heads = set()
for shard_rank in range(n_shards):
shard = shard_qkv_param(param, shard_rank, n_shards, head_size, n_heads, n_heads)
n_local_q_heads, n_local_kv_heads = get_local_heads(shard_rank, n_shards, n_heads, n_heads)
validate_inferred_shape(shard, head_size, n_local_q_heads, n_local_kv_heads)
n_heads_in_shard = shard.shape[0] // head_size // 3
max_heads = max(max_heads, n_heads_in_shard)
min_heads = min(min_heads, n_heads_in_shard)
total_heads += n_heads_in_shard
heads = shard.chunk(n_heads_in_shard * 3, dim=0)
for local_head_id in range(n_heads_in_shard):
head_qkv = torch.cat([
heads[local_head_id], heads[local_head_id + n_heads_in_shard],
heads[local_head_id + 2 * n_heads_in_shard]
],
dim=0)
assert head_qkv.shape == (3 * head_size, head_size * n_heads)
global_head_id = torch.unique_consecutive(head_qkv)
assert len(global_head_id) == 1
seen_heads.add(global_head_id.item())
assert max_heads - min_heads <= 1
assert total_heads == n_heads
assert len(seen_heads) == n_heads
@pytest.mark.inference_v2
@pytest.mark.parametrize("head_size", [64])
@pytest.mark.parametrize("n_heads_q, n_heads_kv, n_shards", [(4, 2, 1), (8, 2, 1), (64, 16, 8)])
def test_gqa_even_sharding(head_size: int, n_heads_q: int, n_heads_kv: int, n_shards: int):
"""
Test GQA sharding when the KV heads are evenly divisible by the number of shards.
"""
param = fill_with_head_ids(head_size, n_heads_q, n_heads_kv)
n_kv_heads_in_shard = n_heads_kv // n_shards
n_q_heads_in_shard = n_heads_q // n_shards
for shard_rank in range(n_shards):
shard = shard_qkv_param(param, shard_rank, n_shards, head_size, n_heads_q, n_heads_kv)
n_local_q_heads, n_local_kv_heads = get_local_heads(shard_rank, n_shards, n_heads_q, n_heads_kv)
validate_inferred_shape(shard, head_size, n_local_q_heads, n_local_kv_heads)
assert shard.shape[0] == (n_q_heads_in_shard + n_kv_heads_in_shard * 2) * head_size
q = shard[:n_q_heads_in_shard * head_size]
k = shard[n_q_heads_in_shard * head_size:(n_q_heads_in_shard + n_kv_heads_in_shard) * head_size]
v = shard[(n_q_heads_in_shard + n_kv_heads_in_shard) * head_size:]
for local_head_id in range(n_q_heads_in_shard):
assert torch.all(q[local_head_id * head_size:(local_head_id + 1) * head_size] == local_head_id +
shard_rank * n_q_heads_in_shard)
for local_head_id in range(n_kv_heads_in_shard):
assert torch.all(k[local_head_id * head_size:(local_head_id + 1) * head_size] == local_head_id +
shard_rank * n_kv_heads_in_shard)
assert torch.all(v[local_head_id * head_size:(local_head_id + 1) * head_size] == local_head_id +
shard_rank * n_kv_heads_in_shard)
@pytest.mark.inference_v2
@pytest.mark.parametrize("head_size", [64])
@pytest.mark.parametrize("n_heads_q, n_heads_kv, n_shards", [(4, 2, 4), (20, 4, 8)])
def test_gqa_uneven_sharding(head_size: int, n_heads_q: int, n_heads_kv: int, n_shards: int):
"""
Test GQA sharding when there are more shards than KV heads.
"""
param = fill_with_head_ids(head_size, n_heads_q, n_heads_kv)
n_kv_heads_in_shard = 1
n_shards_per_kv_head = n_shards // n_heads_kv
max_heads = 0
min_heads = n_heads_q
total_heads = 0
seen_heads = set()
for shard_rank in range(n_shards):
shard = shard_qkv_param(param, shard_rank, n_shards, head_size, n_heads_q, n_heads_kv)
n_local_q_heads, n_local_kv_heads = get_local_heads(shard_rank, n_shards, n_heads_q, n_heads_kv)
validate_inferred_shape(shard, head_size, n_local_q_heads, n_local_kv_heads)
local_n_heads_q = (shard.shape[0] - 2 * n_kv_heads_in_shard * head_size) // head_size
max_heads = max(max_heads, local_n_heads_q)
min_heads = min(min_heads, local_n_heads_q)
total_heads += local_n_heads_q
q = shard[:local_n_heads_q * head_size]
kv = shard[local_n_heads_q * head_size:]
for local_head_id in range(local_n_heads_q):
q_head_id = torch.unique_consecutive(q[local_head_id * head_size:(local_head_id + 1) * head_size])
assert len(q_head_id) == 1
seen_heads.add(q_head_id.item())
kv_id_calc = shard_rank // n_shards_per_kv_head
kv_id = torch.unique_consecutive(kv)
assert len(kv_id) == 1
assert kv_id.item() == kv_id_calc
assert max_heads - min_heads <= 1
assert total_heads == n_heads_q
assert len(seen_heads) == n_heads_q
@pytest.mark.inference_v2
@pytest.mark.parametrize("head_size", [64])
@pytest.mark.parametrize("n_heads, n_shards", [(6, 8)])
def test_unsupported_mha_configs(head_size: int, n_heads: int, n_shards: int):
"""
Sharding should fail if there are fewer heads than shards.
TODO(cmikeh2): Look to support this configuration.
"""
param = fill_with_head_ids(head_size, n_heads)
for shard_rank in range(n_shards):
with pytest.raises(ValueError):
shard_qkv_param(param, shard_rank, n_shards, head_size, n_heads, n_heads)
@pytest.mark.inference_v2
@pytest.mark.parametrize("head_size", [64])
@pytest.mark.parametrize("n_heads_q, n_heads_kv, n_shards", [(5, 2, 1), (40, 10, 8), (30, 5, 8)])
def test_unsupported_gqa_configs(head_size: int, n_heads_q: int, n_heads_kv: int, n_shards: int):
"""
GQA has stricter requirements. We must be able to evenly shard or distribute the KV heads.
Test cases are to test the following preconditions specifically:
1. n_heads_q % n_heads_kv == 0
2. We must be able to evenly distribute KV heads
3. We must be able to evely split KV heads
"""
param = fill_with_head_ids(head_size, n_heads_q, n_heads_kv)
for shard_rank in range(n_shards):
with pytest.raises(ValueError):
shard_qkv_param(param, shard_rank, n_shards, head_size, n_heads_q, n_heads_kv)
@pytest.mark.inference_v2
def test_mha_input_shape_error():
param = torch.empty(256, 128)
n_heads = 2
head_size = 64
with pytest.raises(ValueError):
shard_qkv_param(param, 0, 1, 64)
@pytest.mark.inference_v2
def test_gqa_input_shape_error():
head_size = 64
n_heads_q = 16
n_heads_kv = 4
# Correct shape is 1536 (=16 * 64 + 2 * 4 * 64), 1024
param = torch.empty(2048, 1024)
with pytest.raises(ValueError):
shard_qkv_param(param, 0, 1, head_size, n_heads_q, n_heads_kv)