️ 前言


更新日志:

20220404:新增一个DDP 加载模型时显存分布不均问题,见目录遇到的问题及解决处


主要是上次server12 被自己一个train 直接线程全部拉满了(没错 ... server8 也被拉满过 emm我一开始还没发现 原来是我拉满的)

现场实况

后面刘所就跟我说让我看看是不是dataset里面的处理太多了,这样下来GPU占着 使用率也不高,建议先处理完了再直接由load进来 直接训练;因为server上的cpu都不是很好,一开始那样玩会拉慢训练速度,两种选择:

  1. 把cpu的操作全部放到gpu上去做
  2. 先把cpu操作做了保存成数据,这样不用每次epoch都做一次操作了

下面主要就是针对前言里面说的两条进行分析与方案确认

️ 1. 预处理


最终方案 ( 并发多线程, dataset→pkl )

参考:

线程并发 并行 多机并行等一系列操作:

Ray v1.10.0

tips:

Programming in Ray: Tips for first-time users - RISE Lab

详情代码见:暂时还没开源

  1. 多CPU线程并行 → ray库
  2. 一个进程运行较多sample → ray.append

相关代码与解释

# 记得改一下 num cpus
sample_in_pre_run = 50
@ray.remote(num_cpus=48)
def write_pkl(save_dir, cur_num, dataset, run_num, pba: ActorHandle):
# print(cur_num, cur_num + run_num)
for i in range(cur_num, min(cur_num + run_num, len(dataset))):
with open(f'{save_dir}/%d.pkl'%(i), 'wb') as fd:
pickle.dump(dataset[i], fd)
pba.update.remote(1)
  1. num_cpus 是需要根据自己的线程数设置,不是越多越好!比如服务器上12, 8左右就行,因为每开一个线程就需要很多时间,如果开线程的耗费>>进程的耗费,那还不如单进程自己跑跑跑
  2. 根据上面提示,也就引入了sample_in_pre_run 也就是一个进程里干多少个工作,如果太多 可能进程拖慢了,太少 开线程又浪费了

里面有些操作是进度条可忽略,注意并发的ray 原tqdm进度条会失效!

详情见官方:(也可见上述gitlab中有copy下面进度条)

Ray v1.10.0

ray_train_set = ray.put(train_set)
for cur_num in range(0, len(train_set), sample_in_pre_run):
tasks_pre.append(write_pkl.remote(train_dir, cur_num, ray_train_set, sample_in_pre_run))
ray_val_set = ray.put(val_set)
for cur_num in range(0, len(val_set), sample_in_pre_run):
tasks_pre.append(write_pkl.remote(val_dir, cur_num, val_set, sample_in_pre_run)) pb.print_until_done()
tasks = ray.get(tasks_pre)
# 进度条相关
tasks == list(range(num_ticks))
num_ticks == ray.get(actor.get_counter.remote())
  1. 可以看到每个for里面的进程加上了remote也就是ray所需要的函数,可以看一下上面的walkin

  2. 把每一个任务都append到tasks_pre后,再统一 ray.get(tasks_pre) 即可

  3. 注意新加了一个ray.put 是因为上服务器大数据的时候发现append操作巨慢,然后查了一下和传入的参数有关,所以把参直接放到put共享下了

  4. 不要进度条的话 大概简洁版是这样的:

    from torch.utils.data import DataLoader
    from datasets.config import GlobalConfig # 一开始的数据操作都在dataset的get_item里面做了
    from datasets.dataloader import CARLA_Data
    # 一开始的数据操作都在dataset的get_item里面做了 import ray
    ray.init()
    sample_in_pre_run = 50
    @ray.remote(num_cpus=48)
    def write_pkl(save_dir, cur_num, dataset, run_num):
    # print(cur_num, cur_num + run_num)
    for i in range(cur_num, min(cur_num + run_num, len(dataset))):
    with open(f'{save_dir}/%d.pkl'%(i), 'wb') as fd:
    pickle.dump(dataset[i], fd) ray_train_set = ray.put(train_set)
    for cur_num in range(0, len(train_set), sample_in_pre_run):
    tasks_pre.append(write_pkl.remote(train_dir, cur_num, ray_train_set, sample_in_pre_run))
    ray_val_set = ray.put(val_set)
    for cur_num in range(0, len(val_set), sample_in_pre_run):
    tasks_pre.append(write_pkl.remote(val_dir, cur_num, val_set, sample_in_pre_run)) # 一起并发处理
    ray.get(tasks_pre)

