重要提示

您正在查看 NeMo 2.0 文档。此版本对 API 和新库 NeMo Run 进行了重大更改。我们目前正在将 NeMo 1.0 中的所有功能移植到 2.0。有关先前版本或 2.0 中尚未提供的功能的文档,请参阅 NeMo 24.07 文档

NeVA 序列打包#

概述#

正如吞吐量优化部分所述,大多数多模态 LLM 数据集(例如 LLaVA 数据集)都表现出序列长度的偏斜分布。许多序列都很短,而少数序列非常长,符合齐普夫定律。Transformer 模型需要固定长度的输入,因此需要使用大量未使用的填充标记进行填充,这出于两个原因效率低下

  1. 填充值的计算在最终模型输出中被忽略,导致浪费 FLOP。

  2. 微批量大小通常受包含最长序列的批次限制,导致大多数其他批次中的 GPU 内存未得到充分利用。

序列打包是一种训练技术,其中多个训练序列(示例)被连接成一个长序列(包)。这种方法消除了填充的需要,并允许每个微批量处理更多标记,从而优化 GPU 计算和内存利用率。

对于 LLM 的 SFT/PEFT 中的序列打包,NeVA 考虑以下设计

  1. 原始数据集到序列长度文件

    1.1. 用于数据集处理效率的 PyTorch 加载器

    为了高效管理大型数据集(约 70 万个序列),系统利用 PyTorch 的 DataLoader 及其多工作进程功能,通过并行化加载和预处理步骤,显着加快了数据处理阶段的速度。

    1.2. 处理大型数据集

    系统将序列长度动态写入磁盘,确保可扩展性和高效的内存使用,因为将所有数据加载到内存中是不切实际的。

    1.3. 高效的 I/O 操作

    为了促进并行化数据加载所需的高效 I/O 操作,系统采用了 Megatron-Core 的 IndexedDataset,因为它能够动态构建二进制张量文件。

  2. 将序列打包到存储桶中

    2.1. 算法选择和性能

    最初用于将序列打包到存储桶中的 first_fit_decreasing 和 first_fit_shuffle 算法由于其 O(n^2) 复杂性而显示出性能问题,导致 NeVA 样本的处理非常耗时。

    2.2. shuffle_and_pack 的引入

    为了解决这些低效率问题,引入了 shuffle_and_pack 算法,这是一种 O(n) 复杂度的算法,它在将序列长度按顺序打包到存储桶之前对其进行混洗,从而显着提高了处理时间。

    2.3. 打包过程的并行化

    系统通过将样本分成块(每个约 2 万个样本)并分别处理它们,有效地缓解了二次复杂度问题,从而实现了 first_fit_shuffle 算法的并行化方法。然后,在最后一步中组合来自每个块的存储桶,从而提高了整体效率。

    2.4. 使用 completed_bins 提高效率

    一个小的优化涉及使用 completed_bins 来防止算法迭代无法容纳最小序列长度的存储桶,从而实现更高效的打包过程。

  3. 读取序列长度并将序列打包到新文件中 在确定用于打包的最佳存储桶后,系统从生成的文件中读取序列长度,并根据存储桶的分配将这些长度打包到新文件中。最后一步将序列整合到高效打包的存储桶中,以便进一步处理或分析。

性能提升#

通过优化的序列打包,序列长度为 w/ Vicuna-1.5 13B(LLaVA 1.5 配方)的模型实现了 40% 的速度提升。下表提供了不同配置和阶段的详细性能指标。

微调性能表

阶段

视觉编码器

LLM 模型

TP

PP

精度

序列打包

步进时间(秒)

全局批量大小

样本/秒

性能提升

微调

openai/clip-vit-large- patch14-336

Vicuna-1.5 13B

8

1

BF16

2.008

128

63.745

0%

微调

openai/clip-vit-large- patch14-336

Vicuna-1.5 13B

4

2

BF16

1.889

128

67.761

6%

微调

openai/clip-vit-large- patch14-336

Vicuna-1.5 13B

8

1

BF16

1.302

116.08

89.155

40%

微调

openai/clip-vit-large- patch14-336

Vicuna-1.5 13B

4

2

BF16

1.237

116.08

93.840

47%

如何运行带有打包序列的 NeVA#

准备数据集#

我们提供了一个易于使用的脚本,用于预处理 NeMo 多模态学习框架的数据集。它需要指定数据、图像和分词器模型的路径,以及其他参数。

python examples/multimodal/multimodal_llm/neva/sequence_packing/preprocess_dataset.py \
 --data_path=/path/to/LLaVA-Instruct-150K/llava_v1_5_mix665k_filtered.json \
 --image_folder=/path/to/LLaVA-Instruct-150K/images \
 --tokenizer_path=/path/to/checkpoints/tokenizer_add_special.model \
 --output_dir=/path/to/LLaVA-Instruct-150K/packed_seq_12288_336_v1 \
 --max_seq_length=12288 \
 --packing_algorithm=first_fit_shuffle \
 --hf_vision_encoder=openai/clip-vit-large-patch14-336 \
 --conv_template=v1 \
 --image_aspect_ratio=pad \
 --seed=42

参数: * --data_path:JSON 格式的数据集文件路径。 * --image_folder:包含数据集中引用的图像的目录。 * --tokenizer_path:分词器模型的路径。 * --output_dir:处理后的数据集将存储在的目录。 * --max_seq_length:模型的最大序列长度。 * --packing_algorithm:用于打包序列的算法。默认为 ‘first_fit_shuffle’。 * --hf_vision_encoder:要使用的 Hugging Face 视觉编码器。默认为 ‘openai/clip-vit-large-patch14-336’。 * --conv_template:数据转换的模板。默认为 ‘plain’,可选 ‘v1’。 * --image_aspect_ratio:用于处理图像的宽高比。默认为 ‘square’,‘pad’ 用于填充以保持宽高比。 * --seed:‘first_fit_shuffle’ 中随机操作的种子。 * --hparams_file:包含其他超参数的可选 YAML 文件路径。

备注: 1. 当前版本的数据处理将处理后的图像张量保存在序列打包中,这可能需要大量存储空间。此问题将在未来的迭代中解决。 2. max_seq_length 对于实现最佳性能至关重要。过长的长度可能导致内存不足错误,而长度不足可能会降低性能。 3. 会话提示模板在此步骤中插入,以确保准确的序列长度计算。

调整训练配置#

要使用打包序列进行训练,请修改 SFT/PEFT 配置文件中的四个项目。

  1. 启用 packed_sequence 标志

++model.data.packed_sequence=True
  1. 使用新的数据集文件而不是原始 JSONL 文件,并确保正确指定裁剪大小,因为图像现在已缓存

++model.data.data_path=/path/to/datasets/LLaVA-Instruct-150K/packed_seq_12288_336_v1/packed_seq_dataset
++model.data.crop_size=[336,336]
  1. 调整批量大小

  • 由于预处理步骤中的连接,微批量大小应设置为 1。增加 pack_size 以实现更高的微批量大小。

  • 全局批量大小应根据每个包的平均序列数 (n) 进行调整,计算方法为序列总数除以包数。这通过确保每个梯度迭代平均看到相同数量的标记来保持训练配方。

model.micro_batch_size=1
model.global_batch_size=<GBS divided by n>

现在,您已准备好通过显着提高的吞吐量来微调您的模型!