☰
从零训练小语言模型Xihe:预训练、CPT、SFT、PEFT、蒸馏与DPO全流程实战
2026/9/26 20:43:51 网站建设 项目流程

小语言模型这两年重新火了起来,原因很实在:大模型能力虽强,但推理成本、部署门槛、数据隐私这几道坎,在很多场景里绕不过去。我所在的团队去年做了一个叫 Xihe 的小模型项目,从零走完了预训练、CPT、SFT、PEFT、蒸馏到 DPO 的完整链路,最终产出一个能在单张消费级显卡上跑起来、中文能力可用的模型。这篇文章不打算讲空泛的概念,而是把每个阶段我们实际怎么做的、为什么这么选、踩了哪些坑,原原本本拆开讲一遍。如果你手头有几十 GB 的中文语料、一两张卡,想自己训一个能落地的小模型,这篇内容应该能帮你少走不少弯路。

1. 先想清楚:小模型到底解决什么问题,Xihe 的定位是什么

1.1 为什么不是直接微调一个大模型

很多人第一反应是:既然有现成的开源大模型,为什么还要从零训一个小模型?这个问题我们在立项时争论了很久。直接微调大模型看起来省事,但实际落地时会遇到几个硬约束。

第一是推理成本。一个 7B 模型即使量化到 4bit,推理时显存占用也在 4-5GB 起步,如果要做并发,显存需求成倍上涨。而我们要落地的场景是边缘设备和企业内网的一台普通服务器,预算里根本没有 A100 这种级别的卡。第二是领域适配的深度。通用大模型在垂直领域的表现,往往不如一个在领域语料上从头训过的小模型来得扎实,因为大模型的预训练语料里,垂直领域的内容占比极低,微调只能"唤醒"它已有的能力,唤不醒的就永远学不会。第三是可控性。从零训练意味着词表、数据配比、训练目标全在自己手里,出了问题能定位到具体环节,而不是面对一个黑盒。

所以 Xihe 的定位很明确:参数量控制在 1B 以内,中文为主,面向垂直领域,能在单卡上完成全流程训练和推理。这个定位决定了后面所有的技术选型。

1.2 Xihe 的整体技术路线图

整个项目分六个阶段,每个阶段都有明确的输入输出和验收标准:

阶段目标数据规模产出
预训练建立基础语言能力30-50GB 中文语料Base 模型
CPT注入领域知识2-5GB 领域语料领域 Base 模型
SFT学会指令跟随5-10万条指令数据Chat 模型
PEFT低成本适配子任务千条级数据LoRA 适配器
蒸馏压缩与能力迁移教师模型输出更小/更强的学生模型
DPO对齐人类偏好万级偏好对对齐后模型

这张表看着清晰,但实际执行时阶段之间是有交叉和回退的。比如 CPT 做完发现 SFT 效果不好,可能要回头补预训练数据;DPO 做完发现模型变"油"了,可能要回退到 SFT 版本重新调偏好数据。不要把它当成一条单向流水线,而是一个可以反复迭代的循环。

1.3 硬件与框架的选型逻辑

我们用的是 8 张 A800 做预训练,单张 4090 做后续所有阶段。框架上,预训练用 Megatron-LM 的简化版做张量并行,SFT 之后全部切到 HuggingFace Transformers + TRL + PEFT 这套组合。

为什么这么分?预训练阶段计算量大、通信密集,需要专门的并行框架才能把 8 张卡吃满;而 SFT 之后的阶段数据量小、迭代快,用 HuggingFace 生态开发效率高得多,改一行代码就能跑。不要为了"统一技术栈"而在小阶段硬上大框架,那是给自己找麻烦。

提示:如果你的卡少于 4 张,预训练阶段建议直接放弃从零训,改用现成小模型做 CPT 起步,性价比高得多。

2. 预训练:从零把语言能力"喂"出来

2.1 数据清洗比模型结构重要十倍

预训练阶段我们花了整整三周在数据上,模型代码只写了两天。这不是夸张,是真实的比例。原始语料来自几个公开中文数据集加上我们自己爬的领域文本,总共约 80GB,清洗后剩 42GB。

清洗流程分四步:

  1. 去重:用 MinHash + LSH 做文档级去重,阈值设 0.8。这一步砍掉了约 18% 的重复内容,主要是爬虫抓到的转载文章。
  2. 质量过滤:用规则 + 小分类器双管齐下。规则包括长度过滤(少于 50 字的丢弃)、符号比例过滤(特殊符号占比超 30% 的丢弃)、重复行过滤。分类器用一个在人工标注的 5 万条数据上训的 BERT 小模型,打分低于阈值的丢弃。
  3. 敏感与低质内容剔除:这部分用关键词表加规则匹配,具体规则不便展开,原则是宁可错杀不可放过。
  4. 格式统一:全角半角统一、去除多余空白、统一换行符。