速度效果对比

最终效果速度对比:

大概是2.5倍的速度 处理速度

  • 但是不知为何server跑大数据量的时候一动不动 emmm 是等的时间太短了吗?

    上面提到了append操作 每次传进dataset太慢 放到put里面 共享了就快了

这之后要是还是很慢的话,就不是CPU的锅了,建议检查一下io的读取速度 如下:

记一次服务端 IO 瓶颈问题定位 · TesterHome

使用率对比

主要就是处理完数据后通过pkl等保存的文件直接再读取一次dataset 比如上面处理完CARLA_Data后再写一个直接load pkl的dataset即可

class PRE_Data(Dataset):
def __init__(self, root, config, data_use='train'): self.preload_dict = []
preload_file = os.path.join(root, 'rg_mmfn_diag_pl_'+str(self.seq_len)+'_'+str(self.pred_len)+ '_' + data_use +'.npy')
preload_dict = []
if not os.path.exists(preload_file):
# list sub-directories in root
for pkl_file in os.listdir(root):
if pkl_file.split('.')[-1]=='pkl':
pkl_file = str(root) + '/' + pkl_file
preload_dict.append(pkl_file)
np.save(preload_file, preload_dict) # load from npy if available
preload_dict = np.load(preload_file, allow_pickle=True)
self.preload_dict = preload_dict
print("Preloading sequences from " + preload_file) def __len__(self):
"""Returns the length of the dataset. """
return len(self.preload_dict) def __getitem__(self, index):
"""Returns the item at index idx. """ with open(self.preload_dict[index], 'rb') as fd:
data = pickle.load(fd)
return data

