2.7 手写数字识别之资源配置

简介: 这篇文章讨论了在深度学习任务中,如何通过资源配置优化提升模型训练效率,包括程序运行的全局设备设置、单GPU训练、分布式训练(模型并行和数据并行)以及多GPU训练的启动方式,特别介绍了飞桨框架在单机多卡训练中的简便性。

从前几节的训练看,无论是房价预测任务还是MNIST手写字数字识别任务,训练好一个模型不会超过10分钟,主要原因是我们所使用的神经网络比较简单。但实际应用时,常会遇到更加复杂的机器学习或深度学习任务,需要运算速度更高的硬件(如GPU、NPU),甚至同时使用多个机器共同训练一个任务(多卡训练和多机训练)。本节我们依旧横向展开"横纵式"教学方法,如图1所示,探讨在手写数字识别任务中,通过资源配置的优化,提升模型训练效率的方法

图1:“横纵式”教学法 — 资源配置

前提条件

需要先进行数据处理、设计神经网络结构,代码与上一节保持一致,此处直接调用封装好的代码。

In [1]

import paddle
from data_process import get_MNIST_dataloader
from MNIST_network import MNIST
train_loader, test_loader = get_MNIST_dataloader()

2.7.1 程序运行的全局设备

paddle.device.get_device功能返回当前程序运行的全局设备,返回的是一个类似于cpu、 gpu:x、 xpu:x或者npu:x字符串,如果没有设置全局设备,当cuda可用的时候返回******gpu:0******,当cuda不可用的时候返回******cpu******由于GPU通常为多个计算卡的结构,返回结果******gpu:0******中冒号后面的0代表0号卡。如果是4卡的GPU,编号分别为0、1、2、3。

下面通过代码示例查看当前程序运行的全局设备

In [2]

import paddle
device = paddle.device.get_device()
print("device:",device)

device: gpu:0

如果您启动项目时选用的是GPU,那么会输出device: gpu:0

在飞桨中,可通过******place******函数查看一个Tensor的设备位置。张量可能的设备位置有三种:CPU/GPU/固定内存,其中固定内存也称为不可分页内存或锁页内存, 其与GPU之间具有更高的读写效率,并且支持异步传输,这对网络整体性能会有进一步提升,但其缺点是分配空间过多时可能会降低主机系统的性能, 因为其减少了用于存储虚拟内存数据的可分页内存。

如果想将CPU上的Tensor拷贝到GPU上,通过cuda(device_id=None, blocking=False)函数完成。如果当前Tensor已经在GPU上,且device_id为None,则不会发生任何拷贝。具体参数说明如下:

  • device_id (int, optional) - 目标GPU的设备Id,默认为None,此时为当前Tensor所在的设备Id,如果当前Tensor不在GPU上,则为0
  • blocking (bool, optional) - 如果为******False并且当前Tensor处于固定内存上,将会发生主机到设备端的异步拷贝。******否则,会发生同步拷贝。默认为False。

经过上述操作后, 返回的Tensor不保留在原计算图中。

下面我们在CPU上创建一个Tensor,获取其设备位置,然后将该Tensor拷贝到GPU上,再次获取设备位置:

In [3]

import paddle
x = paddle.to_tensor(1.0, place=paddle.CPUPlace())
print(x.place)        # CPUPlace
y = x.cuda()
print(y.place)        # CUDAPlace(0)

CPUPlace

CUDAPlace(0)

W0901 16:57:38.553731 145 device_context.cc:447] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 11.2, Runtime API Version: 10.1

W0901 16:57:38.558326 145 device_context.cc:465] device: 0, cuDNN Version: 7.6.

2.7.2 单GPU训练

通过paddle.device.set_device API,设置在GPU上训练还是CPU上训练。

paddle.device.set_device (device)

参数 device (str):此参数确定特定的运行设备,可以是******cpu************gpu:x******或者是******xpu:x******。其中,******x******是GPU或XPU的编号。当device是cpu时, 程序在CPU上运行;当device是gpu:x时,程序在GPU上运行。

In [5]

