torch分布式训练笔记

news2024/12/23 17:02:27

torch分布式训练笔记

  • 1. 数据并行(DistributedDataParallel)
  • 2. 模型并行(单机多卡)
  • 3. 混合并行(数据并行 + 模型并行/PipeLine并行)

1. 数据并行(DistributedDataParallel)

官方文档:DISTRIBUTED DATA PARALLEL

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):
    # create default process group
    dist.init_process_group("gloo", rank=rank, world_size=world_size)
    # 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(20, 10).to(rank))
    labels = torch.randn(20, 10).to(rank)
    # backward pass
    loss_fn(outputs, labels).backward()
    # update parameters
    optimizer.step()

def main():
    world_size = 2
    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()

代码从上面链接的 torch 教程中拷贝来的。
上面代码有些注意的地方:

  1. 首先数据并行的基本方式就是多进程,即一个进程管理一张GPU卡。上面代码使用 torch.multiprocessing 发起多个进程,这个 python 的 multiprocessing 是类似的。
  2. mp.spawn 中,每个进程都运行 example 函数,但是传入的参数只有 world_size,而 example 函数却要求两个参数(rank, world_size),看起来少了一个 rank 参数,但实际运行中,example 可以得到 rank 参数,rank 表示当前的进程是管理的哪张卡。
  3. 开始一定要 init_process_group 一下,从传入的参数可以推测出,这句话是将每张卡上对应的进程(rank),一个 world_size 个进程放进一个进程组。这样做是 torch 内部,方便管理实现进程组中进程的通信呢。

数据并行原理很简单,流程至少包括:

  1. 将模型的副本拷贝到每张GPU卡上
  2. 每个GPU卡上更新一次参数后,梯度信息同步更新(All-Reduce)

看起来上面代码并没有“直观”的处理这两个问题。官网文档介绍了,DDP 是如何实现这两个流程的。

正如文档中介绍的那样,

   ddp_model = DDP(model, device_ids=[rank])

DDP的构造函数中把上面说的流程都做了。具体包括:

  1. broadcasts state_dict() from the process with rank 0 to all other processes in the group to make sure that all model replicas start from the exact same state.
  2. Then, each DDP process creates a local Reducer, which later will take care of the gradients synchronization during the backward pass.

此外,还特意说明了为了提高通信效率,是以“bucket”为单位进行通信。最基础的想法是,所有的卡都完成了整个模型的反向传播和梯度更新后,开始通信。但实际上每张卡可能效率不同,有的卡完成了全部梯度的更新,有的还没有。这样一些卡就需要等待,GPU利用率低。

文档中这张图做了很好的解释。这里假设模型只有四个参数,分成两个 bucket,反向传播时,bucket0中的两个参数的梯度信息完成更新后,两个进程就可以分别调用 allreduce 操作,完成梯度信息的通信和同步。而不必等到整个模型中所有的参数梯度信息都更新后再通信。
在这里插入图片描述
这样就可以 overlapping allreduce collectives with computations during backwards.提高性能。

文档最后介绍了 DDP 里面的实现构成,比如广播模型参数和梯度信息同步就使用了 ProcessGroup::broadcast() , ProcessGroup::allreduce() 这些 api。 这也是代码开始需要初始化进程组的原因。

其他:关于 forward 和 backwark 中更多细节,比如通信具体是如何触发的, hook 的实现,这些后续阅读代码细节时再做介绍。

2. 模型并行(单机多卡)

官方文档:SINGLE-MACHINE MODEL PARALLEL BEST PRACTICES
Basic Usage

import torch
import torch.nn as nn
import torch.optim as optim

class ToyModel(nn.Module):
    def __init__(self):
        super(ToyModel, self).__init__()
        self.net1 = torch.nn.Linear(10, 10).to('cuda:0')
        self.relu = torch.nn.ReLU()
        self.net2 = torch.nn.Linear(10, 5).to('cuda:1')

    def forward(self, x):
        x = self.relu(self.net1(x.to('cuda:0')))
        return self.net2(x.to('cuda:1'))
model = ToyModel()
loss_fn = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.001)

optimizer.zero_grad()
outputs = model(torch.randn(20, 10))
labels = torch.randn(20, 5).to('cuda:1')
loss_fn(outputs, labels).backward()
optimizer.step()

直观上看,这里最大区别是没有使用多进程,而是使用单进程管理多张卡。由于是在单机上,并不需要进程组中进程之间的通信,只需要将中间数据在多卡之间进行拷贝。

模型并行将模型不同的 layer 放在不同的卡上。如 init 函数, 中的 to(‘cuda: 0’)。
为啥中间的 relu 不写成 “torch.nn.Relu().to(‘cuda:0’) ” 呢。(当然这样写也是可以的,只不过没有任何作用),因为 relu 运行不需要权重数据。

