chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
# Copyright (c) 2023 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.
|
||||
|
||||
from . import utils as utils
|
||||
@@ -0,0 +1,20 @@
|
||||
# Copyright (c) 2022 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.
|
||||
|
||||
from paddle.distributed.fleet.recompute import (
|
||||
recompute_hybrid,
|
||||
recompute_sequential,
|
||||
)
|
||||
|
||||
__all__ = ["recompute_sequential", "recompute_hybrid"]
|
||||
@@ -0,0 +1,383 @@
|
||||
# Copyright (c) 2019 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 abc
|
||||
|
||||
from paddle import base
|
||||
from paddle.base.executor import Executor
|
||||
from paddle.distributed.fleet.base.role_maker import RoleMakerBase
|
||||
from paddle.optimizer import SGD
|
||||
from paddle.static.amp.decorator import OptimizerWithMixedPrecision
|
||||
|
||||
__all__ = []
|
||||
|
||||
|
||||
class Mode:
|
||||
"""
|
||||
There are various mode for fleet, each of them is designed for different model.
|
||||
"""
|
||||
|
||||
TRANSPILER = 1
|
||||
PSLIB = 2
|
||||
COLLECTIVE = 3
|
||||
|
||||
|
||||
class Fleet(metaclass=abc.ABCMeta):
|
||||
"""
|
||||
Fleet is the base class, transpiler and pslib are implementation of Fleet.
|
||||
|
||||
Args:
|
||||
mode(Mode): the implementation of Fleet's mode.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
def __init__(self, mode):
|
||||
self._is_initialized = False
|
||||
self._mode = mode
|
||||
self._optimizer = None
|
||||
self._role_maker = None
|
||||
self._executor = None
|
||||
|
||||
def is_first_worker(self):
|
||||
"""
|
||||
Check whether the node is the first instance of worker.
|
||||
|
||||
Returns:
|
||||
bool: True if this is the first node of worker,
|
||||
False if not.
|
||||
"""
|
||||
return self._role_maker.is_first_worker()
|
||||
|
||||
def worker_index(self):
|
||||
"""
|
||||
Get current worker index.
|
||||
|
||||
Returns:
|
||||
int: node id
|
||||
"""
|
||||
return self._role_maker.worker_index()
|
||||
|
||||
def worker_num(self):
|
||||
"""
|
||||
Get current total worker number.
|
||||
|
||||
Returns:
|
||||
int: worker numbers
|
||||
"""
|
||||
return self._role_maker.worker_num()
|
||||
|
||||
def is_worker(self):
|
||||
"""
|
||||
Check whether the node is an instance of worker.
|
||||
|
||||
Returns:
|
||||
bool: True if this is a node of worker,
|
||||
False if not.
|
||||
"""
|
||||
return self._role_maker.is_worker()
|
||||
|
||||
def worker_endpoints(self, to_string=False):
|
||||
"""
|
||||
Get current server endpoints, such as ["127.0.0.1:1001", "127.0.0.1:1002"].
|
||||
|
||||
Returns:
|
||||
list/string: server endpoints
|
||||
"""
|
||||
|
||||
if to_string:
|
||||
return ",".join(self._role_maker.get_trainer_endpoints())
|
||||
else:
|
||||
return self._role_maker.get_trainer_endpoints()
|
||||
|
||||
def server_num(self):
|
||||
"""
|
||||
Get current total worker number.
|
||||
|
||||
Returns:
|
||||
int: server number
|
||||
"""
|
||||
return len(self._role_maker.get_pserver_endpoints())
|
||||
|
||||
def server_index(self):
|
||||
"""
|
||||
Get current server index.
|
||||
|
||||
Returns:
|
||||
int: node id
|
||||
"""
|
||||
return self._role_maker.server_index()
|
||||
|
||||
def server_endpoints(self, to_string=False):
|
||||
"""
|
||||
Get current server endpoints, such as ["127.0.0.1:1001", "127.0.0.1:1002"].
|
||||
|
||||
Returns:
|
||||
list/string: server endpoints
|
||||
"""
|
||||
|
||||
if to_string:
|
||||
return ",".join(self._role_maker.get_pserver_endpoints())
|
||||
else:
|
||||
return self._role_maker.get_pserver_endpoints()
|
||||
|
||||
def is_server(self):
|
||||
"""
|
||||
Check whether the node is an instance of server.
|
||||
|
||||
Returns:
|
||||
bool: True if this is a node of server,
|
||||
False if not
|
||||
"""
|
||||
return self._role_maker.is_server()
|
||||
|
||||
def is_xpu(self):
|
||||
"""
|
||||
Check whether the node is an instance of server.
|
||||
|
||||
Returns:
|
||||
bool: True if this is a node of server,
|
||||
False if not.
|
||||
"""
|
||||
return self._role_maker.is_xpu()
|
||||
|
||||
def split_files(self, files):
|
||||
"""
|
||||
split files before distributed training,
|
||||
example 1: files is [a, b, c ,d, e] and trainer_num = 2, then trainer
|
||||
0 gets [a, b, c] and trainer 1 gets [d, e].
|
||||
example 2: files is [a, b], and trainer_num = 3, then trainer 0 gets
|
||||
[a], trainer 1 gets [b], trainer 2 gets []
|
||||
|
||||
Args:
|
||||
files(list): file list need to be read.
|
||||
|
||||
Returns:
|
||||
list: files belongs to this worker.
|
||||
"""
|
||||
if not isinstance(files, list):
|
||||
raise TypeError("files should be a list of file need to be read.")
|
||||
|
||||
trainer_id = self.worker_index()
|
||||
trainers = self.worker_num()
|
||||
|
||||
remainder = len(files) % trainers
|
||||
blocksize = len(files) // trainers
|
||||
|
||||
blocks = [blocksize] * trainers
|
||||
for i in range(remainder):
|
||||
blocks[i] += 1
|
||||
|
||||
trainer_files = [[]] * trainers
|
||||
begin = 0
|
||||
for i in range(trainers):
|
||||
trainer_files[i] = files[begin : begin + blocks[i]]
|
||||
begin += blocks[i]
|
||||
|
||||
return trainer_files[trainer_id]
|
||||
|
||||
def init(self, role_maker=None):
|
||||
"""
|
||||
should be called only once in user's python scripts,
|
||||
init() will initialize RoleMaker which is used for identifying
|
||||
current node's role, e.g. worker, server, etc.
|
||||
|
||||
Args:
|
||||
role_maker(RoleMakerBase): subclass of RoleMakerBase.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
self._executor = Executor(base.CPUPlace())
|
||||
|
||||
if role_maker and not isinstance(role_maker, RoleMakerBase):
|
||||
from paddle.incubate.distributed.fleet.role_maker import (
|
||||
RoleMakerBase as RoleMakerBaseIncubate,
|
||||
)
|
||||
|
||||
if role_maker and not isinstance(role_maker, RoleMakerBaseIncubate):
|
||||
raise TypeError(
|
||||
"role_maker must be an instance of RoleMakerBase"
|
||||
)
|
||||
|
||||
self._role_maker = role_maker
|
||||
self._role_maker.generate_role()
|
||||
self._is_initialized = True
|
||||
|
||||
def all_reduce_worker(self, input, output):
|
||||
"""
|
||||
all reduce between workers, only support array of one dim.
|
||||
|
||||
Args:
|
||||
input(list|numpy.array): array of one dim
|
||||
output(list|numpy.array): array of one dim
|
||||
"""
|
||||
self._role_maker.all_reduce_worker(input, output)
|
||||
|
||||
def barrier_worker(self):
|
||||
"""
|
||||
barrier between workers
|
||||
"""
|
||||
self._role_maker.barrier_worker()
|
||||
|
||||
@abc.abstractmethod
|
||||
def init_worker(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def init_server(self, model_dir=None, **kwargs):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def run_server(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def stop_worker(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def distributed_optimizer(self, optimizer, strategy=None):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def save_inference_model(
|
||||
self,
|
||||
executor,
|
||||
dirname,
|
||||
feeded_var_names,
|
||||
target_vars,
|
||||
main_program=None,
|
||||
export_for_deployment=True,
|
||||
legacy_format=False,
|
||||
):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def save_persistables(self, executor, dirname, main_program=None):
|
||||
pass
|
||||
|
||||
|
||||
class DistributedOptimizer(metaclass=abc.ABCMeta):
|
||||
"""
|
||||
DistributedOptimizer is a wrapper for paddle.base.optimizer
|
||||
A user should pass a paddle.base.optimizer to DistributedOptimizer
|
||||
minimize() function is implemented.
|
||||
DistributedOptimizer is the starting point for a user who wants to
|
||||
run distributed training. The optimized information will be stored in
|
||||
Fleet() instance who holds the global information about current distributed
|
||||
training.
|
||||
|
||||
Args:
|
||||
optimizer(Optimizer): subclass of Optimizer.
|
||||
strategy(any): the user define config for Optimizer.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, optimizer, strategy=None):
|
||||
if not isinstance(optimizer, SGD.__bases__) and not isinstance(
|
||||
optimizer, OptimizerWithMixedPrecision
|
||||
):
|
||||
raise TypeError("optimizer must be an instance of Optimizer")
|
||||
|
||||
self._optimizer = optimizer
|
||||
self._strategy = strategy
|
||||
|
||||
@abc.abstractmethod
|
||||
def backward(
|
||||
self,
|
||||
loss,
|
||||
startup_program=None,
|
||||
parameter_list=None,
|
||||
no_grad_set=None,
|
||||
callbacks=None,
|
||||
):
|
||||
"""
|
||||
First part of `minimize`, do auto-diff to append backward ops for
|
||||
the current program.
|
||||
|
||||
Args:
|
||||
loss (Variable): loss variable to run optimizations.
|
||||
startup_program (Program): startup_program for initializing parameters
|
||||
in `parameter_list`.
|
||||
parameter_list (list): list of Variables to update.
|
||||
no_grad_set (set|None): set of Variables should be ignored.
|
||||
callbacks (list|None): list of callables to run when appending backward
|
||||
operator for one parameter.
|
||||
|
||||
Return:
|
||||
list: list of (param, grad) pair, grad is the output of backward.
|
||||
|
||||
Examples:
|
||||
See examples in `apply_gradients`.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def apply_gradients(self, params_grads):
|
||||
"""
|
||||
Second part of `minimize`, appending optimization operators for
|
||||
given `params_grads` pairs.
|
||||
|
||||
Args:
|
||||
params_grads (list): list of (param, grad) pair to do optimization.
|
||||
|
||||
Returns:
|
||||
list: A list of operators appended to the current program.
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> # doctest: +SKIP('The network is not defined.')
|
||||
>>> loss = network()
|
||||
>>> optimizer = base.optimizer.SGD(learning_rate=0.1)
|
||||
>>> params_grads = optimizer.backward(loss)
|
||||
>>> # you may append operations for params_grads here
|
||||
>>> # ...
|
||||
>>> optimizer.apply_gradients(params_grads)
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def minimize(
|
||||
self,
|
||||
losses,
|
||||
scopes=None,
|
||||
startup_programs=None,
|
||||
parameter_list=None,
|
||||
no_grad_set=None,
|
||||
):
|
||||
"""
|
||||
Add operations to minimize `loss` by updating `parameter_list`.
|
||||
|
||||
This method combines interface `backward()` and
|
||||
`apply_gradients()` into one.
|
||||
|
||||
Args:
|
||||
losses (Variable|Variable List): loss variable to run optimizations.
|
||||
scopes (Scope| Scope List): scope instance.
|
||||
startup_programs (Program|Program List): startup_program for initializing parameters
|
||||
in `parameter_list`.
|
||||
parameter_list (list): list of Variables to update.
|
||||
no_grad_set (set|None): set of Variables should be ignored.
|
||||
|
||||
Returns:
|
||||
tuple: (optimize_ops, params_grads) which are, list of operators appended;
|
||||
and list of (param, grad) Variables pair for optimization.
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,554 @@
|
||||
# Copyright (c) 2019 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
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
import paddle
|
||||
import paddle.distributed.transpiler.distribute_transpiler as dist_transpiler
|
||||
from paddle import base
|
||||
from paddle.base.compiler import CompiledProgram
|
||||
from paddle.base.executor import Executor
|
||||
from paddle.base.framework import Program
|
||||
from paddle.base.incubate.checkpoint.checkpoint_saver import (
|
||||
CheckpointSaver,
|
||||
PaddleModel,
|
||||
)
|
||||
from paddle.distributed.fleet.meta_optimizers import RawProgramOptimizer
|
||||
from paddle.incubate.distributed.fleet.base import (
|
||||
DistributedOptimizer,
|
||||
Fleet,
|
||||
Mode,
|
||||
)
|
||||
from paddle.static import io
|
||||
|
||||
|
||||
class Collective(Fleet):
|
||||
def __init__(self):
|
||||
super().__init__(Mode.COLLECTIVE)
|
||||
self._local_ip = 0
|
||||
|
||||
self.startup_program = None
|
||||
self._origin_program = None
|
||||
self._transpiled_program = None
|
||||
self.main_program = None
|
||||
self._checkpoint_prefix = "__paddle_fleet_checkpoint__"
|
||||
self._param_file_name = "_paddle_fleet_param__"
|
||||
|
||||
def init_worker(self):
|
||||
logging.warning(
|
||||
"You should not call 'init_worker' method for collective mode."
|
||||
)
|
||||
|
||||
def run_worker(self, main_programs=None, scopes=None):
|
||||
logging.warning(
|
||||
"You should not call 'run_worker' method for collective mode."
|
||||
)
|
||||
|
||||
def init_server(self, model_dir=None):
|
||||
logging.warning(
|
||||
"You should not call 'init_server' method for collective mode."
|
||||
)
|
||||
|
||||
def run_server(self):
|
||||
logging.warning(
|
||||
"You should not call 'run_server' method for collective mode."
|
||||
)
|
||||
|
||||
def stop_worker(self):
|
||||
logging.warning(
|
||||
"You should not call 'stop_worker' method for collective mode."
|
||||
)
|
||||
|
||||
def distributed_optimizer(self, optimizer, strategy=None):
|
||||
self._optimizer = CollectiveOptimizer(optimizer, strategy)
|
||||
return self._optimizer
|
||||
|
||||
def save_inference_model(
|
||||
self,
|
||||
executor,
|
||||
path_prefix,
|
||||
feeded_vas=None,
|
||||
fetch_vars=None,
|
||||
program=None,
|
||||
legacy_format=False,
|
||||
):
|
||||
"""
|
||||
Prune the given `main_program` to build a new program especially for
|
||||
inference, and then save it and all related parameters to given
|
||||
`dirname` by the `executor`.
|
||||
"""
|
||||
assert isinstance(executor, Executor), (
|
||||
"In fleet.save_inference_model() function, executor must be as"
|
||||
" Executor type."
|
||||
)
|
||||
|
||||
if program is None:
|
||||
program = self._origin_program
|
||||
assert isinstance(program, Program), (
|
||||
"In fleet.save_inference_model() function, main_program "
|
||||
"must be as Program type."
|
||||
)
|
||||
|
||||
io.save_inference_model(
|
||||
path_prefix,
|
||||
feeded_vas,
|
||||
fetch_vars,
|
||||
executor,
|
||||
program=program,
|
||||
legacy_format=legacy_format,
|
||||
)
|
||||
|
||||
def save_persistables(
|
||||
self, executor, dirname, main_program=None, filename=None
|
||||
):
|
||||
"""
|
||||
This function filters out all variables with `persistable==True` from
|
||||
the give `main_program` and then saves these variables to the folder
|
||||
`dirname` or file `filename`.
|
||||
|
||||
The `dirname` is used to specify the folder where persistable variables
|
||||
are going to be saved. If you would like to save variables in separate
|
||||
files, set `filename` None; if you would like to save all variables in a
|
||||
single file, use `filename` to specify the file name.
|
||||
"""
|
||||
assert isinstance(executor, Executor), (
|
||||
"In fleet.save_inference_model() function, executor must be as"
|
||||
" Executor type."
|
||||
)
|
||||
|
||||
if main_program is None:
|
||||
main_program = self._origin_program
|
||||
|
||||
assert isinstance(main_program, Program), (
|
||||
"In fleet.save_inference_model() function, main_program "
|
||||
"must be as Program type."
|
||||
)
|
||||
|
||||
paddle.distributed.io.save_persistables(
|
||||
executor, dirname, main_program, filename=filename
|
||||
)
|
||||
|
||||
def save_checkpoint(
|
||||
self,
|
||||
executor,
|
||||
path,
|
||||
trainer_id,
|
||||
train_status,
|
||||
fs,
|
||||
main_program=None,
|
||||
local_cache_path=".cache",
|
||||
remain_all_checkpoint=True,
|
||||
):
|
||||
"""
|
||||
This function save persistables and current epoch num to path.
|
||||
"""
|
||||
if main_program is None:
|
||||
main_program = self._transpiled_program
|
||||
|
||||
m = PaddleModel(executor, main_program)
|
||||
t = train_status
|
||||
c = CheckpointSaver(fs)
|
||||
real_path, checkpoint_no = c.save_checkpoint(
|
||||
path=path,
|
||||
slists=[m, t],
|
||||
trainer_id=trainer_id,
|
||||
local_cache_path=local_cache_path,
|
||||
)
|
||||
|
||||
if not remain_all_checkpoint:
|
||||
c.clean_redundant_checkpoints(path)
|
||||
|
||||
return real_path, checkpoint_no
|
||||
|
||||
def load_checkpoint(
|
||||
self,
|
||||
executor,
|
||||
path,
|
||||
trainer_id,
|
||||
train_status,
|
||||
fs,
|
||||
main_program=None,
|
||||
local_cache_path=".cache",
|
||||
ignore_empty=True,
|
||||
):
|
||||
"""
|
||||
This function load persistables and current epoch num from path.
|
||||
"""
|
||||
|
||||
if main_program is None:
|
||||
main_program = self._transpiled_program
|
||||
|
||||
m = PaddleModel(executor, main_program)
|
||||
c = CheckpointSaver(fs)
|
||||
return c.load_checkpoint(
|
||||
path,
|
||||
[m, train_status],
|
||||
trainer_id=trainer_id,
|
||||
ignore_empty=ignore_empty,
|
||||
local_cache_path=local_cache_path,
|
||||
)
|
||||
|
||||
|
||||
fleet = Collective()
|
||||
|
||||
|
||||
class DistributedStrategy(base.BuildStrategy):
|
||||
"""
|
||||
Init function of DistributedStrategy
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.use_local_sgd = False
|
||||
self.use_dist_fc = False
|
||||
|
||||
self.dist_fc_config = None # DistFCConfig
|
||||
self.mode = "nccl2" # or collective
|
||||
self.collective_mode = None # local_sgd or grad_allreduce
|
||||
self.nccl_comm_num = 1
|
||||
self.forward_recompute = False # use RecomputeOptimizer
|
||||
self.recompute_checkpoints = []
|
||||
self.use_amp = False # use mixed precision optimizer
|
||||
self.amp_loss_scaling = 2**15
|
||||
|
||||
# configurations below are used for unit test
|
||||
self._ut4grad_allreduce = False
|
||||
|
||||
|
||||
class CollectiveOpBasedOptimizer(DistributedOptimizer):
|
||||
"""
|
||||
Collective Operator Base Class For Distributed Optimizer
|
||||
The class is invisible to a user
|
||||
"""
|
||||
|
||||
def __init__(self, optimizer, strategy=None):
|
||||
assert isinstance(strategy, DistributedStrategy), (
|
||||
"strategy must be DistributedStrategy"
|
||||
)
|
||||
super().__init__(optimizer, strategy)
|
||||
|
||||
def backward(
|
||||
self,
|
||||
loss,
|
||||
startup_program=None,
|
||||
parameter_list=None,
|
||||
no_grad_set=None,
|
||||
callbacks=None,
|
||||
):
|
||||
return self._optimizer.backward(
|
||||
loss, startup_program, parameter_list, no_grad_set, callbacks
|
||||
)
|
||||
|
||||
def apply_gradients(self, params_grads):
|
||||
return self._optimizer.apply_gradients(params_grads)
|
||||
|
||||
|
||||
class CollectiveOptimizer(DistributedOptimizer):
|
||||
"""
|
||||
DistributedOptimizer is a wrapper for paddle.base.optimizer
|
||||
A user should pass a paddle.base.optimizer to DistributedOptimizer
|
||||
minimize() function is implemented.
|
||||
DistributedOptimizer is the starting point for a user who wants to
|
||||
run distributed training. The optimized information will be stored in
|
||||
Fleet() instance who holds the global information about current distributed
|
||||
training.
|
||||
"""
|
||||
|
||||
def __init__(self, optimizer, strategy=DistributedStrategy()):
|
||||
if strategy is None:
|
||||
strategy = DistributedStrategy()
|
||||
super().__init__(optimizer, strategy)
|
||||
self._forward_recompute = strategy.forward_recompute
|
||||
if not isinstance(strategy.recompute_checkpoints, list):
|
||||
raise ValueError(
|
||||
"DistStrategy.recompute_checkpoints should be a List"
|
||||
)
|
||||
self._recompute_checkpoints = strategy.recompute_checkpoints
|
||||
self._use_amp = strategy.use_amp
|
||||
self._amp_loss_scaling = strategy.amp_loss_scaling
|
||||
self.print_config = False
|
||||
|
||||
def backward(
|
||||
self,
|
||||
loss,
|
||||
startup_program=None,
|
||||
parameter_list=None,
|
||||
no_grad_set=None,
|
||||
callbacks=None,
|
||||
):
|
||||
return self._optimizer.backward(
|
||||
loss, startup_program, parameter_list, no_grad_set, callbacks
|
||||
)
|
||||
|
||||
def apply_gradients(self, params_grads):
|
||||
return self._optimizer.apply_gradients(params_grads)
|
||||
|
||||
def _check_condition(self, name, **kwargs):
|
||||
for k, v in kwargs.items():
|
||||
if v is True:
|
||||
raise AssertionError(f"you can't use {name} and {k} together")
|
||||
|
||||
def _check_collective_mode(self, main_program, optimizer, strategy):
|
||||
"""
|
||||
Check the conflict conditions.
|
||||
"""
|
||||
if strategy.use_local_sgd:
|
||||
strategy.mode = "collective"
|
||||
strategy.collective_mode = "local_sgd"
|
||||
self._check_condition(
|
||||
"use_local_sgd",
|
||||
use_dgc=main_program._enable_dgc,
|
||||
use_dist_fc=strategy.use_dist_fc,
|
||||
use_lamb=main_program._use_lamb,
|
||||
)
|
||||
|
||||
if strategy.use_dist_fc:
|
||||
self._check_condition(
|
||||
"use_dist_fc",
|
||||
use_dgc=main_program._enable_dgc,
|
||||
use_local_sgd=strategy.use_local_sgd,
|
||||
use_lamb=main_program._use_lamb,
|
||||
)
|
||||
assert strategy.dist_fc_config is not None, (
|
||||
"DistributedStrategy.dist_fc_config should be set"
|
||||
)
|
||||
|
||||
if strategy._ut4grad_allreduce:
|
||||
strategy.mode = "collective"
|
||||
strategy.collective_mode = "grad_allreduce"
|
||||
self._check_condition(
|
||||
"_ut4grad_allreduce",
|
||||
use_dgc=main_program._enable_dgc,
|
||||
use_lamb=main_program._use_lamb,
|
||||
)
|
||||
|
||||
if (
|
||||
self._strategy.collective_mode == "local_sgd"
|
||||
or self._strategy.collective_mode == "grad_allreduce"
|
||||
):
|
||||
assert self._strategy.mode == "collective", (
|
||||
"local_sgd and grad_allreduce can be used under collective mode"
|
||||
)
|
||||
|
||||
def _transpile(self, startup_program, main_program):
|
||||
"""
|
||||
Transpile the programs to distributed programs. And add the variables.
|
||||
"""
|
||||
worker_endpoints = fleet.worker_endpoints()
|
||||
trainer_id = fleet.worker_index()
|
||||
current_endpoint = fleet.worker_endpoints()[trainer_id]
|
||||
worker_endpoints_env = ','.join(worker_endpoints)
|
||||
trainers_num = fleet.worker_num()
|
||||
|
||||
if self.print_config:
|
||||
print(
|
||||
f"worker_endpoints:{worker_endpoints} trainers_num:{trainers_num} current_endpoint:{current_endpoint} \
|
||||
trainer_id:{trainer_id}"
|
||||
)
|
||||
|
||||
# call transpiler
|
||||
config = dist_transpiler.DistributeTranspilerConfig()
|
||||
config.mode = self._strategy.mode
|
||||
config.collective_mode = self._strategy.collective_mode
|
||||
|
||||
config.nccl_comm_num = self._strategy.nccl_comm_num
|
||||
config.use_hierarchical_allreduce = (
|
||||
self._strategy.use_hierarchical_allreduce
|
||||
)
|
||||
config.hierarchical_allreduce_inter_nranks = (
|
||||
self._strategy.hierarchical_allreduce_inter_nranks
|
||||
)
|
||||
|
||||
t = dist_transpiler.DistributeTranspiler(config=config)
|
||||
t.transpile(
|
||||
trainer_id=trainer_id,
|
||||
trainers=worker_endpoints_env,
|
||||
startup_program=startup_program,
|
||||
program=main_program,
|
||||
current_endpoint=current_endpoint,
|
||||
)
|
||||
|
||||
def _get_node_ips_from_endpoints(self, endpoints):
|
||||
ss = set()
|
||||
ips = []
|
||||
for ep in endpoints:
|
||||
ip = ep.split(":")[0].strip()
|
||||
if ip not in ss:
|
||||
ss.add(ip)
|
||||
ips.append(ip)
|
||||
else:
|
||||
continue
|
||||
|
||||
return ips
|
||||
|
||||
def _node_num(self):
|
||||
worker_endpoints = fleet.worker_endpoints()
|
||||
current_endpoint = fleet.worker_endpoints()[fleet.worker_index()]
|
||||
worker_endpoints_env = ','.join(worker_endpoints)
|
||||
|
||||
node_ips = self._get_node_ips_from_endpoints(worker_endpoints)
|
||||
node_ip = current_endpoint.split(":")[0].strip()
|
||||
|
||||
node_num = len(node_ips)
|
||||
|
||||
return node_num
|
||||
|
||||
def _try_to_compile(self, startup_program, main_program):
|
||||
node_num = self._node_num()
|
||||
assert node_num >= 1, f"nccl2 node_num must >= 1, now:{node_num}"
|
||||
|
||||
if node_num <= 1:
|
||||
if self._strategy.nccl_comm_num > 1:
|
||||
logging.warning(
|
||||
"set nccl_comm_num=1 since you only have 1 node."
|
||||
)
|
||||
self._strategy.nccl_comm_num = 1
|
||||
|
||||
if self._strategy.use_hierarchical_allreduce:
|
||||
logging.warning(
|
||||
"set use_hierarchical_allreduce=False since you only have 1 node."
|
||||
)
|
||||
self._strategy.use_hierarchical_allreduce = False
|
||||
|
||||
sync_allreduce = os.getenv("FLAGS_sync_nccl_allreduce")
|
||||
|
||||
# NOTE. open sync_batch_norm will hang when use multi num_threads
|
||||
sync_batch_norm = self._strategy.sync_batch_norm
|
||||
if sync_batch_norm is not None and sync_batch_norm is True:
|
||||
self._strategy.nccl_comm_num = 1
|
||||
self._strategy.use_hierarchical_allreduce = False
|
||||
logging.warning(
|
||||
"use sync_batch_norm will hang when set num_threads > 1, so "
|
||||
"set num_threads=1, nccl_comm_num=1, use_hierarchical_allreduce=False."
|
||||
)
|
||||
|
||||
if self.print_config:
|
||||
print(
|
||||
"node_num:",
|
||||
node_num,
|
||||
"use_hierarchical_allreduce:",
|
||||
self._strategy.use_hierarchical_allreduce,
|
||||
"nccl_comm_num:",
|
||||
self._strategy.nccl_comm_num,
|
||||
"FLAGS_sync_nccl_allreduce:",
|
||||
sync_allreduce,
|
||||
)
|
||||
|
||||
self._transpile(startup_program, main_program)
|
||||
|
||||
if self._strategy.mode == "collective":
|
||||
return main_program
|
||||
|
||||
self._strategy.num_trainers = fleet.worker_num()
|
||||
self._strategy.trainer_id = fleet.worker_index()
|
||||
self._strategy.trainers_endpoints = fleet.worker_endpoints()
|
||||
self._strategy.enable_backward_optimizer_op_deps = True
|
||||
|
||||
comm_opt = RawProgramOptimizer(self._optimizer)
|
||||
comm_opt.fuse_all_reduce_ops = True
|
||||
comm_opt.fuse_grad_size_in_num = True
|
||||
comm_opt.endpoints = self._strategy.trainers_endpoints
|
||||
comm_opt.current_endpoint = comm_opt.endpoints[fleet.worker_index()]
|
||||
comm_opt.rank = fleet.worker_index()
|
||||
comm_opt.nranks = fleet.worker_num()
|
||||
comm_opt.main_program = main_program
|
||||
if comm_opt.nranks > 1:
|
||||
comm_opt._transpile_main_program(self._loss)
|
||||
|
||||
self._compiled_program = CompiledProgram(
|
||||
comm_opt.main_program, build_strategy=self._strategy
|
||||
)
|
||||
|
||||
return self._compiled_program
|
||||
|
||||
def raiseOptimizeError(self, strategy_name, optimize_name):
|
||||
raise ValueError(
|
||||
f"can not use {optimize_name} when you set DistStrategy.{strategy_name} "
|
||||
"as True"
|
||||
)
|
||||
|
||||
def minimize(
|
||||
self, loss, startup_program=None, parameter_list=None, no_grad_set=None
|
||||
):
|
||||
"""
|
||||
minimize a program through loss
|
||||
Args:
|
||||
loss (Variable|Variable List): loss variable or loss variable list to run optimization.
|
||||
startup_program (Program): startup_program for initializing parameters
|
||||
in `parameter_list`.
|
||||
parameter_list (list): list of Variables to update.
|
||||
no_grad_set (set|None): set of Variables should be ignored.
|
||||
Returns:
|
||||
tuple: (optimize_ops, params_grads) which are, list of operators appended;
|
||||
and list of (param, grad) Variables pair for optimization.
|
||||
Note that in parameter server mode, a worker will not get anything about optimize_os
|
||||
Because optimizer algorithms run on pserver side. We will make this usable in pserver
|
||||
process, but currently the optimization part is written into Fleet(). A user does not
|
||||
need to care about how to startup a pserver node.
|
||||
"""
|
||||
|
||||
# check optimizer conflicts
|
||||
if self._forward_recompute:
|
||||
if self._recompute_checkpoints == []:
|
||||
raise ValueError(
|
||||
"please set strategy.recompute_checkpoints"
|
||||
"when set strategy.forward_recompute as True"
|
||||
)
|
||||
if self._optimizer.__class__.__name__ in [
|
||||
"RecomputeOptimizer",
|
||||
"OptimizerWithMixedPrecision",
|
||||
]:
|
||||
self.raiseOptimizeError(
|
||||
"forward_recompute", self._optimizer.__class__.__name__
|
||||
)
|
||||
|
||||
self._optimizer = paddle.incubate.optimizer.RecomputeOptimizer(
|
||||
self._optimizer
|
||||
)
|
||||
self._optimizer._set_checkpoints(self._recompute_checkpoints)
|
||||
|
||||
if self._use_amp:
|
||||
if self._optimizer.__class__.__name__ in [
|
||||
"OptimizerWithMixedPrecision",
|
||||
"DGCMomentumOptimizer",
|
||||
]:
|
||||
self.raiseOptimizeError(
|
||||
"mixed_precision", self._optimizer.__class__.__name__
|
||||
)
|
||||
self._optimizer = paddle.static.amp.decorate(
|
||||
self._optimizer,
|
||||
init_loss_scaling=self._amp_loss_scaling,
|
||||
use_dynamic_loss_scaling=True,
|
||||
)
|
||||
|
||||
main_program = loss.block.program
|
||||
if startup_program is None:
|
||||
startup_program = base.default_startup_program()
|
||||
fleet.startup_program = startup_program
|
||||
|
||||
self._loss = loss
|
||||
|
||||
self._check_collective_mode(
|
||||
main_program, self._optimizer, self._strategy
|
||||
)
|
||||
|
||||
optimize_ops, param_grads = self._optimizer.minimize(
|
||||
loss, startup_program, parameter_list, no_grad_set=no_grad_set
|
||||
)
|
||||
|
||||
fleet._origin_program = main_program.clone(for_test=False)
|
||||
fleet._transpiled_program = main_program
|
||||
fleet.main_program = self._try_to_compile(startup_program, main_program)
|
||||
|
||||
return optimize_ops, param_grads
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
# Copyright (c) 2019 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.
|
||||
+956
@@ -0,0 +1,956 @@
|
||||
# Copyright (c) 2020 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.
|
||||
"""
|
||||
Convert the static program to distributed data-parallelism programs.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import paddle
|
||||
from paddle.base.compiler import CompiledProgram
|
||||
from paddle.distributed.fleet.base.private_helper_function import (
|
||||
wait_server_ready,
|
||||
)
|
||||
from paddle.distributed.transpiler.distribute_transpiler import (
|
||||
DistributeTranspilerConfig,
|
||||
)
|
||||
from paddle.framework import core
|
||||
from paddle.incubate.distributed.fleet.base import (
|
||||
DistributedOptimizer,
|
||||
Fleet,
|
||||
Mode,
|
||||
)
|
||||
from paddle.incubate.distributed.fleet.parameter_server import version
|
||||
from paddle.incubate.distributed.fleet.parameter_server.distribute_transpiler.distributed_strategy import (
|
||||
AsyncStrategy,
|
||||
DistributedStrategy,
|
||||
GeoStrategy,
|
||||
HalfAsyncStrategy,
|
||||
StrategyFactory,
|
||||
SyncStrategy,
|
||||
TrainerRuntimeConfig, # noqa: F401
|
||||
)
|
||||
from paddle.incubate.distributed.fleet.parameter_server.ir import (
|
||||
pserver_pass as server,
|
||||
public,
|
||||
trainer_pass as worker,
|
||||
)
|
||||
from paddle.incubate.distributed.fleet.parameter_server.ir.public import (
|
||||
_get_lr_ops,
|
||||
_has_global_step,
|
||||
get_sparse_tablenames,
|
||||
)
|
||||
from paddle.incubate.distributed.fleet.parameter_server.mode import PSMode
|
||||
from paddle.incubate.distributed.fleet.parameter_server.pslib.optimizer_factory import (
|
||||
DistributedAdam, # noqa: F401
|
||||
)
|
||||
from paddle.incubate.distributed.fleet.role_maker import MPISymmetricRoleMaker
|
||||
from paddle.static import (
|
||||
Executor,
|
||||
Program,
|
||||
default_main_program,
|
||||
default_startup_program,
|
||||
)
|
||||
|
||||
|
||||
class FleetTranspiler(Fleet):
|
||||
"""
|
||||
A subclass for compatibility with distributed.transpiler.DistributeTranspiler.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(Mode.TRANSPILER)
|
||||
|
||||
self._inner_mode = None
|
||||
|
||||
if version.is_transpiler():
|
||||
self._inner_mode = PSMode.TRANSPILER
|
||||
else:
|
||||
self._inner_mode = PSMode.PSLIB
|
||||
|
||||
self._strategy = None
|
||||
self._transpiler = None
|
||||
self._origin_main_program = None
|
||||
self._origin_startup_program = None
|
||||
self._communicator = None
|
||||
self.startup_program = None
|
||||
self.main_program = None
|
||||
|
||||
self._opt_info = None
|
||||
self._local_ip = 0
|
||||
self._fleet_ptr = None
|
||||
self._main_programs = []
|
||||
self._scopes = []
|
||||
self._client2client_request_timeout_ms = 500000
|
||||
self._client2client_connect_timeout_ms = 10000
|
||||
self._client2client_max_retry = 3
|
||||
|
||||
def init(self, role_maker=None):
|
||||
if role_maker is None:
|
||||
role_maker = MPISymmetricRoleMaker()
|
||||
super().init(role_maker)
|
||||
if self._fleet_ptr is None:
|
||||
self._fleet_ptr = core.Fleet()
|
||||
|
||||
def _init_transpiler_worker(self):
|
||||
"""
|
||||
`init_worker` has many many functions to do before training,
|
||||
first, wait for all parameter servers launch completely.
|
||||
second, run executor to initialize startup program
|
||||
third, wait for all worker initialize completely.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
def sync_strategy_envs():
|
||||
kwargs = {}
|
||||
kwargs["pserver_endpoints"] = (
|
||||
self._role_maker.get_pserver_endpoints()
|
||||
)
|
||||
kwargs["trainer_id"] = self._role_maker.worker_index()
|
||||
return kwargs
|
||||
|
||||
def geo_strategy_envs():
|
||||
def get_sparse_attrs():
|
||||
opt_init_map = {}
|
||||
opt_init_map["gaussian_random"] = ["seed", "mean", "std"]
|
||||
opt_init_map["fill_constant"] = ["value"]
|
||||
opt_init_map["uniform_random"] = ["seed", "min", "max"]
|
||||
opt_init_map["truncated_gaussian_random"] = [
|
||||
"seed",
|
||||
"mean",
|
||||
"std",
|
||||
]
|
||||
|
||||
dist_varnames = get_sparse_tablenames(
|
||||
self._origin_main_program, True
|
||||
)
|
||||
sparse_varnames = get_sparse_tablenames(
|
||||
self._origin_main_program, False
|
||||
)
|
||||
|
||||
if len(dist_varnames) != 0:
|
||||
raise ValueError(
|
||||
"GeoStrategy can not support large scale embedding now, please use paddle.static.nn.embedding"
|
||||
)
|
||||
|
||||
init_attrs = []
|
||||
for value_name in sparse_varnames:
|
||||
value_var = self._origin_main_program.global_block().vars[
|
||||
value_name
|
||||
]
|
||||
value_attr = [
|
||||
value_name,
|
||||
",".join([str(dim) for dim in value_var.shape]),
|
||||
]
|
||||
for op in self._origin_startup_program.global_block().ops:
|
||||
if (
|
||||
op.type in opt_init_map.keys()
|
||||
and value_name == op.output("Out")[0]
|
||||
):
|
||||
init_attr = [op.type]
|
||||
for attr in opt_init_map[op.type]:
|
||||
init_attr.append(str(op.attr(attr)))
|
||||
value_attr.append("&".join(init_attr))
|
||||
init_attrs.append(":".join(value_attr))
|
||||
break
|
||||
return "#".join(init_attrs)
|
||||
|
||||
kwargs = {}
|
||||
kwargs["trainers"] = self.worker_num()
|
||||
kwargs["sparse_attrs"] = get_sparse_attrs()
|
||||
return kwargs
|
||||
|
||||
# if MPISymmetricRoleMaker is defined
|
||||
# we suppose a user wants to submit job on mpi cluster
|
||||
|
||||
if isinstance(self._role_maker, MPISymmetricRoleMaker):
|
||||
# check whether server has been initialized
|
||||
wait_server_ready(self.server_endpoints(to_string=False))
|
||||
|
||||
trainer_config = self._strategy.get_trainer_runtime_config()
|
||||
|
||||
print(trainer_config)
|
||||
|
||||
lrs = _has_global_step(_get_lr_ops(self._origin_main_program))
|
||||
|
||||
if lrs > 0:
|
||||
kwargs = {"need_global_step": "1"}
|
||||
else:
|
||||
kwargs = {"need_global_step": "0"}
|
||||
|
||||
if isinstance(self._strategy, GeoStrategy):
|
||||
geo_kwargs = geo_strategy_envs()
|
||||
kwargs.update(geo_kwargs)
|
||||
if isinstance(self._strategy, SyncStrategy):
|
||||
sync_kwargs = sync_strategy_envs()
|
||||
kwargs.update(sync_kwargs)
|
||||
|
||||
kwargs = kwargs if kwargs else None
|
||||
|
||||
send_ctx = fleet.compiled_config.get_communicator_send_context()
|
||||
|
||||
if self.compiled_config.is_geo_mode():
|
||||
recv_ctx = fleet.compiled_config.get_communicator_recv_context(
|
||||
recv_type=4
|
||||
)
|
||||
else:
|
||||
recv_ctx = fleet.compiled_config.get_communicator_recv_context(
|
||||
recv_type=1
|
||||
)
|
||||
|
||||
from paddle.distributed.communicator import Communicator
|
||||
|
||||
self._communicator = Communicator(
|
||||
trainer_config.mode, kwargs, trainer_config.get_communicator_flags()
|
||||
)
|
||||
|
||||
self._communicator.init_with_ctx(send_ctx, recv_ctx)
|
||||
|
||||
if not self._communicator.is_running():
|
||||
self._communicator.start()
|
||||
else:
|
||||
raise ValueError(
|
||||
"Communicator can only be inited once, please check"
|
||||
)
|
||||
|
||||
def init_worker(self):
|
||||
"""
|
||||
`init_worker` has many many functions to do before training,
|
||||
first, wait for all parameter servers launch completely.
|
||||
second, run executor to initialize startup program
|
||||
third, wait for all worker initialize completely.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
if self._inner_mode == PSMode.TRANSPILER:
|
||||
self._init_transpiler_worker()
|
||||
else:
|
||||
raise NotImplementedError("add implement later")
|
||||
|
||||
def _init_transpiler_server(self, model_dir=None):
|
||||
if not self.startup_program:
|
||||
raise ValueError(
|
||||
"startup_program is None, need invoke DistributedOptimizer.minimize first"
|
||||
)
|
||||
|
||||
self._executor.run(self.startup_program)
|
||||
|
||||
if model_dir:
|
||||
if not os.path.isdir(model_dir):
|
||||
raise ValueError(f"There is no directory named '{model_dir}'")
|
||||
|
||||
sparse_varnames = self.compiled_config.get_sparse_varname_on_ps(
|
||||
True
|
||||
)
|
||||
distributed_varnames = (
|
||||
self.compiled_config.get_sparse_varname_on_ps(False)
|
||||
)
|
||||
|
||||
remaining_vars = list(
|
||||
filter(
|
||||
FleetTranspiler.__exclude_vars(
|
||||
sparse_varnames + distributed_varnames
|
||||
),
|
||||
self.main_program.list_vars(),
|
||||
)
|
||||
)
|
||||
|
||||
paddle.static.load_vars(
|
||||
self._executor,
|
||||
main_program=self.main_program,
|
||||
dirname=model_dir,
|
||||
vars=remaining_vars,
|
||||
)
|
||||
|
||||
self._load_sparse_params(
|
||||
dirname=model_dir, varnames=sparse_varnames
|
||||
)
|
||||
|
||||
# todo(tangwei12) load distributed vars
|
||||
# self._load_sparse_params(dirname=model_dir, varnames=distributed_varnames)
|
||||
|
||||
def init_server(self, model_dir=None, **kwargs):
|
||||
"""
|
||||
`init_server` has many many functions to do before start pserver,
|
||||
first, run executor to initialize startup program,
|
||||
second, if the `model_dir` is not empty, it will load parameters from it for increment training.
|
||||
|
||||
Args:
|
||||
model_dir(str): The directory path.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
if self._inner_mode == PSMode.TRANSPILER:
|
||||
self._init_transpiler_server(model_dir)
|
||||
else:
|
||||
raise NotImplementedError("add implement later")
|
||||
|
||||
def run_server(self):
|
||||
"""
|
||||
`run_server` execute executor to start pserver main program.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
if self._inner_mode == PSMode.TRANSPILER:
|
||||
if not self.main_program:
|
||||
raise ValueError(
|
||||
"main_program is None, need invoke DistributedOptimizer.minimize first"
|
||||
)
|
||||
|
||||
self._executor.run(self.main_program)
|
||||
else:
|
||||
raise NotImplementedError("add implement later")
|
||||
|
||||
def stop_worker(self):
|
||||
"""
|
||||
Close this executor.
|
||||
|
||||
For the distributed training, this method would free the resource on PServers related to
|
||||
the current Trainer.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
if self._inner_mode == PSMode.TRANSPILER:
|
||||
self._communicator.stop()
|
||||
if isinstance(self._role_maker, MPISymmetricRoleMaker):
|
||||
self._role_maker._finalize()
|
||||
self._executor.close()
|
||||
else:
|
||||
raise NotImplementedError("add implement later")
|
||||
|
||||
def distributed_optimizer(self, optimizer, strategy=None):
|
||||
"""
|
||||
Optimizer for distributed training.
|
||||
|
||||
For the distributed training, this method would rebuild a new instance of DistributedOptimizer.
|
||||
Which has basic Optimizer function and special features for distributed training.
|
||||
|
||||
Args:
|
||||
optimizer(Optimizer): The executor to run for init server.
|
||||
strategy(DistributeTranspilerConfig): Extra properties for distributed optimizer.
|
||||
|
||||
Returns:
|
||||
TranspilerOptimizer: subclass of DistributedOptimizer.
|
||||
"""
|
||||
|
||||
if not isinstance(optimizer, paddle.optimizer.Optimizer):
|
||||
raise ValueError("optimizer must be an instance of Optimizer")
|
||||
if not self._is_initialized:
|
||||
raise ValueError(
|
||||
"fleet.init(role) to initialize before optimizer.minimize(loss)"
|
||||
)
|
||||
|
||||
if not strategy:
|
||||
_strategy = StrategyFactory.create_async_strategy()
|
||||
|
||||
if isinstance(strategy, DistributedStrategy):
|
||||
_strategy = strategy
|
||||
elif isinstance(strategy, DistributeTranspilerConfig):
|
||||
if strategy.sync_mode:
|
||||
_strategy = SyncStrategy()
|
||||
else:
|
||||
if strategy.runtime_split_send_recv:
|
||||
if strategy.geo_sgd_mode:
|
||||
_strategy = GeoStrategy(strategy.geo_sgd_need_push_nums)
|
||||
elif strategy.half_async:
|
||||
_strategy = HalfAsyncStrategy()
|
||||
else:
|
||||
_strategy = AsyncStrategy()
|
||||
else:
|
||||
_strategy = HalfAsyncStrategy()
|
||||
# for half_async compatibility
|
||||
strategy.half_async = True
|
||||
strategy.runtime_split_send_recv = True
|
||||
_strategy.set_program_config(strategy)
|
||||
elif isinstance(strategy, dict):
|
||||
if self._inner_mode != PSMode.PSLIB:
|
||||
raise TypeError("Dict strategy can only be used at PSLIB Mode")
|
||||
|
||||
_strategy = StrategyFactory.create_async_strategy()
|
||||
_strategy.set_pslib_runtime_config(strategy)
|
||||
else:
|
||||
raise TypeError(
|
||||
"strategy must be an instance of DistributeTranspilerConfig, DistributedStrategy"
|
||||
)
|
||||
|
||||
self._strategy = _strategy
|
||||
self._optimizer = ParameterServerOptimizer(optimizer, _strategy)
|
||||
return self._optimizer
|
||||
|
||||
def save_inference_model(
|
||||
self,
|
||||
executor,
|
||||
dirname,
|
||||
feeded_var_names,
|
||||
target_vars,
|
||||
main_program=None,
|
||||
export_for_deployment=True,
|
||||
legacy_format=False,
|
||||
):
|
||||
"""
|
||||
Prune the given `main_program` to build a new program especially for inference,
|
||||
and then save it and all related parameters to given `dirname` by the `executor`.
|
||||
"""
|
||||
|
||||
if self._inner_mode == PSMode.PSLIB:
|
||||
raise NotImplementedError("add implement later")
|
||||
|
||||
if not isinstance(executor, Executor):
|
||||
raise TypeError(
|
||||
"in fleet.save_inference_model() function, executor must be as Executor type"
|
||||
)
|
||||
|
||||
# Todo(MrChengmo): support recv&save GPU-Kernel for ps-gpu model save
|
||||
if not isinstance(executor.place, paddle.CPUPlace):
|
||||
save_executor = Executor(paddle.CPUPlace())
|
||||
else:
|
||||
save_executor = executor
|
||||
|
||||
if main_program is not None:
|
||||
if isinstance(main_program, CompiledProgram):
|
||||
raise TypeError(
|
||||
"in fleet.save_inference_model() function, main_program must be as Program type, CompiledProgram is not allowed"
|
||||
)
|
||||
paddle.static.save_inference_model(
|
||||
dirname,
|
||||
feeded_var_names,
|
||||
target_vars,
|
||||
executor,
|
||||
main_program,
|
||||
None,
|
||||
None,
|
||||
export_for_deployment,
|
||||
legacy_format=legacy_format,
|
||||
)
|
||||
else:
|
||||
paddle.static.save_inference_model(
|
||||
dirname,
|
||||
feeded_var_names,
|
||||
target_vars,
|
||||
executor,
|
||||
self._origin_main_program,
|
||||
None,
|
||||
None,
|
||||
export_for_deployment,
|
||||
True,
|
||||
legacy_format=legacy_format,
|
||||
)
|
||||
|
||||
model_basename = "__model__"
|
||||
model_filename = os.path.join(dirname, model_basename)
|
||||
|
||||
with open(model_filename, "rb") as f:
|
||||
program_desc_str = f.read()
|
||||
|
||||
program = Program.parse_from_string(program_desc_str)
|
||||
program._copy_dist_param_info_from(self.main_program)
|
||||
self.save_persistables(executor, dirname, program)
|
||||
|
||||
def _load_sparse_params(self, dirname, varnames):
|
||||
from paddle.distributed.communicator import LargeScaleKV
|
||||
|
||||
scale_kv = LargeScaleKV()
|
||||
for varname in varnames:
|
||||
origin_varname, _, _ = public._get_varname_parts(varname)
|
||||
sparse_dir = os.path.join(dirname, origin_varname, varname)
|
||||
scale_kv.load(varname, sparse_dir)
|
||||
|
||||
def _get_optimizer_status(self, op, param_name):
|
||||
supported_opts = [
|
||||
"sgd",
|
||||
"adam",
|
||||
"adagrad",
|
||||
"adamax",
|
||||
"momentum",
|
||||
"lars_momentum",
|
||||
"rmsprop",
|
||||
"decayed_adagrad",
|
||||
"ftrl",
|
||||
]
|
||||
|
||||
reshaped_val_map = {}
|
||||
reshaped_val_map["sgd"] = []
|
||||
reshaped_val_map["adam"] = ["moment1_0", "moment2_0"]
|
||||
reshaped_val_map["adagrad"] = ["moment_0"]
|
||||
reshaped_val_map["adamax"] = ["moment_0", "inf_norm_0"]
|
||||
reshaped_val_map["momentum"] = ["velocity_0"]
|
||||
reshaped_val_map["lars_momentum"] = ["velocity_0"]
|
||||
reshaped_val_map["rmsprop"] = [
|
||||
"momentum_0",
|
||||
"mean_square_0",
|
||||
"mean_grad_0",
|
||||
]
|
||||
reshaped_val_map["decayed_adagrad"] = ["moment_0"]
|
||||
reshaped_val_map["ftrl"] = ["squared_0", "linear_0"]
|
||||
|
||||
orishaped_val_map = {}
|
||||
orishaped_val_map["adam"] = ["beta1_pow_acc_0", "beta2_pow_acc_0"]
|
||||
orishaped_val_map["adamax"] = ["beta1_pow_acc_0"]
|
||||
|
||||
if op not in supported_opts:
|
||||
raise ValueError(
|
||||
f"fleet can not support optimizer: {op}, only this can be supported: {supported_opts}"
|
||||
)
|
||||
|
||||
reshaped_names = [
|
||||
param_name + "_" + val for val in reshaped_val_map[op]
|
||||
]
|
||||
|
||||
if op not in orishaped_val_map:
|
||||
origin_names = []
|
||||
else:
|
||||
origin_names = [
|
||||
param_name + "_" + val for val in orishaped_val_map[op]
|
||||
]
|
||||
return reshaped_names, origin_names
|
||||
|
||||
def _get_optimizer_op(self, param_name):
|
||||
opts = public._get_optimize_ops(self._origin_main_program)
|
||||
for op in opts:
|
||||
if (
|
||||
"Param" in op.input_names
|
||||
and "LearningRate" in op.input_names
|
||||
and op.input("Param")[0] == param_name
|
||||
):
|
||||
return op
|
||||
|
||||
def _save_dense_params(self, executor, dirname, context, main_program):
|
||||
self._communicator.recv()
|
||||
|
||||
prog = Program()
|
||||
block = prog.global_block()
|
||||
local_vars = []
|
||||
|
||||
for name, var_ctx in context.items():
|
||||
if len(var_ctx.origin_varnames()) != 1:
|
||||
raise ValueError("Dense can not support split now.")
|
||||
|
||||
varname = var_ctx.origin_varnames()[0]
|
||||
local_vars.append(varname)
|
||||
|
||||
optimizer = self._get_optimizer_op(varname)
|
||||
reshaped_varnames, origin_varnames = self._get_optimizer_status(
|
||||
optimizer.type, varname
|
||||
)
|
||||
|
||||
for var_name in [varname, *reshaped_varnames, *origin_varnames]:
|
||||
var = self._origin_main_program.global_block().vars[var_name]
|
||||
block.append_op(
|
||||
type='recv_save',
|
||||
attrs={
|
||||
"trainer_id": self._role_maker.worker_index(),
|
||||
"shape": var.shape,
|
||||
"slice_shapes": [",".join([str(i) for i in var.shape])],
|
||||
"slice_varnames": [var.name],
|
||||
"remote_varnames": [var.name],
|
||||
"is_sparse": False,
|
||||
"endpoints": var_ctx.split_endpoints(),
|
||||
"file_path": os.path.join(dirname, var.name),
|
||||
},
|
||||
)
|
||||
|
||||
executor.run(prog)
|
||||
return local_vars
|
||||
|
||||
def _save_sparse_params(self, executor, dirname, context, main_program):
|
||||
prog = Program()
|
||||
block = prog.global_block()
|
||||
local_vars = []
|
||||
|
||||
for name, var_ctx in context.items():
|
||||
if len(var_ctx.origin_varnames()) != 1:
|
||||
raise ValueError("Dense can not support split now.")
|
||||
|
||||
varname = var_ctx.origin_varnames()[0]
|
||||
local_vars.append(varname)
|
||||
|
||||
optimizer = self._get_optimizer_op(varname)
|
||||
reshaped_varnames, origin_varnames = self._get_optimizer_status(
|
||||
optimizer.type, varname
|
||||
)
|
||||
|
||||
var = self._origin_main_program.global_block().vars[varname]
|
||||
slice_shapes = []
|
||||
dims1 = ",".join([str(i) for i in var.shape[1:]])
|
||||
|
||||
for section in var_ctx.sections():
|
||||
slice_shapes.append(str(section) + dims1)
|
||||
|
||||
block.append_op(
|
||||
type='recv_save',
|
||||
attrs={
|
||||
"trainer_id": self._role_maker.worker_index(),
|
||||
"shape": var.shape,
|
||||
"slice_shapes": slice_shapes,
|
||||
"slice_varnames": var_ctx.split_varnames(),
|
||||
"remote_varnames": var_ctx.split_varnames(),
|
||||
"is_sparse": True,
|
||||
"endpoints": var_ctx.split_endpoints(),
|
||||
"pserver_num": len(
|
||||
self._role_maker.get_pserver_endpoints()
|
||||
),
|
||||
"file_path": os.path.join(dirname, var.name),
|
||||
},
|
||||
)
|
||||
|
||||
for reshaped_varname in reshaped_varnames:
|
||||
var = self._origin_main_program.global_block().vars[
|
||||
reshaped_varname
|
||||
]
|
||||
|
||||
slice_varnames = []
|
||||
remote_varnames = []
|
||||
for i in range(len(var_ctx.split_varnames())):
|
||||
slice_varnames.append(f"{reshaped_varname}.block{i}")
|
||||
remote_varnames.append(reshaped_varname)
|
||||
|
||||
block.append_op(
|
||||
type='recv_save',
|
||||
attrs={
|
||||
"trainer_id": self._role_maker.worker_index(),
|
||||
"shape": var.shape,
|
||||
"slice_shapes": slice_shapes,
|
||||
"slice_varnames": slice_varnames,
|
||||
"remote_varnames": remote_varnames,
|
||||
"is_sparse": True,
|
||||
"endpoints": var_ctx.split_endpoints(),
|
||||
"pserver_num": len(
|
||||
self._role_maker.get_pserver_endpoints()
|
||||
),
|
||||
"file_path": os.path.join(dirname, var.name),
|
||||
},
|
||||
)
|
||||
|
||||
for origin_varname in origin_varnames:
|
||||
var = self._origin_main_program.global_block().vars[
|
||||
origin_varname
|
||||
]
|
||||
|
||||
block.append_op(
|
||||
type='recv_save',
|
||||
attrs={
|
||||
"trainer_id": self._role_maker.worker_index(),
|
||||
"shape": var.shape,
|
||||
"slice_shapes": [",".join([str(i) for i in var.shape])],
|
||||
"slice_varnames": [origin_varname],
|
||||
"remote_varnames": [origin_varname],
|
||||
"is_sparse": False,
|
||||
"endpoints": var_ctx.split_endpoints()[:1],
|
||||
"file_path": os.path.join(dirname, var.name),
|
||||
},
|
||||
)
|
||||
executor.run(prog)
|
||||
return context.keys()
|
||||
|
||||
def _save_distributed_params(
|
||||
self, executor, dirname, context, main_program
|
||||
):
|
||||
prog = Program()
|
||||
block = prog.global_block()
|
||||
|
||||
for name, var_ctx in context.items():
|
||||
block.append_op(
|
||||
type='checkpoint_notify',
|
||||
attrs={
|
||||
"varname": name,
|
||||
"is_slice": True,
|
||||
"slice_varnames": var_ctx.split_varnames(),
|
||||
"remote_varnames": var_ctx.split_varnames(),
|
||||
"endpoints": var_ctx.split_endpoints(),
|
||||
"dirname": dirname,
|
||||
},
|
||||
)
|
||||
|
||||
executor.run(prog)
|
||||
return context.keys()
|
||||
|
||||
def _save_distributed_persistables(self, executor, dirname, main_program):
|
||||
dense_ctx = fleet.compiled_config.get_communicator_recv_context(
|
||||
recv_type=1
|
||||
)
|
||||
|
||||
sparse_ctx = fleet.compiled_config.get_communicator_recv_context(
|
||||
recv_type=2
|
||||
)
|
||||
|
||||
distributed_ctx = fleet.compiled_config.get_communicator_recv_context(
|
||||
recv_type=3
|
||||
)
|
||||
|
||||
recv_dense_varnames = self._save_dense_params(
|
||||
executor, dirname, dense_ctx, main_program
|
||||
)
|
||||
|
||||
recv_sparse_varnames = self._save_sparse_params(
|
||||
executor, dirname, sparse_ctx, main_program
|
||||
)
|
||||
|
||||
recv_distributed_varnames = self._save_distributed_params(
|
||||
executor, dirname, distributed_ctx, main_program
|
||||
)
|
||||
|
||||
saved_varnames = (
|
||||
recv_dense_varnames
|
||||
+ list(recv_sparse_varnames)
|
||||
+ list(recv_distributed_varnames)
|
||||
)
|
||||
|
||||
remaining_vars = list(
|
||||
filter(
|
||||
FleetTranspiler.__exclude_vars(saved_varnames),
|
||||
main_program.list_vars(),
|
||||
)
|
||||
)
|
||||
|
||||
paddle.static.save_vars(
|
||||
executor,
|
||||
main_program=main_program,
|
||||
dirname=dirname,
|
||||
vars=remaining_vars,
|
||||
)
|
||||
|
||||
def save_persistables(self, executor, dirname, main_program=None, **kwargs):
|
||||
"""
|
||||
This function filters out all variables with `persistable==True` from the
|
||||
give `main_program` and then saves these variables to the folder `dirname`
|
||||
or file `filename`.
|
||||
|
||||
The `dirname` is used to specify the folder where persistable variables
|
||||
are going to be saved. If you would like to save variables in separate
|
||||
files, set `filename` None;
|
||||
if you would like to save all variables in a
|
||||
single file, use `filename` to specify the file name.
|
||||
"""
|
||||
|
||||
if self._inner_mode == PSMode.PSLIB:
|
||||
raise NotImplementedError("add implement later")
|
||||
|
||||
if not isinstance(executor, Executor):
|
||||
raise TypeError(
|
||||
"in fleet.save_persistables() function, executor must be as Executor type"
|
||||
)
|
||||
# Todo(MrChengmo): support recv&save GPU-Kernel for ps-gpu model save
|
||||
if not isinstance(executor.place, paddle.CPUPlace):
|
||||
save_executor = Executor(paddle.CPUPlace())
|
||||
else:
|
||||
save_executor = executor
|
||||
|
||||
if main_program is None:
|
||||
main_program = self.main_program
|
||||
|
||||
if isinstance(main_program, CompiledProgram):
|
||||
raise TypeError(
|
||||
"in fleet.save_persistables() function, main_program must be as Program type, CompiledProgram is not allowed"
|
||||
)
|
||||
|
||||
self._save_distributed_persistables(
|
||||
save_executor, dirname, main_program
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def __exclude_vars(exclude_var_names=[]):
|
||||
def is_valid(var):
|
||||
if var.name in exclude_var_names:
|
||||
return False
|
||||
|
||||
origin_varname, _, _ = public._get_varname_parts(var.name)
|
||||
if origin_varname.endswith("@GRAD"):
|
||||
return False
|
||||
|
||||
if origin_varname == "learning_rate_0":
|
||||
return False
|
||||
|
||||
if (
|
||||
var.desc.type() == core.VarDesc.VarType.FEED_MINIBATCH
|
||||
or var.desc.type() == core.VarDesc.VarType.FETCH_LIST
|
||||
or var.desc.type() == core.VarDesc.VarType.READER
|
||||
):
|
||||
return False
|
||||
return var.persistable
|
||||
|
||||
return is_valid
|
||||
|
||||
|
||||
# fleet is a global instance for parameter server.
|
||||
fleet = FleetTranspiler()
|
||||
|
||||
|
||||
class ParameterServerOptimizer(DistributedOptimizer):
|
||||
"""
|
||||
DistributedOptimizer is a wrapper for paddle.base.optimizer
|
||||
A user should pass a paddle.base.optimizer to DistributedOptimizer
|
||||
minimize() function is implemented.
|
||||
DistributedOptimizer is the starting point for a user who wants to
|
||||
run distributed training. The optimized information will be stored in
|
||||
Fleet() instance who holds the global information about current distributed
|
||||
training.
|
||||
|
||||
Args:
|
||||
optimizer(Optimizer): subclass of Optimizer.
|
||||
strategy(DistributeTranspilerConfig): instance of DistributeTranspilerConfig.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
def __init__(self, optimizer, strategy, mode=PSMode.TRANSPILER):
|
||||
super().__init__(optimizer, strategy)
|
||||
self._mode = mode
|
||||
if self._mode == PSMode.PSLIB:
|
||||
self._optimizer_name = f"Distributed{optimizer.type.capitalize()}"
|
||||
if optimizer.type != "adam":
|
||||
print(
|
||||
"Currently, distributed optimizer only support Adam"
|
||||
"Will config built-in adam for you."
|
||||
"We will support more functions in DistributedOptimizer",
|
||||
sys.stderr,
|
||||
)
|
||||
self._optimizer_name = "DistributedAdam"
|
||||
|
||||
self._optimizer = globals()[self._optimizer_name](optimizer)
|
||||
else:
|
||||
self._optimizer = optimizer
|
||||
|
||||
self._window = 1
|
||||
self.type = "downpour"
|
||||
self.data_norm_name = [
|
||||
".batch_size",
|
||||
".batch_square_sum",
|
||||
".batch_sum",
|
||||
".batch_size@GRAD",
|
||||
".batch_square_sum@GRAD",
|
||||
".batch_sum@GRAD",
|
||||
]
|
||||
|
||||
def backward(
|
||||
self,
|
||||
loss,
|
||||
startup_program=None,
|
||||
parameter_list=None,
|
||||
no_grad_set=None,
|
||||
callbacks=None,
|
||||
):
|
||||
raise NotImplementedError
|
||||
|
||||
def apply_gradients(self, params_grads):
|
||||
raise NotImplementedError
|
||||
|
||||
def _build_trainer_programs(self, compiled_config):
|
||||
_main = fleet._origin_main_program.clone()
|
||||
_startup = fleet._origin_startup_program.clone()
|
||||
|
||||
if not compiled_config.is_geo_mode():
|
||||
# for main program
|
||||
_main = worker.delete_optimizer_pass(_main, compiled_config)
|
||||
_main = worker.distributed_ops_pass(_main, compiled_config)
|
||||
_main = worker.append_send_ops_pass(_main, compiled_config)
|
||||
|
||||
# for startup program
|
||||
_startup = worker.fake_init_ops_pass(_startup, compiled_config)
|
||||
_startup = worker.init_from_server_pass(_startup, compiled_config)
|
||||
_startup = worker.delete_extra_optimizes_pass(
|
||||
_startup, compiled_config
|
||||
)
|
||||
else:
|
||||
_main = worker.append_send_ops_pass(_main, compiled_config)
|
||||
_startup = _startup
|
||||
|
||||
return _main, _startup
|
||||
|
||||
def _build_pserver_programs(self, compiled_config):
|
||||
_main = paddle.static.Program()
|
||||
_startup = paddle.static.Program()
|
||||
|
||||
if not compiled_config.is_geo_mode():
|
||||
_main = server.add_listen_and_serv_pass(_main, compiled_config)
|
||||
_main = server.add_rpc_global_flags_pass(_main, compiled_config)
|
||||
_main = server.add_optimizer_pass(_main, compiled_config)
|
||||
_main = server.large_scale_sparse_pass(
|
||||
_main, _main, compiled_config, False
|
||||
)
|
||||
_startup = server.build_pserver_startup_program_pass(
|
||||
_startup, _main, compiled_config
|
||||
)
|
||||
_startup = server.large_scale_sparse_pass(
|
||||
_startup, _main, compiled_config, True
|
||||
)
|
||||
|
||||
if not compiled_config.is_sync_mode():
|
||||
_main = server.delete_unused_in_main_pass(
|
||||
_main, compiled_config
|
||||
)
|
||||
|
||||
_startup = server.delete_unused_in_startup_pass(
|
||||
_startup, _main, compiled_config
|
||||
)
|
||||
else:
|
||||
_main = server.add_listen_and_serv_pass(_main, compiled_config)
|
||||
_main = server.add_rpc_global_flags_pass(_main, compiled_config)
|
||||
_main = server.add_geo_optimizer_pass(_main, compiled_config)
|
||||
_main = server.large_scale_sparse_pass(
|
||||
_main, _main, compiled_config, False
|
||||
)
|
||||
_startup = server.build_pserver_startup_program_pass(
|
||||
_startup, _main, compiled_config
|
||||
)
|
||||
_startup = server.large_scale_sparse_pass(
|
||||
_startup, _main, compiled_config, True
|
||||
)
|
||||
_startup = server.delete_unused_in_startup_pass(
|
||||
_startup, _main, compiled_config
|
||||
)
|
||||
|
||||
return _main, _startup
|
||||
|
||||
def minimize(
|
||||
self,
|
||||
losses,
|
||||
scopes=None,
|
||||
startup_programs=None,
|
||||
parameter_list=None,
|
||||
no_grad_set=None,
|
||||
):
|
||||
if isinstance(losses, list):
|
||||
raise ValueError("need implement later")
|
||||
|
||||
self._optimizer.minimize(
|
||||
losses, startup_programs, parameter_list, no_grad_set
|
||||
)
|
||||
|
||||
fleet._origin_main_program = default_main_program().clone()
|
||||
fleet._origin_startup_program = default_startup_program().clone()
|
||||
|
||||
compiled_config = public.CompileTimeStrategy(
|
||||
fleet._origin_main_program,
|
||||
fleet._origin_startup_program,
|
||||
self._strategy,
|
||||
fleet._role_maker,
|
||||
)
|
||||
|
||||
fleet.compiled_config = compiled_config
|
||||
fleet.main_program, fleet.startup_program = (
|
||||
self._build_trainer_programs(compiled_config)
|
||||
if fleet.is_worker()
|
||||
else self._build_pserver_programs(compiled_config)
|
||||
)
|
||||
+421
@@ -0,0 +1,421 @@
|
||||
# Copyright (c) 2019 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.
|
||||
|
||||
__all__ = []
|
||||
|
||||
import os
|
||||
|
||||
from paddle import base
|
||||
from paddle.distributed.transpiler.distribute_transpiler import (
|
||||
DistributeTranspilerConfig,
|
||||
ServerRuntimeConfig,
|
||||
)
|
||||
from paddle.incubate.distributed.fleet.parameter_server.mode import (
|
||||
DistributedMode,
|
||||
)
|
||||
|
||||
|
||||
class TrainerRuntimeConfig:
|
||||
def __init__(self):
|
||||
self.mode = None
|
||||
num_threads = os.getenv("CPU_NUM", "1")
|
||||
|
||||
self.runtime_configs = {}
|
||||
self.runtime_configs['communicator_max_merge_var_num'] = os.getenv(
|
||||
"FLAGS_communicator_max_merge_var_num", num_threads
|
||||
)
|
||||
self.runtime_configs['communicator_send_queue_size'] = os.getenv(
|
||||
"FLAGS_communicator_send_queue_size", num_threads
|
||||
)
|
||||
self.runtime_configs['communicator_independent_recv_thread'] = (
|
||||
os.getenv("FLAGS_communicator_independent_recv_thread", "1")
|
||||
)
|
||||
self.runtime_configs['communicator_min_send_grad_num_before_recv'] = (
|
||||
os.getenv(
|
||||
"FLAGS_communicator_min_send_grad_num_before_recv", num_threads
|
||||
)
|
||||
)
|
||||
self.runtime_configs['communicator_thread_pool_size'] = os.getenv(
|
||||
"FLAGS_communicator_thread_pool_size", "5"
|
||||
)
|
||||
self.runtime_configs['communicator_send_wait_times'] = os.getenv(
|
||||
"FLAGS_communicator_send_wait_times", "5"
|
||||
)
|
||||
self.runtime_configs['communicator_is_sgd_optimizer'] = os.getenv(
|
||||
"FLAGS_communicator_is_sgd_optimizer", "1"
|
||||
)
|
||||
|
||||
# not used
|
||||
self.runtime_configs['rpc_deadline'] = os.getenv(
|
||||
"FLAGS_rpc_deadline", "180000"
|
||||
)
|
||||
self.runtime_configs['rpc_retry_times'] = os.getenv(
|
||||
"FLAGS_rpc_retry_times", "3"
|
||||
)
|
||||
|
||||
def get_communicator_flags(self):
|
||||
need_keys = []
|
||||
num_threads = os.getenv("CPU_NUM", "1")
|
||||
mode_str = ""
|
||||
if self.mode is None or self.mode == DistributedMode.ASYNC:
|
||||
need_keys = self.runtime_configs.keys()
|
||||
mode_str = "async"
|
||||
elif (
|
||||
self.mode == DistributedMode.SYNC
|
||||
or self.mode == DistributedMode.HALF_ASYNC
|
||||
):
|
||||
mode_str = "sync or half_async"
|
||||
need_keys = [
|
||||
'communicator_max_merge_var_num',
|
||||
'communicator_send_wait_times',
|
||||
'communicator_thread_pool_size',
|
||||
'communicator_send_queue_size',
|
||||
]
|
||||
elif self.mode == DistributedMode.GEO:
|
||||
mode_str = "GEO"
|
||||
need_keys = [
|
||||
'communicator_thread_pool_size',
|
||||
'communicator_send_wait_times',
|
||||
'communicator_max_merge_var_num',
|
||||
'communicator_send_queue_size',
|
||||
]
|
||||
else:
|
||||
raise ValueError("Unsupported Mode")
|
||||
|
||||
if (
|
||||
self.mode == DistributedMode.SYNC
|
||||
or self.mode == DistributedMode.HALF_ASYNC
|
||||
):
|
||||
max_merge_var_num = self.runtime_configs[
|
||||
'communicator_max_merge_var_num'
|
||||
]
|
||||
send_queue_size = self.runtime_configs[
|
||||
'communicator_send_queue_size'
|
||||
]
|
||||
if max_merge_var_num != num_threads:
|
||||
print(
|
||||
f'WARNING: In {mode_str} mode, communicator_max_merge_var_num '
|
||||
'must be equal to CPU_NUM. But received, '
|
||||
f'communicator_max_merge_var_num = {max_merge_var_num}, CPU_NUM = '
|
||||
f'{num_threads}. communicator_max_merge_var_num will be forced to {num_threads}.'
|
||||
)
|
||||
self.runtime_configs['communicator_max_merge_var_num'] = (
|
||||
num_threads
|
||||
)
|
||||
if send_queue_size != num_threads:
|
||||
print(
|
||||
f'WARNING: In {mode_str} mode, communicator_send_queue_size '
|
||||
'must be equal to CPU_NUM. But received, '
|
||||
f'communicator_send_queue_size = {send_queue_size}, CPU_NUM = '
|
||||
f'{num_threads}. communicator_send_queue_size will be forced to {num_threads}.'
|
||||
)
|
||||
self.runtime_configs['communicator_send_queue_size'] = (
|
||||
num_threads
|
||||
)
|
||||
|
||||
return {key: str(self.runtime_configs[key]) for key in need_keys}
|
||||
|
||||
def display(self, configs):
|
||||
raw0, raw1, length = 45, 5, 50
|
||||
h_format = "{:^45s}{:<5s}\n"
|
||||
l_format = "{:<45s}{:<5s}\n"
|
||||
|
||||
border = "".join(["="] * length)
|
||||
line = "".join(["-"] * length)
|
||||
|
||||
draws = ""
|
||||
draws += border + "\n"
|
||||
draws += h_format.format("TrainerRuntimeConfig Overview", "Value")
|
||||
draws += line + "\n"
|
||||
|
||||
for k, v in configs.items():
|
||||
draws += l_format.format(k, v)
|
||||
|
||||
draws += border
|
||||
|
||||
_str = f"\n{draws}\n"
|
||||
return _str
|
||||
|
||||
def __repr__(self):
|
||||
return self.display(self.get_communicator_flags())
|
||||
|
||||
|
||||
class PSLibRuntimeConfig:
|
||||
def __init__(self):
|
||||
self.runtime_configs = {}
|
||||
|
||||
def get_runtime_configs(self):
|
||||
return self.runtime_configs
|
||||
|
||||
|
||||
class DistributedStrategy:
|
||||
def __init__(self):
|
||||
self._program_config = DistributeTranspilerConfig()
|
||||
self._trainer_runtime_config = TrainerRuntimeConfig()
|
||||
self._pslib_runtime_config = PSLibRuntimeConfig()
|
||||
self._server_runtime_config = ServerRuntimeConfig()
|
||||
num_threads = int(os.getenv("CPU_NUM", "1"))
|
||||
|
||||
self._build_strategy = base.BuildStrategy()
|
||||
|
||||
if num_threads > 1:
|
||||
self._build_strategy.reduce_strategy = (
|
||||
base.BuildStrategy.ReduceStrategy.Reduce
|
||||
)
|
||||
self.debug_opt = None
|
||||
self.use_ps_gpu = False
|
||||
|
||||
def set_debug_opt(self, opt_info):
|
||||
self.debug_opt = opt_info
|
||||
|
||||
def get_debug_opt(self):
|
||||
opt_info = {}
|
||||
if self.debug_opt is not None and isinstance(self.debug_opt, dict):
|
||||
opt_info["dump_slot"] = bool(self.debug_opt.get("dump_slot", 0))
|
||||
opt_info["dump_converter"] = str(
|
||||
self.debug_opt.get("dump_converter", "")
|
||||
)
|
||||
opt_info["dump_fields"] = self.debug_opt.get("dump_fields", [])
|
||||
opt_info["dump_file_num"] = self.debug_opt.get("dump_file_num", 16)
|
||||
opt_info["dump_fields_path"] = self.debug_opt.get(
|
||||
"dump_fields_path", ""
|
||||
)
|
||||
opt_info["dump_param"] = self.debug_opt.get("dump_param", [])
|
||||
return opt_info
|
||||
|
||||
def get_program_config(self):
|
||||
return self._program_config
|
||||
|
||||
def set_program_config(self, config):
|
||||
if isinstance(config, DistributeTranspilerConfig):
|
||||
self._program_config = config
|
||||
elif isinstance(config, dict):
|
||||
for key in config:
|
||||
if hasattr(self._program_config, key):
|
||||
setattr(self._program_config, key, config[key])
|
||||
else:
|
||||
raise ValueError(
|
||||
f"DistributeTranspilerConfig doesn't have key: {key}"
|
||||
)
|
||||
else:
|
||||
raise TypeError(
|
||||
"program_config only accept input type: dict or DistributeTranspilerConfig"
|
||||
)
|
||||
self.check_program_config()
|
||||
|
||||
def check_program_config(self):
|
||||
raise NotImplementedError(
|
||||
"check_program_config must be implemented by derived class. You should use StrategyFactory to create DistributedStrategy."
|
||||
)
|
||||
|
||||
def get_trainer_runtime_config(self):
|
||||
return self._trainer_runtime_config
|
||||
|
||||
def set_trainer_runtime_config(self, config):
|
||||
if isinstance(config, TrainerRuntimeConfig):
|
||||
self._trainer_runtime_config = config
|
||||
elif isinstance(config, dict):
|
||||
for key, Value in config.items():
|
||||
if key in self._trainer_runtime_config.runtime_configs:
|
||||
self._trainer_runtime_config.runtime_configs[key] = Value
|
||||
else:
|
||||
raise ValueError(
|
||||
f"TrainerRuntimeConfig doesn't have key: {key}"
|
||||
)
|
||||
else:
|
||||
raise TypeError(
|
||||
"trainer_runtime_config only accept input type: dict or TrainerRuntimeConfig"
|
||||
)
|
||||
self.check_trainer_runtime_config()
|
||||
|
||||
def check_trainer_runtime_config(self):
|
||||
raise NotImplementedError(
|
||||
"check_trainer_runtime_config must be implemented by derived class. You should use StrategyFactory to create DistributedStrategy."
|
||||
)
|
||||
|
||||
def get_pslib_runtime_config(self):
|
||||
return self._pslib_runtime_config
|
||||
|
||||
def set_pslib_runtime_config(self, config):
|
||||
self._pslib_runtime_config.runtime_configs = config
|
||||
|
||||
def get_server_runtime_config(self):
|
||||
return self._server_runtime_config
|
||||
|
||||
def set_server_runtime_config(self, config):
|
||||
if isinstance(config, ServerRuntimeConfig):
|
||||
self._server_runtime_config = config
|
||||
elif isinstance(config, dict):
|
||||
for key in config:
|
||||
if hasattr(self._server_runtime_config, key):
|
||||
setattr(self._server_runtime_config, key, config[key])
|
||||
else:
|
||||
raise ValueError(
|
||||
f"ServerRuntimeConfig doesn't have key: {key}"
|
||||
)
|
||||
else:
|
||||
raise TypeError(
|
||||
"server_runtime_config only accept input type: dict or ServerRuntimeConfig"
|
||||
)
|
||||
self.check_server_runtime_config()
|
||||
|
||||
def check_server_runtime_config(self):
|
||||
raise NotImplementedError(
|
||||
"check_server_runtime_config must be implemented by derived class. You should use StrategyFactory to create DistributedStrategy."
|
||||
)
|
||||
|
||||
def get_build_strategy(self):
|
||||
return self._build_strategy
|
||||
|
||||
def set_build_strategy(self, config):
|
||||
if isinstance(config, base.BuildStrategy):
|
||||
self._build_strategy = config
|
||||
elif isinstance(config, dict):
|
||||
for key in config:
|
||||
if hasattr(self._build_strategy, key):
|
||||
setattr(self._build_strategy, key, config[key])
|
||||
else:
|
||||
raise ValueError(f"BuildStrategy doesn't have key: {key}")
|
||||
else:
|
||||
raise TypeError(
|
||||
"build_strategy only accept input type: dict or BuildStrategy"
|
||||
)
|
||||
self.check_build_strategy()
|
||||
|
||||
def check_build_strategy(self):
|
||||
raise NotImplementedError(
|
||||
"check_build_strategy must be implemented by derived class. You should use StrategyFactory to create DistributedStrategy."
|
||||
)
|
||||
|
||||
|
||||
class SyncStrategy(DistributedStrategy):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.check_program_config()
|
||||
self.check_trainer_runtime_config()
|
||||
self.check_server_runtime_config()
|
||||
self.check_build_strategy()
|
||||
|
||||
def check_trainer_runtime_config(self):
|
||||
self._trainer_runtime_config.mode = DistributedMode.SYNC
|
||||
|
||||
def check_program_config(self):
|
||||
self._program_config.sync_mode = False
|
||||
self._program_config.runtime_split_send_recv = True
|
||||
self._program_config.half_async = True
|
||||
self._program_config.completely_not_async = True
|
||||
|
||||
def check_server_runtime_config(self):
|
||||
pass
|
||||
|
||||
def check_build_strategy(self):
|
||||
self._build_strategy.async_mode = True
|
||||
|
||||
|
||||
class AsyncStrategy(DistributedStrategy):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.check_program_config()
|
||||
self.check_trainer_runtime_config()
|
||||
self.check_server_runtime_config()
|
||||
self.check_build_strategy()
|
||||
|
||||
def check_trainer_runtime_config(self):
|
||||
self._trainer_runtime_config.mode = DistributedMode.ASYNC
|
||||
|
||||
def check_program_config(self):
|
||||
self._program_config.sync_mode = False
|
||||
self._program_config.runtime_split_send_recv = True
|
||||
|
||||
def check_server_runtime_config(self):
|
||||
pass
|
||||
|
||||
def check_build_strategy(self):
|
||||
self._build_strategy.async_mode = True
|
||||
|
||||
|
||||
class HalfAsyncStrategy(DistributedStrategy):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.check_program_config()
|
||||
self.check_trainer_runtime_config()
|
||||
self.check_server_runtime_config()
|
||||
self.check_build_strategy()
|
||||
|
||||
def check_trainer_runtime_config(self):
|
||||
self._trainer_runtime_config.mode = DistributedMode.HALF_ASYNC
|
||||
|
||||
def check_program_config(self):
|
||||
self._program_config.sync_mode = False
|
||||
self._program_config.runtime_split_send_recv = True
|
||||
self._program_config.half_async = True
|
||||
|
||||
def check_server_runtime_config(self):
|
||||
pass
|
||||
|
||||
def check_build_strategy(self):
|
||||
self._build_strategy.async_mode = True
|
||||
|
||||
|
||||
class GeoStrategy(DistributedStrategy):
|
||||
def __init__(self, update_frequency=100):
|
||||
super().__init__()
|
||||
self._program_config.geo_sgd_need_push_nums = update_frequency
|
||||
self.check_program_config()
|
||||
self.check_trainer_runtime_config()
|
||||
self.check_server_runtime_config()
|
||||
self.check_build_strategy()
|
||||
|
||||
def check_program_config(self):
|
||||
self._program_config.sync_mode = False
|
||||
self._program_config.runtime_split_send_recv = True
|
||||
self._program_config.geo_sgd_mode = True
|
||||
|
||||
def check_trainer_runtime_config(self):
|
||||
self._trainer_runtime_config.mode = DistributedMode.GEO
|
||||
|
||||
self._trainer_runtime_config.runtime_configs[
|
||||
'communicator_send_queue_size'
|
||||
] = self._program_config.geo_sgd_need_push_nums
|
||||
|
||||
self._trainer_runtime_config.runtime_configs[
|
||||
'communicator_max_merge_var_num'
|
||||
] = self._program_config.geo_sgd_need_push_nums
|
||||
|
||||
def check_server_runtime_config(self):
|
||||
pass
|
||||
|
||||
def check_build_strategy(self):
|
||||
self._build_strategy.async_mode = True
|
||||
|
||||
|
||||
class StrategyFactory:
|
||||
def __init_(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def create_sync_strategy():
|
||||
return SyncStrategy()
|
||||
|
||||
@staticmethod
|
||||
def create_half_async_strategy():
|
||||
return HalfAsyncStrategy()
|
||||
|
||||
@staticmethod
|
||||
def create_async_strategy():
|
||||
return AsyncStrategy()
|
||||
|
||||
@staticmethod
|
||||
def create_geo_strategy(update_frequency=100):
|
||||
return GeoStrategy(update_frequency)
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copyright (c) 2020 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.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Copyright (c) 2020 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 warnings
|
||||
|
||||
import paddle
|
||||
from paddle.incubate.distributed.fleet.parameter_server.ir.trainer_pass import (
|
||||
create_heter_program,
|
||||
create_trainer_program,
|
||||
find_block_joints,
|
||||
find_heter_ops,
|
||||
union_forward_gradient_op,
|
||||
)
|
||||
|
||||
|
||||
def split_heter_worker_ops_pass(program, config, stage_id, device):
|
||||
"""
|
||||
split heter worker program from origin-program
|
||||
1. find heter op (located on different device)
|
||||
2. find input&output of every heter-block
|
||||
3. create heter worker program, add listen&serv op
|
||||
"""
|
||||
default_device = "cpu"
|
||||
program, heter_ops, _, program_block_ops = find_heter_ops(
|
||||
program, default_device
|
||||
)
|
||||
if len(heter_ops) == 0:
|
||||
warnings.warn(
|
||||
"Currently running in Heter Parameter Server mode, but no OP running on heterogeneous devices, Please check your code."
|
||||
)
|
||||
return program
|
||||
|
||||
program_block_ops = union_forward_gradient_op(program_block_ops)
|
||||
block_vars_detail = find_block_joints(program, program_block_ops, heter_ops)
|
||||
heter_program = paddle.static.Program()
|
||||
create_heter_program(
|
||||
program,
|
||||
config,
|
||||
heter_program,
|
||||
program_block_ops,
|
||||
heter_ops,
|
||||
block_vars_detail,
|
||||
device,
|
||||
stage_id,
|
||||
)
|
||||
return heter_program
|
||||
|
||||
|
||||
def split_trainer_ops_pass(program, config, default_device="cpu"):
|
||||
"""
|
||||
split cpu-trainer program from origin-program
|
||||
1. find heter op (located on different device)
|
||||
2. find input&output of every heter-block
|
||||
3. create cpu-trainer program, add send&recv op
|
||||
"""
|
||||
# Todo: support user define default_device (MrChengmo)
|
||||
default_device_ = default_device
|
||||
program, heter_ops, default_ops, program_block_ops = find_heter_ops(
|
||||
program, default_device_
|
||||
)
|
||||
program_block_ops = union_forward_gradient_op(program_block_ops)
|
||||
|
||||
block_vars_detail = find_block_joints(program, program_block_ops, heter_ops)
|
||||
trainer_program = program.clone()
|
||||
create_trainer_program(
|
||||
trainer_program, program, config, program_block_ops, block_vars_detail
|
||||
)
|
||||
return trainer_program
|
||||
@@ -0,0 +1,127 @@
|
||||
# Copyright (c) 2018 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.
|
||||
|
||||
|
||||
class PSDispatcher:
|
||||
"""
|
||||
PSDispatcher is the base class for dispatching vars
|
||||
into different pserver instance.
|
||||
You need to implement the `dispatch` interface.
|
||||
"""
|
||||
|
||||
def __init__(self, pserver_endpoints):
|
||||
self._eps = pserver_endpoints
|
||||
self._step = 0
|
||||
|
||||
@property
|
||||
def eps(self):
|
||||
return self._eps
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
reset the step counter, set it zero.
|
||||
"""
|
||||
self._step = 0
|
||||
|
||||
def dispatch(self, varlist):
|
||||
"""
|
||||
Args:
|
||||
varlist(list): a list of Variables
|
||||
Returns:
|
||||
a map of pserver endpoint -> varname
|
||||
"""
|
||||
raise NotImplementedError("Interface has not been implemented.")
|
||||
|
||||
|
||||
class HashName(PSDispatcher):
|
||||
"""
|
||||
Hash variable names to several endpoints using python
|
||||
"hash()" function.
|
||||
|
||||
Args:
|
||||
pserver_endpoints (list): list of endpoint(ip:port).
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from paddle.incubate.distributed.fleet.parameter_server.ir.ps_dispatcher import RoundRobin
|
||||
|
||||
>>> pserver_endpoints = ["127.0.0.1:6007", "127.0.0.1:6008"]
|
||||
>>> vars = ["var1", "var2", "var3", "var4", "var5"]
|
||||
|
||||
>>> rr = HashName(pserver_endpoints)
|
||||
>>> rr.dispatch(vars)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, pserver_endpoints):
|
||||
super().__init__(pserver_endpoints)
|
||||
|
||||
def _hash_block(self, block_str, total):
|
||||
return hash(block_str) % total
|
||||
|
||||
def dispatch(self, varlist):
|
||||
"""
|
||||
use `HashName` method to dispatch variables with each parameter server.
|
||||
Args:
|
||||
varlist (list): a list of Variables
|
||||
|
||||
"""
|
||||
eplist = []
|
||||
for var in varlist:
|
||||
server_id = self._hash_block(var.name(), len(self._eps))
|
||||
server_for_param = self._eps[server_id]
|
||||
eplist.append(server_for_param)
|
||||
return eplist
|
||||
|
||||
|
||||
class RoundRobin(PSDispatcher):
|
||||
"""
|
||||
Distribute variables to several endpoints using
|
||||
RondRobin<https://en.wikipedia.org/wiki/Round-robin_scheduling> method.
|
||||
|
||||
Args:
|
||||
pserver_endpoints (list): list of endpoint(ip:port).
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from paddle.incubate.distributed.fleet.parameter_server.ir.ps_dispatcher import RoundRobin
|
||||
|
||||
>>> pserver_endpoints = ["127.0.0.1:6007", "127.0.0.1:6008"]
|
||||
>>> vars = ["var1", "var2", "var3", "var4", "var5"]
|
||||
|
||||
>>> rr = RoundRobin(pserver_endpoints)
|
||||
>>> rr.dispatch(vars)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, pserver_endpoints):
|
||||
super().__init__(pserver_endpoints)
|
||||
|
||||
def dispatch(self, varlist):
|
||||
"""
|
||||
use `RoundRobin` method to dispatch variables with each parameter server.
|
||||
Args:
|
||||
varlist (list): a list of Variables
|
||||
|
||||
"""
|
||||
eplist = []
|
||||
for var in varlist:
|
||||
server_for_param = self._eps[self._step]
|
||||
eplist.append(server_for_param)
|
||||
self._step += 1
|
||||
if self._step >= len(self._eps):
|
||||
self._step = 0
|
||||
return eplist
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,64 @@
|
||||
# Copyright (c) 2018 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.
|
||||
|
||||
|
||||
class UnionFind:
|
||||
"""Union-find data structure.
|
||||
|
||||
Union-find is a data structure that keeps track of a set of elements partitioned
|
||||
into a number of disjoint (non-overlapping) subsets.
|
||||
|
||||
Reference:
|
||||
https://en.wikipedia.org/wiki/Disjoint-set_data_structure
|
||||
|
||||
Args:
|
||||
elements(list): The initialize element list.
|
||||
"""
|
||||
|
||||
def __init__(self, elements=None):
|
||||
self._parents = [] # index -> parent index
|
||||
self._index = {} # element -> index
|
||||
self._curr_idx = 0
|
||||
if not elements:
|
||||
elements = []
|
||||
for ele in elements:
|
||||
self._parents.append(self._curr_idx)
|
||||
self._index.update({ele: self._curr_idx})
|
||||
self._curr_idx += 1
|
||||
|
||||
def find(self, x):
|
||||
# Find the root index of given element x,
|
||||
# execute the path compress while finding the root index
|
||||
if x not in self._index:
|
||||
return -1
|
||||
idx = self._index[x]
|
||||
while idx != self._parents[idx]:
|
||||
t = self._parents[idx]
|
||||
self._parents[idx] = self._parents[t]
|
||||
idx = t
|
||||
return idx
|
||||
|
||||
def union(self, x, y):
|
||||
# Union two given element
|
||||
x_root = self.find(x)
|
||||
y_root = self.find(y)
|
||||
|
||||
if x_root == y_root:
|
||||
return
|
||||
self._parents[x_root] = y_root
|
||||
|
||||
def is_connected(self, x, y):
|
||||
# If two given elements have the same root index,
|
||||
# then they are connected.
|
||||
return self.find(x) == self.find(y)
|
||||
@@ -0,0 +1,206 @@
|
||||
# Copyright (c) 2018 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.
|
||||
from functools import reduce
|
||||
|
||||
from paddle.framework import core
|
||||
from paddle.framework.io import Variable
|
||||
|
||||
dtype_to_size = {
|
||||
core.VarDesc.VarType.FP16: 2,
|
||||
core.VarDesc.VarType.FP32: 4,
|
||||
core.VarDesc.VarType.FP64: 8,
|
||||
core.VarDesc.VarType.INT16: 2,
|
||||
core.VarDesc.VarType.INT32: 4,
|
||||
core.VarDesc.VarType.INT64: 8,
|
||||
core.VarDesc.VarType.BOOL: 1,
|
||||
core.VarDesc.VarType.UINT8: 1,
|
||||
}
|
||||
|
||||
|
||||
class VarBlock:
|
||||
def __init__(self, varname, offset, size):
|
||||
self.varname = varname
|
||||
# NOTE: real offset is offset * size
|
||||
self.offset = offset
|
||||
self.size = size
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.varname}:{int(self.offset)}:{int(self.size)}"
|
||||
|
||||
|
||||
def create_var_struct(var):
|
||||
if var.type == core.VarDesc.VarType.SELECTED_ROWS:
|
||||
lod_level = None
|
||||
elif var.type == core.VarDesc.VarType.DENSE_TENSOR:
|
||||
lod_level = var.lod_level
|
||||
else:
|
||||
raise ValueError("can only support SELECTED_ROWS/DENSE_TENSOR now")
|
||||
|
||||
return VarStruct(
|
||||
var.name, var.shape, var.dtype, var.type, lod_level, var.persistable
|
||||
)
|
||||
|
||||
|
||||
class VarStruct:
|
||||
"""
|
||||
record part properties of a Variable in python.
|
||||
"""
|
||||
|
||||
def __init__(self, name, shape, dtype, type, lod_level, persistable):
|
||||
self.name = name
|
||||
self.shape = shape
|
||||
self.dtype = dtype
|
||||
self.type = type
|
||||
self.lod_level = lod_level
|
||||
self.persistable = persistable
|
||||
self.m_size = 1
|
||||
self.m_size = reduce(lambda x, y: x * y, shape, 1)
|
||||
self.m_size *= dtype_to_size[dtype]
|
||||
|
||||
def __str__(self):
|
||||
return f"N: {self.name}, S: {self.shape}, D: {self.dtype}, T: {self.type}, LL: {self.lod_level}, P: {self.persistable}, M: {self.m_size}"
|
||||
|
||||
|
||||
class VarDistributed:
|
||||
"""
|
||||
a class to record the var distributed on parameter servers.
|
||||
the class will record the relationship between origin var and slice var.
|
||||
the slice var's properties, such as type/shape/offset/endpoint.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
origin_var,
|
||||
slice_var,
|
||||
is_slice=None,
|
||||
block_id=None,
|
||||
offset=None,
|
||||
vtype=None,
|
||||
endpoint=None,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
origin_var(Variable|VarStruct): origin var properties
|
||||
slice_var(Variable|VarStruct): slice var properties
|
||||
is_slice(bool|None): slice or not, slice_var=True/False and its block size > 8192 are the judgement standard.
|
||||
block_id(int|None): the number about the slice var.
|
||||
offset(int|None): if the slice var is sliced, offset is the numel before the var.
|
||||
vtype(str|None): a tag, such as Optimizer/Param/RemotePrefetch.
|
||||
endpoint(str|None): which parameter the slice var on, such as "127.0.0.1:1001"
|
||||
"""
|
||||
|
||||
if isinstance(origin_var, Variable):
|
||||
self.origin = create_var_struct(origin_var)
|
||||
else:
|
||||
self.origin = origin_var
|
||||
|
||||
if isinstance(slice_var, Variable):
|
||||
self.slice = create_var_struct(slice_var)
|
||||
else:
|
||||
self.slice = slice_var
|
||||
|
||||
if self.equal(self.origin, self.slice):
|
||||
self.is_slice = False
|
||||
self.block_id = 0
|
||||
self.offset = 0
|
||||
else:
|
||||
self.is_slice = True
|
||||
self.block_id = 0
|
||||
self.offset = 0
|
||||
|
||||
if is_slice is not None:
|
||||
self.is_slice = is_slice
|
||||
if block_id is not None:
|
||||
self.block_id = block_id
|
||||
if offset is not None:
|
||||
self.offset = offset
|
||||
|
||||
self.vtype = vtype
|
||||
self.endpoint = endpoint
|
||||
|
||||
@staticmethod
|
||||
def equal(var1, var2):
|
||||
"""
|
||||
the two var is equal or not.
|
||||
Returns:
|
||||
bool: equal will return True else False
|
||||
"""
|
||||
assert isinstance(var1, VarStruct) and isinstance(var2, VarStruct)
|
||||
|
||||
return (
|
||||
var1.name == var2.name
|
||||
and var1.type == var2.type
|
||||
and var1.shape == var2.shape
|
||||
and var1.dtype == var2.dtype
|
||||
and var1.lod_level == var2.lod_level
|
||||
and var1.persistable == var2.persistable
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
origin_var_str = f"{self.origin.name} : base.{self.origin.type}.shape{self.origin.shape}.astype({self.origin.dtype})"
|
||||
|
||||
slice_var_str = (
|
||||
f"{self.slice.name} : base.{self.slice.type}.shape{self.slice.shape}.astype({self.slice.dtype})"
|
||||
f".slice({self.is_slice}).block({self.block_id}).offset({self.offset})"
|
||||
)
|
||||
|
||||
return f"var owned: {self.vtype}, origin var: ( {origin_var_str} ), slice var: ( {slice_var_str} ), endpoint: {self.endpoint} "
|
||||
|
||||
|
||||
class VarsDistributed:
|
||||
"""
|
||||
a gather about VarDistributed with many methods to find distributed vars.
|
||||
through the class, we can get overview about the distributed parameters on parameter servers.
|
||||
this class may centralized and convenient for developer to manage and get variable's distribute.
|
||||
other module can also use this to find variables such io.py.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.distributed_vars = []
|
||||
|
||||
def add_distributed_var(
|
||||
self,
|
||||
origin_var,
|
||||
slice_var,
|
||||
is_slice=None,
|
||||
block_id=None,
|
||||
offset=None,
|
||||
vtype=None,
|
||||
endpoint=None,
|
||||
):
|
||||
"""
|
||||
add distributed var in this.
|
||||
|
||||
Args:
|
||||
origin_var(Variable|VarStruct): origin var properties
|
||||
slice_var(Variable|VarStruct): slice var properties
|
||||
is_slice(bool|None): slice or not, slice_var=True/False and its block size > 8192 are the judgement standard.
|
||||
block_id(int|None): the number about the slice var.
|
||||
offset(int|None): if the slice var is sliced, offset is the numel before the var.
|
||||
vtype(str|None): a tag, such as Optimizer/Param/RemotePrefetch.
|
||||
endpoint(str|None): which parameter the slice var on, such as "127.0.0.1:1001"
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
self.distributed_vars.append(
|
||||
VarDistributed(
|
||||
origin_var,
|
||||
slice_var,
|
||||
is_slice,
|
||||
block_id,
|
||||
offset,
|
||||
vtype,
|
||||
endpoint,
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
# Copyright (c) 2020 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.
|
||||
|
||||
|
||||
class PSMode:
|
||||
"""
|
||||
There are various mode for fleet, each of them is designed for different model.
|
||||
"""
|
||||
|
||||
TRANSPILER = 1
|
||||
PSLIB = 2
|
||||
|
||||
|
||||
class DistributedMode:
|
||||
SYNC = 0
|
||||
ASYNC = 1
|
||||
HALF_ASYNC = 2
|
||||
GEO = 3
|
||||
@@ -0,0 +1 @@
|
||||
ps_pb2.py
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,798 @@
|
||||
# Copyright (c) 2018 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
|
||||
"""Definition of Server and Worker."""
|
||||
|
||||
# NOTE: reduce removed in functools in python3
|
||||
from functools import reduce
|
||||
|
||||
from . import ps_pb2 as pslib
|
||||
|
||||
|
||||
class Server:
|
||||
"""
|
||||
A Server basic class
|
||||
it's a base class, does not have implementation
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
|
||||
class Worker:
|
||||
"""
|
||||
A Worker basic class.
|
||||
it's a base class, does not have implementation
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
|
||||
class DownpourServer(Server):
|
||||
"""
|
||||
DownpourServer class is used to generate server program_desc
|
||||
Args:
|
||||
server: it is pslib.ServerParameter()
|
||||
Examples:
|
||||
server = DownpourServer()
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._server = pslib.ServerParameter()
|
||||
self._server.downpour_server_param.service_param.server_class = (
|
||||
"DownpourBrpcPsServer"
|
||||
)
|
||||
self._server.downpour_server_param.service_param.client_class = (
|
||||
"DownpourBrpcPsClient"
|
||||
)
|
||||
self._server.downpour_server_param.service_param.service_class = (
|
||||
"DownpourPsService"
|
||||
)
|
||||
self._server.downpour_server_param.service_param.start_server_port = 0
|
||||
self._server.downpour_server_param.service_param.server_thread_num = 12
|
||||
|
||||
def add_sparse_table(self, table_id, strategy):
|
||||
"""
|
||||
Args:
|
||||
table_id(int): id of sparse params table
|
||||
strategy(dict): the config dict.
|
||||
Returns:
|
||||
return None
|
||||
"""
|
||||
|
||||
for table in self._server.downpour_server_param.downpour_table_param:
|
||||
if table.table_id == table_id:
|
||||
if table.type == pslib.PS_SPARSE_TABLE:
|
||||
return
|
||||
else:
|
||||
raise ValueError(
|
||||
f"expect table {table_id} type={pslib.PS_SPARSE_TABLE}, but actual type={table.type}"
|
||||
)
|
||||
if strategy is None:
|
||||
strategy = {}
|
||||
table = self._server.downpour_server_param.downpour_table_param.add()
|
||||
table.table_id = table_id
|
||||
table.type = pslib.PS_SPARSE_TABLE
|
||||
|
||||
support_sparse_key_list = [
|
||||
'sparse_table_class',
|
||||
'sparse_compress_in_save',
|
||||
'sparse_shard_num',
|
||||
'sparse_accessor_class',
|
||||
'sparse_learning_rate',
|
||||
'sparse_initial_g2sum',
|
||||
'sparse_initial_range',
|
||||
'sparse_weight_bounds',
|
||||
'sparse_embedx_dim',
|
||||
'sparse_embedx_threshold',
|
||||
'sparse_nonclk_coeff',
|
||||
'sparse_click_coeff',
|
||||
'sparse_base_threshold',
|
||||
'sparse_delta_threshold',
|
||||
'sparse_delta_keep_days',
|
||||
'sparse_delete_after_unseen_days',
|
||||
'sparse_show_click_decay_rate',
|
||||
'sparse_delete_threshold',
|
||||
'sparse_converter',
|
||||
'sparse_deconverter',
|
||||
'sparse_enable_cache',
|
||||
'sparse_cache_rate',
|
||||
'sparse_cache_file_num',
|
||||
'sparse_beta1_decay_rate',
|
||||
'sparse_beta2_decay_rate',
|
||||
'sparse_ada_epsilon',
|
||||
'sparse_optimizer',
|
||||
'sparse_ssd_unseenday_threshold',
|
||||
'embed_sparse_optimizer',
|
||||
'embed_sparse_learning_rate',
|
||||
'embed_sparse_weight_bounds',
|
||||
'embed_sparse_initial_range',
|
||||
'embed_sparse_initial_g2sum',
|
||||
'embed_sparse_beta1_decay_rate',
|
||||
'embed_sparse_beta2_decay_rate',
|
||||
'embedx_sparse_optimizer',
|
||||
'embedx_sparse_learning_rate',
|
||||
'embedx_sparse_weight_bounds',
|
||||
'embedx_sparse_initial_range',
|
||||
'embedx_sparse_initial_g2sum',
|
||||
'embedx_sparse_beta1_decay_rate',
|
||||
'embedx_sparse_beta2_decay_rate',
|
||||
]
|
||||
|
||||
for key in strategy:
|
||||
if key not in support_sparse_key_list:
|
||||
raise ValueError(f"strategy key '{key}' not support")
|
||||
|
||||
support_table_class = ['DownpourSparseTable', 'DownpourSparseSSDTable']
|
||||
if strategy.get('sparse_table_class') is not None:
|
||||
table_class = strategy.get('sparse_table_class')
|
||||
if table_class not in support_table_class:
|
||||
raise ValueError(
|
||||
f"support sparse_table_class: [ 'DownpourSparseTable', 'DownpourSparseSSDTable'], \
|
||||
but actual {table_class}"
|
||||
)
|
||||
else:
|
||||
table_class = 'DownpourSparseTable'
|
||||
|
||||
table.table_class = table_class
|
||||
|
||||
if (
|
||||
table_class == 'DownpourSparseTable'
|
||||
or table_class == 'DownpourSparseSSDTable'
|
||||
):
|
||||
table.enable_sparse_table_cache = strategy.get(
|
||||
'sparse_enable_cache', True
|
||||
)
|
||||
table.sparse_table_cache_rate = strategy.get(
|
||||
'sparse_cache_rate', 0.00055
|
||||
)
|
||||
table.sparse_table_cache_file_num = strategy.get(
|
||||
'sparse_cache_file_num', 16
|
||||
)
|
||||
table.compress_in_save = strategy.get(
|
||||
'sparse_compress_in_save', True
|
||||
)
|
||||
table.shard_num = strategy.get('sparse_shard_num', 1000)
|
||||
# DownpourFeatureValueAccessor: for ctr task, has cvm, embedding and sgd info
|
||||
# DownpourCtrAccessor : for ctr task, has cvm, slot, embedding and sgd info
|
||||
# DownpourSparseValueAccessor : for general task, has embedding and sgd info
|
||||
# DownpourCtrDoubleAccessor : for ctr task, which show clk are in double
|
||||
# DownpourUnitAccessor : for ctr task, has cvm, slot, embedding and sgd info
|
||||
|
||||
support_accessor_class = [
|
||||
'DownpourFeatureValueAccessor',
|
||||
'DownpourCtrAccessor',
|
||||
'DownpourCtrDymfAccessor',
|
||||
'DownpourSparseValueAccessor',
|
||||
'DownpourCtrDoubleAccessor',
|
||||
'DownpourUnitAccessor',
|
||||
'DownpourDoubleUnitAccessor',
|
||||
]
|
||||
if strategy.get('sparse_accessor_class') is not None:
|
||||
accessor_class = strategy.get('sparse_accessor_class')
|
||||
if accessor_class not in support_accessor_class:
|
||||
raise ValueError(
|
||||
f"support sparse_accessor_class: ['DownpourFeatureValueAccessor', 'DownpourCtrAccessor', 'DownpourCtrDymfAccessor', \
|
||||
'DownpourSparseValueAccessor', 'DownpourCtrDoubleAccessor'], \
|
||||
but actual {accessor_class}"
|
||||
)
|
||||
else:
|
||||
accessor_class = 'DownpourCtrAccessor'
|
||||
|
||||
table.accessor.accessor_class = accessor_class
|
||||
|
||||
if (
|
||||
accessor_class == 'DownpourFeatureValueAccessor'
|
||||
or accessor_class == 'DownpourCtrAccessor'
|
||||
or accessor_class == 'DownpourCtrDoubleAccessor'
|
||||
):
|
||||
table.accessor.sparse_sgd_param.learning_rate = strategy.get(
|
||||
'sparse_learning_rate', 0.05
|
||||
)
|
||||
table.accessor.sparse_sgd_param.initial_g2sum = strategy.get(
|
||||
'sparse_initial_g2sum', 3
|
||||
)
|
||||
table.accessor.sparse_sgd_param.initial_range = strategy.get(
|
||||
'sparse_initial_range', 1e-4
|
||||
)
|
||||
if strategy.get('sparse_weight_bounds') is None:
|
||||
table.accessor.sparse_sgd_param.weight_bounds.extend(
|
||||
[-10, 10]
|
||||
)
|
||||
else:
|
||||
table.accessor.sparse_sgd_param.weight_bounds.extend(
|
||||
strategy.get('sparse_weight_bounds')
|
||||
)
|
||||
table.accessor.embedx_dim = strategy.get('sparse_embedx_dim', 8)
|
||||
table.accessor.embedx_threshold = strategy.get(
|
||||
'sparse_embedx_threshold', 10
|
||||
)
|
||||
table.accessor.fea_dim = int(table.accessor.embedx_dim) + 3
|
||||
table.accessor.downpour_accessor_param.nonclk_coeff = (
|
||||
strategy.get('sparse_nonclk_coeff', 0.1)
|
||||
)
|
||||
table.accessor.downpour_accessor_param.click_coeff = (
|
||||
strategy.get('sparse_click_coeff', 1)
|
||||
)
|
||||
table.accessor.downpour_accessor_param.base_threshold = (
|
||||
strategy.get('sparse_base_threshold', 1.5)
|
||||
)
|
||||
table.accessor.downpour_accessor_param.delta_threshold = (
|
||||
strategy.get('sparse_delta_threshold', 0.25)
|
||||
)
|
||||
table.accessor.downpour_accessor_param.delta_keep_days = (
|
||||
strategy.get('sparse_delta_keep_days', 16)
|
||||
)
|
||||
table.accessor.downpour_accessor_param.delete_after_unseen_days = strategy.get(
|
||||
'sparse_delete_after_unseen_days', 30
|
||||
)
|
||||
table.accessor.downpour_accessor_param.ssd_unseenday_threshold = strategy.get(
|
||||
'sparse_ssd_unseenday_threshold', 1
|
||||
)
|
||||
table.accessor.downpour_accessor_param.show_click_decay_rate = (
|
||||
strategy.get('sparse_show_click_decay_rate', 0.98)
|
||||
)
|
||||
table.accessor.downpour_accessor_param.delete_threshold = (
|
||||
strategy.get('sparse_delete_threshold', 0.8)
|
||||
)
|
||||
converter = strategy.get(
|
||||
'sparse_converter',
|
||||
"(scripts/xbox_compressor_mf.py | bin/xbox_pb_converter)",
|
||||
)
|
||||
deconverter = strategy.get(
|
||||
'sparse_deconverter',
|
||||
"(bin/xbox_pb_deconverter | scripts/xbox_decompressor_mf.awk)",
|
||||
)
|
||||
|
||||
table1 = table.accessor.table_accessor_save_param.add()
|
||||
table1.param = 1
|
||||
table1.converter = converter
|
||||
table1.deconverter = deconverter
|
||||
|
||||
table2 = table.accessor.table_accessor_save_param.add()
|
||||
table2.param = 2
|
||||
table2.converter = converter
|
||||
table2.deconverter = deconverter
|
||||
elif accessor_class == 'DownpourSparseValueAccessor':
|
||||
optimizer_name = strategy.get("sparse_optimizer", "adam")
|
||||
table.accessor.sparse_commonsgd_param.name = optimizer_name
|
||||
table.accessor.embedx_dim = strategy.get('sparse_embedx_dim', 8)
|
||||
table.accessor.fea_dim = int(table.accessor.embedx_dim)
|
||||
if optimizer_name == "naive":
|
||||
table.accessor.sparse_commonsgd_param.naive.learning_rate = strategy.get(
|
||||
'sparse_learning_rate', 0.05
|
||||
)
|
||||
table.accessor.sparse_commonsgd_param.naive.initial_range = strategy.get(
|
||||
'sparse_initial_range', 1e-4
|
||||
)
|
||||
if strategy.get('sparse_weight_bounds') is None:
|
||||
table.accessor.sparse_commonsgd_param.naive.weight_bounds.extend(
|
||||
[-10, 10]
|
||||
)
|
||||
else:
|
||||
table.accessor.sparse_commonsgd_param.naive.weight_bounds.extend(
|
||||
strategy.get('sparse_weight_bounds')
|
||||
)
|
||||
elif optimizer_name == "adagrad":
|
||||
table.accessor.sparse_commonsgd_param.adagrad.learning_rate = strategy.get(
|
||||
'sparse_learning_rate', 0.05
|
||||
)
|
||||
table.accessor.sparse_commonsgd_param.adagrad.initial_range = strategy.get(
|
||||
'sparse_initial_range', 1e-4
|
||||
)
|
||||
table.accessor.sparse_commonsgd_param.adagrad.initial_g2sum = strategy.get(
|
||||
'sparse_initial_g2sum', 3
|
||||
)
|
||||
if strategy.get('sparse_weight_bounds') is None:
|
||||
table.accessor.sparse_commonsgd_param.adagrad.weight_bounds.extend(
|
||||
[-10, 10]
|
||||
)
|
||||
else:
|
||||
table.accessor.sparse_commonsgd_param.adagrad.weight_bounds.extend(
|
||||
strategy.get('sparse_weight_bounds')
|
||||
)
|
||||
elif optimizer_name == "adam":
|
||||
table.accessor.sparse_commonsgd_param.adam.learning_rate = (
|
||||
strategy.get('sparse_learning_rate', 0.001)
|
||||
)
|
||||
table.accessor.sparse_commonsgd_param.adam.initial_range = (
|
||||
strategy.get('sparse_initial_range', 1e-4)
|
||||
)
|
||||
table.accessor.sparse_commonsgd_param.adam.beta1_decay_rate = strategy.get(
|
||||
'sparse_beta1_decay_rate', 0.9
|
||||
)
|
||||
table.accessor.sparse_commonsgd_param.adam.beta2_decay_rate = strategy.get(
|
||||
'sparse_beta2_decay_rate', 0.999
|
||||
)
|
||||
table.accessor.sparse_commonsgd_param.adam.ada_epsilon = (
|
||||
strategy.get('sparse_ada_epsilon', 1e-8)
|
||||
)
|
||||
if strategy.get('sparse_weight_bounds') is None:
|
||||
table.accessor.sparse_commonsgd_param.adam.weight_bounds.extend(
|
||||
[-10, 10]
|
||||
)
|
||||
else:
|
||||
table.accessor.sparse_commonsgd_param.adam.weight_bounds.extend(
|
||||
strategy.get('sparse_weight_bounds')
|
||||
)
|
||||
converter = strategy.get(
|
||||
'sparse_converter',
|
||||
"(scripts/xbox_compressor_mf.py | bin/xbox_pb_converter)",
|
||||
)
|
||||
deconverter = strategy.get(
|
||||
'sparse_deconverter',
|
||||
"(bin/xbox_pb_deconverter | scripts/xbox_decompressor_mf.awk)",
|
||||
)
|
||||
|
||||
table1 = table.accessor.table_accessor_save_param.add()
|
||||
table1.param = 1
|
||||
table1.converter = converter
|
||||
table1.deconverter = deconverter
|
||||
|
||||
table2 = table.accessor.table_accessor_save_param.add()
|
||||
table2.param = 2
|
||||
table2.converter = converter
|
||||
table2.deconverter = deconverter
|
||||
elif (
|
||||
accessor_class == 'DownpourUnitAccessor'
|
||||
or accessor_class == 'DownpourDoubleUnitAccessor'
|
||||
or accessor_class == 'DownpourCtrDymfAccessor'
|
||||
):
|
||||
self.add_sparse_table_common_config(table, strategy)
|
||||
self.add_sparse_optimizer(
|
||||
table.accessor.embed_sgd_param, strategy, "embed_"
|
||||
)
|
||||
self.add_sparse_optimizer(
|
||||
table.accessor.embedx_sgd_param, strategy, "embedx_"
|
||||
)
|
||||
|
||||
def add_dense_table(
|
||||
self, table_id, param_var, grad_var, strategy, sparse_table_names
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
table_id(int): id of sparse params table
|
||||
param_var(list): param vars
|
||||
grad_var(list): param grad vars
|
||||
strategy(dict): the dense config dict
|
||||
sparse_table_names(list): sparse table names
|
||||
Returns:
|
||||
return None
|
||||
"""
|
||||
fea_dim = 0
|
||||
dense_param_vars = []
|
||||
for p in param_var:
|
||||
if p.name not in sparse_table_names:
|
||||
dense_param_vars.append(p)
|
||||
|
||||
for param in dense_param_vars:
|
||||
fea_dim += reduce(lambda x, y: x * y, param.shape, 1)
|
||||
|
||||
for table in self._server.downpour_server_param.downpour_table_param:
|
||||
if table.table_id == table_id:
|
||||
if table.type == pslib.PS_DENSE_TABLE:
|
||||
table.accessor.fea_dim = fea_dim
|
||||
return
|
||||
else:
|
||||
raise ValueError(
|
||||
f"expect table {table_id} type={pslib.PS_DENSE_TABLE}, but actual type={table.type}"
|
||||
)
|
||||
|
||||
if strategy is None:
|
||||
strategy = {}
|
||||
table = self._server.downpour_server_param.downpour_table_param.add()
|
||||
table.table_id = table_id
|
||||
support_dense_key_list = [
|
||||
'dense_table_class',
|
||||
'dense_compress_in_save',
|
||||
'dense_accessor_class',
|
||||
'dense_optimizer',
|
||||
'dense_learning_rate',
|
||||
'dense_avg_decay',
|
||||
'dense_ada_decay',
|
||||
'dense_ada_epsilon',
|
||||
'dense_mom_decay',
|
||||
'dense_naive_lr',
|
||||
]
|
||||
|
||||
for key in strategy:
|
||||
if key not in support_dense_key_list:
|
||||
raise ValueError(f"strategy key '{key}' not support")
|
||||
|
||||
table.table_class = strategy.get(
|
||||
'dense_table_class', "DownpourDenseTable"
|
||||
)
|
||||
table.type = pslib.PS_DENSE_TABLE
|
||||
table.compress_in_save = strategy.get('dense_compress_in_save', True)
|
||||
table.accessor.accessor_class = strategy.get(
|
||||
'dense_accessor_class', "DownpourDenseValueAccessor"
|
||||
)
|
||||
table.accessor.dense_sgd_param.name = strategy.get(
|
||||
'dense_optimizer', "adam"
|
||||
)
|
||||
table.accessor.dense_sgd_param.adam.learning_rate = strategy.get(
|
||||
'dense_learning_rate', 5e-06
|
||||
)
|
||||
table.accessor.dense_sgd_param.adam.avg_decay_rate = strategy.get(
|
||||
'dense_avg_decay', 0.999993
|
||||
)
|
||||
table.accessor.dense_sgd_param.adam.ada_decay_rate = strategy.get(
|
||||
'dense_ada_decay', 0.9999
|
||||
)
|
||||
table.accessor.dense_sgd_param.adam.ada_epsilon = strategy.get(
|
||||
'dense_ada_epsilon', 1e-8
|
||||
)
|
||||
table.accessor.dense_sgd_param.adam.mom_decay_rate = strategy.get(
|
||||
'dense_mom_decay', 0.99
|
||||
)
|
||||
table.accessor.dense_sgd_param.naive.learning_rate = strategy.get(
|
||||
'dense_naive_lr', 0.0002
|
||||
)
|
||||
table.accessor.fea_dim = fea_dim
|
||||
|
||||
def add_data_norm_table(
|
||||
self,
|
||||
table_id,
|
||||
learning_rate,
|
||||
param_var,
|
||||
grad_var,
|
||||
strategy,
|
||||
sparse_table_names,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
table_id(int): id of datanorm table
|
||||
learning_rate(float): the learning rate used to update parameters
|
||||
param_var(list): param vars
|
||||
grad_var(list): param grad vars
|
||||
strategy(dict): the datanorm config dict
|
||||
sparse_table_names(list): sparse table names
|
||||
Returns:
|
||||
return None
|
||||
"""
|
||||
fea_dim = 0
|
||||
dense_param_vars = []
|
||||
for p in param_var:
|
||||
if p.name not in sparse_table_names:
|
||||
dense_param_vars.append(p)
|
||||
|
||||
for param in dense_param_vars:
|
||||
fea_dim += reduce(lambda x, y: x * y, param.shape, 1)
|
||||
|
||||
for table in self._server.downpour_server_param.downpour_table_param:
|
||||
if table.table_id == table_id:
|
||||
if table.type == pslib.PS_DENSE_TABLE:
|
||||
table.accessor.fea_dim = fea_dim
|
||||
return
|
||||
else:
|
||||
raise ValueError(
|
||||
f"expect table {table_id} type={pslib.PS_DENSE_TABLE}, but actual type={table.type}"
|
||||
)
|
||||
if strategy is None:
|
||||
strategy = {}
|
||||
|
||||
support_datanorm_key_list = [
|
||||
'datanorm_table_class',
|
||||
'datanorm_compress_in_save',
|
||||
'datanorm_accessor_class',
|
||||
'datanorm_operation',
|
||||
'datanorm_decay_rate',
|
||||
]
|
||||
|
||||
for key in strategy:
|
||||
if key not in support_datanorm_key_list:
|
||||
raise ValueError(f"strategy key '{key}' not support")
|
||||
|
||||
table = self._server.downpour_server_param.downpour_table_param.add()
|
||||
table.table_id = table_id
|
||||
table.table_class = strategy.get(
|
||||
'datanorm_table_class', 'DownpourDenseTable'
|
||||
)
|
||||
table.type = pslib.PS_DENSE_TABLE
|
||||
table.compress_in_save = strategy.get('datanorm_compress_in_save', True)
|
||||
table.accessor.accessor_class = strategy.get(
|
||||
'datanorm_accessor_class', 'DownpourDenseValueAccessor'
|
||||
)
|
||||
table.accessor.dense_sgd_param.name = strategy.get(
|
||||
'datanorm_operation', 'summary'
|
||||
)
|
||||
table.accessor.dense_sgd_param.summary.summary_decay_rate = (
|
||||
strategy.get('datanorm_decay_rate', 0.999999)
|
||||
)
|
||||
table.accessor.fea_dim = fea_dim
|
||||
|
||||
def add_sparse_optimizer(self, sgd, strategy, prefix):
|
||||
optimizer_name = strategy.get(prefix + "sparse_optimizer", "adagrad")
|
||||
sgd.name = optimizer_name
|
||||
if optimizer_name == "naive":
|
||||
sgd.naive.learning_rate = strategy.get(
|
||||
prefix + 'sparse_learning_rate', 0.05
|
||||
)
|
||||
sgd.naive.initial_range = strategy.get(
|
||||
prefix + 'sparse_initial_range', 1e-4
|
||||
)
|
||||
bounds = strategy.get(prefix + 'sparse_weight_bounds', [-10, 10])
|
||||
sgd.naive.weight_bounds.extend(bounds)
|
||||
elif optimizer_name == "adagrad":
|
||||
sgd.adagrad.learning_rate = strategy.get(
|
||||
prefix + 'sparse_learning_rate', 0.05
|
||||
)
|
||||
sgd.adagrad.initial_range = strategy.get(
|
||||
prefix + 'sparse_initial_range', 1e-4
|
||||
)
|
||||
if prefix == "embed_":
|
||||
sgd.adagrad.initial_range = 0
|
||||
sgd.adagrad.initial_g2sum = strategy.get(
|
||||
prefix + 'sparse_initial_g2sum', 3
|
||||
)
|
||||
bounds = strategy.get(prefix + 'sparse_weight_bounds', [-10, 10])
|
||||
sgd.adagrad.weight_bounds.extend(bounds)
|
||||
elif optimizer_name == "std_adagrad":
|
||||
sgd.adagrad.learning_rate = strategy.get(
|
||||
prefix + 'sparse_learning_rate', 0.05
|
||||
)
|
||||
sgd.adagrad.initial_range = strategy.get(
|
||||
prefix + 'sparse_initial_range', 1e-4
|
||||
)
|
||||
if prefix == "embed_":
|
||||
sgd.adagrad.initial_range = 0
|
||||
sgd.adagrad.initial_g2sum = strategy.get(
|
||||
prefix + 'sparse_initial_g2sum', 3
|
||||
)
|
||||
bounds = strategy.get(prefix + 'sparse_weight_bounds', [-10, 10])
|
||||
sgd.adagrad.weight_bounds.extend(bounds)
|
||||
elif optimizer_name == "adam":
|
||||
sgd.adam.learning_rate = strategy.get(
|
||||
prefix + 'sparse_learning_rate', 0.001
|
||||
)
|
||||
sgd.adam.initial_range = strategy.get(
|
||||
prefix + 'sparse_initial_range', 1e-4
|
||||
)
|
||||
sgd.adam.beta1_decay_rate = strategy.get(
|
||||
prefix + 'sparse_beta1_decay_rate', 0.9
|
||||
)
|
||||
sgd.adam.beta2_decay_rate = strategy.get(
|
||||
prefix + 'sparse_beta2_decay_rate', 0.999
|
||||
)
|
||||
sgd.adam.ada_epsilon = strategy.get(
|
||||
prefix + 'sparse_ada_epsilon', 1e-8
|
||||
)
|
||||
bounds = strategy.get(prefix + 'sparse_weight_bounds', [-10, 10])
|
||||
sgd.adam.weight_bounds.extend(bounds)
|
||||
|
||||
def add_sparse_table_common_config(self, table, strategy):
|
||||
table.accessor.embedx_dim = strategy.get('sparse_embedx_dim', 8)
|
||||
table.accessor.embedx_threshold = strategy.get(
|
||||
'sparse_embedx_threshold', 10
|
||||
)
|
||||
table.accessor.fea_dim = int(table.accessor.embedx_dim) + 3
|
||||
table.accessor.downpour_accessor_param.nonclk_coeff = strategy.get(
|
||||
'sparse_nonclk_coeff', 0.1
|
||||
)
|
||||
table.accessor.downpour_accessor_param.click_coeff = strategy.get(
|
||||
'sparse_click_coeff', 1
|
||||
)
|
||||
table.accessor.downpour_accessor_param.base_threshold = strategy.get(
|
||||
'sparse_base_threshold', 1.5
|
||||
)
|
||||
table.accessor.downpour_accessor_param.delta_threshold = strategy.get(
|
||||
'sparse_delta_threshold', 0.25
|
||||
)
|
||||
table.accessor.downpour_accessor_param.delta_keep_days = strategy.get(
|
||||
'sparse_delta_keep_days', 16
|
||||
)
|
||||
table.accessor.downpour_accessor_param.delete_after_unseen_days = (
|
||||
strategy.get('sparse_delete_after_unseen_days', 30)
|
||||
)
|
||||
table.accessor.downpour_accessor_param.show_click_decay_rate = (
|
||||
strategy.get('sparse_show_click_decay_rate', 0.98)
|
||||
)
|
||||
table.accessor.downpour_accessor_param.delete_threshold = strategy.get(
|
||||
'sparse_delete_threshold', 0.8
|
||||
)
|
||||
converter = strategy.get(
|
||||
'sparse_converter',
|
||||
"(scripts/xbox_compressor_mf.py | bin/xbox_pb_converter)",
|
||||
)
|
||||
deconverter = strategy.get(
|
||||
'sparse_deconverter',
|
||||
"(bin/xbox_pb_deconverter | scripts/xbox_decompressor_mf.awk)",
|
||||
)
|
||||
|
||||
table1 = table.accessor.table_accessor_save_param.add()
|
||||
table1.param = 1
|
||||
table1.converter = converter
|
||||
table1.deconverter = deconverter
|
||||
|
||||
table2 = table.accessor.table_accessor_save_param.add()
|
||||
table2.param = 2
|
||||
table2.converter = converter
|
||||
table2.deconverter = deconverter
|
||||
|
||||
def get_desc(self):
|
||||
"""
|
||||
Return downpour server program_desc
|
||||
"""
|
||||
return self._server
|
||||
|
||||
|
||||
class DownpourWorker(Worker):
|
||||
"""
|
||||
DownpourWorker class is used to generate worker program_desc
|
||||
Args:
|
||||
window (int): push params frequency
|
||||
worker: it is pslib.DownpourTrainerParameter
|
||||
Examples:
|
||||
worker = DownpourWorker(1)
|
||||
"""
|
||||
|
||||
def __init__(self, window):
|
||||
self.window = window
|
||||
self._worker = pslib.DownpourTrainerParameter()
|
||||
|
||||
def add_sparse_table(
|
||||
self, table_id, slot_key_vars, slot_value_vars, slot_value_grads=None
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
table_id(int): id of sparse params table
|
||||
slot_key_vars(list): slot key id
|
||||
slot_value_vars(list): slot key value after embedding
|
||||
slot_value_grads(list): grad of all params, default is None
|
||||
Returns:
|
||||
return None
|
||||
"""
|
||||
if slot_value_grads is None:
|
||||
slot_value_grad_names = [
|
||||
var.name + "@GRAD" for var in slot_value_vars
|
||||
]
|
||||
else:
|
||||
value_to_key = {}
|
||||
for i in range(len(slot_key_vars)):
|
||||
value_to_key[slot_value_vars[i].name] = slot_key_vars[i]
|
||||
slot_value_grad_names = []
|
||||
all_grad_names = [var.name for var in slot_value_grads]
|
||||
for var in slot_value_vars:
|
||||
if var.name + "@GRAD" in all_grad_names:
|
||||
slot_value_grad_names.append(var.name + "@GRAD")
|
||||
sorted_slot_value_vars = [
|
||||
i
|
||||
for i in slot_value_vars
|
||||
if i.name + "@GRAD" in slot_value_grad_names
|
||||
]
|
||||
sorted_slot_value_vars += [
|
||||
i
|
||||
for i in slot_value_vars
|
||||
if i.name + "@GRAD" not in slot_value_grad_names
|
||||
]
|
||||
sorted_slot_key_vars = [
|
||||
value_to_key[v.name] for v in sorted_slot_value_vars
|
||||
]
|
||||
|
||||
target_table = None
|
||||
for table in self._worker.sparse_table:
|
||||
if table.table_id == table_id:
|
||||
keys = table.slot_key
|
||||
key_names = [var.name for var in sorted_slot_key_vars]
|
||||
for key_name in key_names:
|
||||
if key_name not in keys:
|
||||
raise ValueError(
|
||||
f"sparse table {table_id} slot_key error"
|
||||
)
|
||||
target_table = table
|
||||
break
|
||||
|
||||
table = target_table
|
||||
if table is not None:
|
||||
self._worker.sparse_table.remove(table)
|
||||
table = self._worker.sparse_table.add()
|
||||
table.table_id = table_id
|
||||
table.slot_key.extend([var.name for var in sorted_slot_key_vars])
|
||||
table.slot_value.extend([var.name for var in sorted_slot_value_vars])
|
||||
table.slot_gradient.extend(slot_value_grad_names)
|
||||
|
||||
def add_dense_table(
|
||||
self,
|
||||
table_id,
|
||||
learning_rate,
|
||||
param_vars,
|
||||
grad_vars,
|
||||
dense_start_table_id,
|
||||
sparse_table_names,
|
||||
):
|
||||
r"""
|
||||
Args:
|
||||
table_id(int): id of sparse params table
|
||||
learning_rate(float): the learning rate used to update parameters. \
|
||||
Can be a float value
|
||||
param_vars(list): all dense param. it is a list.
|
||||
grad_vars(list): all dense grad param it is a list.
|
||||
dense_start_table_id(int): dense table start index
|
||||
sparse_table_names(list): sparse table names
|
||||
Returns:
|
||||
return None
|
||||
"""
|
||||
sparse_table_name_grad = []
|
||||
for name in sparse_table_names:
|
||||
sparse_table_name_grad.append(name + "@GRAD")
|
||||
|
||||
dense_param_name = []
|
||||
for p in param_vars:
|
||||
if p.name not in sparse_table_names:
|
||||
dense_param_name.append(p.name)
|
||||
|
||||
dense_grad_name = []
|
||||
for g in grad_vars:
|
||||
if g.name not in sparse_table_name_grad:
|
||||
dense_grad_name.append(g.name)
|
||||
|
||||
dense_param_name.sort()
|
||||
dense_grad_name.sort()
|
||||
|
||||
for table in self._worker.dense_table:
|
||||
if table.table_id == table_id:
|
||||
desc_dense_param_name = list(table.dense_variable_name)
|
||||
desc_dense_param_name.sort()
|
||||
|
||||
if dense_param_name == desc_dense_param_name:
|
||||
desc_dense_grad_name = list(
|
||||
table.dense_gradient_variable_name
|
||||
)
|
||||
desc_dense_grad_name.sort()
|
||||
if dense_grad_name == desc_dense_grad_name:
|
||||
return
|
||||
else:
|
||||
raise ValueError(
|
||||
f"dense table {table_id} dense_gradient_variable_name "
|
||||
"error"
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"dense table {table_id} dense_variable_name error"
|
||||
)
|
||||
|
||||
table = self._worker.dense_table.add()
|
||||
table.table_id = table_id
|
||||
|
||||
# def cmp_fc(x, y):
|
||||
# if x.startswith("fc_") and y.startswith("fc_"):
|
||||
# index_x = x.find('.')
|
||||
# index_y = y.find('.')
|
||||
# if index_x > 0 and index_y > 0:
|
||||
# num_x = x[3:index_x]
|
||||
# num_y = y[3:index_y]
|
||||
# if num_x.isdigit() and num_y.isdigit():
|
||||
# if int(num_x) < int(num_y):
|
||||
# return -1
|
||||
# if int(num_x) > int(num_y):
|
||||
# return 1
|
||||
# if x[index_x + 1] == 'w' and y[index_y + 1] == 'b':
|
||||
# return -1
|
||||
# if x[index_x + 1] == 'b' and y[index_y + 1] == 'w':
|
||||
# return 1
|
||||
# if x < y:
|
||||
# return -1
|
||||
# else:
|
||||
# return 1
|
||||
|
||||
# table.dense_variable_name.extend(sorted(dense_param_name, cmp_fc))
|
||||
# table.dense_gradient_variable_name.extend(
|
||||
# sorted(dense_grad_name, cmp_fc))
|
||||
table.dense_variable_name.extend(dense_param_name)
|
||||
table.dense_gradient_variable_name.extend(dense_grad_name)
|
||||
|
||||
def get_desc(self):
|
||||
"""
|
||||
Return downpour worker program_desc
|
||||
"""
|
||||
return self._worker
|
||||
+1058
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,511 @@
|
||||
# Copyright (c) 2020 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 logging
|
||||
import os
|
||||
import subprocess
|
||||
from collections import OrderedDict
|
||||
|
||||
import numpy as np
|
||||
from google.protobuf import text_format
|
||||
|
||||
import paddle
|
||||
from paddle import base
|
||||
from paddle.base import core
|
||||
from paddle.base.framework import Program
|
||||
from paddle.base.proto import framework_pb2
|
||||
from paddle.distributed.fleet.base.util_factory import draw_block_graphviz
|
||||
from paddle.framework import io_utils
|
||||
|
||||
__all__ = [
|
||||
"load_program",
|
||||
"save_program",
|
||||
"program_type_trans",
|
||||
"check_saved_vars_try_dump",
|
||||
"parse_program",
|
||||
"check_pruned_program_vars",
|
||||
"graphviz",
|
||||
]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.INFO)
|
||||
formatter = logging.Formatter(fmt='%(asctime)s - %(levelname)s - %(message)s')
|
||||
ch = logging.StreamHandler()
|
||||
ch.setFormatter(formatter)
|
||||
logger.addHandler(ch)
|
||||
|
||||
persistable_vars_out_fn = "vars_persistable.log"
|
||||
all_vars_out_fn = "vars_all.log"
|
||||
ops_out_fn = "ops.log"
|
||||
|
||||
feed_fetch_type_list = [
|
||||
core.VarDesc.VarType.FEED_MINIBATCH,
|
||||
core.VarDesc.VarType.FETCH_LIST,
|
||||
]
|
||||
not_expected_op_types = ["lookup_table"]
|
||||
|
||||
|
||||
def load_program(model_filename, is_text=False):
|
||||
if is_text:
|
||||
return load_program_text(model_filename)
|
||||
return load_program_binary(model_filename)
|
||||
|
||||
|
||||
def load_program_binary(model_filename):
|
||||
"""load program from binary string file"""
|
||||
with open(model_filename, "rb") as f:
|
||||
program_desc_str = f.read()
|
||||
return Program.parse_from_string(program_desc_str)
|
||||
|
||||
|
||||
def load_program_text(model_filename):
|
||||
"""load program from human-readable text file"""
|
||||
with open(model_filename, "r") as f:
|
||||
program_desc_text = f.read()
|
||||
|
||||
prog_desc = framework_pb2.ProgramDesc()
|
||||
text_format.Merge(program_desc_text, prog_desc)
|
||||
return Program.parse_from_string(prog_desc.SerializeToString())
|
||||
|
||||
|
||||
def save_program(program, model_filename='__model__', is_text=False):
|
||||
if is_text:
|
||||
with open(model_filename, "w") as f:
|
||||
f.write(str(program))
|
||||
else:
|
||||
with open(model_filename, "wb") as f:
|
||||
f.write(program.desc.serialize_to_string())
|
||||
|
||||
|
||||
def check_pruned_program_vars(train_prog, pruned_prog):
|
||||
is_match = True
|
||||
|
||||
pruned_vars = [
|
||||
(v.name, v)
|
||||
for v in pruned_prog.list_vars()
|
||||
if io_utils.is_persistable(v)
|
||||
]
|
||||
pruned_vars = OrderedDict(pruned_vars)
|
||||
pruned_vars_name = list(pruned_vars)
|
||||
logger.info(f"persistable vars in pruned program: {pruned_vars_name}")
|
||||
|
||||
for var_name in pruned_vars:
|
||||
var = pruned_vars[var_name]
|
||||
# feed and fetch op is added in pruned program when pruning, not need to be found in train program
|
||||
if var.type in feed_fetch_type_list:
|
||||
break
|
||||
try:
|
||||
train_prog_var = train_prog.global_block().var(var_name)
|
||||
except ValueError as e:
|
||||
logger.error(
|
||||
f"not find variable '{var_name}' in train program. please check pruning."
|
||||
)
|
||||
logger.error(e)
|
||||
continue
|
||||
if (
|
||||
var.shape != train_prog_var.shape
|
||||
or var.dtype != train_prog_var.dtype
|
||||
):
|
||||
logger.error(
|
||||
f"variable: {var_name} not match. in pruned program shape: {var.shape} dtype:{var.dtype}, in train program shape: {train_prog_var.shape} dtype: {train_prog_var.dtype}"
|
||||
)
|
||||
is_match = False
|
||||
return is_match
|
||||
|
||||
|
||||
def graphviz(block, output_dir="", filename='debug'):
|
||||
dot_path = os.path.join(output_dir, filename + '.dot')
|
||||
pdf_path = os.path.join(output_dir, filename + '.pdf')
|
||||
draw_block_graphviz(block, path=dot_path)
|
||||
cmd = ["dot", "-Tpdf", dot_path, "-o", pdf_path]
|
||||
p = subprocess.Popen(
|
||||
cmd,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
p.wait()
|
||||
|
||||
|
||||
def program_type_trans(prog_dir, prog_fn, is_text):
|
||||
prog = load_program(os.path.join(prog_dir, prog_fn), is_text)
|
||||
prog_out_fn = prog_fn + ".bin" if is_text else prog_fn + ".pbtxt"
|
||||
save_program(prog, os.path.join(prog_dir, prog_out_fn), 1 - is_text)
|
||||
return prog_out_fn
|
||||
|
||||
|
||||
def append_save_op(block, var, path):
|
||||
block.append_op(
|
||||
type='save', inputs={'X': [var]}, outputs={}, attrs={'file_path': path}
|
||||
)
|
||||
|
||||
|
||||
def append_load_op(block, var, path):
|
||||
block.append_op(
|
||||
type='load',
|
||||
inputs={},
|
||||
outputs={'Out': [var]},
|
||||
attrs={'file_path': path},
|
||||
)
|
||||
|
||||
|
||||
def save_var(np_array, var_name, shape_list, dtype, save_path):
|
||||
program = base.Program()
|
||||
place = base.CPUPlace()
|
||||
exe = base.Executor(place)
|
||||
shape = list(shape_list)
|
||||
with base.program_guard(program):
|
||||
d0_data = paddle.static.data(var_name, shape=shape, dtype=dtype)
|
||||
append_save_op(program.global_block(), d0_data, save_path)
|
||||
exe.run(feed={var_name: np_array}, fetch_list=[])
|
||||
|
||||
|
||||
def load_var(var_name, shape_list, dtype, save_path):
|
||||
program = base.Program()
|
||||
place = base.CPUPlace()
|
||||
exe = base.Executor(place)
|
||||
with base.program_guard(program):
|
||||
d0_data = paddle.static.data(var_name, shape=shape_list, dtype=dtype)
|
||||
append_load_op(program.global_block(), d0_data, save_path)
|
||||
outs = exe.run(feed={}, fetch_list=[d0_data])
|
||||
return outs
|
||||
|
||||
|
||||
def reader(batch_size, fn, dim):
|
||||
data = []
|
||||
if isinstance(dim, (list, tuple)):
|
||||
shape = list(dim)
|
||||
_temp = 1
|
||||
for x in dim:
|
||||
_temp = _temp * x
|
||||
dim = _temp
|
||||
else:
|
||||
shape = [dim]
|
||||
|
||||
shape = [batch_size, *shape]
|
||||
dim = dim * batch_size
|
||||
|
||||
for line in open(fn, 'r'):
|
||||
fields = line.strip().split(' ')
|
||||
fields = [float(d) for d in fields]
|
||||
while len(fields) >= dim:
|
||||
tmp = fields[:dim]
|
||||
fields = fields[dim:]
|
||||
data.append(np.array(tmp).reshape(shape))
|
||||
return data
|
||||
|
||||
|
||||
def feed_gen(batch_size, feeded_vars_dims, feeded_vars_filelist):
|
||||
batch_feed = []
|
||||
for i, fn in enumerate(feeded_vars_filelist):
|
||||
batch_feed.append(reader(batch_size, fn, feeded_vars_dims[i]))
|
||||
return batch_feed
|
||||
|
||||
|
||||
def try_load_model_vars(
|
||||
dump_dir,
|
||||
dump_prog_fn,
|
||||
is_text_dump_program,
|
||||
batch_size,
|
||||
feed_config,
|
||||
fetch_config,
|
||||
save_filename,
|
||||
saved_params,
|
||||
):
|
||||
place = base.CPUPlace()
|
||||
exe = base.Executor(place)
|
||||
scope = base.core.Scope()
|
||||
with base.scope_guard(scope):
|
||||
if is_text_dump_program:
|
||||
dump_prog_fn = program_type_trans(
|
||||
dump_dir, dump_prog_fn, is_text_dump_program
|
||||
)
|
||||
|
||||
[
|
||||
inference_program,
|
||||
feed_target_names,
|
||||
fetch_targets,
|
||||
] = paddle.static.io.load_inference_model(
|
||||
dump_dir,
|
||||
exe,
|
||||
model_filename=dump_prog_fn,
|
||||
params_filename=save_filename,
|
||||
)
|
||||
|
||||
# check program vars and saved vars shape
|
||||
orig_para_shape = {
|
||||
each_var.name: tuple(each_var.desc.shape())
|
||||
for each_var in saved_params
|
||||
}
|
||||
for each_var in saved_params:
|
||||
var_temp = base.global_scope().find_var(each_var.name)
|
||||
assert var_temp is not None, "can't not find var: " + each_var.name
|
||||
new_shape = (np.array(var_temp.get_tensor())).shape
|
||||
assert each_var.name in orig_para_shape, (
|
||||
each_var.name + "MUST in var list"
|
||||
)
|
||||
orig_shape = orig_para_shape.get(each_var.name)
|
||||
if new_shape != orig_shape:
|
||||
raise RuntimeError(
|
||||
f"Shape not matching: the Program requires a parameter with a shape of ({orig_shape}), "
|
||||
f"while the loaded parameter (namely [ {each_var.name} ]) has a shape of ({new_shape})."
|
||||
)
|
||||
|
||||
# check feed/fetch vars in program and config
|
||||
fetch_targets_names = [v.name for v in fetch_targets]
|
||||
if not feed_target_names:
|
||||
logger.warning("no feed targets in program.")
|
||||
if not fetch_targets_names:
|
||||
logger.warning("no fetch targets in program.")
|
||||
fetch_list = fetch_targets
|
||||
feed_name_list = feed_target_names
|
||||
if (
|
||||
feed_config.feeded_vars_names is not None
|
||||
and feed_target_names != feed_config.feeded_vars_names
|
||||
):
|
||||
logger.warning(
|
||||
f"feed vars in program and config are diff: feed in program: {feed_target_names}. feed in config {feed_config.feeded_vars_names}."
|
||||
)
|
||||
feed_name_list = feed_config.feeded_vars_names
|
||||
# remove feed op in inference_program. new feed op will be added in exe.run
|
||||
global_block = inference_program.global_block()
|
||||
need_to_remove_op_index = []
|
||||
for i, op in enumerate(global_block.ops):
|
||||
op.desc.set_is_target(False)
|
||||
if op.type == "feed": # only remove feed op here
|
||||
need_to_remove_op_index.append(i)
|
||||
for index in need_to_remove_op_index[::-1]:
|
||||
global_block._remove_op(index)
|
||||
if (
|
||||
fetch_config.fetch_vars_names is not None
|
||||
and fetch_targets_names != fetch_config.fetch_vars_names
|
||||
):
|
||||
logger.warning(
|
||||
f"fetch vars in program and config are diff: fetch in program: {fetch_targets_names}. fetch in config {fetch_config.fetch_vars_names}."
|
||||
)
|
||||
fetch_list = [
|
||||
inference_program.global_block().var(i)
|
||||
for i in fetch_config.fetch_vars_names
|
||||
]
|
||||
# remove fetch op in inference_program. new fetch op will be added in exe.run
|
||||
global_block = inference_program.global_block()
|
||||
need_to_remove_op_index = []
|
||||
for i, op in enumerate(global_block.ops):
|
||||
op.desc.set_is_target(False)
|
||||
if op.type == "fetch": # only remove fetch op here
|
||||
need_to_remove_op_index.append(i)
|
||||
for index in need_to_remove_op_index[::-1]:
|
||||
global_block._remove_op(index)
|
||||
|
||||
# if fetch_list have lod tensor
|
||||
return_numpy = all(v.lod_level == 0 for v in fetch_list)
|
||||
|
||||
# try dump fetch_targets
|
||||
feed_tensors = []
|
||||
assert (
|
||||
len(feed_config.feeded_vars_names)
|
||||
== len(feed_config.feeded_vars_dims)
|
||||
== len(feed_config.feeded_vars_types)
|
||||
)
|
||||
# check program vars and feed tensor shape in config
|
||||
for i in range(len(feed_config.feeded_vars_names)):
|
||||
var = inference_program.global_block().var(
|
||||
feed_config.feeded_vars_names[i]
|
||||
)
|
||||
if not isinstance(feed_config.feeded_vars_dims[i], (list, tuple)):
|
||||
tensor_shape = (feed_config.feeded_vars_dims[i],)
|
||||
else:
|
||||
tensor_shape = tuple(feed_config.feeded_vars_dims[i])
|
||||
feed_config.feeded_vars_dims[i] = tensor_shape
|
||||
var_shape = var.shape[1:]
|
||||
if tensor_shape != var_shape:
|
||||
raise RuntimeError(
|
||||
f"feed variable '{feed_config.feeded_vars_names[i]}' shape not match. infer program shape: {var_shape}. feed tensor shape: {tensor_shape}"
|
||||
)
|
||||
|
||||
if not feed_config.feeded_vars_filelist:
|
||||
logger.info("generate random feed vars.")
|
||||
for i in range(len(feed_config.feeded_vars_names)):
|
||||
var = inference_program.global_block().var(
|
||||
feed_config.feeded_vars_names[i]
|
||||
)
|
||||
# create fake feed tensor. if lod_level > 1, should create_lod_tensor()
|
||||
if var.lod_level == 0:
|
||||
feed_tensors.append(
|
||||
np.array(
|
||||
np.random.random(
|
||||
(
|
||||
batch_size,
|
||||
*list(feed_config.feeded_vars_dims[i]),
|
||||
)
|
||||
),
|
||||
dtype=feed_config.feeded_vars_types[i],
|
||||
)
|
||||
)
|
||||
elif var.lod_level == 1:
|
||||
t = np.array(
|
||||
np.random.random(
|
||||
(
|
||||
batch_size,
|
||||
*list(feed_config.feeded_vars_dims[i]),
|
||||
)
|
||||
),
|
||||
dtype=feed_config.feeded_vars_types[i],
|
||||
)
|
||||
feed_tensors.append(
|
||||
base.create_lod_tensor(t, [[1] * batch_size], place)
|
||||
)
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"vars with lod_level >= 2 is not supported now in this infer program check tool."
|
||||
)
|
||||
results = exe.run(
|
||||
inference_program,
|
||||
feed={
|
||||
name: feed_tensors[i]
|
||||
for i, name in enumerate(feed_name_list)
|
||||
},
|
||||
fetch_list=fetch_list,
|
||||
return_numpy=return_numpy,
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
f"load feed vars from files: {feed_config.feeded_vars_filelist}."
|
||||
)
|
||||
feed_vars = [
|
||||
inference_program.global_block().var(
|
||||
feed_config.feeded_vars_names[i]
|
||||
)
|
||||
for i in range(len(feed_config.feeded_vars_names))
|
||||
]
|
||||
feeder = base.DataFeeder(feed_list=feed_vars, place=place)
|
||||
batch_feed = feed_gen(
|
||||
batch_size,
|
||||
feed_config.feeded_vars_dims,
|
||||
feed_config.feeded_vars_filelist,
|
||||
)
|
||||
slots = [batch_feed]
|
||||
results = exe.run(
|
||||
inference_program,
|
||||
feed=feeder.feed(slots),
|
||||
fetch_list=fetch_list,
|
||||
return_numpy=return_numpy,
|
||||
)
|
||||
for i, v in enumerate(fetch_list):
|
||||
logger.info(f"fetch_targets name: {v.name}")
|
||||
logger.info(f"fetch_targets: {results[i]}")
|
||||
return results
|
||||
|
||||
|
||||
def check_not_expected_ops(prog):
|
||||
op_types_set = set()
|
||||
for op in prog.global_block().ops:
|
||||
if op.type in not_expected_op_types and op.type not in op_types_set:
|
||||
logger.warning(
|
||||
f"find op type '{op.type}' in program, please check if your program is pruned correctly !"
|
||||
)
|
||||
op_types_set.add(op.type)
|
||||
|
||||
|
||||
def check_saved_vars_try_dump(
|
||||
dump_dir,
|
||||
dump_prog_fn,
|
||||
is_text_dump_program,
|
||||
feed_config,
|
||||
fetch_config,
|
||||
batch_size=1,
|
||||
save_filename=None,
|
||||
):
|
||||
dump_prog = load_program(
|
||||
os.path.join(dump_dir, dump_prog_fn), is_text_dump_program
|
||||
)
|
||||
saved_params = [
|
||||
v for v in dump_prog.list_vars() if io_utils.is_persistable(v)
|
||||
]
|
||||
logger.info(
|
||||
f"persistable vars in dump program: {[v.name for v in saved_params]}"
|
||||
)
|
||||
|
||||
check_not_expected_ops(dump_prog)
|
||||
|
||||
return try_load_model_vars(
|
||||
dump_dir,
|
||||
dump_prog_fn,
|
||||
is_text_dump_program,
|
||||
batch_size,
|
||||
feed_config,
|
||||
fetch_config,
|
||||
save_filename,
|
||||
saved_params,
|
||||
)
|
||||
|
||||
|
||||
def parse_program(program, output_dir):
|
||||
# persistable vars
|
||||
output = {}
|
||||
persistable_vars = [
|
||||
v for v in program.list_vars() if io_utils.is_persistable(v)
|
||||
]
|
||||
output["persistable_vars"] = [
|
||||
{
|
||||
'name': str(v.name),
|
||||
'shape': str(v.shape),
|
||||
'lod_level': int(v.lod_level),
|
||||
'dtype': str(v.dtype),
|
||||
'type': str(v.type),
|
||||
}
|
||||
for v in persistable_vars
|
||||
]
|
||||
with open(os.path.join(output_dir, persistable_vars_out_fn), 'w') as f:
|
||||
f.write("persistable vars:\n")
|
||||
for var in output["persistable_vars"]:
|
||||
f.write(str(var))
|
||||
f.write("\n")
|
||||
|
||||
# all vars
|
||||
all_vars = list(program.list_vars())
|
||||
output["all_vars"] = [
|
||||
(
|
||||
{
|
||||
'name': str(v.name),
|
||||
'shape': str(v.shape),
|
||||
'lod_level': int(v.lod_level),
|
||||
'dtype': str(v.dtype),
|
||||
}
|
||||
if v.type not in feed_fetch_type_list
|
||||
else {'name': str(v.name), 'type': str(v.type)}
|
||||
)
|
||||
for v in all_vars
|
||||
]
|
||||
with open(os.path.join(output_dir, all_vars_out_fn), 'w') as f:
|
||||
f.write("all vars:\n")
|
||||
for var in output["all_vars"]:
|
||||
f.write(str(var))
|
||||
f.write("\n")
|
||||
|
||||
# ops
|
||||
ops = program.global_block().ops
|
||||
output["ops"] = [
|
||||
{
|
||||
'type': op.type,
|
||||
'input_arg_names': str(op.input_arg_names),
|
||||
'output_arg_names': str(op.output_arg_names),
|
||||
}
|
||||
for op in ops
|
||||
]
|
||||
with open(os.path.join(output_dir, ops_out_fn), 'w') as f:
|
||||
f.write("ops:\n")
|
||||
for op in output["ops"]:
|
||||
f.write(str(op))
|
||||
f.write("\n")
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copyright (c) 2023 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.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Copyright (c) 2022 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.
|
||||
|
||||
from .gate import BaseGate, GShardGate, NaiveGate, SwitchGate # noqa: F401
|
||||
from .grad_clip import ClipGradForMOEByGlobalNorm
|
||||
from .moe_layer import MoELayer # noqa: F401
|
||||
|
||||
ClipGradByGlobalNorm = ClipGradForMOEByGlobalNorm
|
||||
@@ -0,0 +1,18 @@
|
||||
# Copyright (c) 2022 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.
|
||||
|
||||
from .base_gate import BaseGate # noqa: F401
|
||||
from .gshard_gate import GShardGate # noqa: F401
|
||||
from .naive_gate import NaiveGate # noqa: F401
|
||||
from .switch_gate import SwitchGate # noqa: F401
|
||||
@@ -0,0 +1,43 @@
|
||||
# Copyright (c) 2022 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.
|
||||
#
|
||||
# The file has been adapted from the file:
|
||||
# https://github.com/laekov/fastmoe/blob/master/fmoe/gates/base_gate.py
|
||||
# Git commit hash: 295a615aacce7e54a37e7935274ba15e901c78e4
|
||||
# We retain the following license from the original files:
|
||||
# Copyright 2021, Jiaao He. All rights reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License").
|
||||
|
||||
from paddle import nn
|
||||
|
||||
|
||||
class BaseGate(nn.Layer):
|
||||
def __init__(self, num_expert, world_size):
|
||||
super().__init__()
|
||||
self.world_size = world_size
|
||||
self.num_expert = num_expert
|
||||
self.tot_expert = world_size * num_expert
|
||||
self.loss = None
|
||||
|
||||
def forward(self, x):
|
||||
raise NotImplementedError("Please implement the forward function.")
|
||||
|
||||
def set_loss(self, loss):
|
||||
self.loss = loss
|
||||
|
||||
def get_loss(self, clear=True):
|
||||
loss = self.loss
|
||||
if clear:
|
||||
self.loss = None
|
||||
return loss
|
||||
@@ -0,0 +1,84 @@
|
||||
# Copyright (c) 2022 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.
|
||||
#
|
||||
# The file has been adapted from the file:
|
||||
# https://github.com/laekov/fastmoe/blob/master/fmoe/gates/gshard_gate.py
|
||||
# Git commit hash: 295a615aacce7e54a37e7935274ba15e901c78e4
|
||||
# We retain the following license from the original files:
|
||||
# Copyright 2021, Jiaao He. All rights reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License").
|
||||
|
||||
import math
|
||||
|
||||
import paddle
|
||||
import paddle.nn.functional as F
|
||||
|
||||
from ..utils import limit_by_capacity
|
||||
from .naive_gate import NaiveGate
|
||||
|
||||
|
||||
class GShardGate(NaiveGate):
|
||||
def __init__(
|
||||
self,
|
||||
d_model,
|
||||
num_expert,
|
||||
world_size,
|
||||
topk=2,
|
||||
capacity=(1.2, 2.4),
|
||||
random_routing=True,
|
||||
group=None,
|
||||
):
|
||||
assert topk == 2, "topk should be 2 in gshard"
|
||||
super().__init__(d_model, num_expert, world_size)
|
||||
self.capacity = capacity
|
||||
self.random_routing = random_routing
|
||||
self.group = group
|
||||
|
||||
def forward(self, x):
|
||||
topk_val, topk_idx, gate_score = super().forward(
|
||||
x, return_all_scores=True
|
||||
)
|
||||
s = gate_score.shape[0]
|
||||
top1_idx = topk_idx.flatten()
|
||||
c_e = (
|
||||
paddle.scatter(
|
||||
paddle.zeros(shape=[self.tot_expert]),
|
||||
top1_idx,
|
||||
paddle.ones_like(top1_idx, dtype="float32"),
|
||||
overwrite=False,
|
||||
)
|
||||
/ s
|
||||
)
|
||||
m_e = paddle.mean(F.softmax(gate_score, axis=1), axis=0)
|
||||
loss = paddle.mean(c_e * m_e) * (self.num_expert**2)
|
||||
self.set_loss(loss)
|
||||
|
||||
cap_rate = self.capacity[0 if self.training else 1]
|
||||
capacity = math.ceil(cap_rate * x.shape[0])
|
||||
_new_lec, _new_gec, topk_idx = limit_by_capacity(
|
||||
topk_idx,
|
||||
self.num_expert,
|
||||
self.world_size,
|
||||
capacity,
|
||||
group=self.group,
|
||||
)
|
||||
|
||||
if self.random_routing:
|
||||
rand_routing_prob = paddle.rand(
|
||||
shape=[gate_score.shape[0]], dtype="float32"
|
||||
)
|
||||
topk_idx = paddle.distributed.models.moe.utils._random_routing(
|
||||
topk_idx, topk_val, rand_routing_prob
|
||||
)
|
||||
return topk_val, topk_idx
|
||||
@@ -0,0 +1,44 @@
|
||||
# Copyright (c) 2022 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.
|
||||
#
|
||||
# The file has been adapted from the file:
|
||||
# https://github.com/laekov/fastmoe/blob/master/fmoe/gates/naive_gate.py
|
||||
# Git commit hash: 295a615aacce7e54a37e7935274ba15e901c78e4
|
||||
# We retain the following license from the original files:
|
||||
# Copyright 2021, Jiaao He. All rights reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License").
|
||||
|
||||
import paddle
|
||||
from paddle import nn
|
||||
|
||||
from .base_gate import BaseGate
|
||||
|
||||
|
||||
class NaiveGate(BaseGate):
|
||||
def __init__(self, d_model, num_expert, world_size, topk=2):
|
||||
super().__init__(num_expert, world_size)
|
||||
self.gate = nn.Linear(d_model, self.tot_expert)
|
||||
self.gate.weight.name = "gate_" + self.gate.weight.name
|
||||
self.gate.bias.name = "gate_" + self.gate.bias.name
|
||||
self.top_k = topk
|
||||
|
||||
def forward(self, inp, return_all_scores=False):
|
||||
gate = self.gate(inp)
|
||||
gate_top_k_val, gate_top_k_idx = paddle.topk(
|
||||
gate, k=self.top_k, axis=-1, largest=True, sorted=False
|
||||
)
|
||||
|
||||
if return_all_scores:
|
||||
return gate_top_k_val, gate_top_k_idx, gate
|
||||
return gate_top_k_val, gate_top_k_idx
|
||||
@@ -0,0 +1,84 @@
|
||||
# Copyright (c) 2022 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.
|
||||
#
|
||||
# The file has been adapted from the file:
|
||||
# https://github.com/laekov/fastmoe/blob/master/fmoe/gates/switch_gate.py
|
||||
# Git commit hash: 295a615aacce7e54a37e7935274ba15e901c78e4
|
||||
# We retain the following license from the original files:
|
||||
# Copyright 2021, Jiaao He. All rights reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License").
|
||||
|
||||
import math
|
||||
|
||||
import paddle
|
||||
import paddle.nn.functional as F
|
||||
|
||||
from ..utils import limit_by_capacity
|
||||
from .naive_gate import NaiveGate
|
||||
|
||||
|
||||
class SwitchGate(NaiveGate):
|
||||
def __init__(
|
||||
self,
|
||||
d_model,
|
||||
num_expert,
|
||||
world_size,
|
||||
topk=1,
|
||||
switch_eps=0.1,
|
||||
capacity=(1.2, 2.4),
|
||||
group=None,
|
||||
):
|
||||
assert topk == 1, "topk should be 1 in switch"
|
||||
super().__init__(d_model, num_expert, world_size, topk=1)
|
||||
self.switch_eps = switch_eps
|
||||
self.capacity = capacity
|
||||
self.group = group
|
||||
|
||||
def forward(self, inp):
|
||||
score = self.gate(inp)
|
||||
|
||||
if self.training:
|
||||
noise = paddle.rand(shape=score.shape)
|
||||
noise = noise * 2 * self.switch_eps + 1.0 - self.switch_eps
|
||||
score += noise
|
||||
|
||||
score = F.softmax(score, axis=-1)
|
||||
top1_score, top1_idx = paddle.topk(score, k=1, axis=-1, largest=True)
|
||||
|
||||
cap_rate = self.capacity[0 if self.training else 1]
|
||||
capacity = math.ceil(cap_rate * inp.shape[0])
|
||||
_new_lec, _new_gec, top1_idx = limit_by_capacity(
|
||||
top1_idx,
|
||||
self.num_expert,
|
||||
self.world_size,
|
||||
capacity,
|
||||
group=self.group,
|
||||
)
|
||||
valid_idx = top1_idx[top1_idx > -1]
|
||||
valid_idx_tmp = paddle.reshape(valid_idx, shape=[len(valid_idx), 1])
|
||||
fraction_expert = (
|
||||
paddle.scatter_nd_add(
|
||||
x=paddle.zeros(shape=[self.tot_expert]),
|
||||
index=valid_idx_tmp,
|
||||
updates=paddle.ones_like(
|
||||
valid_idx, dtype=paddle.float32
|
||||
).reshape(shape=[len(valid_idx)]),
|
||||
)
|
||||
/ valid_idx.numel()
|
||||
)
|
||||
prob_expert = score.sum(axis=0) / valid_idx.numel()
|
||||
loss = (fraction_expert * prob_expert).sum() * self.tot_expert
|
||||
self.set_loss(loss)
|
||||
|
||||
return top1_score, top1_idx
|
||||
@@ -0,0 +1,238 @@
|
||||
# Copyright (c) 2018 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 paddle
|
||||
import paddle.distributed as dist
|
||||
from paddle.autograd import no_grad
|
||||
from paddle.framework import core
|
||||
from paddle.nn import clip
|
||||
from paddle.nn.clip import ClipGradBase, _squared_l2_norm
|
||||
|
||||
|
||||
class ClipGradForMOEByGlobalNorm(ClipGradBase):
|
||||
r"""
|
||||
The Algorithm is the same as paddle.nn.ClipGradByGlobalNorm
|
||||
Given a list of Tensor :math:`t\_list` , calculate the global norm for the elements of all tensors in
|
||||
:math:`t\_list` , and limit it to ``clip_norm`` .
|
||||
|
||||
- If the global norm is greater than ``clip_norm`` , all elements of :math:`t\_list` will be compressed by a ratio.
|
||||
|
||||
- If the global norm is less than or equal to ``clip_norm`` , nothing will be done.
|
||||
|
||||
The list of Tensor :math:`t\_list` is not passed from this class, but the gradients of all parameters set in ``optimizer``.
|
||||
If ``need_clip`` of specific param is ``False`` in its ``ParamAttr``, then the gradients of this param will not be clipped.
|
||||
|
||||
Gradient clip will takes effect after being set in ``optimizer`` , see the document ``optimizer``
|
||||
(for example: :ref:`api_paddle_optimizer_SGD`).
|
||||
|
||||
The clipping formula is:
|
||||
|
||||
.. math::
|
||||
|
||||
t\_list[i] = t\_list[i] * \frac{clip\_norm}{\max(global\_norm, clip\_norm)}
|
||||
|
||||
where:
|
||||
|
||||
.. math::
|
||||
|
||||
global\_norm = \sqrt{\sum_{i=0}^{N-1}(l2norm(t\_list[i]))^2}
|
||||
|
||||
Note:
|
||||
``need_clip`` of ``ClipGradyGlobalNorm`` HAS BEEN DEPRECATED since 2.0.
|
||||
Please use ``need_clip`` in ``ParamAttr`` to specify the clip scope.
|
||||
|
||||
Reference:
|
||||
https://github.com/laekov/fastmoe/blob/master/examples/megatron/clip-grad-v2.2.patch
|
||||
Git commit hash: 295a615aacce7e54a37e7935274ba15e901c78e4
|
||||
|
||||
|
||||
Args:
|
||||
clip_norm (float): The maximum norm value.
|
||||
is_expert_param_func (function): a function to decide whether a param should be put into moe_params_grads
|
||||
moe_group (Group): group for moe experts communication.
|
||||
group_name (str, optional): The group name for this clip. Default value is ``default_moe_group``.
|
||||
|
||||
Examples:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
|
||||
>>> x = paddle.uniform([10, 10], min=-1.0, max=1.0, dtype='float32')
|
||||
>>> linear = paddle.nn.Linear(
|
||||
... in_features=10,
|
||||
... out_features=10,
|
||||
... weight_attr=paddle.ParamAttr(need_clip=True),
|
||||
... bias_attr=paddle.ParamAttr(need_clip=False),
|
||||
... )
|
||||
>>> out = linear(x)
|
||||
>>> loss = paddle.mean(out)
|
||||
>>> loss.backward()
|
||||
|
||||
>>> clip = paddle.nn.ClipGradByGlobalNorm(
|
||||
... clip_norm=1.0
|
||||
... ) # Cause paddle.nn hasn't this interface, so we use ClipGradByGlobalNorm here.
|
||||
>>> sdg = paddle.optimizer.SGD(learning_rate=0.1, parameters=linear.parameters(), grad_clip=clip)
|
||||
>>> sdg.step()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
clip_norm,
|
||||
is_expert_param_func=None,
|
||||
moe_group=None,
|
||||
group_name="default_moe_group",
|
||||
):
|
||||
super().__init__()
|
||||
self.clip_norm = float(clip_norm)
|
||||
self.group_name = group_name
|
||||
self.moe_group = moe_group
|
||||
if moe_group is not None and moe_group.nranks > 1:
|
||||
assert is_expert_param_func is not None, (
|
||||
"When moe group size > 1, a function for selecting expert params must be specified."
|
||||
)
|
||||
self.is_expert_param_func = is_expert_param_func
|
||||
|
||||
def __str__(self):
|
||||
return f"Gradient Clip By GlobalNorm, global_norm={self.clip_norm:f}"
|
||||
|
||||
@staticmethod
|
||||
def get_l2_norm_pow(params_grads, sum_dtype=None):
|
||||
sum_square_list = []
|
||||
sum_square_list_fp16 = []
|
||||
sum_square_list_fp32 = []
|
||||
for p, g in params_grads:
|
||||
if g is None:
|
||||
continue
|
||||
if getattr(p, 'need_clip', True) is False:
|
||||
continue
|
||||
merge_grad = g
|
||||
if g.type == core.VarDesc.VarType.SELECTED_ROWS:
|
||||
merge_grad = clip.merge_selected_rows(g)
|
||||
merge_grad = clip.get_tensor_from_selected_rows(merge_grad)
|
||||
sum_square = _squared_l2_norm(merge_grad)
|
||||
if sum_square.dtype == paddle.float16:
|
||||
sum_square_list_fp16.append(sum_square)
|
||||
elif sum_square.dtype == paddle.float32:
|
||||
sum_square_list_fp32.append(sum_square)
|
||||
else:
|
||||
sum_square_list.append(sum_square)
|
||||
|
||||
# all parameters have been filtered out
|
||||
if (
|
||||
len(sum_square_list)
|
||||
+ len(sum_square_list_fp16)
|
||||
+ len(sum_square_list_fp32)
|
||||
== 0
|
||||
):
|
||||
return None, None
|
||||
assert sum_dtype in [
|
||||
"float64",
|
||||
"float32",
|
||||
None,
|
||||
], "sum's type must be float64/ float32 / None"
|
||||
if sum_dtype != "float64":
|
||||
sum_dtype = 'float64' if len(sum_square_list) > 0 else "float32"
|
||||
|
||||
global_norm_var = []
|
||||
if len(sum_square_list_fp16) > 0:
|
||||
global_norm_var_fp16 = paddle.add_n(sum_square_list_fp16)
|
||||
global_norm_var.append(global_norm_var_fp16.astype(sum_dtype))
|
||||
if len(sum_square_list_fp32) > 0:
|
||||
global_norm_var_fp32 = paddle.add_n(sum_square_list_fp32)
|
||||
if sum_dtype == 'float32':
|
||||
global_norm_var.append(global_norm_var_fp32)
|
||||
else:
|
||||
global_norm_var.append(global_norm_var_fp32.astype(sum_dtype))
|
||||
if len(sum_square_list) > 0:
|
||||
global_norm_var_fp64 = paddle.add_n(sum_square_list)
|
||||
global_norm_var.append(global_norm_var_fp64)
|
||||
global_norm_var = paddle.add_n(global_norm_var)
|
||||
return global_norm_var, sum_dtype
|
||||
|
||||
@no_grad()
|
||||
def _dygraph_clip(self, params_grads):
|
||||
normal_params_grads = []
|
||||
moe_params_grads = []
|
||||
|
||||
# separate moe params from normal params
|
||||
if self.moe_group is not None and self.moe_group.nranks > 1:
|
||||
for p, g in params_grads:
|
||||
if self.is_expert_param_func(p):
|
||||
moe_params_grads.append((p, g))
|
||||
else:
|
||||
normal_params_grads.append((p, g))
|
||||
else:
|
||||
normal_params_grads = params_grads
|
||||
|
||||
# why to return sum_dtype?
|
||||
# we will call `get_l2_norm_pow` twice and the precisions may be different.
|
||||
# For convenience and simplification, we use sum_dtype directly instead of global_norm_var_normal.dtype
|
||||
global_norm_var_normal, sum_dtype = self.get_l2_norm_pow(
|
||||
normal_params_grads
|
||||
)
|
||||
global_norm_var_moe = None
|
||||
if len(moe_params_grads) > 0:
|
||||
global_norm_var_moe, _ = self.get_l2_norm_pow(
|
||||
moe_params_grads, sum_dtype
|
||||
)
|
||||
if global_norm_var_moe is not None:
|
||||
dist.all_reduce(
|
||||
global_norm_var_moe,
|
||||
op=dist.ReduceOp.SUM,
|
||||
group=self.moe_group,
|
||||
)
|
||||
|
||||
if global_norm_var_normal is None and global_norm_var_moe is None:
|
||||
return params_grads
|
||||
elif global_norm_var_normal is None:
|
||||
global_norm_var = global_norm_var_moe
|
||||
elif global_norm_var_moe is None:
|
||||
global_norm_var = global_norm_var_normal
|
||||
else:
|
||||
if global_norm_var_normal.dtype != global_norm_var_moe.dtype:
|
||||
# compared with normal norm, moe norm is the later one,
|
||||
# so its precision is no lower than normal norm
|
||||
global_norm_var_normal = global_norm_var_normal.astype(
|
||||
global_norm_var_moe.dtype
|
||||
)
|
||||
global_norm_var = global_norm_var_normal + global_norm_var_moe
|
||||
|
||||
params_and_grads = []
|
||||
global_norm_var = paddle.sqrt(global_norm_var)
|
||||
max_global_norm = paddle.full(
|
||||
shape=[1], dtype=global_norm_var.dtype, fill_value=self.clip_norm
|
||||
)
|
||||
clip_var = paddle.divide(
|
||||
x=max_global_norm,
|
||||
y=paddle.maximum(x=global_norm_var, y=max_global_norm),
|
||||
)
|
||||
for p, g in params_grads:
|
||||
if g is None:
|
||||
continue
|
||||
if getattr(p, 'need_clip', True) is False:
|
||||
params_and_grads.append((p, g))
|
||||
continue
|
||||
# TODO(wangxi): use inplace elementwise_mul
|
||||
clip_input = (
|
||||
clip_var.astype('float16')
|
||||
if g.dtype == paddle.float16
|
||||
else clip_var
|
||||
)
|
||||
new_grad = paddle.multiply(x=g, y=clip_input)
|
||||
params_and_grads.append((p, new_grad))
|
||||
return params_and_grads
|
||||
|
||||
|
||||
ClipGradByGlobalNorm = ClipGradForMOEByGlobalNorm
|
||||
@@ -0,0 +1,503 @@
|
||||
# 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.
|
||||
#
|
||||
# The file has been adapted from the file:
|
||||
# https://github.com/laekov/fastmoe/blob/master/fmoe/layers.py
|
||||
# Git commit hash: 295a615aacce7e54a37e7935274ba15e901c78e4
|
||||
# We retain the following license from the original files:
|
||||
# Copyright 2021, Jiaao He. All rights reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License").
|
||||
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
|
||||
import paddle
|
||||
from paddle import nn
|
||||
from paddle.autograd import PyLayer
|
||||
from paddle.distributed.utils.moe_utils import global_gather, global_scatter
|
||||
from paddle.distributed.utils.nccl_utils import check_nccl_version_for_p2p
|
||||
from paddle.framework import in_dynamic_mode
|
||||
from paddle.incubate.distributed.fleet import recompute_hybrid
|
||||
|
||||
from .gate import BaseGate, GShardGate, NaiveGate, SwitchGate
|
||||
from .utils import count_by_gate
|
||||
|
||||
|
||||
def _local_scatter(inp, pos):
|
||||
if pos.shape != [0]:
|
||||
inp_buf = paddle.index_select(inp, pos, 0)
|
||||
else:
|
||||
inp_buf = paddle.empty([0, inp.shape[1]], dtype=inp.dtype)
|
||||
return inp_buf
|
||||
|
||||
|
||||
def _local_gather(inp, pos, out_batch_size, maybe_overlap=True):
|
||||
if pos.shape != [0]:
|
||||
origin_dtype = inp.dtype
|
||||
inp = paddle.cast(inp, dtype="float32")
|
||||
inp_buf = paddle.scatter(
|
||||
paddle.zeros(
|
||||
shape=[out_batch_size, inp.shape[-1]], dtype="float32"
|
||||
),
|
||||
pos,
|
||||
inp,
|
||||
overwrite=True,
|
||||
)
|
||||
inp_buf = paddle.cast(inp_buf, dtype=origin_dtype)
|
||||
else:
|
||||
inp_buf = paddle.zeros([out_batch_size, inp.shape[-1]], dtype=inp.dtype)
|
||||
return inp_buf
|
||||
|
||||
|
||||
def _all_gather(tensor, group=None, use_calc_stream=True):
|
||||
if group is not None and not group.is_member():
|
||||
return
|
||||
|
||||
if in_dynamic_mode():
|
||||
group = (
|
||||
paddle.distributed.collective._get_default_group()
|
||||
if group is None
|
||||
else group
|
||||
)
|
||||
tensor_shape = list(tensor.shape)
|
||||
tensor_shape[0] *= group.nranks
|
||||
out = paddle.empty(tensor_shape, tensor.dtype)
|
||||
|
||||
task = group.process_group.all_gather(tensor, out)
|
||||
task.wait()
|
||||
return out
|
||||
else:
|
||||
ring_id = 0 if group is None else group.id
|
||||
nranks = (
|
||||
paddle.distributed.collective._get_global_group().nranks
|
||||
if group is None
|
||||
else group.nranks
|
||||
)
|
||||
return paddle._C_ops.all_gather(
|
||||
tensor,
|
||||
ring_id,
|
||||
nranks,
|
||||
)
|
||||
|
||||
|
||||
class MoEScatter(PyLayer):
|
||||
r"""
|
||||
Scatter input samples from [batch x sequences] to contiguous alone experts.
|
||||
If `world_size` is greater than 1, the samples will first be locally
|
||||
scattered, and then exchanged across workers.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def forward(
|
||||
ctx,
|
||||
inp,
|
||||
pos,
|
||||
local_expert_count,
|
||||
global_expert_count,
|
||||
fwd_batch_size,
|
||||
world_size,
|
||||
group=None,
|
||||
):
|
||||
local_input_buf = _local_scatter(inp, pos)
|
||||
if world_size > 1:
|
||||
global_input_buf = global_scatter(
|
||||
local_input_buf,
|
||||
local_expert_count,
|
||||
global_expert_count,
|
||||
group=group,
|
||||
)
|
||||
else:
|
||||
global_input_buf = local_input_buf
|
||||
|
||||
ctx.moe_args = inp.shape[0], world_size, group
|
||||
|
||||
variables = (pos, local_expert_count, global_expert_count)
|
||||
ctx.save_for_backward(*variables)
|
||||
return global_input_buf
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, grad):
|
||||
(pos, local_expert_count, global_expert_count) = ctx.saved_tensor()
|
||||
(inp_batch_size, world_size, group) = ctx.moe_args
|
||||
|
||||
if world_size > 1:
|
||||
local_grad_in = global_gather(
|
||||
grad, local_expert_count, global_expert_count, group=group
|
||||
)
|
||||
else:
|
||||
local_grad_in = grad
|
||||
grad_in = _local_gather(local_grad_in, pos, inp_batch_size)
|
||||
return grad_in, None, None, None
|
||||
|
||||
|
||||
class MoEGather(PyLayer):
|
||||
r"""
|
||||
Gather output samples from contiguous alone experts back to [batch x
|
||||
sequences]. Works symmetrically with MoEScatter.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def forward(
|
||||
ctx,
|
||||
global_output_buf,
|
||||
pos,
|
||||
local_expert_count,
|
||||
global_expert_count,
|
||||
local_batch_size,
|
||||
world_size,
|
||||
group=None,
|
||||
):
|
||||
if world_size > 1:
|
||||
local_output_buf = global_gather(
|
||||
global_output_buf,
|
||||
local_expert_count,
|
||||
global_expert_count,
|
||||
group=group,
|
||||
)
|
||||
else:
|
||||
local_output_buf = global_output_buf
|
||||
output = _local_gather(
|
||||
local_output_buf, pos, local_batch_size, maybe_overlap=False
|
||||
)
|
||||
|
||||
ctx.moe_args = (global_output_buf.shape[0], world_size, group)
|
||||
variables = (pos, local_expert_count, global_expert_count)
|
||||
ctx.save_for_backward(*variables)
|
||||
return output
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, grad_out):
|
||||
pos, local_expert_count, global_expert_count = ctx.saved_tensor()
|
||||
fwd_batch_size, world_size, group = ctx.moe_args
|
||||
grad_out_buf = _local_scatter(grad_out, pos)
|
||||
if world_size > 1:
|
||||
global_grad_out_buf = global_scatter(
|
||||
grad_out_buf,
|
||||
local_expert_count,
|
||||
global_expert_count,
|
||||
group=group,
|
||||
)
|
||||
else:
|
||||
global_grad_out_buf = grad_out_buf
|
||||
return global_grad_out_buf, None, None, None
|
||||
|
||||
|
||||
class AllGather(PyLayer):
|
||||
r"""
|
||||
A wrapper for the All-Gather function to support auto-differentiation.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, inp, rank, world_size, group):
|
||||
tensor_list = []
|
||||
paddle.distributed.all_gather(tensor_list, inp, group=group)
|
||||
output = paddle.concat(tensor_list, axis=0)
|
||||
ctx.args = rank, inp.shape[0]
|
||||
return output
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, grad_out):
|
||||
rank, dim0 = ctx.args
|
||||
return paddle.slice(
|
||||
grad_out, axes=[0], starts=[rank * dim0], ends=[(rank + 1) * dim0]
|
||||
)
|
||||
|
||||
|
||||
class Slice(PyLayer):
|
||||
r"""
|
||||
A wrapper for the Slice function to support auto-differentiation.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, inp, rank, world_size, group):
|
||||
B = inp.shape[0]
|
||||
local_batch_size = B // world_size
|
||||
batch_start = local_batch_size * rank
|
||||
batch_end = min(batch_start + local_batch_size, B)
|
||||
inp = paddle.slice(
|
||||
inp, axes=[0], starts=[batch_start], ends=[batch_end]
|
||||
)
|
||||
ctx.args = world_size, group
|
||||
return inp
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, grad_out):
|
||||
world_size, group = ctx.args
|
||||
return _all_gather(grad_out, group=group)
|
||||
|
||||
|
||||
def prepare_forward(gate, num_expert, world_size, moe_group):
|
||||
pos, local_expert_count, global_expert_count = count_by_gate(
|
||||
gate, num_expert, world_size, group=moe_group
|
||||
)
|
||||
with paddle.no_grad():
|
||||
fwd_expert_count = global_expert_count.reshape_(
|
||||
[world_size, num_expert]
|
||||
).sum(axis=0)
|
||||
fwd_batch_size = int(fwd_expert_count.sum().item())
|
||||
return (
|
||||
pos,
|
||||
local_expert_count,
|
||||
global_expert_count,
|
||||
fwd_expert_count,
|
||||
fwd_batch_size,
|
||||
)
|
||||
|
||||
|
||||
class MoELayer(nn.Layer):
|
||||
"""MoE Layer
|
||||
Args:
|
||||
d_model (int): Model dimension.
|
||||
experts (nn.LayerList): Expert networks list.
|
||||
gate (dict|NaiveGate|SwitchGate|NaiveGate):
|
||||
|
||||
- If gate is a dict:
|
||||
gate is a gate network config, containing 2 keys:
|
||||
`type` (str) value can be: "naive", "gshard", "switch" or None, default is "gshard".
|
||||
`top_k` (int) Default value is 2.
|
||||
else gate is an instance of NaiveGate|SwitchGate|NaiveGate:
|
||||
|
||||
moe_group: moe group for experts communication.
|
||||
mp_group: mp group for mp communication.
|
||||
recompute_interval (int, optional): Whether to use recompute, default 0, means to disable recompute.
|
||||
recompute_ctx (dict, optional): The context for recompute, if recompute_interval > 1, recompute_ctx must be given.
|
||||
|
||||
Examples:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> # doctest: +SKIP('Until Distributed move successfully, just skip it')
|
||||
>>> from paddle.nn import layer, LayerList
|
||||
>>> from paddle.distributed.moe import MoElayer
|
||||
>>> from paddle.distributed.collective import Group
|
||||
>>> from paddle.distributed import fleet
|
||||
|
||||
>>> moe_group = Group(
|
||||
... fleet.worker_index(),
|
||||
... 0,
|
||||
... list(range(fleet.worker_num())),
|
||||
... )
|
||||
>>> mp_group = None
|
||||
|
||||
>>> num_experts = 8
|
||||
>>> dim_feedforward = 512
|
||||
>>> d_model = 8
|
||||
>>> top_k = 2
|
||||
|
||||
>>> class ExpertLayer(Layer):
|
||||
... def __init__(self, d_model, d_hidden, name=None, rank=0, windex=0, num_expert=1):
|
||||
... super().__init__()
|
||||
... self.htoh4 = nn.Linear(d_model, d_hidden)
|
||||
... self.h4toh = nn.Linear(d_hidden, d_model)
|
||||
|
||||
... def forward(self, x):
|
||||
... x = self.htoh4(x)
|
||||
... x = self.h4toh(x)
|
||||
... return x
|
||||
|
||||
>>> gate_config = {
|
||||
... "type": "gshard",
|
||||
... "top_k": top_k,
|
||||
... }
|
||||
|
||||
>>> experts_list = LayerList()
|
||||
>>> for expi in range(num_experts):
|
||||
... exp_layer = ExpertLayer(d_model, dim_feedforward // top_k, windex=expi, num_expert=num_experts)
|
||||
... experts_list.append(exp_layer)
|
||||
|
||||
>>> moeLayer = MoELayer(
|
||||
... d_model=d_model,
|
||||
... experts=experts_list,
|
||||
... gate=gate_config,
|
||||
... moe_group=moe_group,
|
||||
... mp_group=mp_group,
|
||||
... recompute_interval=0,
|
||||
... )
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
d_model,
|
||||
experts,
|
||||
gate=None,
|
||||
moe_group=None,
|
||||
mp_group=None,
|
||||
recompute_interval=0,
|
||||
recompute_ctx=None,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self.recompute_ctx = recompute_ctx
|
||||
|
||||
if gate is None:
|
||||
gate = {}
|
||||
|
||||
assert isinstance(gate, (dict, BaseGate)), (
|
||||
"gate config' type must be dict or an instance of BaseGate"
|
||||
)
|
||||
# only support mp/dp
|
||||
self.group = moe_group
|
||||
|
||||
self.world_size = 1
|
||||
if self.group is not None:
|
||||
self.world_size = self.group.nranks
|
||||
self.num_expert = len(experts)
|
||||
self.recompute_interval = recompute_interval
|
||||
assert experts is not None
|
||||
self.experts = experts
|
||||
|
||||
if (
|
||||
self.world_size > 1
|
||||
and os.getenv("PADDLE_DISTRI_BACKEND", None) != "xccl"
|
||||
):
|
||||
check_nccl_version_for_p2p()
|
||||
|
||||
self.mp_group = mp_group
|
||||
self.d_model = d_model
|
||||
if isinstance(gate, dict):
|
||||
self.top_k = gate.get("top_k", 2)
|
||||
gate = gate.get("type", "gshard")
|
||||
if gate == "naive" or gate is None:
|
||||
gate = NaiveGate(
|
||||
self.d_model,
|
||||
num_expert=len(experts),
|
||||
world_size=self.world_size,
|
||||
topk=self.top_k,
|
||||
)
|
||||
elif gate == "gshard":
|
||||
gate = GShardGate(
|
||||
self.d_model,
|
||||
num_expert=len(experts),
|
||||
world_size=self.world_size,
|
||||
topk=self.top_k,
|
||||
group=self.group,
|
||||
)
|
||||
elif gate == "switch":
|
||||
gate = SwitchGate(
|
||||
self.d_model,
|
||||
num_expert=len(experts),
|
||||
world_size=self.world_size,
|
||||
topk=self.top_k,
|
||||
group=self.group,
|
||||
)
|
||||
else:
|
||||
raise AssertionError(
|
||||
f"We only support naive gate, gshard gate and switch gate, but you choose {gate} gate."
|
||||
)
|
||||
elif isinstance(gate, NaiveGate):
|
||||
self.top_k = gate.top_k
|
||||
elif isinstance(gate, BaseGate):
|
||||
raise TypeError(f"Unimplemented gate type: {type(gate)}")
|
||||
else:
|
||||
raise TypeError("gate's type must be either dict or moe.BaseGate")
|
||||
self.gate = gate
|
||||
|
||||
def forward(self, inp):
|
||||
# inp shape: b * s * m
|
||||
assert len(inp.shape) == 3
|
||||
origin_shape = inp.shape
|
||||
inp = inp.reshape_([-1, origin_shape[2]])
|
||||
|
||||
mp_rank = 0
|
||||
mp_size = 1
|
||||
if self.mp_group is not None:
|
||||
mp_rank = self.mp_group.rank
|
||||
mp_size = self.mp_group.nranks
|
||||
if mp_size > 1:
|
||||
inp = Slice.apply(inp, mp_rank, mp_size, self.mp_group)
|
||||
value, gate = self.gate(inp)
|
||||
|
||||
(
|
||||
pos,
|
||||
local_expert_count,
|
||||
global_expert_count,
|
||||
fwd_expert_count,
|
||||
fwd_batch_size,
|
||||
) = prepare_forward(gate, self.num_expert, self.world_size, self.group)
|
||||
|
||||
topk = 1
|
||||
if len(gate.shape) == 2:
|
||||
topk = gate.shape[1]
|
||||
|
||||
if pos.shape != [0]:
|
||||
temp_pos = pos // topk
|
||||
else:
|
||||
temp_pos = pos
|
||||
assert topk == self.top_k
|
||||
|
||||
x = MoEScatter.apply(
|
||||
inp,
|
||||
temp_pos,
|
||||
local_expert_count,
|
||||
global_expert_count,
|
||||
fwd_batch_size,
|
||||
self.world_size,
|
||||
self.group,
|
||||
)
|
||||
|
||||
d_model = self.d_model
|
||||
|
||||
def experts_fwd(x, fwd_expert_count, experts):
|
||||
if x.shape[0] == 0:
|
||||
return x
|
||||
y = []
|
||||
last_index = 0
|
||||
assert isinstance(fwd_expert_count, np.ndarray)
|
||||
assert len(experts) == len(fwd_expert_count)
|
||||
for idx, expert_count in enumerate(fwd_expert_count):
|
||||
if expert_count <= 0:
|
||||
continue
|
||||
y.append(
|
||||
experts[idx](x[last_index : expert_count + last_index])
|
||||
)
|
||||
last_index = expert_count + last_index
|
||||
return paddle.concat(y, axis=0)
|
||||
|
||||
if self.recompute_interval <= 0 or x.shape[0] == 0:
|
||||
x = experts_fwd(x, fwd_expert_count.numpy(), self.experts)
|
||||
else:
|
||||
x = recompute_hybrid(
|
||||
self.recompute_ctx,
|
||||
experts_fwd,
|
||||
x,
|
||||
fwd_expert_count.numpy(),
|
||||
self.experts,
|
||||
)
|
||||
|
||||
out_batch_size = inp.shape[0]
|
||||
if len(gate.shape) == 2:
|
||||
out_batch_size *= gate.shape[1]
|
||||
|
||||
x = MoEGather.apply(
|
||||
x,
|
||||
pos,
|
||||
local_expert_count,
|
||||
global_expert_count,
|
||||
out_batch_size,
|
||||
self.world_size,
|
||||
self.group,
|
||||
)
|
||||
|
||||
x = x.reshape([-1, self.top_k, d_model])
|
||||
value = value.reshape([x.shape[0], 1, self.top_k])
|
||||
x = paddle.bmm(value, x).reshape([-1, d_model])
|
||||
|
||||
if mp_size > 1:
|
||||
x = AllGather.apply(x, mp_rank, mp_size, self.mp_group)
|
||||
|
||||
x = paddle.reshape_(x, origin_shape)
|
||||
|
||||
return x
|
||||
@@ -0,0 +1,87 @@
|
||||
# Copyright (c) 2022 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.
|
||||
#
|
||||
# The file has been adapted from the file:
|
||||
# https://github.com/laekov/fastmoe/blob/master/fmoe/functions.py
|
||||
# Git commit hash: 295a615aacce7e54a37e7935274ba15e901c78e4
|
||||
# We retain the following license from the original files:
|
||||
# Copyright 2021, Jiaao He. All rights reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License").
|
||||
|
||||
import paddle
|
||||
from paddle.distributed.models.moe.utils import (
|
||||
_assign_pos,
|
||||
_limit_by_capacity,
|
||||
_number_count,
|
||||
_prune_gate_by_capacity,
|
||||
)
|
||||
from paddle.framework import in_dynamic_mode
|
||||
|
||||
|
||||
def _alltoall(in_tensor_list, group=None, use_calc_stream=True):
|
||||
if group is not None and not group.is_member():
|
||||
return
|
||||
|
||||
if in_dynamic_mode():
|
||||
group = (
|
||||
paddle.distributed.collective._get_default_group()
|
||||
if group is None
|
||||
else group
|
||||
)
|
||||
out = paddle.empty(in_tensor_list.shape, in_tensor_list.dtype)
|
||||
task = group.process_group.alltoall(out, in_tensor_list)
|
||||
task.wait()
|
||||
return out
|
||||
else:
|
||||
ring_id = 0 if group is None else group.id
|
||||
return paddle._C_ops.all_to_all(in_tensor_list, ring_id)
|
||||
|
||||
|
||||
def count_by_gate(gate, num_expert, world_size, require_pos=True, group=None):
|
||||
total_expert_count = num_expert * world_size
|
||||
with paddle.no_grad():
|
||||
local_expert_count = _number_count(gate, total_expert_count)
|
||||
|
||||
if world_size > 1:
|
||||
global_expert_count = _alltoall(local_expert_count, group=group)
|
||||
else:
|
||||
global_expert_count = local_expert_count
|
||||
if not require_pos:
|
||||
pos = None
|
||||
else:
|
||||
lec_cum = paddle.cumsum(local_expert_count, axis=0)
|
||||
pos = _assign_pos(gate, lec_cum)
|
||||
return pos, local_expert_count, global_expert_count
|
||||
|
||||
|
||||
def limit_by_capacity(topk_idx, num_expert, world_size, capacity, group=None):
|
||||
with paddle.no_grad():
|
||||
capacity = (
|
||||
paddle.ones(shape=[num_expert], dtype=paddle.int64) * capacity
|
||||
)
|
||||
pos, lec, gec = count_by_gate(
|
||||
topk_idx, num_expert, world_size, require_pos=False, group=group
|
||||
)
|
||||
new_gec = _limit_by_capacity(gec, capacity, world_size)
|
||||
if world_size > 1:
|
||||
assert group.nranks == world_size
|
||||
new_lec = _alltoall(new_gec, group=group)
|
||||
else:
|
||||
new_lec = new_gec
|
||||
|
||||
topk_idx = _prune_gate_by_capacity(
|
||||
topk_idx, new_lec, num_expert, world_size
|
||||
)
|
||||
|
||||
return new_lec, new_gec, topk_idx
|
||||
@@ -0,0 +1,15 @@
|
||||
# Copyright (c) 2023 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.
|
||||
|
||||
from . import io as io
|
||||
@@ -0,0 +1,16 @@
|
||||
# Copyright (c) 2022 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.
|
||||
|
||||
from .dist_load import load # noqa: F401
|
||||
from .dist_save import save, save_for_auto_inference # noqa: F401
|
||||
@@ -0,0 +1,121 @@
|
||||
# Copyright (c) 2022 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 re
|
||||
|
||||
import paddle
|
||||
import paddle.distributed as dist
|
||||
from paddle.base.framework import dygraph_only
|
||||
from paddle.distributed import fleet
|
||||
|
||||
|
||||
@dygraph_only
|
||||
def load(path, **configs):
|
||||
"""
|
||||
Load an object can be used in paddle from specified path.
|
||||
The file is saved by distributed.save
|
||||
|
||||
Note:
|
||||
The file to load must be saved bu the API paddle.incubate.distributed.utils.io.save
|
||||
|
||||
Args:
|
||||
path(str|BytesIO) : The path/buffer to load the target object. Generally, the path is the target
|
||||
file path. When loading state_dict from the saved result of the API used to save
|
||||
the inference model, the path may be a file prefix or directory.
|
||||
**configs (dict, optional): other load configuration options for compatibility. We do not
|
||||
recommend using these configurations, they may be removed in the future. If not necessary,
|
||||
DO NOT use them. Default None.
|
||||
The following options are currently supported:
|
||||
(1) place: where to place the loaded state dict.
|
||||
If the state dict is too large, the place should be set 'cpu'.
|
||||
Note:
|
||||
Other config value may cause some error.Please don't use any more config options.
|
||||
Returns:
|
||||
Object(Object): a target object can be used in paddle
|
||||
|
||||
Examples:
|
||||
import paddle
|
||||
paddle.distributed.init_process_group(backend='nccl')
|
||||
paddle.distributed.fleet.init(is_collective=True)
|
||||
|
||||
model = build_model()
|
||||
optimizer = build_optimizer(model)
|
||||
|
||||
dist_model = paddle.distributed_optimizer(model)
|
||||
dist_optimizer = paddle.distributed_optimizer(optimizer)
|
||||
|
||||
|
||||
# load model state dict
|
||||
model_state_dict = paddle.incubate.distributed.utils.io.load(path="path/to/load.pdparams")
|
||||
dist_model.set_state_dict(model_state_dict)
|
||||
|
||||
# load optimizer state dict
|
||||
optimizer_state_dict = paddle.incubate.distributed.utils.io.load(path="path/to/load.pdopt")
|
||||
dist_optimizer.set_state_dict(optimizer_state_dict)
|
||||
|
||||
"""
|
||||
if dist.get_world_size() == 1:
|
||||
return paddle.load(path, **configs)
|
||||
|
||||
hcg = fleet.get_hybrid_communicate_group()
|
||||
assert (
|
||||
hcg.get_model_parallel_world_size() == 1
|
||||
and hcg.get_pipe_parallel_world_size() == 1
|
||||
), "Sharding and DP are supported only now"
|
||||
|
||||
# assert (
|
||||
# "place" in configs
|
||||
# ), "the arg place ('cpu' or 'gpu:0', 'gpus:1' ...)must be passed"
|
||||
if "place" not in configs:
|
||||
configs["place"] = "cpu"
|
||||
place = configs["place"]
|
||||
assert isinstance(place, str), (
|
||||
f"configs[place] must be a str, but this is a {type(place)}"
|
||||
)
|
||||
|
||||
assert re.search("^(cpu|gpu:[0-9]*)$", place), (
|
||||
"configs[place] must be cpu, gpu:0, gpu:1 ..."
|
||||
)
|
||||
|
||||
return load_with_place(path, **configs)
|
||||
|
||||
|
||||
def load_with_place(path, **configs):
|
||||
place = configs["place"]
|
||||
if place is None:
|
||||
return paddle.load(path)
|
||||
|
||||
origin_place = paddle.get_device()
|
||||
paddle.set_device(place)
|
||||
|
||||
configs = _remove_not_supported_items(configs)
|
||||
state_dict = paddle.load(path, **configs)
|
||||
|
||||
paddle.set_device(origin_place)
|
||||
|
||||
return state_dict
|
||||
|
||||
|
||||
def _remove_not_supported_items(configs):
|
||||
__supported_by_load__ = [
|
||||
"model_filename",
|
||||
"params_filename",
|
||||
"return_numpy",
|
||||
]
|
||||
_configs = copy.copy(configs)
|
||||
for k in configs.keys():
|
||||
if k not in __supported_by_load__:
|
||||
_configs.pop(k, None)
|
||||
return _configs
|
||||
@@ -0,0 +1,432 @@
|
||||
# Copyright (c) 2022 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.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import re
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, Any, Literal, TypedDict
|
||||
|
||||
import paddle
|
||||
import paddle.distributed as dist
|
||||
from paddle.base.framework import dygraph_only
|
||||
from paddle.distributed import fleet
|
||||
from paddle.distributed.fleet.utils.log_util import logger
|
||||
|
||||
from .save_for_auto import save_for_auto_inference
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
from io import BytesIO
|
||||
|
||||
from typing_extensions import Unpack
|
||||
|
||||
from paddle import Tensor
|
||||
from paddle._typing import NestedStructure
|
||||
from paddle.nn.layer.layers import _StateDict
|
||||
from paddle.static import Program
|
||||
|
||||
class _SaveConfig(TypedDict, total=False):
|
||||
use_binary_format: bool
|
||||
gather_to: int | Sequence[int] | None
|
||||
state_type: Literal['params', 'opt']
|
||||
max_grouped_size: str | int
|
||||
|
||||
|
||||
__all__ = ["save", "save_for_auto_inference"]
|
||||
|
||||
|
||||
@dygraph_only
|
||||
def save(
|
||||
state_dict: dict[str, Any] | _StateDict | NestedStructure[Tensor] | Program,
|
||||
path: str | BytesIO,
|
||||
**configs: Unpack[_SaveConfig],
|
||||
) -> None:
|
||||
'''
|
||||
Save a state dict to the specified path in both distributed and single-card environment.
|
||||
|
||||
Note:
|
||||
Now supports saving ``state_dict`` of Layer/Optimizer, Tensor and nested structure containing Tensor, Program.
|
||||
|
||||
Note:
|
||||
Different from ``paddle.jit.save``, since the save result of ``paddle.save`` is a single file,
|
||||
there is no need to distinguish multiple saved files by adding a suffix. The argument ``path``
|
||||
of ``paddle.save`` will be directly used as the saved file name instead of a prefix.
|
||||
In order to unify the saved file name format, we recommend using the paddle standard suffix:
|
||||
1. for ``Layer.state_dict`` , recommend to use ``.pdparams`` ;
|
||||
2. for ``Optimizer.state_dict`` , recommend to use ``.pdopt`` .
|
||||
For specific examples, please refer to API code examples.
|
||||
|
||||
Args:
|
||||
obj(Object) : The object to be saved.
|
||||
path(str|BytesIO) : The path/buffer of the object to be saved.
|
||||
If saved in the current directory, the input path string will be used as the file name.
|
||||
protocol(int, optional): The protocol version of pickle module must be greater than 1 and less than 5.
|
||||
Default: 4.
|
||||
**configs(dict, optional): optional keyword arguments. The following options are currently supported:
|
||||
|
||||
1. use_binary_format(bool):
|
||||
To be used in paddle.save. When the saved object is static graph variable, you can specify ``use_binary_for_var``.
|
||||
If True, save the file in the c++ binary format when saving a single static graph variable; otherwise, save it in pickle format.
|
||||
Default: False.
|
||||
2. gather_to(int|list|tuple|None):
|
||||
To specify which global rank to save in.Default is None.
|
||||
None value means distributed saving with no gathering to a single card.
|
||||
3. state_type(str):
|
||||
Value can be 'params' or 'opt', specifying to save parameters or optimizer state.
|
||||
4. max_grouped_size(str|int):
|
||||
To limit the max size(how many bits) a object group to be transferred a time.
|
||||
If str, the format must be as num+'G/M/K', for example, 3G, 2K, 10M, etc. Default is 3G.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Examples:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> # doctest: +SKIP('TODO: the error will be fixed in the future')
|
||||
>>> # type: ignore
|
||||
>>> import paddle
|
||||
>>> paddle.distributed.init_process_group(backend='nccl')
|
||||
>>> paddle.distributed.fleet.init(is_collective=True)
|
||||
|
||||
>>> model = build_model()
|
||||
>>> optimizer = build_optimizer(model)
|
||||
|
||||
>>> dist_optimizer = paddle.distributed_optimizer(optimizer)
|
||||
>>> dist_model = paddle.distributed_optimizer(model)
|
||||
|
||||
>>> # gather params to rank 0 and then save
|
||||
>>> paddle.incubate.distributed.utils.io.save(
|
||||
... model.state_dict(), path="path/to/save.pdparams", gather_to=[0], state_type="params"
|
||||
... )
|
||||
|
||||
>>> # save whole params on all ranks
|
||||
>>> paddle.incubate.distributed.utils.io.save(
|
||||
... model.state_dict(), path="path/to/save.pdparams", gather_to=[0, 1], state_type="params"
|
||||
... )
|
||||
|
||||
>>> # save optimizer state dict on rank 0
|
||||
>>> paddle.incubate.distributed.utils.io.save(optimizer.state_dict(), path="path/to/save.pdopt", gather=0, state_type="opt")
|
||||
|
||||
'''
|
||||
|
||||
gather_to = configs.get("gather_to", None)
|
||||
if dist.get_world_size() == 1 or gather_to is None:
|
||||
configs = _remove_not_supported_conf(configs)
|
||||
return paddle.save(state_dict, path, **configs)
|
||||
|
||||
# gather_to is not None and world size > 1
|
||||
state_type = configs.get("state_type", None)
|
||||
assert isinstance(state_type, str), (
|
||||
"must pass an arg state_type='params' or state_type='opt' to specify whether to save model state_dict or optimizer state_dict"
|
||||
)
|
||||
assert state_type in [
|
||||
"params",
|
||||
"opt",
|
||||
], "must pass an arg state_type='params' or state_type='opt'"
|
||||
|
||||
if re.search(f"{state_type}$", path) is None:
|
||||
logger.warning(
|
||||
f"You are saving {state_type}, while the path({path} does not end with {state_type})"
|
||||
)
|
||||
|
||||
hcg = fleet.get_hybrid_communicate_group()
|
||||
assert (
|
||||
hcg.get_model_parallel_world_size() == 1
|
||||
and hcg.get_pipe_parallel_world_size() == 1
|
||||
), (
|
||||
f"Only DP and Sharding is supported now. However, current MP={hcg.get_model_parallel_world_size()} , PP={hcg.get_pipe_parallel_world_size()}"
|
||||
)
|
||||
|
||||
sharding_group = hcg.get_sharding_parallel_group()
|
||||
dp_group = hcg.get_data_parallel_group()
|
||||
|
||||
if state_type == "params":
|
||||
if dp_group.nranks > 1:
|
||||
assert _same_keys(state_dict, dp_group), (
|
||||
"only sharding stage 1/2 and DP are supported now"
|
||||
)
|
||||
if sharding_group.nranks > 1:
|
||||
assert _same_keys(state_dict, sharding_group), (
|
||||
"only sharding stage 1/2 and DP are supported now"
|
||||
)
|
||||
configs = _remove_not_supported_conf(configs)
|
||||
return paddle.save(state_dict, path, **configs)
|
||||
|
||||
# state_type == "opt"
|
||||
if sharding_group.nranks == 1:
|
||||
configs = _remove_not_supported_conf(configs)
|
||||
return paddle.save(state_dict, path, **configs)
|
||||
if _same_keys(state_dict, sharding_group):
|
||||
return paddle.save(state_dict, path, **configs)
|
||||
assert isinstance(gather_to, (list, tuple, int))
|
||||
if isinstance(gather_to, int):
|
||||
gather_to = [gather_to]
|
||||
max_size = configs.get("max_grouped_size", "3G")
|
||||
try:
|
||||
logger.info("state_dict_keys:" + str(state_dict.keys()))
|
||||
gathered_state_dict = _gather_state_dict(
|
||||
state_dict, gather_to, sharding_group, max_size=max_size
|
||||
)
|
||||
logger.info("gathered_state_dict_keys:" + str(state_dict.keys()))
|
||||
if dist.get_rank() in gather_to:
|
||||
configs = _remove_not_supported_conf(configs)
|
||||
paddle.save(gathered_state_dict, path, **configs)
|
||||
except:
|
||||
raise RuntimeError(
|
||||
f'''Saving failed. Following are some suggestions:
|
||||
1) pass the param max_grouped_size to turn the grouped size smaller (current value of max_grouped_size is {max_size})
|
||||
2) if sharding stage is 1, use paddle.save rather than paddle.distributed.save
|
||||
3) Concat the developers
|
||||
'''
|
||||
)
|
||||
|
||||
|
||||
def _state_dict_groups(state_dict, max_size):
|
||||
"""
|
||||
Description:
|
||||
Generator of state dict groups to transfer.the size of each group is less than max_size.
|
||||
"""
|
||||
|
||||
# find the max size of a whole tensor
|
||||
# now we only support to transfer at least one whole tensor
|
||||
max_tensor_size = 0
|
||||
for k, v in state_dict.items():
|
||||
if max_tensor_size < sys.getsizeof(v) + sys.getsizeof(k):
|
||||
max_tensor_size = sys.getsizeof(v) + sys.getsizeof(k)
|
||||
|
||||
max_size = max(max_size, max_tensor_size)
|
||||
logger.debug(f"max tensor size: {max_size}")
|
||||
|
||||
state_group = {}
|
||||
k_list = list(state_dict.keys())
|
||||
index = 0
|
||||
bits = 0
|
||||
|
||||
# generate groups utils the end
|
||||
while index < len(k_list):
|
||||
bsize = sys.getsizeof(state_dict[k_list[index]]) + sys.getsizeof(
|
||||
k_list[index]
|
||||
)
|
||||
if bits + bsize >= max_size:
|
||||
yield state_group
|
||||
state_group = {}
|
||||
bits = 0
|
||||
|
||||
state_group[k_list[index]] = state_dict[k_list[index]]
|
||||
index += 1
|
||||
bits += bsize
|
||||
|
||||
if index == len(k_list) and bits > 0:
|
||||
yield state_group
|
||||
|
||||
|
||||
def all_empty(dict_list):
|
||||
"""
|
||||
Check if all items are empty
|
||||
"""
|
||||
for v in dict_list:
|
||||
if len(v) > 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _parse_mem_size_to_bits(max_size):
|
||||
"""
|
||||
Parse an integer or a mem size str to an integer
|
||||
convert xxxG to xxx * 1024^3
|
||||
convert xxxM to xxx * 1024^2
|
||||
convert xxxK to xxx * 1024^1
|
||||
"""
|
||||
assert isinstance(max_size, (int, str))
|
||||
if isinstance(max_size, str):
|
||||
assert re.search("^[0-9]*[GMK]$", max_size), (
|
||||
f"Wrong max_size 's format, the format ust be like 10K, 9M, 200G , etc, or an integer. However this is {max_size}"
|
||||
)
|
||||
num = int(max_size[:-1])
|
||||
if max_size[-1] == "G":
|
||||
max_size = num * 1024**3
|
||||
elif max_size[-1] == "M":
|
||||
max_size = num * 1024**2
|
||||
else:
|
||||
max_size = num * 1024
|
||||
return max_size
|
||||
|
||||
|
||||
def _gather_state_dict(state_dict, dst, group, max_size="3G"):
|
||||
"""
|
||||
Description:
|
||||
Gather state dicts across all group ranks to dst, Depiring the same elements. including LR_Scheduler.
|
||||
Args:
|
||||
state_dict(dict):
|
||||
local state dict
|
||||
dst(int|list|tuple):
|
||||
ranks the state dicts are gathered to
|
||||
group(ProcessGroup):
|
||||
group across which the state dicts are gathered
|
||||
max_size(int|str):
|
||||
The max limitation of the gathered tensor group size transformed a time. Default is 3G bits.
|
||||
Each rank 's max tensor group before gathering is max_size // group.size
|
||||
Returns:
|
||||
Gathered state dict
|
||||
"""
|
||||
assert isinstance(dst, (list, tuple, int)), (
|
||||
"dst' type must be one of int, list and tuple"
|
||||
)
|
||||
if isinstance(dst, int):
|
||||
dst = [dst]
|
||||
|
||||
max_size = _parse_mem_size_to_bits(max_size)
|
||||
max_size //= dist.get_world_size(group)
|
||||
|
||||
logger.debug("len state_dict: len(state_dict)")
|
||||
|
||||
state_dict_ = copy.copy(state_dict)
|
||||
mw = None
|
||||
has_mw = False
|
||||
has_lr = False
|
||||
|
||||
# Remove master_weights and LR_Scheduler to ensure that all the elements of the state dict are str->Tensor
|
||||
if "master_weights" in state_dict_:
|
||||
mw = state_dict_.pop("master_weights", None)
|
||||
has_mw = True
|
||||
if "LR_Scheduler" in state_dict_:
|
||||
lr = state_dict_.pop("LR_Scheduler", None)
|
||||
has_lr = True
|
||||
|
||||
# Gather optimizer state_dict
|
||||
output = _grouped_gather_data_dict(state_dict_, dst, group, max_size)
|
||||
|
||||
# Gather master_weights if it exists
|
||||
if isinstance(mw, dict):
|
||||
masters = _grouped_gather_data_dict(mw, dst, group, max_size)
|
||||
else:
|
||||
assert mw is None, f"Wrong type of master weights . type: {type(mw)}"
|
||||
|
||||
# assign master_weights and LR_Scheduler
|
||||
# Because LR_Schedulers are same across group, it just needs to be reset
|
||||
if has_mw:
|
||||
output["master_weights"] = masters
|
||||
if has_lr:
|
||||
output["LR_Scheduler"] = lr
|
||||
return output
|
||||
|
||||
|
||||
def _grouped_gather_data_dict(state_data_dict, dst, group, max_size):
|
||||
"""
|
||||
Description:
|
||||
Gather state data dict by groups.
|
||||
Args:
|
||||
state__data_dict(dict):
|
||||
local dict to transfer.The state_data_dict only contains the mapping: str->paddle.Tensor
|
||||
dst(int|list|tuple):
|
||||
ranks the state dicts are gathered to
|
||||
group(ProcessGroup):
|
||||
group across which the state dicts are gathered
|
||||
max_size(int|str):
|
||||
The max limitation of the gathered tensor group size transformed a time. Default is 3G bits.
|
||||
Each rank 's max tensor group before gathering is max_size // group.size
|
||||
Returns:
|
||||
Gathered state_data_dict
|
||||
|
||||
"""
|
||||
numpy_dict = {}
|
||||
logger.debug(f"len state_tict_ : {len(state_data_dict)}")
|
||||
|
||||
for k, v in state_data_dict.items():
|
||||
try:
|
||||
numpy_dict[k] = v.numpy()
|
||||
except:
|
||||
raise TypeError(
|
||||
f"the object (type of {type(v)}) of '{k}' is neither tensor nor parameter"
|
||||
)
|
||||
|
||||
total = 0
|
||||
output_state = {}
|
||||
|
||||
logger.info("start all gather ...")
|
||||
# gather all state_dict by groups
|
||||
for state in _state_dict_groups(numpy_dict, max_size):
|
||||
s_list = []
|
||||
total += len(state)
|
||||
logger.info(f"gen to gather: {total} / {len(numpy_dict)}")
|
||||
dist.all_gather_object(s_list, state, group)
|
||||
if dist.get_rank() in dst:
|
||||
for s in s_list:
|
||||
for k, v in s.items():
|
||||
logger.debug(f"gathered: {k}, {v.shape}")
|
||||
output_state.update(s)
|
||||
|
||||
logger.debug(
|
||||
f"s list size: {sum(len(s) for s in s_list)} output: {len(output_state)}"
|
||||
)
|
||||
|
||||
# Because each size of groups may be different, here we should wait all objects gathered.
|
||||
# The while block breaks until all objects from every rank are empty, which means all of the objects transforming is done.
|
||||
while True:
|
||||
s_list = []
|
||||
state = {}
|
||||
logger.debug("while True")
|
||||
dist.all_gather_object(s_list, state, group)
|
||||
if all_empty(s_list):
|
||||
break
|
||||
if dist.get_rank() in dst:
|
||||
for s in s_list:
|
||||
for k, v in s.items():
|
||||
logger.debug(f"gathered: {k}, {v.shape}")
|
||||
output_state.update(s)
|
||||
logger.debug(
|
||||
f"s list size: {sum(len(s) for s in s_list)} output: {len(output_state)}"
|
||||
)
|
||||
|
||||
logger.debug("all gathered ...")
|
||||
|
||||
if dist.get_rank() in dst:
|
||||
# convert numpy.ndarray to Tensor in cpu place
|
||||
place = paddle.CPUPlace()
|
||||
for k in output_state.keys():
|
||||
output_state[k] = paddle.to_tensor(output_state[k], place=place)
|
||||
output_state[k].name = k
|
||||
return output_state
|
||||
return {}
|
||||
|
||||
|
||||
def _same_keys(state_dict, group):
|
||||
"""
|
||||
Check whether all keys in each dict in the group are the same.
|
||||
Used in sharding strategy to determine whether a dict needs to be gathered.
|
||||
"""
|
||||
keys = list(state_dict.keys())
|
||||
key_list = []
|
||||
logger.info(keys)
|
||||
dist.all_gather_object(key_list, keys, group=group)
|
||||
for k in key_list:
|
||||
if not k == keys:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _remove_not_supported_conf(configs):
|
||||
"""
|
||||
Remove the config values not supported by paddle.save
|
||||
"""
|
||||
__supported_by_save__ = ["use_binary_format"]
|
||||
configs_ = copy.copy(configs)
|
||||
for k in configs.keys():
|
||||
if k not in __supported_by_save__:
|
||||
configs_.pop(k, None)
|
||||
return configs_
|
||||
@@ -0,0 +1,368 @@
|
||||
# Copyright (c) 2022 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.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import os
|
||||
import pickle
|
||||
import re
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
|
||||
import paddle
|
||||
import paddle.distributed as dist
|
||||
from paddle.base.framework import dygraph_only
|
||||
from paddle.distributed import fleet
|
||||
from paddle.distributed.fleet.meta_parallel.sharding.group_sharded_stage3 import (
|
||||
GroupShardedStage3,
|
||||
)
|
||||
from paddle.distributed.fleet.utils.log_util import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from paddle.nn import Layer
|
||||
|
||||
__all__ = ["save_for_auto_inference"]
|
||||
|
||||
|
||||
@dygraph_only
|
||||
def save_for_auto_inference(
|
||||
path_prefix: str, dist_model: Layer, cvt2cpu: bool = False
|
||||
) -> None:
|
||||
"""
|
||||
Description:
|
||||
Save model parameters for auto parallel inference.
|
||||
Supporting dp + mp + pp + sharding(stage1), dp + sharding stage2-3.
|
||||
MoE not supported till MoE is supported in auto parallel mode.
|
||||
|
||||
Args:
|
||||
path_prefix: path prefix to save. If `path_prefix` ends with path separator,
|
||||
the path is processed as a directory and parameters will be saved in it,
|
||||
automatically named saved_parameters. Otherwise, the parameters will be saved with name
|
||||
path_prefix_dist{global_rank}.pdparams and path_prefix_dist{global_rank}.pdattrs.
|
||||
dist_model: model in distributed model.
|
||||
cvt2cpu: whether to move parameters to CPU when using sharding stage 3.
|
||||
The var is invalid if not using sharding stage 3.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Examples:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> # doctest: +SKIP('model not exist')
|
||||
>>> from paddle.incubate.distributed.utils.io import save_for_auto_inference
|
||||
>>> dist_model = build_distributed_model() # type: ignore[name-defined]
|
||||
>>> path_prefix = "path/to/save_infer"
|
||||
>>> save_for_auto_inference(path_prefix, dist_model=dist_model, cvt2cpu=False)
|
||||
|
||||
Outputs:
|
||||
path/to/save_infer_dist0.pdparams path/to/save_infer_dist1.pdparams path/to/save_infer_dist2.pdparams ...
|
||||
path/to/save_infer_dist0.pdattr path/to/save_infer_dist1.pdattr path/to/save_infer_dist2.pdattr ...
|
||||
|
||||
"""
|
||||
|
||||
save_dir, basename_prefix = _get_abs_saved_prefix(path_prefix)
|
||||
|
||||
if isinstance(dist_model, GroupShardedStage3):
|
||||
dist_model.get_all_parameters(cvt2cpu)
|
||||
|
||||
wrapped_dict = _get_wrapped_dist_state_dict(dist_model.state_dict())
|
||||
global_rank = paddle.distributed.get_rank()
|
||||
|
||||
# save parameters
|
||||
paddle.save(
|
||||
wrapped_dict,
|
||||
os.path.join(save_dir, f"{basename_prefix}_dist{global_rank}.pdparams"),
|
||||
)
|
||||
|
||||
# save attributes
|
||||
_save_param_attr(
|
||||
wrapped_dict,
|
||||
os.path.join(save_dir, f"{basename_prefix}_dist{global_rank}.pdattr"),
|
||||
)
|
||||
|
||||
# unset dims mapping after saving attrs
|
||||
for _, dist_param in wrapped_dict.items():
|
||||
_unset_dims_mapping(dist_param)
|
||||
|
||||
|
||||
def _is_first_used(param):
|
||||
return not hasattr(param, "is_firstly_shared") or param.is_firstly_shared
|
||||
|
||||
|
||||
def _get_all_ranks_of_pp(pp_rank, dp_degree, mp_degree, pp_degree):
|
||||
"""
|
||||
Description:
|
||||
get all global ranks involving given pp_rank
|
||||
"""
|
||||
|
||||
process_group = []
|
||||
|
||||
world_size = dp_degree * mp_degree * pp_degree
|
||||
|
||||
for i in range(dp_degree):
|
||||
for k in range(mp_degree):
|
||||
process_group.append(
|
||||
i * world_size // dp_degree
|
||||
+ pp_rank * world_size // dp_degree // pp_degree
|
||||
+ k
|
||||
)
|
||||
return process_group
|
||||
|
||||
|
||||
def _save_param_attr(state_dict_, path, dims_mapping_dict=None):
|
||||
"""
|
||||
Description:
|
||||
save params' attr dict
|
||||
Args:
|
||||
state_dict_:
|
||||
state for which to save attrs, when the state is optimizer state, the master and LRScheduler will be removed.
|
||||
path:
|
||||
path to save
|
||||
dims_mapping_dict:
|
||||
Dims mapping dict, mapping from parameter name in state_dict_ to dims_mapping.
|
||||
If parameter in state_dict_ has attribute 'dims_mapping', the dims_mapping is ignored.
|
||||
If parameter has no attribute 'dims_mapping', the dims mapping must contains the parameter's name.
|
||||
"""
|
||||
state_dict = copy.copy(state_dict_)
|
||||
|
||||
# remove master_weights and LRScheduler, which needs no parameter attributes to save
|
||||
state_dict.pop("master_weights", None)
|
||||
state_dict.pop("LR_Scheduler", None)
|
||||
|
||||
if dims_mapping_dict is not None:
|
||||
assert isinstance(dims_mapping_dict, dict), (
|
||||
"dims_mapping_dict must be an instance of dict"
|
||||
)
|
||||
for k in state_dict.keys():
|
||||
assert k in dims_mapping_dict, (
|
||||
f"param {k} cannot find dims mapping in dims_mapping_dict"
|
||||
)
|
||||
if dist.get_world_size() > 1:
|
||||
hcg = fleet.get_hybrid_communicate_group()
|
||||
dp_degree = hcg.get_data_parallel_world_size()
|
||||
mp_degree = hcg.get_model_parallel_world_size()
|
||||
pp_degree = hcg.get_pipe_parallel_world_size()
|
||||
sharding_degree = hcg.get_sharding_parallel_world_size()
|
||||
dp_degree = dp_degree * sharding_degree
|
||||
|
||||
pp_group = hcg.get_pipe_parallel_group()
|
||||
else:
|
||||
pp_degree = 1
|
||||
dp_degree = 1
|
||||
mp_degree = 1
|
||||
pp_group = None
|
||||
hcg = None
|
||||
|
||||
logger.debug(f"dp degree * sharding degree : {dp_degree}")
|
||||
logger.debug(f"mp degree: {mp_degree}")
|
||||
logger.debug(f"pp degree: {pp_degree}")
|
||||
|
||||
pp_rank = dist.get_rank(pp_group)
|
||||
|
||||
# Why condition 'pp_rank < 0' exists?
|
||||
# Because if pp_degree = 1, pp_rank is set -1
|
||||
pp_rank = max(0, pp_rank)
|
||||
|
||||
if dist.get_world_size() > 1:
|
||||
process_group = _get_all_ranks_of_pp(
|
||||
pp_rank, dp_degree, mp_degree, pp_degree
|
||||
)
|
||||
else:
|
||||
process_group = [0]
|
||||
|
||||
attr_dict = {}
|
||||
for k, v in state_dict.items():
|
||||
dims = len(v.shape)
|
||||
logger.debug(f"shape: , {k}, {dims}")
|
||||
attr_d = {
|
||||
"process_shape": [dp_degree, mp_degree] if hcg else [1],
|
||||
"process_group": process_group,
|
||||
"dims_mapping": (
|
||||
v.dims_mapping
|
||||
if hasattr(v, "dims_mapping")
|
||||
else [-1 for _ in v.shape]
|
||||
),
|
||||
}
|
||||
attr_dict[k] = attr_d
|
||||
|
||||
with open(path, "wb") as f:
|
||||
pickle.dump(attr_dict, f)
|
||||
|
||||
|
||||
def _unset_dims_mapping(param):
|
||||
if hasattr(param, "dims_mapping"):
|
||||
delattr(param, "dims_mapping")
|
||||
|
||||
|
||||
def _get_dims_mapping(dist_parameter, mp_group):
|
||||
"""
|
||||
Description:
|
||||
return the splitting mapping:
|
||||
{tensor_name: spiting_strategy}
|
||||
Args:
|
||||
dist_parameters(list): distributed model parameters
|
||||
mp_group(ProcessGroup): Model Parallel communication group
|
||||
Return:
|
||||
The splitting mapping
|
||||
Examples:
|
||||
splitting_strategy's format (-1, -1, -1, 0), meaning the dims
|
||||
of the tensor is 4 and it is splited along the first strategy axis in mesh
|
||||
|
||||
Mesh Examples: (2, 4) means dp=2, mp=4
|
||||
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
dist_shape = np.array(dist_parameter.shape)
|
||||
if hasattr(dist_parameter, "split_axis"):
|
||||
axis = dist_parameter.split_axis
|
||||
mapping = [-1 for _ in dist_shape]
|
||||
mapping[axis] = 1
|
||||
logger.debug(
|
||||
f"{dist_parameter.name} has attr split_axis: mapping: {mapping}"
|
||||
)
|
||||
else:
|
||||
mapping = [-1 for _ in dist_shape]
|
||||
logger.debug(f"normal parameter: {dist_parameter.name}")
|
||||
return mapping
|
||||
|
||||
|
||||
def _get_abs_saved_prefix(path_prefix):
|
||||
"""
|
||||
Description:
|
||||
Get absolute dir path and basename prefix of path_prefix, with making path_prefix's directories.
|
||||
If path_prefix is a directory name, basename is set 'saved_parameters'.
|
||||
If path_prefix is a file name, basename is extracted from path_prefix.
|
||||
Args:
|
||||
path_prefix: str
|
||||
Return:
|
||||
(dirpath: str, basename: str)
|
||||
"""
|
||||
abs_prefix = os.path.abspath(path_prefix)
|
||||
if abs_prefix[-1] == os.path.sep:
|
||||
save_dir = abs_prefix
|
||||
basename_prefix = "saved_parameters"
|
||||
else:
|
||||
save_dir = os.path.dirname(abs_prefix)
|
||||
basename_prefix = os.path.basename(abs_prefix)
|
||||
os.makedirs(save_dir, exist_ok=True)
|
||||
return save_dir, basename_prefix
|
||||
|
||||
|
||||
def _name_mapping_dist2single(state_dict, pp_group):
|
||||
key_list = []
|
||||
param_keys = [
|
||||
v.name
|
||||
for _, v in state_dict.items()
|
||||
if isinstance(v, paddle.Tensor) and _is_first_used(v)
|
||||
]
|
||||
|
||||
if pp_group.nranks == 1:
|
||||
return {k: k for k in param_keys}
|
||||
|
||||
dist.all_gather_object(key_list, param_keys, pp_group)
|
||||
|
||||
# find how many a op in a each pp:
|
||||
# {"linear:"[0, 2,0,1,1,...]}
|
||||
param_types = {}
|
||||
|
||||
matcher = re.compile(r"^\w+_\d+(?=\.)")
|
||||
|
||||
for pp, keys in enumerate(key_list):
|
||||
param_type_idx = {}
|
||||
for k in keys:
|
||||
matched = matcher.search(k)
|
||||
logger.debug(f"matched: {k}: {matched}")
|
||||
assert matched is not None, (
|
||||
f"the name of param, '{k}', is not satisfied the format 'name_idx.xxx'"
|
||||
)
|
||||
name_idx = k[matched.start() : matched.end()]
|
||||
logger.debug(f"get param_type_idx: {name_idx}")
|
||||
|
||||
if name_idx in param_type_idx:
|
||||
continue
|
||||
|
||||
name = "_".join(name_idx.split("_")[:-1])
|
||||
idx = int(name_idx.split("_")[-1])
|
||||
param_type_idx.update({name_idx: (name, idx)})
|
||||
if name not in param_types:
|
||||
param_types[name] = [0] * pp_group.nranks
|
||||
param_types[name][pp] += 1
|
||||
|
||||
# check if continuous
|
||||
types_idx = {}
|
||||
for _, v in param_type_idx.items():
|
||||
if v[0] not in types_idx:
|
||||
types_idx.update({v[0]: [v[1]]})
|
||||
else:
|
||||
types_idx[v[0]].append(v[1])
|
||||
for k, v in types_idx.items():
|
||||
assert v == list(range(v[0], v[-1] + 1)), (
|
||||
f"{k} is not continuous: {v}"
|
||||
)
|
||||
|
||||
logger.debug(f"param type: {param_types}")
|
||||
|
||||
# analyse starting index
|
||||
for k in param_types.keys():
|
||||
param_types[k] = np.cumsum([0, *param_types[k][:-1]])
|
||||
|
||||
logger.debug(f"params type: {param_types}")
|
||||
|
||||
name_mapping = {}
|
||||
pp_rank = dist.get_rank(pp_group)
|
||||
for k in key_list[pp_rank]:
|
||||
matched = matcher.search(k)
|
||||
name_idx = k[matched.start() : matched.end()]
|
||||
name = "_".join(name_idx.split("_")[:-1])
|
||||
idx = int(name_idx.split("_")[-1])
|
||||
logger.debug(f"idx: {idx}")
|
||||
|
||||
new_idx = param_types[name][pp_rank] + idx
|
||||
logger.debug(f"new idx: {new_idx}")
|
||||
new_name_idx = name + "_" + str(new_idx)
|
||||
name_mapping[k] = new_name_idx + k[matched.end() :]
|
||||
|
||||
return name_mapping
|
||||
|
||||
|
||||
def _get_wrapped_dist_state_dict(dist_state_dict):
|
||||
wrapped_state_dict = {}
|
||||
if dist.get_world_size() <= 1:
|
||||
for _, v in dist_state_dict.items():
|
||||
wrapped_state_dict[v.name] = v
|
||||
return wrapped_state_dict
|
||||
|
||||
hcg = fleet.get_hybrid_communicate_group()
|
||||
|
||||
pp_group = hcg.get_pipe_parallel_group()
|
||||
mp_group = hcg.get_model_parallel_group()
|
||||
logger.debug("execute _name_mapping_dist2single")
|
||||
|
||||
name_mapping = _name_mapping_dist2single(dist_state_dict, pp_group)
|
||||
for _, v in dist_state_dict.items():
|
||||
if not _is_first_used(v):
|
||||
logger.debug(f"not first used : {v.name}")
|
||||
continue
|
||||
wrapped_state_dict[name_mapping[v.name]] = v
|
||||
v.dims_mapping = _get_dims_mapping(v, mp_group)
|
||||
logger.debug(
|
||||
f"saving param: {v.name} -> {name_mapping[v.name]} shape: {v.shape}"
|
||||
)
|
||||
return wrapped_state_dict
|
||||
Reference in New Issue
Block a user