第十八章 番外篇:混合精度训练

news2024/11/22 18:15:48

参考教程:
https://pytorch.org/tutorials/recipes/recipes/amp_recipe.html?highlight=amp
https://pytorch.org/docs/stable/amp.html
https://arxiv.org/pdf/1710.03740.pdf
https://zhuanlan.zhihu.com/p/79887894

文章目录

  • 原理
    • float 32
    • float 16
    • 混合精度
  • 代码实现
    • torch.amp
    • torch.autocast
    • torch.cuda.amp.GradScaler
    • GradScaler with penalty

原理

float 32

参考资料:wikipedia/float32

float32的格式如上图。一共三十二位。

  1. 蓝色区域占一位:表示sign。
  2. 绿色区域占八位:表示exponent。
  3. 红色区域占23位:表示fraction。

它的计算规则如下:
在这里插入图片描述
假如你的 Exponent位全为0,那么fraction位有两种情况。

  • fraction全为0,则最终结果为0。
  • fraction不为0,则表示一个subnormal numbers,也就是一个非规格化的浮点数。
    ( − 1 ) s i g n × 2 − 126 × ( f r a c t i o n 2 23 ) (-1)^{sign} \times2^{-126} \times(\frac{fraction}{2^{23}}) (1)sign×2126×(223fraction)

假如你的Exponent位全为1,那么fraction位有两种情况。

  • fraction全为0, 则最终结果为inf。
  • fraction不为0,则最终结果为NaN。

假如你的Exponent是其他的情况,那么数据的计算公式为:
( − 1 ) s i g n × 2 ( e x p o n e n t − 127 ) × ( 1 + f r a c t i o n 2 23 ) (-1)^{sign}\times2^{(exponent-127)}\times(1+\frac{fraction}{2^{23}}) (1)sign×2(exponent127)×(1+223fraction)
因此float32能取到的最小正数是 2 − 126 2^{-126} 2126,最大正数是 2 2 8 − 2 − 127 × ( 1 + 2 23 − 1 2 23 ) 2^{2^8-2-127}\times(1+\frac{2^{23}-1}{2^{23}}) 2282127×(1+2232231)

值得一提的是,float16表示的数是不均匀的,也就是不同的区间范围有着不一样的精度。具体的例子可以从wikipedia/float32上看。下面只给出一点点例子。

MinMaxInterval
1 2 2 2 2 − 23 2^{-23} 223
2 22 2^{22} 222 2 23 2^{23} 223 2 − 1 2^{-1} 21
2 127 2^{127} 2127 2 128 2^{128} 2128 2 104 2^{104} 2104

float 16

参考资料:wikipedia/float16
在这里插入图片描述
float16的格式如上图。一共十六位。

  1. 蓝色区域占一位:表示sign。
  2. 绿色区域占五位:表示exponent。
  3. 红色区域占十位:表示fraction。

它的计算规则如下:
在这里插入图片描述
假如你的 Exponent位全为0,那么fraction位有两种情况。

  • fraction全为0,则最终结果为0。
  • fraction不为0,则表示一个subnormal numbers,也就是一个非规格化的浮点数。
    ( − 1 ) s i g n × 2 − 14 × ( f r a c t i o n 1024 ) (-1)^{sign} \times2^{-14} \times(\frac{fraction}{1024}) (1)sign×214×(1024fraction)

假如你的Exponent位全为1,那么fraction位有两种情况。

  • fraction全为0, 则最终结果为inf。
  • fraction不为0,则最终结果为NaN。

假如你的Exponent为是其它情况,则进行正常的计算。
( − 1 ) s i g n × 2 ( e x p o n e n t − 15 ) × ( 1 + f r a c t i o n 1024 ) (-1)^{sign} \times2^{(exponent-15)} \times(1+\frac{fraction}{1024}) (1)sign×2(exponent15)×(1+1024fraction)

因此float16能取到的最小正数是 2 − 14 2^{-14} 214,最大数是 2 30 − 15 × ( 1 + 1023 1024 ) = 65504 2^{30-15}\times(1+\frac{1023}{1024}) = 65504 23015×(1+10241023)=65504

值得一提的是,float16表示的数是不均匀的,也就是不同的区间范围有着不一样的精度。具体的例子可以从wikipedia/float16上看。下面只给出一点点例子。