模型具体来说可以理解为两个部分:拓扑结构(计算图/计算流程) + 权重(参数)

将模型的不同layer放在不同的卡上,本质是将模型的不同layer 的权重数据放在不同的卡上。
因此需要在构造函数中将模型 split 到不同的 GPU 上。
如上述代码将第一个线性层的权重放在 0 卡,第二个线性层的权重放在 1 卡。 这个进程执行时,CPU 沿着 forward 函数代码开始执行(调度),CPU 在 GPU_0 上 launch 一个 liner 和 一个 relu kernel, 计算的结果存在 GPU_0 上,然后将这个结果拷贝到 GPU_1上, 继续在 GPU_1 上 launch 一个 linear kernnel。最后完成了整个计算。

这种计算方式最大的问题就是在同一时刻,只有一张卡在工作,其他卡在摸鱼。为了避免其他卡摸鱼,文档中用了 Pipeline Inputs

简单来说,当 0 卡把计算结果给到1卡,1卡开始计算时,0卡不能闲着,这时候给 0 卡新的输入数据(之前说 1 卡计算完了之后,再给 0 卡新的输入数据),0卡开始做运算。

文档使用了 resnet50 做了实验,核心代码如下:

class PipelineParallelResNet50(ModelParallelResNet50):
    def __init__(self, split_size=20, *args, **kwargs):
        super(PipelineParallelResNet50, self).__init__(*args, **kwargs)
        self.split_size = split_size

    def forward(self, x):
        splits = iter(x.split(self.split_size, dim=0))
        s_next = next(splits)
        s_prev = self.seq1(s_next).to('cuda:1')
        ret = []

        for s_next in splits:
            # A. ``s_prev`` runs on ``cuda:1``
            s_prev = self.seq2(s_prev)
            ret.append(self.fc(s_prev.view(s_prev.size(0), -1)))

            # B. ``s_next`` runs on ``cuda:0``, which can run concurrently with A
            s_prev = self.seq1(s_next).to('cuda:1')

        s_prev = self.seq2(s_prev)
        ret.append(self.fc(s_prev.view(s_prev.size(0), -1)))

        return torch.cat(ret)

这个代码比较简单,获得了 49% 的加速比。

这和 100% 的加速比还有差距,模型中加了一个参数 split_size。这个参数如果太小,则 forward 中 for 循环的次数增加,这会使得 kernel lauch 的数量较大,影响性能。 如果split_size 很大,则在第一个 split 时和最后一个 split,只有一张卡在工作。因此需要实验才能找到最佳的 split_size。

Intuitively speaking, using small split_size leads to many tiny CUDA kernel launch, while using large split_size results to relatively long idle times during the first and last splits. Neither are optimal.

在这里插入图片描述
文档中通过实验,发现 split_size = 12 时,可以获得最佳性能。但是加速比依旧只有 3.75/2.43-1=54%。因此还有进一步优化的空间。

优化思路为:
当前 kernel 的计算和数据的拷贝都在同一流上,那么发生数据拷贝时,这张卡就停止计算了。所以一个很容易想到的优化思路是将一张卡上 kernel 计算和数据拷贝放在两个不同的流上, 这样将当前 pre_split 结果的拷贝和 next_split 的计算时间重叠起来。 但这也需要一些同步操作,
比如第二卡上的计算流上的一些 kernel ,需要等待第一张卡上某些 copy 操作完成。

For example, all operations on cuda:0 is placed on its default stream. It means that computations on the next split cannot overlap with the copy operation of the prev split. However, as prev and next splits are different tensors, there is no problem to overlap one’s computation with the other one’s copy.

3. 混合并行(数据并行 + 模型并行/PipeLine并行)

官方文档:GETTING STARTED WITH DISTRIBUTED DATA PARALLEL/Combining DDP with Model Parallelism
最开始看到这个“混合并行时”不免产生了一些疑惑。因为之前的经验告诉我:数据并行要求每张 GPU 卡保存着完整的模型副本(其实这句话是不对的),而模型并行每张卡上只保存着模型的一部分,这听起来是矛盾的。
我们把文档中混合并行部分的完整代码扣出来:

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

def setup(rank, world_size):
    os.environ['MASTER_ADDR'] = 'localhost'
    os.environ['MASTER_PORT'] = '12355'

    # initialize the process group
    dist.init_process_group("gloo", rank=rank, world_size=world_size)

def cleanup():
    dist.destroy_process_group()

