torchtune 数据集体系完整拆解:原始数据如何经过四个插槽变成模型输入
【免费下载链接】torchtunePyTorch native post-training library项目地址: https://gitcode.com/GitHub_Trending/to/torchtune
在 torchtune 中,任何数据集进入训练都走同一条四插槽流水线:source加载数据、message_transform标准化为Message列表、model_transform分词、collate_fn组装 batch。前三个插槽全部收敛在SFTDataset这个类里,而数据加载被整体委托给 Hugging Face 的load_dataset,torchtune 不自建任何下载或解析层。弄懂这四个插槽的边界后,接入自己的数据最多只需要一个自定义Transform子类和一行 yaml。
本文面向后训练的数据侧工作:读完你能为自己的数据写出可用的 yaml dataset 段,也能在 labels 里看到 -100、样本超长报错时直接定位到对应组件。全文跟随一条 sharegpt 风格的本地 JSON 样本,按它流经的环节从磁盘一路拆到训练 batch。
数据加载委托给 HF load_dataset:source 与 split 都跟随 HF 语义
SFTDataset不管数据文件本身,它只是load_dataset的一个薄封装,证据在 datasets/_sft.py 的__init__里:self._data = load_dataset(source, **load_dataset_kwargs)是数据相关的第一行实质代码。
你看到的现象是:yaml 里source既可以写 HF 仓库名从 Hub 拉取,也可以写json、csv、text这类文件类型并配合data_files加载本地或远程文件;split甚至能写"train[:10%]"这种 HF 切片语法。原因很直接:split、name、data_dir、data_files这些参数都通过**load_dataset_kwargs原样透传给load_dataset,HF 支持什么加载方式,torchtune 就支持什么加载方式。
另一个入口是filter_fn/filter_kwargs:它在加载完成后、任何转换发生之前对行做过滤,适合在进入分词前剔除低质量样本。注意加载动作发生在构造期,__getitem__只触发逐样本转换,所以普通 SFT 路径下构建 dataset 之后第一次取样本的开销很小;后面第六节的PackedDataset是例外。
下面这段配置就是"透传"的直接证据:
dataset: _component_: torchtune.datasets.instruct_dataset source: json # 本地文件时传文件类型 data_files: my_data.csv split: "train[:10%]" # HF 切片语法,同样透传message_transform 把原始行变成 Message 列表:内建转换与 column_map
message_transform的职责只有一个:按column_map从样本行里取列,产出Message对象列表,结果必须放在返回字典的"messages"键下(偏好数据集用"chosen"/"rejected"两个键)。data/_messages.py 里的Message是整条流水线的唯一数据单元,四个字段各管一件事:role限定了system/user/assistant/ipython/tool五种取值;content是字符串或[{"type": "text"|"image", "content": ...}]列表,图片必须是load_image产出的torch.Tensor而不是路径;masked是 loss 开关;eot控制消息尾部是否追加换轮 token。
仓库内置了四个转换类,覆盖绝大多数公开 schema:
| transform | 期望的输入格式 | column_map 的必备键 |
|---|---|---|
InputOutputToMessages | 两列文本:input(prompt)、output(响应) | input、output |
ShareGPTToMessages | conversations:[{"from": "system"|"human"|"gpt", "value": ...}] | conversations |
OpenAIToMessages | messages:[{"role": ..., "content": ...}],content 可含image_url段 | messages |
AlpacaToMessages | 三列instruction/input/output,内置 alpaca 原始模板 | instruction、output |
column_map的语义是"期望列名 → 你的实际列名",例如{"input": "question", "output": "answer"};传入时若缺少任何期望键会直接抛ValueError,这在构造期就能发现数据问题。ShareGPTToMessages内部用{"system": "system", "human": "user", "gpt": "assistant"}做角色映射;若设置了new_system_prompt,原始 system 消息会被跳过并在头部补一条masked=True的系统消息,即"覆盖"而非"追加"。
多模态样本在这里完成图片接入:ShareGPTToMessages发现image列存在时调用load_image加载为 Tensor,若给了image_tag就用format_content_with_images按占位符把文本和图像交错切分(占位符数量与图像数量不一致会抛ValueError),没给则整图前置到第一条 user 消息;官方 builder 的文档注明当前一个样本支持单张图像路径。
消息合法性由validate_messages兜底,规则是:至少 2 条消息;system 只能在第 0 位;assistant 前必须出现过 user/tool/ipython;不允许连续两条 user。这些错误都会在__getitem__时抛出,是你自查数据格式的第一现场。
一条 sharegpt 样本及其转换产物如下:
{ "conversations": [ {"from": "human", "value": "What's the capital of France?"}, {"from": "gpt", "value": "Paris."} ] }经过ShareGPTToMessages后得到两条Message:user 消息带masked=True,assistant 消息带masked=False(默认策略下),随后才进入分词。
masking_strategy 决定 -100 出现在哪里:train_on_input 已是废弃参数
现象先说:对任意样本取ds[0]["labels"],prompt 对应的区间全是 -100,只有响应部分是真实 token id,末尾恒为 -100。原因分两跳。第一跳在 data/_messages.py 的mask_messages:它按策略给每条Message设置masked,其中 system 消息无条件 mask,多模态的 user 消息也无条件 mask。第二跳在 datasets/_sft.py 的SFTTransform.__call__里,labels 的生成就这几行:
# mask 为 True 的位置置 CROSS_ENTROPY_IGNORE_IDX(-100),其余保留 token tokenized_dict["labels"] = list( np.where(tokenized_dict["mask"][1:], CROSS_ENTROPY_IGNORE_IDX, tokenized_dict["tokens"][1:]) ) tokenized_dict["labels"].append(CROSS_ENTROPY_IGNORE_IDX)取[1:]再补一个 -100,是把 labels 相对输入右移一位以对齐交叉熵的 logits;CROSS_ENTROPY_IGNORE_IDX = -100定义在 data/_common.py。
策略本身是MaskingStrategy枚举的三个值:
| masking_strategy | masked 结果 | 适用场景 |
|---|---|---|
train_on_all | user 与 assistant 都不 mask(多模态 user 除外) | 想让 prompt 也参与 loss |
train_on_assistant | 只有 assistant 不 mask | 常规指令微调,默认值 |
train_on_last | 只放最后一条 assistant | 多轮中只学最后一答 |
📌 易错点:各 builder 暴露的train_on_input已经触发DeprecationWarning,官方映射是True → "train_on_all"、False → "train_on_assistant";两者同时传时,在 0.7 版本前旧标志优先、新参数被忽略。而且默认值不一致:instruct_dataset/chat_dataset/preference_dataset默认train_on_input=False(只学响应),alpaca_dataset默认True(prompt 也计入 loss),迁移配置时先核对这一项。
tokenizer 就是 model_transform:tokens、mask 与截断的出处
text 数据里model_transform槽直接放 tokenizer 对象,因为每个模型 tokenizer 类(如 models/llama3/_tokenizer.py 的Llama3Tokenizer)都实现了__call__,输入带messages键的样本、输出带tokens/mask键的样本。ModelTokenizer这个 Protocol 在 modules/transforms/tokenizers/_utils.py 里要求三个成员:special_tokens、max_seq_len,以及返回(list[int] tokens, list[bool] mask)的tokenize_messages。
以Llama3Tokenizer.tokenize_messages为例,"模型专属"逻辑全在这里:开头放<|begin_of_text|>且该位 mask 恒为 True;每条消息先拼<|start_header_id|>角色<|end_header_id|>\n\n这样的头部再编码正文,该消息所有 token 的 mask 复制自message.masked;结尾追加<|eot_id|>(eot=False时是<|eom_id|>);末尾补<|end_of_text|>并同样 mask。超出max_seq_len时按truncation_type(默认 right)截断并保住尾部 eos。prompt_template(如QuestionAnswerTemplate)在分词前作用于消息,模板文本按普通文本编码,不是特殊 token。
SFTTransform对model_transform的契约只有一条:返回字典必须同时含tokens和mask两个键,否则抛ValueError并把实际键名列出来。这就是多模态场景下要把普通 tokenizer 换成如llama3_2_vision_transform的原因——它额外产出图像 tile 张量,同时仍满足该契约。
tokenizer 的文档示例直观展示了 tokens 与 mask 的对应关系:
messages = [ Message(role="user", content="Hello world!", masked=True), Message(role="assistant", content="How are you?", masked=False), ] Llama3Tokenizer("/path/to/tt_model").tokenize_messages(messages) # ([1, 31587, 29644, 102, 1, 31587, 29644, 102, 2], # [True, True, True, True, True, False, False, False, True])collate_fn 从五个 collater 中选型,完成 padding 与 batch 组装
dataset 单样本输出只是变长 dict,tensor 形状由 yaml 顶层的collate_fn决定。data/_collate.py 提供五个实现:padded_collate(L56,通用版,参数pad_direction/keys_to_pad/padding_idx/pad_to_multiple_of)、padded_collate_sft(L190,SFT 文本样本)、padded_collate_tiled_images_and_mask(L282,VLM 的 tile 图像与 mask)、padded_collate_packed(L562,打包样本的块状因果 mask)、padded_collate_dpo(L623,偏好对的四五个键);另有一个left_pad_sequence辅助函数支持左 padding。
真实配置里能直接看到选型写法,Llama 3.2 Vision 11B 的单卡 LoRA 配置 recipes/configs/llama3_2_vision/11B_lora_single_device.yaml:
dataset: _component_: torchtune.datasets.multimodal.the_cauldron_dataset packed: False subset: ocrvqa collate_fn: torchtune.data.padded_collate_tiled_images_and_mask其中 tokenizer 段的image_size: 560与 model 段必须一致,这也是该配置里特意注释提醒的一处。而PackedDataset的输出在构造期就已 pad 到max_seq_len,batch 层面不再需要额外 padding,collate 退化为直通。
六个任务 builder 是预填组合:packed 与 preference 有硬限制
torchtune.datasets的导出清单(datasets/init.py)里,除通用入口外还有alpaca_dataset、grammar_dataset、samsum_dataset、slimorca_dataset、hh_rlhf_helpful_dataset、stack_exchange_paired_dataset、cnn_dailymail_articles_dataset、wikitext_dataset等面向公开数据集的 builder,以及ConcatDataset/PackedDataset两个组合工具。核心入口的差异只是预填了哪个 message_transform、附加了哪些限制:
| builder | 预填 transform | 特有参数 | 硬限制 |
|---|---|---|---|
instruct_dataset(_instruct.py) | InputOutputToMessages | column_map、new_system_prompt | packed=True要求max_seq_len |
chat_dataset(_chat.py) | ShareGPTToMessages或OpenAIToMessages | conversation_column、conversation_style | style 不在sharegpt/openai内直接ValueError |
preference_dataset(_preference.py) | ChosenRejectedToMessages | column_map(chosen/rejected) | packed=True抛错,暂不支持 |
text_completion_dataset(_text_completion.py) | 无 Message,直接encode | column、add_eos、split_across_pack | 无 |
multimodal_chat_dataset(multimodal/_multimodal.py) | ShareGPTToMessages+ 图像列 | image_tag、image_dir | packed=True抛错 |
| 自定义 | 自己写的Transform | filter_fn | 无 |
PackedDataset(datasets/_packed.py)的机制值得单独看:它在__init__里就把整个数据集 tokenize 并按max_seq_len贪心拼包,训练期不再分词。打包样本返回四件套tokens/labels/input_pos/seq_lens,配套的padded_collate_packed用块状下三角 mask 防止样本互相注意,文档里的示例很直观:max_seq_len=6、样本长 3 和 2 时,
tokens = [S1, S1, S1, S2, S2, pad] mask = [1 0 0 0 0 0 1 1 0 0 0 0 1 1 1 0 0 0 0 0 0 1 0 0 0 0 0 1 1 0 0 0 0 0 0 1] input_pos= [0, 1, 2, 0, 1, 2]split_across_pack控制超长样本的去向:text_completion_dataset默认True(把样本切进下一个包,适合语料),instruct_dataset/chat_dataset走PackedDataset默认False(样本超过max_seq_len时初始化直接抛 "Dataset sample is too long",适合保证指令完整性)。另外两条路径要记住差异:偏好数据由PreferenceDataset单独实现,输出chosen_input_ids/chosen_labels/rejected_input_ids/rejected_labels四个键,labels 与 input_ids 一一对齐、不做 SFT 路径那种右移一位;持续预训练数据则完全不经过Message,TextCompletionDataset直接encode(add_bos=True, add_eos=add_eos)、截断到max_seq_len - 1,labels 就是tokens[1:]再补 -100。
自定义 schema 接入实操:一个 Transform 进 recipe
当数据 schema 落不进上表时,官方路径就是自己写一个Transform再交给SFTDataset,三步完成。第一步,继承Transform实现__call__,返回带messages键的字典:
# data/message_transform.py from typing import Any, Mapping from torchtune.data import Message from torchtune.modules.transforms import Transform class MyMessageTransform(Transform): def __call__(self, sample: Mapping[str, Any]) -> Mapping[str, Any]: return { "messages": [ Message(role="user", content=sample["prompt"], masked=True, eot=True), Message(role="assistant", content=sample["answer"], masked=False, eot=True), ] }masked=True/False在这里直接决定 loss 范围,对应前文 labels 的 -100 逻辑;eot控制尾部 token。第二步,写一个小型 builder 把 transform 包进SFTDataset:
# data/dataset.py from torchtune.datasets import SFTDataset from data.message_transform import MyMessageTransform def custom_dataset(tokenizer, **load_dataset_kwargs): return SFTDataset( source="json", data_files="my_data.json", split="train", message_transform=MyMessageTransform(), model_transform=tokenizer, # 文本数据直接传 tokenizer **load_dataset_kwargs, )第三步,yaml 里让dataset段指向你的模块即可,tokenizer 仍由 recipe 传入:
dataset: _component_: data.dataset.custom_dataset📌 上线前用这份清单自查:column_map缺期望键会构造期报错;train_on_input是废弃参数,新配置改用masking_strategy;packed=True而max_seq_len为None会抛ValueError,且 preference 与 multimodal 两个 builder 目前明确不支持打包;Message里的图像必须是load_image的 Tensor 输出,放路径字符串会在分词时报错;占位符数量与图像数量不一致同样构造期即炸;样本超max_seq_len且split_across_pack=False时,PackedDataset初始化会报错,二选一调参解决。
四个插槽回到一句话:加载是 HF 的语义、Message是唯一下游数据单元、tokenizer 是model_transform、collater 只管形状,所以接入新数据的决策面只有"数据放哪(source)、列怎么映射(column_map)、loss 算哪段(masked)"三件事。
- 数据加载零自研:
SFTDataset的__init__一行load_dataset(source, **load_dataset_kwargs)决定了 source、split、data_files 全部跟随 HF 行为。 - loss 范围由
masked传导:mask_messages设旗、SFTTransform把 mask 转成 -100 并右移一位,这是 labels 里 -100 的唯一来源。 - 各任务 builder 只是"预填 transform + 附加限制"的组合,理解一个
SFTDataset就理解了全部六个入口。 - 自定义 schema 的成本被压到一个
Transform子类加一行_component_,不需要碰分词与 batch 组装。
延伸阅读:torchtune/data/_messages.py 里各 transform 与validate_messages的完整规则;torchtune/datasets/multimodal/ 下的the_cauldron_dataset与llava_instruct_dataset;recipes/configs/ 目录下的真实任务配置。
【免费下载链接】torchtunePyTorch native post-training library项目地址: https://gitcode.com/GitHub_Trending/to/torchtune
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考