一句话:MoE 是对 Transformer 中 FFN 层的改进——把一个 FFN 替换成多个并行的"专家"FFN,每个 token 只激活其中少数几个专家,从而在不增加计算量的前提下大幅扩展模型参数量。


一、问题背景:Dense 模型扩参数的代价

标准 Transformer(Dense 模型)每个 token 都要经过所有参数:

参数量翻倍 → 计算量翻倍 → 训练/推理成本翻倍

想要更强的模型,就必须付出更高的计算代价。有没有办法让参数量和计算量解耦


二、核心思想:条件计算(Conditional Computation)

不是每个 token 都需要用到所有知识,让不同的 token 走不同的"专家"路径。

MoE 的核心设计:

把 FFN 层替换成 N 个并行的专家 FFN
每个 token 由一个路由器(Router)决定激活哪 K 个专家
只有被选中的专家才参与计算,其余专家跳过

结果

  • 总参数量 = N 个专家的参数之和(很大)
  • 每次计算量 = 只激活 K 个专家(很小)
  • 参数量和计算量不再线性绑定

三、结构对比

Dense FFN(原始)

输入 x(形状:[seq_len, d_model])
  ↓
FFN(W_gate, W_up, W_down)
  ↓
输出(形状:[seq_len, d_model])

MoE FFN(替换后)

输入 x(形状:[seq_len, d_model])
  ↓
Router(路由器):为每个 token 计算各专家的得分,选出 Top-K
  ↓
并行调用被选中的 K 个专家 FFN
  ↓
按路由权重加权求和,合并输出
  ↓
输出(形状:[seq_len, d_model])← 形状和 Dense FFN 完全一致

注意:Attention 层不变,只有 FFN 层被替换成 MoE。


四、路由器(Router)的工作原理

Router 是一个简单的线性层,输出每个专家的"得分":

$$\text{scores} = \text{softmax}(x \cdot W_{\text{router}})$$

其中 $W_{\text{router}}$ 形状为 $[d_model, N_experts]$,输出 N 个专家的概率分布。

然后取 Top-K 个得分最高的专家:

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

# 构造最小示例:4 个 token,d_model=8,共 4 个专家,Top-2
torch.manual_seed(0)
N_tokens, d_model, num_experts, top_k = 4, 8, 4, 2

x = torch.randn(N_tokens, d_model)                        # 输入 token 向量
W_router = torch.randn(d_model, num_experts)               # Router 权重矩阵
experts = [nn.Linear(d_model, d_model, bias=False) for _ in range(num_experts)]

# 第一步:Router 计算每个 token 对各专家的得分
scores = F.softmax(x @ W_router, dim=-1)                   # [4, 4]

# 第二步:选 Top-2 专家
topk_scores, topk_indices = scores.topk(k=top_k, dim=-1)  # 各形状 [4, 2]

# 第三步:对 Top-K 权重重新归一化,使每个 token 的权重之和为 1
topk_scores = topk_scores / topk_scores.sum(dim=-1, keepdim=True)

# 第四步:对每个 token,加权求和被选中专家的输出
output = torch.zeros(N_tokens, d_model)
for k_pos in range(top_k):
    expert_ids = topk_indices[:, k_pos]        # 每个 token 在第 k_pos 位选的专家编号,形状 [4]
    weights = topk_scores[:, k_pos]            # 对应权重,形状 [4]
    for token_i in range(N_tokens):
        expert_id = expert_ids[token_i].item()
        output[token_i] += weights[token_i] * experts[expert_id](x[token_i])

print(output.shape)   # torch.Size([4, 8])
print(topk_indices)   # 每个 token 选中的专家编号,如 tensor([[2,1],[0,3],[1,2],[3,0]])

五、完整代码示例(PyTorch)

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


class ExpertFFN(nn.Module):
    """单个专家:标准的两层 FFN"""

    def __init__(self, d_model, d_ffn):
        super().__init__()
        self.w1 = nn.Linear(d_model, d_ffn, bias=False)
        self.w2 = nn.Linear(d_ffn, d_model, bias=False)

    def forward(self, x):
        return self.w2(F.silu(self.w1(x)))


