DDP详解

张开发
2026/6/14 7:14:13 15 分钟阅读

分享文章

DDP详解
在 PyTorch 生态中DDP即torch.nn.parallel.DistributedDataParallel是官方推荐的用于多卡、多机分布式训练的绝对核心。理解了 DDP 的底层逻辑才能真正明白为什么大模型在训练时网卡和网络拓扑会成为决定性的瓶颈。一、 DDP 的核心工作流从前向到反向DDP 的本质是“模型全量复制数据均匀切分”。假设你有 4 张 GPU4 个进程/Ranks它的完整生命周期如下环境初始化Initialization每个 GPU 独立启动一个进程Rank 0 到 Rank 3。Rank 0作为主节点把最初的模型参数Weights和优化器状态Optimizer States广播Broadcast给所有其他 Rank确保训练开始前所有卡上的模型一模一样。数据切分Distributed Sampler使用DistributedSampler将一个原本很大的全局 Batch 数据均匀地切分成 4 份互不重叠的 Mini-Batch分别喂给 4 张卡。前向传播Forward Pass每张卡独立运行前向计算算出各自对应的 Loss。在这个阶段卡与卡之间完全不通信各算各的。反向传播与梯度同步Backward Gradient All-Reduce—— DDP 的灵魂当每张卡开始反向计算梯度时DDP不会等整张卡全部算完才去同步。Bucket桶机制DDP 在底层把模型的参数从后往前反向传播的顺序划分成一个个的“桶”Buckets默认大小通常为 25MB。当某一层的参数算完了梯度并且它所在的“桶”满了DDP 就会立刻、异步地在所有 GPU 之间触发All-Reduce通信。计算与通信交叠Overlap当底层的网络通过 NCCL 库在跨卡传输、平均这 25MB 的梯度时GPU 的计算单元还在继续往前算更早一层的梯度。这种设计极大地榨干了硬件效率。参数更新Optimizer Step当反向传播彻底结束所有桶的 All-Reduce 也全部完成了。此时所有卡上每个参数对应的梯度都已经变成了一模一样的全局平均值。每张卡独立调用optimizer.step()更新自己的模型参数。因为梯度是一样的更新后的模型在所有卡上依然保持完全一致无缝进入下一轮循环。二、 DDP 与老一代 DPDataParallel的本质区别很多人在刚接触 PyTorch 时会分不清DataParallel单进程多线程和DistributedDataParallel多进程。在工业界DP 已经被完全淘汰DDP 是唯一的选择。特性DP (DataParallel)DDP (DistributedDataParallel)架构单进程多线程。由一个主线程控制多张卡。多进程Multi-Processing。每张卡拥有一个独立的 Python 进程。通信瓶颈严重主卡瓶颈。主卡负责分发数据、分发模型并在反向传播后把所有卡的梯度拉回主卡求平均再分发出去。主卡极易 OOM其余卡围观。无中心化Ring-AllReduce。所有卡是对等的Peers通过环形Ring或树形架构直接对等同步梯度通信负载完美均摊。GIL 锁限制受限于 Python 的全局解释器锁GIL多线程无法打满多核 CPU。每个进程独立拥有自己的 GIL完美利用多核 CPU 进行数据加载DataLoader。扩展性只能单机多卡无法跨机器Nodes扩展。完美支持多机多卡通过 InfiniBand/RoCE 跨机通信。三、 DDP 在大模型时代遇到的瓶颈为什么需要 Megatron / DeepSpeedDDP 虽好但它有一个致命的前提单张 GPU 的显存必须能装下整个模型和优化器。随着模型参数量走向 7B、13B、70B 甚至千亿一张 A100/H10080GB 显存在 FP16 下最多只能勉强塞下一个 7B~13B 左右的模型还要考虑庞大的 Adam 优化器状态和激活值。一旦模型本身单卡装不下了DDP 的“全量复制”逻辑直接宣告破产。这时候业界为了保留 DDP 的数据并行优势同时解决显存问题演进出了两种路线FSDP / ZeRO完全分片数据并行不改变 DDP 的宏观逻辑但把模型参数、梯度、优化器状态切碎分摊到各张卡上。计算到哪一层临时用 All-Gather 拉过来算完立马释放这就是前面聊到的 DeepSpeed ZeRO 思想。混合并行DDP TP PP把 DDP 降级。例如你有 64 张卡不再是 64 路 DDP而是 8 路 DDP。每路 DDP 内部由 8 张卡通过 Megatron-LM 的张量并行TP共同拼出一个完整的模型。四、 最简 DDP 代码骨架PyTorch 原生在实际工程中启动一个标准的 DDP 训练通常需要以下几个关键步骤importosimporttorchimporttorch.distributedasdistimporttorch.multiprocessingasmpfromtorch.nn.parallelimportDistributedDataParallelasDDPfromtorch.utils.data.distributedimportDistributedSamplerdeftrain_fn(rank,world_size):# 1. 初始化分布式环境默认使用 NVIDIA NCCL 通信库dist.init_process_group(backendnccl,rankrank,world_sizeworld_size)torch.cuda.set_device(rank)# 2. 创建模型并搬运到对应 GPUmodelMyModel().to(rank)# 3. 包装成 DDP 模型这一步会自动处理后文的梯度同步桶机制modelDDP(model,device_ids[rank])# 4. 配置数据加载器必须加 DistributedSampler 保证每张卡拿到的数据不同datasetMyDataset()samplerDistributedSampler(dataset,num_replicasworld_size,rankrank)dataloadertorch.utils.data.DataLoader(dataset,batch_size32,samplersampler)optimizertorch.optim.AdamW(model.parameters(),lr1e-4)forepochinrange(10):# 顺手避坑每个 epoch 开始前设置 sampler 的 set_epoch保证数据打散的随机种子同步sampler.set_epoch(epoch)forinputs,targetsindataloader:inputs,targetsinputs.to(rank),targets.to(rank)outputsmodel(inputs)losscriterion(outputs,targets)optimizer.zero_grad()loss.backward()# 此时底层已经异步触发了 NCCL All-Reduceoptimizer.step()# 此时所有卡上的梯度已同步安全更新dist.destroy_process_group()if__name____main__:# 假设单机有 8 张卡world_size8os.environ[MASTER_ADDR]localhostos.environ[MASTER_PORT]12355# 启动多进程mp.spawn(train_fn,args(world_size,),nprocsworld_size,joinTrue) 总结DDP 是所有现代分布式深度学习的“基本盘”。无论是跑传统的 CV/NLP 模型还是配合 DeepSpeed / Megatron 去切分超大模型数据并行DP/DDP这一维度的拓扑永远存在。

更多文章