深度学习-GPU多卡并行训练总结

news2024/9/20 12:37:02

1.为什么要使用多GPU并行训练

        简单来说,有两种原因:第一种是模型在一块GPU上放不下,两块或多块GPU上就能运行完整的模型(如早期的AlexNet)。第二种是多块GPU并行计算可以达到加速训练的效果。想要成为“炼丹大师“,多GPU并行训练是不可或缺的技能。

2. 常见的多GPU训练方法

2.1模型并行方式

        如果模型特别大,GPU显存不够,无法将一个显存放在GPU上,需要把网络的不同模块放在不同GPU上,这样可以训练比较大的网络。(下图左半部分)

2.2 数据并行方式

        将整个模型放在一块GPU里,再复制到每一块GPU上,同时进行正向传播和反向误差传播。相当于加大了batch_size。(下图右半部分)

        在pytorch1.7 + cuda10 + TeslaV100的环境下,使用ResNet34,batch_size=16, SGD对花草数据集训练的情况如下:使用一块GPU需要9s一个epoch,使用两块GPU是5.5s, 8块是2s。这里有一个问题,为什么运行时间不是9/8≈1.1s ? 因为使用GPU数量越多,设备之间的通讯会越来越复杂,所以随着GPU数量的增加,训练速度的提升也是递减的。

3. 误差梯度如何在不同设备之间通信?

        在每个GPU训练step结束后,将每块GPU的损失梯度求平均,而不是每块GPU各计算各的。

4. BN如何在不同设备之间同步?

        假设batch_size=2, 每个GPU计算的均值和方差都针对这两个样本而言的。而BN的特性是:batch_size越大,均值和方差越接近与整个数据集的均值和方差,效果越好。使用多块GPU时,会计算每个BN层在所有设备上输入的均值和方差。如果GPU1和GPU2都分别得到两个特征层,那么两块GPU一共计算4个特征层的均值和方差,可以认为batch_size=4。注意:如果不用同步BN,而是每个设备计算自己的批次数据的均值方差,效果与单GPU一致,仅仅能提升训练速度;如果使用同步BN效果会有一定提升但是会损失一部分并行速度。

        单GPU、以及是否使用同步BN训练的三种情况,使用同步BN比不使用同步BN总体效果要好一些,不过训练时间也会更长。使用单GPU和不使用同步BN的效果是差不多的。

5.  两种GPU训练方法:DataParallel 和 DistributedDataParallel:

  • DataParallel是单进程多线程的,仅仅能工作在单机中。而DistributedDataParallel是多进程的,可以工作在单机或多机器中。
  • DataParallel通常会慢于DistributedDataParallel。所以目前主流的方法是DistributedDataParallel。

6. pytorch中常见的GPU启动方式:

注:distributed.launch方法如果开始训练后,手动终止程序,最好先看下显存占用情况,有小概率进程没kill的情况,会占用一部分GPU显存资源。下面以分类问题为基准,详细介绍使用DistributedDataParallel时的过程:

首先要初始化各进程环境:

def init_distributed_mode(args):
    # 如果是多机多卡的机器,WORLD_SIZE代表使用的机器数,RANK对应第几台机器
    # 如果是单机多卡的机器,WORLD_SIZE代表有几块GPU,RANK和LOCAL_RANK代表第几块GPU
    if'RANK'in os.environ and'WORLD_SIZE'in os.environ:
        args.rank = int(os.environ["RANK"])
        args.world_size = int(os.environ['WORLD_SIZE'])
        # LOCAL_RANK代表某个机器上第几块GPU
        args.gpu = int(os.environ['LOCAL_RANK'])
    elif'SLURM_PROCID'in os.environ:
        args.rank = int(os.environ['SLURM_PROCID'])
        args.gpu = args.rank % torch.cuda.device_count()
    else:
        print('Not using distributed mode')
        args.distributed = False
        return

    args.distributed = True

    torch.cuda.set_device(args.gpu)  # 对当前进程指定使用的GPU
    args.dist_backend = 'nccl'# 通信后端,nvidia GPU推荐使用NCCL
    dist.barrier()  # 等待每个GPU都运行完这个地方以后再继续

        在main函数初始阶段,进行以下初始化操作。需要注意的是,学习率需要根据使用GPU的张数增加。在这里使用简单的倍增方法。

