跳到主要内容

G5 仿真器 Multi-Port 集合通信扩展

概述

目标

为 G5 指令级仿真器的集合通信模块增加 multi-port 支持,使其能够精确建模双向 Ring 和 Recursive Halving-Doubling 算法,并根据硬件端口配置自动选择最优算法。

动机

当前 G5 的集合通信实现全部基于 1-port 单向模型(Ring AllReduce 需要 2(N-1) 步),但 SG2260 硬件拥有 CDMA + VSDMA 双独立 DMA 引擎(2-port),SCCL 库实际使用双向 Ring(N 步)。当前建模与真实硬件行为不匹配,仿真结果偏保守。

范围

内容是否包含
双向 Ring AllReduce / ReduceScatter / AllGather
Recursive Halving-Doubling AllReduce
端口模型感知自动选择(Rust expand 层)
分维并行 Hierarchical否(改动大,当前场景不需要)
NVLS 网内计算否(需要建模交换芯片引擎)
AllToAll 双向优化否(Pairwise 非 Ring 结构)

现状分析

当前代码结构

perfmodel/evaluation/g5/src/
collective/
mod.rs -- 模块导出
expand.rs -- 调度路由:CommOp -> 算法展开函数
allreduce.rs -- Ring(单向)+ Ring SendRecv
allgather.rs -- Ring(单向)
reducescatter.rs-- Ring(单向)
alltoall.rs -- Pairwise
p2p.rs -- 点对点
tree_allreduce.rs -- Double Binary Tree
hierarchical.rs -- 多维串行
types.rs -- CDMACommand, CommOp, CDMAOpType
input.rs -- TopologyConfig(含 c2c_ports_per_chip,已解析未使用)
top/multi_chip.rs -- 仿真入口,调用 expand_collectives

当前算法

原语算法步数端口模型
AllReduceRing(单向)2(N-1)1-port
AllReduceRing SendRecv2(N-1)1-port
AllReduceDouble Binary Tree2 log2(N)1-port
AllGatherRing(单向)N-11-port
ReduceScatterRing(单向)N-11-port
AllToAllPairwiseN-11-port

已有基础设施

  • c2c_ports_per_chip:TopologyConfig 中已解析(input.rs line 239),但未传入展开逻辑
  • thread_id:CDMACommand 已有 thread_id 字段(0..7),仿真器支持多 thread 并行调度
  • cmd_id_dep:依赖链系统支持任意 DAG,可表达双向独立的依赖链
  • algorithm 字段:CommOp.algorithm 是 String,可扩展新算法名

关键限制

  • expand_collectives() 签名不接收 TopologyConfig,展开函数无法获知端口数
  • Ring 展开只生成单方向命令(i -> (i+1)%n)
  • 无 Halving-Doubling 实现

需求

R1: 双向 Ring AllReduce

R1.1: 新增 expand_ring_allreduce_bidir() 函数,生成双向 Ring AllReduce 的 CDMACommand 序列。

  • 将 N 个 chunk 分为两组:前半沿顺时针(CW: i -> (i+1)%n),后半沿逆时针(CCW: i -> (i-1+n)%n)
  • ReduceScatter 阶段:ceil(N/2) 步,每步 CW 和 CCW 各生成一条传输命令
  • AllGather 阶段:ceil(N/2) 步,同理
  • 总步数:2 * ceil(N/2) = N(偶数时)或 N+1(奇数时)
  • CW 命令使用 thread_id=0,CCW 命令使用 thread_id=1
  • CW 和 CCW 的依赖链相互独立(各自维护 cmd_id_dep 链)
  • 两个方向在同一步内的命令共享相同的 cmd_id 区间,但无互相依赖

R1.2: 新增 expand_ring_allgather_bidir()expand_ring_reducescatter_bidir() 函数,逻辑与 R1.1 的 AllGather / ReduceScatter 阶段一致,但独立使用。

  • AllGather 双向:ceil(N/2) 步
  • ReduceScatter 双向:ceil(N/2) 步

R1.3: num_chunks 流水线支持。双向变体必须与现有的 chunk 流水线机制兼容——当 num_chunks > 1 时,每个 chunk 在两个方向上独立流水。

cmd_id 分配规则:按 (step, direction, chunk) 三元组排列。每步内先 CW 后 CCW,每个方向内按 chunk 编号递增。同方向同 chunk 的依赖链:当前步依赖上一步同方向同 chunk 的最后一条命令。不同方向、不同 chunk 之间无依赖。示例(N=4, num_chunks=2):

