AI 工程基础体系 · 第 76/100 篇。内容覆盖机器学习、深度学习与生成式 AI;模型、数据、评测、权限和成本会作为同一生产系统处理。

MoE 模型原理:路由、专家负载、容量、通信与推理成本

Mixture of Experts(MoE,混合专家)是一种条件计算结构:模型拥有很多参数,但每个 token 只激活其中一部分专家。这里的“专家”通常不是完整模型,而是 Transformer 层中的一个前馈网络(Feed-Forward Network,FFN);路由器(router)根据 token 的隐藏状态选择专家。

MoE 的目标不是让每个 token 都经过更大的稠密网络,而是在大幅增加总参数量的同时,限制每个 token 的激活计算量。它因此同时引入了五个必须一起分析的问题:

  1. 路由器如何选择专家;
  2. token 是否均匀地分布到专家;
  3. 每个专家一次最多能接收多少 token;
  4. token 如何在设备之间移动;
  5. 稀疏激活是否真的降低了训练和推理成本。

如果只讨论“每个 token 选择 top-k 个专家”,而不讨论负载、容量和通信,就还没有解释 MoE 的核心工程行为。


1. 前置:Transformer 中被 MoE 替换的是什么

Transformer 层通常包含自注意力和逐 token 的前馈网络。简化表示为:

X=X+Attention(Norm(X))X' = X + \operatorname{Attention}(\operatorname{Norm}(X))

Y=X+FFN(Norm(X))Y = X' + \operatorname{FFN}(\operatorname{Norm}(X'))

其中:

  • XRT×dX \in \mathbb{R}^{T \times d} 是一个 batch 中的 token 隐藏状态;
  • TT 是 token 数量;
  • dd 是模型隐藏维度;
  • FFN\operatorname{FFN} 对每个 token 独立计算;
  • TT 可以是 batch size 与序列长度的乘积。

典型 FFN 为:

FFN(x)=W2σ(W1x+b1)+b2\operatorname{FFN}(x)=W_2 \sigma(W_1x+b_1)+b_2

其中 W1W_1 将维度从 dd 扩展到 dffd_{\text{ff}}W2W_2 再投影回 dd。Transformer 原论文《Attention Is All You Need》中的编码器和解码器都包含这种逐位置前馈子层。

MoE 的典型改造是把一个 FFN 替换为多个 FFN:

MoE(x)=iS(x)p~i(x)Ei(x)\operatorname{MoE}(x)=\sum_{i \in S(x)} \tilde{p}_i(x) E_i(x)

其中:

  • EiE_i 是第 ii 个专家 FFN;
  • EE 是专家总数;
  • S(x)S(x) 是路由器为 token xx 选出的专家集合;
  • k=S(x)k=|S(x)| 是每个 token 激活的专家数量;
  • p~i(x)\tilde{p}_i(x) 是选中专家的组合权重。

k=1k=1 时,称为 top-1 MoE;当 k=2k=2 时,称为 top-2 MoE。专家总数增加了,但单个 token 通常只执行 kk 个专家,而不是全部 EE 个专家。

需要注意,MoE 通常只替换 FFN,不会自动稀疏化自注意力。因此一个 MoE Transformer 仍然可能把大量计算花在注意力、归一化、投影和通信上。


2. 路由器如何选择专家

2.1 路由分数

对于 token 隐藏状态 xtRdx_t \in \mathbb{R}^{d},路由器先计算每个专家的分数:

st=Wrxt+brs_t = W_r x_t + b_r

其中:

  • WrRE×dW_r \in \mathbb{R}^{E \times d}
  • stREs_t \in \mathbb{R}^{E}
  • st,is_{t,i} 表示 token tt 选择专家 ii 的未归一化分数。

然后通过 softmax 得到路由概率:

pt,i=exp(st,i)j=1Eexp(st,j)p_{t,i} = \frac{\exp(s_{t,i})} {\sum_{j=1}^{E}\exp(s_{t,j})}

这里的概率主要用于排序和组合专家输出,并不表示一个严格意义上的统计概率。它是一个可学习的权重分布。

对所有 token 计算后得到:

PRT×EP \in \mathbb{R}^{T \times E}

朴素实现的路由器计算量约为 O(TdE)O(TdE)。当专家数量 EE 很大时,路由矩阵本身也可能成为开销来源,因此实际系统会采用分组、分层路由或其他优化;不能简单认为“专家不激活,所以路由成本为零”。

