357 lines
15 KiB
Python
357 lines
15 KiB
Python
# Copyright (c) Microsoft Corporation.
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
# DeepSpeed Team
|
|
|
|
import re
|
|
from typing import Type
|
|
|
|
import torch
|
|
|
|
from deepspeed.accelerator import get_accelerator
|
|
from deepspeed.compat import get_annotations_from_namespace, get_annotations
|
|
from .parameter_base import ParameterBase, ParametrizedList
|
|
from ..inference_parameter import InferenceParameter
|
|
|
|
# Currently have dependency loops for the type hints.
|
|
InferenceModel = Type["InferenceModel"]
|
|
LayerContainer = Type["LayerContainer"] # noqa: F811
|
|
|
|
MAPPING_KEY = "PARAM_MAPPING"
|
|
PLIST_HELPERS = "_ds_plist_strip_vals"
|
|
|
|
|
|
def make_finalization_callback(all_names: str):
|
|
"""
|
|
Helper method for building the finalization callback for a LayerContainer. This
|
|
is not client code and should not be used or called directly.
|
|
"""
|
|
|
|
def finalization_callback(self, param: ParameterBase, finalized_param: torch.Tensor) -> None:
|
|
"""
|
|
Callback for when a parameter is finalized.
|
|
"""
|
|
self._finalized_params += 1
|
|
|
|
for name in all_names:
|
|
if getattr(self, name) is param:
|
|
setattr(self, name, finalized_param)
|
|
|
|
return finalization_callback
|
|
|
|
|
|
class LayerMetaclass(type):
|
|
"""
|
|
MetaClass for the LayerContainer base class. This class will parse the annotations
|
|
of the class that correspond to `ParameterBase` and create None initializers for each
|
|
as well as a finalization callback that for when each `ParameterBase` is finalized
|
|
and should be replaced with a Tensor.
|
|
"""
|
|
|
|
def __new__(cls, clsname, bases, attrs):
|
|
|
|
annotations = get_annotations_from_namespace(attrs)
|
|
|
|
for base in bases:
|
|
# We'll pick up all annotations on any base classes. This will allow us to
|
|
# to use inheritance to share common parameter groups in base classes.
|
|
annotations.update(get_annotations(base))
|
|
|
|
if hasattr(base, MAPPING_KEY):
|
|
if MAPPING_KEY not in attrs:
|
|
# This is likely a fail state. If a parent has MAPPING KEY but the child does
|
|
# not, then we're guaranteed only a subset of the parameters will be mapped.
|
|
attrs[MAPPING_KEY] = {}
|
|
attrs[MAPPING_KEY].update(getattr(base, MAPPING_KEY))
|
|
|
|
all_names = [name for name, annotation in annotations.items() if issubclass(annotation, ParameterBase)]
|
|
|
|
if MAPPING_KEY in attrs:
|
|
# If we have a mapping key at all, then we will enter the validation mode for building
|
|
# helpers for mapping and ensuring we have complete mapping.
|
|
|
|
# First we'll build a flat list of every dependency for this layer.
|
|
all_deps = set()
|
|
for name in all_names:
|
|
parameter_deps = [
|
|
name for name, annotation in get_annotations(annotations[name]).items()
|
|
if issubclass(annotation, (torch.Tensor, ParametrizedList))
|
|
]
|
|
|
|
all_deps.update([f"{name}.{dep}" for dep in parameter_deps])
|
|
|
|
# Create static helper for doing the string processing only once.
|
|
attrs[PLIST_HELPERS] = []
|
|
|
|
# Iterate over all the mappings
|
|
for src_name, target_or_targets in attrs[MAPPING_KEY].items():
|
|
if isinstance(target_or_targets, str):
|
|
target_or_targets = [target_or_targets]
|
|
|
|
actual_targets = []
|
|
for target_name in target_or_targets:
|
|
base_dependency, dependency_attr = target_name.split(".")
|
|
|
|
# Check for invalid mappings
|
|
if base_dependency not in all_names:
|
|
raise ValueError(
|
|
"Target parameter \"{}\" not found in this layer. Valid targets are {}".format(
|
|
base_dependency, all_names))
|
|
if dependency_attr not in get_annotations(annotations[base_dependency]):
|
|
# This check is not universal (see below) if a single dependency is being
|
|
# mapped to by a single row.
|
|
raise ValueError(
|
|
"Target dependency \"{}\" not found on parameter \"{}\". Valid targets are {}".format(
|
|
dependency_attr, base_dependency,
|
|
get_annotations(annotations[base_dependency]).keys()))
|
|
if target_name not in all_deps:
|
|
raise ValueError(
|
|
"Target dependency \"{}\" was targeted with multiple mapping rules.".format(target_name))
|
|
|
|
# If we've made it this far, the dependency definitely exists.
|
|
actual_targets.append(get_annotations(annotations[base_dependency])[dependency_attr])
|
|
|
|
all_deps.remove(target_name)
|
|
|
|
are_plists = [issubclass(target, ParametrizedList) for target in actual_targets]
|
|
if all(are_plists):
|
|
# We can do direct sets on everything but ParametrizedLists, so we'll only explicitly
|
|
# handle these here.
|
|
# TODO(cmikeh2): SPLIT, error if more than 1
|
|
glob_count = src_name.count("*")
|
|
if glob_count > 1:
|
|
raise ValueError(
|
|
"ParametrizedList index inference can only work with a single glob: {}".format(src_name))
|
|
elif glob_count == 0:
|
|
raise ValueError(
|
|
"Must have wildcard (*) in source name for ParametrizedList mapping: {}".format(src_name))
|
|
|
|
wildcard_idx = src_name.find("*")
|
|
prefix = src_name[:wildcard_idx]
|
|
suffix = src_name[wildcard_idx + 1:]
|
|
attrs[PLIST_HELPERS].append((prefix, suffix, target_or_targets))
|
|
elif any(are_plists):
|
|
raise ValueError("Cannot mix ParametrizedLists and Tensors in a single mapping rule.")
|
|
|
|
if len(all_deps) > 0:
|
|
raise ValueError(
|
|
"A parameter mapping was provided for {}, but the following dependencies were not mapped: {}".
|
|
format(clsname, all_deps))
|
|
|
|
attrs["finalization_callback"] = make_finalization_callback(all_names)
|
|
|
|
new_obj = super().__new__(cls, clsname, bases, attrs)
|
|
|
|
setattr(new_obj, "_n_params", len(all_names))
|
|
setattr(new_obj, "_annotation_attrs", all_names)
|
|
|
|
return new_obj
|
|
|
|
def __call__(cls, *args, **kwargs):
|
|
instance = cls.__new__(cls, *args, **kwargs)
|
|
instance.__init__(*args, **kwargs)
|
|
|
|
for name, annotation in get_annotations(instance).items():
|
|
if issubclass(annotation, ParameterBase):
|
|
# TODO(cmikeh2): Do we want to make this a property
|
|
# It might also make sense to do this in the base class __init__
|
|
# but since it is tied with the changes made in __new__ it feels
|
|
# to me like it should be here.
|
|
setattr(instance, name, annotation(instance.inference_model, instance))
|
|
|
|
return instance
|
|
|
|
|
|
class LayerContainer(metaclass=LayerMetaclass): # noqa: F811
|
|
"""
|
|
Abstract base class for containing model parameters.
|
|
|
|
This is primarily a guidance abstraction since we do not put any restrictions
|
|
on how the parameters are stored.
|
|
|
|
To use this class, annotate the class with `ParameterBase` subclasses and give them
|
|
names. As a checkpoint is loaded into this container, the `ParameterBase` instances
|
|
will be replaced with realized Tensors as soon as each of their dependencies are met.
|
|
|
|
To enable automatic mapping, add a static attribute `PARAM_MAPPING` to the class
|
|
definition. This should be a dictionary mapping from a source string to one or
|
|
more dependencies.
|
|
|
|
```python
|
|
class MyLayer(LayerContainer):
|
|
PARAM_MAPPING = {
|
|
"path.to.param.dependency", "container_param_1.dependency",
|
|
"path.to.param2.dependency", "container_param_2.dependency",
|
|
"path.to.param3.*.dependency", "container_param_3.list_dependency"
|
|
}
|
|
|
|
...
|
|
```
|
|
"""
|
|
|
|
def __init__(self, model: InferenceModel) -> None:
|
|
"""
|
|
Initialization of the LayerContainer. This method does not need to be overridden
|
|
for any children classes.
|
|
|
|
Args:
|
|
model (InferenceModel): Inference model that will be used to shard and transform
|
|
parameters correctly, as well as provide specific information about the model
|
|
for `ParameterizedList`s that may be part of one of the member `ParameterBase`s.
|
|
"""
|
|
self.inference_model = model
|
|
self._finalized_params = 0
|
|
|
|
def _initialization_checker(self, check_device: bool = True) -> bool:
|
|
"""
|
|
Returns whether or not all parameters have been initialized and transformed by
|
|
the model. Once this returns True, all the `ParameterBase` instances will be
|
|
torch.Tensors.
|
|
"""
|
|
if self._finalized_params != self.n_params:
|
|
return False
|
|
|
|
for name in self._annotation_attrs:
|
|
tensor = getattr(self, name)
|
|
if tensor is None:
|
|
continue
|
|
elif not isinstance(tensor, InferenceParameter):
|
|
raise ValueError("Layer should be finalized, but {} ({}) is neither InferenceParameter or None".format(
|
|
name, type(tensor)))
|
|
elif check_device and tensor.device != torch.device(get_accelerator().current_device()):
|
|
raise RuntimeError("Layer should be finalized, but {} is not on device {}".format(
|
|
name,
|
|
get_accelerator().current_device()))
|
|
return True
|
|
|
|
@property
|
|
def is_populated(self) -> bool:
|
|
"""
|
|
Returns whether or not all parameters have been populated by the checkpoint engine, but
|
|
does not validat the parameters are on the correct device.
|
|
"""
|
|
return self._initialization_checker(check_device=False)
|
|
|
|
@property
|
|
def is_initialized(self) -> bool:
|
|
"""
|
|
Returns whether or not all parameters have been initialized and transformed by
|
|
the model and are located on the appropriate device. Once this returns True, all
|
|
the `ParameterBase` instances ``InferenceParameter``s or explicitly set to ``None``.
|
|
"""
|
|
return self._initialization_checker()
|
|
|
|
@property
|
|
def n_params(self) -> int:
|
|
"""
|
|
The number of parameters this container holds. This is a read-only value
|
|
that is set by the metaclass.
|
|
"""
|
|
return self._n_params
|
|
|
|
@property
|
|
def annotation_attrs(self) -> list:
|
|
return self._annotation_attrs
|
|
|
|
@property
|
|
def mapping_params(self) -> dict:
|
|
return getattr(self.__class__, MAPPING_KEY, {})
|
|
|
|
@property
|
|
def plist_helpers(self) -> list:
|
|
return getattr(self.__class__, PLIST_HELPERS, [])
|
|
|
|
def direct_injection(self, name: str, tensor: InferenceParameter) -> None:
|
|
|
|
if name not in self._annotation_attrs:
|
|
raise ValueError(f"Cannot directly inject {name}, not a valid parameter.")
|
|
|
|
setattr(self, name, tensor)
|
|
self._finalized_params += 1
|
|
|
|
def set_dependency(self, dep_name: str, dep_value: torch.Tensor) -> None:
|
|
"""
|
|
Set dependency can be used for managing dependencies when a mapping is provided
|
|
in the class definition for the layer. The dep_name here should have any prefix
|
|
for transformer layers removed (such as model.layers.*.attn.qkv.weight -> attn.qkv.weight).
|
|
|
|
Args:
|
|
dep_name (str): The name of the dependency to set.
|
|
dep_value (torch.Tensor): The value to set the dependency to.
|
|
"""
|
|
|
|
def get_dep_name_target(dep_name: str) -> str:
|
|
"""
|
|
Helper method for getting the target name for a dependency from the
|
|
mapping params. Tries to match exact string first, then looks for
|
|
wildcards and attempts regex matching. Will return empty string if
|
|
no match found.
|
|
"""
|
|
if dep_name in self.mapping_params:
|
|
# If we have an exact match, it's a direct mapping and we can
|
|
# immediately set the value.
|
|
return self.mapping_params[dep_name]
|
|
|
|
matched_targets = []
|
|
for key, target in self.mapping_params.items():
|
|
regex_key = key.replace("*", ".*")
|
|
if re.match(regex_key, dep_name):
|
|
matched_targets.append(target)
|
|
if len(matched_targets) > 1:
|
|
raise ValueError(f"Multiple targets matched for dependency {dep_name}: {matched_targets}")
|
|
if matched_targets:
|
|
return matched_targets[0]
|
|
return ""
|
|
|
|
if dep_name in self.mapping_params:
|
|
# If we have an exact match, it's a direct mapping and we can immediately set
|
|
# the value.
|
|
target = self.mapping_params[dep_name]
|
|
|
|
# Convert single targets to a list for consistency
|
|
if isinstance(target, str):
|
|
target = [target]
|
|
|
|
for target_name in target:
|
|
# Double setting doesn't set the attribute correctly, so we do a getattr then setattr
|
|
target_param_name, target_dependency_name = target_name.split(".")
|
|
target_param = getattr(self, target_param_name)
|
|
setattr(target_param, target_dependency_name, dep_value)
|
|
return
|
|
|
|
# Otherwise we need to map to one of the parameter lists.
|
|
for prefix, suffix, dests in self.plist_helpers:
|
|
if dep_name.startswith(prefix) and dep_name.endswith(suffix):
|
|
# We have a match, so we can set the value.
|
|
target_idx = int(dep_name[len(prefix):-len(suffix)])
|
|
|
|
# Convert single targets to a list for consistency
|
|
if isinstance(dests, str):
|
|
dests = [dests]
|
|
|
|
for dest in dests:
|
|
target_param_name, target_dependency_name = dest.split(".")
|
|
target_param = getattr(self, target_param_name)
|
|
target_dependency = getattr(target_param, target_dependency_name)
|
|
target_dependency[target_idx] = dep_value
|
|
return
|
|
|
|
# TODO: Refactor this with the help of cmikeh2
|
|
# We should be able to combine this with the wildcard matching above.
|
|
target = get_dep_name_target(dep_name)
|
|
if target:
|
|
# Convert single targets to a list for consistency
|
|
if isinstance(target, str):
|
|
target = [target]
|
|
|
|
for target_name in target:
|
|
# Double setting doesn't set the attribute correctly, so we do a getattr then setattr
|
|
target_param_name, target_dependency_name = target_name.split(".")
|
|
target_param = getattr(self, target_param_name)
|
|
setattr(target_param, target_dependency_name, dep_value)
|
|
return
|
|
|
|
raise ValueError(
|
|
"Could not find a mapping for dependency \"{}\". Check that it is included in the ``MAPPING_PARAMS``. See docstring for more on ``MAPPING_PARAMS``"
|
|
.format(dep_name))
|