☰
消费级显卡从零训练小语言模型:预训练到DPO全流程实践
2026/9/28 15:38:06 网站建设 项目流程

大概从年初开始,我就在琢磨一件事:手里没有大规模集群,也调不起商业API,能不能就用一两张消费级显卡,把一个小语言模型从零到可用的全流程完整跑一遍。最后我用代号 Xihe 把这条路走通了。整个过程压缩成一句话就是:预训练练语感,CPT转方向,SFT教规矩,PEFT省成本,蒸馏借智慧,DPO调偏好。这篇文章会把每个环节里我真实用过的参数、数据和调试方法摊开来讲,也会把当时踩过的坑指出来。适合已经会 PyTorch 和 Transformers 基础操作、想完整走一遍 LLM 训练链路的人参考。

1. 从零训一个小模型:先算清“小”字怎么写

很多人一说“训练语言模型”,就想到动辄几千亿参数、上万张显卡的大工程。真到自己动手时,反而被这套阵仗吓住了。我的看法很直接:如果你只是想验证预训练、SFT、DPO 这条技术路线,甚至想做一个能跑在本地、回答垂直领域问题的小助手,完全不需要复刻 LLaMA 的训练规模。一个参数在 0.1B~1B 之间的小模型,配上几亿到几十亿 token 的数据,已经足够把整条链路跑通,还能看出不同训练阶段带来的实际差异。

1.1 为什么选 2 亿参数左右作为起点

Xihe 的初始版本我定在 2 亿参数上下。这个体量不是拍脑袋拍的,主要卡在硬件条件上:我的主力机器是两张 24GB 显存的消费级显卡,如果做全量预训练,模型本身加上激活值、优化器状态,显存压力会非常大。2 亿参数配合混合精度和梯度检查点,单卡能塞下,双卡还可以做一些简单的数据并行,正好卡在“个人折腾”和“实验室小集群”之间的位置。

另一个原因是训练速度。小模型迭代极快,一轮 1 亿 token 的预训练在双卡环境下大概只需要几小时到一天。这意味着我可以频繁改数据、改超参数,快速验证思路。大模型做不到这一点,哪怕一次小实验都要排队等几天。做训练链路验证,速度就是最大的杠杆。

还有一点容易被忽略:小模型的推理成本低,后期做蒸馏、DPO 评估时,可以不停地在本地跑生成测试,不用心疼算力。项目命名为 Xihe,本来就是我给自己这套流程起的代号,模型不大,但五脏俱全。

1.2 数据、词表与 Tokenizer:最容易被低估的一环

真正开始准备训练数据,我才意识到“模型大小”反而不是最难的部分,最难的是弄到干净、多样、符合目标场景的文本语料。预训练阶段我主要用了三类开源数据:通用网页清洗文本、中文百科类文本、以及一批书籍和论文语料。总量上大概收集了 20GB 原始文本,去重清洗后剩下一半左右。

清洗流程比模型训练本身更琐碎。我按下面的顺序来处理,每一步都有明确目的:

  1. 去掉 HTML 标签、Markdown 标记、多余空行和零宽字符,避免模型学到大量无意义格式。
  2. 按 MinHash 做近似去重。重复文本会让模型背数据,直接表现为 Loss 异常偏低但生成内容泛化差。
  3. 过滤掉过短的句子和纯列表类文本,防止数据碎片化。
  4. 统一全半角符号,处理掉广告、导航等噪声区块。

Tokenizer 这块也花了不少时间。我直接用tokenizers库训练了一个 BPE 词表,词表大小设为 32000。这个大小对中文场景比较合适:太小了中文一个字要拆成多个 token,序列利用率低;太大了 embedding 层占用显存和内存都更多。训练词表时我特意混入了一定比例的英文代码和数学符号,方便后续做代码相关的 CPT 和指令微调。词表训练好后一定要自己检查几个样本,看看中文常用字、常见英文单词、数字串有没有被拆得离谱。Tokenizer 出了问题,后面所有阶段都会被污染。

1.3 参数量、计算量和显存的粗估公式

