Transformer 训练优化

news2025/1/15 6:47:35

前言

自 BERT 出现以来,NLP 领域已经进入了大模型的时代,大模型虽然效果好,但是毕竟不是人人都有着丰富的 GPU 资源,在训练时往往就捉襟见肘,出现显存 out of memory 的问题,或者训练时间非常非常的久,因此,这篇文章主要解决的问题就是如何在 GPU 资源受限的情况下训练 transformers 库上面的大模型。

这篇文章源自 @Vadim Irtlach 大佬在 Kaggle 的开源 notebook,感谢原作者的分享,本 NLP 小白觉得受益良多,因此搬运分享给大家,已取得作者授权,大部分内容是照搬翻译过来的,小部分内容结合自己的理解进行了补充和修改,不对的地方请大家批评指正。

尽管 Huggingface 开源的 Transformers 在自然语言处理(NLP)任务中取得了惊人的成功,但由于里面的模型参数数量庞大,即使是使用 GPU 进行训练或者部署,也仍具有非常大的挑战性,因为用如此大的模型进行训练或推理,会很容易发生显存不足(OOM)以及训练时间过长的问题。(这里想吐槽一句的是,kaggle 上面的 NLP 比赛现在动不动就用五折 debert-large-v3,没几块 V100 根本玩不起这种比赛,所以这篇文章对我这种只能用 colab 的 p100 来跑实验的穷学生来说真的是福音啊!)

然而,有很多方法可以避免显存不足以及训练时间过长的方法,这篇文章的主要贡献就是介绍了这些方法的原理以及如何实现,具体包括以下几种方法:

  1. 梯度累积(Gradient Accumulation)

  2. 冻结(Freezing)

  3. 自动混合精度(Automatic Mixed Precision)

  4. 8位优化器(8-bit Optimizers)

  5. 梯度检查点(Gradient Checkpointing)

  6. 快速分词器(Fast Tokenizers)

  7. 动态填充(Dynamic Padding)

  8. 均匀动态填充(Uniform Dynamic Padding)

其中 1-5 是神经网络通用的方法,可以用在任何网络的性能优化上,6-8 是针对 NLP 领域的性能优化方法。

1 梯度累积

梯度累积背后的想法非常简单,就是为了模拟更大的批量(batch)。有时,为了更好地收敛或提高性能,需要使用大批量进行训练,但是,这通常需要更大的显存。这个问题的一种可能的解决方案是使用较小的批量,但是,一方面,小批量训练会增加训练和推理时间,另一方面,梯度下降算法对批量大小的选择非常敏感,小批量可能会导致不稳定的收敛和性能降低。

所以,我们可以先执行几次前向传播和反向传播,使得梯度进行累积,当我们有足够的计算梯度时,再对参数进行优化,从而利用小显存,模拟大批量的效果,并且训练时间也不会大幅增加。

代码实现

steps = len(loader)

# perform validation loop each `validation_steps` training steps!
validation_steps = int(validation_steps * gradient_accumulation_steps)

for step, batch in enumerate(loader, 1):

    # prepare inputs and targets for the model and loss function respectively.

    # forward pass
    outputs = model(inputs)

    # computing loss
    loss = loss_fn(outputs, targets)

    # accumulating gradients over steps
    if gradient_accumulation_steps > 1:
        loss = loss / gradient_accumulation_steps

    # backward pass
    loss.backward()

        # perform optimization step after certain number of accumulating steps and at the end of epoch
    if step % gradient_accumulation_steps == 0 or step == steps:
        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm)
        optimizer.step()
        model.zero_grad()

            # perform validation loop
    if step % validation_steps == 0:
        validation_loop()

2 冻结

冻结是一种非常有效的方法,通过取消计算模型某些层中的梯度计算(如 embedding 层,bert 的前几层),可以大大加快训练速度并且降低了显存占用,而且几乎不会损失模型的性能。

深度学习中的一个众所周知的事实是,网络的底层学习输入数据的通用特征,而网络顶层学习目标任务特定的高级特征,所以在对预训练模型进行微调时,一般网络底层的参数都不怎么需要变,这些都是通用的知识,需要学习的是顶层的那些参数,当使用某种优化算法(如 SGD、AdamW 或 RMSprop)执行优化步骤时,网络的底层的梯度就都很小,因此参数几乎保持不变,这也被称为梯度消失,因此,与其花费大量的时间和算力来计算底层这些“无用”梯度,并对此类梯度很小的参数进行优化,不如直接冻结它们,直接不计算梯度也不进行优化。

