pytorch原生支持的apex混合精度和nvidia apex混合精度AMP技术加速模型训练效果对比

目录

一、apex原理简介

1、apex和amp

2、为什么要使用低精度

3、Fp16带来的问题和解决办法

a、溢出错误

b、舍入误差

二、apex的两种方式

1、NVIDIA apex

2、torch 原生支持的apex

三、模型训练效果对比

1、时间

2、内存

3、loss趋势

4、模型准确率


最近在训练模型的时候,接触到使用apex混合精度来提升模型训练速度的技术,在仔细了解学习和使用后,觉得有必要进行一个博客输出。

一、apex原理简介

1、apex和amp

从英伟达网页Apex (A PyTorch Extension) — Apex 0.1.0 documentation可以得到apex的全称——A PyTorch Extension(Apex)——其实就是一种pytorch的拓展插件。它的目的就是为了是用户能够快速实现amp——自动混合精度技术在它们的N卡系列上训练模型也构建的一个库。简简单单的在你自定义的模型训练代码中添加少量代码(4行代码)就能够使用自动混合精度的技术来提高模型的训练速度,提高生产力。简单的理解apex就是一个用来支持模型训练在pytorch框架下使用混合精度进行加速训练的拓展插件之类的库;也可以理解为一种模型训练加速技术——其实是amp自动混合精度搭配硬件一起才能加速的。

amp——auto mixed  precision——自动半精度,它的最核心的东西在于低精度Fp16。它能够提供一种非常可靠和友好的方式进行模型在Fp16精度下进行训练。

2、为什么要使用低精度

深度学习系统大都采用的都是Fp32来进行表示的,随着模型越来越大,已经硬件的进步,加速训练模型的需求就产生了。现在深度学习系统中广泛使用Fp32进行表示,主要存在2个问题,第一模型尺寸大,训练的时候对显卡的显存要求高;第二模型训练速度慢。

与单精度float(32bit,4个字节)相比,半进度float16仅有16bit,2个字节组成。很明显可以换看到,使用Fp16可以解决或者缓解上面fp32的两个问题,fp16的优势就是:

显存占用更少:通用的模型 fp16 占用的内存只需原来的一半,训练的时候可以使用更大的batchsize。

计算速度更快:有论文指出半精度的计算吞吐量可以是单精度的 2-8 倍。

由于fp16 的有效的动态范围约为 (  ),比单精度的float要狭窄很多,精度下降(数点后16相比较小数点后8位要精确的多),那么必然就会产生一系列的问题。如何安全有效的达成使用低精度训练模型显存占用减小和时间减少而不出问题,这里就要使用到一套训练技术和适配的硬件支持了——技术就是apex提供的amp和N系显卡某些型号。

3、Fp16带来的问题和解决办法

上面说到Fp16会导致一些问题,使得模型训练可能出差,那么到底会有那些问题呢?

a、溢出错误

Fp16表示的范围是(6\times 10^{-8}\sim 65504),Fp32表示的范围是(1.4\times 10^{-45}\sim 1.7\times 10^{38}),前者的范围比后者的小很多。当由Fp32计算转化为Fp16的时候,有很大的概率会出现溢出错误。当一个数比6\times 10^{-8}还小的时候,Fp16就表示不了了会出现下溢出Underflow;当一个数比65504还要大的时候,就会出现上溢出Overflow。

一般而言,深度学习模型训练过程中,神经网络的激活函数的梯度比权重的梯度要小,也更容易出现下溢出错误。当出现上下出错误的时候,反向传播中误差累积可以把这些数字变成0或者 nans; 这会导致不准确的梯度更新,影响你的网络收敛。。

b、舍入误差

 舍入误差指的是当梯度过小,小于当前区间内的最小间隔时,该次梯度更新可能会失败。

知乎——【PyTorch】唯快不破:基于Apex的混合精度加速 - 知乎——上找了一张图:

发生舍入误差的时候,权重得不到更新。

针对上述问题,解决的办法是什么呢?聪明的研究人员发明了——混合精度训练+动态损失放大——这种高明的技术来解决上述问题。原理很复杂,简单的描述一下大致思想和步骤:

a、混合精度训练

在内存中用FP16做储存和乘法从而加速计算,用FP32做累加避免舍入误差。这样在权重更新的时候就不会出现舍入误差导致更新失败。

b、损失放大

使用了混合精度训练,还是会存在无法收敛的情况。当梯度值非常小,小到F16不能表示就会出现下溢出错误。就要对损失进行放大:

首先,反向传播之前,把损失变化手动增加2^{n}倍,这个时候反向传播过程中得到的中间变量激活函数梯度之类的不会溢出。

