chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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