2.2 Top-k 选择

路由器保留每个 token 分数最高的 kk 个专家:

St=TopK(pt,k)S_t = \operatorname{TopK}(p_t,k)

一种常见的组合方式是对选中的权重重新归一化:

p~t,i=pt,ijStpt,j,iSt\tilde{p}_{t,i} = \frac{p_{t,i}} {\sum_{j\in S_t}p_{t,j}}, \qquad i\in S_t

最终输出为:

yt=iStp~t,iEi(xt)y_t=\sum_{i\in S_t}\tilde{p}_{t,i}E_i(x_t)

不同实现可能不对 top-k 权重重新归一化,或者使用额外的缩放方式。因此在复现模型时,不能只看“top-2”这个名称,还必须确认组合权重的具体定义。

2.3 Top-1 与 Top-2 的区别

Top-1

yt=Ei(xt),i=argmaxipt,iy_t=E_{i^*}(x_t), \qquad i^*=\arg\max_i p_{t,i}

它的优势是每个 token 只执行一次专家计算,通信和调度更简单;缺点是路由错误或专家负载不均衡时,单个 token 没有第二个专家提供补偿。

Top-2

yt=p~t,i1Ei1(xt)+p~t,i2Ei2(xt)y_t=\tilde p_{t,i_1}E_{i_1}(x_t) +\tilde p_{t,i_2}E_{i_2}(x_t)

它通常提供更灵活的表示能力和更平滑的路由,但专家计算、通信和容量需求大致按 kk 增长。top-2 并不意味着总计算一定是 top-1 的两倍,因为实际系统还受到填充、丢弃、通信和负载不均衡的影响。

2.4 路由选择不是完全可微的

Top-k 包含离散的索引选择。被选中的专家可以通过组合权重接收梯度,但“某个专家是否进入 top-k”这个边界本身不是普通连续函数。

因此,路由训练通常依赖两部分信号:

  1. 主任务损失,例如语言模型的交叉熵;
  2. 负载均衡辅助损失,使专家不会长期只使用少数几个。

路由器并不是只通过主任务损失就一定能学出均衡分布。一个专家如果早期偶然表现较好,可能吸引更多 token,进一步获得更多训练信号,形成“赢家通吃”的反馈。


3. 专家负载:概率均衡不等于实际均衡

3.1 三种不同的“负载”

讨论负载时至少要区分三个概念:

  1. 分配数量:有多少 token 被分配给专家;
  2. 实际计算数量:容量截断和 padding 后,专家真正处理多少 token;
  3. 运行时间负载:专家所在设备实际花费了多少时间。

这三者并不总相同。例如某专家接收 100 个 token,另一个接收 50 个 token,但前者所在设备可能因为通信、显存或其他 kernel 冲突而耗时远超两倍。

3.2 一个常见的负载均衡辅助损失

设:

fi=1Tt=1T1[iSt]f_i=\frac{1}{T}\sum_{t=1}^{T}\mathbf{1}[i\in S_t]

表示实际路由到专家 ii 的 token 比例。

再设:

pˉi=1Tt=1Tpt,i\bar p_i=\frac{1}{T}\sum_{t=1}^{T}p_{t,i}

表示路由器对专家 ii 的平均概率质量。

一种常见的辅助损失形式是:

Laux=αEi=1EfipˉiL_{\text{aux}} = \alpha E \sum_{i=1}^{E} f_i\bar p_i

其中 α\alpha 是辅助损失系数。

如果路由完全均匀:

fi=pˉi=1Ef_i=\bar p_i=\frac{1}{E}

则:

Laux=αEE1E2=αL_{\text{aux}} = \alpha E \cdot E\cdot \frac{1}{E^2} =\alpha

如果绝大多数 token 都路由到同一个专家,则该专家的 fif_ipˉi\bar p_i 都较大,损失会增大。

这个损失只是一种常见形式,不是 MoE 的唯一标准。不同模型还可能使用重要性损失、专家选择损失、路由熵正则、router z-loss 等机制。它们的共同目标是防止路由退化,但并不保证真实设备运行时间完全均衡。

3.3 完整算例:为什么平均概率不能代替实际分配

假设有 T=8T=8 个 token 和 E=2E=2 个专家,top-1 路由结果为:

[1,1,1,1,1,1,2,2][1,1,1,1,1,1,2,2]

于是:

f1=68=0.75,f2=28=0.25f_1=\frac{6}{8}=0.75,\qquad f_2=\frac{2}{8}=0.25

如果路由器的平均概率为:

pˉ1=0.55,pˉ2=0.45\bar p_1=0.55,\qquad \bar p_2=0.45

那么概率看起来并不极端,但真实 token 分配已经是 6:2。此时:

ifipˉi=0.75×0.55+0.25×0.45=0.525\sum_i f_i\bar p_i =0.75\times0.55+0.25\times0.45 =0.525

Laux=α×2×0.525=1.05αL_{\text{aux}}=\alpha \times 2 \times 0.525 =1.05\alpha

这说明仅观察平均路由概率并不能判断 dispatch 后的实际负载。生产监控必须同时记录:

  • 每个专家接收的 token 数;
  • 每个专家被丢弃的 token 数;
  • 每个设备的 all-to-all 通信量;
  • 专家 kernel 的执行时间;
  • 设备之间的最大/平均负载比。

4. 容量:为什么专家不能无限接收 token

4.1 容量的定义

专家容量(expert capacity)是某个专家在一次路由批次中最多处理的 token 数。

设:

  • TT 为本次路由组中的 token 数;
  • EE 为专家数;
  • kk 为每个 token 的路由数量;
  • cc 为 capacity factor。

常见容量公式为:

C=cTkEC= \left\lceil c\cdot \frac{T k}{E} \right\rceil

当 top-1 时:

C=cTEC=\left\lceil c\cdot \frac{T}{E}\right\rceil

当 top-2 时,理论路由槽位翻倍,因此:

C=c2TEC=\left\lceil c\cdot \frac{2T}{E}\right\rceil

这里的 TT 必须对应具体实现的路由分组。有的系统按整个 batch 计算,有的按数据并行 rank、本地 micro-batch 或 token group 计算。公式形式相同,但统计范围不同,结果会不同。

4.2 容量算例

仍假设:

  • T=8T=8
  • E=2E=2
  • top-1,即 k=1k=1
  • capacity factor c=1c=1

则:

C=82=4C=\left\lceil\frac{8}{2}\right\rceil=4

如果路由结果是:

[1,1,1,1,1,1,2,2][1,1,1,1,1,1,2,2]

专家 1 需要 6 个位置,但容量只有 4,因此只有前 4 个 token 能被专家 1 处理,后 2 个 token 会被标记为 overflow。专家 2 只使用 2 个位置,剩余容量不能自动转给专家 1,因为容量是按专家分别限制的。

如果把 cc 调整为 1.51.5

C=1.5×4=6C=\lceil 1.5\times 4\rceil=6

此时专家 1 的 6 个 token 都能处理,但代价是每个专家都必须为最多 6 个 token 预留空间;如果实际平均只有 4 个 token,就会产生 padding 或更高的显存占用。

4.3 Overflow token 的处理

超过容量的 token 常见有三种处理方式:

  1. 丢弃专家计算,直接走残差路径
  2. 改送到候选列表中的下一个专家
  3. 动态扩容或使用不规则 kernel

第一种方式实现简单,但被丢弃 token 的 FFN 变换缺失。若 MoE 输出采用残差结构,token 仍可能保留前一层表示,但这不是“完整执行了该层”。

第二种方式可以减少丢弃,但可能使第二候选专家进一步超载。需要明确容量是在第一次分配后统一截断,还是逐候选顺序分配。

第三种方式减少了硬容量限制,却会牺牲静态形状和高效矩阵乘法,尤其不利于 GPU 批量 kernel 和编译优化。

因此容量不是一个单纯的精度参数,而是显存、通信、矩阵形状、token 丢失率和吞吐量之间的约束

4.4 容量 factor 的边界

容量 factor 过低时:

  • overflow 增多;
  • token 丢弃率上升;
  • 训练梯度覆盖减少;
  • 不同 batch 之间的有效计算量波动增大。

容量 factor 过高时:

  • 显存预留增加;
  • padding 增加;
  • all-to-all 的传输和缓冲区变大;
  • 实际负载不均时,最忙专家仍可能决定关键路径延迟。