Step 0:
CW chunk0: cmd_id 0..3 (thread_id=0)
CW chunk1: cmd_id 4..7 (thread_id=0, dep -> 0..3)
CCW chunk0: cmd_id 8..11 (thread_id=1)
CCW chunk1: cmd_id 12..15 (thread_id=1, dep -> 8..11)
Step 1:
CW chunk0: cmd_id 16..19 (dep -> 0..3)
CW chunk1: cmd_id 20..23 (dep -> 4..7)
CCW chunk0: cmd_id 24..27 (dep -> 8..11)
CCW chunk1: cmd_id 28..31 (dep -> 12..15)

R2: Recursive Halving-Doubling AllReduce

R2.1: 新增 expand_halving_doubling_allreduce() 函数,实现 Recursive Halving-Doubling 算法。

  • 要求 N 为 2 的幂(不满足时 expand.rs 回退到 Ring)
  • ReduceScatter 阶段(Recursive Halving):log2(N) 步
    • 第 k 步(k=0..log2(N)-1):节点 i 与节点 i XOR 2^k 交换数据
    • 交换数据量:M / (2^(k+1))(逐步减半)
    • 每步生成 Send + Receive 命令对(使用 CDMAOpType::Send / Receive)
  • AllGather 阶段(Recursive Doubling):log2(N) 步
    • 第 k 步(k=log2(N)-1..0):节点 i 与节点 i XOR 2^k 交换数据
    • 交换数据量:M / (2^(k+1))(逐步翻倍)
    • 同样生成 Send + Receive 命令对
  • 总步数:2 * log2(N)
  • 延迟最优 + 带宽最优(同时达到两个下界)

R2.2: 拓扑要求。Halving-Doubling 要求任意两节点间可通信(不要求直连,但需要路径存在)。展开函数使用 participants 数组中的逻辑编号做 XOR 配对,物理 chip_id 通过 participants[logical_id] 映射。

端口无关性:Halving-Doubling 使用 Send + Receive 配对(tcredit 机制),是逻辑上的半双工——先发 Receive 预备接收,再发 Send 触发传输。不需要双 DMA 引擎同时收发,因此不依赖 ports_per_chip >= 2。这与双向 Ring(需要 CW/CCW 两个方向同时传输)的端口要求不同。

R3: 端口模型感知自动选择

R3.1: 修改 expand_collectives() 签名,接收 ports_per_chip: u32 参数(从 TopologyConfig.c2c_ports_per_chip 提取,默认值 1)。

R3.2: 修改 top/multi_chip.rsexpand_collectives 的调用处,传入 topology_config.c2c_ports_per_chip。

R3.3: expand.rs 中的自动选择逻辑:

allreduce:
if algorithm == "tree" -> tree(不受端口数影响)
if algorithm == "ring_sendrecv" -> ring_sendrecv(显式指定,不自动切换)
if algorithm == "halving_doubling" -> halving_doubling(显式指定)
if algorithm == "ring" or "auto":
if ports_per_chip >= 2 && participants 形成 Ring:
-> ring_bidir
else:
-> ring(单向)

allgather / reducescatter:
if ports_per_chip >= 2:
-> bidir 变体
else:
-> 单向 Ring

alltoall:
-> pairwise(不变)

R3.4: 当 algorithm 字段为 "auto" 时,expand 层同时考虑端口数和参与者数量:

  • N 为 2 的幂:优先选 Halving-Doubling
  • 否则根据 ports_per_chip 选 ring 或 ring_bidir

设计决策:expand 层不检查 participants 间的连通性。走到集合通信展开的 participants 是同一并行组的成员,拓扑层(ParallelismPlanner + 路由)已保证组内任意两节点可达。在 expand 层重复检查连通性既需要传入拓扑图(增加接口复杂度),又与上层职责重叠。

R4: 接口兼容性

R4.1: Python 层(program.py)不做改动。现有 algorithm="ring" 的 CommOp 在 ports_per_chip >= 2 时自动升级为双向,无需修改上层代码。

R4.2: algorithm 字段新增合法值 "ring_bidir""halving_doubling",允许用户显式指定。

R4.3: 当用户显式指定 "ring_bidir" 但 ports_per_chip < 2 时,打印警告并回退到单向 Ring。


设计

类型变更

