Open
Trainer 接入 Pipeline Parallel 方案 #10
yide12Add on  17 days ago
yide12
yide12Collaborators
17 days ago Create

Trainer 接入 Pipeline Parallel 设计方案

1. 背景与目标

当前 Trainer 以完整 Hugging Face 模型执行普通 forward。启用 Pipeline
Parallel(PP)后,用户仍提供完整模型,框架自动按 decoder layer 切分为逻辑
stage;每个物理 PP rank 只保留本地 stage,并在 stage 内继续应用 TP、CP、EP
和 FSDP。Trainer 保留数据准备、loss 统计、梯度裁剪、优化器和学习率调度。

当前交付覆盖 Qwen 风格 decoder-only causal language model,已验证裁剪的
Qwen3 dense 和 Qwen3-MoE。其他模型需要满足 stage forward 输入契约;跨层共享
可微状态、多模态输入或 encoder-decoder 结构需要额外适配。

2. 明确的对外配置

当前 VPP 配置如下:

accelerator:
  pp_size: 4

pipeline:
  enabled: true
  schedule: interleaved1f1b
  pp_microbatch_size: 1
  split: [[1, 1, 1, 1], [1, 2, 2, 1]]
  p2p_overlap: true
  p2p_multi_stream: false

该示例表示 4 个物理 PP rank、2 个 virtual chunk。二维 split 的每一行是一个
virtual chunk,每一列对应一个物理 PP rank。10 层模型的逻辑 stage 层数为
[1,1,1,1,1,2,2,1],rank 0 持有 S0/S4,rank 1 持有 S1/S5,rank 2 持有
S2/S6,rank 3 持有 S3/S7。

普通 PP 使用一维 split:

pipeline:
  enabled: true
  schedule: 1f1b       # 或 gpipe
  pp_microbatch_size: 1
  split: [2, 3, 3, 2]
  p2p_overlap: true
  p2p_multi_stream: false

interleaved1f1b 是 VPP 的一种交错 1F1B 调度,VPP 本身指一个物理 rank 持有多个
逻辑 stage。两者不是同一个概念,但本接口当前只通过 interleaved1f1b 开放 VPP。
普通一维 PP 仍使用 1f1b 或 gpipe;二维 split 配这两个普通 schedule 会报错。
因此,原配置中的 schedule: 1f1b 在这个二维 split 示例中改为 interleaved1f1b,
其余字段保持不变;不再根据 split 静默改选调度器。

具体分配如下(layer 下标从 0 开始):

逻辑 stage PP rank chunk 原始 layer 附加模块
S0 0 0 0 embedding
S1 1 0 1 —
S2 2 0 2 —
S3 3 0 3 —
S4 0 1 4 —
S5 1 1 5、6 —
S6 2 1 7、8 —
S7 3 1 9 final norm、LM head

逻辑 stage 按行展开,stage_id = chunk_id * pp_size + pp_rank。前向顺序为
S0 → S1 → … → S7;S3 → S4 需要从 rank 3 通信回 rank 0,不能把同 rank 的
S0 和 S4 直接串成一个 Sequential。反向沿相反的逻辑 stage 顺序运行。

3. 配置字段和约束

配置 含义与约束
accelerator.pp_size 物理 PP rank 数量;VPP 不增加设备数量。
pipeline.enabled 必须与 pp_size 大于 1 一致。
pipeline.schedule 普通 PP 为 gpipe 或 1f1b;VPP 为 interleaved1f1b。
pipeline.pp_microbatch_size 每个 pipeline microbatch 的样本数,必须为正数。
pipeline.split 空列表自动均衡;一维列表表示普通 PP 各 rank 层数;二维列表表示 VPP 的 virtual_chunk 与 pp_rank 层数。
pipeline.p2p_overlap 是否延迟 P2P 等待到计算消费前,默认 true。
pipeline.p2p_multi_stream 是否使用多流 P2P,默认 false;开启时必须同时开启 p2p_overlap。

二维 split 必须至少两行,每行长度等于 pp_size,元素为正整数,且总层数等于
模型 decoder layer 数。VPP 数量由二维 split 行数推导,不另设 vpp_size。空行、
空 stage 和层数不匹配会在构建阶段报错。

Trainer 的 batch 关系为:

B_local = global_batch_size / dp_size
num_microbatches = B_local / pp_microbatch_size

