Skip to content

79. Distributed Parallel Benchmark | 训练状态并行基准 ​

难度: Hard | 环境: CPU-first | 标签: 并行通信, 分布式, 基准对比 | 目标人群: 项目决策练习者

🚀 云端运行环境

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

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


本节导读 ​

本节沿训练状态这条主线比较 DDP、FSDP 与 ZeRO:参数、梯度和优化器状态怎样驻留在各个 rank,状态分片节省的显存是否值得新增的通信与重组代价。你将先建立每 rank 状态账本,再用显存、吞吐和通信门槛筛选候选,最后形成可复测的训练并行决策。

关键词: distributed training, benchmark, parallelism


前置阅读 ​

导语: 本项目以训练状态驻留为判断起点。先理解 ZeRO/FSDP 的分片对象和底层通信,再比较真实训练中的显存与扩展效率。


Step 1:从复制状态走向分片状态 ​

数据并行首先复制模型和训练状态。DDP 保留完整参数、梯度与优化器状态;ZeRO/FSDP 逐步分片优化器状态、梯度和参数。world size 增大时,常驻状态下降,但参数临时聚合、通信 buffer 和 activation 仍会参与峰值。

策略参数梯度优化器状态关键通信
DDP复制复制复制梯度 All-Reduce
ZeRO-1复制复制分片梯度归约与参数同步
ZeRO-2复制分片分片Reduce-Scatter 与参数同步
ZeRO-3 / FSDP full shard分片分片分片参数 All-Gather、梯度归约与 reshard

Step 2:沿训练生命周期识别峰值与通信 ​

状态分片改变的不只是静态字节数。forward 前的参数聚合、backward 中的梯度归约、optimizer step 的分片更新以及 checkpoint 重组,会在不同时间形成临时峰值和同步点。

生命周期位置主要状态应观察的代价
加载与初始化参数、主机缓冲区启动峰值、rank 0 放大
forward / backwardactivation、聚合参数、梯度 bucketpeak memory、All-Gather / Reduce-Scatter
optimizer step参数、梯度、优化器状态step time、吞吐、同步等待
checkpointfull / local / sharded state dict保存峰值、产物大小、恢复兼容性

Step 3:用门槛筛选训练状态策略 ​

候选首先要满足单 rank 显存上限和正确性要求,再比较吞吐与通信占比。显存最低的策略不一定最好:更高阶段的状态分片可能用频繁聚合换取容量,因此应保留 peak_memory_mb、throughput、step_time_ms、communication_ratio 和 failure。

Step 4:实现状态账本、准入与选型 ​

题目区把项目拆成三个机制责任:计算不同阶段的每 rank 常驻训练状态;按显存、吞吐和通信门槛标记候选是否准入;在合格候选中优先选择吞吐更高、通信占比更低的策略。

函数机制责任重点测试
training_state_ledger按策略计算参数、梯度和优化器状态的每 rank 驻留量world size、未知策略、分片顺序
evaluate_candidate应用显存、吞吐、通信与 failure 门槛临界值、失败运行、多个不满足原因
select_training_state_strategy从合格候选中形成策略结论无候选、吞吐排序、通信 tie-break
python
from copy import deepcopy
python
def training_state_ledger(parameter_mb, gradient_mb, optimizer_mb, world_size, strategy):
    """计算指定训练状态策略下的每 rank 常驻状态账本。"""
    if world_size < 1:
        raise ValueError('world_size 必须至少为 1')

    shard_factors = {
        'ddp': (1, 1, 1),
        'zero1': (1, 1, world_size),
        'zero2': (1, world_size, world_size),
        'zero3': (world_size, world_size, world_size),
        'fsdp_full_shard': (world_size, world_size, world_size),
    }
    if strategy not in shard_factors:
        raise ValueError(f'未知训练状态策略:{strategy}')

    # TODO 1:训练状态分片机制——分别计算参数、梯度和优化器状态的每 rank 驻留量。
    # parameter_factor, gradient_factor, optimizer_factor = ???
    # parameter_per_rank_mb = ???
    # gradient_per_rank_mb = ???
    # optimizer_per_rank_mb = ???
    resident_state_mb = parameter_per_rank_mb + gradient_per_rank_mb + optimizer_per_rank_mb
    return {
        'strategy': strategy,
        'parameter_per_rank_mb': parameter_per_rank_mb,
        'gradient_per_rank_mb': gradient_per_rank_mb,
        'optimizer_per_rank_mb': optimizer_per_rank_mb,
        'resident_state_mb': resident_state_mb,
    }


def evaluate_candidate(metrics, memory_limit_mb, min_throughput, max_communication_ratio):
    """按容量、吞吐、通信和运行状态判断候选是否可以进入决策。"""
    result = deepcopy(metrics)
    # TODO 2:候选准入机制——逐项检查 failure、显存、吞吐与通信占比。
    # failures = ???
    result['eligible'] = not failures
    result['gate_failures'] = failures
    return result


