SFT(监督微调)训练实战指南:从参数到技巧,一文讲透

前面的小节详细介绍了大模型SFT的数据部分,包括合成、过滤、多样性等等,数据极大的影响着大模型SFT的效果。介绍完数据从这一节开始具体介绍大模型SFT的过程,包括参数设置、训练策略、训练技巧、效果评估等等。


1、SFT训练参数设置

  • SFT 很少魔改 Loss/策略,训练策略基本和大模型预训练时保持一致,重点在 数据训练可控性
  • 每次开跑前,把这些参数逐一过一遍:epoch / gradient_accumulation_steps / global_batch_size/ learning_rate / lr_scheduler_type / dropout / zero_stage / max_seq_len / offload / gradient_checkpointing / seq_parallel_size / weight_decay / per_device_train_batch_size / num_warmup_steps
  • 一般 1 epoch 就够;对于垂域+小数据(≤1 万条) 可设 3 epochs 以获得更高拟合。
  • SFT 学习率 ≈ Pretrain 的 10倍:若预训练 LR=1.5×10−6\text{LR}=1.5\times 10^{-6}LR=1.5×106,SFT 可取 ≈3×10−5\approx 3\times 10^{-5}3×105
  • Deepspeed:显存足够首选 ZeRO-2;ZeRO-3 在弱带宽集群上 训练会显著变慢。 (ZeRO-2 和 ZeRO-3 的核心区别在于状态拆分的粒度和通信开销,ZeRO-2相当于大家自己留着全套模型,只分工存放优化器和梯度的一部分。而ZeRO-3是大家连模型本体都分开存,想用哪块就去别人那里“借”,所以通信更频繁)
  • Packing:SFT 阶段不建议默认开启。对短 query/短答案拟合有损,尤其是数据量小或难样本占比高时。
    -(在正常训练中,每条样本(query + answer)会被单独处理,并且都被 pad 到同一个 max_seq_length。如果样本普遍很短,就会产生大量 padding token → 浪费显存和算力。
    Packing 会把多条样本拼接到同一个序列里(中间用特殊分隔符 < eos > / < sep >),尽量填满 max_seq_length,减少 padding)

1. 关键参数与可解释性

1.1 epoch

  • 经验值:通用 SFT 设 1垂域小数据≤104\leq 10^{4}104 样本)可设 3 提升记忆度与可用性。
  • 风险:过多 epoch → 过拟合、风格僵化、幻觉式“背书”增强。

1.2 梯度累计gradient_accumulation_steps(GAS) 与 有效批量Global Batch Size(GBS)

gradient_accumulation_steps(简称 GAS) 的作用,就是用多次小批量的前向+反向计算,来累积梯度,相当于变相增大批量大小,直到累计到指定步数后才更新一次模型参数。
在常规训练中,每个小批量(mini-batch)都会立即做一次前向计算、反向传播,并更新参数。但如果显存不足,没法一次放下一个很大的批量,就可以:
用较小的 per_device_batch_size(每张卡上的批量大小)跑一次前向+反向计算。不立刻更新参数,而是把计算出的梯度保存下来。
重复上面的过程多次(次数由 gradient_accumulation_steps 决定),不断把梯度加起来。到了设定的累计步数后,再一次性更新参数,然后梯度清零,开始下一轮累计。
那么一次参数更新的**有效批量(Global Batch Size)**就是:
GBS  (Global Batch Size)  =  GAS×per_device_batch× devices \text{GBS} \;(\text{Global Batch Size}) \;=\; \text{GAS} \times \text{per\_device\_batch} \times \text{\ devices} GBS(Global Batch Size)=GAS×per_device_batch× devices
其中 GAS=gradient_accumulation_steps\text{GAS}=\text{gradient\_accumulation\_steps}GAS=gradient_accumulation_steps


接下来计算一下每个 epoch 需要多少次参数更新(steps)

  • 每个 epoch 的 优化步数
    steps_per_epoch  =  ⌈NGBS⌉ \text{steps\_per\_epoch} \;=\; \left\lceil \frac{N}{\text{GBS}} \right\rceil steps_per_epoch=GBSN
    NNN 为样本条数(或样本对数)。

    你有 N 条训练样本,每次更新参数时会用到的有效批量大小是 Global Batch Size(GBS),也就是所有设备加上梯度累计之后的总批量。那么一个 epoch 需要的更新次数,就是 “总样本数除以 GBS”,如果除不尽,就向上取整(因为最后一次可能不满一整个批量)。GBS 越大,每个 epoch 的更新次数就越少;GBS 越小,每个 epoch 的更新次数就越多。