两次除法都必须整除;pp_microbatch_size 与数据加载器的
training.micro_batch_size 是不同配置。
以上 YAML 是合并到已有模型、数据、优化器配置中的片段,不是独立训练配置。
例如 DP=1、global_batch_size=4 时,本例产生 4 个 PP microbatch;VPP chunk 数
不参与 batch 的再次切分。所有元素表示 layer 数量,而不是 layer ID 或边界下标。
空列表只用于普通 PP 自动均衡;interleaved1f1b 配空列表、一维列表或只有一行的
二维列表均拒绝。关闭 PP 时同时设置 enabled=false 和 pp_size=1。

4. 对外 Python 接口

device = torch.device("npu", torch.npu.current_device())
pipeline = AutoPipeline(world_mesh=mesh, config=config.pipeline)
pipeline.build(
    model=full_model,
    parallelize_fn=apply_stage_parallelism,
    batch_size=dp_local_batch_size,
)
optimizer = torch.optim.SGD(pipeline.parameters(), lr=1e-3)
optimizer.zero_grad()

losses = pipeline.step(
    input_ids if pipeline.info.has_first_stage else None,
    target=labels if pipeline.info.has_last_stage else None,
    attention_mask=attention_mask,
)
optimizer.step()

AutoPipeline.build 接收完整模型,完成切分、裁剪、stage forward 改写、
stage-local 并行包装、PipelineStage 构建和 schedule 构建。Trainer 不直接对
单个 stage 调用普通 forward,而是调用 pipeline.step 驱动完整调度。
AutoPipeline 本身就是返回给 Trainer 的 nn.Module;parts 注册当前 rank 全部
本地 chunk,info 保存 schedule、stages 及首尾 stage 标志。当前接口不存在
pipeline.local_model,不使用早期 PipelineRuntime 设计。上述直接调用示例要求
已经初始化分布式 mesh,完整模型位于正确设备且参数有效;Trainer 场景自动构建。

5. 构建顺序

完整模型(默认 Meta 构建)
  -> 发现 decoder.layers 并生成逻辑 stage 分配
  -> 当前 PP rank 选择本地 stage
  -> 深拷贝并删除非本地 layer,保持原始 FQN
  -> 改写首、中、末 stage forward
  -> 保持 stage 参数为 Meta,不提前分配 NPU 参数存储
  -> 对每个本地 stage 应用 TP、CP、EP、activation 和 FSDP
  -> 构造 PipelineStage(stage_index=逻辑 stage id),通信设备取当前 NPU
  -> 按 schedule 构造 GPipe、1F1B 或 Interleaved1F1B
  -> 外层统一 model.to_empty(device),再使用已有流程读取权重或初始化参数
  -> 重建必要的 buffer
  -> Trainer 构建包含全部本地 chunk 参数的 optimizer

FQN 保持是 splitter 的硬性契约。TP/CP/EP planner、FSDP 参数分组和 checkpoint
依赖原始名称;外层 parts 前缀通过 checkpoint_fqn_aliases 映射。

PP 与非 PP 使用同一套 materialize 生命周期:Meta 模型先完成切分以及 stage-local
TP/CP/EP/FSDP 包装,FSDP 记录参数的 DTensor 和 shard 元数据;随后由外层
_materialize_and_load_model() 对整个 AutoPipeline 调用 model.to_empty(device),
再加载 checkpoint 或执行随机初始化。PipelineStage 的 P2P 通信设备取当前
NPU 加速器,不从仍处于 Meta 状态的参数推导通信设备。

该顺序不是“先把完整模型权重加载到每张卡”:切分和 stage-local sharding 仍先完成,
to_empty 只为当前 rank 的本地参数分配存储,随后才写入 checkpoint 或初始化值。
不过单个 stage、多个 VPP chunk、临时 buffer、激活和原始实参模型副本仍会影响峰值,
不能承诺任意模型不 OOM。首次调度执行时,FSDP 的 lazy initialization 会重新读取
materialize 后的参数,刷新内部 shard 引用,并检查参数已不在 Meta 上。

关键实现伪代码(device 仅为 NPU):

device = torch.device("npu", torch.npu.current_device())
with torch.device("meta"):
    full_model = Model(config)

def parallelize_stage(stage):
    stage = apply_tp_cp_ep(stage)
    stage = apply_activation_features(stage)
    return apply_fsdp(stage)             # 参数仍为 Meta

model = AutoPipeline(pp_mesh, pipeline_config).build(
    full_model,
    parallelize_fn=parallelize_stage,
    batch_size=dp_local_batch_size,
)
# 以下由已有 _materialize_and_load_model / _move_model_to_device 统一执行。
model.to_empty(device=device)           # 为本地参数分配存储,不初始化值
load_or_initialize(model)
rebuild_non_persistent_buffers(model)
optimizer = Optimizer(model.parameters())
model.step(...)                         # HSDP lazy init/reset,随后执行调度