可以看到提前通过处理后,CPU的使用率基本不会在拉满到100%,甚至等同于无(因为同时还有其他人使用这个server

同时GPU使用率也提高了两倍,主要是之前的使用率一直在20%不动,CPU都跑满了都不动

️ 2. 单机多卡并行


参考:

官方的DDP教程:

Getting Started with Distributed Data Parallel - PyTorch Tutorials 1.10.1+cu102 documentation

github 211star 中文:

https://github.com/jia-zhuang/pytorch-multi-gpu-training

主要就是调研情况里面的 那个参考链接 的做法,首先根据官方文档 我们看一下 选择 DistributedDataParallelDataParallel 之间的官方给出的区别及效率,官方推荐前者 DistributedDataParallel :Before we dive in, let’s clarify why, despite the added complexity, you would consider using DistributedDataParallel over DataParallel:

  1. 首先如果不是那么急切的话 其实 DataParallel 也行,主要是因为实现起来真的很方便加一行 真就一行
  2. DistributedDataParallel 就不一样了 要加很多行(还行还行),但是呢 是高效的

两者之间的效率对比我并没有做,所以我们就根据官方文档而的来这个结论了

两种方案 理论知识


以下为 官方对比链接人工翻译

  • DataParallel 是单进程多线程的,而且只能在一个机器上进行 也就是单机多卡
  • DistributedDataParallel 多进程 既可以用于单机训练,也可以用于多机训练

再者因为 DataParallel 跨线程的操作使得 线程之间的 GIL 竞争 、每次迭代间复制模型同步 以及 分散输入 和 集成输出,这些都会导致额外开销,即使在单台机器上,DataParallel 通常也比 DistributedDataParallel 慢。

这是其一,其二呢如果 你的模型太太太 大以至于一个小GPU都装不下,那么 DataParallel 就失效了,因为必须使用 模型并行 将其拆分到多个 GPU 上。 DistributedDataParallel 是与模型并行工作的,而DataParallel 目前没有实现

第三点是小提醒 与对比无关:DDP 与模型并行相结合时,每个 DDP 进程将使用模型并行,所有进程共同使用数据并行。如果模型需要跨越多台机器,或者 模型方案等 不适合数据并行范式,请参阅 RPC API 以获得更通用的分布式训练支持。


理论知识学习完了 进入代码实践部分

代码部分 修改

https://github.com/jia-zhuang/pytorch-multi-gpu-training

正如参考的github中那样 (其实那个写的挺不错的 hhhh 以下为部分重复及补充 因为遇到一些意想不到的情况 emmm 一言难尽)

DataParallel


之所以简单是因为... 只需要一行,只需要把自己的model放进去就行网络

注意参考所说的这点,更为详情 点击参考链接查看

为方便说明,我们假设模型输入为(32, input_dim),这里的 32 表示batch_size,模型输出为(32, output_dim),使用 4 个GPU训练。nn.DataParallel 起到的作用是将这 32 个样本拆成 4 份,发送给 4 个GPU 分别做 forward,然后生成 4 个大小为(8, output_dim)的输出,然后再将这 4 个输出都收集到cuda:0 上并合并成(32, output_dim)

可以看出,nn.DataParallel没有改变模型的输入输出,因此其他部分的代码不需要做任何更改,非常方便。但弊端是,后续的loss计算只会在cuda:0上进行,没法并行,因此会导致负载不均衡的问题;如果把loss放在模型里计算的话,则可以缓解上述负载不均衡的问题

# Model
model = TransFuser(config, args.device)
if args.is_multi_gpu:
print(bcolors.OKGREEN + "Multi GPU USE"+ bcolors.ENDC)
model = nn.DataParallel(model)

DistributedDataParallel


实现起来更为麻烦”一“点,因为是多进程

从一开始就会启动多个进程(进程数等于GPU数),每个进程独享一个GPU,每个进程都会独立地执行代码。这意味着每个进程都独立地初始化模型、训练,当然,在每次迭代过程中会通过进程间通信共享梯度,整合梯度,然后独立地更新参数。

CUDA_VISIBLE_DEVICES=0,1 python -m torch.distributed.launch --nproc_per_node=2 --nnodes=1 train.py

所以你运行了一句话 但是类似于一个脚本运行了四个python,所以呢 所有打印和保存都会运行四个,办法就是判断这是args.local_rank是0 的时候再进行这些保存打印操作

CUDA_VISIBLE_DEVICES 为运行时脚本能看到的GPU id,nproc_per_node 为GPU个数,nnodes 为主机个数 单机就是1,train.py 就是你的正常训练代码,注意需要经过以下几点修改:

  1. 要有local_rank的传入 因为运行的时候 他会出入一个local rank指定

    from torch.utils.data.distributed import DistributedSampler
    
    parser = argparse.ArgumentParser()
    parser.add_argument("--local_rank", type=int, default=-1)
    args = parser.parse_args()
    • 碎碎念,也可以 走 自己在 程序里指定 的方式, 比如官方所示这样

      """run.py:"""
      #!/usr/bin/env python
      import os
      import torch
      import torch.distributed as dist
      import torch.multiprocessing as mp def run(rank, size):
      """ Distributed function to be implemented later. """
      pass def init_process(rank, size, fn, backend='gloo'):
      """ Initialize the distributed environment. """
      os.environ['MASTER_ADDR'] = '127.0.0.1'
      os.environ['MASTER_PORT'] = '29500'
      dist.init_process_group(backend, rank=rank, world_size=size)
      fn(rank, size) if __name__ == "__main__":
      size = 2
      processes = []
      mp.set_start_method("spawn")
      for rank in range(size):
      p = mp.Process(target=init_process, args=(rank, size, run))
      p.start()
      processes.append(p) for p in processes:
      p.join()
  2. 因为每个进程都会初始化一份模型,为保证模型初始化过程中生成的随机权重相同,需要设置随机种子。方法如下:

    def set_seed(seed):
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)
  3. train_set给一下DistributedSampler

    # Data
    train_set = PRE_Data(root=config.train_data, config=config, data_use='train')
    val_set = PRE_Data(root=config.val_data, config=config, data_use='val') # 多GPU训练
    train_sampler = DistributedSampler(train_set)
    val_sampler = DistributedSampler(val_set) dataloader_train = DataLoader(train_set, batch_size=args.batch_size, sampler=train_sampler, num_workers=8, pin_memory=True)
    dataloader_val = DataLoader(val_set, batch_size=args.batch_size, sampler=val_sampler, num_workers=4, pin_memory=True)
  4. 保存和eval都只需要进行一次即可 → wandb记录和打印等等都只需要一次哈

    if epoch % args.val_every == 0 and args.local_rank == 0:
    trainer.validate(model, dataloader_val, config)
    if epoch % args.save_every == 0:
    trainer.save(model, optimizer) # 官方保存
    if rank == 0:
    # All processes should see same parameters as they all start from same
    # random parameters and gradients are synchronized in backward passes.
    # Therefore, saving it in one process is sufficient.
    torch.save(ddp_model.state_dict(), CHECKPOINT_PATH)

    保存也只需要一次是因为(注释也有):所有进程都应该看到相同的参数,因为它们都从相同的随机参数开始,并且梯度在反向传递中是同步的。 因此,将其保存在一个进程中就足够了。

  5. 保存模型时应注意只需要保存一次,而且必须在GPU上,cpu会有问题 见后问题栏有提到

    torch.save(model.module.state_dict(), os.path.join(self.logdir, 'best_model.pth'))

    后续导入的时候 一定要注意 1. map到cpu上 2. 映射一下所有layer的东西,见后问题栏也有提示

    state_dict = torch.load(os.path.join(self.config_path.model_path, 'best_model.pth'), map_location=torch.device('cpu'))
    pretrained_dict = {key.replace("module.", ""): value for key, value in state_dict.items()}
    self.net.load_state_dict(pretrained_dict)
  6. 为保证所有gpu分配均匀显存,请早模型前执行,详情见问题栏

    # 就是这两句话
    torch.cuda.set_device(args.local_rank)
    torch.cuda.empty_cache()
    # 就是这两句话 # Model
    model = TransFuser(config, args.device)
    model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[args.local_rank], output_device=args.local_rank, find_unused_parameters=True)

