已开启
# 【RFC】分布式算子 ST 测试架构重构 #223
hedongdong创建于 6月16日
6月16日 修改了issue 的描述
6月16日 修改了issue 的描述
6月16日 修改了issue 的描述
6月16日 修改标题为 “【RFC】shard_ops 分布式算子测试框架重构”,原标题为“shard_ops 分布式算子测试框架重构”
6月16日 修改了issue 的描述
6月16日 修改了issue 的描述
6月17日 修改了issue 的描述
6月21日 修改了issue 的描述
6月21日 修改了issue 的描述
6月21日 关联了pull request:test: gate fail-fast and error reporting validation for #223
6月23日 关联了pull request:feat: migrate getitem and setitem ST to new declarative case scheme
Hhedongdong
6月24日 关联了pull request:test(shard-ops): finalize OpShardCase migration, remove legacy ST tests
6月24日 关联了pull request:test(shard-ops): finalize OpShardCase migration, remove legacy ST tests
6月25日 关联了pull request:chore: demote 83 npu_level0 cases to level1 to reduce ST timeout
6月26日 关联了pull request:chore: revert npu_level0 demotion and demote test_submesh_group1
Hhedongdong
7月2日 关联了pull request:test(shard-ops): finalize OpShardCase migration, remove legacy ST tests
7月2日 关联了pull request:test(shard-ops): finalize OpShardCase migration, remove legacy ST tests
7月2日 关联了pull request:test: mark submesh case as level1
8月22日 修改标题为 “# 【RFC】分布式算子 ST 测试架构重构”,原标题为“【RFC】shard_ops 分布式算子测试框架重构”
8月22日 修改了issue 的描述
8月22日 修改了issue 的描述
【RFC】分布式算子 ST 测试架构重构
0. RFC 摘要
本 RFC 面向 HyperParallel 分布式算子系统测试(ST),将原有“每个算子独立 launcher + worker”的
脚本式测试方式重构为声明式、跨平台、可调度的测试架构。
核心目标是让算子开发者只描述:
测试框架统一承担单卡参考计算、分布式执行、结果汇聚、数值比较、进程编排、设备分配和报告聚合。
范围
非目标
derived_inputs、needs_mesh、solo_launcher等必要扩展点;1. 背景与问题
旧框架采用
test_parallel_op_X.py(pytest launcher)与_test_parallel_op_X.py(torchrun/msrun worker)双文件模式。每个算子和场景都重复实现初始化通信、创建 DeviceMesh、
构造输入、切分张量、汇聚输出和比较结果。
1.1 原框架问题
1.2 分布式测试硬约束
torchrun或msrun创建多个独立 rank 进程@arg_mark接入现有 level0/level1 门禁2. 架构目标与质量属性
InputSpec.seed固定输入;数值可比较场景必须得到稳定参考结果ShardBackend接口,后端独立完成设备操作case_{op}.py并声明OpShardCase,无需手写 launcher/worker 样板2.1 关键性能目标
3. 4+1 架构视图
3.1 逻辑视图
框架由声明模型、注册发现、套件规划、父进程调度、rank 执行、后端适配和报告聚合组成。
classDiagram direction LR class OpShardCase { +name +fn +inputs +placements +mesh_shape +tags } class InputSpec class DerivedSpec class CompareSpec class Registry { +register(case) +load_cases_from_package(package) } class SuitePlanner { +build_suite_groups(...) +pack_into_batches(...) } class Runner { +run_groups(groups, framework, device_type) } class ShardBackend { <<interface>> +maybe_init_dist() +make_tensor(spec) +distribute(tensor, mesh, placement) +local_to_global(tensor) +assert_close(expected, actual, compare) +recover_after_failure() } class TorchHcclBackend class TorchGlooBackend class MindSporeAscendBackend class Reporter OpShardCase *-- InputSpec OpShardCase *-- DerivedSpec OpShardCase *-- CompareSpec Registry o-- OpShardCase SuitePlanner --> Registry Runner --> SuitePlanner Runner --> ShardBackend Runner --> Reporter ShardBackend <|.. TorchHcclBackend ShardBackend <|.. TorchGlooBackend ShardBackend <|.. MindSporeAscendBackend核心职责边界:
OpShardCase只描述测试意图,不处理进程、设备和通信;SuitePlanner只规划 case、group、batch 和设备预算,不执行算子;Runner管理 launcher 生命周期和设备隔离,不包含平台张量操作;ShardBackend封装平台张量、分布式初始化、切分、汇聚和比较;entry在各 rank 上执行统一流水线,并将结果交给 Reporter。3.2 开发视图
平台无关共享框架本身不直接依赖 PyTorch 或 MindSpore;平台张量和通信操作统一通过
ShardBackend实现。case package 在用例发现阶段注册声明,真正的分布式初始化和设备执行发生在launcher 创建的 rank 进程中。
3.3 进程视图
sequenceDiagram actor CI as CI / Developer participant Pytest as pytest parent participant Suite as SuitePlanner participant Runner participant Launcher as torchrun / msrun participant Rank as rank processes participant Backend as ShardBackend participant Report as Reporter CI->>Pytest: run suite entry or CLI Pytest->>Suite: build_suite_groups(tags, filter) Suite-->>Pytest: GroupSpec list Pytest->>Runner: run_groups(groups) Runner->>Runner: pack groups into 8-card batches loop each serial batch par non-overlapping device groups Runner->>Launcher: start group with assigned devices Launcher->>Rank: create N rank processes Rank->>Backend: init distributed and DeviceMesh loop each case in group Rank->>Backend: build full tensors Rank->>Rank: derive full inputs if configured Rank->>Rank: run standalone reference Rank->>Backend: distribute inputs Rank->>Rank: run distributed function Rank->>Backend: gather distributed output Rank->>Backend: compare expected and actual Rank->>Report: append per-rank JSONL result end Launcher-->>Runner: group exit status end end Runner->>Report: summarize all groups and ranks Report-->>Pytest: pass or aggregated failure不包含派生输入时,每个 case 执行原稿定义的基础六步流水线:
配置
derived_inputs后,在 build 与 reference 之间增加 derive,扩展为七步流程:derive必须在完整主输入上执行一次,再按声明的 placement 切分;不能在各 rank 的局部分片上重算依赖全局信息的派生量。七步流程是六步基础模型对 attention 等场景的兼容扩展,不是两套执行架构。
3.4 部署视图
torchrunTorchGlooBackendtorchrunTorchHcclBackendmsrunMindSporeAscendBackend一台 8 卡节点同时承担两类角色:
mesh_shape决定num_proc,必须满足num_proc == math.prod(mesh_shape)。对普通算子,mesh 轴名是文档标签并可按 ndim 归一化;对
needs_mesh=True的 MC2 算子,轴名参与通信组解析,必须保留真实名称。3.5 场景视图(+1)
register(OpShardCase(...))compare_outputs=(...)CompareSpec.shape()DerivedSpecneeds_mesh、真实mesh_dim_namesHYPER_PARALLEL_SHARD_CASE_FILTERrecover_after_failure()4. 架构模式
OpShardCase、InputSpec、CompareSpecShardBackend为端口,三个 Backend 为适配器register()和 case/backend 注册表5. 架构决策与权衡
5.1 ADR:自定义 Runner 还是 pytest fixture
备选方案 A:纯 pytest conftest + fixture
将
torchrun/msrun放在最外层,由 session fixture 初始化通信和 DeviceMesh,使用pytest_generate_tests/parametrize驱动全部 case。优点:
--lf、--durations等生态能力可直接使用;局限:
备选方案 B:自定义 Suite + Runner + Entry
普通 pytest 父进程完成分桶、装箱和调度,每个 group 启动独立 torchrun/msrun,rank 进程通过统一 entry
执行声明式 case。
优点:
@arg_mark门禁入口,CI 无需重构;代价:
决策
采用方案 B。当前项目同时存在 2/4/8 卡 case、设备并发调度和通信恢复的真实需求,调度是框架的核心能力,
不能简单外移给 CI。对于只含单一 mesh 的新项目,方案 A 仍然是更轻量的选择。
5.2 其他关键决策
OpShardCase,而非每个 case 手写流程fnfn尽量保持纯算子语义DerivedSpec模型tags+ suitetag_includesolo_launcher作为隔离逃生口6. 设计与交付过程
tests/shard_ops/framework/和平台适配层pr-check-pass与ci-pipeline-passed这套过程体现了“问题与约束识别 → 质量属性定义 → 多方案权衡 → 架构设计 → 试点 → 全量迁移 →
量化验证 → 门禁交付”的架构设计和交付闭环。
7. 风险与缓解
num_proc与 mesh 大小不一致导致 HCCL 挂起DerivedSpec强制在完整输入上计算一次后再切分solo_launcher=Truefn8. 接口与执行规范
8.1 最小用例
import torch from hyper_parallel.core.dtensor.placement_types import Replicate, Shard from tests.shard_ops.framework import CompareSpec, InputSpec, OpShardCase, register def _cat_dim1(x, y): return torch.cat((x, y), dim=1) register(OpShardCase( name="cat_ops_dp_dim1", fn=_cat_dim1, inputs=[ InputSpec(shape=(8, 16), init="randn", seed=42), InputSpec(shape=(8, 8), init="randn", seed=43), ], placements=[ (Shard(0), Replicate()), (Shard(0), Replicate()), ], compare=CompareSpec.allclose(rtol=1e-4, atol=1e-4), mesh_shape=(2, 2), mesh_dim_names=("dp", "tp"), tags=("cpu_level0", "npu_level0"), ))8.2 InputSpec 字段
shape()表示标量张量init"randn"randn/uniform/ones/zeros/arangeseedNonedtype"float32"dataNonenp.ndarray精确值,设置后覆盖init8.3 平台抽象接口
ShardBackend定义统一端口,各后端独立实现:maybe_init_dist()init_dist()init_dist_gloo()D.init()+ms.set_device("Ascend")make_tensor(spec)torch.from_numpy().npu()torch.from_numpy()Tensor(arr)distribute(...)distribute_tensor()distribute_tensor()local_to_global(...)local_to_global()dist_tensor.full_tensor()assert_close(...)torch.equal/allclosenp.array_equal/allcloserecover_after_failure()dist.barrier()comm_func.all_reduce8.4 Placement 约定
placement 元组长度必须等于 mesh 维数,而不是张量维数:
# 4-D tensor on a 2-D mesh (dp, tp) mesh_shape = (2, 2) placements = [(Shard(0), Shard(1))] # 2-D tensor on a 1-D mesh (tp) mesh_shape = (2,) placements = [(Shard(1),)]8.5 门禁路由
cpu_level0npu_level0cpu_level1npu_level1tag 采用
{platform}_{level}约定:tags=("cpu_level0", "npu_level0") # CPU/NPU 均进入 level0 tags=("cpu_level0", "npu_level1") # 不同硬件进入不同等级 tags=("npu_level0",) # 仅 NPUsuite entry 通过
tag_include进行单一入口过滤,再使用@arg_mark对接现有 CI:_GROUPS_CPU_LEVEL0 = build_suite_groups( cases_pkg=CASES_PKG, tag_include={"cpu_level0"}, fail_fast=True, ) @arg_mark( plat_marks=["cpu_linux"], level_mark="level0", card_mark="allcards", essential_mark="essential", ) def test_shard_ops_cpu_level0(): _run_groups(_GROUPS_CPU_LEVEL0, "torch", "cpu", fail_fast=True)8.6 8 卡并发调度
_plan_group_count按global_num_proc // num_proc计算并发槽位;4 卡 case 最多两路并发;_pack_into_batches按num_proc降序,以 first-fit 方式装箱到 8 卡 batch;mp.Process并行启动,并分配互不重叠的设备切片;max_cases_per_group=256控制单组容量、避免过度碎片化;实际 group 数还会根据并发槽位规划;8.7 子进程执行流程
基础六步模型:
配置
derived_inputs时,在第 1、2 步之间增加 derive:在完整主输入上计算派生值,再分别供参考路径使用并按 placement 切分到分布式路径。
8.8 本地运行
# CLI:运行文件内全部 case python -m tests.shard_ops.framework tests/torch/shard/ops/cases/case_sort.py # CLI:运行文件内单个 case python -m tests.shard_ops.framework \ tests/torch/shard/ops/cases/case_sort.py::sort_ops_2d_dp # CLI:显式切换框架 python -m tests.shard_ops.framework --framework mindspore --case sort_ops_2d_dp # Suite entry + 环境变量过滤 HYPER_PARALLEL_SHARD_CASE_FILTER="sort_ops_*" \ pytest tests/torch/shard/ops/test_shard_ops_suite.py::test_shard_ops_cpu_level0 -vs完整字段、特殊场景和排错说明见
OpShardCase 介绍与实践指导。
9. 验收结果
9.1 功能与迁移
迁移覆盖 elementwise、reduce、view、index、embedding、matmul、softmax、attention、MC2,以及
DFunction DSA/MHC 等算子。确认属于 CANN/ACLNN 层且无法在 HyperParallel 测试框架内解决的问题,
单独记录为 D 类问题,不通过放宽断言或静默跳过掩盖。
9.2 回归与性能
PR 857 记录的六组回归结果:
level0 与旧框架代表用例对比:
性能收益主要来自 launcher 启动合并和设备调度,而不是单个算子计算加速。评估时必须同时说明用例数量、
后端环境和 wall-clock,不能将两组数据误写成同规模 benchmark。
9.3 交付状态
10. 影响范围
tests/shard_ops/framework/11 个文件;framework/共 4 个文件;python -m tests.shard_ops.framework;HYPER_PARALLEL_SHARD_CASE_FILTER;derived_inputs、CompareSpec.shape()、compare_outputs、needs_mesh、solo_launcher和非 MC2 mesh 轴名归一化;_test_parallel_op_*.py文件未在该 PR 中破坏性删除;11. 总结
本次重构将分布式算子 ST 从分散的 launcher/worker 脚本集合,演进为声明式测试平台:
该架构以更多框架代码换取跨平台复用、自动调度、通信恢复和可量化的执行效率。ADR 明确了这一选择的
适用边界:HyperParallel 当前复杂的混合 mesh 和门禁场景需要自定义调度;对于单一 mesh、无设备并发
需求的新项目,应优先评估更轻量的 pytest fixture 方案。