CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
tensor_parallel.py1117 linesDownload Raw Back to integrations
1# Copyright 2024 The HuggingFace Team. All rights reserved.2#3# Licensed under the Apache License, Version 2.0 (the "License");4# you may not use this file except in compliance with the License.5# You may obtain a copy of the License at6#7#     http://www.apache.org/licenses/LICENSE-2.08#9# Unless required by applicable law or agreed to in writing, software10# distributed under the License is distributed on an "AS IS" BASIS,11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12# See the License for the specific language governing permissions and13# limitations under the License.14from __future__ import annotations15 16import math17import operator18import os19import re20from functools import partial, reduce21 22import torch23import torch.distributed as dist24from torch import nn25 26from ..distributed import DistributedConfig27from ..utils import is_torch_greater_or_equal, logging28from ..utils.generic import GeneralInterface29 30 31logger = logging.get_logger(__name__)32 33# Cache this result has it's a C FFI call which can be pretty time-consuming34_torch_distributed_available = torch.distributed.is_available()35 36 37if is_torch_greater_or_equal("2.5") and _torch_distributed_available:38    from torch.distributed.tensor import DTensor, Placement, Replicate, Shard39 40 41def initialize_tensor_parallelism(tp_plan, tp_size=None):42    r"""43    Sets up the device mesh and initialized the backend for tensor parallelism.44    This function is called when the model is loaded and the TP plan is set to 'auto'.45    """46    if tp_plan is None:47        return None, None, None48 49    if not is_torch_greater_or_equal("2.5"):50        raise OSError("Tensor parallel is only supported for `torch>=2.5`.")51 52    # Detect the accelerator on the machine. If no accelerator is available, it returns CPU.53    device_type = torch._C._get_accelerator().type54    current_device = getattr(torch, device_type)55    if not torch.distributed.is_initialized():56        try:57            rank = int(os.environ["RANK"])58            local_rank = int(os.environ["LOCAL_RANK"])59            world_size = int(os.environ["WORLD_SIZE"])60 61            backend_map = {"cuda": "nccl", "cpu": "gloo", "xpu": "xccl", "hpu": "hccl"}62            backend = backend_map.get(device_type)63            if device_type == "cpu" and int(os.environ.get("CCL_WORKER_COUNT", "0")):64                backend = "ccl"65            if device_type == "xpu" and not is_torch_greater_or_equal("2.8", accept_dev=True):66                backend = "ccl"67 68            torch.distributed.init_process_group(backend=backend, rank=rank, world_size=world_size)69            current_device = getattr(torch, device_type)70            if device_type != "cpu":71                current_device.set_device(local_rank)72 73        except Exception as e:74            raise OSError(75                "We tried to initialize torch.distributed for you, but it failed. Make "76                "sure you init torch distributed in your script to use `tp_plan='auto'`."77            ) from e78 79    if device_type != "cpu":80        current_device.set_device(int(os.environ["LOCAL_RANK"]))81    index = current_device.current_device() if device_type != "cpu" else None82    tp_device = torch.device(device_type, index)83 84    # Silence output for non-primary ranks85    if index is not None and index > 0:86        import sys87 88        sys.stdout = open(os.devnull, "w")89        sys.stderr = open(os.devnull, "w")90 91    device_map = tp_device92    tp_size = tp_size if tp_size is not None else torch.distributed.get_world_size()93    device_mesh = torch.distributed.init_device_mesh(tp_device.type, (tp_size,))94    return tp_device, device_map, device_mesh, tp_size95 96 97def _blocks_to_block_sizes(total_size: int, blocks: int | list[int]) -> list[int]:98    """99    Convert block count or proportions to block sizes.100 101    This function accepts102 103    - The number of blocks (int), in which case the block size is104      total_size//blocks; or105    - A list of block sizes (list[int]).106 107    In the second case, if sum(blocks) < total_size, the ratios between108    the block sizes will be preserved. For instance, if blocks is109    [2, 1, 1] and total_size is 1024, the returned block sizes are110    [512, 256, 256].111    """112    if isinstance(blocks, list):113        total_blocks = sum(blocks)114        assert total_size % total_blocks == 0, f"Cannot split {total_size} in proportional blocks: {blocks}"115        part_size = total_size // total_blocks116        return [part_size * block for block in blocks]117    else:118        assert total_size % blocks == 0, f"Prepacked is not divisible by {blocks}"119        single_size = total_size // blocks120        return [single_size] * blocks121 122 123def _get_parameter_tp_plan(parameter_name: str, tp_plan: dict[str, str], is_weight=True) -> str | None:124    """125    Get the TP style for a parameter from the TP plan.126 127    The TP plan is a dictionary that maps parameter names to TP styles.128    The parameter name can be a generic name with wildcards (e.g. "*.weight") or a specific name (e.g. "layer_1.weight").129 130    The `is_weight` is important because for weights, we want to support `.weights` and `.bias` cases seamlessly! but131    not parent classes for `post_init` calls132    """133    generic_param_name = re.sub(r"\d+", "*", parameter_name)134    if generic_param_name in tp_plan:135        return tp_plan[generic_param_name]136    elif "." in generic_param_name and generic_param_name.rsplit(".", 1)[0] in tp_plan and is_weight:137        return tp_plan[generic_param_name.rsplit(".", 1)[0]]138    return None139 140 141str_to_dtype = {142    "BOOL": torch.bool,143    "U8": torch.uint8,144    "I8": torch.int8,145    "I16": torch.int16,146    "F16": torch.float16,147    "BF16": torch.bfloat16,148    "I32": torch.int32,149    "F32": torch.float32,150    "F64": torch.float64,151    "I64": torch.int64,152    "F8_E4M3": torch.float8_e4m3fn,153}154 155 156def get_packed_weights(param, empty_param, device_mesh, rank, dim):157    """158    When weights are packed (gate_up_proj), we need to make sure each shard gets its correct share.159    So if you have: gate_proj       ( 16, 5120, 8190)160    and             up_proj         ( 16, 5120, 8190)161    packed as       gate_up_proj    ( 16, 5120, 2 * 8190)162    And you shard along the last dimension, you need to interleave the gate and up values:163 164    Now, if we shard along the last dimension across TP_size (Tensor Parallelism size), we must interleave the values from gate and up projections correctly.165 166    Let's take TP_size = 4 for an example:167 168    Packed tensor `gate_up_proj`169    ---------------------------------------------------------------170    [ G0  G1  G2  G3 | G4  G5  G6  G7 | ... | U0  U1  U2  U3 | U4  U5  U6  U7 | ... ]171     ↑─────────────↑   ↑─────────────↑        ↑─────────────↑  ↑─────────────↑172       Gate Slice 0      Gate Slice 1            Up Slice 0       Up Slice 1173 174    Explanation:175    - The first half of the tensor (left of the center) holds the gate_proj values.176    - The second half (right of the center) holds the up_proj values.177    - For TP=4, we divide each half into 4 slices. In this example, we show two slices for brevity.178    - Each shard receives one slice from the gate part and the corresponding slice from the up part.179 180    For instance:181    • Shard 0 gets: [ Gate Slice 0, Up Slice 0 ] = [ G0, G1, G2, G3, U0, U1, U2, U3 ]182    • Shard 1 gets: [ Gate Slice 1, Up Slice 1 ] = [ G4, G5, G6, G7, U4, U5, U6, U7 ]183    • … and so on.184 185    This ensures that each shard receives an equal portion of both gate and up projections, maintaining consistency across tensor parallelism.186    """187    slice_ = param188    total_size = empty_param.shape[dim]189    world_size = device_mesh.size()190    block_sizes = _blocks_to_block_sizes(total_size=total_size, blocks=2)191 192    tensors_slices = []193    block_offset = 0194    for block_size in block_sizes:195        shard_block_size = block_size // world_size196        start = rank * shard_block_size197        stop = (rank + 1) * shard_block_size198        tensors_slices += range(block_offset + start, block_offset + stop)199        block_offset += block_size200 201    slice_dtype = slice_.get_dtype()202    # Handle F8_E4M3 dtype by converting to float16 before slicing203    # Without upcasting, the slicing causes : RuntimeError: "index_cpu" not implemented for 'Float8_e4m3fn'204    casted = False205    if slice_dtype == "F8_E4M3" or slice_dtype == "F8_E5M2":206        slice_ = slice_[...].to(torch.float16)207        casted = True208 209    if dim == 0:210        tensor = slice_[tensors_slices, ...]211    elif dim == 1 or dim == -2:212        tensor = slice_[:, tensors_slices, ...]213    elif dim == 2 or dim == -1:214        tensor = slice_[..., tensors_slices]215    else:216        raise ValueError(f"Unsupported dim {dim}, only dim 0, 1 or 2 are supported")217 218    if casted:219        return tensor220    else:221        return tensor.to(str_to_dtype[slice_dtype])222 223 224def repack_weights(225    packed_parameter: torch.Tensor,226    sharded_dim: int,  # The dimension index in the global tensor that was sharded227    world_size: int,228    num_blocks: int = 2,229) -> torch.Tensor:230    """231    Reorders a tensor that was reconstructed from sharded packed weights into its canonical packed format.232 233    For example, if a weight was packed (e.g., gate_proj and up_proj) and then sharded,234    DTensor.full_tensor() might produce an interleaved layout like [G0, U0, G1, U1, ...]235    along the sharded dimension. This function reorders it to [G0, G1, ..., U0, U1, ...].236    This is an inverse operation to get_packed_weights.237 238    Args:239        reconstructed_tensor: The tensor reconstructed from DTensor (e.g., via .full_tensor().contiguous()).240        sharded_dim: The dimension index in the reconstructed_tensor that was originally sharded.241        world_size: The tensor parallel world size.242        num_packed_projs: The number of projections that were packed together (e.g., 2 for gate_up_proj).243 244    Returns:245        The reordered tensor in canonical packed format.246    """247 248    if num_blocks != 2:249        raise ValueError(250            "Num blocks different from 2 is not supported yet. This is most likely a bug in your implementation as we only pack gate and up projections together."251        )252 253    actual_sharded_dim = sharded_dim if sharded_dim >= 0 else sharded_dim + packed_parameter.ndim254    total_size_on_sharded_dim = packed_parameter.shape[actual_sharded_dim]255    original_block_size_on_dim = total_size_on_sharded_dim // num_blocks256    shard_chunk_size = original_block_size_on_dim // world_size257 258    prefix_shape = packed_parameter.shape[:actual_sharded_dim]259    suffix_shape = packed_parameter.shape[actual_sharded_dim + 1 :]260 261    tensor_view = packed_parameter.view(262        *prefix_shape,263        world_size,264        num_blocks,265        shard_chunk_size,266        *suffix_shape,267    )268 269    # Permute to bring num_packed_projs first, then world_size, then shard_chunk_size270    # This groups all chunks of G together, then all chunks of U together.271    # Target order of these middle dimensions: (num_packed_projs, world_size, shard_chunk_size)272    # Current order of view's middle dimensions: (world_size, num_packed_projs, shard_chunk_size)273    # Absolute indices of the dimensions to be permuted (world_size, num_packed_projs)274    axis_ws_abs = len(prefix_shape)275    axis_npp_abs = len(prefix_shape) + 1276 277    permute_order = list(range(tensor_view.ndim))278    permute_order[axis_ws_abs], permute_order[axis_npp_abs] = permute_order[axis_npp_abs], permute_order[axis_ws_abs]279 280    tensor_permuted = tensor_view.permute(*permute_order)281 282    # Reshape back to the original tensor's ndim, with the sharded dimension now correctly ordered as [G_all, U_all].283    # The final shape should be the same as reconstructed_tensor.284    final_ordered_tensor = tensor_permuted.reshape_as(packed_parameter)285 286    return final_ordered_tensor287 288 289def get_tensor_shard(param, empty_param, device_mesh, rank, dim):290    """291    Generalized tensor sharding across a multi-dimensional device mesh.292    Extract only the fraction of the parameter owned by the given `rank` when the parameter would have gone sharding at provided `dim`.293    Extraction follows the pytorch `Shard` placement so that sharding and materializing back to full tensor follows `Shard` semantics.294    `Shard` follows torch.chunk style sharding of the tensor. We demonstrate some cases below on how sharding happens including some edge cases295    such as some ranks having an empty tensor as shard. Below implementation is robut to all these cases.296 297    Case (1)298    empty_param                 (16, 5120, 8190)299    dim                         0300    device_mesh.size()          4301    rank 0 gets					(4, 5120, 8190)			 (0 ... 4, 5120, 8190)302    rank 1 gets					(4, 5120, 8190)			 (4 ... 8, 5120, 8190)303    rank 2 gets					(4, 5120, 8190)			 (8 ... 12, 5120, 8190)304    rank 3 gets					(4, 5120, 8190)			 (12 ... 16, 5120, 8190)305 306    Case (2)307    empty_param                 (16, 5120, 8190)308    dim                         0309    device_mesh.size()          14310    rank 0 gets					(2, 5120, 8190)			 (0 ... 2, 5120, 8190)311    rank 1 gets					(2, 5120, 8190)			 (2 ... 4, 5120, 8190)312    rank 2 gets					(2, 5120, 8190)			 (4 ... 6, 5120, 8190)313    rank 3 gets					(2, 5120, 8190)			 (6 ... 8, 5120, 8190)314    rank 4 gets					(2, 5120, 8190)			 (8 ... 10, 5120, 8190)315    rank 5 gets					(2, 5120, 8190)			 (10 ... 12, 5120, 8190)316    rank 6 gets					(2, 5120, 8190)			 (12 ... 14, 5120, 8190)317    rank 7 gets					(2, 5120, 8190)			 (14 ... 16, 5120, 8190)318    rank 8 gets					(0, 5120, 8190)319    rank 9 gets					(0, 5120, 8190)320    rank 10 gets			    (0, 5120, 8190)321    rank 11 gets				(0, 5120, 8190)322    rank 12 gets				(0, 5120, 8190)323    rank 13 gets				(0, 5120, 8190)324 325    Case (3)326    empty_param                 (16, 5120, 8190)327    dim                         0328    device_mesh.size()          3329    rank 0 gets					(6, 5120, 8190)			 (0 ... 6, 5120, 8190)330    rank 1 gets					(6, 5120, 8190)			 (6 ... 12, 5120, 8190)331    rank 2 gets					(4, 5120, 8190)			 (12 ... 16, 5120, 8190)332 333    In case (2), empty shards are returned with appropriate dimension to allow for operations to work smoothly.334    Args:335        param (torch.Tensor): The tensor to shard.336        empty_param (torch.Tensor): A tensor used for shape reference.337        device_mesh (torch.Tensor): Shape [d_0, ..., d_n] representing the mesh.338        rank (int): Global rank of the current process/device.339        dim (int): Dimension along which to shard the tensor.340    """341    param_dim = empty_param.dim()342 343    if dim < 0:344        dim = param_dim + dim345    if dim >= param_dim:346        raise ValueError(f"dim {dim} is out of bounds for tensor of dimension {param_dim}")347 348    # Flatten the mesh to get the total number of devices349    mesh_shape = device_mesh.shape350    world_size = reduce(operator.mul, mesh_shape)351 352    if rank >= world_size:353        raise ValueError(f"Rank {rank} is out of bounds for mesh size {world_size}")354 355    shard_size = math.ceil(empty_param.shape[dim] / world_size)356    start = rank * shard_size357 358    # Construct slicing index dynamically359    end = min(start + shard_size, empty_param.shape[dim])360    slice_indices = [slice(None)] * param_dim361    if start < empty_param.shape[dim]:362        slice_indices[dim] = slice(start, end)363        return param[tuple(slice_indices)]364    dimensions = list(param.shape)365    dimensions[dim] = 0366    return torch.empty(tuple(dimensions), dtype=torch.int64)367 368 369def distribute_module(370    module: nn.Module,371    device_mesh=None,372    input_fn=None,373    output_fn=None,374) -> nn.Module:375    """376    Copy pasted from torch's function but we remove the communications (partitioning)377    as well as buffer registering that is similarly not efficient.378    """379    if len(module._forward_pre_hooks) == 0:380        if input_fn is not None:381            module.register_forward_pre_hook(lambda mod, inputs: input_fn(mod, inputs, device_mesh))382        if output_fn is not None:383            module.register_forward_hook(lambda mod, inputs, outputs: output_fn(mod, outputs, device_mesh))384    return module385 386 387class TensorParallelLayer:388    """389    General tensor parallel layer for transformers.390    """391 392    use_dtensor = True393 394    @staticmethod395    def _prepare_input_fn(input_layouts, desired_input_layouts, mod, inputs, device_mesh): ...396 397    @staticmethod398    def _prepare_output_fn(output_layouts, use_local_output, mod, outputs, device_mesh): ...399 400    def partition_tensor(self, param, empty_param, param_type, param_casting_dtype, to_contiguous, rank, device_mesh):401        raise NotImplementedError402 403    def prepare_module_tp(self, module: nn.Module, device_mesh) -> nn.Module:404        if self.use_dtensor:405            distribute_module(406                module,407                device_mesh,408                partial(self._prepare_input_fn, self.input_layouts, self.desired_input_layouts),409                partial(self._prepare_output_fn, self.output_layouts, self.use_local_output),410            )411 412 413# use_dtensor needs to be set to false for nn.Parameter when you want to view, chunk, slice414# you name it. Whatever you want to do that is a bit unconventional, you need local tensors415class GatherParallel(TensorParallelLayer):416    """417    Simple class used to define the hooks to add to a layer when we just want to gather the outputs418    """419 420    def __init__(421        self,422        *,423        input_layouts: Placement | None = None,424        output_layouts: Placement | None = None,425        use_local_output: bool = True,426    ):427        super().__init__()428        self.input_layouts = (input_layouts or Replicate(),)429        self.output_layouts = output_layouts430        self.desired_input_layouts = (Replicate(),)431        self.use_local_output = use_local_output432 433    @staticmethod434    def _prepare_input_fn(input_layouts, desired_input_layouts, mod, inputs, device_mesh):435        mod.expert_parallel_group = device_mesh.get_group()436        if inputs and isinstance(inputs[0], DTensor):437            inputs = inputs[0].to_local()438        return inputs439 440    @staticmethod441    def _prepare_output_fn(output_layouts, use_local_output, mod, outputs, device_mesh):442        if isinstance(outputs, torch.Tensor):443            dist.all_reduce(outputs, op=dist.ReduceOp.SUM, async_op=False)444        else:445            dist.all_reduce(outputs[0], op=dist.ReduceOp.SUM, async_op=False)446        return outputs447 448    def prepare_module_tp(self, module: nn.Module, device_mesh) -> nn.Module:449        distribute_module(450            module,451            device_mesh,452            partial(self._prepare_input_fn, None, None),453            partial(self._prepare_output_fn, None, None),454        )455 456 457class IsolatedParallel(TensorParallelLayer):458    """459    This class is used to isolate computation in a TP layer from the rest of the world.460    Parameters need to be LOCAL, so not dtensors461    """462 463    @staticmethod464    def _prepare_input_fn(input_layouts, desired_input_layouts, mod, inputs, device_mesh=None):465        # annotate module input placements/sharding with input_layouts466        input_tensor = inputs[0]467        if isinstance(input_tensor, DTensor):468            input_tensor = input_tensor.to_local()469        return input_tensor470 471    @staticmethod472    def _prepare_output_fn(output_layouts, use_local_output, mod, outputs, device_mesh=None):473        # TODO: figure out dynamo support for instance method and switch this to instance method474        return outputs475 476    def partition_tensor(self, param, empty_param, param_type, param_casting_dtype, to_contiguous, rank, device_mesh):477        param = param[...].to(param_casting_dtype)478        if to_contiguous:479            param = param.contiguous()480        param = param / device_mesh.size()  # TODO should be optionable481        # TODO: assumes parent module will allreduce the output afterwards (e.g rowlinear bias is IsolatedParallel and parent module is GatherParallel)482        return param483 484    def prepare_module_tp(self, module: nn.Module, device_mesh) -> nn.Module:485        distribute_module(486            module,487            device_mesh,488            partial(self._prepare_input_fn, None, None),489            partial(self._prepare_output_fn, None, None),490        )491 492 493class ReplicateParallel(TensorParallelLayer):494    """495    This class is used to replicate computation in a TP layer (used in SP regions when we don't use sequence parallelism for example)496    """497 498    def __init__(self, *, use_dtensor=True, use_local_output=True):499        super().__init__()500        self.input_layouts = (Replicate(),)501        self.output_layouts = (Replicate(),)502        self.desired_input_layouts = (Replicate(),)503        self.use_local_output = use_local_output504        self.use_dtensor = use_dtensor505 506    @staticmethod507    def _prepare_input_fn(input_layouts, desired_input_layouts, mod, inputs, device_mesh):508        # TODO: figure out dynamo support for instance method and switch this to instance method509        # annotate module input placements/sharding with input_layouts510        input_tensor = inputs[0]511        if not isinstance(input_tensor, DTensor):512            input_tensor = DTensor.from_local(input_tensor, device_mesh, input_layouts, run_check=False)513 514        return input_tensor515 516    @staticmethod517    def _prepare_output_fn(output_layouts, use_local_output, mod, outputs, device_mesh):518        return outputs.to_local() if use_local_output and isinstance(outputs, DTensor) else outputs519 520    def partition_tensor(self, param, empty_param, param_type, param_casting_dtype, to_contiguous, rank, device_mesh):521        param = param[...].to(param_casting_dtype)522        if to_contiguous:523            param = param.contiguous()524        param = DTensor.from_local(param, device_mesh, [Replicate()], run_check=False)525        return param526 527 528class ColwiseParallel(TensorParallelLayer):529    """530    General tensor parallel layer for transformers.531    """532 533    def __init__(534        self,535        *,536        input_layouts: Placement | None = None,537        output_layouts: Placement | None = None,538        use_local_output: bool = True,539        use_dtensor=True,540    ):541        super().__init__()542        self.input_layouts = (input_layouts or Replicate(),)543        self.output_layouts = (output_layouts or Shard(-1),)544        self.desired_input_layouts = (Replicate(),)545        self.use_local_output = use_local_output546        self.use_dtensor = use_dtensor547 548    @staticmethod549    def _prepare_input_fn(input_layouts, desired_input_layouts, mod, inputs, device_mesh):550        # TODO: figure out dynamo support for instance method and switch this to instance method551        # annotate module input placements/sharding with input_layouts552        input_tensor = inputs[0]553        if not isinstance(input_tensor, DTensor):554            input_tensor = DTensor.from_local(input_tensor, device_mesh, input_layouts, run_check=False)555 556        # transform the input layouts to the desired layouts of ColwiseParallel557        if input_layouts != desired_input_layouts:558            input_tensor = input_tensor.redistribute(placements=desired_input_layouts, async_op=False)559        return input_tensor560 561    def partition_tensor(self, param, empty_param, param_type, param_casting_dtype, to_contiguous, rank, device_mesh):562        # colwise shard weight/bias to Shard(0), weight be Shard(-2) (0 if you have 1 dim only)563        # means Colwise as Linear is input * weight^T + bias, where564        # weight would become Shard(1)565        if param_type == "bias":566            parameter = get_tensor_shard(param, empty_param, device_mesh, rank, -1)567            shard = [Shard(-1)]568        else:569            shard = [Shard(-2)]570            parameter = get_tensor_shard(param, empty_param, device_mesh, rank, -2)571 572        parameter = parameter.to(param_casting_dtype)573        if to_contiguous:574            parameter = parameter.contiguous()575        if self.use_dtensor:576            parameter = DTensor.from_local(577                parameter, device_mesh, shard, run_check=False, shape=empty_param.size(), stride=empty_param.stride()578            )579        return nn.Parameter(parameter, requires_grad=parameter.is_floating_point())580 581    @staticmethod582    def _prepare_output_fn(output_layouts, use_local_output, mod, outputs, device_mesh):583        # outputs is a shard on last dimension DTensor, i.e. Shard(-1)584        if outputs.placements != output_layouts:585            outputs = outputs.redistribute(placements=output_layouts, async_op=False)586        # back to local tensor587        return outputs.to_local() if use_local_output and isinstance(outputs, DTensor) else outputs588 589 590class PackedColwiseParallel(ColwiseParallel):591    def partition_tensor(self, param, empty_param, param_type, param_casting_dtype, to_contiguous, rank, device_mesh):592        # colwise shard weight/bias to Shard(0), weight be Shard(-2) (0 if you have 1 dim only)593        # means Colwise as Linear is input * weight^T + bias, where594        # weight would become Shard(1)595        parameter = get_packed_weights(param, empty_param, device_mesh, rank, -2)596        parameter = parameter.to(param_casting_dtype)597        if to_contiguous:598            parameter = parameter.contiguous()599        if self.use_dtensor:600            parameter = DTensor.from_local(parameter, device_mesh, [Shard(-2)], run_check=False)601        return nn.Parameter(parameter, requires_grad=parameter.is_floating_point())602 603 604class RowwiseParallel(TensorParallelLayer):605    """606    Partition a compatible nn.Module in a row-wise fashion. Currently supports nn.Linear and nn.Embedding.607    Users can compose it with ColwiseParallel to achieve the sharding of more complicated modules.608    (i.e. MLP, Attention)609 610    Keyword Args:611        input_layouts (Placement, optional):612            The DTensor layout of input tensor for the nn.Module, this is used to annotate the input tensor to613            become a DTensor. If not specified, we assume the input tensor to be sharded on the last dimension.614        output_layouts (Placement, optional):615            The DTensor layout of the output for the nn.Module, this is used to ensure the output of the nn.Module616            with the user desired layout. If not specified, the output tensor is replicated.617        use_local_output (bool, optional):618            Whether to use local :class:`torch.Tensor` instead of :class:`DTensor` for the module output, default: True.619    Returns:620        A :class:`ParallelStyle` object that represents Rowwise sharding of the nn.Module.621    """622 623    def __init__(624        self,625        *,626        input_layouts: Placement | None = None,627        output_layouts: Placement | None = None,628        use_local_output: bool = True,629        use_dtensor=True,630    ):631        super().__init__()632        self.input_layouts = (input_layouts or Shard(-1),)633        self.output_layouts = (output_layouts or Replicate(),)634        self.use_local_output = use_local_output635        self.use_dtensor = use_dtensor636 637    def partition_tensor(self, param, empty_param, param_type, param_casting_dtype, to_contiguous, rank, device_mesh):638        # Rowwise shard weight to Shard(1), bias to Replicate(), weight be Shard(1)639        # means Rowwise as nn.Linear is input * weight^T + bias, where640        # weight would become Shard(0)641        if param_type != "bias":642            parameter = get_tensor_shard(param, empty_param, device_mesh, rank, -1)643            shard = [Shard(-1)]644        else:645            shard = [Replicate()]646            parameter = param[:]647 648        parameter = parameter.to(param_casting_dtype)649        if to_contiguous:650            parameter = parameter.contiguous()651        if self.use_dtensor:652            parameter = DTensor.from_local(653                parameter, device_mesh, shard, run_check=False, shape=empty_param.size(), stride=empty_param.stride()654            )655        return nn.Parameter(parameter, requires_grad=parameter.is_floating_point())656 657    @staticmethod658    def _prepare_input_fn(input_layouts, desired_input_layouts, mod, inputs, device_mesh):659        if hasattr(mod, "bias") and mod.bias is not None:660            mod._bias = mod.bias.to_local()661            mod.bias = None662 663        input_tensor = inputs[0]664        if not isinstance(input_tensor, DTensor):665            input_tensor = DTensor.from_local(input_tensor, device_mesh, input_layouts, run_check=False)666 667        if input_layouts != desired_input_layouts:668            input_tensor = input_tensor.redistribute(placements=desired_input_layouts, async_op=True)669        return input_tensor670 671    @staticmethod672    def _prepare_output_fn(output_layouts, use_local_output, mod, outputs, device_mesh):673        # Rowwise sharding produces partial output, depending on output layouts:674        # 1. to replicate -> allreduce675        # 2. to shard -> reduce_scatter676        if outputs.placements != output_layouts:677            outputs = outputs.redistribute(placements=output_layouts, async_op=True)678        outputs = outputs.to_local()  # otherwise the `+=` op will gather679        if hasattr(mod, "_bias"):680            outputs = outputs + mod._bias681        # back to local tensor if use_local_output is True682        return outputs683 684    def prepare_module_tp(self, module: nn.Module, device_mesh) -> nn.Module:685        module._distribute_module_applied = True686        if self.use_dtensor:687            if isinstance(module, nn.Linear):688                # rowwise linear runtime sharding requires input tensor shard on last dim689                self.desired_input_layouts: tuple[Placement, ...] = (Shard(-1),)690            elif isinstance(module, nn.Embedding):691                # rowwise embedding runtime sharding requires input tensor replicated692                self.desired_input_layouts = (Replicate(),)693            elif isinstance(module, nn.Parameter):694                # rowwise embedding runtime sharding requires input tensor replicated695                self.desired_input_layouts = (Shard(-1),)696            else:697                raise NotImplementedError("RowwiseParallel currently only support nn.Linear and nn.Embedding!")698 699            distribute_module(700                module,701                device_mesh,702                partial(self._prepare_input_fn, self.input_layouts, self.desired_input_layouts),703                partial(self._prepare_output_fn, self.output_layouts, self.use_local_output),704            )705 706 707class PackedRowwiseParallel(RowwiseParallel):708    def partition_tensor(self, param, empty_param, param_type, param_casting_dtype, to_contiguous, rank, device_mesh):709        # colwise shard weight/bias to Shard(0), weight be Shard(-2) (0 if you have 1 dim only)710        # means Colwise as Linear is input * weight^T + bias, where711        # weight would become Shard(1)712        parameter = get_packed_weights(param, empty_param, device_mesh, rank, -1)713        parameter = parameter.to(param_casting_dtype)714        if to_contiguous:715            parameter = parameter.contiguous()716        if self.use_dtensor:717            parameter = DTensor.from_local(parameter, device_mesh, [Shard(-1)], run_check=False)718        return nn.Parameter(parameter, requires_grad=parameter.is_floating_point())719 720 721class SequenceParallel(TensorParallelLayer):722    """723    SequenceParallel replicates a compatible ``nn.Module`` parameters and runs the sharded computation with724    input sharded on the sequence dimension. This currently supports ``nn.LayerNorm``, ``nn.Dropout``, and the725    `RMSNorm python implementation <https://github.com/facebookresearch/llama/blob/main/llama/model.py#L34>`__726 727    This style implements the operation that is described in the paper728    `Reducing Activation Recomputation in Large Transformer Models <https://huggingface.co/papers/2205.05198>`__729 730    If the input passed in to this ``nn.Module`` is a :class:`torch.Tensor`, it assumes that the input is already sharded731    on the sequence dimension and converts the input to a :class:`DTensor` sharded on the sequence dimension. If the input732    passed in to this ``nn.Module`` is already a :class:`DTensor` but is not sharded on the sequence dimension, it would733    redistribute the input to be sharded on the sequence dimension.734 735    The output of the ``nn.Module`` will be sharded on the sequence dimension.736 737    Keyword Args:738        sequence_dim (int, optional):739            The sequence dimension of the input tensor for the ``nn.Module``, this is used to annotate the input tensor to740            become a DTensor that is sharded on the sequence dimension, default: 1.741        use_local_output (bool, optional):742            Whether to use local :class:`torch.Tensor` instead of :class:`DTensor` for the module output, default: False.743    Returns:744        A :class:`ParallelStyle` object that represents Sequence Parallel of the ``nn.Module``.745 746    Example::747        >>> # xdoctest: +SKIP(failing)748        >>> from torch.distributed.tensor.parallel import parallelize_module, SequenceParallel749        >>> from torch.distributed.device_mesh import init_device_mesh750        >>> ...751        >>> m = Model(...)  # m is a nn.Module that contains a "norm" nn.LayerNorm submodule752        >>> tp_mesh = init_device_mesh("cuda", (8,))753        >>>754        >>> # By default, the input of the "norm" will be converted to DTensor that shards on the sequence dim755        >>> # and the output of "norm" will return a sharded on sequence dimension :class:`DTensor`.756        >>>757        >>> sharded_mod = parallelize_module(m, tp_mesh, {"norm": SequenceParallel()}),758        >>> ...759 760    .. note:: SequenceParallel style assumes ones initialization if there are weights in the nn.Module (i.e.761        ``nn.LayerNorm`` or ``RMSNorm``, and they by default have ones initialization). If you have custom762        inits for the weights on those modules, you need to broadcast the weights before/after parallelizing763        to ensure that they are replicated.764    """765 766    def __init__(self, *, sequence_dim: int = 1, use_local_output: bool = False, use_dtensor=False):767        super().__init__()768        self.input_layouts = (Replicate(),)769        self.desired_input_layouts = (Shard(1),)770        self.output_layouts = (Replicate(),)771        self.use_local_output = use_local_output772        self.use_dtensor = True773        self.sequence_sharding = (Shard(sequence_dim),)774        self.use_local_output = use_local_output775 776    @staticmethod777    def _prepare_input_fn(input_layouts, desired_input_layouts, mod, inputs, device_mesh):778        input_tensor = inputs[0]779        if not isinstance(input_tensor, DTensor):780            input_tensor = DTensor.from_local(input_tensor, device_mesh, input_layouts, run_check=False)781        if input_layouts != desired_input_layouts:782            input_tensor = input_tensor.redistribute(placements=desired_input_layouts, async_op=True)783        return input_tensor784 785    @staticmethod786    def _prepare_output_fn(output_layouts, use_local_output, mod, outputs, device_mesh):787        outputs = outputs.redistribute(788            placements=(Replicate(),), async_op=True789        )  # maybe we have to replicate ? because next layer is not sharded790        return outputs.to_local()  # if use_local_output else outputs791 792    def partition_tensor(self, param, empty_param, param_type, param_casting_dtype, to_contiguous, rank, device_mesh):793        # colwise shard weight/bias to Shard(0), weight be Shard(-2) (0 if you have 1 dim only)794        # means Colwise as Linear is input * weight^T + bias, where795        # weight would become Shard(1)796        parameter = param[...]797        parameter = parameter.to(param_casting_dtype)798        if to_contiguous:799            parameter = parameter.contiguous()800        if self.use_dtensor:801            parameter = DTensor.from_local(parameter, device_mesh, [Replicate()], run_check=False)802        return nn.Parameter(parameter, requires_grad=parameter.is_floating_point())803 804 805class GroupedGemmParallel(TensorParallelLayer):806    """807    Applies Expert Parallelism to MoE experts by loading the correct experts on each device.808    """809 810    def __init__(self):811        super().__init__()812        self.use_dtensor = False813 814    def partition_tensor(self, param, empty_param, param_type, param_casting_dtype, to_contiguous, rank, device_mesh):815        ep_rank = rank816        global_num_experts = empty_param.shape[0]817        if global_num_experts % device_mesh.size() != 0:818            raise ValueError(819                f"Global number of experts must be divisible by number of devices: {global_num_experts} % {device_mesh.size()} != 0"820            )821        local_num_experts = global_num_experts // device_mesh.size()822        param = param[ep_rank * local_num_experts : (ep_rank + 1) * local_num_experts].to(param_casting_dtype)823        if to_contiguous:824            param = param.contiguous()825        return param826 827 828class RouterParallel(TensorParallelLayer):829    """830    Allows to reshape the router scores to support running expert parallel.831    """832 833    def __init__(self, *args, **kwargs):834        self.args = args835        self.kwargs = kwargs836        self.use_dtensor = False837 838    @staticmethod839    def _prepare_input_fn(input_layouts, desired_input_layouts, mod, inputs, device_mesh):840        input_tensor = inputs[0]841        if isinstance(input_tensor, DTensor):842            raise NotImplementedError("RouterParallel does not support DTensor input for now")843        return input_tensor844 845    @staticmethod846    def _prepare_output_fn(output_layouts, use_local_output, mod, outputs, device_mesh):847        """848        Imagine if you had 4 tokens, top_k = 4, and 128experts.849        With EP = 8. The num_local_expert should be 128/8 = 16850        Imagine router_indices being:851        [ 52,  42, 119,  67],852        [102,  89,  61,  40],853        [ 82, 103,   4,  34],854        [ 93,  23, 109,  11],855 856        then you can map which rank should be getting which values857 858        [3, 2, 7, 4],859        [6, 5, 3, 2],860        [5, 6, 0, 2],861        [5, 1, 6, 0],862 863        Thus for say rank 0, you fill with 16 (num_local_expert) the index tensor864 865        [ 16, 16, 16, 16],866        [ 16, 16, 16, 16],867        [ 16, 16, 4, 16],868        [ 16, 16, 16, 11],869 870        This works well. For another rank you need to make sure you round to num_local_expert871        because the next operation will one hot encode the router index vector.872 873        This allows us to know directly which local expert is hit.874        Similarly the scores are indexed with something created form875        router_indices.876 877        The kinda naive training loop that we use for device_map "auto" uses a similar logic.878        Here we are just making each rank believe that he is alone, and he computes his part of the hiddenstates.879        Mask invalid indices with num_local_expert for one-hot encoding, so the computes will skip the masking index.880        """881        ep_rank, ep_size = device_mesh.get_local_rank(), device_mesh.size()882        if mod.num_experts % ep_size != 0:883            raise ValueError(884                f"The number of experts must be divisible by number of ep_size: {mod.num_experts} % {ep_size} != 0"885            )886        num_local_experts = mod.num_experts // ep_size887        router_scores, router_indices = outputs888        router_scores = router_scores[:, ep_rank * num_local_experts : (ep_rank + 1) * num_local_experts]889        router_indices = router_indices.masked_fill((router_indices // num_local_experts) != ep_rank, -1)890        # As -1 % 1 is 0, we can only use mask fill when num_local_experts is 1891        if num_local_experts > 1:892            router_indices = torch.fmod(router_indices, num_local_experts)893        else:894            router_indices = router_indices.masked_fill(router_indices > 0, 0).masked_fill(router_indices < 0, -1)895        router_indices = router_indices.masked_fill(896            router_indices == -1, num_local_experts897        )  # masking class for one hot898        return router_scores, router_indices899 900    def partition_tensor(self, param, empty_param, param_type, param_casting_dtype, to_contiguous, rank, device_mesh):901        # TODO: i'd like for this to be the default902        param = param[...].to(param_casting_dtype)903        if to_contiguous:904            param = param.contiguous()905        return param906 907    def prepare_module_tp(self, module: nn.Module, device_mesh) -> nn.Module:908        # TODO: need an abstract Parallel class that is different from TensorParallelLayer909        distribute_module(910            module,911            device_mesh,912            partial(self._prepare_input_fn, None, None),913            partial(self._prepare_output_fn, None, None),914        )915 916 917class ParallelInterface(GeneralInterface):918    # Class instance object, so that a call to `register` can be reflected into all other files correctly, even if919    # a new instance is created (in order to locally override a given entry)920    _global_mapping = (921        {922            "colwise": ColwiseParallel(),923            "rowwise": RowwiseParallel(),924            "colwise_rep": ColwiseParallel(output_layouts=Replicate()),925            "rowwise_rep": RowwiseParallel(input_layouts=Replicate()),926            "local_colwise": ColwiseParallel(use_dtensor=False),927            "local_rowwise": RowwiseParallel(use_dtensor=False),928            "local": IsolatedParallel(),929            "gather": GatherParallel(),930            "local_packed_rowwise": PackedRowwiseParallel(use_dtensor=False),931            "sequence_parallel": SequenceParallel(),932            "replicate": ReplicateParallel(),933            "grouped_gemm": GroupedGemmParallel(),934            "ep_router": RouterParallel(),935        }936        if is_torch_greater_or_equal("2.5") and _torch_distributed_available937        else {}938    )939 940 941ALL_PARALLEL_STYLES: ParallelInterface = ParallelInterface()942 943 944def convert_local_tensor_to_dtensor(945    parameter: torch.Tensor, parameter_name: str, device_mesh, tp_plan: dict[str, str]946) -> DTensor:947    """948    Converts a local variant of weights to a DTensor with corresponding placements. Shouldn't be done ever except of before saving the model.949    """950    _, param_type = parameter_name.rsplit(".", 1) if "." in parameter_name else parameter_name951    tp_style = _get_parameter_tp_plan(parameter_name, tp_plan)952    if not tp_style:953        return parameter954 955    if tp_style not in ["local_packed_rowwise", "local_rowwise", "local_colwise"]:956        return parameter957    # TODO: this logic should be wrapped in a function, this is copied from corresponding tp classes.958    if tp_style == "local_packed_rowwise":959        placements = [Shard(-1)]960    elif tp_style == "local_rowwise":961        if param_type == "bias":962            placements = [Replicate()]963        else:964            placements = [Shard(-1)]965    elif tp_style == "local_colwise":966        if param_type == "bias":967            placements = [Shard(-1)]968        else:969            placements = [Shard(-2)]970    return DTensor.from_local(parameter, device_mesh, placements, run_check=False)971 972 973def replace_state_dict_local_with_dtensor(974    state_dict: dict[str, torch.Tensor],975    tp_plan: dict[str, str],976    device_mesh,977) -> dict[str, torch.Tensor]:978    """979    Replaces all tensors that were sharded with `local_*` strategy with DTensor to make determining their proper size possible.980    """981    for key, value in state_dict.items():982        if isinstance(value, torch.Tensor) and not isinstance(value, DTensor):983            state_dict[key] = convert_local_tensor_to_dtensor(value, key, device_mesh, tp_plan)984    return state_dict985 986 987def add_tensor_parallel_hooks_to_module(988    model, module, tp_plan, layer_name, current_module_plan, device_mesh, parameter_name=None989):990    r"""991    This function is called in `PretrainedModel.post_init()`. It is responsible of adding hooks992    to the modules of the `model`, based on the `PretrainedModel._tp_plan`.993 994    This is the place where we add the `pre_forward` and `post_forwards` hooks. These are defined995    for each `TensorParallelLayer` as `_prepare_input_fn` and `_prepare_output_fn`.996 997    """998    if current_module_plan is not None:999        tp_layer = ALL_PARALLEL_STYLES[current_module_plan]1000        try:1001            tp_layer.prepare_module_tp(module, device_mesh)1002        except NotImplementedError as e:1003            print(1004                f"Trying to prepare {layer_name}, but it's not supported. Corresponding module: {module} Fix it's TP plan: {e}"1005            )1006 1007        module._hf_tp_plan = current_module_plan1008        module.__repr__ = lambda: f"{module.__repr__()}\nTP Plan: {current_module_plan}"1009 1010 1011def shard_and_distribute_module(1012    model, param, empty_param, parameter_name, param_casting_dtype, is_contiguous, rank, device_mesh1013):  # TODO: rename to shard_and_distribute_param1014    r"""1015    This function is called in `from_pretrained` when loading a model's checkpoints.1016    It receives the pointer to the parameter (or the parameter itself) and takes care of "sharding".1017    All process run this function, so they just load the partition of the tensor that they require.1018 1019    Main uses cases:1020    - column / rowise parallelism, you just shard all the weights of the layer (weight and bias)1021    - packed layers: you slice the weights, then shard like above1022    - custom operation:1023        - you want to add an all-gather at the end of a local layer.1024        - you want to have a layer that is isolated from the rest of the world (because torch.DTensor does not work well with `.view` for instance)1025 1026    """1027    param_name, param_type = parameter_name.rsplit(".", 1) if "." in parameter_name else parameter_name1028    tp_plan = model.tp_plan or {}1029    module_to_tp = model.get_submodule(param_name)  # TODO: can i loop over modules?1030    rank = int(rank)1031    current_shard_plan = _get_parameter_tp_plan(parameter_name, tp_plan)1032 1033    if dist.get_rank() == 0:1034        if current_shard_plan is None:1035            logger.info(f"Tensor sharding plan for {param_name} not found, using default 'replicate' plan.")1036        else:1037            logger.info(f"Tensor sharding plan for {param_name}: {current_shard_plan}")1038 1039    if current_shard_plan is not None:1040        try:1041            tp_layer = ALL_PARALLEL_STYLES[current_shard_plan]1042            param = tp_layer.partition_tensor(1043                param, empty_param, param_type, param_casting_dtype, is_contiguous, rank, device_mesh1044            )1045        except NotImplementedError as e:1046            print(1047                f"Trying to prepare {parameter_name}, but it's not supported. Corresponding module: {module_to_tp} Fix it's TP plan, current layer: {tp_layer} : {e}"1048            )1049    else:1050        param = param[:].to(param_casting_dtype)1051 1052    # SUPER IMPORTANT we have to use setattr1053    # otherwise loading is crazy slow1054    if not isinstance(param, torch.nn.Parameter):1055        param = torch.nn.Parameter(param, requires_grad=empty_param.is_floating_point())1056    setattr(module_to_tp, param_type, param)1057    # module_to_tp.load_state_dict({param_type: param}, strict=False, assign=True)1058    return param1059 1060 1061def verify_tp_plan(expected_keys: list[str], tp_plan: dict[str, str] | None):1062    """1063    Verify the TP plan of the model, log a warning if the layers that were not sharded and the rules that were not applied.1064    """1065 1066    if tp_plan is None:1067        return1068 1069    generic_keys = {re.sub(r"\d+", "*", key) for key in expected_keys}1070    unsharded_layers = set(generic_keys)1071    unused_rules = tp_plan1072 1073    for key in generic_keys:1074        param_name = key.rsplit(".", 1)[0] if "." in key else key1075        generic_param_name = re.sub(r"\d+", "*", param_name)1076 1077        if generic_param_name in tp_plan:1078            unused_rules.pop(generic_param_name)1079            unsharded_layers.discard(key)1080        elif "." in generic_param_name and (parent_param_name := generic_param_name.rsplit(".", 1)[0]) in tp_plan:1081            unused_rules.pop(parent_param_name)1082            unsharded_layers.discard(key)1083        else:1084            pass  # we couldn't find the rule for this parameter, so it's not sharded1085 1086    if len(unused_rules) > 0:1087        logger.warning(f"The following TP rules were not applied on any of the layers: {unused_rules}")1088    if len(unsharded_layers) > 0:1089        logger.warning(f"The following layers were not sharded: {', '.join(unsharded_layers)}")1090 1091 1092def distribute_model(model, distributed_config, device_mesh, tp_size):1093    model._tp_size = tp_size1094    model._device_mesh = device_mesh1095    if distributed_config is not None:1096        if isinstance(distributed_config, dict):1097            distributed_config = DistributedConfig.from_dict(distributed_config)1098        model.config.distributed_config = distributed_config1099    model_plan = model.tp_plan1100    if model_plan is not None and is_torch_greater_or_equal("2.5") and _torch_distributed_available:1101        for v in model_plan.values():1102            if v not in ALL_PARALLEL_STYLES:1103                raise ValueError(f"Unsupported tensor parallel style {v}. Supported styles are {ALL_PARALLEL_STYLES}")1104        for name, module in model.named_modules():1105            if not getattr(module, "_is_hooked", False):1106                plan = _get_parameter_tp_plan(parameter_name=name, tp_plan=model_plan, is_weight=False)1107                add_tensor_parallel_hooks_to_module(1108                    model=model,1109                    module=module,1110                    tp_plan=model_plan,1111                    layer_name="",1112                    current_module_plan=plan,1113                    device_mesh=device_mesh,1114                )1115            module._is_hooked = True1116    return model1117 
Aluode/PerceptionLabPortable · CoolFace