chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
# Copyright (c) 2024 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
__all__ = []
|
||||
@@ -0,0 +1,391 @@
|
||||
# Copyright (c) 2024 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import paddle
|
||||
import paddle.distributed as dist
|
||||
from paddle.distributed.auto_parallel.ring_attention import (
|
||||
shard_seq_load_balance,
|
||||
)
|
||||
|
||||
from .tensor_parallel import PlanBase
|
||||
|
||||
|
||||
class PrepareContextParallel(PlanBase):
|
||||
"""
|
||||
|
||||
Prepare Input for context parallel optimizations.
|
||||
|
||||
This will work for Layer that calls like whole-llama Layer which is the first layer in the network.
|
||||
|
||||
Users can set backend='p2p/all2all' for different context parallel strategys.
|
||||
|
||||
backend='p2p' will use Ring FlashAttention strategy which segments input with balance in the sequence dimension before whole-llama Layer.
|
||||
backend='all2all' will use Deepspeed Ulysses strategy(Paddle SegmentParallel strategy) which segments input in the sequence dimension before whole-llama Layer.
|
||||
|
||||
Args:
|
||||
backend (string): select strategy for context parallel, now support 'p2p' and 'all2all'.
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
|
||||
>>> class SDPALayer(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
...
|
||||
... def forward(self, q, k, v):
|
||||
... return paddle.nn.functional.scaled_dot_product_attention(q, k, v)
|
||||
>>>
|
||||
>>> class AttentionLayer(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.hidden_size = 64
|
||||
... self.num_key_value_heads = 10
|
||||
... self.head_dim = 64
|
||||
... self.sdpa = SDPALayer()
|
||||
... self.q = paddle.nn.Linear(
|
||||
... self.hidden_size,
|
||||
... self.hidden_size,
|
||||
... bias_attr=False,
|
||||
... )
|
||||
... self.k = paddle.nn.Linear(
|
||||
... self.hidden_size,
|
||||
... self.num_key_value_heads * self.head_dim,
|
||||
... bias_attr=False,
|
||||
... )
|
||||
... self.v = paddle.nn.Linear(
|
||||
... self.hidden_size,
|
||||
... self.num_key_value_heads * self.head_dim,
|
||||
... bias_attr=False,
|
||||
... )
|
||||
...
|
||||
... def forward(self, input):
|
||||
... q = self.q(input)
|
||||
... k = self.k(input)
|
||||
... v = self.v(input)
|
||||
... return self.sdpa(q, k, v)
|
||||
>>>
|
||||
>>> class LlamaLayer(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.attention = AttentionLayer()
|
||||
...
|
||||
... def forward(self, input, label):
|
||||
... return self.attention(input)
|
||||
>>>
|
||||
>>> class LlamaForCausalLayer(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.llama = LlamaLayer()
|
||||
... self.weight = self.create_parameter(shape=[64, 1024])
|
||||
... self.loss_func = paddle.nn.CrossEntropyLoss()
|
||||
...
|
||||
... def forward(self, input, label):
|
||||
... out = self.llama(input, label)
|
||||
... logits = paddle.matmul(out, self.weight)
|
||||
... loss = self.loss_func(logits, label)
|
||||
... return logits
|
||||
>>>
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> layer = LlamaForCausalLayer()
|
||||
>>> mp_config = {
|
||||
... 'llama': dist.PrepareContextParallel('p2p'),
|
||||
... 'sdpa': dist.ContextParallel('p2p'),
|
||||
... }
|
||||
"""
|
||||
|
||||
def __init__(self, backend: str = 'p2p') -> None:
|
||||
super().__init__()
|
||||
self.backend = backend
|
||||
assert self.backend in [
|
||||
'p2p',
|
||||
'all2all',
|
||||
], f"backend must be 'p2p' or 'all2all', but got {self.backend}"
|
||||
|
||||
def all2all_split_input_pre_hook(self, process_mesh):
|
||||
def shard_tensor(input_tensor, seq_dim):
|
||||
cp_index = process_mesh.dim_names.index('sep')
|
||||
placements = input_tensor.placements
|
||||
if placements is None:
|
||||
placements = [
|
||||
dist.Replicate() for _ in range(len(process_mesh.shape))
|
||||
]
|
||||
# split sequence dim
|
||||
placements[cp_index] = dist.Shard(seq_dim)
|
||||
reshard_input = dist.reshard(input_tensor, process_mesh, placements)
|
||||
return reshard_input
|
||||
|
||||
def all2all_split_input(layer, args):
|
||||
cp_index = process_mesh.dim_names.index('sep')
|
||||
cp_degree = process_mesh.shape[cp_index]
|
||||
# check input_ids
|
||||
if isinstance(args, (list, tuple)):
|
||||
all_args = []
|
||||
for input_tensor in args:
|
||||
assert input_tensor.is_dist(), (
|
||||
"Input tensor must be a distributed tensor."
|
||||
)
|
||||
assert len(input_tensor.shape) == 2, (
|
||||
f"input_ids should be [batch_size, seq_len], but got {input_tensor.shape}"
|
||||
)
|
||||
_, seq_len = input_tensor.shape
|
||||
assert seq_len % cp_degree == 0, (
|
||||
f"sequence length {seq_len} must be divisible by cp degree {cp_degree}"
|
||||
)
|
||||
reshard_input = shard_tensor(input_tensor, 1)
|
||||
all_args.append(reshard_input)
|
||||
new_args = tuple(all_args)
|
||||
return new_args
|
||||
elif isinstance(args, paddle.Tensor):
|
||||
reshard_input = shard_tensor(args, 1)
|
||||
return reshard_input
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported argument type: {type(args)}. Expected list of tensors or single tensor."
|
||||
)
|
||||
|
||||
return all2all_split_input
|
||||
|
||||
def p2p_split_input_pre_hook(self, process_mesh):
|
||||
def p2p_split_input(layer, args):
|
||||
cp_index = process_mesh.dim_names.index('sep')
|
||||
cp_degree = process_mesh.shape[cp_index]
|
||||
if isinstance(args, (list, tuple)):
|
||||
all_args = []
|
||||
for input_tensor in args:
|
||||
# check input_ids
|
||||
assert input_tensor.is_dist(), (
|
||||
"Input tensor must be a distributed tensor."
|
||||
)
|
||||
assert len(input_tensor.shape) == 2, (
|
||||
f"input_ids should be [batch_size, seq_len], but got {input_tensor.shape}"
|
||||
)
|
||||
placements = input_tensor.placements
|
||||
if placements is None:
|
||||
placements = [
|
||||
dist.Replicate()
|
||||
for _ in range(len(process_mesh.shape))
|
||||
]
|
||||
assert placements[cp_index] == dist.Replicate(), (
|
||||
"Input tensor must be a replicated tensor in cp mesh."
|
||||
)
|
||||
reshard_input = shard_seq_load_balance(input_tensor, 1)
|
||||
all_args.append(reshard_input)
|
||||
new_args = tuple(all_args)
|
||||
return new_args
|
||||
elif isinstance(args, paddle.Tensor):
|
||||
reshard_input = shard_seq_load_balance(input_tensor, 1)
|
||||
return reshard_input
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported argument type: {type(args)}. Expected list of tensors or single tensor."
|
||||
)
|
||||
|
||||
return p2p_split_input
|
||||
|
||||
def apply(self, layer, process_mesh, shard_param_list):
|
||||
if self.backend == 'all2all':
|
||||
# Deepspeed Ulysses
|
||||
layer.register_forward_pre_hook(
|
||||
self.all2all_split_input_pre_hook(process_mesh)
|
||||
)
|
||||
elif self.backend == 'p2p':
|
||||
# Ring FlashAttention
|
||||
layer.register_forward_pre_hook(
|
||||
self.p2p_split_input_pre_hook(process_mesh)
|
||||
)
|
||||
else:
|
||||
logging.warning(
|
||||
f'{self.backend} is not supported backend for context parallel'
|
||||
)
|
||||
|
||||
|
||||
class ContextParallel(PlanBase):
|
||||
"""
|
||||
|
||||
Applies context parallel optimizations to the attention layer.
|
||||
|
||||
This will work for Layer that calls paddle.nn.functional.scaled_dot_product_attention).
|
||||
|
||||
Users can set backend='p2p/all2all' for different context parallel strategys.
|
||||
|
||||
backend='p2p' will use Ring FlashAttention strategy which segments q/k/v in the sequence dimension and communicates k/v between ranks.
|
||||
backend='all2all' will use Deepspeed Ulysses strategy(Paddle SegmentParallel strategy) which inserts all2all before and after sdpa compute.
|
||||
|
||||
Note:
|
||||
|
||||
|
||||
Args:
|
||||
backend (string): select strategy for context parallel, now support 'p2p' and 'all2all'.
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
|
||||
>>> class SDPALayer(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
...
|
||||
... def forward(self, q, k, v):
|
||||
... return paddle.nn.functional.scaled_dot_product_attention(q, k, v)
|
||||
>>>
|
||||
>>> class AttentionLayer(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.hidden_size = 64
|
||||
... self.num_key_value_heads = 10
|
||||
... self.head_dim = 64
|
||||
... self.sdpa = SDPALayer()
|
||||
... self.q = paddle.nn.Linear(
|
||||
... self.hidden_size,
|
||||
... self.hidden_size,
|
||||
... bias_attr=False,
|
||||
... )
|
||||
... self.k = paddle.nn.Linear(
|
||||
... self.hidden_size,
|
||||
... self.num_key_value_heads * self.head_dim,
|
||||
... bias_attr=False,
|
||||
... )
|
||||
... self.v = paddle.nn.Linear(
|
||||
... self.hidden_size,
|
||||
... self.num_key_value_heads * self.head_dim,
|
||||
... bias_attr=False,
|
||||
... )
|
||||
...
|
||||
... def forward(self, input):
|
||||
... q = self.q(input)
|
||||
... k = self.k(input)
|
||||
... v = self.v(input)
|
||||
... return self.sdpa(q, k, v)
|
||||
>>>
|
||||
>>> class LlamaLayer(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.attention = AttentionLayer()
|
||||
...
|
||||
... def forward(self, input, label):
|
||||
... return self.attention(input)
|
||||
>>>
|
||||
>>> class LlamaForCausalLayer(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.llama = LlamaLayer()
|
||||
... self.weight = self.create_parameter(shape=[64, 1024])
|
||||
... self.loss_func = paddle.nn.CrossEntropyLoss()
|
||||
...
|
||||
... def forward(self, input, label):
|
||||
... out = self.llama(input, label)
|
||||
... logits = paddle.matmul(out, self.weight)
|
||||
... loss = self.loss_func(logits, label)
|
||||
... return logits
|
||||
>>>
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> layer = LlamaForCausalLayer()
|
||||
>>> mp_config = {
|
||||
... 'llama': dist.PrepareContextParallel('p2p'),
|
||||
... 'sdpa': dist.ContextParallel('p2p'),
|
||||
... }
|
||||
"""
|
||||
|
||||
def __init__(self, backend: str = 'p2p') -> None:
|
||||
super().__init__()
|
||||
self.backend = backend
|
||||
|
||||
def all2all_reshard_pre_hook(self, process_mesh):
|
||||
def all2all_reshard_hook(layer, args):
|
||||
cp_index = process_mesh.dim_names.index('sep')
|
||||
cp_degree = process_mesh.shape[cp_index]
|
||||
all_args = []
|
||||
for arg in args:
|
||||
# check q k v
|
||||
assert arg.is_dist(), f"arg {arg} must be a distributed tensor."
|
||||
assert len(arg.shape) == 3 or len(arg.shape) == 4
|
||||
placements = arg.placements
|
||||
assert placements[cp_index] == dist.Shard(1), (
|
||||
f"arg {arg} must be sharded in sequence dimension."
|
||||
)
|
||||
# reshard [batch_size,seq_len/sep,num_head,head_dim] -> [batch_size,seq_len,num_head/sep,head_dim]
|
||||
placements[cp_index] = dist.Shard(2)
|
||||
target_arg = dist.reshard(arg, process_mesh, placements)
|
||||
all_args.append(target_arg)
|
||||
new_args = tuple(all_args)
|
||||
return new_args
|
||||
|
||||
return all2all_reshard_hook
|
||||
|
||||
def all2all_reshard_post_hook(self, process_mesh):
|
||||
def all2all_reshard_hook(layer, input, output):
|
||||
cp_index = process_mesh.dim_names.index('sep')
|
||||
cp_degree = process_mesh.shape[cp_index]
|
||||
placements = output.placements
|
||||
assert output.is_dist(), (
|
||||
f"output {output} must be a distributed tensor."
|
||||
)
|
||||
assert len(output.shape) == 4 or len(output.shape) == 3
|
||||
assert placements[cp_index] == dist.Shard(2), (
|
||||
f"output {output} must be Shard(2) in sequence dimension."
|
||||
)
|
||||
# reshard [batch_size,seq_len,num_head/seq,head_dim] -> [batch_size,seq_len/sep,num_head,head_dim]
|
||||
placements[cp_index] = dist.Shard(1)
|
||||
target_output = dist.reshard(output, process_mesh, placements)
|
||||
return target_output
|
||||
|
||||
return all2all_reshard_hook
|
||||
|
||||
def p2p_reshard_pre_hook(self, process_mesh):
|
||||
def input_hook(layer, args, kwargs):
|
||||
cp_index = process_mesh.dim_names.index('sep')
|
||||
cp_degree = process_mesh.shape[cp_index]
|
||||
for arg in args:
|
||||
# check q k v
|
||||
assert arg.is_dist(), (
|
||||
"Input tensor must be a distributed tensor."
|
||||
)
|
||||
assert len(arg.shape) == 3 or len(arg.shape) == 4
|
||||
placements = arg.placements
|
||||
assert placements[cp_index] == dist.Shard(1), (
|
||||
f"arg {arg} must be Shard(1) in sequence dimension."
|
||||
)
|
||||
# edit kwarg backend to 'p2p'
|
||||
new_kwargs = kwargs
|
||||
new_kwargs['backend'] = 'p2p'
|
||||
return args, new_kwargs
|
||||
|
||||
return input_hook
|
||||
|
||||
def apply(self, layer, process_mesh, shard_param_list):
|
||||
if self.backend == 'all2all':
|
||||
# Deepspeed Ulysses
|
||||
layer.register_forward_pre_hook(
|
||||
self.all2all_reshard_pre_hook(process_mesh)
|
||||
)
|
||||
layer.register_forward_post_hook(
|
||||
self.all2all_reshard_post_hook(process_mesh)
|
||||
)
|
||||
elif self.backend == 'p2p':
|
||||
# Ring FlashAttention
|
||||
layer.register_forward_pre_hook(
|
||||
self.p2p_reshard_pre_hook(process_mesh), with_kwargs=True
|
||||
)
|
||||
else:
|
||||
logging.warning(
|
||||
f'{self.backend} is not supported backend for context parallel'
|
||||
)
|
||||
@@ -0,0 +1,294 @@
|
||||
# Copyright (c) 2024 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
|
||||
import paddle.distributed as dist
|
||||
from paddle import pir
|
||||
from paddle.base.framework import (
|
||||
in_dygraph_mode,
|
||||
in_pir_mode,
|
||||
)
|
||||
from paddle.distributed import fleet
|
||||
from paddle.nn import Layer
|
||||
from paddle.optimizer import Optimizer
|
||||
|
||||
|
||||
def is_tensor(tensor):
|
||||
if in_dygraph_mode():
|
||||
return isinstance(tensor, paddle.Tensor)
|
||||
elif in_pir_mode():
|
||||
return isinstance(tensor, pir.Value)
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"PipelineParallel are only supported in dynamic or pir mode."
|
||||
)
|
||||
|
||||
|
||||
class ParallelOptimizer:
|
||||
def __init__(
|
||||
self,
|
||||
optimizer,
|
||||
level=None,
|
||||
sharding_mesh_dim=None,
|
||||
):
|
||||
self.level = None
|
||||
self.sharding_mesh_dim = None
|
||||
self.optimizer = None
|
||||
|
||||
if isinstance(optimizer, ParallelOptimizer):
|
||||
self.optimizer = optimizer.optimizer
|
||||
if level is None:
|
||||
self.level = optimizer.level
|
||||
self.sharding_mesh_dim = optimizer.sharding_mesh_dim
|
||||
else:
|
||||
if isinstance(level, int):
|
||||
level = str(level)
|
||||
assert level in ("0", "1", "2", "3", None)
|
||||
if optimizer.level is not None:
|
||||
assert level == optimizer.level, (
|
||||
f"The level passed in is not identical with previous level. Current level is {level}, previous level is {optimizer.level}"
|
||||
)
|
||||
self.level = level
|
||||
self.sharding_mesh_dim = sharding_mesh_dim
|
||||
else:
|
||||
assert isinstance(optimizer, Optimizer)
|
||||
self.optimizer = optimizer
|
||||
if isinstance(level, int):
|
||||
level = str(level)
|
||||
assert level in ("0", "1", "2", "3", None)
|
||||
# level=0 and level=None are all mean pure dp
|
||||
self.level = level
|
||||
self.sharding_mesh_dim = sharding_mesh_dim
|
||||
|
||||
self.is_initialized = False
|
||||
|
||||
def parallelize(self):
|
||||
assert self.optimizer is not None
|
||||
if self.is_initialized:
|
||||
return self.optimizer
|
||||
|
||||
mesh = fleet.auto.get_mesh()
|
||||
if self.level == "1":
|
||||
self.optimizer = dist.shard_optimizer(
|
||||
self.optimizer,
|
||||
dist.ShardingStage1(self.sharding_mesh_dim, mesh),
|
||||
)
|
||||
elif self.level == "2":
|
||||
self.optimizer = dist.shard_optimizer(
|
||||
self.optimizer,
|
||||
dist.ShardingStage2(self.sharding_mesh_dim, mesh),
|
||||
)
|
||||
elif self.level == "3":
|
||||
self.optimizer = dist.shard_optimizer(
|
||||
self.optimizer,
|
||||
dist.ShardingStage3(self.sharding_mesh_dim, mesh),
|
||||
)
|
||||
else:
|
||||
self.optimizer = dist.shard_optimizer(self.optimizer, None)
|
||||
self.is_initialized = True
|
||||
|
||||
return self.optimizer
|
||||
|
||||
def update_param_list(self, parallelized_parameters):
|
||||
self.optimizer._parameter_list = parallelized_parameters
|
||||
if isinstance(parallelized_parameters[0], dict):
|
||||
self.optimizer._param_groups = []
|
||||
for param_group in self.parallelized_parameters:
|
||||
self.optimizer._add_param_group(param_group.copy())
|
||||
else:
|
||||
self.optimizer._param_groups = self.optimizer._parameter_list
|
||||
|
||||
|
||||
class ParallelModel:
|
||||
def __init__(self, model):
|
||||
super().__init__()
|
||||
self.pp_parallelizer = None
|
||||
self.tp_parallelizer = None
|
||||
self.sharding_parallelizer = None
|
||||
self.model = None
|
||||
self.share_param_list = {}
|
||||
self.layer_param_placements = {}
|
||||
if isinstance(model, ParallelModel):
|
||||
self.pp_parallelizer = model.pp_parallelizer
|
||||
self.tp_parallelizer = model.tp_parallelizer
|
||||
self.sharding_parallelizer = model.sharding_parallelizer
|
||||
self.model = model.model
|
||||
else:
|
||||
assert isinstance(model, Layer)
|
||||
self.model = model
|
||||
|
||||
self.is_parallelized = False
|
||||
|
||||
def get_mesh(self, pp_idx=0):
|
||||
mesh = fleet.auto.get_mesh()
|
||||
if "pp" in mesh.dim_names:
|
||||
mesh = mesh.get_mesh_with_dim("pp", pp_idx)
|
||||
return mesh
|
||||
|
||||
def parallelize_model(self):
|
||||
assert self.model is not None
|
||||
if self.is_parallelized:
|
||||
return self.model
|
||||
|
||||
if self.pp_parallelizer is not None:
|
||||
assert callable(self.pp_parallelizer)
|
||||
self.model = self.pp_parallelizer(self.model)
|
||||
|
||||
if self.tp_parallelizer is not None:
|
||||
assert callable(self.tp_parallelizer)
|
||||
self.model, self.layer_param_placements = self.tp_parallelizer(
|
||||
self.model
|
||||
)
|
||||
if self.sharding_parallelizer is not None:
|
||||
assert callable(self.sharding_parallelizer)
|
||||
self.model = self.sharding_parallelizer(self.model)
|
||||
self._shard_all_param(self.model)
|
||||
self.is_parallelized = True
|
||||
|
||||
return self.model
|
||||
|
||||
def _process_share_weight_layer(
|
||||
self, layer, origin_weight, param_name, param_placements
|
||||
):
|
||||
ipp = (
|
||||
layer.pipeline_stage_index
|
||||
if hasattr(layer, "pipeline_stage_index")
|
||||
else 0
|
||||
)
|
||||
|
||||
def create_pre_hook(origin_weight, param_name):
|
||||
def forward_pre_hook(layer, input):
|
||||
setattr(
|
||||
layer,
|
||||
param_name,
|
||||
None,
|
||||
)
|
||||
delattr(layer, param_name)
|
||||
mesh = self.get_mesh(ipp)
|
||||
share_weight = dist.reshard(
|
||||
origin_weight,
|
||||
mesh,
|
||||
param_placements,
|
||||
)
|
||||
setattr(
|
||||
layer,
|
||||
param_name,
|
||||
share_weight,
|
||||
)
|
||||
|
||||
return forward_pre_hook
|
||||
|
||||
def create_post_hook(origin_weight, param_name):
|
||||
def forward_post_hook(layer, input, output):
|
||||
setattr(
|
||||
layer,
|
||||
param_name,
|
||||
origin_weight,
|
||||
)
|
||||
|
||||
return forward_post_hook
|
||||
|
||||
layer.register_forward_pre_hook(
|
||||
create_pre_hook(origin_weight, param_name)
|
||||
)
|
||||
layer.register_forward_post_hook(
|
||||
create_post_hook(origin_weight, param_name)
|
||||
)
|
||||
|
||||
def _shard_all_param(self, model):
|
||||
param_name_to_shard_param = {}
|
||||
param_name_to_pp_stage = {}
|
||||
|
||||
def shard_layer_param(layer):
|
||||
if self.pp_parallelizer is not None:
|
||||
assert hasattr(layer, "pipeline_stage_index")
|
||||
for param_name in list(layer._parameters.keys()):
|
||||
param = getattr(layer, param_name)
|
||||
if param is not None:
|
||||
param_full_name = param.name
|
||||
ipp = (
|
||||
layer.pipeline_stage_index
|
||||
if hasattr(layer, "pipeline_stage_index")
|
||||
else 0
|
||||
)
|
||||
mesh = self.get_mesh(ipp)
|
||||
param_placements = [
|
||||
dist.Replicate() for _ in range(len(mesh._shape))
|
||||
]
|
||||
if layer in self.layer_param_placements:
|
||||
if param_name in self.layer_param_placements[layer]:
|
||||
param_placements = (
|
||||
self.layer_param_placements[layer][param_name]
|
||||
if self.layer_param_placements[layer][
|
||||
param_name
|
||||
]
|
||||
is not None
|
||||
else param_placements
|
||||
)
|
||||
if not param.is_dist():
|
||||
if param_full_name in param_name_to_shard_param:
|
||||
setattr(
|
||||
layer,
|
||||
param_name,
|
||||
param_name_to_shard_param[param_full_name],
|
||||
)
|
||||
if ipp != param_name_to_pp_stage[param_full_name]:
|
||||
self._process_share_weight_layer(
|
||||
layer,
|
||||
param_name_to_shard_param[param_full_name],
|
||||
param_name,
|
||||
param_placements,
|
||||
)
|
||||
else:
|
||||
param = dist.shard_tensor(
|
||||
param, mesh, param_placements
|
||||
)
|
||||
param_name_to_shard_param[param_full_name] = param
|
||||
param_name_to_pp_stage[param_full_name] = ipp
|
||||
setattr(layer, param_name, param)
|
||||
else:
|
||||
if (
|
||||
param_full_name in param_name_to_shard_param
|
||||
and ipp != param_name_to_pp_stage[param_full_name]
|
||||
):
|
||||
self._process_share_weight_layer(
|
||||
layer,
|
||||
param_name_to_shard_param[param_full_name],
|
||||
param_name,
|
||||
param_placements,
|
||||
)
|
||||
elif param_full_name not in param_name_to_shard_param:
|
||||
param_name_to_shard_param[param_full_name] = param
|
||||
param_name_to_pp_stage[param_full_name] = ipp
|
||||
|
||||
for name, layer in model.named_sublayers():
|
||||
shard_layer_param(layer)
|
||||
|
||||
|
||||
def parallelize_model_and_optimizer(model, optimizer=None):
|
||||
if not isinstance(model, ParallelModel):
|
||||
assert not isinstance(optimizer, ParallelOptimizer)
|
||||
logging.warning(
|
||||
"The method `parallelize_model_and_optimizer` won't do anything since the model is not parallelized."
|
||||
)
|
||||
return model, optimizer
|
||||
parallelized_model = model.parallelize_model()
|
||||
parallelized_optimizer = None
|
||||
if optimizer is not None:
|
||||
assert isinstance(optimizer, ParallelOptimizer)
|
||||
optimizer.update_param_list(parallelized_model.parameters())
|
||||
parallelized_optimizer = optimizer.parallelize()
|
||||
|
||||
return parallelized_model, parallelized_optimizer
|
||||
@@ -0,0 +1,385 @@
|
||||
# Copyright (c) 2024 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from typing import TYPE_CHECKING, TypedDict
|
||||
|
||||
from typing_extensions import NotRequired
|
||||
|
||||
from paddle.distributed import fleet
|
||||
from paddle.framework import core
|
||||
|
||||
from .parallel_base import ParallelOptimizer, parallelize_model_and_optimizer
|
||||
from .pipeline_parallel import pipeline_parallel
|
||||
from .sharded_data_parallel import sharded_data_parallel
|
||||
from .tensor_parallel import tensor_parallel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import paddle
|
||||
|
||||
from .pipeline_parallel import SplitPoint
|
||||
from .tensor_parallel import PlanBase
|
||||
|
||||
class _DPConfig(TypedDict):
|
||||
sharding_level: str | int
|
||||
|
||||
class _MPConfig(TypedDict):
|
||||
parallelize_plan: dict[str, PlanBase | list[PlanBase]]
|
||||
|
||||
class _PPConfig(TypedDict):
|
||||
split_spec: str | dict[str, SplitPoint]
|
||||
global_spec: NotRequired[str]
|
||||
|
||||
class _ParallelizeConfig(TypedDict):
|
||||
dp_config: NotRequired[_DPConfig]
|
||||
mp_config: NotRequired[_MPConfig]
|
||||
pp_config: NotRequired[_PPConfig]
|
||||
|
||||
|
||||
def parallelize(
|
||||
model: paddle.nn.Layer,
|
||||
optimizer: paddle.optimizer.Optimizer | None = None,
|
||||
mesh: paddle.distributed.ProcessMesh | None = None,
|
||||
config: _ParallelizeConfig | None = None,
|
||||
) -> tuple[paddle.nn.Layer, paddle.optimizer.Optimizer]:
|
||||
"""
|
||||
|
||||
Parallelize the model and optimizer from a single card version to a distributed version.
|
||||
|
||||
Args:
|
||||
model (paddle.nn.Layer): the model to be parallelized.
|
||||
optimizer (paddle.optimizer.Optimizer, optional): the optimizer to be parallelized.
|
||||
Could be `None` if no optimizer to be parallelized.
|
||||
mesh (paddle.distributed.ProcessMesh, optional): the process mesh for parallelize the model and the optimizer.
|
||||
Best practice: calling `dist.auto_parallel.set_mesh` to set the global mesh ahead of calling `parallelize`
|
||||
and keep the `mesh` parameter as `None.
|
||||
If the `mesh` is not None, the mesh passed to `parallelize` will overwrite the mesh set by `set_mesh`.
|
||||
config (dict, optional): a dict contains the parallel config.
|
||||
The keys of the dict can be chosen from `dp_config`, `mp_config` and `pp_config` which will be used to
|
||||
determine the parallel method for data parallel, tensor parallel and pipeline parallel separately.
|
||||
A valid config can be like this: {"dp_config": for more information refer the `dp_config` section of
|
||||
this doc, "mp_config": for more information refer the `mp_config` section of this doc, "pp_config":
|
||||
for more information refer the `pp_config` section of this doc}.
|
||||
|
||||
dp_config (dict): a dict specifying the data parallel config. The keys of `dp_config` is `sharding_level`.
|
||||
The value of `sharding_level` can be chosen from 0/1/2/3, which means pure data parallel, sharding
|
||||
parallel stage 1, sharding parallel stage 2 and sharding parallel stage 3 separately. A valid
|
||||
dp_config can be like this: {"sharding_level": 2}.
|
||||
|
||||
mp_config (dict): a dict specifying the tensor parallel config. The keys of `mp_config` is
|
||||
`parallelize_plan`. The value of `parallelize_plan` is another dict, mapping a layer name or a param
|
||||
name to a specific parallel plan. Note that the layer name could be written in regular format. If
|
||||
mapping a param name to a specific plan, the name of the param must be ended with `weight` or `bias`.
|
||||
And all valid parallel plan is `ColWiseParallel`, `RowWiseParallel`, `SequenceParallelBegin,
|
||||
`SequenceParallelDisable`, `SequenceParallelEnable`, `SequenceParallelEnd`, `PrepareLayerInput` and
|
||||
`PrepareLayerOutput`. A valid mp_config can be like this: {"llama.embed_tokens": dist.ColWiseParallel(),
|
||||
"llama.norm": dist.SequenceParallelEnable(), "lm_head.weight": dist.ColWiseParallel()}.
|
||||
|
||||
pp_config (dict): a dict specifying the pipeline parallel config. The keys of `pp_config` is `split_spec`
|
||||
and `global_spec`. The `split_spec` can be a dict or a string. If the `split_spec` is a dict, it maps
|
||||
a layer name to a `SplitPoint`, note that the layer name could be written in regular format. The
|
||||
pipeline parallel will exactly split the model at the point indicated by the map. If the `split_spec`
|
||||
is a string, it contains the prefix of a set of layers. The pipeline parallel will automatically split
|
||||
the model evenly at target layer. The `global_spec` is a string indicating a layer that contains global
|
||||
tensors, which will be duplicated through all stages of the pipeline parallel. Some valid pp_config
|
||||
can be list these: {"split_spec": "llama.layers", "global_spec": "llama.global_layer"}
|
||||
or {"split_spec": {"llama.layers.1": SplitPoint.END}}.
|
||||
|
||||
cp_config (dict): a dict specifying the context parallel config. The keys of `cp_config` is
|
||||
`parallelize_plan`. The value of `parallelize_plan` is another dict, mapping a layer name or a param
|
||||
name to a specific parallel plan. All valid parallel plan is `ContextParallel` and `PrepareContextParallel`.
|
||||
A valid cp_config can be like this: {"llama": dist.PrepareContextParallel('p2p'),
|
||||
"llama.sdpa": dist.ContextParallel('p2p')}.
|
||||
|
||||
Note:
|
||||
If the mesh is `None` or neither of `dp_config`, `mp_config`, `pp_config` and `cp_config` is in the config, this
|
||||
api will do nothing but return the model and optimizer passed in.
|
||||
|
||||
Returns:
|
||||
model, optimizer: the model and the optimizer after parallelize
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
|
||||
>>> class ModelConfig:
|
||||
... def __init__(self):
|
||||
... self.vocab_size = 10
|
||||
... self.hidden_size = 20
|
||||
... self.intermediate_size = 20
|
||||
... self.num_layers = 2
|
||||
|
||||
>>> model_config = ModelConfig()
|
||||
|
||||
>>> class LlamaRMSNorm(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.weight = paddle.create_parameter(
|
||||
... shape=[model_config.hidden_size],
|
||||
... dtype=paddle.get_default_dtype(),
|
||||
... )
|
||||
...
|
||||
... def forward(self, input):
|
||||
... pass
|
||||
|
||||
>>> class LlamaAttention(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
...
|
||||
... self.qkv_proj = paddle.nn.Linear(
|
||||
... model_config.hidden_size,
|
||||
... model_config.hidden_size * 3,
|
||||
... bias_attr=False,
|
||||
... )
|
||||
...
|
||||
... self.o_proj = paddle.nn.Linear(
|
||||
... model_config.hidden_size,
|
||||
... model_config.hidden_size,
|
||||
... bias_attr=False,
|
||||
... )
|
||||
...
|
||||
... def forward(self, input):
|
||||
... pass
|
||||
|
||||
>>> class LlamaMLP(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.gate_up_proj = paddle.nn.Linear(
|
||||
... model_config.hidden_size,
|
||||
... model_config.intermediate_size * 2,
|
||||
... bias_attr=False,
|
||||
... )
|
||||
...
|
||||
... self.down_proj = paddle.nn.Linear(model_config.intermediate_size, model_config.hidden_size, bias_attr=False)
|
||||
...
|
||||
... def forward(self, input):
|
||||
... pass
|
||||
|
||||
>>> class LlamaDecoderLayer(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.self_attn = LlamaAttention()
|
||||
... self.mlp = LlamaMLP()
|
||||
... self.input_layernorm = LlamaRMSNorm()
|
||||
... self.post_attention_layernorm = LlamaRMSNorm()
|
||||
...
|
||||
... def forward(self, input):
|
||||
... pass
|
||||
|
||||
>>> class LlamaModel(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.embedding = paddle.nn.Embedding(model_config.vocab_size, model_config.hidden_size)
|
||||
... decoder_layers = []
|
||||
... for _ in range(model_config.num_layers):
|
||||
... decoder_layers.append(LlamaDecoderLayer())
|
||||
...
|
||||
... self.layers = paddle.nn.LayerList(decoder_layers)
|
||||
... self.norm = LlamaRMSNorm()
|
||||
...
|
||||
... def forward(self, input):
|
||||
... pass
|
||||
|
||||
>>> class LlamaLMHead(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.weight = self.create_parameter(
|
||||
... shape=[model_config.hidden_size, model_config.vocab_size],
|
||||
... dtype=paddle.get_default_dtype(),
|
||||
... )
|
||||
...
|
||||
... def forward(self, input):
|
||||
... pass
|
||||
|
||||
>>> class LlamaForCausalLM(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.llama = LlamaModel()
|
||||
... self.lm_head = LlamaLMHead()
|
||||
...
|
||||
... def forward(self, input):
|
||||
... pass
|
||||
|
||||
>>> mesh = dist.ProcessMesh([[[0, 1], [2, 3]], [[4, 5], [6, 7]]], dim_names=["dp", "mp", "pp"])
|
||||
>>> dist.auto_parallel.set_mesh(mesh)
|
||||
>>> parallel_config = {
|
||||
... "dp_config": {'sharding_level': 1},
|
||||
... "mp_config": {
|
||||
... "parallelize_plan": {
|
||||
... "llama.embed_tokens": [
|
||||
... dist.ColWiseParallel(),
|
||||
... dist.SequenceParallelBegin(),
|
||||
... ],
|
||||
... "llama.position_embedding": [
|
||||
... dist.ColWiseParallel(),
|
||||
... dist.SequenceParallelBegin(),
|
||||
... ],
|
||||
... "llama.layers.*.self_attn.qkv_proj": dist.ColWiseParallel(),
|
||||
... "llama.layers.*.self_attn.o_proj": dist.RowWiseParallel(),
|
||||
... "llama.layers.*.self_attn": dist.SequenceParallelDisable(),
|
||||
... "llama.layers.*.mlp.gate_up_proj": dist.ColWiseParallel(),
|
||||
... "llama.layers.*.mlp.down_proj": dist.RowWiseParallel(),
|
||||
... "llama.layers.*.mlp": dist.SequenceParallelDisable(need_transpose=False),
|
||||
... "lm_head.weight": dist.ColWiseParallel(),
|
||||
... "lm_head": dist.SequenceParallelEnd(),
|
||||
... }
|
||||
... },
|
||||
... "pp_config": {'split_spec': "llama.layers"},
|
||||
... }
|
||||
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> model = LlamaForCausalLM()
|
||||
>>> optimizer = paddle.optimizer.AdamW(parameters=model.parameters())
|
||||
>>> dist_model, dist_optimizer = dist.parallelize(model, optimizer, config=parallel_config) # type: ignore[arg-type]
|
||||
>>> # This case need to be executed in multi-card environment
|
||||
>>> # python -m paddle.distributed.launch --gpus=0,1,2,3,4,5,6,7 {test_case}.py
|
||||
|
||||
"""
|
||||
if config is None:
|
||||
warnings.warn(
|
||||
"The `parallelize will do nothing since the config is `None`."
|
||||
)
|
||||
return model, optimizer
|
||||
assert isinstance(config, dict)
|
||||
if mesh is not None:
|
||||
assert isinstance(mesh, core.ProcessMesh), (
|
||||
"The mesh must be an instance of paddle.distributed.ProcessMesh."
|
||||
)
|
||||
g_mesh = fleet.auto.get_mesh()
|
||||
if g_mesh is not None and g_mesh != mesh:
|
||||
warnings.warn(
|
||||
"The mesh set by `fleet.auto.set_mesh` is different with the mesh pass to "
|
||||
"`parallelize`. Will overwrite the previous mesh"
|
||||
)
|
||||
fleet.auto.set_mesh(mesh)
|
||||
pp_config = config.get('pp_config')
|
||||
mp_config = config.get('mp_config')
|
||||
dp_config = config.get('dp_config')
|
||||
cp_config = config.get('cp_config')
|
||||
if pp_config is not None:
|
||||
assert isinstance(pp_config, dict)
|
||||
model, optimizer = pipeline_parallel(
|
||||
model,
|
||||
optimizer,
|
||||
pp_config,
|
||||
)
|
||||
if mp_config is not None:
|
||||
assert isinstance(mp_config, dict)
|
||||
if cp_config is not None:
|
||||
assert isinstance(cp_config, dict)
|
||||
assert "parallelize_plan" in cp_config.keys()
|
||||
assert "parallelize_plan" in mp_config.keys()
|
||||
mp_config['parallelize_plan'].update(cp_config['parallelize_plan'])
|
||||
model, optimizer = tensor_parallel(model, optimizer, mp_config)
|
||||
elif cp_config is not None:
|
||||
assert isinstance(cp_config, dict)
|
||||
model, optimizer = tensor_parallel(
|
||||
model,
|
||||
optimizer,
|
||||
cp_config,
|
||||
)
|
||||
if dp_config is not None:
|
||||
assert isinstance(dp_config, dict)
|
||||
if 'sharding_level' not in dp_config.keys():
|
||||
warnings.warn(
|
||||
"The dp_config doesn't contain sharding_level, will run under dp."
|
||||
)
|
||||
model, optimizer = sharded_data_parallel(
|
||||
model,
|
||||
optimizer,
|
||||
config=dp_config,
|
||||
)
|
||||
model, optimizer = parallelize_model_and_optimizer(model, optimizer)
|
||||
return model, optimizer
|
||||
|
||||
|
||||
has_parallelized_model = False
|
||||
|
||||
|
||||
def parallelize_model(model, mesh=None, config=None):
|
||||
if config is None:
|
||||
warnings.warn(
|
||||
"The `parallelize_model will do nothing since the config is `None`."
|
||||
)
|
||||
return model
|
||||
assert isinstance(config, dict)
|
||||
if mesh is not None:
|
||||
assert isinstance(mesh, core.ProcessMesh), (
|
||||
"The mesh must be an instance of paddle.distributed.ProcessMesh."
|
||||
)
|
||||
g_mesh = fleet.auto.get_mesh()
|
||||
if g_mesh is not None and g_mesh != mesh:
|
||||
warnings.warn(
|
||||
"The mesh set by `fleet.auto.set_mesh` is different with the mesh pass to "
|
||||
"`parallelize_model`. Will overwrite the previous mesh"
|
||||
)
|
||||
fleet.auto.set_mesh(mesh)
|
||||
global has_parallelized_model
|
||||
has_parallelized_model = True
|
||||
model, _ = parallelize(model, None, mesh, config)
|
||||
return model
|
||||
|
||||
|
||||
def parallelize_optimizer(optimizer, mesh=None, config=None):
|
||||
if config is None:
|
||||
warnings.warn(
|
||||
"The `parallelize_optimizer will do nothing since the config is `None`."
|
||||
)
|
||||
return optimizer
|
||||
assert isinstance(config, dict)
|
||||
if mesh is not None:
|
||||
assert isinstance(mesh, core.ProcessMesh), (
|
||||
"The mesh must be an instance of paddle.distributed.ProcessMesh."
|
||||
)
|
||||
g_mesh = fleet.auto.get_mesh()
|
||||
if g_mesh is not None and g_mesh != mesh:
|
||||
warnings.warn(
|
||||
"The mesh set by `fleet.auto.set_mesh` is different with the mesh pass to "
|
||||
"`parallelize_optimizer`. Will overwrite the previous mesh"
|
||||
)
|
||||
fleet.auto.set_mesh(mesh)
|
||||
|
||||
global has_parallelized_model
|
||||
assert has_parallelized_model, (
|
||||
"Please parallelize the model before parallelize optimizer."
|
||||
)
|
||||
param_list = optimizer._parameter_list
|
||||
if isinstance(param_list[0], dict):
|
||||
for param_group in param_list:
|
||||
for param in param_group['params']:
|
||||
assert param.is_dist(), (
|
||||
"Please use model after parallelize to create optimizer."
|
||||
)
|
||||
else:
|
||||
for param in param_list:
|
||||
assert param.is_dist(), (
|
||||
"Please use model after parallelize to create optimizer."
|
||||
)
|
||||
|
||||
dp_config = config.get('dp_config')
|
||||
level = None
|
||||
sharding_mesh_dim = None
|
||||
if dp_config is not None:
|
||||
if 'sharding_level' not in dp_config.keys():
|
||||
warnings.warn(
|
||||
"The dp_config doesn't contain sharding_level, will run under dp."
|
||||
)
|
||||
level = dp_config.get('sharding_level')
|
||||
sharding_mesh_dim = dp_config.get('sharding_mesh_dim', "dp")
|
||||
optimizer = ParallelOptimizer(optimizer, level, sharding_mesh_dim)
|
||||
optimizer = optimizer.parallelize()
|
||||
return optimizer
|
||||
@@ -0,0 +1,419 @@
|
||||
# Copyright (c) 2024 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 itertools
|
||||
import logging
|
||||
import re
|
||||
from collections import OrderedDict
|
||||
from enum import Enum
|
||||
|
||||
import paddle.distributed as dist
|
||||
from paddle.distributed import fleet
|
||||
from paddle.distributed.utils.log_utils import get_logger
|
||||
|
||||
from .parallel_base import ParallelModel, ParallelOptimizer, is_tensor
|
||||
|
||||
logger = get_logger("INFO", __name__)
|
||||
|
||||
|
||||
class SplitPoint(Enum):
|
||||
"""
|
||||
Marking the position of the split.
|
||||
BEGINNING: will split the model before the specified layer.
|
||||
END: will split the model after the specified layer.
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
|
||||
>>> class MLP(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.fc1 = paddle.nn.Linear(8, 8)
|
||||
... self.fc2 = paddle.nn.Linear(8, 8)
|
||||
...
|
||||
... def forward(self, input):
|
||||
... return self.fc2(self.fc1(input))
|
||||
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> layer = MLP()
|
||||
>>> pp_config = {
|
||||
... 'fc1': dist.SplitPoint.END,
|
||||
... }
|
||||
"""
|
||||
|
||||
BEGINNING = 0
|
||||
END = 1
|
||||
|
||||
|
||||
class PipelineParallel(ParallelModel):
|
||||
def __init__(self, model, split_spec, global_spec, pipeline_layers=None):
|
||||
super().__init__(model)
|
||||
self.split_spec = split_spec
|
||||
self.global_spec = global_spec
|
||||
self.pipeline_layers = pipeline_layers
|
||||
self.pp_parallelizer = self.pipeline_parallel_fn
|
||||
self.name_to_layer = {}
|
||||
for layer_name, layer in model.named_sublayers():
|
||||
self.name_to_layer[layer_name] = layer
|
||||
|
||||
def get_layer_by_name(self, name):
|
||||
assert name in self.name_to_layer, (
|
||||
f"layer name:{name} not in the model, please check the split_spec"
|
||||
)
|
||||
return self.name_to_layer[name]
|
||||
|
||||
def pipeline_parallel_fn(self, model):
|
||||
mesh = fleet.auto.get_mesh()
|
||||
pipeline_stage_num = mesh.get_dim_size("pp")
|
||||
assert len(self.split_spec) == pipeline_stage_num - 1
|
||||
|
||||
def forward_post_hook(layer, input, output):
|
||||
pipeline_stage_index = layer.pipeline_stage_index
|
||||
split_point = layer.split_point
|
||||
assert split_point == SplitPoint.END
|
||||
# reshard to next pipeline stage
|
||||
if isinstance(output, (dict, OrderedDict)):
|
||||
for key, tensor in output.items():
|
||||
assert is_tensor(tensor)
|
||||
output[key] = dist.reshard(
|
||||
tensor,
|
||||
self.get_mesh(pipeline_stage_index + 1),
|
||||
tensor.placements,
|
||||
)
|
||||
elif isinstance(output, list):
|
||||
for i in range(len(output)):
|
||||
assert is_tensor(output[i])
|
||||
output[i] = dist.reshard(
|
||||
output[i],
|
||||
self.get_mesh(pipeline_stage_index + 1),
|
||||
output[i].placements,
|
||||
)
|
||||
elif isinstance(output, tuple):
|
||||
output = list(output)
|
||||
for i in range(len(output)):
|
||||
assert is_tensor(output[i])
|
||||
output[i] = dist.reshard(
|
||||
output[i],
|
||||
self.get_mesh(pipeline_stage_index + 1),
|
||||
output[i].placements,
|
||||
)
|
||||
output = tuple(output)
|
||||
elif is_tensor(output):
|
||||
output = dist.reshard(
|
||||
output,
|
||||
self.get_mesh(pipeline_stage_index + 1),
|
||||
output.placements,
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"output between pp stages should be a dict of tensors or list of tensors or tuple of tensors or tensor, but {type(output)}"
|
||||
)
|
||||
return output
|
||||
|
||||
def forward_pre_hook(layer, input):
|
||||
split_point = layer.split_point
|
||||
assert split_point == SplitPoint.BEGINNING
|
||||
# TODO(deepllz): support in the future
|
||||
return input
|
||||
|
||||
# step1: set every layer's own pipeline_stage_index
|
||||
split_layer_names = list(self.split_spec.keys())
|
||||
sublayer_names = [name for name, _ in model.named_sublayers()]
|
||||
# Mark which layer is the next pipeline stage
|
||||
pipeline_layer_mark = [0 for _ in range(len(sublayer_names))]
|
||||
for split_layer_name in split_layer_names:
|
||||
split_point = self.split_spec[split_layer_name]
|
||||
index = sublayer_names.index(split_layer_name)
|
||||
if split_point == SplitPoint.END:
|
||||
is_valid = False
|
||||
for i in range(index + 1, len(sublayer_names)):
|
||||
if not sublayer_names[i].startswith(split_layer_name):
|
||||
pipeline_layer_mark[i] = 1
|
||||
is_valid = True
|
||||
break
|
||||
assert is_valid, (
|
||||
f"the last layer:{split_layer_name} must not be SplitPoint.END, please check the split_spec"
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"SplitPoint.BEGINNING is not supported currently"
|
||||
)
|
||||
pipeline_layer_mark[index] = 1
|
||||
# the inclusiveSum of pipeline_layer_mark is the pipeline stage index
|
||||
pipeline_stage_index = list(itertools.accumulate(pipeline_layer_mark))
|
||||
for index, (name, layer) in enumerate(model.named_sublayers()):
|
||||
layer.pipeline_stage_index = pipeline_stage_index[index]
|
||||
|
||||
# step2: insert reshard
|
||||
for name in split_layer_names:
|
||||
layer = self.get_layer_by_name(name)
|
||||
split_point = self.split_spec[name]
|
||||
layer.split_point = split_point
|
||||
if split_point == SplitPoint.END:
|
||||
layer.register_forward_post_hook(forward_post_hook)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"SplitPoint.BEGINNING is not supported currently"
|
||||
)
|
||||
layer.register_forward_pre_hook(forward_pre_hook)
|
||||
|
||||
if self.global_spec:
|
||||
self.process_global_mesh_layers()
|
||||
|
||||
return model
|
||||
|
||||
def process_global_mesh_layers(self):
|
||||
g_mesh = fleet.auto.get_mesh()
|
||||
g_mesh = g_mesh.get_mesh_with_dim("pp")
|
||||
|
||||
def forward_post_hook(layer, input, output):
|
||||
if isinstance(output, (list, tuple)):
|
||||
global_output = list(output)
|
||||
for ind in range(len(global_output)):
|
||||
output_i = global_output[ind]
|
||||
if is_tensor(output_i):
|
||||
if output_i.is_dist():
|
||||
global_output[ind] = dist.reshard(
|
||||
output_i,
|
||||
g_mesh,
|
||||
[
|
||||
dist.Replicate()
|
||||
for _ in range(len(g_mesh._shape))
|
||||
],
|
||||
)
|
||||
else:
|
||||
global_output[ind] = dist.shard_tensor(
|
||||
output_i,
|
||||
g_mesh,
|
||||
[
|
||||
dist.Replicate()
|
||||
for _ in range(len(g_mesh._shape))
|
||||
],
|
||||
)
|
||||
|
||||
if isinstance(output, tuple):
|
||||
global_output = tuple(global_output)
|
||||
return global_output
|
||||
elif is_tensor(output):
|
||||
if output.is_dist():
|
||||
return dist.reshard(
|
||||
output,
|
||||
g_mesh,
|
||||
[dist.Replicate() for _ in range(len(g_mesh._shape))],
|
||||
)
|
||||
else:
|
||||
return dist.shard_tensor(
|
||||
output,
|
||||
g_mesh,
|
||||
[dist.Replicate() for _ in range(len(g_mesh._shape))],
|
||||
)
|
||||
else:
|
||||
raise TypeError(
|
||||
"layer output can only be tensor or list/tuple of tensor"
|
||||
)
|
||||
|
||||
def forward_pre_hook(layer, args, kwargs):
|
||||
pp_idx = getattr(layer, "pipeline_stage_index", 0)
|
||||
new_args = []
|
||||
new_kwargs = {}
|
||||
|
||||
def reshard_not_mesh_match_tensor(arg):
|
||||
cur_pp_mesh = self.get_mesh(pp_idx)
|
||||
if (
|
||||
arg is not None
|
||||
and is_tensor(arg)
|
||||
and arg.is_dist()
|
||||
and arg.process_mesh != cur_pp_mesh
|
||||
):
|
||||
return dist.reshard(
|
||||
arg,
|
||||
cur_pp_mesh,
|
||||
[dist.Replicate(), dist.Replicate()],
|
||||
)
|
||||
return arg
|
||||
|
||||
for arg in args:
|
||||
new_args.append(reshard_not_mesh_match_tensor(arg))
|
||||
|
||||
for key, arg in kwargs.items():
|
||||
new_kwargs[key] = reshard_not_mesh_match_tensor(arg)
|
||||
|
||||
return (tuple(new_args), new_kwargs)
|
||||
|
||||
# wa because of pir in vpp mode send receive bug
|
||||
for layer_name in self.global_spec:
|
||||
layer = self.get_layer_by_name(layer_name)
|
||||
layer.register_forward_post_hook(forward_post_hook)
|
||||
|
||||
if self.pipeline_layers is not None:
|
||||
for layer_name in self.pipeline_layers:
|
||||
layer = self.get_layer_by_name(layer_name)
|
||||
layer.register_forward_pre_hook(
|
||||
forward_pre_hook, with_kwargs=True
|
||||
)
|
||||
else:
|
||||
for layer in self.name_to_layer.values():
|
||||
layer.register_forward_pre_hook(
|
||||
forward_pre_hook, with_kwargs=True
|
||||
)
|
||||
|
||||
|
||||
def pipeline_parallel(model, optimizer=None, config=None):
|
||||
"""
|
||||
pipeline_parallel converts model and optimizer to pipelined distributed model
|
||||
|
||||
Args:
|
||||
model (paddle.nn.Layer): A single card model to be distributed
|
||||
optimizer (paddle.optimizer.Optimizer): An optimizer to be distributed
|
||||
config (dict): {
|
||||
"split_spec": OrderedDict|dict|str|list(str), The pipeline parallel split point.
|
||||
if split_spec is a string or list, such as "llama.layer" or ["llama.layerA", "llama.layerB"], Then the layer with same prefix a will be divided equally according to the size of pipeline degree.
|
||||
if split_spec is a OrderedDict|dict, key is the layer name, and the value is the split position that can be SplitPoint.BEGINNING or SplitPoint.END, the order of the keys is the order of the pipeline stage.
|
||||
NOTE: dict is also ordered after python3.7, so use dict at this time.
|
||||
"global_spec": str|list(str), make the output tensor of specific layers on global mesh.
|
||||
}
|
||||
|
||||
Returns:
|
||||
PipelineParallel: a distributed model
|
||||
ParallelOptimizer: a distributed optimizer
|
||||
"""
|
||||
|
||||
split_spec = config.get("split_spec")
|
||||
if split_spec is None:
|
||||
logging.warning("No split_spec, pipeline parallel won't do anything.")
|
||||
return model, optimizer
|
||||
|
||||
mesh = fleet.auto.get_mesh()
|
||||
assert mesh is not None, (
|
||||
"global mesh must not be None, please call fleet.auto.set_mesh(global_mesh) firstly"
|
||||
)
|
||||
assert "pp" in mesh.dim_names, (
|
||||
"pp must in the mesh dim_names when use pipeline_parallel"
|
||||
)
|
||||
|
||||
global_spec = config.get("global_spec")
|
||||
if isinstance(split_spec, str):
|
||||
split_spec = [split_spec]
|
||||
|
||||
matched_layer_name = None
|
||||
if isinstance(split_spec, (list, tuple)):
|
||||
# match layer_name with split_spec following by a dot and numbers and no other characters
|
||||
# such as split_spec = ["llama.layer"], then llama.layer.0 is matched, llama.layer.0.mlp is not matched
|
||||
patterns = [rf"{prefix}\.\d+$" for prefix in split_spec]
|
||||
|
||||
def is_match(layer_name):
|
||||
for pattern in patterns:
|
||||
if re.match(pattern, layer_name) or layer_name in split_spec:
|
||||
return True
|
||||
return False
|
||||
|
||||
def filter_matched_layer(matched_layer_name):
|
||||
# remove the base name if it has a numbered suffix
|
||||
string_set = set(matched_layer_name)
|
||||
to_remove = set()
|
||||
|
||||
numbered_pattern = re.compile(r'^(.+)\.\d+$')
|
||||
for s in matched_layer_name:
|
||||
match = numbered_pattern.match(s)
|
||||
if match:
|
||||
base_name = match.group(1)
|
||||
if base_name in string_set:
|
||||
to_remove.add(base_name)
|
||||
|
||||
res = []
|
||||
for s in matched_layer_name:
|
||||
if s not in to_remove:
|
||||
res.append(s)
|
||||
return res
|
||||
|
||||
matched_layer_name = [
|
||||
name for name, _ in model.named_sublayers() if is_match(name)
|
||||
]
|
||||
matched_layer_name = filter_matched_layer(matched_layer_name)
|
||||
pp_size = mesh.get_dim_size("pp")
|
||||
layer_num = len(matched_layer_name)
|
||||
assert layer_num > 0, (
|
||||
"No layer match the split_spec, please check its correctness"
|
||||
)
|
||||
assert layer_num >= pp_size, (
|
||||
"The number of layers must not be less than the pp size"
|
||||
)
|
||||
if layer_num % pp_size != 0:
|
||||
logger.warning(
|
||||
f"The number of layers({layer_num}) must be divisible by the pp size({pp_size}), but got {layer_num} and {pp_size}"
|
||||
)
|
||||
|
||||
def divide_list_indices(n, k):
|
||||
base_size = n // k
|
||||
extra = n % k
|
||||
|
||||
indices = []
|
||||
current_index = -1
|
||||
|
||||
for i in range(k - 1):
|
||||
current_index += base_size
|
||||
if i < extra:
|
||||
current_index += 1
|
||||
indices.append(current_index)
|
||||
return indices
|
||||
|
||||
indices = divide_list_indices(layer_num, pp_size)
|
||||
split_spec_dict = OrderedDict(
|
||||
[
|
||||
(matched_layer_name[indices[i]], SplitPoint.END)
|
||||
for i in range(pp_size - 1)
|
||||
]
|
||||
)
|
||||
else:
|
||||
layers_per_rank = layer_num // pp_size
|
||||
split_spec_dict = OrderedDict(
|
||||
[
|
||||
(
|
||||
matched_layer_name[i * layers_per_rank - 1],
|
||||
SplitPoint.END,
|
||||
)
|
||||
for i in range(1, pp_size)
|
||||
]
|
||||
)
|
||||
else:
|
||||
sublayer_names = [name for name, _ in model.named_sublayers()]
|
||||
split_spec_dict = split_spec
|
||||
for key, value in split_spec_dict.items():
|
||||
assert key in sublayer_names, (
|
||||
f"wrong split layer, expected one of {sublayer_names}"
|
||||
)
|
||||
assert value is SplitPoint.END, "not supported split point at now."
|
||||
|
||||
if global_spec:
|
||||
if isinstance(global_spec, str):
|
||||
global_spec = [global_spec]
|
||||
else:
|
||||
assert isinstance(global_spec, (list, tuple)), (
|
||||
f"global_spec can only be list or list(str), but got:{type(global_spec)}"
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"split_spec_dict: {split_spec_dict}, global_spec: {global_spec}, matched_layer_name: {matched_layer_name}"
|
||||
)
|
||||
|
||||
model = PipelineParallel(
|
||||
model, split_spec_dict, global_spec, matched_layer_name
|
||||
)
|
||||
if optimizer is not None:
|
||||
optimizer = ParallelOptimizer(optimizer)
|
||||
|
||||
return model, optimizer
|
||||
@@ -0,0 +1,88 @@
|
||||
# Copyright (c) 2024 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
from paddle.distributed import fleet
|
||||
|
||||
from .parallel_base import ParallelModel, ParallelOptimizer
|
||||
|
||||
|
||||
class ShardedDataParallel(ParallelModel):
|
||||
"""
|
||||
ShardedDataParallel converts a single card model to a distributed data parallel model
|
||||
|
||||
Args:
|
||||
model (paddle.nn.Layer): A single card model to be distributed.
|
||||
optimizer (paddle.optimizer.Optimizer): an optimizer to be distributed.
|
||||
level (str): Zero stage, can be the following values:
|
||||
0: no sharding (pure dp)
|
||||
1: Zero Stage1
|
||||
2: Zero Stage2
|
||||
3: Zero Stage3
|
||||
Default: None, which means optimizer is replicated among all process.
|
||||
offload (bool): whether enable cpu offload strategy, not implemented currently.
|
||||
exclude_layer (list): Specify which layers do not use the zero stage strategy, not implemented currently.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model,
|
||||
offload=False,
|
||||
exclude_layer=None,
|
||||
):
|
||||
super().__init__(model)
|
||||
assert offload is False
|
||||
assert exclude_layer is None
|
||||
|
||||
self.sharding_parallelizer = self.sharding_parallelizer_func
|
||||
|
||||
def sharding_parallelizer_func(self, model):
|
||||
return model
|
||||
|
||||
|
||||
def sharded_data_parallel(model, optimizer=None, config=None):
|
||||
"""
|
||||
sharded_data_parallel converts model and optimizer to distributed and supports set zero stage1/2/3
|
||||
|
||||
Args:
|
||||
model (paddle.nn.Layer): A single card model to be distributed
|
||||
optimizer (paddle.optimizer.Optimizer): an optimizer to be distributed
|
||||
config (dict): {
|
||||
"sharding_level": 0,
|
||||
"offload": False,
|
||||
"exclude_layer": None,
|
||||
"sharding_mesh_dim": "dp",
|
||||
}
|
||||
|
||||
Returns:
|
||||
ShardedDataParallel: a distributed model
|
||||
ParallelOptimizer: a distributed optimizer
|
||||
"""
|
||||
sdp_model = ShardedDataParallel(
|
||||
model, bool(config.get('offload')), config.get('exclude_layer')
|
||||
)
|
||||
if optimizer is not None:
|
||||
level = config.get('sharding_level')
|
||||
sharding_mesh_dim = config.get('sharding_mesh_dim', "dp")
|
||||
optimizer = ParallelOptimizer(optimizer, level, sharding_mesh_dim)
|
||||
|
||||
# check global_mesh
|
||||
mesh = fleet.auto.get_mesh()
|
||||
assert mesh is not None, (
|
||||
"global mesh must not be None, please call fleet.auto.set_mesh(global_mesh) firstly"
|
||||
)
|
||||
assert "dp" in mesh.dim_names, (
|
||||
"dp must in the mesh dim_names when use sharded_data_parallel"
|
||||
)
|
||||
return sdp_model, optimizer
|
||||
@@ -0,0 +1,954 @@
|
||||
# Copyright (c) 2024 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import paddle
|
||||
import paddle.distributed as dist
|
||||
|
||||
from .parallel_base import ParallelModel, ParallelOptimizer, is_tensor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from paddle import Tensor
|
||||
from paddle.distributed import ProcessMesh
|
||||
from paddle.nn import Layer
|
||||
|
||||
|
||||
def c_split(x, process_mesh, need_transpose, split_type="sp"):
|
||||
mp_index = process_mesh.dim_names.index('mp') # get the axis for the split
|
||||
dp_index = process_mesh.dim_names.index('dp')
|
||||
if isinstance(x, tuple):
|
||||
target_x = x[0]
|
||||
else:
|
||||
target_x = x
|
||||
assert is_tensor(target_x)
|
||||
assert len(target_x.shape) == 3
|
||||
if need_transpose:
|
||||
target_x = paddle.transpose(target_x, perm=[1, 0, 2])
|
||||
placements = target_x.placements
|
||||
if placements is None:
|
||||
placements = [dist.Replicate() for _ in range(len(process_mesh.shape))]
|
||||
if split_type == "sp":
|
||||
if placements[dp_index] == dist.Shard(0):
|
||||
# NOTE(zhangwl):if shard(0) , input shape should be [b,s,h]
|
||||
split_dims = dist.Shard(1)
|
||||
elif placements[dp_index] == dist.Shard(1):
|
||||
# NOTE(zhangwl):if shard(1) , input shape should be [s,b,h]
|
||||
split_dims = dist.Shard(0)
|
||||
else:
|
||||
logging.warning(
|
||||
f"parallel api don't know {target_x.shape} which dimension is batch, default is to cut to the 0th dimension"
|
||||
)
|
||||
split_dims = dist.Shard(0)
|
||||
elif split_type == "mp":
|
||||
split_dims = dist.Shard(2) # split h [b,s,h]
|
||||
else:
|
||||
raise ValueError(f"Unsupported split type {split_type}")
|
||||
|
||||
placements[mp_index] = split_dims
|
||||
target_x = dist.reshard(target_x, process_mesh, placements)
|
||||
if isinstance(x, tuple):
|
||||
x = list(x)
|
||||
x[0] = target_x
|
||||
x = tuple(x)
|
||||
else:
|
||||
x = target_x
|
||||
|
||||
return x
|
||||
|
||||
|
||||
def c_concat(x, process_mesh, need_transpose):
|
||||
index = process_mesh.dim_names.index('mp') # get the axis for the split
|
||||
if isinstance(x, tuple):
|
||||
target_x = x[0]
|
||||
else:
|
||||
target_x = x
|
||||
assert is_tensor(target_x)
|
||||
assert len(target_x.shape) == 3
|
||||
placements = target_x.placements
|
||||
if placements is None:
|
||||
placements = [dist.Replicate() for _ in range(len(process_mesh.shape))]
|
||||
placements[index] = dist.Replicate()
|
||||
target_x = dist.reshard(target_x, process_mesh, placements)
|
||||
if need_transpose:
|
||||
target_x = paddle.transpose(target_x, perm=[1, 0, 2])
|
||||
if isinstance(x, tuple):
|
||||
x = list(x)
|
||||
x[0] = target_x
|
||||
x = tuple(x)
|
||||
else:
|
||||
x = target_x
|
||||
|
||||
return x
|
||||
|
||||
|
||||
class PlanBase:
|
||||
def __init__(self):
|
||||
self.share_param_list = {}
|
||||
|
||||
def apply(self, layer, process_mesh, shard_param_list):
|
||||
raise NotImplementedError("Don't call the PlanBase directly.")
|
||||
|
||||
|
||||
class ColWiseParallel(PlanBase):
|
||||
"""
|
||||
Col wise parallel plan for mp config.
|
||||
Will try to split weight on the second dim and the bias on the first dim.
|
||||
This api is designed for paddle.nn.Linear or paddle.nn.Embedding.
|
||||
If any other instance of paddle.nn.Layer is passed,
|
||||
this plan will try to split `layer.weight` and `layer.bias` if it has.
|
||||
|
||||
Note:
|
||||
1. `layer.weight` should have two dims.
|
||||
2. `layer.bias` should have one dim.
|
||||
|
||||
Args:
|
||||
gather_output (bool): Whether gather the output to change it from a local tensor to a global tensor.
|
||||
If gather the local tensor to global, an extra communication will be called.
|
||||
The default value is `False`, which means keeping the output as a local tensor.
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
|
||||
>>> class MLP(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.fc1 = paddle.nn.Linear(8, 8)
|
||||
... self.fc2 = paddle.nn.Linear(8, 8)
|
||||
...
|
||||
... def forward(self, input):
|
||||
... return self.fc2(self.fc1(input))
|
||||
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> layer = MLP()
|
||||
>>> mp_config = {
|
||||
... 'fc1': dist.ColWiseParallel(),
|
||||
... }
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, gather_output: bool = False) -> None:
|
||||
super().__init__()
|
||||
self.gather_output = gather_output
|
||||
|
||||
def gather_output_hook(self, process_mesh):
|
||||
def gather_hook(layer, input, output):
|
||||
assert output is not None
|
||||
return c_concat(output, process_mesh, False)
|
||||
|
||||
return gather_hook
|
||||
|
||||
def apply(self, layer, process_mesh, shard_param_list):
|
||||
index = process_mesh.dim_names.index('mp') # get the axis for the split
|
||||
size = len(process_mesh.shape)
|
||||
placement = [dist.Replicate() for _ in range(size)]
|
||||
param_placements = {}
|
||||
assert isinstance(layer, paddle.nn.Layer)
|
||||
if not isinstance(layer, (paddle.nn.Linear, paddle.nn.Embedding)):
|
||||
logging.warning(
|
||||
f"ColWiseParallel is designed to handle Linear and Embedding. "
|
||||
f"But got {layer.__class__.__name__}. "
|
||||
f"Will try to shard weight and bias if the layer contains one."
|
||||
)
|
||||
shard_param_list = set(shard_param_list)
|
||||
if len(shard_param_list) == 0:
|
||||
shard_param_list.add("weight")
|
||||
shard_param_list.add("bias")
|
||||
|
||||
def shard_param(param_name):
|
||||
if (
|
||||
hasattr(layer, param_name)
|
||||
and getattr(layer, param_name) is not None
|
||||
):
|
||||
layer_param = getattr(layer, param_name)
|
||||
|
||||
if layer_param.is_dist():
|
||||
return
|
||||
|
||||
if len(layer_param.shape) == 2:
|
||||
placement[index] = dist.Shard(1)
|
||||
elif len(layer_param.shape) == 1:
|
||||
placement[index] = dist.Shard(0)
|
||||
else:
|
||||
raise ValueError(f"{layer_param} should have 1 or 2 dims.")
|
||||
# NOTE(zhangweilong):for share parameter, the parameter should be handled uniformly in the end
|
||||
if (
|
||||
self.share_param_list is not None
|
||||
and layer_param.name in self.share_param_list
|
||||
and self.share_param_list[layer_param.name] > 1
|
||||
):
|
||||
param_placements.update({param_name: placement})
|
||||
else:
|
||||
layer_param = dist.shard_tensor(
|
||||
layer_param,
|
||||
process_mesh,
|
||||
placement,
|
||||
)
|
||||
setattr(layer, param_name, layer_param)
|
||||
|
||||
for param_name in shard_param_list:
|
||||
shard_param(param_name)
|
||||
if self.gather_output:
|
||||
layer.register_forward_post_hook(
|
||||
self.gather_output_hook(process_mesh)
|
||||
)
|
||||
return param_placements
|
||||
|
||||
|
||||
class RowWiseParallel(PlanBase):
|
||||
"""
|
||||
Row wise parallel plan for mp config.
|
||||
Will try to split weight on the first dim.
|
||||
This api is designed for paddle.nn.Linear or paddle.nn.Embedding.
|
||||
If any other instance of paddle.nn.Layer is passed, this plan will try to split `layer.weight` if it has.
|
||||
|
||||
Note:
|
||||
`layer.weight` should have two dims.
|
||||
|
||||
Args:
|
||||
is_input_parallel (bool): Whether the input is a local tensor or a global tensor. If the input is a
|
||||
global tensor, an extra split will be called. The default value is `True`,
|
||||
which means the input is a local tensor.
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
|
||||
>>> class MLP(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.fc1 = paddle.nn.Linear(8, 8)
|
||||
... self.fc2 = paddle.nn.Linear(8, 8)
|
||||
...
|
||||
... def forward(self, input):
|
||||
... return self.fc2(self.fc1(input))
|
||||
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> layer = MLP()
|
||||
>>> mp_config = {
|
||||
... 'fc1': dist.RowWiseParallel(),
|
||||
... }
|
||||
"""
|
||||
|
||||
def __init__(self, is_input_parallel: bool = True) -> None:
|
||||
super().__init__()
|
||||
self.is_input_parallel = is_input_parallel
|
||||
|
||||
def split_input_hook(self, process_mesh):
|
||||
def split_hook(layer, input):
|
||||
return c_split(input, process_mesh, False, split_type="mp")
|
||||
|
||||
return split_hook
|
||||
|
||||
def apply(self, layer, process_mesh, shard_param_list):
|
||||
index = process_mesh.dim_names.index('mp') # get the axis for the split
|
||||
size = len(process_mesh.shape)
|
||||
placement = [dist.Replicate() for _ in range(size)]
|
||||
placement[index] = dist.Shard(0)
|
||||
param_placements = {}
|
||||
assert isinstance(layer, paddle.nn.Layer)
|
||||
if not isinstance(layer, (paddle.nn.Linear, paddle.nn.Embedding)):
|
||||
logging.warning(
|
||||
f"RowWiseParallel is designed to handle Linear and Embedding. "
|
||||
f"But got {layer.__class__.__name__}. "
|
||||
f"Will try to shard weight if the layer contains one."
|
||||
)
|
||||
shard_param_list = set(shard_param_list)
|
||||
shard_param_list.discard("bias")
|
||||
if len(shard_param_list) == 0:
|
||||
shard_param_list.add("weight")
|
||||
|
||||
def shard_param(param_name):
|
||||
if (
|
||||
hasattr(layer, param_name)
|
||||
and getattr(layer, param_name) is not None
|
||||
):
|
||||
layer_param = getattr(layer, param_name)
|
||||
if layer_param.is_dist():
|
||||
return
|
||||
if len(layer_param.shape) != 2:
|
||||
raise ValueError(f"{layer_param} should have 2 dims.")
|
||||
# NOTE(zhangweilong):for share parameter, the parameter should be handled uniformly in the end
|
||||
if (
|
||||
self.share_param_list is not None
|
||||
and layer_param.name in self.share_param_list
|
||||
and self.share_param_list[layer_param.name] > 1
|
||||
):
|
||||
param_placements.update({param_name: placement})
|
||||
else:
|
||||
layer_param = dist.shard_tensor(
|
||||
layer_param,
|
||||
process_mesh,
|
||||
placement,
|
||||
)
|
||||
setattr(layer, param_name, layer_param)
|
||||
|
||||
for param_name in shard_param_list:
|
||||
shard_param(param_name)
|
||||
if not self.is_input_parallel:
|
||||
layer.register_forward_pre_hook(self.split_input_hook(process_mesh))
|
||||
return param_placements
|
||||
|
||||
|
||||
class PrepareLayerInput(PlanBase):
|
||||
"""
|
||||
Prepare the input of specific layer. User should provide one callable function.
|
||||
|
||||
Args:
|
||||
fn (callable): A function that prepare the layer input. The function should take exactly
|
||||
one parameter named `process_mesh` and return the pre hook.
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
|
||||
>>> class MLP(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.fc1 = paddle.nn.Linear(8, 8)
|
||||
... self.fc2 = paddle.nn.Linear(8, 8)
|
||||
...
|
||||
... def forward(self, input):
|
||||
... return self.fc2(self.fc1(input))
|
||||
|
||||
>>> def layer_input_hook(process_mesh):
|
||||
... def hook(layer, input, output):
|
||||
... return input
|
||||
...
|
||||
... return hook
|
||||
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> layer = MLP()
|
||||
>>> mp_config = {
|
||||
... 'fc1': dist.PrepareLayerOutput(layer_input_hook),
|
||||
... }
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
fn: (
|
||||
Callable[
|
||||
[ProcessMesh],
|
||||
Callable[
|
||||
[Layer, tuple[Tensor], tuple[Tensor]], [tuple[Tensor]]
|
||||
],
|
||||
]
|
||||
| None
|
||||
) = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
assert callable(fn)
|
||||
self.fn = fn
|
||||
|
||||
def apply(self, layer, process_mesh, shard_param_list):
|
||||
layer.register_forward_pre_hook(self.fn(process_mesh=process_mesh))
|
||||
|
||||
|
||||
class PrepareLayerOutput(PlanBase):
|
||||
"""
|
||||
Prepare the output of specific layer. User should provide one callable function.
|
||||
|
||||
Args:
|
||||
fn (callable): A function that prepare the layer input. The function should take exactly
|
||||
one parameter named `process_mesh` and return the post hook.
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
|
||||
>>> class MLP(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.fc1 = paddle.nn.Linear(8, 8)
|
||||
... self.fc2 = paddle.nn.Linear(8, 8)
|
||||
...
|
||||
... def forward(self, input):
|
||||
... return self.fc2(self.fc1(input))
|
||||
|
||||
>>> def layer_output_hook(process_mesh):
|
||||
... def hook(layer, input, output):
|
||||
... return output
|
||||
...
|
||||
... return hook
|
||||
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> layer = MLP()
|
||||
>>> mp_config = {
|
||||
... 'fc1': dist.PrepareLayerOutput(layer_output_hook),
|
||||
... }
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
fn: (
|
||||
Callable[
|
||||
[ProcessMesh],
|
||||
Callable[
|
||||
[Layer, tuple[Tensor], tuple[Tensor]], [tuple[Tensor]]
|
||||
],
|
||||
]
|
||||
| None
|
||||
) = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
assert callable(fn)
|
||||
self.fn = fn
|
||||
|
||||
def apply(self, layer, process_mesh, shard_param_list):
|
||||
layer.register_forward_post_hook(self.fn(process_mesh=process_mesh))
|
||||
|
||||
|
||||
class SequenceParallelBegin(PlanBase):
|
||||
"""
|
||||
Sequence parallel plan for mp config.
|
||||
This plan marks the beginning of the sp and should be added to the LAST layer before the sp range.
|
||||
|
||||
Note:
|
||||
DON'T mark any layer in the sp range.
|
||||
|
||||
Args:
|
||||
need_transpose (bool): the default value is `True`. With `need_transpose=True`, this plan will transfer
|
||||
the output from [b, s, h] to [s/mp, b, h]. With `need_transpose=False`, this plan will transfer
|
||||
the output from [s, b, h] to [s/mp, b, h].
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
|
||||
>>> class MLP(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.fc1 = paddle.nn.Linear(8, 8)
|
||||
... self.fc2 = paddle.nn.Linear(8, 8)
|
||||
...
|
||||
... def forward(self, input):
|
||||
... return self.fc2(self.fc1(input))
|
||||
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> layer = MLP()
|
||||
>>> mp_config = {
|
||||
... 'fc1': dist.SequenceParallelBegin(),
|
||||
... }
|
||||
"""
|
||||
|
||||
def __init__(self, need_transpose: bool = True) -> None:
|
||||
super().__init__()
|
||||
self.need_transpose = need_transpose
|
||||
|
||||
def sequence_parallel_begin(self, process_mesh):
|
||||
def begin(layer, input, output):
|
||||
assert output is not None
|
||||
return c_split(output, process_mesh, self.need_transpose)
|
||||
|
||||
return begin
|
||||
|
||||
def apply(self, layer, process_mesh, shard_param_list):
|
||||
layer.register_forward_post_hook(
|
||||
self.sequence_parallel_begin(process_mesh)
|
||||
)
|
||||
|
||||
|
||||
class SequenceParallelEnd(PlanBase):
|
||||
"""
|
||||
Sequence parallel plan for mp config.
|
||||
This plan marks the ending of the sp and should be added to the FIRST layer after the sp range.
|
||||
|
||||
Note:
|
||||
DON'T mark any layer in the sp range.
|
||||
|
||||
Args:
|
||||
need_transpose (bool): the default value is `True`. With `need_transpose=True`, this plan will transfer
|
||||
the input from [s/mp, b, h] to [b, s, h]. With `need_transpose=False`, this plan will transfer the
|
||||
input from [s/mp, b, h] to [s, b, h].
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
|
||||
>>> class MLP(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.fc1 = paddle.nn.Linear(8, 8)
|
||||
... self.fc2 = paddle.nn.Linear(8, 8)
|
||||
...
|
||||
... def forward(self, input):
|
||||
... return self.fc2(self.fc1(input))
|
||||
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> layer = MLP()
|
||||
>>> mp_config = {
|
||||
... 'fc1': dist.SequenceParallelEnd(),
|
||||
... }
|
||||
"""
|
||||
|
||||
def __init__(self, need_transpose: bool = True) -> None:
|
||||
super().__init__()
|
||||
self.need_transpose = need_transpose
|
||||
|
||||
def sequence_parallel_end(self, process_mesh):
|
||||
def end(layer, input, output=None):
|
||||
assert input is not None
|
||||
return c_concat(input, process_mesh, self.need_transpose)
|
||||
|
||||
return end
|
||||
|
||||
def apply(self, layer, process_mesh, shard_param_list):
|
||||
layer.register_forward_pre_hook(
|
||||
self.sequence_parallel_end(process_mesh)
|
||||
)
|
||||
|
||||
|
||||
class SequenceParallelEnable(PlanBase):
|
||||
"""
|
||||
Sequence parallel plan for mp config.
|
||||
Do sequence parallel on the layer. Note the input should be in [b, s, h] format.
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
|
||||
>>> class MLP(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.fc1 = paddle.nn.Linear(8, 8)
|
||||
... self.fc2 = paddle.nn.Linear(8, 8)
|
||||
...
|
||||
... def forward(self, input):
|
||||
... return self.fc2(self.fc1(input))
|
||||
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> layer = MLP()
|
||||
>>> mp_config = {
|
||||
... 'fc1': dist.SequenceParallelEnable(),
|
||||
... }
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
def sequence_parallel_begin(self, process_mesh):
|
||||
def begin(layer, input, output=None):
|
||||
assert input is not None
|
||||
return c_split(input, process_mesh, True)
|
||||
|
||||
return begin
|
||||
|
||||
def sequence_parallel_end(self, process_mesh):
|
||||
def end(layer, input, output):
|
||||
assert output is not None
|
||||
return c_concat(output, process_mesh, True)
|
||||
|
||||
return end
|
||||
|
||||
def apply(self, layer, process_mesh, shard_param_list):
|
||||
logging.warning(
|
||||
"Sequence parallel with the usage of SequenceParallel may not reach the best throughput. "
|
||||
"Try to use SequenceParallelBegin/End to achieve better performance"
|
||||
)
|
||||
layer.register_forward_pre_hook(
|
||||
self.sequence_parallel_begin(process_mesh)
|
||||
)
|
||||
layer.register_forward_post_hook(
|
||||
self.sequence_parallel_end(process_mesh)
|
||||
)
|
||||
|
||||
|
||||
class SequenceParallelDisable(PlanBase):
|
||||
"""
|
||||
Sequence parallel plan for mp config.
|
||||
Disable sequence parallel on the layer.
|
||||
|
||||
Args:
|
||||
need_transpose (bool): the default value is `True`. If the need_transpose is `True`: this plan will transfer
|
||||
the input from [s/mp, b, h] to [b, s, h] and then transfer the output from [b, s, h] to [s/mp, b, h].
|
||||
If the need_transpose is `False`: this plan will transfer the input from [s/mp, b, h] to [s, b, h] and
|
||||
then transfer the output from [s, b, h] to [s/mp, b, h].
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
|
||||
>>> class MLP(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.fc1 = paddle.nn.Linear(8, 8)
|
||||
... self.fc2 = paddle.nn.Linear(8, 8)
|
||||
...
|
||||
... def forward(self, input):
|
||||
... return self.fc2(self.fc1(input))
|
||||
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> layer = MLP()
|
||||
>>> mp_config = {
|
||||
... 'fc1': dist.SequenceParallelDisable(),
|
||||
... }
|
||||
"""
|
||||
|
||||
def __init__(self, need_transpose: bool = True) -> None:
|
||||
super().__init__()
|
||||
self.need_transpose = need_transpose
|
||||
|
||||
def sequence_parallel_begin(self, process_mesh):
|
||||
def begin(layer, input, output=None):
|
||||
return c_split(output, process_mesh, self.need_transpose)
|
||||
|
||||
return begin
|
||||
|
||||
def sequence_parallel_end(self, process_mesh):
|
||||
def end(layer, input, output=None):
|
||||
return c_concat(input, process_mesh, self.need_transpose)
|
||||
|
||||
return end
|
||||
|
||||
def apply(self, layer, process_mesh, shard_param_list):
|
||||
layer.register_forward_pre_hook(
|
||||
self.sequence_parallel_end(process_mesh)
|
||||
)
|
||||
|
||||
layer.register_forward_post_hook(
|
||||
self.sequence_parallel_begin(process_mesh)
|
||||
)
|
||||
|
||||
|
||||
class ConvParallel(PlanBase):
|
||||
"""
|
||||
A strategy for enabling spatial parallelism on ``paddle.nn.Conv2D`` layers
|
||||
by sharding the input tensor along its Width (W) dimension.
|
||||
|
||||
When this ``ConvParallel`` configuration is applied to a ``Conv2D`` layer,
|
||||
the layer's input tensor will have its width dimension split across devices
|
||||
in the model parallel group. This can help reduce memory usage from activations,
|
||||
especially when dealing with inputs that have a large width.
|
||||
|
||||
To enable width-wise input sharding correctly, make sure your `Conv2D` layer
|
||||
satisfies the following conditions along the width dimension:
|
||||
|
||||
- **Dilation** must be set to `1`.
|
||||
- **If no width padding is used:**
|
||||
- The input width must be evenly divisible by the stride width.
|
||||
- The stride width must be equal to the kernel width.
|
||||
- **If width padding is used:**
|
||||
- The stride width must be `1`.
|
||||
- The total input width must be at least half the kernel width.
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.nn as nn
|
||||
>>> import paddle.distributed as dist
|
||||
|
||||
>>> class SimpleConvNet(nn.Layer):
|
||||
... def __init__(self, data_format="NCHW"):
|
||||
... super().__init__()
|
||||
... self.conv1 = nn.Conv2D(
|
||||
... 3,
|
||||
... 8,
|
||||
... kernel_size=3,
|
||||
... padding=1,
|
||||
... data_format=data_format,
|
||||
... )
|
||||
... self.relu = nn.ReLU()
|
||||
...
|
||||
... def forward(self, x):
|
||||
... x = self.conv1(x)
|
||||
... return self.relu(x)
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> model = SimpleConvNet(data_format="NCHW")
|
||||
>>> mp_config = {
|
||||
... "parallelize_plan": {
|
||||
... "conv1": dist.ConvParallel(),
|
||||
... },
|
||||
... }
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
@staticmethod
|
||||
def _is_supported(
|
||||
input_size,
|
||||
kernel_size,
|
||||
stride,
|
||||
padding,
|
||||
dilation,
|
||||
data_format,
|
||||
mp_group_size,
|
||||
):
|
||||
idx_w_input = -1
|
||||
idx_w_kernel = -1
|
||||
|
||||
if data_format == "NCHW":
|
||||
idx_w_input = 3
|
||||
idx_w_kernel = 3
|
||||
elif data_format == "NHWC":
|
||||
idx_w_input = 2
|
||||
idx_w_kernel = 3
|
||||
else:
|
||||
return False
|
||||
|
||||
if input_size[idx_w_input] % mp_group_size != 0:
|
||||
return False
|
||||
|
||||
dilation_w = dilation[1]
|
||||
padding_w = padding[1]
|
||||
stride_w = stride[1]
|
||||
|
||||
input_w = input_size[idx_w_input]
|
||||
kernel_w = kernel_size[idx_w_kernel]
|
||||
|
||||
if dilation_w != 1:
|
||||
# RingConv2d only supports dilation=1.
|
||||
# Larger dilation would require enlarged halo regions and more complex communication.
|
||||
return False
|
||||
|
||||
if padding_w == 0:
|
||||
# To avoid halo exchange when padding=0, we require:
|
||||
# - input_w must be divisible by stride_w, so partitions align evenly across ranks.
|
||||
# - stride_w == kernel_w, so each kernel operates on disjoint local regions.
|
||||
if input_w % stride_w != 0:
|
||||
return False
|
||||
if stride_w != kernel_w:
|
||||
return False
|
||||
|
||||
else:
|
||||
# When padding > 0, halo exchange is needed.
|
||||
# To simplify halo logic, we require:
|
||||
# - stride_w == 1: ensures each output element is computed from overlapping input,
|
||||
# and no input region is skipped, simplifying halo construction.
|
||||
# - kernel_w // 2 <= input_w: prevents the kernel from exceeding local input.
|
||||
if stride_w != 1:
|
||||
return False
|
||||
if kernel_w // 2 > input_w:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def conv_parallel_start(self, process_mesh, data_format):
|
||||
def start(layer, input, output=None):
|
||||
if data_format == "NCHW":
|
||||
shard_w_dim = 3
|
||||
elif data_format == "NHWC":
|
||||
shard_w_dim = 2
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported data_format: {data_format}. "
|
||||
"Only NCHW and NHWC are supported."
|
||||
)
|
||||
|
||||
if isinstance(input, tuple):
|
||||
x = input[0]
|
||||
else:
|
||||
x = input
|
||||
|
||||
placements = x.placements
|
||||
mp_index = process_mesh.dim_names.index('mp')
|
||||
mp_group_size = process_mesh.get_dim_size('mp')
|
||||
|
||||
# Note(luchang): for intermediate api, when this ConvLayer is
|
||||
# not supported, we just skip apply parallelization.
|
||||
if not ConvParallel._is_supported(
|
||||
x.shape,
|
||||
layer.weight.shape,
|
||||
layer._stride,
|
||||
layer._updated_padding,
|
||||
layer._dilation,
|
||||
data_format,
|
||||
mp_group_size,
|
||||
):
|
||||
return input
|
||||
|
||||
if placements is None:
|
||||
placements = [
|
||||
dist.Replicate() for _ in range(len(process_mesh.shape))
|
||||
]
|
||||
if placements[mp_index] == dist.Shard(shard_w_dim):
|
||||
return input
|
||||
|
||||
placements[mp_index] = dist.Shard(shard_w_dim)
|
||||
|
||||
if not x.is_dist():
|
||||
x = dist.shard_tensor(x, process_mesh, placements)
|
||||
else:
|
||||
x = dist.reshard(x, process_mesh, placements)
|
||||
|
||||
if isinstance(input, tuple):
|
||||
input = list(input)
|
||||
input[0] = x
|
||||
input = tuple(input)
|
||||
else:
|
||||
input = x
|
||||
return input
|
||||
|
||||
return start
|
||||
|
||||
def apply(self, layer, process_mesh, shard_param_list):
|
||||
layer.register_forward_pre_hook(
|
||||
self.conv_parallel_start(process_mesh, layer._data_format)
|
||||
)
|
||||
|
||||
|
||||
class TensorParallel(ParallelModel):
|
||||
def __init__(self, model, parallelize_plan=None):
|
||||
super().__init__(model)
|
||||
if parallelize_plan is not None:
|
||||
assert isinstance(parallelize_plan, dict)
|
||||
for key, plan in parallelize_plan.items():
|
||||
assert isinstance(key, str), (
|
||||
"The key of the parallelize plan should be a string."
|
||||
)
|
||||
if not isinstance(plan, list):
|
||||
plan = [plan]
|
||||
for p in plan:
|
||||
assert isinstance(p, PlanBase), (
|
||||
"The value the the parallelize plan should be a instance of PlanBase or a list of PlanBase."
|
||||
)
|
||||
|
||||
self.global_mesh = dist.auto_parallel.get_mesh()
|
||||
self.parallelize_plan = parallelize_plan
|
||||
self.tp_parallelizer = self.tensor_parallelizer_fn
|
||||
|
||||
def match_layer(self, layer, name):
|
||||
# Match the layer to a plan.
|
||||
# Will return the plan if the layer hits one, otherwise return None.
|
||||
plans = []
|
||||
for key, plan in self.parallelize_plan.items():
|
||||
attr_name = key.split('.')[-1]
|
||||
shard_param_list = []
|
||||
# Find some plan for specific parameter, such as
|
||||
# "lm_head.weight": ColWiseParallel()
|
||||
# "qkv_proj.lora_A" ColWiseParallel()
|
||||
# if there is no plan for specific parameter, layer will be sharded by default: layer.weight and layer.bias
|
||||
if key.endswith(f".{attr_name}"):
|
||||
if hasattr(layer, attr_name) and is_tensor(
|
||||
getattr(layer, attr_name)
|
||||
):
|
||||
key = key.replace(f".{attr_name}", "")
|
||||
shard_param_list.append(attr_name)
|
||||
re_find = re.match(key, name)
|
||||
if key == name or (
|
||||
re_find is not None
|
||||
and int(re_find.end()) - int(re_find.start()) == len(name)
|
||||
):
|
||||
if isinstance(plan, PlanBase):
|
||||
plan = [plan]
|
||||
plans.append([plan, shard_param_list])
|
||||
return plans
|
||||
|
||||
def tensor_parallelizer_fn(self, model):
|
||||
if self.parallelize_plan is None:
|
||||
return
|
||||
layer_param_placements = {}
|
||||
share_param_list = {}
|
||||
for name, layer in model.named_sublayers():
|
||||
for param_name in list(layer._parameters.keys()):
|
||||
param = getattr(layer, param_name)
|
||||
if param.name not in share_param_list:
|
||||
share_param_list[param.name] = 1
|
||||
continue
|
||||
share_param_list[param.name] += 1
|
||||
for name, layer in model.named_sublayers():
|
||||
plans = self.match_layer(layer, name)
|
||||
layer_param_placements[layer] = {}
|
||||
if len(plans) > 0:
|
||||
pp_idx = getattr(layer, "pipeline_stage_index", 0)
|
||||
for plan in plans:
|
||||
real_plan, shard_param_list = plan
|
||||
for p in real_plan:
|
||||
p.share_param_list = share_param_list
|
||||
param_placements = p.apply(
|
||||
layer, self.get_mesh(pp_idx), shard_param_list
|
||||
)
|
||||
if param_placements is not None and param_placements:
|
||||
layer_param_placements[layer].update(
|
||||
param_placements
|
||||
)
|
||||
return model, layer_param_placements
|
||||
|
||||
|
||||
def tensor_parallel(model, optimizer=None, config=None):
|
||||
"""
|
||||
Tensor parallel.
|
||||
Args:
|
||||
model (paddle.nn.Layer): the model to be shard into tensor parallel.
|
||||
optimizer (paddle.optimizer.Optimizer): the optimizer.
|
||||
config (dict): {
|
||||
"parallelize_plan": dict, the plan to shard the layer.
|
||||
}
|
||||
Returns:
|
||||
model: model after tp
|
||||
optimizer: optimizer after tp
|
||||
|
||||
NOTE: the plan should be a dict maps layer name or parameter name to a split_plan,
|
||||
which will be used to split the layer or the parameter. The name can be written in regular format.
|
||||
|
||||
An example for the plan is:
|
||||
```
|
||||
plan = {
|
||||
"llama.embed_tokens": ColWiseParallel(),
|
||||
"llama.layers.*.self_attn.q_proj": ColWiseParallel(),
|
||||
"llama.layers.*.self_attn.k_proj": ColWiseParallel(),
|
||||
"llama.layers.*.self_attn.v_proj": ColWiseParallel(),
|
||||
"llama.layers.*.self_attn.o_proj": RowWiseParallel(),
|
||||
"llama.layers.*.mlp.gate_proj": ColWiseParallel(),
|
||||
"llama.layers.*.mlp.up_proj": ColWiseParallel(),
|
||||
"llama.layers.*.mlp.down_proj": RowWiseParallel(),
|
||||
"lm_head.weight": ColWiseParallel(),
|
||||
}
|
||||
```
|
||||
"""
|
||||
parallelize_plan = config.get("parallelize_plan")
|
||||
if parallelize_plan is None:
|
||||
# Do nothing if no plan.
|
||||
logging.warning(
|
||||
"No parallelize plan, tensor parallel won't do anything."
|
||||
)
|
||||
return model, optimizer
|
||||
|
||||
global_mesh = dist.auto_parallel.get_mesh()
|
||||
|
||||
assert global_mesh is not None, (
|
||||
"global mesh must not be None, please call fleet.auto.set_mesh(global_mesh) firstly"
|
||||
)
|
||||
assert "mp" in global_mesh.dim_names, (
|
||||
"mp must in the mesh dim_names when use tensor_parallel"
|
||||
)
|
||||
|
||||
model = TensorParallel(model, parallelize_plan)
|
||||
if optimizer is not None:
|
||||
optimizer = ParallelOptimizer(optimizer)
|
||||
|
||||
return model, optimizer
|
||||
Reference in New Issue
Block a user