class MoELayer(nn.Module):
    """MoE 层:N 个专家 + Router,每个 token 激活 Top-K 个专家"""

    def __init__(self, d_model, d_ffn, num_experts=8, top_k=2):
        super().__init__()
        self.num_experts = num_experts
        self.top_k = top_k

        # N 个独立的专家 FFN
        self.experts = nn.ModuleList([
            ExpertFFN(d_model, d_ffn) for _ in range(num_experts)
        ])

        # Router:线性层,输出每个专家的得分
        self.router = nn.Linear(d_model, num_experts, bias=False)

    def forward(self, x):
        batch_size, seq_len, d_model = x.shape
        x_flat = x.view(-1, d_model)  # [batch*seq_len, d_model]

        # 路由:计算每个 token 对应各专家的得分
        router_logits = self.router(x_flat)                          # [N_tokens, N_experts]
        router_scores = F.softmax(router_logits, dim=-1)             # 归一化为概率

        # 选 Top-K 个专家
        topk_scores, topk_indices = router_scores.topk(self.top_k, dim=-1)
        # 对 Top-K 的权重重新归一化(使权重之和为 1)
        topk_scores = topk_scores / topk_scores.sum(dim=-1, keepdim=True)

        # 对每个 token,加权求和被选中专家的输出
        output = torch.zeros_like(x_flat)
        for expert_idx in range(self.num_experts):
            # 找出哪些 token 选了这个专家
            token_mask = (topk_indices == expert_idx).any(dim=-1)
            if not token_mask.any():
                continue

            expert_input = x_flat[token_mask]
            expert_output = self.experts[expert_idx](expert_input)

            # 取该专家对应的权重
            expert_weight = topk_scores[token_mask][
                (topk_indices[token_mask] == expert_idx)
            ].unsqueeze(-1)

            output[token_mask] += expert_weight * expert_output

        return output.view(batch_size, seq_len, d_model)


# 验证
d_model, d_ffn = 512, 2048
moe = MoELayer(d_model=d_model, d_ffn=d_ffn, num_experts=8, top_k=2)

x = torch.randn(2, 16, d_model)   # batch=2, seq_len=16
out = moe(x)
print(out.shape)  # torch.Size([2, 16, 512])

六、负载均衡问题:专家塌陷

MoE 有一个严重的训练问题:专家塌陷(Expert Collapse)

Router 一旦发现某个专家效果好,就会一直选它
→ 这个专家越训越强,其他专家越来越少被选
→ 最终退化成只有 1~2 个专家在工作,其余专家形同虚设

解决方案1:辅助损失(Auxiliary Loss)

在训练损失里加一项,惩罚负载不均衡:

$$\mathcal{L}{\text{aux}} = \alpha \cdot N \cdot \sum{i=1}^{N} f_i \cdot p_i$$

其中 $f_i$ 是专家 $i$ 被选中的频率,$p_i$ 是 Router 给专家 $i$ 的平均概率。鼓励每个专家被均匀使用。

解决方案2:无辅助损失的负载均衡(DeepSeek 方案)

DeepSeek-V3 提出了无辅助损失的方案:给每个专家加一个可学习的偏置项,动态调整各专家被选中的概率,不需要额外的损失函数。


七、DeepSeek-V3 的 MoE 设计

DeepSeek-V3(671B)是目前最具代表性的 MoE 大模型:

总专家数:256 个
每个 token 激活专家数:8 个(Top-8)
共享专家:1 个(每个 token 必选,保证基础能力)
总参数:671B
每次推理激活参数:~37B(约 5.5% 的参数参与计算)

共享专家是 DeepSeek 的创新:除了路由选出的 8 个专家,还有 1 个专家是所有 token 都必须经过的,用来保留通用知识,避免专家过度特化。

每个 token 的计算路径:
  共享专家(必选 1 个)+ 路由专家(Top-8)= 共 9 个专家的输出加权求和

八、MoE 的优缺点

优点

优点 说明
参数量和计算量解耦 671B 参数但只激活 37B,推理速度接近 37B 模型
专家自然分工 不同专家会自发学习不同领域的知识
扩展性强 增加专家数量几乎不增加推理计算量

缺点

缺点 说明
显存需求高 所有专家的参数都要加载进显存,即使大多数不参与计算
训练不稳定 专家塌陷问题需要额外处理
通信开销大 分布式训练时,不同 token 路由到不同设备的专家,需要大量跨设备通信
推理延迟不均 不同 batch 里激活的专家不同,难以做静态优化

九、核心要点速查

问题 答案
MoE 改的是哪一层? FFN 层,Attention 层不变
专家是什么? 独立的 FFN 子网络,结构和普通 FFN 一样
Router 做什么? 为每个 token 计算各专家得分,选 Top-K
为什么参数多但计算少? 每次只激活 K 个专家,其余专家跳过
最大的训练问题是什么? 专家塌陷,需要负载均衡策略
DeepSeek-V3 的规模? 671B 总参数,每次激活约 37B(Top-8 + 1 共享专家)
和 Dense 模型的本质区别? Dense:所有参数都激活;MoE:条件激活,参数量≠计算量