第 10 课:专家并行 EP

参考答案版:包含完整代码实现与问答题深度参考解析。建议先独立完成练习题后再展开核对。

Important统一完成标准与及格线

代码 4 分、Q1~Q3 各 2 分,通过线 8/10。必须解释正确性边界、显存/通信公式中的单位与分片维度;未实际运行的内容只能标记为静态审查。

本课目标

完成后你需要能够:

  • 说明 MoE 层的基本结构与 EP 的切分方式(专家维,不是 token 维)。
  • 解释 EP 为什么必须与 DP 组合,以及与 TP 相比”轻量”在哪。
  • 推演 token 路由产生的 all-to-all 通信与负载不均问题。
  • 解释 DeepSeek-V3 的节点限制路由(每 token ≤ M 个节点)的设计动机。

核心概念

1. 混合专家架构(MoE)与专家并行(EP)的本质语义

混合专家模型(Mixture-of-Experts, MoE)是当前前沿大模型(如 DeepSeek-V3/R1、Mixtral、Grok)实现“超级参数容量(Trillion-scale Parameters)”与“可控计算预算(Constant FLOPs)”解耦的核心技术。

在标准 Transformer 架构中,全连接层(FFN/MLP)消耗了全网约 23\frac{2}{3} 的参数量。MoE 将单一密集的 FFN 替换为一个门控路由网络(Gating Router)与 EE 个结构同构的独立专家网络(Expert FFNs):

  • 给定输入 Token 特征向量 x∈ℝhx \in \mathbb{R}^h,Router 计算该 Token 对各专家的匹配亲和度分数: s(x)=Softmax(TopK(x⋅Wg,K))或采用 Sigmoid 归一化s(x) = \text{Softmax}(\text{TopK}(x \cdot W_g, \ K)) \quad \text{或采用 Sigmoid 归一化}

  • 系统仅激活得分最高的 KK 个专家(如 Top-2,或 DeepSeek-V3 的 Top-8 细粒度专家),最终输出为激活专家输出的加权求和: y=∑i∈TopKsi(x)⋅FFNi(x)y = \sum_{i \in \text{TopK}} s_i(x) \cdot \text{FFN}_i(x)

专家并行(Expert Parallelism, EP)的切分维度: 如图 10-1 所示,EP 既不切分矩阵乘法(不同于 TP),也不切分序列层深(不同于 PP),而是沿着“专家索引维度(Expert Dimension)”进行切分:

flowchart LR
    subgraph ExpertParallel["专家并行 (EP) 拓扑"]
        Tokens["输入序列 Tokens"] --> Gate["门控网络 Top-K Routing"]
        Gate -->|分发| E0["GPU 0: 专家 0, 1"]
        Gate -->|分发| E1["GPU 1: 专家 2, 3"]
        Gate -->|分发| E2["GPU 2: 专家 4, 5"]
        Gate -->|分发| E3["GPU 3: 专家 6, 7"]
    end

图 10-1:MoE 门控路由网络与跨 GPU 专家切分架构

  • 设集群中有 NepN_{\text{ep}} 张 GPU,模型拥有 EE 个专家,则每张 GPU 常驻持有 ENep\frac{E}{N_{\text{ep}}} 个完整的专家模型权重。
  • 与张量并行(TP)的关键区别:
    • 在 TP 中,算子权重矩阵被强行拆散,各卡执行局部矩阵乘并产出“不完整的碎片(部分和 Partial Sum)”,必须通过阻塞式 AllReduce 沿特征维累加求和。
    • 在 EP 中,每张 GPU 上的专家权重是 100% 完整且独立的!矩阵乘法在本地全量执行。EP 的本质是“计算不动,数据动”——根据 Router 的决策,将不同卡上的 Token 动态投递搬迁到对应专家所在的 GPU 上。

2. 为什么 EP 必须与数据并行(DP)深度绑定(2D DP×EP\text{DP} \times \text{EP} 网格)

大模型中除了 MoE FFN 层之外,还包含大量的密集模块(Dense Layers):Embedding 词表、多头自注意力(Attention)、RMSNorm 归一化等。

