基本原理
核心思想:按需调用,而非“全员出动”
传统的密集模型(Dense Model)处理任何输入时,都会激活整个神经网络的所有参数,好比每次回答问题都要动用全公司所有员工。
而MoE模型的做法是:根据输入内容,只激活最擅长处理这类问题的少数“专家”,其他专家保持静默。这就像一家医院,病人挂什么科,就由相应科室的医生接诊,而不是所有医生一起上。
两大核心组件
1. 稀疏MoE层(Sparse MoE Layers)
- 在Transformer架构中,原本的全连接前馈网络层被替换为MoE层。
- 每个MoE层里包含多个“专家”(每个专家本质上是一个小型神经网络)。
- 关键点是“稀疏”——对于每个输入,只有一小部分专家被激活(比如8个专家里只选2个),从而大幅节省计算资源。
2. 门控网络/路由器(Gate Network / Router)
- 它的任务是:决定每个输入token(词元)该交给哪个(或哪几个)专家处理。
- 路由器会为每个专家计算一个“适配分数”,然后选出分数最高的Top-K个专家来实际处理该token,确保“专业的人做专业的事”。
带来的好处
| 维度 | 效果 |
|---|---|
| 训练速度 | 更快,因为每次更新只需计算部分专家,而非整个网络 |
| 推理效率 | 更高,推理时只跑少量专家,计算量远小于同参数量的密集模型 |
| 模型容量 | 可以在不显著增加计算成本的前提下,把模型做得非常大(比如GPT-4、Gemini等主流大模型都在使用MoE) |
一个直观的类比
想象你有一本百科全书,每当你问一个问题,MoE就像一个聪明的图书管理员——他不会把整本书从头翻到尾,而是根据你的问题,直接抽取出相关的几个章节递给你。既快又准,还省力。
这个过程是怎样的
为了让你直观地“看见”整个过程,我们不妨跟随一个具体的句子,一步步走过MoE模型内部。假设我们要翻译或理解这句话:
“猫喜欢吃鱼。”
这个过程可以分为4个关键步骤,从输入到输出依次展开:
第1步:输入分词(Token化)
模型首先把句子拆成最小单元(Token):
["猫", "喜", "欢", "吃", "鱼", "。"]
此时,每个Token都是一串数字编码(向量),代表它的初始语义。
第2步:经过自注意力层(Transformer标准流程)
这些Token向量先进入Transformer的自注意力层(Self-Attention)。
- 这一步不涉及专家,是所有MoE模型都有的通用步骤。
- 它的作用是让每个Token“互相看一眼”,理解上下文。比如“吃”这个字会注意到“猫”和“鱼”,知道是“猫吃鱼”而不是“鱼吃猫”。
经过这一步,每个Token的向量都融合了上下文信息,变成了带有语境的新向量。
第3步:进入MoE层(关键步骤)——路由器决策 + 专家计算
这是MoE的核心环节。每个Token(此时已带上下文)会独立地进入MoE层,经历以下子步骤:
3.1 路由器(Router)打分
路由器是一个小型全连接网络,它为当前Token计算一个分数向量,长度等于专家的数量(假设有4个专家:E1, E2, E3, E4)。
假设当前Token是 “吃”,路由器给出的分数可能是:
E1: 0.1 E2: 0.8 E3: 0.05 E4: 0.05
分数代表“这个Token适合交给哪位专家”。
3.2 选择Top-K专家(稀疏激活)
假设模型设定 K=2(即每次选2个专家)。那么路由器选出分数最高的两位:
选中 E2(0.8)和 E1(0.1),E3和E4被直接忽略(不激活,不消耗计算)。
3.3 专家并行处理
- E2专家(可能擅长处理“动作/动词”)对“吃”的向量进行自己的前馈计算,输出一个新向量。
- E1专家(可能擅长处理“食物/名词”)也对“吃”的向量进行自己的计算,输出另一个新向量。
两个专家同时独立工作,互不干扰。
3.4 加权融合
路由器不只看排名,还会将分数作为权重,把两个专家的输出加权求和:
最终输出 = 0.8 × (E2的输出) + 0.1 × (E1的输出)
(注意:分数通常还会经过Softmax归一化,这里简化为直观比例)
第4步:依次处理所有Token + 多层叠加
- 上面的第3步,对句子中的每个Token(“猫”“喜”“欢”“吃”“鱼”“。”)都独立执行一遍。
- 注意:不同Token选中的专家可能完全不同。比如“鱼”可能选中E1(名词专家)和E4(生物专家),而“吃”选的是E2和E1。
- 整个Transformer模型通常有数十层,每一层都包含上述MoE结构。Token经过第1层MoE后,再进入第2层的自注意力,再进入第2层的MoE……逐层递进,直到最后输出结果。
整个过程的时间线图示
text
输入句子: "猫喜欢吃鱼。"
│
▼
[分词] → ["猫","喜","欢","吃","鱼","。" ]
│
▼
[自注意力层] → 每个Token融合上下文("吃"知道主语是"猫",宾语是"鱼")
│
▼
┌─────────────── MoE层(对每个Token独立执行)───────────────┐
│ │
│ 对Token="吃": 路由器打分 → 选E2(0.8)和E1(0.1) │
│ E2和E1并行计算 → 加权求和 → 输出新向量 │
│ │
│ 对Token="鱼": 路由器打分 → 选E1(0.7)和E4(0.2) │
│ E1和E4并行计算 → 加权求和 → 输出新向量 │
│ │
│ ... 其他Token同理 ... │
└─────────────────────────────────────────────────────────────┘
│
▼
[重复多层] → 每层都做上述自注意力 + MoE
│
▼
[最终输出] → 预测下一个词 / 翻译结果 / 文本理解
两个容易被忽略的关键细节
| 细节 | 说明 |
|---|---|
| 每个Token独立选专家 | 同一个句子中,“猫”可能选专家A,“吃”可能选专家B,它们是独立决策的,不是整句话统一选一批专家。 |
| 负载均衡(Load Balancing) | 训练时,模型会额外加一个损失函数,防止所有Token都挤去选E2,而E3、E4长期闲置。这就像排班一样,要保证每个专家都有活干。 |
存在的问题
核心矛盾:省了“算力”,但省不了“显存”
想象你开了一家大型专科医院(MoE模型),有100个不同科室的专家(专家总数)。
- 推理(看病)时:每个病人只需挂1-2个相关科室的号(只激活部分专家),计算量确实小,看病速度快。
- 但问题是:你不能把其他98个科室的大楼拆掉。整家医院(所有100个专家)必须完整地存在硬盘和内存里,随时待命,因为下一个病人可能挂的是骨科或眼科。
映射到模型:推理时虽然只用了2个专家,但所有专家的参数都必须加载到显存(VRAM)中。所以MoE模型的显存占用,几乎等于同参数量的密集模型(Dense Model),而不是按激活比例缩小。
微调(Fine-tuning)为什么更难?
普通密集模型微调,只是让所有参数统一做小幅调整。
但MoE微调时,模型需要同时做两件相互矛盾的事:
- 路由器(Router)要重新学习:哪些专家适合新任务,门控权重需要大范围调整。
- 专家本身也要调整:每个专家内部的知识要适配新数据。
问题是,路由器的决策直接决定专家能否收到梯度信号。如果路由器一开始把某些专家“判了死刑”(分数极低),这些专家就得不到训练,形成专家闲置(Expert Dropping)。所以训练时必须额外加负载均衡损失(Load Balancing Loss),强行让所有专家都有活干——这增加了调参的难度和稳定性风险。
两个关键参数(你给的英文里提到了)
| 参数 | 含义 | 影响 |
|---|---|---|
| num_local_experts(专家总数) | 整个模型里总共有多少个专家,比如Mixtral 8x7B里有8个。 | 决定显存上限。这个数越大,模型总参数量越大,加载到显存时占用的VRAM就越高。 |
| num_experts_per_tok(每个Token激活的专家数) | 每个Token在每一层实际调用几个专家来处理,比如Mixtral里是2个。 | 决定计算速度和精度之间的权衡。 • 调大这个值(比如从2变4):效果可能略有提升,但收益递减(边际效益变小),计算量却翻倍。 • 调小这个值(比如1):推理和训练飞快,但可能因专家知识不足而损失精度。 |
一个直观的总结对比
| 维度 | 密集模型(Dense) | 混合专家模型(MoE) |
|---|---|---|
| 显存占用 | 等于总参数量 | 也等于总参数量(所有专家必须加载) |
| 推理计算量 | 100%参数都算 | 只算 激活专家数/专家总数 的比例(比如2/8=25%) |
| 微调难度 | 相对简单,统一调参 | 困难,需平衡路由器 + 专家 + 负载均衡 |
| 参数效率 | 低(增大模型,计算成本线性增长) | 高(增大专家数,计算成本几乎不变,但显存成本线性增长) |
一句话记住MoE的取舍
MoE用“显存换计算”:你把所有专家都装在显存里(贵),但每次推理只算一小部分(快)。适合显存大、推理量大、且需要超大规模参数的场景。
推荐阅读:
Fine-tune Mistral-7b with Direct Preference Optimization – Maxime Labonne