提高容量并不能修复一个严重偏置的路由器。它只是让更多超载 token 有机会被处理;如果所有 token 持续集中到一个专家,容量最终仍然会成为瓶颈。


5. Dispatch 与 Combine:token 如何进入专家

路由通常不是逐 token 调用专家函数,而是把 token 按专家重新排列成批次。

设隐藏状态为:

XRT×dX\in\mathbb{R}^{T\times d}

路由后,系统会生成:

  • expert_indices:每个 token 对应的专家;
  • dispatch_mask 或等价索引:token 在专家缓冲区中的位置;
  • combine_weights:专家输出合并时使用的权重。

逻辑数据流如下:

flowchart LR
    X[Token hidden states T x d] --> R[Router]
    R --> P[Routing probabilities]
    P --> S[Top-k selection]
    S --> C[Capacity assignment]
    C --> D[Dispatch / reorder]
    D --> A[All-to-all if experts are sharded]
    A --> E[Expert FFNs]
    E --> B[All-to-all reverse]
    B --> O[Combine by routing weights]
    O --> Y[MoE output T x d]

关键路径是:

  1. 每个 token 产生专家候选;
  2. 根据容量为 token 分配专家槽位;
  3. 将同一专家的 token 聚集到一起;
  4. 若专家分布在不同设备,执行 token dispatch;
  5. 每个专家执行自己的 FFN;
  6. 将输出按原 token 顺序送回;
  7. 按路由权重合并 top-k 专家输出。

“专家并行”并不意味着 token 自动出现在正确的 GPU 上。若 token 所属专家位于其他设备,就必须发送激活值;专家输出也要返回原来的设备。


6. 通信:稀疏计算为什么可能变慢

6.1 专家并行下的 all-to-all

假设有 4 个 GPU,每个 GPU 放置一部分专家。一个 GPU 上的 token 可能被路由到另外 3 个 GPU 的专家,因此需要 all-to-all 通信。

通信对象通常是 token 激活:

activation shape[Nrouted,d]\text{activation shape} \approx [N_{\text{routed}}, d]

其中 NroutedN_{\text{routed}}TTkk、容量和 padding 有关。若使用低精度,每个元素占用 bb 字节,则一次方向上的理论数据量近似为:

VNrouteddbV\approx N_{\text{routed}}\cdot d\cdot b

实际网络流量还会受到:

  • padding;
  • 元数据;
  • 分片策略;
  • 集合通信算法;
  • 链路拓扑;
  • 跨节点与节点内通信差异;

的影响。

一次 MoE 层通常至少涉及“发送到专家”和“从专家返回”两个方向。因此即使专家 FFN 的 FLOPs 较少,通信仍可能成为关键路径。

6.2 All-to-all 与参数并行的区别

需要区分三类并行:

  • 数据并行:不同设备处理不同样本或 token;
  • 张量并行:同一个矩阵或层被切分到多个设备;
  • 专家并行:不同设备保存不同专家,token 按路由结果移动。

MoE 通常会组合使用它们。例如,一个训练组可以先通过数据并行产生 token,再通过 all-to-all 把 token 发送到专家所在设备;专家内部还可能使用张量并行拆分 FFN 矩阵。

这会产生多个并行组和通信域。一个实现能否高效运行,不能只看专家数量,还要看:

  • 专家是否跨节点分布;
  • 每个设备上的专家数;
  • 数据并行与专家并行的排列;
  • 每个通信组的成员;
  • 通信是否与专家计算重叠。

6.3 为什么负载均衡直接影响延迟

在同步训练中,一个 MoE 层通常要等待所有相关专家完成。若某设备收到的 token 数量最多,则该设备的处理时间可能决定整个层的完成时间:

tlayermaxrranks(tcomm,r+texpert,r)t_{\text{layer}} \approx \max_{r\in\text{ranks}} \left( t_{\text{comm},r}+t_{\text{expert},r} \right)

因此平均 token 数低并不代表延迟低。一个极端忙碌的 rank 就可能让所有 rank 等待。

通信还可能出现“局部均衡、全局不均衡”:每个 rank 内部的专家负载看似接近,但某些 rank 之间的远程 token 发送比例很高,导致跨节点链路成为瓶颈。


7. 一个可运行的容量路由示例

下面的 PyTorch 示例实现一个简化的 top-1 路由器。它展示:

  • 如何计算路由概率;
  • 如何选择专家;
  • 如何按专家容量保留 token;
  • 如何统计 overflow。