class ToyMpModel(nn.Module):
    def __init__(self, dev0, dev1):
        super(ToyMpModel, self).__init__()
        self.dev0 = dev0
        self.dev1 = dev1
        self.net1 = torch.nn.Linear(10, 10).to(dev0)
        self.relu = torch.nn.ReLU()
        self.net2 = torch.nn.Linear(10, 5).to(dev1)

    def forward(self, x):
        x = x.to(self.dev0)
        x = self.relu(self.net1(x))
        x = x.to(self.dev1)
        return self.net2(x)

def demo_model_parallel(rank, world_size):
    print(f"Running DDP with model parallel example on rank {rank}.")
    setup(rank, world_size)

    # setup mp_model and devices for this process
    dev0 = rank * 2
    dev1 = rank * 2 + 1
    mp_model = ToyMpModel(dev0, dev1)
    ddp_mp_model = DDP(mp_model)

    loss_fn = nn.MSELoss()
    optimizer = optim.SGD(ddp_mp_model.parameters(), lr=0.001)

    optimizer.zero_grad()
    # outputs will be on dev1
    outputs = ddp_mp_model(torch.randn(20, 10))
    labels = torch.randn(20, 5).to(dev1)
    loss_fn(outputs, labels).backward()
    optimizer.step()

    cleanup()

def run_demo(demo_fn, world_size):
    mp.spawn(demo_fn,
             args=(world_size,),
             nprocs=world_size,
             join=True)

if __name__ == "__main__":
    n_gpus = 4
    world_size = n_gpus//2
    run_demo(demo_model_parallel, world_size)

上面代码需要注意的一点是:device_ids and output_device must NOT be set
因为如果设置的话,这会在进程组内部进程之间广播模型。这并不是模型并行所期望的。
上述代码的分布式示意图如下:
在这里插入图片描述
从纵向看,是DP,横向看,是MP。所以只需要两个进程,一个进程管理 GPU_0 和 GPU_1,另外一个进程管理 GPU_2 和 GPU_3 ,这两个进程在同一个进程组中。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/746035.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

github搜索案例

目录结构 public/index.html <!DOCTYPE html> <html lang""><head><meta charset"utf-8"><!-- 针对IE浏览器的一个特殊配置&#xff0c;含义是让IE浏览器以最高的渲染级别渲染页面 --><meta http-equiv"X-UA-Comp…

海量文件高速传输解决方案(基于Rsync)

​​随着互联网的飞速发展和社会的数字化转型&#xff0c;企业信息化建设推动了数据的快速增长&#xff0c;越来越多的信息服务依赖海量数据的采集与应用。传统的FTP、网盘等工具无法满足海量数据的传输与分发&#xff0c;导致企业无法高效完成海量数据传输 。 传统的ftp传输效…

【使用驱动代码实现如下要求 应用程序通过阻塞的io模型来读取number变量的值】

驱动应用层代码 #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> #include <sys/ioctl.h> #include "head.h"…

记一次JVM调优过程

文档修订记录 版本 日期 撰写人 审核人 批准人 变更摘要 & 修订位置 JVM相关理论 JVM内存 可分配内存&#xff1a; JVM可以调度使用的总的内存数&#xff0c;这个数量受操作系统进程寻址范围、系统虚…

学无止境·MySQL⑦(索引和视图)

索引和视图练习 索引练习1、建立一个utf8编码的数据库test12、建立商品表goods和栏目表category3、删除 goods 表中的 goods_desc 字段及货号字段,并增加 click_count 字段4、在 goods_name 列上加唯一性索引&#xff08;用alter table方式&#xff09;5、在 shop_price 列上加…

基于linux下的高并发服务器开发(第一章)-GCC(1)1.2

打开XShell,在连接虚拟机Ubuntu的窗口中输入&#xff1a;sudo apt install gcc g gcc -v,查看gcc的版本,gcc version 7.5.0 也可以是gcc --version,查看信息相对少一些 g -v g --version ls查看当前目录的文件/文件夹 cd Linux/ 进入Linux文件夹 mkdir lession02 创建lession0…

校内VPN如何访问web of science?

web of science简介 Web of Science是获取全球学术信息的重要数据库&#xff0c;它收录了全球13000多种权威的、高影响力的学术期刊&#xff0c;内容涵盖自然科学、工程技术、生物医学、社会科学、艺术与人文等领域。Web of Science收录了论文中所引用的参考文献&#xff0c;通…

【Leetcode】24. 两两交换链表中的节点

给你一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题&#xff08;即&#xff0c;只能进行节点交换&#xff09;。 画图&#xff01;&#xff01;&#xff01; 1. 先定义一个头节点之前的节点 2.…

《向量数据库指南》——向量数据库与向量搜索库

