MiniMind 学习笔记之 21 训练细节:数据、分词器、断点续训

上承〔20 · 实验性变体:扩散语言模型与线性注意力〕。前面二十篇,我们把 MiniMind 的模型架构、训练算法、对齐流程、多模态扩展,全部拆了一遍。但有一层东西一直没正面讲:让这套流程真正跑起来的工程细节。 数据怎么洗、分词器怎么训、DDP 怎么配、断点续训怎么恢复------这些不在模型的数学里,但缺了它们,模型跑不起来。这一篇补上这些"幕后"的事。

一、数据清洗:从原始文本到训练数据

1.1 为什么要清洗

预训练数据的质量,直接决定模型的知识储备。原始数据里混杂着乱码、重复内容、低质量文本。这些东西不清理掉,模型学到的就是这些噪音。

MiniMind 的 README 里说得很清楚:提供全阶段开源数据,覆盖收集、蒸馏、清洗与去重后的高质量数据集。 清洗不是可选项,是训练流程的一部分。

1.2 清洗流程

MiniMind 的数据清洗管线做三件事:

第一,格式标准化。 把不同来源的数据统一成 JSONL 格式。每一行是一条数据,字段统一。

第二,启发式清洗。 按长度、语言比例、噪音模式过滤。太短的文本丢掉,中英文比例异常的丢掉,包含乱码的丢掉。

第三,动态注入。 在数据集加载时,通过 pre_processing_chat 和 post_processing_chat 函数动态注入系统提示和清理推理标签。

1.3 系统提示注入

pre_processing_chat 函数负责在对话历史中注入系统提示。

逻辑是:如果第一条消息不是 system 角色,有默认 20% 的概率 (add_system_ratio=0.2)会在前面加上一条随机系统提示。

系统提示包括"minimind, a lightweight intelligent assistant"这样的身份设定,以及通用的有帮助的 AI 人设。

工具调用数据被排除在系统提示注入之外,以保持复杂函数调用轨迹的完整性。

1.4 推理标签清理

post_processing_chat 函数负责清理推理标签。

对于训练了推理能力的模型,数据里的空 <think> 标签有 80% 的概率被移除 (由 empty_think_ratio=0.2 控制)。

目的: 防止模型学会输出空的思考标签,鼓励它要么实质性思考,要么完全跳过标签。

1.5 工具调用数据处理

MiniMind-3 把工具调用能力直接整合进了 SFT 主线。

工具数据在对话 JSON 里包含 tools 和 tool_calls 字段。SFTDataset.create_chat_prompt 方法会检测包含工具定义的 system 消息和包含工具调用的 assistant 消息,确保它们被正确传给 tokenizer.apply_chat_template。

二、分词器训练:词表的构成与缓冲区

2.1 训练脚本

train_tokenizer.py 负责训练自定义的 BPE 分词器。虽然 MiniMind 自带 6400 词表的默认分词器,但脚本仍然开放,供领域特定场景(医疗、法律、代码)使用。

训练参数:

参数 值 说明
vocab_size 6400 词表总大小
initial_alphabet ByteLevel.alphabet() 所有 256 个字节值
model_max_length 131 分词器最大长度

2.2 数据提取

训练过程通过 get_texts 生成器消费数据,从输入 JSONL 的 conversations 列表里提取 content 字段。默认处理 10000 行样本来构建词表统计。

2.3 词表的三个组成部分

词表由三部分拼成:

第一,标准 BPE token。 从语料里学出来的,达到 VOCAB_SIZE 上限。

第二,结构特殊 token。 模型运行必需的,比如 <|im_start|>(BOS)和 <|im_end|>(EOS)。

第三,功能标签。 MiniMind 特性专用的,包括 <think>(推理)和 <tool_call>(Agent 工作流)。

2.4 缓冲区机制

一个容易被忽略的细节:MiniMind 预留了缓冲 token。

为了确保特殊 token 的总数达到固定的 SPECIAL_TOKENS_NUM(默认 36),脚本会计算剩余槽位,用 <|buffer1|> 这样的占位 token 填充。

