Transformers 4.37 中文文档(十二)(4)

简介: Transformers 4.37 中文文档(十二)

Transformers 4.37 中文文档(十二)(3)https://developer.aliyun.com/article/1564914


模型训练解剖学

原文链接:huggingface.co/docs/transformers/v4.37.2/en/model_memory_anatomy

为了了解可以应用的性能优化技术,以提高模型训练速度和内存利用效率,有助于熟悉在训练期间 GPU 的使用情况,以及根据执行的操作而变化的计算强度。

让我们从探索 GPU 利用率和模型训练运行的一个激励示例开始。为了演示,我们需要安装一些库:

pip install transformers datasets accelerate nvidia-ml-py3

nvidia-ml-py3库允许我们从 Python 内部监视模型的内存使用情况。您可能熟悉终端中的nvidia-smi命令-这个库允许直接在 Python 中访问相同的信息。

然后,我们创建一些虚拟数据:100 到 30000 之间的随机标记 ID 和用于分类器的二进制标签。总共,我们得到 512 个长度为 512 的序列,并将它们存储在 PyTorch 格式的Dataset中。

>>> import numpy as np
>>> from datasets import Dataset
>>> seq_len, dataset_size = 512, 512
>>> dummy_data = {
...     "input_ids": np.random.randint(100, 30000, (dataset_size, seq_len)),
...     "labels": np.random.randint(0, 1, (dataset_size)),
... }
>>> ds = Dataset.from_dict(dummy_data)
>>> ds.set_format("pt")

为了打印 GPU 利用率和使用 Trainer 进行训练的摘要统计信息,我们定义了两个辅助函数:

>>> from pynvml import *
>>> def print_gpu_utilization():
...     nvmlInit()
...     handle = nvmlDeviceGetHandleByIndex(0)
...     info = nvmlDeviceGetMemoryInfo(handle)
...     print(f"GPU memory occupied: {info.used//1024**2} MB.")
>>> def print_summary(result):
...     print(f"Time: {result.metrics['train_runtime']:.2f}")
...     print(f"Samples/second: {result.metrics['train_samples_per_second']:.2f}")
...     print_gpu_utilization()

让我们验证一下我们从空闲 GPU 内存开始:

>>> print_gpu_utilization()
GPU memory occupied: 0 MB.

看起来不错:GPU 内存没有被占用,正如我们在加载任何模型之前所期望的那样。如果在您的计算机上不是这种情况,请确保停止使用 GPU  内存的所有进程。然而,并非所有空闲 GPU 内存都可以被用户使用。当模型加载到 GPU 时,内核也会被加载,这可能占用 1-2GB  的内存。为了查看有多少内存被占用,我们将一个微小的张量加载到 GPU 中,这将触发内核的加载。

>>> import torch
>>> torch.ones((1, 1)).to("cuda")
>>> print_gpu_utilization()
GPU memory occupied: 1343 MB.

我们看到内核单独占用了 1.3GB 的 GPU 内存。现在让我们看看模型使用了多少空间。

加载模型

首先,我们加载bert-large-uncased模型。我们直接将模型权重加载到 GPU,以便我们可以检查仅权重使用了多少空间。

>>> from transformers import AutoModelForSequenceClassification
>>> model = AutoModelForSequenceClassification.from_pretrained("bert-large-uncased").to("cuda")
>>> print_gpu_utilization()
GPU memory occupied: 2631 MB.

我们可以看到模型权重单独占用了 1.3GB 的 GPU 内存。确切的数字取决于您使用的具体 GPU。请注意,在较新的 GPU 上,模型有时可能占用更多空间,因为权重以优化的方式加载,加快了模型的使用速度。现在我们还可以快速检查是否与nvidia-smi CLI 得到相同的结果:

nvidia-smi
Tue Jan 11 08:58:05 2022
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 460.91.03    Driver Version: 460.91.03    CUDA Version: 11.2     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  Tesla V100-SXM2...  On   | 00000000:00:04.0 Off |                    0 |
| N/A   37C    P0    39W / 300W |   2631MiB / 16160MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|    0   N/A  N/A      3721      C   ...nvs/codeparrot/bin/python     2629MiB |
+-----------------------------------------------------------------------------+

我们得到了与之前相同的数字,您还可以看到我们正在使用一块具有 16GB 内存的 V100 GPU。现在我们可以开始训练模型并查看 GPU 内存消耗的变化。首先,我们设置一些标准的训练参数:

default_args = {
    "output_dir": "tmp",
    "evaluation_strategy": "steps",
    "num_train_epochs": 1,
    "log_level": "error",
    "report_to": "none",
}

如果您计划运行多个实验,为了在实验之间正确清除内存,请在实验之间重新启动 Python 内核。

在普通训练中的内存利用率

让我们使用 Trainer 并在不使用任何 GPU 性能优化技术的情况下训练模型,批量大小为 4:

>>> from transformers import TrainingArguments, Trainer, logging
>>> logging.set_verbosity_error()
>>> training_args = TrainingArguments(per_device_train_batch_size=4, **default_args)
>>> trainer = Trainer(model=model, args=training_args, train_dataset=ds)
>>> result = trainer.train()
>>> print_summary(result)
Time: 57.82
Samples/second: 8.86
GPU memory occupied: 14949 MB.

我们看到即使是一个相对较小的批量大小几乎填满了我们 GPU  的整个内存。然而,较大的批量大小通常会导致模型更快地收敛或获得更好的最终性能。因此,理想情况下,我们希望根据模型的需求而不是 GPU  的限制来调整批量大小。有趣的是,我们使用的内存比模型的大小要多得多。为了更好地理解为什么会这样,让我们看一下模型的操作和内存需求。

模型操作解剖学

Transformers 架构包括 3 个主要的操作组,根据计算强度分组如下。

  1. 张量收缩
    线性层和多头注意力组件都进行批量矩阵-矩阵乘法。这些操作是训练 transformer 最计算密集的部分。
  2. 统计归一化
    Softmax 和层归一化的计算密度低于张量收缩,并涉及一个或多个减少操作,其结果然后通过映射应用。
  3. 逐元素运算符
    这些是剩余的运算符:偏置、丢弃、激活和残差连接。这些是计算密集度最低的操作。

这些知识在分析性能瓶颈时可能会有所帮助。

这个总结来源于数据移动就是你需要的一切:优化 Transformer 的案例研究 2020

模型内存的解剖

我们已经看到,训练模型使用的内存比仅将模型放在 GPU 上要多得多。这是因为在训练期间有许多组件使用 GPU 内存。在 GPU 内存中的组件包括以下内容:

  1. 模型权重
  2. 优化器状态
  3. 梯度
  4. 为梯度计算保存的前向激活
  5. 临时缓冲区
  6. 功能特定的内存

使用 AdamW 进行混合精度训练的典型模型每个模型参数需要 18 字节加上激活内存。对于推断,没有优化器状态和梯度,因此我们可以减去这些。因此,对于混合精度推断,每个模型参数需要 6 字节,加上激活内存。

让我们看看细节。

模型权重:

  • 每个参数的 4 字节,用于 fp32 训练
  • 每个参数的 6 字节,用于混合精度训练(在内存中维护一个 fp32 模型和一个 fp16 模型)

优化器状态:

  • 每个参数的 8 字节,用于普通的 AdamW(维护 2 个状态)
  • 每个参数的 8 位 AdamW 优化器需要 2 字节,例如bitsandbytes
  • 每个参数的 4 字节,用于像带有动量的 SGD 这样的优化器(仅维护 1 个状态)

梯度

  • 每个参数的 4 字节,用于 fp32 或混合精度训练(梯度始终保持在 fp32 中)

前向激活

  • 大小取决于许多因素,关键因素是序列长度、隐藏大小和批量大小。

