跳转至

XTuner Domino EP

导言

XTuner 的 Domino EP 不是一种新的 EP 通信算子,而是位于 MoE 层内部的跨微批调度方法:把多个原本独立做梯度累积的 micro-batch 一起送入模型,用异步通信流把一个 micro-batch 的 token dispatch/combine 与另一个 micro-batch 的专家计算重叠。

普通 EP 定义专家如何切分;All-to-All、DeepEP、AGRS 决定 token 如何搬运;Domino EP 决定多个 micro-batch 如何交错;MC2 则把相邻通信和矩阵乘融合进一个算子。只有先分清这些层次,才能正确讨论它们的优劣和组合关系。

HybridEP 论文进一步提出了一个更上游的问题:跨数据中心带宽太低、通信已经无法完全隐藏时,是否还应坚持“专家不动、所有 token 都跨域搬运”?它通过比例 \(p\)token All-to-All压缩 expert AllGather 之间选择,改变的是通信对象与专家放置,而不是给 DeepEP 或 MC2 换一个名字。

2026 年 8 月的增量观察再补两层:MoonEP 与 UltraEP 都根据当前 micro-batch 的真实路由结果复制热点 expert,但 MoonEP 自己拥有 dispatch/combine 并追求每 rank 固定 S×K 行,UltraEP 则把配额规划、跨层副本 buffer 和权重/梯度同步做成独立运行时,继续复用外部 dispatcher 与 Grouped GEMM。HyperParallel-MoE 则在 Ascend A3 上把 Dispatch、两次 GMM、SwiGLU 和 Combine 编译成 AIC/AIV 瓦片任务流。这些路线分别回答“热点怎么摊平”和“单个 MoE-FFN 怎么细粒度交错”,都不是 XTuner 当前已有开关。

XTuner Domino EP cross-microbatch overlap

自绘逻辑图:微批 A 的专家计算与微批 B 的通信在不同 CUDA stream 上交错,减少显式通信气泡。

结论

截至 XTuner commit 9989fb7,结合 HybridEP 论文后,相关技术可以按下面五层理解:1

加入 MoonEP、UltraEP 与 HyperParallel-MoE 后,还应额外区分“冗余 expert 负载均衡运行时”和“完整 MoE-FFN 瓦片调度”两层;下面保留原有五层,并在表尾做增量扩展。

层次 技术 解决的问题 是否能与 Domino EP 组合
并行语义 普通 EP 把不同 experts 放到不同 rank,路由 token 后再合并 Domino EP 的基础
通信对象与放置 HybridEP 论文 跨域时在搬 token 与搬压缩 expert 之间选择 思想上可组合,但 XTuner 当前没有该实现
通信后端 All-to-All / DeepEP / AGRS 具体怎样 dispatch 和 combine token 可以,三选一作为底层 dispatcher
调度方法 Domino EP 怎样用多个 micro-batch 隐藏 EP 通信 本身不替代 dispatcher
融合算子 MC2 在一个 micro-batch 内融合通信与 GMM/MM 思想上正交,但 XTuner 当前未接入该 dispatcher
负载均衡与冗余 expert MoonEP 怎样复制热点 expert、预取权重,并把每个 rank 的 routed token 行数固定 概念上可置于 Domino 下方,但 XTuner 当前没有该后端
负载均衡与冗余 expert UltraEP 怎样按本批 exact load 联合求副本与 quota,并复用外部 dispatcher/GMM 概念上可置于 Domino 下方,但 XTuner 当前没有该运行时
完整 MoE 瓦片调度 HyperParallel-MoE 怎样把 Dispatch、GMM、SwiGLU、GMM、Combine 编译成 AIC/AIV 任务流 与 Domino 粒度不同,但硬件与接口都需重新接入

Domino HybridEP DeepEP and MC2 optimize different EP layers

概念图:Domino 决定何时搬,HybridEP 决定搬什么,DeepEP 优化怎么搬,MC2 则把搬运与专家计算融合。四者作用层级不同。

因此,最准确的比较不是“Domino EP 和 DeepEP 谁替代谁”,而是:

  1. All-to-All、DeepEP、AGRS 比通信实现。
  2. Domino EP 比跨微批调度。
  3. MC2 比单微批内的算子融合。
  4. HybridEP 比跨域场景下的数据/专家混合传输与放置。
  5. MoonEP 比路由偏斜下的冗余 expert 规划与固定计算量。
  6. UltraEP 比 exact-load 驱动的 quota 复制、跨层 buffer 与副本状态同步。
  7. HyperParallel-MoE 比单微批完整 MoE-FFN 的瓦片级异构调度。

一句话判断

如果 profiler 中 EP 通信很长、专家 GEMM 足以覆盖通信,并且显存能容纳两个微批的在途状态,Domino EP 才更可能有效;否则异步事件、更多激活和更小 GEMM 的开销可能抵消收益。

执行路径

XTuner 的入口参数是 intra_layer_micro_batch。训练引擎不再逐个 micro-batch 调用模型,而是按该参数切成列表;相应的梯度累积次数也会除以这个数。2

for i in range(0, len(data_batches), intra_layer_micro_batch):
    data_batch = data_batches[i : i + intra_layer_micro_batch]
    seq_ctx_list = [item["seq_ctx"] for item in data_batch]
    loss_ctx_list = [item["loss_ctx"] for item in data_batch]

    if self.intra_layer_micro_batch == 1:
        output = self.model(seq_ctx=seq_ctx_list[0], loss_ctx=loss_ctx_list[0])
    else:
        output = self.model(seq_ctx=seq_ctx_list, loss_ctx=loss_ctx_list)

进入每个 MoE decoder layer 后,_micro_batch_forward 分成三个阶段:3

  1. 批量准备:各微批依次完成 attention、router 和异步 dispatch_preprocess
  2. 分发与专家计算:为各微批发起 dispatch,只在消费结果前建立必要的事件依赖,然后执行 experts 和 combine_preprocess
  3. 批量回收:发起各微批的 combine,最后逐个 combine_postprocess,再做残差和 shared experts 合并。

下面是从实际实现压缩出的关键控制流,省略了上下文保存代码:

for hidden_states, seq_ctx, position_embeddings in micro_batches:
    residual, hidden_states, router_results = self._pre_moe_forward(...)
    pre_dispatched = self.dispatcher.dispatch_preprocess(..., async_op=True)

for router_results, pre_dispatched in zip(router_results_list, pre_dispatched_list):
    dispatched = self.dispatcher.dispatch(..., async_op=True)
    post_dispatched = self.dispatcher.dispatch_postprocess(..., async_op=True)
    experts_out = self.experts(post_dispatched["hidden_states"], ...)
    pre_combined = self.dispatcher.combine_preprocess(..., async_op=True)

for item in pre_combined_list:
    combined_list.append(self.dispatcher.combine(..., async_op=True))

关键不在 Python 循环本身,而在 dispatcher 维护的通信 stream、计算 stream 和 CUDA event。CPU 可以继续提交后续微批的工作;GPU 则在真正使用数据的位置等待事件,从而形成“微批 A 算 experts,微批 B 做 dispatch/combine”的流水。

这也解释了它为什么叫 Domino:一个微批的通信被下一个微批的计算接力遮住。但它仍然不是完整的 pipeline parallel 调度,也不是原论文实现的直接复制。

通信后端

XTuner 的 build_dispatcherep_size > 1 时支持 deepepall2allagrs;未指定时默认选择 all2all4 Domino EP 对这三种后端调用同一组 preprocess/dispatch/combine 接口。

All-to-All

普通 All-to-All 路径先按 expert 排列 token,交换各 rank 的 token 计数,计算可变长 input_splitsoutput_splits,再调用 autograd 版本的 all_to_all_single5

优点是语义直接、依赖少、最适合作为正确性基线;缺点是负载不均时通信尺寸不规则,而且 token count、重排和两次 All-to-All 的成本更容易暴露在关键路径上。

这里的“普通 EP”通常就是“EP 语义 + 普通 All-to-All dispatcher + intra_layer_micro_batch=1”,而不是另一种与 All-to-All 无关的算法。

DeepEP

DeepEP 是为 MoE dispatch/combine 定制的通信库。XTuner 使用 low-latency buffer,先计算 dispatch layout,再以 async_finish=True 发起 dispatch,并通过 event handle 表达依赖;实现还把 DeepEP 通信占用的 SM 数设为 20。6

它的优势是对 NVLink/RDMA 拓扑、跨节点转发和低延迟 kernel 做了专门优化;代价是依赖自定义 CUDA 通信栈,对版本、拓扑和 buffer 配置更敏感。需要特别注意:XTuner 的构建脚本固定的是 DeepEP v1.2.1 commit 9af0e0d,不能把 DeepEP 当前主分支或 V2 的公开 benchmark 直接当成 XTuner 实测结果。7

DeepEP 负责让一次通信更快,Domino EP 负责尽量把这次通信藏起来。 二者通常是互补关系。

这里还要纠正一个容易由 API 名称引起的误读:XTuner 确实以 low_latency_mode=True 创建 DeepEP buffer,但固定版本的调用路径是 get_dispatch_layout -> Buffer.dispatch -> 外部 expert GMM -> Buffer.combine,并没有直接调用公开 API low_latency_dispatch8 因而本文讨论的是 XTuner 固定适配器的真实算子边界,而不是 DeepEP 主分支所有模式的统称。

为什么不是 MC2

DeepEP 并没有把通信语义变成另一种集体通信:它完成的仍是 MoE 的 dispatch All-to-All 与 combine All-to-All。区别在于,通用 All-to-All 把“已经按 expert 展开的 token 行”当作待发送对象;DeepEP 先理解 topk_idx,再把发送对象压缩为“这个 token 是否需要到达某个目标 rank”。

设 token \(i\) 选择的远端 expert 集合为 \(\mathcal{E}_i\),expert \(e\) 所属 rank 为 \(r(e)\)。普通 XTuner All-to-All 路径需要发送的 hidden 行数近似为:

\[ N_{\text{A2A}}=\sum_i |\mathcal{E}_i|, \qquad N_{\text{DeepEP}}=\sum_i \left|\{r(e):e\in\mathcal{E}_i\}\right| \]

DeepEP 的 layout kernel 明确维护 is_token_in_rank [T,R]:同一个 token 即使选择了同一远端 rank 上的多个 experts,也只把 hidden state 发到该 rank 一次,到达后再按 expert 本地展开。9 例如一个 token 的 Top-8 分别命中 4 个远端 rank、每个 rank 2 个 experts,普通路径发送 8 行 hidden,DeepEP 只发送 4 行;这里只能说 hidden payload 约减半,不能据此宣称端到端必然 加速。若每个 expert 都位于不同 rank,则这一项没有字节收益。

第二层收益来自专用通信内核。跨节点 kernel 把 RDMA sender、RDMA/NVLink forwarder 和 NVLink receiver 分配给不同 warp,让数据按 chunk 在节点内外转发,减少通用 collective 周围的 pack、动态 split、同步和 kernel launch 开销。10 这是一种通信子步骤融合,但 expert GMM、SwiGLU 和 down projection 仍在框架侧执行。因此它与 MC2 的边界不同:MC2 把 expert 权重交给 fused operator,在算子内部安排通信—GMM 流水;XTuner 的 DeepEP 路径只交付 recv_x,随后才调用外部 Grouped GEMM。

