yolov5 DDP


跑yolov5的DDP记录

一开始是pytorch1.7,python3.7 跑ddp模式训练,各种问题报错。但是后来好像把一个参数不兼容的给注释就好了?但是从机没有打印以为是有问题的?

# EarlyStopping
        if RANK != -1:  # if DDP training
            broadcast_list = [stop if RANK == 0 else None]
            dist.broadcast_object_list(broadcast_list, 0)  ##pytorch1.7 接口这里不一样

然后就重新搭建高版本环境torch1.10.1_py38,环境搭好了跑yolov5果然很顺滑。
这里先给出运行指令

python -m torch.distributed.launch --nproc_per_node 1 --nnodes 2 --node_rank 0 --master_addr "10.188.192.11" --master_port 12379 train.py --batch 12 --data coco128.yaml --epochs 300 --weights yolov5s.pt


python -m torch.distributed.launch --nproc_per_node 2 --nnodes 2 --node_rank 1 --master_addr "10.188.192.11" --master_port 12379 train.py --batch 12 --data coco128.yaml --epochs 300 --weights yolov5s.pt
# 假设我们只在一台机器上运行,可用卡数是8
python -m torch.distributed.launch --nproc_per_node 8 main.py

这里的--nproc_per_node 2可以根据当前机器指定,有一张卡就1,用2张卡就2
跑起来的时候需要两个机器都要运行才能训练,某一个机器运行起来就会等待另外一个机器跑起来来运行。
但是这个代码跑起来的时候只有主机才会有训练的log打印出来,从机没有打印。
一开始我还以为哪里有问题,但是观察到从机有显存占用,然后我在从机代码添加打印信息,发现其实从机是在跑的,只是打印都屏蔽了,代码里面默认只打印rank0的。

0. 一些概念:

在16张显卡,16的并行数下,DDP会同时启动16个进程。下面介绍一些分布式的概念。

group
即进程组。默认情况下,只有一个组。这个可以先不管,一直用默认的就行。

world size
表示全局的并行数,简单来讲,就是2x8=16。

# 获取world size,在不同进程里都是一样的,得到16
torch.distributed.get_world_size()

rank
表现当前进程的序号,用于进程间通讯。对于16的world sizel来说,就是0,1,2,…,15。
注意:rank=0的进程就是master进程。

# 获取rank,每个进程都有自己的序号,各不相同
torch.distributed.get_rank()

local_rank
又一个序号。这是每台机子上的进程的序号。机器一上有0,1,2,3,4,5,6,7,机器二上也有0,1,2,3,4,5,6,7

# 获取local_rank。一般情况下,你需要用这个local_rank来手动设置当前模型是跑在当前机器的哪块GPU上面的。
torch.distributed.local_rank()

这里给出DDP模式下需要修改的代码:

1. local_rank参数

需要添加 parser.add_argument('--local_rank', type=int, default=-1, help='Automatic DDP Multi-GPU argument, do not modify')
使用torch.distributed.launch启动DDP模式,
其会给main.py一个local_rank的参数。这就是之前需要"新增:从外面得到local_rank参数"的原因
python -m torch.distributed.launch --nproc_per_node 4 main.py

代码里面需要根据local_rank这个值来初始化具体哪个卡加载

    if LOCAL_RANK != -1:
        msg = 'is not compatible with YOLOv5 Multi-GPU DDP training'
        assert not opt.image_weights, f'--image-weights {msg}'
        assert not opt.evolve, f'--evolve {msg}'
        assert opt.batch_size != -1, f'AutoBatch with --batch-size -1 {msg}, please pass a valid --batch-size'
        assert opt.batch_size % WORLD_SIZE == 0, f'--batch-size {opt.batch_size} must be multiple of WORLD_SIZE'
        assert torch.cuda.device_count() > LOCAL_RANK, 'insufficient CUDA devices for DDP command'
        torch.cuda.set_device(LOCAL_RANK)
        device = torch.device('cuda', LOCAL_RANK)
        dist.init_process_group(backend="nccl" if dist.is_nccl_available() else "gloo")
# 构造模型
device = torch.device("cuda", local_rank)
model = nn.Linear(10, 10).to(device)
# 新增:构造DDP model
model = DDP(model, device_ids=[local_rank], output_device=local_rank)

2. init_process_group, torch.distributed.barrier需要先初始化一下