def main(args):
    if torch.cuda.is_available() isFalse:
        raise EnvironmentError("not find GPU device for training.")

    # 初始化各进程环境
    init_distributed_mode(args=args)

    rank = args.rank
    device = torch.device(args.device)
    batch_size = args.batch_size
    num_classes = args.num_classes
    weights_path = args.weights
    args.lr *= args.world_size  # 学习率要根据并行GPU的数倍增

        实例化数据集可以使用单卡相同的方法,但在sample样本时,和单机不同,需要使用DistributedSampler和BatchSampler。

#给每个rank对应的进程分配训练的样本索引
train_sampler=torch.utils.data.distributed.DistributedSampler(train_data_set)
val_sampler=torch.utils.data.distributed.DistributedSampler(val_data_set)
#将样本索引每batch_size个元素组成一个list
train_batch_sampler=torch.utils.data.BatchSampler(
train_sampler,batch_size,drop_last=True)

        DistributedSampler原理如图所示:假设当前数据集有0~10共11个样本,使用2块GPU计算。首先打乱数据顺序,然后用 11/2 =6(向上取整),然后6乘以GPU个数2 = 12,因为只有11个数据,所以再把第一个数据(索引为6的数据)补到末尾,现在就有12个数据可以均匀分到每块GPU。然后分配数据:间隔将数据分配到不同的GPU中。

        BatchSampler原理: DistributedSmpler将数据分配到两个GPU上,以第一个GPU为例,分到的数据是6,9,10,1,8,7,假设batch_size=2,就按顺序把数据两两一组,在训练时,每次获取一个batch的数据,就从组织好的一个个batch中取到。注意:只对训练集处理,验证集不使用BatchSampler。

接下来使用定义好的数据集和sampler方法加载数据: 

#给每个rank对应的进程分配训练的样本索引
train_sampler=torch.utils.data.distributed.DistributedSampler(train_data_set)
val_sampler=torch.utils.data.distributed.DistributedSampler(val_data_set)
#将样本索引每batch_size个元素组成一个list
train_batch_sampler=torch.utils.data.BatchSampler(
train_sampler,batch_size,drop_last=True)

        如果有预训练权重的话,需要保证每块GPU加载的权重是一模一样的。需要在主进程保存模型初始化权重,在不同设备上载入主进程保存的权重。这样才能保证每块GOU上加载的权重是一致的:

# 实例化模型
    model = resnet34(num_classes=num_classes).to(device)

    # 如果存在预训练权重则载入
    if os.path.exists(weights_path):
        weights_dict = torch.load(weights_path, map_location=device)
        # 简单对比每层的权重参数个数是否一致
        load_weights_dict = {k: v for k, v in weights_dict.items()
                             if model.state_dict()[k].numel() == v.numel()}
        model.load_state_dict(load_weights_dict, strict=False)
    else:
        checkpoint_path = os.path.join(tempfile.gettempdir(), "initial_weights.pt")
        # 如果不存在预训练权重,需要将第一个进程中的权重保存,然后其他进程载入,保持初始化权重一致
        if rank == 0:
            torch.save(model.state_dict(), checkpoint_path)

        dist.barrier()
        # 这里注意,一定要指定map_location参数,否则会导致第一块GPU占用更多资源
        model.load_state_dict(torch.load(checkpoint_path, map_location=device))

        如果需要冻结模型权重,和单GPU基本没有差别。如果不需要冻结权重,可以选择是否同步BN层。然后再把模型包装成DDP模型,就可以方便进程之间的通信了。多GPU和单GPU的优化器设置没有差别,这里不再赘述。

# 是否冻结权重
    if args.freeze_layers:
        for name, para in model.named_parameters():
            # 除最后的全连接层外,其他权重全部冻结
            if"fc"notin name:
                para.requires_grad_(False)
    else:
        # 只有训练带有BN结构的网络时使用SyncBatchNorm采用意义
        if args.syncBN:
            # 使用SyncBatchNorm后训练会更耗时
            model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(model).to(device)

