chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:40:42 +08:00
commit e25996e7db
15472 changed files with 3536181 additions and 0 deletions
@@ -0,0 +1,153 @@
# 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.
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from paddle.distributed import fleet
from . import ( # noqa: F401
hybrid_parallel_util,
log_util,
mix_precision_utils,
sequence_parallel_utils,
tensor_parallel_utils,
)
from .fs import HDFSClient, LocalFS
from .ps_util import DistributedInfer
if TYPE_CHECKING:
from collections.abc import Callable
from paddle.nn import Layer
__all__ = ["LocalFS", "recompute", "DistributedInfer", "HDFSClient"]
def recompute(
function: Layer | Callable[..., Any], *args: Any, **kwargs: Any
) -> Any:
"""
recompute intermediate activations to save the memory.
Parameters:
function(paddle.nn.Layer): layer of sequence of layers that describes part of forward pass of the model
whose intermediate activations will be released to save memory in forward stage and will be recomputed
in backward stage for gradient calculation.
*args(Tensor): inputs to the function.
**kwargs(Dict): Kwargs should only contain two kinds of key-value params, the one is part of function's key-value params,
and the other contains ``preserve_rng_state`` and ``use_reentrant``. the key-value pair of ``preserve_rng_state``,
which is used to indicate whether to save the forward rng. If it is True, then the last forward rng value
will be restored when the forward recalculation of backpropagation is performed, its default value is True.
the key-value pair of ``use_reentrant`` is used to indicate which implementation of recompute you will be used.
``use_reentrant=True`` means to use the PyLayer implementation of recompute, ``use_reentrant=False`` means to
use the Hook implementation of recompute, its default value is True.
Returns:
Output of function on args.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env:DISTRIBUTED, env:GPU)
>>> import paddle
>>> from paddle.distributed.fleet.utils import recompute
>>> import random
>>> paddle.seed(2023)
>>> def get_fc_block(block_idx, input_size, is_last=False):
... block_name = "block_" + str(block_idx)
... block = paddle.nn.Sequential(
... (block_name + "_fc_0", paddle.nn.Linear(input_size, input_size, bias_attr=False)),
... (block_name + "_dropout", paddle.nn.Dropout(p=0.5)),
... (block_name + "_relu_1", paddle.nn.ReLU()),
... (block_name + "_fc_1", paddle.nn.Linear(input_size, input_size, bias_attr=False)),
... (block_name + "_relu_2", paddle.nn.ReLU()),
... )
... if is_last:
... block.add_sublayer(
... block_name + "_fc_2",
... paddle.nn.Linear(input_size, 1, bias_attr=False),
... )
... else:
... block.add_sublayer(
... block_name + "_fc_2",
... paddle.nn.Linear(input_size, input_size, bias_attr=False),
... )
... return block
>>> class Naive_fc_net(paddle.nn.Layer):
... def __init__(
... self,
... input_size=10,
... recompute_blocks=[1, 3],
... recompute_kwargs={},
... ):
... super().__init__()
... self.recompute_blocks = recompute_blocks
... self.recompute_kwargs = recompute_kwargs
... self.runfunc0 = get_fc_block(0, input_size, is_last=False)
... self.runfunc1 = get_fc_block(1, input_size, is_last=False)
... self.runfunc2 = get_fc_block(2, input_size, is_last=False)
... self.runfunc3 = get_fc_block(3, input_size, is_last=False)
... self.runfunc4 = get_fc_block(4, input_size, is_last=True)
... self.total_func = [self.runfunc0, self.runfunc1, self.runfunc2, self.runfunc3, self.runfunc4]
...
... def forward(self, inputs):
... nums = len(self.total_func)
... for i in range(nums):
... if i in self.recompute_blocks:
... inputs = recompute(self.total_func[i], inputs, **{"preserve_rng_state": True})
... else:
... inputs = self.total_func[i](inputs)
... return inputs
>>> def run_model(cuda_state, recompute_block=[], recompute_kwargs={}):
... gen = paddle.seed(10)
... gen.manual_seed(10)
... random.seed(10)
... if cuda_state:
... paddle.set_cuda_rng_state(cuda_state)
... batch_size, input_size = 1, 10
... model = Naive_fc_net(
... input_size,
... recompute_blocks=recompute_block,
... recompute_kwargs=recompute_kwargs,
... )
... optimizer = paddle.optimizer.SGD(learning_rate=0.01, parameters=model.parameters())
... loss_ = []
... param_ = []
... grad_ = []
... for _ in range(5):
... x = paddle.rand(shape=[batch_size, input_size], dtype="float32")
... y_pred = model(x)
... loss = y_pred.mean()
... loss_.append(loss.item())
... loss.backward()
... optimizer.step()
... param_.append(model.parameters()[9])
... grad_.append(model.parameters()[3]._grad_ivar())
... optimizer.clear_grad()
... return loss_, param_, grad_
>>> cuda_state = paddle.get_cuda_rng_state()
>>> # without recompute
>>> loss_ref, param_ref, grad_ref = run_model(cuda_state, recompute_block=[])
>>> loss, param, grad = run_model(cuda_state, recompute_block=[1, 2])
>>> print("normal_loss: {}, recompute_loss: {}".format(loss_ref, loss))
>>> # The result of the recompute_loss should be the same as the normal_loss.
normal_loss: [0.0018744759727269411, 0.0, 0.035971127450466156, 0.0, 0.0], recompute_loss: [0.0018744759727269411, 0.0, 0.035971127450466156, 0.0, 0.0]
"""
return fleet.recompute.recompute(function, *args, **kwargs)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,190 @@
# 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.
"""Http Server."""
import http.server as SimpleHTTPServer
import logging
import threading
from http.server import HTTPServer
__all__ = []
def get_logger(name, level, fmt):
logger = logging.getLogger(name)
logger.setLevel(level)
handler = logging.FileHandler('http.log', mode='w')
formatter = logging.Formatter(fmt=fmt)
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.propagate = False
return logger
_http_server_logger = get_logger(
__name__, logging.INFO, fmt='%(asctime)s-%(levelname)s: %(message)s'
)
class KVHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
"""
kv handler class for kv http server,
it defines the way to get/set kv in server.
"""
def do_GET(self):
"""
get method for kv handler, get value according to key.
"""
log_str = "GET " + self.address_string() + self.path
paths = self.path.split('/')
if len(paths) < 3:
print('len of request path must be 3: ' + self.path)
self.send_status_code(400)
return
_, scope, key = paths
with self.server.kv_lock:
value = self.server.kv.get(scope, {}).get(key)
if value is None:
log_str += ' , key not found: ' + key
self.send_status_code(404)
else:
log_str += ' , key found: ' + key
self.send_response(200)
self.send_header("Content-Length", str(len(value)))
self.end_headers()
self.wfile.write(value)
_http_server_logger.info(log_str)
def do_PUT(self):
"""
put method for kv handler, set value according to key.
"""
log_str = "PUT " + self.address_string() + self.path
paths = self.path.split('/')
if len(paths) < 3:
print('len of request path must be 3: ' + self.path)
self.send_status_code(400)
return
_, scope, key = paths
content_length = int(self.headers['Content-Length'])
try:
value = self.rfile.read(content_length)
except:
print("receive error invalid request")
self.send_status_code(404)
return
with self.server.kv_lock:
if self.server.kv.get(scope) is None:
self.server.kv[scope] = {}
self.server.kv[scope][key] = value
self.send_status_code(200)
_http_server_logger.info(log_str)
def do_DELETE(self):
"""
delete method for kv handler, set value according to key.
"""
log_str = "DELETE " + self.address_string() + self.path
paths = self.path.split('/')
if len(paths) < 3:
print('len of request path must be 3: ' + self.path)
self.send_status_code(400)
return
_, scope, key = paths
with self.server.delete_kv_lock:
if self.server.delete_kv.get(scope) is None:
self.server.delete_kv[scope] = set()
self.server.delete_kv[scope].add(key)
self.send_status_code(200)
_http_server_logger.info(log_str)
def log_message(self, format, *args):
"""
ignore all logging messages in kv handler.
"""
pass
def send_status_code(self, code):
"""
send status code back to client.
"""
self.send_response(code)
self.send_header("Content-Length", 0)
self.end_headers()
class KVHTTPServer(HTTPServer):
"""
it is a http server storing kv pairs.
"""
def __init__(self, port, handler):
"""Init."""
super().__init__(('', port), handler)
self.delete_kv_lock = threading.Lock()
self.delete_kv = {}
self.kv_lock = threading.Lock()
self.kv = {}
def get_deleted_size(self, key):
"""
get deleted size in key.
"""
ret = 0
with self.delete_kv_lock:
ret = len(self.delete_kv.get(key, set()))
return ret
class KVServer:
"""
it is a server storing kv pairs, has a http server inside.
"""
def __init__(self, port, size={}):
"""Init."""
self.http_server = KVHTTPServer(port, KVHandler)
self.listen_thread = None
self.size = size
def start(self):
"""
start server until user calls stop to let it quit.
"""
self.listen_thread = threading.Thread(
target=lambda: self.http_server.serve_forever()
)
self.listen_thread.start()
def stop(self):
"""
stop server and clear its resources.
"""
self.http_server.shutdown()
self.listen_thread.join()
self.http_server.server_close()
def should_stop(self):
"""
return whether the server should stop.
Returns:
ret(bool): whether the server should stop
"""
for key in self.size:
s = self.http_server.get_deleted_size(key)
if s != self.size.get(key, 0):
return False
return True
@@ -0,0 +1,842 @@
# 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.
from collections import defaultdict
import numpy as np
# (TODO: GhostScreaming) It will be removed later.
from paddle.base import core
from paddle.distributed import fleet
from paddle.framework import Block, Program, in_dynamic_mode
class HybridParallelInferenceHelper:
"""
A helper class to split program for inference with hybrid parallelism.
Args:
startup_program (Program): the startup program.
main_program (Program): the main program.
num_mp (int): number of model parallel degree. Default ``1``.
num_pp (int): number of pipeline parallel degree. Default ``1``.
micro_batch_size (int): number of micro batch size. Default ``1``.
beam_size (int): number of beam search size. Default ``1``.
init_comm (bool): whether if initialize communication group. Default ``True``.
role_maker (RoleMakerBase or subclass): user custom define RoleMakerBase.
If ``role_maker==None``, then use PaddleCloudRoleMaker. Default ``None``.
Returns:
None.
Write Paradigm:
.. code-block:: text
:name: text-example1
>>> # doctest: +REQUIRES(env:DISTRIBUTED, env:GPU)
>>> import paddle
>>> # while op pattern
>>> with paddle.base.device_guard(f'{device}:all'):
... # init global cond
... max_len = paddle.full(shape=[1], dtype="int64", fill_value=10)
... step_idx = paddle.full(shape=[1], dtype="int64", fill_value=0)
... cond_int = paddle.full(shape=[1], dtype="int64", fill_value=0, name="cond_int")
... cond = layers.cast(step_idx < max_len, dtype="bool")
... while_op = layers.While(cond, is_test=True)
... # init global lod_tensor_array for generation task
... arr = paddle.tensor.array_write(data, step_idx)
>>> with while_op.block():
... with paddle.base.device_guard(f'{device}:all'):
... # read data from global lod_tensor_array
... element_in_arr = paddle.tensor.array_read(array=arr, i=step_idx)
... # write placeholder data to global lod_tensor_array,
... # it need for send_v2 of lod_tensor_array
... paddle.increment(x=step_idx, value=1.0)
... paddle.tensor.array_write(element_in_arr, i=step_idx, array=arr)
... with paddle.base.device_guard(f'{device}:0'):
... pass # some code
... with paddle.base.device_guard(f'{device}:1'):
... pass # some code
... with paddle.base.device_guard(f'{device}:{num_pp - 1}'):
... # generate some data in while block and write to global lod_tensor_array
... # that they are read in next while step.
... # we will using send_v2 to send global lod_tensor_array to other pipeline and sync
... paddle.tensor.array_write(other_var, i=step_idx, array=arr)
... # update cond and assign to cond_int, we will sync cond_int
... layers.assign(layers.cast(cond, dtype="int32"), cond_int)
... with paddle.base.device_guard(f'{model._device}:all'):
... # the code below must at end of while block and exists in device:all
... layers.assign(layers.cast(cond_int, dtype='bool'), cond)
>>> with paddle.base.device_guard(f'{model._device}:all'):
... # use a empty lod_tensor_array to clear lod_tensor_array
... layers.assign(layers.create_array(data.dtype), arr)
Examples:
.. code-block:: pycon
:name: code-example1
>>> # doctest: +REQUIRES(env:DISTRIBUTED, env:GPU)
>>> import os
>>> import numpy as np
>>> import paddle
>>> import paddle.distributed.fleet as fleet
>>> from paddle.distributed.fleet.utils import hybrid_parallel_inference
>>> paddle.enable_static()
>>> nranks = int(os.getenv("PADDLE_TRAINERS_NUM", 1))
>>> rank = int(os.getenv("PADDLE_TRAINER_ID", 0))
>>> dev_id = int(os.getenv("FLAGS_selected_gpus", 0))
>>> main_program = paddle.static.Program()
>>> startup_program = paddle.static.Program()
>>> if nranks > 1:
... dist_strategy = fleet.DistributedStrategy()
... dist_strategy.without_graph_optimization = True
... fleet.init(is_collective=True, strategy=dist_strategy)
>>> device = "gpu"
>>> with paddle.static.program_guard(main_program, startup_program):
... with paddle.base.device_guard(f'{device}:0'):
... X = paddle.static.data(name='X', shape=[None, 2], dtype='float32')
... with paddle.base.device_guard(f'{device}:all'):
... max_len = paddle.full(shape=[1], dtype="int64", fill_value=5, name="n")
... step_idx = paddle.full(shape=[1], dtype="int64", fill_value=0, name="i")
... data = paddle.tensor.array_write(X, step_idx)
... cond_int = paddle.full(shape=[1], dtype="int64", fill_value=0, name="cond_int")
... cond = paddle.less_than(x=step_idx, y=max_len)
... while_op = paddle.static.nn.control_flow.While(cond, is_test=True)
... with while_op.block():
... with paddle.base.device_guard(f'{device}:all'):
... input = paddle.tensor.array_read(array=data, i=step_idx)
... paddle.increment(x=step_idx, value=1.0)
... paddle.tensor.array_write(input, i=step_idx, array=data)
... with paddle.base.device_guard(f'{device}:0'):
... param_attr = paddle.ParamAttr(initializer=paddle.nn.initializer.Constant(1.0))
... weight1 = paddle.static.create_parameter(shape=[2, 5], dtype='float32', attr=param_attr, is_bias=False)
... hidden1 = paddle.matmul(input, weight1)
... with paddle.base.device_guard(f'{device}:1'):
... param_attr = paddle.ParamAttr(initializer=paddle.nn.initializer.Constant(2.0))
... weight2 = paddle.static.create_parameter(shape=[5, 2], dtype='float32', attr=param_attr, is_bias=False)
... hidden2 = paddle.matmul(hidden1, weight2)
... paddle.tensor.array_write(hidden2, i=step_idx, array=data)
... # update cond and assign to cond_int, we will sync cond_int
... paddle.assign(paddle.less_than(x=step_idx, y=max_len), cond)
... paddle.assign(paddle.cast(cond, dtype="int32"), cond_int)
... with paddle.base.device_guard(f'{device}:all'):
... # the code below must at end of while block and exists in device:all
... paddle.assign(paddle.cast(cond_int, dtype='bool'), cond)
... with paddle.base.device_guard(f'{device}:all'):
... out = paddle.tensor.create_array(data.dtype)
... paddle.assign(data, out)
... with paddle.base.device_guard(f'{device}:all'):
... # use a empty lod_tensor_array to clear lod_tensor_array
... paddle.assign(paddle.tensor.create_array(data.dtype), data)
>>> helper = hybrid_parallel_inference.HybridParallelInferenceHelper(
... startup_program,
... main_program,
... micro_batch_size=2,
... num_pp=2,
... init_comm=nranks > 1,
... )
>>> helper.gen_infer_program(['array_write_0.out'], ['cond_int.tmp_0'])
>>> exe = paddle.static.Executor(paddle.CUDAPlace(dev_id))
>>> exe.run(startup_program)
>>> np.random.seed(2333)
>>> for step in range(5):
... init_data = np.random.uniform(low=0.0, high=1.0, size=[2, 2]).astype('float32')
... [res] = exe.run(main_program, feed={"X": init_data}, fetch_list=[out])
... print('-------- step', step, ' --------')
... print(res)
"""
def __init__(
self,
startup_program,
main_program,
num_mp=1,
num_pp=1,
micro_batch_size=1,
beam_size=1,
init_comm=True,
role_maker=None,
):
assert isinstance(startup_program, Program)
assert isinstance(main_program, Program)
self._device = None
if core.is_compiled_with_cuda():
self._device = "gpu"
assert self._device, "Only gpu are supported."
assert not in_dynamic_mode(), "Only static graph mode is supported."
op_maker = core.op_proto_and_checker_maker
self._op_role = op_maker.OpRole
self._op_role_key = op_maker.kOpRoleAttrName()
self._op_device_key = op_maker.kOpDeviceAttrName()
self._param_device_map = {}
self._pipeline_pair = []
self._pipeline_pair_in_while = []
self._pp_ring_map = {}
self.ring_id = 20 # Just a magic number
self.micro_batch_size = micro_batch_size
self.beam_size = beam_size
self.init_comm = init_comm
self._output_var_to_op = None
self._input_var_to_op = None
self._main_program = main_program
self._startup_program = startup_program
if role_maker is None:
self.role_maker = fleet.base.role_maker.PaddleCloudRoleMaker(
is_collective=True
)
else:
if isinstance(role_maker, fleet.base.role_maker.RoleMakerBase):
assert role_maker._is_collective
self.role_maker = role_maker
# communication_group info
self.mp_ring_id = 0
self.global_ring_id = 1
self.endpoints = self.role_maker._get_trainer_endpoints()
self.current_endpoint = self.endpoints[self.role_maker._worker_index()]
self.rank = self.role_maker._worker_index()
self.nranks = self.role_maker._worker_num()
assert num_mp * num_pp == self.nranks
self.num_pp = num_pp
self.num_mp = num_mp
# global ring info
self.global_endpoints = self.endpoints
self.global_rank = self.rank
self.global_nranks = self.nranks
arr = np.arange(0, self.num_pp * self.num_mp).reshape(
[self.num_pp, self.num_mp]
)
ipp, imp = np.where(arr == self.rank)
ipp = ipp[0]
imp = imp[0]
self.mp_group = arr[ipp, :]
self.pp_group = arr[:, imp]
self._stage = ipp
def _init_communication_group(self):
dev_ids = []
for pair in self._pipeline_pair:
prev_id, cur_id = pair
if prev_id not in dev_ids:
dev_ids.append(prev_id)
if cur_id not in dev_ids:
dev_ids.append(cur_id)
num_pp = len(dev_ids)
num_pp = max(1, num_pp)
assert num_pp == self.num_pp, (
f'num_pp: {num_pp}, self.num_pp: {self.num_pp}'
)
collective_helper = fleet.meta_optimizers.common.CollectiveHelper(
self.role_maker, wait_port=False
)
# Create global rings
collective_helper._init_communicator(
self._startup_program,
self.current_endpoint,
self.global_endpoints,
self.global_rank,
self.global_ring_id,
True,
self.global_ring_id,
True,
)
# Create mp rings
if self.num_mp > 1:
mp_endpoints = [self.endpoints[mp_idx] for mp_idx in self.mp_group]
mp_rank = next(
idx
for idx, mp_idx in enumerate(self.mp_group)
if mp_idx == self.rank
)
collective_helper._init_communicator(
self._startup_program,
self.current_endpoint,
mp_endpoints,
mp_rank,
self.mp_ring_id,
True,
self.global_ring_id,
True,
)
# Create pipeline rings
if self.num_pp > 1:
for pair in self._pipeline_pair:
pair_key = pair[0] * 1000 + pair[1]
ring_id = self._pp_ring_map[pair_key]
first_node = self.pp_group[pair[0]]
second_node = self.pp_group[pair[1]]
if self.rank != first_node and self.rank != second_node:
collective_helper._init_communicator(
self._startup_program,
None,
None,
None,
None,
False,
self.global_ring_id,
True,
)
continue
pipeline_endpoints = [
self.endpoints[first_node],
self.endpoints[second_node],
]
pipeline_rank = 0 if self.rank == first_node else 1
collective_helper._init_communicator(
self._startup_program,
self.current_endpoint,
pipeline_endpoints,
pipeline_rank,
ring_id,
False,
self.global_ring_id,
True,
)
def _get_input_output_info(self, block):
'''
Get info of op input and output.
'''
# A map from output var to op which generate it.
output_var_to_op = defaultdict(list)
# A map from var to op which takes it as input.
input_var_to_op = defaultdict(list)
for index, op in enumerate(block.ops):
for var_name in op.input_arg_names:
input_var_to_op[var_name].append([op, index])
for var_name in op.output_arg_names:
output_var_to_op[var_name].append([op, index])
return output_var_to_op, input_var_to_op
def _update_param_device_map(self):
"""
Get the device info for parameters.
"""
params = [param.name for param in self._main_program.all_parameters()]
for each_block in self._main_program.blocks:
for op in each_block.ops:
for var_name in op.input_arg_names:
if (
var_name not in params
or var_name in self._param_device_map
):
continue
device = op.attr(self._op_device_key)
self._param_device_map[var_name] = device
def _split_program(self, program, stage, block_idx):
"""
Split a program and get the one with the given pipeline stage.
Args:
stage (int): pipeline stage
block_idx (int): block index
Returns:
used_var_names (set): used var names in block_idx block
"""
used_var_names = set()
block = program.block(block_idx)
op_idx = 0
for op in list(block.ops):
op_stage = op.attr(self._op_device_key).split(':')[1]
# Copy ops whose op_device set to "gpu:all" to all sections.
if op_stage == "all" or int(op_stage) == stage:
op_idx += 1
if op.type == "while":
sub_block_id = int(op.attr('sub_block').id)
sub_used_var_names = self._split_program(
program, stage, sub_block_id
)
used_var_names.update(sub_used_var_names)
input_idxs = []
input_arg_names = op.input("X")
for i, name in enumerate(input_arg_names):
if name not in sub_used_var_names:
input_idxs.append(i)
if len(input_idxs) > 0:
for i in reversed(input_idxs):
input_arg_names.pop(i)
op.desc.set_input("X", input_arg_names)
output_idxs = []
output_arg_names = op.output("Out")
for i, name in enumerate(output_arg_names):
if name not in sub_used_var_names:
output_idxs.append(i)
if len(output_idxs) > 0:
for i in reversed(output_idxs):
output_arg_names.pop(i)
op.desc.set_output("Out", output_arg_names)
for var_name in op.input_arg_names + op.output_arg_names:
used_var_names.add(var_name)
else:
block._remove_op(op_idx)
for var_name in list(block.vars.keys()):
if var_name not in used_var_names:
block._remove_var(var_name)
return used_var_names
# def _find_post_op(self, index, var_name):
# """
# Find the post op that has variable named var_name as input.
# """
# # bugfix for uniform hybrid parallelism
# if '.cast_fp32' in var_name:
# var_name = var_name.replace('.cast_fp32', '')
# if '.cast_fp16' in var_name:
# var_name = var_name.replace('.cast_fp16', '')
# post_ops = self._input_var_to_op[var_name]
# if post_ops == None: return None
# result_op = None
# for post_op, post_idx in reversed(post_ops):
# if post_idx > index:
# result_op = post_op
# break
# return result_op
def _find_prev_op(self, index, var_name):
"""
Find the previous op of op with index that outputs
variable named var_name.
"""
prev_ops = self._output_var_to_op[var_name]
if prev_ops is None:
return None
result_op = None
for prev_op, prev_idx in reversed(prev_ops):
if prev_idx < index:
result_op = prev_op
break
return result_op
def _add_op_device_attr(self, block):
"""
Add op_device attribute for ops in block that have
not that attribute set.
Args:
block (Block): the block to process.
"""
assert isinstance(block, Block)
# Ops should be copied to all pipeline stages.
device_all_ops = [
"create_py_reader",
"read",
"create_double_buffer_reader",
"while",
]
for op in block.ops:
if op.type in device_all_ops:
# We use "gpu:all" to represent an op should be put on all
# pipeline stages, such as read ops. Note that: "gpu:all"
# is only used by pipeline as an indicator.
op._set_attr(self._op_device_key, self._device + ":all")
if op.type == "while":
sub_block_id = op.attr('sub_block').id
sub_block = block.program.block(sub_block_id)
self._add_op_device_attr(sub_block)
def _check_validation(self, block):
"""
Check whether ops in a block have both the op_device and the
op_role attributes set.
"""
assert isinstance(block, Block)
pre_stage_id = None
for op in block.ops:
assert op.has_attr(self._op_role_key), (
f"{op.type} has no {self._op_role_key} set ."
)
op_role = op.attr(self._op_role_key)
assert op_role == int(self._op_role.Forward), (
"Only forward is supported for inference."
)
if not op._has_kernel(op.type):
assert op.type in [
"while",
"conditional_block",
], "The only supported op without kernel is while."
sub_block_id = op.attr('sub_block').id
sub_block = block.program.block(sub_block_id)
self._check_validation(sub_block)
assert op.has_attr(self._op_device_key), (
f"{op.type} has no {self._op_device_key} set."
)
device = op.attr(self._op_device_key)
assert device, f"{op.type} has no {self._op_device_key} set."
if device.split(':')[1] == "all":
continue
dev_type = device.split(':')[0]
assert dev_type == self._device
stage_id = int(device.split(':')[1])
pre_stage_id = stage_id
def _insert_sendrecv_ops_for_boundaries(self, block, is_while_block):
"""
Insert a pair of send and recv ops for every two
consecutive ops on different devices.
"""
# A map from var to device where op takes it as input,
# avoiding multiple send and recv ops.
input_var_to_device = {}
extra_index_info = {
'index': 0,
}
for index, op in enumerate(list(block.ops)):
cur_device = op.attr(self._op_device_key)
if cur_device.split(':')[-1] == "all":
continue
for var_name in op.input_arg_names:
if not block.has_var(var_name) and block._find_var_recursive(
var_name
):
continue
var = block.var(var_name)
# skip data var
if var.is_data:
continue
prev_device = None
generate_ops = self._output_var_to_op.get(var_name)
if generate_ops is None:
if var_name not in self._param_device_map:
continue
prev_device = self._param_device_map[var_name]
prev_op = self._find_prev_op(index, var_name)
if not prev_device:
prev_device = (
prev_op.attr(self._op_device_key) if prev_op else None
)
if prev_device is None or prev_device.split(":")[-1] == "all":
continue
if prev_device == cur_device:
continue
if var_name not in input_var_to_device:
input_var_to_device[var_name] = []
if (cur_device, prev_device) in input_var_to_device[var_name]:
continue
assert self._device == cur_device.split(':')[0], (
"More than one device type found."
)
device_type = cur_device.split(':')[0] + ':'
def _insert_send_recv(cur_id, prev_id):
assert cur_id > prev_id
cur_dev = device_type + str(cur_id)
prev_dev = device_type + str(prev_id)
if (cur_dev, prev_dev) in input_var_to_device[var_name]:
return
if cur_id - prev_id > 1:
_insert_send_recv(cur_id - 1, prev_id)
_insert_send_recv(cur_id, cur_id - 1)
input_var_to_device[var_name].append(
(cur_dev, prev_dev)
)
return
assert cur_id - prev_id == 1
input_var_to_device[var_name].append((cur_dev, prev_dev))
op_role = op.attr(self._op_role_key)
var = block.vars[var_name]
pair = (prev_id, cur_id)
if (
is_while_block
and pair not in self._pipeline_pair_in_while
):
self._pipeline_pair_in_while.append(pair)
# 1000 is just a magic number
pair_key = prev_id * 1000 + cur_id
if pair not in self._pipeline_pair:
self._pipeline_pair.append(pair)
self._pp_ring_map[pair_key] = self.ring_id
ring_id = self.ring_id
self.ring_id += 1
else:
ring_id = self._pp_ring_map[pair_key]
block._insert_op_without_sync(
index=index + extra_index_info['index'],
type='send_v2',
inputs={'X': var},
attrs={
self._op_device_key: prev_dev,
self._op_role_key: op_role,
'use_calc_stream': True,
'peer': 1,
'ring_id': ring_id,
},
)
extra_index_info['index'] += 1
var_shape = list(var.shape)
if var_shape[0] < 0:
if is_while_block:
var_shape[0] = (
self.micro_batch_size * self.beam_size
)
else:
var_shape[0] = self.micro_batch_size
block._insert_op_without_sync(
index=index + extra_index_info['index'],
type='recv_v2',
outputs={'Out': [var]},
attrs={
'out_shape': var_shape,
'dtype': var.dtype,
self._op_device_key: cur_dev,
self._op_role_key: op_role,
'use_calc_stream': True,
'peer': 0,
'ring_id': ring_id,
},
)
extra_index_info['index'] += 1
_insert_send_recv(
int(cur_device.split(':')[1]),
int(prev_device.split(':')[1]),
)
block._sync_with_cpp()
def _insert_sendrecv_ops_in_while_block(
self,
block,
sync_in_while_lastpp2firstpp_var_names,
sync_in_while_var_names,
stage,
):
dev_ids = []
for pair in self._pipeline_pair_in_while:
prev_id, cur_id = pair
if prev_id not in dev_ids:
dev_ids.append(prev_id)
if cur_id not in dev_ids:
dev_ids.append(cur_id)
if len(dev_ids) == 0:
return
first_id = min(dev_ids)
last_id = max(dev_ids)
assert len(block.ops) > 2, (
"It must have more than 2 ops in while sub block, "
"layers.assign(layers.cast(cond_int, dtype='bool'), cond) must at end of while block, "
"because nccl cannot send bool dtype var"
)
index = len(block.ops) - 2
for prev_id in dev_ids:
if prev_id == cur_id:
continue
assert cur_id > prev_id
pair = (prev_id, cur_id)
# 1000 is just a magic number
pair_key = prev_id * 1000 + cur_id
if pair not in self._pipeline_pair:
self._pipeline_pair.append(pair)
self._pp_ring_map[pair_key] = self.ring_id
ring_id = self.ring_id
self.ring_id += 1
else:
ring_id = self._pp_ring_map[pair_key]
if cur_id == last_id and prev_id == first_id:
var_names = (
sync_in_while_lastpp2firstpp_var_names
+ sync_in_while_var_names
)
else:
var_names = sync_in_while_var_names
for var_name in var_names:
var = block._var_recursive(var_name)
if stage == cur_id:
block._insert_op_without_sync(
index=index,
type='send_v2',
inputs={'X': var},
attrs={
self._op_device_key: self._device
+ ':'
+ str(cur_id),
self._op_role_key: int(self._op_role.Forward),
'use_calc_stream': True,
'peer': 0,
'ring_id': ring_id,
},
)
else:
var_shape = list(var.shape)
print(var_name)
if len(var.shape) > 0:
var_shape[0] = (
self.micro_batch_size
if var_shape[0] < 0
else var_shape[0]
)
block._insert_op_without_sync(
index=index,
type='recv_v2',
outputs={'Out': [var]},
attrs={
'out_shape': var_shape,
'dtype': var.dtype,
self._op_device_key: self._device
+ ':'
+ str(prev_id),
self._op_role_key: int(self._op_role.Forward),
'use_calc_stream': True,
'peer': 1,
'ring_id': ring_id,
},
)
index += 1
block._sync_with_cpp()
def _get_while_block(self):
"""
Get the while sub-block.
"""
main_block = self._main_program.global_block()
num_while = 0
sub_block_id = None
for op in main_block.ops:
assert num_while < 2, "More than one while op found."
if op.type == 'while':
sub_block_id = op.attr('sub_block').id
num_while += 1
if sub_block_id:
return op, self._main_program.block(sub_block_id)
return None, None
def gen_infer_program(
self,
sync_in_while_lastpp2firstpp_var_names=None,
sync_in_while_var_names=None,
debug=False,
):
"""
Generate inference program.
Params:
sync_in_while_lastpp2firstpp_var_names (list(str)): the vars in the last pipeline
that need to send var to first pipeline and exclude bool dtype var
sync_in_while_var_names (list(str)): the vars sync among all pipeline in while block
e.g cond. Note that cond cannot be bool dtype.
debug (bool): the flag indicate debug
"""
main_block = self._main_program.global_block()
startup_block = self._startup_program.global_block()
if debug:
with open('main_program.txt', 'w') as f:
f.write(str(self._main_program))
with open('startup_program.txt', 'w') as f:
f.write(str(self._startup_program))
# step1: add op_device attribute for all ops
self._add_op_device_attr(startup_block)
self._check_validation(startup_block)
self._add_op_device_attr(main_block)
self._check_validation(main_block)
# step2: add send/recv ops
self._update_param_device_map()
# step2.1: add send/recv for main_block
out_var_to_op, in_var_to_op = self._get_input_output_info(main_block)
self._output_var_to_op = out_var_to_op
self._input_var_to_op = in_var_to_op
self._insert_sendrecv_ops_for_boundaries(main_block, False)
# step2.2: add send/recv for while_block
while_op, while_block = self._get_while_block()
if while_block:
out_var_to_op, in_var_to_op = self._get_input_output_info(
while_block
)
self._output_var_to_op = out_var_to_op
self._input_var_to_op = in_var_to_op
self._insert_sendrecv_ops_for_boundaries(while_block, True)
self._insert_sendrecv_ops_in_while_block(
while_block,
sync_in_while_lastpp2firstpp_var_names,
sync_in_while_var_names,
self._stage,
)
# step3: split programs
self._split_program(self._startup_program, self._stage, 0)
self._split_program(self._main_program, self._stage, 0)
if debug:
with open(f'main_program.txt.{self.rank}', 'w') as f:
f.write(str(self._main_program))
with open(f'startup_program.txt.{self.rank}', 'w') as f:
f.write(str(self._startup_program))
if self.init_comm:
self._init_communication_group()
@@ -0,0 +1,340 @@
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import paddle
from paddle import framework
# (TODO: GhostScreaming) It will be removed later.
from paddle.base import core
from paddle.distributed.parallel import (
_split_tensors,
build_groups,
in_dynamic_mode,
sync_params_buffers,
)
from .log_util import logger
__all__ = []
def obtain_optimizer_parameters_list(optimizer):
if getattr(optimizer, '_param_groups', None) and isinstance(
optimizer._param_groups[0], dict
):
parameters_list = []
for group in optimizer._param_groups:
for param in group['params']:
parameters_list.append(param)
else:
parameters_list = list(optimizer._parameter_list)
return parameters_list
def _apply_collective_grads(parameters, comm_group, bucket_size, scale=None):
grad_var_set = set()
grad_vars = []
sparse_grad_vars = []
for param in parameters:
if param.trainable and (param._grad_ivar() is not None):
g_var = param._grad_ivar()
assert not g_var._is_sparse(), (
"Now, it doesn't support sparse parameters"
)
grad_vars.append(g_var)
assert g_var not in grad_var_set
grad_var_set.add(g_var)
coalesced_grads_and_vars = build_groups(grad_vars, bucket_size)
nranks = (
paddle.distributed.get_world_size()
if comm_group is None
else comm_group.nranks
)
scale = nranks if scale is None else 1.0 / scale
scale = None if scale == 1.0 else scale
for coalesced_grad, _, _ in coalesced_grads_and_vars:
# need to div nranks
if scale is not None:
div_factor = paddle.to_tensor(scale, dtype=coalesced_grad.dtype)
paddle.base.framework._dygraph_tracer().trace_op(
type="elementwise_div",
inputs={'X': coalesced_grad, 'Y': div_factor},
outputs={'Out': coalesced_grad},
attrs={'axis': -1},
)
paddle.distributed.all_reduce(coalesced_grad, group=comm_group)
_split_tensors(coalesced_grads_and_vars)
def _apply_collective_grads_eager(
parameters, comm_group, bucket_size, scale=None
):
grad_var_set = set()
grad_vars = []
for param in parameters:
g_var = None
if param.trainable and (param._grad_ivar() is not None):
g_var = param._grad_ivar()
if param.trainable and hasattr(param, "main_grad"):
assert param._grad_ivar() is None, "param.grad is not None"
g_var = param.main_grad
if g_var is not None:
assert not g_var.is_sparse(), (
"Now, it doesn't support sparse parameters"
)
grad_vars.append(g_var)
assert g_var not in grad_var_set
grad_var_set.add(g_var)
if len(grad_vars) == 0:
return
coalesced_grads_and_vars = build_groups(grad_vars, bucket_size)
nranks = (
paddle.distributed.get_world_size()
if comm_group is None
else comm_group.nranks
)
scale = 1.0 / nranks if scale is None else scale
scale = None if scale == 1.0 else scale
for coalesced_grad, _, _ in coalesced_grads_and_vars:
# need to div nranks
if scale is not None:
coalesced_grad.scale_(scale)
paddle.distributed.all_reduce(coalesced_grad, group=comm_group)
_split_tensors(coalesced_grads_and_vars)
def _broadcast_data_help(data, shape, dtype, hcg):
model_parallel_group = hcg.get_model_parallel_group()
src_rank = hcg.get_model_parallel_group_src_rank()
mp_rank = hcg.get_model_parallel_rank()
shape_gpu = paddle.to_tensor(shape, dtype="int32")
paddle.distributed.broadcast(
shape_gpu, src=src_rank, group=model_parallel_group, sync_op=True
)
if mp_rank != 0:
input_data = paddle.zeros(shape_gpu, dtype=dtype)
else:
input_data = data
paddle.distributed.broadcast(
input_data, src=src_rank, group=model_parallel_group, sync_op=True
)
if mp_rank != 0:
if in_dynamic_mode():
data._clear_data()
input_data._share_buffer_to(data)
else:
data.value().get_tensor()._clear()
data.value().get_tensor()._share_data_with(
input_data.value().get_tensor()
)
def _broadcast_object_list_help(object_list, hcg):
model_parallel_group = hcg.get_model_parallel_group()
src_rank = hcg.get_model_parallel_group_src_rank()
mp_rank = hcg.get_model_parallel_rank()
paddle.distributed.broadcast_object_list(
object_list, src=src_rank, group=model_parallel_group
)
def _process_element(hcg, dev, place, element):
if isinstance(element, core.eager.Tensor):
with framework.no_grad():
if (
in_dynamic_mode()
and not eval(f"element.place.is_{dev}_place")()
):
element_gpu = element._copy_to(place, True)
element._clear_data()
element_gpu._share_buffer_to(element)
_broadcast_data_help(element, element.shape, element.dtype, hcg)
elif isinstance(element, (dict, list, tuple)):
return _broadcast_nested_data(hcg, dev, place, element)
else:
_broadcast_object_list_help([element], hcg)
def _broadcast_nested_data(hcg, dev, place, data):
if isinstance(data, dict):
return {
key: _process_element(hcg, dev, place, value)
for key, value in data.items()
}
elif isinstance(data, list):
return [_process_element(hcg, dev, place, item) for item in data]
elif isinstance(data, tuple):
return tuple(_process_element(hcg, dev, place, item) for item in data)
else:
raise TypeError(f"Unsupported data type: {type(data)}")
def broadcast_input_data(hcg, *inputs, **kwargs):
cur_device = paddle.get_device()
dev = cur_device.split(":")[0]
assert (
dev
in [
"xpu",
"gpu",
]
or dev in paddle.device.get_all_custom_device_type()
), f"Only support xpu, gpu and custom_device now, but this is {dev}"
dev_idx = int(cur_device.split(':')[1])
if dev == "gpu":
place = paddle.CUDAPlace(dev_idx)
elif dev in paddle.device.get_all_custom_device_type():
place = paddle.CustomPlace(dev, dev_idx)
dev = 'custom'
else:
place = eval(f"paddle.{dev.upper()}Place")(dev_idx)
if len(inputs) > 0:
inputs = _broadcast_nested_data(hcg, dev, place, inputs)
if len(kwargs) > 0:
kwargs = _broadcast_nested_data(hcg, dev, place, kwargs)
return inputs, kwargs
def broadcast_mp_parameters(model, hcg, fuse_params=True):
model_parallel_group = hcg.get_model_parallel_group()
src_rank = hcg.get_model_parallel_group_src_rank()
sync_params_buffers(
model,
model_parallel_group,
src_rank,
is_model_parallel=True,
fuse_params=fuse_params,
)
def broadcast_dp_parameters(model, hcg, fuse_params=True):
data_parallel_group = hcg.get_data_parallel_group()
src_rank = hcg.get_data_parallel_group_src_rank()
sync_params_buffers(
model,
data_parallel_group,
src_rank,
is_model_parallel=False,
fuse_params=fuse_params,
)
def fused_allreduce_gradients_with_group(
parameter_list, group, bucket_size=128 * 1024 * 1024, scale=None
):
apply_func = (
_apply_collective_grads_eager
if in_dynamic_mode()
else _apply_collective_grads
)
with framework.no_grad():
apply_func(parameter_list, group, bucket_size, scale)
def fused_allreduce_gradients(parameter_list, hcg):
group = None
scale = None
if hcg is not None:
dp_enabled = hcg.get_data_parallel_world_size() > 1
sep_enabled = hcg.get_sep_parallel_world_size() > 1
assert dp_enabled or sep_enabled, (
f"dp_enabled {dp_enabled}; sep_enabled {sep_enabled}"
)
group = None
# sep all reduce is not scaled
scale = 1.0
if dp_enabled:
group = hcg.get_data_parallel_group()
scale = scale / group.nranks
if sep_enabled:
sep_group = hcg.get_sep_parallel_group()
dp_sep_group = hcg.get_dp_sep_parallel_group()
group = sep_group if group is None else dp_sep_group
logger.debug("dp or sep start fuse allreduce gradients")
from paddle.distributed import in_auto_parallel_align_mode
if in_auto_parallel_align_mode():
scale = 1.0
fused_allreduce_gradients_with_group(parameter_list, group, scale=scale)
def broadcast_sharding_parameters(model, hcg, fuse_params=True):
# TODO TO save memory, use un-fused broadcast to avoid potential OOM
logger.debug("sharding start init parameters sync")
sharding_parallel_group = hcg.get_sharding_parallel_group()
src_rank = hcg.get_sharding_parallel_group_src_rank()
sync_params_buffers(
model,
sharding_parallel_group,
src_rank,
is_model_parallel=False,
fuse_params=fuse_params,
)
def broadcast_sep_parameters(model, hcg, fuse_params=True):
# TODO TO save memory, use un-fused broadcast to avoid potential OOM
logger.debug("sep start init parameters sync")
sep_group = hcg.get_sep_parallel_group()
src_rank = hcg.get_sep_parallel_group_src_rank()
sync_params_buffers(
model,
sep_group,
src_rank,
is_model_parallel=False,
fuse_params=fuse_params,
)
def broadcast_moe_sharding_parameters(model, hcg, fuse_params=True):
# TODO TO save memory, use un-fused broadcast to avoid potential OOM
logger.debug("moe sharding start init parameters sync")
moe_sharding_group = hcg.get_moe_sharding_parallel_group()
src_rank = hcg.get_moe_sharding_parallel_group_src_rank()
sync_params_buffers(
model,
moe_sharding_group,
src_rank,
is_model_parallel=False,
fuse_params=fuse_params,
is_moe_sharding_parallel=True,
)
def unwrap_optimizer(optimizer, optimizer_instances=()):
_inner_opt = optimizer
while isinstance(_inner_opt, optimizer_instances):
_inner_opt = _inner_opt._inner_opt
return _inner_opt
@@ -0,0 +1,174 @@
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import os
import subprocess
from logging.handlers import RotatingFileHandler
import paddle
from paddle.distributed.utils.log_utils import get_logger
logger = get_logger("INFO", __name__)
def set_log_level(level):
"""
Set log level
Args:
level (str|int): a specified level
Example 1:
import paddle
import paddle.distributed.fleet as fleet
fleet.init()
fleet.setLogLevel("DEBUG")
Example 2:
import paddle
import paddle.distributed.fleet as fleet
fleet.init()
fleet.setLogLevel(1)
"""
assert isinstance(level, (str, int)), "level's type must be str or int"
if isinstance(level, int):
logger.setLevel(level)
else:
logger.setLevel(level.upper())
def get_log_level_code():
"""
Return current log level code
"""
return logger.getEffectiveLevel()
def get_log_level_name():
"""
Return current log level name
"""
return logging.getLevelName(get_log_level_code())
def layer_to_str(base, *args, **kwargs):
name = base + "("
if args:
name += ", ".join(str(arg) for arg in args)
if kwargs:
name += ", "
if kwargs:
name += ", ".join(f"{key}={value}" for key, value in kwargs.items())
name += ")"
return name
class DistributedLogger(logging.Logger):
def __init__(self, name, level=logging.NOTSET):
super().__init__(name, level)
def info(self, msg, *args, **kwargs):
paddle.device.synchronize()
super().info(f"Distributed Debug: {msg}", *args, **kwargs)
def get_rotate_file_logger(log_level, name='root'):
distributed_logger = DistributedLogger(name + '_rotate', level=log_level)
distributed_logger.propagate = False
device_id = int(os.getenv("FLAGS_selected_gpus", "0"))
log_dir = os.path.join(os.getcwd(), "hybrid_parallel")
os.makedirs(log_dir, exist_ok=True)
path = os.path.join(log_dir, f"worker_{device_id}.log")
handler = RotatingFileHandler(
path,
maxBytes=2 * 1024 * 1024 * 1024,
backupCount=3, # 2GB
)
log_format = logging.Formatter(
'[%(asctime)-15s] [%(levelname)8s] %(filename)s:%(lineno)s - %(message)s'
)
handler.setFormatter(log_format)
distributed_logger.addHandler(handler)
return distributed_logger
g_sync_rotate_logger = None
def get_sync_logger():
global logger
paddle.device.synchronize()
return logger
def sync_rotate_logger():
global g_sync_rotate_logger
if g_sync_rotate_logger is None:
g_sync_rotate_logger = get_rotate_file_logger("INFO", __name__)
return g_sync_rotate_logger
def check_memory_usage(msg=""):
GB = 1024.0 * 1024.0 * 1024.0
mem_dict = {}
mem_dict['max_memory_allocated_size'] = (
paddle.device.cuda.max_memory_allocated() / GB
)
mem_dict['max_memory_reserved_size'] = (
paddle.device.cuda.max_memory_reserved() / GB
)
mem_dict['memory_allocated_size'] = (
paddle.device.cuda.memory_allocated() / GB
)
mem_dict['memory_reserved_size'] = paddle.device.cuda.memory_reserved() / GB
mem_msg = f"checking gpu memory usage {msg}:"
for key in mem_dict:
mem_msg += f"\n{key}: {mem_dict[key]}GB"
logger.info(mem_msg)
if hasattr(paddle.device.cuda, 'max_pinned_memory_allocated'):
mem_dict = {}
mem_dict['max_memory_allocated_size'] = (
paddle.device.cuda.max_pinned_memory_allocated() / GB
)
mem_dict['max_memory_reserved_size'] = (
paddle.device.cuda.max_pinned_memory_reserved() / GB
)
mem_dict['memory_allocated_size'] = (
paddle.device.cuda.pinned_memory_allocated() / GB
)
mem_dict['memory_reserved_size'] = (
paddle.device.cuda.pinned_memory_reserved() / GB
)
mem_msg = f"checking pinned memory usage {msg}:"
for key in mem_dict:
mem_msg += f"\n{key}: {mem_dict[key]}GB"
logger.info(mem_msg)
# Execute the command and get the output
result = subprocess.run(["free", "-h"], capture_output=True, text=True)
lines = result.stdout.strip().split('\n')
# Extract data
mem_data = lines[1].split()
swap_data = lines[2].split()
# Format and print
formatted_output = f"checking CPU memory usage: {msg} Memory - Total: {mem_data[1]}, Used: {mem_data[2]}, Free: {mem_data[3]} Available:{mem_data[-1]}"
logger.info(formatted_output)
@@ -0,0 +1,260 @@
# 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 collections import defaultdict
from types import MethodType
import numpy as np
import paddle
from paddle import _legacy_C_ops, nn
from paddle.base import framework
from paddle.base.dygraph import (
base as imperative_base,
)
from paddle.distributed import fleet
from paddle.distributed.fleet.utils.hybrid_parallel_util import (
obtain_optimizer_parameters_list,
)
from paddle.framework import core
from paddle.utils import deprecated
class MixPrecisionLayer(nn.Layer):
def __init__(self, layers, dtype="float16"):
super().__init__(layers.full_name() + "_mix_precision")
self._layers = layers
self._dtype = dtype
assert self._dtype in ["float16", "bfloat16"]
for param in self._layers.parameters():
if not hasattr(param, "main_grad"):
param.main_grad = None
param._register_grad_hook(self._update_main_grad_hook(param))
def _update_main_grad_hook(self, param):
"""Create the update_main_grad hook for back-prop."""
# Hook used for back-prop and grad-merge.
@paddle.autograd.no_grad()
def param_hook(tmp_grad):
assert param.grad is None, (
f"In main_grad node, param.grad should be None, but find param[{param.name}] has grad."
)
if tmp_grad is not None and tmp_grad._is_initialized():
# Some previous pylayer may return None, should check grad validation.
if param.main_grad is None:
param.main_grad = core.eager.Tensor(
value=tmp_grad.cast(paddle.float32).value(),
place=tmp_grad.place,
name="main_grad@" + param.name,
)
else:
param.main_grad.add_(tmp_grad)
tmp_grad._clear_data()
return param_hook
def forward(self, *inputs, **kwargs):
outputs = self._layers(*inputs, **kwargs)
return outputs
def state_dict(
self,
destination=None,
include_sublayers=True,
structured_name_prefix="",
):
return self._layers.state_dict(
destination=destination,
include_sublayers=include_sublayers,
structured_name_prefix=structured_name_prefix,
)
@framework.deprecate_stat_dict
def set_state_dict(self, state_dict, use_structured_name=True):
self._layers.set_state_dict(
state_dict, use_structured_name=use_structured_name
)
class MixPrecisionOptimizer:
def __init__(self, optimizer):
self._inner_opt = optimizer
self._parameter_list = obtain_optimizer_parameters_list(optimizer)
@imperative_base.no_grad
@framework.dygraph_only
def step(self):
need_shard = any(
hasattr(p, '_need_shard') for p in self._parameter_list
)
if need_shard:
fleet.meta_parallel.sharding.group_sharded_fully_shard.FullyShardOptimizer(
self
)
self.step()
return
if not isinstance(self._parameter_list[0], dict):
params_grads = []
for param in self._parameter_list:
if param.stop_gradient:
continue
grad_var = param.main_grad
if grad_var is None:
continue
if paddle.in_dynamic_mode():
if (
hasattr(grad_var, "is_selected_rows")
and grad_var.is_selected_rows()
and self._inner_opt.regularization is not None
):
raise RuntimeError(
"AdamW don't support weight_decay with sparse parameters, please set it to None."
)
else:
if (
hasattr(grad_var, "_is_sparse")
and grad_var._is_sparse()
and self._inner_opt.regularization is not None
):
raise RuntimeError(
"AdamW don't support weight_decay with sparse parameters, please set it to None."
)
params_grads.append((param, grad_var))
optimize_ops = self._inner_opt._apply_optimize(
loss=None, startup_program=None, params_grads=params_grads
)
else:
# optimize parameters in groups
for param_group in self._inner_opt._param_groups:
params_grads = defaultdict(lambda: [])
for param in param_group['params']:
if param.stop_gradient:
continue
grad_var = param.main_grad
if grad_var is None:
continue
if paddle.in_dynamic_mode():
if (
hasattr(grad_var, "is_selected_rows")
and grad_var.is_selected_rows()
and self._inner_opt.regularization is not None
):
raise RuntimeError(
"AdamW don't support weight_decay with sparse parameters, please set it to None."
)
else:
if (
hasattr(grad_var, "_is_sparse")
and grad_var._is_sparse()
and self._inner_opt.regularization is not None
):
raise RuntimeError(
"AdamW don't support weight_decay with sparse parameters, please set it to None."
)
params_grads['params'].append((param, grad_var))
params_grads.update(
{k: v for k, v in param_group.items() if k != 'params'}
)
self._apply_optimize(
loss=None, startup_program=None, params_grads=params_grads
)
@framework.dygraph_only
def clear_grad(self, set_to_zero=True):
param_list = []
if self._parameter_list is None or not isinstance(
self._parameter_list[0], dict
):
for p in self._parameter_list:
if not p.stop_gradient:
param_list.append(p)
else:
for param_group in self._param_groups:
for p in param_group['params']:
if not p.stop_gradient:
param_list.append(p)
for p in param_list:
if hasattr(p, "main_grad") and p.main_grad is not None:
if set_to_zero:
p.main_grad.zero_()
else:
p.main_grad._clear()
p.main_grad = None
elif not hasattr(p, "main_grad"):
p.clear_gradient(set_to_zero)
def __getattr__(self, item):
return getattr(self._inner_opt, item)
def unscale_method(self, optimizer):
if not self._enable:
return
param_grads = []
if getattr(optimizer, '_param_groups', None) and isinstance(
optimizer._param_groups[0], dict
):
for group in optimizer._param_groups:
for param in group['params']:
if param.main_grad is not None:
assert param.main_grad.dtype == paddle.float32
param_grads.append(param.main_grad)
else:
for param in optimizer._parameter_list:
if param.main_grad is not None:
assert param.main_grad.dtype == paddle.float32
param_grads.append(param.main_grad)
temp_found_inf = paddle.to_tensor(np.array([0]).astype(np.bool_))
if len(param_grads):
_legacy_C_ops.check_finite_and_unscale(
param_grads,
self._scale,
param_grads,
temp_found_inf,
)
self._found_inf = 1 if temp_found_inf else 0
hcg = fleet.get_hybrid_communicate_group()
if hcg is not None and hcg.nranks > hcg.get_data_parallel_world_size():
is_found_inf = paddle.to_tensor([self._found_inf], dtype="int32")
paddle.distributed.all_reduce(
is_found_inf, op=paddle.distributed.ReduceOp.MAX, group=None
)
self._found_inf = int(is_found_inf)
@deprecated(
since="2.5.0",
update_to="paddle.distributed_scaler",
level=1,
)
class MixPrecisionScaler:
def __init__(self, scaler):
self._inner_scaler = scaler
self._inner_scaler._unscale = MethodType(unscale_method, scaler)
def __getattr__(self, item):
return getattr(self._inner_scaler, item)
@@ -0,0 +1,604 @@
# 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.
import argparse
import math
import re
import shutil
from collections import OrderedDict
import paddle
class ParallelConfig:
def __init__(self, mp: int, pp: int, vpp: int = 1, sharding: int = 1):
self.mp = mp
self.pp = pp
self.vpp = vpp
self.sharding = sharding
def pipe_parallel_group(self, i: int, j: int):
ans = []
for k in range(self.pp):
ans.append((i, j, k))
return ans
class LayerReNamingHelper:
def __init__(self, template: str):
self._template = template
self._i = -1
self._last_old_layer_name = None
def get_new_layer_name(self, old_layer_name: str):
old_layer_name = old_layer_name.split(".")[0]
if (
self._last_old_layer_name is None
or old_layer_name != self._last_old_layer_name
):
self._i = self._i + 1
self._last_old_layer_name = old_layer_name
return self._template.format(self._i)
class LayerReNamingManager:
def __init__(self):
self._renaming_helpers = OrderedDict()
self._renaming_helpers["linear"] = LayerReNamingHelper("linear_{}")
self._renaming_helpers["layer_norm"] = LayerReNamingHelper(
"layer_norm_{}"
)
self._renaming_helpers["embedding"] = LayerReNamingHelper(
"embedding_{}"
)
def get_new_layer_name(self, old_name: str):
layer_name = ""
for k, v in self._renaming_helpers.items():
if old_name.startswith(k):
layer_name = v.get_new_layer_name(old_name)
break
return layer_name
def get_new_param_name(self, old_name: str):
names = old_name.split(".")
layer_name = self.get_new_layer_name(names[0])
assert layer_name, f"can not rename layer {names[0]}"
names[0] = layer_name
return ".".join(names)
class PipeLineModelAdaptor:
def __init__(
self,
src_parallel_config: ParallelConfig,
dst_parallel_config: ParallelConfig,
transformer_layer_num: int,
segment_method: str = "layer",
):
self._src_parallel_config = src_parallel_config
self._dst_parallel_config = dst_parallel_config
self._transformer_layer_num = transformer_layer_num
self._segment_method = segment_method
def apply(self, src_model_path: str, dst_model_path: str):
for i in range(self._src_parallel_config.mp):
for j in range(self._src_parallel_config.sharding):
# TODO(liuzhenhai): use multiple process
layers = []
# 1、extract layers in the same pp group
group = self._src_parallel_config.pipe_parallel_group(i, j)
src_dirs = [
"{}/mp_{:0>2d}_sharding_{:0>2d}_pp_{:0>2d}".format(
src_model_path, *e
)
for e in group
]
# first rank extract shared layer
with_shared = True
for dir in src_dirs:
print(f"extract layer params in dir {dir}")
layers.extend(self.extract_layers(dir, with_shared))
with_shared = False
# 2、sort and unique layers
layers = self.sort_layers(layers)
# 3、resplit layers among pp group according new pp config
layer_segments = self.segment_layers(
layers, self._dst_parallel_config, self._segment_method
)
dst_group = self._dst_parallel_config.pipe_parallel_group(i, j)
dst_dirs = [
"{}/mp_{:0>2d}_sharding_{:0>2d}_pp_{:0>2d}".format(
dst_model_path, *e
)
for e in dst_group
]
# 4、merge layers belonging to the same node
for layer_segment, dir_ in zip(layer_segments, dst_dirs):
print(f"merge {len(layer_segment)} layers to {dir_}")
self.merge_layers(layer_segment, dir_)
# 5、copy meta_state.pdopt
for src_dir, dst_dir in zip(src_dirs, dst_dirs):
shutil.copyfile(
f"{src_dir}/meta_state.pdopt",
f"{dst_dir}/meta_state.pdopt",
)
def peek_model(self, model_dir: str):
for i in range(self._src_parallel_config.mp):
for j in range(self._src_parallel_config.sharding):
group = self._src_parallel_config.pipe_parallel_group(i, j)
dirs = [
"{}/mp_{:0>2d}_sharding_{:0>2d}_pp_{:0>2d}".format(
model_dir, *e
)
for e in group
]
for dir in dirs:
print(f"peek partial model in {dir}:")
self.peek_partial_model(dir)
def peek_partial_model(self, sub_dir: str):
state_dict = paddle.load(f"{sub_dir}/model.pdparams")
for k, v in state_dict.items():
print(f"\t{k} -> {v.name}")
def extract_layers(self, dir: str, with_shared: bool):
opt = paddle.load(dir + "/model_state.pdopt")
params = paddle.load(dir + "/model.pdparams")
shared_layer_parsed = False
# tname -> (layer, param_name)
tname_to_layer_and_pname = {}
for k, v in params.items():
layer = self._extract_layer_name(k)
assert layer
# special treatment for embedding layer, skip duplicated shared layer
# shared layer may exist or not, if it exist it share weight with _layers.0
# _layers.shared_layers.embed.word_embeddings.weight -> embedding_0.w_0
# _layers.shared_layers.embed.position_embeddings.weight -> embedding_1.w_0
# _layers.0.word_embeddings.weight -> embedding_0.w_0
# _layers.0.position_embeddings.weight -> embedding_1.w_0
shared_layer_parsed = shared_layer_parsed or (
"_layers.shared_layers" in layer
)
if (
"_layers.shared_layers" not in layer
and ("word_embeddings" in k or "position_embeddings" in k)
and shared_layer_parsed
):
continue
tname_to_layer_and_pname[v.name] = (layer, k)
# get opt-> param mapping
tensor_names = list(tname_to_layer_and_pname.keys())
opt_names = [
e for e in opt.keys() if e not in ["master_weights", "LR_Scheduler"]
]
opt_to_t = self._opt_name_to_tname(tensor_names, opt_names)
# gather tensors belonging to one layer together
layers = OrderedDict()
for k, v in params.items():
layer, p = tname_to_layer_and_pname[v.name]
if layer not in layers:
layers[layer] = {}
layers[layer]["opt"] = OrderedDict()
layers[layer]["params"] = OrderedDict()
layers[layer]["master_weights"] = OrderedDict()
layers[layer]["params"][p] = v
for k, v in opt.items():
if k in ["master_weights", "LR_Scheduler"]:
continue
layer, _ = tname_to_layer_and_pname[opt_to_t[v.name]]
layers[layer]["opt"][k] = v
if "master_weights" in opt:
for k, v in opt["master_weights"].items():
layer, _ = tname_to_layer_and_pname[k]
layers[layer]["master_weights"][k] = v
if "LR_Scheduler" in opt:
for layer in layers:
layers[layer]["LR_Scheduler"] = opt["LR_Scheduler"]
ans = []
for layer_name, layer in layers.items():
# special treatment for embedding layer
if (not with_shared) and "shared_layers" in layer_name:
continue
file_name = f"./tmp_layer_files/{layer_name}.tmp"
paddle.save(layer, file_name)
ans.append((layer_name, file_name))
print(f"save layer {layer_name} to {file_name}")
return ans
def sort_layers(self, layers: list):
def priority(elem):
layer_name = elem[0]
if "shared_layers" in layer_name:
return -0.5
match = re.search(
r"^_layers((\.\d+)+|(\.shared_layers\.[^\.]+))", layer_name
)
assert match, f"{layer_name} not a valid layer name"
return float(match.group(1).lstrip("."))
# strictly sort layers
print("before sort {}".format("|".join([e[0] for e in layers])))
layers.sort(key=priority)
# unique
unique_layers = []
for e in layers:
if unique_layers and e[0] == unique_layers[-1][0]:
continue
unique_layers.append(e)
print("after sort {} ".format("|".join([e[0] for e in unique_layers])))
return unique_layers
def segment_layers(
self,
layers: list,
config: ParallelConfig,
segment_method: str = "layer",
):
layer_num = len(layers)
stage_num = config.pp * config.vpp
# segment by weights
def segment_by_layer():
# assume model is of the structure below
# embedding -> n*(transformer layer) -> [optional output layer]
# segment index
weights = [0 for _ in range(layer_num)]
non_zero_layers = range(1, layer_num - 1)
# input layer is embedding
if self._transformer_layer_num:
assert self._transformer_layer_num < layer_num
non_zero_layers = range(1, 1 + self._transformer_layer_num)
for i in non_zero_layers:
weights[i] = 1
part_size = sum(weights) // stage_num
result = [0 for _ in range(stage_num + 1)]
memory_counter = 0
result_idx = 1
for idx, weight in enumerate(weights):
memory_counter += weight
if memory_counter == part_size:
result[result_idx] = idx + 1
result_idx += 1
memory_counter = 0
result[stage_num] = layer_num
return result
def segment_uniform():
result = [0 for _ in range(stage_num + 1)]
part_size = math.floor(layer_num / stage_num)
extra_layers = layer_num % stage_num
for i in range(1, stage_num):
offset = 1 if i > (stage_num - extra_layers) else 0
result[i] = int(
min(result[i - 1] + part_size + offset, layer_num)
)
result[stage_num] = layer_num
return result
result = (
segment_uniform()
if (segment_method == "uniform")
else segment_by_layer()
)
index_segments = [[] for _ in range(config.pp)]
for i in range(stage_num):
index_segments[i % config.pp].append((result[i], result[i + 1]))
# name layers
segments = [[] for i in range(config.pp)]
for i in range(config.pp):
for start, end in index_segments[i]:
for j in range(start, end):
if config.vpp > 1:
segments[i].append(
(
[f"_layers.{start}.{j - start}"],
layers[j][1],
)
)
else:
segments[i].append(([f"_layers.{j}"], layers[j][1]))
shared_layer_exist = any(
"_layers.shared_layers" in e[0] for e in layers
)
if shared_layer_exist:
# special treatment for shared layer
if config.vpp > 1:
segments[0] = [
([layers[0][0], segments[0][0][0][0]], layers[0][1]),
*segments[0][1:],
]
else:
segments[0] = [([layers[0][0]], layers[0][1]), *segments[0][1:]]
for i in range(1, config.pp):
segments[i] = [([layers[0][0]], layers[0][1])] + segments[i]
for pp_rank, segs in enumerate(segments):
print(f"segment result for pp_rank {pp_rank}:")
print(50 * "=")
for seg in segs:
print(f"{seg[0]} => {seg[1]}")
return segments
def merge_layers(self, layers_segment: list, save_dir: str):
params = OrderedDict()
opt = OrderedDict()
master_weights = OrderedDict()
renaming_manager = LayerReNamingManager()
def merge(src, dst, map_k=None):
for k, v in src.items():
k = map_k(k) if map_k is not None else k
dst[k] = v
lr_scheduler = None
for layer_names, file_path in layers_segment:
print(f"load {file_path}")
layer = paddle.load(file_path)
def get_param_name_mapper(layer_name):
# replace layer name
def map_param_name(param_name):
layer_pre = self._extract_layer_name(param_name)
return layer_name + param_name[len(layer_pre) :]
return map_param_name
(
layer_params,
layer_opt,
layer_master_weight,
) = self._map_tensor_names(
layer["params"],
layer["opt"],
layer["master_weights"],
renaming_manager,
)
for layer_name in layer_names:
merge(layer_params, params, get_param_name_mapper(layer_name))
merge(layer_opt, opt)
merge(layer_master_weight, master_weights)
lr_scheduler = layer["LR_Scheduler"]
opt = self._pack_opt_state_dict(opt, master_weights, lr_scheduler)
paddle.save(params, save_dir + "/model.pdparams")
paddle.save(opt, save_dir + "/model_state.pdopt")
def _pack_opt_state_dict(self, opt, master_weights, lr_scheduler):
opt["master_weights"] = master_weights
opt["LR_Scheduler"] = lr_scheduler
return opt
def _extract_layer_name(self, param_name: str):
match = re.search(
r"^_layers((\.\d+)+|(\.shared_layers\.[^\.]+))", param_name
)
layer_name = ""
return "" if (not match) else match.group()
# map opt names to tensor name
def _opt_name_to_tname(self, tensor_names, opt_names):
tensor_names = set(tensor_names)
all_names = []
all_names.extend(list(tensor_names))
all_names.extend(opt_names)
all_names.sort()
pre_t_name = ""
opt_to_t = {}
for n in all_names:
if n in tensor_names:
# we get a param
pre_t_name = n
else:
assert pre_t_name
opt_to_t[n] = pre_t_name
return opt_to_t
def _map_tensor_names(self, params, opt, master_weights, renaming_manager):
opt_renamed = OrderedDict()
master_weights_renamed = OrderedDict()
# old name to new name
t_name_mapping = {}
# map tensor names
for k, v in params.items():
t_name_mapping[v.name] = renaming_manager.get_new_param_name(v.name)
v.name = t_name_mapping[v.name]
# map opt names
opt_to_tname = self._opt_name_to_tname(
t_name_mapping.keys(), opt.keys()
)
for k, v in opt.items():
old_t_name = opt_to_tname[k]
t_name = t_name_mapping[old_t_name]
opt_name = t_name + k[len(old_t_name) :]
v.name = opt_name
opt_renamed[opt_name] = v
# map master names
for k, v in master_weights.items():
t_name = t_name_mapping[k]
v.name = t_name + v.name[len(k) :]
master_weights_renamed[t_name] = v
return (params, opt_renamed, master_weights_renamed)
def parse_args():
parser = argparse.ArgumentParser(
prog='model converter', description='converter a model'
)
parser.add_argument(
'--src_path',
type=str,
default="./output/epoch_0_step_30",
help='path of the model to convert',
)
parser.add_argument(
'--dst_path',
type=str,
default="./test_adapt",
help='path to saved the converted model',
)
parser.add_argument(
'--src_mp',
type=int,
default=2,
help='mp degree of the origin training task that dumped this model',
)
parser.add_argument(
'--src_pp',
type=int,
default=2,
help='pp degree of the origin training task that dumped this model',
)
parser.add_argument(
'--src_vp',
type=int,
default=2,
help='vp degree of the origin training task that dumped this model',
)
parser.add_argument(
'--dst_mp',
type=int,
default=None,
help='mp degree of the origin training task that dumped this model',
)
parser.add_argument(
'--dst_pp',
type=int,
default=None,
help='pp degree of the expected training task that would recover this model',
)
parser.add_argument(
'--dst_vp',
type=int,
default=2,
help='vp degree of the expected training task that would recover this model',
)
parser.add_argument(
'--sharding',
type=int,
default=1,
help=" sharding degree of both the origin training task that dumped this model and the expected training task that would recover this model",
)
parser.add_argument(
'--method',
type=str,
default="adapt_model",
help='vp degree of the expected training task that would recover this model',
)
parser.add_argument(
'--segment_method',
type=str,
default="layer",
help='method to segment layers to pp or vp stages',
)
parser.add_argument(
'--transformer_layer_num',
type=int,
default=0,
help='transformer_layer_num of the model',
)
# assume model is of the structure below
# embedding -> n*[transformer layer] -> optional output layer
args = parser.parse_args()
if args.dst_mp is None:
args.dst_mp = args.src_mp
if args.dst_pp is None:
args.dst_pp = args.src_pp
assert args.src_mp == args.dst_mp, (
f"src mp {args.src_mp} dst mp {args.dst_mp}"
)
assert args.method in [
'peek_model',
'adapt_model',
], "method should be in ['peek_model', 'adapt_model']"
assert args.segment_method in [
"uniform",
"layer",
], "segment_method should be 'uniform' or 'layer"
print(
f"adapt model dumped by task with pp degree:{args.src_pp}, vp degree:{args.src_vp}, mp degree:{args.src_mp} to task with pp degree:{args.dst_pp}, vp degree:{args.dst_vp}, mp degree:{args.dst_mp}"
)
return args
def adaptor_from_args(args):
src_parallel_config = ParallelConfig(
args.src_mp, args.src_pp, args.src_vp, args.sharding
)
dst_parallel_config = ParallelConfig(
args.dst_mp, args.dst_pp, args.dst_vp, args.sharding
)
adaptor = PipeLineModelAdaptor(
src_parallel_config,
dst_parallel_config,
args.transformer_layer_num,
args.segment_method,
)
return adaptor
def main():
args = parse_args()
adaptor = adaptor_from_args(args)
if args.method == "peek_model":
adaptor.peek_model(args.dst_path)
elif args.method == "adapt_model":
adaptor.apply(args.src_path, args.dst_path)
if __name__ == "__main__":
"""
Usage:
python pp_parallel_adaptor.py --src_mp xxx --src_path xxx --method \
adapt_model/peek_model --dst_path xxx --sharding xxx --segment_method xxx --transformer_layer_num xxx
for the meaning of a specific arg, please use:
python pp_parallel_adaptor.py -h
"""
main()
@@ -0,0 +1,354 @@
# 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.
"""Parameter Server utils"""
from __future__ import annotations
import os
import warnings
from typing import TYPE_CHECKING
import paddle
if TYPE_CHECKING:
from paddle import Tensor
from paddle.distributed.fleet.base.role_maker import RoleMakerBase
from paddle.static import Executor, Program
__all__ = []
class DistributedInfer:
"""
Utility class for distributed infer of PaddlePaddle.
"""
def __init__(
self,
main_program: Program | None = None,
startup_program: Program | None = None,
) -> None:
if main_program:
self.origin_main_program = main_program.clone()
else:
self.origin_main_program = (
paddle.static.default_main_program().clone()
)
if startup_program:
self.origin_startup_program = startup_program
else:
self.origin_startup_program = (
paddle.static.default_startup_program()
)
self.sparse_table_maps = None
def init_distributed_infer_env(
self,
exe: Executor,
loss: Tensor,
role_maker: RoleMakerBase | None = None,
dirname: str | None = None,
) -> None:
from paddle.distributed import fleet
if fleet.fleet._runtime_handle is None:
fleet.init(role_maker=role_maker)
fake_optimizer = paddle.optimizer.SGD()
strategy = fleet.DistributedStrategy()
strategy.a_sync = True
optimizer = fleet.distributed_optimizer(
fake_optimizer, strategy=strategy
)
optimizer.minimize(
loss, startup_program=self.origin_startup_program
)
if fleet.is_server():
fleet.init_server(dirname=dirname)
fleet.run_server()
else:
exe.run(paddle.static.default_startup_program())
fleet.init_worker()
self._init_dense_params(exe, dirname)
global_startup_program = paddle.static.default_startup_program()
global_startup_program = self.origin_startup_program
global_main_program = paddle.static.default_main_program()
global_main_program = self.origin_main_program
def _get_sparse_table_map(self):
from paddle.distributed import fleet
if self.sparse_table_maps is None:
self.sparse_table_maps = {}
send_ctx = fleet.fleet._runtime_handle._send_ctx
for gradname, ctx in send_ctx.items():
if ctx.is_sparse:
param = gradname.strip("@GRAD")
self.sparse_table_maps[param] = ctx.table_id()
else:
continue
return self.sparse_table_maps
def _init_dense_params(self, exe=None, dirname=None):
sparse_table_maps = self._get_sparse_table_map()
if dirname is not None and exe is not None:
all_persist_vars = [
v
for v in self.origin_main_program.list_vars()
if paddle.static.io.is_persistable(v)
]
dense_persist_vars = [
(v.name, v)
for v in all_persist_vars
if v.name not in sparse_table_maps
]
need_load_vars = [
v[1]
for v in dense_persist_vars
if os.path.isfile(os.path.join(dirname, v[0]))
]
paddle.static.load_vars(
exe,
dirname,
main_program=self.origin_main_program,
vars=need_load_vars,
)
def get_dist_infer_program(self) -> Program:
varname2tables = self._get_sparse_table_map()
convert_program = self._convert_program(
self.origin_main_program, varname2tables
)
return convert_program
def _convert_program(self, main_program, varname2tables):
def distributed_ops_pass(program):
SPARSE_OP_TYPE_DICT = {"lookup_table": "W", "lookup_table_v2": "W"}
def _get_pull_sparse_ops(_program):
pull_sparse_ops = {}
for op in _program.global_block().ops:
if (
op.type in SPARSE_OP_TYPE_DICT.keys()
and op.attr('remote_prefetch') is True
):
param_name = op.input(SPARSE_OP_TYPE_DICT[op.type])[0]
ops = pull_sparse_ops.get(param_name, [])
ops.append(op)
pull_sparse_ops[param_name] = ops
return pull_sparse_ops
def _pull_sparse_fuse(_program, pull_sparse_ops):
def dag_check_up_and_reorder(program, inputs, outputs):
global_block = program.global_block()
min_output_index = len(global_block.ops)
max_input_index = -1
input_indexes = [0] * len(global_block.ops)
output_indexes = [0] * len(global_block.ops)
for idx, op in enumerate(global_block.ops):
for i in range(0, len(op.output_names)):
if input_indexes[idx] == 1:
break
outs = op.output(op.output_names[i])
for in_id, in_var in enumerate(inputs):
if in_var.name in outs:
input_indexes[idx] = 1
max_input_index = max(max_input_index, idx)
break
for i in range(0, len(op.input_names)):
if output_indexes[idx] == 1:
break
ins = op.input(op.input_names[i])
for out_id, out_var in enumerate(outputs):
if out_var.name in ins:
output_indexes[idx] = 1
min_output_index = min(
min_output_index, idx
)
for i in range(len(global_block.ops)):
if input_indexes[i] == 1 and output_indexes[i] == 1:
warnings.warn(
"unable to re-arrange dags order to combine distributed embedding ops because a op both needs embedding table's output as input and produces ids as the same embedding table's input"
)
return
if min_output_index < max_input_index:
move_ops = []
for i in range(
min_output_index + 1, len(input_indexes)
):
if input_indexes[i] == 1:
move_ops.append((global_block.ops[i], i))
for i, op in enumerate(move_ops):
queue = []
visited = set()
queue.append(op[1])
visited.add(op[0])
start = 0
while start < len(queue):
pos = queue[start]
op = global_block.ops[pos]
op_inputs = []
for k in range(0, len(op.input_names)):
ins = op.input(op.input_names[k])
op_inputs.append(ins)
for j in range(
pos - 1, min_output_index - 1, -1
):
op1 = global_block.ops[j]
if op1 in visited:
continue
found = False
for k in range(0, len(op1.output_names)):
outs = op1.output(op1.output_names[k])
for t in range(len(op_inputs)):
for y in op_inputs[t]:
if y in outs:
found = True
break
if found:
break
if found:
break
if found:
if output_indexes[j]:
warnings.warn(
"unable to re-arrange dags order to combine distributed embedding ops"
)
return
queue.append(j)
visited.add(global_block.ops[j])
start = start + 1
queue.sort()
for index in queue:
desc = global_block.desc._insert_op(
min_output_index
)
desc.copy_from(global_block.ops[index].desc)
global_block.desc._remove_op(
index + 1, index + 2
)
global_block.ops[index].desc = desc
insert_op = global_block.ops.pop(index)
input_state = input_indexes.pop(index)
output_state = output_indexes.pop(index)
global_block.ops.insert(
min_output_index, insert_op
)
input_indexes.insert(
min_output_index, input_state
)
output_indexes.insert(
min_output_index, output_state
)
min_output_index = min_output_index + 1
assert global_block.desc.op_size() == len(
global_block.ops
)
for i in range(len(global_block.ops)):
assert (
global_block.desc.op(i)
== global_block.ops[i].desc
)
for param, ops in pull_sparse_ops.items():
all_ops = program.global_block().ops
inputs = [
program.global_block().vars[op.input("Ids")[0]]
for op in ops
]
w = program.global_block().vars[ops[0].input("W")[0]]
if w.name not in varname2tables.keys():
raise ValueError(
f"can not find variable {w.name}, please check your configuration"
)
table_id = varname2tables[w.name]
padding_idx = ops[0].attr("padding_idx")
is_distributed = ops[0].attr("is_distributed")
op_type = ops[0].type
outputs = [
program.global_block().vars[op.output("Out")[0]]
for op in ops
]
dag_check_up_and_reorder(program, inputs, outputs)
op_idxs = [all_ops.index(op) for op in ops]
for idx in op_idxs[::-1]:
program.global_block()._remove_op(idx)
inputs_idxs = [-1] * len(inputs)
outputs_idxs = [len(program.global_block().ops) + 1] * len(
outputs
)
for idx, op in enumerate(program.global_block().ops):
for i in range(0, len(op.output_names)):
outs = op.output(op.output_names[i])
for in_id, in_var in enumerate(inputs):
if in_var.name in outs:
inputs_idxs[in_id] = max(
idx, inputs_idxs[in_id]
)
for i in range(0, len(op.input_names)):
ins = op.input(op.input_names[i])
for out_id, out_var in enumerate(outputs):
if out_var.name in ins:
outputs_idxs[out_id] = min(
idx, outputs_idxs[out_id]
)
if min(outputs_idxs) - max(inputs_idxs) >= 1:
distributed_idx = max(inputs_idxs) + 1
program.global_block()._insert_op(
index=distributed_idx,
type="distributed_lookup_table",
inputs={"Ids": inputs, 'W': w},
outputs={"Outputs": outputs},
attrs={
"is_distributed": is_distributed,
"padding_idx": padding_idx,
"table_id": table_id,
"is_test": True,
"lookup_table_version": op_type,
},
)
else:
raise ValueError(
"something wrong with Fleet, submit a issue is recommended"
)
pull_sparse_ops = _get_pull_sparse_ops(program)
warnings.warn(
"lookup_table will be forced to test mode when use DistributedInfer"
)
_pull_sparse_fuse(program, pull_sparse_ops)
return program
covert_program = distributed_ops_pass(main_program)
return covert_program
@@ -0,0 +1,711 @@
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
# Copyright 2018 The OpenAI Team Authors and HuggingFace Inc. team.
# Copyright (c) 2018, NVIDIA CORPORATION. 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 os
import paddle
from paddle import distributed as dist
from paddle.autograd import PyLayer
from paddle.base import core
from paddle.distributed import fleet
from paddle.distributed.fleet.meta_parallel import get_rng_state_tracker
from paddle.distributed.fleet.utils.hybrid_parallel_util import (
fused_allreduce_gradients_with_group,
)
from paddle.distributed.flex_checkpoint.dcp.sharded_weight import (
build_sharded_state_dict,
)
from paddle.nn import (
Layer,
functional as F,
)
from .log_util import logger
####################################################
# #
# Distributed Communication Operator #
# #
####################################################
def scatter(input):
hcg = fleet.get_hybrid_communicate_group()
group = hcg.get_model_parallel_group()
parallelism = group.nranks
rank = group.rank
seq_len = input.shape[0]
assert seq_len % parallelism == 0, (
f"Input sequence length {seq_len} can't be divided exactly by sequence parallelism {parallelism}"
)
interval = seq_len // parallelism
input = paddle.slice(
input, axes=[0], starts=[interval * rank], ends=[interval * (rank + 1)]
)
return input
def all_gather(input):
hcg = fleet.get_hybrid_communicate_group()
group = hcg.get_model_parallel_group()
parallelism = group.nranks
output_shape = input.shape
output_shape[0] = output_shape[0] * parallelism
output = paddle.empty(shape=output_shape, dtype=input.dtype)
group.process_group.all_gather(input, output).wait()
return output
def reduce_scatter(input):
hcg = fleet.get_hybrid_communicate_group()
group = hcg.get_model_parallel_group()
parallelism = group.nranks
output_shape = input.shape
assert input.shape[0] % parallelism == 0, (
f"Input sequence length {input.shape[0]} can't be divided exactly by sequence parallelism {parallelism}"
)
output_shape[0] = output_shape[0] // parallelism
output = paddle.empty(shape=output_shape, dtype=input.dtype)
dist.stream.reduce_scatter(
output, input, op=dist.ReduceOp.SUM, group=group, sync_op=True
)
return output
class ScatterOp(PyLayer):
# input shape: [s, b, h], n is mp parallelism
# after forward shape: [s/n, b, h]
@staticmethod
def forward(ctx, input):
return scatter(input)
@staticmethod
def backward(ctx, grad):
return all_gather(grad)
class GatherOp(PyLayer):
# input shape: [s/n, b, h], n is mp parallelism
# after forward shape: [s, b, h]
@staticmethod
def forward(ctx, input):
return all_gather(input)
@staticmethod
def backward(ctx, grad):
return scatter(grad)
# All gather along the first dim during forward pass
# All reduce and scatter along the first dim during backward pass
class AllGatherOp(PyLayer):
# input shape: [s/n, b, h], n is mp parallelism
# after forward shape: [s, b, h]
@staticmethod
def forward(ctx, input):
return all_gather(input)
# grad shape: [s, b, h], n is mp parallelism
# after forward shape: [s/n, b, h]
@staticmethod
def backward(ctx, grad):
return reduce_scatter(grad)
# All reduce and scatter along the first dim during forward pass
# All gather along the first dim during backward pass
class ReduceScatterOp(PyLayer):
# input shape: [s, b, h], n is mp parallelism
# after forward shape: [s/n, b, h]
@staticmethod
def forward(ctx, input):
return reduce_scatter(input)
# grad shape: [s/n, b, h], n is mp parallelism
# after forward shape: [s, b, h]
@staticmethod
def backward(ctx, grad):
return all_gather(grad)
###################################################
# #
# Modified Parallel Linear Operator #
# #
###################################################
def mark_as_sequence_parallel_parameter(parameter):
parameter.sequence_parallel = True
def is_sequence_parallel_parameter(parameter):
return getattr(parameter, "sequence_parallel", False)
def create_fused_allreduce_gradient_hook(parameter_list, accumulation_steps):
hcg = fleet.get_hybrid_communicate_group()
group = hcg.get_model_parallel_group()
step = [0]
accumulation_steps *= len(parameter_list)
def __impl__(grad):
step[0] += 1
if step[0] == accumulation_steps:
step[0] = 0
fused_allreduce_gradients_with_group(
parameter_list, group=group, scale=1.0
)
return grad
return __impl__
def create_non_fused_allreduce_gradient_hook(param, accumulation_steps):
hcg = fleet.get_hybrid_communicate_group()
pg = hcg.get_model_parallel_group().process_group
step = [0]
@paddle.autograd.no_grad()
def __impl__():
step[0] += 1
if (step[0] % accumulation_steps) == 0:
if hasattr(param, "main_grad"):
pg.allreduce(param.main_grad).wait()
else:
pg.allreduce(param.grad).wait()
return __impl__
def register_sequence_parallel_allreduce_hooks(
model, accumulation_steps, fuse_sequence_parallel_allreduce
):
if accumulation_steps <= 0 or not paddle.distributed.is_initialized():
return
mp_group = fleet.get_hybrid_communicate_group().get_model_parallel_group()
if mp_group.nranks <= 1:
return
params = []
for p in model.parameters():
if is_sequence_parallel_parameter(p) and not p.stop_gradient:
params.append(p)
if fuse_sequence_parallel_allreduce:
hook = create_fused_allreduce_gradient_hook(params, accumulation_steps)
for p in params:
p._register_backward_hook(hook)
else:
for p in params:
hook = create_non_fused_allreduce_gradient_hook(
p, accumulation_steps
)
p._register_backward_hook(hook)
def is_fused_matmul_bias_supported():
if (
paddle.is_compiled_with_cuda()
and not paddle.is_compiled_with_rocm()
or paddle.is_compiled_with_xpu()
):
return hasattr(core.eager.ops.legacy, "fused_gemm_epilogue")
else:
return False
def is_fused_linear_param_grad_add_supported():
if (
paddle.is_compiled_with_cuda() and not paddle.is_compiled_with_rocm()
) or paddle.is_compiled_with_xpu():
return hasattr(paddle._C_ops, 'fused_linear_param_grad_add')
else:
return False
_raise_cuda_env_unset_warning_for_sp = True
def _check_environment_for_overlap():
if int(os.getenv("CUDA_DEVICE_MAX_CONNECTIONS", "0")) != 1:
global _raise_cuda_env_unset_warning_for_sp
if _raise_cuda_env_unset_warning_for_sp:
logger.warning(
"You set mp_async_allreduce=True or recompute_allgather=True, but you forget to set environment "
"variable CUDA_DEVICE_MAX_CONNECTIONS=1, which may leads to performance "
"loss. Try to export CUDA_DEVICE_MAX_CONNECTIONS=1 for better performance."
)
_raise_cuda_env_unset_warning_for_sp = False
# Using small operation to preempt GPU SMs for all_gather or reduce_scatter to achieve overlap.
tmp = paddle.ones([512])
class SPInnerOverlapLinear(paddle.autograd.PyLayer):
@staticmethod
def forward(
ctx,
x,
weight,
bias,
fuse_matmul_bias,
recompute_allgather,
mp_fused_linear_param_grad_add,
model_parallel_group,
):
ctx.recompute_allgather = recompute_allgather
ctx.mp_fused_linear_param_grad_add = mp_fused_linear_param_grad_add
ctx.model_parallel_group = model_parallel_group
world_size = model_parallel_group.nranks
input_parallel = all_gather(x)
if not recompute_allgather:
ctx.save_for_backward(x, weight, bias, input_parallel)
else:
ctx.save_for_backward(x, weight, bias)
if not fuse_matmul_bias:
output = paddle._C_ops.linear(input_parallel, weight, bias)
else:
output = paddle._legacy_C_ops.fused_gemm_epilogue(
input_parallel, weight, bias
)
return output
@staticmethod
def backward(ctx, dy):
group = ctx.model_parallel_group
parallelism = group.nranks
if not ctx.recompute_allgather:
x, weight, bias, input_parallel = ctx.saved_tensor()
else:
x, weight, bias = ctx.saved_tensor()
# all-gather x
input_parallel_shape = x.shape
input_parallel_shape[0] = input_parallel_shape[0] * parallelism
input_parallel = paddle.empty(
shape=input_parallel_shape, dtype=x.dtype
)
allgather_task = dist.all_gather(
input_parallel, x, group=group, sync_op=False
)
# compute dx
_check_environment_for_overlap()
if dy.dtype == weight.dtype:
dinput_parallel = paddle.matmul(dy, weight, transpose_y=True)
else:
dinput_parallel = paddle.matmul(
dy, paddle.cast(weight, dtype=dy.dtype), transpose_y=True
)
assert dinput_parallel.shape[0] % parallelism == 0, (
f"Input sequence length {dinput_parallel.shape[0]} can't be divided exactly by sequence parallelism {parallelism}"
)
if ctx.recompute_allgather:
# wait the finish of all-gather of x
allgather_task.wait()
# reduce-scatter dx
dx_shape = dinput_parallel.shape
dx_shape[0] = dx_shape[0] // parallelism
dx = paddle.empty(shape=dx_shape, dtype=dinput_parallel.dtype)
task = dist.stream.reduce_scatter(
dx,
dinput_parallel,
op=dist.ReduceOp.SUM,
group=group,
sync_op=False,
)
# compute dw and dbias
_check_environment_for_overlap()
if ctx.mp_fused_linear_param_grad_add:
if not is_fused_linear_param_grad_add_supported():
raise NotImplementedError(
"You set mp_fused_linear_param_grad_add=True, "
"however, the paddle you are using not support this operation. "
"Please unset fused_linear_param_grad_add or use paddle compiled "
"with cuda 11.6 or higher."
)
if bias is None:
if hasattr(weight, "main_grad"):
(
weight.main_grad,
_,
) = paddle._C_ops.fused_linear_param_grad_add(
input_parallel, dy, weight.main_grad, None, True, False
)
task.wait()
return dx, None
else:
if weight.grad is not None:
(
weight.grad,
_,
) = paddle._C_ops.fused_linear_param_grad_add(
input_parallel, dy, weight.grad, None, False, False
)
task.wait()
return dx, None
else:
(
dw,
_,
) = paddle._C_ops.fused_linear_param_grad_add(
input_parallel, dy, None, None, False, False
)
task.wait()
return dx, dw
if hasattr(weight, "main_grad") and hasattr(bias, "main_grad"):
(
weight.main_grad,
bias.main_grad,
) = paddle._C_ops.fused_linear_param_grad_add(
input_parallel,
dy,
weight.main_grad,
bias.main_grad,
True,
True,
)
task.wait()
return dx, None, None
else:
if weight.grad is not None:
assert bias.grad is not None
(
weight.grad,
bias.grad,
) = paddle._C_ops.fused_linear_param_grad_add(
input_parallel, dy, weight.grad, bias.grad, False, True
)
task.wait()
return dx, None, None
else:
# When main_grad is not enabled and gradient_accumulation is used, the grad is not initialized for the first acc step.
(
dw,
dbias,
) = paddle._C_ops.fused_linear_param_grad_add(
input_parallel, dy, None, None, False, True
)
task.wait()
return dx, dw, dbias
else:
dy = dy.reshape([-1, dy.shape[-1]])
dw = paddle.matmul(
input_parallel.reshape([-1, input_parallel.shape[-1]]),
dy,
transpose_x=True,
)
if bias is None:
task.wait()
return dx, dw
else:
dbias = paddle.sum(dy, axis=0)
task.wait()
return dx, dw, dbias
class ColumnSequenceParallelLinear(Layer):
def __init__(
self,
in_features,
out_features,
weight_attr=None,
has_bias=None,
gather_output=True,
fuse_matmul_bias=False,
mp_group=None,
name=None,
):
super().__init__()
hcg = fleet.get_hybrid_communicate_group()
self.model_parallel_group = (
hcg.get_model_parallel_group() if mp_group is None else mp_group
)
self.world_size = (
hcg.get_model_parallel_group().nranks
if mp_group is None
else mp_group.nranks
)
assert self.world_size > 1, (
"tensor parallel degree must be greater than 1 in sequence parallel"
)
self._name = name
self.is_mp = self.world_size > 1
assert gather_output is False, (
"If sequence_parallel is True, gather_output is False"
)
self.gather_output = gather_output
assert out_features % self.world_size == 0, (
f"Number of column of the weight for linear ({out_features}) must be"
f" divisible by model parallel size ({self.world_size})"
)
self.output_size_per_partition = out_features // self.world_size
self._weight_attr = weight_attr
self._dtype = self._helper.get_default_dtype()
if self.is_mp and paddle.in_dynamic_mode():
with get_rng_state_tracker().rng_state():
self.weight = self.create_parameter(
shape=[in_features, self.output_size_per_partition],
attr=self._weight_attr,
dtype=self._dtype,
is_bias=False,
)
else:
self.weight = self.create_parameter(
shape=[in_features, self.output_size_per_partition],
attr=self._weight_attr,
dtype=self._dtype,
is_bias=False,
)
self.weight.is_distributed = True if self.is_mp else False
self.fuse_matmul_bias = fuse_matmul_bias
if has_bias:
# initialize bias to zero like Megatron
self.bias = self.create_parameter(
shape=[self.output_size_per_partition],
attr=paddle.nn.initializer.Constant(value=0.0),
dtype=self._dtype,
is_bias=True,
)
self.bias.is_distributed = True if self.is_mp else False
else:
self.bias = None
if self.weight.is_distributed:
self.weight.split_axis = 1
if has_bias and self.bias.is_distributed:
self.bias.split_axis = 0
self.linear = F.linear
if fuse_matmul_bias:
if not is_fused_matmul_bias_supported():
raise NotImplementedError(
"You set fuse_matmul_bias=True in ColumnSequenceParallelLinear, "
"however, the paddle you are using not support this operation. "
"Please set fuse_matmul_bias=False or use paddle compiled "
"with cuda 11.6 or higher, or use xpu version."
)
from paddle.incubate.nn.functional import fused_linear
self.linear = fused_linear
mp_configs = fleet.fleet._user_defined_strategy.hybrid_configs[
"mp_configs"
]
self.mp_async_allreduce = mp_configs.mp_async_allreduce
self.sp_async_reduce_scatter = mp_configs.sp_async_reduce_scatter
self.recompute_allgather = mp_configs.recompute_allgather
self.mp_fused_linear_param_grad_add = (
self.mp_async_allreduce
and mp_configs.mp_fused_linear_param_grad_add
)
def forward(self, x):
# sequence parallel is same as tensor parallel, if sequence parallel is true, input shape is [s, b, h], else input shape is [b, s, h]
if self.sp_async_reduce_scatter:
output = SPInnerOverlapLinear.apply(
x,
self.weight,
self.bias,
self.fuse_matmul_bias,
self.recompute_allgather,
self.mp_fused_linear_param_grad_add,
self.model_parallel_group,
)
else:
if self.is_mp:
input_parallel = AllGatherOp.apply(x)
else:
input_parallel = x
output = self.linear(
input_parallel, self.weight, self.bias, name=self._name
)
return output
def sharded_state_dict(
self,
structured_name_prefix: str = "",
):
state_dict = self.state_dict(structured_name_prefix="")
return build_sharded_state_dict(
state_dict, {"weight": 1, "bias": 0}, structured_name_prefix
)
class MPScale(PyLayer):
@staticmethod
def forward(ctx, x, mp_degree):
out = paddle.scale(x, 1.0 / mp_degree)
return out
@staticmethod
def backward(ctx, dout):
return dout
class RowSequenceParallelLinear(Layer):
def __init__(
self,
in_features,
out_features,
weight_attr=None,
has_bias=True,
input_is_parallel=False,
fuse_matmul_bias=False,
mp_group=None,
name=None,
):
super().__init__()
self.in_features = in_features
self.out_features = out_features
assert input_is_parallel is True, (
"If sequence_parallel is True, input_is_parallel should be true."
)
self.input_is_parallel = input_is_parallel
self._weight_attr = weight_attr
self._dtype = self._helper.get_default_dtype()
self._name = name
hcg = fleet.get_hybrid_communicate_group()
self.model_parallel_group = (
hcg.get_model_parallel_group() if mp_group is None else mp_group
)
self.world_size = (
hcg.get_model_parallel_group().nranks
if mp_group is None
else mp_group.nranks
)
self.rank = (
hcg.get_model_parallel_group().rank
if mp_group is None
else mp_group.rank
)
self.is_mp = self.world_size > 1
assert in_features % self.world_size == 0, (
f"Number of row of the weight for linear ({in_features}) must be"
f" divisible by model parallel size ({self.world_size})"
)
self.input_size_per_partition = in_features // self.world_size
if self.is_mp and paddle.in_dynamic_mode():
with get_rng_state_tracker().rng_state():
self.weight = self.create_parameter(
shape=[self.input_size_per_partition, self.out_features],
attr=self._weight_attr,
dtype=self._dtype,
is_bias=False,
)
else:
self.weight = self.create_parameter(
shape=[self.input_size_per_partition, self.out_features],
attr=self._weight_attr,
dtype=self._dtype,
is_bias=False,
)
self.weight.is_distributed = True if self.is_mp else False
# if sequence parallel is true,
# register hook to all_reduce gradient of weight and bias
if has_bias:
self.bias = self.create_parameter(
shape=[self.out_features],
attr=paddle.nn.initializer.Constant(value=0.0),
dtype=self._dtype,
is_bias=True,
)
if self.is_mp:
mark_as_sequence_parallel_parameter(self.bias)
else:
self.bias = None
if self.weight.is_distributed:
self.weight.split_axis = 0
self.linear = F.linear
self.mp_scale = None
if fuse_matmul_bias:
if not is_fused_matmul_bias_supported():
raise NotImplementedError(
"You set fuse_matmul_bias=True in RowParallelLinear, "
"however, the paddle you are using not support this operation. "
"Please set fuse_matmul_bias=False or use paddle compiled "
"with cuda 11.6 or higher."
)
from paddle.incubate.nn.functional import fused_linear
self.linear = fused_linear
if self.is_mp and has_bias:
self.mp_scale = MPScale.apply
def forward(self, x):
input_parallel = x
if self.is_mp:
if self.mp_scale is not None:
bias = self.mp_scale(self.bias, self.world_size)
else:
bias = None
output_parallel = self.linear(
input_parallel, self.weight, bias, name=self._name
)
output_ = ReduceScatterOp.apply(output_parallel)
# if self.bias is not none, sequence parallel will use
# register_hook to all_reduce self.bias
if bias is None and self.bias is not None:
output = output_ + self.bias
else:
output = output_
else:
output = self.linear(
input_parallel, self.weight, self.bias, name=self._name
)
return output
def sharded_state_dict(
self,
structured_name_prefix: str = "",
):
state_dict = self.state_dict(structured_name_prefix="")
return build_sharded_state_dict(
state_dict, {"weight": 0}, structured_name_prefix
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,351 @@
# 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.
import logging
import paddle
logger = logging.getLogger(__name__)
formatter = logging.Formatter(
fmt='%(asctime)s %(levelname)-8s %(message)s', datefmt='%Y-%m-%d %H:%M:%S'
)
ch = logging.StreamHandler()
ch.setFormatter(formatter)
logger.addHandler(ch)
from paddle.base import core
from paddle.distributed.fleet.meta_optimizers.common import OP_ROLE_KEY
from paddle.static import Parameter
_supported_optimizer_type = [
"adam",
"adamax",
"adamw",
"decayed_adagrad",
"momentum",
"dgc_momentum",
"lars_momentum",
"merged_momentum",
"lamb",
"sgd",
]
def tensor_parallel_sync_filter_fn(
param, pos_emb=True, layer_norm=True, bias=True
):
"""
Layer filter function for tensor parallelism transformer.
In tensor parallelism of transformer like model, there is 4 kind of param
that are supposed to be the same in all tensor parallel peers:
* position embedding
* scale of layer norm
* bias of layer norm
* bias of row parallel linear
set corresponding input args to select specific layers.
NOTE adopting the param name pattern for different transformer blocks.
"""
p_name = param.name
if pos_emb and p_name.startswith("pos_embedding"):
return True
elif layer_norm and p_name.endswith("_layer_norm_bias"):
return True
elif layer_norm and p_name.endswith("_layer_norm_scale"):
return True
elif bias and ".b_" in p_name and (param.is_distributed is False):
return True
else:
return False
def resolute_tensor_parallel_ring_id(program):
ops = program.global_block().ops
ring_id = None
for op in ops:
if op.type == "c_identity":
if ring_id is None:
ring_id = int(op.attr("ring_id"))
else:
assert ring_id == int(op.attr("ring_id")), (
"Found two different ring_id for Tensor Parallel: ring_id={} and ring_id={}.".format(
ring_id, int(op.attr("ring_id"))
)
)
assert ring_id is not None, "Could NOT found ring_id for Tensor Parallel."
return ring_id
def copy_parameters(block_, params):
for param in params:
new_p = Parameter(
block=block_,
shape=param.shape,
dtype=param.dtype,
type=param.type,
lod_level=(
param.lod_level
if param.type == core.VarDesc.VarType.DENSE_TENSOR
else None
),
stop_gradient=param.stop_gradient,
trainable=param.trainable,
optimize_attr=param.optimize_attr,
regularizer=param.regularizer,
error_clip=param.error_clip,
name=param.name,
)
assert param.is_distributed is False, (
f"Try to sync Distributed Parameter: {param}"
)
new_p.is_distributed = False
block_.vars[new_p.name] = new_p
def insert_sync_op(
block, idx, tp_degree, sync_mode, sync_ring_id, src_rank, varname, op_role
):
if sync_mode == "broadcast":
block._insert_op_without_sync(
idx,
type='broadcast',
inputs={'x': varname},
outputs={'out': varname},
attrs={
'ring_id': sync_ring_id,
'root': src_rank,
OP_ROLE_KEY: op_role,
},
)
elif sync_mode == "average":
block._insert_op_without_sync(
idx,
type='scale',
inputs={'X': varname},
outputs={'Out': varname},
attrs={'scale': 1.0 / tp_degree, OP_ROLE_KEY: op_role},
)
block._insert_op_without_sync(
idx,
type='all_reduce',
inputs={'x': varname},
outputs={'out': varname},
attrs={
'ring_id': sync_ring_id,
'reduce_type': paddle.distributed.ReduceOp.SUM,
OP_ROLE_KEY: op_role,
},
)
else:
raise NotImplementedError(
f'Sync mode of [{sync_mode}] is NOT supported.'
)
def insert_synchronization(
block,
params_to_sync,
tp_degree,
sync_ring_id,
sync_param,
sync_grad,
sync_moment,
sync_mode,
src_rank,
):
unsync_param_names = [p.name for p in params_to_sync]
for idx, op in reversed(list(enumerate(block.ops))):
if op.type in _supported_optimizer_type:
assert "Param" in op.input_names
assert len(op.input("Param")) == 1
param_name = op.input("Param")[0]
op_role = op.attr(OP_ROLE_KEY)
if param_name in unsync_param_names:
unsync_param_names.remove(param_name)
# Param sync after opt
if sync_param:
assert (
"ParamOut" in op.output_names
and op.output("ParamOut")[0] == param_name
)
insert_sync_op(
block,
idx + 1,
tp_degree,
sync_mode,
sync_ring_id,
src_rank,
param_name,
op_role,
)
if (
"MasterParamOut" in op.output_names
and len(op.output("MasterParamOut")) == 1
):
sync_var = op.output("MasterParamOut")[0]
insert_sync_op(
block,
idx + 1,
tp_degree,
sync_mode,
sync_ring_id,
src_rank,
sync_var,
op_role,
)
# Moment sync after opt
if sync_moment:
if (
"Moment1Out" in op.output_names
and len(op.output("Moment1Out")) == 1
):
sync_var = op.output("Moment1Out")[0]
insert_sync_op(
block,
idx + 1,
tp_degree,
sync_mode,
sync_ring_id,
src_rank,
sync_var,
op_role,
)
if (
"Moment2Out" in op.output_names
and len(op.output("Moment2Out")) == 1
):
sync_var = op.output("Moment2Out")[0]
insert_sync_op(
block,
idx + 1,
tp_degree,
sync_mode,
sync_ring_id,
src_rank,
sync_var,
op_role,
)
# Grad sync before opt
if sync_grad:
assert (
"Grad" in op.input_names and len(op.input("Grad")) == 1
)
sync_var = op.input("Grad")[0]
insert_sync_op(
block,
idx,
tp_degree,
sync_mode,
sync_ring_id,
src_rank,
sync_var,
op_role,
)
assert len(unsync_param_names) == 0, (
f"The following param is unsync by some error: {unsync_param_names}"
)
def add_extra_synchronization(
program,
params_filter_fn=tensor_parallel_sync_filter_fn,
tp_degree=8,
sync_mode="broadcast",
sync_param=True,
sync_grad=False,
sync_moment=False,
src_rank=0,
sync_ring_id=None,
):
"""
Inplace add extra synchronization for input program.
program(Paddle.Program): distributed train program.
params_filter_fn(callable): function to filter out parameter for synchronization.
sync_mode(string): select from
"broadcast": parameter is sync by broadcasted from 'src_rank' to all other ranks.
"average": parameter is sync by average among all ranks
src_rank(int): the src used in broadcast sync_mode.
sync_param(bool): extra synchronize parameters.
sync_grad(bool): extra synchronize gradients.
sync_grad(bool): extra synchronize optimizer momentum.
sync_ring_id(int): communicator id use for synchronization, if it is None, use the ring_id of tensor parallel.
"""
logger.info("Constructing Extra Parameter Synchronization.")
logger.info(
f"Tensor Parallel Degree: {tp_degree}, Synchronization mode: {sync_mode}"
)
# adopt for pipeline opt
if program._pipeline_opt is not None:
assert program._pipeline_opt['section_program'] is not None, (
"Pipeline is enable but section_program is None"
)
program = program._pipeline_opt['section_program']
# step1: collect the param that need to be sync
params_to_sync = []
# TODO support multiple blocks with different parameter.
all_params = program.global_block().all_parameters()
for param in all_params:
if params_filter_fn(param):
params_to_sync.append(param)
logger.info(
"The following param are going to be synchronization everytime the optimizer update phase of the program is run: "
)
logger.info([p.name for p in params_to_sync])
# step2: resolute synchronization communicator group (ring_id)
if sync_ring_id is None:
sync_ring_id = resolute_tensor_parallel_ring_id(program)
# step3: insert synchronization
# TODO support gradient merge with different update block
block = program.global_block()
insert_synchronization(
block,
params_to_sync,
tp_degree,
sync_ring_id,
sync_param,
sync_grad,
sync_moment,
sync_mode,
src_rank,
)
@@ -0,0 +1,134 @@
# 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.
import os
import time
import paddle
from paddle.base import core
_GLOBAL_TIMERS = None
def is_timer_initialized():
return _GLOBAL_TIMERS is not None
def _ensure_var_is_not_initialized(var, name):
"""Make sure the input variable is not None."""
assert var is None, f"{name} has been already initialized."
def _ensure_var_is_initialized(var, name):
"""Make sure the input variable is not None."""
assert var is not None, f"{name} is not initialized."
def get_timers():
_ensure_var_is_initialized(_GLOBAL_TIMERS, "timers")
return _GLOBAL_TIMERS
def set_timers():
"""Initialize timers."""
global _GLOBAL_TIMERS
_ensure_var_is_not_initialized(_GLOBAL_TIMERS, "timers")
_GLOBAL_TIMERS = Timers()
class _Timer:
"""Timer."""
def __init__(self, name):
self.name = name
self.elapsed_ = 0.0
self.started_ = False
self.start_time = time.time()
def start(self):
"""Start the timer."""
assert not self.started_, "timer has already started"
paddle.device.cuda.synchronize()
self.start_time = time.time()
self.started_ = True
def stop(self):
"""Stop the timers."""
assert self.started_, "timer is not started."
paddle.device.cuda.synchronize()
self.elapsed_ += time.time() - self.start_time
self.started_ = False
def reset(self):
"""Reset timer."""
self.elapsed_ = 0.0
self.started_ = False
def elapsed(self, reset=True):
"""Calculate the elapsed time."""
started_ = self.started_
# If the timing in progress, end it first.
if self.started_:
self.stop()
# Get the elapsed time.
elapsed_ = self.elapsed_
# Reset the elapsed time
if reset:
self.reset()
# If timing was in progress, set it back.
if started_:
self.start()
return elapsed_
class _GPUEventTimer:
"""GPUEventTimer."""
def __init__(self, name):
self.name = name
dev_id = int(os.getenv("FLAGS_selected_gpus", "0"))
self.timer = core.GPUEventTimer(core.CUDAPlace(dev_id))
def __getattr__(self, name):
return getattr(self.timer, name)
class Timers:
"""Group of timers."""
def __init__(self):
self.timers = {}
def __call__(self, name, use_event=False):
clazz = _GPUEventTimer if use_event else _Timer
timer = self.timers.get(name)
if timer is None:
timer = clazz(name)
self.timers[name] = timer
else:
assert type(timer) == clazz, (
f"Invalid timer type: {clazz} vs {type(timer)}"
)
return timer
def log(self, names, normalizer=1.0, reset=True):
"""Log a group of timers."""
assert normalizer > 0.0
string = "time (ms)"
for name in names:
elapsed_time = (
self.timers[name].elapsed(reset=reset) * 1000.0 / normalizer
)
string += f" | {name}: {elapsed_time:.2f}"
print(string, flush=True)