Skip to content

05. Communication Topologies | 通信拓扑与分布式基石

难度: Medium | 环境: CPU-first | 标签: 并行通信, 分布式训练, 通信拓扑 | 目标人群: 需要理解多卡训练通信代价的学习者

🚀 云端运行环境

本章节的实战代码可以点击以下链接在免费 GPU 算力平台上直接运行:

Open In ColabOpen In Studio (国内推荐:魔搭社区免费实例)


本节导读

大模型训练从单卡扩展到多卡后,性能取决于两件事:每张卡如何切分数据、张量或网络层,以及切分后需要交换多少数据。先从 DP、TP、PP 的切分对象入手,再追踪 All-Reduce、All-Gather、Reduce-Scatter 和点到点传输,最后把链路带宽与同步等待放回吞吐判断。

本节沿着“并行方式 → 数据交换 → 拓扑带宽 → 计算与通信重叠”推进。完成后,你应能为一种并行方案指出主要通信原语,做出通信量级的初步估算,并解释链路带宽和同步等待如何影响并行收益。

关键词: DP, TP, PP

本节概念关系


前置阅读

导语: 先复习 GPU 内存层级和设备间连接,再用 DP、TP、PP 的示例回答三个问题:数据放在哪里、哪些数据需要交换、链路带宽如何改变并行收益。


Q1:DP、TP、PP 分别如何切分数据、张量和层?

点击展开查看解析

先分别看清数据并行(DP)、张量并行(TP)和流水线并行(PP)各自切分什么,再说明它们组合后为什么称为 3D 并行,最后追踪切分产生的数据交换。

  • DP:不同卡处理不同数据批次,再同步梯度。
  • TP:把单层中的大张量切到多卡上共同计算。
  • PP:把不同层切到不同设备或设备组上形成流水线。

将 DP、TP、PP 组合使用时,通常称为 3D 并行。它们的目标不是“越多越好”,而是让模型、算力和通信拓扑能一起匹配。先把切分对象、主要交换的数据和常见通信方式分开看:

并行方式切分对象主要交换的数据常见通信方式
DP不同数据批次梯度或参数更新结果常见 All-Reduce
TP层内张量或激活局部激活、局部结果可能使用 All-Gather、Reduce-Scatter 或 All-Reduce
PP不同层或阶段相邻阶段的激活点到点传输,并伴随流水线等待
### Q1小验证:对照并行维度的切分对象与通信关注点

逐项检查每种并行方式切分什么、需要交换什么,以及对应的通信方式。

python
def three_d_parallel(dp, tp, pp):
    """组合 DP、TP、PP 三个并行维度,返回切分关系和通信关注点。"""
    if any(value <= 0 for value in (dp, tp, pp)):
        raise ValueError('dp、tp、pp 必须为正数')
    # 先记录三个并行维度,再观察它们组合后的 worker 数量和通信关系。
    return {
        'dp_groups': dp,
        'tp_shards': tp,
        'pp_stages': pp,
        'effective_workers': dp * tp * pp,
        'partition_axis': {'dp': 'data', 'tp': 'tensor', 'pp': 'layers'},
        'communication_focus': {
            'dp': 'gradient synchronization',
            'tp': 'activation gather / reduce-scatter',
            'pp': 'stage activation transfer',
        },
    }

cases = [
    three_d_parallel(8, 1, 1),
    three_d_parallel(4, 2, 2),
    three_d_parallel(2, 4, 4),
]
for case in cases:
    print(case)
assert cases[0]['partition_axis'] == {'dp': 'data', 'tp': 'tensor', 'pp': 'layers'}
assert 'gradient' in cases[1]['communication_focus']['dp']
assert 'activation' in cases[2]['communication_focus']['pp']
print('3D parallelism = DP × TP × PP')

Q2:All-Reduce、All-Gather、Reduce-Scatter 分别有什么区别?

点击展开查看解析

这三种集合通信原语的区别,先从每张卡的输入、输出和数据变化看起。下面的形状只是数据流示意;实际耗时还会受到消息大小、world size、拓扑、collective 算法和通信库实现影响。代码只模拟这些输入 / 输出形状,不执行真实 NCCL 通信。

通信原语每卡输入每卡输出数据变化常见使用场景
All-Reduce局部张量相同形状的归约结果归约后复制到每卡DP 梯度同步
All-Gather局部分片拼接后的完整张量收集各卡分片TP 激活或参数收集
Reduce-Scatter较大的局部输入归约后的局部结果先归约,再切分分片梯度或参数同步
### Q2小验证:通信原语如何改变每张卡看到的数据

先观察“聚合”“收集”“切分再发回”三种数据流。

python
def collective_shape(kind, world_size, local_shape):
    """返回集合通信的输入 / 输出形状;只模拟数据流,不执行真实通信。"""
    if world_size <= 0 or not local_shape or any(dim <= 0 for dim in local_shape):
        raise ValueError('world_size 和 local_shape 必须为正数')
    table = {
        'allreduce': {'operation': 'reduce + broadcast', 'input_per_rank': local_shape, 'output_per_rank': local_shape},
        'allgather': {'operation': 'gather all pieces', 'input_per_rank': local_shape, 'output_per_rank': (local_shape[0] * world_size, *local_shape[1:])},
        'reducescatter': {'operation': 'reduce then scatter', 'input_per_rank': (local_shape[0] * world_size, *local_shape[1:]), 'output_per_rank': local_shape},
    }
    if kind not in table:
        raise ValueError(f'未知通信原语: {kind}')
    return table[kind]