这里分享一个我一直在用的粗估方法。对于 decoder-only 的 Transformer,训练总计算量近似是6 * 参数量 * token数,这个数字对应的是前向反向的标准 FLOPs。Xihe 按 2 亿参数算,如果训 10 亿 token,总计算量大概是 1.2e18 FLOPs。两张消费级显卡的实际 FP16 算力加起来按 80 TFLOPS 算,理论上几十小时能完成,但加上数据加载、通信、评估、断点保存这些开销,实际一般按天计算。所以我的建议是:先把训练数据量缩减到一个“能在一天内看到明显 Loss 变化”的水平,再逐步扩大,不要一上来就追求刷完整个数据集。

显存方面可以直接套经验值:全量训练时,模型参数、梯度、Adam 优化器状态大约需要16 bytes * 参数量的显存,2 亿参数就是 3.2GB 左右。真正吃显存的是激活值,尤其是长序列和了大 batch size。序列长度 512、batch size 32 的情况下,加上中间激活,单卡 24GB 是够用的。如果后续把序列长度拉到 2048,就必须开梯度检查点或减小 batch。计算之前先拿这个公式估一遍,能少走很多弯路。

2. 预训练实操:先让模型把“人话”练顺

预训练是整个流程的地基。这一步的目标不是让模型会回答问题,而是让它学会词汇、语法、常识和基本的文本连贯性。很多初学者跳过了预训练,直接拿一个开源中文模型做 SFT,总觉得节省了时间,但其实很多“模型怎么教都教不会”的问题,根源就在预训练数据不够或者质量太差。

2.1 基础超参数:序列长度、batch size、学习率

Xihe 预训练阶段的配置我记在下面,直接抄作业可以参考:

超参数设置值说明
模型层数 / 隐藏层16 层 / 1024 维约 2 亿参数的配置
序列长度512降低显存压力,足够学习语感
全局 batch size64两张卡各 32,配合梯度累积
优化器AdamWbeta=(0.9, 0.95)
学习率3e-4峰值学习率
Warmup 步数1000稳定训练初期
学习率调度cosine衰减到峰值的 10%
Weight decay0.1对 embedding 不做衰减
混合精度bf16窄精度训练,减少显存

这里的核心原则是:预训练学习率可以比微调高一个数量级。因为预训练数据量极大,模型需要快速拟合文本分布;到了 SFT 阶段再用小学习率慢慢修行为。峰值学习率 3e-4 对 2 亿参数的小模型是安全的,如果发现 Loss 震荡,优先调低而不是加大。训练时我没用 dropout,这是语言模型预训练的习惯,Dropout 更多是微调阶段为了防止过拟合才加的。

2.2 训练过程中的监控与断点续训

预训练跑起来之后,我最关心的不是单步 Loss,而是平滑后的 Loss 曲线和 Perplexity。可以在每 200 步打印一次日志,每 2000 步做一次小样本困惑度评估。小模型训练容易出现一个假象:Loss 下降很快,但打开生成结果一看全是重复的套话。这时候需要看生成样本而不是只看数字。

断点续训一定要做扎实。我用的是 Transformers 的 Trainer 框架,自动保存设置在save_steps=1000。保存内容除了模型权重,还必须包含 optimizer、scheduler、RNG state,这样中断后可以继续训练。考虑到我经常改数据,我会在加载时检查当前步数对应的数据集位置,避免续训时重复读前面的数据导致数据分布不一致。这个细节很关键,很多训练中断后 Loss 反弹,就是因为没有正确恢复到数据流位置。

2.3 预训练阶段最容易踩的坑

第一个坑是数据重复和“脏数据”导致 Loss 抖动。一开始我拿爬虫数据直接训,Loss 降到 3 左右就再也下不去了,打开生成结果发现模型会重复一些电商广告语。后来重新做了去重和规则过滤,Loss 才继续下降。语言模型对数据分布极其敏感,少量噪声数据就会在生成结果里被放大。

第二个坑是序列拼接方式。小模型训练时如果把多个短文本随便拼到一个序列里,模型会学到跨文本的伪关联。我的做法是在每个文档末尾加上<eos>分隔符,再按自然边界切分,尽量不让一句话被切断到两个序列。