它没有实现 GPU 间 all-to-all,也没有反向传播所需的高效稀疏 kernel,因此适合验证路由逻辑,不适合作为生产 MoE 层。

import torch
import torch.nn as nn

torch.manual_seed(7)

class ToyTop1MoE(nn.Module):
    def __init__(self, d_model=4, d_hidden=8, num_experts=2,
                 capacity_factor=1.0):
        super().__init__()
        self.num_experts = num_experts
        self.capacity_factor = capacity_factor

        self.router = nn.Linear(d_model, num_experts, bias=False)
        self.experts = nn.ModuleList([
            nn.Sequential(
                nn.Linear(d_model, d_hidden),
                nn.ReLU(),
                nn.Linear(d_hidden, d_model),
            )
            for _ in range(num_experts)
        ])

    def forward(self, x):
        # x: [T, d_model]
        T, d_model = x.shape

        logits = self.router(x)                 # [T, E]
        probs = torch.softmax(logits, dim=-1)   # [T, E]
        top_prob, top_idx = probs.max(dim=-1)   # [T], [T]

        capacity = int(
            torch.ceil(torch.tensor(
                self.capacity_factor * T / self.num_experts
            )).item()
        )

        # 输出先置为零;生产实现通常还要明确残差/丢弃策略
        y = torch.zeros_like(x)
        kept = torch.zeros(T, dtype=torch.bool, device=x.device)
        counts = torch.zeros(
            self.num_experts, dtype=torch.long, device=x.device
        )

        for expert_id, expert in enumerate(self.experts):
            token_ids = torch.nonzero(
                top_idx == expert_id, as_tuple=False
            ).flatten()

            # 保留该专家容量以内的 token
            token_ids = token_ids[:capacity]
            if token_ids.numel() == 0:
                continue

            expert_out = expert(x[token_ids])
            y[token_ids] += top_prob[token_ids, None] * expert_out

            kept[token_ids] = True
            counts[expert_id] = token_ids.numel()

        overflow = (~kept).sum().item()

        stats = {
            "capacity": capacity,
            "assigned_per_expert": counts.tolist(),
            "overflow_tokens": overflow,
            "router_prob_mean": probs.mean(dim=0).detach().tolist(),
        }
        return y, stats


x = torch.randn(8, 4)
model = ToyTop1MoE(
    d_model=4,
    d_hidden=8,
    num_experts=2,
    capacity_factor=1.0,
)

y, stats = model(x)
print("output shape:", tuple(y.shape))
print("stats:", stats)

T=8,E=2,c=1T=8,E=2,c=1 时,容量应为 4。由于随机初始化会产生不同路由结果,输出中的 assigned_per_expert 不一定是 [4, 2],但每个元素都不应超过 4,overflow_tokens 等于未被容量接收的 token 数。

这个示例有几个刻意简化之处:

  • 它逐专家执行 Python 循环,吞吐量很低;
  • 它没有把 token 重新排列为 [num_experts, capacity, d_model] 的规整矩阵;
  • 它没有实现 top-2;
  • overflow token 的输出保持为零,而真实模型通常会采用残差、备选专家或其他定义;
  • 路由概率的梯度只通过保留的专家路径传播。

因此,“代码能运行”不代表它复现了某个具体 Transformers 实现。使用 Hugging Face Transformers 时,应以目标模型配置和对应版本文档为准,确认专家数量、top-k、容量、辅助损失和并行后端的实际定义。


8. MoE 的参数量、激活参数量与 FLOPs

8.1 参数量

假设每个专家 FFN 有 PffnP_{\text{ffn}} 个参数,专家数为 EE,则该 MoE 层的专家参数约为:

PMoE=EPffnP_{\text{MoE}}=E\cdot P_{\text{ffn}}

相比单个 FFN,参数量增加约 EE 倍,但每个 token 并不执行所有专家。

路由器参数约为:

Prouter=dEP_{\text{router}}=dE

EE 很大时,路由器参数和路由 logits 计算也不能忽略。

8.2 激活参数量

对一个 token 而言,若 top-k 选择 kk 个专家,它实际访问的专家参数约为:

PactivekPffnP_{\text{active}}\approx k\cdot P_{\text{ffn}}

