0x00 摘要

本文以 PyTorch 官方文档 pytorch.org/tutorials/i… 为根底,对怎么编写分布式进行了介绍,而且加上了自己的了解。

PyTorch 的分布式包(即 torch.distributed)使研究人员和从业人员能够轻松地跨进程和跨机器集群并行核算。它运用音讯传递语义来答应每个进程与任何其他进程通讯数据。与 multiprocessing ( torch.multiprocessing) 包相反,进程能够运用不同的通讯后端,而且不限于在同一台机器上履行。

在这个简短的教程中,咱们将介绍 PyTorch 的分布式包。咱们将看到怎么设置分布式,运用不同的通讯策略,并了解包的一些内部结构。

本系列其他文章如下:


深度学习利器之主动微分(1)

深度学习利器之主动微分(2)

深度学习利器之主动微分(3) — 示例解读

[源码解析]PyTorch怎么完结前向传达(1) — 根底类(上)

[源码解析]PyTorch怎么完结前向传达(2) — 根底类(下)

[源码解析] PyTorch怎么完结前向传达(3) — 详细完结

[源码解析] Pytorch 怎么完结后向传达 (1)—- 调用引擎

[源码解析] Pytorch 怎么完结后向传达 (2)—- 引擎静态结构

[源码解析] Pytorch 怎么完结后向传达 (3)—- 引擎动态逻辑

[源码解析] PyTorch 怎么完结后向传达 (4)—- 详细算法

[源码解析] PyTorch 分布式(1)——前史和概述

[源码解析] PyTorch 怎么运用GPU

[源码解析] PyTorch 分布式(2) —– DataParallel(上)

[源码解析] PyTorch 分布式(3) —– DataParallel(下)

0x01 基本概念

咱们首要介绍一些 torch.distributed 中的要害概念,这些概念在编写程序时至关重要。

  • Node – 物理实例或容器。

  • Worker – 分布练习环境中的worker。

  • Group(进程组):咱们一切进程的子集,用于团体通讯等。

    • 默许情况下,只要一个组,一个 job 即为一个组,也即一个 world。
    • 当需求进行更加精细的通讯时,能够经过 new_group 接口,运用 world 的子集来创立新组。
  • Backend(后端):进程通讯库。PyTorch 支撑NCCL,GLOO,MPI。

  • World_size :进程组中的进程数,能够认为是大局进程个数。

  • Rank :分配给分布式进程组中每个进程的唯一标识符。

    • 从 0 到 world_size 的连续整数,能够了解为进程序号,用于进程间通讯。
    • rank = 0 的主机为 master 节点。
    • rank 的调集能够认为是一个大局GPU资源列表。
  • local rank:进程内的 GPU 编号,非显式参数,这个一般由 torch.distributed.launch 内部指定。例如, rank = 3,local_rank = 0 表明第 3 个进程内的第 1 块 GPU。

0x02 设计思路

分布式练习最首要的问题便是:worker 之间怎么通讯。为了处理通讯问题,PyTorch 引入了几个概念,咱们先剖析通讯的需求,然后看看 PyTorch 怎么经过这几个概念来满足需求的。

2.1 通讯需求

咱们总结一下分布式练习的详细需求:

  • worker 之间怎么互相发现?
  • worker 之间怎么进行点对点通讯?
  • worker 之间怎么做调集通讯?
  • 怎么把练习进程和调集通讯联系起来?

接下来围绕这几个问题和文档内容进行剖析。

2.2 概念