PyTorch 为关闭梯度计算提供了一个舒适的 API,可以通过 torch.Tensor 的属性 requires_ grad 设置。

代码实现

def freeze(module):
    """
    Freezes module's parameters.
    """
    for parameter in module.parameters():
        parameter.requires_grad = False

def get_freezed_parameters(module):
    """
    Returns names of freezed parameters of the given module.
    """

    freezed_parameters = []
    for name, parameter in module.named_parameters():
        if not parameter.requires_grad:
            freezed_parameters.append(name)

    return freezed_parameters
import torch
from transformers import AutoConfig, AutoModel


# initializing model
model_path = "microsoft/deberta-v3-base"
config = AutoConfig.from_pretrained(model_path)
model = AutoModel.from_pretrained(model_path, config=config)


# freezing embeddings and first 2 layers of encoder
freeze(model.embeddings)
freeze(model.encoder.layer[:2])

freezed_parameters = get_freezed_parameters(model)
print(f"Freezed parameters: {freezed_parameters}")

# selecting parameters, which requires gradients and initializing optimizer
model_parameters = filter(lambda parameter: parameter.requires_grad, model.parameters())
optimizer = torch.optim.AdamW(params=model_parameters, lr=2e-5, weight_decay=0.0)

3 自动混合精度

自动混合精度(AMP)是另一种在不损失最终质量的情况下减少显存消耗和训练时间的方法,该方法由 NVIDIA 和百度研究人员在 2017 年的 “Mixed Precision Training” 论文中提出。该方法背后的关键思想是使用较低的精度将模型的梯度和参数保留在内存中,即不使用全精度(float32),而是使用半精度(例如 float16)将张量保存在内存中。然而,当以较低精度计算梯度时,某些值可能太小,以至于被视为零,这种现象被称为“溢出”。为了防止“溢出”,原始论文的作者提出了一种梯度缩放方法。

PyTorch从1.6 的版本开始提供了一个包:torch.cuda.amp,具有使用自动混合精度所需的功能(从降低精度到梯度缩放),自动混合精度作为上下文管理器实现,因此可以随时随地的插入到训练和推理脚本中。

代码实现

from torch.cuda.amp import autocast, GradScaler


scaler = GradScaler()

for step, batch in enumerate(loader, 1):

    # prepare inputs and targets for the model and loss function respectively.

    # forward pass with `autocast` context manager
    with autocast(enabled=True):
        outputs = model(inputs)

    # computing loss
    loss = loss_fn(outputs, targets)

    # scale gradint and perform backward pass
    scaler.scale(loss).backward()

    # before gradient clipping the optimizer parameters must be unscaled.
    scaler.unscale_(optimizer)

    # perform optimization step
    torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm)

    scaler.step(optimizer)
    scaler.update()

4 8-bit Optimizers

8-bit Optimizers 的思想类似于自动混合精度(模型的参数和梯度使用较低的精度保存),但 8-bit Optimizers 还让优化器的状态使用低精度保存。作者(Meta Research)在最初的论文 “8-bit Optimizers via Block-wise Quantization” 中详细介绍了 8-bit Optimizers,表明 8-bit Optimizers 显著降低了显存占用,略微加快了训练速度。

此外,作者研究了不同超参数设置的影响,表明 8-bit Optimizers 对不同的学习率、beta 和权重衰减参数的效果是稳定的,不会降低性能或影响收敛性。因此,作者为 8 位优化器提供了一个高级库,叫做 bitsandbytes。

代码实现

!pip install -q bitsandbytes-cuda110
def set_embedding_parameters_bits(embeddings_path, optim_bits=32):
    """
    https://github.com/huggingface/transformers/issues/14819#issuecomment-1003427930
    """

    embedding_types = ("word", "position", "token_type")
    for embedding_type in embedding_types:
        attr_name = f"{embedding_type}_embeddings"

        if hasattr(embeddings_path, attr_name): 
            bnb.optim.GlobalOptimManager.get_instance().register_module_override(
                getattr(embeddings_path, attr_name), 'weight', {'optim_bits': optim_bits}
            )

import bitsandbytes as bnb


# selecting parameters, which requires gradients
model_parameters = filter(lambda parameter: parameter.requires_grad, model.parameters())

