PyTorch DDP分布式训练Pytorch代码讲解

本文参考视频:

https://www.bilibili.com/video/BV1iWCfYCEYx/?spm_id_from=333.337.search-card.all.click&vd_source=132c74f7a893f6ef64b723d9600c40b7https://www.bilibili.com/video/BV1iWCfYCEYx/?spm_id_from=333.337.search-card.all.click&vd_source=132c74f7a893f6ef64b723d9600c40b7

DDP分布式训练理论基础

DDP具体细节可以参考我的另一篇专门讲DDP理论的博客:

多卡显存优化技术:DP/DDP,Deepspeed ZeRO 1/2/3 (Offload),Gradient(Activation) checkpointing详解https://blog.csdn.net/Younai2021/article/details/155561071?spm=1001.2014.3001.5501

DDP支持单机多卡和多机多卡

每个GPU上的模型都是完整的,一样的

DDP将dataloader换成distributed sampler。把一个batch的内容分发给多个GPU

思想是数据并行

由于喂的数据不一样,所以每个机器的模型更新会不一样

DDP通过环形同步(All reduce)的方式将彼此的模型信息同步到一起,这些过程都封装到了反向传播的函数中,不用用户来考虑

所有卡之间的梯度和损失都是同步对齐的

DDP代码实现

在代码实现上其实很简单,就是要注意几个点:

1,原有的代码前面加上trochrun

python 复制代码
export CUDA_VISIBLE_DEVICES=4,5,6,7 && torchrun --nproc_per_node=4 dist_train.py

目前仅考虑单机多卡的情况,nproc_per_node 的全称是 "number of processes per node" ,意思是 每个节点(机器)启动的进程数量, 在分布式训练中,这通常对应 每张 GPU 启动一个训练进程,保证每个 GPU 都被占用并参与梯度同步。

这个代码会在每个GPU上都运行dist_train.py这个脚本

2,让各个进程进入全局的通信组

由于DDP需要各个进程之间进行通信,因此需要在一开始的之后对每个进程进行通信的初始化。

DDP需要对原来的代码做以下几个地方的改动:

1,为每个GPU上的进程的模型显式的分配GPU

python 复制代码
device = torch.device(f"cuda:{local_rank}")

2,使用DistributedSampler把原始的dataset做切分

python 复制代码
train_sampler = DistributedSampler(train_dataset)

    train_loader = DataLoader(
        train_dataset, batch_size=8, shuffle=(train_sampler is None), # 这里的batchsize是local的batchsize
        sampler=train_sampler, collate_fn=dict_collate_fn
    )

3,把模型包装成DDP模型,用于DDP的模型同步通信(包括梯度和优化器的同步)

python 复制代码
    if is_ddp:
        from torch.nn.parallel import DistributedDataParallel as DDP
        model = DDP(model, device_ids=[local_rank], output_device=local_rank) # 作用:把你的模型包成 DDP 模型,让它支持分布式训练。

4,在训练的循环中为了时数据同步要加上set_epoch和dist.barrier()等函数

python 复制代码
        if dist.is_initialized():
            print(f"Rank: {dist.get_rank()}")
        print("local batch size:", batch["pixel_values"].size(0)) # batch[0]:[B, 3, 128, 128]

        # 多GPU时,确保所有进程都同步完成这个batch的计算
        # dist.barrier() 等待所有进程初始化完毕

5,训练完的最后要注销通信组

python 复制代码
    if is_ddp:
        dist.destroy_process_group()
        # 开始 DDP 时用了 init_process_group
        # 结束 DDP 时要调用 destroy_process_group

注意事项:

1,梯度同步:

python 复制代码
grad_global = sum(grad_local_i for i in all_processes) / world_size

DDP中dataloader的batchsize是local的batchsize,最后梯度更新的实际batchsize是global的batchsize

python 复制代码
    train_loader = DataLoader(
        train_dataset, batch_size=8, shuffle=(train_sampler is None), # 这里的batchsize是local的batchsize
        sampler=train_sampler, collate_fn=dict_collate_fn
    )

2,如果print()时没有判断rank时,所有的进程都会打印一次

因此在evaluate时,往往只需要rank0的机器eval,其他机器等待即可

涉及计算和打印loss时,如果不声明,每个进程都会打印一次,而且打印的是本地的local batch的loss。(但是在更新梯度时DDP会自动同步各个进程的local 梯度,最终是global的梯度)

如果想看到global batch上的loss,需要额外将所有进程的loss平均出来打印。

同样的,如果是保存文件这种操作,如果不判断rank时,所有的进程都会保存一次

完整代码:

python 复制代码
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms, datasets
import os
from dataset import CatDogDataset
from dataloader import MyDataLoader, dict_collate_fn
from model import SimpleCNN
from tqdm import tqdm
from utils import train_one_epoch, evaluate