第三个坑是 bf16 下的 Loss spike。fp16 在小模型上偶尔会因为梯度溢出导致 Loss 突然飙到几十,然后再也回不来。后来我把整个训练切到 bf16,同时开启梯度裁剪为 1.0,这个现象基本消失了。如果是老显卡不支持 bf16,就老老实实用 fp32 跑,别为了省显存硬上 fp16。

3. CPT 继续预训练:把通用模型“拉”到目标领域

预训练出来的 Xihe 是一个“什么都知道一点”的通才,但当我需要它专注在特定领域,比如医疗文本、法律文本或者编程代码时,直接 SFT 往往会发现模型缺少领域术语和表达习惯。CPT,也就是继续预训练,就是在通用模型基础上,用大量领域语料再做一轮语言模型训练,让模型先“读”够领域内容,再去微调问答能力。

3.1 CPT 和预训练有什么不一样

表面上 CPT 和预训练都是 next token prediction,但两者的目标完全不同。预训练是从零开始学语言,CPT 是在已经会语言的基础上“补课”。所以 CPT 的学习率必须更低,训练步数也不用太多,否则模型会把之前学到的通用能力冲掉。

我给 Xihe 做 CPT 时,用的是大约 2GB 的领域文本,包含医学问答、药品说明书、诊疗指南这类内容。目标不是让它成为专家,而是让它熟悉医学术语的分布,比如“适应症”“禁忌症”“不良反应”这些词在什么上下文中出现。训练设置上,学习率降到 1e-4,序列长度提升到 1024,batch size 保持不变,总共只训练了大约 2 个 epoch。

这里有个很重要的判断标准:CPT 阶段结束后,通用能力不能明显下降。我通常会在同一个测试集上对比 CPT 前后的困惑度,如果通用困惑度上涨超过 10%,就说明训练步数太多或者领域数据比例太高了。

3.2 数据配比与混合策略

做 CPT 最忌讳的就是只拿领域数据训。如果 100% 使用医疗文本,模型的语言能力会慢慢偏向医疗风格,日常对话能力退化得非常快。我采用的数据配比是 70% 领域语料 + 30% 通用语料。通用语料可以是预训练时用过的数据,也可以重新采一批百科、新闻文本。

训练时我把领域数据和通用数据混合成一个个 shard,每个 shard 内部做随机洗牌,但保证整体比例不变。这样每个训练 batch 里都有两种来源的文本,模型每一轮都能同时看到“新知识”和“旧知识”,从机制上缓解灾难性遗忘。

另一个容易被忽略的点是领域数据的重复检测。很多医疗语料网站之间互相抄,表面上看起来 2GB 数据很多,实际上可能只有几十万篇不重复的文章。我做了归一化去重之后,数据量直接缩水到 1.2GB。宁可少而精,不要多而杂。

3.3 减少灾难性遗忘的三招

如果跑完 CPT 发现通用能力明显下降,可以试试这三个办法:

第一招是回放通用数据。我刚才说的数据混合策略本质就是回放,只不过把通用数据比例提高一些,比如从 30% 提高到 50%,多跑一小段观察恢复情况。

第二招是降低学习率。把学习率从 1e-4 降到 5e-5 甚至 3e-5,CPT 虽然学得慢,但对原有参数的破坏更小。领域知识不需要模型一下记住,本来后续 SFT 还会强化。

第三招是只训练部分层。有些实现会把模型前几层 freeze 掉,只更新后面的层和 LayerNorm,相当于把“通用语法能力”锁住,只调整“高层语义映射”。我当时试过冻结 embedding 层和前 4 层 Transformer,效果还不错,但收敛速度会略慢。

这三招不是互斥的,实际项目中我经常用第一招加第二招的组合。核心思路就一条:CPT 不是二次预训练,而是有节制的领域适配。

4. SFT 监督微调:教模型回答问题

预训练和 CPT 做完了,Xihe 已经能续写文本,但你问它问题,它只会顺着你的问题往下编,不会规规矩矩回答。SFT 阶段就是给它看大量“用户提问-标准回答”的样本,让它学会对话的格式和内容。

4.1 指令数据长什么样

指令数据的质量直接决定了模型后续的上限。我整理了两类数据来源:开源指令数据集和自造数据。自造数据主要来自一个很土但有效的方法:先让一个大模型基于领域文档生成问答对,再人工抽检修正。一个标准的训练样本长这样:

