返回关卡地图
预判 0 / 3 闯关题:0 / 3 XP 0 / 300

Level 06 | 零基础导学关卡

MoE 路由器

MoE Router

先通过图解和小例子理解原理,再做预测与练习;不懂的地方可以反复尝试,最后把理解写进官方 Notebook。

本课官方 Notebook ↗
06_MoE_Router.ipynb 模型结构MoERouter
Mission 1 概念练习

先让每个 token 给所有专家分配概率

🎯 先猜一猜

router_logits.shape=[8,8],其中第 0 维是 token,第 1 维是专家。

softmax 应该沿哪个维度计算,才能让每个 token 独立给所有专家分配概率?

先猜一个答案,再对照下面的讲解。可以随时修改选择,猜错会得到解释。

先补的知识

  • 输入 hidden_states 的 shape 是 [batch_size, seq_len, hidden_size]。B 是句子数,S 是每句 token 数,H 是每个 token 的特征数。
  • view(-1, hidden_size) 把 B 和 S 合并成 token 总数 T=B×S;它不改变数据,只把 [B,S,H] 看成 [T,H]。
  • gate 是 nn.Linear(hidden_size, num_experts, bias=False),所以每个 token 的 H 个特征会变成 E=num_experts 个打分。
  • logit 是未归一化分数,可以为负,也不要求总和为 1;softmax 才把一行分数变成概率分布。

图解原理

把每个 token 想成一张问诊单,E 位专家都先给它一个匹配分。Router 要沿专家这一维把分数变成概率,因此同一 token 的 E 个概率相加为 1。Notebook 还先把 logits 转成 FP32,因为 softmax 里的指数运算对低精度更敏感。

[B,S,H]句子中的 token 特征 [T,H]T=B×S,排成 token 队列 [T,E]gate 给每位专家打分 [T,E]每行变成概率分布

先认清最后一维

hidden_states[2,4,16],共 8 个 token router_logits[8,8],每行对应 8 位专家 dim=-1在每行的专家列表内归一化 输出 dtypeTODO 先用 FP32,返回前再恢复输入 dtype

一行只属于一个 token

logits
[-1, 2, 0, 1]
转 FP32 softmax
全部为正
一行总和 = 1

不能沿 token 维做 softmax,否则不同 token 会互相争概率,Router 就失去“每个 token 自己选专家”的含义。

变量shape最后一维表示什么本步是否改变 shape
hidden_states[T,H]隐藏特征已在前面展平
router_logits[T,E]专家原始分数gate 把 H 改为 E
routing_probs[T,E]专家全局概率softmax 不改 shape
这里“全局 Softmax”是指对一个 token 的全部 E 位专家归一化,不是把 batch 中所有 token 混在一起归一化。

语法热身:给每位学生的所有兴趣方向分配概率

scores = torch.tensor([
    [1.0, 3.0, -1.0],
    [2.0, 0.0, 1.0],
])

# 每一行是一位学生,最后一维是 3 个方向
probabilities = F.softmax(scores.float(), dim=-1)
print(probabilities.shape)       # [2, 3]
print(probabilities.sum(dim=-1)) # [1, 1]

独立例子如何迁移回 Notebook

  • scores 对应 gate 产出的 router_logits
  • 3 个方向 对应 num_experts 位专家。
  • probabilities 对应 TODO 1 要创建的 routing_probs
  • 保留 .float()dim=-1:前者提高 softmax 稳定性,后者明确沿专家维计算。

巩固一下

输入是 [2,4,16],num_experts=8。展平、gate、softmax 后 routing_probs 的 shape 是什么?

学完这一段,试着做

用一个小动作确认自己理解了;最后再进入官方题目。

Mission 2 概念练习

再用 topk 同时取出权重和专家编号

🎯 先猜一猜

某 token 的概率是 [0.10, 0.55, 0.20, 0.15],top_k=2。

torch.topk 返回的 values 与 indices 分别是什么?

先猜一个答案,再对照下面的讲解。可以随时修改选择,猜错会得到解释。

先补的知识

  • torch.topk(input, k, dim) 会返回两个 Tensor:values 是最大的 k 个值,indices 是这些值原来所在的下标。
  • TODO 2 的输入是完整概率表 routing_probs,不是原始 hidden_states,也不是已经截断的 logits。
  • 每个 token 只保留 K 位专家,因此最后一维会从 E 变成 K。
  • 权重是浮点 Tensor;专家编号是整数 Tensor,后续 torch.where 会用编号决定 token 去哪个 expert。

图解原理

Top-K 不只回答“最高的两个概率是多少”,还必须回答“这两个概率属于哪两位专家”。values 决定加权比例,indices 决定分发位置;少拿任何一张表,后面的 SparseMoEBlock 都无法工作。

同一个操作返回两种信息

输入一行[0.05, 0.62, 0.08, 0.25] values[0.62, 0.25],后续叫 routing_weights indices[1, 3],后续叫 selected_experts

shape 只缩短专家维

routing_probs
[T,E]
每行选 K 个 两个输出
[T,K]