在批量大小变化时,学习率通常如何调整呢?

  • 线性缩放法则(经验):若以 GBS0\text{GBS}_0GBS0LR0\text{LR}_0LR0 为基准,则
    LReff≈LR0×GBSGBS0 \text{LR}_\text{eff} \approx \text{LR}_0 \times \frac{\text{GBS}}{\text{GBS}_0} LReffLR0×GBS0GBS
    (SFT 中建议保守调 LR,先稳住 loss 曲线的平滑与下降速度。)

在大多数训练经验中,如果你把有效批量 成倍放大,可以考虑按比例增大学习率。例如:原本的有效批量是 GBS₀,学习率是 LR₀。现在批量变成了原来的两倍,那么学习率也可以变成原来的两倍。这样做的原因是:批量越大,梯度估计的噪声越小,模型可以承受更大的学习率。

但 SFT 阶段建议保守调整,因为 SFT 往往是微调已有模型,更关注稳定收敛而不是追求最快速度,所以不要直接完全按比例放大,而是先试小一点的学习率,看 loss 曲线是否平滑下降。

  • Megatron vs Deepspeed 名称差异
    • Megatron 常见显式 global_batch_size
    • Deepspeed / OpenRLHF 可能以 micro_train_batch_size(单卡微批)与 train_batch_size(全局)组合表达,语义等价于上式。

1.3 学习率(laerning_rate)与调度器 (lr_scheduler_type)

  • SFT 学习率
    LRSFT≈10×LRPretrain \text{LR}_\text{SFT} \approx 10 \times \text{LR}_\text{Pretrain} LRSFT10×LRPretrain
    学习率设置策略前面已经介绍了,例如预训练 1.5×10−6⇒3×10−51.5\times10^{-6}\Rightarrow 3\times10^{-5}1.5×1063×105 可作为起点。

  • 调度器选择
    常用 cosine(余弦退火),也可用 linearconstant 适合短程/复刻特定实验;exponential 较少用在 SFT。

  • 热身步数(Warmup)
    warmup_steps∈[5%, 10%]×total_steps \text{warmup\_steps} \in [5\%,\,10\%] \times \text{total\_steps} warmup_steps[5%,10%]×total_steps
    例如总步数 10k,取 500∼1000500\sim10005001000 步。


1.4 正则与 Dropout

  • weight_decay:默认 (0.01) 足够稳妥。
  • dropout:SFT 阶段一般不开。收益小、拖慢训练;除非明显过拟合且数据极其单一。

1.5 序列长度与分布式切分

序列并行(Sequence Parallelism) 是一种分布式训练的并行方式。
不同于 张量并行(Tensor Parallelism, TP) 按参数维度切分模型,序列并行是按序列长度维度来切分输入数据。

为什么要这么做呢?

当序列特别长(例如 8K、16K、32K token)时,单卡显存可能放不下全部激活值(activations)和中间计算结果。
通过序列并行,把一个长序列拆到多张卡上,每张卡只负责其中一段,就能:
1、降低单卡显存占用(每卡只存自己负责的 token 部分)
2、提升吞吐(多个 GPU 同时处理不同 token)

seq_parallel_size 就是把序列切成多少块并行处理的大小设置。

比如你的输入长度是 8K,seq_parallel_size = 2 就会把它分成两段(每段 4K)分配给不同 GPU 去算。

  • max_seq_len:设置成和预训练阶段一致的最大长度,通常是 4K (注意与你的 Base 模型 RoPE/位置编码一致)。
  • seq_parallel_size:序列并行的切块大小。对于超长序列和超大模型,配合张量并行使用;并非必要选项,主要用于提升吞吐/突破显存瓶颈,但要求通信良好。

1.6 ZeRO 与 Offload

  • zero_stage(Deepspeed):
    • ZeRO-2:参数/优化器状态分片,综合吞吐与易用性最佳
    • ZeRO-3:进一步分片参数本体,对带宽要求高,在 NVLink/IB 充足时可扛住,否则训练很慢