import paddle.nn.functional as F
#仅优化算法的设置有所差别
def train(model):
    #开启GPU
    use_gpu = True
    paddle.device.set_device('gpu:0') if use_gpu else paddle.device.set_device('cpu')
    model.train()
    
    #设置不同初始学习率
    opt = paddle.optimizer.Adam(learning_rate=0.01, parameters=model.parameters())
    
    EPOCH_NUM = 5
    for epoch_id in range(EPOCH_NUM):
        for batch_id, data in enumerate(train_loader()):
            #准备数据,变得更加简洁
            images, labels = data
            images = paddle.to_tensor(images)
            labels = paddle.to_tensor(labels)
            
            #前向计算的过程
            predicts = model(images)
            
            #计算损失,取一个批次样本损失的平均值
            loss = F.cross_entropy(predicts, labels)
            avg_loss = paddle.mean(loss)
            
            #每训练了100批次的数据,打印下当前Loss的情况
            if batch_id % 200 == 0:
                print("epoch: {}, batch: {}, loss is: {}".format(epoch_id, batch_id, avg_loss.numpy()))
            
            #后向传播,更新参数的过程
            avg_loss.backward()
            opt.step()
            opt.clear_grad()
    #保存模型参数
    paddle.save(model.state_dict(), 'mnist.pdparams')
#创建模型    
model = MNIST()
#启动训练过程
train(model)

epoch: 0, batch: 0, loss is: [3.3544707]

epoch: 0, batch: 200, loss is: [0.31601915]

epoch: 0, batch: 400, loss is: [0.24964087]

epoch: 0, batch: 600, loss is: [0.0728729]

epoch: 0, batch: 800, loss is: [0.02721875]

epoch: 4, batch: 0, loss is: [0.01339576]...

epoch: 4, batch: 200, loss is: [0.2479971]

epoch: 4, batch: 400, loss is: [0.02366359]

epoch: 4, batch: 600, loss is: [0.04403502]

epoch: 4, batch: 800, loss is: [0.08038075]

尝试把******use_gpu******设置成False,训练5个epoch需要的时间是GPU用时的10倍左右,可见GPU的并行能力要比CPU强大很多。

2.7.3 分布式训练

在工业实践中,很多较复杂的任务需要使用更强大的模型。强大模型加上海量的训练数据,经常导致模型训练耗时严重。比如在计算机视觉分类任务中,训练一个在ImageNet数据集上精度表现良好的模型,大概需要一周的时间,因为过程中我们需要不断尝试各种优化的思路和方案。如果每次训练均要耗时1周,这会大大降低模型迭代的速度。在机器资源充沛的情况下,建议采用分布式训练,大部分模型的训练时间可压缩到小时级别。

分布式训练有两种基本的实现模式:模型并行和数据并行

2.7.3.1 模型并行

模型并行是将一个网络模型拆分为多份,拆分后的模型分到多个设备上(GPU)训练,每个设备的训练数据是相同的。模型并行的实现模式可以节省内存,但是应用较为受限

模型并行的方式一般适用于如下两个场景:

  1. 模型架构过大: 完整的模型无法放入单个GPU。如2012年ImageNet大赛的冠军模型AlexNet是模型并行的典型案例,由于当时GPU内存较小,单个GPU不足以承担AlexNet,因此研究者将AlexNet拆分为两部分放到两个GPU上并行训练。
  2. 网络模型的结构设计相对独立:网络模型的设计结构可以并行化时,采用模型并行的方式。如在计算机视觉目标检测任务中,一些模型(如YOLO9000)的边界框回归和类别预测是独立的,可以将独立的部分放到不同的设备节点上完成分布式训练

2.7.3.2 数据并行

数据并行与模型并行不同,数据并行每次读取多份数据,读取到的数据输入给多个设备(GPU)上的模型,每个设备上的模型是完全相同的,飞桨采用的就是这种方式。


说明:

当前GPU硬件技术快速发展,深度学习使用的主流GPU的内存已经足以满足大多数的网络模型需求,所以大多数情况下使用数据并行的方式


数据并行的方式与众人拾柴火焰高的道理类似,如果把训练数据比喻为砖头,把一个设备(GPU)比喻为一个人,那单GPU训练就是一个人在搬砖,多GPU训练就是多个人同时搬砖,每次搬砖的数量倍数增加,效率呈倍数提升。值得注意的是,每个设备的模型是完全相同的,但是输入数据不同,因此每个设备的模型计算出的梯度是不同的。如果每个设备的梯度只更新当前设备的模型,就会导致下次训练时,每个模型的参数都不相同。因此我们还需要一个梯度同步机制,保证每个设备的梯度是完全相同的。

梯度同步有两种方式:PRC通信方式和NCCL2通信方式(Nvidia Collective multi-GPU Communication Library)。