def select_training_state_strategy(candidates):
    """在通过准入门槛的候选中选择训练状态策略。"""
    eligible = [item for item in candidates if item['eligible']]
    if not eligible:
        return {'decision': 'reject', 'strategy': None, 'reason': '没有候选通过容量、吞吐与通信门槛'}

    # TODO 3:策略选择机制——优先吞吐,其次通信占比,再比较峰值显存。
    # selected = ???
    return {
        'decision': 'accept',
        'strategy': selected['strategy'],
        'reason': '候选通过准入门槛,并在吞吐、通信占比和峰值显存排序中最优',
    }

测试 ​

python
def test_training_state_ledger():
    ddp = training_state_ledger(400, 400, 800, world_size=4, strategy='ddp')
    zero2 = training_state_ledger(400, 400, 800, world_size=4, strategy='zero2')
    zero3 = training_state_ledger(400, 400, 800, world_size=4, strategy='zero3')
    assert ddp['resident_state_mb'] == 1600
    assert zero2['resident_state_mb'] == 700
    assert zero3['resident_state_mb'] == 400
    assert ddp['resident_state_mb'] > zero2['resident_state_mb'] > zero3['resident_state_mb']
    for kwargs in (
        {'world_size': 0, 'strategy': 'ddp'},
        {'world_size': 4, 'strategy': 'pipeline'},
    ):
        try:
            training_state_ledger(400, 400, 800, **kwargs)
            raise AssertionError('非法 world size 或未知策略必须被拒绝')
        except ValueError:
            pass


def test_candidate_gate():
    metrics = {'strategy': 'zero2', 'peak_memory_mb': 9000, 'throughput': 110, 'communication_ratio': 0.22, 'failure': None}
    accepted = evaluate_candidate(metrics, memory_limit_mb=10000, min_throughput=100, max_communication_ratio=0.25)
    rejected = evaluate_candidate(metrics, memory_limit_mb=8000, min_throughput=120, max_communication_ratio=0.20)
    assert accepted['eligible'] is True
    assert rejected['eligible'] is False
    assert set(rejected['gate_failures']) == {'memory', 'throughput', 'communication'}
    runtime_failed = evaluate_candidate({**metrics, 'failure': 'oom'}, 10000, 100, 0.25)
    assert runtime_failed['eligible'] is False
    assert runtime_failed['gate_failures'] == ['runtime']


def test_strategy_selection():
    candidates = [
        {'strategy': 'ddp', 'eligible': False, 'throughput': 130, 'communication_ratio': 0.12, 'peak_memory_mb': 15000},
        {'strategy': 'zero2', 'eligible': True, 'throughput': 112, 'communication_ratio': 0.20, 'peak_memory_mb': 9000},
        {'strategy': 'zero3', 'eligible': True, 'throughput': 105, 'communication_ratio': 0.31, 'peak_memory_mb': 7000},
    ]
    decision = select_training_state_strategy(candidates)
    assert decision['strategy'] == 'zero2'
    assert select_training_state_strategy([])['decision'] == 'reject'


def run_training_state_tests():
    try:
        test_training_state_ledger()
        test_candidate_gate()
        test_strategy_selection()
        print("✅ 训练状态账本、候选准入与策略选择通过基础校验。")
    except NotImplementedError:
        print("请先完成 TODO 代码!")
        raise
    except (AttributeError, NameError, TypeError, ValueError) as e:
        print("代码可能未完成,导致变量未定义")
        raise NotImplementedError("请先完成 TODO 代码!") from e
    except AssertionError as e:
        print(f"❌ 测试失败: {e}")
        raise NotImplementedError("请先完成 TODO 代码!") from e


run_training_state_tests()

🛑 STOP HERE 🛑









请先尝试自己完成代码并跑通测试。
如果你正在 Colab 中运行,并且遇到困难没有思路,可以向下滚动查看参考答案。










参考代码与解析 ​

代码 ​