剩余问题 见后续问题部分,请提前进行查看,了解相关会遇到的问题

效果对比

多GPU 速度

相同数据量,两块2080Ti和一块2080Ti的对比,使用 DistributedDataParallel 最终效果速度对比:

DP 和 DDP 的使用率对比

DP弊端还有就是... GPU利用率很低,不知道是不是因为样本拆成分配的问题,利用率直接掉半,如下图对比所示 同样的数据集和训练 DDP 和 DP(随着GPU数量越多 利用率越低) 的使用率对比

3. 遇到的问题及解决

DistributedDataParallel


  1. RuntimeError: NCCL communicator was aborted

    这个问题是我随机遇到的同样模型下 小数据集没啥报错,但是全的时候发现emmm 挺随机的;相关pytorch 讨论区链接如下:

    RuntimeError: NCCL communicator was aborted

    好像找到原因了,不应该在保存是.cpu 模型文件 因为即使.to 回去 也是会报错的,所以直接保存args.local_rank为0的那个模型就行,因为官方里面也是这么干的...

    所有进程都应该看到相同的参数,因为它们都从相同的随机参数开始,并且梯度在反向传递中是同步的。 因此,将其保存在一个进程中就足够了。

  2. Pytorch distributed RuntimeError: Address already in use

    因为紧急kill掉了 留下了后患,虽然通过命令行kill了所属pid 但是好像还是占着那个默认口,比如通过htop里面的命令行进行选择所有 kill

    kill -9 $(pgrep -f "/opt/conda/envs/python37/bin/python -u mmfn" | xargs echo)

    然后好像口还是没能释放 换一下端口号就行:

    python -m torch.distributed.launch --nproc_per_node=4 --master_port 12120 --nnodes=1 mmfn/phase2_train_multipgpu.py
  3. 注意 使用了DDP 不知道是需要多匀一些显存的,比如一下,用了一块3090,显存是24G,正常单独GPU训练时 batch_size设置了64,占用了20G显存,那么到DDP这个方案的时候,虽然是多进程运行按理来说应该也设置64,毕竟其他也是3090嘛 但是呢!实际运行的时候 发现第一块需要占用更多的东西,如果GPU越多 他越需要占用(2G/块) → 但是我看华哥的好像... 没有这种现象产生 估计哪里我没注意到

    • 现场实况:

    杰哥太强了!啊!找到原因啦!他喵... 竟然要在model前说明一下

    Extra 10GB memory on GPU 0 in DDP tutorial

    # 就是这两句话
    torch.cuda.set_device(args.local_rank)
    torch.cuda.empty_cache()
    # 就是这两句话 # Model
    model = TransFuser(config, args.device)
    model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[args.local_rank], output_device=args.local_rank, find_unused_parameters=True)
  4. 上面这点 还有可能是需要加载模型再进行下一个模型的训练,那么也有可能导致GPU显存分布不均匀

    需要提前load的时候专门map_location

    state_dict = torch.load(model_name, map_location=self.args.device)
    self.load_state_dict(state_dict)

    相关链接:DDP taking up too much memory on rank 0

  5. 如果resume的话,需要重新组织一下读取的layer id,相关链接:

    https://github.com/bearpaw/pytorch-classification/issues/27

    Missing keys & unexpected keys in state_dict when loading self trained model

    # for DDP model load use
    state_dict = torch.load(os.path.join(args.logdir, 'best_model.pth'))
    optimizer.load_state_dict(torch.load(os.path.join(args.logdir, 'best_optim.pth')))
    from collections import OrderedDict
    new_state_dict = OrderedDict() for k, v in state_dict.items():
    if 'module' not in k:
    k = 'module.'+k
    else:
    k = k.replace('features.module.', 'module.features.')
    new_state_dict[k]=v model.load_state_dict(new_state_dict)