MinMaxInterval
0 2 − 13 2^{-13} 213 2 − 24 2^{-24} 224
2 − 9 2^{-9} 29 2 − 9 2^{-9} 29 2 − 19 2^{-19} 219
2 − 5 2^{-5} 25 2 − 4 2^{-4} 24 2 − 15 2^{-15} 215

混合精度

增大神经网络的规模,往往能带来效果上的提升。但是模型规模的增大也意味着更多的内存和计算量需求。综合来说,你的模型表现受到三个方面的限制:

  1. arithmetic bandwidth.
  2. memory bandwidth.
  3. latency.

在神经网络训练中,通常使用的数据类型都是float32,也就是单精度。而所谓的半精度,也就是float16。通过使用半精度,可以解决上面的两个限制。

  1. arithmetic bandwidth。在GPU上,半精度的吞吐量可以达到单精度的2到8倍。
  2. memory bandwidth。 半精度的内存占用是单精度的一半。

看起来半精度和单精度相比很有优势,当然,半精度也有半精度自身的问题。

以下部分参考自:https://zhuanlan.zhihu.com/p/79887894

  1. 溢出错误: float16的范围比float32的范围要小,所以也更容易溢出,更容易出现’NaN’的问题。
  2. 舍入误差: 当前更新的梯度过小时,即更新的梯度达不到区间间隔的大小时,可能会出现梯度更新失败的情况。

在论文中为了在使用float16的训练的同时又能保证模型的准确性,采用了以下三个方法:

  1. single-precision master weights and update。
    在训练过程在,weights,activation,gradients等数据都用float16存储,同时拷贝一个float32的weights,用来更新。这样float16的梯度在更新是又转为了float32,避免了舍入误差的问题。
  2. loss-scaling。
    为了解决梯度过小的问题,一个比较高效的方法是对计算出的loss进行scale。在更新梯度的时候,只要将梯度转为float32再将scale去掉就可以了。
  3. accumulation。
    网络中的数学计算可以分为以下三类:vector dot-products, reductions and point-wise opeartions。有的运算为了维持精度,必须使用float32,有的则可以使用float16。

代码实现

torch.amp

torch.amp提供了很方便的混合精度方法,一些计算会使用float32的类型而另一些则会用半精度float16。混合精度会尝试给每个操作旋转比较合适的类型。

在实现自动半精度训练时,通常会用到torch.autocasttorch.cuda.amp.GradScaler两个方法。

torch.autocast可以在保证模型表现的情况下,为不同的操作旋转合适的精度。

torch.cuda.amp.GradScaler和它的名字一样,帮助进行梯度的缩放,帮助使用flaot16的网络收敛。就像我们之前说的一样,可以减少溢出的问题。

torch.autocast

autocast提供了两种api。

torch.autocast(“cuda”, args…) is equivalent to torch.cuda.amp.autocast(args…).
torch.autocast(“cpu”, args…) is equivalent to torch.cpu.amp.autocast(args…).

autocast可以以上下文管理器context manager或者装饰器decorator的形式使用。允许你的这部分代码以混合精度的形式运行。

torch.autocast(device_type, dtype=None, enabled=True, cache_enabled=None)

传入参数包括:

  1. device_type: 当前设备类型,一般是cuda和cpu,也有xpu和hpu。
  2. dtype:如果你指定了dtype的类型,那么就会使用这个类型作为target dtype,如果没有的话就是按设备获取。
  3. enabled默认是True,假如你的dtype类型不支持或者device_type不对的时候,enabled就会被改为False,表示不可以使用autocast。

现在来看一下autocast的两种用法,第一种是当作context manger。

with autocast(device_type='cuda', dtype=torch.float16):
      output = model(input)
      loss = loss_fn(output, target)

在这个范围内的model的forward和loss的计算都会以半精度的形式进行。

第二种是当作decorator来使用。你可以直接用它来修饰你的模型的forward()过程。

class AutocastModel(nn.Module):
    ...
    @autocast()
    def forward(self, input):
        ...

这里要注意的是,在autocast范围内的计算结果得到的tensor可能会是float16的类型,当你想用这个结果在autocast的范围外进行别的计算时,要注意把它变回float32。

pytorch tutorial中给出了这样一个例子。

在下面这个例子中,a,b,c,d在创建时的类型都是float32,然后在autocast的范围内进行了torch.mm的计算,torch.mm是矩阵乘法,支持float16的半精度,所以这时你得到的结果e和f都是float16的类型。