针对通讯需求,PyTorch 供给的几个概念是:进程组,后端,初始化,Store。

  • 进程组 :DDP是真实的分布式练习,能够运用多台机器来组成一次并行运算的使命。为了能够让 DDP 的各个worker之间通讯,PyTorch 设置了进程组这个概念。组是咱们一切进程的子集。

    • 看其本质,便是进行通讯的进程们。
    • 从代码来看,给每一个练习的process 树立一个 通讯thread,在后台做通讯。比方关于 ProcessGroupMPI,在通讯线程增加了一个 queue,做 buffer 和 异步处理。
  • 后端 :后端是一个逻辑上的概念。

    • 本质上后端是一种IPC通讯机制。PyTorch 既然能够在不同的进程间进行通讯,那必然是依赖于一些IPC的通讯机制,这些通讯机制一般是由PyTorch之外的三方完结的,比方后端运用 ProcessGroupMPI 还是 ProcessGroupGloo 。
    • 后端 答应进程经过同享它们的位置来互相通讯。关于用户来说,便是采用哪种方法来进行调集通讯,从代码上看,便是走什么流程(一系列流程)…..
  • 初始化 : 虽然有了后端和进程组的概念,可是怎么让 worker 在树立进程组之前发现互相? 这就需求一种初始化办法来告知大家传递一个信息:怎么联系到其它机器上的进程。现在DDP模块支撑3种初始化办法。

  • Store : 分布式包(distributed package)有一个分布式键值存储服务,这个服务在组中的进程之间同享信息以及初始化分布式包 (经过显式创立存储来作为init_method的替代)。

  • 初始化 vs Store

    • 当 MPI 为后端时分, init_method 没有用途。
    • 在非 MPI 后端时分,假如没有 store 参数,则运用 init_method 构建一个store,所以最终还是落到了 store 之上。

关于这些概念,咱们用下图来看看 DDP 是怎么运用这些概念。

假定 DDP 包括两个worker 做练习,其间每个 worker 会:

  • 在 Main Thread 之中做练习,在 Reducer 之中做 allreduce,详细是往 ProcessGroupMPI 的 workerThread_ 发送指令。
  • workerThread_ 会调用 MPI_Allreduce 进行 调集通讯,运用的便是 MPI 后端。

[源码解析] PyTorch 分布式(4)------分布式应用基础概念

0x03 设置

首要,咱们需求能够同时运转多个进程。假如您有权访问核算集群,您应该咨询您的本地体系管理员或运用您最喜欢的和谐东西(例如, pdsh、 clustershell或 其他)。本文咱们将在一台机器之上运用以下模板来fork多个进程。

"""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. """
  passdef 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()

上述脚本发生两个进程,每个进程将设置分布式环境,初始化进程组 ( dist.init_process_group),最后履行给定的run 函数。

0x04 点对点通讯

以下是点对点通讯的一个示意图 :发送和接纳。

[源码解析] PyTorch 分布式(4)------分布式应用基础概念

从一个进程到另一个进程的数据传输称为点对点通讯。这些是经过sendrecv函数或isendirecv 来完结的。

"""Blocking point-to-point communication."""
def run(rank, size):
    tensor = torch.zeros(1)
    if rank == 0:
        tensor += 1
        # Send the tensor to process 1
        dist.send(tensor=tensor, dst=1)
    else:
        # Receive tensor from process 0
        dist.recv(tensor=tensor, src=0)
    print('Rank ', rank, ' has data ', tensor[0])

在上面的比如中,两个进程都以零张量开始,然后进程 0 递加张量并将其发送到进程 1,这样它们都以 1.0 完毕。请留意,进程 1 需求分配内存以存储它将接纳的数据。

还要留意send/recv堵塞完结:两个进程都中止,直到通讯完结。另一方面,isendirecv非堵塞的,在非堵塞情况下脚本持续履行,办法返回一个Work目标,咱们能够挑选在其之上进行 wait()

"""Non-blocking point-to-point communication."""
def run(rank, size):
    tensor = torch.zeros(1)
    req = None
    if rank == 0:
        tensor += 1
        # Send the tensor to process 1
        req = dist.isend(tensor=tensor, dst=1)
        print('Rank 0 started sending')
    else:
        # Receive tensor from process 0
        req = dist.irecv(tensor=tensor, src=0)
        print('Rank 1 started receiving')
    req.wait()
    print('Rank ', rank, ' has data ', tensor[0])

运用isendirecv 时,咱们有必要小心运用。由于咱们不知道数据何时会传送到其他进程,因而咱们不应在req.wait()完结之前修正发送的张量或访问接纳的张量。换句话说,

  • dist.isend()之后写入tensor,将导致未定义的行为。

  • dist.irecv() 之后读取tensor,将导致未定义的行为。

可是,在req.wait() 履行之后,咱们能够保证通讯发生了,而且能够保证存储的tensor[0]值为 1.0。

0x05 调集通讯

以下是调集通讯的示意图。