原问题在于 device=next(part.parameters()).device 将 Meta 参数设备作为 P2P
buffer 设备,导致 dist.irecv(meta_tensor, ...) 失败;不是 FSDP 禁止 Meta 构建,
也不是外层 to_empty 必然破坏 FSDP 引用。因此删除 stage 内提前 to_empty 和
already_materialized 跳过分支,修复运行设备与参数存储设备混用的问题,复用原有
非 PP 初始化流程。AutoPipeline.build 不新增运行设备参数;在明确使用 NPU 的
Trainer 路径中,PipelineStage 直接取当前 NPU 作为 P2P 通信设备,无需新增 YAML
配置。

6. Stage forward 和输入

  • 首 stage 接收本地 input_ids,执行 embedding 和自己的 decoder layers。
  • 中间 stage 接收上游 hidden_states,并在本 rank 使用本地 attention_mask、
    position_ids 等辅助输入。
  • 末 stage 执行 final norm、LM head 和 loss。
  • P2P 只传输 stage 边界所需 Tensor;辅助输入由 Trainer 在各 rank 重建或提供。

当前 Qwen stage forward 的反向边界是 hidden_states。下游 backward 得到输入
hidden_states.grad,通过 PP runtime 传给上游;每个 stage 只更新自己的本地参数。
每个 stage 是本地 autograd 图,跨进程梯度连接由显式 P2P 实现,不是共享整张图。
当前 patch 本地构建 causal mask 和 rotary embedding,use_cache=False;输入 batch
及辅助 kwargs 的 sample/microbatch 顺序必须在同一 PP line 上一致。
compressed KV、index key、candidate blocks、4D mHC residual 等模型专有可微
状态需要扩展 StageState 输入、输出和梯度协议,不能仅通过 split 配置自动支持。

7. Schedule 行为

普通 gpipe:所有 microbatch 先完成前向,再按逆序完成反向,显存占用较高。

普通 1f1b:warmup 后交替执行前向和反向,降低激活峰值。

interleaved1f1b:每个物理 rank 拥有多个 virtual chunk,调度器按
logical_stage_id 和 microbatch_id 交错执行本地 chunk 的前向、反向和 P2P。
多个 chunk 必须由同一个 Interleaved1F1B 调度器管理。

Trainer 一个 step 的核心流程:

batch = concat_dp_local_batches()
optimizer.zero_grad()
losses = pipeline.step(
    batch.input_ids if pipeline.info.has_first_stage else None,
    target=batch.labels if pipeline.info.has_last_stage else None,
    attention_mask=batch.attention_mask,
)
grad_norm = trainer.prepare_optimizer_step()
trainer.step_optimizers_and_schedulers()

pipeline.step 已完成本 step 的 forward、backward、P2P 和本地梯度累计;Trainer
不再对返回 loss 额外调用 backward。
只有最后逻辑 stage 产生 loss;Trainer 将各 rank 指标求和,再除以
world_size / pp_size,使用物理 PP 数而不是逻辑 stage 总数。FSDP 的 unshard、
reshard、梯度归约由底层 schedule 针对各本地 stage 管理。

注意 loss 指标归约不等于训练梯度归一化。当前默认 loss 是每个 microbatch 的
mean CE,反向累计不自动等价于完整 batch 的 token mean;严格精度比较必须
固定 loss reduction、有效 token 权重和归一化。跨 stage MoE auxiliary loss
汇总不能从普通 CE 路径推断为已实现,需要单独适配和测试。

8. 混合并行、checkpoint 和边界

PP 使用独立的一维 pp_mesh。每个物理 PP rank 的本地 stage 使用对应的
stage-local mesh 应用 TP、CP、EP 和 FSDP;VPP chunk 不增加设备网格维度。
所有 rank 必须使用一致的 pp_size、split、schedule 和 P2P 配置。
拓扑满足 world_size = pp_size * tp_size * cp_size * dp_size;EP 依赖现有
专家布局和 planner 的嵌套关系,不能简单作为额外维度再乘一次。

p2p_overlap 透传 overlap_p2p;开启多流时 p2p_transport=multi_stream,否则为
auto。多流是底层实验性传输,不等同于 zero-bubble 或 B/F 计算重叠。调度器
消费接收 buffer 前必须等待完成,发送 buffer 也不能提前释放或复用。

保存和加载使用完整模型的原始 FQN;每个 PP rank 只处理自己负责的 layer,再按
TP/CP/EP/FSDP layout 读写本地 shard。完整断点续训、跨 PP size 转换、跨层 tied
weight 和特殊跨层状态仍需单独验收。