这里有通过前向和后向函数传递和返回的输入和输出,以及为梯度计算保存的前向激活。

临时内存

此外,还有各种临时变量,一旦计算完成就会释放,但在某些时刻这些变量可能需要额外的内存并可能导致 OOM。因此,在编码时,战略性地考虑这些临时变量并有时明确释放那些不再需要的变量是至关重要的。

功能特定的内存

然后,您的软件可能有特殊的内存需求。例如,使用波束搜索生成文本时,软件需要维护多个输入和输出的副本。

前向后向执行速度

对于卷积和线性层,与前向相比,后向中的 flops 是前向的 2 倍,这通常会导致大约 2  倍的速度变慢(有时更多,因为后向中的大小往往更加尴尬)。激活通常受带宽限制,一个激活在后向中通常需要读取比前向更多的数据(例如,激活前向读取一次,写入一次,激活后向读取两次,gradOutput  和前向的输出,然后写入一次,gradInput)。

正如您所看到的,有一些地方我们可以节省 GPU 内存或加快操作速度。现在您已经了解了影响 GPU 利用率和计算速度的因素,请参考单个 GPU 上高效训练的方法和工具文档页面,了解性能优化技术。

优化 LLMs 的速度和内存

原文链接:huggingface.co/docs/transformers/v4.37.2/en/llm_tutorial_optimization

诸如 GPT3/4、FalconLlama等大型语言模型(LLMs)正在快速发展,能够处理以人类为中心的任务,成为现代知识型产业中不可或缺的工具。然而,在实际任务中部署这些模型仍然具有挑战性:

  • 为了展示接近人类文本理解和生成能力,目前 LLMs 需要由数十亿参数组成(参见Kaplan 等人Wei 等人)。这进一步增加了推断的内存需求。
  • 在许多现实世界的任务中,LLMs 需要提供广泛的上下文信息。这要求模型在推断过程中能够处理非常长的输入序列。

这些挑战的关键在于增强 LLMs 的计算和内存能力,特别是在处理庞大的输入序列时。

在本指南中,我们将介绍高效 LLM 部署的有效技术:

  1. **低精度:**研究表明,以降低的数值精度,即 8 位和 4 位可以在不显著降低模型性能的情况下实现计算优势。
  2. **快闪注意力:**快闪注意力是注意力算法的一种变体,不仅提供了更节省内存的方法,还通过优化 GPU 内存利用率实现了增加的效率。
  3. **架构创新:**考虑到 LLMs 在推断过程中始终以相同方式部署,即具有长输入上下文的自回归文本生成,已经提出了专门的模型架构,允许更高效的推断。在模型架构方面最重要的进展是AlibiRotary embeddings多查询注意力(MQA)分组查询注意力(GQA)

在本指南中,我们将从张量的角度对自回归生成进行分析。我们深入探讨采用低精度的利弊,全面探索最新的注意力算法,并讨论改进的 LLM 架构。在此过程中,我们运行实际示例展示每个功能改进。

1. 低精度

通过将 LLM 视为一组权重矩阵和向量,将文本输入视为一系列向量,可以更好地理解 LLMs 的内存需求。在接下来的内容中,定义权重将用于表示所有模型权重矩阵和向量。

在撰写本指南时,LLMs 至少包含数十亿参数。因此,每个参数由一个十进制数组成,例如4.5689,通常以float32bfloat16float16格式存储。这使我们能够轻松计算加载 LLM 到内存所需的内存量:

加载具有 X 十亿参数的模型的权重大约需要 4X GB 的 VRAM,精度为 float32*

如今,模型很少以完整的 float32 精度进行训练,而通常以 bfloat16 精度或更少的 float16 精度进行训练。因此,经验法则变为:

加载具有 X 十亿参数的模型的权重大约需要 2X GB 的 VRAM,精度为 bfloat16/float16*