如果仅仅孤立地做专家并行而不引入数据并行:

  • 整个集群只能处理单份样本,这会导致所有 GPU 必须在非 MoE 区域完全冗余地重复计算相同的 Token 特征,造成严重的算力雪崩。
  • 因此,工业界标准架构必然采用 DP×EP\text{DP} \times \text{EP} 二维网格拓扑:
    • 在全局层面上,各个 GPU 接收互不重叠的批次样本(承担数据并行 DP 角色);
    • 进入非 MoE 区域(如 Attention)时,各卡在本地独立前向计算;
    • 一旦执行到 MoE 层,系统切换为 EP 语义:各卡本地样本产出的 Token 根据 Router 派发目标,跨卡重组并打散投递到各个专家卡上。

3. 双向 All-to-All 通信原语与流转闭环

如图 10-2 所示,一次 MoE 专家计算在底层严格由两次全交换(All-to-All)通信构成闭环:

sequenceDiagram
    autonumber
    participant Rank as 本地卡 (Rank i)
    participant A2A_D as All-to-All Dispatch (分发)
    participant Expert as 远端专家计算
    participant A2A_C as All-to-All Combine (聚合)

    Rank->>A2A_D: 发射分配给远端专家的 Tokens
    A2A_D-->>Expert: 远端专家接收属于自己的 Tokens 并执行 MLP
    Expert->>A2A_C: 将计算完成的隐藏状态发射回源卡
    A2A_C-->>Rank: 源卡接收输出并按门控权重做加权求和

图 10-2:MoE 专家并行双向 All-to-All 通信与计算全流程

  1. 路由分发阶段(Dispatch All-to-All):
    • 处于 Rank rr 的 GPU 拥有本地批次的 TT 个 Token。通过本地 Router 算理,每个 Token 选出 KK 个目标专家。
    • 系统将发往远端各个 Rank 的 Token 特征向量分别打包(Pack)。
    • 发起一次非均匀的全交换通信算子(dist.all_to_all_single 或带偏移量的 Ragged All-to-All): 每个 Rank 将本地收集的 Token 交换给持有对应专家的目标 GPU。
  2. 专家局部批量计算(Local Batched GEMM):
    • 经过 All-to-All 后,每张 GPU 收到了来自全网各卡、且指名由自身管辖专家计算的所有 Token。
    • GPU 将这些 Token 按本地专家索引分流聚类,调用 Grouped GEMM 连续执行前向推理。
  3. 结果汇聚送回阶段(Combine All-to-All):
    • 本地专家计算完毕后,产出的特征张量必须原路返回其“发源地”Rank!
    • 系统再次发射反向的 All-to-All,将特征向量送回原始 Token 所在的 GPU。
  4. 门控加权求和(Weighted Summation):
    • 原始 GPU 收到返回的 KK 个专家计算结果后,乘上第一步中留存的门控权重 si(x)s_i(x),累加合并后输出,无缝接入下一层网络。

单步单层 MoE 的通信量严格为: VMoE_Comm=2×Tokens⏟总数×K⏟激活数×h⏟隐藏维×sizeof(dtype)V_{\text{MoE\_Comm}} = 2 \times \underbrace{\text{Tokens}}_{\text{总数}} \times \underbrace{K}_{\text{激活数}} \times \underbrace{h}_{\text{隐藏维}} \times \text{sizeof(dtype)}

4. 木桶效应:专家负载失衡(Load Imbalance)与应对演进