# initializing optimizer 
bnb_optimizer = bnb.optim.AdamW(params=model_parameters, lr=2e-5, weight_decay=0.0, optim_bits=8)
# bnb_optimizer = bnb.optim.AdamW8bit(params=model_parameters, lr=2e-5, weight_decay=0.0) # equivalent to the above line

# setting embeddings parameters
set_embedding_parameters_bits(embeddings_path=model.embeddings)

print(f"8-bit Optimizer:\n\n{bnb_optimizer}")

5 梯度检查点

有时候,即使用了上面的几种方法,显存可能还是不够,尤其是在模型足够大的情况下。那么梯度检查点(Gradient Checkpointing)就是压箱底的招数了,这个方法第一次在 “Training Deep Nets With Sublinear Memory Cost”,作者表明梯度检查点可以显著降低显存利用率,从 O(n) 降低到 ,其中 n 是模型的层数。这种方法允许在单个 GPU 上训练大型模型,或者提供更多内存以增加批量大小,从而更好更快地收敛。 

梯度检查点背后的思想是在小数据块中计算梯度,同时在正向和反向传播过程中从内存中移除不必要的梯度,从而降低内存利用率,但是这种方法需要更多的计算步骤来再现整个反向传播图,其实就是一种用时间来换空间的方法。

▲ 梯度检查点如何在正向和反向传播过程中工作

PyTorch框架里也有梯度检查点的实现,通过这两个函数:torch.utils.checkpoint.checkpoint  和 torch.utils.checkpoint.checkpoint_sequential 这边引用一段 torch 官网对梯度检查点的介绍。

梯度检查点通过用计算换取内存来工作。检查点部分不是存储整个计算图的所有中间激活以进行反向计算,而是不保存中间激活,而是在反向过程中重新计算它们。它可以应用于模型的任何部分。具体而言,在前向传播中,该函数将以 torch.no_grad() 的方式运行,即不存储中间激活。然而,前向传播保存了输入元组和函数参数。在反向传播时,检索保存的输入和函数,然后再次对函数进行前向传播,现在跟踪中间激活,然后使用这些激活值计算梯度。

此外,HuggingFace Transformers 也支持梯度检查点。梯度检查点可以通过 PreTrainedModel 实例的 gradient_checkpointing_enable 方法执行,一行代码直接搞定!

代码实现

from transformers import AutoConfig, AutoModel
# https://github.com/huggingface/transformers/issues/9919
from torch.utils.checkpoint import checkpoint

# initializing model
model_path = "microsoft/deberta-v3-base"
config = AutoConfig.from_pretrained(model_path)
model = AutoModel.from_pretrained(model_path, config=config)

# gradient checkpointing
model.gradient_checkpointing_enable()
print(f"Gradient Checkpointing: {model.is_gradient_checkpointing}")

6 快速分词器

HuggingFace Transformers 提供两种类型的分词器:基本分词器和快速分词器。它们之间的主要区别在于,快速分词器是在 Rust 上编写的,因为 Python 在循环中非常慢,但在分词的时候又要用到循环。快速分词器是一种非常简单的方法,允许我们在分词的时候获得额外的加速。要使用快速分词器也很简单,只要把 transformers.AutoTokenizer 里面的 from_pretrained 方法的 use_fast 的值修改为 True 就可以了。

▲ 分词器是如何工作的 

代码实现

from transformers import AutoTokenizer

# initializing Base version of Tokenizer
model_path = "microsoft/deberta-v3-base"
tokenizer = AutoTokenizer.from_pretrained(model_path, use_fast=False)
print(f"Base version Tokenizer:\n\n{tokenizer}", end="\n"*3)

# initializing Fast version of Tokenizer
fast_tokenizer = AutoTokenizer.from_pretrained(model_path, use_fast=True)
print(f"Fast version Tokenizer:\n\n{fast_tokenizer}")

7 动态填充

通常来说,模型是用批量数据输入训练的,批中的每个输入必须具有固定大小,即一批量的数据必须是矩阵的表示,所有批量数据的尺寸都一样。固定尺寸通常是根据数据集中的长度分布、特征数量和其他因素来选择的。在 NLP 任务中,输入大小称为文本长度,或者最大长度(max length)。