T 个 token 都保留;每个 token 的专家候选从 E 个缩到 K 个。

1input = routing_probs使用 TODO 1 的完整专家概率表
2k = self.top_k不要把 Notebook 测试里的 2 硬编码进模块
3dim = -1每个 token 在自己的专家维中选择
4values, indices = topk(...)按 forward 的返回合同给两个结果命名
不要只写 selected_experts = torch.topk(...)。topk 返回的是具名二元结果;本题需要把“值”和“下标”分别接住。

语法热身:从每场比赛中选出前两名及其赛道号

race_scores = torch.tensor([
    [0.10, 0.55, 0.20, 0.15],
    [0.40, 0.05, 0.35, 0.20],
])

top_scores, lane_ids = torch.topk(
    race_scores, k=2, dim=-1
)
print(top_scores) # 成绩值
print(lane_ids)   # 原来的列下标

独立例子如何迁移回 Notebook

  • race_scores 对应 routing_probs
  • top_scores 对应浮点的 routing_weights
  • lane_ids 对应整数的 selected_experts
  • k=2 要改成模块配置 self.top_k,这样测试换 K 时仍能工作。

巩固一下

为什么 selected_experts 必须保留整数索引,而不能只返回 routing_weights?

学完这一段,试着做

用一个小动作确认自己理解了;最后再进入官方题目。

Mission 3 概念练习

最后重归一化,并读懂专家输出如何汇合

🎯 先猜一猜

routing_weights.shape=[8,2],要让每行两个权重重新相加为 1。

分母最合适的 shape 是什么?

先猜一个答案,再对照下面的讲解。可以随时修改选择,猜错会得到解释。

先补的知识

  • 全量 routing_probs 每行和为 1,但丢掉未入选专家后,Top-K 权重之和通常小于 1。
  • sum(dim=-1, keepdim=True) 会把 [T,K] 的每行求和为 [T,1];保留长度为 1 的维度后可以广播除回 K 个权重。
  • 返回前 routing_weights.to(hidden_states.dtype) 会恢复输入精度;selected_experts 仍保持整数类型。
  • SparseMoEBlock 已经写好:torch.where 找到选择当前专家的 token,专家计算后乘对应权重,再用 += 汇总多位专家的贡献。

图解原理

只留下两位专家后,他们原来的概率可能合计只有 0.8。重归一化就是在入选者内部重新分配 100% 的话语权,同时保持 0.6:0.2 的相对比例不变。这样不同 token 的专家混合尺度一致。

截取后还不是完整混合权重

Top-2
[0.60,0.20]
行和
0.80
分别除 0.80 [0.75,0.25]
和为 1

keepdim 让广播意图清楚

权重[T,K] 不 keepdim 的和[T],不能按预期对齐最后一维 keepdim 的和[T,1],一行的和广播给本行 K 个值
selected_experts决定 token 去哪里 torch.where找 token 行和 Top-K 槽位 unsqueeze(-1)把 [n] 权重变 [n,1] +=把 K 位专家贡献累加
测试检查期望失败时优先检查
Router 权重[B*S,K]topk 的 dim 和 k
每行权重和接近 1TODO 3 的分母与 keepdim
最终 MoE 输出恢复为 [B,S,H]聚合是否保留 token 与 hidden 维

语法热身:把入选评委的票数重新归一化

chosen_votes = torch.tensor([
    [0.60, 0.20],
    [0.30, 0.15],
])

vote_total = chosen_votes.sum(dim=-1, keepdim=True)
normalized_votes = chosen_votes / vote_total
print(vote_total.shape)                 # [2, 1]
print(normalized_votes.sum(dim=-1))     # [1, 1]

独立例子如何迁移回 Notebook

  • chosen_votes 对应 TODO 2 产出的 routing_weights
  • vote_total 对应每个 token 的 Top-K 概率和。
  • TODO 3 可以把求和直接写进除法,但必须保留 dim=-1, keepdim=True
  • 完成后不要删掉 Notebook 已给出的 dtype 恢复与返回语句。

巩固一下

为什么专家聚合使用 final_hidden_states[token_idx] += current_output * current_weight?

学完这一段,试着做

用一个小动作确认自己理解了;最后再进入官方题目。

把理解变成自己的代码

准备好,去官方题目试一试

在本页用小例子建立直觉,再去官方 Notebook 完成实现。先运行你自己的测试,遇到困难时再查看官方提示与参考答案。

前往本课官方 Notebook ↗

这节课会遇到的代码对象

TopKRouter · forward · SparseMoEBlock

先找题目里的输入、输出与 TODO,再把本课的手算过程对应进去;以官方题目中的函数说明和测试为准。

练习来源:Datawhale 官方仓库 · 518cc45。这里的入口直接打开官方版本,不读取或分享你的本地 Notebook。

完成 3 道闯关题后,本关即算完成;作业 checklist 用来辅助你回 notebook 练习。
进度只保存在当前浏览器 localStorage,分享 HTML 不会带走你的记录。
上一关:L05 下一关:L07