三个独立加速层次

  1. 减少重复激活:Top-k 多个 experts 落在同一目标 rank 时,hidden state 按 rank 去重发送。
  2. 缩短一次通信:用 NVLink/RDMA 感知的 dispatch/combine kernel 代替通用变长 All-to-All 的控制路径。
  3. 隐藏剩余通信async_finish=True、event 和限定的通信 SM 让 Domino 有机会用另一微批的 expert 计算覆盖通信。

前两项属于 DeepEP,第三项是 DeepEP 为 Domino 提供的异步边界;三者不能混成一个“融合算子加速比”。

完整运行路径

对象 DeepEP 产生或消费 后续所有者 生命周期
x [T,H]topk_idx [T,K] 计算 layout 并发起 dispatch DeepEP buffer dispatch 前后
recv_x [T_recv,H] dispatch 输出的已路由 token 框架侧 expert GMM combine 前
handle、counts、event 路由元数据与异步完成信号 combine、反向和计算 stream 一次 MoE 通信周期
expert_out [T_recv,H] combine 的输入 由框架侧 GMM 产生 combine 前
combined_x [T,H] combine 后返回原 token 所属 rank 下一层与残差路径 当前层结束

XTuner DeepEP V1 operator boundary and data flow

机制图:DeepEP 固定适配器拥有 token dispatch/combine、buffer、handle 与 event;expert 权重和 Grouped GEMM 仍由框架持有。图中反向路径由 combine/dispatch 的逆调用构成。

把源码归一化为不省略关键对象的伪代码,前向路径如下;反向则按相反顺序消费保存的 handle、counts 与路由排列:

layout = get_dispatch_layout(topk_idx, num_experts, ep_group)
recv_x, recv_topk_idx, recv_topk_weight, counts, handle, dispatch_event = \
    deep_ep_buffer.dispatch(
        x=x,
        topk_idx=topk_idx,
        topk_weight=topk_weight,
        layout=layout,
        async_finish=True,
    )

dispatch_event.wait(compute_stream)
permuted_x, tokens_per_expert, inverse_map = permute(
    recv_x, recv_topk_idx, counts
)
gate = grouped_gemm(permuted_x, expert_w1)
expert_hidden = silu(gate[:, 0::2]) * gate[:, 1::2]
expert_out = grouped_gemm(expert_hidden, expert_w2)
recv_out = unpermute(expert_out, inverse_map, recv_topk_weight)
combined_x, combine_event = deep_ep_buffer.combine(
    recv_out, handle=handle, async_finish=True
)
save_for_backward(handle, counts, inverse_map, dispatch_event, combine_event)

DeepEP compared with generic All-to-All five-view mechanism

DeepEP 五视图:按 rank 去重 hidden、节点内外分层转发、dispatch—外部 GMM—combine 的所有权边界,以及 event 驱动的可重叠时序。该图解释机制,不代表固定硬件上的实测加速比。

效果与边界

从效果与资源占用看,DeepEP 优化的是 token 通信时间和可重叠性,并不自动减少 expert 激活。峰值显存仍要计入持久通信 buffer、recv_x、路由 handle,以及 Domino 同时保留的多个微批状态;async_finish=True 只表示结果可以通过 event 延后等待,不表示这些对象已经不存在。

DeepEP 官方固定版本的 standalone benchmark 表明其 dispatch/combine 能接近测试平台的 NVLink 或 RDMA 链路上限,但它没有给出与 XTuner 普通 All-to-All 在同一模型、同一 shape、同一调度下的 A/B,因此不能从该表推导“比 All-to-All 快多少”。真正需要测量的是:rank 去重后的实际行数、dispatch/combine 裸时延、通信 SM 占用、Domino 覆盖比例、峰值显存和端到端 step time。

MoonEP

MoonEP 把 DeepEP 路径仍可能暴露的另一个瓶颈拉到台前:即使 token 搬得足够快,只要 router 把大量 token 压到少数 expert,最忙 rank 的 Grouped GEMM 仍决定整层尾延迟。MoonEP 最新公开仓库 commit 0f385f0 的做法不是丢 token 或修改 top-k 语义,而是在线规划热点 expert 的冗余副本,把副本权重预取到空槽,再把 routed token 直接写入最终 virtual-expert 分组位置。23

设当前 rank 的输入 token 数为 \(S\)、每个 token 选择 \(K\) 个 experts。MoonEP 的目标是让每个 rank 承担固定的有效 routed 行数:

\[ N_r=S\times K,\qquad r\in[0,R) \]

通信 shard 还会为 virtual-expert 分组加入 padding,因此物理容量是 NvS = S*K + padding。在线 planner 根据 tokens_per_expert 选择复制哪些热点 expert、每个 token 的目的 rank 与目的槽位;权重预取则把 home expert 的行搬入对端预留区。这样,动态的是副本放置和 token→副本映射,进入 GMM 的总行数与 shape 则保持静态。

MoonEP five-view mechanism diagram

源码机制教学重绘:MoonEP 用在线计划、远端 token 写入、expert 权重预取和副本梯度归并,把路由偏斜转换为固定工作量。图固定到 commit `0f385f0`,不表示 XTuner 已接入。

MoonEP 不是只有一次 dispatch;训练闭环至少包含下列对象:

对象 形状或内容 生产者 消费者与生命周期
hidden_shard [S,H] BF16 框架与 router dispatch;一次 MoE 调用
topk_idx / tpe [S,K] / [E] router 与直方图 planner、dispatch、combine;plan 需跨反向保存
plan.dst / experts_to_copy token 目的位置、[R,B] 副本表 GPU planner dispatch、prefetch、combine 与反向
NVL token shard [NvS,H] BF16 dispatch 直接远端写 virtual-expert GMM;zero-copy view 会被下一次 dispatch/combine 覆盖
expert weights [E+B,H,H′] BF16 对称内存 框架与 prefetch [0,E) 是 home 行,[E,E+B) 是副本槽
cu_seqlens [E+B] 分组边界 plan/dispatch epilogue virtual-expert GMM
grad / reduce pool [E+B,H,H′][R,B,H,H′] FP32 backward GMM 与远端读 把副本梯度累加回 home expert;进程级 pool 可跨层复用

下面是归一化伪代码。它把“先削平热点、再固定目的 shard、最后归并副本梯度”三件中心工作展开;变量名对齐公开 API,但不是逐行翻译 Triton/CUDA 内核:

def build_balanced_plan(topk_idx, tokens_per_expert, rank_count, rows_per_rank):
    assignments = initial_home_assignments(topk_idx, rank_count)
    rank_load = count_rows_per_rank(assignments, rank_count)
    experts_to_copy = empty_copy_table(rank_count)

    while max(rank_load) > rows_per_rank:
        source_rank = argmax(rank_load)
        destination_rank = argmin(rank_load)
        expert_id = largest_movable_expert_on_rank(
            assignments,
            source_rank,
            tokens_per_expert,
        )
        movable_rows = min(
            count_expert_rows(assignments, expert_id, source_rank),
            rows_per_rank - rank_load[destination_rank],
        )
        copy_slot = reserve_redundant_expert_slot(experts_to_copy, destination_rank)
        move_token_slice(
            assignments=assignments,
            expert_id=expert_id,
            source_rank=source_rank,
            destination_rank=destination_rank,
            row_count=movable_rows,
            copy_slot=copy_slot,
        )
        rank_load[source_rank] -= movable_rows
        rank_load[destination_rank] += movable_rows

    assignments = add_static_padding(assignments, rows_per_rank)
    assert all(load == rows_per_rank for load in rank_load)
    return materialize_plan(assignments, experts_to_copy)


def moonep_forward(hidden_shard, topk_idx, topk_weight, expert_weights, config):
    tokens_per_expert = histogram(topk_idx, minlength=config.num_experts)
    plan = build_balanced_plan(
        topk_idx=topk_idx,
        tokens_per_expert=tokens_per_expert,
        rank_count=config.ep_size,
        rows_per_rank=config.tokens_per_rank * config.topk,
    )
    routed_view, cu_seqlens = moonep_dispatch(
        hidden_shard=hidden_shard,
        topk_idx=topk_idx,
        plan=plan,
        zero_copy=True,
    )
    moonep_prefetch_weight(expert_weights, plan.experts_to_copy)
    expert_output = virtual_expert_grouped_gemm(
        routed_view,
        expert_weights,
        cu_seqlens,
    )
    output_shard = moonep_combine(expert_output, topk_weight, plan)
    saved_state = save_for_backward(plan, cu_seqlens, topk_idx, topk_weight)
    return output_shard, saved_state


def moonep_backward(output_grad, saved_state, expert_weights, grad_pool):
    routed_grad, cu_seqlens = moonep_dispatch(
        hidden_shard=output_grad,
        topk_idx=saved_state.topk_idx,
        plan=saved_state.plan,
        zero_copy=True,
    )
    input_grad_routed, expert_grad = virtual_expert_grouped_gemm_backward(
        routed_grad,
        expert_weights,
        cu_seqlens,
        grad_pool,
    )
    input_grad = moonep_combine(
        input_grad_routed,
        saved_state.topk_weight,
        saved_state.plan,
    )
    moonep_reduce_duplicate_gradients(
        expert_grad,
        saved_state.plan.experts_to_copy,
        grad_pool,
    )
    return input_grad, expert_grad.home_rows

训练时公开说明要求 B=E/R 个预取槽;推理可降低 B,仓库建议从 3–4 起测,超出预取容量的 expert 会回读 home 权重。zero-copy 也有严格生命周期:返回 tensor 是通信 buffer 的 alias,下一次 dispatch/combine 会覆盖它,不能把该 view 跨调用长期保存或交给 autograd 任意留存。24

新增显存不能只数 token shard。按单个线性权重矩阵的量级,可把增量上界拆成:

\[ M_{\text{add}}\approx B|W_e|+4(E+B)HH' + 4RBHH' + M_{\text{plan}}+M_{\text{comm-buffer}} \]

其中第一项是 BF16 副本槽,两个系数 4 的项是 FP32 grad/reduce buffer;实际实现用进程级 pool 跨层共享,不能把它机械地乘层数,也不能忽略其峰值存在。

MoonEP official communication benchmark against DeepEP v2

MoonEP 仓库自报通信对比:单机 H20、EP=8、E=384、H=7168、K=8、S=8192/rank、32 SM,并扫描不同 MaxVio。MoonEP 的 dispatch 柱包含 planning/prefetch;脚本把 `grad_reduce` 视为可重叠而未计入图中关键路径。该图不能用来给 XTuner、MC2 或 HyperParallel 做跨硬件排名。

因此 MoonEP 与 Domino 的关系是:MoonEP 尝试缩短并固定每个微批的通信/专家尾部,Domino 尝试拿另一个微批的计算遮住剩余尾部。 两者概念上能分层,但 XTuner 当前 dispatcher 不接受 MoonEP 的 symmetric-memory 权重布局、plan 生命周期和梯度 pool;同时启用后还会共同争用 NVLink、SM 与显存,收益不能直接相加。25