DataParallel


这个主要是注意那个batch_size 是分配的,也就是会除GPU个数,如果你的处理里有涉及到这样的情况,则会出现相关数据的size对不上

比如在dataloader里 进行了 对batch里的数据取最大,作为一个长度;然后再到模型的forward里继续处理一次数据 作为长度,那么;四个分散的batch就会有四个不同的size长度,也就会发生一定的问题,前方实况:

建议措施,所有对数据size的确定在loader部分进行完成


以下为一开始调研情况

相关原因分析

  1. GPU利用率低:https://zhuanlan.zhihu.com/p/410244780

  2. 待做:还在分析是否能解决速度慢的问题 → 能解决一半

    https://github.com/jia-zhuang/pytorch-multi-gpu-training

​ 附加:cpu htop参数相关解释:Understanding and using htop to monitor system resources

这里也提到了loader 一个个小的太小了 最好处理成大的再读取:https://github.com/Lyken17/Efficient-PyTorch#data-loader

pkl

已经写了但是直接在loader出来的 也就是batch_size 从这儿就定下来了 → 错误做法

lmdb:https://github.com/dotchen/WorldOnRails/blob/release/docs/DATASET.md

看一下是否能携程lmdb式,loader式有点过于暴力了


赠人点赞 手有余香 ;正向回馈 才能更好开放记录 hhh

