这两年大家都在聊大模型,但真正动手从零训练一个小语言模型的人还是少数。我自己花了将近一个月,用开源工具把一个纯实验性质的小模型 Xihe 完整跑了一遍:预训练、CPT、SFT、PEFT、蒸馏、DPO,每一环都没有跳过。今天这篇文章就把整套决策思路、具体参数和踩过的坑写出来,适合那些不想只调包、想搞清楚一个 LLM 从无到有全生命周期的工程师和学生。我会尽量说人话,也尽量把“为什么这么做”讲透,而不是甩一堆可以照抄但不一定理解的操作命令。
先说结论:如果你只是想做一个通用聊天助手,直接下载现成的开源权重最省事。但如果你需要完全掌握数据配比、词表、训练细节,或者想在小团队、小预算条件下把整条管线跑通,那从零训练一个小模型是性价比极高的选择。Xihe 这个名字没什么特殊含义,就是我自己给实验模型起的代号。下面按完整流水线顺序展开。
1. 先想清楚为什么从零训练:Xihe 的定位与预算
1.1 从零训练和下载现成模型的本质区别
很多人看到“从零训练”第一反应是不理解:开源社区已经有一堆预训练语言模型,随便下一个不就行了吗?这句话对通用场景确实成立,但“从零训练”和“下载现成权重再微调”解决的问题本质上不一样。
下载现成模型相当于精装修二手房到手后改软装,效率确实高,但你没法换户型。体现在技术上就是:词表不是你自己定的,里面中文字符切得碎不碎你说了不算;预训练数据配比不是你决定的,模型当前积累的知识边界也不是你设计的;更关键的是,如果你想针对某个垂直领域(比如法律条文、内部文档、古籍、特定产品话术)做深度优化,现成权重的数据分布和你真正需要的数据分布之间有一道很难跨过去的坎。
从零训练则相当于自己买地盖楼。地基用什么材料、每层楼多高、窗户开多大,全都可以按需求来。代价是成本高、周期长、工程细节多。Xihe 这个项目之所以选择从零做,是因为我想跑通一条完全可控的玩具级生产线:从清洗语料开始,体验一个 Base 模型怎么从随机权重变成能对话、能守规矩、能对齐偏好的完整过程。这个经历对理解微调、RLHF 替代方案、模型收敛问题都特别有帮助。
1.2 目标模型规格与实际预算
Xihe 的规格我最终定成了两档:一档是 0.5B 参数,用于快速验证流程;另一档是 1.3B 参数,作为最终实验基线。结构上采用目前主流的 decoder-only Transformer,具体配置类似 LLaMA:RMSNorm、SwiGLU 激活、RoPE 位置编码,词表是单独训练的 32K 中文 BPE。
为什么选 1.3B 而不是直接上 7B?原因很现实:1.3B 在消费级多卡上训练可控,单卡推理也毫无压力,而且很多实验现象(比如学习率敏感度、灾难性遗忘、偏好对齐漂移)在 1.3B 已经体现得很明显。你用 1.3B 踩过的坑,放到 7B 以上一样会遇到,只不过换了个更大的台阶。
预算方面,只按预训练阶段算一笔账。假设用 1.3B 参数、5B token 数据量,训练总计算量大约是 FLOPs 6ND,也就是 6 × 1.3e9 × 5e9 ≈ 3.9e19 FLOPs。用 8 张 H100(单卡稠密算力约 989 TFLOPS),按 30%-45% 的实际利用率估算,纯计算时间大概在 4-6 小时。如果换成 8 张 4090,利用率低一些,大概要 2-3 天。这只是预训练,后面还有 CPT、SFT、蒸馏、DPO,所以完整流程建议按一周左右的机器时间来规划。如果预算紧张,把模型降到 0.5B,数据量也降到 2B token,预训练时间可以直接缩短一个数量级,整条流程仍然能跑完。
1.3 小语言模型的独特价值和适用边界
小模型经常被拿来和大模型对比效果,但这其实混淆了目标。小语言模型的价值从来不是超越大模型,而是在特定约束下做到“够用且可控”。比如单位内部的知识助手、教学用的示例模型、需要低延迟离线部署的边缘场景,1B 级别比 7B/13B 有优势得多。
另外,小模型的失败模式也是学习大模型技术最好的教材。参数少意味着容量有限,它会把训练数据里的问题赤裸裸地暴露出来:数据里有重复,它就学会复读;数据里废话多,它就口若悬河但空洞;偏好数据质量差,它立刻变成“谄媚怪”。这些现象在 7B 上也会出现,但小模型出现得更快、更明显,定位起来更容易。所以 Xihe 这个项目的真正定位不是一个“能打的模型”,而是一条“能跑通、能调参、能理解问题”的完整流水线。
2. 预训练与 CPT:先把 Base 模型造出来
2.1 数据准备:小模型比大模型更依赖高质量语料
预训练的第一步不是写代码,而是洗数据。一个大模型可能靠着超大数据量硬扛数据噪声,小模型没有这个余量,脏数据会直接体现在困惑度和生成质量上。
我 v1 版本直接用了一个开源中文语料包,大概 200GB 原始文本。清洗流程我分成了四步:第一步去 HTML 标签、控制字符、零宽字符,把所有不可见字符清掉;第二步做精确去重和近似去重,近似去重用 MinHash 加 SimHash,阈值设在 0.85 左右,把重复段落占比压到 1% 以下;第三步做语言过滤和质量过滤,保留中文、英文和代码,用困惑度过滤掉乱码和拼凑文本;第四步是检查标点符号和段落结构,不能为了一味“干净”把所有标点都删掉,标点是模型学语法的重要线索。
数据配比我最终选了中文 70%、英文 20%、代码 10%。这个比例没有绝对最优,可以根据后续应用场景调整。但有一点一定要记住:小模型学不完海量语料,所以数据质量远比数量重要。宁可只留 5B token 的干净语料,也不要硬上 50B token 的低质量文本。
2.2 Tokenizer 训练:词表是模型理解中文的底层视角
很多教程会直接跳过 tokenizer 训练,直接拿现成词表用。但既然是从零训练,Tokenizer 这一关绕不过去。我用 SentencePiece 或 Hugging Face Tokenizers 库训练了一个 BPE 词表,语料只用了清洗后数据的抽样部分,约 1 亿 token。词表大小定为 32K,不要盲目追求大词表,因为 embedding 参数会跟着膨胀,1.3B 模型在 32K 词表下的 embedding 矩阵只占很小一部分,但如果你把词表提到 100K,存储和显存开销都会明显上涨。
最关键的一步是在训完词表之后做“中文可读性检查”。我拿“人工智能”“自然语言处理”“甲骨文”这类词去分词,观察它们被切成了几个 token。如果常见的领域专有名词被切得过碎,说明 BPE 训练语料的覆盖不够或者参数不合适。Tokenizer 的好坏直接影响后续所有阶段的性能,这个环节值得多花几小时反复验证。
2.3 预训练超参与训练细节
预训练阶段我用的是 1.3B 配置,序列长度设为 1024,全局 batch size 控制在 0.5M token 左右。8 张卡的情况下,单卡 batch 是 32 条序列,全局就是 256 条序列,但 1.3B 模型在 4090 上放不下这个 batch,所以实际用了梯度累积把物理 batch 拆小,累积步数按显存调整。
学习率我优先尝试峰值 1e-3,warmup 500 步,采用 cosine schedule 衰减到 1e-4。如果你发现训练不稳定,先降到 5e-4,不要纠结。小模型把学习率调大一点通常能加速收敛,但梯度 norm 一旦超过 10,就要警惕发散。混合精度我用的是 BF16,它比 FP16 稳很多,基本不用处理溢出问题。梯度裁剪开在 1.0。
预训练跑完后的 base 模型不需要能“对话”,它的任务是续写文本。判断预训练质量的指标是困惑度和 loss,而不是生成效果。我跑 5B token 后 loss 降到 2.2 附近,说实话这个量级下 base 不算训透,但作为后续阶段的基础已经够用了。如果你想更接近理论最优,按 Chinchilla 的估计,1.3B 模型可能需要 20B 以上的训练数据,但这个成本在小项目里不太现实。
2.4 CPT:让通用基座变得更懂目标领域
如果你的语料已经以中文为主,其实可以不做 CPT。但如果后续要接入法律、医疗或者某个垂直领域,CPT 就非常关键。CPT 全称是 Continual Pre-Training,在已有模型基础上用领域语料继续做自回归训练。
做 CPT 最大的坑是灾难性遗忘。本来 base 已经学会的通用知识,你用领域语料一冲击,可能迅速退化。我的处理方法是领域数据和通用数据按 1:2 的比例混合训练,学习率从预训练的 1e-3 级别大幅降到 5e-5 级别,训练步数控制在预训练总步数的 10%-20% 以内。观察指标不能只看领域困惑度,还得同时盯通用困惑度,如果通用 PPL 突然暴涨,就说明回放比例不够。
这里顺带提一句,很多人搜“RoBERTa 中文预训练模型”,以为那可以作为生成模型的基座。其实 RoBERTa 这类模型是针对分类、NER 等判别任务的,网络结构是 encoder-only,不是生成式 LLM 的架构。视觉那边常有人搜 YOLOv8 预训练权重下载,那本质上是迁移学习里的“复用预训练权重再微调”,思路和 NLP 里的 checkpoint 复用一个道理,但和语言模型的从零预训练是两码事。如果要做生成,老老实实把 decoder-only 基座训练好,不要指望拿判别模型的权重来续写。
3. SFT 与 PEFT:让模型学会指令跟随
3.1 指令数据怎么构建和筛选
有了 base 模型之后,模型还是一个“续写机器”,你问它问题它不会按对话格式回答,只会按概率续写。SFT 阶段就是教会它“用户说什么、助手答什么”的交互模式。
数据格式我用的很朴素的 JSONL 结构:包含 instruction、input、output,然后按自定义的对话模板拼成一条完整文本。模板长这样:用户消息用特殊 token 包裹,助手回复用另一个特殊 token 包裹,训练时只对助手部分算 loss,用户部分在 label 里全部置为 -100,否则模型会学着把用户问题也复述一遍,损失函数的方向就错了。
数据量方面,我实测对 1.3B 模型来说 1 万到 3 万条高质量指令是甜点区间。少于 5 千条模型学不到模式,多于 5 万条非常容易出现重复输出和过度背诵。SFT 数据的质量筛选比数量更重要。我做了三件事:第一按输出长度过滤,少于 20 个字的删掉;第二做全局重复筛选,确保没有大量雷同话术;第三随机抽 100 条人工看,重点检查格式是否统一、答案是否明显错误。如果你接到的数据来源是大模型生成的,抽检更要严格,因为大模型也会一本正经地胡说八道。
3.2 SFT 训练细节与收敛判断
SFT 阶段的超参和预训练完全不同。预训练学习率可以用 1e-3,SFT 全参微调我建议从 2e-5 到 5e-5 之间试起。epoch 不要贪多,1 到 2 轮就够了。loss 降低速度和预训练相比会快得多,因为模型只需要调整到适合当前任务分布的参数状态,而不是从零学语法。
一个经常被忽略的问题是“不要只看 token 平均 loss”。SFT 阶段经常出现训练 loss 已经非常低,但实际生成质量很差的怪现象。原因是模型可能把助手模板背下来了,回复却空洞套路化。我习惯在训练过程中每几百步保存一次 checkpoint,然后用固定测试集做生成测试,肉眼判断输出是否自然。自动化指标只能辅助,SFT 阶段的最终标准必须是“抽看生成的文本”。
如果你发现模型训练完之后只会回“好的,请问还有什么可以帮您”这类话,大概率是数据里这类套话太多。不要试图靠调高采样温度来掩盖,必须回到数据层面删掉同质化输出。
3.3 PEFT:LoRA 和 QLoRA 到底怎么选
PEFT 的全称是参数高效微调,核心思想是冻结大部分模型权重,只训练少量新增参数。现在最常用的就是 LoRA:写成矩阵形式就是 W′ = W + BA,B 和 A 是两个低秩小矩阵,通过低秩分解把更新量约束在一个很小的子空间里。
我用的 LoRA 典型配置是 r=16、lora_alpha=32、lora_dropout=0.05,target_modules 覆盖注意力层和 FFN 层,也就是 q_proj、k_proj、v_proj、o_proj、gate_proj、up_proj、down_proj。embedding 和 lm_head 一般不做 LoRA,因为这两个地方对生成的影响很敏感,训练不稳定。
用 PEFT 库的写法非常直接:
from peft import LoraConfig, get_peft_model lora_config = LoraConfig( r=16, lora_alpha=32, target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], lora_dropout=0.05, task_type="CAUSAL_LM", ) model = get_peft_model(base_model, lora_config) model.print_trainable_parameters()那到底什么时候用全参 SFT,什么时候用 PEFT?我的经验是如果模型只有 1.3B,而且你已经决定只做一个最终模型,那全参 SFT 并不会贵到哪里去,效果通常是最稳的。LoRA 的真正价值在于三件事:显存不足时救命、需要同时保留多套风格时做到可插拔、以及快速做 A/B 实验时不用反复复制完整权重。QLoRA 更加激进,把基座模型量化到 4bit 再加 LoRA,适合在单卡上微调 7B、13B 甚至更大模型。但 Xihe 本身只有 1.3B,QLoRA 就不太必要了。
LoRA 的实战经验还谈不上完美:它往往能达到全参微调八成到九成的效果,但在知识密度高、推理链路长的任务上差距会更明显。rank 不是越大越好,我试过 r=64,在小数据量上反而更容易过拟合。如果数据量不大,r=16 是一个稳重的起点。
4. 蒸馏:让小模型借大模型的判断力涨知识
4.1 蒸馏的核心逻辑:不是复制权重,而是学习判断
模型已经能对话了,但效果离大模型还有明显差距。蒸馏阶段要做的事情,是让大模型当“老师”,把它的知识和判断方式迁移到小模型上。蒸馏不是直接复制大模型的权重,而是让小模型学会“在哪些答案之间做选择”。
老师傅带学徒有两种带法:一种是把每个动作的发力方式拆解出来一点点讲——这对应 logits 蒸馏;另一种是直接让学徒做大量练习,老师在旁边批改——这对应行为蒸馏。对语言模型来说,前者需要师生模型共享完全相同的词表,因为 logits 是按词表维度对齐的;后者则没有这个限制,更通用。
对 Xihe 来说,我训练的小模型词表和绝大多数开源大模型都不一样,所以 logits 蒸馏做不了。但这不代表蒸馏无法进行,把大模型生成的答案当作新的 SFT 数据,本质上就是一种蒸馏。这条路线网上一般叫“指令蒸馏”,它实现简单、效果显著,是小模型获取高质量行为模式的主流方式。
4.2 logits 蒸馏的参数设计和实现要点
如果以后你的学生模型和老师模型词表一致,可以尝试离线 logits 蒸馏。做法是先把大模型在大量固定 prompt 上的输出 logits 提前算好并缓存,然后让学生模型在这些 logits 上学习。
温度是 logits 蒸馏最核心的超参。常规做法是将 logits 除以一个温度 T 再做 softmax,让概率分布变得更平滑,这样“次优答案”之间的差异也能被小模型学到。T 一般取 2 到 5。损失函数是学生分布的 KL 散度加上硬标签的交叉熵,两者按权重混合。因为 KL 散度对温度缩放后的 logits 梯度会有 T² 量级的变化,所以训练时要乘回 T²,否则实际学习率会被带偏。
伪代码大致长这样:
teacher_logits = teacher(input_ids) # 提前缓存 student_logits = student(input_ids) T = 3.0 loss_kl = kl_div( log_softmax(student_logits / T), softmax(teacher_logits / T), ) * T * T loss_ce = cross_entropy(student_logits, hard_labels) loss = loss_kl + alpha * loss_ce这个方案的坑在于对齐细节:prompt 的 padding 方式、序列长度截断策略、位置编码偏移,都会影响 logits 分布。如果 logits 对不齐,蒸馏效果会非常差,而且不好排查。我个人的建议是:只有在你确实需要极限压缩模型且词表一致的前提下,才选择 logits 蒸馏;否则无脑用行为蒸馏更省心。
4.3 行为蒸馏实操:用大模型生成 SFT 数据
我把大模型当成“数据生产机器”,先收集了一批 seed 问题,然后用大模型批量生成答案。seed 问题主要来自开源指令集、网页问答和我自己手写的覆盖性题目,最终筛出 2 万条左右作为蒸馏数据。
大模型生成答案时我调的温度在 0.7 到 0.9 之间,太低容易模板化,太高会胡编。生成完以后清洗流程和 SFT 数据一样:去重、过滤空回复、过滤明显格式错误,然后人工抽检 200 条。这里要特别强调:行为蒸馏最大的坑是大模型自己也会“翻车”,如果种子问题带有诱导性,或者生成内容包含错误知识,小模型会原封不动学会。我最后会在数据里混入 20% 到 30% 的人工高质量答案,用来对冲大模型的固定语气和潜在错误。
行为蒸馏的训练目标就是普通 SFT,区别只在于数据来源。跑完之后用同样的测试集对比上一步 SFT 全参模型,我发现 Xihe 在开放域问答上的通顺程度明显提升,幻觉比例也有所下降,因为“老师”提供的答案通常更有逻辑层次,小模型模仿这种结构后输出不再碎片化。
4.4 蒸馏后的评估方式
蒸馏后不能只看 loss,因为小模型可能只是“学会了老师的口吻”,并没有真正记住更多知识。我的评估分两层:一层是固定指令集上的自动指标,包括关键词命中率、答案长度、重复率;另一层是人工打分,把 SFT 版和蒸馏版的输出并排放一起盲评。盲评结果比任何指标都直观:小模型有没有把大模型的“骨架”学到,一眼就能看出来。
这里提醒一句:蒸馏之后模型的参数量和推理速度完全不变,提升的是数据效率和监督信号强度。所以不要期待蒸馏能让模型“变聪明”到超越理论容量上限,它只是让模型在同样规模下发挥得更充分。
5. DPO:轻量级对齐,不碰 RLHF 也能有偏好
5.1 DPO 和 RLHF 的本质区别
对齐阶段传统方案是 RLHF:先训练奖励模型,再用 PPO 去优化策略,中间涉及大量工程细节,包括奖励模型的校准、PPO 的 KL 惩罚、各项超参的平衡,对 1.3B 这种规模的实验来说性价比很低。DPO 出现后,这条路径被大幅简化了。
DPO 的核心思路是直接通过偏好数据构造优化目标,不再单独训练奖励模型,也不再需要强化学习。它假设存在一个隐含的奖励函数,通过“chosen 回答优于 rejected 回答”的约束去调整策略分布。训练目标是让模型提高 chosen 答案的概率、降低 rejected 答案的概率,同时用参考模型(ref model)当作锚点,防止模型偏离 SFT 后的分布太远。
DPO 的 loss 可以简写成这样:
L = -log σ(β * (log πθ(y_w) - log πref(y_w) - log πθ(y_l) + log πref(y_l)))
其中 y_w 是更优回答,y_l 是更差回答,β 控制偏离参考模型的强度。这个式子看起来很抽象,但直觉很好理解:如果模型对 chosen 和 rejected 的得分差距越大,loss 越小;β 越小,模型越被允许在偏好数据上大步修改。
对 Xihe 这种小项目来说,DPO 的优势非常明显:训练稳定,不需要维护多个模型和采样环节,数据只需要 prompt、chosen、rejected 三要素,几千到一万条偏好对就能看到效果。这比 RLHF 动辄要搭一堆组件友好太多了。
5.2 偏好数据从哪来
偏好数据的构建是最容易偷懒、也是最影响 DPO 效果的地方。理想情况下,你需要对同一个 prompt 有两段答案,一段明显更好、一段明显更差。数据来源有三类:第一类是人工标注,质量最高但成本也最高;第二类是模型对比,用多个大模型生成多个回答,再人工或规则评选优劣;第三类是让一个大模型同时扮演生成者和打分者,先产出多个答案,再用评委指令打分排序。
我实验时用的是混合方案:找了一批种子 prompt,让两个不同参数规模的开源模型分别生成回答,再用一个更强的模型当裁判投票选优劣。裁判也会误判,所以 final 前抽了 500 条人工复核,把明显不合理的样本剔掉。最终留下约 8000 条有效偏好对。数量不需要多,但 chosen 和 rejected 之间的差异必须要明显。如果两者只是措辞小差别,DPO 基本学不到东西。
数据格式长这样:
{ "prompt": "给我解释一下什么是梯度消失。", "chosen": "梯度消失是指反向传播时梯度逐层变小,导致前面的层几乎无法更新……", "rejected": "梯度消失就是梯度变小,可能产生一些影响。" }5.3 DPO 训练参数与典型坑
DPO 训练我直接用了 TRL 库,配置很简单:
from trl import DPOTrainer, DPOConfig dpo_config = DPOConfig( beta=0.1, lr=5e-6, max_length=1024, max_prompt_length=512, ) trainer = DPOTrainer( model=model, ref_model=ref_model, train_dataset=dataset, args=dpo_config, ) trainer.train()这里的 model 用 SFT 之后的学生模型初始化,ref_model 必须是在相同 SFT 权重下冻结的副本。如果 ref_model 用错,loss 的绝对数值会异常,而且训练方向可能完全错误。β 常用 0.1,可以先按这个值跑一个 epoch,如果训练集 loss 在稳定下降,说明数据分布和参考模型距离合理;如果 loss 掉得极快但是生成质量变差,说明 β 偏大,改成 0.05 再试。学习率比 SFT 低很多,1e-5 已经算上限。
DPO 阶段最大的坑是 reward hacking。模型找到了提高 loss 的捷径,表现为输出变得非常“谄媚”,过度使用肯定语气和高频词汇,但内容空洞甚至出错。应对方法很简单:训练轮数限制在一个 epoch,最多两个;观测验证集上的人工评分,而不是盯着 DPO loss 不放。DPO 不会给模型补充新知识,它只改变模型对回答风格的偏好,所以不要指望它对事实性错误有修复作用。
5.4 蒸馏、PEFT 和 DPO 的组合路线
完整的流程不是只能走一条直线,可以自由组合。我在 Xihe 上实际跑了三条并行版本线:第一条是全参 SFT 后直接 DPO,用来观察对齐对全参模型的增益;第二条是在 LoRA 适配器上做 SFT 和 DPO,适合需要保留多套风格的部署场景;第三条是蒸馏数据 SFT 后再 DPO,这条线最终效果最好,因为蒸馏已经提升了模型对高质量回答的模仿能力,DPO 再进一步压缩偏好差距。
路径选择没有绝对标准,我建议按模块化看待:预训练和 CPT 提供语言能力,SFT 提供任务格式,蒸馏提供更强的监督,DPO 提供偏好约束。每一步都是可插拔的,你完全可以在某个阶段单独停止或替换数据。
6. 完整训练管线、显存省法与问题排查实录
6.1 各阶段配置速查与成本总览
把整个流水线放在一张表里看会更清楚:
| 阶段 | 模型规模 | 数据量 | 学习率 | 训练轮数/步数 | 参考耗时(8×4090) |
|---|---|---|---|---|---|
| 预训练 | 1.3B | 5B token | 1e-3 → 1e-4 | 约 6000 步 | 2-3 天 |
| CPT | 1.3B | 1B token 混合 | 5e-5 | 约 1000 步 | 0.5 天 |
| SFT 全参 | 1.3B | 1-3 万条 | 2e-5 起 | 1-2 epoch | 2-4 小时 |
| LoRA SFT | 1.3B | 同上 | 1e-4 起 | 1-2 epoch | 1-2 小时 |
| 蒸馏 SFT | 1.3B | 2 万条生成数据 | 2e-5 起 | 1 epoch | 3-5 小时 |
| DPO | 1.3B | 8000 条偏好对 | 5e-6 到 1e-5 | 1 epoch | 1-3 小时 |
这个表只是我实验中的参考值,不是标准答案。你的数据质量和机器配置不同,数字会浮动很大,但阶段之间的相对关系是稳定的:SFT 和 DPO 的消耗远小于预训练,所以把预算重点放在预训练和 CPT 上不会错。
6.2 训练 NaN 与不收敛排查顺序
这个可能是最多人卡住的问题。Loss 变成 NaN,不要急着调学习率,按顺序排查:先检查数据里有没有空样本、控制字符、非法 token id;其次检查 labels 是不是某个 batch 里全部为 -100,这会让 loss 计算变成除零,我实际就遇到过 SFT 阶段因为序列截断导致 loss 缺失的情况,加上“有效 token 数小于 1 就跳过”的判断即可解决;再检查混合精度,FP16 容易溢出,换 BF16;最后看学习率和梯度裁剪,梯度 norm 大于 10 时优先降学习率。
Loss 不下降的排查思路不一样。如果预训练 loss 平着不降,先确认数据是否真的切分并 shuffle 过,我见过有人把同一个连续文本切片当成独立样本送进去,数据顺序没打乱,模型只能背顺序。其次检查 warmup 和峰值学习率是否匹配,1e-3 对 1.3B 已经不小了,如果还在大规模波动,降一半重新跑。
6.3 生成重复、空话连篇的常见原因
生成重复这个问题在小模型和微调模型里特别常见。最直接的原因是训练数据里重复句子太多,比如“好的,请问还有什么可以帮您”在指令数据里出现几百次,模型自然会把这句话变成高频模式。还有一种原因是 epoch 过多,模型开始机械记忆训练集,而不是学习泛化规则。我的经验是宁可把 epoch 减少到 0.5 epoch,也不要跑到 3 个 epoch 以上。
如果模型只是回答短、不会展开,通常是数据里长答案不足。加一些长输出样本,或者用大模型生成的蒸馏数据补充,就能缓解。调解码参数对这种情况帮助有限,那是治标不治本。
6.4 显存不够的省钱方案
小模型项目最常见的问题是“单卡放不下”。实际上 1.3B 模型在 24G 显存的 4090 上是能全参 SFT 的,只要你把序列长度压到 512、开启梯度 checkpointing、使用 BF16 混合精度。如果显存还是不够,有几个优先级很高的操作:先把 batch size 降到最小,用梯度累积补全局 batch;再开 activation checkpointing,代价是训练速度慢 20%-30%;还可以做 4bit 量化走 QLoRA 路线。不到万不得已不要压缩序列长度,因为序列长度直接影响模型对长上下文的建模能力。
云 GPU 按小时租用也是一个好办法。预训练阶段租 8 卡 A100 或 H100 实例,SFT 和 DPO 阶段直接用 4090 实例,整体成本可能比买卡便宜很多。记得关实例,我见过不少人忘了关导致费用翻倍。
写在最后:完整跑一遍的价值
如果只追求从开源项目里拉一个模型下来跑推理,你永远不会知道数据清洗在整条流水线里的分量,也不会理解为什么 SFT 学习率要比预训练低两个数量级,更不会明白 DPO 里的参考模型为什么必须冻结。这些知识不是看文档能补上的,必须自己撞一次墙才能内化。
我做完 Xihe 之后最大的体会是:小模型从头训练,难的不是某一个单独环节,而是统筹一整条流水线。每个环节的决策都会传导到下一环,预训练时偷懒不做去重,SFT 时可能被重复数据带偏;SFT 时多跑了几个 epoch,DPO 阶段可能怎么调都救不回来。如果你打算自己动手,建议先用 0.5B 模型跑通全流程,再把规模放大到 1.3B。等你完整跑完一次之后,你再看“预训练模型下载”和“微调”这类话题,视角会完全不一样。