qinghuan
← 回到知识探索

AI 直觉 / 可下载小项目

还有空着的专家,为什么 MoE 仍跳过一部分 Token?

用八个输入、两个专家和固定名额,分清路由偏好、实际负载、溢出旁路与闲置容量。

展开本文目录
  1. 下载完整练习包
  2. 先动手看看
  3. 先把三种数量分开
  4. 总位置够用,不等于每个目的地的位置够用
  5. 换顺序会换掉谁;一个均衡数值也可能遮住偏斜
  6. 运行一个完整的固定路由实验
  7. 从这个例子能推到哪里
  8. 记住这些要点
  9. 别把例子用过头
  10. 自己试一小步
  11. 查原始资料
开始前,会这些就够了
  • 知道 Token 是模型处理的输入单位
  • 会读 Python 列表和 if 条件
  • 知道概率分布的各项相加为 1
  • 需要补基础时:先理解分数怎样变成概率
遇到不熟悉的词,先看这 2 个解释
专家容量
在采用固定容量的混合专家层中,每位专家一次最多处理指定数量的输入。路由器想把多少输入送给它,与它最后能接收多少输入,是两个数量。
残差旁路
保留原来的输入,计算分支给出额外改动;分支没执行时,原输入仍可继续传下去。
打开完整术语小抄 ↗

把八张单子交给两个办事窗口,每个窗口最多接四张。前六张都被分给窗口 0,后两张分给窗口 1。窗口 1 还空两个位置,窗口 0 却已经满了。只要规则没有允许重新分配,多出来的两张单子仍然进不了窗口。

某些混合专家模型也会遇到这种情况:路由器先为每个 Token 选择一个专家,再受每个专家容量限制。这里的“跳过”特指跳过本层专家计算;示例保留残差分支,所以 Token 本身没有从序列里被删除。

把例子带到自己的电脑上

下载完整练习包

需要 Python 3.10 或更新版本,无第三方依赖;制作时实测 Windows / Python 3.14.0。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。

下载 ZIP · 5 个文件
包里有什么
  • moe_capacity.py:完整可运行示例
  • README.md:运行说明与独立练习
  • observed.stdout.txt:本次实际输出
  • verification.json:源码和产物验证记录
  • SHA256SUMS.txt:内容摘要

在这个文件夹打开 PowerShell,逐行运行

python --version
python -B moe_capacity.py

解压到独立练习文件夹,在含 moe_capacity.py 的目录打开终端运行。固定路由概率、标量专家和容量仲裁的规则模型;不训练模型,不运行真实 MoE/GPU/API,不测吞吐。

找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。

还有空槽,为什么一些 Token 仍进不去?

8 个 Token 各自选择概率最高的专家。每位专家有自己的容量,按当前输入顺序分配;容量用完后,该 Token 保留原输入,跳过专家分支。

每位专家容量 = 向上取整(8 ÷ 2 × 1.00) = 4。

想选 E0 / E1
6 / 2
实际进入 E0 / E1
4 / 2
跳过专家分支
2
仍然空闲的槽
2

t4、t5 没有进入选中的专家,但它们没有从序列里消失。

负载辅助项的未乘系数部分为 1.20;可切换到“同分”情况,检查这个数值本身能否证明实际分配均匀。

教学函数 E0(x) = 2x,E1(x) = −x;输出 = x + 概率 × 专家结果
处理顺序输入 x所选专家专家分支最终输出
t01E0 · 0.91.82.8
t12E0 · 0.93.65.6
t23E0 · 0.95.48.4
t34E0 · 0.97.211.2
t45E0 · 0.9跳过,记为 05.0
t56E0 · 0.9跳过,记为 06.0
t67E1 · 0.9-6.30.7
t78E1 · 0.9-7.20.8

这里只计算两个标量函数,演示一种 Switch 风格的 top-1 容量策略,没有训练或运行真实大模型。容量取整、同分与先后顺序是本实验的明确约定。其他 MoE 实现可以使用不同的排序、重路由或无丢弃策略,不能用这里的空槽数直接估计速度。

先把三种数量分开

MoE 中的“专家”通常是一组神经网络参数,不必理解成有明确职业分工的独立助手。本例只定义两个算术函数:专家 0 把 x 乘 2,专家 1 把 x 取负。它们没有学过任何文本,仅用来让计算路径可见。

路由概率是“想送哪里”;需求量是按最高概率选完以后,每个专家收到多少请求;接收量才是容量限制后真正进入计算的数量。把三个数混为一谈,会误把平均概率均匀当成真实负载均匀。

本例固定八个输入 t0 到 t7,数值为 1 到 8。前六个的路由概率为 [0.9, 0.1],后两个为 [0.1, 0.9]。因此需求为 [6, 2]。没有抽样,top-1 就是选择较大的概率;同分时明确选择索引更小的专家。

