深蓝黑背景封面。左侧是一幅扇形路由示意图:三个 token 卡片汇入中间的「门控网络 G(x)=softmax」圆角框,再分出八条连线指向右侧纵向排列的 E1 至 E8 八个专家小卡片;其中 E2 与 E5 两张卡片以青色高亮、连线加粗,表示 Top-2 被选中,其余专家为灰色细虚线表示未激活。底部一句话「Top-2 路由:参数 ×8,单 token 只算 2 个专家 —— 稀疏激活」。右侧大标题「MoE 混合专家的门控网络是怎么路由的」,副标题「Top-K 打分路由 + 负载均衡,稀疏激活的架构设计」,并附注「不激活的专家不花算力 —— 容量与成本解耦」。左上标注「面试经典 · 模型架构」,右上圆角胶囊标注「Day 09 · 11-26」。 图 1:本文封面,Top-2 路由下只有被选中的专家参与计算。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料,与任何公司的当前招聘无关。MoE(Mixture-of-Experts,混合专家)是大模型架构方向的高频考点:模型总参数可以翻到几千亿甚至上万亿,单个 token 的计算量却不能同比例膨胀,靠的就是门控网络的路由机制。

基本思路:把一层换成一组专家,每次只叫醒少数几个。

稠密模型里,每个 token 都要流过网络的全部参数。MoE 的做法是把 Transformer 块里的前馈层(FFN)替换成 N 个结构相同、参数各自独立的「专家」子网络,再配一个可训练的门控网络。每个 token 进来,门控决定把它交给哪几个专家处理,其余专家这一轮完全不参与计算。2020 年的 GShard 论文用这套稀疏门控结构把多语言翻译模型做到了六千亿参数规模,2021 年的 Switch Transformer 论文则把路由简化到每个 token 只选 1 个专家,显著降低了通信与计算开销。

门控怎么打分:一次线性变换加 softmax,再取 Top-K。

设 token 的隐状态为 h,门控网络本质上是一个权重矩阵 W。第一步算 W·h,得到 N 个专家的原始分数;第二步过 softmax,把分数变成概率分布 G(h);第三步取分数最高的 Top-K 个专家(K 通常取 1 或 2),把 h 送进去;最后输出是选中专家输出的加权和:输出 = Σ G(h)ᵢ·Eᵢ(h),求和只覆盖被选中的专家。注意这个加权系数本身就来自 softmax,所以门控不是不可微的硬开关,而是一条可微的加权路径,梯度可以沿着被选中的专家正常回传,门控矩阵 W 也能跟着训练。

稀疏激活的账本:算力按激活专家算,显存按总参数算。

理解 MoE 一定要分清两本账。单 token 的计算量约等于只经过 K 个专家的小模型,所以训练和推理的浮点运算量几乎不随总参数膨胀——这是它「参数翻几倍、成本几乎不变」的来源。但全部 N 个专家的参数都必须常驻显存,显存占用按总参数算。一句话概括:MoE 用显存换算力,把「模型容量」和「计算成本」解耦了。部署时的专家并行(把不同专家放到不同卡上)也是顺着这本账来的。

负载均衡损失:防止所有 token 挤向同一个专家。

训练初期如果某个专家碰巧学得略好,门控就倾向把更多 token 派给它;它拿到更多梯度、变得更强,形成正反馈,最终少数专家过载、其余专家闲置——这叫「专家塌陷」。塌陷不只是浪费参数:工程上每个设备能处理的 token 有容量上限,超额的 token 会被丢弃或顺延,直接伤效果。对策有两层:训练时加一项负载均衡损失,惩罚 token 在专家间的分布不均,引导门控把流量摊平;路由实现上给每个专家设容量上限,满了就派给次优专家。

用代码把路由和塌陷跑一遍。

下面这段程序用 Node.js 22.15.0 运行(node moe.js),模拟 8 个专家、800 个 token 的 Top-2 路由:先纯按门控分数派单,再给每个专家加 1.1 倍平均容量的上限。

let s = 42; // 固定种子,结果可复现
const rand = () => (s = (s * 1103515245 + 12345) % 2147483648) / 2147483648;
const E = 8, DIM = 4, TOKENS = 800, K = 2;
const W = Array.from({length: E}, () => Array.from({length: DIM}, () => rand() - 0.5));
const tokens = Array.from({length: TOKENS}, () => Array.from({length: DIM}, () => rand() - 0.5));
function softmax(v) {
  const m = Math.max(...v);
  const e = v.map(x => Math.exp(x - m));
  const sum = e.reduce((a, b) => a + b, 0);
  return e.map(x => x / sum);
}
const topK = (scores, k) =>
  scores.map((x, i) => [x, i]).sort((a, b) => b[0] - a[0]).slice(0, k).map(p => p[1]);
const scoreAll = h => softmax(W.map(w => w.reduce((a, x, j) => a + x * h[j], 0)));

const free = new Array(E).fill(0);
for (const h of tokens) for (const e of topK(scoreAll(h), K)) free[e]++;
console.log('纯分数路由(无约束):', free.join(' '));

const cap = Math.ceil(TOKENS * K / E * 1.1);
const capped = new Array(E).fill(0);
for (const h of tokens) {
  const order = scoreAll(h).map((x, i) => [x, i]).sort((a, b) => b[0] - a[0]).map(p => p[1]);
  let taken = 0;
  for (const e of order) { if (capped[e] < cap) { capped[e]++; if (++taken === K) break; } }
}
console.log('容量约束路由(均衡后):', capped.join(' '));

实测输出:

纯分数路由(无约束): 163 126 304 125 240 200 191 251
最忙/最闲 = 2.4 倍
容量约束路由(均衡后): 199 144 221 170 221 221 203 221
最忙/最闲 = 1.5 倍

即使门控权重是随机初始化的,纯分数路由下最忙专家收到的 token 也是最闲专家的 2.4 倍——真实训练里这个偏斜会被梯度正反馈越放越大。加上容量约束后分布明显摊平,这正是负载均衡项要达到的效果方向。

深蓝黑背景概念图。上半部分是一条横向四段流水线:输入 token 隐状态 h,进入门控网络 G=softmax(W·h) 打分(框内画出八个专家的分数小横条,E2、E5 两条青色最长),再经 Top-2 选择框「选中 E2、E5」,最后输出框给出公式「输出 = 0.62·E2(h) + 0.38·E5(h)」。下半部分左右两张对比卡:左卡「不加约束:专家塌陷」,柱状图中 E1 一根柱子极高、其余专家贴近零,注「所有 token 挤向同一个专家,其他专家白占显存」;右卡「加负载均衡损失之后」,八根柱子高度接近,注「损失惩罚分布不均,token 被摊到各专家」。底部总结条:「稀疏激活的意义:总参数翻 8 倍,单 token 计算量约等于稠密模型 —— 用显存换算力」。 图 2:门控打分、Top-K 选择、加权输出的完整流水线,以及负载均衡前后的对比。

面试现场的答题结构。

先一句话给出定义——MoE 用稀疏激活把模型容量与计算成本解耦;再写门控公式,讲清线性打分、softmax、Top-K、加权和四步;然后主动补负载均衡:塌陷的成因是正反馈,对策是均衡损失加容量上限;最后点一句账本:算力按激活专家算,显存按总参数算。答出公式是及格线,能主动讲塌陷与均衡、说清两本账才是加分项。你在训练或部署中见过专家塌陷吗?是靠均衡损失还是硬路由容量解决的?欢迎聊聊。