[源码解析] PyTorch 分布式(4)------分布式应用基础概念
Scatter
[源码解析] PyTorch 分布式(4)------分布式应用基础概念
Gather
[源码解析] PyTorch 分布式(4)------分布式应用基础概念
Reduce
[源码解析] PyTorch 分布式(4)------分布式应用基础概念
All-Reduce
[源码解析] PyTorch 分布式(4)------分布式应用基础概念
Broadcast
[源码解析] PyTorch 分布式(4)------分布式应用基础概念
All-Gather

与点对点通讯相反,调集是答应一个组中一切进程进行通讯的模式。组是咱们一切进程的子集。要创立一个组,咱们能够将一个rank列表传递给dist.new_group(group)。默许情况下,调集通讯在一切进程上履行,”一切进程”也称为world。例如,为了取得一切进程中一切张量的总和,咱们能够运用dist.all_reduce(tensor, op, group)

""" All-Reduce example."""
def run(rank, size):
  """ Simple collective communication. """
  group = dist.new_group([0, 1])
  tensor = torch.ones(1)
  dist.all_reduce(tensor, op=dist.ReduceOp.SUM, group=group)
  print('Rank ', rank, ' has data ', tensor[0])

由于咱们想要组中一切张量的总和,因而咱们将其 dist.ReduceOp.SUM用作归约运算符。一般来说,任何可交流的数学运算都能够用作运算符。PyTorch 带有 4 个这样开箱即用的运算符,它们都在元素等级作业:

  • dist.ReduceOp.SUM,
  • dist.ReduceOp.PRODUCT,
  • dist.ReduceOp.MAX,
  • dist.ReduceOp.MIN.

除了 dist.all_reduce(tensor, op, group)之外,现在在 PyTorch 中总共完结了以下调集操作。

  • dist.broadcast(tensor, src, group):从 src仿制tensor到一切其他进程。
  • dist.reduce(tensor, dst, op, group):施加op于一切 tensor,并将成果存储在dst.
  • dist.all_reduce(tensor, op, group): 和reduce操作相同,但成果保存在一切进程中。
  • dist.scatter(tensor, scatter_list, src, group): 仿制张量列表scatter_list[i]中第 ith i^{\text{th}} 个张量到 第ith i^{\text{th}} 个进程。
  • dist.gather(tensor, gather_list, dst, group): 从一切进程仿制tensor dst
  • dist.all_gather(tensor_list, tensor, group): 在一切进程之上,履行从一切进程仿制tensor tensor_list的操作。
  • dist.barrier(group):阻挠组内一切进程,直到每一个进程都现已进入该function。

0x06 分布式练习

**留意:**您能够在此 GitHub 存储库中找到本节的示例脚本。

现在咱们了解了分布式模块的作业原理,让咱们用它写一些有用的东西。咱们的目标是仿制DistributedDataParallel的功用 。当然,这将是一个教育示例,在实践情况下,您应该运用上面链接的经过充分测验和优化的官方版别。

咱们想要完结随机梯度下降的分布式版别。咱们的脚本将让一切进程在他们本地具有的一批数据上核算本地模型的梯度,然后均匀他们的梯度。为了在改变进程数量时保证类似的收敛成果,咱们首要有必要对咱们的数据集进行分区(您也能够运用 tnt.dataset.SplitDataset,而不是下面的代码段)。

""" Dataset partitioning helper """
class Partition(object):
    def __init__(self, data, index):
        self.data = data
        self.index = index
    def __len__(self):
        return len(self.index)
    def __getitem__(self, index):
        data_idx = self.index[index]
        return self.data[data_idx]
class DataPartitioner(object):
    def __init__(self, data, sizes=[0.7, 0.2, 0.1], seed=1234):
        self.data = data
        self.partitions = []
        rng = Random()
        rng.seed(seed)
        data_len = len(data)
        indexes = [x for x in range(0, data_len)]
        rng.shuffle(indexes)
        for frac in sizes:
            part_len = int(frac * data_len)
            self.partitions.append(indexes[0:part_len])
            indexes = indexes[part_len:]
    def use(self, partition):
        return Partition(self.data, self.partitions[partition])

运用上面的代码片段,咱们现在能够运用以下几行简单地对任何数据集进行分区:

""" Partitioning MNIST """
def partition_dataset():
    dataset = datasets.MNIST('./data', train=True, download=True,
                             transform=transforms.Compose([
                                 transforms.ToTensor(),
                                 transforms.Normalize((0.1307,), (0.3081,))
                             ]))
    size = dist.get_world_size()
    bsz = 128 / float(size)
    partition_sizes = [1.0 / size for _ in range(size)]
    partition = DataPartitioner(dataset, partition_sizes)
    partition = partition.use(dist.get_rank())
    train_set = torch.utils.data.DataLoader(partition,
                                         batch_size=bsz,
                                         shuffle=True)
    return train_set, bsz

假定咱们有 2 个副本,那么每个进程具有的train_set 将包括 60000 / 2 = 30000 个样本。咱们还将批量巨细除以副本数,以坚持整体批量巨细为 128。

咱们现在能够编写常见的前向后向优化练习代码,并增加一个函数调用来均匀咱们模型的梯度(以下内容首要受PyTorch MNIST官方示例的启发)。

""" Distributed Synchronous SGD Example """
def run(rank, size):
    torch.manual_seed(1234)
    train_set, bsz = partition_dataset()
    model = Net()
    optimizer = optim.SGD(model.parameters(),
                          lr=0.01, momentum=0.5)
    num_batches = ceil(len(train_set.dataset) / float(bsz))
    for epoch in range(10):
        epoch_loss = 0.0
        for data, target in train_set:
            optimizer.zero_grad()
            output = model(data)
            loss = F.nll_loss(output, target)
            epoch_loss += loss.item()
            loss.backward()
            average_gradients(model)
            optimizer.step()
        print('Rank ', dist.get_rank(), ', epoch ',
              epoch, ': ', epoch_loss / num_batches)

它依然需求完结该average_gradients(model)函数,该函数仅仅接纳一个模型并在整个国际(一切练习进程)中均匀其梯度。

""" Gradient averaging. """
def average_gradients(model):
    size = float(dist.get_world_size())
    for param in model.parameters():
        dist.all_reduce(param.grad.data, op=dist.ReduceOp.SUM)
        param.grad.data /= size

现在,咱们成功完结了分布式同步 SGD,而且能够在大型核算机集群上练习任何模型。

**留意:**虽然最后一句在技能上是正确的,但完结同步 SGD 的出产级完结需求更多技巧。再次运用经过测验和优化的内容。

0x07 Ring-Allreduce

作为额定的应战,假定咱们想要完结 DeepSpeech 的高效 ring allreduce。运用点对点调集能够很容易地完结这一点。

""" Implementation of a ring-reduce with addition. """
def allreduce(send, recv):
   rank = dist.get_rank()
   size = dist.get_world_size()
   send_buff = send.clone()
   recv_buff = send.clone()
   accum = send.clone()
   left = ((rank - 1) + size) % size
   right = (rank + 1) % size
   for i in range(size - 1):
       if i % 2 == 0:
           # Send send_buff
           send_req = dist.isend(send_buff, right)
           dist.recv(recv_buff, left)
           accum[:] += recv_buff[:]
       else:
           # Send recv_buff
           send_req = dist.isend(recv_buff, right)
           dist.recv(send_buff, left)
           accum[:] += send_buff[:]
       send_req.wait()
   recv[:] = accum[:]

在上面的脚本中, allreduce(send, recv) 函数的签名与 PyTorch 中 函数的签名略有不同。它承受一个recv 张量并将一切send张量的总和存储在其间。作为留给读者的练习,咱们的版别与 DeepSpeech 中的版别之间仍有一个差异:它们的完结将梯度张量分红,以便最佳地运用通讯带宽(提示: torch.chunk)。

0x08 高级主题

由于要包括的内容很多,因而本节分为两个小节:

  1. 通讯后端:咱们学习怎么运用 MPI 和 Gloo 进行 GPU-GPU 通讯。
  2. 初始化办法:咱们了解在dist.init_process_group()之中怎么树立初始和谐阶段。

8.1 通讯后端

torch.distributed最优雅的方面之一是它能够在不同的后端之上抽象和构建。如前所述,现在在 PyTorch 中完结了三个后端:Gloo、NCCL 和 MPI。它们每个都有不同的规格和权衡,详细取决于所需的用例。可在此处找到支撑功用的比较表 。

以下信息来自 pytorch.org/docs/stable…

8.1.1 后端品种

torch.distributed支撑三个内置后端,每个后端都有不同的功用。下表显现了哪些函数可用于 CPU / CUDA 张量。