总位置够用,不等于每个目的地的位置够用

本例容量 C = ceil(Token 数 ÷ 专家数 × 容量系数)。系数为 1 时,每个专家 C=4,总共八个位置。按照输入顺序占槽,t0 到 t3 进入专家 0,t4、t5 溢出;t6、t7 进入专家 1。于是同时出现两次跳过和两个闲置位置。

系数改成 1.5 后,每个专家有六个位置,八个 Token 都能进入对应专家;代价是总共预留十二个位置,空着四个。程序只计算槽位,不测显存、通信或运行时间,不能把空槽数直接换算为某款 GPU 的耗时。

输出规则也写死:保留时 output = x + p × E(x),溢出时 output = x。前面的 x 表示残差通路。以 t4 的 x=5 为例,受限时输出 5;容量放大后进入专家 0,输出为 5 + 0.9×10 = 14。

换顺序会换掉谁;一个均衡数值也可能遮住偏斜

固定采用先到先占槽时,倒转输入顺序仍然只接收六个 Token,但溢出的编号从 t4、t5 换成 t1、t0。这是容量仲裁规则产生的变化;不是路由概率发生变化,也不是所有 MoE 都必须有相同顺序依赖。

Switch Transformer 的辅助项可以写成 α × N × Σ(fᵢPᵢ):fᵢ 是选择专家 i 的 Token 比例,Pᵢ 是该专家概率在整批上的平均值。本例只显示去掉 α 后的项。偏斜例为 1.2,均衡例为 1.0。它是训练目标的一部分,不是质量得分。

再把所有路由概率设为 [0.5, 0.5]。平均概率看起来十分均匀,辅助项也等于 1.0;但我们的同分规则让八个 Token 全选专家 0,仍有四个溢出。这个具体反例说明,单看辅助项或平均概率都不能替代硬路由计数、溢出率和任务指标。

运行一个完整的固定路由实验

把下面代码保存为 moe_capacity.py,运行 python -B moe_capacity.py。它会输出五个场景,并在当前目录写入 report.json;下载包还保留制作时的实际输出与摘要。输入和专家函数都在源码里明示,不需模型权重或网络。

展开收起代码 · 小实验python · 80 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

小实验 · python
"""A deterministic top-1 router arithmetic model, not an MoE training run."""
import json
import math
from pathlib import Path


def route(rows, factor=1.0):
    if not rows or not math.isfinite(factor) or factor <= 0:
        raise ValueError("nonempty rows and positive finite factor required")
    experts = len(rows[0][2])
    if experts != 2:
        raise ValueError("this demonstration defines exactly two expert functions")
    if len({row[0] for row in rows}) != len(rows):
        raise ValueError("token IDs must be unique")
    for _, value, probabilities in rows:
        if not math.isfinite(value) or len(probabilities) != experts:
            raise ValueError("invalid input shape")
        if any(not math.isfinite(p) or p < 0 for p in probabilities):
            raise ValueError("invalid router probabilities")
        if not math.isclose(sum(probabilities), 1.0, abs_tol=1e-12):
            raise ValueError("router probabilities must sum to one")
    capacity = math.ceil(len(rows) * factor / experts)
    demand = [0] * experts
    accepted = [0] * experts
    records = []
    for token_id, value, probabilities in rows:
        # Python max keeps the first expert on a tie: our explicit policy.
        chosen = max(range(experts), key=lambda e: probabilities[e])
        demand[chosen] += 1
        keep = accepted[chosen] < capacity
        if keep:
            accepted[chosen] += 1
        expert_output = (2 * value if chosen == 0 else -value) if keep else 0.0
        output = value + probabilities[chosen] * expert_output
        records.append({"id": token_id, "expert": chosen, "kept": keep,
                        "output": round(output, 6)})
    f = [count / len(rows) for count in demand]
    mean_p = [sum(row[2][e] for row in rows) / len(rows) for e in range(experts)]
    return {"capacity": capacity, "demand": demand, "accepted": accepted,
            "dropped_ids": [r["id"] for r in records if not r["kept"]],
            "unused_slots": experts * capacity - sum(accepted),
            "balance_term_without_alpha": round(experts * sum(a*b for a,b in zip(f, mean_p)), 6),
            "records": records}


