核心摘要
**混合专家模型(Mixture of Experts,MoE)**是一种条件计算架构。路由器为每个 Token 表示打分,选择一个或多个专家子网络,把 Token 状态分发给专家计算,再合并专家输出。它能在不让每个 Token 都经过全部专家的前提下扩大总参数容量。
稀疏激活不等于免费算力。生产 MoE 仍要承担共享层计算、专家权重存储、路由、Token 排列、内存搬运、集合通信、专家负载不均和小矩阵计算。因此:
总参数量 != 激活参数量 != FLOPs != 延迟 != 质量
真正有用的工程问题不是“MoE 是否普遍更高效”,而是某个确定版本在固定工作负载和硬件契约下,能否提供更好的可接受质量、延迟、吞吐和成本。
目录
- MoE 改变了 Transformer 的什么
- 批量路由的完整数据路径
- 总参数与激活参数的真实成本
- 训练中的负载平衡与有效路由
- 推理部署中的内存通信与算子
- Prefill 与 Decode 压力不同
- 公开架构证据能证明什么
- 可复现的 MoE 评测契约
- 故障模式与诊断
- 常见问题
- 总结
MoE 改变了 Transformer 的什么
1991 年的自适应局部专家混合使用多个独立网络与可学习门控,把训练样本划分给不同子任务。现代稀疏语言模型把同一种条件计算思想扩展到了更大的系统。
一种常见 Transformer 设计会把部分稠密前馈网络(FFN)替换为 MoE 块:
Token 状态
-> 归一化与注意力
-> 残差路径
-> MoE 路由器
-> 被选中的专家 FFN
-> 加权合并
-> 残差路径
注意力、Embedding、归一化、输出头以及部分 FFN 仍可能保持稠密或共享。不同架构还会选择不同的 MoE 层频率、是否设置始终激活的共享专家、路由专家数量以及每次选择的专家数量。
专家通常是拥有独立权重的 FFN,但它不会自动成为人类可读的“领域专家”。路由器可能形成稳定偏好,却未必得到“Python”“生物”或“语法”等整齐类别。语义专门化必须通过路由统计、受控干预和质量测量证明。
批量路由的完整数据路径
单 Token 循环隐藏了 MoE 最困难的部分。真实训练与推理会处理一批 Token 状态:
- 打分:路由器把每个 Token 状态映射为专家 Logits。
- 选择:路由策略选择 Top-k 专家、专家组或其他稀疏分配。
- 归一化:按架构规则把选中分数变成组合权重。
- 处理容量:实现决定分配是填充、丢弃、改派,还是用变长计算执行。
- 重排:按目标专家聚合 Token 状态。
- 分发:若专家位于其他设备,集合通信把 Token 状态发送给专家所有者。
- 计算:使用分组或批量矩阵乘执行本地专家。
- 合并与还原:专家输出加权、返回,并恢复原始 Token 顺序。
Token 状态 x_t 的概念公式是:
MoE(x_t) = 对选中专家 i 求和:g_i(x_t) * E_i(x_t)
这个公式没有描述容量、设备放置、通信、填充、精度和算子调度。它表达模型语义,而不是生产实现。
2017 年的稀疏门控 MoE 论文证明了可学习门控能够激活大量 FFN 专家的稀疏组合。Switch Transformer进一步把单专家路由作为一种特定简化。两项工作都不能证明 Top-1 或 Top-2 是通用默认值。
总参数与激活参数的真实成本
必须把四个量分开报告:
| 指标 | 描述内容 | 不能证明什么 |
|---|---|---|
| 总参数量 | 包括未激活专家在内的全部模型权重 | 单 Token 算术量、延迟或质量 |
| 激活参数量 | 一个 Token 选中路径使用的权重 | 实际 FLOPs、内存访问或稠密模型等价关系 |
| FLOPs | 在给定序列与批次形态下的算术量 | 算子利用率、通信或真实延迟 |
| 常驻内存 | 权重、KV Cache、激活、工作区与运行时状态 | 工作负载能否达到延迟或吞吐目标 |
激活参数量可以描述架构,但不是性能模型。共享注意力和稠密层仍要执行;专家权重必须常驻、分片、流式加载或卸载;路由引入数据搬运与同步;小而不均匀的专家批次可能无法充分使用加速器。
对一个确定版本和工作负载,应拆解:
请求成本 =
共享稠密计算
+ 路由专家计算
+ 路由与重排
+ 本地内存访问
+ 专家并行通信
+ 同步与填充
+ 运行时开销
因此,“激活 13B 参数”不能证明“拥有 13B 稠密模型的速度”,总参数量也不能证明与某个稠密模型拥有等价质量。
训练中的负载平衡与有效路由
路由器与专家需要联合训练,但离散选择和需求倾斜会产生耦合问题:
- 少数专家可能接收大部分 Token;
- 低使用率专家学习过慢;
- 过载设备决定整个训练步耗时;
- 容量上限可能丢弃分配或浪费填充;
- 过强的均衡目标可能干扰语言模型目标;
- 路由分数接近时,低精度计算可能改变专家选择。
辅助负载均衡损失是一种方案,不是定律。Switch Transformer 使用均衡和容量机制,而 DeepSeek-V3描述了动态专家偏置,希望不用传统辅助损失也能控制负载。生产框架还提供微批次、序列级或全局辅助损失、Sinkhorn 路由、动态偏置和不均衡等多种选择。
至少跟踪:
每个专家与每台设备的 Token 数
每个专家获得的路由概率质量
负载变异系数与峰值/均值
溢出、丢弃、改派与填充率
专家批次大小分布
路由熵与分配变化率
语言模型损失与分片任务质量
通信时间与专家计算时间
均匀路由不是最终目标。目标是在不出现死亡专家或失控热点的条件下,得到稳定质量和高效执行。
推理部署中的内存通信与算子
专家并行不等于张量并行
- **专家并行(EP)**把不同专家放在不同 Worker 上,并把 Token 状态发给持有所选专家的 Worker。
- **张量并行(TP)**切分单层内部的张量并同步局部结果。
- **流水线并行(PP)**把不同层区间分配给不同 Stage。
- **数据并行(DP)**在不同数据分片上复制模型执行。
大型部署会组合这些维度。最佳映射取决于专家数量、隐藏维度、序列长度、拓扑、内存和流量,不能只看总参数量。
分发是系统操作
采用 EP 时,一层 MoE 通常执行类似 All-to-All 的分发、本地专家计算和返回/合并通信。PyTorch 的大规模 MoE 实践与固定版本的 Megatron-Core 0.16 MoE 指南都把这条数据搬运路径视为核心问题。
运行时可能使用无丢弃变长执行、容量填充、热点专家复制、Grouped GEMM、重排融合、通信重叠或拓扑感知路由。这些都是实现选择。旧页面把“推理框架普遍通过 Token Dropping 防止崩溃”当成定律,混淆了容量训练选项与所有推理系统。
当前 vLLM 专家并行部署指南也把 Backend 选择、Expert Parallel Load Balancing、额外内存、网络配置和 Benchmark 作为不同部署决策。功能可用性与性能仍取决于 Release、模型、拓扑和硬件。
本地运行需要实测内存计划
量化能减少权重存储,但本地可运行性还取决于:
- 精确 Checkpoint 与量化格式;
- 运行时元数据和临时工作区;
- 上下文长度、批次和并发对应的 KV Cache;
- CPU、GPU 或统一内存放置;
- 内存带宽与卸载流量;
- Prompt 处理与 Decode 目标。
固定 RAM 数字不能证明一个 MoE 模型“流畅运行”。应公布 Checkpoint 摘要、运行时版本、上下文、并发、Token 速率、延迟分位数、峰值内存和质量检查。
Prefill 与 Decode 压力不同
Prefill 会一起处理大量 Prompt Token,可能形成更大的专家批次并提高 Grouped GEMM 效率,但长 Prompt 也会增加注意力、激活、KV Cache、分发和网络流量。
Decode 通常让每个活跃序列每步前进一个 Token。连续批处理可以聚合工作,但专家批次仍可能更小、更不均匀,算子启动、同步、内存带宽和互联延迟可能主导性能。
两条路径都要报告:
Prefill:输入 Token 吞吐、首 Token 延迟、专家批次大小
Decode:输出 Token 吞吐、单输出 Token 时间、Token 间延迟
共同指标:p50/p95/p99 延迟、负载倾斜、通信占比、峰值内存
提高 Prefill 吞吐的优化可能对 Decode 无效,甚至让它变差。
公开架构证据能证明什么
公开架构只能作为有边界的实例:
- Mixtral 8x7B报告每层 8 个 FFN 专家、每 Token 选择 2 个、总参数 47B、激活参数 13B。这些数字只属于该 Checkpoint,不能迁移到所有 MoE。
- DeepSeekMoE在其评测设计中研究细粒度路由专家和始终激活的共享专家,以降低冗余并提高专门化。
- DeepSeek-V3报告总参数 671B、每 Token 激活 37B,并描述无辅助损失负载均衡、节点受限路由、训练设置中的无 Token 丢弃,以及分开的 Prefill 与 Decode 部署策略。
- Switch Transformer 在自己的训练配置中评测 Top-1 路由与容量控制。
不能根据泄露信息或重复传闻推断闭源模型内部结构。OpenAI 公开的 GPT-4 材料没有披露专家数量,也没有证实旧页面曾展示的参数计算,因此这些说法不能作为 MoE 架构证据。
可复现的 MoE 评测契约
比较稠密与稀疏候选前,应固定 Tokenizer 与精度、工作负载 Trace、硬件拓扑、门禁 Revision 和指标边界。下面的零依赖 Go Fixture 会在比较有效结果吞吐前,拒绝未配对证据、质量或安全门禁失败和非法计数。
package main
import (
"fmt"
"math"
"strings"
)
type Trial struct {
Name string
ArtifactRevision string
RuntimeRevision string
TokenizerRevision string
Precision string
WorkloadRevision string
HardwareRevision string
GateRevision string
AcceptedOutputs int
CompletedRequests int
TotalRequests int
DurationSeconds float64
TTFTP95MS float64
TPOTP95MS float64
PeakMemoryGB float64
CommunicationShare float64
CriticalFailures int
}
func validateTrial(trial Trial, minAcceptance, maxTTFT, maxTPOT float64) error {
if minAcceptance < 0 || minAcceptance > 1 || maxTTFT <= 0 || maxTPOT <= 0 {
return fmt.Errorf("release gate is invalid")
}
identity := []string{
trial.Name, trial.ArtifactRevision, trial.RuntimeRevision,
trial.TokenizerRevision, trial.Precision, trial.WorkloadRevision,
trial.HardwareRevision, trial.GateRevision,
}
for _, value := range identity {
if strings.TrimSpace(value) == "" {
return fmt.Errorf("%s has incomplete identity", trial.Name)
}
}
if trial.TotalRequests <= 0 ||
trial.AcceptedOutputs < 0 ||
trial.AcceptedOutputs > trial.CompletedRequests ||
trial.CompletedRequests > trial.TotalRequests ||
trial.CriticalFailures < 0 ||
trial.DurationSeconds <= 0 {
return fmt.Errorf("%s has invalid counters", trial.Name)
}
metrics := []float64{
trial.DurationSeconds, trial.TTFTP95MS, trial.TPOTP95MS,
trial.PeakMemoryGB, trial.CommunicationShare,
}
for _, value := range metrics {
if math.IsNaN(value) || math.IsInf(value, 0) || value < 0 {
return fmt.Errorf("%s has an invalid metric", trial.Name)
}
}
if trial.CommunicationShare > 1 {
return fmt.Errorf("%s has an invalid communication share", trial.Name)
}
acceptance := float64(trial.AcceptedOutputs) / float64(trial.TotalRequests)
if trial.CriticalFailures > 0 || acceptance < minAcceptance ||
trial.TTFTP95MS > maxTTFT || trial.TPOTP95MS > maxTPOT {
return fmt.Errorf("%s failed a release gate", trial.Name)
}
return nil
}
func compare(dense, moe Trial) (string, float64, float64, error) {
paired := dense.TokenizerRevision == moe.TokenizerRevision &&
dense.Precision == moe.Precision &&
dense.WorkloadRevision == moe.WorkloadRevision &&
dense.HardwareRevision == moe.HardwareRevision &&
dense.GateRevision == moe.GateRevision
if !paired {
return "", 0, 0, fmt.Errorf("trials are not paired")
}
for _, trial := range []Trial{dense, moe} {
if err := validateTrial(trial, 0.85, 900, 45); err != nil {
return "", 0, 0, err
}
}
denseGoodput := float64(dense.AcceptedOutputs) / dense.DurationSeconds
moeGoodput := float64(moe.AcceptedOutputs) / moe.DurationSeconds
winner := dense.Name
if moeGoodput > denseGoodput {
winner = moe.Name
}
return winner, denseGoodput, moeGoodput, nil
}
func main() {
common := Trial{
TokenizerRevision: "tokenizer/12", Precision: "bf16",
WorkloadRevision: "traffic/44", HardwareRevision: "cluster/7",
GateRevision: "quality-policy/9", TotalRequests: 100,
CompletedRequests: 100, DurationSeconds: 8,
}
dense := common
dense.Name = "dense"
dense.ArtifactRevision = "dense/17"
dense.RuntimeRevision = "runtime/31"
dense.AcceptedOutputs = 88
dense.TTFTP95MS = 640
dense.TPOTP95MS = 31
dense.PeakMemoryGB = 62
moe := common
moe.Name = "moe"
moe.ArtifactRevision = "moe/22"
moe.RuntimeRevision = "runtime/31"
moe.AcceptedOutputs = 92
moe.TTFTP95MS = 710
moe.TPOTP95MS = 34
moe.PeakMemoryGB = 78
moe.CommunicationShare = 0.18
winner, denseGoodput, moeGoodput, err := compare(dense, moe)
if err != nil {
panic(err)
}
fmt.Printf(
"dense_goodput=%.2f moe_goodput=%.2f winner_in_fixture=%s\n",
denseGoodput,
moeGoodput,
winner,
)
}
预期输出:dense_goodput=11.00 moe_goodput=11.50 winner_in_fixture=moe。
两个制品及其执行路径可以不同,因为这正是生产选型要比较的对象;共享字段则证明工作负载与验收契约已经配对。胜者只代表当前 Fixture,不构成「MoE 架构普遍更快」的因果结论。有效 Benchmark 应计入被拒绝输出和失败请求,而不只是原始 Tokens/s。
故障模式与诊断
| 现象 | 需要采集的证据 | 可能原因 |
|---|---|---|
| 少数专家占据大部分流量 | Token 数、路由概率、熵、分片路由 | 路由坍塌、真实流量倾斜、均衡控制过弱或过强 |
| 尾延迟上升 | 专家队列、设备负载、All-to-All 时间 | 热点专家、跨拓扑通信、小 GEMM、同步 |
| 训练质量回退 | 任务分片、路由变化、丢弃率、各损失项 | 溢出、路由不稳定、均衡目标干扰 |
| 增加专家没有收益 | 等算力质量曲线、利用率、内存 | 容量收益递减、数据不足、通信开销 |
| 本地推理缓慢 | 带宽、卸载字节、缺页、峰值内存 | 权重无法常驻、KV Cache 压力、算子低效 |
| 量化后输出变化 | 路由一致率、分片质量、校准 | 路由分数扰动、专家权重误差、运行时不一致 |
不能只用平均利用率诊断 MoE。应把模型质量与路由、专家批次、集合通信、内存和请求级延迟关联起来。
常见问题
MoE 一定比稠密模型快吗
不一定。稀疏专家的算术量可能低于相同总参数量的稠密设计,但通信、内存访问、小专家批次、负载不均和共享层可能成为主导。只有目标工作负载下的端到端 Benchmark 能回答。
每个 MoE 都必须为每个 Token 路由两个专家吗
不是。公开系统使用不同 Top-k、共享专家、分组路由和均衡策略。Top-k 是版本化架构的一部分,不是行业常量。
专家是彼此独立的模型吗
在 Transformer MoE 中通常不是。专家一般是部分层中的 FFN 模块,与其他专家共享模型的其余部分,并在同一次前向传播中共同产生结果。
专家容量是否意味着推理必须丢弃 Token
不是。容量和 Token Dropping 是实现选择。无丢弃算子和变长专家批次都已存在,一些架构也明确避免丢弃。应记录实际运行时行为。
激活参数量能比较模型质量吗
不能。质量还取决于训练数据、优化、架构、总容量、激活路径、Tokenizer、后训练和评测协议。应直接在代表性分片上比较质量。
总结
MoE 把部分稠密计算改为条件计算:
路由
-> 分组与分发
-> 执行选中专家
-> 合并并恢复顺序
它的价值是扩大模型容量而不让每个 Token 激活全部专家;代价则体现在权重常驻、路由稳定性、专家负载、集合通信、算子效率与运维复杂度。总参数、激活参数、FLOPs、内存、延迟、吞吐、成本和质量必须分别测量。