a_float32 = torch.rand((8, 8), device="cuda")
b_float32 = torch.rand((8, 8), device="cuda")
c_float32 = torch.rand((8, 8), device="cuda")
d_float32 = torch.rand((8, 8), device="cuda")

with autocast():
    # torch.mm is on autocast's list of ops that should run in float16.
    # Inputs are float32, but the op runs in float16 and produces float16 output.
    # No manual casts are required.
    e_float16 = torch.mm(a_float32, b_float32)
    # Also handles mixed input types
    f_float16 = torch.mm(d_float32, e_float16)

# After exiting autocast, calls f_float16.float() to use with d_float32
g_float32 = torch.mm(d_float32, f_float16.float())

你想用float16类型的f和float32类型的d进行乘法运算是不行的,所以需要先把f变回float32。

pytorch中还给出了在autocast-enabled region的局部禁止使用autocast的例子,就是在代码内再次套一个emabled=False的autocast。

with autocast():
    e_float16 = torch.mm(a_float32, b_float32)
    with autocast(enabled=False):
        # Calls e_float16.float() to ensure float32 execution
        # (necessary because e_float16 was created in an autocasted region)
        f_float32 = torch.mm(c_float32, e_float16.float())

torch.cuda.amp.GradScaler

在更新的梯度过小时,可能会超出float16数字的边界,导致下溢出或者无法更新的情况。gradient scaling就是为了解决这个问题。

它在神经网络的loss上乘以一个缩放因子,这个缩放因子会随着反向传播传递,使得各个层的梯度都不至于过小。

torch.cuda.amp.GradScaler(init_scale=65536.0, growth_factor=2.0, backoff_factor=0.5, growth_interval=2000, enabled=True)

传入参数包括:

  1. init_scale:初始的缩放因子。
  2. growth_factor:你的缩放因子不是固定的,假如在训练过程中又发现了为NaN或inf的梯度,那么这个缩放因子是会按照growth_factor进行更新的。
  3. backoff_factor:我理解的是和growth_factor相反的过程。
  4. growth_interval:假如没有出现NaN/inf,也会按照interval进行scale的更新。

来看一下GradScaler的用法。

scaler = torch.cuda.amp.GradScaler()

for epoch in range(0): # 0 epochs, this section is for illustration only
    for input, target in zip(data, targets):
        with torch.autocast(device_type=device, dtype=torch.float16):
            output = net(input)
            loss = loss_fn(output, target)

        # Scales loss. Calls ``backward()`` on scaled loss to create scaled gradients.
        scaler.scale(loss).backward()

        # ``scaler.step()`` first unscales the gradients of the optimizer's assigned parameters.
        # If these gradients do not contain ``inf``s or ``NaN``s, optimizer.step() is then called,
        # otherwise, optimizer.step() is skipped.
        scaler.step(opt)

        # Updates the scale for next iteration.
        scaler.update()

        opt.zero_grad() # set_to_none=True here can modestly improve performance

我们来看一下在这个过程中GradScaler()都做了什么。

首先,在计算得到loss之后,出现了。

scaler.scale(loss).backward()

用我们的scaler中的scale方法对得到的loss进行缩放后,再进行backward()。scale方法也比较简单,返回的结果可以直接理解为我们的loss和缩放因子scale_factor的乘积。

然后会进行scaler.step(opt),这里的opt是我们的optimizer。我们常用的应该是opt.step()。也就是说在这里optimizer的step()梯度更新的步骤被放到scaler里完成了。

def step(self, optimizer, *args, **kwargs):它的作用是这样的。它首先会确认一下梯度里面是否存在Inf和NaN的情况,如果有的话,optimizer.step()这个步骤就会被跳过去,避免引发一些错误;如果没有的话,就会用unscale过的梯度来进行optimizer.step()。

最后呢则是 scaler.update(),也就是其中的scale的更新过程。

简而言之,scaler总共做了三件事:

  1. 对loss进行scale,这样在backward的时候防止梯度的underflow。
  2. 对梯度进行unscale,用于更新。
  3. 更新scaler中的scale factor。

GradScaler with penalty

tutorial中还额外提到了到penalty的情况。

在你没有使用混合精度的情况下,你的grad penalty是这样计算的。以L2 loss为例:

for input, target in data:
        optimizer.zero_grad()
        output = model(input)
        loss = loss_fn(output, target)

        # Creates gradients
        grad_params = torch.autograd.grad(outputs=loss,                                   inputs=model.parameters(),                                      create_graph=True)
        # Computes the penalty term and adds it to the loss
        grad_norm = 0
        for grad in grad_params:
            grad_norm += grad.pow(2).sum()
        grad_norm = grad_norm.sqrt()
        loss = loss + grad_norm

        loss.backward()

        # clip gradients here, if desired

        optimizer.step()

对于grad_params中的每一项,都求他的平方和,最后对grad_norm开根号,作为loss和我们的模型的预测损失组合在一起使用。起到了一种gradient regularization的作用。

当你要使用scaler时,因为你的loss是scale过的,所以你的梯度也都受到了影响,你应该把它们先变回去unscale的状态,再进行计算。

scaled_grad_params = torch.autograd.grad(outputs=scaler.scale(loss),                                                inputs=model.parameters(),                                              create_graph=True)

注意看这里用的是scale过的loss,得到的grad也是scale过的,所以你需要自己手动计算来得到unscale的grad和penalty loss。

inv_scale = 1./scaler.get_scale()
# scale_factor是缩放倍数,我们要unscale所以用1/scale_factor
grad_params = [p * inv_scale for p in scaled_grad_params]

之后就是正常的计算过程:


# Computes the penalty term and adds it to the loss
with autocast(device_type='cuda', dtype=torch.float16):
    grad_norm = 0
    for grad in grad_params:
        grad_norm += grad.pow(2).sum()
    grad_norm = grad_norm.sqrt()
    loss = loss + grad_norm
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()

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

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

相关文章

cmake编译mingw下使用的zlib

目录 一、准备 二、cmake构建 三、make编译 一、准备 zlib Home Site zlib1.2.11(2017.2.15) 二、cmake构建 有cmakeLists.txt,直接用cmake进行构建 然后点击generate,接下来只能用命令行编译,在build目录执行…

选购螺杆支撑座要考虑哪些因素?

为了可以保证螺杆支撑座的使用效果,同时也能够发挥出更好的使用功能,避免出现各种质量隐患,建议大家在购买的时候一定要在专业正规的厂家进行选购,那么,我们在选购的时候要考虑哪些方面的因素呢? 1、考虑到…

曲柄滑块运动学求解基于Matlab

参考文档: 曲柄滑块机构运动分析..doc-原创力文档 偏置曲柄滑块机构的运动学分析 - 豆丁网 偏置式曲柄滑块机构仿真与运动分析 - 豆丁网 https://www.cnblogs.com/aksoam/p/17013811.html function main %输入已知数据 close all clear; i1100; i2300; e56; hd …

统一异常处理,自定义异常

目录 一、制造异常 Swagger中测试 二、统一异常处理 1、创建统一异常处理器 2、测试 三、处理特定异常 1、添加依赖 2、添加异常处理方法 3、测试 4、恢复制造的异常 四、自定义异常 1、创建自定义异常类 2、添加异常处理方法 3、修改Controller 4、测试 返回异…

3.1 Bootstrap 字体图标(Glyphicons)

文章目录 Bootstrap 字体图标(Glyphicons)什么是字体图标?获取字体图标CSS 规则解释带有导航栏的字体图标定制字体图标定制字体尺寸定制字体颜色应用文本阴影 Bootstrap 字体图标(Glyphicons) 本章将讲解字体图标(Glyphicons),并通过一些实例了解它的使用…

【SpringBoot】SpringBoot的自动配置源码解析

文章目录 1. SpringBoot的自动配置概念2. SpringBoot自动配置的原理3. EnableAutoConfiguration4. 常用的Conditional注解 1. SpringBoot的自动配置概念 SpringBoot相对于SSM来说,主要的优点就是简化了配置,不再需要像SSM哪有写一堆的XML配置&#xff0…

SQLSERVER的truncate和delete有区别吗?

一:背景 1. 讲故事 在面试中我相信有很多朋友会被问到 truncate 和 delete 有什么区别 ,这是一个很有意思的话题,本篇我就试着来回答一下,如果下次大家遇到这类问题,我的答案应该可以帮你成功度过吧。 二&#xff1…

全网最细,Pytest自动化框架fixture和conftest.py实战详解(细致)

目录:导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结(尾部小惊喜) 前言 fixture说明 fix…