CDMACommand:不新增字段。利用现有 thread_id 区分方向:

  • thread_id=0:顺时针(CW)或默认方向
  • thread_id=1:逆时针(CCW)
  • thread_id=2..7:保留给未来扩展

CommOp:不变。algorithm 字段新增合法值 "ring_bidir" / "halving_doubling" / "auto"

expand_collectives 签名变更

// 之前
pub fn expand_collectives(schedule: &[CommOp]) -> HashMap<u32, Vec<CDMACommand>>

// 之后
pub fn expand_collectives(schedule: &[CommOp], ports_per_chip: u32) -> HashMap<u32, Vec<CDMACommand>>

expand_one 同步接收 ports_per_chip 参数并传入各展开函数。

双向 Ring 展开逻辑(以 AllReduce 为例)

输入: participants = [c0, c1, c2, c3], data_bytes = M, num_chunks = 1
N = 4, half = N/2 = 2

ReduceScatter 阶段(ceil(N/2) = 2 步):
Step 0:
CW chunk 0: c0->c1, c1->c2, c2->c3, c3->c0 (thread_id=0)
CCW chunk 2: c0->c3, c3->c2, c2->c1, c1->c0 (thread_id=1)
Step 1:
CW chunk 1: 转发上一步的部分规约结果 (thread_id=0)
CCW chunk 3: 转发上一步的部分规约结果 (thread_id=1)
-> 每个节点持有 2 个完整规约的 chunk

AllGather 阶段(ceil(N/2) = 2 步):
Step 2-3: 双向广播已完成的 chunk
-> 每个节点持有全部 4 个 chunk 的完整规约结果

总计:4 步 × N 条命令/步/方向 × 2 方向 = 32 条 CDMACommand
对比单向:6 步 × N 条命令/步 = 24 条,但耗时更长

Halving-Doubling 展开逻辑

输入: participants = [c0, c1, c2, c3], data_bytes = M
N = 4, log2(N) = 2

ReduceScatter 阶段(Recursive Halving, 2 步):
Step 0 (mask=1): c0<->c1 交换 M/2, c2<->c3 交换 M/2
每对生成: Send(src->dst, M/2) + Receive(dst->src, M/2)
Step 1 (mask=2): c0<->c2 交换 M/4, c1<->c3 交换 M/4
-> 每个节点持有 M/4 的完整规约结果

AllGather 阶段(Recursive Doubling, 2 步):
Step 2 (mask=2): c0<->c2 交换 M/4, c1<->c3 交换 M/4
Step 3 (mask=1): c0<->c1 交换 M/2, c2<->c3 交换 M/2
-> 每个节点持有完整 M 的规约结果

总计:4 步,每步 N/2 对 Send+Receive = 16 条 CDMACommand

文件改动清单

文件改动类型内容
collective/expand.rs修改签名加 ports_per_chip,dispatch 加自动选择逻辑
collective/allreduce.rs新增函数expand_ring_allreduce_bidir() + expand_halving_doubling_allreduce()
collective/allgather.rs新增函数expand_ring_allgather_bidir()
collective/reducescatter.rs新增函数expand_ring_reducescatter_bidir()
collective/mod.rs修改导出新模块(如果拆文件)
top/multi_chip.rs修改传 ports_per_chip 给 expand_collectives
input.rs不改c2c_ports_per_chip 已解析
types.rs不改不新增字段

验证计划

V1: 双向 Ring 正确性

V1.1 命令数量验证

场景Nports预期步数预期命令数
AllReduce 4 芯双向424每步 4(CW)+4(CCW)=8, 总 32
AllReduce 8 芯双向828每步 8+8=16, 总 128
ReduceScatter 4 芯双向422总 16
AllGather 4 芯双向422总 16

V1.2 依赖链验证

  • CW 链(thread_id=0)内部:每条命令的 cmd_id_dep 指向同 chip 同方向的前一条命令
  • CCW 链(thread_id=1)内部:同上
  • CW 和 CCW 之间:无互相依赖(两个方向完全独立)
  • RS 到 AG 过渡:AG 第一条命令依赖同 chip 同方向 RS 最后一条命令

V1.3 数据量验证

  • 每条命令的 data_bytes = M / (N * num_chunks)(与单向 Ring 相同)
  • 总传输量(所有命令 data_bytes 之和)= 单向 Ring 总传输量(数据搬运量不变,时间减半)

V1.4 方向验证

  • CW 命令:src_chip_id=participants[i], dst_chip_id=participants[(i+1)%n]
  • CCW 命令:src_chip_id=participants[i], dst_chip_id=participants[(i-1+n)%n]