Backend gloo mpi nccl
Device CPU GPU CPU GPU CPU GPU
send ?
recv ?
broadcast ?
all_reduce ?
reduce ?
all_gather ?
gather ?
scatter ?
reduce_scatter
all_to_all ?
barrier ?

PyTorch 分布式包支撑 Linux(稳定)、MacOS(稳定)和 Windows(原型)。关于 Linux,默许情况下,Gloo 和 NCCL 后端包括在分布式 PyTorch 中(仅在运用 CUDA 构建时才支撑NCCL)。MPI是一个可选的后端,只要从源代码构建PyTorch时才干包括它(例如,在装置了MPI的主机上编译PyTorch)。

8.1.2 运用哪个后端?

过去,人们经常会问:“我应该运用哪个后端”?下面是答案:

  • 经历规律
    • 运用 NCCL 后端进行分布式GPU练习
    • 运用 Gloo 后端进行分布式CPU练习。
  • 假如 GPU 主机 具有 InfiniBand 互连
    • 运用 NCCL,由于它是现在唯一支撑 InfiniBand 和 GPUDirect 的后端。
  • 假如 GPU 主机 具有以太网互连
    • 运用 NCCL,由于它现在供给了最好的分布式 GPU 练习功能,特别是关于多进程单节点或多节点分布式练习。假如您在运用 NCCL 时遇到任何问题,请运用 Gloo 作为后备选项。(请留意,关于 GPU练习,Gloo 现在的运转速度比 NCCL 慢。)
  • 具有 InfiniBand 互连的 CPU 主机
    • 假如您的 InfiniBand 已启用 IP over IB,请运用 Gloo,不然,请改用 MPI。咱们方案在行将发布的版别中增加对 Gloo 的 InfiniBand 支撑。
  • 具有以太网互连的 CPU 主机
    • 运用 Gloo,除非您有特定原因一定需求运用 MPI。

8.1.3 Gloo 后端

到现在为止,Gloo 后端 现已得到了广泛运用。它作为开发平台十分便利,由于它包括在预编译的 PyTorch 二进制文件中,而且适用于 Linux(自 0.2 起)和 macOS(自 1.3 起)。它支撑 CPU 上的一切点对点和调集操作,以及 GPU 上的一切调集操作。可是其针对 CUDA 张量调集运算的完结不如 NCCL 后端所优化的那么好。

您肯定现已留意到,假如您的模型运用 GPU ,咱们的分布式 SGD 示例将不起作用。为了运用多个GPU,咱们也做如下修正:

  1. device = torch.device("cuda:{}".format(rank))
  2. model = Net() →\rightarrow model = Net().to(device)
  3. data, target = data.to(device), target.to(device)

经过上述修正,咱们的模型现在能够在两个 GPU 上进行练习,您能够运用.watch nvidia-smi来监控运用情况。

8.1.4 MPI后端

音讯传递接口 (MPI) 是来自高功能核算领域的标准化东西。它答应进行点对点和团体通讯,而且是 torch.distributed 的首要创意来源。现在存在多种 MPI 完结(例如 Open-MPI、 MVAPICH2、Intel MPI),每一种都针对不同目的进行了优化。运用 MPI 后端的优势在于 MPI 在大型核算机集群上的广泛可用性和高度优化。最近的一些 完结还能够运用 CUDA IPC 和 GPU Direct 技能,这样能够防止经过 CPU 进行内存仿制。

不幸的是,PyTorch 的二进制文件不能包括 MPI 完结,咱们有必要手动重新编译它。走运的是,这个进程适当简单,由于在编译时,PyTorch 会自行 寻找可用的 MPI 完结。以下过程经过从源码装置 PyTorch来装置 MPI 后端。

  1. 创立并激活您的 Anaconda 环境,依据 the guide 装置一切继先决需求,但 运转python setup.py install
  2. 挑选并装置您最喜欢的 MPI 完结。请留意,启用 CUDA-aware MPI 或许需求一些额定的过程。在咱们的比如中,咱们将运用没有GPU 支撑的Open-MPI : conda install -c conda-forge openmpi
  3. 现在,转到您克隆的 PyTorch 存储库并履行 .python setup.py install

为了测验咱们新装置的后端,需求进行一些修正。

  1. if __name__ == '__main__': 替换为init_process(0, 0, run, backend='mpi')
  2. 运转 mpirun -n 4 python myscript.py