(1)PRC通信方式

PRC通信方式通常用于CPU分布式训练,它有两个节点:参数服务器Parameter server和训练节点Trainer,结构如图2所示。

图2:Pserver通信方式的结构

parameter server收集来自每个设备的梯度更新信息,并计算出一个全局的梯度更新。Trainer用于训练,每个Trainer上的程序相同,但数据不同。当Parameter server收到来自Trainer的梯度更新请求时,统一更新模型的梯度

(2)NCCL2通信方式(Collective)

当前飞桨的GPU分布式训练使用的是基于NCCL2的通信方式,结构如图3所示。

图3:NCCL2通信方式的结构

相比PRC通信方式,使用NCCL2(Collective通信方式)进行分布式训练,不需要启动Parameter server进程,每个Trainer进程保存一份完整的模型参数,在完成梯度计算之后通过Trainer之间的相互通信,Reduce梯度数据到所有节点的所有设备,然后每个节点在各自完成参数更新。

飞桨提供了便利的数据并行训练方式,用户只需要对程序进行简单修改,即可实现在多GPU上并行训练(NCCL2的通信方式)。接下来将讲述如何将一个单机程序通过简单的改造,变成单机多卡程序。

单机多卡程序通过如下两步改动即可完成:

1)初始化并行环境。

2)使用paddle.DataParallel封装模型。

后续通过启动时候指定运行程序的多个GPU卡,飞桨会自动切分数据进行多卡分布式训练,加速训练过程。

In [6]

import paddle 
import paddle.distributed as dist
def train_multi_gpu(model):
    # 修改1- 初始化并行环境
    dist.init_parallel_env()
    # 修改2- 增加paddle.DataParallel封装
    model = paddle.DataParallel(model)
    model.train()
    
    opt = paddle.optimizer.Adam(learning_rate=0.01, parameters=model.parameters())
    EPOCH_NUM = 5
    for epoch_id in range(EPOCH_NUM):
        for batch_id, data in enumerate(train_loader()):
            #准备数据,变得更加简洁
            images, labels = data
            images = paddle.to_tensor(images)
            labels = paddle.to_tensor(labels)
            #前向计算的过程
            predicts = model(images)
            #计算损失,取一个批次样本损失的平均值
            loss = F.cross_entropy(predicts, labels)
            avg_loss = paddle.mean(loss)
            #每训练了100批次的数据,打印下当前Loss的情况
            if batch_id % 200 == 0:
                print("epoch: {}, batch: {}, loss is: {}".format(epoch_id, batch_id, avg_loss.numpy()))
            #后向传播,更新参数的过程
            avg_loss.backward()
            opt.step()
            opt.clear_grad()
    #保存模型参数
    paddle.save(model.state_dict(), 'mnist.pdparams')
#创建模型    
model = MNIST()
#启动训练过程
train_multi_gpu(model)

/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/paddle/distributed/parallel.py:136: UserWarning: Currently not a parallel execution environment, `paddle.distributed.init_parallel_env` will not do anything.

"Currently not a parallel execution environment, `paddle.distributed.init_parallel_env` will not do anything."

/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/paddle/fluid/dygraph/parallel.py:596: UserWarning: The program will return to single-card operation. Please check 1, whether you use spawn or fleetrun to start the program. 2, Whether it is a multi-card program. 3, Is the current environment multi-card.

warnings.warn("The program will return to single-card operation. "

epoch: 0, batch: 0, loss is: [4.0620155]

epoch: 0, batch: 200, loss is: [0.1424572]

epoch: 0, batch: 400, loss is: [0.27101123]

epoch: 0, batch: 600, loss is: [0.11927377]

epoch: 0, batch: 800, loss is: [0.07573487]....

epoch: 4, batch: 0, loss is: [0.01736185]

epoch: 4, batch: 200, loss is: [0.02390713]

epoch: 4, batch: 400, loss is: [0.01427157]

epoch: 4, batch: 600, loss is: [0.23381296]

epoch: 4, batch: 800, loss is: [0.04849435]

2.7.4 多GPU训练

启动多GPU的训练,有两种方式:1)基于launch启动;2)基于spawn方式启动。

2.7.4.1 基于launch方式启动

需要在命令行中设置参数变量。打开终端,运行如下命令:

单机单卡启动,默认使用第0号卡。

$ python train.py

单机多卡启动,默认使用当前可见的所有卡。