python
def training_state_ledger(parameter_mb, gradient_mb, optimizer_mb, world_size, strategy):
    """计算指定训练状态策略下的每 rank 常驻状态账本。"""
    if world_size < 1:
        raise ValueError('world_size 必须至少为 1')
    shard_factors = {
        'ddp': (1, 1, 1),
        'zero1': (1, 1, world_size),
        'zero2': (1, world_size, world_size),
        'zero3': (world_size, world_size, world_size),
        'fsdp_full_shard': (world_size, world_size, world_size),
    }
    if strategy not in shard_factors:
        raise ValueError(f'未知训练状态策略:{strategy}')
    # TODO 1:训练状态分片机制。
    parameter_factor, gradient_factor, optimizer_factor = shard_factors[strategy]
    parameter_per_rank_mb = parameter_mb / parameter_factor
    gradient_per_rank_mb = gradient_mb / gradient_factor
    optimizer_per_rank_mb = optimizer_mb / optimizer_factor
    resident_state_mb = parameter_per_rank_mb + gradient_per_rank_mb + optimizer_per_rank_mb
    return {
        'strategy': strategy,
        'parameter_per_rank_mb': parameter_per_rank_mb,
        'gradient_per_rank_mb': gradient_per_rank_mb,
        'optimizer_per_rank_mb': optimizer_per_rank_mb,
        'resident_state_mb': resident_state_mb,
    }


def evaluate_candidate(metrics, memory_limit_mb, min_throughput, max_communication_ratio):
    """按容量、吞吐、通信和运行状态判断候选是否可以进入决策。"""
    result = deepcopy(metrics)
    # TODO 2:候选准入机制。
    failures = []
    if metrics.get('failure'):
        failures.append('runtime')
    if metrics['peak_memory_mb'] > memory_limit_mb:
        failures.append('memory')
    if metrics['throughput'] < min_throughput:
        failures.append('throughput')
    if metrics['communication_ratio'] > max_communication_ratio:
        failures.append('communication')
    result['eligible'] = not failures
    result['gate_failures'] = failures
    return result


def select_training_state_strategy(candidates):
    """在通过准入门槛的候选中选择训练状态策略。"""
    eligible = [item for item in candidates if item['eligible']]
    if not eligible:
        return {'decision': 'reject', 'strategy': None, 'reason': '没有候选通过容量、吞吐与通信门槛'}
    # TODO 3:策略选择机制。
    selected = sorted(
        eligible,
        key=lambda item: (-item['throughput'], item['communication_ratio'], item['peak_memory_mb']),
    )[0]
    return {
        'decision': 'accept',
        'strategy': selected['strategy'],
        'reason': '候选通过准入门槛,并在吞吐、通信占比和峰值显存排序中最优',
    }

Step 5(可选):GPU / 多卡并行 benchmark ​

5.1 环境与固定 workload ​

本项目固定模型结构、global batch、micro-batch、序列长度、dtype、GPU 数与重复次数,再比较 DDP 与不同训练状态分片阶段。G0、G1、G2 的策略和 JSON 路径在 5.3 集中声明。

类别固定并记录的字段用途
运行环境GPU 数、PyTorch / CUDA、NCCL、互联拓扑解释通信与每 rank 显存
workload模型宽度 / 层数、global / micro batch、序列长度、dtype、warmup、repeats让策略结果可比较
对照G0 baseline、G1 单策略候选、G2 可选扩展逐组解释策略带来的变化
输出RESULT_DIR、每组 JSON、summary.json保留复测、failure 与决策
python
# 5.1:默认关闭;只比较 DDP、FSDP/ZeRO 训练状态策略。
RUN_GPU_EXPERIMENT = False
WORLD_SIZE = 2
MODEL_WIDTH = 1024
MODEL_LAYERS = 8
MICRO_BATCH = 1
GRADIENT_ACCUMULATION = 2
SEQ_LEN = 256
DTYPE = 'bf16'
WARMUP = 2
REPEATS = 5
RESULT_DIR = 'benchmarks/results/79_distributed_parallel'
ZERO_STRATEGIES = ['ddp', 'zero1', 'zero2', 'zero3']
SUMMARY_PATH = f'{RESULT_DIR}/summary.json'
print({'run': RUN_GPU_EXPERIMENT, 'world_size': WORLD_SIZE, 'strategies': ZERO_STRATEGIES, 'summary': SUMMARY_PATH})

5.2 多卡环境预检 ​

开启实验前检查 CUDA、可见 GPU 数、NCCL 与 torchrun。预检结果帮助你确认后续记录来自可启动的多卡环境。

python
# 5.2:默认关闭;开启时检查运行多卡实验所需条件。
GPU_READY = False
if RUN_GPU_EXPERIMENT:
    import shutil
    import torch
    GPU_READY = torch.cuda.is_available() and torch.cuda.device_count() >= WORLD_SIZE and shutil.which('torchrun') is not None
    print({'cuda': torch.cuda.is_available(), 'visible_gpus': torch.cuda.device_count(), 'world_size': WORLD_SIZE, 'torchrun': bool(shutil.which('torchrun')), 'ready': GPU_READY})
else:
    print({'run': False, 'required_gpus': WORLD_SIZE, 'ready': GPU_READY})

5.3 配置实验组与 JSON 路径 ​

