Files
wehub-resource-sync a203934033
Lint test / lint (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:34:58 +08:00

148 lines
6.0 KiB
Python

# 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