$ python -m paddle.distributed.launch train.py

单机多卡启动,设置当前使用的第0号和第1号卡。

$ python -m paddle.distributed.launch --gpus '0,1' --log_dir ./mylog train.py
$ export CUDA_VISIABLE_DEVICES='0,1'
$ python -m paddle.distributed.launch train.py

相关参数含义如下:

  • paddle.distributed.launch:启动分布式运行。
  • gpus:设置使用的GPU的序号(需要是多GPU卡的机器,通过命令watch nvidia-smi查看GPU的序号)。
  • log_dir:存放训练的log,若不设置,每个GPU上的训练信息都会打印到屏幕。
  • train.py:多GPU训练的程序,包含修改过的train_multi_gpu()函数。

训练完成后,在指定的./mylog文件夹下会产生四个日志文件,其中worklog.0的内容如下:

In [ ]

grep: warning: GREP_OPTIONS is deprecated; please use an alias or script
dev_id 0
I1104 06:25:04.377323 31961 nccl_context.cc:88] worker: 127.0.0.1:6171 is not ready, will retry after 3 seconds...
I1104 06:25:07.377645 31961 nccl_context.cc:127] init nccl context nranks: 3 local rank: 0 gpu id: 1↩
W1104 06:25:09.097079 31961 device_context.cc:235] Please NOTE: device: 1, CUDA Capability: 61, Driver API Version: 10.1, Runtime API Version: 9.0
W1104 06:25:09.104460 31961 device_context.cc:243] device: 1, cuDNN Version: 7.5.
start data reader (trainers_num: 3, trainer_id: 0)
epoch: 0, batch_id: 10, loss is: [0.47507238]
epoch: 0, batch_id: 20, loss is: [0.25089613]
epoch: 0, batch_id: 30, loss is: [0.13120805]
epoch: 0, batch_id: 40, loss is: [0.12122715]
epoch: 0, batch_id: 50, loss is: [0.07328521]
epoch: 0, batch_id: 60, loss is: [0.11860339]
epoch: 0, batch_id: 70, loss is: [0.08205047]
epoch: 0, batch_id: 80, loss is: [0.08192863]
epoch: 0, batch_id: 90, loss is: [0.0736289]
epoch: 0, batch_id: 100, loss is: [0.08607423]
start data reader (trainers_num: 3, trainer_id: 0)
epoch: 1, batch_id: 10, loss is: [0.07032011]
epoch: 1, batch_id: 20, loss is: [0.09687119]
epoch: 1, batch_id: 30, loss is: [0.0307216]
epoch: 1, batch_id: 40, loss is: [0.03884467]
epoch: 1, batch_id: 50, loss is: [0.02801813]
epoch: 1, batch_id: 60, loss is: [0.05751991]
epoch: 1, batch_id: 70, loss is: [0.03721186]
.....

File"", line1grep: warning: GREP_OPTIONS is deprecated; please use an alias or script^SyntaxError:invalid syntax

2.7.4.2 基于spawn方式启动

Launch方式启动训练,是以文件为单位启动多进程,需要用户在启动时调用paddle.distributed.launch对于进程的管理要求较高;飞桨最新版本中,增加了spawn启动方式,可以更好地控制进程,在日志打印、训练和退出时更加友好。spawn方式和launch方式仅在启动上有所区别。

In [7]

# 用法一:启动train多进程训练,默认使用所有可见的GPU卡。
# 只传入函数,训练函数不需要任何参数
if __name__ == '__main__':
    dist.spawn(train)

In [9]

# 用法二:传入函数和参数
# 当训练函数需要一些参数时,利用所有可见设备并行训练
if __name__ == '__main__':
    dist.spawn(train, args=(model,))

In [ ]

# 用法三:传入函数,参数和进程数目
# 当训练函数需要参数,仅使用部分可见的设备并行训练
# 如果当前机器有8张卡,并且均可见,下面的例子将使用0号和1号卡
# 如果设置CUDA_VISIBLE_DEVICES=4,5,6,7,该例子将会使用4和5号卡
if __name__ == '__main__':
    dist.spawn(train, args=(model,), nprocs=2)

In [ ]

# 用法四:输入函数,参数,进程数目和gpus
# 如果训练函数需要一些参数,并且只使用部分可见的设备用于并行训练,但是不能设置机器环境变量CUDA_VISIBLE_DEVICES,如该变量为None或所有卡,
# 通过输入gpus来选择想要使用的卡。
if __name__ == '__main__':
    dist.spawn(train, args=(model,), nprocs=2, gpus='4,5')