这样做的好处: 防止后续添加新功能 token 时索引发生偏移。词表一旦固定,token id 就固定了。如果后来加了新 token 导致 id 偏移,之前训练的 embedding 矩阵就全对不上了。缓冲区就是给未来留的"扩展槽"。

三、分布式训练:DDP、混合精度与编译加速

3.1 训练栈的组成

MiniMind 的训练栈是一个手写的 PyTorch harness,组合了三样东西:

  • 原生 torch.distributed(DDP) :数据并行。
  • bfloat16/float16 混合精度 :通过 torch.cuda.amp 实现。
  • 可选的 torch.compile:图级别的优化。

3.2 DDP 的启动

通过 torchrun 启动 DDP:

bash 复制代码
torchrun --nproc_per_node 4 train_pretrain.py

RANK / LOCAL_RANK 环境变量由 torchrun 提供,用于建立进程组和分配设备。

一个细节:DDP 包装模型时,显式忽略了 RoPE 的 freqs_cos 和 freqs_sin 缓冲区,不参与 DDP 的状态广播和分片。因为这些是预先计算好的常量表,每个 rank 都一样,不需要同步。

3.3 混合精度

混合精度通过 torch.cuda.amp.autocast 实现。GradScaler 只在用户选择 --dtype float16 时启用;如果选 bfloat16,不需要 GradScaler。

原因:bfloat16 的数值范围和 fp32 一样大,不会出现梯度下溢。fp16 的范围小,需要 GradScaler 把损失放大,防止梯度变成 0。

3.4 torch.compile

可选的 --use_compile 标志会把模型包装进 torch.compile。

保存 checkpoint 时,需要把 DDP 包装器(model.module)和编译包装器(_orig_mod)都剥掉,还原回原始的 nn.Module。否则加载时找不到对应的模块结构。

3.5 分布式采样

数据分片用 DistributedSampler,确保每个 rank 看到不同的数据子集。

还有一个自定义的 SkipBatchSampler 包装器,让断点续训能精确地从上次中断的 micro-batch 位置继续。

四、断点续训:双检查点策略

4.1 两种检查点

MiniMind 实现了一个双检查点策略:

检查点类型 文件名 内容 用途
最终模型权重 {weight}_{dim}{moe}.pth 模型参数 推理、部署
可恢复训练状态 {weight}_{dim}{moe}_resume.pth 模型 + 优化器 + epoch + step + wandb_id 断点续训

4.2 命名规则

text 复制代码
{save_dir}/{weight}_{hidden_size}{moe_path}.pth          # 最终权重
{save_dir}/{weight}_{hidden_size}{moe_path}_resume.pth   # 恢复状态

weight 是训练阶段标识(pretrain、full_sft、dpo、ppo_actor 等),hidden_size 是模型维度,moe_path 在 MoE 模型时为 _moe。

4.3 恢复机制

加载时,lm_checkpoint 函数会检查是否存在 _resume.pth 文件。如果存在,就从中恢复模型参数、优化器状态、scalar 状态、epoch 和 step。

支持跨 GPU 数量恢复。 如果中断时用了 4 张卡,恢复时只有 2 张卡,脚本会自动调整 step 计数。

保留 wandb_id。 恢复训练时,wandb 会接续同一个实验记录,不会新建一个 run。

原子文件操作。 保存时先写临时文件,再用 os.replace() 替换,防止写入过程中断导致检查点损坏。

4.4 使用方式

bash 复制代码
python train_pretrain.py --from_resume 1

加上这个参数,训练脚本会自动检测最新的检查点并恢复。

4.5 注意事项

版本兼容性:确保检查点保存和加载时使用的 MiniMind 代码版本一致,避免因代码变更导致不兼容。

硬件一致性:在不同硬件设备间迁移检查点时,注意处理设备映射问题(如从 GPU 到 CPU)。

训练数据顺序:确保恢复训练时数据加载的顺序与之前一致。

五、一个完整的训练命令

把上面的东西拼起来,一个完整的预训练启动命令是:

bash 复制代码
cd trainer
torchrun --nproc_per_node 4 train_pretrain.py \
  --from_resume 1 \
  --use_compile \
  --dtype bfloat16

