在训练之前可以使用swift export进行 token 缓存,使用这个指令:
展开代码IMAGE_MAX_TOKEN_NUM=5000 USE_AUDIO_IN_VIDEO=true VIDEO_MAX_PIXELS=307200 USE_AUDIO_IN_VIDEO=true \ swift export \ --model /mnt/cpfs/model/Qwen3-Omni-30B-A3B-Instruct \ --dataset /mnt/cpfs/datasets/ShortsTemplateEdits/anno_jianying_mp4_seed18_train.jsonl \ --split_dataset_ratio 0 \ --dataset_num_proc 72 \ --to_cached_dataset true \ --max_length 20000 --truncation_strategy left \ --output_dir /mnt/cpfs/datasets/ShortsTemplateEdits/pack_cached_cpfs/anno_jianying_mp4_seed18_train_len20k
truncation_strategy split 策略只允许用于预训练(pre-training),不允许用于 SFT
swift export --to_cached_dataset true 流程中不会执行 packing。代码在 swift/llm/argument/export_args.py:151-153 明确禁止:
python展开代码if self.to_cached_dataset:
self.lazy_tokenize = False
if self.packing:
raise ValueError('Packing will be handled during training; here we only perform tokenization '
'in advance, so you do not need to set up packing separately.')
to_cached_dataset 做的事是:对每条样本执行 tokenize,计算 length,应用 truncation_strategy,然后把结果以 Arrow 格式存盘。Packing 只在后续训练时(swift sft --packing true)才会执行。
核心逻辑在 swift/llm/template/base.py:1279-1330 的 _encode_truncated 方法。
input_ids(70k tokens)length (70000) > max_length (20000)truncation_strategy='left',调用 _truncate 方法(base.py:1246-1266)_truncate 的具体逻辑:
left 截断:保留非 protected tokens 中最靠后的 max_length - n_protected 个(即截掉左边/前面的文本 token)labels[0] = -100, loss_scale[0] = 0(截断后首个 token 不计算 loss)length 字段)存入 cached dataset关键点:对于多模态数据(Qwen3-Omni),视频/图片/音频的 placeholder token 不会被截,被截掉的是普通文本 token 的左侧部分(即对话的前面内容)。
当使用 cached dataset 训练并开启 --packing true 时:
length 字段swift/llm/dataset/utils.py:135-233) 初始化时:
lengthbinpacking.to_constant_volume,参考论文 arXiv:2404.10830)将样本分组max_length)__getitem__ 返回一组样本的列表packing_row(base.py:575-596)把同组多条样本的 input_ids、labels 等直接拼接position_ids 从 0 开始重新编号,Flash Attention 通过 cu_seqlens 实现因果隔离如果光是视频和语音的 token 就超过设定值 20k。
_truncate 的边界行为当视频/音频的 placeholder token 数量本身就超过 max_length 时,_truncate(base.py:1246-1266)的关键分支:
python展开代码n_protected = protected.sum().item() # 多模态占位符 token 数量
if n_protected < self.max_length: # 关键判断
# 正常路径:protected 数量 < max_length,还有空间留给文本 token
non_protected = (~protected).nonzero(as_tuple=True)[0]
idx = non_protected[-(self.max_length - n_protected):] # left 截断
protected[idx] = True
# 如果 n_protected >= max_length,直接跳过 if,不做任何额外保留
input_ids = input_ids_tensor[protected].tolist() # 只保留 protected 的 token
当 n_protected >= max_length 时(例如视频 token = 25000,max_length = 20000):
if n_protected < self.max_length 条件不成立,整个 if 块被跳过input_ids = input_ids_tensor[protected].tolist()n_protected = 25000,超过 max_length 20000,但代码不报错cached dataset 只存原始数据字段(messages、images 等)+ 预计算的 length 字段,不存 input_ids。训练时每条样本会通过 LazyLLMDataset 重新 tokenize。
展开代码加载 cached dataset (load_from_disk) │ ├─ _select_dataset (swift/llm/infer/utils.py:161-169): │ 过滤: length > 训练时的 max_length → 整条样本丢弃 │ 注意: 这里用的是 export 时预计算的 length 值 │ ├─ 剩余样本进入 LazyLLMDataset (swift/llm/dataset/utils.py:62-116) │ │ │ └─ __getitem__ 时重新 tokenize: │ template.encode() → _encode_truncated() → _truncate() │ 此时再次应用 max_length 截断 │ ├─ PackingDataset (swift/llm/dataset/utils.py:135-233): │ bin-packing 分组,约束每组总 token ≤ packing_length │ 不对单条样本做截断或拒绝 │ ├─ packing_row (swift/llm/template/base.py:575-596): │ 纯拼接,无任何长度校验 │ └─ data_collator → 送入模型 无截断,无长度校验
展开代码export 时: max_length=20000, 样本多模态 token=25000 → length 存为 25000 训练时: max_length=20000 _select_dataset: 25000 > 20000 → 该样本被丢弃,不参与训练
结果:安全,样本被过滤掉。
展开代码export 时: max_length=20000, 样本多模态 token=25000 → length 存为 25000 训练时: max_length=30000 _select_dataset: 25000 ≤ 30000 → 样本通过过滤 LazyLLMDataset: 重新 tokenize → _truncate → n_protected=25000 < max_length=30000 → 正常截断,保留 25000 个多模态 token + 5000 个文本 token = 30000
结果:安全,但单条样本占用 30000 token 的显存。
展开代码训练时: max_length=20000, 样本多模态 token=25000 _encode_truncated → _truncate: n_protected=25000 >= max_length=20000 → 跳过 if 保留全部 25000 个多模态 token,文本全丢 返回 length=25000,超过 max_length=20000 后续流程无任何截断 → 25000 token 的序列直接送入模型
结果:有 OOM 风险。序列长度超过预期的 max_length。
| 训练方式 | 多模态 token > max_length 时的行为 | OOM 风险 |
|---|---|---|
| 用 cached dataset,训练 max_length ≤ export max_length | _select_dataset 丢弃该样本 | 安全 |
| 用 cached dataset,训练 max_length > export max_length | 重新 tokenize,_truncate 保留全部多模态 token + 剩余文本 token | 取决于实际长度 |
| 不用 cached dataset,直接训练 | _truncate 保留全部多模态 token,超长序列直接送入模型 | 有 OOM 风险 |
从源头控制多模态 token 数量,确保其不超过 max_length:
VIDEO_MAX_PIXELS:控制视频帧的分辨率,降低视频 token 数量IMAGE_MAX_TOKEN_NUM:直接限制单张图片的最大 token 数FPS(如适用):降低视频采样帧率,减少总帧数多模态 token 数 + 必要文本 token 数 ≤ max_length| 功能 | 文件路径 | 行号 |
|---|---|---|
_select_dataset 过滤 | swift/llm/infer/utils.py | 161-169 |
get_cached_dataset 加载入口 | swift/llm/infer/utils.py | 187-257 |
LazyLLMDataset.__getitem__ 重新 tokenize | swift/llm/dataset/utils.py | 90-102 |
_data_collator 无截断 | swift/llm/template/base.py | 1702-1819 |
| 训练时加载 cached dataset | swift/llm/train/sft.py | 119-146 |


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