PyTorch 可以通过 torch.nn.DataParallel 直接切分数据并行在单机多卡上,实践证明这个接口并行力度并不尽如人意,主要问题在于数据在 master 上处理然后下发到其他 slaver 上训练,而且由于 GIL 的存在只有计算是并行的。torch.distributed 提供了更好的接口和并行方式,搭配多进程接口 torch.multiprocessing 可以提供更加高效的并行训练。
GIL含义解释
多进程
- 我们都知道由于 GIL 的存在, python 要想真正的并行必须使用多进程,IO频繁可以勉强使用多线程。torch.nn.DataParallel 全局只有一个进程,受到了 GIL 的限制,所以肯定会拖累并行的力度。
- python 自带的 multiprocessing 是多进程常用的实现,但是有一个巨大的问题,不支持 CUDA,所以我们使用GPU训练的时候不能用这个包,需要使用 PyTorch 提供的 torch.multiprocessing。它提供了和 multiprocessing 几乎一样的接口,所以用起来也比较方便。
torch.distributed 可以通过 torch.distributed.launch 启动多卡训练,也可以使用 torch.multiprocessing 手动提交多进程并行。
我们分别介绍torch.distributed.launch 和 torch.multiprocessing
(1)torch.distributed.launch
(2)torch.multiprocessing
分布式训练
torch.distributed 提供了和通用分布式系统常见的类似概念。
In the single-machine synchronous case, torch.distributed or the torch.nn.parallel.DistributedDataParallel() wrapper may still have advantages over other approaches to data-parallelism, including torch.nn.DataParallel():也就是说orch.distributed 和 the torch.nn.parallel.DistributedDataParallel() wrapper更有效率,具体原因见:
DISTRIBUTED COMMUNICATION PACKAGE - TORCH.DISTRIBUTED
1.初始化
初始化操作一般在程序刚开始的时候进行
在调用任何其他方法之前,需要使用torch.distributed.init_process_group()函数对包进行初始化。这会阻塞所有进程,直到所有进程都已连接。
torch.distributed.init_process_group(backend, init_method=None, timeout=datetime.timedelta(0, 1800), world_size=-1, rank=-1, store=None, group_name='')
两种初始化方式
1.Specify store, rank, and world_size explicitly.
2.Specify init_method (a URL string) which indicates where/how to discover peers. Optionally specify rank and world_size, or encode all required parameters in the URL and omit them.
If neither is specified, init_method is assumed to be “env://”.
backend (str or Backend) – 。根据构建时配置,有效值包括mpi、gloo和nccl。该字段应该以小写字符串的形式给出(例如,“gloo”),它也可以通过后端属性访问(例如,back . gloo)。如果在每台机器上使用nccl后端多个进程,那么每个进程必须独占访问它使用的每个GPU,因为在进程之间共享GPU可能会导致死锁。
根据官网的介绍, 如果是使用cpu的分布式计算, 建议使用gloo, 因为表中可以看到 gloo对cpu的支持是最好的, 然后如果使用gpu进行分布式计算, 建议使用nccl, 实际测试中我也感觉到, 当使用gpu的时候, nccl的效率是高于gloo的. 根据博客和官网的态度, 好像都不怎么推荐在多gpu的时候使用mpi
对于后端选择好了之后, 我们需要设置一下网络接口, 因为多个主机之间肯定是使用网络进行交换, 那肯定就涉及到ip之类的, 对于nccl和gloo一般会自己寻找网络接口, 但是某些时候, 比如我测试用的服务器, 不知道是系统有点古老, 还是网卡比较多, 需要自己手动设置. 设置的方法也比较简单, 在Python的代码中, 使用下面的代码进行设置就行:
import os
# 以下二选一, 第一个是使用gloo后端需要设置的, 第二个是使用nccl需要设置的
os.environ['GLOO_SOCKET_IFNAME'] = 'eth0'
os.environ['NCCL_SOCKET_IFNAME'] = 'eth0'
我们怎么知道自己的网络接口呢, 打开命令行, 然后输入ifconfig, 然后找到那个带自己ip地址的就是了, 我见过的一般就是em0, eth0, esp2s0之类的, 当然具体的根据你自己的填写. 如果没装ifconfig, 输入命令会报错, 但是根据报错提示安装一个就行了.
init_method (str, optional) –指定如何初始化进程组的URL。如果没有指定init_method或store,Default是“env://”。与store相互排斥。
初始化init_method的方法有两种, 一种是使用TCP进行初始化, 另外一种是使用共享文件系统进行初始化:
使用TCP初始化:
import torch.distributed as dist
dist.init_process_group(backend, init_method='tcp://10.1.1.20:23456',
rank=rank, world_size=world_size)
注意这里使用格式为tcp://ip:端口号, 首先ip地址是你的主节点的ip地址, 也就是rank参数为0的那个主机的ip地址, 然后再选择一个空闲的端口号, 这样就可以初始化init_method了.
使用共享文件系统初始化
好像看到有些人并不推荐这种方法, 因为这个方法好像比TCP初始化要没法, 搞不好和你硬盘的格式还有关系, 特别是window的硬盘格式和Ubuntu的还不一样, 我没有测试这个方法, 看代码:
import torch.distributed as dist
dist.init_process_group(backend, init_method='file:///mnt/nfs/sharedfile',
rank=rank, world_size=world_size)
根据官网介绍, 要注意提供的共享文件一开始应该是不存在的, 但是这个方法又不会在自己执行结束删除文件, 所以下次再进行初始化的时候, 需要手动删除上次的文件, 所以比较麻烦, 而且官网给了一堆警告, 再次说明了这个方法不如TCP初始化的简单.
world_size (int, optional) -参与作业的进程数。如果指定了store,则必须指定它。
rank (int, optional) – Rank of the current process (it should be a number between 0 and world_size-1). Required if store is specified.
表示进程序号,用于进程间通信,可以用于表示进程的优先级。一般设置 rank=0 的主机为 master 节点。
store (Store, optional) – (int, optional) –Key/value store accessible to all workers, used to exchange connection/address information. Mutually exclusive with init_method.
timeout (timedelta, optional) –默认值为30分钟。这适用于gloo后端。对于nccl,只有当环境变量NCCL_BLOCKING_WAIT或NCCL_ASYNC_ERROR_HANDLING设置为1时,这才适用。当设置了NCCL_BLOCKING_WAIT时,这时进程将阻塞并等待集合在抛出异常之前完成的持续时间。当设置了NCCL_ASYNC_ERROR_HANDLING时,这时集合将异步中止并且进程将崩溃的持续时间。NCCL_BLOCKING_WAIT将向用户提供可以捕获和处理的错误,但由于其阻塞性质,它有性能开销。另一方面,NCCL_ASYNC_ERROR_HANDLING的性能开销很少,但在出现错误时会使进程崩溃。这是因为CUDA执行是异步的,并且继续执行用户代码不再安全,因为失败的异步NCCL操作可能导致后续的CUDA操作在损坏的数据上运行。应该只设置这两个环境变量中的一个。
group_name (str, optional, deprecated) – 组名
local_rank:进程内 GPU 编号,非显式参数,由 torch.distributed.launch 内部指定。比方说, rank=3,local_rank=0 表示第 3 个进程内的第 1 块 GPU。
注意初始化rank和world_size
你需要确保, 不同机器的rank值不同, 但是主机的rank必须为0, 而且使用init_method的ip一定是rank为0的主机, 其次world_size是你的进程数量, 你不能随便设置这个数值,它的值一般设置为每个节点的gpu卡个数乘以节点个数。.
初始化中一些需要注意的地方
首先是代码的统一性, 所有的节点上面的代码, 建议完全一样, 不然有可能会出现一些问题, 其次, 这些初始化的参数强烈建议通过argparse模块(命令行参数的形式)输入, 不建议写死在代码中, 也不建议使用pycharm之类的IDE进行代码的运行, 强烈建议使用命令行直接运行.
多机的启动方式可以是直接传递参数并在代码内部解析环境变量,或者通过torch.distributed.launch来启动,两者在格式上面有一定的区别,总之要保证代码与启动方式对应。
例如使用下面的命令运行代码distributed.py:
在代码上添加如下:
torch.multiprocessing