如果只有单卡:

bash 复制代码
cd trainer
python train_pretrain.py --from_resume 1

预训练输出在 out/pretrain_768.pth,SFT 输出在 out/full_sft_768.pth。

六、回到开头的问题

数据清洗、分词器训练、DDP 配置、断点续训------这些不在模型的数学里,但缺了它们,模型跑不起来。

数据清洗决定了模型学到的"教材"质量。分词器的缓冲区机制决定了词表能不能安全扩展。DDP 和混合精度决定了训练能不能在合理的硬件上跑完。断点续训决定了训练中断后能不能接着来。

这些是 MiniMind 的"幕后",也是从"看懂原理"到"真的跑起来"之间必须跨过的那道坎。

七、本篇概念清单

概念 本篇交代到什么程度
数据清洗管线 讲透:格式标准化、启发式过滤、动态注入
系统提示注入 讲透:20% 概率、工具数据排除
推理标签清理 讲透:80% 概率移除空 think
分词器训练 讲透:词表三部分、缓冲区机制、10000 行采样
DDP 讲透:torchrun 启动、RoPE buffer 忽略
混合精度 讲透:bfloat16 vs fp16、GradScaler 条件
torch.compile 讲清:可选的编译加速、checkpoint 剥离
SkipBatchSampler 讲清:精确恢复 micro-batch 位置
双检查点策略 讲透:.pth vs _resume.pth
跨 GPU 恢复 讲清:step 自动换算
原子文件操作 讲清:临时文件 + os.replace

本篇要牢记的只有三个词:数据清洗、断点续训、缓冲区。

八、思考题

  1. 分词器的缓冲区机制预留了 36 个特殊 token 槽位。如果未来需要加第 37 个功能 token,会发生什么?
  2. DDP 包装时忽略了 RoPE 的 freqs_cos 和 freqs_sin 缓冲区。如果不忽略,会有什么问题?
  3. _resume.pth 里保存了优化器状态。如果只保存模型权重,不保存优化器状态,恢复训练后会有什么不同?
  4. 数据清洗里,空 <think> 标签有 80% 的概率被移除。如果设成 100% 移除,模型的推理能力会怎样?

备注:本篇的源码分析基于 MiniMind 仓库的 dataset/lm_dataset.py、trainer/train_tokenizer.py、trainer/trainer_utils.py 和 DeepWiki 的相关文档。数据清洗逻辑参考 DeepWiki 的 Data Preparation Pipeline。分词器训练的缓冲区机制参考 DeepWiki 的 Training Custom Tokenizers。DDP 和混合精度的实现参考 opendeep.wiki 的 Distributed, Mixed-Precision, and Compiled Training。断点续训的双检查点策略参考 DeepWiki 的 Checkpoint Management 和 gitcode 博客的源码分析。

相关推荐
RobinXYuan2 小时前
【Kafka 源码手记】01 从消息队列到 Event Streaming
kafka·源码阅读
cuiZero04 小时前
主流大模型系列对比:GPT、GLM、DeepSeek 与 Claude 选型指南
大数据·人工智能·gpt
还是奇怪1 天前
GPT-6 Sol 与 Luna 发布后,选型还要确认什么?从官方参数到迁移验证
人工智能·gpt·openai
星哥说事1 天前
GPT-6 Astra 发布了
gpt
深频率2 天前
6倍价格买8倍速度?GPT-6.1 Sol极速版的两个倍率
人工智能·gpt
RobinXYuan2 天前
【LevelDB 源码阅读】05 | WriteBatch:原子写的载体
源码阅读·leveldb
AI你一生一世2 天前
GPT-6 Astra 深度拆解:当推理模型开始为“长任务”重新设计
人工智能·gpt·大语言模型·astra·推理模型·长任务·gpt-6
Tangyuewei2 天前
GPT-6.1 Astra 被砍内情
gpt
Sammyyyyy2 天前
Claude Haiku 5.5 与 GPT-6.1 Sol 选型拆解:20 倍价差不等于 20 倍总成本,附网关分流思路
人工智能·gpt