Files
wehub-resource-sync 2aaeece67c
Codestyle Check / Lint (push) Has been cancelled
Codestyle Check / Check bypass (push) Has been cancelled
Pipelines-Test / Pipelines-Test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:37:14 +08:00

421 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Copyright (c) 2024 PaddlePaddle Authors. All Rights Reserved.
# Copyright (c) Microsoft Corporation.
# Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
# Copyright (C) 2024 THL A29 Limited, a Tencent company. 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
from typing import Any, List, Tuple
import numpy as np
import paddle
import paddle.distributed as dist
from paddle import Tensor, nn
from paddle.distributed.communication.group import Group
from paddlenlp.utils.log import logger
from .moe_gate import PretrainedMoEGate
from .token_dispatcher import MoEFlexTokenDispatcher
def dispatching(x, dispatch_mask, scatter_index, num_experts, capacity):
"""
Rearranges the input tensor `x` based on gate results, truncates it according to the specified capacity, and performs padding.
Args:
x (Tensor)[Seq, Dim]: The input tensor.
dispatch_mask (List[Tensor[Seq, 1], Tensor[Seq, 1]]): A list of dispatch masks.
scatter_index (Union[List[Tensor[Seq,], Tensor[Seq]], Tensor[Seq, 2]]): A list or tensor representing scatter indices.
num_experts (int): The number of experts.
capacity (int): The capacity size.
Returns:
Tensor [Expert*Capacity, Dim]: The output tensor after dispatching.
"""
output = None
orig_dtype = x.dtype
if isinstance(scatter_index, paddle.Tensor):
scatter_index = scatter_index.unbind(1)
for i_scatter_index, i_dispatch_mask in zip(scatter_index, dispatch_mask):
init_output = paddle.zeros([num_experts * capacity, x.shape[-1]], dtype="float32")
updates = x * i_dispatch_mask.cast(x.dtype)
if output is None:
output = paddle.scatter(
init_output,
i_scatter_index,
updates,
overwrite=False,
)
else:
output = output + paddle.scatter(
init_output,
i_scatter_index,
updates,
overwrite=False,
)
if output.dtype != orig_dtype:
output = output.cast(orig_dtype)
return output
def combining(x, combine_weights, scatter_index):
"""
Performs combination and aggregation operations on the input matrix.
Args:
x: Tensor[num_experts * capacity, dim] - The input matrix to be processed, where the last dimension represents the number of features.
combine_weights: Union[List[Tensor[seq, 1], Tensor[seq, 1]], Tensor[seq, 2, 1]] - A list or tensor containing combination weights for each feature.
scatter_index: Union[List[Tensor[seq], Tensor[seq]], Tensor[seq, 2]] - A tuple of indices indicating which elements are to be aggregated, where the first element is the row index and the second element is the column index.
Returns:
Tensor: The output matrix after combination and aggregation, with a shape of [n, dim * num_features], where n is the number of samples in the input matrix.
"""
dim = x.shape[-1]
if isinstance(scatter_index, (list, tuple)):
scatter_index = paddle.concat([i.unsqueeze([-1]) for i in scatter_index], -1)
scatter_index = scatter_index.reshape([-1])
num_k = len(combine_weights) if isinstance(combine_weights, (list, tuple)) else combine_weights.shape[-1]
x = paddle.gather(x, scatter_index).reshape([-1, num_k, dim]) # [seq,2,dim]
if isinstance(combine_weights, (list, tuple)):
combine_weights = paddle.concat(combine_weights, -1).unsqueeze([1])
return paddle.matmul(combine_weights, x).squeeze(1) # [seq,1,2] @ [seq,2,dim] -> [seq,1,dim]
class _AllToAll(paddle.autograd.PyLayer):
@staticmethod
def forward(
ctx: Any,
output_shape: List,
input: Tensor,
out_split_sizes: List = None,
in_split_sizes: List = None,
group: Group = None,
) -> Tensor: # type: ignore
"""
All-to-all communication in the group.
Args:
ctx (Any): Context object.
output_shape (List): Output shape.
input (Tensor): Input tensor.
out_split_sizes (List): Output split sizes.
in_split_sizes (List): Input split sizes.
group (Group): The group object.
Returns:
Tensor: Output tensor.
"""
ctx.group = group
ctx.input_shape = input.shape
ctx.out_split_sizes = out_split_sizes
ctx.in_split_sizes = in_split_sizes
# return input
if dist.get_world_size(group) <= 1:
return input
output = paddle.empty(output_shape, dtype=input.dtype)
task = dist.alltoall_single(
output,
input,
out_split_sizes=out_split_sizes,
in_split_sizes=in_split_sizes,
sync_op=False,
group=group,
)
task.wait()
return output
@staticmethod
def backward(ctx: Any, *grad_output: Tensor) -> Tuple[Tensor]:
"""
Aggregates gradient information from all input tensors into a single tensor.
Args:
ctx (Any): The context object used to store information that needs to be passed.
*grad_output (Tensor): A list of input tensors whose gradients are to be aggregated.
Returns:
Tuple[Tensor]: A tuple containing a tensor that holds the gradients of all input tensors.
"""
# return grad_output
return _AllToAll.apply(ctx.input_shape, *grad_output, ctx.in_split_sizes, ctx.out_split_sizes, ctx.group)
class MoELayer(nn.Layer):
def __init__(
self,
config,
moe_num_experts: int, # 128
expert_class: nn.Layer,
expert_kwargs: dict,
gate: PretrainedMoEGate,
capacity: int = 1.0,
moe_group: str = "tp", # will be re-assigned from config
all_to_all_dropout=0.0,
):
super().__init__()
self.config = config
self.moe_num_experts = moe_num_experts
self.capacity = capacity
self.is_tp_moe = False
self.is_dp_moe = False
try:
dist.fleet.get_hybrid_communicate_group()
is_fleet_init = True
except AttributeError:
is_fleet_init = False
if (
is_fleet_init
and dist.fleet.get_hybrid_communicate_group().get_data_parallel_world_size() > 1
and moe_group == "dp"
):
self.moe_group = dist.fleet.get_hybrid_communicate_group().get_data_parallel_group()
self.moe_rank = dist.get_rank(self.moe_group)
self.moe_rank = 0 if self.moe_rank < 0 else self.moe_rank
self.expert_parallel_degree = dist.get_world_size(self.moe_group)
self.expert_parallel_degree = 1 if self.expert_parallel_degree < 0 else self.expert_parallel_degree
self.moe_num_experts_per_device = self._parse_moe_expert_parallel(
self.moe_num_experts, self.expert_parallel_degree
)
self.is_dummy_moe = False if self.expert_parallel_degree > 1 else True
self.is_dp_moe = True
elif (
is_fleet_init
and dist.fleet.get_hybrid_communicate_group().get_model_parallel_world_size() > 1
and moe_group == "tp"
):
# for qwen3moe,moe_group should be "tp", since dp always == 1
self.moe_group = dist.fleet.get_hybrid_communicate_group().get_model_parallel_group()
self.moe_rank = dist.get_rank(self.moe_group) # i for num_worker in a TP group
self.moe_rank = 0 if self.moe_rank < 0 else self.moe_rank # 1
self.expert_parallel_degree = dist.get_world_size(self.moe_group)
self.expert_parallel_degree = 1 if self.expert_parallel_degree < 0 else self.expert_parallel_degree # 4
self.moe_num_experts_per_device = self._parse_moe_expert_parallel(
self.moe_num_experts, self.expert_parallel_degree
) # e.g. 单机2路tp 那么 32 = 128/4
self.is_dummy_moe = False if self.expert_parallel_degree > 1 else True # False
self.is_tp_moe = True
else:
self.moe_group = None
self.moe_rank = 0
self.expert_parallel_degree = 1
self.moe_num_experts_per_device = self.moe_num_experts
self.is_dummy_moe = True
self.all_to_all_dropout = all_to_all_dropout
self.enable_recompute = False
self.experts = nn.LayerList([])
for i in range(self.moe_num_experts):
if i // self.moe_num_experts_per_device == self.moe_rank:
self.experts.append(expert_class(**expert_kwargs))
else:
self.experts.append(None)
self.gate = gate
self.gate.group = self.moe_group
self._post_init()
def _parse_moe_expert_parallel(self, moe_num_experts, expert_parallel_degree):
assert (
moe_num_experts >= expert_parallel_degree
), f"expert moe_num_experts={moe_num_experts} >= moe_world_size={expert_parallel_degree}"
assert (
moe_num_experts % expert_parallel_degree == 0
), f"expert moe_num_experts={moe_num_experts} % moe_world_size={expert_parallel_degree} == 0"
moe_num_experts_per_device = moe_num_experts // expert_parallel_degree
return moe_num_experts_per_device
def _post_init(self):
for p in self.gate.parameters():
p.is_gate = True
for k in self.experts:
if k is not None:
for p in k.parameters():
p.expert = not (self.is_tp_moe or self.is_dummy_moe) # type: ignore
p.no_sync = not (self.is_tp_moe or self.is_dummy_moe)
logger.info(f"expert param={p.name}, no-sync={p.no_sync}")
if self.is_tp_moe or self.is_dp_moe:
p.is_distributed = True
def forward(
self,
hidden_state: paddle.Tensor,
):
"""MoE Layer forward function
1. Gate Forward.
2. Dispatch export.
3. Experts Forward.
Args:
hidden_state: MoE Layer input
Returns:
final_out: MoE Layer main output.
l_aux: MoE auxiliary loss. l_zloss: MoE z loss."""
batch_size, seq_len, d_model = hidden_state.shape
reshaped_input = hidden_state.reshape([-1, d_model])
# self.l_aux :
# topk_weight : se
# topk_ids : sk
# token_priority : se
# self.exp_counts :
capacity, topk_weight, topk_ids, token_priority, l_aux, l_zloss = self.gate(hidden_state) # here
"""MoE expert dispatch from: https://huggingface.co/deepseek-ai/DeepSeek-V3/blob/main/modeling_deepseek.py"""
cnts = paddle.zeros([topk_ids.shape[0], len(self.experts)], dtype=topk_ids.dtype)
cnts = cnts.put_along_axis(topk_ids, 1, axis=1)
tokens_per_expert = cnts.sum(axis=0)
idxs = topk_ids.reshape([topk_ids.shape[0] * topk_ids.shape[1]]).argsort()
sorted_tokens = reshaped_input[idxs // topk_ids.shape[1]]
tokens_per_expert = tokens_per_expert.detach()
sorted_tokens_shape = sorted_tokens.shape
if self.expert_parallel_degree > 1:
tokens_per_ep_rank = tokens_per_expert.reshape([self.expert_parallel_degree, -1]).sum(axis=1)
tokens_per_expert_group = _AllToAll.apply(
[tokens_per_expert.shape[0]], tokens_per_expert, group=self.moe_group
)
output_splits = (
tokens_per_expert_group.reshape([self.expert_parallel_degree, -1]).sum(axis=1).cpu().tolist()
)
input_split_sizes = tokens_per_ep_rank.cpu().tolist()
gathered_tokens = _AllToAll.apply(
[tokens_per_expert_group.sum(axis=0).cpu().item(), sorted_tokens.shape[1]],
sorted_tokens,
out_split_sizes=output_splits,
in_split_sizes=input_split_sizes,
group=self.moe_group,
)
tokens_per_expert_post_gather = tokens_per_expert_group.reshape(
[self.expert_parallel_degree, self.moe_num_experts_per_device]
).sum(axis=0)
gatherd_idxs = np.zeros(shape=(gathered_tokens.shape[0],), dtype=np.int32)
s = 0
for i, k in enumerate(tokens_per_expert_group.cpu().numpy()):
gatherd_idxs[s : s + k] = i % self.moe_num_experts_per_device
s += k
gatherd_idxs = gatherd_idxs.argsort()
sorted_tokens = gathered_tokens[gatherd_idxs]
tokens_per_expert = tokens_per_expert_post_gather
outputs = []
start_idx = 0
for i, num_tokens in enumerate(tokens_per_expert):
end_idx = start_idx + num_tokens
if num_tokens == 0:
continue
expert = self.experts[i + self.moe_rank * self.moe_num_experts_per_device]
tokens_for_this_expert = sorted_tokens[start_idx:end_idx]
expert_out = expert(tokens_for_this_expert)
outputs.append(expert_out)
start_idx = end_idx
outs = paddle.concat(outputs, axis=0) if len(outputs) > 0 else paddle.to_tensor(0, dtype=sorted_tokens.dtype)
if self.expert_parallel_degree > 1:
new_x = paddle.empty_like(outs)
new_x[gatherd_idxs] = outs
gathered_tokens = _AllToAll.apply(
sorted_tokens_shape,
new_x,
out_split_sizes=input_split_sizes,
in_split_sizes=output_splits,
group=self.moe_group,
)
outs = gathered_tokens
new_x = paddle.empty_like(outs)
new_x[idxs] = outs
final_out = (
new_x.reshape(topk_ids.shape + [-1])
.astype(topk_weight.dtype)
.multiply_(topk_weight.unsqueeze(-1))
.multiply_(token_priority.unsqueeze(-1))
.sum(axis=1)
.astype(new_x.dtype)
.reshape([batch_size, seq_len, -1])
)
return final_out, l_aux, l_zloss
class MoEFlexTokenLayer(nn.Layer):
def __init__(self, config, moe_num_experts, expert_class, expert_kwargs, gate, moe_group):
super().__init__()
self.config = config
self.moe_group = moe_group
self.ep_size = dist.get_world_size(self.moe_group)
self.moe_router_topk = gate.top_k
self.moe_num_experts = moe_num_experts
self.num_local_experts = moe_num_experts // self.ep_size
self.token_dispatcher = MoEFlexTokenDispatcher(
self.num_local_experts, self.moe_router_topk, self.moe_num_experts, moe_group
)
self.experts = nn.LayerList([expert_class(**expert_kwargs)] * self.num_local_experts)
self.router = gate
def expert_forward(self, dispatched_input, tokens_per_expert):
outputs = []
tokens_per_expert = tokens_per_expert.tolist()
chunks = paddle.split(dispatched_input, num_or_sections=tokens_per_expert, axis=0)
for chunk, expert in zip(chunks, self.experts):
chunk = chunk.contiguous()
# assert chunk.shape[0] != 0, "Cannot dispatch empty input"
outputs += [expert(chunk)]
return paddle.concat(outputs, axis=0)
def forward(self, hidden_states: paddle.Tensor):
_, _, d_model = hidden_states.shape
# reshaped_input = hidden_states.reshape([-1, d_model])
probs, routing_map, l_aux, l_zloss = self.router(hidden_states)
(dispatched_input, tokens_per_expert) = self.token_dispatcher.token_permutation(
hidden_states, probs, routing_map
)
expert_output = self.expert_forward(dispatched_input, tokens_per_expert)
output, _ = self.token_dispatcher.token_unpermutation(expert_output, None)
return output, l_aux, l_zloss