Packing(数据打包) 是一种将多个短样本合并成一个长样本的训练优化技术。通过智能地将多个训练样本拼接在一起,可以显著提高 GPU 利用率和训练效率。
展开代码传统训练(带 padding): ┌─────────────────────────────────┐ │ 样本1: [tokens] + [PAD PAD PAD] │ 实际利用率: 50% ├─────────────────────────────────┤ │ 样本2: [tokens tokens] + [PAD] │ 实际利用率: 75% ├─────────────────────────────────┤ │ 样本3: [tokens] + [PAD PAD PAD] │ 实际利用率: 50% └─────────────────────────────────┘ Packing 训练(无 padding): ┌─────────────────────────────────┐ │ Pack1: [样本1][样本2][样本3] │ 实际利用率: 98% ├─────────────────────────────────┤ │ Pack2: [样本4][样本5] │ 实际利用率: 95% └─────────────────────────────────┘
在多模态大模型训练中,样本长度往往差异巨大:
如果设置 max_length=10240,短样本会产生大量 padding,导致:
通过 Bin-packing 算法,将多个短样本智能组合:
ms-swift 的 Packing 分为两个独立阶段:
graph LR
A[原始JSON数据] --> B[swift export]
B --> C[Tokenization]
C --> D[计算长度]
D --> E[保存到磁盘]
E --> F[Cached Dataset]
graph LR
A[Cached Dataset] --> B[加载数据+长度]
B --> C[Bin-packing算法]
C --> D[生成打包索引]
D --> E[训练时拼接]
确保数据格式正确(JSON 格式):
json展开代码[
{
"messages": [
{"role": "user", "content": "你好"},
{"role": "assistant", "content": "你好!有什么可以帮你的?"}
],
"images": ["s3://path/to/image.jpg"]
},
...
]
bash展开代码#!/bin/bash
# 设置环境变量(与训练时保持一致)
export MODELSCOPE_CACHE='/mnt/jfs/copilot/yhl/modelscope_cache'
export IMAGE_MAX_TOKEN_NUM=5000
# 数据集路径
DATASET="s3://yanhaolong/data/sft_data/click_pretrain.json"
OUTPUT_DIR="s3://yanhaolong/data/sft_data/click_pretrain_cached_resize0_q3_len10k_img5k"
# 执行预处理
IMAGE_MAX_TOKEN_NUM=5000 swift export \
--resize 0 \
--model /mnt/jfs/copilot/yhl/checkpoint/Qwen3-VL-30B-A3B-Instruct \
--dataset $DATASET \
--to_cached_dataset true \
--split_dataset_ratio 0 \
--dataset_num_proc 10 \
--max_length 10240 \
--output_dir $OUTPUT_DIR
参数说明:
--resize 0:不调整图像大小(0表示原始大小)--model:模型路径(用于加载 tokenizer 和图像处理器)--dataset:原始数据集路径(支持本地/S3/OSS)--to_cached_dataset true:关键,启用 cached dataset 生成--max_length 10240:最大序列长度(与训练时保持一致)--dataset_num_proc 10:数据处理并行进程数--output_dir:输出目录(支持 S3/OSS)展开代码s3://yanhaolong/data/.../click_pretrain_cached_resize0_q3_len10k_img5k/ ├── train/ │ ├── data-00000-of-00001.arrow # 编码后的数据(Arrow格式) │ ├── dataset_info.json # 数据集元信息 │ └── state.json # 状态信息 └── val/ (如果设置了 split_dataset_ratio > 0) ├── data-00000-of-00001.arrow └── ...
包含的字段:
input_ids: List[int] - tokenized 输入序列labels: List[int] - 标签序列(用于计算 loss)length: int - 序列实际长度(Packing 的关键)images: 图像数据(如果有)videos: 视频数据(如果有)bash展开代码#!/bin/bash
# 环境变量(必须与预处理时一致)
export IMAGE_MAX_TOKEN_NUM=5000
export MODELSCOPE_CACHE='/mnt/jfs/copilot/yhl/modelscope_cache'
# 训练
NPROC_PER_NODE=8 swift sft \
--model /mnt/jfs/copilot/yhl/checkpoint/Qwen3-VL-30B-A3B-Instruct \
--cached_dataset 's3://yanhaolong/data/sft_data/click_pretrain_cached_resize0_q3_len10k_img5k' \
--packing true \
--max_length 10240 \
--packing_length 10240 \
--packing_num_proc 4 \
--train_type lora \
--num_train_epochs 3 \
--per_device_train_batch_size 1 \
--gradient_accumulation_steps 16 \
--learning_rate 1e-4 \
--output_dir output/qwen3_vl_packed
关键参数:
--cached_dataset:指向预处理后的数据目录(支持 S3/OSS)--packing true:启用 Packing--max_length 10240:必须与预处理时一致--packing_length 10240:打包后的目标长度(默认=max_length)--packing_num_proc 4:打包算法的并行进程数(可选)启用 Packing 后,你会看到以下日志:
展开代码================================================================================ 数据打包(Packing)处理详情: -------------------------------------------------------------------------------- 数据集样本总数: 393230 数据长度列表总数: 393230 最大打包长度(packing_length): 10240 打包进程数(packing_num_proc): 4 每批次处理大小(PACKING_BATCH_SIZE): 1000 严格模式(strict): False 开始使用 4 个进程并行打包 393230 条数据... ================================================================================ Packing (num_proc=4): 100%|███████████████████| 393230/393230 [02:15<00:00]
打包完成后:
展开代码train_dataset: Dataset({ features: ['input_ids', 'labels', 'images', 'length'], num_rows: 185432 # 注意:从 393230 → 185432,减少了约 53% })
ms-swift 使用 First Fit Decreasing (FFD) 变体的 Bin-packing 算法:
python展开代码# swift/llm/dataset/utils.py:122-132
def calculate_matched_group(template, sequences, packing_length: int):
import binpacking
# sequences: [(index, length), (index, length), ...]
# 将样本打包成总长度 ≈ packing_length 的组
sequences = binpacking.to_constant_volume(sequences, packing_length, weight_pos=1)
return sequences
算法示例:
展开代码输入样本: 样本0: 3000 tokens 样本1: 8000 tokens 样本2: 2000 tokens 样本3: 5000 tokens 样本4: 1000 tokens 样本5: 7000 tokens packing_length = 10240 Bin-packing 结果: Group 0: [样本1(8000), 样本4(1000)] → 总长 9000 (87.9%) Group 1: [样本5(7000), 样本2(2000)] → 总长 9000 (87.9%) Group 2: [样本3(5000), 样本0(3000)] → 总长 8000 (78.1%) 原始:6个样本 → 打包后:3个 packed 样本 训练效率提升:6 steps → 3 steps (50% 加速)
python展开代码# swift/llm/template/base.py:575-596
def packing_row(self, row: List[Dict[str, Any]]) -> Dict[str, Any]:
packed = {}
length = []
# 拼接 input_ids, labels, loss_scale 等
for key in ['input_ids', 'labels', 'loss_scale', 'position_ids']:
packed[key] = sum((x.get(key) or [] for x in row), start=[])
# 重新计算 position_ids(每个样本独立计算)
if 'position_ids' not in packed:
packed['position_ids'] = sum((list(range(x)) for x in length), start=[])
# 处理多模态数据(图像、视频等)
packed.update(self._data_collator_mm_data(row))
return packed
拼接示例:
python展开代码# 原始 3 个样本
row = [
{
'input_ids': [1, 2, 3, 4],
'labels': [-100, -100, 3, 4],
'length': 4
},
{
'input_ids': [5, 6, 7],
'labels': [-100, 6, 7],
'length': 3
},
{
'input_ids': [8, 9, 10, 11, 12],
'labels': [-100, -100, 10, 11, 12],
'length': 5
}
]
# Packing 后
packed = {
'input_ids': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], # 直接拼接
'labels': [-100, -100, 3, 4, -100, 6, 7, -100, -100, 10, 11, 12], # 保留 mask
'position_ids': [0, 1, 2, 3, 0, 1, 2, 0, 1, 2, 3, 4], # 每个样本独立计算位置
'length': [4, 3, 5] # 保留原始长度信息
}
关键特性:
-100 标记保留,确保 loss 计算正确python展开代码# swift/llm/dataset/utils.py:162-209
# 将数据分成多个 chunk,使用多进程并行处理
packing_num_proc = 4
chunked_lengths = split_list(lengths, packing_num_proc)
for i in range(packing_num_proc):
worker = mp.Process(target=self.create_packed_idx, args=(i, offset, chunked_lengths[i]))
worker.start()
# 收集结果
packed_idx = []
packed_length = []
for rank, sequences, data_len in queue:
packed_idx += [[x[0] for x in seq] for seq in sequences]
packed_length += [sum(x[1] for x in seq) for seq in sequences]
并行效果:
| 参数 | 说明 | 推荐值 | 注意事项 |
|---|---|---|---|
--to_cached_dataset | 启用 cached dataset 生成 | true | 必须设置 |
--max_length | 最大序列长度 | 10240 | 与训练时必须一致 |
--dataset_num_proc | 数据处理并行数 | 10-20 | 根据 CPU 核心数调整 |
--split_dataset_ratio | 验证集比例 | 0 或 0.01 | 0 表示不划分 |
--resize | 图像缩放 | 0 | 0 表示不缩放 |
IMAGE_MAX_TOKEN_NUM | 图像最大 token 数 | 5000 | 与训练时必须一致 |
| 参数 | 说明 | 推荐值 | 注意事项 |
|---|---|---|---|
--cached_dataset | Cached dataset 路径 | S3/本地路径 | 必须指向预处理输出目录 |
--packing | 启用 Packing | true | 核心参数 |
--max_length | 最大序列长度 | 10240 | 与预处理时必须一致 |
--packing_length | 打包目标长度 | 10240 | 默认=max_length |
--packing_num_proc | 打包并行进程数 | 4-8 | 建议 4-8 |
IMAGE_MAX_TOKEN_NUM | 图像最大 token 数 | 5000 | 与预处理时必须一致 |
以下配置必须在预处理和训练时保持一致:
bash展开代码# 预处理时
--max_length 10240
IMAGE_MAX_TOKEN_NUM=5000
--resize 0
# 训练时(必须相同)
--max_length 10240
IMAGE_MAX_TOKEN_NUM=5000
--resize 0 # 如果训练时需要图像处理
如果不一致,可能导致:
展开代码Dataset saved to local directory `/tmp/cached_dataset_xxx` Copying dataset to remote storage: s3://yanhaolong/data/.../cached_dataset Train dataset copied: 4 files Successfully copied dataset to: s3://yanhaolong/data/.../cached_dataset
展开代码================================================================================ 数据打包(Packing)处理详情: -------------------------------------------------------------------------------- 数据集样本总数: 393230 数据长度列表总数: 393230 最大打包长度(packing_length): 10240 打包进程数(packing_num_proc): 4 每批次处理大小(PACKING_BATCH_SIZE): 1000 严格模式(strict): False 开始使用 4 个进程并行打包 393230 条数据... ================================================================================
展开代码# 打包前 train_dataset: Dataset({ features: ['input_ids', 'labels', 'length', 'images'], num_rows: 393230 }) # 打包后 train_dataset: Dataset({ features: ['input_ids', 'labels', 'length', 'images'], num_rows: 185432 # 减少约 53% })
展开代码# 无 Packing Epoch 1/3: 100%|████████| 49154/49154 [2:15:30<00:00] # 使用 Packing Epoch 1/3: 100%|████████| 23179/23179 [1:05:20<00:00]
训练 step 数减少约 53%,训练时间减少约 50%+(取决于样本长度分布)。
bash展开代码# 预处理所有数据集
for DATASET in "${DATASET_PATHS[@]}"; do
swift export \
--model $MODEL \
--dataset $DATASET \
--to_cached_dataset true \
--max_length 10240 \
--output_dir "${DATASET}_cached"
done
优点:
bash展开代码# 不预处理,直接训练时 Packing
swift sft \
--model $MODEL \
--dataset $DATASET \
--packing true \
--max_length 10240
优点:
缺点:
bash展开代码# 保守策略:略小于 max_length,留出空间
--packing_length 9216 # 90% of 10240
# 激进策略:等于 max_length,最大化利用
--packing_length 10240
# 自适应策略:根据样本长度分布决定
# 如果大部分样本 < 5000 tokens:
--packing_length 10240 # 可以打包 2-3 个样本
# 如果大部分样本 > 8000 tokens:
--packing_length 8192 # 避免单个样本浪费空间
bash展开代码# 小数据集(< 10k 样本)
--packing_num_proc 1
# 中等数据集(10k-100k 样本)
--packing_num_proc 4
# 大数据集(> 100k 样本)
--packing_num_proc 8
注意:过多进程可能导致内存压力,建议监控内存使用。
bash展开代码# 多个 cached dataset 拼接
swift sft \
--cached_dataset 's3://path/dataset1_cached' \
's3://path/dataset2_cached' \
's3://path/dataset3_cached' \
--packing true \
--max_length 10240
注意:
per_dataset_loss_scale 功能bash展开代码# dataset_info.json
{
"dataset1": {
"file_name": "data1.json",
"loss_scale": "last_round" # 只计算最后一轮的 loss
},
"dataset2": {
"file_name": "data2.json"
# 使用全局默认 loss_scale
}
}
# 训练
swift sft \
--cached_dataset 's3://path/dataset1_cached' \
's3://path/dataset2_cached' \
--packing true \
--loss_scale default # 全局默认
Packing 会保留每个样本的 loss_scale 信息,正确计算 loss。
A: 不会。Packing 只是改变了样本的组合方式,每个原始样本的数据和 loss 计算都保持不变。
训练的有效数据量和 epoch 定义不变。
A: 研究表明,Packing 不会损害模型效果,甚至可能略有提升:
参考论文:Efficient Training of Language Models to Fill in the Middle
A: 不可以。Cached dataset 已经按照预处理时的 max_length 进行了截断和编码,修改后会导致:
如需修改 max_length,必须重新运行 swift export 预处理。
A: 查看训练日志:
python展开代码# 方法1:对比样本数
原始样本数 / Packing后样本数 = 打包效率
例如:393230 / 185432 ≈ 2.12
表示平均每个 packed 样本包含 2.12 个原始样本
# 方法2:对比训练时间
无Packing训练时间 / Packing训练时间 = 加速比
例如:2小时15分 / 1小时5分 ≈ 2.08x 加速
A: 完全支持。ms-swift 的 Packing 机制对多模态数据(图像、视频、音频)做了特殊处理:
python展开代码# swift/llm/template/base.py:595
packed.update(self._data_collator_mm_data(row))
A: 启用详细日志:
bash展开代码export SWIFT_LOG_LEVEL=DEBUG
swift sft \
--cached_dataset $DATASET \
--packing true \
--logging_steps 1 \
--save_steps 100
查看关键日志:
A: ms-swift 完全支持 S3/OSS 远程存储:
bash展开代码# 预处理:输出到 S3
swift export \
--dataset s3://bucket/data.json \
--output_dir s3://bucket/cached_dataset
# 训练:从 S3 加载
swift sft \
--cached_dataset s3://bucket/cached_dataset
注意:
s3:// 和 oss:// 协议s3:/ 单斜杠(代码会自动修正)A: 支持,但使用不同的实现:
bash展开代码swift sft \
--dataset $DATASET \
--streaming true \
--packing true
IterablePackingDataset 而非 PackingDatasetpacking_interval 控制cached_dataset + streaming 组合A: 使用脚本自动化:
bash展开代码#!/bin/bash
DATASET_PATHS=(
"s3://path/dataset1.json"
"s3://path/dataset2.json"
"s3://path/dataset3.json"
)
for DATASET in "${DATASET_PATHS[@]}"; do
DATASET_DIR=$(dirname "$DATASET")
DATASET_FILENAME=$(basename "$DATASET" .json)
OUTPUT_DIR="${DATASET_DIR}/${DATASET_FILENAME}_cached_q3_len10k_img5k"
# 跳过已处理
if aws s3 ls "$OUTPUT_DIR" &> /dev/null; then
echo "⚠️ Skipping $DATASET_FILENAME (already exists)"
continue
fi
# 预处理
IMAGE_MAX_TOKEN_NUM=5000 swift export \
--model $MODEL \
--dataset $DATASET \
--to_cached_dataset true \
--max_length 10240 \
--output_dir $OUTPUT_DIR
echo "✓ Completed: $DATASET_FILENAME"
done
A: 完全兼容,效果叠加:
bash展开代码swift sft \
--cached_dataset $DATASET \
--packing true \
--per_device_train_batch_size 1 \
--gradient_accumulation_steps 16
实际效果:
graph TD
A[准备原始数据] --> B[执行 swift export 预处理]
B --> C[验证 cached dataset]
C --> D[训练时启用 --packing true]
D --> E[监控日志验证效果]
E --> F[多次训练复用 cached dataset]
bash展开代码# 1. 预处理
IMAGE_MAX_TOKEN_NUM=5000 swift export \
--model Qwen/Qwen3-VL-8B \
--dataset s3://path/data.json \
--to_cached_dataset true \
--max_length 10240 \
--output_dir s3://path/data_cached
# 2. 训练
NPROC_PER_NODE=8 swift sft \
--model Qwen/Qwen3-VL-8B \
--cached_dataset s3://path/data_cached \
--packing true \
--max_length 10240 \
--train_type lora \
--output_dir output


本文作者:Dong
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC。本作品采用《知识共享署名-非商业性使用 4.0 国际许可协议》进行许可。您可以在非商业用途下自由转载和修改,但必须注明出处并提供原作者链接。 许可协议。转载请注明出处!