# Copyright (c) ModelScope Contributors. All rights reserved. import torch import torch.distributed as dist from typing import Any, Tuple # Code borrowed from deepspeed, here is why: # 1. Reduce the dependency # 2. The original code is complex def _generate_layout_params(scatter_idx, seq_world_size, input): if scatter_idx < 2: bs, global_seq_len, num_local_head, head_dim = input.shape pre_all2all_inp_shape = [bs, seq_world_size, global_seq_len // seq_world_size, num_local_head, head_dim] pre_all2all_permute_idx = (1, 0, 2, 3, 4) post_all2all_permute_idx = (1, 2, 0, 3, 4) post_all2all_res_shape = [bs, global_seq_len // seq_world_size, seq_world_size * num_local_head, head_dim] else: bs, local_seq_len, num_total_head, head_dim = input.shape assert num_total_head % seq_world_size == 0, (f'Number of heads ({num_total_head}) must be divisible ' f'by the sequence parallel size ({seq_world_size})!') pre_all2all_inp_shape = [bs, local_seq_len, seq_world_size, num_total_head // seq_world_size, head_dim] pre_all2all_permute_idx = (2, 0, 1, 3, 4) post_all2all_permute_idx = (1, 0, 2, 3, 4) post_all2all_res_shape = [bs, seq_world_size * local_seq_len, num_total_head // seq_world_size, head_dim] return pre_all2all_permute_idx, pre_all2all_inp_shape, post_all2all_permute_idx, post_all2all_res_shape def post_all2all(permute_idx, res_shape): """ Post-processing function for `all2all` communication. """ def post_func(input): if permute_idx is not None: input = input.permute(permute_idx).contiguous() output = input.reshape(res_shape).contiguous() return output return post_func def pre_all2all_fun(permute_idx, inp_shape, input): """ Pre-processing function for `all2all` communication. """ input_t = input.reshape(inp_shape).contiguous() if permute_idx is not None: input_t = input_t.permute(permute_idx).contiguous() return input_t def single_all_to_all(input, scatter_idx, gather_idx, group, **kwargs): seq_world_size = dist.get_world_size(group) num_heads = input.shape[2] if num_heads % seq_world_size != 0 and not scatter_idx < 2: raise NotImplementedError(f'num_heads {num_heads} cannot be split by sp world size {seq_world_size}') pre_all2all_permute_idx, pre_all2all_inp_shape, post_all2all_permute_idx, post_all2all_res_shape = ( _generate_layout_params(scatter_idx, seq_world_size, input)) input_t = pre_all2all_fun(pre_all2all_permute_idx, pre_all2all_inp_shape, input) post_all2all_fun = post_all2all(post_all2all_permute_idx, post_all2all_res_shape) output = torch.empty_like(input_t) dist.all_to_all_single(output, input_t, group=group) res = post_all2all_fun(output) return res class _SeqAllToAll(torch.autograd.Function): @staticmethod def forward( ctx: Any, group: dist.ProcessGroup, input: torch.Tensor, scatter_idx: int, gather_idx: int, ) -> torch.Tensor: ctx.group = group ctx.scatter_idx = scatter_idx ctx.gather_idx = gather_idx res = single_all_to_all(input, scatter_idx, gather_idx, group) return res @staticmethod def backward(ctx: Any, *grad_output: torch.Tensor) -> Tuple[None, torch.Tensor, None, None]: return None, _SeqAllToAll.apply(ctx.group, *grad_output, ctx.gather_idx, ctx.scatter_idx), None, None class DistributedAttention(torch.nn.Module): def __init__( self, local_attention, sequence_parallel, scatter_idx: int = 2, gather_idx: int = 1, ) -> None: super(DistributedAttention, self).__init__() self.local_attn = local_attention self.sequence_parallel = sequence_parallel self.scatter_idx = scatter_idx self.gather_idx = gather_idx def forward(self, query: torch.Tensor, key: torch.Tensor, value: torch.Tensor, attention_mask: torch.Tensor, *args: Any, **kwargs) -> torch.Tensor: if self.sequence_parallel.world_size == 1: return self.local_attn(query, key, value, attention_mask, *args, **kwargs) # gather ulysses first, ring-attention next if self.sequence_parallel.sp_world_size > 1: query_layer = _SeqAllToAll.apply(self.sequence_parallel.sp_group, query, self.scatter_idx, self.gather_idx) key_layer = _SeqAllToAll.apply(self.sequence_parallel.sp_group, key, self.scatter_idx, self.gather_idx) value_layer = _SeqAllToAll.apply(self.sequence_parallel.sp_group, value, self.scatter_idx, self.gather_idx) else: query_layer, key_layer, value_layer = query, key, value if self.sequence_parallel.rp_world_size > 1: # if need ring-attention kwargs.pop('position_ids', None) # Get the real position ids, this is filled by `sequence_parallel.prepare_inputs` # real position ids is different from the position_ids when model uses mrope position_ids = self.sequence_parallel.real_position_ids # pad and split it by zigzag method position_ids = self.sequence_parallel.pad(position_ids, padding_value=-1, position_ids=position_ids) else: # only ulysses position_ids = kwargs.pop('position_ids') if position_ids is not None: # Reuse the generic gather path so 2D and 3D position_ids share the same SP behavior. position_ids = self.sequence_parallel.gather(position_ids.contiguous(), dim=-1, position_ids=None) context_layer = self.local_attn( query_layer, key_layer, value_layer, attention_mask, *args, position_ids=position_ids, **kwargs) if self.sequence_parallel.sp_world_size > 1: output = _SeqAllToAll.apply(self.sequence_parallel.sp_group, context_layer, self.gather_idx, self.scatter_idx) else: output = context_layer return output