UltraEP

UltraEP 与 MoonEP 处理的是同一个一阶瓶颈:router 已经决定 top-k 后,最热 expert 所在 rank 的 token 行数远高于平均值,整层只能等待最慢 rank。它的核心差异不是换一种 token collective,而是在 router 与外部 dispatcher 之间插入一个独立的 exact-load balancing runtime:当前 layer、当前 micro-batch 的 gate 结果一产生,就联合决定热点 expert 的副本位置和每个物理实例应承担的 quota,再把 logical routing 改写为 physical routing。26

可以把 UltraEP 分成三层,避免把论文思想、通用机制和仓库补丁混为一谈:

  1. 概念:不再用历史热度预测下一批,而是用本批 post-gate load 直接压低最忙 rank 的执行上限。
  2. 可复用机制:固定 master experts,只在每 rank 的少量冗余槽中物化热点副本;quota 同时约束副本放置与 token 分流;前向复制权重,反向把副本梯度归并回 master。
  3. 框架补丁:本文固定到 UltraEP v1.0.0、commit 94cab09Manager 注册框架持有的 master weight/grad 指针并暴露跨层复用 buffer;external dispatcher 仍搬 token,external Grouped GEMM 仍完成 expert 计算。它不是 XTuner dispatcher="ultraep" 这样的现成配置。27

UltraEP exact-load replication metaphor

认知类比:主 expert 像固定抽屉,UltraEP 读取本批实时负载后展开可复用的临时副本槽,并只把超出目标上限的 quota 分出去。该图只解释直觉,精确对象与约束见后文。

exact load 不等于无条件完美均衡

Exact 描述的是输入给 planner 的 load 来自当前 routing,而不是历史估计;near-optimal 描述的是在 N_slot、同一 logical expert 不得在同 rank 重复、最小有效 quota、NVLink domain 和关键路径时限等约束下,寻找尽可能低的最大 rank load。槽数不足或 NVLink domain 小于 EP group 时,最终 imbalance 可以高于 1.0

配额规划

\(\lambda_{r,e}\) 是源 rank \(r\) 在当前 micro-batch 中路由给 logical expert \(e\) 的 token 数,\(\lambda_e=\sum_r\lambda_{r,e}\)。master expert 的 home rank 为 \(h(e)\),复制前 rank \(r\) 的 expert 计算量是:

\[ \ell_r=\sum_{e:h(e)=r}\lambda_e \]

UltraEP 不先拍脑袋选副本再 round-robin 分 token,而是搜索一个最小可行上限 \(\tau\)。对候选 \(\tau\),每个 rank 的超额和余量分别为:

\[ \operatorname{exc}_r(\tau)=\max(\ell_r-\tau,0),\qquad \operatorname{slk}_r(\tau)=\max(\tau-\ell_r,0) \]

feasibility oracle 从最过载 rank 的最热 expert 开始,把最多 \(\delta=\min(\operatorname{exc},\operatorname{slk},\text{remaining expert load})\) 的 quota 转给仍有冗余槽且尚未承载该 expert 的目标 rank;若 \(\delta<u_{\min}\) 就不创建这个低收益副本。可行则继续向更低 \(\tau\) 搜索,不可行则提高下界。最终 quota \(u_{e,t}\) 和 source split \(q_{r,e,t}\) 满足:28

\[ \sum_{t\in\mathcal{H}(e)}q_{r,e,t}=\lambda_{r,e},\qquad \sum_r q_{r,e,t}=u_{e,t} \]

前一式保证每个源 rank 的原始路由数量不变,后一式保证物理实例真正收到 solver 为它预留的 quota。locality-aware decomposition 会先让 source rank 消耗本地实例的 quota,再按剩余容量比例分配远端需求;它只改变“哪个来源使用 quota”,不改变已求得的实例负载上限。

用归一化的小例子看,四个 rank 初始负载为 [40,160,32,24],平均值是 64。若每个空闲 rank 都有一个可用槽,planner 可以把 R1 热点 expert 的 95 个负载单位分成三个有效副本 quota,使结果接近 [65,64,64,63]总路由量仍是 256,master 仍在 R1;改变的是一对多的 logical-to-physical 映射。 论文默认的最小有效 quota 是 1024 tokens,因此这个四 rank 数字只用于解释上限搜索,不是实际配置。28

下面的归一化伪代码完整展开 threshold、slot、quota、locality 和 deterministic rounding;它映射论文 Algorithm 1 与 placement.cu,但省去了 warp/shared-memory 的性能实现细节:

import math


