pytorch分布式训练(一):torch.nn.DataParallel

  本文介绍最简单的pytorch分布式训练方法:使用torch.nn.DataParallel这个API来实现分布式训练。环境为单机多gpu,不妨假设有4个可用的gpu。

一、构建方法

使用这个API实现分布式训练的步骤非常简单,总共分为3步骤:
1、创建一个model,并将该model推到某个gpu上(这个gpu也将作为output_device,后面具体解释含义),不妨假设推到第0号gpu上,

device = torch.device("cuda:0")
model.to(device)

2、将数据推到output_device对应的gpu上,

data = data.to(device)

3、使用torch.nn.DataParallel这个API来在0,1,2,3四个gpu上构建分布式模型,

model = torch.nn.DataParallel(model, device_ids=[0,1,2,3], output_device=0)

然后这个model就可以像普通的单gpu上的模型一样开始训练了。

二、原理详解

2.1 原理图

  首先通过图来看一下这个最简单的分布式训练API的工作原理,然后结合代码详细阐述。
1
将模型和数据推入output_device(也就是0号)gpu上。
在这里插入图片描述
0号gpu将当前模型在其他几个gpu上进行复制,同步模型的parameter、buffer和modules等;将当前batch尽可能平均的分为len(device)=4份,分别推给每一个设备,并开启多线程分别在每个设备上进行前向传播,得到各自的结果,最后将各自的结果全部汇总在一起,拷贝回0号gpu。
在这里插入图片描述
在0号gpu进行反向传播和模型的参数更新,并将结果同步给其他几个gpu,即完成了一个batch的训练。

2.2 代码原理

  通过分析torch.nn.DataParallel的代码,可以看到具体的过程,这里重点看一下几个关键的地方。

# 继承自nn.Module,只要实现__init__和forward函数即可
class DataParallel(Module):
    # 构造函数里没有什么关键内容,主要是根据传进来的model、device_ids和output_device进行一些变量生成
    def __init__(self, module, device_ids=None, output_device=None, dim=0):
        super(DataParallel, self).__init__()

        device_type = _get_available_device_type()
        if device_type is None:
            self.module = module
            self.device_ids = []
            return

        if device_ids is None:
            device_ids = _get_all_device_indices()

        if output_device is None:
            output_device = device_ids[0]

        self.dim = dim
        self.module = module
        self.device_ids = list(map(lambda x: _get_device_index(x, True), device_ids))
        self.output_device = _get_device_index(output_device, True)
        self.src_device_obj = torch.device(device_type, self.device_ids[0])

        _check_balance(self.device_ids)

        if len(self.device_ids) == 1:
            self.module.to(self.src_device_obj)
    def forward(self, *inputs, **kwargs):
        if not self.device_ids:
            return self.module(*inputs, **kwargs)

        for t in chain(self.module.parameters(), self.module.buffers()):
            if t.device != self.src_device_obj:
                raise RuntimeError("module must have its parameters and buffers "
                                   "on device {} (device_ids[0]) but found one of "
                                   "them on device: {}".format(self.src_device_obj, t.device))

        inputs, kwargs = self.scatter(inputs, kwargs, self.device_ids)
        if len(self.device_ids) == 1:
            return self.module(*inputs[0], **kwargs[0])
        # 在每个gpu上都复制一个model
        replicas = self.replicate(self.module, self.device_ids[:len(inputs)])
        # 开启多线程进行前向传播,得到结果
        outputs = self.parallel_apply(replicas, inputs, kwargs)
        # 将每个gpu上得到的结果都gather到0号gpu上
        return self.gather(outputs, self.output_device)

    def replicate(self, module, device_ids):
        return replicate(module, device_ids, not torch.is_grad_enabled())

    def scatter(self, inputs, kwargs, device_ids):
        return scatter_kwargs(inputs, kwargs, device_ids, dim=self.dim)

    def parallel_apply(self, replicas, inputs, kwargs):
        return parallel_apply(replicas, inputs, kwargs, self.device_ids[:len(replicas)])

    def gather(self, outputs, output_device):
        return gather(outputs, output_device, dim=self.dim)

再看一下parallel_apply这个关键的函数,