然后,反向传播后,又将权重梯度值缩小2^{n}倍,这样就解决了下溢出的错误。

当然这里仍然存在着上溢出的问题,那这个时候就直接跳过这一个步的权重更新。只要不是训练过程中一直出现上溢出的问题,训练就是正常的。

二、amp的两种方式

上文中大致描述了半精度训练的一些理论知识,实现层面到底怎么操作呢?首先要有一定的硬件支持——Tensor Core;其次就是要运用相关的代码来实现半精度自动训练。

1、NVIDIA apex

由英伟达开发了一个支持半精度自动训练的pytorch拓展插件,添加几行代码就能实现自动半精度训练。代码流程如下:

from apex import amp

>>>>>>>>>>
other code
>>>>>>>>>>
model, optimizer = amp.initialize(model, optimizer, opt_level="O1") # 这里是“欧一”,不是“零一”
with amp.scale_loss(loss, optimizer) as scaled_loss:
    scaled_loss.backward()#梯度自动缩放
optimizer.step()#优化器更新梯度
>>>>>>>>>>
other code
>>>>>>>>>>

这个插件库很好用,解决了一些模型训练显卡显存不足和训练时间长的问题。这里也有一些坑,主要是apex 库安装比较麻烦,不太能顺利安装成功。apex github给出的安装方法:

下载github上的代码和文件,然后通过 pip 安装,不能直接 pip install apex

$ git clone https://github.com/NVIDIA/apex
$ cd apex
$ pip install -v --no-cache-dir --global-option="--cpp_ext" --global-option="--cuda_ext" ./

上述安装步骤中

pip install -v --no-cache-dir --global-option="--cpp_ext" --global-option="--cuda_ext" ./

非常容易报错,非常注意的点就是:

pytorch和cuda版本一定要一致;显卡驱动也要适配;还有其他的依赖库najia等也要安装。

2、torch 原生支持的amp

最简单了,只需要安装有pytorch就能使用,而且代码也简单。限制条件只有一个就是pytorch的版本一定>1.6。主要是利用了这两个API——torch.cuda.amp.GradScalar 和 torch.cuda.amp.autocast。

标准流程代码如下:

from torch.cuda.amp import autocast as autocast, GradScaler

>>>>>>>>
other code
>>>>>>>>

# 在训练最开始之前实例化一个GradScaler对象
scaler = GradScaler()

>>>>>>>>
other code
>>>>>>>>
        # 前向过程(model + loss)开启 autocast
        with autocast():
            output = model(input)
            loss = loss_fn(output, target)

        # Scales loss,这是因为半精度的数值范围有限,因此需要用它放大
        scaler.scale(loss).backward()

        # scaler.step() unscale之前放大后的梯度,但是scale太多可能出现inf或NaN
        # 故其会判断是否出现了inf/NaN
        # 如果梯度的值不是 infs 或者 NaNs, 那么调用optimizer.step()来更新权重,
        # 如果检测到出现了inf或者NaN,就跳过这次梯度更新,同时动态调整scaler的大小
        scaler.step(optimizer)

        # 查看是否要更新scaler,这个要注意不能丢
        scaler.update()


>>>>>>>>
other code
>>>>>>>>

三、模型训练效果对比

amp效果到底如何,下面训练一个模型看看实际情况。

显卡采用的是:RTX TITAN 24G显存,任务是NER,模型是Bert

简单放一放模型训练部分的代码:

train_without_amp

    for epoch in tqdm(range(args.epochs), desc='Epoch'):
        pbar = ProgressBar(n_total=len(train_dataloader), desc='Training Iteraction')
        for step, batch in enumerate(train_dataloader):
            model.train()
            batch = tuple(t.to(args.device) for t in batch)
            inputs = {'input_ids': batch[0], 'attention_mask': batch[1], 'bio_labels': batch[2]}
            model.zero_grad()
            optimizer.zero_grad()
            outputs = model(**inputs)
            loss = outputs[0]
            loss.backward()
            optimizer.step()
            scheduler.step()
            total_loss += loss.item()
            losses.append(loss.item())
            pbar(step, {"loss": loss.item()})
            memo_info = pynvml.nvmlDeviceGetMemoryInfo(handle)
            gpu_use_rato.append(round(memo_info.used/1024/1024/1024,4))