【深度学习 有效炼丹】多GPU使用教程, DP与DDP对比, ray多线程并行处理等 [GPU利用率低的分析]的更多相关文章

  1. 给深度学习入门者的Python快速教程 - 番外篇之Python-OpenCV

    这次博客园的排版彻底残了..高清版请移步: https://zhuanlan.zhihu.com/p/24425116 本篇是前面两篇教程: 给深度学习入门者的Python快速教程 - 基础篇 给深度 ...

  2. 给深度学习入门者的Python快速教程 - numpy和Matplotlib篇

    始终无法有效把word排版好的粘贴过来,排版更佳版本请见知乎文章: https://zhuanlan.zhihu.com/p/24309547 实在搞不定博客园的排版,排版更佳的版本在: 给深度学习入 ...

  3. 深度学习标注工具 LabelMe 的使用教程(Windows 版本)

    深度学习标注工具 LabelMe 的使用教程(Windows 版本) 2018-11-21 20:12:53 精灵标注助手:http://www.jinglingbiaozhu.com/ LabelM ...

  4. 给深度学习入门者的Python快速教程

    给深度学习入门者的Python快速教程 基础篇 numpy和Matplotlib篇 本篇部分代码的下载地址: https://github.com/frombeijingwithlove/dlcv_f ...

  5. TensorFlow深度学习基础与应用实战高清视频教程

    TensorFlow深度学习基础与应用实战高清视频教程,适合Python C++ C#视觉应用开发者,基于TensorFlow深度学习框架,讲解TensorFlow基础.图像分类.目标检测训练与测试以 ...

  6. 深度学习入门者的Python快速教程 - 基础篇

      5.1 Python简介 本章将介绍Python的最基本语法,以及一些和深度学习还有计算机视觉最相关的基本使用. 5.1.1 Python简史 Python是一门解释型的高级编程语言,特点是简单明 ...

  7. (通用)深度学习环境搭建:tensorflow安装教程及常见错误解决

    区别于其他入门教程的"手把手式",本文更强调"因"而非"果".我之所以加上"通用"字样,是因为在你了解了这个开发环境之后 ...

  8. 给深度学习入门者的Python快速教程 - 基础篇

    实在搞不定博客园的排版,排版更佳的版本在: https://zhuanlan.zhihu.com/p/24162430 Life is short, you need Python 人生苦短,我用Py ...

  9. 给深度学习入门者的Python快速教程 - 基础篇(转)

    原文:https://zhuanlan.zhihu.com/p/24162430 5.1 Python简介 本章将介绍Python的最基本语法,以及一些和深度学习还有计算机视觉最相关的基本使用. 5. ...

  10. [深度学习] ncnn安装和调用基础教程

    目录 1 介绍 2 Ubuntu 18下ncnn安装和使用 2.1 Ubuntu 18下ncnn编译安装 2.2 Ubuntu 18下ncnn使用 3 Windows 10下ncnn安装和使用 3.1 ...

随机推荐

  1. Windows下绑定线程到指定的CPU核心

    在某些场景下,需要把程序绑定到指定CPU核心提高执行效率.通过微软官方文档查询到Windows提供了两个Win32函数:SetThreadAffinityMask和SetProcessAffinity ...

  2. 如何禁用IntelliJ IDEA的LightEdit模式

    更新pycharm之后发现有了个新功能,默认打开文件的时候会单独打开一个窗口,以文本编辑的模式打开,而不是用项目模式.这种打开方式被称为LightEdit Mode.效果如下, 可以注意到窗口很简洁, ...

  3. 【爬虫实战】用python爬小红书任意话题的笔记,以#杭州亚运会#为例

    目录 一.爬取目标 二.爬虫代码讲解 2.1 分析过程 2.2 爬虫代码 三.演示视频 四.获取完整代码 一.爬取目标 您好!我是@马哥python说,一名10年程序猿. 最近的亚运会大家都看了吗.除 ...

  4. SpringBoot 二维码生成

    一.基于Google开发工具包ZXing生成二维码 1.引入需要的依赖 <!-- zxing生成二维码 --> <dependency> <groupId>com. ...

  5. fastposter v2.8.3 发布 电商海报生成器

    fastposter v2.8.3 发布 电商海报生成器 fastposter海报生成器,电商海报编辑器,电商海报设计器,fast快速生成海报 海报制作 海报开发.贰维海报,图片海报,分享海报贰维码推 ...

  6. 基于ADB Shell 实现的 Android TV、电视盒子万能遥控器 — ADB Remote ATV

    ADB Remote ATV Android TV 的遥控器,基于 ADB Shell 命令 ADB Remote ATV 是一个 Android TV 的遥控器,基于 ADB Shell 命令,泛用 ...

  7. 胃食管反流之 SAP分析( in the Ohmega software)

    原文:https://note.youdao.com/s/GED6wise SAP analysis in the Ohmega software ohmega software 关于胃食管反流疾病 ...

  8. C# 数独 解法

    按照人的思维来做的. MyPos 就是 数组 行 列值. public class CalcShuDu { public int[][] IArrSd; public int IBlankCount; ...

  9. cmder右键打开方式

    第一步: 新打开一个cmder窗口 第二步: 输入: Cmder.exe /register user 或 Cmder.exe /register all 第三步: 回车执行命令

  10. FFmpeg下载编译、代码结构以及编译系统

    从这里开始,就要踏上学习FFmpeg的旅程了,使用的FFmpeg版本5.0.1 1.ubuntu下,如何下载并编译FFmpeg源码 打开FFmpeg官网 Download FFmpeg,我们可以通过g ...