对于较短的文本输入(少于 1024 个标记),推理的内存需求主要受加载权重的内存需求支配。因此,现在让我们假设推理的内存需求等于将模型加载到 GPU VRAM 中的内存需求。

举例说明加载 bfloat16 模型大致需要多少 VRAM:

截至撰写本文时,市场上最大的 GPU 芯片是 A100 & H100,提供 80GB 的 VRAM。之前列出的大多数模型需要超过 80GB 的内存才能加载,因此必然需要张量并行处理和/或管道并行处理。

🤗 Transformers 不支持张量并行处理,因为它要求模型架构以特定方式编写。如果您有兴趣以张量并行友好的方式编写模型,请随时查看文本生成推理库

天真的管道并行处理是开箱即用的。为此,只需使用 device="auto" 加载模型,它将自动将不同的层放置在可用的 GPU 上,如此处所述。请注意,尽管非常有效,但这种天真的管道并行处理并未解决 GPU 空闲的问题。为此,需要更高级的管道并行处理,如此处所述。

如果您可以访问一个 8 x 80GB A100 节点,您可以按照以下方式加载 BLOOM

!pip install transformers accelerate bitsandbytes optimum
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained("bigscience/bloom", device_map="auto", pad_token_id=0)

通过使用 device_map="auto",注意力层将均匀分布在所有可用的 GPU 上。

在本指南中,我们将使用bigcode/octocoder,因为它可以在单个 40 GB A100 GPU 设备芯片上运行。请注意,我们将要应用的所有内存和速度优化都同样适用于需要模型或张量并行处理的模型。

由于模型以 bfloat16 精度加载,根据我们上面的经验法则,我们预计使用 bigcode/octocoder 运行推理的内存需求约为 31 GB VRAM。让我们试一试。

我们首先加载模型和分词器,然后将两者传递给 Transformers 的管道对象。

from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
import torch
model = AutoModelForCausalLM.from_pretrained("bigcode/octocoder", torch_dtype=torch.bfloat16, device_map="auto", pad_token_id=0)
tokenizer = AutoTokenizer.from_pretrained("bigcode/octocoder")
pipe = pipeline("text-generation", model=model, tokenizer=tokenizer)
prompt = "Question: Please write a function in Python that transforms bytes to Giga bytes.\n\nAnswer:"
result = pipe(prompt, max_new_tokens=60)[0]["generated_text"][len(prompt):]
result

输出

Here is a Python function that transforms bytes to Giga bytes:\n\n```python\ndef bytes_to_giga_bytes(bytes):\n    return bytes / 1024 / 1024 / 1024\n```py\n\nThis function takes a single

很好,我们现在可以直接使用结果将字节转换为千兆字节。

def bytes_to_giga_bytes(bytes):
  return bytes / 1024 / 1024 / 1024

让我们调用torch.cuda.max_memory_allocated来测量 GPU 内存分配的峰值。

bytes_to_giga_bytes(torch.cuda.max_memory_allocated())

输出

29.0260648727417

接近我们粗略计算的结果!我们可以看到数字并不完全正确,因为从字节到千字节需要乘以 1024 而不是 1000。因此,粗略计算公式也可以理解为“最多 XGB”的计算。请注意,如果我们尝试以完整的 float32 精度运行模型,将需要 64GB 的 VRAM。

几乎所有模型现在都是在 bfloat16 中训练的,如果您的 GPU 支持 bfloat16,就没有理由以完整的 float32 精度运行模型。Float32 不会比用于训练模型的精度提供更好的推断结果。

如果您不确定模型权重以哪种格式存储在 Hub 上,您可以随时查看检查点的配置,在"torch_dtype"下,例如这里。建议在使用from_pretrained(..., torch_dtype=...)加载模型时,将模型设置为与配置中写入的相同精度类型,除非原始类型为 float32,此时可以在推断中使用float16bfloat16

让我们定义一个flush(...)函数来释放所有分配的内存,以便我们可以准确地测量分配的 GPU 内存峰值。