# 转为DDP模型
         model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[args.gpu])

 # optimizer使用SGD+余弦淬火策略
    pg = [p for p in model.parameters() if p.requires_grad]
    optimizer = optim.SGD(pg, lr=args.lr, momentum=0.9, weight_decay=0.005)
    lf = lambda x: ((1 + math.cos(x * math.pi / args.epochs)) / 2) * (1 - args.lrf) + args.lrf  # cosine
    scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda=lf)

        与单GPU不同的地方:rain_sampler.set_epoch(epoch),这行代码会在每次迭代的时候获得一个不同的生成器,每一轮开始迭代获取数据之前设置随机种子,通过改变传进的epoch参数改变打乱数据顺序。通过设置不同的随机种子,可以让不同GPU每轮拿到的数据不同。后面的部分和单GPU相同。

for epoch in range(args.epochs):
        train_sampler.set_epoch(epoch)  

        mean_loss = train_one_epoch(model=model,
                                    optimizer=optimizer,
                                    data_loader=train_loader,
                                    device=device,
                                    epoch=epoch)

        scheduler.step()

        sum_num = evaluate(model=model,
                           data_loader=val_loader,
                           device=device)
        acc = sum_num / val_sampler.total_size

我们详细看看每个epoch是训练时和单GPU训练的差异(上面的train_one_epoch)

def train_one_epoch(model, optimizer, data_loader, device, epoch):
    model.train()
    loss_function = torch.nn.CrossEntropyLoss()
    mean_loss = torch.zeros(1).to(device)
    optimizer.zero_grad()

    # 在进程0中打印训练进度
    if is_main_process():
        data_loader = tqdm(data_loader)

    for step, data in enumerate(data_loader):
        images, labels = data

        pred = model(images.to(device))

        loss = loss_function(pred, labels.to(device))
        loss.backward()
        loss = reduce_value(loss, average=True)  #  在单GPU中不起作用,多GPU时,获得所有GPU的loss的均值。
        mean_loss = (mean_loss * step + loss.detach()) / (step + 1)  # update mean losses

        # 在进程0中打印平均loss
        if is_main_process():
            data_loader.desc = "[epoch {}] mean loss {}".format(epoch, round(mean_loss.item(), 3))

        ifnot torch.isfinite(loss):
            print('WARNING: non-finite loss, ending training ', loss)
            sys.exit(1)

        optimizer.step()
        optimizer.zero_grad()

    # 等待所有进程计算完毕
    if device != torch.device("cpu"):
        torch.cuda.synchronize(device)

    return mean_loss.item()

def reduce_value(value, average=True):
    world_size = get_world_size()
    if world_size < 2:  # 单GPU的情况
        return value

    with torch.no_grad():
        dist.all_reduce(value)   # 对不同设备之间的value求和
        if average:  # 如果需要求平均,获得多块GPU计算loss的均值
            value /= world_size

        return value

接下来看一下验证阶段的情况,和单GPU最大的额不同之处预测正确样本个数的地方

@torch.no_grad()
def evaluate(model, data_loader, device):
    model.eval()

    # 用于存储预测正确的样本个数,每块GPU都会计算自己正确样本的数量
    sum_num = torch.zeros(1).to(device)

    # 在进程0中打印验证进度
    if is_main_process():
        data_loader = tqdm(data_loader)

    for step, data in enumerate(data_loader):
        images, labels = data
        pred = model(images.to(device))
        pred = torch.max(pred, dim=1)[1]
        sum_num += torch.eq(pred, labels.to(device)).sum()

    # 等待所有进程计算完毕
    if device != torch.device("cpu"):
        torch.cuda.synchronize(device)

    sum_num = reduce_value(sum_num, average=False)  # 预测正确样本个数

    return sum_num.item()

需要注意的是:保存模型的权重需要在主进程中进行保存。