def main():
    # -----------------------------
    # ✅ 1. DDP 初始化(最小修改)
    # -----------------------------
    import torch.distributed as dist
    import torch
    import os

    if "RANK" in os.environ and "WORLD_SIZE" in os.environ:
        # nccl 这个跟显卡品牌有关系,是用于GPU之间的通信
        dist.init_process_group(backend="nccl") # 让当前进程加入一个全局的通信组
        # NCCL 是 NVIDIA Collective Communications Library 的缩写,是 NVIDIA 提供的一个高性能通信库,专门用于多 GPU 之间的通信和协作计算。
        local_rank = int(os.environ["LOCAL_RANK"])
        torch.cuda.set_device(local_rank) # 当前进程之后所有的 CUDA 操作都在 local_rank 这张 GPU 上执行。
        device = torch.device(f"cuda:{local_rank}")
        is_ddp = True
        print(f"Running in DDP mode on rank {dist.get_rank()} / {dist.get_world_size()}")
    else:
        device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
        is_ddp = False
        local_rank = 0

    # -----------------------------
    # ✅ 2. 原本你的代码(完全不动)
    # -----------------------------
    transform = transforms.Compose([
        transforms.Resize((128, 128)),
        transforms.ToTensor()
    ])

    train_dataset = CatDogDataset("/data/czhang/datasets/cat_and_dog/train", transform=transform)
    val_dataset = CatDogDataset("/data/czhang/datasets/cat_and_dog/train", transform=transform)

    # ✅ sampler 替代 shuffle(DDP需要)
    from torch.utils.data import DataLoader, DistributedSampler
    if is_ddp: # 这个sampler是给dataloader用的
        train_sampler = DistributedSampler(train_dataset) # DistributedSampler 会自动把数据集进行划分,保证每个进程看到不同的数据
        val_sampler = DistributedSampler(val_dataset, shuffle=False)
    else:
        train_sampler = None
        val_sampler = None

    train_loader = DataLoader(
        train_dataset, batch_size=8, shuffle=(train_sampler is None), # 这里的batchsize是local的batchsize
        sampler=train_sampler, collate_fn=dict_collate_fn
    )
    val_loader = DataLoader(
        val_dataset, batch_size=8, shuffle=False,
        sampler=val_sampler, collate_fn=dict_collate_fn
    )

    model = SimpleCNN().to(device) # 每个进程单独加载完整的模型

    # ✅ 如果是 DDP 模式则包一层 
    if is_ddp:
        from torch.nn.parallel import DistributedDataParallel as DDP
        model = DDP(model, device_ids=[local_rank], output_device=local_rank) # 作用:把你的模型包成 DDP 模型,让它支持分布式训练。
        # dist.broadcast_parameters(model.state_dict(), src=0) # 从0号进程广播,使得初始参数完成对齐
        # 在这个里面,rank 0的进程会将梯度广播到其他进程,从而保证所有进程的模型参数是一致的。

    criterion = nn.CrossEntropyLoss()
    optimizer = optim.Adam(model.parameters(), lr=1e-4) # 优化器的部分就不用管了,会自动对齐

    epochs = 5
    for epoch in range(1, epochs + 1):
        train_loss, train_acc = train_one_epoch(model, train_loader, optimizer, criterion, device, epoch)
        if not is_ddp or dist.get_rank() == 0:
            print(f"[Epoch {epoch}] Train Loss: {train_loss:.4f} | Train Acc: {train_acc:.3f}")

    if not is_ddp or dist.get_rank() == 0: # 如果是 DDP,只让 rank 0 执行 evaluate 打印和保存
        val_loss, val_acc = evaluate(model, val_loader, criterion, device)
        print(f"Val Loss: {val_loss:.4f} | Val Acc: {val_acc:.3f}")
        torch.save(model.state_dict(), "ckpts/model_final.pth")

    if is_ddp:
        dist.destroy_process_group()
        # 开始 DDP 时用了 init_process_group
        # 结束 DDP 时要调用 destroy_process_group


if __name__ == "__main__":
    main()
    # CUDA_VISIBLE_DEVICES=0,1,2,3 torchrun --nproc_per_node=4 /data/czhang/projects/mini_framework/dist_train.py
相关推荐
Warren2Lynch3 小时前
利用 AI 协作优化软件更新逻辑:构建清晰的 UML 顺序图指南
人工智能·uml
ModelWhale4 小时前
当“AI+制造”遇上商业航天:和鲸助力头部企业,构建火箭研发 AI 中台
人工智能
ATMQuant4 小时前
量化指标解码13:WaveTrend波浪趋势 - 震荡行情的超买超卖捕手
人工智能·ai·金融·区块链·量化交易·vnpy
weixin_509138344 小时前
语义流形探索:大型语言模型中可控涌现路径的实证证据
人工智能·语义空间
多米Domi0114 小时前
0x3f第33天复习 (16;45-18:00)
数据结构·python·算法·leetcode·链表
soldierluo4 小时前
大模型的召回率
人工智能·机器学习
Gofarlic_oms14 小时前
Windchill用户登录与模块访问失败问题排查与许可证诊断
大数据·运维·网络·数据库·人工智能
童话名剑4 小时前
人脸识别(吴恩达深度学习笔记)
人工智能·深度学习·人脸识别·siamese网络·三元组损失函数
_YiFei4 小时前
2026年AIGC检测通关攻略:降ai率工具深度测评(含免费降ai率方案)
人工智能·aigc
freepopo4 小时前
天津商业空间设计:材质肌理里的温度与质感[特殊字符]
python·材质