del pipe
del model
import gc
import torch
def flush():
  gc.collect()
  torch.cuda.empty_cache()
  torch.cuda.reset_peak_memory_stats()

现在让我们为下一个实验调用它。

flush()

在最近的 accelerate 库版本中,您还可以使用一个名为release_memory()的实用方法

from accelerate.utils import release_memory
# ...
release_memory(model)

那么如果您的 GPU 没有 32GB 的 VRAM 怎么办?已经发现模型权重可以量化为 8 位或 4 位而不会显著降低性能(参见Dettmers 等人)。正如最近的GPTQ 论文所示,模型可以量化为 3 位或 2 位,性能损失是可以接受的🤯。

不深入细节,量化方案旨在降低权重的精度,同时尽可能保持模型推断结果的准确性(即尽可能接近 bfloat16)。请注意,量化在文本生成方面特别有效,因为我们只关心选择最可能的下一个标记集,并不真正关心下一个标记logit分布的确切值。重要的是下一个标记logit分布保持大致相同,以便argmaxtopk操作给出相同的结果。

有各种量化技术,我们这里不会详细讨论,但总的来说,所有量化技术的工作方式如下:

  1. 将所有权重量化为目标精度
  2. 加载量化的权重,并以 bfloat16 精度传递输入序列的向量
  3. 动态将权重去量化为 bfloat16,以 bfloat16 精度执行计算

简而言之,这意味着输入-权重矩阵乘法,其中X X X 是输入,W W W 是权重矩阵,Y Y Y 是输出:Y=X∗W Y = X * W Y=X∗W

被改变为Y=X∗dequantize(W) Y = X * \text{dequantize}(W) Y=X∗dequantize(W)

对于每个矩阵乘法。当输入通过网络图时,权重矩阵的反量化和重新量化是按顺序执行的。

因此,当使用量化权重时,推理时间通常不会减少,而是增加。足够的理论,让我们试一试!要使用 Transformers 量化权重,您需要确保已安装bitsandbytes库。

!pip install bitsandbytes

然后,我们可以通过简单地在from_pretrained中添加load_in_8bit=True标志来加载 8 位量化的模型。

model = AutoModelForCausalLM.from_pretrained("bigcode/octocoder", load_in_8bit=True, pad_token_id=0)

现在,让我们再次运行我们的示例并测量内存使用情况。

pipe = pipeline("text-generation", model=model, tokenizer=tokenizer)
result = pipe(prompt, max_new_tokens=60)[0]["generated_text"][len(prompt):]
result

输出

Here is a Python function that transforms bytes to Giga bytes:\n\n```python\ndef bytes_to_giga_bytes(bytes):\n    return bytes / 1024 / 1024 / 1024\n```py\n\nThis function takes a single

很好,我们得到了与之前相同的结果,因此在准确性上没有损失!让我们看看这次使用了多少内存。

bytes_to_giga_bytes(torch.cuda.max_memory_allocated())

输出

15.219234466552734

显著减少!我们只剩下略高于 15GB,因此可以在像 4090 这样的消费级 GPU 上运行此模型。我们在内存效率上获得了非常好的收益,几乎没有对模型输出的降级。但是,在推理过程中我们也可以注意到略微减速。

我们删除模型并再次清空内存。

del model
del pipe
flush()

让我们看看 4 位量化对 GPU 内存消耗的峰值。将模型量化为 4 位可以通过与之前相同的 API 完成 - 这次是通过传递load_in_4bit=True而不是load_in_8bit=True来完成。

model = AutoModelForCausalLM.from_pretrained("bigcode/octocoder", load_in_4bit=True, low_cpu_mem_usage=True, pad_token_id=0)
pipe = pipeline("text-generation", model=model, tokenizer=tokenizer)
result = pipe(prompt, max_new_tokens=60)[0]["generated_text"][len(prompt):]
result

输出