<bos><user>高血压患者可以服用哪些降压药?</user><assistant>高血压患者应在医生指导下,根据自身情况选择药物。常用的降压药包括 ACEI、ARB、钙通道阻滞剂、利尿剂等。具体用药需要综合考虑患者年龄、合并症和血压水平。</assistant><eos>

这个格式本身不是唯一的,但你一旦定了格式,预训练、SFT、后续测试都要保持一致。我见过不少项目在 SFT 时随便换 prompt 模板,结果模型生成混乱。从预训练开始就确定特殊分隔符,后面所有阶段都不要改。

SFT 数据量不需要幻想几十万条。对于一个小模型,几万条高质量的问答对已经能带来肉眼可见的变化。我最终用了 5 万条左右,覆盖:知识问答、多轮对话、意图识别、文本改写四种类型。数据数量上宁缺毋滥,一条答非所问的坏样本会拖累很多好样本。

4.2 训练时只让模型学 Response 部分的 Loss

SFT 和预训练虽然都在做自回归,但有一个关键细节:SFT 时不能把所有 token 的 Loss 都算进去,否则模型会把“问题”的内容也背下来,白白浪费容量。正确做法是对标签做 mask,把用户部分的标签设为 -100,这样计算交叉熵时自动忽略这些位置。

labels = input_ids.clone() labels[user_token_mask] = -100

在 HuggingFace 里,直接把labels传给模型就行,模型只会计算非 -100 位置的 Loss。这个操作看起来简单,但很容易被忽略。如果忘记 mask,模型会花费大量能力去记忆用户问题,最终表现是回答内容正确率下降,而且更容易重复问题中的词句。

另一个需要注意的点是 assistant 部分的结束符。我的格式里在回答末尾加<eos>,这样模型能学会在回答结束后停下来,而不是一直啰嗦。如果你发现模型生成内容总是戛然而止或无限续写,大概率跟结束符处理有关。

4.3 SFT 实操要点:学习率、轮数、早停

SFT 的常规配置是学习率 2e-5 到 3e-5,训练轮数 2 到 3 个 epoch,全局 batch size 可以比预训练小一些。因为指令数据通常只有几万条,大 batch 会让模型很快见过全部数据,反而容易过拟合。

训练时我留了 5% 的样本作为验证集,每个 epoch 结束算一次验证 Loss。如果验证 Loss 在上升而训练 Loss 还在下降,就是典型的过拟合信号,果断早停。小模型在 SFT 中过拟合的现象非常常见,因为模型容量相对数据量来说太大了,一旦反复看过太多遍同样的数据,就会开始“背答案”。

还有一个我踩过的坑:把多个 epoch 的结果平均。有些人会训练 5 个 epoch 然后把 checkpoints 做权重平均来提升效果,这种方法在 SFT 上是有效的,但前提是每个 checkpoint 的验证 Loss 都在可接受范围内。如果某个 checkpoint 已经过拟合,平均只会拖累整体。只选验证 Loss 最低的 checkpoint 是更稳妥的做法。

5. PEFT:用 LoRA 给模型“换脑子”

SFT 之后的 Xihe 已经能回答问题了。但你会发现一个现实问题:业务场景很多,今天想适配医疗,明天想适配法律,后天想适配代码。每次都用全量微调成本太高,而且每微调一次都要保存一个完整模型副本。这时候就该 PEFT 上场了。

5.1 为什么不用全量微调

全量微调的意思是所有模型参数都参与更新。对于 2 亿参数的 Xihe 其实还能接受,但如果你用的是 7B 或 13B 的开源模型,全量微调一张卡完全放不下,而且每个任务都要存一份完整权重,维护成本很高。

PEFT 的核心思想是冻结原始权重,只训练极少数新增参数。我用的是最普遍的 LoRA。LoRA 的原理可以用一句话说明:在 Transformer 的线性层旁边加两个低秩矩阵,用低秩矩阵的乘积来模拟权重更新量。推理时可以把低秩矩阵合并回原权重,不增加任何额外推理耗时。

对 Xihe 这种小模型,LoRA 的好处更多是工程上的:训练速度快、显存占用低、多个任务可以各自训一个 adapter,切换时只需要加载不同的 adapter 文件。我甚至给同一个基座模型同时训了医疗问答和代码生成两个 LoRA,使用场景完全不同,互不干扰。