V2: Halving-Doubling 正确性

V2.1 配对验证

NStepMask配对
401(0,1), (2,3)
412(0,2), (1,3)
801(0,1), (2,3), (4,5), (6,7)
812(0,2), (1,3), (4,6), (5,7)
824(0,4), (1,5), (2,6), (3,7)

V2.2 数据量验证

  • RS 第 k 步:每对交换 M / 2^(k+1) 字节
  • AG 第 k 步(逆序):每对交换 M / 2^(k+1) 字节
  • 总传输量:2 * sum(M/2^(k+1), k=0..log2(N)-1) = 2 * M * (N-1)/N(带宽最优)

V2.3 非 2 幂回退

  • N=3, 5, 6, 7 时,Halving-Doubling 不可用,expand 应回退到 Ring

V2.4 Send/Receive 配对

  • 每对节点生成一条 Send + 一条 Receive
  • Send 的 remote_thread_id 匹配 Receive 的 thread_id
  • Receive 先于 Send 执行(tcredit 机制)

V3: 端口感知自动选择

V3.1 自动选择验证

algorithmports_per_chipN预期选择
"ring"14ring(单向)
"ring"24ring_bidir
"auto"24 (2^k)halving_doubling
"auto"26 (非 2^k)ring_bidir
"auto"18 (2^k)ring(单向)
"tree"28tree(不受端口数影响)
"ring_bidir"14ring(回退 + 警告)
"halving_doubling"14halving_doubling(不依赖端口数)
"ring_sendrecv"24ring_sendrecv(显式指定不切换)

V3.2 回归测试

  • ports_per_chip=1(或未设置,默认 1)时,所有原语行为与改动前完全一致
  • 已有的 Ring、Tree、SendRecv、Pairwise 算法的命令序列不变

V4: 仿真端到端验证

V4.1 时序对比

  • 4 芯 Ring AllReduce (M=1MB):
    • 1-port 仿真时间 ~= 2(N-1) * (alpha + M/(N*beta))
    • 2-port 仿真时间 ~= N * (alpha + M/(N*beta)),约为 1-port 的 ~67%(N=4 时 4/6)
  • 差异应来自步数减少,单步传输时间不变

V4.2 大规模验证

  • 8 芯、16 芯场景下双向 Ring 和 Halving-Doubling 的仿真结果与理论公式对比
  • Halving-Doubling 在 N=8 时应明显优于 Ring(延迟项 6alpha vs 14alpha)

不做的事

  • 不改 CDMACommand 结构体:利用现有 thread_id 区分方向,不新增字段
  • 不改 Python 层:algorithm="ring" 自动升级,用户代码无需修改
  • 不改 AllToAll:Pairwise 非 Ring 结构,双向优化不适用
  • 不改 Tree AllReduce:Tree 的端口收益有限,现有实现够用
  • 不改 Hierarchical 串行逻辑:分维并行是独立的大改动
  • 不改仿真器调度层:仿真器已支持多 thread 并行,只需展开层生成正确的命令

实施顺序

Phase内容依赖验证
1签名变更:expand_collectivesports_per_chip 参数 + multi_chip.rs 调用处传参编译通过 + 所有现有测试不变
2双向 Ring AllReduce:expand_ring_allreduce_bidir()Phase 1V1.1~V1.4
3双向 AllGather + ReduceScatter:expand_ring_allgather_bidir() + expand_ring_reducescatter_bidir()Phase 1V1.1 对应行
4Halving-Doubling AllReduce:expand_halving_doubling_allreduce()Phase 1(可与 Phase 3 并行)V2.1~V2.4
5自动选择:expand.rs dispatch 逻辑Phase 2~4V3.1~V3.2
6端到端时序验证Phase 5V4.1~V4.2

Phase 2 和 Phase 3 可并行,Phase 4 可与 Phase 3 并行。关键路径:1 → 2 → 5 → 6。


参考资料

  • 01-理论下界 — 延迟/带宽下界推导框架
  • 07-all-reduce — AllReduce 算法与 2-port 分析
  • 11-端口模型与硬件成本 — 端口模型硬件实践
  • [SCCL 内部文档] — SG2260 双向 Ring 实现参考
  • [Chan et al., CCPE 2007] — Recursive Halving-Doubling 算法
  • [Thakur et al., IJHPCA 2005] — MPICH 集合通信优化