本文介绍: 现代深度学习模型中的参数数量越来越大,数据集的规模也急剧增加。要在大型数据集上训练复杂的现代深度学习模型,必须使用节点训练,否则会花费很长时间。在分布式深度学习训练中,人们可能总会看到数据并行模型并行。在这篇博文中,将讨论这两种深度学习并行方法的理论、逻辑和一些误导点。
分布式深度学习训练中的数据并行(DP/DDP) VS 模型并行

一. 介绍

二. 并行数加载

2.1. 加载数据步骤

  1. 将数据从磁盘加载主机:在这个阶段,数据从磁盘(可能是HDD或SSD)读取到主机内存中。这个过程涉及文件系统的I/O操作,通常使用高级API,如Pythonopen函数或者在深度学习框架中,可以使用数据加载器(如PyTorch的DataLoader或TensorFlowtf.data API)来实现。这些数据加载器通常具有多线程或多进程功能可以异步读取数据,并将其加载到CPU的内存中。
  2. 将数据从分页内存传输主机上的固定内存。请参阅有关分页固定的内存更多信息:可分页pageable)内存和固定(pinned)内存都是主机内存的类型。可分页内存是普通的系统内存,操作系统可以将其页(一个内存管理单位移动到磁盘上(即分页)。固定内存,又称为分页内存,是指操作系统不能移动到磁盘的内存区域。固定内存的数据传输到GPU通常比从可分页内存传输更快,因为它避免了额外复制步骤,并且可以直接通过DMA(直接内存访问进行。在深度学习训练中,经常将数据从可分页内存复制到固定内存以准备传输到GPU。
  3. 将数据从固定内存传输GPU:一旦数据位于固定内存中,它就可以通过高带宽PCIe总线(Peripheral Component Interface Express总线接口标准) 高效地传输到GPU内存中。深度学习框架通常提供了简化这个过程工具例如,在PyTorch中,你可以使用.to(device)或.cuda()方法将张量移动到GPU。此过程是由DMA引擎管理的,可以在不占用CPU资源的情况下进行
  4. 在GPU上向前和向后传递:当数据位于GPU内存中时,可以开始训练过程,即进行模型的前向和反向传播。在前向传播中,模型参数(也必须在GPU内存中)用于计算输出损失函数然后,通过反向传播,根据损失函数相对于模型参数梯度更新模型参数。这些计算完全在GPU上进行利用其并行计算能力来加速训练过程

2.2. PyTorch 1.0 中的数据加载器(Dataloader)

三. 数据并行

3.1. DP(DataParallel)的基本原理

  • DP 的好处是,使用起来非常方便,只需要将原来单卡的 module 用 DP 改成多卡:
model = nn.DataParallel(model)

3.1.1. 从流程理解

  1. minibatch 数据从pagelocked memory 传输到 GPU 0(master),Master GPU 也持有模型,其他GPU拥有模型的 stale copy
  2. 在 GPUs 之间 scatter minibatch 数据。具体是将输入一个 minibatch 的数据均分成多份,分别送到对应的 GPU 进行计算。
  3. 在 GPUs 之间复制模型。与 Module 相关所有数据也都会复制多份。
  4. 每个GPU之上运行前向传播,计算输出PyTorch 使用多线程来并行前向传播每个 GPU 在单独的线程上将针对各自的输入数据独立并行地进行 forward 计算
  5. master GPU 之上收集(gather)输出,计算损失。即通过将网络输出与批次中每个元素的真实数据标签进行比较来计算损失函数值。
  6. 损失在 GPUs 之间 scatter,在各个GPU之上运行后向传播,计算参数梯度。
  7. 在 GPU 0 之上归并梯度。
  8. 更新梯度参数。①进行梯度下降,并更新主GPU上的模型参数;②由于模型参数仅在主GPU上更新,而其他从属GPU此时并不是同步更新的,所以需要更新后的模型参数复制到剩余的从属 GPU 中,以此来实现并行。

在这里插入图片描述

3.1.2. 从模式角度理解

在这里插入图片描述

  • 有人说GPU:0是个自私的家伙,它把其他GPU都当成工具人来用,核心机密不传授,我只给你们数据,不给你label,你们得到结果之后给我我给你们计算lossloss的梯度,然后分发给你们去给我计算参数的梯度,之后我得到这些参数的梯度之后我去更新参数,之后等下回需要你们的时候再去给你们其他GPU去分发我更新好的参数。
  • 这是一个悲伤的故事,首先 进程多线程 就似乎已经注定的结局,python全局解释锁给这些附属的GPU戴上了沉沉的牢拷,其他GPU想奋起反抗,但是DP里面只有一个优化器Optimizer,这个优化器Optimizer只在主GPU上进行参数更新,当环境不在改变的时候,其他GPU选择躺平,当GPU:0忙前忙后去分发数据、汇总梯度,更新参数的时候,其他GPU就静静躺着。

3.1.3. 从操作系统角度

3.1.4. 低效率

  • 这种效率不高的数据并行方法,注定要被淘汰。是的,我们迎来了DDP(DistributedDataParallel)

3.2. DDP(DistributedDataParallel)的基本原理

3.2.1. 原理介绍

在这里插入图片描述

  • 假如我们有N张显卡
  • 缓解GIL限制)在DDP模式下,会有N个进程被启动每个进程在一张卡上加载一个模型,这些模型的参数在数值上是相同的。
  • (Ring-Reduce加速)在模型训练时,各个进程通过一种叫Ring-Reduce的方法与其他进程通讯交换各自的梯度,从而获得所有进程的梯度;
  • (实际上就是Data Parallelism)各个进程用平均后的梯度更新自己的参数,因为各个进程的初始参数、更新梯度是一致的,所以更新后的参数也是完全相同的。

3.2.2. Parameter Server vs Ring AllReduce通信机制对比

在这里插入图片描述

在这里插入图片描述

  • Scatter Reduce过程:首先,我们将参数分为N份,相邻的GPU传递不同的参数,在传递N-1次之后,可以得到每一份参数的累积(在不同的GPU上)。
  • 举个例子假设现有 5 个 GPU,那么就将梯度分为 5 份,如下图,分别是

    a

    i

    ,

    b

    i

    ,

    c

    i

    ,

    d

    i

    ,

    e

    i

    a_{i},b_{i},c_{i},d_{i},e_{i}

    ai,bi,ci,di,ei, 这里

    i

    i

    i 指的是 GPU 编号 从对角的位置开始传,每次传输时 GPU 之间只有一个块在传输,比如

    a

    0

    a_0

    a0,在传播 4 次后 GPU 4 上就有了一个完整的梯度块。

  • All Gather:得到每一份参数的累积之后,再做一次传递,同步到所有的GPU上。

  • 根据这两个过程,我们可以计算到All Reduce的通信成本为:

    N

    N

    N 个GPU/通信一次完整的参数所需时间

    K

    K

    K可以看到通信成本T与GPU数量

    N

    N

    N 无关。 由于All Reduce算法在通信成本上的优势,现在几个框架基本上都实现了其对于的官方API

    T

    =

    2

    (

    N

    1

    )

    K

    N

    T=2(N-1)frac{K}{N}

    T=2(N1)NK

3.3. DP和DDP对比

  • 1. 每个进程对应一个独立的训练过程,且只对梯度等少量数据进行信息交换

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

3.4. 分布式中的几个概念

3.5. torch.nn.parallel.DistributedDataParallel(v1.4版本)

import os
import torch
import torch.distributed as dist
import torch.multiprocessing as mp
import torch.nn as nn
import torch.optim as optim
from torch.nn.parallel import DistributedDataParallel as DDP

def example(rank, world_size):
    print(rank, world_size)
    # world_size=4,print结果如下
    # 0 4
    # 3 4
    # 2 4
    # 1 4
    # create default process group
    # PyTorch中初始化分布式训练环境的一个关键步骤,为进程间的通信建立了一个所谓的"进程组"
    # "gloo": 这是指定后端类型。Gloo是一个跨平台的通信后端,适用于CPU和GPU,它支持各种集体通信操作。PyTorch还支持其他后端,如"nccl"(适用于NVIDIA GPUs)和 "mpi"。
    # rank: 这是当前进程在进程组中的唯一标识符。在分布式训练中,每个进程都有一个唯一的rank,通常从0开始到world_size-1。rank用于标识每个进程,使得进程间可以知道彼此的身份
    # world_size: 这是进程组中的进程总数。在分布式训练中,这通常等于用于训练的总GPU或节点的数量。
    # 执行此函数时,并不是每个进程都创建自己独立的进程组,而是所有进程共同在一个全局的进程组中注册,从而能够相互通信。
    # 一旦进程组被初始化,进程间就可以使用PyTorch提供的集体通信操作(如dist.broadcast, dist.all_reduce等)进行通信和数据同步。
    # 这些通信操作允许进程共享数据(例如模型参数或梯度),这对于保证分布式训练中所有节点的模型同步至关重要
    dist.init_process_group("gloo", rank=rank, world_size=world_size)  # nccl
    # create local model
    model = nn.Linear(10, 10).to(rank)
    # construct DDP model
    ddp_model = DDP(model, device_ids=[rank])
    # define loss function and optimizer
    loss_fn = nn.MSELoss()
    optimizer = optim.SGD(ddp_model.parameters(), lr=0.001)

    # forward pass
    outputs = ddp_model(torch.randn(200, 10).to(rank))
    labels = torch.randn(200, 10).to(rank)
    # backward pass
    loss_fn(outputs, labels).backward()
    # update parameters
    optimizer.step()


def main():
    # world_size 指的是总的并行进程数目比如16张卡单卡单进程就是16,但是如果是8卡单进程就是1
    world_size = 4
    # torch.multiprocessing启动多个进程
    # example: 这是一个函数,它会在每个新创建的进程中运行。在分布式训练的上下文中,这个函数通常会包含模型的初始化、训练循环等。
    # args=(world_size,): 这个参数提供了一个元组,其中包含传递给example函数的参数
    # nprocs=world_size: 这个参数指定了要启动的进程数。
    # join=True: 这个参数控制mp.spawn是否应该等待所有进程完成。如果设置为True,mp.spawn调用将会阻塞,直到所有进程都运行完毕。这意味着主程序等待所有由mp.spawn启动的进程结束后才继续执行。
    # mp.spawn自动为每个新创建的进程生成一个rank,其值从0到nprocs-1。然后,mp.spawn会将这个rank和args中的参数一起传递给example函数。
    mp.spawn(example, args=(world_size,), nprocs=world_size, join=True)


if __name__ == "__main__":
    # Environment variables which need to be set when using c10d's default "env" initialization mode.
    os.environ["MASTER_ADDR"] = "localhost"
    os.environ["MASTER_PORT"] = "29500"
    main()

四. 模型并行(ModelParallel)

  • 模型并行性对我来说听起来很可怕,但它实际上与数学无关。这是分配计算机资源的本能。有时我们无法将所有数据放入(GPU)内存中,因为我们的深度学习模型中有太多层和参数。因此,我们可以将深度学习模型分成几个部分,将几个连续的层放在一个节点上并计算其梯度。这样,单个节点的参数数量就减少了,并且可以利用数据进行训练,得到更准确的梯度。
  • 例如,我们有 10 个 GPU,我们想要训练一个简单的 ResNet50 模型。我们可以将前 5 层分配给 GPU #1,后 5 层分配给 GPU #2,依此类推,最后 5 层分配给 GPU #10。在训练期间,在每次迭代中,前向传播必须首先在 GPU #1 中完成。 GPU #2 正在等待 GPU #1 的输出,GPU #3 正在等待 GPU #2 的输出,依此类推。一旦前向传播完成。我们计算驻留在 GPU #10 中的最后一层的梯度,并更新 GPU #10 中这些层的模型参数。然后梯度反向传播到 GPU #9 中的前一层,等等。每个 GPU/节点就像工厂生产线中的一个隔间,它等待来自前一个隔间的产品,并将自己产品发送到下一个隔间。
  • 在我看来,模型并行性的名称具有误导性,不应将其视为并行计算的示例更好名称可能“模型序列化因为它在并行计算中使用串行方法而不是并行方法。然而,在某些场景下,某些神经网络中的某些层(例如 Siamese Network)实际上是“并行的”。这样,模型并行性可以在某种程度上表现得像真正的并行计算。然而,数据并行是100%并行计算

五. 参考文献

原文地址:https://blog.csdn.net/abc13526222160/article/details/134683412

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_50451.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注