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