# 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