上承〔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 |
本篇要牢记的只有三个词:数据清洗、断点续训、缓冲区。
八、思考题
- 分词器的缓冲区机制预留了 36 个特殊 token 槽位。如果未来需要加第 37 个功能 token,会发生什么?
- DDP 包装时忽略了 RoPE 的
freqs_cos和freqs_sin缓冲区。如果不忽略,会有什么问题? _resume.pth里保存了优化器状态。如果只保存模型权重,不保存优化器状态,恢复训练后会有什么不同?- 数据清洗里,空
<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 博客的源码分析。