给 MoE 批推理排个序:按领域分组能少读多少专家

受 Hugging Face 文章 Sort your prompts 启发,用模拟路由演示:MoE 每个解码步要读取批内所有请求用到的专家,把同领域提示词排在同一批,能减少每步触达的不同专家数;专家数越少的粗粒度 MoE,节省越小。

已验证 Python 3.12.15 耗时 0.8s 运行于 2026-10-23 22:10 UTC
结论

模拟中按领域排序让每步读取的专家从 101.1 个降到 74.0 个(少 26.8%),专家越少的 MoE 节省越小(15.0%)

检查 4/4 通过
  • 按领域排序后,每步触达的不同专家数少于随机打乱
  • 128 专家配置下少读专家比例超过 5%
  • 粗粒度 MoE(32 专家)的节省比例小于细粒度(128 专家)
  • 单领域内前 1/4 专家所占路由比例高于均匀路由的 25%
实验指标(个)
随机批 每步触达专家数 (128 专家)101.1个
按领域排序 每步触达专家数 (128 专家)74.0个

数值取自 GitHub Actions 实际运行输出中的 METRIC 行

实验指标(%)
少读专家比例 (128 专家)26.8%
少读专家比例 (32 专家)15.0%
单领域前 1/4 专家所占路由比例78.4%

数值取自 GitHub Actions 实际运行输出中的 METRIC 行

以上内容从实际运行输出中提取,完整输出见下方。

python main.py
设定:768 条提示词、6 个领域、批大小 32
METRIC: 随机批 每步触达专家数 (128 专家) = 101.1 个
METRIC: 按领域排序 每步触达专家数 (128 专家) = 74.0 个
METRIC: 少读专家比例 (128 专家) = 26.8 %
METRIC: 少读专家比例 (32 专家) = 15.0 %
METRIC: 单领域前 1/4 专家所占路由比例 = 78.4 %
[PASS] 按领域排序后,每步触达的不同专家数少于随机打乱
[PASS] 128 专家配置下少读专家比例超过 5%
[PASS] 粗粒度 MoE(32 专家)的节省比例小于细粒度(128 专家)
[PASS] 单领域内前 1/4 专家所占路由比例高于均匀路由的 25%
RESULT: 模拟中按领域排序让每步读取的专家从 101.1 个降到 74.0 个(少 26.8%),专家越少的 MoE 节省越小(15.0%)

目的

Hugging Face 社区文章 Sort your prompts: a free speed-up for MoE batch inference 指出:混合专家(MoE)模型的每个解码步,要读取批内所有请求路由到的专家权重。同一领域的提示词倾向于使用重叠的专家,所以离线批处理前先按领域(或嵌入聚类)给提示词排序,就能让每步读取的专家更少、解码更快——不改模型、不改内核。

本实验用模拟路由演示这个机制。注意:路由分布是脚本按固定随机种子构造的,只演示机制,不复现文章中的任何数字。

步骤

  1. 设定 128 个专家、每个 token 选 8 个;6 个领域,每个领域有一组「偏好专家」被更频繁地选中
  2. 生成 768 条提示词(6 个领域各占 1/6),批大小 32
  3. 分别按「随机打乱」和「按领域排序」切批,每批模拟 20 个解码步,统计每步触达的不同专家数
  4. 换成 32 个专家、每 token 选 4 个的粗粒度配置,重复比较
  5. 统计单个领域内最常用的 1/4 专家占全部路由决策的比例

运行:

python main.py

程序打印 METRIC: 指标、[PASS]/[FAIL] 检查和最后一行 RESULT:,任一检查失败时以非零状态退出。

预期结果

  • 按领域排序后,每步触达的不同专家数明显少于随机打乱
  • 32 专家配置的节省比例小于 128 专家配置(与文章「粗粒度 MoE 收益较小」的观察方向一致)
  • 单领域内前 1/4 专家占的路由比例高于均匀路由下的 25%

启示与可以继续尝试

  • 「少读的专家数」只是上限估计:文章实测的提速约为按读取字节估算的一半,因为每步只有 MoE 部分变便宜
  • 可以把领域偏好调弱(把权重 12.0 改小),观察节省如何缩小——这对应真实数据里领域边界不清晰的情况
  • 可以把批大小调大,观察当一批几乎触达全部专家时,排序的收益如何消失
  • 在线服务中排序需要攒一批请求,会增加排队延迟,文章也指出这类场景不适用

内网复现

  • 无需任何第三方依赖,只用 Python 3.8+ 标准库,无需联网,可直接在内网运行
  • 把 main.py 拷贝到内网机器执行 python main.py 即可
  • 如要在真实模型上验证,需要 vLLM、MoE 模型权重(如 Qwen3-30B-A3B)和 GPU;权重可通过内网的 Hugging Face 镜像下载,本实验不依赖这些

代码