当前明确支持 Qwen 风格 decoder-only 模型、普通 GPipe、普通 1F1B、二维 split
的 VPP Interleaved1F1B、stage-local TP/CP/EP/FSDP,以及 P2P overlap 和可选
多流 P2P。不支持空 stage、二维 split 配普通 schedule,未适配模型的特殊
forward contract、encoder-decoder 和多模态输入。

9. 测试和交付状态

已运行本地回归:

tests/ut/auto_models/_transformers/test_pipeline.py
tests/ut/auto_models/_transformers/test_deferred_pretrained.py
tests/ut/trainer/test_distributed_runtime.py
tests/ut/trainer/test_text_trainer.py
合计 20 passed,60 subtests passed

四卡 NPU ST 已覆盖真实 apply_model_infrastructure 路径:Qwen3 普通 1F1B、
VPP interleaved1f1b、FSDP comm_fusion=false/true 均完成两步训练;Meta 参数在
FSDP 包装后再由外层 to_empty 成功 materialize,loss、梯度和参数更新与完整模型
对齐。独立四层 Qwen3 诊断记录中,最大梯度误差约 1.9e-6,最大更新后参数误差
约 3.7e-9。本轮另已重跑四卡十层 Qwen3 的 GPipe、普通 1F1B、VPP
以及 P2P 关闭/overlap/多流的九种组合,均完成两步 loss、梯度、参数更新对照。
本轮未重新验证八卡 Qwen3-MoE 的 TP/CP/EP 混合并行及断点续训。

最终接口不新增 runtime_device 参数,直接复用 PipelineStage 当前加速器默认值,
在本环境中为当前 NPU。移除该参数后重新独立启动上述两项四卡 ST,分别通过
(32.48 秒、35.24 秒);本轮未再次发生此前的 HCCL/SDMA 内部错误。

对应四卡 ST:

  • tests/torch/pipeline_parallel/test_auto_pipeline.py::test_qwen3_meta_pipeline_fsdp_matches_reference
  • tests/torch/pipeline_parallel/test_auto_pipeline.py::test_qwen3_pipeline_splits_match_reference

验收要求:覆盖 split 合法/非法组合、首/中/末 stage、所有本地 chunk 并行包装
和 FQN alias;同初始化、batch、dtype、loss 归一化下比较非 PP/PP/VPP 的 loss、
各参数梯度及更新后参数;执行多 microbatch、多 optimizer step 以检查缓存残留
和死锁;覆盖 P2P 关闭/overlap/多流、混合并行及 PP 关闭回归。Checkpoint 需
另外验收保存、恢复 optimizer 状态、继续训练;性能用固定硬件与 batch 实测
吞吐、step time、峰值内存,不承诺 VPP 一定加速。

主要实现文件:

  • hyper_parallel/models/_transformers/pipeline.py
  • hyper_parallel/models/_transformers/model_builder.py
  • hyper_parallel/trainer/runtime/distributed.py
  • hyper_parallel/trainer/text_trainer.py
  • hyper_parallel/core/pipeline_parallel/scheduler.py

代码基线位于 GitHub trainer_pp_master 分支,PR:
https://github.com/mindspore-ai/hyper-parallel/pull/1280

截至 2026-10-26,本正文中的 Meta/materialize 生命周期修正及当前 NPU 默认通信
设备修订已实现并通过上述 UT/ST,提交 07155cc1 已推送到 PR 的
trainer_pp_master 源分支。

likedislike
yide12yide12Collaborators
17 days ago Modify description of issue
yide12yide12Collaborators
17 days ago Modify description of issue
yide12yide12Collaborators
17 days ago Modify description of issue
yide12yide12Collaborators
13 days ago Modify description of issue
yide12yide12Collaborators
13 days ago Modify description of issue
yide12
yide12Collaborators
13 days ago Comments:

是的,之前尝试过。当时做的是“直接复用 Qwen 的 AutoPipeline 接入”,没有完成 DeepSeek V4.1 的专用适配。当前仍需要补齐模型的 stage forward 和跨 stage 状态协议,不能只改 PP 配置。

1. 之前验证到了哪里

验证记录:

场景 结果
DeepSeek V4.1 四层模型,CPU 非 PP 前反向及更新成功,loss=7.62963009
Qwen3,NPU PP2 前反向及更新成功
DeepSeek V4.1,NPU PP2,GPipe/1F1B 首 stage 的 RoPE 调用失败,未执行到 loss
DeepSeek V4.1,NPU 非 PP 当时另有 mHC 自定义算子错误

PP 首个错误是缺少 layer_type,触发 None_inv_freq。但修复 RoPE 只是第一步,后面还有模型状态传递问题。上述为历史结果,本轮没有重新运行。