这些更改的原因是 MPI 需求在生成进程之前创立自己的环境。MPI 还将发生自己的进程并履行初始化办法中描绘的握手操作,从而使init_process_groupranksize 参数变得多余。这实践上十分强大,由于您能够传递额定的参数来mpirun为每个进程定制核算资源(例如每个进程的核心数、将机器手动分配到特定rank等等)。这样做,您应该取得与其他通讯后端相同的熟悉输出。

8.1.5 NCCL后端

该NCCL后端供给了一个优化的,针对对CUDA张量完结的调集操作。假如您仅将 CUDA 张量用于调集操作,请考虑运用此后端以取得最佳功能。NCCL 后端包括在具有 CUDA 支撑的预构建二进制文件中。

NCCL 的全称为 Nvidia 聚合通讯库(NVIDIA Collective Communications Library),是一个能够完结多个 GPU、多个结点间聚合通讯的库,在 PCIe、Nvlink、InfiniBand 上能够完结较高的通讯速度。

NCCL 高度优化和兼容了 MPI,而且能够感知 GPU 的拓扑,促进多 GPU 多节点的加快,最大化 GPU 内的带宽运用率,所以深度学习结构的研究员能够运用 NCCL 的这个优势,在多个结点内或者跨界点间能够充分运用一切可运用的 GPU。

NCCL 对 CPU 和 GPU 均有较好支撑,且 torch.distributed 对其也供给了原生支撑。

关于每台主机均运用多进程的情况,运用 NCCL 能够取得最大化的功能。每个进程内,不许对其运用的 GPUs 具有独占权。若进程之间同享 GPUs 资源,则或许导致 deadlocks。

8.2 初始化办法

为了完结本教程,让咱们谈谈咱们调用的第一个函数 dist.init_process_group(backend, init_method)。咱们将介绍担任每个进程之间初始和谐过程的不同初始化办法。这些办法答应您定义怎么完结这种和谐。根据您的硬件设置,这些办法之一自然应该比其他办法更合适。除了以下部分,您还应该查看官方文档。

环境变量

在本教程中,咱们一直在运用环境变量初始化办法 。此办法将从环境变量中读取配置,答应彻底自定义获取信息的方法。经过在一切机器上设置以下四个环境变量,一切进程都能够正常连接到master(便是 rank 0 进程),获取其他进程的信息,并最终与它们握手。

  • MASTER_PORT:承载等级 0 进程的机器上的一个空闲端口。
  • MASTER_ADDR:承载等级 0 进程的机器上的 IP 地址。
  • WORLD_SIZE: 进程总数,因而master知道要等待多少worker。
  • RANK: 每个进程的rank,所以他们会知道自己是否是master。

同享文件体系

同享文件体系要求一切进程都能够访问同享文件体系,并将经过同享文件和谐它们。这意味着每个进程都将翻开文件,写入其信息,并等待每个人都这样做。之后,一切所需的信息都将可供一切流程运用。为了防止竞争条件,文件体系有必要经过fcntl支撑确定 。

dist.init_process_group(
    init_method='file:///mnt/nfs/sharedfile',
    rank=args.rank,
    world_size=4)

TCP

TCP 初始化方法是经过供给rank 0进程的IP和端口来完结的,在这里,一切worker都能够连接到等级为 0 的进程并交流有关怎么互相联系的信息。

dist.init_process_group(
    init_method='tcp://10.1.1.20:23456',
    rank=args.rank,
    world_size=4)

0xEE 个人信息

★★★★★★关于日子和技能的考虑★★★★★★

微信大众账号:罗西的考虑

0xFF 参阅

pytorch.org/docs/stable…

pytorch.org/tutorials/i…

m.w3cschool.cn/pytorch/pyt…

pytorch.org/tutorials/b…

pytorch.org/tutorials/i…

pytorch.org/tutorials/i…

pytorch.org/tutorials/i…

pytorch.org/tutorials/i…

pytorch.org/tutorials/i…

pytorch.org/tutorials/i…

pytorch.org/tutorials/a…

pytorch.org/tutorials/i…

pytorch.org/tutorials/a…

pytorch.org/docs/master…

pytorch.org/docs/master…

pytorch.org/tutorials/i…