在理想状态下,每个专家接收到的 Token 数量均等,各卡计算耗时一致。但在真实语言分布中,某些专家(如处理标点符号、高频词)会被频繁路由,而专业领域的专家被选中极少。

  • 木桶短板灾难(The Straggler Problem): 若专家 0 接收到 10 倍于均值的 Token,则持有专家 0 的 GPU 计算耗时将飙升 10 倍。而在分布式执行中,第二阶段的 Combine All-to-All 必须等待最慢的卡算完才能汇聚!单卡过载将拖垮整个万卡集群的时钟步长。

  • 经典对抗方案:

    • 负载均衡辅助损失(Auxiliary Loss):在总损失中加入正则项 ℒaux=αE∑i=1EfiPi\mathcal{L}_{\text{aux}} = \alpha E \sum_{i=1}^E f_i P_i(fif_i 为路由频率,PiP_i 为概率均值),惩罚扎堆现象。但强加 Aux Loss 往往会损伤模型的语言建模精度。
    • 容量因子截断(Capacity Factor, CF):为每个专家设置缓冲上限 C=CF×K⋅TEC = \text{CF} \times \frac{K \cdot T}{E}。超出上限的 Token 直接被强行丢弃(Drop Tokens)或旁路跳过。虽然保证了计算时间确定,但在推理和长推理时丢 Token 会引发严峻的幻觉。

5. 架构革新:节点限制路由(Node-Limited Routing,DeepSeek-V3 核心发明)

在大规模跨机训练中(如 2048 卡),全量 All-to-All 会对数据中心机间交换机网络造成近乎崩溃的横断面流量压力(East-West Bisection Bandwidth)。如果一个 Token 激活了 8 个专家,且这 8 个专家散落在 8 个不同的物理机柜中,该 Token 将产生 8 次跨机网络传输!

DeepSeek-V3 提出了颠覆性的 节点限制路由(Node-Limited Routing):

  • 核心约束:在选择 Top-KK(如 K=8K=8)个专家时,强制施加硬件拓扑亲和性约束——每个 Token 选出的所有专家,最多只能分布在 MM 个物理节点之内(例如 M≤4M \le 4)。
  • 系统红利:
    • 路由选择被高概率锚定在机内(NVLink 域)或极少数跨机节点内。
    • 极大地将跨节点 RDMA 网络通信流量截留或压缩了近 50%!
    • 彻底规避了网络链路争抢和交换机拥塞,使万卡规模下的 EP 吞吐提升至前所未有的高度。

具体演示与数值推演

设集群规模为 2 张 GPU(Rank 0 与 Rank 1),模型共配置 4 个专家(每张卡部署 2 个专家:Rank 0 持有 Expert 0, 1;Rank 1 持有 Expert 2, 3)。总批次输入 16 个 Token:

  • 理想均匀路由分布(Balanced Routing):
    • 4 个专家各分得 4 个 Token(即 [4, 4, 4, 4])。
    • Rank 0 的专家计算负载:4+4=84 + 4 = 8 个 Token;
    • Rank 1 的专家计算负载:4+4=84 + 4 = 8 个 Token;
    • 失衡比(Imbalance Ratio):max⁡(8,8)mean(8,8)=88=1.00\frac{\max(8, 8)}{\text{mean}(8, 8)} = \frac{8}{8} = 1.00(完美线性加速)。
  • 极端失衡路由分布(Skewed Routing):
    • 专家 0 成为热门专家,分得 10 个 Token;其余专家分别分得 4, 4, 2 个 Token(即 [10, 4, 4, 2])。
    • Rank 0 的专家计算负载:10+4=1410 + 4 = 14 个 Token;
    • Rank 1 的专家计算负载:4+2=64 + 2 = 6 个 Token;
    • 失衡比:max⁡(14,6)mean(14,6)=1410=1.40\frac{\max(14, 6)}{\text{mean}(14, 6)} = \frac{14}{10} = 1.40。
    • 耗时推演:由于跨卡 Combine All-to-All 必须等待 Rank 0 算完全部 14 个 Token,Rank 1 即使早已算完 6 个 Token 也必须在网络同步点挂起等待。整步耗时由 14 决定,硬件整体有效算力利用率暴跌至 14+62×14=2028≈71.4%\frac{14 + 6}{2 \times 14} = \frac{20}{28} \approx 71.4\%。

代码填空题

模拟专家到 rank 的映射、负载统计与路由通信量。

Code
def expert_to_rank(num_experts: int, experts_per_rank: int) -> dict[int, int]:
    """专家均匀分到各 rank:rank r 持有专家 [r*e, (r+1)*e)。"""
    mapping = {}
    for e in range(num_experts):
        mapping[e] = ______      # 填空:专家 e 所属的 rank
    return mapping