2. 为什么当前 Qwen 方案不能直接复用

当前 stage forward 基本假设:

hidden = layer(hidden, position_embeddings=rope, ...)
# 最后一个 stage:norm → lm_head

DeepSeek V4.1 的真实协议不同:

项目 DeepSeek 接入要求
mHC 残差流 hidden 是 [B,S,hc_mult,H],还要携带 pre_mix
RoPE 分别生成 main、compress 两组
共享 Attention 状态 compressed KV、index key、Top-k、candidate blocks 可能来自上游 stage
原始 token IDs 非首 stage 的 Engram 仍需要,不能用 hidden 替代
shifted mHC 开启时还要传递 pending output、post mix、residual mix
模型末端 先完成 pending mHC,再 _hc_pre(hidden, pre_mix) → norm → lm_head

具体见 decoder layer 接口、输入初始化和shifted mHC 执行。

3. 建议如何接入

保留现有 AutoPipeline 对外配置和调度,新增可选的模型级 PP adapter。 DeepSeek 特有逻辑放在模型目录中,公共 Trainer 不判断模型名称。这也符合仓库模型接入规范。

当前 AutoPipeline.build 固定调用通用 forward patch,需要扩展为可选 provider;以下是建议方案,不是已有接口:

# 保持原始层编号/FQN,按完整模型依赖生成每个边界的状态清单
parts = split_model(model, split)
for part in parts:
    adapter.install_stage_forward(part, boundary_schema)
    part = apply_tp_cp_ep_fsdp(part)
    stages.append(PipelineStage(part, ...))

schedule = make_schedule(stages, pipeline_config)

DeepSeek adapter 负责首 stage 初始化、中间 stage 恢复状态、运行本地层、末 stage 收尾和 loss。原有分层、stage-local 并行包装、调度和 optimizer 生命周期继续复用。

4. 跨 stage 最关键的是传什么

以之前的四层 PP2、split: [2,2] 为例:

Stage 0:Embedding → 初始化 mHC → Layer 0、1
    │
    │ hidden、pre_mix
    │ 下游仍需要的 compressed KV、index key、Top-k、candidate blocks
    │ shifted mHC 开启时的 pending 状态
    ▼
Stage 1:恢复状态 → Layer 2、3 → mHC 收尾 → Norm → Head → Loss

共享状态应按原始 source layer ID 打包成顺序固定的 Tensor tuple,接收端再恢复,不能直接传 Python 字典或每个 stage 重建空状态。

反向不仅传 hidden.grad,还要传共享 KV、index key、混合系数等实际可微状态的梯度;整数索引只传前向。状态经过中间 stage 时,要累计本地消费和更下游产生的梯度,不能 detach。每个 microbatch 的状态必须独立,并保留到 backward/recompute 完成。

现有 PipelineStage 通信和反向实现已有多 Tensor、requires_grad 过滤基础,不需要先重写调度引擎;但仍需验证混合整数/浮点 payload、未使用输入的梯度和多 microbatch 行为。

5. 还必须补齐的入口与验证

当前 TextTrainer将 input_ids 取出,只交给首 stage。需要通过通用输入适配机制,让各 stage 获得对应 microbatch 的原始 tokens、位置和 packed 元数据;packed 对象也不能简单沿用第一份后再机械切 batch。

实施顺序建议:先验证单进程切层与完整模型的 logits/loss/梯度一致,再验证 PP2 GPipe、1F1B;确认当前 NPU 非 PP 基线后,再组合 TP/CP/EP/FSDP 和 VPP。视觉、DSpark、Host Engram 的参数归属与附加 loss 单独覆盖,不能由四层文本实验推断已支持。

归根结底:切层机制已有,缺的是 DeepSeek 的“完整 stage 计算契约”和“带梯度的跨层状态传递”。

likedislike
yide12
yide12Collaborators
13 days ago Comments:

_should_defer_group_init 是一个非常小的判定函数:决定某个 DeviceMesh 维度在构造时是否暂不创建通信组。

def _should_defer_group_init(sub_layout, backend_override):
    return backend_override == "fake" or sub_layout.numel() == 1

源码位置:device_mesh.py:138

判定规则如下:

条件 返回值 行为
backend_override == "fake" True 跳过当前维度的立即建组
当前逻辑维度只有一个 rank,即 sub_layout.numel() == 1 True 单卡维度没有立即建通信组的必要
其他情况 False 立即调用 split_group 创建该维度的通信组