world_size = 4
local_shape = (8, 1024)
for kind in ['allreduce', 'allgather', 'reducescatter']:
    print(kind, '->', collective_shape(kind, world_size, local_shape))
assert collective_shape('allreduce', 4, (8, 1024))['output_per_rank'] == (8, 1024)
assert collective_shape('allgather', 4, (8, 1024))['output_per_rank'] == (32, 1024)
assert collective_shape('reducescatter', 4, (8, 1024))['input_per_rank'] == (32, 1024)
try:
    collective_shape('unknown', 4, (8, 1024))
except ValueError:
    print('✅ 非法通信原语校验通过')
else:
    raise AssertionError('未知通信原语应报错')

Q3:通信拓扑如何改变通信代价?

点击展开查看解析

先区分通信发生在哪里,再比较相同消息大小经过不同链路的理想传输时间。下表中的带宽统一按 Gb/s 表示,只是教学示例,不代表所有 GPU、主板、驱动或网络配置;真实实验应以 nvidia-smi topo -m、NCCL 测试或厂商规格为准。

通信位置常见链路教学示例带宽(Gb/s)主要特点适合承载的通信
GPU 机内NVLink / NVSwitch900 级带宽高、延迟较低高频 TP 或集合通信
GPU 到 CPUPCIe64 级带宽较低、路径更长参数搬运、低频同步
跨节点网络互连以实际集群为准受网络和拓扑影响大低频或可重叠通信
### Q3小验证:相同消息经过不同链路需要多久

在相同消息大小下,比较不同链路的理想传输时间。

python
def bandwidth_ratio(intra_gbps=900, inter_gbps=64):
    """返回同一单位下的链路带宽比;数值只是拓扑教学假设。"""
    if intra_gbps <= 0 or inter_gbps <= 0:
        raise ValueError('intra_gbps 和 inter_gbps 必须为正数')
    return intra_gbps / inter_gbps

print(f'ratio ≈ {bandwidth_ratio():.1f}x')

Q4:如何根据通信代价选择并行策略?

点击展开查看解析

用下面的条件表把并行方式、带宽、同步频率和计算重叠连接起来。表中的“初步策略”只表示通信账本给出的方向,不替代真实系统测量。

观察条件可能的风险初步策略
DP 需要跨低带宽链路同步梯度梯度通信拖慢每轮更新减少同步频率,或提高通信与计算重叠
TP 需要跨低带宽链路频繁同步集合通信成为瓶颈缩小 TP 范围,优先放在高速互连内
PP 跨阶段传输激活较多阶段间等待、pipeline bubble调整 stage 边界或 micro-batch
通信时间接近计算时间扩展收益下降尝试通信与计算重叠
只有理想带宽估算无法确认真实部署收益把同一 workload 带入后续系统实验
### Q4小验证:比较并行策略的通信占比
python
def comm_time_ms(size_mb, bandwidth_gbps, sync_rounds=1):
    """估算重复同步的理想单向传输时间;只作为带宽下界。"""
    if size_mb < 0:
        raise ValueError('size_mb must be non-negative')
    if bandwidth_gbps <= 0 or sync_rounds <= 0:
        raise ValueError('bandwidth_gbps 和 sync_rounds 必须为正数')
    # MB -> Mb,除以 Gb/s,再换算为毫秒;忽略协议、同步和重叠开销。
    return size_mb * 8 / bandwidth_gbps * sync_rounds

payload_mb = 256
sync_rounds = 4
nvlink_gbps = 900
pcie_gbps = 64
nvlink_time = comm_time_ms(payload_mb, nvlink_gbps, sync_rounds)
pcie_time = comm_time_ms(payload_mb, pcie_gbps, sync_rounds)
ratio = pcie_time / nvlink_time

print(f'{payload_mb} MB × {sync_rounds} sync rounds over NVLink: {nvlink_time:.2f} ms')
print(f'{payload_mb} MB × {sync_rounds} sync rounds over PCIe: {pcie_time:.2f} ms')
print(f'PCIe / NVLink time ratio: {ratio:.1f}x')

def parallel_strategy_report(strategy, payload_mb, bandwidth_gbps, sync_rounds, compute_ms):
    """用理想通信时间比较 DP、TP、PP 的通信占比;不模拟真实 collective。"""
    if strategy not in {'DP', 'TP', 'PP'}:
        raise ValueError('strategy 必须是 DP、TP 或 PP')
    if compute_ms <= 0:
        raise ValueError('compute_ms 必须为正数')
    communication_ms = comm_time_ms(payload_mb, bandwidth_gbps, sync_rounds)
    ratio = communication_ms / compute_ms
    if strategy == 'DP':
        action = '检查梯度同步频率与重叠'
    elif strategy == 'TP':
        action = '优先放在高速互连内'
    else:
        action = '检查 stage 边界与 micro-batch'
    return {
        'strategy': strategy,
        'communication_ms': round(communication_ms, 3),
        'communication_compute_ratio': round(ratio, 3),
        'next_action': action,
    }

reports = [
    parallel_strategy_report('DP', 256, 64, 4, 20),
    parallel_strategy_report('TP', 256, 900, 8, 20),
    parallel_strategy_report('PP', 64, 64, 2, 20),
]
print('并行策略通信占比:', reports)
assert {row['strategy'] for row in reports} == {'DP', 'TP', 'PP'}

相关阅读

本节可以继续接到 NCCL 原语、并行策略选择和通信调度;论文与开源实现用于补充真实系统中的数据交换路径。


Released under the MIT License.