这个在介绍Deepspeed的时候已经介绍,正常选择ZeRO-2即可。(ZeRO-2相当于大家自己留着全套模型,只分工存放优化器和梯度的一部分。而ZeRO-3是大家连模型本体都分开存,想用哪块就去别人那里“借”,所以通信更频繁)

  • offload(CPU/NVMe):SFT 阶段一般不建议,I/O 带宽成为瓶颈;除非你就是显存/内存严重不足且能接受明显降速。
    (在正常训练中,模型参数、优化器状态、梯度这些数据都放在 GPU 显存 里。如果模型太大、显存放不下,就会用 offload 把一部分数据临时存放到:CPU 内存(RAM)和NVMe 固态硬盘(SSD),用到的时候再把它们搬回 GPU 参与计算。)

为什么不建议?
1、I/O 带宽成瓶颈:从 CPU 或 NVMe 把数据搬到 GPU,要走 PCIe 或硬盘总线,这比显存内部的数据传输慢一个数量级甚至更多。结果是 GPU 一直在等数据,训练速度会明显下降。
2、延迟高:特别是 NVMe offload,需要读写 SSD,延迟可能从微秒级(显存)变成毫秒级甚至更慢。
3、SFT 场景没必要:SFT 通常 batch 较小、数据量有限,本来就不需要特别大的显存支持,所以开 offload 反而是得不偿失。


1.7 梯度检查点(Gradient Checkpointing)

Activation Checkpointing(激活检查点 / 重计算),也叫 Gradient Checkpointing。

  • 作用:省显存,用重算换内存。

在训练神经网络时,反向传播需要用到前向传播的中间激活值(activations)。默认情况下,这些激活值会全部保存在显存里,直到对应的梯度计算完毕。如果模型层数多、序列长,这部分激活会占掉大量显存。Activation Checkpointing 的做法是:不保存所有中间激活,只保存**关键节点(检查点)**的激活。反向传播时,需要的激活如果没保存,就重新跑一遍前向计算来生成。这样用额外计算换显存节省。

  • 常见结论:计算开销 ≈2×\approx 2\times2× 前向;理论上可将中间激活内存从
    O(N)    降到    O(N)(分段策略) O(N) \;\;\text{降到}\;\; O(\sqrt{N}) \quad (\text{分段策略}) O(N)降到O(N )(分段策略)
    甚至
    O(log⁡N)(树状策略) O(\log N) \quad (\text{树状策略}) O(logN)(树状策略)
    这里 NNN 近似为层数或检查点切分段数的量级(具体与实现相关)。

  • 何时开:当显存紧张但仍想维持较大 GBS\text{GBS}GBS/较长 max_seq_len 时。若集群算力富余,开它很划算。


2. 一些“影响不大”的参数

  • per_device_train_batch_size:单卡微批,SFT 常设 12 以降低 OOM 风险。

  • lr_scheduler_typecosine 更常用;需要可复现实验时考虑 linear/constant


3. 训练技巧与曲线解读

3.1 分渠道监控 loss(channel_loss)

  • 不同 task_type 的损失分布差异很大
    • 创作/开放式生成:答案多样的任务,目标分布更“平”,loss 更高是正常的
    • 结构化/固定答案(翻译、抽取、选择)→ loss 更低
  • 建议为主要任务类型分别统计/绘图,观察各自收敛性。

3.2 特殊符号(special_token)loss

在 tokenizer 里,有一些不是自然语言本身,而是用来控制模型行为的特殊符号,例如:开始/结束标记:< bos >, < eos >,分隔符:< sep >, < pad >,任务相关标记:< user >, < assistant >, < system >, 模板内固定的 prompt 结构:比如 SFT 常见的 指令 + 分隔符 + 答案 格式里的固定 token

这些 token 在预训练和微调中,它们的预测也会参与 loss 计算(除非在 label_mask 里屏蔽掉)。

  • 训练初期常见 高一点,随后下降很快。 (训练刚开始时,模型对这些特殊符号的位置和用法还没在新数据里重新适应,因此预测准确率较低,loss 会比普通文本 token 高。但它们的模式高度固定(比如每次都 < bos > 开头, < eos > 结尾),只要模型看到几个 batch,就能学会,所以 loss 会快速下降。)
  • 若始终偏高,检查:
    • tokenizer/模板拼接是否一致; (训练数据里的特殊符号和 tokenizer 定义的 ID 对不上(比如 在数据里写成了别的形式,或者漏加了必要的 token), 可能数据准备时拼接模板用的符号和推理时用的不一致,导致模型“学的是错的格式”)
    • prompt 格式化中的特殊 token 是否被错误截断/重复; (在数据生成或预处理时,把 prompt 和答复拼接时,特殊 token 被错误截断、重复或位置错乱。例如 < bos > 被删掉,或者 < sep > 出现了两次)
    • label_mask 是否正确(不要把 prompt 指令也当成可学习标签)。(SFT 时我们一般会屏蔽 prompt 指令部分的 loss(label_mask=0),只让模型学回答部分(label_mask=1)。如果 mask 错了,把 prompt 里的特殊 token 也算进 loss,就会让模型被迫学习它们,但这些 token 在 prompt 中本应是固定输入,学它没意义,还会影响 loss 统计)