清洗完的数据,我们抽样了 500 条人工检查,合格率在 92% 左右。这个合格率已经算不错了,但剩下 8% 的噪声在预训练里会被放大,所以能再洗一轮就再洗一轮。

2.2 分词器训练:词表大小是个权衡题

Xihe 用的是 BPE 分词器,词表大小最终定在 32000。这个数字不是拍脑袋来的,我们试了 16000、32000、50000 三档。

词表太小,一个中文字会被拆成多个 token,序列变长,训练和推理都变慢;词表太大,embedding 层参数膨胀,小模型本来参数就少,浪费在 embedding 上不划算。实测下来,32000 在中文场景下,平均每个汉字对应 1.3 个 token,压缩率和参数量比较平衡。

训练分词器时有个细节:一定要用清洗后的最终语料来训,不要用原始语料。我们第一版用原始语料训的分词器,结果词表里混进了一堆乱码组合,后来重训才解决。

2.3 模型结构:小模型要"瘦"得合理

Xihe 的 Base 版本是 24 层、hidden size 1024、16 个注意力头,总参数约 0.35B。这个配置参考了同类小模型的经验,但做了两处调整。

一是用了 RoPE 位置编码而不是可学习的位置 embedding。RoPE 在长文本外推上表现更好,而且不占额外参数,对小模型很友好。二是FFN 的中间维度用了 2.67 倍而不是常见的 4 倍。小模型里 FFN 占了大量参数,压缩到 2.67 倍后,参数量降了约 15%,实测效果损失很小。

这里有个反直觉的点:小模型不是越大越好,而是要"参数效率"高。同样 0.35B 参数,怎么分配到注意力、FFN、embedding 上,比单纯堆参数重要得多。

2.4 训练配置与稳定性处理

预训练用了 4096 的序列长度,batch size 全局 512,学习率峰值 3e-4,warmup 2000 步,cosine 衰减。总共训了约 300B token。

训练过程中遇到的最大问题是loss spike。在第 40B token 左右,loss 突然从 2.3 跳到 4.1,然后慢慢降回来。这种情况在小模型预训练里很常见,处理方式是:保留 spike 前的 checkpoint,降低学习率,跳过出问题的那批数据重新训。我们后来加了梯度裁剪(阈值 1.0)和动态 loss scaling,spike 频率明显下降。

另一个经验是不要频繁保存 checkpoint。预训练阶段一个 checkpoint 好几个 GB,存太密磁盘扛不住。我们的策略是每 5000 步存一次,同时只保留最近 3 个加一个最佳。

3. CPT:把领域知识"焊"进模型里

3.1 CPT 和预训练的本质区别

CPT(Continue Pre-training,继续预训练)经常被和微调混为一谈,但它们的逻辑完全不同。预训练是建立通用语言能力,CPT 是在已有能力基础上,用领域语料继续训练,让模型"熟悉"领域内的表达方式和知识。

关键区别在于数据配比。CPT 不能只用领域数据,否则模型会"灾难性遗忘",通用能力退化。我们的配比是领域数据 70% + 通用数据 30%。这个比例试过 90/10 和 50/50,90/10 时模型在通用任务上明显变笨,50/50 时领域知识注入不够,70/30 是比较好的平衡点。

3.2 领域语料的处理要点

CPT 的领域语料只有 3GB 左右,量不大,所以每一条都要精挑细选。我们的处理原则是:

  • 优先选"知识密度高"的文本:技术文档、专业书籍、行业报告优先,论坛灌水、新闻通稿靠后。
  • 保留领域特有表达:领域内的术语、缩写、固定搭配不要做归一化,这些正是模型要学的。
  • 控制单文档长度:太长的文档切分时注意不要切断语义单元,我们按段落边界切,单条控制在 512-2048 token。

3.3 CPT 的训练策略与学习率设置

CPT 的学习率要比预训练低一个数量级,我们用 3e-5,warmup 500 步。为什么?因为模型已经学到了通用语言能力,学习率太高会把原有能力"冲掉"。

训练步数上,我们跑了约 2 个 epoch。这里有个判断标准:看领域验证集的 loss 是否还在下降,同时看通用验证集的 loss 是否开始上升。当领域 loss 下降变缓、通用 loss 开始抬头时,就该停了。我们实测在 1.5 个 epoch 左右就到了这个点,多跑的那半个 epoch 收益很小。

