是的,之前尝试过。当时做的是“直接复用 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 计算契约”和“带梯度的跨层状态传递”。


_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 中主要覆盖两类场景:
-
普通 DeviceMesh 初始化。
DeviceMesh初始化通信状态时会进入_init_process_groups,最终调用上述判定。即使没有传backend_override,大小为 1 的维度也会自动延迟建组,避免启动时创建无意义的单 rank PG。 -
_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 创建开销。


会有,而且不只是函数名不同。当前替换是通过适配器对齐部分接口,并非把 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 构造和调用路径覆盖范围仍需区分。


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/缓存路径。


【关于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'


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时,从这里面去获取信息;


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


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 配置如下:
该示例表示 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:
interleaved1f1b 是 VPP 的一种交错 1F1B 调度,VPP 本身指一个物理 rank 持有多个
逻辑 stage。两者不是同一个概念,但本接口当前只通过 interleaved1f1b 开放 VPP。
普通一维 PP 仍使用 1f1b 或 gpipe;二维 split 配这两个普通 schedule 会报错。
因此,原配置中的 schedule: 1f1b 在这个二维 split 示例中改为 interleaved1f1b,
其余字段保持不变;不再根据 split 静默改选调度器。
具体分配如下(layer 下标从 0 开始):
逻辑 stage 按行展开,stage_id = chunk_id * pp_size + pp_rank。前向顺序为
S0 → S1 → … → S7;S3 → S4 需要从 rank 3 通信回 rank 0,不能把同 rank 的
S0 和 S4 直接串成一个 Sequential。反向沿相反的逻辑 stage 顺序运行。
3. 配置字段和约束
二维 split 必须至少两行,每行长度等于 pp_size,元素为正整数,且总层数等于
模型 decoder layer 数。VPP 数量由二维 split 行数推导,不另设 vpp_size。空行、
空 stage 和层数不匹配会在构建阶段报错。
Trainer 的 batch 关系为:
两次除法都必须整除;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 接口
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. 构建顺序
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 参数设备作为 P2Pbuffer 设备,导致
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 和输入
position_ids 等辅助输入。
当前 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 的核心流程:
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. 测试和交付状态
已运行本地回归:
四卡 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_referencetests/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 一定加速。
主要实现文件:
代码基线位于 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源分支。