def parallel_apply(modules, inputs, kwargs_tup=None, devices=None):
    assert len(modules) == len(inputs)
    if kwargs_tup is not None:
        assert len(modules) == len(kwargs_tup)
    else:
        kwargs_tup = ({},) * len(modules)
    if devices is not None:
        assert len(modules) == len(devices)
    else:
        devices = [None] * len(modules)
    devices = list(map(lambda x: _get_device_index(x, True), devices))
    # 创建一个互斥锁,防止前后两个batch的数据覆盖
    lock = threading.Lock()
    results = {}
    grad_enabled, autocast_enabled = torch.is_grad_enabled(), torch.is_autocast_enabled()
    # 线程的target函数,实现每个gpu上进行推理,其中i为gpu编号
    def _worker(i, module, input, kwargs, device=None):
        torch.set_grad_enabled(grad_enabled)
        if device is None:
            device = get_a_var(input).get_device()
        try:
            # 根据当前gpu编号确定推理硬件环境
            with torch.cuda.device(device), autocast(enabled=autocast_enabled):
                # this also avoids accidental slicing of `input` if it is a Tensor
                if not isinstance(input, (list, tuple)):
                    input = (input,)
                output = module(*input, **kwargs)
            # 锁住赋值,防止后一个batch的数据将前一个batch的结果覆盖
            with lock:
                results[i] = output
        except Exception:
            with lock:
                results[i] = ExceptionWrapper(
                    where="in replica {} on device {}".format(i, device))

    if len(modules) > 1:
        # 创建多个线程,进行不同gpu的前向推理
        threads = [threading.Thread(target=_worker,
                                    args=(i, module, input, kwargs, device))
                   for i, (module, input, kwargs, device) in
                   enumerate(zip(modules, inputs, kwargs_tup, devices))]

        for thread in threads:
            thread.start()
        for thread in threads:
            thread.join()
    else:
        _worker(0, modules[0], inputs[0], kwargs_tup[0], devices[0])
    # 将不同gpu上推理的结果打包起来,后面会gather到output_device上
    outputs = []
    for i in range(len(inputs)):
        output = results[i]
        if isinstance(output, ExceptionWrapper):
            output.reraise()
        outputs.append(output)
    return outputs

结论

  至此我们看到了torch.nn.DataParallel模块进行分布式训练的原理,数据和模型首先推入output_device对应的gpu,然后将分成多个子batch的数据和模型分别推给其他gpu,每个gpu单独处理各自的子batch,结果再打包回原output_device对应的gpu算梯度和更新参数,如此循环往复,其本质是一个单进程多线程的并发程序。
  由此我们也很容易得到torch.nn.DataParallel模块进行分布式的缺点,
1、每个batch的数据先分发到各gpu上,结果再打包回output_device上,在output_device一个gpu上进行梯度计算和参数更新,再把更新同步给其他gpu上的model。其中涉及数据的来回拷贝,网络通信耗时严重,GPU使用率低。
2、这种模式只支持单机多gpu的硬件拓扑结构,不支持Apex的混合精度训练等。
3、torch.nn.DataParallel也没有很完整的考虑到多个gpu做数据并行的一些问题,比如batchnorm,在训练时各个gpu上的batchnorm的mean和variance是子batch的计算结果,而不是原来整个batch的值,可能会导致训练不稳定影响收敛等问题。

  • 11
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
torch.nn.parallel.DataParallelPyTorch中的一个模块,用于在单个节点上进行多GPU数据并行训练。它能够自动将输入数据划分为多个小批次,并将这些小批次分发到不同的GPU上进行计算。每个GPU上都有一个模型副本,每个副本都独立地计算损失和梯度,并将梯度聚合后进行参数更新。 然而,torch.nn.parallel.DataParallel有一些限制,例如需要在每个GPU上有足够的显存来存储模型和梯度,因为它会复制模型到每个GPU上。此外,它还要求输入数据能够被划分为多个小批次,并且每个小批次的大小是相同的。 相比之下,torch.nn.parallel.DistributedDataParallel是一个更高级的模块,可以在单节点或多节点上进行多进程的分布式训练。每个模型副本由独立的进程控制,可以在不同的机器上运行。它不需要将模型复制到每个GPU上,而是通过进程间通信来在各个进程之间共享模型参数和梯度。这样可以更好地利用多个GPU和多个机器的计算资源,提高训练速度。 值得注意的是,torch.nn.parallel.DistributedDataParallel的初始化和使用方法与torch.nn.parallel.DataParallel略有不同,需要进行一些额外的设置和配置。但是,对于单节点的多GPU数据并行训练torch.nn.parallel.DistributedDataParallel已被证明比torch.nn.parallel.DataParallel更快。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [Pytorchtorch.nn.parallel.DistributedDataParallel](https://blog.csdn.net/baidu_35120637/article/details/110816619)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *3* [torch.nn.parallel.DistributedDataParallel](https://blog.csdn.net/weixin_45216013/article/details/125472676)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值