分布式深度学习训练中的数据并行(DP/DDP) VS 模型并行 |
一. 介绍
二. 并行数据加载
2.1. 加载数据步骤
- 将数据从磁盘加载到主机:在这个阶段,数据从磁盘(可能是HDD或SSD)读取到主机的内存中。这个过程涉及文件系统的I/O操作,通常使用高级API,如Python的open函数,或者在深度学习框架中,可以使用数据加载器(如PyTorch的DataLoader或TensorFlow的tf.data API)来实现。这些数据加载器通常具有多线程或多进程功能,可以异步读取数据,并将其加载到CPU的内存中。
- 将数据从可分页内存传输到主机上的固定内存。请参阅此有关分页和固定的内存更多信息:可分页(pageable)内存和固定(pinned)内存都是主机内存的类型。可分页内存是普通的系统内存,操作系统可以将其页(一个内存管理单位)移动到磁盘上(即分页)。固定内存,又称为非分页内存,是指操作系统不能移动到磁盘的内存区域。固定内存的数据传输到GPU通常比从可分页内存传输更快,因为它避免了额外的复制步骤,并且可以直接通过DMA(直接内存访问)进行。在深度学习训练中,经常将数据从可分页内存复制到固定内存以准备传输到GPU。
- 将数据从固定内存传输到GPU:一旦数据位于固定内存中,它就可以通过高带宽的 PCIe总线(Peripheral Component Interface Express,总线和接口标准) 高效地传输到GPU内存中。深度学习框架通常提供了简化这个过程的工具。例如,在PyTorch中,你可以使用.to(device)或.cuda()方法将张量移动到GPU。此过程是由DMA引擎管理的,可以在不占用CPU资源的情况下进行。
- 在GPU上向前和向后传递:当数据位于GPU内存中时,可以开始训练过程,即进行模型的前向和反向传播。在前向传播中,模型的参数(也必须在GPU内存中)用于计算输出和损失函数。然后,通过反向传播,根据损失函数相对于模型参数的梯度,更新模型参数。这些计算完全在GPU上进行,利用其并行计算能力来加速训练过程。
2.2. PyTorch 1.0 中的数据加载器(Dataloader)
三. 数据并行
- 当一张 GPU 可以存储一个模型时,可以采用数据并行得到更准确的梯度或者加速训练,即每个 GPU 复制一份模型,将一批样本分为多份输入各个模型并行计算。因为求导以及加和都是线性的,数据并行在数学上也有效。
- 假设我们一个 batch 有
n
n
n 个样本,一共有
k 个 GPU 每个 GPU 分到
j
m_j
mj 个样本。假设样本刚好等分,则有
j
=
n
mj=kn 。我们考虑总的损失函数
l
l
l 对参数
w 的导数:
∂
L
o
∂
=
∂
⌊
1
n
∑
=
1
n
l
(
,
y
)
⌋
∂
=
1
n
∑
=
1
n
∂
l
(
,
y
)
∂
=
1
n
∂
[
1
1
∑
i
=
1
m
1
l
(
i
,
y
i
)
]
∂
+
m
2
n
∂
[
1
m
2
∑
i
=
m
1
+
1
m
1
+
m
2
l
(
i
,
y
i
)
]
∂
+
⋯
+
m
n
∂
[
1
m
∑
i
=
m
−
1
+
1
m
−
1
+
m
k
l
(
i
,
y
i
)
]
∂
=
∑
j
=
1
k
m
j
n
∂
[
1
m
j
∑
i
=
m
j
−
1
+
1
m
j
−
1
+
m
j
l
(
x
i
,
y
i
)
]
∂
=
∑
j
=
1
k
m
j
n
∂
l
o
j
∂
w
begin{aligned} begin{aligned}frac{partial Loss}{partial w}end{aligned}& =frac{partialleftlfloorfrac1nsum_{i=1}^nl(x_i,y_i)rightrfloor}{partial w} \ &=frac1nsum_{i=1}^nfrac{partial l(x_i,y_i)}{partial w} \ &=frac{m_1}nfrac{partialleft[frac1{m_1}sum_{i=1}^{m_1}l(x_i,y_i)right]}{partial w}+frac{m_2}nfrac{partialleft[frac1{m_2}sum_{i=m_1+1}^{m_1+m_2}l(x_i,y_i)right]}{partial w}+cdots+frac{m_k}nfrac{partialleft[frac1{m_k}sum_{i=m_{k-1}+1}^{m_{k-1}+m_k}l(x_i,y_i)right]}{partial w} \ &=sum_{j=1}^kfrac{m_j}nfrac{partialleft[frac1{m_j}sum_{i=m_{j-1}+1}^{m_{j-1}+m_j}l(x_i,y_i)right]}{partial w} \ &=sum_{j=1}^kfrac{m_j}nfrac{partial loss_j}{partial w} end{aligned}
∂w∂Loss=∂w∂⌊n1∑i=1nl(xi,yi)⌋=n1i=1∑n∂w∂l(xi,yi)=nm1∂w∂[m11∑i=1m1l(xi,yi)]+nm2∂w∂[m21∑i=m1+1m1+m2l(xi,yi)]+⋯+nmk∂w∂[mk1∑i=mk−1+1mk−1+mkl(xi,yi)]=j=1∑knmj∂w∂[mj1∑i=mj−1+1mj−1+mjl(xi,yi)]=j=1∑knmj∂w∂lossj
- 其中:
w
w
w 是模型参数,
∂
L
o
s
s
∂
w
∂w∂Loss 是大小为
n
n
n 的big batch的真实梯度,
∂
l
o
s
s
j
∂
w
frac{partial loss_j}{partial w}
∂w∂lossj 是 GPU/node
k
k
k 的小批量梯度,
x
i
x_i
xi 和
y
i
y_i
yi 是数据点
i
i
i,
l
(
x
i
,
y
i
)
l(x_i,y_i)
l(xi,yi) 是根据前向传播计算出的数据点
i
i
i 的损失,
n
n
n 是数据集中数据点的总数,
k
k
k 是 GPU/Node的总数,
m
k
m_k
mk 是分配给 GPU/节点的样本数量,
m
1
+
m
2
+
⋯
+
m
k
=
n
m_1+m_2+cdots+m_k=n
m1+m2+⋯+mk=n。当
m
1
=
m
2
=
⋯
=
m
k
=
n
k
m_1=m_2=cdots=m_k=frac nk
m1=m2=⋯=mk=kn时,我们可以进一步有:
∂
L
o
s
s
∂
w
=
1
k
[
∂
l
o
s
s
1
∂
w
+
∂
l
o
s
s
2
∂
w
+
⋯
+
∂
l
o
s
s
k
∂
w
]
frac{partial{Loss}}{ partial w }=frac{1}{k}big[frac{partial loss_1}{partial w}+frac{partial loss_2}{partial w}+cdots+frac{partial loss_k}{partial w}big]
∂w∂Loss=k1[∂w∂loss1+∂w∂loss2+⋯+∂w∂lossk]
- 这里对于每个GPU/Node,我们使用相同的模型/参数进行前向传播,我们向每个节点发送一小批不同的数据,正常计算梯度,并将梯度发送回主节点。此步骤是异步的,因为每个 GPU/节点的速度略有不同。一旦我们获得了所有梯度(我们在这里进行同步),我们就计算梯度的(加权)平均值,并使用梯度的(加权)平均值来更新模型/参数。然后我们继续下一次迭代。
3.1. DP(DataParallel)的基本原理
model = nn.DataParallel(model)
3.1.1. 从流程上理解
- 把 minibatch 数据从page–locked memory 传输到 GPU 0(master),Master GPU 也持有模型,其他GPU拥有模型的 stale copy。
- 在 GPUs 之间 scatter minibatch 数据。具体是将输入一个 minibatch 的数据均分成多份,分别送到对应的 GPU 进行计算。
- 在 GPUs 之间复制模型。与 Module 相关的所有数据也都会复制多份。
- 在每个GPU之上运行前向传播,计算输出。PyTorch 使用多线程来并行前向传播,每个 GPU 在单独的线程上将针对各自的输入数据独立并行地进行 forward 计算。
- 在 master GPU 之上收集(gather)输出,计算损失。即通过将网络输出与批次中每个元素的真实数据标签进行比较来计算损失函数值。
- 把损失在 GPUs 之间 scatter,在各个GPU之上运行后向传播,计算参数梯度。
- 在 GPU 0 之上归并梯度。
- 更新梯度参数。①进行梯度下降,并更新主GPU上的模型参数;②由于模型参数仅在主GPU上更新,而其他从属GPU此时并不是同步更新的,所以需要将更新后的模型参数复制到剩余的从属 GPU 中,以此来实现并行。
3.1.2. 从模式角度理解
- 从官方图也可以看到同样的信息。
- DataParallel会开启 单进程多线程 进行数据并行化
- 前向传播:首先,模型和mini-batch的数据会被放到GPU:0上(master GPU),之后,GPU:0会把数据分割成sub-mini-batch并scatter(分发)到其他GPU上,第二步,GPU:0会把自己的模型参数复制到其他GPU上,每个GPU拥有相同的模型参数。第三步,每个GPU在单独的线程上对其sub-mini-batch的数据前向传播,得到模型的输出结结果。第四部,GPU:0会收集所有GPU的输出结果。
- 反向传播:GPU:0得到所有的结果之后会与真实的label计算loss并得到loss的梯度,GPU:0会把loss梯度 Scatter到所有GPU上,每个GPU会根据loss梯度反向传播计算所有参数的梯度,之后,所有GPU上计算得到的参数梯度会汇总到GPU:0上,GPU:0进而对参数进行更新。这就完成了一个batch的模型训练。
- 有人说GPU:0是个自私的家伙,它把其他GPU都当成工具人来用,核心机密不传授,我只给你们数据,不给你label,你们得到结果之后给我我给你们计算loss和loss的梯度,然后分发给你们去给我计算参数的梯度,之后我得到这些参数的梯度之后我去更新参数,之后等下回需要你们的时候再去给你们其他GPU去分发我更新好的参数。
- 这是一个悲伤的故事,首先 单进程多线程 就似乎已经注定的结局,python的全局解释锁给这些附属的GPU戴上了沉沉的牢拷,其他GPU想奋起反抗,但是DP里面只有一个优化器Optimizer,这个优化器Optimizer只在主GPU上进行参数更新,当环境不在改变的时候,其他GPU选择了躺平,当GPU:0忙前忙后去分发数据、汇总梯度,更新参数的时候,其他GPU就静静躺着。
3.1.3. 从操作系统角度看
3.1.4. 低效率
3.2. DDP(DistributedDataParallel)的基本原理
3.2.1. 原理介绍
- DistributedDataParallel,支持
all-reduce,broadcast,send 和 receive
等等。通过 MPI 实现 CPU 通信,通过 NCCL 实现 GPU 通信。可以用于 单机多卡也可用于多机多卡, 官方也曾经提到用 DistributedDataParallel 解决 DataParallel 速度慢,GPU负载不均衡的问题。- 效果比DataParallel好太多!!!torch.distributed相对于torch.nn.DataParalle 是一个底层的API,所以我们要修改我们的代码,使其能够独立的在机器(节点)中运行。
- 与 DataParallel 的单进程控制多 GPU 不同,在 distributed 的帮助下,我们只需要编写一份代码,torch 就会自动将其分配给n个进程,分别在 n 个 GPU 上运行。不再有主GPU,每个GPU执行相同的任务。对每个GPU的训练都是在自己的过程中进行的。每个进程都从磁盘加载其自己的数据。分布式数据采样器可确保加载的数据在各个进程之间不重叠。损失函数的前向传播和计算在每个GPU上独立执行。因此,不需要收集网络输出。在反向传播期间,梯度下降在所有GPU上均被执行,从而确保每个GPU在反向传播结束时最终得到平均梯度的相同副本。
3.2.2. Parameter Server vs Ring AllReduce通信机制对比
- 所以我们亟需一种新的算法来提高深度学习模型训练的并行效率。2017 年 Facebook 发布了《Accurate, large minibatch SGD: Training ImageNet in 1 hour 》验证了大数据并行的高效性,同年百度发表了《Bringing HPC techniques to deep learning 》,验证了全新的梯度同步和权值更新算法的可行性,并提出了一种利用带宽优化环解决通信问题的方法——Ring AllReduce。Ring AllReduce链接
- Parameter Service最大的问题就是通信成本和GPU的数量线性相关。而Ring AllReduce的通信成本与GPU数量无关。Ring AllReduce分为两个步骤:Scatter Reduce和All Gather。
3.3. DP和DDP对比
- 1. 每个进程对应一个独立的训练过程,且只对梯度等少量数据进行信息交换。
3.4. 分布式中的几个概念
- 在 PyTorch 中,分布式并行处理是指在多个进程之间分配任务以便并行执行的一种方法,特别是在多个计算节点上进行深度学习模型的训练。以下是一些关键概念的详细解释:
- Group(进程组):在分布式计算中,group指的是进程组。一个进程组包含了一系列可以进行集体通信操作的进程。在PyTorch中,默认情况下所有的进程都会被自动分配到一个全局默认的进程组中,我们称之为 “world”。在这个默认组中,所有的进程都可以互相通信。
- 有时,你可能需要更精细的控制,比如你只想在一部分进程之间进行通信。在这种情况下,你可以通过 torch.distributed.new_group 接口创建一个新的进程组,该组包含全局进程组的一个子集。
- World Size(全局进程个数):world size是全局进程组中进程的总数。在多机分布式训练中,world size等于所有机器上的进程总和。例如,如果你在每台机器上启动了4个进程,且一共使用了3台机器,那么 world size 就是12。
- Rank(进程序号):在分布式训练中,rank是分配给每个进程的唯一标识符,其范围是从0到world size – 1。每个进程在通信时都会使用它的rank作为标识。通常情况下,rank为0的进程被认为是主进程(master),它可能负责一些协调工作,比如汇总数据或打印日志。
- Local Rank(局部GPU编号):在单个节点(机器)上可能有多个进程,每个进程可能管理一个或多个GPU。local_rank是指一个进程所管理的GPU在该节点上的编号。例如,如果一个节点上有8个GPU,那么每个进程的local_rank的范围将是0到7。在PyTorch中,这通常是由torch.distributed.launch模块在启动进程时内部设置的。这对于确保每个进程使用不同的GPU至关重要,以避免资源冲突。
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()
- DDP的实现主要基于下图所示结构
- Distributed.py:是
DDP
的Python
入口点。它实现了调用C++
库的nn.parallel.DistributedDataParallel
模块的初始化步骤和forward
函数。当一个DDP
进程在多个设备上工作时,其_sync_param
函数执行进程内参数同步,并且还将Rank 0
进程的模型缓冲区广播到所有其他进程。进程间参数同步发生在Reducer.cpp
中。- comm.h:实现合并广播辅助函数,该函数被调用以在初始化期间广播模型状态并在前向传递之前同步模型缓冲区。
- reducer.h:提供反向传递中梯度同步的核心实现。它具有三个入口点函数:
四. 模型并行(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/节点就像工厂生产线中的一个隔间,它等待来自前一个隔间的产品,并将自己的产品发送到下一个隔间。
五. 参考文献
- pytorch(分布式)数据并行个人实践总结——DataParallel/DistributedDataParallel
- Data Parallelism VS Model Parallelism in Distributed Deep Learning Training
- https://d2l.ai/chapter_computational-performance/parameterserver.html
- 💥 Training Neural Nets on Larger Batches: Practical Tips for 1-GPU, Multi-GPU & Distributed setups
- [原创][深度][PyTorch] DDP系列第一篇:入门教程
- [原创][深度][PyTorch] DDP系列第二篇:实现原理与源代码解析
- PyTorch 源码解读之 DP & DDP:模型并行和分布式训练解析
- 【DDP详解: 1】起底DP和DDP,数据分布式训练炼丹秘籍!
- 同时推荐一个官方设计笔记,讲得很详细,有兴趣可以看看。https://pytorch.org/docs/stable/notes/ddp.html
- 将高性能计算 (HPC)技术引入深度学习
- 【分布式训练】单机多卡的正确打开方式(一):理论基础
原文地址:https://blog.csdn.net/abc13526222160/article/details/134683412
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.7code.cn/show_50451.html
如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除!