0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

深度学习Pytorch翻车记录:单卡改多卡踩坑记

深度学习自然语言处理 来源:深度学习自然语言处理 作者:哟林小平 2021-01-18 17:06 次阅读

先说明一下背景,目前正在魔改以下这篇论文的代码:

https://github.com/QipengGuo/GraphWriter-DGLgithub.com

由于每次完成实验需要5个小时(baseline),自己的模型需要更久(2倍),非常不利于调参和发现问题,所以开始尝试使用多卡加速。

torch.nn.DataParallel ==> 简称 DP

torch.nn.parallel.DistributedDataParallel ==> 简称DDP

一开始采用dp试图加速,结果因为dgl的实现(每个batch的点都会打包进一个batch,从而不可分割),而torch.nn.DataParallel的实现是把一个batch切分成更小,再加上他的加速性能也不如ddp,所以我开始尝试魔改成ddp。

另外,作者在实现Sampler的时候是继承了torch.utils.data.Sampler这个类的,目的在于agenda数据集的文本长度严重不均衡,如下:

68176276-58b2-11eb-8b86-12bb97331649.jpg

为了让模型更快train完,把长度相近的文本打包成一个batch(温馨提醒,torchtext也有相关的类 bucketiterator[1],大概形式如下:

class BucketSampler(torch.utils.data.Sampler):
    def __init__(self, data_source, batch_size=32):
        self.data_source = data_source
        self.batch_size = batch_size 

    def __iter__(self):
        idxs, lens, batch, middle_batch_size, long_batch_size = basesampler(self.data_source , self.batch_size)
        for idx in idxs:
            batch.append(idx)
            mlen = max([0]+[lens[x] for x in batch])
            #if (mlen<100 and len(batch) == 32) or (mlen>100 and mlen<220 and len(batch) >= 24) or (mlen>220 and len(batch)>=8) or len(batch)==32:
            if (mlen<100 and len(batch) == self.batch_size) or (mlen>100 and mlen<220 and len(batch) >= middle_batch_size) or (mlen>220 and len(batch)>=long_batch_size) or len(batch)==self.batch_size:
                yield batch
                batch = []
        if len(batch) > 0:
            yield batch

    def __len__(self):
        return (len(self.data_source)+self.batch_size-1)//self.batch_size

这是背景。

写bug第一步:继承DistributedSampler的漏洞百出

我一开始理想当然的把作者的sampler源码crtl-cv下来,唯独只改动了这里:

class DDPBaseBucketSampler(torch.utils.data.distributed.DistributedSampler):

随后就发现了几个问题:

  • dataloader不会发包;
  • dataloader给每个进程发的是完整的数据,按武德来说,应该是1/n的数据,n为你设置的gpu数量;

然后我就开始看起了源码[2],很快啊:

 def __iter__(self) -> Iterator[T_co]:
        if self.shuffle:
            # deterministically shuffle based on epoch and seed
            g = torch.Generator()
            g.manual_seed(self.seed + self.epoch)
            indices = torch.randperm(len(self.dataset), generator=g).tolist()  # type: ignore
        else:
            indices = list(range(len(self.dataset)))  # type: ignore

        if not self.drop_last:
            # add extra samples to make it evenly divisible
            padding_size = self.total_size - len(indices)
            if padding_size <= len(indices):
                indices += indices[:padding_size]
            else:
                indices += (indices * math.ceil(padding_size / len(indices)))[:padding_size]
        else:
            # remove tail of data to make it evenly divisible.
            indices = indices[:self.total_size]
        assert len(indices) == self.total_size

        # subsample
        indices = indices[self.rankself.num_replicas] # 这一步保证每个进程拿到的数据不同
        assert len(indices) == self.num_samples

        return iter(indices)

这里最关键的问题是是什么呢?首先在torch.utils.data.distributed.DistributedSampler里面,数据集的变量叫self.dataset而不是data_source;其次和torch.utils.data.Sampler要求你_重写__iter__函数不同:

def __iter__(self) -> Iterator[T_co]:
        raise NotImplementedError

DistributedSampler这个父类里有部分实现,如果你没有考虑到这部分,就自然会出现每个进程拿到的数据都是all的情况。

于是我重写了我的DDPBaseBucketSampler类:

def basesampler(lens, indices, batch_size):
    # the magic number comes from the author's code
    t1 = []
    t2 = []
    t3 = []
    for i, l in enumerate(lens):
        if (l<100):
            t1.append(indices[i])
        elif (l>100 and l<220):
            t2.append(indices[i])
        else:
            t3.append(indices[i])
    datas = [t1,t2,t3]
    random.shuffle(datas)
    idxs = sum(datas, [])
    batch = []

    #为了保证不爆卡,我们给不同长度的数据上保护锁
    middle_batch_size = min(int(batch_size * 0.75) , 32)
    long_batch_size = min(int(batch_size * 0.5) , 24)

    return idxs, batch, middle_batch_size, long_batch_size

class DDPBaseBucketSampler(torch.utils.data.distributed.DistributedSampler):
    '''
    这里要注意和单GPU的sampler类同步
    '''
    def __init__(self, dataset, num_replicas, rank, shuffle=True, batch_size=32):
        super(DDPBaseBucketSampler, self).__init__(dataset, num_replicas, rank, shuffle)
        self.batch_size = batch_size

    def __iter__(self):
        # deterministically shuffle based on epoch
        g = torch.Generator()
        g.manual_seed(self.epoch)
        #print('here is pytorch code and you can delete it in the /home/lzk/anaconda3/lib/python3.7/site-packages/torch/utils/data')
        if self.shuffle:
            indices = torch.randperm(len(self.dataset), generator=g).tolist()
        else:
            indices = list(range(len(self.dataset)))
        # add extra samples to make it evenly divisible
        indices += indices[:(self.total_size - len(indices))]
        assert len(indices) == self.total_size

        indices = indices[self.rankself.num_replicas]
        assert len(indices) == self.num_samples

        # 然后我也要拿到每个数据的长度 (每个rank不同)
        lens = torch.Tensor([len(x) for x in self.dataset])

        idxs, batch, middle_batch_size, long_batch_size = basesampler(lens[indices], indices, self.batch_size)
        
        for idx in idxs:
            batch.append(idx)
            mlen = max([0]+[lens[x] for x in batch])
            #if (mlen<100 and len(batch) == 32) or (mlen>100 and mlen<220 and len(batch) >= 24) or (mlen>220 and len(batch)>=8) or len(batch)==32:
            if (mlen<100 and len(batch) == self.batch_size) or (mlen>100 and mlen<220 and len(batch) >= middle_batch_size) or (mlen>220 and len(batch)>=long_batch_size) or len(batch)==self.batch_size:
                yield batch
                batch = []
        # print('应该出现2次如果是2个进程的话')
        if len(batch) > 0:
            yield batch

    def __len__(self):
        return (len(self.dataset)+self.batch_size-1)//self.batch_size

后面每个进程终于可以跑属于自己的数据了(1/n,n=进程数量=GPU数量,单机)

紧接着问题又来了,我发现训练过程正常结束后,主进程无法退出mp.spawn()函数。

写bug第二步,master进程无法正常结束

number workers ddp pytorch下无法正常结束。具体表现为,mp.spawn传递的函数参数可以顺利运行完,但是master进程一直占着卡,不退出。一开始我怀疑是sampler函数的分发batch的机制导致的,什么意思呢?就是由于每个进程拿到的数据不一样,各自进程执行sampler类的时候,由于我规定了长度接近的文本打包在一起,所以可能master进程有一百个iter,slave只有80个,然后我马上试了一下,很快啊:

68655972-58b2-11eb-8b86-12bb97331649.jpg

▲DDPBucketSampler(torch.utils.data.distributed.DistributedSampler)类迭代函数__iter__

6897a242-58b2-11eb-8b86-12bb97331649.jpg

▲都能够正常打印,证明__iter__函数没有问题

发现只有细微的差别,并且,程序最后都越过了这些print,应该不会是batch数量不一致导致的问题。(顺便指的一提的是,sampler在很早的时候就把batch打包好了)

加了摧毁进程,也于事无补

if args.is_ddp:
     dist.destroy_process_group()
     print('rank destroy_process_group: ' , rank)

然后只能点击强制退出

File "train.py", line 322, in 
    main(args.gpu, args)
  File "/home/lzk/anaconda3/lib/python3.7/site-packages/torch/multiprocessing/spawn.py", line 171, in spawn
    while not spawn_context.join():
  File "/home/lzk/anaconda3/lib/python3.7/site-packages/torch/multiprocessing/spawn.py", line 77, in join
    timeout=timeout,
  File "/home/lzk/anaconda3/lib/python3.7/multiprocessing/connection.py", line 920, in wait
    ready = selector.select(timeout)
  File "/home/lzk/anaconda3/lib/python3.7/selectors.py", line 415, in select
    fd_event_list = self._selector.poll(timeout)
TypeError: keyboard_interrupt_handler() takes 1 positional argument but 2 were given
^CError in atexit._run_exitfuncs:
Traceback (most recent call last):
  File "/home/lzk/anaconda3/lib/python3.7/multiprocessing/popen_fork.py", line 28, in poll
    pid, sts = os.waitpid(self.pid, flag)
TypeError: keyboard_interrupt_handler() takes 1 positional argument but 2 were given

代码参考:基于Python初探Linux下的僵尸进程和孤儿进程(三)[3]、Multiprocessing in python blocked[4]

很显然是pytorch master进程产生死锁了,变成了僵尸进程。

再探究,发现当我把dataloader的number workers设为0的时候,程序可以正常结束。经过我的注释大法后我发现,哪怕我把for _i , batch in enumerate(dataloader)内的代码全部注释改为pass,程序还是会出现master无法正常结束的情况。所以问题锁定在dataloader身上。参考:nero:PyTorch DataLoader初探[5]

另外一种想法是,mp.spawn出现了问题。使用此方式启动的进程,只会执行和 target 参数或者 run() 方法相关的代码。Windows 平台只能使用此方法,事实上该平台默认使用的也是该启动方式。相比其他两种方式,此方式启动进程的效率最低。参考:Python设置进程启动的3种方式[6]

现在试一下,绕开mp.spawn函数,用shell脚本实现ddp,能不能不报错:

python -m torch.distributed.launch --nproc_per_node=2 --nnodes=1 --node_rank=0 --master_addr="192.168.1.201" --master_port=23456 我的文件.py

参数解释:

  • nnodes:因为是单机多卡,所以设为1,显然node_rank 只能是0了
  • local_rank:进程在运行的时候,会利用args插入local_rank这个参数标识进程序号

一番改动后,发现问题有所好转,最直观的感受是速度快了非常多!!现在我没有父进程的问题了,但还是在运行完所有的程序后,无法正常结束:

68c16578-58b2-11eb-8b86-12bb97331649.jpg

此时我的代码运行到:

692dbfc0-58b2-11eb-8b86-12bb97331649.jpg

上面的代码是main函数,2个进程(master,salve)都可以越过barrier,其中slave顺利结束,但是master却迟迟不见踪影:

6968adba-58b2-11eb-8b86-12bb97331649.jpg

这个时候ctrl+c终止,发现:

69a049e6-58b2-11eb-8b86-12bb97331649.jpg

顺着报错路径去torch/distributed/launch.py, line 239找代码:

def main():
    args = parse_args()

    # world size in terms of number of processes
    dist_world_size = args.nproc_per_node * args.nnodes

    # set PyTorch distributed related environmental variables
    current_env = os.environ.copy()
    current_env["MASTER_ADDR"] = args.master_addr
    current_env["MASTER_PORT"] = str(args.master_port)
    current_env["WORLD_SIZE"] = str(dist_world_size)

    processes = []

    if 'OMP_NUM_THREADS' not in os.environ and args.nproc_per_node > 1:
        current_env["OMP_NUM_THREADS"] = str(1)
        print("*****************************************
"
              "Setting OMP_NUM_THREADS environment variable for each process "
              "to be {} in default, to avoid your system being overloaded, "
              "please further tune the variable for optimal performance in "
              "your application as needed. 
"
              "*****************************************".format(current_env["OMP_NUM_THREADS"]))

    for local_rank in range(0, args.nproc_per_node):
        # each process's rank
        dist_rank = args.nproc_per_node * args.node_rank + local_rank
        current_env["RANK"] = str(dist_rank)
        current_env["LOCAL_RANK"] = str(local_rank)

        # spawn the processes
        if args.use_env:
            cmd = [sys.executable, "-u",
                   args.training_script] + args.training_script_args
        else:
            cmd = [sys.executable,
                   "-u",
                   args.training_script,
                   "--local_rank={}".format(local_rank)] + args.training_script_args

        process = subprocess.Popen(cmd, env=current_env)
        processes.append(process)

    for process in processes:
        process.wait() # 等待运行结束
        if process.returncode != 0:
            raise subprocess.CalledProcessError(returncode=process.returncode,
                                                cmd=cmd)

可恶,master和dataloader到底有什么关系哇。。

这个问题终于在昨天(2020/12/22)被解决了,说来也好笑,左手是graphwriter的ddp实现,无法正常退出,右手是minst的ddp最小例程,可以正常退出,于是我开始了删减大法。替换了数据集,model,然后让dataloader空转,都没有发现问题,最后一步步逼近,知道我把自己的代码这一行注释掉以后,终于可以正常结束了:

def main(args):
    ############################################################
    print('local_rank : ' , args.local_rank )
    if args.is_ddp:
        dist.init_process_group(
        backend='nccl',
       init_method='env://',
        world_size=args.world_size,
        rank=args.local_rank
        )
    ############################################################
    # torch.multiprocessing.set_sharing_strategy('file_system')  万恶之源

    os.environ["CUDA_VISIBLE_DEVICES"] = os.environ["CUDA_VISIBLE_DEVICES"].split(',')[args.local_rank]
    args.device = torch.device(0) 
    ...

为什么我当时会加上这句话呢?因为当时在调试number worker的时候(当时年轻,以为越大越好,所以设置成了number workers = cpu.count()),发现系统报错,说超出了打开文件的最大数量限制。在torch.multiprocessing的设定里,共享策略(参考pytorch中文文档[7])默认是File descriptor,此策略将使用文件描述符作为共享内存句柄。当存储被移动到共享内存中,一个由shm_open获得的文件描述符被缓存。当时,文档还提到:

如果你的系统对打开的文件描述符数量有限制,并且无法提高,你应该使用file_system策略。

所以我换成了torch.multiprocessing.set_sharing_strategy('file_system'),但是却忽略文档里的共享内存泄露警告。显然,或许这不是严重的问题,文档里提到:

69eb6b92-58b2-11eb-8b86-12bb97331649.jpg

也有可能我所说的master进程就是这个torch_shm_manager,因为destory进程组始终无法结束0号进程:

6a35e19a-58b2-11eb-8b86-12bb97331649.jpg

这个BUG结束了,真开心,期待下一个BUG快快到来。

责任编辑:xj

原文标题:Pytorch翻车记录:单卡改多卡踩坑记!

文章出处:【微信公众号:深度学习自然语言处理】欢迎添加关注!文章转载请注明出处。


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 机器学习
    +关注

    关注

    66

    文章

    8347

    浏览量

    132290
  • 深度学习
    +关注

    关注

    73

    文章

    5463

    浏览量

    120876
  • pytorch
    +关注

    关注

    2

    文章

    802

    浏览量

    13111

原文标题:Pytorch翻车记录:单卡改多卡踩坑记!

文章出处:【微信号:zenRRan,微信公众号:深度学习自然语言处理】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    如何使用 PyTorch 进行强化学习

    强化学习(Reinforcement Learning, RL)是一种机器学习方法,它通过与环境的交互来学习如何做出决策,以最大化累积奖励。PyTorch 是一个流行的开源机器
    的头像 发表于 11-05 17:34 187次阅读

    Pytorch深度学习训练的方法

    掌握这 17 种方法,用最省力的方式,加速你的 Pytorch 深度学习训练。
    的头像 发表于 10-28 14:05 115次阅读
    <b class='flag-5'>Pytorch</b><b class='flag-5'>深度</b><b class='flag-5'>学习</b>训练的方法

    pytorch和python的关系是什么

    PyTorch已经成为了一个非常受欢迎的框架。本文将介绍PyTorch和Python之间的关系,以及它们在深度学习领域的应用。 Python简介 Python是一种高级、解释型、通用
    的头像 发表于 08-01 15:27 1489次阅读

    PyTorch深度学习开发环境搭建指南

    PyTorch作为一种流行的深度学习框架,其开发环境的搭建对于深度学习研究者和开发者来说至关重要。在Windows操作系统上搭建
    的头像 发表于 07-16 18:29 693次阅读

    pytorch中有神经网络模型吗

    当然,PyTorch是一个广泛使用的深度学习框架,它提供了许多预训练的神经网络模型。 PyTorch中的神经网络模型 1. 引言 深度
    的头像 发表于 07-11 09:59 597次阅读

    PyTorch的介绍与使用案例

    PyTorch是一个基于Python的开源机器学习库,它主要面向深度学习和科学计算领域。PyTorch由Meta Platforms(原Fa
    的头像 发表于 07-10 14:19 332次阅读

    如何使用PyTorch建立网络模型

    PyTorch是一个基于Python的开源机器学习库,因其易用性、灵活性和强大的动态图特性,在深度学习领域得到了广泛应用。本文将从PyTorch
    的头像 发表于 07-02 14:08 336次阅读

    TensorFlow与PyTorch深度学习框架的比较与选择

    深度学习作为人工智能领域的一个重要分支,在过去十年中取得了显著的进展。在构建和训练深度学习模型的过程中,深度
    的头像 发表于 07-02 14:04 835次阅读

    新手小白怎么学GPU云服务器跑深度学习?

    新手小白想用GPU云服务器跑深度学习应该怎么做? 用个人主机通常pytorch可以跑但是LexNet,AlexNet可能就直接就跑不动,如何实现更经济便捷的实现GPU云服务器深度
    发表于 06-11 17:09

    痛苦“电池电压侦测电路”,含泪总结设计要点

    和大家分享这个电路的设计要点,以及当时的设计失误,帮助大家积累经验,以后不要这种。 设计要点一:设定分压电阻的大小 这种便携式掌上阅读器,当然是内置锂电池的:
    的头像 发表于 04-07 14:31 2444次阅读
    痛苦<b class='flag-5'>踩</b><b class='flag-5'>坑</b>“电池电压侦测电路”,含泪总结设计要点

    网关相比单卡网关有哪些优势?

    网关相比单卡网关有哪些优势? 双网关是指具备同时连接两个SIM的网关设备,而单卡网关则是只支持一张SIM
    的头像 发表于 12-19 14:26 569次阅读

    反相输入放大器的,你过没有?

    反相输入放大器的,你过没有?
    的头像 发表于 12-06 15:35 565次阅读
    反相输入放大器的<b class='flag-5'>坑</b>,你<b class='flag-5'>踩</b>过没有?

    R128点屏笔记

    本文所整理的R128点屏笔记均是在参考以下应用开发案例进行屏幕适配时遇到的实际开发问题,涵盖了屏幕花屏、屏幕白屏、LVGL显示颜色错误等常见问题的解决方法。
    的头像 发表于 12-03 10:17 1193次阅读
    R128点屏<b class='flag-5'>踩</b><b class='flag-5'>坑</b>笔记

    关于图像传感器图像质量的四大误区!你过几个

    关于图像传感器图像质量的四大误区!你过几个
    的头像 发表于 11-27 16:56 407次阅读
    关于图像传感器图像质量的四大误区!你<b class='flag-5'>踩</b>过几个<b class='flag-5'>坑</b>?

    推挽电路的,你过没?

    推挽电路的,你过没?
    的头像 发表于 11-24 16:25 1042次阅读
    推挽电路的<b class='flag-5'>坑</b>,你<b class='flag-5'>踩</b>过没?