返回关卡地图
预判 0 / 2 闯关题:0 / 2 XP 0 / 200

Level 06 | 零基础导学关卡

MoE Router

每个 Mission 先猜一猜(押个假设解锁讲解)→ 看图解原理 → 做巩固题 → 回 notebook 写代码。带着问题学,比直接读记得牢。

06_MoE_Router.ipynb MoERouterTop-k
先猜一猜
图解原理
巩固 + 作业
expert_ids, weights = topk(softmax(router(x)), k)
Mission 1 TODO 1 / TODO 2

Router 打分:每个 token 先看所有专家,再选 Top-K

🎯 先猜一猜(下注解锁)

Router 有多种等价写法。本关 notebook 明确要求先得到完整概率表,再从概率里取 Top-K。

先判断:为什么 notebook 要先对全量 router_logits 做 softmax?

先押一个假设,下面的讲解就会解锁。猜错完全没关系——带着疑问读,记得最牢。

先补的知识

  • MoE 的目标是“大容量、低激活”:模型有很多专家,但每个 token 只去少数几个专家那里计算。
  • Router 是一个很小的线性层,把每个 token 的 hidden state 映射成 num_experts 个 logit 分数。
  • 本 notebook 的实现路径是:先对所有专家做 softmax 得到完整概率表,再从概率里取 Top-K,最后对 Top-K 权重重归一化。
  • Router 不负责真正执行专家网络,它只输出两张清单:选了哪些专家 selected_experts,以及每个专家拿多少权重 routing_weights。

图解原理

把 Router 想成分诊台。每个 token 先拿自己的特征名片给所有专家打分,softmax 把分数变成全局概率,再只保留最适合的 K 个专家。Notebook 的 TODO 1/2 本质就是这两步。初学时先盯住一行:一行代表一个 token,这一行里所有列代表它对所有专家的偏好。

0. 先读 forward 的输入输出合同

TopKRouter.forward 的输入仍是三维句子张量,但输出是按 token 排好的两张清单。先把这个合同读清楚,后面代码就不会绕。

输入hidden_states [B,S,H] 输出权重routing_weights [B*S,K] 输出专家selected_experts [B*S,K]
TODO 1 / 2 只负责“选专家”

先算每个 token 对所有专家的概率,再保留 Top-K。真正执行专家网络是后面的 SparseMoEBlock,不要在 Router 里混进去。

TODO 1softmax 概率表 TODO 2topk 权重 + 专家 id
主线:先把 [B,S,H] 变成 token 队列,再沿专家维做 softmax/topk
hidden_states[B,S,H] flatten[B*S,H] gaterouter_logits [B*S,E] softmaxrouting_probs [B*S,E] topkweights / experts [B*S,K]

这里的关键不是画很多线,而是抓住最后一维的含义:展平后每一行是一枚 token,最后一维是全部专家。

1. 每一行是一枚 token

如果有 4 个专家,一行 logits 就是这个 token 对 4 个专家的偏好分数。softmax 和 topk 都只在这一行内部做。

E0
1.2
E1
0.1
E2
2.0
E3
-0.5
2. dim=-1 就是专家维

不要把不同 token 混在一起归一化。Router 要让“同一个 token 的所有专家概率”加起来等于 1。

正确F.softmax(router_logits.float(), dim=-1) 读法每个 token 自己在专家列表里分配概率
topk 返回两张表:values 是权重,indices 是专家编号
3. 值和编号要一起拿
routing_probs 一行[0.05, 0.62, 0.08, 0.25] valuesrouting_weights = [0.62, 0.25] indicesselected_experts = [1, 3]
4. 最容易掉坑的地方

notebook 里 TODO 后面有 zero 占位变量。写完 TODO 后,要返回你真正算出的 routing_weightsselected_experts,不要让占位值覆盖结果。

行业视角:Router 是 MoE 的调度器

MoE 的专家像并行工作台,Router 只决定每个 token 送去哪几个工作台,以及每个工作台占多少权重。调度得好,模型容量变大但每个 token 只激活少数专家;调度得差,热门专家拥堵,冷门专家闲置。

下一关的负载均衡损失,就是专门约束这个调度器不要总偏心同几个专家。

语法热身:先用一行玩具分数练 softmax 和 topk

# 1 个 token, 4 个专家
toy_logits = torch.tensor([[1.2, 0.1, 2.0, -0.5]])

# dim=-1:只在“专家这一行”里归一化,得到每个专家的概率
toy_probs = F.softmax(toy_logits.float(), dim=-1)

# topk 同时返回值(values)和下标(indices)
toy_weights, toy_ids = torch.topk(
    toy_probs, k=2, dim=-1
)
print(toy_weights)
print(toy_ids)

举一反三:把玩具张量换成 notebook 变量

玩具例子只有 1 个 token。notebook 只是把很多 token 排成 [tokens, experts],同样沿最后一维做 softmax/topk。

toy_logitsrouter_logits toy_probsrouting_probs toy_weightsrouting_weights toy_idsselected_experts k=2self.top_k

迁移口诀:先把输入展平成 [tokens, hidden],gate 后得到 [tokens, experts];之后所有 softmax/topk 都沿 dim=-1,因为最后一维就是专家维。

巩固一下

hidden_states shape 是 [2,4,16],num_experts=8,top_k=2。展平并 topk 后 selected_experts 的 shape 是?

回到 notebook 的作业

这里不直接写答案。你已经拿到足够输入,最后用 notebook 的 TODO 做举一反三。

Mission 2 TODO 3 / 阅读已给聚合逻辑

Top-K 之后:重归一化,再把专家输出加权合成

🎯 先猜一猜(下注解锁)

Top-K 后,两个留下的概率可能是 0.60 和 0.27。它们已经是概率了,看起来可以直接用。

