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

221 lines
9.3 KiB
Python

# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
import json
from abc import ABC, ABCMeta, abstractmethod
from typing import Any, Iterable, List, Optional, Union
import torch
from ..config_v2 import RaggedInferenceEngineConfig
from ..checkpoint import CheckpointEngineBase
from ..logging import inference_logger
from .layer_container_base import LayerContainer
from .inference_model_base import DSInferenceModelBase
from .flat_model_helpers import (
flatten_inference_model,
make_param_filename,
make_metadata_filename,
ModelMetadata,
restore_inference_model,
)
POLICIES = {}
class ContainerMap:
def __init__(self) -> None:
self._prefix_map = {}
self._transformer_params = None
self._non_transformer_params = None
@property
def transformer_params(self) -> Iterable[LayerContainer]:
return self._transformer_params
@property
def non_transformer_params(self) -> LayerContainer:
return self._non_transformer_params
def set_transformer_params(self, prefixes: Union[str, Iterable[str]], containers: List[LayerContainer]) -> None:
if not isinstance(containers, list):
raise ValueError(
f"The transformer containers should be a list, of one container per layer, but got {type(containers)} instead."
)
self._transformer_prefixes = prefixes if isinstance(prefixes, list) else [prefixes]
self._transformer_params = containers
def set_non_transformer_params(self, container: LayerContainer) -> None:
self._non_transformer_params = container
def set_unmapped_params(self, prefixes: Union[str, Iterable[str]]) -> None:
self._unmapped_prefixes = prefixes
def map_param(self, name, parameter) -> None:
for unmapped_prefix in self._unmapped_prefixes:
if name.startswith(unmapped_prefix):
inference_logger().debug(f"Ignoring: {name} for {unmapped_prefix}")
return
for transformer_prefix in self._transformer_prefixes:
if name.startswith(transformer_prefix):
popped_name = name[len(transformer_prefix) + 1:]
layer_idx = popped_name.split(".")[0]
assert layer_idx.isdigit(
), f"expected name to start w. list index but got {layer_idx} instead, name={name}"
layer_idx = int(layer_idx)
inference_logger().debug(
f"Setting: {'.'.join(popped_name.split('.')[1:])} for layer-idx={layer_idx} to {parameter.shape}")
self._transformer_params[layer_idx].set_dependency(".".join(popped_name.split(".")[1:]), parameter)
return
try:
inference_logger().debug(f"Setting: {name} to {parameter.shape}")
self._non_transformer_params.set_dependency(name, parameter)
except ValueError:
# Catch the ValueError here from the non_transformer_params because we are knowingly
# calling it with something that may not match. This should allow us to raise a slightly more
# informative error message.
raise ValueError(f"Cannot find container for {name}, please double check the Containers/ContainerMap")
def validate(self) -> None:
if not self._non_transformer_params.is_initialized:
raise RuntimeError("Non-transformer parameters not fully initialized after checkpoint load.")
for layer_idx, container in enumerate(self._transformer_params):
if not container.is_initialized:
raise RuntimeError(
f"Transformer container at index {layer_idx} not fully initialized after checkpoint load.")
class PolicyMeta(ABCMeta):
def __new__(cls, name, bases, dct):
new_obj = super().__new__(cls, name, bases, dct)
if name != "InferenceV2Policy":
POLICIES[name] = new_obj
return new_obj
class InferenceV2Policy(ABC, metaclass=PolicyMeta):
"""
The InferenceV2Policy is the base class for all inference policies. An inference policy
is responsible for instantiating the inference model and mapping the parameters from the
checkpoint engine to the model itself.
"""
def __init__(
self,
model_config: Any,
checkpoint_engine: Optional[CheckpointEngineBase] = None,
inf_checkpoint_path: Optional[str] = None,
) -> None:
"""
Create the Policy with sufficient context to build the model. There are two supported
model creation mechanisms.
The first is the generalized ``checkpoint_engine`` which
will iterate over the parameters of the model and provide them to the policy. These in
turn will be sharded/transformed by the model implementation.
The second is used to re-create a previously serialized DeepSpeed inference model. These
checkpoints should not be used across different model backend configurations.
TODO(cmikeh2): Enforce this in code
"""
if checkpoint_engine is None and inf_checkpoint_path is None:
raise ValueError("Either checkpoint_engine or ds_checkpoint_path must be provided.")
if checkpoint_engine is not None and inf_checkpoint_path is not None:
raise ValueError("Only one of checkpoint_engine or ds_checkpoint_path can be provided.")
self._checkpoint_engine = checkpoint_engine
self._inf_checkpoint_path = inf_checkpoint_path
self._model_config = model_config
def build_model(self, engine_config: RaggedInferenceEngineConfig, mp_group: Any) -> DSInferenceModelBase:
"""
Completely instantiate the inference model. This will both create the ops needed to run the
model, as well as load the model parameters via the checkpoint engine. For more context
on each of these components please see ``instantiate_model`` and ``populate_model_parameters``.
Arguments:
engine_config: The config that has been used to instantiate the engine. This is used
to communicate to the model implementation the limits on batches (sequences/tokens)
and bound the size of intermediate buffers.
mp_group: Object to enable communication between tensor parallel ranks.
Returns:
DSInferenceModelBase: An implementation of the inference model abstraction that will be
run by the engine.
"""
self.model = self.instantiate_model(engine_config, mp_group)
self.populate_model_parameters()
return self.model
@abstractmethod
def instantiate_model(self, engine_config: RaggedInferenceEngineConfig) -> DSInferenceModelBase:
"""
Instantiate the inference model. Depending on the engine/model config, this could be where
different model implementations could be selected.
Arguments:
engine_config: The config that has been used to instantiate the engine. This is used
to communicate to the model implementation the limits on batches (sequences/tokens)
and bound the size of intermediate buffers.
Returns:
DSInferenceModelBase: An implementation of the inference model abstraction that will be
run by the engine.
"""
...
@abstractmethod
def build_container_map(self) -> ContainerMap:
"""
Build a dictionary representing the structure of the string prefixes leading
to the parameters to be mapped to the container.
Returns:
ContainerMap: An instantiated mapping describing how checkpoint prefixes map
to ``LayerContainer`` instances.
"""
raise NotImplementedError()
def populate_model_parameters(self) -> None:
"""
This model will iterate over the parameters (as provided by the checkpoint engine) and
use the container map built by ``build_container_map`` to populate the model
"""
container_map = self.build_container_map()
if self._checkpoint_engine is not None:
for name, parameter in self._checkpoint_engine.parameters():
container_map.map_param(name, parameter)
buffer, metadata = flatten_inference_model(container_map.transformer_params,
container_map.non_transformer_params, self.__class__.__name__)
else:
buffer_path = make_param_filename(self._inf_checkpoint_path, self.model.tp_rank, self.model.tp_size)
metadata_path = make_metadata_filename(self._inf_checkpoint_path, self.model.tp_rank, self.model.tp_size)
buffer = torch.load(buffer_path, weights_only=False)
metadata = json.load(open(metadata_path, "r"))
metadata = ModelMetadata.parse_raw(metadata)
restore_inference_model(buffer, metadata, container_map.transformer_params,
container_map.non_transformer_params)
container_map.validate()
self.model.set_parameters(transformer=container_map.transformer_params,
non_transformer=container_map.non_transformer_params,
flattened_param_buffer=buffer,
flattened_param_metadata=metadata)