ms-swift swift export --to_cached_dataset true
2026-02-10
ms-swift
00

目录

核心结论:export 阶段不执行 Packing
不同长度样本的处理(truncationstrategy=left, maxlength=20000)
情况:样本 tokenize 后长度 = 70k(超过 max_length 20000)
后续训练时 Packing 如何工作
多模态 token 过多
多模态占位符 token 超过 max_length 的情况
_truncate 的边界行为
完整流程图
三种训练场景下的行为
场景 A:用 cached dataset,训练 maxlength ≤ export maxlength
场景 B:用 cached dataset,训练 maxlength > export maxlength
场景 C:不用 cached dataset,直接训练
OOM 风险总结
根本解决方案
关键代码位置补充

在训练之前可以使用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

核心结论:export 阶段不执行 Packing

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)才会执行。

不同长度样本的处理(truncation_strategy=left, max_length=20000)

核心逻辑在 swift/llm/template/base.py:1279-1330_encode_truncated 方法。

情况:样本 tokenize 后长度 = 70k(超过 max_length 20000)

  1. Template 对样本做完整 tokenize,得到 input_ids(70k tokens)
  2. 检测到 length (70000) > max_length (20000)
  3. 因为 truncation_strategy='left',调用 _truncate 方法(base.py:1246-1266
  4. _truncate 的具体逻辑
    • 找出所有 placeholder tokens(多模态占位符,如图片/视频/音频对应的特殊 token)标记为 protected
    • 找出所有 非 protected 的 token 位置
    • left 截断:保留非 protected tokens 中最靠后的 max_length - n_protected 个(即截掉左边/前面的文本 token)
    • 所有多模态占位符 token 都被保留,不会被截断
    • 最终拼接 protected tokens + 保留的非 protected tokens,保持原始相对顺序
  5. 截断后长度变为 20000(或接近 20000)
  6. labels[0] = -100, loss_scale[0] = 0(截断后首个 token 不计算 loss)
  7. 将截断后的结果(含 length 字段)存入 cached dataset

关键点:对于多模态数据(Qwen3-Omni),视频/图片/音频的 placeholder token 不会被截,被截掉的是普通文本 token 的左侧部分(即对话的前面内容)。

后续训练时 Packing 如何工作

当使用 cached dataset 训练并开启 --packing true 时:

  1. 读取 cached dataset:每条样本已经 tokenize 好,并且有 length 字段
  2. PackingDataset (swift/llm/dataset/utils.py:135-233) 初始化时:
    • 读取所有样本的 length
    • 使用 bin-packing 算法binpacking.to_constant_volume,参考论文 arXiv:2404.10830)将样本分组
    • 每组的 总 token 数 ≤ packing_length(默认等于 max_length
    • 例如:如果 packing_length=20000,一条 12k 的样本和一条 8k 的样本会被分到同一组
  3. __getitem__ 返回一组样本的列表
  4. Data Collator 中,packing_rowbase.py:575-596)把同组多条样本的 input_idslabels 等直接拼接
  5. 每条子序列的 position_ids 从 0 开始重新编号,Flash Attention 通过 cu_seqlens 实现因果隔离

多模态 token 过多

如果光是视频和语音的 token 就超过设定值 20k。

多模态占位符 token 超过 max_length 的情况

_truncate 的边界行为

当视频/音频的 placeholder token 数量本身就超过 max_length 时,_truncatebase.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):

  1. if n_protected < self.max_length 条件不成立,整个 if 块被跳过
  2. 直接执行 input_ids = input_ids_tensor[protected].tolist()
  3. 结果:只保留全部 protected(多模态占位符)token,所有文本 token 全部丢弃
  4. 最终序列长度 = n_protected = 25000超过 max_length 20000,但代码不报错

后续训练时超长样本的完整数据流

cached dataset 的重要特性(ms-swift >= 3.11)

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 → 送入模型 无截断,无长度校验

三种训练场景下的行为

场景 A:用 cached dataset,训练 max_length ≤ export max_length

展开代码
export 时: max_length=20000, 样本多模态 token=25000 → length 存为 25000 训练时: max_length=20000 _select_dataset: 25000 > 20000 → 该样本被丢弃,不参与训练

结果:安全,样本被过滤掉。

场景 B:用 cached dataset,训练 max_length > export max_length

展开代码
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 的显存。

场景 C:不用 cached dataset,直接训练

展开代码
训练时: 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。


OOM 风险总结

训练方式多模态 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.py161-169
get_cached_dataset 加载入口swift/llm/infer/utils.py187-257
LazyLLMDataset.__getitem__ 重新 tokenizeswift/llm/dataset/utils.py90-102
_data_collator 无截断swift/llm/template/base.py1702-1819
训练时加载 cached datasetswift/llm/train/sft.py119-146
如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:Dong

本文链接:

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