G0 是 DDP 复制基线,G1 比较优化器与梯度分片,G2 比较参数也参与分片的 ZeRO-3/FSDP 路径。每种策略保留独立 source JSON,再由项目汇总生成 summary.json。

python
# 5.3:实验组与预期 source;实际执行在 5.4。
EXPERIMENT_GROUPS = {
    'G0': {'purpose': 'ddp_baseline', 'sources': ['zero_ddp.json']},
    'G1': {'purpose': 'optimizer_and_gradient_sharding', 'sources': ['zero_zero1.json', 'zero_zero2.json']},
    'G2': {'purpose': 'full_state_sharding', 'sources': ['zero_zero3.json']},
}
print({'groups': EXPERIMENT_GROUPS, 'result_dir': RESULT_DIR})

5.4 执行并保存 JSON ​

依次运行 DDP 与 ZeRO 各阶段,在相同 workload 下保存每 rank 显存、step time、吞吐和通信证据,再汇总 source JSON。

python
# 5.4:运行机制实验并汇总证据;默认关闭。
import subprocess
from pathlib import Path

if RUN_GPU_EXPERIMENT and GPU_READY:
    Path(RESULT_DIR).mkdir(parents=True, exist_ok=True)
    result_paths = []
    for strategy in ZERO_STRATEGIES:
        output = Path(RESULT_DIR) / f'zero_{strategy}.json'
        command = ['torchrun', '--standalone', f'--nproc_per_node={WORLD_SIZE}', 'tools/run_zero_stage_benchmark.py', '--strategy', strategy, '--output', str(output), '--width', str(MODEL_WIDTH), '--layers', str(MODEL_LAYERS), '--micro-batch', str(MICRO_BATCH), '--gradient-accumulation', str(GRADIENT_ACCUMULATION), '--seq-len', str(SEQ_LEN), '--warmup', str(WARMUP), '--repeats', str(REPEATS), '--dtype', DTYPE]
        print(' '.join(command))
        subprocess.run(command, check=False)
        result_paths.append(str(output))
    summary_command = ['python', 'tools/aggregate_distributed_evidence.py', '--project', '79', '--output', SUMMARY_PATH] + sum((['--input', path] for path in result_paths), [])
    subprocess.run(summary_command, check=True)
elif RUN_GPU_EXPERIMENT:
    print('多卡预检未通过;请先检查 5.2 的 CUDA、GPU 数和 torchrun 输出。')
else:
    print('GPU benchmark 默认关闭。')

5.5 读取结果与记录证据 ​

读取项目汇总后,先确认每个 source 的运行状态和统一 workload,再把策略收益与通信代价放在同一张记录中。

python
# 5.5:读取项目汇总。每个 source 的 evidence level 会保留在 sources 中。
import json
from pathlib import Path

if Path(SUMMARY_PATH).exists():
    result = json.loads(Path(SUMMARY_PATH).read_text())
    required = {'strategy', 'workload', 'hardware', 'metrics', 'evidence_level', 'failure', 'decision'}
    missing = required - set(result)
    if missing:
        raise ValueError(f'结果 JSON 缺少项目证据字段:{sorted(missing)}')
    print({'decision': result['decision'], 'failure': result['failure'], 'sources': [(item['path'], item['evidence_level'], item['decision']) for item in result.get('sources', [])]})
else:
    print(f'尚无项目汇总结果:{SUMMARY_PATH}')

5.6 解释结果与形成决策 ​

读取项判断与下一步
运行状态failure 非空时先记录启动、OOM 或通信问题,再调整实验条件
可比口径对照 workload / hardware / strategy,确认策略变化来自预期实验组
核心指标联合查看每 rank peak memory、最慢 rank step time、throughput 与 communication time
项目决策收益与训练压力匹配时 accept;需要切分或 batch 调整时 tune;运行失败或收益不足时 reject

解析 ​

1. TODO 1:训练状态账本

DDP 复制三类训练状态;ZeRO-1 先分片优化器状态,ZeRO-2 再分片梯度,ZeRO-3/FSDP full shard 继续分片参数。账本描述常驻状态,不把临时参数聚合、activation 或通信 buffer 错算成已经消失。

2. TODO 2:候选准入

候选必须同时通过运行状态、单 rank 显存、最低吞吐与最高通信占比门槛。保留全部失败原因,可以区分 OOM、吞吐不足和通信暴露,而不是只返回一个布尔值。

3. TODO 3:策略选择

先排除未通过门槛的策略,再按吞吐、通信占比和峰值显存排序。这个顺序表达的是当前项目偏好;改变业务目标时应显式改变排序规则,而不是修改测量结果。

相关阅读 ​

以下资料按“并行训练机制 → 通信实现 → 多卡项目验证”排列,用于把本节的显存、吞吐和通信证据连接到真实多卡环境。

Released under the MIT License.