LLM Post-Training and Fine-Tuning Practice: From SFT and LoRA to Human Alignment
后训练不是一个算法名
预训练结束时,我们得到的是一个擅长预测下一个 token 的基座模型。它可能掌握了大量语言与代码模式,却不一定会按用户期待的方式回答:格式不稳定,任务边界模糊,遇到冲突指令时也未必知道该优先遵守什么。后训练要做的,是用更小、更有目标的数据重新塑造这种行为。
我早期把指令微调、人类对齐、显存估算、数据格式和框架选择分成了两篇。问题是,训练方法和工程条件被拆开以后,很容易出现一些奇怪结论:只讨论 LoRA 不讨论激活值,只比较数据格式却不看 chat template,或者把 QLoRA 当成部署量化。现在把它们放回同一条链里看。
graph LR
A["预训练模型"] --> B["持续预训练"]
A --> C["监督微调 SFT"]
B --> C
C --> D["偏好数据"]
D --> E["DPO 等直接偏好优化"]
D --> F["奖励模型 + RL"]
C --> G["评估 / 安全 / 领域测试"]
E --> G
F --> G
G --> H["部署与数据回流"]
这几条路线改变的对象并不相同:
- 持续预训练继续学习领域文本分布,适合补语言、知识和代码风格;
- SFT模仿高质量输入输出对,建立指令遵循、格式和任务行为;
- PEFT决定哪些参数参与更新,是训练资源方案,不是独立的数据目标;
- 偏好优化比较多个回答,让模型更倾向于被选择的行为;
- 强化学习把奖励或环境反馈转成策略更新,适合可交互、可验证或需要探索的问题。
后训练没有固定流水线。一个领域补全模型可能只需要持续预训练,一个分类器可能只做 SFT,Agent 则可能依次使用 SFT 轨迹和环境奖励。先确认要改变什么行为,再决定数据与算法,顺序不能反过来。
数据工程:先定义训练对象
训练脚本最后看到的是 token 序列和 loss mask,团队日常维护的却应该是结构化样本。比较常见的对象有三类。
持续预训练文本
持续预训练通常保留自然文本或代码的连续结构:
1 | {"text": "领域文档、代码或其他连续语料……"} |
这类数据没有天然的 instruction 和 answer。训练目标仍然是预测下一个 token,因此它更接近预训练,只是数据分布和计算规模更集中。若目标是教模型稳定遵守某个输出格式,单靠持续预训练通常不够直接。
指令与单轮样本
Alpaca 风格常用 instruction、input、output 表示单轮任务:
1 | { |
它适合数据制作和人工审阅,但这些字段不是模型最终读取的格式。训练前仍要把它们渲染成目标模型的 token 序列。
多轮消息
现代对话数据通常使用消息数组:
1 | { |
ChatML、ShareGPT、Alpaca 是常见约定,不是跨模型统一标准。ShareGPT 常用 conversations/from/value,消息格式常用 messages/role/content,不同训练框架还会接受自己的列名。数据进入 tokenizer 前,应先转换成内部统一 schema,再由模型对应的 chat template 渲染。
Chat template 决定模型实际看到什么
Chat template 会把角色、消息和特殊 token 拼成模型训练时使用的字符串。下面两个消息对象语义相同,经过不同 tokenizer 后可能得到完全不同的起止符和 role token。模板不匹配时,模型会看到训练阶段从未见过的控制符,表现往往比数据内容本身的问题更隐蔽。
Hugging Face 的 Chat Templates 文档给出了 apply_chat_template 的使用方式。训练和推理应复用同一个模板,并明确是否添加 generation prompt、EOS 放在哪里、工具消息怎样编码。不要手工猜一个“看起来像该模型”的格式。
Loss mask 决定模型模仿谁
一段对话可以对所有 token 计算 loss,也可以只训练 assistant 回复。后者常被称为 response-only 或 assistant-only loss:system 和 user token 仍作为条件输入,却不要求模型去预测它们。
这里没有对所有任务都正确的答案。只训练 assistant 更符合“根据请求生成回答”的目标,也能避免模型浪费容量复述用户;对某些需要学习完整交互结构或特殊控制 token 的训练,团队也可能保留更多位置的 loss。重要的是把 mask 明确记录下来。两份文本完全相同的数据,只要 loss mask 不同,训练目标就已经不同。
Packing 提高利用率,也改变样本边界
大量短样本如果逐条 padding,会浪费很大一部分计算。Packing 把多条样本拼进同一序列,提高有效 token 比例。实现时要确认注意力是否允许跨样本、位置 ID 怎样处理、EOS 是否正确插入,以及 loss mask 是否在边界处错位。
Packing 是吞吐优化,不该改变数据语义。若训练后出现模型把上一条答案接到下一条问题上的现象,先检查样本边界和模板,而不是立刻调学习率。
高质量不是一个模型评分
后训练数据至少要检查:
- 指令是否可执行,输入是否包含完成任务所需的信息;
- 回答是否正确、完整,并符合希望模型学习的风格;
- 多轮对话中角色、工具调用和环境状态是否前后一致;
- 是否混入评测集、模板泄漏、错误引用和无法复现的外部状态;
- 数据来源、生成模型、过滤规则和修改历史能否追踪。
合成数据降低了写样本的成本,也更容易规模化复制同一种错误。更完整的数据合成流程见《数据合成正在成为一门工程:从 Terminal-Corpus 说起》。那篇文章讨论的是怎样从任务世界、环境和 verifier 反推训练数据;这里关心的是数据进入训练器后怎样被编码和计算 loss。
显存估算:先拆账,再谈能不能训练
“7B 模型需要多少显存”没有脱离配置的固定答案。全参数还是 LoRA、权重精度、优化器、序列长度、batch size、是否保存 master weights、是否使用 checkpointing 和分片,都会改变结果。比较稳的做法是先把显存拆成几本账。
权重
参数量为 、存储精度为 bit 时,权重的理论大小为:
7B 参数使用 BF16/FP16 时约为 14 GB,INT8 约 7 GB,4 bit 约 3.5 GB。这只是参数数据本身,量化 scale、zero point、分组元数据和运行时缓冲会增加额外占用。
梯度与优化器状态
如果可训练参数量为 ,梯度显存近似为:
AdamW 通常为每个可训练参数保存一阶和二阶矩,若两者都用 FP32,仅这部分就是 bytes。有些混合精度实现还会保留 FP32 master weights,再增加 bytes。框架、优化器和分片策略不同,不能把某个“每参数固定字节数”当成通用常量。
以 7B 全参数 BF16 训练为例,不考虑分片与激活:权重约 14 GB,BF16 梯度约 14 GB,两份 FP32 Adam 状态约 56 GB;若另存 FP32 master weights,再增加约 28 GB。也就是说,光这些状态就可能是 84 GB 或 112 GB,激活值、通信缓冲和 CUDA workspace 还没开始算。
LoRA 的差别在这里很明显:基座权重仍需加载,但只有低秩参数参与梯度和优化器更新,因此 。不过“梯度和优化器变小”不代表激活值消失,长序列和大 batch 仍可能把显存吃满。
激活值
激活值与 batch size、序列长度、隐藏维度、层数、注意力实现和保存哪些中间结果有关。它不会由参数量单独决定,也很难用一个固定系数跨架构估算。
实践中常用这些手段控制激活显存:
- gradient checkpointing 不保存全部中间结果,反向传播时重新计算;
- FlashAttention 一类实现减少注意力中间矩阵的物化;
- sequence packing 提高有效 token 比例,但总 token 增加后激活也会增加;
- gradient accumulation 用多次小 micro-batch 模拟较大的有效 batch;
- 缩短序列长度通常比微调几个 LoRA rank 更直接地降低激活开销。
分片与框架开销
FSDP、ZeRO 会把参数、梯度和优化器状态分片到多张卡。它们降低单卡占用,却增加通信、调度和配置复杂度。CUDA context、kernel workspace、临时张量、dataloader 预取和显存碎片也需要留余量。
所以显存估算应该写成区间,并通过一次目标配置的短跑验证。能够完成 forward/backward 还不够,要观察峰值显存、吞吐、是否频繁 offload,以及保存 checkpoint 时会不会再次 OOM。
SFT:让模型模仿目标行为
监督微调使用输入和目标输出计算交叉熵,让模型提高目标 token 的概率。InstructGPT、FLAN 和 Self-Instruct 分别展示了人工指令、任务混合与合成指令在 instruction tuning 中的作用。
SFT 最适合学习可示范的行为:回答结构、工具调用轨迹、领域术语、拒答方式、代码修改模式和对话风格。它不保证把训练样本里的事实可靠写进模型,也不能让模型稳定完成基座模型完全不会的任务。若训练数据本身在猜,模型只会更熟练地模仿这种猜法。
学习率、batch 和 epoch 没有脱离数据的答案
后训练通常使用比预训练更低的学习率,但具体范围受模型规模、全参数/PEFT、数据量、优化器和目标任务影响。比背一组默认参数更有用的是观察:
- 训练 loss 是否下降而验证任务没有改善;
- 模型是否快速丢失原有通用能力;
- 少数模板是否占据输出,造成风格坍缩;
- 长回答和短回答是否因为 token 数差异获得不同权重;
- 多个数据源混合后,哪一类样本主导了梯度。
小数据重复多个 epoch 很容易过拟合。数据规模大时,训练一部分 epoch 也可能足够。最终选择应由留出的任务集、格式通过率和回归测试决定,而不是只看训练 loss。
PEFT:减少更新参数,不改变训练目标
参数高效微调(PEFT)冻结大部分基座参数,只训练少量附加参数或被选择的参数。它降低梯度和优化器状态的显存,也方便为多个任务分别保存 adapter。更完整的技术谱系见《参数高效微调(PEFT):从 Adapter 到 LoRA 的技术演进》。
LoRA
LoRA 假设下游适配所需的权重更新具有较低的内在秩。对于原始权重 ,它冻结 ,用两个低秩矩阵表示更新:
若 ,秩为 ,则 、。当 远小于输入输出维度时,可训练参数显著减少。
LoRA 的主要决策包括 target modules、rank、alpha、dropout,以及是否训练 embedding、lm head 或 bias。只写“使用 LoRA”仍然缺少足够信息。对注意力投影加 adapter 和对所有线性层加 adapter,参数量与效果可能差很多。
Adapter 可以在推理前合并进基座权重,也可以保持独立加载。合并后通常不会增加额外矩阵计算,但失去运行时切换多个 adapter 的便利;不合并则更灵活,也需要推理框架正确支持。
QLoRA
QLoRA 把冻结的基座权重以 4 bit 形式存储和计算,同时训练较高精度的 LoRA 参数。它进一步压低权重显存,但训练时仍需反量化计算、激活值和 LoRA 优化器状态。
QLoRA 不是“用 INT4 训练全部参数”,也不是部署量化格式的统称。它解决的是低资源微调;训练完成后要不要合并 adapter、用什么精度导出、目标服务是否支持对应量化内核,仍是另一组决定。
Adapter 和参数选择
经典 adapter 在 Transformer 层中插入小型瓶颈网络;prompt tuning、prefix tuning 则训练连续向量,把可学习信息放进输入或注意力前缀。另一类方法直接选择部分层或参数更新。它们都在减少 ,但插入位置、推理开销和多任务组合方式不同。
PEFT 节省的是训练状态,不会自动修复数据问题。若 full fine-tuning 学不到目标行为,换成 LoRA 通常也不会凭空解决;反过来,任务只需要轻量行为适配时,全参数更新可能只是增加成本和遗忘风险。
框架选择:按职责组合,而不是选一个万能框架
微调工具经常被放在同一张排名表里,但它们处在不同层。比较它们之前,先看各自负责什么。
| 组件 | 主要职责 | 官方入口 |
|---|---|---|
| Transformers | 模型、tokenizer、chat template、训练基础接口 | Transformers |
| Datasets | 数据加载、映射、流式处理与缓存 | Datasets |
| PEFT | LoRA、IA3、prompt/prefix tuning 等参数高效方法 | PEFT |
| TRL | SFT、DPO、奖励模型、PPO/GRPO 等后训练器 | TRL |
| Accelerate | 单机多卡、混合精度和分布式启动 | Accelerate |
| PyTorch FSDP | 参数、梯度和优化器状态分片 | FSDP |
| DeepSpeed | ZeRO、offload、并行训练与推理组件 | ZeRO |
| Unsloth | 针对常见模型与 LoRA/RL 工作流的性能优化封装 | Unsloth Docs |
一个常见组合是 Transformers 负责模型和 tokenizer,Datasets 负责数据,PEFT 注入 LoRA,TRL 提供 SFTTrainer 或偏好训练器,再由 Accelerate、FSDP 或 DeepSpeed 处理设备和分片。Unsloth 会在这套生态上提供优化过的模型加载、kernel 和训练入口。
选择框架时要看目标模型是否支持、chat template 是否正确、训练器是否能表达 loss mask 和 packing、分布式 checkpoint 能否恢复,以及最终权重能否被部署框架读取。API 写得短只是体验的一部分。遇到新模型和特殊 loss 时,抽象层越高,越需要准备回到底层确认它究竟做了什么。
Base 还是已经 instruction-tuned 的模型
Base 模型保留更原始的预训练分布,适合拥有足够数据、希望重新定义交互行为的团队;instruction-tuned 模型已经具备对话和指令遵循,少量领域数据通常更容易得到可用结果,也可能继承原有模板、拒答和风格偏好。
不存在“超过多少条数据就必须选 Base”的通用阈值。更实际的做法是用同一批验证任务比较两个起点:若 instruction 模型已经会任务,只需补领域行为,继续微调它通常更省;若原有对齐严重干扰目标格式或语言分布,再评估 Base 模型与更完整的数据配方。
人类对齐:从模仿答案到比较行为
SFT 数据告诉模型“这里有一个目标回答”。偏好数据则给出同一 prompt 下的多个回答及其相对选择,让训练目标从模仿单一答案变成调整回答之间的概率关系。
经典 RLHF
InstructGPT 中常被引用的流程包括三个阶段:
- 用人工示范做 SFT,得到可以遵循指令的初始策略;
- 对多个模型回答进行排序,训练奖励模型;
- 使用 PPO 优化策略,使回答获得更高奖励,同时用 KL 约束避免策略偏离参考模型过远。
奖励模型不是“人类价值观函数”。它学习的是特定标注规范、样本和模型分布下的偏好代理。策略持续针对它优化时,可能利用奖励漏洞,因此需要保留人工评估、独立任务集和行为约束。
PPO 的难点也不只在算法公式。生成样本、计算奖励、估计 advantage、训练 value model、控制 KL、处理长度偏差和维持多机吞吐,都会让系统复杂起来。关于 reward、baseline、advantage 和 normalization 的完整讨论,见《LLM 对齐中的强化学习:从奖励信号到优势估计》。
直接偏好优化
DPO 把偏好建模改写成直接优化策略与参考模型的目标,不需要单独训练奖励模型并运行在线 PPO。它让偏好训练更接近普通监督学习,但并没有消除数据问题:chosen/rejected 的差异是否表达了标注者想要的偏好、回答长度是否形成捷径、训练分布是否覆盖上线请求,仍然决定最终效果。
DPO、KTO、ORPO 等名称会继续增加。阅读这些方法时,可以先问三个问题:使用什么反馈数据,参考模型或隐式奖励怎样进入 loss,训练是离线比较还是需要在线采样。这样比按“是不是 RL”简单分组更容易看出工程差异。
SFT、偏好优化和 RL 怎样分工
- 有明确标准答案或示范轨迹时,先做 SFT;
- 多个回答都可用,但需要学习风格、安全性或质量排序时,使用偏好数据;
- 奖励来自环境、测试或交互结果,并且需要探索不同策略时,再考虑在线 RL;
- 无论采用哪种算法,都要保留独立评估,防止只对训练信号变得更熟练。
不少项目太早进入复杂偏好训练,实际问题却是 SFT 数据格式错、chat template 不一致或评估集太弱。后训练方法越复杂,越应该先证明前一阶段已经稳定。
一份实施检查表
| 问题 | 需要记录的决定 |
|---|---|
| 目标 | 希望改变哪类行为,哪些能力必须保持不退化 |
| 基座 | Base 还是 instruction model,许可证与 tokenizer 是否匹配 |
| 数据 | schema、来源、比例、去重、质量门和评估集怎样定义 |
| 模板 | chat template、特殊 token、EOS 和工具消息如何编码 |
| Loss | 哪些 token 计算 loss,长短样本怎样加权 |
| 训练 | full/LoRA/QLoRA,target modules、精度、batch、序列长度 |
| 显存 | 权重、梯度、优化器、激活、分片和余量分别是多少 |
| 评估 | 任务质量、格式通过率、安全性、回归、延迟和成本 |
| 导出 | adapter 是否合并,最终精度与部署框架是否兼容 |
| 回滚 | checkpoint、数据版本和训练配置能否完整复现 |
后训练经常被描述成选择一个框架、准备一份 JSONL、启动训练。实际结果取决于目标、数据、模板、loss、资源和评估是否彼此一致。LoRA 可以减少参数更新,不能替你定义任务;RLHF 可以使用偏好信号,也不能把不可靠标注变成可靠价值观。
如果只保留一个工作顺序,我会选:先写评估,再整理数据;先跑通 SFT,再决定是否需要偏好优化;先拆显存账,再选择框架。这样做不够花哨,但能让大部分失败更早暴露。
参考资料
- Training Language Models to Follow Instructions with Human Feedback
- LoRA: Low-Rank Adaptation of Large Language Models
- QLoRA: Efficient Finetuning of Quantized LLMs
- Direct Preference Optimization: Your Language Model is Secretly a Reward Model
- Self-Instruct: Aligning Language Models with Self-Generated Instructions
- Finetuned Language Models Are Zero-Shot Learners
- Title: LLM Post-Training and Fine-Tuning Practice: From SFT and LoRA to Human Alignment
- Author: Hyacehila
- Created at : 2024-11-01 12:00:00
- Link: https://hyacehila.github.io//blog/2024/11/01/llm-post-training-and-finetuning/
- License: This work is licensed under CC BY-NC-SA 4.0.