注意:CPT 阶段一定要同时监控领域和通用两个验证集,只看领域 loss 会让你在模型已经退化时还浑然不觉。

3.4 怎么验证 CPT 真的"注入"了知识

光看 loss 不够,我们设计了一套探针测试:从领域语料里抽 200 个知识点,做成填空和问答形式,在 CPT 前后分别测。比如"XX 协议默认端口是____"这类。CPT 后准确率从 31% 提升到 68%,说明知识确实注入了。

但也要警惕**"背题"现象**:如果探针题目和训练语料高度重合,模型可能只是记住了原文。所以探针题要改写表述,避免和训练数据字面重复。

4. SFT:让模型学会"听话"

4.1 指令数据的构造:质量远比数量重要

SFT 阶段我们用了约 8 万条指令数据,其中 3 万条是人工标注,5 万条是用强模型生成的。这里必须强调:SFT 数据的质量比数量重要得多,1 万条高质量数据的效果往往好过 10 万条噪声数据。

数据构造上,我们遵循几个原则:

  • 多样性优先:任务类型覆盖问答、摘要、改写、分类、抽取、代码等,每类都要有。
  • 难度分层:简单、中等、困难各占一定比例,避免模型只学会处理简单指令。
  • 格式统一:所有数据统一成"指令 + 输入 + 输出"的三段式,用特殊 token 分隔。

人工标注的 3 万条,我们定了详细的标注规范,标注员之间做了 Kappa 一致性检验,低于 0.7 的批次打回重标。生成的那 5 万条,用规则 + 小模型双重过滤,去掉了约 20% 的低质样本。

4.2 训练配置:小模型 SFT 的坑

SFT 用全参数微调,学习率 2e-5,batch size 128,训 3 个 epoch。这里踩过一个坑:一开始用了 1e-4 的学习率,结果模型直接"崩"了,输出全是重复的。小模型对学习率比大模型敏感,后来降到 2e-5 才稳定。

另一个坑是过拟合。SFT 数据量不大,训到第 3 个 epoch 时,训练 loss 还在降,但验证集效果开始变差。我们的做法是每个 epoch 存一个 checkpoint,最后用验证集选最佳,而不是用最后一个。

4.3 怎么判断 SFT 模型"会听话"了

我们用了三个维度的评估:

  1. 指令遵循率:随机抽 500 条指令,人工判断模型是否按指令要求完成了任务,SFT 后从 42% 提升到 89%。
  2. 格式正确率:模型输出是否符合要求的格式(如 JSON、列表),从 55% 提升到 94%。
  3. 通用能力保持:用几个标准 benchmark 测,确认没有明显退化。

这里要提醒:指令遵循率和格式正确率是两个独立指标。有的模型内容答对了但格式乱了,有的格式对了但内容跑偏,要分开看。

5. PEFT:用最小的代价适配子任务

5.1 为什么 SFT 之后还要 PEFT

SFT 出来的是一个通用 Chat 模型,但实际落地时,每个客户、每个子任务的需求都不一样。如果每个子任务都做一次全参数 SFT,成本高、周期长,而且多个版本管理起来很乱。

PEFT(Parameter-Efficient Fine-Tuning,参数高效微调)就是解决这个问题的:只训练一小部分参数,就能让模型适配新任务。我们主要用 LoRA,偶尔用 Prefix Tuning。

5.2 LoRA 的秩和 alpha 怎么选

LoRA 的核心参数是秩 r 和缩放系数 alpha。我们的经验是:

任务类型ralpha说明
简单分类/抽取816参数少,够用
中等改写/摘要1632平衡点
复杂生成/推理3264需要更强表达力

alpha 一般设为 r 的 2 倍,这是社区经验值,实测也符合。不要盲目调大 r,r 太大就失去了 PEFT 的意义,还不如直接全参数微调。

5.3 PEFT 实操中的几个细节

第一,LoRA 加在哪些层上。我们试过只加在 attention 的 q、v 上,也试过加在所有线性层上。结论是:加在 q、v、k、o 和 FFN 的所有线性层上,效果最好,参数量增加也不多。

第二,学习率要比全参数微调高。因为只训练少量参数,学习率设 1e-4 到 3e-4 比较合适,我们常用 2e-4。

第三,多个 LoRA 适配器的管理。我们给每个子任务训一个 LoRA,推理时按需加载。这里要注意 LoRA 的合并与切换,用 PEFT 库的merge_and_unload可以合并,但合并后就无法再切换了,所以生产环境建议保留独立适配器,动态加载。