5.2 LoRA 关键参数:rank、alpha 和 target_modules

LoRA 最关键的三个参数是r(rank)、alpha(缩放系数)和target_modules(作用目标)。我给 Xihe 的 SFT 后 LoRA 适配配置如下:

lora_config = LoraConfig( r=16, lora_alpha=32, target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], lora_dropout=0.05, bias="none", )

r决定了低秩矩阵的宽度。理论上r越大表达能力越强,但训练参数和显存也会增加,而且过大的r在小规模数据上反而容易过拟合。从实践中看,r=16是多数任务的安全起点。alpha实际上是缩放系数,最终生效的权重更新量为alpha / r * 低秩乘积。我习惯把alpha设为2 * r,即 16 配 32。

target_modules很多人只设置q_proj和v_proj,觉得这样参数量少。我的经验是如果要适配一个全新领域,最好把q/k/v/o四个投影层都加上。注意力层是模型捕捉语义关系最核心的地方,只调其中两个投影,信息通路不完全,效果会打折扣。当然参数量会多一些,但对应的是效果更稳。

5.3 LoRA 训练与合并的实操细节

用 LoRA 做 SFT 时,把基座模型冻结,只给 adapter 传梯度。训练时优化器状态只包含 LoRA 参数,所以显存占用会小很多。如果基座模型本身已经很大,还可以用量化后的基座配 LoRA,也就是常说的 QLoRA:把基座量化为 4-bit,LoRA 保持 16-bit 训练。XLora 这套流程我后来也跑过,效果基本不损失。

训练完成后,LoRA 权重可以单独保存,只有几十 MB。但实际部署时我更喜欢把 adapter 合并回基座模型。合并操作本身很简单:

model = PeftModel.from_pretrained(base_model, lora_path) merged_model = model.merge_and_unload() merged_model.save_pretrained("xihe_lora_merged")

这么做的好处是推理时不再依赖 PEFT 库,直接用原生模型加载,速度和稳定性都更可控。多个 LoRA 场景下,我建议保留独立 adapter,不要合并到同一个基座里,不然就失去“切换”的意义了。

6. 蒸馏:拿大模型当老师,把小模型教得更聪明

到这里,Xihe 已经经过预训练、CPT、SFT 和 LoRA 适配,能完成不少任务了。但它的表现和那些动辄几十B 的大模型相比,答案的流畅度、严谨度和“聪明感”还是差了一截。知识蒸馏就是让大模型当老师,把它的知识搬给 Xihe 这个“学生模型”。

6.1 蒸馏的本质

常规训练里,模型学习的是硬标签,也就是“这句话只属于第 3 类”这种确定答案。但大模型输出的概率分布里藏着更多信息,比如“高血压”这个词后面更可能跟“患者”还是“病人”,大模型的概率分布中这些细微倾向都能体现。蒸馏就是让学生模型去拟合老师模型的软概率分布,把这些“暗知识”学过来。

实际操作中,我让一个更大的商用或开源模型对同一批问题生成回答,然后让 Xihe 学习这些回答。这里有两种做法:第一种是把大模型的回答当作硬文本来做 SFT;第二种是用大模型输出的 logits 分布做 KL 散度蒸馏。前者实现简单,我大部分场景都用它;后者收敛更稳,但需要拿到老师模型的 logits,工程上更麻烦。

6.2 温度、KL 散度与损失权重

做软标签蒸馏时,温度 T 是一个绕不开的参数。温度的作用是平滑概率分布:温度越高,概率分布越平滑,模型能学到更多“候选词”之间的关系;温度太低,分布接近 one-hot,和硬标签没什么区别。我常用 T=2 到 T=4 之间。

蒸馏的总损失一般是两项加权:学生模型与硬标签的交叉熵 + 学生模型与教师软标签的 KL 散度。权重系数 alpha 通常取 0.5,表示两边各占一半。如果数据里硬标签本身很干净,可以调高硬标签权重;如果你想让学生多学老师“回答风格”,就调高 KL 项。

温度在训练结束后需要调回 1,因为推理时的真实分布不需要平滑。这一点新手很容易忘记,导致模型生成结果过于保守。

