# 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