if rank == 0:
            print("[epoch {}] accuracy: {}".format(epoch, round(acc, 3)))
            tags = ["loss", "accuracy", "learning_rate"]
            tb_writer.add_scalar(tags[0], mean_loss, epoch)
            tb_writer.add_scalar(tags[1], acc, epoch)
            tb_writer.add_scalar(tags[2], optimizer.param_groups[0]["lr"], epoch)

            torch.save(model.module.state_dict(), "./weights/model-{}.pth".format(epoch))

        如果从头开始训练,主进程生成的初始化权重是以临时文件的形式保存,需要训练完后移除掉。最后还需要撤销进程组。

if rank == 0:# 删除临时缓存文件   

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

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

相关文章

【原生HTML+SpringBoot】电子病历编辑器源码

一、简介 本系统主要面向医院医生、护士&#xff0c;提供对住院病人的电子病历书写、保存、修改、打印等功能。本系统基于云端SaaS服务方式&#xff0c;通过浏览器方式访问和使用系统功能&#xff0c;提供电子病历在线制作、管理和使用的一体化电子病历解决方案&#x…

常见面试题之JVM组成

1. JVM由那些部分组成&#xff0c;运行流程是什么&#xff1f; JVM是什么 Java Virtual Machine Java程序的运行环境&#xff08;java二进制字节码的运行环境&#xff09; 好处&#xff1a; 一次编写&#xff0c;到处运行 自动内存管理&#xff0c;垃圾回收机制 JVM由哪些…

sqldeveloper 连接 MySQL

sqldeveloper 连接 MySQL 工作中使用 Oracle 用户的小伙伴&#xff0c;sqldeveloper 是常用的开发和运维工具之一 工作中如果连接MySQL需要安装额外的客户端工具 不但学习成本高而且维护也较为麻烦 能不能使用 sqldeveloper 可以同时管理Oracle和MySQL两种甚至更多种数据库 成为…

基于Javaweb实现ATM机系统开发实战(一)基础配置和搭建

一、环境介绍 JDK:JDK1.8 IDEA:2020 MYSQL :MYSQL5.7 Navcat :mysql的客户端工具 Tomcat:tomcat5.7 二、项目简介 本项目主要实现一个模拟ATM机存款、取款、转账功能的一个系统&#xff0c;可以查看打印交易明细&#xff0c;后台用户可以管理用户账户卡信息。本系统主要是针对计…

Java易忘知识点

Java八大基本类型 整数类型&#xff1a;byte&#xff0c;1字节&#xff0c;8位&#xff0c;最大存储数据量是255&#xff0c;存放的数据范围是-128~127之间。整数类型&#xff1a;short&#xff0c;2字节&#xff0c;16位&#xff0c;最大数据存储量是65536&#xff0c;数据范…

centos下./configure报错:Permission denied

./configure 文章目录 ./configure报错解决方案使用chmod给./configure赋予x权限sftp给configure文件赋予x权限 ./configure报错 -bash: ./configure: Permission denied解决方案 使用chmod给./configure赋予x权限 sudo chmod x ./configuresftp给configure文件赋予x权限

推荐一个基于Java 的在线网盘开源程序

目录 一、软件简介 二、功能列表 文件列表 画廊模式 视频预览 文本预览 音频预览 PDF 预览 Office 预览 3d 文件预览 生成直链 页面设置 后台设置-登录 后台设置-存储源列表 后台设置-存储源权限控制 后台设置-添加存储源(本地存储) 后台设置-添加存储源 后…

Spring Boot中的请求参数绑定及使用

Spring Boot中的请求参数绑定及使用 在Web应用程序中&#xff0c;请求参数绑定是非常重要的操作。Spring Boot框架使得请求参数绑定变得非常简单&#xff0c;通过使用注解和预定义的类可以轻松地实现此操作。本文将介绍Spring Boot中的请求参数绑定及其使用。 请求参数绑定 在…

第十章:C语言数据结构与算法初阶之链式二叉树

系列文章目录 文章目录 系列文章目录前言一、链式二叉树的定义二、链式二叉树的实现三、链式二叉树的遍历1、前序遍历/先根遍历2、中序遍历/中根遍历3、后序遍历/后根遍历4、层序遍历5、前/中/后序遍历的关系 四、节点个数以及高度等1. 二叉树节点的个数2. 二叉树叶子节点个数3…

Java面试知识点复习​_kaic