你一定不知道的自动化测试的9大规则

目录 前言 应该去做的事情 雇用合适的人 在寻找正确的测试自动化工具方面花点时间 轻装上阵 让开发人员参与到自动化过程中来 在ci/cd上投资时间 不应该做的事情 不要因为一个工具被追捧就选择它 不要试图将一切都自动化 不要太早实现自动化 永远不要用自动化来取代…

C语言-报错集锦-02-munmap_chunk(): invalid pointer: 0x0000000001d2e150 ***

一、报错信息 [2023-7]--[ Debug ]--Destroy DqlResult Struct OK [2023-7]--[ Debug ]--Destroy Moia Base Job : OK [2023-7]--[ Debug ]--Destroy Moia Base Job : OK [2023-7]--[ Debug ]--Destroy Moia Base Job : OK [2023-7]--[ Debug ]--Destroy Mo…

Redis学习(二)线程安全、分布式锁、消息队列

文章目录 优惠券秒杀全局ID生成器优惠券秒杀下单超卖问题一人一单 分布式锁基于Redis的setnx指令实现分布式锁解决锁误删问题基于Lua脚本实现多条指令原子性Redis调用Lua脚本Java中使用Lua脚本 RedissonRedisson快速入门Redisson可重入锁原理Redisson的锁重试和Watchdog机制Red…

【经济调度】基于多目标宇宙优化算法优化人工神经网络环境经济调度研究(Matlab代码实现)

💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 ⛳️座右铭&a…

适配各类大模型应用!手把手教你选择 Zilliz Cloud 实例类型

作为大模型时代备受关注的细分赛道,向量数据库可以不仅为大模型提供存储和向量检索的功能,还能适配各种 AI 应用场景,例如聊天机器人、内容审核、增强 LLM 知识库等。 不过,对于向量数据库的开发者而言,成本是绕不开的…

Spring学习笔记---SpringBoot快速入门

Spring学习笔记---SpringBoot快速入门 Spring学习笔记---SpringBoot1 SpringBoot简介1.1 SpringBoot快速入门1.1.1 开发步骤1.1.1.1 创建新模块1.1.1.2 创建 Controller1.1.1.3 启动服务器1.1.1.4 进行测试 1.1.2 对比1.1.3 官网构建工程1.1.3.1 进入SpringBoot官网1.1.3.2 选择…

(二)springboot实战——springboot基于多端内容协商适配实现json、xml、yaml等格式数据统一返回

前言 在实际应用开发场景中,我们有需求实现多端内容请求的适配,例如某些客户端需要返回json数据,有些客户端需要返回xml数据,有些客户端要返回yaml数据,这个时候就需要服务端做内容返回的适配,如果按照提供…

搭建vsto的clickonce一键发布IIS环境FTP

要在 Windows 上启用 IIS(Internet Information Services),可以按照以下步骤进行操作:1. 打开“控制面板”:点击 Windows 开始菜单,然后在搜索栏中输入“控制面板”,并选择相应的结果。2. 打开“…

Maven 项目构建生命周期

Maven 项目构建生命周期 一句话: Maven 构建生命周期描述的是一次构建过程经历了多少个事件 生命周期的3 大阶段 clean 清理工作 default 核心工作,例如编译,测试,打包,部署等 site 产生报告,发布站点等 生命周期…

zsh自定义命令行提示符

环境: oh-my-zsh 插件 效果: 本来的样子:感觉元素很多,比较挤占地方 现在的样子:简洁了很多 步骤: 打开主题的配置文件(我的主题是agnoster ) cd /Users/你的家目录/.oh-my-zsh/the…

Windows操纵kafka

这里写目录标题 启动kafk创建一个测试主题查看所有主题查看first详细信息修改分区数(分区数只能增加 不能减少)删除主题生产者生产数据消费命令 启动kafk 安装目录下 .\bin\windows\kafka-server-start.bat .\config\server.properties创建一个测试主题 安装目录下 .\bin\wi…

【Unity2D像素风格小游戏】期末考考完,和搭档一个月从零开始的Unity速成作品!

游戏实况视频 六月十八号,期末考完后,大佬搭档和我开始自学unity,并在七月一号正式开始一个unity2D像素小游戏的制作,这是一段很有意义,很有收获的日子。 这个项目由搭档提出,另一位超级大佬进行前期指导…