Here is a Python function that transforms bytes to Giga bytes:\n\n```\ndef bytes_to_gigabytes(bytes):\n    return bytes / 1024 / 1024 / 1024\n```py\n\nThis function takes a single argument

我们几乎看到与之前相同的输出文本 - 只是在代码片段之前缺少了python。让我们看看需要多少内存。

bytes_to_giga_bytes(torch.cuda.max_memory_allocated())

输出

9.543574333190918

只有 9.5GB!对于一个超过 15 亿参数的模型来说,这真的不多。

虽然我们在这里看到模型准确性几乎没有下降,但实际上,4 位量化通常会导致与 8 位量化或完整的bfloat16推理相比产生不同的结果。这取决于用户是否尝试。

还要注意,与 8 位量化相比,这里的推理速度再次稍慢一些,这是因为 4 位量化使用了更激进的量化方法,导致在推理过程中量化和反量化过程需要更长的时间。

del model
del pipe
flush()

总的来说,我们发现在 8 位精度下运行 OctoCoder 将所需的 GPU VRAM 从 32G GPU VRAM 减少到仅 15GB,并且在 4 位精度下运行模型进一步将所需的 GPU VRAM 减少到略高于 9GB。

4 位量化使模型可以在 RTX3090、V100 和 T4 等 GPU 上运行,这对大多数人来说非常容易获得。

有关量化的更多信息以及如何将模型量化以便比 4 位更少地使用 GPU VRAM 内存,我们建议查看AutoGPTQ实现。

最后,重要的是要记住,模型量化在内存效率和准确性之间进行了权衡,并且在某些情况下会增加推理时间。

如果 GPU 内存对您的用例不是限制,通常不需要考虑量化。但是许多 GPU 无法在没有量化方法的情况下运行 LLMs,在这种情况下,4 位和 8 位量化方案是非常有用的工具。

有关更详细的使用信息,我们强烈建议查看Transformers 量化文档。接下来,让我们看看如何通过使用更好的算法和改进的模型架构来提高计算和内存效率。


Transformers 4.37 中文文档(十二)(5)https://developer.aliyun.com/article/1564916

相关实践学习
部署Stable Diffusion玩转AI绘画(GPU云服务器)
本实验通过在ECS上从零开始部署Stable Diffusion来进行AI绘画创作,开启AIGC盲盒。
相关文章
|
6月前
|
存储 自然语言处理 算法
Transformers 4.37 中文文档(十四)(3)
Transformers 4.37 中文文档(十四)
84 4
|
6月前
|
存储 缓存 安全
Transformers 4.37 中文文档(十四)(7)
Transformers 4.37 中文文档(十四)
48 4
|
6月前
|
存储 缓存 PyTorch
Transformers 4.37 中文文档(十四)(4)
Transformers 4.37 中文文档(十四)
143 4
|
6月前
|
存储 PyTorch 测试技术
Transformers 4.37 中文文档(十四)(5)
Transformers 4.37 中文文档(十四)
71 4
|
6月前
|
机器学习/深度学习 缓存 算法
Transformers 4.37 中文文档(十二)(5)
Transformers 4.37 中文文档(十二)
48 1
|
6月前
|
机器学习/深度学习 编解码 自然语言处理
Transformers 4.37 中文文档(十二)(2)
Transformers 4.37 中文文档(十二)
93 1
|
6月前
|
机器学习/深度学习 自然语言处理 自动驾驶
Transformers 4.37 中文文档(十二)(1)
Transformers 4.37 中文文档(十二)
55 1
|
6月前
|
自然语言处理 算法 安全
Transformers 4.37 中文文档(十二)(3)
Transformers 4.37 中文文档(十二)
106 1
|
6月前
|
存储 数据可视化 PyTorch
Transformers 4.37 中文文档(十四)(1)
Transformers 4.37 中文文档(十四)
103 1
|
6月前
|
存储 JSON 算法框架/工具
Transformers 4.37 中文文档(十四)(2)
Transformers 4.37 中文文档(十四)
65 1