CoolFace
Apppublic

SubstanceSHIFT/SeedVR2-3B

sourceHugging Faceapache-2.0updated 1y agoView on Hugging Face
0likes
ops.py495 linesDownload Raw Back to distributed
1# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates2# //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.14 15"""16Distributed ops for supporting sequence parallel.17"""18 19from collections import defaultdict20from typing import Any, Callable, Dict, List, Optional, Tuple, Union21import torch22import torch.distributed as dist23from torch import Tensor24 25from common.cache import Cache26from common.distributed.advanced import (27    get_sequence_parallel_group,28    get_sequence_parallel_rank,29    get_sequence_parallel_world_size,30)31 32from .basic import get_device33 34_SEQ_DATA_BUF = defaultdict(lambda: [None, None, None])35_SEQ_DATA_META_SHAPES = defaultdict()36_SEQ_DATA_META_DTYPES = defaultdict()37_SEQ_DATA_ASYNC_COMMS = defaultdict(list)38_SYNC_BUFFER = defaultdict(dict)39 40 41def single_all_to_all(42    local_input: Tensor,43    scatter_dim: int,44    gather_dim: int,45    group: dist.ProcessGroup,46    async_op: bool = False,47):48    """49    A function to do all-to-all on a tensor50    """51    seq_world_size = dist.get_world_size(group)52    prev_scatter_dim = scatter_dim53    if scatter_dim != 0:54        local_input = local_input.transpose(0, scatter_dim)55        if gather_dim == 0:56            gather_dim = scatter_dim57        scatter_dim = 058 59    inp_shape = list(local_input.shape)60    inp_shape[scatter_dim] = inp_shape[scatter_dim] // seq_world_size61    input_t = local_input.reshape(62        [seq_world_size, inp_shape[scatter_dim]] + inp_shape[scatter_dim + 1 :]63    ).contiguous()64    output = torch.empty_like(input_t)65    comm = dist.all_to_all_single(output, input_t, group=group, async_op=async_op)66    if async_op:67        # let user's code transpose & reshape68        return output, comm, prev_scatter_dim69 70    # first dim is seq_world_size, so we can split it directly71    output = torch.cat(output.split(1), dim=gather_dim + 1).squeeze(0)72    if prev_scatter_dim:73        output = output.transpose(0, prev_scatter_dim).contiguous()74    return output75 76 77def _all_to_all(78    local_input: Tensor,79    scatter_dim: int,80    gather_dim: int,81    group: dist.ProcessGroup,82):83    seq_world_size = dist.get_world_size(group)84    input_list = [85        t.contiguous() for t in torch.tensor_split(local_input, seq_world_size, scatter_dim)86    ]87    output_list = [torch.empty_like(input_list[0]) for _ in range(seq_world_size)]88    dist.all_to_all(output_list, input_list, group=group)89    return torch.cat(output_list, dim=gather_dim).contiguous()90 91 92class SeqAllToAll(torch.autograd.Function):93    @staticmethod94    def forward(95        ctx: Any,96        group: dist.ProcessGroup,97        local_input: Tensor,98        scatter_dim: int,99        gather_dim: int,100        async_op: bool,101    ) -> Tensor:102        ctx.group = group103        ctx.scatter_dim = scatter_dim104        ctx.gather_dim = gather_dim105        ctx.async_op = async_op106        if async_op:107            output, comm, prev_scatter_dim = single_all_to_all(108                local_input, scatter_dim, gather_dim, group, async_op=async_op109            )110            ctx.prev_scatter_dim = prev_scatter_dim111            return output, comm112 113        return _all_to_all(local_input, scatter_dim, gather_dim, group)114 115    @staticmethod116    def backward(ctx: Any, *grad_output: Tensor) -> Tuple[None, Tensor, None, None]:117        if ctx.async_op:118            input_t = torch.cat(grad_output[0].split(1), dim=ctx.gather_dim + 1).squeeze(0)119            if ctx.prev_scatter_dim:120                input_t = input_t.transpose(0, ctx.prev_scatter_dim)121        else:122            input_t = grad_output[0]123        return (124            None,125            _all_to_all(input_t, ctx.gather_dim, ctx.scatter_dim, ctx.group),126            None,127            None,128            None,129        )130 131 132class Slice(torch.autograd.Function):133    @staticmethod134    def forward(ctx: Any, group: dist.ProcessGroup, local_input: Tensor, dim: int) -> Tensor:135        ctx.group = group136        ctx.rank = dist.get_rank(group)137        seq_world_size = dist.get_world_size(group)138        ctx.seq_world_size = seq_world_size139        ctx.dim = dim140        dim_size = local_input.shape[dim]141        return local_input.split(dim_size // seq_world_size, dim=dim)[ctx.rank].contiguous()142 143    @staticmethod144    def backward(ctx: Any, grad_output: Tensor) -> Tuple[None, Tensor, None]:145        dim_size = list(grad_output.size())146        split_size = dim_size[0]147        dim_size[0] = dim_size[0] * ctx.seq_world_size148        output = torch.empty(dim_size, dtype=grad_output.dtype, device=torch.cuda.current_device())149        dist._all_gather_base(output, grad_output, group=ctx.group)150        return (None, torch.cat(output.split(split_size), dim=ctx.dim), None)151 152 153class Gather(torch.autograd.Function):154    @staticmethod155    def forward(156        ctx: Any,157        group: dist.ProcessGroup,158        local_input: Tensor,159        dim: int,160        grad_scale: Optional[bool] = False,161    ) -> Tensor:162        ctx.group = group163        ctx.rank = dist.get_rank(group)164        ctx.dim = dim165        ctx.grad_scale = grad_scale166        seq_world_size = dist.get_world_size(group)167        ctx.seq_world_size = seq_world_size168        dim_size = list(local_input.size())169        split_size = dim_size[0]170        ctx.part_size = dim_size[dim]171        dim_size[0] = dim_size[0] * seq_world_size172        output = torch.empty(dim_size, dtype=local_input.dtype, device=torch.cuda.current_device())173        dist._all_gather_base(output, local_input.contiguous(), group=ctx.group)174        return torch.cat(output.split(split_size), dim=dim)175 176    @staticmethod177    def backward(ctx: Any, grad_output: Tensor) -> Tuple[None, Tensor]:178        if ctx.grad_scale:179            grad_output = grad_output * ctx.seq_world_size180        return (181            None,182            grad_output.split(ctx.part_size, dim=ctx.dim)[ctx.rank].contiguous(),183            None,184            None,185        )186 187 188def gather_seq_scatter_heads_qkv(189    qkv_tensor: Tensor,190    *,191    seq_dim: int,192    qkv_shape: Optional[Tensor] = None,193    cache: Cache = Cache(disable=True),194    restore_shape: bool = True,195):196    """197    A func to sync splited qkv tensor198    qkv_tensor: the tensor we want to do alltoall with. The last dim must199        be the projection_idx, which we will split into 3 part. After200        spliting, the gather idx will be projecttion_idx + 1201    seq_dim: gather_dim for all2all comm202    restore_shape: if True, output will has the same shape length as input203    """204    group = get_sequence_parallel_group()205    if not group:206        return qkv_tensor207    world = get_sequence_parallel_world_size()208    orig_shape = qkv_tensor.shape209    scatter_dim = qkv_tensor.dim()210    bef_all2all_shape = list(orig_shape)211    qkv_proj_dim = bef_all2all_shape[-1]212    bef_all2all_shape = bef_all2all_shape[:-1] + [3, qkv_proj_dim // 3]213    qkv_tensor = qkv_tensor.view(bef_all2all_shape)214    qkv_tensor = SeqAllToAll.apply(group, qkv_tensor, scatter_dim, seq_dim, False)215    if restore_shape:216        out_shape = list(orig_shape)217        out_shape[seq_dim] *= world218        out_shape[-1] = qkv_proj_dim // world219        qkv_tensor = qkv_tensor.view(out_shape)220 221    # remove padding222    if qkv_shape is not None:223        unpad_dim_size = cache(224            "unpad_dim_size", lambda: torch.sum(torch.prod(qkv_shape, dim=-1)).item()225        )226        if unpad_dim_size % world != 0:227            padding_size = qkv_tensor.size(seq_dim) - unpad_dim_size228            qkv_tensor = _unpad_tensor(qkv_tensor, seq_dim, padding_size)229    return qkv_tensor230 231 232def slice_inputs(x: Tensor, dim: int, padding: bool = True):233    """234    A func to slice the input sequence in sequence parallel235    """236    group = get_sequence_parallel_group()237    if group is None:238        return x239    sp_rank = get_sequence_parallel_rank()240    sp_world = get_sequence_parallel_world_size()241    dim_size = x.shape[dim]242    unit = (dim_size + sp_world - 1) // sp_world243    if padding and dim_size % sp_world:244        padding_size = sp_world - (dim_size % sp_world)245        x = _pad_tensor(x, dim, padding_size)246    slc = [slice(None)] * len(x.shape)247    slc[dim] = slice(unit * sp_rank, unit * (sp_rank + 1))248    return x[slc]249 250 251def remove_seqeunce_parallel_padding(x: Tensor, dim: int, unpad_dim_size: int):252    """253    A func to remove the padding part of the tensor based on its original shape254    """255    group = get_sequence_parallel_group()256    if group is None:257        return x258    sp_world = get_sequence_parallel_world_size()259    if unpad_dim_size % sp_world == 0:260        return x261    padding_size = sp_world - (unpad_dim_size % sp_world)262    assert (padding_size + unpad_dim_size) % sp_world == 0263    return _unpad_tensor(x, dim=dim, padding_size=padding_size)264 265 266def gather_heads_scatter_seq(x: Tensor, head_dim: int, seq_dim: int) -> Tensor:267    """268    A func to sync attention result with alltoall in sequence parallel269    """270    group = get_sequence_parallel_group()271    if not group:272        return x273    dim_size = x.size(seq_dim)274    sp_world = get_sequence_parallel_world_size()275    if dim_size % sp_world != 0:276        padding_size = sp_world - (dim_size % sp_world)277        x = _pad_tensor(x, seq_dim, padding_size)278    return SeqAllToAll.apply(group, x, seq_dim, head_dim, False)279 280 281def gather_seq_scatter_heads(x: Tensor, seq_dim: int, head_dim: int) -> Tensor:282    """283    A func to sync embedding input with alltoall in sequence parallel284    """285    group = get_sequence_parallel_group()286    if not group:287        return x288    return SeqAllToAll.apply(group, x, head_dim, seq_dim, False)289 290 291def scatter_heads(x: Tensor, dim: int) -> Tensor:292    """293    A func to split heads before attention in sequence parallel294    """295    group = get_sequence_parallel_group()296    if not group:297        return x298    return Slice.apply(group, x, dim)299 300 301def gather_heads(x: Tensor, dim: int, grad_scale: Optional[bool] = False) -> Tensor:302    """303    A func to gather heads for the attention result in sequence parallel304    """305    group = get_sequence_parallel_group()306    if not group:307        return x308    return Gather.apply(group, x, dim, grad_scale)309 310 311def gather_outputs(312    x: Tensor,313    *,314    gather_dim: int,315    padding_dim: Optional[int] = None,316    unpad_shape: Optional[Tensor] = None,317    cache: Cache = Cache(disable=True),318    scale_grad=True,319):320    """321    A func to gather the outputs for the model result in sequence parallel322    """323    group = get_sequence_parallel_group()324    if not group:325        return x326    x = Gather.apply(group, x, gather_dim, scale_grad)327    if padding_dim is not None:328        unpad_dim_size = cache(329            "unpad_dim_size", lambda: torch.sum(torch.prod(unpad_shape, dim=1)).item()330        )331        x = remove_seqeunce_parallel_padding(x, padding_dim, unpad_dim_size)332    return x333 334 335def _pad_tensor(x: Tensor, dim: int, padding_size: int):336    shape = list(x.shape)337    shape[dim] = padding_size338    pad = torch.zeros(shape, dtype=x.dtype, device=x.device)339    return torch.cat([x, pad], dim=dim)340 341 342def _unpad_tensor(x: Tensor, dim: int, padding_size):343    slc = [slice(None)] * len(x.shape)344    slc[dim] = slice(0, -padding_size)345    return x[slc]346 347 348def _broadcast_data(data, shape, dtype, src, group, async_op):349    comms = []350    if isinstance(data, (list, tuple)):351        for i, sub_shape in enumerate(shape):352            comms += _broadcast_data(data[i], sub_shape, dtype[i], src, group, async_op)353    elif isinstance(data, dict):354        for key, sub_data in data.items():355            comms += _broadcast_data(sub_data, shape[key], dtype[key], src, group, async_op)356    elif isinstance(data, Tensor):357        comms.append(dist.broadcast(data, src=src, group=group, async_op=async_op))358    return comms359 360 361def _traverse(data: Any, op: Callable) -> Union[None, List, Dict, Any]:362    if isinstance(data, (list, tuple)):363        return [_traverse(sub_data, op) for sub_data in data]364    elif isinstance(data, dict):365        return {key: _traverse(sub_data, op) for key, sub_data in data.items()}366    elif isinstance(data, Tensor):367        return op(data)368    else:369        return None370 371 372def _get_shapes(data):373    return _traverse(data, op=lambda x: x.shape)374 375 376def _get_dtypes(data):377    return _traverse(data, op=lambda x: x.dtype)378 379 380def _construct_broadcast_buffer(shapes, dtypes, device):381    if isinstance(shapes, torch.Size):382        return torch.empty(shapes, dtype=dtypes, device=device)383 384    if isinstance(shapes, (list, tuple)):385        buffer = []386        for i, sub_shape in enumerate(shapes):387            buffer.append(_construct_broadcast_buffer(sub_shape, dtypes[i], device))388    elif isinstance(shapes, dict):389        buffer = {}390        for key, sub_shape in shapes.items():391            buffer[key] = _construct_broadcast_buffer(sub_shape, dtypes[key], device)392    else:393        return None394    return buffer395 396 397class SPDistForward:398    """A forward tool to sync different result across sp group399 400    Args:401        module: a function or module to process users input402        sp_step: current training step to judge which rank to broadcast its result to all403        name: a distinct str to save meta and async comm404        comm_shape: if different ranks have different shape, mark this arg to True405        device: the device for current rank, can be empty406    """407 408    def __init__(409        self,410        name: str,411        comm_shape: bool,412        device: torch.device = None,413    ):414        self.name = name415        self.comm_shape = comm_shape416        if device:417            self.device = device418        else:419            self.device = get_device()420 421    def __call__(self, inputs) -> Any:422        group = get_sequence_parallel_group()423        if not group:424            yield inputs425        else:426            device = self.device427            sp_world = get_sequence_parallel_world_size()428            sp_rank = get_sequence_parallel_rank()429            for local_step in range(sp_world):430                src_rank = dist.get_global_rank(group, local_step)431                is_src = sp_rank == local_step432                local_shapes = []433                local_dtypes = []434                if local_step == 0:435                    local_result = inputs436                    _SEQ_DATA_BUF[self.name][-1] = local_result437                    local_shapes = _get_shapes(local_result)438                    local_dtypes = _get_dtypes(local_result)439                    if self.comm_shape:440                        group_shapes_lists = [None] * sp_world441                        dist.all_gather_object(group_shapes_lists, local_shapes, group=group)442                        _SEQ_DATA_META_SHAPES[self.name] = group_shapes_lists443                    else:444                        _SEQ_DATA_META_SHAPES[self.name] = [local_shapes] * sp_world445                    _SEQ_DATA_META_DTYPES[self.name] = local_dtypes446                shapes = _SEQ_DATA_META_SHAPES[self.name][local_step]447                dtypes = _SEQ_DATA_META_DTYPES[self.name]448                buf_id = local_step % 2449                if local_step == 0:450                    sync_data = (451                        local_result452                        if is_src453                        else _construct_broadcast_buffer(shapes, dtypes, device)454                    )455                    _broadcast_data(sync_data, shapes, dtypes, src_rank, group, False)456                    _SEQ_DATA_BUF[self.name][buf_id] = sync_data457 458                # wait for async comm ops459                if _SEQ_DATA_ASYNC_COMMS[self.name]:460                    for comm in _SEQ_DATA_ASYNC_COMMS[self.name]:461                        comm.wait()462                # before return the sync result, do async broadcast for next batch463                if local_step < sp_world - 1:464                    next_buf_id = 1 - buf_id465                    shapes = _SEQ_DATA_META_SHAPES[self.name][local_step + 1]466                    src_rank = dist.get_global_rank(group, local_step + 1)467                    is_src = sp_rank == local_step + 1468                    next_sync_data = (469                        _SEQ_DATA_BUF[self.name][-1]470                        if is_src471                        else _construct_broadcast_buffer(shapes, dtypes, device)472                    )473                    _SEQ_DATA_ASYNC_COMMS[self.name] = _broadcast_data(474                        next_sync_data, shapes, dtypes, src_rank, group, True475                    )476                    _SEQ_DATA_BUF[self.name][next_buf_id] = next_sync_data477                yield _SEQ_DATA_BUF[self.name][buf_id]478 479 480sync_inputs = SPDistForward(name="bef_fwd", comm_shape=True)481 482 483def sync_data(data, sp_idx, name="tmp"):484    group = get_sequence_parallel_group()485    if group is None:486        return data487    # if sp_idx in _SYNC_BUFFER[name]:488    #     return _SYNC_BUFFER[name][sp_idx]489    sp_rank = get_sequence_parallel_rank()490    src_rank = dist.get_global_rank(group, sp_idx)491    objects = [data] if sp_rank == sp_idx else [None]492    dist.broadcast_object_list(objects, src=src_rank, group=group)493    # _SYNC_BUFFER[name] = {sp_idx: objects[0]}494    return objects[0]495