先判断:为什么还要做 routing_weights / routing_weights.sum(..., keepdim=True)?

先押一个假设,下面的讲解就会解锁。猜错完全没关系——带着疑问读,记得最牢。

先补的知识

  • 全量 softmax 的一整行概率和为 1;但丢掉非 Top-K 专家后,留下来的概率和通常小于 1。
  • 重归一化就是把留下来的 Top-K 权重按比例放大,让每个 token 的这 K 个权重重新加和为 1。
  • SparseMoEBlock 的聚合逻辑是:找到选中某专家的 token,跑这个专家,再乘对应权重,加回 final_hidden_states。
  • 同一个 token 会被 top_k 个专家各处理一次,所以 final_hidden_states 需要用累加,而不是赋值覆盖。

图解原理

Top-K 像只留下票数最高的两位专家,但他们原本只拿到了全体专家票数的一部分。重归一化把这部分票按比例摊成 100%,保证输出尺度稳定;聚合时每个专家贡献多少,由 routing weight 决定。把这段想成“先重新分配投票权,再按投票权合成专家意见”。

0. 本 Mission 分两层读

你真正要补的是 TODO 3:把 Top-K 权重重新归一化。后面的专家聚合逻辑已经给好,目标是读懂它为什么这样写。

要补代码routing_weights / row_sum 要读懂where 找 token, expert 计算, 加权累加
先看权重,再看专家输出

不要一开始就盯着 for 循环。Router 先给出权重和专家编号;SparseMoEBlock 再按编号把 token 送到专家,并把结果加回来。

Routerweights + experts MoE Blockexpert outputs weighted sum
第一层:Top-K 留下来的概率要重新分成 100%
1. 为什么要除以每行 sum

全量概率一行加起来是 1,但只保留 Top-2 后可能只剩 0.87。直接混合专家输出会把整体尺度压小,所以要在留下的专家内部重新分配。

Top-2.60 + .27 = .87 divide.60/.87, .27/.87 renorm.69 + .31 = 1.00
2. keepdim=True 保住可广播维度

routing_weights 是 [tokens, top_k]。sum 后保留成 [tokens, 1],才能自动广播除回每个 Top-K 槽位。

原权重routing_weights [8,2] 行和routing_weights.sum(dim=-1, keepdim=True) [8,1] 归一化后routing_weights [8,2], 每行和接近 1
第二层:SparseMoEBlock 逐专家找 token,再把加权输出累加回去
selected_experts专家编号表 torch.wheretoken_idx + kth_expert routing_weights取当前权重 unsqueeze[n] 变 [n,1] +=累加到 final_hidden_states

这段的读法是“谁选了当前专家,就把谁拿出来跑当前专家;再按对应权重写回”。

3. where 返回两个坐标

第一个坐标告诉你第几个 token 选中了当前专家;第二个坐标告诉你它是在 Top-K 的第几个槽位选中的。

selected_experts[[2,0], [1,2], [3,1]] expert_idx=2token_idx=[0,1], kth_expert=[0,1] 取权重routing_weights[token_idx, kth_expert]
4. 为什么必须是 +=

top_k=2 时,同一个 token 会从两个专家收集贡献。第一次写回不能覆盖第二次,第二次也不能抹掉第一次,所以这里是累加。

expert2(x0) * w0,0 expert0(x0) * w0,1 token0 = 两份贡献相加
5. 用测试反查三件事
weights / indices[batch_size * seq_len, top_k] weights.sum(dim=-1)每行接近 1 MoE 输出回到 [batch_size, seq_len, hidden_size]
行业视角:玩具循环易懂,工业实现会先排队再批处理

这里用 for 循环遍历专家,是为了让逻辑透明。真实推理框架会把同一个专家的 token 先排序聚到一起,一次性组成大 batch 喂给专家网络,减少小矩阵调用带来的 GPU 空转。

但核心协议没变:Router 给专家 id 和权重,专家产出向量,最后按权重加回对应 token。

语法热身:先用一行权重练 keepdim=True

toy_weights = torch.tensor([
    [0.60, 0.27],
    [0.40, 0.10],
])

# keepdim=True:sum 后保留成 [2, 1]
row_sum = toy_weights.sum(
    dim=-1, keepdim=True
)
toy_normed = toy_weights / row_sum
print(toy_normed.sum(dim=-1))

语法热身:再用玩具 selected_experts 读懂 where 和 unsqueeze

selected = torch.tensor([
    [2, 0],
    [1, 2],
    [3, 1],
])
expert_idx = 2

# where 返回两个坐标:第几个 token、第几个 top-k 槽位
token_idx, kth = torch.where(selected == expert_idx)
print(token_idx)  # tensor([0, 1])
print(kth)        # tensor([0, 1])

weights = torch.tensor([
    [0.7, 0.3],
    [0.4, 0.6],
    [0.5, 0.5],
])
w = weights[token_idx, kth]
w = w.unsqueeze(-1)  # [2] -> [2, 1]

举一反三:把玩具写法搬回 notebook

TODO 3 只需要第一张卡的重归一化写法。第二张卡是为了读懂后面已经给出的专家聚合代码。

toy_weightsrouting_weights selectedselected_experts expert_idxfor 循环里的 expert_idx wcurrent_weight

迁移口诀:归一化看最后一维 top_k,所以 sum(dim=-1, keepdim=True);聚合时权重先从 [n] 变 [n,1],才能广播乘到专家输出的 hidden 维。

巩固一下

routing_weights[token_idx, kth_expert] 的 shape 是 [n],要乘 expert 输出 [n, hidden],应该先做什么?

回到 notebook 的作业

这里不直接写答案。你已经拿到足够输入,最后用 notebook 的 TODO 做举一反三。

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