def solve_exact_load_plan(load, home_rank, slots_per_rank, beta, min_quota):
    # load[r][e] = current-microbatch token count from source rank r to expert e
    rank_count = len(load)
    expert_count = len(load[0])
    expert_load = [sum(load[r][e] for r in range(rank_count))
                   for e in range(expert_count)]
    initial_rank_load = [
        sum(expert_load[e] for e in range(expert_count)
            if home_rank[e] == r)
        for r in range(rank_count)
    ]
    total_load = sum(initial_rank_load)
    low = max(math.ceil(beta * total_load / rank_count), 0)
    high = max(initial_rank_load)
    best_quota = None
    best_slots = None

    while low < high:
        threshold = (low + high) // 2
        excess = [max(value - threshold, 0) for value in initial_rank_load]
        slack = [max(threshold - value, 0) for value in initial_rank_load]
        quota = [[0 for _ in range(rank_count)] for _ in range(expert_count)]
        occupied = [[False for _ in range(rank_count)] for _ in range(expert_count)]
        slots_used = [0 for _ in range(rank_count)]

        for expert_id in range(expert_count):
            source = home_rank[expert_id]
            quota[expert_id][source] = expert_load[expert_id]
            occupied[expert_id][source] = True

        feasible = True
        source_order = sorted(range(rank_count), key=lambda r: (-excess[r], r))
        for source in source_order:
            experts = [e for e in range(expert_count) if home_rank[e] == source]
            experts.sort(key=lambda e: (-expert_load[e], e))
            for expert_id in experts:
                transferable = quota[expert_id][source]
                while excess[source] > 0 and transferable > 0:
                    targets = [
                        target for target in range(rank_count)
                        if slack[target] > 0
                        and slots_used[target] < slots_per_rank
                        and not occupied[expert_id][target]
                    ]
                    if not targets:
                        break
                    target = max(targets, key=lambda r: (slack[r], -r))
                    moved = min(excess[source], slack[target], transferable)
                    if moved < min_quota:
                        break
                    quota[expert_id][source] -= moved
                    quota[expert_id][target] += moved
                    occupied[expert_id][target] = True
                    slots_used[target] += 1
                    excess[source] -= moved
                    slack[target] -= moved
                    transferable -= moved
            if excess[source] > 0:
                feasible = False
                break

        if feasible:
            best_quota = quota
            best_slots = occupied
            high = threshold
        else:
            low = threshold + 1

    if best_quota is None:
        best_quota = [[0 for _ in range(rank_count)] for _ in range(expert_count)]
        best_slots = [[False for _ in range(rank_count)] for _ in range(expert_count)]
        for expert_id in range(expert_count):
            source = home_rank[expert_id]
            best_quota[expert_id][source] = expert_load[expert_id]
            best_slots[expert_id][source] = True

    split = [[[0 for _ in range(rank_count)] for _ in range(expert_count)]
             for _ in range(rank_count)]
    for expert_id in range(expert_count):
        residual_demand = [load[source][expert_id]
                           for source in range(rank_count)]
        residual_quota = [best_quota[expert_id][target]
                          for target in range(rank_count)]

        for target in range(rank_count):
            local_count = min(residual_demand[target], residual_quota[target])
            split[target][expert_id][target] += local_count
            residual_demand[target] -= local_count
            residual_quota[target] -= local_count

        for source in range(rank_count):
            demand = residual_demand[source]
            capacity = sum(residual_quota)
            if demand == 0:
                continue
            assert capacity >= demand
            raw = [demand * residual_quota[target] for target in range(rank_count)]
            allocated = [value // capacity for value in raw]
            remaining = demand - sum(allocated)
            remainder_order = sorted(
                range(rank_count),
                key=lambda target: (-(raw[target] % capacity), target),
            )
            for target in remainder_order[:remaining]:
                allocated[target] += 1
            for target in range(rank_count):
                split[source][expert_id][target] += allocated[target]
                residual_quota[target] -= allocated[target]

    return low, best_slots, best_quota, split

运行闭环

UltraEP 的关键所有权边界是:它计算 physical expert map,并负责副本 weight/grad 的物化与回收;它不接管 token dispatcher 或 Grouped GEMM。 公开接口先为一个 EP group 建一个 Manager,每层注册 master 指针;每个 rank 只预留相同数量的冗余槽。max_microbatches(real_layer_id, microbatch_slot) 映射为 virtual layer ID,避免 PP/VPP 或 Domino 一类多在途微批覆盖彼此的 plan。27

对象 Shape / 内容 生产者 消费者与生命周期
routing_map / probs [T,L] bool / float router update_placementreroute;当前 MoE 前向到对应反向
global exact load [R,L] int32 notify/all-gather + histogram quota solver;只在规划窗口使用
physical_to_logical / logical_to_physical physical slot 与 logical expert 的一对多映射 placement kernel weight sync、reroute、GMM;按 virtual layer slot 保存
quota 与 prefix [L,max_replicas] int32 placement 与 locality decomposition token-to-instance lookup;按 virtual layer slot 保存
master pointer pools 每层本地 expert 的 weight/grad 指针 framework 注册 weight sync / grad reduce;参数本体归 framework
replica weight buffer [N_slot,F_1/F_2],BF16 或配置精度 初始化分配、逐层 weight sync 外部 GMM;buffer 常驻、内容逐层重写
replica grad buffer [N_slot,F_1/F_2],当前实现 FP32 backward GMM grad_reduce;归并并清理后跨层复用
virtual layer ID (layer,mb_slot) 的环形槽 allocate_microbatch_slot 前向 plan、反向重物化与 join;对应反向结束后复用
expanded physical routes [T,P] float / bool reroute 外部 dispatcher;autograd 反向聚回 logical expert
token / expert output [T_recv,H] / [T,H] 外部 dispatcher、GMM、combine 当前 MoE activation;不由 UltraEP buffer 持有

公开 README 的最小集成路径把四个核心调用放在 gate 与 token dispatch 之间;下面保留关键调用,不把示例仓库代码说成 XTuner 已接入的补丁:26

vid = manager.allocate_microbatch_slot(layer_id)
probs, routing_map = router(hidden)
manager.update_placement(vid, routing_map)
ws_event = manager.weight_sync(vid, async_finish=True)
probs, routing_map = manager.reroute(vid, probs, routing_map)
ws_event.current_stream_wait()
recv_tokens = dispatch(hidden, probs, routing_map)
recv_tokens = experts(recv_tokens, probs)
out = combine(recv_tokens)

完整教学路径还必须把外部 GMM、反向 restore、梯度归并和 slot 最后消费者展开:

def ultraep_forward(hidden, layer_id, manager, router, dispatcher,
                    master_w1, master_w2, replica_w1, replica_w2):
    virtual_id = manager.allocate_microbatch_slot(layer_id)
    residual = hidden
    logical_probs, logical_routes = router(hidden)
    manager.update_placement(virtual_id, logical_routes)
    weight_event = manager.weight_sync(virtual_id, async_finish=True)
    physical_probs, physical_routes = manager.reroute(
        virtual_id, logical_probs, logical_routes
    )
    weight_event.current_stream_wait()

    recv_x, dispatch_handle = dispatcher.dispatch(
        hidden, physical_probs, physical_routes
    )
    grouped_x, group_offsets, inverse_permutation = permute_by_physical_expert(
        recv_x, physical_routes
    )
    physical_w1 = concatenate_expert_rows(master_w1, replica_w1)
    physical_w2 = concatenate_expert_rows(master_w2, replica_w2)
    gate_up = grouped_gemm(grouped_x, physical_w1, group_offsets)
    expert_hidden = silu(gate_up[:, 0::2]) * gate_up[:, 1::2]
    expert_y = grouped_gemm(expert_hidden, physical_w2, group_offsets)
    recv_y = inverse_permute_and_weight(
        expert_y, inverse_permutation, physical_probs
    )
    output = dispatcher.combine(recv_y, dispatch_handle)
    saved = save_for_backward(
        virtual_id, logical_routes, physical_routes, physical_probs,
        dispatch_handle, grouped_x, group_offsets, inverse_permutation,
    )
    return residual + output, saved


def ultraep_backward(output_grad, saved, manager, dispatcher,
                     master_w1, master_w2, replica_w1, replica_w2):
    recv_output_grad = dispatcher.combine_backward(
        output_grad, saved.dispatch_handle
    )
    restore_event = manager.weight_sync(saved.virtual_id, async_finish=True)
    grouped_output_grad = permute_with_saved_inverse(
        recv_output_grad, saved.inverse_permutation
    )
    physical_w1 = concatenate_expert_rows(master_w1, replica_w1)
    physical_w2 = concatenate_expert_rows(master_w2, replica_w2)
    w2_grad, expert_hidden_grad = grouped_gemm_wgrad(
        grouped_output_grad, saved.group_offsets
    )
    restore_event.current_stream_wait()
    grouped_x_grad, w1_grad = grouped_gemm_dgrad_and_wgrad(
        expert_hidden_grad, saved.grouped_x, physical_w1, physical_w2,
        saved.group_offsets,
    )
    recv_x_grad = inverse_permute(grouped_x_grad, saved.inverse_permutation)
    input_grad = dispatcher.dispatch_backward(
        recv_x_grad, saved.dispatch_handle
    )
    write_master_and_replica_grad_buffers(w1_grad, w2_grad)
    grad_event = manager.grad_reduce(saved.virtual_id, async_finish=True)
    router_grad = reroute_backward_to_logical_experts(
        saved.virtual_id, saved.logical_routes, saved.physical_routes,
        saved.physical_probs,
    )
    attention_grad = run_attention_backward(input_grad)
    grad_event.current_stream_wait()
    release_virtual_layer_slot_after_last_consumer(saved.virtual_id)
    return input_grad + attention_grad, router_grad

这里的 release_virtual_layer_slot_after_last_consumer 是对生命周期约束的规范化表达,不是 UltraEP 当前公开 API;实现必须等该 virtual layer 对应的反向和异步归并都不再消费 plan/buffer 后,才能复用槽位。

UltraEP five-view mechanism diagram

UltraEP 五视图,自绘示意图:依次显示 master 固定与 quota 分流、exact-load 因果链、前后向路径、virtual layer/buffer 生命周期,以及 external dispatcher/GMM 的 tensor 边界。图固定到 arXiv v3 与 commit `94cab09`;四 rank 数字是归一化教学示例。

效果边界

论文 Figure 8 把前向关键路径画得很清楚:gate 后先收集 exact load,replication planning 与 reroute 在 compute stream 上推进;weight distribution 在 communication stream 上与 reroute 重叠,但 token dispatch 必须等待 replica 权重就绪。因此 UltraEP 缩短最忙 rank 的 GMM/A2A 尾部,同时把 plan + max(reroute, weight_sync) 加到前向关键路径;它不是“均衡免费”。29

UltraEP paper Figure 8 forward pipeline

来自 UltraEP 论文 Figure 8:replication planning 与 weight distribution 是标准 MoE 前向之外的额外步骤;reroute 可与权重分发并行,但 token dispatch 等待权重物化。

公开论文的主要训练证据固定在 BF16、64 GPU/rack 的 RSN:GLM4.5-106B 用 128 GPUs,Qwen3-235B 与 DeepSeek-V3-671B 用 256 GPUs;三个训练配置均为 EP64,并分别结合 DP/PP。各 baseline 从同一第 3500 个 global batch checkpoint 继续 20 个 batch。Figure 11 报告 UltraEP 的平均 rank imbalance 为 1.01–1.03,三模型平均达到 force-balanced ideal 的 94.6%,相对 Megatron-LM 平均提升 42%30

UltraEP paper Figure 11 end-to-end training

来自 UltraEP 论文 Figure 11:在三种 106B–671B 模型和固定 RSN/EP64 设置上比较 Megatron-LM、EPLB、LPLB、EPLB+、UltraEP 与 force-balanced ideal。它支撑论文内的训练吞吐与 imbalance 结论,不构成对 MoonEP、Domino、MC2 或 HyperParallel 的同口径排名。

同一论文还报告 prefill 平均达到 ideal 的 93.9%,相对 SGLang/EPLB 为 1.56×/1.29×,相对使用 exact load 但采用 EPLB placement + round-robin reroute 的 EPLB+ 为 5%–24%。这些是 Qwen3-235B/GLM4.7-358B 的 prefill RPS–TTFT 结果,不能写成训练吞吐提升。论文的生产 RefMoE-288B 结果是达到 ideal 的 92% 以上、比关闭 balancing 平均高 9.6%,但只说明多 rack、EP32,没有披露可复现的内部训练栈和精确 GPU 总数。30

仓库 test_e2e.py 还给了一个 EP64、256 experts、每 rank 4 master + 2 redundant slots、Top-8、8K tokens/rank 的 runtime 样本:31

初始 imbalance 1.51 2.01 3.03
最终 imbalance 1.01 1.01 1.01
update_placement 0.067 ms 0.072 ms 0.078 ms
reroute 0.039 ms 0.037 ms 0.036 ms
adaptive weight_sync 0.194 ms 0.195 ms 0.188 ms
overlapped grad_reduce 1.935 ms 2.181 ms 2.425 ms

这张表的 grad_reduce 已标为 overlapped,不能把各行直接相加成 step time。Figure 16 的 3.1–5.5× 也只比较同一 placement plan 下的 expert weight distribution backend,不是 token dispatch、MoonEP 或端到端训练加速;adaptive relay 的 1.3–1.8× 只说明高 fan-out 权重分发比 no-relay 更快。31

显存方面,冗余槽没有 optimizer state,且 weight/FP32 grad buffer 跨层复用。若单 expert 的两组权重元素数为 \(F_1+F_2\),每 rank 的主要增量上界近似为:

\[ M_{\text{add}}\approx N_{\text{slot}} \left[b_w(F_1+F_2)+4(F_1+F_2)\right] +M_{\text{plan}}(N_{\text{layer}}N_{\text{mb}},L,P)+M_{\text{comm}} \]

其中 \(b_w\) 是 weight element bytes,系数 4 来自当前 FP32 replica gradients。论文以 94 个 MoE 层的 Qwen3-235B 为例:跨层复用把每 rank 的一个冗余槽从所有层合计 3.3 GB weights + 6.6 GB gradients 降到 36 MB + 72 MB。另一方面,负载拉平还可降低最热接收 rank 的 MoE activation 峰值;这两件事分别作用于 新增副本状态原有热点 activation,不能合并成固定显存节省比例。32

当前公开发行还有明确适用条件:SM90/SM100,CUDA 12.3+/12.9+,PyTorch 2.10+,NVSHMEM 3.4.5,并要求 NVLink 承担域内 expert replication;训练 replica gradients 只支持 FP32。NVLink domain <=8 时实现强制 direct weight sync,更大域才根据 fan-out 自适应 relay。SGLang 集成仍标为 experimental/unreleased;公开 Megatron fork与 8-GPU demo 能证明接入路径,不能证明任意 XTuner recipe 可直接复用。26

与 MoonEP 比较

两者都比 history-based EPLB 更“晚”地做决定:等当前 gate 结果出现后再复制热点 expert,因此都支付在线规划和权重预取/分发成本。但它们不是同一实现的两个名字:

维度 MoonEP 0f385f0 UltraEP 94cab09
规划输入 当前 topk_idx/tokens_per_expert 当前 [R,L] post-gate exact load
目标 每 rank 固定 S×K 有效 routed 行,外加 NvS padding 在 slot/quota/locality 约束下最小化最大 rank load,通常接近而非强制完美相等
决策变量 virtual-expert plan、token 目的槽、experts_to_copy threshold、slot assignment X、instance quota U、source split Q
master/replica 布局 contiguous symmetric [E+B,H,H′],训练建议 B=E/R master 不重排;每 rank 配置少量 N_slot,pointer-register + 跨层 weight/grad buffer
token 路径所有权 自己实现 dispatch/prefetch/combine、zero-copy shard 与 fused permute 与 token dispatcher/GMM 解耦,只改 physical routes 和 replica state
权重 fan-out 直接预取到副本槽 persistent tile streaming;大 NVLink 域可 adaptive relay
反向 plan 复用、virtual expert backward、FP32 pool、duplicate grad reduce virtual layer ID 恢复前向 plan;重物化 weight;async replica-to-master grad reduce
公开规模证据 单机 H20、EP8 通信图;仓库无同名论文 128/256 GPU 研究实验、EP40/64 prefill/training、生产多 rack;论文 + repo runtime
关键限制 H20/EP8 公共路径、zero-copy 生命周期、静态 NvS、完整 symmetric layout SM90/100、NVSHMEM/NVLink、关键路径 weight sync、slot/domain floor、框架物理 expert 接口改造

因此更准确的判断是:MoonEP 用更强的 dispatcher/GMM 内存契约换取固定计算 shape;UltraEP 用更模块化的 quota runtime 与较少冗余槽换取大 EP/RSN 下的近理想负载。 MoonEP 的公共仓库图与 UltraEP 论文不共享硬件、模型、dispatcher、计时窗口或端到端边界,目前没有足够证据判定谁“绝对更快”。

把 UltraEP 放回本文分层后,组合关系如下:

  1. 与 All-to-All / DeepEP / AGRS:UltraEP 先把 logical routes 展开为 physical routes,后者再搬 token;论文集成使用 DeepEP hybrid-ep branch,但该 branch 名称不是本文跨数据中心 HybridEP 论文。
  2. 与 Domino:UltraEP 缩短一个 micro-batch 的最忙 rank 尾部,Domino 用另一 micro-batch 的计算隐藏剩余通信;virtual layer ID 已考虑多个在途 micro-batch,但两者尚无 XTuner 联合实现或 benchmark,且会争用 NVLink、通信 SM、activation 与 replica buffer。
  3. 与 MC2 / HyperParallel:这些 fused runtime 需要接管 expert weights 和 tile 依赖;动态 physical expert map 与逐层 replica weight deadline 必须进入编译/算子接口,因此不是可直接叠加的开关。
  4. 与跨数据中心 HybridEP:HybridEP 决定跨域搬 token 还是压缩 expert,UltraEP 在一个 NVLink scale-up domain 内复制热点实例;拓扑尺度不同,理论上可分层,当前没有共同实现证据。

UltraEP 的验收不能只看 planner 输出。至少要在同一 routing trace 下同时记录:pre/post rank imbalance、物理实例数、remote token ratio、plan/reroute/weight sync 裸时延、grad reduce 的未覆盖尾部、最热 rank activation、replica/dispatcher buffer 峰值、端到端 step time 与收敛;只有这些量保持模型、batch、精度、EP、拓扑和测量窗口一致时,才能给出数值收益。

AGRS

AGRS 在 XTuner 中可按 AllGather + ReduceScatter 理解:dispatch 时 AllGather hidden states,同时用小规模 All-to-All 交换 top-k IDs/weights;本地 experts 计算后,combine 用 ReduceScatter(sum) 把结果送回 token 所属 rank。11

等价性前提

这里的关键不是 AllGather 天生比 All-to-All 搬得少,而是 grouped router 把路由问题改写成了一个规则的代数分解。当前 XTuner 路径固定 ep_size = router_n_groups = top_k = 8:experts 被划成 8 个连续 group,每个 token 在每个 group 中只选 1 个 expert;一个 group 对应一个 EP rank。13 因而每个 rank 都必然需要看到每个 token,但只需计算这个 token 属于本 rank 的一项 expert 贡献。

\(R\) 个 rank 各有输入 \(X_r\in\mathbb{R}^{S\times H}\)。AllGather 后,每个 rank \(d\) 都持有:

\[ X^{\text{AG}}=\operatorname{concat}(X_0,\ldots,X_{R-1}) \in\mathbb{R}^{RS\times H} \]

小规模 metadata All-to-All 把 rank \(d\) 对应的 expert ID 与路由权重送到该 rank。其本地 experts 计算加权贡献 \(Z_d\in\mathbb{R}^{RS\times H}\),最后:

\[ Y_r=\sum_{d=0}^{R-1} Z_d[rS:(r+1)S] \]

ReduceScatter(sum) 恰好同时完成“跨 rank 求和”和“把第 \(r\) 段送回源 rank \(r\)”这两件事。以 4 个 rank 的缩小示例看:token \(x_0\) 被 AllGather 到 4 个 rank,每个 rank 只算自己的 group contribution \(z_{0,d}\),ReduceScatter 再返回 \(y_0=z_{0,0}+z_{0,1}+z_{0,2}+z_{0,3}\)。这与普通 All-to-All 把 \(x_0\) 的 4 份 expert 路由结果送回源 rank 后求和,在数学上等价。

为什么可能更快

它用更规则的 collective 替换可变长 token All-to-All,在某些拓扑和 collective 实现上更容易获得稳定吞吐。不过当前实现有严格前提:必须使用 grouped router,并要求 ep_size == router_n_groups == 812

在当前 \(K=R=8\)、每个 rank 一个 group 的约束下,理想化的大 hidden 通信量并没有数量级差异。忽略 metadata 后,每个 rank 单向发送或接收的主 payload 都约为:

\[ V_{\text{hidden}}\approx (R-1)SHb_x \]

其中 \(b_x\) 是单个 hidden 元素的字节数。AGRS 更快时,主要省在内存重排与控制路径,而不是凭空少传一份 activation:

  1. 省掉发送前的大张量复制:普通路径先把 [S,H] 按 Top-k 展开为接近 [SK,H],再按目标 rank 排列;AGRS 直接 AllGather 连续的 [S,H]
  2. 省掉动态 split 同步:普通路径统计 token count、交换 count,并把 split size 搬到 CPU 形成 Python list;AGRS 的主 collective 是等长 shape。
  3. 减少一次源端重排:普通 combine 先做反向 All-to-All,再在源 rank 按 token 恢复排列并加权求和;AGRS 在目标 rank 先 unpermute(..., probs),随后由 ReduceScatter(sum) 合并“回传 + expert 求和”。
  4. 利用成熟规则 collective:固定大小的 AllGather/ReduceScatter 更容易使用 NCCL 的 ring/tree 算法和连续访存,但实际优劣仍取决于拓扑、消息大小和 collective 实现。

因此,AGRS 更准确的表述是:用可预测的全量复制换掉不规则 token 搬运与多次重排。它不是无条件优于 All-to-All,更不是通信—GMM 融合;Grouped GEMM 仍然位于两个 collective 之间,由框架独立调用。

完整运行路径

阶段 主对象与形状 所有者 与普通 All-to-All 的差异
路由 x [S,H]topk_idx/topk_weight [S,8] router 每个 group 恰选一个 expert
主 dispatch all_x [8S,H] AllGather 不先复制成 [8S,H] 再做变长 A2A
metadata dispatch local_idx/local_weight [8S,1] 小规模 All-to-All 只交换 ID/weight,不交换大 hidden
expert 计算 permuted_x -> expert_out [8S,H] 框架侧 GMM 与 collective 没有算子融合
本地恢复 local_out [8S,H] unpermute(..., probs) 在目标 rank 提前乘路由权重
返回与求和 y [S,H] ReduceScatter(sum) 一次 collective 同时回传并求和
all_x = all_gather_autograd(x, ep_group)                  # [R*S, H]
local_idx = all_to_all_small(transpose(topk_idx))         # [R*S, 1]
local_weight = all_to_all_small(transpose(topk_weight))   # [R*S, 1]

permuted_x, tokens_per_expert, inverse_map = permute(all_x, local_idx)
gate = grouped_gemm(permuted_x, local_expert_w1)
expert_hidden = silu(gate[:, 0::2]) * gate[:, 1::2]
expert_out = grouped_gemm(expert_hidden, local_expert_w2)
local_out = unpermute(expert_out, inverse_map, local_weight)
y = reduce_scatter_autograd(local_out, op="sum", group=ep_group)

AGRS compared with token All-to-All five-view mechanism

AGRS 五视图:grouped router 的一 rank 一贡献前提、AllGather hidden 与小型 metadata All-to-All、框架侧 GMM,以及 ReduceScatter(sum) 合并回传和专家求和。主 hidden 字节量同阶,收益来自规则 collective 与更短的数据整理路径。

效果与边界

此外,AGRS 会 AllGather activation,通信量和在途 buffer 不一定更小;ReduceScatter 的求和顺序也与普通路径不同。XTuner 的对照测试使用 atol=rtol=1e-2,对应 PR 也明确提示了浮点归约顺序造成的小误差。14

公开 PR 没有提供可复用的 AGRS 性能表,所以不能给出通用加速倍数。若 \(K<R\)、路由不是“一 group 一 rank”,AllGather 会让不需要某个 token 的 rank 也接收 activation;如果 [RS,H] buffer 造成显存压力,或目标拓扑的 AllGather/ReduceScatter 实现不占优,AGRS 甚至可能更慢。测试时至少应分开记录 collective 时间、permute/unpermute 时间、host sync、峰值显存与数值误差。

三种通信后端怎么选

Three postal routes metaphor for All-to-All, DeepEP, and AGRS

直观类比:普通 All-to-All 按 expert 复制“信件”;DeepEP 对同一目标 rank 只装一袋、到站再分 expert;AGRS 把原件广播到各站,各站计算一项贡献后在返回时相加。类比只帮助建立直觉,精确张量流以两张五视图为准。
维度 普通 All-to-All DeepEP AGRS
数学语义 按 expert 路由 token 同一 dispatch/combine 语义 在 grouped router 下重写为 AG + RS
GMM 所在位置 框架侧 框架侧,不是 MC2 框架侧,不是 MC2
主要收益 语义直接、适用面广 按 rank 去重、专用拓扑 kernel、异步 event 规则 collective、少大张量重排、RS 合并回传与求和
关键前提 只需 EP 可用 DeepEP/CUDA/拓扑与 buffer 匹配 当前实现要求 EP=groups=top_k=8
主要代价 变长 split、重排与 host 控制 持久 buffer、handle、通信 SM、版本敏感 [RS,H] activation、固定路由约束、归约顺序变化
最稳妥用途 正确性与性能基线 NVIDIA MoE 通信优化候选 满足 grouped-router 约束时的 A/B 候选

判断顺序应是:先用普通 All-to-All 建立正确性基线;若瓶颈来自不规则 dispatch/combine,再测试 DeepEP;只有模型本来就满足 AGRS 的 grouped-router 语义时,才比较 AG/RS 是否在目标拓扑更快。Domino 是三者之上的跨微批调度层,不能替代这组后端 A/B。

HybridEP 论文的启发

Domino、DeepEP 和 MC2 都建立在一个默认前提上:专家参数留在原 rank,路由后的 token 去找专家。 在 NVLink、InfiniBand 或同数据中心高速互联中,这通常合理;但跨数据中心链路只有 10 Gbps 一类的低带宽时,token All-to-All 的长尾可能远大于 pre-expert compute,继续增加 overlap 也藏不住它。

HybridEP 论文的核心反问是:既然 token 数据量 \(D\) 与 expert 参数量 \(P_E\) 的相对大小会随模型、序列和网络变化,为什么通信对象必须固定?论文引入比例 \(p\),让一部分工作继续走 token All-to-All,另一部分改为提前迁移 expert,再让 token 留在本域计算:18

\[ p^\*=\arg\min_{p\in\mathcal{P}}T_{\text{step}}(p;D,P_E,B) \]

其中,\(\mathcal{P}\) 是离散候选比例集合,\(D\) 是一次 MoE 路由涉及的 token 激活数据量,\(P_E\) 是待迁移 expert 的参数负载,\(B\) 表示分层网络带宽。\(p=1\) 退化为标准 EP:只搬 token;\(p=0\) 则只通过 expert AllGather 搬参数。中间值对应两条路径并存,并用论文的线性延迟模型选择预计 step time 最小的拓扑。

两个 HybridEP 不是同一实现

本节讨论的是论文 HybridEP: Scaling Expert Parallelism to Cross-Datacenter Scenario via Hybrid Expert/Data Transmission。NVIDIA Megatron Core 也有一个名为 HybridEP 的 token-dispatch backend,但它是另一套工程实现;不能把 Megatron 的 API、约束或 benchmark 直接当作这篇跨数据中心论文的证据。19

混合传输

直接跨域复制完整 expert 往往比搬 token 更贵,因此 HybridEP 又引入两层结构:

  1. Expert Domain:域内使用 expert AllGather,域间继续使用 token All-to-All;比例 \(p\) 决定 domain 的划分和两类流量的份额。
  2. SR 压缩:先构造共享 expert \(W_{\text{shared}}\),再只发送每个 expert 相对共享值的稀疏残差:
\[ W_{\text{shared}}=\frac{1}{E}\sum_{e=1}^{E}W_e,\qquad \Delta W_e=W_e-W_{\text{shared}},\qquad \widehat{W}_e=W_{\text{shared}}+\operatorname{Decode}\!\left(\operatorname{TopK}(\Delta W_e)\right) \]

这里 \(E\) 是参与构造共享基底的 expert 数,\(W_e\) 是第 \(e\) 个 expert 权重,\(\Delta W_e\) 是残差,\(\widehat{W}_e\) 是接收端恢复出的近似 expert。通信包必须同时携带 Top-k 数值和索引;稀疏化减少的是链路 payload,而不是免费得到完整权重。

关键对象 产生位置 消费位置 资源含义
\(p\) 与 Expert Domain 延迟模型、拓扑构造器 通信组与路由拆分 控制量,不是 activation
token \(x_i[T,H]\) router 前后 跨域 A2A、expert GMM 在途 activation
\(W_{\text{shared}}\) expert 均值与异步同步 SR 编解码 持久或域内复制参数
\(C(\Delta W_e)\) Top-k 残差编码 expert AG、接收端解码 稀疏 value/index buffer
\(\widehat{W}_e\) SRDecode 迁移 expert GMM 预取的近似参数
Send/Recv Queue 异步 communicator 网络与 decoder 有界通信状态

下面的伪代码只表达论文机制,不对应 XTuner 当前 API:

def hybrid_ep_forward(tokens, router_result, experts, candidate_p, link_profile):
    token_bytes = tokens.numel() * tokens.element_size()
    expert_bytes = sum(weight.numel() * weight.element_size() for weight in experts)
    p = min(
        candidate_p,
        key=lambda ratio: modeled_step_time(
            ratio=ratio,
            token_bytes=token_bytes,
            expert_bytes=expert_bytes,
            link_profile=link_profile,
        ),
    )

    domains = build_expert_domains(p, router_result, link_profile)
    shared_weight = mean_expert_weight(experts, domains)
    sparse_packets = {
        expert_id: topk_encode(experts[expert_id] - shared_weight[domains[expert_id]])
        for expert_id in range(len(experts))
    }

    expert_ag_handle = async_expert_allgather(sparse_packets, domains)
    local_tokens, cross_domain_tokens = split_tokens(router_result, domains, p)
    token_a2a_handle = async_token_alltoall(cross_domain_tokens, domains)

    local_output = run_local_experts(local_tokens, experts, router_result)
    migrated_packets = expert_ag_handle.wait()
    migrated_experts = {
        expert_id: shared_weight[domains[expert_id]] + topk_decode(packet)
        for expert_id, packet in migrated_packets.items()
    }
    remote_tokens = token_a2a_handle.wait()
    remote_output = run_migrated_experts(remote_tokens, migrated_experts, router_result)
    return combine_routed_outputs(local_output, remote_output, router_result)

HybridEP five-view mechanism diagram

机制图:HybridEP 先根据数据量、参数量和链路选择 \(p\),再并行推进 token A2A 与压缩 expert AG。该图为源码与论文机制的教学重绘,不是论文原图。

HybridEP paper Figure 7 hybrid transmission logic

HybridEP 论文 Figure 7:从纯数据通信过渡到纯 expert 通信的比例化思路。原图用于支撑机制,不代表 XTuner 已实现该路径。

效果与边界

HybridEP 的直接收益目标是降低受限跨域链路上的关键路径时间,并用 expert 预取与 pre-expert compute 重叠;它不保证降低 GPU 峰值显存。更完整的峰值预算应包含:

\[ M_{\text{peak}}\approx M_{\text{base}}+M_{\text{shared}}+M_{\text{sparse-packet}}+M_{\text{prefetch}}+M_{\text{token-inflight}} \]

SR 压缩可以减小 \(M_{\text{sparse-packet}}\) 和网络 payload,但共享权重、恢复后的 \(\widehat{W}_e\)、收发队列与预取窗口都可能抬高峰值;CPU offload 只是把其中一部分压力转移到主存和 PCIe。

HybridEP paper Table V performance evidence

HybridEP 论文 Table V:论文按其 A800、10 Gbps 跨数据中心链路、缩小模型变体和指定基线报告最高 5.60× 平均加速。该结果不能横向排序 DeepEP、MC2 或 XTuner Domino EP。

论文模型还依赖 balanced routing、分段线性延迟与特定网络层级,并把 backward AllReduce 干扰近似为常量。SR 是有损 expert 恢复,必须另外验证前向误差、梯度、收敛和共享基底更新频率。因此,HybridEP 更像跨数据中心极低带宽下的系统设计空间,而不是在普通单集群训练中默认替换 DeepEP 的现成开关。

与 MC2 对比

DeepEP 与 MC2

DeepEP 与 EP MC2 的相同点,是都不改变 top-k 路由语义,而是在 router 与 unpermute 之间优化 MoE 数据面;两者都需要 token counts、rank/expert 映射、专用通信 workspace,并在反向中调用与前向相反的通信方向。

真正的差异是算子边界。DeepEP 接收 token 与路由元数据,输出路由后的 token;expert 权重不进入 DeepEP 调用。MindSpeed-MM commit 64eeb735 的 EP MC2 则把 fc1_weightfc2_weight 与 HCCL communicator 一起传入两个 fused op:npu_alltoallv_gmm 融合 A2Av 与第一层 GMM,npu_gmm_alltoallv 融合第二层 GMM 与 A2Av,二者之间的 SwiGLU 仍是独立操作。16

def deepep_moe_path(x, topk_idx, topk_weight, expert_weights, num_experts, group):
    recv_x, recv_idx, recv_weight, counts, handle, dispatch_event = dispatch_forward(
        x=x,
        topk_idx=topk_idx,
        topk_weights=topk_weight,
        num_experts=num_experts,
        group=group,
    )
    dispatch_event.current_stream_wait()
    expert_out = grouped_expert_gemm(recv_x, counts, expert_weights)
    combined_x, combine_event = combine_forward(
        x=expert_out,
        num_experts=num_experts,
        handle=handle,
        group=group,
    )
    combine_event.current_stream_wait()
    return combined_x


def mc2_moe_path(x, selected_experts, routing_weights, fc1_weight, fc2_weight, hcomm):
    send_counts, recv_counts = allgather_expert_counts(selected_experts)
    permuted_x, reverse_mapping = permute_by_expert(x, selected_experts)
    fc1_output, permute_output = npu_alltoallv_gmm(
        permuted_x, fc1_weight, send_counts, recv_counts, hcomm
    )
    activated = swiglu(fc1_output)
    routed_output = npu_gmm_alltoallv(
        activated, fc2_weight, send_counts, recv_counts, hcomm
    )
    return unpermute(routed_output, reverse_mapping, routing_weights), permute_output

这段代码是边界等价伪代码:DeepEP 的 GMM 明确位于通信调用外,MC2 的两层 GMM 则分别进入 fused call。实际 shape、packed layout 与 workspace 由固定版本后端决定,不能从 Python wrapper 擅自补齐。

EP MC2 operator boundary and data flow

机制图:EP MC2 让 A2Av tile 与 FC1 GMM tile、FC2 GMM tile 与回程 A2Av 在 fused operator 内流水;SwiGLU 仍位于两个 fused call 之间。
维度 DeepEP(XTuner 固定 V1 路径) EP MC2(MindSpeed-MM 固定提交)
输入 token、top-k 元数据、layout/handle token、counts、expert 权重、HCCL communicator
输出 routed/combined token、counts、handle、event 已完成一层 GMM 或回程 GMM+A2Av 的 activation
算子所有权 通信库拥有 dispatch/combine;框架拥有 GMM fused op 同时拥有通信 tile 与 GMM tile
重叠位置 event 暴露给框架,由外部 stream 调度 fused op 内部切 tile 流水
中间对象 recv_x 明确暴露给 expert GMM packed layout/workspace 由后端内部持有
反向 dispatch 的反向用 combine,combine 的反向用 dispatch 使用相反 fused op;权重梯度另做 npu_grouped_matmul
硬件栈 NVIDIA CUDA、NVLink/RDMA Ascend NPU、CANN、HCCL
接入 Domino 现有 dispatcher 接口可直接调度 必须改写 dispatcher/expert 的可分离边界
主要风险 buffer/拓扑/版本敏感,额外在途 token shape/CANN 约束、融合调试与潜在精度差异

因此二者不是“同一个算子在不同硬件上的名字”。DeepEP 保留通信与 expert compute 的软件边界,换来调度自由度;MC2 牺牲这条边界,把通信和 GMM 的 tile 调度交给后端,换取更深的单算子融合。 前者的显存要显式看到 recv_x + handle + buffer,后者可能减少暴露的中间张量,但仍有 backend-owned packed layout、workspace 与为反向保存的 permute_output,不能把“Python 看不见”误当成“显存不存在”。

MindSpeed-MM PR #2480 的公开材料只能证明功能合入与本地自测,没有公开多 rank 单测、统一 benchmark 或收敛曲线;DeepEP 公布的带宽数据又来自不同硬件和软件栈。因此本文不做跨 NVIDIA/Ascend 的数值排名,只比较固定源码中的对象与边界。

Domino EP 与 MC2

此前 MindSpeed-MM 接入的 EP MC2 面向 Ascend NPU,把 AllToAllv -> GroupedMatmulGroupedMatmul -> AllToAllv 分别融合为 NPU fused operator。15 它让算子内部把大通信和大计算切块、流水,减少中间张量、launch 和单微批内的串行等待。

维度 Domino EP EP MC2
重叠粒度 跨多个 micro-batch 一个 micro-batch 的 fused operator 内部
核心机制 异步 stream/event 调度 通信与 GMM/MM 融合、切块流水
最低并发来源 至少两个可同时在途的微批 一个微批也可产生算子内流水
通信后端 可搭配 All-to-All、DeepEP、AGRS 绑定支持 MC2 的 NPU/HCCL 算子
可移植性 调度思想较通用,但实现仍依赖 dispatcher 强硬件、CANN 与算子版本约束
主要代价 更高激活峰值、事件调度与小 GEMM 融合算子约束、调试困难、潜在精度差异

两者在理论上可以叠加:用 MC2 优化每个微批的 dispatch/GMM,再用 Domino 在微批之间继续遮蔽剩余气泡。但 XTuner 当前没有 MC2 dispatcher,而且两种机制都会争用通信带宽、计算单元和 stream;真正组合需要新接入与 profiler 证明,不能默认收益相加。MindSpeed-LLM 的 MC2 文档也明确提示部分模型可能出现精度问题,并列出了模型与硬件限制。17

HyperParallel-MoE

HyperParallel-MoE 把 MC2 的问题再扩一层。MC2 融合两对相邻边界,即 A2Av→GMMGMM→A2Av;HyperParallel 则把一次 MoE-FFN 的 Dispatch → GMM → SwiGLU → GMM → Combine 全路径建成 Operator Dependency Graph(ODG),再按 rank、expert 和 token tile 拆成异构任务,静态排入 Ascend 的 Cube Task Queue(CTQ/AIC)与 Vector Task Queue(VTQ/AIV)。33

机制 调度范围 最小并发单位 谁拥有通信—计算边界 当前硬件与接入状态
Domino EP 多个 micro-batch 一个微批的一段通信或专家计算 XTuner framework + dispatcher event CUDA 路径已存在
EP MC2 相邻 A2Av/GMM fused op 内部 tile CANN/HCCL fused op Ascend;XTuner 未接入
HyperParallel-MoE 单微批完整 MoE-FFN rank × expert × token tile Host compiler + AIC/AIV workers Ascend A3;XTuner 未接入
MoonEP dispatch、权重副本与 virtual-expert GMM 边界 token row / redundant expert MoonEP symmetric-memory buffer NVIDIA;XTuner 未接入

HyperParallel-MoE five-view mechanism diagram

论文与源码教学重绘:Host 把完整 MoE-FFN 编译为带 wait/trigger 事件的 CTQ/VTQ 静态任务;Device 端 AIC/AIV workers 在单次 launch 内按 tile 推进。图固定到 arXiv v2 与源码 `b5e80ae`,不代表 CUDA 或 XTuner 可直接复用。

从整体 collective 到目的 tile 事件

HyperParallel 的关键不是“再开一条 stream”,而是改变下游工作的就绪条件。普通算子级路径通常等整个 All-to-All 返回后才启动 GMM;HyperParallel 的 AIV task 使用一侧 put_mem_signal,先把 tile 写进目的 rank 的可读位置,再更新目的端 event counter。只依赖该 tile 的 GMM task 在计数器达到阈值后即可执行,不必等其它 rank、其它 expert 的所有 tile 都完成。固定源码的 forward graph 也明确给出五算子依赖与 CTQ/VTQ 归属。34

HyperParallel-MoE paper Figure 5 overview

HyperParallel-MoE 论文 Figure 5:从 operator graph、任务拆分与重排,到 AIC/AIV workers 消费静态调度代码的整体设计。该图支撑机制边界,不是 XTuner 集成图。

Host compiler 与 Device runtime 之间需要完整对象契约:

对象 产生者 消费者 生命周期与资源含义
ODG MoE graph builder splitter、dependency builder 编译期五算子逻辑图
SplitSpec 每个 operator 的合法切分定义 task generator 指定 rank/expert/token tile 维度
TaskDescriptor task generator AIC/AIV worker 含 op、tile、queue、wait 与 trigger
Event Table dependency compiler/runtime init worker wait/trigger loop Device 端计数器状态
SSC legal reorder + serializer 统一 device kernel CTQ/VTQ 的静态有序任务数组
remote dispatch tile AIV put_mem_signal 目的端 GMM 写入后以 event 表示可读,而不是整个 collective 完成
AIC/AIV intermediate GMM 或 SwiGLU/Add handler 下一个 tile task tile 流水中的显式或 backend-owned 中间状态
combine tile 第二个 GMM AIV return task 与 token owner 回程写与目的端事件完成后结束

下面的伪代码把编译期和运行期都展开。reorder_ready_tasks 只能在依赖允许的候选集中做 RATR 目的 rank 均衡或 backward GMM 的 L2 复用重排,不能跨越真实数据依赖:

def compile_hyperparallel_moe(split_specs):
    operators = ["dispatch", "gmm_up", "swiglu", "gmm_down", "combine"]
    queue_of = {
        "dispatch": "VTQ",
        "gmm_up": "CTQ",
        "swiglu": "VTQ",
        "gmm_down": "CTQ",
        "combine": "VTQ",
    }
    graph = build_linear_dependency_graph(operators)
    tasks = []

    for operator in operators:
        tiles = split_operator(graph[operator], split_specs[operator])
        for tile in tiles:
            tasks.append(
                TaskDescriptor(
                    operator=operator,
                    tile=tile,
                    queue=queue_of[operator],
                    wait_events=[],
                    trigger_events=[],
                )
            )

    task_index = index_tasks_by_operator_and_tile(tasks)
    event_table = EventTable()
    for producer_name, consumer_name in graph.edges:
        for producer_task, consumer_task in match_dependent_tiles(
            task_index[producer_name],
            task_index[consumer_name],
        ):
            event_id = event_table.allocate_counter()
            producer_task.trigger_events.append(event_id)
            consumer_task.wait_events.append((event_id, 1))

    ctq_tasks = reorder_ready_tasks(
        select_queue(tasks, "CTQ"),
        policy="backward_gmm_l2_reuse",
    )
    vtq_tasks = reorder_ready_tasks(
        select_queue(tasks, "VTQ"),
        policy="rank_aware_task_reordering",
    )
    return StaticScheduleCode(ctq_tasks, vtq_tasks, event_table)


def execute_hyperparallel_moe(ssc, tensors, communicators):
    def worker(task_queue):
        for task in task_queue:
            for event_id, threshold in task.wait_events:
                wait_until(ssc.event_table[event_id] >= threshold)

            if task.operator in {"dispatch", "combine"}:
                put_mem_signal_kernel(task.tile, tensors, communicators)
            elif task.operator in {"gmm_up", "gmm_down"}:
                grouped_matmul_kernel(task.tile, tensors)
            elif task.operator == "swiglu":
                swiglu_add_kernel(task.tile, tensors)

            for event_id in task.trigger_events:
                atomic_increment(ssc.event_table[event_id])

    launch_one_ascendc_kernel(
        aic_worker=lambda: worker(ssc.ctq_tasks),
        aiv_worker=lambda: worker(ssc.vtq_tasks),
    )
    return tensors["combined_output"]

这套静态化的收益前提很强:shape 和 tile 集合要能在 Host 侧确定,单 task 开销必须足够小,而且 CTQ/VTQ 重排不能制造缓存抖动。论文报告静态任务开销约 0.1 μs,对照的动态调度为 2.36 μs;在 \(M=32K\) 的 SwiGLU+Add 微基准中,交错把 723.29 μs 降到 588.38 μs、L2 hit 从 5.20% 提到 25.44%,但在 8K 小尺寸上出现轻微退化。这说明“切得更细”本身不是单调收益。35

结果与组合边界

HyperParallel-MoE paper Figure 7 module latency

HyperParallel-MoE 论文 Figure 7:在 64 张 Ascend A3、序列 4096、micro-batch 2、hidden 7168、top-k 8 的设置中,论文对 EP4/8/16 报告完整 MoE 模块总加速 1.49–1.58×。这是整套编译、重排与 runtime 的合并结果,不能归因给某个单独变换。

论文的 end-to-end 提升只有 1.08–1.09×,明显小于模块的 1.49–1.58×;完整模型覆盖仍被列为后续工作。这个差距非常重要:模块外还有 attention、其它通信、pipeline 气泡和框架开销,单层局部加速不能直接当训练吞吐加速。36

把它纳入本文后的组合判断是:

  1. 与 Domino:一个在单微批内部按 tile 推进,一个跨微批调度;理论上可层叠,但 unified kernel 会改变 Domino 能看到的 event 和可重叠边界。
  2. 与 MC2:HyperParallel 覆盖完整五算子路径,MC2 覆盖两对 fused 边界;在 Ascend 上更像两种算子所有权方案,不是默认叠加的独立开关。
  3. 与 MoonEP:MoonEP 依赖 NVIDIA symmetric memory、动态冗余 expert 和权重预取;HyperParallel 依赖 Ascend AIC/AIV 与静态 tile 编译。两者解决的问题可同时存在,但当前硬件、内存与 runtime 契约并不兼容。
  4. 与 HybridEP:HybridEP 决定跨数据中心搬 token 还是压缩 expert;HyperParallel 优化 Ascend 集群内单个 MoE-FFN 路径的细粒度推进,尺度与通信对象都不同。

所以,HyperParallel 为 EP 方案增加的是一条平台专用的完整 MoE 编译路线,不是“把 XTuner dispatcher 名称改成 hyperparallel”。真正接入必须重画框架与 fused runtime 的所有权边界,并用 Ascend timeline、峰值内存、精度和端到端吞吐共同验收。

论文证据

Domino paper Figure 11 throughput comparison

Domino 论文 Figure 11:在 2/4 台 DGX-H100 上比较 Domino 与 Megatron-LM。该图验证的是 TP AllReduce 场景的通用重叠机制,不是 XTuner Domino EP benchmark。

原始 Domino 论文处理的是 tensor parallel:沿 batch 维切输入、沿输出维切权重,让一个切片的 AllReduce 与另一个切片的矩阵乘重叠。论文报告在 DGX-H100 集群上相对 Megatron-LM 最高约 1.3× 加速。20 这为“切分独立工作并用异步通信跨切片重叠”提供了机制证据,但通信对象是 TP AllReduce,不是 MoE EP 的 token dispatch/combine。

与 XTuner 实现更接近的论文先例来自 DeepSeek-V3:其预填充阶段同时处理两个 micro-batch,使一个微批的 attention/MoE 计算与另一个微批的 dispatch/combine 重叠。21 不过 DeepSeek-V3 的 DualPipe 还包含 pipeline parallel 调度,不能与 XTuner 的单个 MoE 层实现画等号。

截至上述 XTuner commit,公开代码和 PR 能证明实现、测试与持续修复过程,但没有提供可复用的 Domino EP 专项性能表。PR #1078 是功能接入,后续 PR #1824 修复了 H2D 同步延迟第二个微批 DeepEP dispatch 的问题;这恰好说明收益依赖细致的异步路径,而不能只看开关名称。22

选择方法

建议把“后端”和“调度”拆开做 A/B,而不是一次同时更改:

在进入 A/B 前,先判断瓶颈是否真的是跨数据中心低带宽。如果只是同机或同数据中心 EP 通信长,优先从 DeepEP、MC2、AGRS 和 Domino 逐层优化;只有 token A2A 的长尾明显超过可覆盖计算、并且迁移压缩 expert 更便宜时,HybridEP 的通信对象切换才进入候选集。

还要单独看 router skew 与设备执行时间线:如果 step 被最热 expert 所在 rank 拖尾,MoonEP/UltraEP 一类实时冗余 expert 方案才进入候选。偏好固定 S×K 计算 shape、愿意交出 dispatch/combine 与 symmetric-memory 布局时看 MoonEP;希望保留外部 dispatcher/GMM、以少量跨层槽换大 EP 近似均衡时看 UltraEP。如果目标是 Ascend A3,且 AIC/AIV 空闲与 EP 裸露通信同时存在,才评估 HyperParallel 的完整 MoE 瓦片编译。它们分别需要新的后端/运行时接入,不能当成已有 XTuner 配置项。

  1. 建立基线dispatcher="all2all"intra_layer_micro_batch=1,确认 loss、吞吐、峰值显存和通信占比。
  2. 只换后端:分别测 DeepEP 或满足约束时的 AGRS,保持 global batch、micro-batch shape、EP size 和精度配置不变。
  3. 再开 Domino:先把 intra_layer_micro_batch 改为 2,确认数据批次数可整除,并保留相同 global batch 语义。
  4. 看时间线而非开关:确认 dispatch/combine 确实与另一微批的 GMM 重叠,而不是被 H2D copy、host sync 或 event wait 串行化。
  5. 同时验精度:比较前向、梯度、loss 曲线和长程收敛;AGRS、低精度 DeepEP 与 MC2 都不能只用 step time 验收。
  6. 跨域再测 HybridEP:扫描候选 \(p\)、SR sparsity 与 expert prefetch window,同时记录跨域字节数、主存/显存峰值和收敛,不能只复用论文的 10 Gbps 结论。
  7. 有路由偏斜再测 MoonEP:固定模型、路由结果、EP size 与 S×K,记录最忙/最闲 rank 的 token 行数、权重预取字节、plan 时间、grad reduce 尾部和 FP32 pool 峰值。
  8. 大 EP/NVLink 域再测 UltraEP:重放同一 routing trace,扫描 N_slot、quota floor、direct/relay 与 grad-reduce SM 数,同时记录 post-balance max/mean、physical instances、weight fan-out、未覆盖尾部和跨层 buffer 峰值。
  9. Ascend 再测 HyperParallel:先做算子级基线,再逐项核对 tile 依赖、CTQ/VTQ 利用率、event wait、L2 hit、模块延迟和端到端 step;模块加速不能代替端到端验收。

一个最小配置思路如下,具体对象名应以所用 XTuner recipe 为准:

model_cfg.dispatcher = "deepep"       # 也可单独测试 all2all / agrs
trainer_cfg.intra_layer_micro_batch = 2

常见失败条件

  • 显存上涨:多个微批的 residual、router 结果、dispatch handle 和激活同时在途。
  • 形状限制:当前 _micro_batch_forward 要求同组 hidden states 形状一致。
  • 覆盖不足:通信太长而计算太短时仍会露出尾部;通信本来很短时调度成本反而突出。
  • 计算退化:为了塞进两个微批而缩小单微批,Grouped GEMM 的效率可能下降。
  • 版本错配:DeepEP、CUDA/NCCL、GroupedGEMM 或 NPU CANN/MC2 的版本都会改变结果。
  • 通信对象错判:HybridEP 的 SR 压缩会增加共享权重、稀疏包、解码与预取状态,并引入有损近似;低带宽并不自动等于净收益。
  • 负载均衡错判:MoonEP 的固定 routed 行数不能消除 plan、权重预取、zero-copy 生命周期与副本梯度归并成本。
  • exact-load 错判:UltraEP 的输入 load 是精确的,但 slot 数、NVLink domain、quota floor 与 weight-sync deadline 仍会形成高于 1.0 的均衡下限。
  • 局部加速外推:HyperParallel 的模块收益来自完整 bundle;平台、shape 或 tile 变化,以及模块外瓶颈,都会改变端到端收益。

总结

XTuner Domino EP 的本质是:把原本串行的梯度累积微批提升为 MoE 层内部可共同调度的工作集合,用通信 stream 和 event 建立跨微批流水。 它不改变 EP 的专家切分,也不规定 token 必须通过哪一种 collective 搬运。

  • 追求通用基线与易调试,优先 All-to-All。
  • NVIDIA 多机 MoE 且拓扑合适,优先评估 DeepEP + Domino EP
  • 满足 EP=router groups=8 且 AG/RS collective 更占优,可评估 AGRS + Domino EP
  • Ascend 路径若已有成熟 fused operator,MC2 更像单微批内的深度融合;它与 Domino 的比较是算子融合对跨微批调度,不是同一层替代关系。
  • 跨数据中心链路低到 overlap 无法隐藏长尾时,才进一步评估 HybridEP 的 token/expert 混合传输;论文结果不能直接外推到 XTuner。
  • NVIDIA NVLink 域内若瓶颈来自严重 router skew,可研究 MoonEP 的 动态冗余 expert + 固定 S×K 工作量;先验条件是接受权重预取、symmetric-memory 布局与梯度 pool 接口改造。
  • NVIDIA 大 EP/RSN 若希望保留外部 dispatcher 与 GMM,可研究 UltraEP 的 exact-load quota + 跨层 replica buffer + 自适应权重 relay;先验条件是接入 physical expert map、virtual layer state 与副本梯度回收。
  • Ascend A3 若单微批的通信、Cube 与 Vector 空闲可通过 tile 事件互相覆盖,可研究 HyperParallel-MoE;必须同时看其 MoE 模块与端到端 两级收益。

DeepEP 与 MC2 的核心分界也可以压缩成一句话:DeepEP 把 routed token 交还框架再做 GMM,MC2 把 expert 权重交给 fused operator 并在内部完成通信—计算流水。

最终选择应由同一模型、同一 batch 语义下的 profiler、显存和收敛结果决定,而不是由技术名称决定。

加入三条新路线后,这个判断仍不变:MoonEP 用固定工作量、UltraEP 用 quota 上限缩短负载偏斜造成的 rank 尾部,HyperParallel 重排单微批完整 MoE 的瓦片关键路径,Domino 再尝试跨微批遮蔽剩余气泡。 这些层能否共存是待实现、待测量的系统问题,不是名称上的可组合性证明。


  1. XTuner commit 9989fb74130d8f848662c5021458480ba161b9fa。本文源码结论均固定到该版本,避免主分支变化造成歧义。 

  2. XTuner train_engine.py:微批分组与梯度累积。 

  3. XTuner moe_decoder_layer.py:跨微批 MoE 调度。 

  4. XTuner build_dispatcher:三种 dispatcher 与默认 All-to-All。 

  5. XTuner torch_all2all.py:token count、split 与 All-to-All。 

  6. XTuner deepep_op.py:DeepEP SM 配置low-latency buffer 与异步 dispatch。 

  7. XTuner image_build.sh:DeepEP v1.2.1 固定版本DeepEP 官方仓库。 

  8. XTuner deepep_op.py 固定版本源码DeepEP commit 9af0e0dBuffer.dispatch/combine。 

  9. DeepEP commit 9af0e0d 的 layout kernelis_token_in_rank 对每个 token/rank 只记录一次命中;XTuner DeepEP dispatcher 在收到 rank 级 dispatch 结果后再做 framework-side permute。 

  10. DeepEP commit 9af0e0d 的跨节点 dispatch kernel:warp 分工覆盖 RDMA sender、RDMA/NVLink forwarder 与 NVLink receiver。它证明的是通信 kernel 的分层转发,不包含 expert GMM。 

  11. XTuner agrs.py:AllGather、All-to-All IDs/weights 与 ReduceScatter。 

  12. XTuner MoEConfig:AGRS grouped router 与 8-way 约束。 

  13. XTuner grouped router 明确要求 top_k == n_group == 8,并在每个连续 expert group 中选择一个 expert;AGRS dispatch/combine 实现 则分别交换大 hidden 与小型 metadata。 

  14. XTuner AGRS/All-to-All 对照测试XTuner PR #1245。 

  15. MindSpeed-MM PR #2480:feat: support ep mc2。 

  16. MindSpeed-MM commit 64eeb735:EP dispatcher 调用链两个 MC2 fused op 与反向实现。 

  17. MindSpeed-LLM:MC2 特性说明。 

  18. HybridEP: Scaling Expert Parallelism to Cross-Datacenter Scenario via Hybrid Expert/Data Transmission,本文固定到 arXiv v1(2025-10-22),机制主要见 Sections III-IV、Figures 7-9 与 Equations 8-12。 

  19. HybridEP 论文Megatron Core MoE README 中的 HybridEP backend。二者名称相同,但研究目标、接口与实现来源不同。 

  20. Domino: Eliminating Communication in LLM Training via Generic Tensor Slicing and Overlapping。 

  21. DeepSeek-V3 Technical Report。 

  22. XTuner PR #1078:Domino EPXTuner PR #1824:DeepEP dispatch overlap 修复。 

  23. MoonEP 官方仓库,固定 commit 0f385f038fc33bec22e3bcf5a07a8a22693e754cGPU planner 对象与约束。截至该提交,仓库没有关联的 MoonEP 同名论文,本文把源码和 README 当作实现证据,不把仓库说明提升为同行评审结论。 

  24. MoonEP api.py:dispatch、prefetch、combine、plan 复用与 gradient reductionREADME:训练/推理 B 约束README:zero-copy 生命周期。 

  25. MoonEP bench_vs_deepep.py官方通信图。脚本明确列出计时边界,并说明 grad_reduce 因假设可与后续计算重叠而不计入该 benchmark。 

  26. UltraEP 官方仓库 tag v1.0.0,固定 commit 94cab099b44fffa99a82fea99e7c12d89cf65e4fREADME 的依赖、Manager 接口与前后向集成骨架。公开发行支持 SM90/SM100;SGLang 集成在该版本仍标为 experimental/unreleased。 

  27. UltraEP Manager:跨层 replica buffer、FP32 gradient、virtual layer ID 与核心 APIallocate_microbatch_slotgrad_reduceweight_syncupdate_placementreroute。这些代码证明独立 runtime 的接口,不证明 XTuner 已集成。 

  28. UltraEP 论文 arXiv v3,§5 Quota-Driven Planning 与 Algorithm 1固定源码 placement.cu 的 quota solver。论文工作中 u_min=1024、目标 balancing coefficient 为 1.01;仓库 tuning env 可调整相应阈值。 

  29. UltraEP: Unleash MoE Training and Inference on Rack-Scale Nodes with Near-Optimal Load Balancing,本文固定到 arXiv v3;前向/反向 pipeline 见 Figures 8–9 与 §4.2,RSN tile streaming/relay 见 §6。 

  30. UltraEP arXiv v3 §8 与 Figures 11–12:训练平均 94.6% ideal、相对 Megatron-LM 平均 +42%;prefill 平均 93.9% ideal、相对 SGLang/EPLB 为 1.56×/1.29×。模型、GPU 数、EP、precision 与 continuation window 见 Table 2 和 §8.1–8.2;production 结果见 Figure 17/§8.6。 

  31. UltraEP README 的 EP64 runtime 表与 tuning 开关论文 Figure 16 与 §8.5 比较的是 expert-weight distribution backend,不是 token dispatch 或端到端训练。 

  32. UltraEP arXiv v3 Figure 7/§4.1 的跨层 buffer 复用Figure 14/§8.4 的 activation memory。前者减少 replica state 的层数倍增,后者是负载拉平后的实测峰值变化,二者不能相加为固定节省比例。 

  33. HyperParallel-MoE: Multi-Core Interleaved Scheduling for Fast MoE Training on Ascend NPUs,本文固定到 arXiv v2(2026-06-01)。 

  34. MindSpore HyperParallel forward graph,固定 commit b5e80aeput_mem_signal task 构造。worker kernel 与 forward AIV worker 也固定到同一提交。 

  35. HyperParallel-MoE arXiv v2,任务调度开销与 SwiGLU/Add 微基准。这些是论文在 Ascend A3 上的局部测量,不代表所有 shape 都加速。 

  36. HyperParallel-MoE arXiv v2,MoE 模块与端到端评估。论文设置为 64 张 Ascend A3、25 AIC + 50 AIV/设备、192 MB L2;模块与端到端数字必须分开引用。 

评论