5.4 PEFT 的效果边界

PEFT 不是万能的。我们的实测结论是:当目标任务和基座模型的能力差距不大时,PEFT 效果接近全参数微调;当差距很大时,PEFT 明显不如全参数微调。比如让模型学一个全新的输出格式,PEFT 几十条数据就能搞定;但让模型掌握一个全新的知识领域,PEFT 就力不从心了,这时候得回到 CPT。

6. 蒸馏:把大模型的能力"搬"到小模型

6.1 蒸馏的两种路线:logits 蒸馏 vs 数据蒸馏

蒸馏分两大类。logits 蒸馏是让学生模型去拟合教师模型的输出分布,需要教师模型的 logits,信息量大但实现复杂;数据蒸馏是用教师模型生成数据,学生模型在这些数据上训练,实现简单但信息有损。

我们两条路都走了。logits 蒸馏用在预训练和 CPT 阶段,让 Xihe 去拟合一个更大模型的输出分布;数据蒸馏用在 SFT 阶段,用强模型生成指令数据来训 Xihe。

6.2 logits 蒸馏的温度与损失权重

logits 蒸馏的核心是温度 T 和损失权重 alpha。温度的作用是"软化"教师模型的输出分布,让暗知识(dark knowledge)更容易被学生学到。

我们的配置是 T=2.0,alpha=0.7(蒸馏损失占 0.7,硬标签损失占 0.3)。温度试过 1.0、2.0、4.0,2.0 效果最好。温度太高,分布太软,学生学不到明确的信号;温度太低,接近硬标签,蒸馏的意义就没了。

损失函数是:

loss = alpha * KL(student/T || teacher/T) * T^2 + (1-alpha) * CE(student, label)

注意那个 T² 是必须的,因为软化后的梯度会缩小 T² 倍,乘回来才能保持梯度量级一致。

6.3 数据蒸馏的实操:怎么用好教师模型

数据蒸馏看着简单,其实坑不少。第一,教师模型的输出要过滤。强模型也会犯错,我们用一个小的奖励模型或者规则来过滤教师输出,去掉明显错误的样本。第二,要控制教师数据的比例。全用教师数据,学生模型会学到教师的"风格"甚至"偏见",我们一般教师数据占 60-70%,剩下用真实数据。

第三,教师模型的选择。不是越大越好,要选和目标任务匹配的。我们试过用不同规模的教师,最后选了一个在领域任务上表现最好的中等规模模型,而不是最大的那个。

6.4 蒸馏后的效果验证

蒸馏后我们做了对比测试:学生模型(0.35B)在领域任务上达到了教师模型(7B)约 85% 的效果,而推理速度快了 15 倍,显存占用降到 1/10。这个性价比是蒸馏最大的价值。

但也要清醒:蒸馏能迁移的是"能力",不是"知识"。教师模型见过的海量知识,学生模型不可能全部学到,能学到的是处理任务的"方法"。所以蒸馏适合能力迁移,不适合知识注入,知识注入还得靠 CPT。

7. DPO:让模型输出更符合人类偏好

7.1 DPO 相比 RLHF 的优势

DPO(Direct Preference Optimization,直接偏好优化)这两年很火,核心优势是去掉了 RLHF 里的奖励模型和强化学习环节,直接用偏好数据优化策略。实现简单、训练稳定,对小团队特别友好。

RLHF 要训奖励模型、要跑 PPO,工程复杂度高,还容易训崩。DPO 只需要偏好对数据(一个 prompt 配一个好回答和一个坏回答),直接优化,我们实测下来稳定性好很多。

7.2 偏好数据的构造

DPO 的效果高度依赖偏好数据质量。我们的偏好数据来源有三块:

  1. 人工标注:让标注员对同一 prompt 的两个回答做选择,约 1 万对。
  2. 模型对比:用不同版本的模型对同一 prompt 生成回答,人工或规则判断优劣。
  3. 拒绝采样:从模型生成的多个回答里,按规则选出好的和差的组成偏好对。

构造偏好数据有个关键点:好回答和坏回答的差距要适中。差距太大,模型学不到细粒度偏好;差距太小,模型分不清。我们一般让差距在"明显但不过分"的程度。

7.3 DPO 的训练参数与常见问题

DPO 的学习率比 SFT 还要低,我们用 5e-7,beta 参数设 0.1。beta 控制模型偏离参考模型的程度,太小会学不动,太大会训崩。