6.3 蒸馏数据怎么做才划算

知识蒸馏最大的成本不是训练,而是数据生成。大模型生成数据很慢,而且同一个问题如果反复问太多遍,生成的答案会趋向单一,学生模型学到的多样性就不够。我一般控制每个问题只生成 2~3 个候选回答,再结合人工抽检和规则过滤,去掉空回答和明显错误的内容。

另一个经验是蒸馏数据里的 prompt 要尽量贴近真实用户,不要全用照本宣科的格式。用户可能说“降压药有哪些?”也可能说“医生开的这个药我能不能吃?”prompt 一多样,学生模型才能真正泛化到真实场景,而不是只记住训练时的固定模板。

如果条件允许,还可以把老师模型的“自我纠错”过程也生成出来,比如让老师先给一版答案,再说“上面哪里不对,修正版本是什么”。这类数据对提升学生模型的长尾能力很有帮助,但要注意控制数据长度,否则训练时长会显著增加。

7. DPO:让模型学会说“人爱听的话”

SFT 和蒸馏做完之后,Xihe 基本能做到“有问必答”,但它偶尔还是会输出冗长废话、立场摇摆或者语气不好的内容。要解决这类偏好问题,过去的主流方案是 RLHF:训练奖励模型再做强化学习。但 RLHF 实在太不稳定了,奖励模型训练容易崩溃,PPO 超参数又多。DPO(Direct Preference Optimization)直接跳过了训练奖励模型的环节,只用偏好对来优化策略模型,非常适合在小模型上落地。

7.1 从 RLHF 到 DPO:简化偏好对齐

传统 RLHF 需要一个奖励模型给答案打分,再用强化学习算法调整策略模型。DPO 的核心思想是:策略模型和参考模型之间的概率比本身就隐含了奖励信息,可以直接用偏好数据构造损失函数,不需要单独的奖励模型。

用大白话说,RLHF 相当于先请一个“评委”,再让选手按评委意见反复练;DPO 则是直接拿“评委的评分记录”让选手学习,省掉评委这个角色。这个简化对小项目来说太友好了,因为训练奖励模型本身就需要大量人工标注,代价很高。

我在实际使用中,DPO 的训练稳定性和 SFT 差不多,只要数据构造正确,基本不会出现 PPO 那种奖励模型 Reward Hacking 的问题。当然 DPO 也不是万能的,它对偏好数据的质量要求非常高,一对烂数据可能抵消十对好数据。

7.2 偏好数据构建:成对样本怎么来

DPO 的标准训练数据是一对样本:一个是较好回答(chosen),一个是较差回答(rejected)。我需要构造出同一问题下两个质量明显有差异的回答。最简单的方式是用大模型生成两个风格不同的回答,然后人工排序。

我的排序标准通常考虑三个维度:一是正确性,错误信息必须排在劣;二是相关性,答非所问算劣;三是风格,简洁清晰优于冗长绕圈。如果一个 pair 的两个回答差异不明显,我会直接丢弃,因为 DPO 需要的是清晰偏好信号,模糊 pair 会让梯度方向反复震荡。

偏好数据量不需要太大,几千对高质量 pair 就能看到效果。我用 3 万对数据做了实验,发现模型在上面花太多步数后会出现一种“为了偏好而失去内容”的现象,生成变得过于谨慎,什么都说“请咨询专业医生”。后来把训练步数减半,同时只保留差异明显的 pair,效果反而更好。

7.3 DPO 训练的 beta、学习率与稳定性

DPO 有一个核心超参数beta,它控制对参考模型的偏离程度。beta越大,模型越不敢偏离参考模型,训练更稳定但偏好改变也越弱;beta越小,模型可以更大胆地改变行为,但容易跑偏。我大多数实验用beta=0.1,如果数据噪声大就提高到 0.2,数据很干净就降到 0.05。

DPO 训练时会把参考模型和策略模型都加载进显存,内存开销接近两份模型。我的做法是参考模型完全不更新梯度,用torch.inference_mode()包起来,只保留策略模型的反向传播,能省不少显存。