这只是“参与该 token 前向计算的参数规模”,不是显存占用。分布式训练中,某个设备可能仍然需要保存本地专家参数、梯度和优化器状态,即使当前 batch 没有访问所有专家。

8.3 FFN 计算量

忽略 bias 和激活函数成本,普通两层 FFN 的前向矩阵乘法 FLOPs 约为:

2×(2ddff)=4ddff2 \times (2d d_{\text{ff}}) =4d d_{\text{ff}}

这里每个矩阵乘法按一次乘法和一次加法计为 2 FLOPs。若 top-k MoE 不考虑 padding:

FLOPsMoEkT4ddff\text{FLOPs}_{\text{MoE}} \approx kT\cdot 4d d_{\text{ff}}

EE 个专家全部执行的稠密版本约为:

FLOPsdense-allET4ddff\text{FLOPs}_{\text{dense-all}} \approx ET\cdot 4d d_{\text{ff}}

所以 MoE 的专家计算相对于“每个 token 执行全部专家”大约减少到 k/Ek/E。但它与一个只有单个 FFN 的稠密模型相比,并不一定更便宜,因为 top-k 仍然要执行 kk 个 FFN,而不是一个。

还需要加入以下成本:

总成本=注意力+普通投影与归一化+路由+专家计算+dispatch/combine 通信+padding 与调度\text{总成本} = \text{注意力} +\text{普通投影与归一化} +\text{路由} +\text{专家计算} +\text{dispatch/combine 通信} +\text{padding 与调度}

因此不能用“总参数量大、激活参数量小”直接推出“推理一定更快”。

8.4 Padding 会改变实际计算量

如果每个专家都按固定容量 CC 执行矩阵乘法,那么实际计算量可能接近:

ECFFN-costE\cdot C\cdot \text{FFN-cost}

而不是实际接收 token 数乘以专家成本。容量越大、负载越不均,padding 越多,理论稀疏性越难转化为实际吞吐量。


9. 训练成本与推理成本不是同一个问题

9.1 训练阶段

训练时需要保存反向传播所需的激活,并计算:

  • 路由 logits;
  • token dispatch;
  • 专家前向;
  • 专家反向;
  • combine 的反向;
  • all-to-all 的反向通信;
  • 优化器状态更新。

如果专家参数跨设备分片,每个设备只保存部分专家参数,可以降低单卡参数显存;但训练组仍然需要通过通信让 token 到达对应专家。

训练显存通常还包括:

  • 参数;
  • 梯度;
  • 优化器状态;
  • 激活;
  • 通信缓冲区;
  • padding 后的专家输入输出。

因此 MoE 的“总参数量”可能很高,单卡参数量却较低;这两种容量指标必须分开报告。

9.2 Prefill 推理

Prefill 是一次处理较长输入序列的阶段。此时同一批次有较多 token,专家能够形成较大的矩阵乘法,GPU 利用率通常比逐 token 解码好。

但 prefill 仍然要承担:

  • 路由器计算;
  • token 重新排列;
  • 专家间通信;
  • 反向 all-to-all;
  • padding 和负载不均衡。

如果 batch 很大,通信量也随 token 数增加;如果专家跨节点,网络带宽可能成为瓶颈。

9.3 Decode 推理

自回归 decode 通常每次只新增少量 token。对单条请求而言,TT 可能接近 1;这会造成:

  • 专家矩阵乘法批量很小;
  • all-to-all 消息碎片化;
  • 通信启动延迟占比升高;
  • 某个热门专家被多个请求同时争用;
  • 为保持专家常规矩阵形状而产生更多 padding。

因此 MoE 在大 batch、连续 batching 和足够 token 聚合时更容易获得收益;在低并发、短请求、严格低延迟场景下,MoE 可能不如参数更少的稠密模型。

9.4 KV cache 不会因 MoE 自动消失

MoE 通常替换 FFN,不改变注意力的基本结构。因此自回归推理仍然需要维护注意力 KV cache。MoE 可以减少部分 FFN 的激活计算,但不会自动解决:

  • KV cache 显存;
  • 长上下文注意力;
  • 多请求 cache 管理;
  • attention kernel 的延迟。

把 MoE 的“激活参数稀疏”误认为“整个 Transformer 都是稀疏的”,会导致错误的显存和延迟估算。


10. 路由失败的典型表现与诊断

10.1 专家坍缩