常见问题有两个。一是模型变"油":DPO 训过头后,模型会学会"讨好"偏好数据,输出变得冗长、谄媚。解决办法是控制训练步数,同时监控输出长度和多样性。二是偏好数据有偏:如果偏好数据里长回答总是被选为好,模型就会倾向生成长回答。所以偏好数据要刻意平衡长度、风格等因素。

7.4 DPO 之后的评估

DPO 之后不能只看 loss,要看实际输出。我们用了三个评估:

  • 偏好胜率:用一批新 prompt,让 DPO 前后的模型各生成回答,人工或强模型判断哪个更好,DPO 后胜率约 62%。
  • 输出质量人工评估:从有用性、准确性、流畅性、安全性四个维度打分。
  • 回归测试:确认 DPO 没有破坏 SFT 阶段学到的指令遵循能力。

这里要提醒:DPO 的胜率不是越高越好。如果胜率超过 75%,往往意味着模型过拟合了偏好数据,泛化能力反而下降。62% 左右是比较健康的区间。

8. 全流程串起来:几个容易被忽略的工程细节

8.1 版本管理与实验追踪

六个阶段、几十次实验,如果没有好的版本管理,很快就会乱套。我们用 Git 管代码,用 DVC 管数据和模型,用 WandB 管实验记录。每个实验都记录:数据版本、代码 commit、超参数、评估结果。

这里有个血泪教训:早期我们没做数据版本管理,结果发现某个 checkpoint 效果特别好,却找不到当时用的是哪版数据,只能重跑,浪费了一周。从那以后,数据、代码、模型三者严格绑定版本。

8.2 评估体系的搭建

每个阶段都要有独立的评估,不能只看 loss。我们的评估体系分三层:

  • 自动指标:perplexity、BLEU、ROUGE、准确率等,快速筛选。
  • 模型评估:用强模型做裁判,评估生成质量。
  • 人工评估:关键节点做人工打分,保证方向不偏。

三层评估的成本递增,所以先用自动指标筛,再用模型评估,最后人工把关。

8.3 显存与速度的优化技巧

小模型训练也有显存压力,尤其是长序列。我们用了几个技巧:

  • 梯度检查点:用时间换空间,显存降约 40%,速度慢约 20%。
  • Flash Attention:加速注意力计算,长序列下提速明显。
  • 混合精度:bf16 训练,显存和速度都有改善。
  • 梯度累积:小 batch 模拟大 batch,稳定训练。

这些技巧不是都要上,要根据实际瓶颈选。显存不够就上梯度检查点,速度慢就上 Flash Attention。

8.4 从训练到部署的最后一公里

训完模型只是开始,部署才是真正的考验。我们把模型导出成多种格式:PyTorch 原版用于研究,ONNX 用于跨平台,量化版本(int8/int4)用于边缘设备。

量化这里有个坑:不是所有层都适合量化。我们发现 embedding 层和最后的输出层量化后掉点明显,所以这两层保持 fp16,中间层量化。这样混合量化后,模型体积降了 60%,效果损失控制在 2% 以内。

推理框架上,我们用 vLLM 做服务端,用 llama.cpp 做端侧。vLLM 的 PagedAttention 对并发很友好,llama.cpp 在 CPU 上也能跑,覆盖了不同场景。

9. 一些掏心窝子的经验

整个项目做下来,最大的体会是:小模型训练是个"木桶"工程,任何一个环节短板都会拖累整体。数据脏一点、学习率高一档、评估漏一项,最后的效果就差一截。所以每个环节都要认真对待,不能有"差不多就行"的心态。

第二个体会是不要迷信单一技术。预训练、CPT、SFT、PEFT、蒸馏、DPO,每个技术都有它的适用边界。CPT 擅长注入知识,SFT 擅长教格式,PEFT 擅长低成本适配,蒸馏擅长能力迁移,DPO 擅长对齐偏好。搞清楚每个技术"能做什么、不能做什么",比盲目堆技术重要得多。

第三个体会是评估比训练难。训一个模型可能几天,但设计一套靠谱的评估体系可能要几周。而且评估体系要随着项目推进不断迭代,早期用的指标到后期可能就不适用了。我的建议是,项目一开始就把评估当一等公民对待,别等到最后才想起来。

最后说个具体的:小模型的"涌现能力"很有限。大模型在某个规模后会突然出现一些能力,小模型基本不会。所以不要指望小模型能处理需要复杂推理的任务,它的定位是"在特定领域把特定任务做好",而不是"什么都能干"。想清楚这一点,很多技术选型就顺了。

如果你也在做类似的项目,欢迎交流。这个领域变化很快,今天的最佳实践明天可能就被推翻,保持学习和迭代的心态最重要。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询