File size: 6,705 Bytes
5e6d9f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
"""DeepSeekMoE:细粒度专家 + 共享专家 + 无辅助损失负载均衡。

三个和普通 MoE 不一样的地方:

1. 细粒度专家(fine-grained):把专家切得更小更多,top-k 也调大。
   同样的激活参数量,组合数从 C(8,2) 变成 C(64,8),专家分工能更专一。

2. 共享专家(shared expert):抽出 1 个专家让所有 token 都过。
   通用知识(语法、常识)由它承担,路由专家就不必人手一份重复学。

3. 无辅助损失负载均衡(aux-loss-free,V3 的关键改进):
   传统做法是加一个负载均衡损失,但它和语言建模目标是打架的,会损伤效果。
   V3 改成给每个专家挂一个**不参与梯度**的偏置 b_i,只在 top-k 选择时加上:
       选择依据 = s_i + b_i,  聚合权重仍用原始 s_i
   每步训练后,谁超载就把谁的 b_i 调小,谁欠载就调大:
       b_i += γ * sign(平均负载 - 该专家负载)
   于是负载被掰平了,而梯度完全没被污染。
"""

from typing import Tuple

import torch
import torch.nn as nn
import torch.nn.functional as F

from .layers import SwiGLU


class Gate(nn.Module):
    """路由门控:决定每个 token 该交给哪几个专家。"""

    def __init__(self, cfg):
        super().__init__()
        self.dim = cfg.dim
        self.n_experts = cfg.n_routed_experts
        self.topk = cfg.n_activated_experts
        self.n_groups = cfg.n_expert_groups
        self.topk_groups = cfg.n_limited_groups
        self.score_func = cfg.score_func
        self.route_scale = cfg.route_scale
        self.aux_alpha = cfg.aux_loss_alpha

        self.weight = nn.Parameter(torch.empty(self.n_experts, self.dim))
        nn.init.normal_(self.weight, std=0.02)

        # 负载均衡偏置:只影响"选谁",不参与反向传播
        self.register_buffer("expert_bias", torch.zeros(self.n_experts))
        # 统计窗口内每个专家接了多少 token,供 update_bias 使用
        self.register_buffer("load_count", torch.zeros(self.n_experts), persistent=False)
        self.last_imbalance = 1.0

    def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
        """x: (N, dim) -> (weights (N,topk), indices (N,topk), aux_loss 标量)"""
        logits = F.linear(x.float(), self.weight.float())
        if self.score_func == "sigmoid":
            scores = logits.sigmoid()
        else:
            scores = logits.softmax(dim=-1)
        original = scores

        # ---- 选择用的分数 = 原始分数 + 负载偏置 ----
        sel = scores + self.expert_bias

        # ---- group-limited routing:先选组再选专家,限制跨设备通信量 ----
        if self.n_groups > 1:
            g = sel.view(-1, self.n_groups, self.n_experts // self.n_groups)
            per_group = g.size(-1)
            if per_group > 1:
                group_score = g.topk(min(2, per_group), dim=-1)[0].sum(dim=-1)
            else:
                group_score = g.squeeze(-1)
            keep = group_score.topk(self.topk_groups, dim=-1)[1]            # (N, topk_groups)
            gmask = torch.zeros_like(group_score, dtype=torch.bool).scatter_(1, keep, True)
            sel = sel.masked_fill(~gmask.unsqueeze(-1).expand_as(g).reshape_as(sel),
                                  torch.finfo(sel.dtype).min)

        indices = sel.topk(self.topk, dim=-1)[1]                            # (N, topk)
        weights = original.gather(1, indices)
        if self.score_func == "sigmoid":
            weights = weights / weights.sum(dim=-1, keepdim=True).clamp_min(1e-9)
        weights = weights * self.route_scale

        # ---- 统计负载 + 兜底的序列级辅助损失(权重很小)----
        onehot = F.one_hot(indices, self.n_experts).sum(dim=1).float()      # (N, E)
        counts = onehot.sum(dim=0)
        with torch.no_grad():
            self.load_count += counts.detach()

        if self.training and self.aux_alpha > 0:
            f = counts / counts.sum().clamp_min(1.0)                        # 实际负载占比
            p = original.mean(dim=0)                                        # 平均路由概率
            aux_loss = self.aux_alpha * self.n_experts * (f * p).sum()
        else:
            aux_loss = x.new_zeros(())

        return weights.type_as(x), indices, aux_loss

    @torch.no_grad()
    def update_bias(self, speed: float):
        """训练循环每步调一次:把超载专家的偏置压低、欠载的抬高。"""
        total = self.load_count.sum()
        if total <= 0:
            return
        self.last_imbalance = (self.load_count.max() / total * self.n_experts).item()
        target = total / self.n_experts
        err = target - self.load_count
        self.expert_bias += speed * torch.sign(err)
        self.load_count.zero_()

    @torch.no_grad()
    def load_stats(self) -> dict:
        """负载不均衡度 = 最大负载 / 平均负载。1.0 完全均衡,topk/E 的倒数为最差。"""
        total = self.load_count.sum()
        if total <= 0:
            return {"imbalance": self.last_imbalance}
        frac = self.load_count / total
        return {"imbalance": (frac.max() * self.n_experts).item()}


class MoE(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        self.dim = cfg.dim
        self.n_experts = cfg.n_routed_experts
        self.gate = Gate(cfg)
        self.experts = nn.ModuleList([SwiGLU(cfg.dim, cfg.moe_inter_dim)
                                      for _ in range(cfg.n_routed_experts)])
        self.shared = (SwiGLU(cfg.dim, cfg.moe_inter_dim * cfg.n_shared_experts)
                       if cfg.n_shared_experts > 0 else None)

    def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
        shape = x.shape
        x = x.view(-1, self.dim)
        weights, indices, aux = self.gate(x)

        y = torch.zeros_like(x)
        for i, expert in enumerate(self.experts):
            token_idx, slot = torch.where(indices == i)
            if token_idx.numel() == 0:
                continue
            y.index_add_(0, token_idx,
                         expert(x[token_idx]) * weights[token_idx, slot].unsqueeze(-1))

        if self.shared is not None:
            y = y + self.shared(x)
        return y.view(shape), aux


class DenseFFN(nn.Module):
    """前几层用的普通 FFN。V3 的做法:靠前的层先稠密,训练更稳。"""

    def __init__(self, cfg):
        super().__init__()
        self.ffn = SwiGLU(cfg.dim, cfg.dense_inter_dim)

    def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
        return self.ffn(x), x.new_zeros(())