413 lines
16 KiB
Python
413 lines
16 KiB
Python
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||
#
|
||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||
# you may not use this file except in compliance with the License.
|
||
# You may obtain a copy of the License at
|
||
#
|
||
# http://www.apache.org/licenses/LICENSE-2.0
|
||
#
|
||
# Unless required by applicable law or agreed to in writing, software
|
||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
# See the License for the specific language governing permissions and
|
||
# limitations under the License
|
||
|
||
import copy
|
||
import inspect
|
||
|
||
import paddle
|
||
from paddle.framework import Block
|
||
from paddle.static import Parameter, Variable
|
||
|
||
from .dist_attribute import TensorDistAttr
|
||
from .utils import __no_shape_var_type__, _linear_idx2coordinate
|
||
|
||
|
||
class DistributedTensor:
|
||
"""
|
||
DistributedTensor represents the distribution of tensor on the process group and
|
||
local tensors can be created by DistributedTensor.
|
||
Only support even sharding now and uneven sharding will be supported in the future.
|
||
Local tensor information can be obtained from the DistributedTensor instance object,
|
||
or obtained by the static methods provided by DistributedTensor,
|
||
including shard (i.e. the index in the serial tensor), offsets, and sizes.
|
||
"""
|
||
|
||
@staticmethod
|
||
def _validate_sizes_and_dist_attr(
|
||
sizes, dims_mapping, topology, processes, rank=None, shard_sizes=None
|
||
):
|
||
if not (
|
||
isinstance(sizes, (list, tuple))
|
||
and all(isinstance(x, int) and x >= 0 for x in sizes)
|
||
):
|
||
raise ValueError(
|
||
f"The sizes must be list or tuple and item in sizes must be non-negative integer, but got {sizes}"
|
||
)
|
||
if not (
|
||
isinstance(dims_mapping, (list, tuple))
|
||
and all(isinstance(x, int) and x >= -1 for x in dims_mapping)
|
||
):
|
||
raise ValueError(
|
||
f"The dims_mapping must be list or tuple and item in dims_mapping must >= -1, but got {dims_mapping}"
|
||
)
|
||
if not (
|
||
isinstance(processes, (list, tuple))
|
||
and all(isinstance(x, int) and x >= 0 for x in processes)
|
||
):
|
||
raise ValueError(
|
||
f"The processes must be list or tuple and item in processes must be integer, but got {processes}"
|
||
)
|
||
if not (
|
||
isinstance(topology, (list, tuple))
|
||
and all(isinstance(x, int) and x > 0 for x in topology)
|
||
):
|
||
raise ValueError(
|
||
f"The topology must be list or tuple and item in topology must be non-negative integer, but got {topology}"
|
||
)
|
||
if rank is not None and not (isinstance(rank, int) and rank >= 0):
|
||
raise ValueError(f"The rank must >= 0, but got {rank}")
|
||
|
||
# # NOTE: Only support even sharding now
|
||
# if shard_sizes is not None:
|
||
# raise ValueError("Only support even sharding now.")
|
||
|
||
@staticmethod
|
||
def get_local_sizes(
|
||
global_sizes,
|
||
dims_mapping,
|
||
topology,
|
||
processes,
|
||
rank=None,
|
||
shard_sizes=None,
|
||
):
|
||
DistributedTensor._validate_sizes_and_dist_attr(
|
||
global_sizes, dims_mapping, topology, processes, rank, shard_sizes
|
||
)
|
||
|
||
local_sizes = []
|
||
# for even sharding, the local sizes of every rank are equal
|
||
|
||
for idx, item in enumerate(global_sizes):
|
||
# This is a trick to avoid dims_mapping is []
|
||
val = dims_mapping[idx] if idx < len(dims_mapping) else -1
|
||
if val == -1:
|
||
local_sizes.append(item)
|
||
else:
|
||
local_sizes.append(item // topology[dims_mapping[idx]])
|
||
|
||
return local_sizes
|
||
|
||
@staticmethod
|
||
def get_local_offsets(
|
||
global_sizes, dims_mapping, topology, processes, rank, shard_sizes=None
|
||
):
|
||
local_sizes = DistributedTensor.get_local_sizes(
|
||
global_sizes, dims_mapping, topology, processes, rank, shard_sizes
|
||
)
|
||
local_offsets = []
|
||
rank_relative = processes.index(rank)
|
||
coordinate = _linear_idx2coordinate(topology, rank_relative)
|
||
|
||
for i in range(len(global_sizes)):
|
||
if dims_mapping[i] == -1:
|
||
local_offsets.append(0)
|
||
else:
|
||
local_offsets.append(
|
||
coordinate[dims_mapping[i]] * local_sizes[i]
|
||
)
|
||
return local_offsets
|
||
|
||
@staticmethod
|
||
def get_global_sizes(
|
||
local_sizes,
|
||
dims_mapping,
|
||
topology,
|
||
processes,
|
||
rank=None,
|
||
shard_sizes=None,
|
||
):
|
||
DistributedTensor._validate_sizes_and_dist_attr(
|
||
local_sizes, dims_mapping, topology, processes, rank, shard_sizes
|
||
)
|
||
global_sizes = []
|
||
for idx, item in enumerate(local_sizes):
|
||
if dims_mapping[idx] == -1:
|
||
global_sizes.append(item)
|
||
else:
|
||
global_sizes.append(item * topology[dims_mapping[idx]])
|
||
return global_sizes
|
||
|
||
@staticmethod
|
||
def get_local_shard(
|
||
global_sizes, dims_mapping, topology, processes, rank, shard_sizes=None
|
||
):
|
||
local_offsets = DistributedTensor.get_local_offsets(
|
||
global_sizes, dims_mapping, topology, processes, rank, shard_sizes
|
||
)
|
||
local_sizes = DistributedTensor.get_local_sizes(
|
||
global_sizes, dims_mapping, topology, processes, rank, shard_sizes
|
||
)
|
||
assert len(local_sizes) == len(local_offsets), (
|
||
f"The length of local_sizes must be equal to local_offsets, but got {len(local_sizes)} and {len(local_offsets)}."
|
||
)
|
||
|
||
local_end_offsets = [
|
||
x[0] + x[1] for x in zip(local_offsets, local_sizes)
|
||
]
|
||
local_shard = list(zip(local_offsets, local_end_offsets))
|
||
return local_shard
|
||
|
||
def __init__(self, serial_tensor, dist_attr=None, dist_context=None):
|
||
self._serial_tensor = serial_tensor
|
||
if dist_attr is not None and isinstance(dist_attr, TensorDistAttr):
|
||
# TODO: remove this deepcopy after we fix the issue
|
||
self._dist_attr = copy.deepcopy(dist_attr)
|
||
# self._dist_attr = dist_attr
|
||
# TODO: Do we really need to write dist_attr back to serial_tensor?
|
||
self._serial_tensor.dist_attr = dist_attr
|
||
else:
|
||
assert dist_attr is None, f"{dist_attr}"
|
||
# Use the dist attr of serial_tensor to do the initialization
|
||
self._dist_attr = self._serial_tensor.dist_attr
|
||
|
||
self._batch_dim = 0
|
||
self._local_offsets_map = {}
|
||
self._local_shard_map = {}
|
||
self._local_tensor_map = {}
|
||
|
||
from .dist_context import get_default_distributed_context
|
||
|
||
self._dist_context = (
|
||
dist_context
|
||
if dist_context is not None
|
||
else get_default_distributed_context()
|
||
)
|
||
# TODO: Add Automatically to dist_context after initialized and it will be adapted in the future.
|
||
# self._dist_context.add_dist_tensor_for_program(self)
|
||
|
||
@property
|
||
def serial_tensor(self):
|
||
return self._serial_tensor
|
||
|
||
@property
|
||
def dist_attr(self):
|
||
return self._dist_attr
|
||
|
||
@dist_attr.setter
|
||
def dist_attr(self, dist_attr):
|
||
self._dist_attr = dist_attr
|
||
# TODO: Do we really need to write back dist_attr to serial_tensor?
|
||
self._serial_tensor.dist_attr = dist_attr
|
||
|
||
@property
|
||
def dist_context(self):
|
||
return self._dist_context
|
||
|
||
# def _init_default_dist_attr(self):
|
||
# if self._dist_attr.dims_mapping is None:
|
||
# if self.serial_tensor.type in __no_shape_var_type__:
|
||
# tensor_shape = []
|
||
# else:
|
||
# tensor_shape = self._serial_tensor.shape
|
||
# tensor_dims_mapping = [-1 for _ in range(len(tensor_shape))]
|
||
# self._dist_attr.dims_mapping = tensor_dims_mapping
|
||
|
||
def validate_dist_attr(self):
|
||
if self.serial_tensor.type in __no_shape_var_type__:
|
||
return True
|
||
tensor_shape = self.serial_tensor.shape
|
||
if len(tensor_shape) != len(self.dist_attr.dims_mapping):
|
||
return False
|
||
for i in range(len(self.dist_attr.dims_mapping)):
|
||
if self.dist_attr.dims_mapping[
|
||
i
|
||
] < -1 or self.dist_attr.dims_mapping[i] >= len(
|
||
self.dist_attr.process_mesh.shape
|
||
):
|
||
return False
|
||
for i in range(len(self.dist_attr.process_mesh.shape)):
|
||
if self.dist_attr.dims_mapping.count(i) > 1:
|
||
return False
|
||
return True
|
||
|
||
def local_sizes(self, rank=None):
|
||
"""Get local sizes of the given rank."""
|
||
rank = paddle.distributed.get_rank() if rank is None else rank
|
||
global_sizes = self.serial_tensor.shape
|
||
dims_mapping = self.dist_attr.dims_mapping
|
||
# shard_sizes = self.dist_attr.shard_sizes
|
||
processes = self.dist_attr.process_mesh.process_ids
|
||
topology = self.dist_attr.process_mesh.shape
|
||
local_sizes = DistributedTensor.get_local_sizes(
|
||
global_sizes, dims_mapping, topology, processes, rank
|
||
)
|
||
|
||
return local_sizes
|
||
|
||
def local_offsets(self, rank=None):
|
||
rank = paddle.distributed.get_rank() if rank is None else rank
|
||
local_offsets = None
|
||
if rank in self._local_offsets_map.keys():
|
||
local_offsets = self._local_offsets_map[rank]
|
||
else:
|
||
global_sizes = self.serial_tensor.shape
|
||
dims_mapping = self.dist_attr.dims_mapping
|
||
# shard_sizes = self.dist_attr.shard_sizes
|
||
processes = self.dist_attr.process_mesh.process_ids
|
||
topology = self.dist_attr.process_mesh.shape
|
||
local_offsets = DistributedTensor.get_local_offsets(
|
||
global_sizes, dims_mapping, topology, processes, rank
|
||
)
|
||
self._local_offsets_map[rank] = local_offsets
|
||
|
||
return local_offsets
|
||
|
||
def global_sizes(self):
|
||
return self.serial_tensor.shape
|
||
|
||
def local_shard(self, rank=None):
|
||
rank = paddle.distributed.get_rank() if rank is None else rank
|
||
local_shard = None
|
||
if rank in self._local_shard_map.keys():
|
||
local_shard = self._local_shard_map[rank]
|
||
else:
|
||
global_sizes = self.serial_tensor.shape
|
||
dims_mapping = self.dist_attr.dims_mapping
|
||
# shard_sizes = self.dist_attr.shard_sizes
|
||
processes = self.dist_attr.process_mesh.process_ids
|
||
topology = self.dist_attr.process_mesh.shape
|
||
local_shard = DistributedTensor.get_local_shard(
|
||
global_sizes, dims_mapping, topology, processes, rank
|
||
)
|
||
self._local_shard_map[rank] = local_shard
|
||
|
||
return local_shard
|
||
|
||
def new_local_tensor(self, block=None, rank=None, name=None):
|
||
"""
|
||
Create a new local tensor of serial tensor corresponding to rank.
|
||
Args:
|
||
block (Block): The block contains the new tensor. Default value is recommend and it will be created in the block of dist main program corresponding to the serial tensor block id. Default: None.
|
||
rank (int): The rank id. Default value is recommend and it will be the current rank. Default: None.
|
||
"""
|
||
|
||
def _copy_kwargs(serial_tensor):
|
||
kwargs = {}
|
||
no_need_copy_args = ["self", "block", "shape", "name"]
|
||
arg_spec = inspect.getfullargspec(Variable.__init__)
|
||
|
||
for key in arg_spec.args:
|
||
# TODO: Check the copied attribute from serial tensor whether valid
|
||
if key in no_need_copy_args:
|
||
continue
|
||
elif key not in kwargs:
|
||
if key == "type":
|
||
kwargs[key] = serial_tensor.desc.type()
|
||
elif key == "dtype":
|
||
kwargs[key] = serial_tensor.desc.dtype()
|
||
elif key == "lod_level":
|
||
kwargs[key] = serial_tensor.desc.lod_level()
|
||
elif key == "persistable":
|
||
kwargs[key] = serial_tensor.desc.persistable()
|
||
elif key == "stop_gradient":
|
||
kwargs[key] = serial_tensor.desc.stop_gradient()
|
||
elif key == "need_check_feed":
|
||
kwargs[key] = serial_tensor.desc.need_check_feed()
|
||
# TODO: Get capacity by framework
|
||
elif key == "capacity":
|
||
continue
|
||
else:
|
||
kwargs[key] = self.serial_tensor.__dict__[key]
|
||
|
||
if isinstance(serial_tensor, Parameter):
|
||
kwargs["trainable"] = serial_tensor.trainable
|
||
kwargs["optimize_attr"] = serial_tensor.trainable
|
||
kwargs["regularizer"] = serial_tensor.regularizer
|
||
kwargs["do_model_average"] = serial_tensor.do_model_average
|
||
kwargs["need_clip"] = serial_tensor.need_clip
|
||
kwargs["is_distributed"] = serial_tensor.is_distributed
|
||
kwargs["is_parameter"] = serial_tensor.is_parameter
|
||
|
||
return kwargs
|
||
|
||
if rank is not None and not (isinstance(rank, int) and rank >= 0):
|
||
raise ValueError(f"The rank must >= 0, but got {rank}")
|
||
if block is not None and not isinstance(block, Block):
|
||
raise TypeError(f"The block must be Block, but got {type(block)}.")
|
||
rank = paddle.distributed.get_rank() if rank is None else rank
|
||
|
||
if block is None:
|
||
block_id = self.serial_tensor.block.idx
|
||
block = self.dist_context.dist_main_programs[rank].block(block_id)
|
||
|
||
# copy serial tensor attribute
|
||
kwargs = _copy_kwargs(self.serial_tensor)
|
||
kwargs["name"] = name
|
||
kwargs["shape"] = self.local_sizes(rank)
|
||
|
||
if isinstance(self.serial_tensor, Parameter):
|
||
kwargs.pop("persistable")
|
||
local_tensor = Parameter(block=block, **kwargs)
|
||
else:
|
||
local_tensor = block.create_var(**kwargs)
|
||
|
||
# TODO: Set original id when set original_id is approved
|
||
local_tensor.desc.set_original_id(self.serial_tensor.desc.id())
|
||
self._local_tensor_map[rank] = local_tensor
|
||
return local_tensor
|
||
|
||
def local_tensor(self, rank=None):
|
||
rank = paddle.distributed.get_rank() if rank is None else rank
|
||
assert rank in self._local_tensor_map, (
|
||
f"The rank {rank} local tensor has not been created."
|
||
)
|
||
return self._local_tensor_map[rank]
|
||
|
||
def __deepcopy__(self, memo):
|
||
cls = self.__class__
|
||
result = cls.__new__(cls)
|
||
memo[id(self)] = result
|
||
for k, v in self.__dict__.items():
|
||
if k == "_serial_tensor" or k == "_local_tensor_map":
|
||
setattr(result, k, v)
|
||
else:
|
||
setattr(result, k, copy.deepcopy(v, memo))
|
||
return result
|
||
|
||
def __str__(self):
|
||
str = f"{{tensor name: {self.serial_tensor.desc.name()}, tensor id: {self.serial_tensor.desc.id()}, tensor original_id {self.serial_tensor.desc.original_id()}"
|
||
|
||
# str += ", {}".format(self.dist_attr)
|
||
# return str
|
||
|
||
if self.dist_attr.is_annotated("process_mesh"):
|
||
annotated_str = "annotated"
|
||
else:
|
||
annotated_str = "non-annotated"
|
||
str += (
|
||
f", process_mesh ({annotated_str}): {self.dist_attr.process_mesh}"
|
||
)
|
||
|
||
str += f", is_parameter: {self.serial_tensor.is_parameter}"
|
||
str += f", chunk_id: {self.dist_attr.chunk_id}"
|
||
|
||
if self.dist_attr.is_annotated("dims_mapping"):
|
||
annotated_str = "annotated"
|
||
else:
|
||
annotated_str = "non-annotated"
|
||
str += f", dims_mapping ({annotated_str}): {self.dist_attr.dims_mapping} }}"
|
||
|
||
# if self.dist_attr.is_annotated("shard_mask"):
|
||
# annotated_str = "annotated"
|
||
# else:
|
||
# annotated_str = "non-annotated"
|
||
# str += ", shard_mask ({}): {}".format(annotated_str, None)
|
||
|
||
# if self.dist_attr.is_annotated("offload_device"):
|
||
# annotated_str = "annotated"
|
||
# else:
|
||
# annotated_str = "non-annotated"
|
||
# str += ", offload_device ({}): {} }}".format(annotated_str, None)
|
||
return str
|