专家坍缩是指大多数 token 长期集中到少数专家。表现包括:

  • tokens_per_expert 分布高度偏斜;
  • 某些专家几乎没有梯度;
  • 某些设备显存和 kernel 时间明显更高;
  • overflow token 比例持续升高;
  • 训练吞吐下降或 loss 不稳定。

诊断时应同时看三组数据:

{fi},{pˉi},{ti}\{f_i\},\qquad \{\bar p_i\},\qquad \{t_i\}

分别对应实际 token 比例、平均概率质量和专家运行时间。只看 pˉi\bar p_i 可能掩盖容量截断;只看 fif_i 可能忽略专家 kernel 或通信差异。

10.2 路由概率正常但设备超时

如果专家 token 数接近均衡,但某些 rank 仍然慢,问题可能在:

  • 跨节点 token 比例过高;
  • all-to-all 拓扑不适合当前专家布局;
  • 某些专家使用不同的 kernel 或形状;
  • 通信未与计算重叠;
  • 显存压力触发重新分配或同步;
  • decode 阶段批量过小。

这说明“负载均衡损失下降”不是系统性能正常的充分条件。

10.3 训练正常但线上变慢

训练时通常有较大的 token batch,专家可以获得规则矩阵计算;线上 decode 的 token 数更少,通信启动和调度成本占比更高。

此外,训练数据分布和线上请求分布不同。一个在训练集上均衡的路由器,面对代码、数学、长上下文或某个领域的线上流量时,可能产生新的专家热点。因此线上应按请求类型和时间窗口监控路由,而不是只引用训练阶段的均值。

10.4 精度下降但总 FLOPs 没增加

可能原因包括:

  • 容量不足造成 token 丢弃;
  • overflow 路径定义与训练目标不匹配;
  • 辅助损失过强,迫使路由器牺牲任务相关性;
  • top-k 权重组合方式不一致;
  • 训练和推理使用了不同的容量或路由策略;
  • 专家负载均衡按 batch 计算,导致跨 batch 专家专门化不稳定。

性能诊断必须把任务指标、overflow 比例、专家分布和路由熵放在一起分析。


11. 常见误解与反例

误解一:总参数多,所以每个 token 都更贵

反例是 E=64,k=2E=64,k=2。每个 token 只进入两个专家,专家 FFN 的激活计算不是 64 个专家全部执行。但这不等于总延迟只与两个 FFN 成正比,因为还存在路由、通信、padding 和同步等待。

正确表述应区分:

  • 总参数量:决定模型容量、存储和训练状态规模;
  • 激活参数量:决定单 token 访问的专家参数规模;
  • 实际延迟:由计算、通信、调度和最慢设备共同决定。

误解二:把 capacity factor 调大就不会丢 token

如果路由分布持续集中,任何有限容量都可能被耗尽。capacity factor 从 1 增加到 2,只是把每个专家的缓冲上限扩大一倍,同时也可能把 padding 和通信缓冲区扩大一倍。

真正修复超载还需要改变路由分布、专家布局或 token 分配策略。

误解三:负载均衡损失越强越好

辅助损失过强时,路由器可能为了均匀使用专家而降低任务相关性。理想状态不是每个 token 随机平均分配,而是在满足计算约束的前提下,让专家形成有用的专门化。

负载均衡是约束,不是主任务目标的替代品。

误解四:专家可以理解为多个独立模型

MoE 专家通常共享前面的注意力、嵌入和层间上下文,只在某些 FFN 子层分叉。专家并不是各自拥有完整的输入处理和输出头,也不一定对应人类可解释的主题。

一个专家可能在多个领域共享模式,专家之间的功能边界也可能随训练变化。

误解五:MoE 一定降低成本

MoE 可能降低每 token 的专家 FLOPs,但总拥有成本还包括:

  • 更多参数的存储;
  • 多卡或多节点部署;
  • 高速互联网络;
  • 通信缓冲区;
  • 更复杂的 batching 和调度;
  • 低并发 decode 下的利用率损失;
  • 专家副本和容错开销。

如果部署环境只有单卡、带宽有限或请求并发低,稠密模型可能具有更低的实际成本。


12. 生产系统中的容量、权限与成本边界

MoE 不是只存在于模型权重中的数学结构,它会影响整个生产系统。