这里的 sub_layout 是“一个逻辑 mesh 维度”的布局,而不是整个 DeviceMesh。例如 (pp=1, dp=2, tp=4) 中,pp 对应的 sub_layout.numel() 是 1,因此会延迟;dp 和 tp 通常会立即建组。

在 HP 中,它唯一的直接调用点位于 _init_process_groups_for_layout:device_mesh.py:481。每个 mesh 维度都会执行以下流程:

计算该维度的 rank 分组
        ↓
_should_defer_group_init(...)
   ├─ True  → _dim_group_names[dim] = None,不调用 split_group
   └─ False → split_group(...),并缓存通信组和 group_key

对应核心代码在 device_mesh.py:497。注意,返回 True 时只是跳过 _utils.split_group();rank 分组信息的计算仍会执行。

它在 HP 中主要覆盖两类场景:

  1. 普通 DeviceMesh 初始化。DeviceMesh 初始化通信状态时会进入 _init_process_groups,最终调用上述判定。即使没有传 backend_override,大小为 1 的维度也会自动延迟建组,避免启动时创建无意义的单 rank PG。

  2. _flatten / _unflatten 构造派生 mesh。HP 为兼容 Torch 风格的 mesh 变换,允许按维度传入 backend_override:device_mesh.py:1051、device_mesh.py:1164。例如:

mesh = world_mesh._unflatten(
    0,
    (2, 2, 2),
    ("dp", "cp", "tp"),
    backend_override={"dp": "fake"},
)

此时 cp、tp 会立即建组,dp 的 _dim_group_names[0] 暂时记录为 None。仓库测试验证了初始化只调用两次 split_group,第一次调用 mesh["dp"].get_group() 时才调用第三次:test_device_mesh.py:709。

延迟并不等于永远不创建。之后调用 get_group() 时,如果 group key 为 None 或缓存中没有该组,会进入 _materialize_dim_group(),重新计算 ranks、调用 split_group、写入 EXISTING_COMM_GROUPS,再返回通信组:device_mesh.py:1298、device_mesh.py:1344。切片得到的子 mesh 也会继承 group 名、backend 标记和 group 来源,从而复用/触发原维度的延迟组:device_mesh.py:676。

需要特别注意:HP 当前的 "fake" 更像“延迟初始化标记”,并不是完整的 fake 通信后端。_materialize_dim_group() 没有读取 _dim_group_backends,按需创建时仍然调用普通 _utils.split_group();而后者最终使用 dist.new_group():device_mesh.py:1314、_utils.py:141。因此:

  • fake 可以减少 mesh 构造阶段的建组开销;
  • 如果后续真的对该维度调用 get_group(),HP 仍会创建真实通信组;
  • 其他 backend 字符串虽然能被保存和规范化,但当前这条建组路径没有把它们传给 dist.new_group(backend=...)。

仓库内的实际情况是:fake override 主要出现在 DeviceMesh/TorchTitan 兼容测试中;HP 的 encoder/context parallel 生产代码虽然使用 _unflatten,目前没有显式传 fake。不过“大小为 1 的维度自动延迟”会自然作用于所有 DeviceMesh 初始化流程。单元测试也分别覆盖了 fake、单元素布局和普通布局三种判定:test_device_mesh.py:326。

一句话总结:这个函数是 HP DeviceMesh 的“是否启动时立即建组”开关,用 fake 或单 rank 维度把通信组创建推迟到首次 get_group(),以降低初始化阶段不必要的 PG 创建开销。

likedislike
yide12yide12Collaborators
13 days ago Modify description of issue
yide12
yide12Collaborators
8 days ago Comments:

会有,而且不只是函数名不同。当前替换是通过适配器对齐部分接口,并非把 Omni 算子原样换成 CANN 算子。

功能 Omni 路径 当前 CANN 路径 主要差异
Indexer 选 TopK Omni 的 npu_lightning_indexer 封装 cann_ops_transformer.lightning_indexer CANN 调用需要布局、mask、metadata 等参数;适配器转换长度和权重 dtype,并整理输出。
Sparse attention dsa_sparse_attention_rescale 使用 Omni 增强版 sparse attention,再与 parameter-sink attention 合并 CP 路径使用 CANN sparse_flash_attention CANN 这条调用不接收 sink 参数,也不做 Omni wrapper 中的 sink 输出重缩放;K/V 共享和反向梯度的处理也由适配器实现。两者不是语义等价的直接替换。
Sparse KL Omni 的融合算子 npu_sparse_lightning_indexer_grad_kl_loss 接收主 attention 状态及 softmax 统计,返回 Indexer 梯度和 loss CANN sparse_lightning_indexer_kl_loss_grad 接收预先算好的 target,返回梯度和 prediction 适配器必须额外构造 target、计算标量 KL loss。
Dense Indexer/KL 原先的 Omni 接入路径 CANN dense LSE 和 KL grad 算子 Torch 路径由 FlashAttention 提供 dense teacher,再调用 CANN Indexer 算子;调用边界及 loss 归一化由适配器负责。