目录 概览 向量数据库与向量搜索库 在这个数据量与日俱增的时代,大部分数据都可以归为半结构化数据和非结构化数据。近似最近邻(Approximate Nearest Neighbor,ANN)搜索是处理这类数据的有效方式。向量数据库是一种数据库管理系统,有助于处理不断增加的非结构化数据。 …

MySQL原理探索——29 如何判断一个数据库是不是出问题了

在第25和27篇文章中&#xff0c;介绍了主备切换流程。通过这些内容的讲解&#xff0c;你应该已经很清楚了&#xff1a;在一主一备的双 M 架构里&#xff0c;主备切换只需要把客户端流量切到备库&#xff1b;而在一主多从架构里&#xff0c;主备切换除了要把客户端流量切到备库外…

Linux:rsync+inotify实时同步

首先要客户机向服务器单次下载 而实时同步是向服务器实时上传 首先要实现单次下载&#xff0c;本章基于下面这章的续作 Linux&#xff1a;rsync_鲍海超-GNUBHCkalitarro的博客-CSDN博客 准备一个inotify-tools源码包 服务器配置 vim /etc/rsyncd.conf read only no setfa…

msfconsole

msfconsole 文章目录 msfconsole安装使用 msfconsole Msfconsole是Metasploit框架的主要控制台界面。它提供了一个命令行界面来与Metasploit框架进行交互&#xff0c;并允许用户执行各种渗透测试任务。Msfconsole是Metasploit的核心组件之一&#xff0c;它充当了一个交互式命令…

基于霍夫变换的航迹起始算法研究(Matlab代码实现)

目录 &#x1f4a5;1 概述 &#x1f4da;2 运行结果 &#x1f389;3 参考文献 &#x1f468;‍&#x1f4bb;4 Matlab代码 &#x1f4a5;1 概述 一、设计内容 利用Hough变换处理量测得到的含杂波的二维坐标&#xff0c;解决多目标航迹起始问题。使用Matlab进行仿真&#x…

设计模式详解(一):工厂方法——Factory Method

目录导航 工厂方法及其作用工厂方法的好处工厂方法的实现关系图实现步骤 工厂方法的适用场景工厂方法举例 工厂方法及其作用 工厂方法是一种创建型设计模式。所谓创建型设计模式是说针对创建对象方面的设计模式。在面向对象的编程语言里&#xff0c;我们通过对象间的相互协作&…

【聚类算法】密度峰值聚类算法DPC(Density Peak Clustering Algorithm)

every blog every motto: You can do more than you think. https://blog.csdn.net/weixin_39190382?typeblog 0. 前言 密度峰值聚类算法&#xff08;Density Peak Clustering Algorithm&#xff09;&#xff0c;能够自动发现数据中的密度峰值点&#xff0c;并根据峰值点将数…

Java:创建文件夹并输出内容到文件中

前言 在实际开发当中我们往往会遇到需要用 Java 代码来进行在我们项目的部署的服务器上创建一个文件并向里面写入想要的数据&#xff0c;下面就是具体的实现方式&#xff0c;希望对小伙伴们有用。 代码 这里我把这个操作封装成了一个工具类&#xff0c;方便根据不同业务场景…

中国品牌在海外做独立站有什么优势?

从人口红利驱动转向消费升级驱动——品牌出海行业的变局正在上演&#xff0c;低价不再是适用于所有出海企业的策略。从产品出海走向真正意义上的品牌出海&#xff0c;正在成为一部分中国商家的选择。 无论是做品牌还是做全球化&#xff0c;都不容易。消费者购买的不止产品&…

从零开始 verilog 以太网交换机(六)帧处理单元设计与实现

从零开始 verilog 以太网交换机&#xff08;六&#xff09;帧处理单元设计与实现 &#x1f508;声明&#xff1a; &#x1f603;博主主页&#xff1a;王_嘻嘻的CSDN主页 &#x1f9e8; 从零开始 verilog 以太网交换机系列专栏&#xff1a;点击这里 &#x1f511;未经作者允许&a…

Android Java代码与JNI交互 JNI访问Java构造方法(九)

🔥 Android Studio 版本 🔥 🔥 创建包含JNI的类 JNIConstructorClass.java 🔥 package com.cmake.ndk1.jni;import com.cmake.ndk1.model.Animal;public class JNIConstructorClass {static {System.loadLibrary("constructor-class-lib");}public native…

深度学习——CNN卷积神经网络

基本概念 概述 卷积神经网络&#xff08;Convolutional Neural Network&#xff0c;CNN&#xff09;是一种深度学习中常用于处理具有网格结构数据的神经网络模型。它在计算机视觉领域广泛应用于图像分类、目标检测、图像生成等任务。 核心思想 CNN 的核心思想是通过利用局部…