在模型层,需要保存和校验:

  • 专家数量与专家维度;
  • top-k;
  • 容量计算范围;
  • 路由权重组合规则;
  • overflow 行为;
  • 辅助损失配置;
  • 专家到设备的映射。

如果加载模型时修改了这些配置,可能出现权重形状不匹配、路由行为变化或结果不可比。对于依赖特定版本实现的配置,应以实际模型配置和 Hugging Face Transformers 对应版本文档为准,而不能根据参数名称猜测语义。

在服务层,专家路由会产生额外的资源状态:

  • 哪些专家驻留在哪些设备;
  • 哪些设备正在处理哪些 token;
  • all-to-all 通信是否成功;
  • 哪些 token 因容量被丢弃;
  • 专家副本是否处于同步状态。

如果一台专家设备故障,影响的不只是该设备上的局部计算,还可能阻塞涉及这些专家的请求。恢复策略通常包括重新映射专家、使用副本、降级为备用专家或拒绝部分流量;每种策略都会改变输出一致性和延迟,不能把它当作普通数据并行副本故障处理。

权限方面,路由器不应被视为安全边界。一个 token 被发送给哪个专家,是模型内部的计算决策,不代表该专家具备独立的访问控制能力。若不同租户、数据域或权限级别的输入需要隔离,必须在请求进入模型前完成数据授权和边界控制,不能依赖专家路由来防止信息混用。

成本核算也应至少拆成:

成本=GPU/加速器时间+显存与参数存储+节点间网络+模型加载与副本+监控和故障恢复\text{成本} = \text{GPU/加速器时间} +\text{显存与参数存储} +\text{节点间网络} +\text{模型加载与副本} +\text{监控和故障恢复}

只用“激活参数量”估算云端账单,会漏掉专家权重常驻、网络带宽和低并发利用率等成本。


13. 如何判断 MoE 是否真正带来收益

一个有意义的对比不能只比较参数量或单次前向 FLOPs,而应在相同任务和服务约束下比较:

  • 相同精度目标;
  • 相同输入长度分布;
  • 相同并发度;
  • 相同 batch 或 continuous batching 策略;
  • 相同硬件和网络拓扑;
  • 相同输出长度;
  • 相同故障和副本要求。

至少应测量:

tokens/s首 token 延迟每 token 解码延迟GPU 利用率all-to-all 带宽与等待时间每专家 token 数overflow ratiopadding ratio单位请求成本\begin{aligned} &\text{tokens/s}\\ &\text{首 token 延迟}\\ &\text{每 token 解码延迟}\\ &\text{GPU 利用率}\\ &\text{all-to-all 带宽与等待时间}\\ &\text{每专家 token 数}\\ &\text{overflow ratio}\\ &\text{padding ratio}\\ &\text{单位请求成本} \end{aligned}

例如,MoE 的平均专家 FLOPs 比稠密模型低,但如果最忙设备等待时间、跨节点通信和 padding 很高,端到端 tokens/s 仍然可能更差。反过来,在大 batch、互联带宽高、专家布局合理的环境中,MoE 才更可能把参数规模优势转化为吞吐或能力优势。


14. 总结:必须同时看五个层次

MoE 的核心可以按以下因果链理解:

隐藏状态路由概率Top-k 专家容量分配Dispatch/通信专家计算Combine\text{隐藏状态} \rightarrow \text{路由概率} \rightarrow \text{Top-k 专家} \rightarrow \text{容量分配} \rightarrow \text{Dispatch/通信} \rightarrow \text{专家计算} \rightarrow \text{Combine}

其中任一环节都可能决定最终表现:

  • 路由器决定 token 去哪里;
  • 负载均衡决定专家和设备是否被充分利用;
  • 容量决定超载 token 是否被截断;
  • 通信决定稀疏计算能否跨设备执行;
  • 推理批量和并发决定专家矩阵乘法是否高效。

因此,MoE 不是简单的“多个 FFN 加一个 softmax”。它是一个同时包含离散路由、受限资源分配、分布式通信和条件计算的系统。只有把总参数、激活参数、实际 FLOPs、容量损失、通信量和关键路径延迟分别测量,才能判断一个 MoE 模型究竟是在扩大模型能力,还是仅仅把计算成本从矩阵乘法转移到了调度和网络。


系列导航与关联阅读

官方资料

本文依据研究论文、标准组织与主流框架官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。