3.3 初始与最终 loss 的经验范围

  • 通用语料 + 采样均衡 条件下的初始 loss(仅经验区间):
    • 7B/13B:≈2\approx 22 左右,难数据可到 ≈3\approx 33
    • 72B:1∼21\sim 212
  • 最终 loss≈0.5\approx 0.50.5 左右常见。
    • 更低并不一定更好:从语言建模角度看,过低可能意味着过拟合/过度记忆(“只会说这一句”)。

3.4 异常:loss 持续升高怎么办?

  • 先怀疑训练代码/配置,再怀疑数据难度。
  • next-token 预测在理论上“总能背下来”,即使数据是无意义随机串,也不应该出现持续上升,最多持平
  • 排查清单(建议顺序):
    1. label_shift(是否对齐); (next-token 预测时,标签序列应该是输入序列右移一位(预测下一个 token)。如果错位了,比如预测当前 token 或者错了几个位置,模型根本学不到正确关系,loss 会一直飙升)
    2. label_mask(是否把 prompt 也算入监督标签);
    3. 学习率过大/无 warmup;
    4. 混精度溢出(loss 变 NaN 前的抖动);
    5. 数据/模板拼接错误(样本为空、越界索引等)。

3.5 关于 SFT Packing 的理性看法

Packing在上面也解释过了,这里不作解释。

  • 结论:SFT 默认不建议开(尤其是小数据/难样本)。
  • 原因(直观解释):
    1. 非 packing 时,若 batch_size=1\text{batch\_size}=1batch_size=1 且样本很短,padding 占多,但该步的 梯度几乎全来自这个短样本,因此对其拟合更强
    2. packing 把多个短样本拼成一个长序列,本步梯度被多样本稀释,短样本拟合强度变弱
  • 影响:对短 query/短答案的训练容易受损,生成的续写/对话在某些基准上会被拉低。
  • 例外:大规模数据/高吞吐场景,packing 对泛化通常无损,但依然建议实测对比

备注:如使用 Llama-Factory,一些“贪心策略/packing 相关默认开关”需手动确认,避免不知情地改变训练行为。


4. 推荐的参数“起点表”

7B/13B 级别、4K 序列长度、单任务占比不过度失衡 的通用 SFT 为例(仅起点,务必根据显存/数据分布/吞吐迭代调优)。

  • epoch: 1(垂域≤1 万条可 3
  • per_device_train_batch_size: 1(显存允许取 2)
  • gradient_accumulation_steps: 8~32(看吞吐/显存)
  • GBS: 由上式计算,保证 steps_per_epoch数千~上万步区间更稳
  • learning_rate: 3e-5(若 base 较脆、混精度不稳,可降到 1e-5
  • lr_scheduler_type: cosine
  • num_warmup_steps: 5%~10%total_steps
  • weight_decay: 0.01
  • dropout: 0
  • zero_stage: 2(弱带宽慎用 3)
  • max_seq_len: 4096(与 Base 模型一致)
  • offload:
  • gradient_checkpointing: (算力换显存)

5. 参考配置片段

5.1 Transformers TrainingArguments(Deepspeed)

training_args = TrainingArguments(
    output_dir="ckpts/sft",
    num_train_epochs=1,
    per_device_train_batch_size=1,
    gradient_accumulation_steps=16,
    learning_rate=3e-5,
    lr_scheduler_type="cosine",
    warmup_ratio=0.06,                 # ~6%
    weight_decay=0.01,
    fp16=True,                         # 或 bf16=True,看硬件
    logging_steps=10,
    save_steps=1000,
    save_total_limit=2,
    gradient_checkpointing=True,
    deepspeed="ds_zero2.json",         # ZeRO-2
    max_grad_norm=1.0
)

这一节详细介绍了SFT参数设置和训练技巧。下一节将会介绍SFT时常用的策略。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