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

618 lines
23 KiB
Python

# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
from abc import abstractmethod
from typing import Optional
import torch
from deepspeed.accelerator import get_accelerator
from ..config_v2 import RaggedInferenceEngineConfig
from ..inference_utils import ActivationType, ceil_div, is_gated
from ..model_implementations import *
from ..model_implementations.sharding import *
from ..modules.configs import (
DSEmbeddingsConfig,
DSLinearConfig,
DSMoEConfig,
DSNormConfig,
DSSelfAttentionConfig,
DSUnembedConfig,
NormTypeEnum,
PositionalEmbeddingType,
RotateHalfConfig,
)
from ..modules import heuristics
from ..ragged import (
DSSequenceDescriptor,
KVCacheConfig,
RaggedBatchWrapper,
)
from .inference_model_base import (
DSInferenceModelBase,
DSModelImplementationConfig,
MPType,
)
from ..inference_parameter import InferenceParameter
try:
from functools import cached_property
except ImportError:
def cached_property(func):
return property(func)
class DSTransformerModelBase(DSInferenceModelBase):
"""
Dimensioning properties
"""
@property
@abstractmethod
def num_layers(self) -> int:
"""
Number of the layers in the model
"""
...
@property
@abstractmethod
def model_dim(self) -> int:
"""
Size of embedding projection and residuals.
"""
...
@property
@abstractmethod
def vocab_size(self) -> int:
"""
Size of the vocabulary (including padding).
"""
...
@property
@abstractmethod
def head_size(self) -> int:
"""
Size of each attention head.
"""
...
@property
@abstractmethod
def n_heads(self) -> int:
"""
The number of query heads on the model. This should not take into account
any dimension reductions from model sharding.
"""
...
@property
def n_heads_q(self) -> int:
"""
Alias to n_heads.
"""
return self.n_heads
@property
def n_heads_kv(self) -> int:
"""
The number of key and value heads on the model. For GQA or MQA, overload this attribute.
Otherwise it adopts MHA formulations and uses n_heads. This should not take into account
any dimension reductions from model sharding.
"""
return self.n_heads
@property
@abstractmethod
def intermediate_dim(self) -> int:
"""
The size of the (unsharded) intermediate projection dim. For a gated activation function
this is the size of the input to the second MLP layer. This should not take into account
any dimension reductions from model sharding.
"""
...
@property
@abstractmethod
def positional_embedding_type(self) -> PositionalEmbeddingType:
"""
The type of positional embedding used by the model.
"""
...
"""
Architectural properties
"""
@property
@abstractmethod
def activation_dtype(self) -> torch.dtype:
"""
The activation dtype of the model.
"""
...
@property
@abstractmethod
def mlp_activation_fn(self) -> ActivationType:
"""
The activation function used in the MLP.
"""
...
@property
@abstractmethod
def norm_type(self) -> NormTypeEnum:
"""
The type of normalization used in the model.
"""
...
@property
@abstractmethod
def positional_embedding_config(self) -> Optional[RotateHalfConfig]:
"""
The positional embedding configuration for the model.
"""
...
"""
Derived helpers
"""
@cached_property
def n_heads_q_local(self) -> int:
"""
Number of local heads post sharding.
"""
return get_local_heads(self.tp_rank, self.tp_size, self.n_heads_q, self.n_heads_kv)[0]
@cached_property
def n_heads_kv_local(self) -> int:
"""
Number of local heads post sharding.
"""
return get_local_heads(self.tp_rank, self.tp_size, self.n_heads_q, self.n_heads_kv)[1]
@property
def gated_mlp(self) -> bool:
"""
Return a boolean to determine whether the model uses a gated activation function.
"""
return is_gated(self.mlp_activation_fn)
"""
Method implementations
"""
def __init__(self, config: DSModelImplementationConfig, engine_config: RaggedInferenceEngineConfig,
base_mp_group: MPType) -> None:
"""
Base implementation for initialization. By default, this will initialize
the traditional components of a transformer model:
- Embedding
- QKV projection
- Self attention
- Attention output projection
- Feed forward network
- Normalization
- Unembedding
Arguments:
config (DSModelImplementationConfig): Model-specific configuration. No assumptions
should be made about this config that are not closely tied to the specific
model implementation.
engine_config (RaggedInferenceEngineConfig): Engine configuration.
base_mp_group (MPType): Base communication group for Tensor-parallel inference.
"""
super().__init__(config, engine_config, base_mp_group)
self.make_norm_layer()
self.make_qkv_layer()
self.make_attn_layer()
self.make_attn_out_layer()
self.make_mlp_1_layer()
self.make_mlp_2_layer()
self.make_embedding_layer()
self.make_unembedding_layer()
self._kv_cache_config = None
######### Embedding #########
def make_embedding_layer(self) -> None:
"""
Performs setup and creates embedding DSModule. This will set the `self.embed` attribute.
"""
embed_config = DSEmbeddingsConfig(
max_tokens=self._engine_config.state_manager.max_ragged_batch_size,
residual_dtype=self.activation_dtype,
embedding_dim=self.model_dim,
)
self.embed = heuristics.instantiate_embed(embed_config, self._engine_config)
def transform_embedding_param(self, param: torch.Tensor) -> InferenceParameter:
"""
Performs embedding sharding along the channels dimension.
"""
# Until we can do non-contiguous all-gather, we won't shard the embedding parameters.
param = param.to(self.activation_dtype.value)
return InferenceParameter.initialize(param)
######### Unembedding #########
def make_unembedding_layer(self) -> None:
"""
Performs setup and creates an unembedding layer. This implementation assumes
normalization prior to the LM head projection. If this does not match the model's
implementation, override this method. This will set the ``self.unembed`` attribute.
"""
unembed_dim = sharded_unembed_dim(self.vocab_size, self.tp_rank, self.tp_size)
unembed_config = DSUnembedConfig(
max_tokens=self._engine_config.state_manager.max_ragged_batch_size,
max_sequences=self._engine_config.state_manager.max_ragged_sequence_count,
dtype=self.activation_dtype,
model_dim=self.model_dim,
vocab_size=unembed_dim,
norm_type=self.norm_type,
)
self.unembed = heuristics.instantiate_unembed(unembed_config, self._engine_config)
if self.tp_size > 1:
self._comm_logits = torch.empty(self.tp_size,
self._engine_config.state_manager.max_ragged_sequence_count,
unembed_dim,
device=get_accelerator().current_device(),
dtype=self.activation_dtype.value)
self._return_logits = torch.empty(self._engine_config.state_manager.max_ragged_sequence_count,
self.vocab_size,
device=get_accelerator().current_device(),
dtype=self.activation_dtype.value)
def transform_unembed_param(self, param: torch.Tensor) -> InferenceParameter:
"""
Performs sharding along the vocab dimension.
"""
param = shard_unembed_param(param, self.tp_rank, self.tp_size).to(self.activation_dtype.value)
return InferenceParameter.initialize(param)
######### QKV #########
def make_qkv_layer(self) -> None:
"""
Instantiates the linear projection layer for the QKV linear layer. This sets the
`self.qkv` attribute.
"""
out_features = qkv_out_features(self.model_dim, self.tp_rank, self.tp_size, self.head_size, self.n_heads_q,
self.n_heads_kv)
linear_config = DSLinearConfig(
max_tokens=self._engine_config.state_manager.max_ragged_batch_size,
in_channels=self.model_dim,
out_channels=out_features,
input_dtype=self.activation_dtype,
output_dtype=self.activation_dtype,
)
self.qkv = heuristics.instantiate_linear(linear_config, self._engine_config)
def transform_qkv_param(self, param: torch.Tensor) -> InferenceParameter:
"""
Passes a QKV parameter to the underlying implementation for any necessary
transformations.
Args:
param (torch.Tensor): The parameter to transform. This may be either a bias or weight and should have
the shape (out_neurons, in_neurons)
"""
param = shard_qkv_param(param, self.tp_rank, self.tp_size, self.head_size, self.n_heads_q, self.n_heads_kv)
return self.qkv.transform_param(param)
######### Attention #########
def make_attn_layer(self) -> None:
"""
Builds the attention layer for the model. This sets the `self.attn` attribute.
"""
softmax_scale = 1.0 / (self.head_size**0.5)
attn_config = DSSelfAttentionConfig(max_tokens=self._engine_config.state_manager.max_ragged_batch_size,
n_heads_q=self.n_heads_q_local,
n_heads_kv=self.n_heads_kv_local,
head_size=self.head_size,
max_sequences=self._engine_config.state_manager.max_ragged_sequence_count,
scale_factor=softmax_scale,
input_dtype=self.activation_dtype,
output_dtype=self.activation_dtype,
positional_embedding_type=self.positional_embedding_type,
positional_embedding_config=self.positional_embedding_config)
self.attn = heuristics.instantiate_attention(attn_config, self._engine_config)
def get_kv_requirements(self, sequence: DSSequenceDescriptor, max_new_tokens: int,
max_new_blocks: int) -> Tuple[int, int]:
"""
See ``DSInferenceModelBase.get_kv_requirements`` for documentation.
This method assumes an autoregressive dense attention pattern. Override this method
if this does not match the model's attention pattern.
"""
total_tokens = sequence.seen_tokens + max_new_tokens
req_blocks = ceil_div(total_tokens, self.attn.kv_block_size)
block_lim = req_blocks - sequence.cur_allocated_blocks
if block_lim <= max_new_blocks:
return max_new_tokens, block_lim
token_capacity = (max_new_blocks +
sequence.cur_allocated_blocks) * self.attn.kv_block_size - sequence.seen_tokens
return token_capacity, max_new_blocks
def get_remaining_block_capacity(self, sequence: DSSequenceDescriptor) -> int:
return sequence.seen_tokens % self.attn.kv_block_size
def maybe_allocate_kv(self, sequence: DSSequenceDescriptor, n_new_tokens: int) -> None:
"""
See ``DSInferenceModelBase.maybe_allocate_kv`` for documentation.
This method assumes an autoregressive dense attention pattern. Override this method
if this does not match the model's attention pattern.
"""
free_block = self.state_manager.free_blocks[0]
_, n_needed_blocks = self.get_kv_requirements(sequence, n_new_tokens, free_block)
if n_needed_blocks > 0:
new_blocks = self.state_manager.allocate_blocks(n_needed_blocks)
sequence.extend_kv_cache(new_blocks)
def kv_cache_config(self) -> Tuple[KVCacheConfig, ...]:
"""
See ``DSInferenceModelBase.kv_cache_config`` for documentation.
This method assumes an autoregressive dense attention pattern. Override this method
if this does not match the model's attention pattern.
"""
if self._kv_cache_config is None:
cache_shape = (self.num_layers, self.n_heads_kv_local, self.head_size)
max_blocks = ceil_div(self.max_sequence_length, self.attn.kv_block_size)
self._kv_cache_config = KVCacheConfig(block_size=self.attn.kv_block_size,
cache_shape=cache_shape,
cache_dtype=self.activation_dtype,
max_blocks_per_allocation_group=max_blocks)
return (self._kv_cache_config, )
def prepare_batch(self, wrapped_batch: RaggedBatchWrapper) -> None:
"""
See ``DSInferenceModelBase.prepare_batch`` for documentation.
This method assumes an autoregressive dense attention pattern. Override this method
if this does not match the model's attention pattern.
"""
self.attn.build_atoms(wrapped_batch)
######### Attention output #########
def make_attn_out_layer(self) -> None:
"""
Instantiates the linear projection layer for the attention output linear layer. This sets the
`self.attn_out` attribute.
"""
in_features = attn_out_in_features(self.model_dim, self.tp_rank, self.tp_size, self.head_size, self.n_heads_q,
self.n_heads_kv)
linear_config = DSLinearConfig(
max_tokens=self._engine_config.state_manager.max_ragged_batch_size,
in_channels=in_features,
out_channels=self.model_dim,
input_dtype=self.activation_dtype,
output_dtype=self.activation_dtype,
)
self.attn_out = heuristics.instantiate_linear(linear_config, self._engine_config)
def transform_attn_out_param(self, param: torch.Tensor) -> Optional[InferenceParameter]:
"""
Shards an attention output projection parameter and passes it to the underlying
implementation for any necessary transformations. This will return `None` for bias parameters
if they are not on TP rank 0.
Args:
param (torch.Tensor): The parameter to transform. This may be either a bias or weight and should have
the shape (out_neurons, in_neurons).
"""
param = shard_attn_out_param(param, self.tp_rank, self.tp_size, self.head_size, self.n_heads_q,
self.n_heads_kv)
if param is not None:
param = self.attn_out.transform_param(param)
return param
######### MLP #########
def make_mlp_1_layer(self) -> None:
"""
Instantiates the linear projection layer for the first MLP in the feedforward network.
This sets the `self.mlp_1` attribute.
"""
shard_size = sharded_intermediate_dim(self.intermediate_dim, self.tp_size, self.tp_rank)
linear_config = DSLinearConfig(
max_tokens=self._engine_config.state_manager.max_ragged_batch_size,
in_channels=self.model_dim,
out_channels=shard_size,
activation=self.mlp_activation_fn,
input_dtype=self.activation_dtype,
output_dtype=self.activation_dtype,
)
self.mlp_1 = heuristics.instantiate_linear(linear_config, self._engine_config)
def transform_mlp_1_param(self, param: torch.Tensor) -> InferenceParameter:
"""
Shards the first MLP parameter and passes it to the underlying implementation
for any necessary transformations.
Args:
param (torch.Tensor): The parameter to transform. This may be either a bias or weight and should have
the shape (out_neurons, in_neurons).
"""
param = shard_mlp_1_param(param, self.tp_rank, self.tp_size, gated=self.gated_mlp)
return self.mlp_1.transform_param(param)
def make_mlp_2_layer(self) -> None:
"""
Instantiates the linear projection layer for the second MLP in the feedforward network.
This sets the `self.mlp_2` attribute.
"""
shard_size = sharded_intermediate_dim(self.intermediate_dim, self.tp_size, self.tp_rank)
linear_config = DSLinearConfig(
max_tokens=self._engine_config.state_manager.max_ragged_batch_size,
in_channels=shard_size,
out_channels=self.model_dim,
input_dtype=self.activation_dtype,
output_dtype=self.activation_dtype,
)
self.mlp_2 = heuristics.instantiate_linear(linear_config, self._engine_config)
def transform_mlp_2_param(self, param: torch.Tensor) -> Optional[InferenceParameter]:
"""
Shards the second MLP parameter and passes it to the underlying implementation
for any necessary transformations. This will return `None` for bias parameters
if they are not on TP rank 0.
Args:
param (torch.Tensor): The parameter to transform. This may be either a bias or weight and should have
the shape (out_neurons, in_neurons).
"""
param = shard_mlp_2_param(param, self.tp_rank, self.tp_size)
if param is not None:
param = self.mlp_2.transform_param(param)
return param
######### Norm #########
def make_norm_layer(self) -> None:
"""
Instantiates the normalization layer for the model. This sets the `self.norm` attribute.
TODO(cmikeh2): In the future we'll distinguish between the different norm objects,
but for now we'll just use the same one for all of them.
"""
norm_config = DSNormConfig(
max_tokens=self._engine_config.state_manager.max_ragged_batch_size,
type=self.norm_type,
channels=self.model_dim,
residual_dtype=self.activation_dtype,
input_dtype=self.activation_dtype,
output_dtype=self.activation_dtype,
)
self.norm = heuristics.instantiate_pre_norm(norm_config, self._engine_config)
def transform_norm_param(self, param: torch.Tensor) -> InferenceParameter:
"""
Passes a normalization parameter to the underlying implementation for any
necessary transformations.
TODO(cmikeh2): In the future we'll distinguish between the different norm objects,
but for now we'll just use the same one for all of them.
Args:
param (torch.Tensor): The parameter to transform. This may be either a bias or weight and should have
shape (model_dim,)
"""
return self.norm.transform_param(param)
class DSMoETransformerModelBase(DSTransformerModelBase):
@property
def n_experts(self) -> int:
"""
Return the number of experts in the model.
"""
raise NotImplementedError("Attempted to access an unimplemented number of experts")
@property
def n_top_k(self) -> int:
"""
Number of experts per token.
"""
raise NotImplementedError("Attempted to access an unimplemented number of experts per token")
@property
def normalize_expert_scores(self) -> bool:
"""
Whether to normalize expert scores. If true, sum(expert_scores) = 1.
"""
raise NotImplementedError("Attempted to access an unimplemented normalization flag")
def make_moe_layer(self) -> None:
"""
Instantiates the MoE layer for the model. This sets the `self.moe` attribute.
"""
sharded_dim = sharded_intermediate_dim(self.intermediate_dim, self.tp_size, self.tp_rank)
moe_config = DSMoEConfig(
max_tokens=self._engine_config.state_manager.max_ragged_batch_size,
model_dim=self.model_dim,
intermediate_features=sharded_dim,
activation=self.mlp_activation_fn,
n_experts=self.n_experts,
top_k=self.n_top_k,
input_dtype=self.activation_dtype,
output_dtype=self.activation_dtype,
normalize_scores=self.normalize_expert_scores,
)
self.moe = heuristics.instantiate_moe(moe_config, self._engine_config)
def transform_moe_gate_param(self, param: torch.Tensor) -> InferenceParameter:
"""
Passes a MoE gate parameter to the underlying implementation for any necessary transformations.
TODO(cmikeh2): This will need to be updated/overridden for expert parallelism.
"""
return self.moe.transform_gate_param(param)
def transform_moe_mlp_1_param(self, param: torch.Tensor) -> InferenceParameter:
"""
Shards the first MoE param and passes it to the underlying implementation. Since it's possible for an architecture
to have both MoE and non-MoE layers, this can't be overloaded on the MLP1 transform. Furthermore, since both
the MoE DSModule owns both MLP1 and MLP2, under certain sharding conditions it's not possible for the model implementation
to infer from the shape whether to perform a different transformation based on MLP1 or MLP2. This (and the below)
separations are intended to solve both these issues.
Args:
param (torch.Tensor): The parameter to transform. This should have shape (n_experts, out_neurons, in_neurons).
"""
param = shard_mlp_1_param(param, self.tp_rank, self.tp_size, gated=self.gated_mlp, is_moe=True)
return self.moe.transform_moe_mlp_1_param(param)
def transform_moe_mlp_2_param(self, param: torch.Tensor) -> Optional[torch.Tensor]:
"""
Shards the second MoE param and passes it to the underlying implementation. See the above for context on why this API
exists.
This will return `None` for expert bias params not on TP rank 0. NOTE(cmikeh2): Does it make sense to round-robin assign?
My intuition is that this will make debugging much more difficult for minimal memory reduction.
Args:
param (torch.Tensor): The parameter to transform. This should have shape (n_experts, out_neurons, in_neurons).
"""
param = shard_mlp_2_param(param, self.tp_rank, self.tp_size, is_moe=True)
if param is not None:
param = self.moe.transform_moe_mlp_2_param(param)
return param