对应实现可见 V3.2 CP 调用分支、CANN 适配层 和仍保留 Omni sink/rescale 的 稀疏 attention 实现。

还有两个重要边界:

  • 当前不是全仓所有 DSA 路径都已切到 CANN。 V3.2 CP wrapper 使用 CANN DSA 接口;通用 dsa_attention.py 路径仍调用 dsa_sparse_attention_rescale,它导入 Omni custom ops。
  • sink 行为需要单独确认。 Omni rescale 路径支持把参数 sink attention 与 sparse attention 合并;V3.2 CP 的 CANN sparse 分支没有对应输入或合并步骤。如果目标模块启用了 parameter sink,这就是需要处理的语义差异,不能仅凭接口适配认定结果等价。

另外,长度也不是原样传递:当前 public DSA 接口使用不含起始零的累计结束位置,适配层为 CANN TND 格式补 0,并转换为 int32。因此结论是:接口存在差异,部分已由适配层处理;sink、loss 构造和调用路径覆盖范围仍需区分。

likedislike
yide12
yide12Collaborators
8 days ago Comments:

V1 和 V2 是 CANN ACLNN 为同一个 GradKLLoss 算子提供的两套 C/C++ 调用接口版本。V1 是原有接口 aclnnSparseLightningIndexerGradKLLoss;CANN 9.2 新增了 V2 接口aclnnSparseLightningIndexerGradKLLossV2,相比 V1 在 actualSeqLengthsKey 与 scaleValue 之间多了一个可选的 sinks 输入。CANN 9.2 仍保留 V1 以兼容旧代码,因此调用 V1 本身是合法的,但内部需要将 V1 参数适配到新版实现。

定位上,已绕过 HyperParallel、DTensor、分布式和 DFunction,直接通过 MindSpore C++ 扩展调用 V1 仍可复现;同一封装改为直接调用 V2、并显式传入 sinks=nullptr后,连续调用不再改写 query/key。Torch NPU 调用同一算子并补齐 FA 前置路径也未复现。当前最强结论是:问题集中在 CANN 9.2 的 V1 兼容适配或相关 executor/缓存路径。

likedislike
yide12
yide12Collaborators
8 days ago Comments:

【关于group】

1, mindspeed的group
1)初始化,创建各维度的group,各维度的group都是全局变量,比如:(注:开启进程快恢时,加use_local_synchronization=True入参)
global _TENSOR_MODEL_PARALLEL_GROUP

2)每次使用,都从全局变量里面取:def get_tensor_model_parallel_group:
def get_tensor_model_parallel_group(check_initialized=True):
"""Getthetensor-model-parallelgroupthecallerrankbelongto."""
if check_initialized:
assert(
_TENSOR_MODEL_PARALLEL_GROUP is not None
) , 'tensormodelparallelgroupisnotinitialized'
return _TENSOR_MODEL_PARALLEL_GROUP

tp_group=mpu.get_tensor_model_parallel_group()
torch.distributed.all_reduce(tensor, op=op, group=tp_group)

3)故障后新拉起进程的初始化:按原流程创建各维度group,创建group打monkey-patch,加use_local_synchronization=True入参;

4)重建group时,所有进程,通过reinit_process_group处理ProcessGroup对象;
arf_rebuild_process_group_callback-->init_tensor_parallel_group()

def init_tensor_parallel_group():
os.environ['TORCH_DIST_INIT_BARRIER']='1' #让恢复组的时候带有barrier
.....
if mpu._TENSOR_MODEL_PARALLEL_GROUP is not None:
torch.distributed.reinit_process_group(mpu._TENSOR_MODEL_PARALLEL_GROUP,rebuild_link=True)
.....
os.environ['TORCH_DIST_INIT_BARRIER']='0'

likedislike
yide12
yide12Collaborators
8 days ago Comments:

2,hyper-parallel的group
1)初始化及使用:【还有一个world group】
a, 根据devicemesh创建各维度的group:DeviceMesh._ init_____>_init process_groups --->_init process_groups_for_layout --> split_group:对传入的整个 rank
列表集合逐一执行new_group():把rank-list字符串作为key,缓存到EXISTING_COMM_GROUPS
【使用】:mesh.get_group(dim)
【说明】: 创建group时,生成key,并放入缓存;使用group时,内部计算出key,到缓存里面找;
b, pp中创建多流group: create_p2p_multi_stream_groups: 把rank-1ist字符串作为key,缓存到_P2P_MULTI_STREAM_GROUPS
【使用】创建时保存在classPipelineScheduleRuntime的self._p2p_multi_stream_groups, 使用时,从self._p2p_multi_stream_groups中取;
c, dcp中创建广播group: _build_broadcast_groups
【使用】def broadoast shard(似乎没有地方用到?
d, 重排中创建group:_construct_all_concat/_construct_all_concat_new/_construct_all_to_all -->_utils.create_group(rank_list):把rank-list字符串作为key,缓存到
EXISTING_COMM_GROUPS (一般情况下,会和a中重复,不会重复创建,因为缓存中有)
【使用】:调用_utils.create_group(rank_1ist)后直接使用,里面先去EXISTING_COMM_GROUPS中查找
e, muon优化器:_get_or_create_sub_pg(注:存放在class BaseDistributedOptimizer中)-----针对910B的优化,可以先不考虑它
【使用】_broadcast_op_fused--->_hierarchical_broadcast_buffer >dist.broadcast(batch_buffer,sro=global_src_rank, group=pg)
【其他】hsdp_group.shard pgs ...从dtensor中直接获取,使用mesh.get_group(),没有其他group
f, pp中:_init_pp_group/_init_shared parameter_group, 分别用于pp stage间send/recv及 共享参数的广播(都有个是否存在mesh的条件)的
【说明】
第一个存放在classPipelineStage(PipelineStageBase):的self.pp_group中,如果有mesh,也是用self.mesh.get_group();传给pp调度,在pp调度中send/recv中用;
第二个:如果有mesh,创建group时,就从mesh里面拿,self._init_shared_parameter_group后直接使用;
【总结】要处理的主要为两类,一类是EXISTING_COMM_GROUPS,另一类是_P2P_MULTI_STREAM_GROUPS
3)故障后新拉起的进程的初始化,按原流程创建各维度group,创建group打monkey-patch, 加use_local_synchronization=True入参;
4)重建group时,所有进程,也通过reinit process_group去 reinit一下
【说明】:需要在正常流程中,把EXISTING_COMM_GROUPS和_P2P_MULTI_STREAM_GROUPS 的 信息保存下来;重建group时,从这里面去获取信息;

likedislike
yide12
yide12Collaborators
6 days ago Comments:

在 PyTorch 的官方公开 API 中,没有直接获取所有 Process Group(进程组)的接口。PyTorch 的设计初衷是让用户自己去管理和维护通过 dist.new_group() 创建的子进程组引用。

不过,如果你在调试或由于某种特殊原因必须获取所有已创建的进程组,可以通过访问 PyTorch 分布式底层的私有变量来实现。

所有实例化的进程组都被缓存在 torch.distributed.distributed_c10d 模块的私有字典 _pg_map 中。

获取所有 Process Group 的代码示例:

import torch.distributed as dist

# 确保分布式环境已初始化
if dist.is_initialized():
    # 导入底层的私有变量
    from torch.distributed.distributed_c10d import _pg_map, _pg_names
    
    # _pg_map 的 key 就是所有的 ProcessGroup 对象
    all_groups = list(_pg_map.keys())
    
    print(f"当前共有 {len(all_groups)} 个 Process Group (包含默认的 WORLD group)")
    
    for pg in all_groups:
        # 获取 backend (如 'nccl', 'gloo')
        backend = dist.get_backend(pg)
        # _pg_names 存储了 group 的名字
        name = _pg_names.get(pg, "Unknown")
        
        # 获取该组的全局 rank 和 world size (当前进程如果在组内的话)
        try:
            rank = dist.get_rank(pg)
            size = dist.get_world_size(pg)
            print(f"Group Name: {name} | Backend: {backend} | World Size: {size} | Rank in group: {rank}")
        except RuntimeError:
            print(f"Group Name: {name} | Backend: {backend} | 当前进程不在此 group 中")
else:
    print("分布式环境尚未初始化。")

⚠️ 注意事项:

  1. API 稳定性:带有下划线前缀(如 _pg_map, _pg_names)的变量属于 PyTorch 的内部私有实现。虽然这个变量在 PyTorch 1.x 和 2.x 版本中一直存在,但在未来的大版本更新中有被重构或更改名称的风险。
  2. 默认 Group:当你调用 dist.init_process_group() 时,PyTorch 会自动创建一个默认的全局进程组( WORLD 组)。这个组也会包含在 _pg_map 中。如果你需要引用全局组,在公开 API 中推荐直接传参 group=None,或者使用 dist.GroupMember.WORLD。
likedislike