main.py123 行
"""给 MoE 批推理的提示词排个序:按领域分组能少读多少专家?

受 Hugging Face 社区文章 "Sort your prompts: a free speed-up for MoE batch inference" 启发。
文章的机制:MoE 每个解码步要读取批内所有请求路由到的专家权重;同一领域的提示词倾向于
使用重叠的专家,把同领域请求放进同一批,可以减少每步读取的不同专家数。

本脚本用标准库和固定随机种子模拟这一机制(全部为模拟数据,不复现文章数字):
- 128 个专家、每个 token 选 8 个(与文章中 Qwen3-30B-A3B 的配置相同,仅作设定)
- 6 个领域,每个领域对专家有不同的偏好分布(少数专家被频繁选中)
- 比较两种排批方式下每个解码步触达的不同专家数:随机打乱 vs 按领域排序
- 另比较 32 个专家(粗粒度 MoE)时节省比例是否更小
"""
from __future__ import annotations

import random
import sys

SEED = 24
N_PROMPTS = 768
N_DOMAINS = 6
BATCH = 32
STEPS = 20  # 每批模拟的解码步数


def make_domain_prefs(rng: random.Random, n_experts: int):
    """每个领域:随机挑一部分「偏好专家」,赋予较大权重,其余专家权重较小。"""
    prefs = []
    hot = max(4, n_experts // 4)
    for _ in range(N_DOMAINS):
        weights = [1.0] * n_experts
        for e in rng.sample(range(n_experts), hot):
            weights[e] = 12.0
        prefs.append(weights)
    return prefs


def route(rng: random.Random, weights, k: int):
    """按权重无放回抽 k 个专家,模拟一个 token 的 top-k 路由。"""
    chosen = set()
    w = list(weights)
    for _ in range(k):
        total = sum(w)
        r = rng.random() * total
        acc = 0.0
        for e, x in enumerate(w):
            acc += x
            if acc >= r:
                chosen.add(e)
                w[e] = 0.0
                break
    return chosen


def avg_experts_per_step(rng, domains, prefs, k, steps=STEPS):
    counts = []
    for start in range(0, len(domains), BATCH):
        batch = domains[start:start + BATCH]
        for _ in range(steps):
            touched = set()
            for d in batch:
                touched |= route(rng, prefs[d], k)
            counts.append(len(touched))
    return sum(counts) / len(counts)


def top_quarter_share(rng, prefs, n_experts, k, samples=400):
    """单个领域内,最常被选中的 1/4 专家占全部路由决策的比例。"""
    shares = []
    for d in range(N_DOMAINS):
        freq = [0] * n_experts
        for _ in range(samples):
            for e in route(rng, prefs[d], k):
                freq[e] += 1
        freq.sort(reverse=True)
        shares.append(sum(freq[: n_experts // 4]) / sum(freq))
    return sum(shares) / len(shares) * 100


def run(n_experts: int, k: int, seed: int):
    rng = random.Random(seed)
    prefs = make_domain_prefs(rng, n_experts)
    domains = [i % N_DOMAINS for i in range(N_PROMPTS)]
    shuffled = domains[:]
    rng.shuffle(shuffled)
    sorted_ = sorted(shuffled)  # 稳定排序:按领域分组
    mixed = avg_experts_per_step(random.Random(seed + 1), shuffled, prefs, k)
    grouped = avg_experts_per_step(random.Random(seed + 1), sorted_, prefs, k)
    share = top_quarter_share(random.Random(seed + 2), prefs, n_experts, k)
    saving = (mixed - grouped) / mixed * 100
    return mixed, grouped, saving, share


def main() -> int:
    mixed, grouped, saving, share = run(128, 8, SEED)
    mixed32, grouped32, saving32, _ = run(32, 4, SEED)

    print(f"设定:{N_PROMPTS} 条提示词、{N_DOMAINS} 个领域、批大小 {BATCH}")
    print(f"METRIC: 随机批 每步触达专家数 (128 专家) = {mixed:.1f} 个")
    print(f"METRIC: 按领域排序 每步触达专家数 (128 专家) = {grouped:.1f} 个")
    print(f"METRIC: 少读专家比例 (128 专家) = {saving:.1f} %")
    print(f"METRIC: 少读专家比例 (32 专家) = {saving32:.1f} %")
    print(f"METRIC: 单领域前 1/4 专家所占路由比例 = {share:.1f} %")

    checks = [
        (grouped < mixed, "按领域排序后,每步触达的不同专家数少于随机打乱"),
        (saving > 5, "128 专家配置下少读专家比例超过 5%"),
        (saving32 < saving, "粗粒度 MoE(32 专家)的节省比例小于细粒度(128 专家)"),
        (share > 25, "单领域内前 1/4 专家所占路由比例高于均匀路由的 25%"),
    ]
    ok = True
    for passed, desc in checks:
        print(f"[{'PASS' if passed else 'FAIL'}] {desc}")
        ok &= passed

    print(
        f"RESULT: 模拟中按领域排序让每步读取的专家从 {mixed:.1f} 个降到 {grouped:.1f} 个"
        f"(少 {saving:.1f}%),专家越少的 MoE 节省越小({saving32:.1f}%)"
    )
    return 0 if ok else 1


if __name__ == "__main__":
    sys.exit(main())
requirements.txt1 行
# 无第三方依赖,只使用 Python 标准库
Esc
↑ ↓ 选择Enter 打开

键盘快捷键

j / k
下一条 / 上一条
o
展开或收起当前条目
Enter
打开当前条目详情
c
复制当前条目链接
s
收藏 / 取消收藏当前条目
/ 或 ⌘K
搜索
1 2 3
切换密度:标题 / 摘要 / 完整
t
切换主题
?
显示本帮助