def init_distributed_mode(args):
    """ init for distribute mode """
    if 'RANK' in os.environ and 'WORLD_SIZE' in os.environ:
        args.rank = int(os.environ["RANK"])
        args.world_size = int(os.environ['WORLD_SIZE'])
        args.gpu = int(os.environ['LOCAL_RANK'])
    elif 'SLURM_PROCID' in os.environ:
        args.rank = int(os.environ['SLURM_PROCID'])
        args.gpu = args.rank % torch.cuda.device_count()
    else:
        print('Not using distributed mode')
        args.distributed = False
        return

    args.distributed = True

    torch.cuda.set_device(args.gpu)
    args.dist_backend = 'nccl'
    '''
    This is commented due to the stupid icoding pylint checking.
    print('distributed init rank {}: {}'.format(args.rank, args.dist_url), flush=True)
    '''
    torch.distributed.init_process_group(backend=args.dist_backend, init_method=args.dist_url,
                                         world_size=args.world_size, rank=args.rank)
    torch.distributed.barrier()
 # DDP mode
    device = select_device(opt.device, batch_size=opt.batch_size)
    if LOCAL_RANK != -1:
        msg = 'is not compatible with YOLOv5 Multi-GPU DDP training'
        assert not opt.image_weights, f'--image-weights {msg}'
        assert not opt.evolve, f'--evolve {msg}'
        assert opt.batch_size != -1, f'AutoBatch with --batch-size -1 {msg}, please pass a valid --batch-size'
        assert opt.batch_size % WORLD_SIZE == 0, f'--batch-size {opt.batch_size} must be multiple of WORLD_SIZE'
        assert torch.cuda.device_count() > LOCAL_RANK, 'insufficient CUDA devices for DDP command'
        torch.cuda.set_device(LOCAL_RANK)
        device = torch.device('cuda', LOCAL_RANK)
        dist.init_process_group(backend="nccl" if dist.is_nccl_available() else "gloo")
@contextmanager
def torch_distributed_zero_first(local_rank: int):
    # Decorator to make all processes in distributed training wait for each local_master to do something
    if local_rank not in [-1, 0]:
        dist.barrier(device_ids=[local_rank])
    yield
    if local_rank == 0:
        dist.barrier(device_ids=[0])

3. 注意随机种子需要设置每个进程不一样

在DDP训练中,如果还是像以前一样,使用0作为随机数种子,不做修改,就会造成以下后果:
DDP的N个进程都使用同一个随机数种子在生成数据时,如果我们使用了一些随机过程的数据扩充方法,那么,各个进程生成的数据会带有一定的同态性。
比如说,YOLOv5会使用mosaic数据增强(从数据集中随机采样3张图像与当前的拼在一起,组成一张里面有4张小图的大图)。这样,因为各卡使用了相同的随机数种子,你会发现,各卡生成的图像中,除了原本的那张小图,其他三张小图都是一模一样的!同态性的数据,降低了训练数据的质量,也就降低了训练效率!最终得到的模型性能,很有可能是比原来更低的。

所以,我们需要给不同的进程分配不同的、固定的随机数种子:

    device = "cpu"
    if torch.cuda.is_available():
        device = "cuda"
        cudnn.benchmark = True

    # fix the seed for reproducibility
    seed = config.seed + dist.get_rank()
    torch.manual_seed(seed)
    np.random.seed(seed)
def init_seeds(seed=0, deterministic=False):
    # Initialize random number generator (RNG) seeds https://pytorch.org/docs/stable/notes/randomness.html
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)  # for Multi-GPU, exception safe
    # torch.backends.cudnn.benchmark = True  # AutoBatch problem https://github.com/ultralytics/yolov5/issues/9287
    if deterministic and check_version(torch.__version__, '1.12.0'):  # https://github.com/ultralytics/yolov5/pull/8213
        torch.use_deterministic_algorithms(True)
        torch.backends.cudnn.deterministic = True
        os.environ['CUBLAS_WORKSPACE_CONFIG'] = ':4096:8'
        os.environ['PYTHONHASHSEED'] = str(seed)

init_seeds(opt.seed + 1 + RANK, deterministic=True)

4. model 需要ddp包装一下

但是需要在optimizer之后

def smart_DDP(model):
    # Model DDP creation with checks
    assert not check_version(torch.__version__, '1.12.0', pinned=True), \
        'torch==1.12.0 torchvision==0.13.0 DDP training is not supported due to a known issue. ' \
        'Please upgrade or downgrade torch to use DDP. See https://github.com/ultralytics/yolov5/issues/8395'
    if check_version(torch.__version__, '1.11.0'):
        return DDP(model, device_ids=[LOCAL_RANK], output_device=LOCAL_RANK, static_graph=True)
    else:
        return DDP(model, device_ids=[LOCAL_RANK], output_device=LOCAL_RANK)
优化器optimizer应用gradient,更新参数(optimizer.step())。这一步,是和DDP没关系的。

虽然DDP的实现代码与optimizer没有关系,但是关于optimizer有个额外的东西需要说明。更新后的参数最终能在各进程间保持一致,是由以下因素保证的:

参数初始值相同
参数更新值相同,更新值相同由optimizer初始状态相同和每次opimizer.step()时的梯度相同保证的。

因为optimizer和DDP是没有关系的,所以optimizer初始状态的同一性是不被DDP保证的!
大多数官方optimizer,其实现能保证从同样状态的model初始化时,其初始状态是相同的。所以这边我们只要保证在DDP模型创建后才初始化optimizer,就不用做额外的操作。但是,如果自定义optimizer,则需要你自己来保证其统一性!
从代码中看出optimizer确实是在DDP之后定义的。这个时候的模式已经是被初始化为相同的参数,所以能够保证优化器的初始状态是相同的。

