跳转至

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 则把相邻通信和矩阵乘融合进一个算子。只有先分清这四层,才能正确讨论它们的优劣和组合关系。

XTuner Domino EP cross-microbatch overlap

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

结论

截至 XTuner commit 9989fb7,相关技术可以按下面四层理解:1

层次 技术 解决的问题 是否能与 Domino EP 组合
并行语义 普通 EP 把不同 experts 放到不同 rank,路由 token 后再合并 Domino EP 的基础
通信后端 All-to-All / DeepEP / AGRS 具体怎样 dispatch 和 combine token 可以,三选一作为底层 dispatcher
调度方法 Domino EP 怎样用多个 micro-batch 隐藏 EP 通信 本身不替代 dispatcher
融合算子 MC2 在一个 micro-batch 内融合通信与 GMM/MM 思想上正交,但 XTuner 当前未接入该 dispatcher

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

  1. All-to-All、DeepEP、AGRS 比通信实现。
  2. Domino EP 比跨微批调度。
  3. MC2 比单微批内的算子融合。

一句话判断

如果 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 负责尽量把这次通信藏起来。 二者通常是互补关系。

AGRS

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

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

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

与 MC2 对比

此前 MindSpeed-MM 接入的 EP MC2 面向 Ascend NPU,把 AllToAllv -> GroupedMatmulGroupedMatmul -> AllToAllv 分别融合为 NPU fused operator。11 它让算子内部把大通信和大计算切块、流水,减少中间张量、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 文档也明确提示部分模型可能出现精度问题,并列出了模型与硬件限制。12

论文证据

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× 加速。13 这为“切分独立工作并用异步通信跨切片重叠”提供了机制证据,但通信对象是 TP AllReduce,不是 MoE EP 的 token dispatch/combine。

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

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

选择方法

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

  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 验收。

一个最小配置思路如下,具体对象名应以所用 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 的版本都会改变结果。

总结

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 的比较是算子融合对跨微批调度,不是同一层替代关系。

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

评论