一、后端基础 1.Java基础、集合、线程、异常&#xff08;自定义异常&#xff09;流 2.mysql、redis、mongodb(为什么使用) 3.ssm、springboot、springcloud、mybatis-plus 1.接口和抽象类的区别 二、前端基础 1.事件 三、实习和项目 1.博客项目的日志配置&#xff08;切面&a…

flutter tabBar 的属性及自定义实现

flutter tabBar 的属性及自定义实现 前言一、TabBar是什么&#xff1f;二、TabBar 自定义三、 Tab 自定义总结 前言 在Flutter中&#xff0c;TabBar的indicatorPadding属性用于设置指示器的内边距&#xff0c;而不是用于调整指示器和文字之间的间距。要调整TabBar中指示器和文字…

Field ‘非主键_id‘ doesn‘t have a default value

参考文章 Field ‘非主键_id‘ doesn‘t have a default value 的sql报错有两种情况 1.如果id是主键的话,一般是主键没有添加自增导致的错误 2.如果报错的是非主键id 那么是数据库设置错误 前端请求参数根本没有传入business_id 但是数据库报错 解决方法 把数据库数据限制n…

python 第九章 常用操作

系列文章目录 第一章 初识python 第二章 变量 第三章 基础语句 第四章 字符串str 第五章 列表list [] 第六章 元组tuple ( ) 第七章 字典dict {} 第八章 集合set {} 文章目录 系列文章目录9.1公共操作运算符 合并* 复制判断数据是否存在 公共方法len()delmax()min()rangeenume…

css实现文档目录(中间分割线宽度不定)

css实现文档目录&#xff08;中间分割线宽度不定&#xff09; 先上图片看效果 布局 数据 css样式 其实挺简单的&#xff0c;flex直接搞定

十六、centos7 下给python3创建虚拟环境

十五、centos7 下给python3创建虚拟环境 1.先说下为什么要创建虚拟环境 python3安装完成后&#xff0c;安装第三方库 pip3 install requests -i https://pypi.doubanio.com/simple提示如下让使用虚拟环境&#xff0c;不使用&#xff0c;即便是安装&#xff0c;也不能使用已经…

长连接和短链接详解

长连接和短链接详解 日常工作和生活中接触到的软件系统大多数情况下是由多个进程共同协作进行的&#xff0c;所以&#xff0c;了解进程之间如何进行高效的通信至关重要。 进程之间的通信有不少方式包括管道&#xff08;包括无名管道和命名管道&#xff09;、消息队列、信号量、…

Vue3在外部js文件获取Vue实例对象中的方法

刚开始想用getCurrentInstance 获取 发现这玩意只能写在setUp里面 然后百度发现在外部js文件只能先引入main.js在使用&#xff0c;这里注意要先将app实例导出 然后在外部js引用 就能获取到实例对象了 这点跟Vue2还是有很大区别

【SLAM14讲】 SLAM数学基础

一、向量 我们确定一个坐标系&#xff0c;也就是一个线性空间的基 (e1, e2, e3)&#xff0c; 那就可以谈论向量 a 在这组基下的坐标了&#xff1a; 1.1 内积 内积可以描述向量间的投影关系 1.2 外积 外积的方向垂直于这两个向量&#xff0c;大小为 |a| |b|sin 〈a, b〉&…

【生态系统服务】构建生态安全格局--权衡与协同动态分析--多情景模拟预测--社会价值评估

生态系统服务 生态系统服务&#xff08;ecosystem services&#xff09;是指人类从生态系统获得的所有惠益&#xff0c;包括供给服务&#xff08;如提供食物和水&#xff09;、调节服务&#xff08;如控制洪水和疾病&#xff09;、文化服务&#xff08;如精神、娱乐和文化收益…

vite + vue3 + tailwindcss

要在Vite Vue3中使用Tailwind CSS 创建项目 pnpm create vite在项目根目录中打开终端&#xff0c;并运行以下命令安装Tailwind CSS和相关依赖&#xff1a; pnpm install tailwindcss postcss autoprefixer执行 npx tailwindcss init&#xff0c;自动生成配置文件在tailwind.c…