# 新增:构造DDP model
model = DDP(model, device_ids=[local_rank], output_device=local_rank)

# 优化器:要在构造DDP model之后,才能初始化optimizer。
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.8)

5. SyncBatchNorm

    # SyncBatchNorm
    if opt.sync_bn and cuda and RANK != -1:
        model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(model).to(device)
        LOGGER.info('Using SyncBatchNorm()')

6. distributed.DistributedSampler

sampler = distributed.DistributedSampler(dataset, shuffle=shuffle)

generator = torch.Generator()
    generator.manual_seed(6148914691236517205 + seed + RANK)
    return loader(dataset,
                  batch_size=batch_size,
                  shuffle=shuffle and sampler is None,
                  num_workers=nw,
                  sampler=sampler,
                  pin_memory=PIN_MEMORY,
                  collate_fn=LoadImagesAndLabels.collate_fn4 if quad else LoadImagesAndLabels.collate_fn,
                  worker_init_fn=seed_worker,
                  generator=generator)
# 假设我们的数据是这个
def get_dataset():
    transform = torchvision.transforms.Compose([
        torchvision.transforms.ToTensor(),
        torchvision.transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
    ])
    my_trainset = torchvision.datasets.CIFAR10(root='./data', train=True, 
        download=True, transform=transform)
    # DDP:使用DistributedSampler,DDP帮我们把细节都封装起来了。
    #      用,就完事儿!sampler的原理,第二篇中有介绍。
    train_sampler = torch.utils.data.distributed.DistributedSampler(my_trainset)
    # DDP:需要注意的是,这里的batch_size指的是每个进程下的batch_size。
    #      也就是说,总batch_size是这里的batch_size再乘以并行数(world_size)。
    trainloader = torch.utils.data.DataLoader(my_trainset, 
        batch_size=16, num_workers=2, sampler=train_sampler)
    return trainloader

7. train_loader.sampler.set_epoch(epoch)

for epoch in iterator:
    # DDP:设置sampler的epoch,
    # DistributedSampler需要这个来指定shuffle方式,
    # 通过维持各个进程之间的相同随机数种子使不同进程能获得同样的shuffle效果。
    trainloader.sampler.set_epoch(epoch)
    for epoch in range(start_epoch, config.solver.epochs):
        if args.distributed:
            train_loader.sampler.set_epoch(epoch)        

        train(model_full, train_loader, optimizer, criterion, scaler,
              epoch, device, lr_scheduler, config, classes_features, logger)
    for epoch in range(start_epoch, epochs):  # epoch ------------------------------------------------------------------
        callbacks.run('on_train_epoch_start')
        model.train()
        print("epoch=", epoch)

        if RANK != -1:
            train_loader.sampler.set_epoch(epoch)
        for i, (imgs, targets, paths, _) in pbar:
           pass

8. 只rank=0保存模型

  # DDP:
    # 1. save模型的时候,和DP模式一样,有一个需要注意的点:保存的是model.module而不是model。
    #    因为model其实是DDP model,参数是被`model=DDP(model)`包起来的。
    # 2. 只需要在进程0上保存一次就行了,避免多次保存重复的东西。
    if dist.get_rank() == 0:
        torch.save(model.module.state_dict(), "%d.ckpt" % epoch)

9. batch_size

# DDP:需要注意的是,这里的batch_size指的是每个进程下的batch_size。
    #      也就是说,总batch_size是这里的batch_size再乘以并行数(world_size)。
    trainloader = torch.utils.data.DataLoader(my_trainset, 
        batch_size=16, num_workers=2, sampler=train_sampler)

摘自网络 https://blog.csdn.net/weixin_44823313/article/details/124182370
注意,在DDP模式中Batchnorm会出现inplace操作导致梯度无法反传,可以用nn.SyncBatchNorm()替代nn.BatchNormxd()
如果必须要用到batchnorm,按照这个设置将DDP模型broadcast_buffers设置为False,https://github.com/pytorch/pytorch/issues/22095
model = torch.nn.parallel.DistributedDataParallel(model.cuda(args.local_rank),
device_ids=[args.local_rank],
output_device=args.local_rank,
broadcast_buffers=False)

10. 链接:

ddp的github工程
https://github.com/whwu95/Text4Vis
该作者知乎链接:https://zhuanlan.zhihu.com/p/373395654?utm_id=0

https://github.com/ultralytics/yolov5

比较好的讲解的链接
[原创][深度][PyTorch] DDP系列第一篇:入门教程 https://zhuanlan.zhihu.com/p/178402798
https://zhuanlan.zhihu.com/p/187610959
https://zhuanlan.zhihu.com/p/250471767

PyTorch分布式训练基础--DDP使用 https://zhuanlan.zhihu.com/p/358974461

Pytorch多机多卡分布式训练(有git工程demo) https://zhuanlan.zhihu.com/p/373395654?utm_id=0

posted @ 2023-02-15 11:54  无左无右  阅读(296)  评论(0编辑  收藏  举报