[v1] add FSDPTurbo EP/EFSDP plugin for MoE training (#10676)

This commit is contained in:
Hazeldxq
2026-08-13 20:45:55 +08:00
committed by GitHub
parent bc4b42cefc
commit f28afaf635
21 changed files with 1341 additions and 29 deletions

View File

@@ -0,0 +1,215 @@
# FSDPTurbo EP/EFSDP 与 LlamaFactory FSDP2/CP 设计说明
English version: [FSDPTurbo EP/EFSDP and LlamaFactory FSDP2/CP Design](../../../en/advanced/distributed/fsdpturbo-ep-efsdp.md)
本文描述 `fsdpturbo` distributed plugin 的当前实现。核心原则是保持两侧职责清晰:
- FSDPTurbo 负责专家并行EP、专家参数分片EFSDP和设备算子注册。
- LlamaFactory 负责进程初始化、基础 DeviceMesh、外层 FSDP2、CP、模型初始化与权重加载。
- LlamaFactory 的集成层负责把两套参数布局组合起来,并处理跨 Mesh 的梯度范数。
## 1. 配置边界
公共并行拓扑放在 `TrainingArguments` 顶层FSDPTurbo 私有参数保留在 `dist_config`
```yaml
cp_size: 1
dist_config:
name: fsdpturbo
ep_size: 16
ep_dispatcher: eager
```
最小示例中的字段职责如下:
- `ep_size`:专家并行组大小。
- `ep_dispatcher`FSDPTurbo EP dispatcher默认为 `eager`
`dp_size``cp_size``cp_mode``mp_replicate_size``mp_shard_size``dist_timeout`
属于公共拓扑字段,继续放在顶层。`dist_config` 会被严格解析为 `FSDPTurboParams`;如果把公共拓扑
字段误放进去,会直接报错,而不是静默忽略。
顶层训练参数 `bf16` 同时控制 FSDPTurbo 的参数存储和计算 dtype。backend 会在 FSDP materialization
前完成模型 dtype 转换,因此 `ModelEngine` 不需要读取 distributed backend 配置。
以下高级字段为可选项,因此没有写入上面的最小 YAML 示例:
- `fsdp_ignored_modules`:额外排除在 LlamaFactory 外层 FSDP2 之外的模块。模型规格选中的专家参数
会被集成层自动加入忽略集合,普通配置无需重复填写。
- `hook_modules`FSDPTurbo EFSDP hook 的可选模块模式,默认为空列表。
- `fsdp_implementation`FSDPTurbo EFSDP 实现,可选 `native``custom`,默认为 `native`
EFSDP 的目标由模型规格决定。Attention、Embedding、LM Head 等非专家参数不进入 FSDPTurbo
EFSDP plan而是继续由 LlamaFactory 外层 FSDP2 管理。
模型相关的模块路径和准备逻辑统一由 `FSDPTurboEPModelSpec` 注册表管理。当前内置 `qwen3_moe`
`qwen3_5_moe`;未注册的模型会明确报错。`ep_modules``ep_fsdp_modules` 不属于 YAML
接口,严格参数解析会拒绝这两个字段,避免用户配置与模型实际结构失配。
## 2. Mesh 初始化
LlamaFactory 的 `DistributedInterface` 只初始化自身原有的 model/data mesh。它不感知 EP、EFSDP
也不为 distributed plugin 提供额外 mesh 注册接口。FSDPTurbo 的专家拓扑由插件文件内的
`FSDPTurboParallelState` 独立创建和持有:
```text
run_sft / run_dpo / run_rm
-> DistributedInterface(training_args)
-> 初始化 LlamaFactory model/data mesh
-> DistributedPlugin("fsdpturbo").shard_model(...)
-> FSDPTurboFSDP2Engine.__init__()
-> FSDPTurboParallelState.initialize()
-> 初始化并保存 expert parent mesh 及其子 mesh
```
`FSDPTurboParallelState` 创建专家侧四维父 Mesh
```text
(edp, efsdp, ep, expert_cp)
```
当前尺寸计算为:
```text
dp_size = world_size / cp_size
ep_fsdp_size = dp_size / ep_size
edp_size = dp_size / (ep_size * ep_fsdp_size)
mesh_shape = (edp_size, ep_fsdp_size, ep_size, cp_size)
```
状态对象保存 `edp_mesh``efsdp_mesh``ep_mesh``expert_cp_mesh`。插件内部模型切分和梯度范数
都从这个状态对象读取专家通信域LlamaFactory 其他 backend 不需要实现或感知这些接口。状态初始化
会校验 `ep_size` 为正数且能够整除 `dp_size`,重复初始化时也会拒绝拓扑发生变化。
## 3. 模型切分顺序
模型包装顺序必须保持为“专家侧优先,外层 FSDP2 随后”:
```text
DistributedPlugin("fsdpturbo")
-> FSDPTurboFSDP2Engine.shard_model(model)
-> prepare_model_ep(model)
-> expert_parallelize_modules(model, ep_mesh, ep_plan)
-> expert_fully_shard_modules(model, efsdp_mesh, ep_plan, fsdp_plan)
-> 收集专家参数作为 ignored_params
-> FSDP2Engine.prepare_model(model, ignored_params=...)
-> 对剩余 Transformer Layer 和根模块执行 outer fully_shard
```
这样可以避免同一专家参数同时被 EFSDP 和外层 FSDP2 管理。外层 FSDP2 仍复用 LlamaFactory
原有的初始化、checkpoint 和保存流程。
LlamaFactory 集成层接受 `eager``fused``mc2``domino`,并将选项原样传给
FSDPTurbo。这四种模式的实现边界和当前验证状态不同
| Dispatcher | 主要路径 | 额外要求 | 本 PR 验证状态 |
| --- | --- | --- | --- |
| `eager` | 使用 PyTorch 实现 permute、unpermute 和 grouped matmul张量仍在当前加速设备上通过标准 AllToAll 完成 token dispatch/combine | 依赖最少,用作参考实现 | 已在 A3 上完成精度和性能验证 |
| `fused` | 保持相同的 AllToAll 拓扑,将 permute、unpermute 和 grouped matmul 切换为设备融合算子 | 需要对应的设备算子、dtype 和 layout 支持;存在空专家时可回退到 eager 局部算子 | 已在 A3 上完成精度和性能验证 |
| `mc2` | 使用专用算子融合 AllToAllV 和 grouped matmul减少通信与计算之间的中间开销 | 依赖 MC2 NPU 算子、HCCL communicator 以及对应的 shape/dtype 约束 | FSDPTurbo 提供实现,本 PR 未做端到端验证 |
| `domino` | 将专家模块输入的第一维分成两片,使用独立通信流和 event 重叠 AllToAll 与专家计算 | 需要异步 stream/event 支持,且两个分片都要有足够的 token 工作量才能覆盖调度开销 | FSDPTurbo 提供实现,本 PR 未做端到端验证 |
当前只验证 `eager``fused`,是因为它们分别覆盖参考实现和 A3 常用设备融合路径,可用于隔离并验证
LlamaFactory 与 FSDPTurbo 之间的 EP/EFSDP 集成正确性。本次实验矩阵没有继续扩展到 `mc2`
`domino`:它们还引入了额外的算子、通信调度和输入形状约束,需要独立比较数值、长步稳定性和 profiler 结果。
因此,它们在配置接口上可选,但不应从本 PR 的实验结果推断为已达到相同的稳定性、精度或性能水平。
## 4. FSDPTurbo 依赖入口
LlamaFactory 从各功能的定义模块直接导入所需对象:
```python
from fsdp_turbo.distributed.expert_parallel.expert_fully_shard_parallel import (
expert_fully_shard_modules,
)
from fsdp_turbo.distributed.expert_parallel.expert_parallel import expert_parallelize_modules
from fsdp_turbo.fsdp_turbo_config import EPPlanConfig, FSDPPlanConfig
from fsdp_turbo.utils.str_match import module_name_match
```
导入发生在 `prepare_model_ep()` 内,因此没有安装 FSDPTurbo 时,其他 distributed backend 仍可正常导入。
这里不通过 `fsdp_turbo.distributed.__init__` 聚合导出,避免 package 初始化期间的额外依赖和潜在循环导入。
## 5. 梯度范数
外层参数和专家参数可能属于不同 DTensor Mesh不能直接放入一次标准 `clip_grad_norm_()`
`fsdpturbo` plugin 按参数所属 Mesh 分组计算局部 p 次方和:
- 非专家参数沿 DP 和 CP group 汇总。
- 专家参数沿 `FSDPTurboParallelState` 保存的 EFSDP、EP 和 expert-CP group 汇总。
- 汇总得到全局范数后,对所有本地梯度应用同一个 clipping coefficient。
启动阶段会执行一次零梯度 warmup使相关 collective 在正式训练前完成初始化。
当前这是 `fsdpturbo` backend 的专用实现;其他 backend 继续保留原有梯度范数路径,等待上游
distributed plugin 解耦后再统一公共接口。
## 6. 权重加载
LlamaFactory 保留 `init_on_meta` 和 safetensors 加载流程。父类 `FSDP2Engine` 的加载器通过
`self._copy_weights(...)` 动态调用 FSDPTurbo engine 的覆写实现,因此该方法不是未使用代码。
它支持包含多个 `Shard` placement 的 DTensor按各 Mesh 维度依次计算当前 rank 对应的本地切片。
模型保存和 checkpoint 接口继续复用 LlamaFactory FSDP2 实现。
## 7. Kernel plugin
FLA 算子不属于 distributed config。算子选择通过独立的 `kernel_config` 完成:
```yaml
kernel_config:
name: auto, flash-linear-attention
include_kernels: chunk_gated_delta_rule, fused_recurrent_gated_delta_rule
chunk_size: 32
```
调用链如下:
```text
ModelEngine
-> apply_kernels("auto, flash-linear-attention")
-> LlamaFactory 当前加速器对应的 auto kernels
-> KernelPlugin("flash-linear-attention").apply(...)
-> fsdp_turbo.ops.get_op()
-> FSDPTurbo device operator registry
-> fsdp_turbo.utils.patch.patch_model_members()
-> FLA backend implementation
```
`chunk_size` 当前支持 `16``32``64`,默认值为 `64`。Kernel plugin 与 distributed plugin
彼此独立。`name: flash-linear-attention` 只安装所选 FLA 算子;逗号分隔的
`name: auto, flash-linear-attention` 会在分布式切分前组合 LlamaFactory 当前加速器的 auto kernels
与 FLA plugin。LlamaFactory 负责算子名到模型属性的映射和 `chunk_size` 参数绑定FSDPTurbo 负责设备
算子注册、选择和通用 callable patch。FLA 依赖可选的外部三方件,因此保持显式选择,不属于内置
`auto` 集合。FSDPTurbo
随后会替换目标专家模块的 `forward`,所以专家计算的最终路径由 `ep_dispatcher` 决定auto 阶段
应用的 MoE kernel 不会作为独立的第二条专家执行路径保留下来。
## 8. CP 运行约束与验证范围
`init_on_meta` 构造模型时必须与 `from_pretrained` 路径一样传递 `attn_implementation`,否则模型会退回
非 FlashAttention 实现Ulysses CP 无法启动。Ulysses 在调用 Hugging Face FlashAttention 前重建全局
attention mask只有二维 position IDs 才参与 packed-sequence 检测。Qwen3.5 mRoPE 等多轴 position IDs
已经在 rotary embedding 中消费,不应传入 FlashAttention 的 packed-sequence 检测逻辑。
当前实现已在 Atlas 900 A3 SuperPoD 和 Atlas 950 SuperPoD 上用 Qwen3.5-35B-A3B 完成以下
BF16、AdamW full SFT 验证。本次重验证使用 FSDPTurbo `0e96fbc`A3 环境为 CANN 9.0.0、
PyTorch 2.7.1 和 torch-npu 2.7.1.post4A5 环境为 CANN 9.1.0-beta.3、PyTorch 2.10.0 和
torch-npu 2.10.0.post2。表中性能按第 1 步至第 100 步的日志时间戳计算,不包含首步前的初始化、
编译和训练后的模型保存时间:
| 机器型号 | CP | EP | EFSDP | Checkpoint | Kernel / Dispatcher | 步数 | Loss首步 -> 末步) | 性能 | 结果 |
| --- | ---: | ---: | ---: | --- | --- | ---: | --- | ---: | --- |
| Atlas 900 A3 SuperPoD | 1 | 16 | 1 | 关闭 | FLAchunk size 16/ eager | 100 | 1.3361 -> 0.0793 | 2.51 s/it | 通过并完成保存 |
| Atlas 900 A3 SuperPoD | 1 | 16 | 1 | 关闭 | FLAchunk size 16/ fused | 100 | 1.3354 -> 0.1179 | 2.17 s/it | 通过并完成保存 |
| Atlas 900 A3 SuperPoD | 2 | 4 | 2 | 关闭 | auto + FLAchunk size 64/ fused | 100 | 1.8114 -> 0.5260 | 7.65 s/it | 通过并完成保存 |
| Atlas 900 A3 SuperPoD | 2 | 4 | 2 | 关闭 | auto + FLAchunk size 64/ eager | 100 | 1.8095 -> 0.5596 | 5.88 s/it | 通过并完成保存 |
| Atlas 950 SuperPoD | 1 | 8 | 1 | 关闭 | 未配置 kernel plugin / eager | 100 | 1.3575 -> 0.4439 | 2.68 s/it | 通过并完成保存 |
五组训练的 loss 和 grad norm 均保持有限,并完成 100 步及模型保存。同一切分下EP16 eager/fused
的逐步 loss 相关系数为 0.997CP2/EP4/EFSDP2 eager/fused 为 0.977,说明两种 dispatcher 的
优化轨迹一致。性能收益与切分有关EP16 下 fused 比 eager 快约 13%,而加入 CP 和 EFSDP 后 fused
比 eager 慢约 30%,因此不能把 fused 视为所有 mesh 的默认最优选择。
EP16 两组使用 global batch 16 和 cutoff length 256CP2 两组使用 global batch 8 和 cutoff length
128A5 组使用 global batch 8 和 cutoff length 256。因此首末 loss 用于验证各组自身的收敛趋势,
不同切分组之间的绝对 loss 不应直接作为精度等价结论。