相关实践学习
在云上部署ChatGLM2-6B大模型(GPU版)
ChatGLM2-6B是由智谱AI及清华KEG实验室于2023年6月发布的中英双语对话开源大模型。通过本实验,可以学习如何配置AIGC开发环境,如何部署ChatGLM2-6B大模型。
相关文章
在某龙芯平台上编译FFMPEG报错:ERROR: xxx not found using pkg-config
在某龙芯平台上编译FFMPEG报错:ERROR: xxx not found using pkg-config
712 0
github项目文件大小查看方法
github项目文件大小查看方法
3143 1
github项目文件大小查看方法
|
存储 机器学习/深度学习 人工智能
【DSW Gallery】DSW基础使用介绍
PAI-DSW是一款云端机器学习开发IDE,为您提供交互式编程环境,适用于不同水平的开发者。本文为您介绍PAI-DSW的功能特点以及界面的基础使用。
【DSW Gallery】DSW基础使用介绍
|
缓存 测试技术 API
解锁开源模型高性能服务:SGLang Runtime 应用场景与实践
SGLang 是一个用于大型语言模型和视觉语言模型的推理框架。
|
数据采集 机器学习/深度学习 数据可视化
使用Jupyter Notebook进行数据分析:入门与实践
【6月更文挑战第5天】Jupyter Notebook是数据科学家青睐的交互式计算环境,用于创建包含代码、方程、可视化和文本的文档。本文介绍了其基本用法和安装配置,通过一个数据分析案例展示了如何使用Notebook进行数据加载、清洗、预处理、探索、可视化以及建模。Notebook支持多种语言,提供直观的交互体验,便于结果呈现和分享。它是高效数据分析的得力工具,初学者可通过本文案例开始探索。
1233 3
|
机器学习/深度学习 人工智能 边缘计算
为何人们喜欢推理胜于训练大模型?
在AI和机器学习领域,越来越多的人转向重视推理而非大规模模型训练。推理的即时性和高效性使其在需要快速响应的场景中占优,如自然语言处理和图像识别。推理过程的可视化能帮助用户理解模型决策,便于调试和提升性能。此外,推理在边缘计算和移动设备上的应用降低了延迟和带宽成本,同时保护了用户隐私。相比于训练大模型的高资源消耗,推理更为节能且成本效益高,尤其在数据挖掘和新知识探索方面展现出创新潜力。推理在实际应用中与训练模型相结合,提供了性能与成本的有效平衡。随着技术进步,推理将在推动人工智能领域发展中发挥更大作用。
|
12月前
|
Ubuntu Linux Docker
Ubuntu22.04上Docker的安装
通过以上详细的安装步骤和命令,您可以在Ubuntu 22.04系统上顺利安装
8391 12
|
存储 物联网 Serverless
什么是Serverless
Serverless是一种云计算架构模式,让开发者通过第三方服务直接构建和部署应用。在此模式下,应用被分解为无状态函数,由云平台统一管理。开发者仅需关注业务逻辑编写,无需管理底层服务器及基础设施。这种方式具有诸多优点:无需管理服务器,可专注业务逻辑;能够根据负载自动调整资源,实现按需计费;具备高可用性和灵活性,并支持多种编程语言。此外,Serverless应用按需启动和停止,无请求时不消耗资源,进一步提高了效率和成本效益。已被广泛应用于Web应用、移动应用、数据分析和物联网等领域。
942 3
|
关系型数据库 MySQL 数据库
navicat 查看,设计并导出数据库 ER图
navicat 查看,设计并导出数据库 ER图
2754 5
|
图形学 C++ C#
Unity插件开发全攻略:从零起步教你用C++扩展游戏功能,解锁Unity新玩法的详细步骤与实战技巧大公开
【8月更文挑战第31天】Unity 是一款功能强大的游戏开发引擎,支持多平台发布并拥有丰富的插件生态系统。本文介绍 Unity 插件开发基础,帮助读者从零开始编写自定义插件以扩展其功能。插件通常用 C++ 编写,通过 Mono C# 运行时调用,需在不同平台上编译。文中详细讲解了开发环境搭建、简单插件编写及在 Unity 中调用的方法,包括创建 C# 封装脚本和处理跨平台问题,助力开发者提升游戏开发效率。
1432 0