从零训练1亿参数小模型:单卡走通预训练到DPO全流程

发布时间:2026/9/26 18:49:26
从零训练1亿参数小模型:单卡走通预训练到DPO全流程 1. 项目起点为什么给一个小模型起名 Xihe还要从零开始训去年冬天我给自己定了一个看起来很疯狂的KPI用一张消费级显卡把一条完整的语言模型训练链路全部走一遍——预训练、CPT、SFT、PEFT、蒸馏、DPO一个环节都不跳过。最终产物是一个我自己命名的参数量只有1亿出头的小模型代号就叫 Xihe。这个名字没有特殊含义纯粹是项目目录里的一个随手代号但跑完整个流程之后我才意识到它更像一条“西合”之路把大模型工业界常用的训练手段全部压缩到一个可以靠个人算力完成的规模上逐一验证、逐一踩坑、逐一复盘。先说明一下我做这件事不是想和开源社区已有的中文底座模型竞争。说实话直接下载一个成熟的中文预训练模型再跑SFT和DPO两三天就能交差效果还比我这种从零开始的方案好得多。但这么做的代价是预训练、CPT这些偏“地基”的阶段对你来说永远是黑盒。我看到太多人拿着现成预训练权重做微调遇到中文领域效果不好第一反应是改Prompt、堆SFT数据却很少有人想过继续预训练这条路遇到对话风格不对只知道RLHF却连DPO和传统PPO的差别都讲不清。所以我坚持从最底层开始把全流程打通一遍。Xihe 的技术路线是先做通用语料预训练得到 base 模型再做中文继续预训练CPT然后用 LoRA 做指令微调SFT接着用更大的教师模型蒸馏压缩最后用 DPO 做偏好对齐。整条链路里 PEFT 不是独立的第五步而是贯穿 SFT 和 DPO 的“省钱”策略后面会详细展开。这篇文章适合两类人一是想在有限算力下完整见识大模型训练全流程的开发者二是有一定SFT/微调经验、但想补上预训练和偏好对齐知识盲区的研究者。我把自己的超参设置、数据配比、踩坑过程都写了进去你可以把它当一份“单卡从零训语言模型”的工程手记来读。2. 预训练的地基语料、分词器与超参清单2.1 语料选择与清洗管道预训练的质量七分靠数据。Xihe 的目标是中文通用能力但我并不想完全丢掉英文语料因为小模型的跨语言迁移能力很差如果只喂中文后续做代码、技术文档类任务时会非常吃力。我最初收集了大约 20GB 原始文本来源是几个公开可用的网页语料、百科语料和开源代码快照。这里必须强调语料清洗不是洁癖而是刚需。我见过太多新手把网上爬下来的文本直接丢进 tokenizer结果模型学会了一堆网站导航、重复弹窗、乱码和“阅读全文”。我的清洗管道是按行去重和文档级去重。用MinHash处理近似重复文档这一步能砍掉约30%数据。过滤过短文档、纯标点文档、乱码比例高的文档。去除明显的导航文案、版权声明、Cookie提示等噪声模板。统一全角半角规范标点但保留代码缩进和换行结构。按语言粗分类中文和英文语料分开统计方便后面配比。清洗后最终留下约 15GB 有效文本。字符数大约 48 亿用 32K 词表的 BBPE 分词器切完后有效训练 token 大约 2.1B。这个规模对 0.12B 参数的模型来说不算少但也没到很充裕的程度所以我用 2 个 epoch 来做预训练。2.2 训练分词器词表大小与特殊Token分词器我选择训练一个 32K 词表的 BBPEByte-level BPE而不是直接用现成的中英文分词器。原因是 Xihe 要自己控制词表与模型规模的匹配度。0.1B 级小模型的 embedding 占比很大词表从 50K 降到 32Kembedding 参数直接少了约 1800 万个这对显存和收敛速度都是实打实的收益。训练词表时要记得预留特殊 token 位置。除了[PAD],[UNK],[BOS],[EOS]我额外安插了 20 个保留 token 槽位后面做 SFT 的 system/user/assistant 标记时不用重新扩展词表、不用改 embedding 维度。这个经验是从同事那学来的先别扭五分钟后面省五小时。2.3 模型架构与超参配置Xihe 的架构参考了 GPT-2/LLaMA 系列的做法decoder-only12 层 Transformer隐藏维度 76812 个注意力头中间 FFN 维度 3072最大序列长度 1024。总参数量约 0.12B其中 embedding 占大头的现象非常明显。Loss 用标准交叉熵计算时把 padding 部分 mask 掉。训练超参我根据自己的经验做了如下设定参数设定值说明序列长度1024小于GPT常用的2048降低显存Batch Size64 条 × 1024 token等效 batch 约 64K token学习率3e-4采用 cosine decayWarmup Steps2000前2000步线性上升到峰值AdamW beta(0.9, 0.95)参考LLM常用配置权重衰减0.1只作用于非 bias 参数Gradient Clip1.0防止早期 spikeDropout0.1小模型可适当提高训练轮数2token 量不够适度重复我在单张 24GB 显存的卡上跑显存占用峰值约 18GB。如果显存更低可以用更大的梯度累积步数把有效 batch 撑上去。预训练阶段的 loss 曲线从初始的 9.5 左右一路降到 2.4 左右。这里有个很重要的判断小模型的 loss 降得越快不代表数据质量越好很可能只是语料太简单。所以我会定期抽样生成一些句子看模型是否在语法正确的前提下产生有语义的延续而不是只会背语料。3. CPT 继续预训练让通用模型“回炉”中文与垂直语境3.1 为什么 base 模型之后还要 CPT按标题的顺序预训练之后是 CPT。可能有人觉得预训练不是已经训过中文了吗为什么还要单独加一个继续预训练如果你用的是纯中文语料从零预训练CPT 确实可以跳过。但我的 base 语料里中英混训模型对中文的理解只能说“够用”一旦进入技术文档、客服对话这种行文风格更固定的场景生成内容会明显变得泛泛而谈。CPTContinue Pre-Training本质是在已训练好的 base 模型上用特定领域的高质量数据继续做自回归训练。它的位置很微妙太靠前会被后续 SFT 冲掉太靠后英语基础又不够牢靠。我选择把 CPT 放在预训练和 SFT 之间专用语料占比在 30% 左右。3.2 数据配比与训练策略CPT 的数据配比是60% 中文通用语料、20% 代码与技术文档、20% 英文通用语料。注意最后那 20% 英文非常关键它是用来“防遗忘”的。如果整个 CPT 阶段完全换成中文语料模型对英文的理解会很快退化预训练阶段积累的跨语言能力就亏了。这种混合比例的做法在领域继续预训练里很常见效果也比单纯堆领域数据稳。训练时我用了更保守的策略学习率降到 2e-5warmup 只设 200 步训练 1 个 epoch。为什么学习率要降这么多因为 base 模型已经收敛到比较好的状态过大的学习率等价于把模型从当前最优位置粗暴地拽向新领域容易造成灾难性遗忘。实测下来CPT 后中文困惑度从 8.2 降到 5.6英文困惑度只反弹了约 8%属于可接受范围。3.3 判断 CPT 是否生效CPT 结束后的评估不能只看 loss。我当时专门准备了一组“领域探测”问题例如让模型补全一段技术文档、续写一份客服工单再看生成结果是否明显更贴合领域术语。如果 base 模型生成的是“你的问题我们已经收到现在为您转接”CPT 后的模型应该能生成包含具体处理流程的回复。这种感受只有人工检查才可靠loss 下降并不等于真的学到了领域语义。我在这里踩过最大的坑是CPT 阶段数据里插入了大量“QA对”但模型还没学会指令跟随它只会把问题也当作正文的一部分继续复述导致后面 SFT 时还要花额外精力清洗掉这种掺杂格式。CPT 语料尽量保持自然文本或半结构化文本不要提前混入指令数据各阶段的数据边界要清晰。4. SFT 指令微调把“会续写”变成“会听话”4.1 指令数据集如何构造SFT 是整个流水线里最能“人工干预”质量的环节。Xihe 的指令数据来源主要有三块公开的 Alpaca 风格中文指令集、我自己人工写的大约 500 条高质量问答、以及从技术文档中自动生成的摘要与改写任务。总量 3 万条左右对 0.12B 模型来说足够触发明显的格式变化。数据格式我统一转成system / user / assistant三段式。system 里写明角色约束例如“你是技术助手请用简洁准确的中文回答”。user 是用户输入assistant 是标准答案。训练时最重要的一个细节是user 部分和 system 部分的 token 在计算 loss 时要 mask 掉只让模型学习 assistant 部分的生成。否则模型会学到“复述用户问题”这种坏习惯而不是真正回答问题。4.2 SFT 训练细节与超参SFT 阶段我直接用了 LoRA 来做参数高效微调这是标题里 PEFT 在 SFT 中的首次落地。LoRA 只更新注入的低秩矩阵训练参数量只有全参微调的 1.2%显存峰值降到 12GB 以内。LoRA 的具体内容下一节展开这里先给 SFT 的关键超参学习率2e-4比全参微调高一个量级因为 LoRA 可训练的参数量少需要更大步长。LoRA rank16alpha32作用于 attention 的 q、v、k、o 四个投影矩阵。epoch3按 loss 和人工样例综合判断跑到第 3 个 epoch 时已经开始轻微过拟合但生成质量反而最好。使用 packing 方式把短样本拼接成 1024 长度训练效率更高但必须在样本边界插入 eos 并保持 attention mask 正确。4.3 SFT 的效果怎么验收SFT 的验收标准不是 train loss而是三个硬指标格式正确率、首 token 延迟、回复长度分布。我构造了 50 条固定测试题覆盖问答、翻译、摘要、代码解释四类。格式正确率指回复是否以“assistant”的格式自然开始、是否有明显重复或截断。Xihe 在 SFT 前完全不会“回答问题”它会像续写小说一样把用户的话接下去SFT 后已经能输出像样的三段式回复但内容深度依然有限知识密度也不高这是小模型的体能边界不是训练方法的问题。这里想提醒一句SFT 不是数据越多越好3 万条对 0.12B 模型已经很够用堆到 10 万以上反而容易让预训练学到的通用知识被指令格式淹没。小模型的容量有限指令数据要精细不要贪多。5. PEFT 参数高效微调预算有限时LoRA 是性价比主线5.1 为什么不用全参微调标题把 PEFT 单列在 SFT 之后但我的实际操作里SFT 和后续 DPO 都用的是 PEFT所以这一章专门把方法论说透。全参微调一个 0.12B 模型虽然也能跑但在这个项目里我刻意不用一方面全参微调会把预训练学到的通用分布冲掉小模型尤其明显另一方面后面 DPO 阶段通常要跑多个实验如果每次都全参微调存储和显存成本很夸张。PEFT 让我可以在同一个 base 模型上叠加不同任务的 LoRA 适配器切换成本几乎为零。5.2 LoRA 的原理与直觉理解LoRA 的思路用一句话说全量参数更新矩阵往往是低秩的不需要直接学习一个完整矩阵只要学两个小矩阵的乘积。假设原始权重矩阵 W 是 768×768LoRA 注入一个 r16 的低秩路径A 矩阵是 768×16B 矩阵是 16×768训练时只更新 A 和 B最后 W 相当于原权重加了一个 AB 的乘积。这样参数量从 589824 降到 24576少了 96%。推理时可以把 AB 合并回 W不增加额外显存。初始化时 B 通常置为 0这样训练开始的一瞬间模型输出和原始模型完全一致不会有突然的扰动。这个设计是 LoRA 能稳定训练的关键自己在实现时可别把 A、B 都随机初始化。5.3 LoRA 超参与调优经验参数SFT阶段取值DPO阶段取值说明rank168DPO用更低秩防止破坏SFTalpha3216控制缩放幅度target_modulesq,v,k,oq,v按需缩窄范围lora_dropout0.050.0推理稳定性优先biasnonenoneLoRA项目惯例一个很反直觉的结论是LoRA 的 rank 不是越大越好。rank 从 16 提到 64SFT 的 loss 可以降得更快但生成质量没有提升反而更容易过拟合训练集的“机械答法”。DPO 阶段我甚至故意把 rank 降到 8目的就是限制模型在偏好对齐时不要太大幅度偏离 SFT 的策略。如果你显存特别紧张可以进一步用 QLoRA把 base 模型用 4bit NF4 量化后加载再插入 LoRA 训练。但要做好训练速度下降约 20% 的预期。Xihe 的显存是够的所以 QLoRA 我只做了验证实验没有正式使用。6. 蒸馏用大模型的“软答案”喂出一个能上生产的小模型6.1 为什么要蒸馏以及蒸馏谁的输出SFT 和 DPO 之后Xihe 对话质量已经能看但 0.12B 模型的知识上限就摆在那。如果要把能力再提一档最直接的方法是换更大的底座但推理成本急剧上升。于是我用蒸馏做一个“能力移植”让一个更大的教师模型我这边用的是开源 7B 级模型生成软化的输出分布让 Xihe 去拟合这个分布。蒸馏的本质是把教师模型在软概率里携带的“类间关系”教给学生。常规训练只给“正确答案”一个 one-hot 标签学生不知道“苹果”和“水果”在语义上更接近而教师模型的 logits 分布会把这种相似性体现出来学生学到的信息密度大得多。6.2 在线蒸馏还是离线蒸馏我建议在个人项目里用离线蒸馏。在线蒸馏需要每个 step 都跑一遍教师模型推理7B 模型对学生来说代价太大。离线流程是用一批高质量指令/对话让教师模型逐条生成回复保存回复文本或 logits。学生对同样的输入做前向和教师输出计算 KL 散度损失。同时混入少量真实标注的交叉熵损失防止学生只学“模仿”而没有标准答案约束。蒸馏的 loss 公式我用的经典版L alpha * CE(student, hard_label) (1-alpha) * T^2 * KL(student_logits/T, teacher_logits/T)。温度 T 设为 3alpha 设为 0.3。T 越大分布越平滑学生能学到更多暗知识但太大也会让学生输出变得过于保守像在“念标准答案”。6.3 蒸馏实操中的坑首先要把学生的温度缩放同步处理。KL 项里教师和学生都要除以 T计算完再乘回 T^2这个缩放系数如果写错loss 会明显波动。其次教师模型的输出并不总是安全合理的我用了一条规则蒸馏数据里教师生成的每条回复都过一遍长度、重复度、敏感词过滤不合格的直接丢弃。第三蒸馏后小模型会有回复同质化倾向——五条不同提问可能得到结构高度相似的回答这是 KL 散度把分布拉得过平的副作用可以在 DPO 阶段用偏好对拉回多样性。7. DPO 直接偏好优化不用奖励模型也能拒绝坏输出7.1 DPO 与 RLHF 的差异最后到 DPO。传统 RLHF 要训练一个奖励模型再通过 PPO 让策略模型在奖励模型的指导下更新流程长、超参多、很容易崩。DPO 直接利用偏好对数据从 Bradley-Terry 偏好模型中推出隐式奖励一步到位优化策略。DPO 的直觉可以用一个对比来说明给定同一个提示你有两个回答一个是用户更喜欢的 chosen一个是相对较差的 rejected。DPO 要做的事是——让策略模型在 chosen 上的概率相对参考模型更高在 rejected 上的概率相对参考模型更低同时用 KL 约束防止模型跑到参考分布太远的地方。这样就不需要显式的奖励模型了。7.2 偏好数据怎么造Xihe 的偏好对主要来自两个渠道一是用 SFT 后模型对同一批指令做高温采样生成多个候选回复请标注者排序得到 chosen/rejected二是用外部评分器对齐排序评分标准包括准确性、格式、冗余度。最后保留 1600 个偏好对。数量不多但对偏好对齐来说够用DPO 数据质量比数量更重要。构造偏好对时要特别注意三个问题chosen 和 rejected 不能只是措辞略有差异否则模型学不到实质偏好同一指令下要保证两个回复长度不完全失衡否则模型会投机取巧学“长回复更好”不能把安全边界内的正常回答和安全拒绝回答混成一对这会让模型在安全边界上表现错乱。7.3 DPO 训练细节与效果DPO 的标准 loss 形式是这样的loss -log(sigmoid(beta * (log p_theta(y_w|x) - log p_ref(y_w|x) - log p_theta(y_l|x) log p_ref(y_l|x))))其中 beta 控制 KL 约束强度我取 0.1。beta 越大模型越不敢偏离参考模型beta 越小模型越激进地拉大 chosen 和 rejected 的概率差。0.1 是一个比较稳的起点。DPO 训练时我用 SFT 后的模型既当策略模型初始值也当参考模型。注意参考模型的 logprob 要在训练前一次性算好缓存而不是每个 step 重新算否则成本高且不稳定。学习率设 1e-6只训 1.5 个 epoch。多了会直接看到 chosen 和 rejected 的 margin 疯狂上升但生成质量断崖式下跌。实测效果DPO 后最明显的变化是回复不再说车轱辘话格式稳定性提高碰壁问题会直接承认不知道而不是编造。比如在 SFT 阶段模型遇到超出知识范围的问题会硬编一个答案DPO 后更倾向于给出“我不确定”或“建议查阅文档”的回复。这种行为不是 SFT 能简单教出来的必须靠偏好数据对齐。8. 全程最值得记下的坑loss、遗忘、OOV 与重复生成8.1 预训练 loss 不降先怀疑数据而不是模型很多人遇到预训练 loss 降不下去第一反应是改模型结构频繁加层、加头、调学习率。我的建议是先做 100 步小规模测试采样训练数据看 tokenizer 切分是否合理。Xihe 第一次跑的时候我用一个没清洗干净的语料里面大量“阅读全文”的重复模板loss 在 6.0 附近卡了很久。后来把清洗管道加强同样步数下 loss 就恢复正常下降趋势。模型结构在百万级数据量上很少是瓶颈脏数据才是。8.2 CPT 时的灾难性遗忘怎么控制CPT 阶段最怕模型学会新领域、忘掉旧能力。我在训练日志里同时监控英文 loss 和中文 loss发现中文 loss 下降的同时英文 loss 在 4000 步后开始抬头。解决方法是把英文通用语料从 20% 提到 35%并降低学习率。这里有一个通用的教训继续预训练不是“只喂领域语料”而是在通用语料与领域语料之间找平衡。8.3 OOV 问题与 tokenizer 的边界BBPE 理论上不会出现严格意义的 OOV但我发现如果对生僻中文字符做 byte 级切分会切成一串无意义的字节碎片生成时偶尔出现“锟斤拷”风格碎字。原因是我的词表训练语料里生僻字覆盖率低。解决方案是训练词表时保证全字表覆盖尤其是 GB2312 和 GBK 常用字范围全部收录。虽然 32K 词表对中文来说偏小但通过 byte fallback 机制可以兜底只是生僻字会以更多 token 表示拖慢收敛。这个问题在 SFT 阶段不易察觉但在知识类问答中一踩一个准。8.4 生成循环“好的好的好的”怎么查我排查过一个最经典的重复生成问题模型在 50% 以上的测试样本里都会陷入“好的好的好的……”死循环。既然模型训练时 loss 已经降得比较好了我怀疑是采样参数的问题。把温度从 0.8 调到 0.6重复惩罚从 1.0 调到 1.2循环明显减少但语义变得干瘪最后发现根因是训练数据里有一段客服语料大量出现“好的好的”作为回复开头模型把这个高概率路径学得太牢了。清洗数据并重新补一轮 SFT 后问题消失。这个坑说明生成阶段的 post-processing 只能缓解问题根源大概率在训练数据。8.5 显存不够时的三板斧总共 15GB 左右的数据量加上 0.12B 模型单卡训练是可以的但 SFT 和 DPO 阶段如果序列长度设到 2048显存还是会告急。我的处理顺序是先降 batch size 并加大梯度累积步数接着打开 gradient checkpointing最后再用 LoRA 把训练参数量砍掉。不要一上来就降低序列长度小模型长上下文能力本身弱输入长度砍半会让效果雪上加霜。9. 结算Xihe 的最终形态与我的实操体会整套流程跑完后Xihe 变成了两个版本一个是 0.12B 的“完整版”保留全部能力适合本地研究和二次微调另一个是经过蒸馏的 0.05B 小号版专门给低资源环境推理用。完整版在单张 4090 上大约 280ms 生成 128 token吞吐量对我个人使用绰绰有余小号版显存占用只要 1.6GB速度还要再快一倍。能力上当然不能和主流的开源中文大模型比但作为一条从零打通全链路训练pipeline的验证我愿意给它打80分。个人最真实的体会是从零训练小语言模型最大的价值不在模型本身而在建立一套可迭代的训练手感。你会慢慢知道“数据比例调 5%”和“学习率调一倍”分别会带来什么反应知道什么阶段看 loss、什么阶段必须看生成样例。别人给你一个 SFT 流水线你不再是机械地跑通而是能判断哪一步少了、哪一步数据不对。最后再分享一个小技巧如果要复现这套流程建议先用一个很小的语料子集比如 200MB 文本打通全流程几个小时内把预训练、CPT、SFT、LoRA、蒸馏、DPO 的代码全部跑通再去冲完整数据。很多新手一上来就上完整 15GB 语料结果卡在某个阶段好几天连问题出在代码还是数据上都分不清。小规模试跑把风险前置后面就是加数据和调参的事。Xihe 项目最大的收获就是这个“先小后大”的节奏。