然而,不同的文本具有不同的长度,为了处理这种情况,研究人员提出了填充标记和截断。当最大长度小于输入文本的长度时,会使用截断,因此会删除一些标记。当输入文本的长度小于最大长度时,会将填充标记,比如 [PAD],添加到输入文本的末尾,值得注意的是,填充标记不应包含在某些任务的损失计算中(例如掩蔽语言建模或命名实体识别)

▲ 固定长度填充

然而,填充标记有明显的缺点。比如在输入文本相对于选定的最大长度非常短的情况下,效率就很低,需要更多的额外内存,比如我有一条文本长度 512,然后其他文本长度都在 10 左右,那么如果将 max seq 设置为 512,就会导致很多无效计算。

为了防止额外的计算操作,研究人员提出了一种非常有效的方法,就是将批量的输入填充到这一批量的最大输入长度,如下图所示,这种方法可以将训练速度提高 35% 甚至 50%,当然这种方法加速的效果取决于批量的大小以及文本长度的分布,批量越小,加速效果越明显,文本长度分布越不均,加速效果也越好。

▲ 动态填充

8 均匀动态填充

还有一种基于动态填充的方法,叫做均匀动态填充。其思想是在分 batch 时,先按文本的长度对文本进行排序,这样同一个 batch 里面的文本长度就都差不多。这种方法非常有效,在训练或推理期间的计算量都比动态填充要来的少。但是,不建议在训练期间使用均匀动态填充,因为训练时数据最好是要 shuffer 的,但是推理时如果一次性要推理很多文本的话可以考虑这么做

▲ 均匀动态填充

9 总结

即使在现代 GPU 上,优化内存和时间也是开发模型的必要步骤,因此,本文介绍了加速训练和减少 transformers 等大型模型内存消耗的最强大、最流行的方法。

参考文献

[1] Performance and Scalability: How To Fit a Bigger Model and Train It Faster

https://huggingface.co/docs/transformers/performance

[2] Speeding up Transformer w/ Optimization Strategies

https://www.kaggle.com/code/rhtsingh/speeding-up-transformer-w-optimization-strategies

[3] Things you can try to speed up training speed and preventing memory shortage if you are using transformers.

https://www.kaggle.com/competitions/AI4Code/discussion/327777

[4] 8-bit Adam and other memory optimizations

https://www.kaggle.com/competitions/feedback-prize-2021/discussion/303131

[5] Fitting larger networks into memory.

https://medium.com/tensorflow/fitting-larger-networks-into-memory-583e3c758ff9

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

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

相关文章

web期末大作业:基于html+css+js制作深圳大学网站(13页) 学校班级网页制作模板 学生静态HTML网页源码