def load_per_rank(tokens_per_expert: list[int], experts_per_rank: int) -> list[int]:
    """每 rank 的专家负载 = 其专家收到的 token 总数。"""
    num_experts = len(tokens_per_expert)
    rank_load = [0] * (num_experts // experts_per_rank)
    for e, tokens in enumerate(tokens_per_expert):
        r = expert_to_rank(num_experts, experts_per_rank)[e]
        rank_load[r] += ______   # 填空:累加
    return rank_load

def imbalance(loads: list[int]) -> float:
    """失衡比 = 最大负载 / 平均负载。"""
    return ______                # 填空

def routing_volume(tokens_per_expert: list[int]) -> int:
    """
    一次前向的 all-to-all 通信量(token 数)。
    每个 token 都被路由到其目标专家所在 rank——注意目标专家可能就在本地
    rank(这部分不需要网络传输)。严格计算要区分本地/远程;
    这里先返回"被路由的 token 总数"作为上界,远程占比写在注释里。
    """
    return ______                # 填空

if __name__ == "__main__":
    # 均匀路由:每专家 4 token
    tokens = [4, 4, 4, 4]
    print("均匀:", load_per_rank(tokens, 2), "失衡比", f"{imbalance(load_per_rank(tokens, 2)):.2f}")

    # 失衡路由:专家 0 拿到 10 个
    tokens2 = [10, 4, 4, 2]
    loads = load_per_rank(tokens2, 2)
    print("失衡:", loads, "失衡比", f"{imbalance(loads):.2f}")
    print("路由总量:", routing_volume(tokens2), "token")

    # DeepSeek-V3 量级:256 专家、每卡 8 卡节点内放若干专家
    print("专家映射示例:", expert_to_rank(256, 32))   # 每 rank 32 个专家

三个问答题

Q1

为什么说 EP “比 TP 轻量”?两类并行各自传输的数据分别是什么(部分和 vs token 搬运)?为什么 EP 单独使用会造成非 MoE 部分的重复计算,必须与 DP 组合?

Q2

EP 的 all-to-all 通信发生在什么时机、传输什么数据?DeepSeek-V3 为什么限制”每个 token 最多送往 M=4 个节点”?这个约束与”均匀路由”的目标有什么区别与联系?

Q3

负载不均为什么是 EP 的致命问题(对比 DP 中负载天然均匀)?请列出至少两种缓解手段,并说明它们各自的代价。极端失衡(某专家 token 数超过容量)时会发生什么?

检查与通过标准

总分 10 分:代码正确 4 分(映射、负载、失衡比、路由量)、三题各 2 分、通过线 8 分。

一票否决项:

  • 认为 EP 切分的是 token 维或输入 batch(那是 DP/CP)。
  • 认为 EP 不需要与 DP 组合。
  • 把 EP 通信说成 all-reduce 权重同步(是 all-to-all 路由)。
  • 意识不到负载均衡问题及其与路由设计的耦合。

参考答案(仅 answer 分支)

先完成题目再核对;核对后必须解释关键公式,并改一个规模重新计算。

Code
def expert_to_rank(num_experts: int, experts_per_rank: int) -> dict[int, int]:
    """专家均匀分到各 rank:rank r 持有专家 [r*e, (r+1)*e)。"""
    mapping = {}
    for e in range(num_experts):
        mapping[e] = e // experts_per_rank      # 填空:专家 e 所属的 rank
    return mapping

def load_per_rank(tokens_per_expert: list[int], experts_per_rank: int) -> list[int]:
    """每 rank 的专家负载 = 其专家收到的 token 总数。"""
    num_experts = len(tokens_per_expert)
    rank_load = [0] * (num_experts // experts_per_rank)
    for e, tokens in enumerate(tokens_per_expert):
        r = expert_to_rank(num_experts, experts_per_rank)[e]
        rank_load[r] += tokens   # 填空:累加
    return rank_load

def imbalance(loads: list[int]) -> float:
    """失衡比 = 最大负载 / 平均负载。"""
    if not loads or sum(loads) == 0:
        raise ValueError("loads must be non-empty with positive total")
    return max(loads) / (sum(loads) / len(loads))                # 填空

def routing_volume(tokens_per_expert: list[int]) -> int:
    """
    一次前向的 all-to-all 通信量(token 数)。
    每个 token 都被路由到其目标专家所在 rank——注意目标专家可能就在本地
    rank(这部分不需要网络传输)。严格计算要区分本地/远程;
    这里先返回"被路由的 token 总数"作为上界,远程占比写在注释里。
    """
    return sum(tokens_per_expert)                # 填空

if __name__ == "__main__":
    # 均匀路由:每专家 4 token
    tokens = [4, 4, 4, 4]
    print("均匀:", load_per_rank(tokens, 2), "失衡比", f"{imbalance(load_per_rank(tokens, 2)):.2f}")

    # 失衡路由:专家 0 拿到 10 个
    tokens2 = [10, 4, 4, 2]
    loads = load_per_rank(tokens2, 2)
    print("失衡:", loads, "失衡比", f"{imbalance(loads):.2f}")
    print("路由总量:", routing_volume(tokens2), "token")

    # DeepSeek-V3 量级:256 专家、每卡 8 卡节点内放若干专家
    print("专家映射示例:", expert_to_rank(256, 32))   # 每 rank 32 个专家

补全后的代码(关键填空处)

def expert_to_rank(num_experts, experts_per_rank):
    mapping[e] = e // experts_per_rank      # rank r 持有 [r*e, (r+1)*e)

def load_per_rank(tokens_per_expert, experts_per_rank):
    rank_load[r] += tokens                  # 累加该 rank 所有专家的 token

def imbalance(loads):
    mean = sum(loads) / len(loads)
    return max(loads) / mean

def routing_volume(tokens_per_expert):
    return sum(tokens_per_expert)           # 全部被路由 token 的上界

运行结果:

均匀: [8, 8] 失衡比 1.00
失衡: [14, 6] 失衡比 1.40
路由总量: 20 token
专家映射示例: {e: e//32}

三个问答题要点

Q1

  • TP 切分矩阵乘法本身:每个 rank 算部分和,需要 all-gather/all-reduce 合并;
  • EP 不拆 matmul:把整张 FFN(专家)放到某个 rank,token 整体搬过去——通信是 all-to-all(token 路由),不产生部分和同步;
  • 因此 EP”轻量”指实现简单、通信模式直接;但只做 EP 时 attention/embedding 等非 MoE 模块仍全量存在于每卡 → 重复计算 → 必须与 DP 组合(DP 并行 batch、EP 并行专家)。

Q2

  • 时机:router 决定每个 token 的目标专家后、专家计算前(token→专家 rank),以及专家计算后(结果→原 rank);
  • 传输数据:token 的隐藏状态(seq·mbs·h 量级),不是权重/梯度;
  • 节点限制(每 token ≤ M=4 节点):把跨节点通信量压到可控(token 尽量留在节点内,只有少数跨节点),同时保住负载均衡的灵活性;它与”均匀路由”目标不同——均匀路由是统计层面的均衡,节点限制是通信层面的约束,两者共同作用。

Q3

  • 为什么致命:collective/同步步调下,慢的 rank 拖住全体;DP 中各卡算相同量的不同数据(负载天然均衡),EP 的负载由 router 决定、可能极端不均;
  • 缓解:a) router aux loss(惩罚不均,收敛质量有代价);b) 节点限制路由(限制通信扩散);c) 容量因子/动态跳过(可能丢 token);d) 更细粒度的专家切分(load 更平滑);
  • 极端情况:某专家 token 数超过其处理能力 → 该 rank 成为瓶颈(吞吐塌陷)或 token 被丢弃(质量损失);生产上通常两者都有兜底。

通过要点

  • EP 切专家维(模型容量维),不切 batch、不切序列;
  • 通信 = all-to-all 路由(token 搬运),不是权重同步;
  • 负载均衡是 EP 的头号工程问题,与 router 设计强耦合。

官方主参考