train_torch_amp

    scaler = GradScaler()
    gpu_use_rato = []
    losses = []
    t1 = time.time()
    for epoch in tqdm(range(args.epochs), desc='Epoch'):
        pbar = ProgressBar(n_total=len(train_dataloader), desc='Training Iteraction')
        for step, batch in enumerate(train_dataloader):
            model.train()
            batch = tuple(t.to(args.device) for t in batch)
            inputs = {'input_ids': batch[0], 'attention_mask': batch[1], 'bio_labels': batch[2]}
            model.zero_grad()
            optimizer.zero_grad()
            torch.nn.utils.clip_grad_norm_(model.parameters(), args.max_grad_norm)
            with autocast():
                outputs = model(**inputs)
                loss = outputs[0]
            scaler.scale(loss).backward()
            scaler.unscale_(optimizer)
            scaler.step(optimizer)
            scaler.update()
            total_loss += loss.item()
            losses.append(loss.item())
            pbar(step, {"loss": loss.item()})
            memo_info = pynvml.nvmlDeviceGetMemoryInfo(handle)
            gpu_use_rato.append(round(memo_info.used/1024/1024/1024,4))

train_nvidia_amp

    model,optimizer = amp.initialize(model,optimizer,opt_level='O1')
    for epoch in tqdm(range(args.epochs), desc='Epoch'):
        pbar = ProgressBar(n_total=len(train_dataloader), desc='Training Iteraction')
        for step, batch in enumerate(train_dataloader):
            model.train()
            batch = tuple(t.to(args.device) for t in batch)
            inputs = {'input_ids': batch[0], 'attention_mask': batch[1], 'bio_labels': batch[2]}
            outputs = model(**inputs)
            loss = outputs[0]
            torch.nn.utils.clip_grad_norm_(amp.master_params(optimizer), args.max_grad_norm)
            model.zero_grad()
            optimizer.zero_grad()

            with amp.scale_loss(loss,optimizer) as scaled_loss:
                scaled_loss.backward()
            optimizer.step()
            total_loss += loss.item()
            losses.append(loss.item())
            scheduler.step()
            pbar(step, {"loss": loss.item()})
            memo_info = pynvml.nvmlDeviceGetMemoryInfo(handle)
            gpu_use_rato.append(round(memo_info.used/1024/1024/1024,4)) 

结果如下

1、时间

train_without_amp:135s

train_torch_amp:116s

train_nvidia_amp: 141s

使用torch.apex.amp获得了加速效果,时间减少14%;而nvidia_apex使得训练时间增加4.4%。

2、内存

train_without_amp: 4.3G

train_torch_amp:4.2G

train_nvidia_amp: 4.35G

同样的torch.cuda.amp这种方式在显存占用上也获得了收益2.3%

3、loss趋势

可以看到loss变化趋势都是大致相同的。

4、模型准确率

train_without_amp:recall = 0.8033,F1=0.7850

train_torch_amp:recall = 0.7941,F1=0.7646

train_nvidia_amp: recall = 0.7970,F1=0.7726

可以发现recall和F1都掉了大致1-2个百分点,综合考量相比较nvidia,还是使用torch.cuda.amp好一点。

参考文章

Pytorch 1.6使用自动混合精度训练(AMP)

使用AMP得到更高效的PyTorch模型

浅谈混合精度训练

【PyTorch】唯快不破:基于Apex的混合精度加速

PyTorch AMP(Automatic Mixed Precision)是一种用于深度学习模型训练加速技术,它可以将低精度的计算操作与高精度的计算操作混合使用,从而在保持模型精度的同时提高训练速度和减少显存占用。具体来说,PyTorch AMP 使用了 NVIDIA Apex 库中的混合精度训练技术,将一些计算操作转换为 FP16(半精度浮点数)格式,从而减少计算和存储的需求。 实现 PyTorch AMP 混合精度训练的步骤如下: 1. 引入必要的库和模块: ```python import torch from torch.cuda.amp import autocast, GradScaler ``` 2. 定义模型和优化器: ```python model = MyModel() optimizer = torch.optim.SGD(model.parameters(), lr=1e-3) ``` 3. 定义混合精度训练相关的参数: ```python scaler = GradScaler() ``` 4. 在训练过程中使用 autocast 和 GradScaler 完成混合精度训练: ```python for data, target in train_loader: # 将数据和目标值转换为合适的类型 data, target = data.to(device), target.to(device) # 使用 autocast 进行前向计算和反向传播 with autocast(): output = model(data) loss = loss_function(output, target) # 使用 GradScaler 进行梯度缩放和反向传播 scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() # 清空梯度 optimizer.zero_grad() ``` 在上面的代码中,autocast 用于自动将一些计算操作转换为 FP16 格式,从而提高训练速度;GradScaler 用于梯度缩放和反向传播,确保在低精度的计算下仍能保持模型精度。 需要注意的是,不是所有的计算操作都能够使用 FP16 格式,一些数值较大的计算操作可能会出现溢出等问题。因此,在使用 PyTorch AMP 进行混合精度训练时,需要仔细选择转换的计算操作,并且进行必要的检查和调整。
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值