先说明一下背景,目前正在魔改以下这篇论文的代码:
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数据集的文本长度严重不均衡,如下:
为了让模型更快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 (mlen100 and mlen= 24) or (mlen>220 and len(batch)>=8) or len(batch)==32: if (mlen100 and mlen= 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 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 (l100 and l220 and len(batch)>=8) or len(batch)==32: if (mlen100 and mlen= 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个,然后我马上试了一下,很快啊:
▲ddpbucketsampler(torch.utils.data.distributed.distributedsampler)类迭代函数__iter__
▲都能够正常打印,证明__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这个参数标识进程序号 一番改动后,发现问题有所好转,最直观的感受是速度快了非常多!!现在我没有父进程的问题了,但还是在运行完所有的程序后,无法正常结束:
此时我的代码运行到:
上面的代码是main函数,2个进程(master,salve)都可以越过barrier,其中slave顺利结束,但是master却迟迟不见踪影:
这个时候ctrl+c终止,发现:
顺着报错路径去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'),但是却忽略文档里的共享内存泄露警告。显然,或许这不是严重的问题,文档里提到:
也有可能我所说的master进程就是这个torch_shm_manager,因为destory进程组始终无法结束0号进程:
这个bug结束了,真开心,期待下一个bug快快到来。
原文标题:pytorch翻车记录:单卡改多卡踩坑记!
文章出处:【微信公众号:深度学习自然语言处理】欢迎添加关注!文章转载请注明出处。
为什么对于特斯拉、Uber来说,无人驾驶出租车市场之争关乎生死存亡
高通公司发布Gobi连接技术的最新路线图
爱立信携手沃达丰将扩大5G网络在爱尔兰的覆盖范围
国产AI芯片进展几何?国产AI芯片之争才刚刚开始
基于USB接口的经典制作案例
深度学习Pytorch翻车记录:单卡改多卡踩坑记
一文带你看懂CCD传感器
首创!蓝牙5.2技术在骨传导耳机的首次成功应用,南卡新品发布
光纤环形器的应用有几种?有什么作用?
场强突变检测器电路原理图
兆声功率、温度和时间的相互影响
慕尼黑上海光博会:高德红外自研的晶圆级红外模组是此次展出的重头戏
三星s8发布会撞脸小米mix,意外还是巧合?
电感分为哪几种?有什么作用?芯片引脚的电感值怎样取值?
微软Pluton安全处理器发布,提高微软防范物理攻击的能力
索尼PS4好强 销量碾压微软Xbox
嵌入式系统中的USB控制器与框架结构
Verilog HDL的程序结构_veriloghdl的关键字
电动机三角形接法与星形接法的区别
OPPO与诺基亚专利纠纷案的全球费率判决