def main():
    skew = [(f"t{i}", float(i+1), [0.9, 0.1] if i < 6 else [0.1, 0.9]) for i in range(8)]
    balanced = [(f"t{i}", float(i+1), [0.9, 0.1] if i < 4 else [0.1, 0.9]) for i in range(8)]
    tied = [(f"t{i}", float(i+1), [0.5, 0.5]) for i in range(8)]
    reports = {"skew_capacity_1": route(skew), "skew_capacity_1_5": route(skew, 1.5),
               "skew_reversed": route(list(reversed(skew))),
               "balanced": route(balanced), "uniform_probabilities_tie": route(tied)}
    assert reports["skew_capacity_1"]["dropped_ids"] == ["t4", "t5"]
    assert reports["skew_capacity_1_5"]["dropped_ids"] == []
    assert reports["skew_capacity_1_5"]["unused_slots"] == 4
    assert set(reports["skew_reversed"]["dropped_ids"]) == {"t0", "t1"}
    assert reports["balanced"]["balance_term_without_alpha"] == 1.0
    assert reports["uniform_probabilities_tie"]["balance_term_without_alpha"] == 1.0
    assert len(reports["uniform_probabilities_tie"]["dropped_ids"]) == 4
    assert reports["skew_capacity_1"]["records"][4]["output"] == 5.0
    rejected = 0
    for rows, factor in [([], 1), (skew, 0), (skew, float("nan")),
                         ([("x", 1, [0.2, 0.2])], 1),
                         ([("x", 1, [1.1, -0.1])], 1),
                         ([("x", 1, [0.5, 0.5])]*2, 1)]:
        try:
            route(rows, factor)
        except ValueError:
            rejected += 1
    assert rejected == 6
    for name, result in reports.items():
        print(f"{name}: capacity={result['capacity']} demand={result['demand']} "
              f"kept={result['accepted']} dropped={result['dropped_ids']} "
              f"unused={result['unused_slots']} balance={result['balance_term_without_alpha']:.3f}")
    print("checks=14 passed; no model, training framework, GPU or API was used")
    Path("report.json").write_text(json.dumps({"scenarios": reports, "checks": 14}, indent=2), encoding="utf-8")


if __name__ == "__main__":
    main()

怎样运行:在独立练习文件夹中执行 python -B moe_capacity.py。2026-10-17 已在 Windows、Python 3.14.0 运行此份源码并通过自带检查。固定路由概率、标量专家和容量仲裁的规则模型;不训练模型,不运行真实 MoE/GPU/API,不测吞吐。

运行后对照结果
skew_capacity_1: capacity=4 demand=[6, 2] kept=[4, 2] dropped=['t4', 't5'] unused=2 balance=1.200
skew_capacity_1_5: capacity=6 demand=[6, 2] kept=[6, 2] dropped=[] unused=4 balance=1.200
skew_reversed: capacity=4 demand=[6, 2] kept=[4, 2] dropped=['t1', 't0'] unused=2 balance=1.200
balanced: capacity=4 demand=[4, 4] kept=[4, 4] dropped=[] unused=0 balance=1.000
uniform_probabilities_tie: capacity=4 demand=[8, 0] kept=[4, 0] dropped=['t4', 't5', 't6', 't7'] unused=4 balance=1.000
checks=14 passed; no model, training framework, GPU or API was used

从这个例子能推到哪里

Switch 论文描述了固定容量、溢出跳过专家层并保留残差的方案;GShard 的路由设置包含 top-2 等不同处理;MegaBlocks 则提出避免丢弃 Token 的计算实现。因此“MoE 一定丢 Token”与“空着的位置一定能自动接走溢出 Token”都不成立。

这次只执行 Python 标准库的路由与标量运算,没有训练路由器,没有运行真实专家网络,没有执行 GPU、跨卡通信或服务压测。观察到的是给定规则的结果,不能据此宣布某个模型生成质量变差或获得多少倍加速。

真正在排查一层 MoE 时,先记录路由策略、分组大小、容量如何取整、同分如何处理、超额如何选择和是否存在残差,再同时看每专家需求、接收量、溢出与任务效果。否则两个实现即使叫同一个容量系数,也可能没有做同一件事。

记住这些要点

  • 总容量足够,仍可能因某个专家拥挤而溢出。
  • 固定容量与先到先占槽会让输入顺序影响被跳过的编号。
  • 平均路由概率与均衡辅助项不能替代真实接收量。
  • 跳过专家计算、删掉 Token 和重新路由是不同策略。

别把例子用过头

  • 把本例固定专家函数称为训练出的神经网络。
  • 把 Switch 的具体溢出策略推广到所有 MoE。
  • 把更低的均衡项直接解释成更高回答质量。
  • 只显示总槽位,不显示各专家的需求与接收量。

自己试一小步

保留八个 Token 与两个专家,怎样比较“先到先占槽”和“每个专家优先接路由概率最高者”?

  1. 为前六个 Token 设置互不相同、但仍偏向专家 0 的概率。
  2. 分别实现两种容量仲裁,并把结果恢复到原始 Token 编号。
  3. 倒转输入顺序,比较两种策略的接收集合。
  4. 增加同概率、空输入和容量系数小于 1 的边界场景。

做到这些,就算完成

  • 报告每个专家的需求、接收和闲置,而不只报告总数。
  • 说明同分规则,并让重复运行可复现。
  • 区分接收集合是否稳定与任务质量是否提升;后者不能由本实验确认。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。