学习率用 1e-6 到 1e-5 之间,比 SFT 还要低。因为 DPO 是在 SFT 后的模型上做“微调中的微调”,步子大了容易摧毁已经学到的格式和内容。训练时每隔几百步就在固定测试集上生成一些样本,人工看一眼。如果发现模型变得话痨或者过于保守,立刻降低 beta 或者减小学习率。别等到一个完整 epoch 跑完再检查,那就太迟了。

8. 常见问题与排查实录

整条链路跑下来,我遇到的问题远比上面写的多。这里把最典型、最容易被搜索引擎翻到的问题整理成速查表,希望能让后来者少交点学费。

8.1 训练 Loss 不降或突然飞升

Loss 不降,先看数据,再看学习率,最后看模型实现。一个通用排查顺序:

现象可能原因解决办法
Loss 一直很高数据噪声大、tokenizer 词表不匹配清洗数据、重训 tokenizer
Loss 缓慢下降后平台期学习率太低、数据量不足调高学习率、增加数据
Loss 突然暴涨后不恢复fp16 梯度溢出换 bf16、增加 gradient clip
Loss 出现周期性波动每个 epoch 重复数据打乱 shard 顺序、做去重
训练 Loss 很低但生成很差数据重复、模型在背答案去重、增加数据多样性

其中“Loss 很低但生成很差”是最迷惑人的情况。我在预训练阶段遇到过,后来意识到是因为训练集里大量网页模板重复,模型只需要背住模板就能把 Loss 刷得很低。换了去重后的数据,Loss 稍微回升了一些,但生成质量肉眼可见地变好了。

8.2 显存不足与序列长度平衡

显存不够是最常见的硬件问题,尤其是当你从 SFT 切到 DPO,发现要同时加载两个模型的时候。我建议按顺序尝试:

  1. 减小 batch size,并用梯度累积补足全局 batch。
  2. 开启梯度检查点,用少量速度换大量显存。
  3. 减小序列长度,比如从 2048 降到 1024,如果任务不依赖长上下文,收益很大。
  4. 全量微调改 LoRA,冻结基座模型。
  5. 基座模型量化到 4-bit,再挂 LoRA。

有一点要提醒:序列长度影响的不只是显存,还有模型对长文本的理解能力。如果你最终部署场景需要处理长文档,不要在预训练阶段就把序列长度降到 256,后面 SFT 再想靠短序列模型处理长文本会非常吃力。最好从头到尾保持一个能接受的序列长度,比如 512 或 1024。

8.3 怎么判断模型真的变好了

Loss 只是训练信号,不是产品指标。我在每个阶段都会准备一个固定的 20~50 条 prompt 测试集,包含知识问答、开放生成、多轮对话、恶意输入等类型。每个阶段训练完,手工逐条看生成结果,给每一条打一个“可用/勉强/不可用”的标签。这个动作看起来很原始,但比任何自动化指标都可靠。

如果你想要一个自动化指标,也可以用分类准确率、BLEU 等,但对生成式语言模型来说,这些指标很容易被“高分低质”骗过。我后来养成的习惯是:每天跑完训练后,随机抽 10 条生成结果粘贴到聊天软件里,当普通消息发给自己,看看像不像一个真人写出来的。如果一眼就能看出是模型套话,说明还没训练到位。

另外一个容易被忽略的评估维度是“变化幅度”。同一个 prompt 在预训练、CPT、SFT、蒸馏、DPO 之后的输出应该逐步变好。如果某个阶段之后突然变差,大概率是哪一步超参数出了问题,要及时回滚到上一个 checkpoint,而不是继续硬着头皮往下训。


最后再分享一点个人心得。整套流程跑完,我最大的体会是:训练小语言模型最难的不是某个算法原理,而是把数据、格式、超参数和评估连成一条线。预训练、CPT、SFT、PEFT、蒸馏、DPO 每一步单独看都不复杂,但它们之间互相影响,前面埋的雷会在后面突然爆出来。如果你也想自己跑一遍,我建议先别追求大模型、大数据,从 1 亿参数的模型和几亿 token 的数据开始,把链路跑通,再逐步放大。每个阶段都固定好测试集,记录每一版模型的输出样本,你会看到模型一步步“懂事”的过程,这个正反馈是坚持下去的最大动力。

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

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

立即咨询