🎉精彩专栏推荐 💭文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 💂 作者主页: 【主页——🚀获取更多优质源码】 🎓 web前端期末大作业: 【📚毕设项目精品实战案例 (10…

【easypoi 模板导出嵌套 list 问题】

easypoi 模板导出多 list 问题1背景1.1 espoi 模板定义1.2 导出结果1.3发现共享单车的数据没有显示,手动操作取消共享单车的单元格合并1.4手动取消单元格合并后数据,正常再把框线画好1.5 代码操作:用 esaypoi 处理到1.2,用基础 po…

聊聊与前端工程师天然互补的 Serverless

作为前端工程师,我们的使命是为用户提供良好的前端用户体验。随着云原生时代的到来,显而易见的,我们能做的更多了。Serverless 产品的特点是免运维、按量付费和自适应弹性,所以我们可以利用云上的各种 Serverless 能力&#xff0c…

《程序员的自我修养》程序实现的两大环境

学习内容 翻译环境 预处理 编译 汇编 链接 执行环境 在标准C的任何一种实现中,都存在两个不同的环境: 1.翻译环境:在这个环境中,源代码被翻译成为可执行的机器指令。 2.执行环境:用于执行实际的代码 在VS2022中&…

字符串的读入方式

文章目录1、scanf2、fgets()3、cin4、cin.getline()5、getline()1、scanf scanf只能读入不带空格的字符串,遇到空格则结束。scanf只能读入字符数组,不能读入string。scanf在读入的时候,会自动在字符串的末尾加上’\0’。定义字符数组长度时&…

虚表指针在C++类的继承/多态与重载中的基本逻辑

文章目录前言重载继承虚函数虚函数表动态绑定的实现析构函数构造函数多态子类直接继承父类的方法,不覆盖多重继承纯虚函数前言 C在C语言的基础上增加了类的概念,而类的最关键的特性就是三个: 继承多态重载 这篇文章想接着上两篇C相关的文章…

[附源码]Python计算机毕业设计SSM基于推荐算法的汽车租赁系统(程序+LW)

项目运行 环境配置: Jdk1.8 Tomcat7.0 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术: SSM mybatis Maven Vue 等等组成,B/S模式 M…

2023最新SSM计算机毕业设计选题大全(附源码+LW)之java动物爱心救助平台s9dza

首先选择计算机题目的时候先看定什么主题,一般的话都选择当年最热门的话题进行组题,就比如说,今年的热门话题有奥运会,全运会,残运会,或者疫情相关的,这些都是热门话题,所以你就可以…

HTML做一个抗疫逆行者感动人物页面(纯html代码)

🎉精彩专栏推荐 💭文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 💂 作者主页: 【主页——🚀获取更多优质源码】 🎓 web前端期末大作业: 【📚毕设项目精品实战案例 (10…

AI公平性研究进展(2022)

最近AI公平性方面出了不少新的研究成果,如有遗漏,欢迎补充↓↓↓↓ 公平性提升 MAAT: A Novel Ensemble Approach to Addressing Fairness and Performance Bugs for Machine Learning Software, FSE, 2022. 利用模型集成的方式获得公平的预测结果&am…

Windows下如何查看某个端口被谁占用

开发时经常遇到端口被占用的情况,这个时候我们就需要找出被占用端口的程序,然后结束它,本文为大家介绍如何查找被占用的端口。 1、打开命令窗口(以管理员身份运行) 开始—->运行—->cmd,或者是 windowR 组合键&#xff0c…

含冰蓄冷空调的冷热电联供型微网多时间尺度优化调度(Matlab代码实现)

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

开关电源 - LLC电路的谐振工作模态浅析

LLC电路的谐振工作模态浅析 LLC谐振电路是常用的拓扑,广泛应用在目前的热门应用中,本文主要从典型谐振状态来分析其基本工作过程,后续我们将逐步扩展到其它工作状态。 一、电路工作基本条件及电路组成 图1 电路主要组成部分 如上图1所示&a…

c#入门-运算符

可用运算符 值的运算也是自定义的,但是这个就看不出了。 要么翻源文件,要么翻说明书才知道一个类型能怎么运算。 但是一个默认情况下的类型,是没有定义任何运算符的。 也就是说你适用的运算一定是由人先写出来的,没有系统默认的…

MYSQL的索引事务

文章目录1:索引1.1:概念:1.2 作用:1.3 使用场景:1.4 使用:补充:**2:了解一下B树:****3:了解一下B树**4:为什么使用B树/B树来实现索引2&#xff1a…

通过IP地址如何查到用户真实个人信息

当人们使用服务时,互联网上所有科技巨头公司都会收集大量的数据,他们就是这样发财的。微博知道您的朋友是谁、您 “喜欢” 什么,以及您在新闻源上阅读的内容种类;百度知道您的想法、您想知道什么、以及任何时候的去向;…

[附源码]JAVA毕业设计小区物业管理系统(系统+LW)

[附源码]JAVA毕业设计小区物业管理系统(系统LW) 项目运行 环境项配置: Jdk1.8 Tomcat8.5 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术…

Spring cloud Eureka服务注册中心实战

Spring cloud Eureka服务注册中心一、注册中心二、存在意义三、角色四、运行流程五、入门案例1、创建Spring Cloud 工程2、创建 Eureka 服务pom.xml3、配置 application.yml4、服务间调用方式一:RestTemplate DiscoveryClient方式二:RestTemplate Load…

【Golang】关于slice调用append函数后分配新数组的问题(slice的底层实现)

问题描述 今天在写代码的时候遇到一个很奇怪的现象,先看下面两段代码 func push(a []int, v int) {a[1] 2a append(a, v) } func main() {a : []int{0, 1, 2}push(a, 3)fmt.Println(a) }结果:[0 2 2] func push(a []int, v int) {a append(a, v)a[…

如何看股票l2接口的最佳买卖价位委托明细?

股票l2接口的买卖价位委托明细即买一卖一位置的挂挡情况,挂单可以让用户及时了解交易队列,从而了解完整的开盘情况。用户可以通过观察挂单来判断档位是否有主力资金介入,从而判断开盘时的主力动态,进而把握投资机会。 1.股票l2接…