# !/usr/bin/env python3 # Copyright (c) 2025 PaddlePaddle Authors. 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. import os import unittest import numpy as np import paddle import paddle.nn.functional as F from paddle.autograd import PyLayer from paddle.incubate.nn.functional import ( moe_gate_dispatch, ) os.environ["FLAGS_flash_attn_version"] = "v1" os.environ["FLAGS_cudnn_deterministic"] = "1" os.environ["FLAGS_embedding_deterministic"] = "1" os.environ["XPU_PADDLE_FC_LOCAL_INT16"] = "1" def topk_grad(x, dy, indices, w): """ y=gather(topk(x)) 的反向过程 x: [s,e] dy: [s,k] """ s, e = x.shape _, k = dy.shape dx = paddle.zeros([s, e]) # mask for i in range(s): for j in range(k): if w[i, j] > 0: index = indices[i, j] dx[i, index] = dy[i, j] return dx # dx 保持高精度 class GateDispatch(PyLayer): """doc""" @staticmethod def forward(ctx, x, gate_prob, k, capacity, use_pad, eps=1e-12): """ 对`gate_prob` 进行 softmax 并根据结果选取 topk 路由expert。 最后根据 expert 号对 `x` 进行重排。 Args: x: [s, d] 输入的 activateion gate_prob: [s, e] k: int capacity: int #no use Returns: y: [s*k, d] 将所有 `x` 根据其路由的 `expert-id` 升序的排序,融合到 s 维度。 当截断发生时 s 会比输入 s 小。 combine_weights: [s, k], float: 每个 token 第 k 选择的 expert 的权重。 当截断发生时 s 会比输入 s 小。 scatter_index: [k, s] : 每个 token 第 k 次选择对应到 `y` 中的位置。 expert_offset: [e]: `y`中每个 expert-id 的分割位置。 expert_id: [s] `x` 中激活的 expert 号 """ ctx.k = k ctx.eps = eps ctx.capacity = capacity ctx.gate_prob = gate_prob y, combine_weights, scatter_index, expert_offset, expert_id = ( moe_gate_dispatch( x, gate_prob, None, k=k, capacity=capacity, use_pad=use_pad, ) ) ctx.combine_weights = combine_weights scatter_index = scatter_index.transpose([1, 0]) # [k,s] ->[s,k] ctx.scatter_index = scatter_index ctx.expert_id = expert_id num_experts = gate_prob.shape[-1] ctx.num_experts = num_experts ctx.seqlen = gate_prob.shape[0] return y, combine_weights, scatter_index, expert_offset, expert_id @staticmethod def backward(ctx, dy, dw, *_): """ 关于 softmax 对 logits 的导数,参考: https://stats.stackexchange.com/questions/215521/ how-to-find-derivative-of-softmax-function-for-the-purpose-of-gradient-descent/328095#328095 """ s, k = ctx.combine_weights.shape grad = F.embedding(ctx.scatter_index, dy) # [s, k,d] mask = (ctx.combine_weights > 0.0).astype(grad.dtype) # [s,k] dx = paddle.matmul(mask.unsqueeze(1), grad).squeeze( 1 ) # [s,1,k] @ [s,k,d] -> [s,1,d] if ctx.gate_prob.stop_gradient: return dx, None combine_weights_unnorm = ctx.combine_weights dw = dw.astype(combine_weights_unnorm.dtype) d_prob = topk_grad( ctx.gate_prob, dw, ctx.expert_id, combine_weights_unnorm ) return dx, d_prob class MoELayer(paddle.nn.Layer): def forward(self, x, gate_prob, k, capacity): y, combine_weights, scatter_index, expert_offset, expert_id = ( moe_gate_dispatch( x, gate_prob, None, k=k, capacity=capacity, use_pad=True ) ) scatter_index = scatter_index.transpose([1, 0]) # [k,s] ->[s,k] return y, combine_weights, scatter_index, expert_offset, expert_id class TestFused(unittest.TestCase): def test_moe_ops(self): """ test `moe-ops` w/ bias """ # S, E, D = 8192, 64, 128 S, E, D = 4, 4, 2 # k = 4 k = 2 # cap = 512 cap = 2 # x = paddle.randn([S, D], dtype="bfloat16") x = paddle.randn([S, D], dtype="float32") gate_logits = paddle.randn([S, E], dtype="float32") x_ = x.clone() gate_logits_ = gate_logits.clone() x.stop_gradient = False x_.stop_gradient = False gate_logits.stop_gradient = False gate_logits_.stop_gradient = False bias = paddle.zeros([E], dtype="float32") layer = MoELayer() y, combine_weihgts, scatter_index, expert_offset, expert_id = layer( x, gate_logits, k, cap, ) grad_y_numpy = np.random.randn(*y.shape).astype(np.float32) grad_w_numpy = np.random.randn(*combine_weihgts.shape).astype( np.float32 ) grad_y = paddle.to_tensor(grad_y_numpy) grad_w = paddle.to_tensor(grad_w_numpy) paddle.autograd.backward([y, combine_weihgts], [grad_y, grad_w]) y_, combine_weihgts_, scatter_index_, expert_offset_, expert_id_ = ( GateDispatch.apply(x_, gate_logits_, k, cap, True) ) grad_y_ = paddle.to_tensor(grad_y_numpy) grad_w_ = paddle.to_tensor(grad_w_numpy) paddle.autograd.backward( [y_, combine_weihgts_], [grad_y_, grad_w_], True ) np.testing.assert_equal( y.astype("float32").numpy(), y_.astype("float32").numpy(), err_msg="incubate w bias not match", ) # bias 不影响 prob 概率 np.testing.assert_equal( combine_weihgts.astype("float32").numpy(), combine_weihgts_.astype("float32").numpy(), err_msg="incubate w bias not match", ) np.testing.assert_equal( scatter_index.astype("float32").numpy(), scatter_index_.astype("float32").numpy(), err_msg="incubate w bias not match", ) np.testing.assert_equal( expert_offset.astype("float32").numpy(), expert_offset_.astype("float32").numpy(), err_msg="incubate w bias not match", ) np.testing.assert_equal( expert_id.astype("float32").numpy(), expert_id_.astype("float32").numpy(), err_msg="incubate w bias not match", ) np.testing.assert_allclose( x.grad.astype("float32").numpy(), x_.grad.astype("float32").numpy(), atol=1e-5, rtol=1e-5, ) np.testing.assert_allclose( gate_logits.grad.astype("float32").numpy(), gate_logits_.grad.astype("float32").numpy(), atol=1e-5, rtol=1e-5, ) if __name__ == "__main__": unittest.main()