大模型微调面¶
全参数微调(Full Fine-Tuning)指的是什么?与PEFT的根本区别是什么?¶
全参数微调,是指在预训练模型的基础上,对模型的所有可训练参数进行更新,以适应下游任务。以 LLaMA‑7B 为例,它包含约 70 亿个参数,全参数微调意味着这 70 亿个权重全部参与前向传播、反向传播和优化器更新,没有任何参数被冻结。与之相对,PEFT(Parameter‑Efficient Fine‑Tuning,参数高效微调)仅更新极少量新增或选定的参数,而冻结绝大部分预训练权重。
两者的根本区别可以从以下六个维度深入对比:

① 参数更新范围与模型容量的利用
全参数微调把整个模型的容量都交给下游任务,能够最大程度地重塑模型的行为。例如,当预训练模型的知识与目标领域存在巨大差异(如英文模型应用于中文法律文书生成)时,全参数微调可以重新调整底层语言表征和高层语义。PEFT 则通过在冻结的基座模型上添加“适配器”(如 LoRA 的低秩矩阵),让模型在原有能力基础上进行有限的偏移。这种有限性既是一种正则化,也是一种约束——它难以彻底改变模型的底层特征。
② 显存、计算与通信开销
全参数微调的显存需求极高,因为必须存储所有参数的梯度(与参数同大小)和优化器状态(Adam 需要额外 8 字节/参数)。对于 7B 模型,仅权重、梯度和优化器状态就需要约 84 GB 显存(见第 2 题),远超单张 80 GB GPU 的容量。此外,多卡训练时的梯度同步(All‑Reduce)通信量与参数量成正比,通信开销巨大。
PEFT 则只需要为极少量可训练参数保存梯度和优化器状态。以 LoRA(r=8)为例,可训练参数不到总参数的 0.1%,显存和通信量成数量级下降,甚至可以在单张消费级 GPU 上微调 13B 模型。
③ 灾难性遗忘风险
全参数微调直接修改所有参数,当目标域数据量小、分布窄时,模型极易发生灾难性遗忘:它将预训练阶段储存的广谱知识和通用语言能力“洗掉”,转而只记住微调数据的表面模式。PEFT 则几乎不会遗忘,因为基座模型的参数完全没有变动,新任务的能力被编码在新增的极轻量模块中,预训练知识得到完整保留。
④ 训练速度与收敛性
全参数微调由于参数空间巨大,需要更谨慎的学习率、更多的训练步数和更强的正则化。收敛过程也更不稳定,容易出现梯度爆炸。PEFT 则相对稳定,收敛更快,因为优化问题被约束在一个低维子空间内。
⑤ 部署与存储
全参数微调每得到一个任务专有模型,就需要保存一份完整的大模型(7B FP16 约 14 GB)。如果有 100 个下游任务,就需要存储 1.4 TB 的模型。而 PEFT 只需为每个任务保存一个极小的适配器(几 MB),一个基座模型可以搭配数百个适配器,存储成本几乎为零。
⑥ 最终性能与泛化
全参数微调在数据和算力充足时,能够达到更高的性能天花板。但 PEFT 在许多指令微调、风格迁移等任务上表现出与全参数微调持平甚至更优的泛化性能,因为冻结大部分参数起到了极强的隐式正则化作用,防止了过拟合和遗忘。
综上所述,全参数微调可以视为“重塑大脑”,代价高、风险高,但潜力大;PEFT 则是“在大脑上外接一个可插拔的技能模块”,安全、经济、灵活。工业界在绝大多数场景下优先选择 PEFT,而全参数微调仅在需要深度重塑模型底层能力或追求极致性能且资源充沛时才被考虑。
全参数微调 70B 模型需要多少显存?给出估算过程。¶
以 LLaMA‑2‑70B(或类似稠密架构)为例,采用混合精度训练(FP16/BF16 用于前向和反向,FP32 用于优化器状态),使用 AdamW 优化器,我们来逐项估算单卡所需的显存。
- 模型权重
70B 参数,每个参数以 FP16(2 字节)存储:

在混合精度训练中,通常还会维护一份 FP32 的主权重副本用于精确更新,但这部分通常可以分摊到优化器状态中或由 ZeRO 管理,这里先不计入。
- 梯度
反向传播需要为每个参数存储与权重相同精度的梯度(FP16):

- 优化器状态 (Adam) Adam 优化器为每个参数维护两个 FP32 的矩估计(一阶动量 m 和二阶动量 v),每个占用 4 字节:

- 中间激活与临时缓冲区
激活显存取决于 batch size、序列长度、隐藏维度和模型层数。对于 70B 模型,假设 batch size=1,序列长度=2048,激活显存的经验公式约为:

带入典型值(hidden=8192,层数=80),单层激活约十几 MB,整模型激活约 200‑400 GB。具体值与是否使用梯度检查点、FlashAttention 有关。开启梯度检查点后可大幅降低激活显存至数十 GB。为了保守估算,我们取约 300 GB。
- 框架与碎片开销
PyTorch 缓存分配器、NCCL 通信缓冲区等会额外占用 10‑20 GB。
总计(无任何优化):

显然,单卡完全不可能。必须借助分布式策略(见第 3 题)。如果使用 ZeRO‑3 将模型状态分片到 8 张 GPU,则单卡显存可大幅降低。例如,权重、梯度、优化器状态各降至 1/8,单卡这三项合计约 (140+140+560)/8=105 GB,再加上激活和开销,单卡约 150‑200 GB,仍然超出 80 GB 容量。此时需要叠加张量并行(TP)进一步切分激活,最终实现 70B 全量微调。
在单卡无法容纳全参数微调时,有哪些分布式策略可用?¶
当模型过大无法装入单卡显存时,分布式训练策略将模型状态(权重、梯度、优化器)和激活值切分到多张 GPU。主要策略包括:
选择思路:一般情况下,对于 7‑13B 模型全量微调,优先尝试 ZeRO‑2 或 ZeRO‑3;若 ZeRO‑3 单卡仍无法容纳(如 70B),则叠加 TP(节点内)和 PP(节点间),形成 3D 并行。卸载通常作为最后手段。
数据并行(DP/DDP)在全参数微调中如何工作?有什么局限?¶
数据并行(Data Parallelism, DP)是最基础的分布式策略。在 PyTorch 中,DataParallel(DP)和 DistributedDataParallel(DDP)是两种主要实现,DDP 因性能更优而成为主流。
工作流程 (DDP):
-
模型复制:每个 GPU 拥有一个完整的模型副本。
-
数据划分:每个 worker 加载不同的 mini‑batch 数据。
-
前向传播:独立计算,各自得到 loss。
-
反向传播:各自计算本地梯度。
-
梯度同步:通过 All‑Reduce 操作,将所有 worker 的梯度求平均,每个 worker 得到相同的全局梯度。
-
参数更新:每个 worker 用全局梯度独立更新自己的模型参数,保证各副本一致。
局限:
-
单卡显存瓶颈:每张卡必须存放完整的模型、梯度和优化器状态。对于大模型(如 7B),即使使用 ZeRO‑2,单卡显存也往往不够,必须引入模型并行或 ZeRO‑3。
-
通信开销:All‑Reduce 通信量与参数量成正比。当卡数增多,通信时间可能成为瓶颈,需要高带宽网络(如 InfiniBand)和通信计算重叠技术。
-
负载均衡:要求每张卡上的计算量(由 batch size 和序列长度决定)相近,否则会出现短板效应。
-
无法处理超大模型:单纯增加数据并行度无法解决单卡放不下的问题,必须与张量并行、流水线并行或 ZeRO 联合使用。
因此,数据并行通常作为 扩展吞吐量 的手段,而非 降低单卡显存 的手段。
模型并行中的张量并行和流水线并行的原理分别是什么?¶
模型并行(Model Parallelism)将模型本身切分到不同设备上,以解决单卡装不下的问题。主要有张量并行和流水线并行两种方式。
张量并行 (Tensor Parallelism, TP)

流水线并行 (Pipeline Parallelism, PP)
-
原理:将模型按层深度切分成连续的 stage,每个 stage 放在不同的 GPU 上。例如,32 层的 Transformer,GPU0 负责 1‑8 层,GPU1 负责 9‑16 层,依此类推。前向时,激活从 GPU0 传至 GPU1,再传至 GPU2……反向时梯度沿原路返回。
-
消除气泡:简单的串行执行会导致 GPU 大量空闲。为减少这种“流水线气泡”,通常将一个 batch 分成多个 micro‑batch,以流水线方式注入(如 GPipe 的“先全部前向再全部反向”或 1F1B 的“交错前向/反向”)。
-
特点:PP 的通信量很小(只传输层间的隐藏状态),适合跨机扩展。但存在气泡,GPU 利用率达不到 100%,且首尾层可能因 embedding 和 lm_head 导致显存不均衡。
什么是3D并行?如何结合数据、张量、流水线并行?¶
3D 并行是将 数据并行 (DP)、张量并行 (TP) 和 流水线并行 (PP) 三者结合起来,共同作用于一个大规模训练任务上。其设计原则是根据硬件拓扑分层使用不同策略,最大化 GPU 利用率和通信效率。
结合方式:

ZeRO的三个阶段分别优化了什么?各自节省多少显存?¶
ZeRO(Zero Redundancy Optimizer)在数据并行的框架内,通过对模型状态(优化器状态、梯度、参数)进行分片,消除冗余存储。其三个阶段依次优化:
以 7B 模型、FP16 精度、Adam 优化器、数据并行度 N=8 为例,不考虑激活的情况下:
-
ZeRO‑1 将优化器状态(Adam 中的 m 和 v)分片到 N 张卡,每卡只存 1/N 的优化器状态。通信量增加约 1.5 倍(在梯度 All‑Reduce 基础上增加一次参数 All‑Gather)。
-
ZeRO‑2 额外分片梯度,每卡只存自己那部分参数的梯度,通过 Reduce‑Scatter 实现。
-
ZeRO‑3 将参数也分片,每卡只存 1/N 的完整参数。前向/反向时需要 All‑Gather 收集完整参数,用完即释放。总通信量约为标准 DP 的 1.5 倍。
由此可见,ZeRO‑3 可将模型状态显存降至原来的 1/N,使得在 8 卡 V100‑32G 上全量微调 13B 甚至 30B 模型成为可能。
使用ZeRO‑3进行全参数微调时,前向和反向传播的通信开销如何?¶
ZeRO‑3 将参数分片到所有数据并行卡上,计算时需要实时收集所需参数。这带来了额外的通信开销。设模型总参数量为 Φ,数据并行度(ZeRO group 大小)为 N。
前向传播:
对于每一层,每张卡只持有该层参数的 1/N 分片。计算该层前向之前,需要通过一次 All‑Gather 将所有卡上的参数分片收集成全量参数副本。计算完成后,本地立即释放非自己分片的参数,以控制显存。因此,前向每层额外通信量为该层参数量,整个前向的通信量为 总参数量 Φ。
反向传播:
反向时,同样需要先 All‑Gather 该层的完整参数(通信量 Φ),计算该层梯度。然后执行一次 Reduce‑Scatter 操作,将各卡计算出的部分梯度按分片规则聚合,每张卡只得到自己负责的那部分参数的完整梯度。Reduce‑Scatter 的通信量也为 Φ。因此,反向每层总通信量为 2Φ。
总通信量 = 前向 Φ + 反向 2Φ = 3Φ。
作为对比,标准数据并行(无 ZeRO)只在反向传播结束时对梯度进行一次 All‑Reduce,通信量为 2Φ。因此,ZeRO‑3 的通信量是标准 DP 的 1.5 倍。
此外,ZeRO‑3 的通信是逐层发生的,会产生大量的小数据量通信,更容易受到网络延迟的影响。为了减轻这一影响,通常需要:
-
使用高带宽、低延迟的网络(如 InfiniBand)。
-
开启通信与计算重叠(如 DeepSpeed 的
overlap_comm)。 -
通过 bucketing 将小梯度合并后再通信(
reduce_bucket_size等参数)。
DeepSpeed ZeRO和FSDP的主要区别是什么?如何选择?¶
DeepSpeed ZeRO (由 Microsoft 开发) 和 FSDP (Fully Sharded Data Parallel,PyTorch 原生) 都实现了模型状态的完全分片(等同于 ZeRO‑3),其核心思想相同,但在实现、功能和生态上存在差异。
如何选择?
-
如果你的模型规模极大(>100B),或者需要CPU/NVMe offload来进一步节约显存,或者要训练 MoE 模型,DeepSpeed ZeRO 几乎是唯一选择。
-
如果你追求简洁、稳定、与 PyTorch 生态的高兼容性,且模型规模适中(<70B),FSDP 更易上手,调试也更方便。尤其在与
torch.compile配合时,FSDP 有时能获得额外的性能提升。 -
如果团队对 PyTorch 更熟悉,希望尽量减少外部依赖,FSDP 是理想选择;如果已经使用 DeepSpeed 的其他功能(如 Autotuning),ZeRO 的集成性更好。
实践中,很多团队从 FSDP 开始尝试,若遇到显存瓶颈或需要 offload 时,再迁移到 DeepSpeed ZeRO。两者核心性能差距不大,选择更多取决于团队偏好和特定需求。
在全参数微调中,如何处理梯度累积?为什么需要使用它?¶
梯度累积(Gradient Accumulation)是一种“以小博大”的训练技巧,允许我们在显存受限的情况下模拟出更大的全局批量大小(Global Batch Size)。其原理简单却极其有效:不是每个micro‑batch后都立即更新模型参数,而是连续计算多个micro‑batch的梯度,将它们累积求和,直到累积的步数达到预设值,再用累积的总梯度一次性更新参数。
处理步骤
-
确定目标全局Batch Size:根据训练稳定性和收敛经验,设定一个理想的全局Batch Size(例如128)。
-
确定单卡最大Micro‑batch Size:受限于GPU显存,测试出模型能承受的最大单次输入样本数(例如8)。
-
计算累积步数:
accumulation_steps = global_batch_size / (micro_batch_size * num_gpus)。例如,单机8卡,micro_batch_size=8,目标全局128,则累积步数 = 128 / (8*8) = 2。 -
训练循环实现:
- 每个micro‑batch前向、反向,得到梯度,不调用
optimizer.step()。 - 将梯度累加到模型参数的
.grad属性中(PyTorch会自动累加,除非手动清零)。 - 当处理了
accumulation_steps个micro‑batch后,执行一次optimizer.step(),然后optimizer.zero_grad()清零梯度,完成一次真正的参数更新。 - 注意:loss通常要除以累积步数,以保持每次更新的有效学习率一致。
为什么需要使用梯度累积?
-
突破显存瓶颈:这是最直接的原因。全参数微调需要为所有参数存储梯度、优化器状态和中间激活,显存消耗极大。通过梯度累积,我们不必为了追求大Batch Size而增大micro‑batch,从而将单次前向的激活和临时缓冲区控制在显存允许的范围内。
-
维持大Batch训练效果:很多研究表明,过小的Batch Size会导致梯度噪声过大,训练不稳定,收敛困难。大Batch Size能提供更准确的梯度估计,有助于稳定收敛和提升最终性能。梯度累积让我们在有限的硬件上也能享受到大Batch Size的好处。
-
支持高分辨率或长序列输入:当任务需要处理长文本或高分辨率图像时,micro‑batch size可能被迫设为1或2。若不使用梯度累积,有效Batch Size会极小,模型几乎无法学习。通过累积数十步,可以恢复正常的训练动态。
-
模拟更大的分布式规模:在单机或少量GPU上调试时,可以使用梯度累积来模拟未来在多机多卡上的大Batch行为,提前发现训练不稳定等问题。
注意事项
-
Batch Normalization的兼容性:梯度累积打破了BN层跨micro‑batch的统计,因为BN通常在每个micro‑batch内独立计算均值/方差。对于LLM常用的LayerNorm,没有这个问题;若使用了BN,需要特殊处理(如SyncBN)。
-
优化器状态更新频率:Adam等优化器的一阶/二阶矩估计依赖于每次更新的梯度。梯度累积减少了更新频率,但累积的梯度量级与单步大Batch的梯度一致,因此优化器动态基本等价。
-
梯度裁剪时机:应在累积完成后、
optimizer.step()之前进行梯度裁剪,裁剪累积后的总梯度范数。
梯度检查点(Gradient Checkpointing)的原理是什么?对微调速度影响多大?¶
梯度检查点(Gradient Checkpointing)是一种“用时间换空间”的技术。在全参数微调中,显存的一大杀手是中间激活值(activations)——前向传播时每一层的输出都需要保留,供反向传播计算梯度。梯度检查点通过选择性丢弃部分中间激活,并在反向传播时重新计算它们,从而将显存峰值大幅降低,代价是增加了额外的计算量。
原理
-
前向传播时,不保存所有层的完整激活,而是将网络划分为若干个“检查点段”(通常每个Transformer Block就是一个段)。在每个段的边界,强制保存该段的输入张量(即检查点),而段内各层的中间结果全部丢弃。
-
反向传播时,当需要计算段内某一层的梯度时,利用之前保存的检查点输入,重新执行一次该段的前向计算,临时恢复出所需的中间激活,然后立即计算梯度并释放这些临时激活。
-
这样,显存中不再需要同时存储整个模型的激活,而只需存储每个检查点的输入(通常很小)以及当前正在处理的段的临时激活。显存复杂度从 O(n) 降到 O(√n) 或更低。
对微调速度的影响
梯度检查点会显著增加计算量,从而降低训练速度。具体影响取决于检查点的粒度:
-
如果每个Transformer Block作为一段,那么反向传播时每个Block都需要重计算一次前向。这意味着额外的前向计算量约等于整个模型的一次前向,因此总训练时间大约增加 25% ~ 30%。
-
如果使用更粗粒度的分段(如每两个Block一个检查点),则额外计算量减少,但节省的显存也会减少。
-
在实际微调中,尤其是结合FlashAttention等高效注意力实现,梯度检查点的额外开销往往被部分掩盖,因为重计算过程中的矩阵乘法依然高度并行。但无论如何,它是对训练吞吐量有明显影响的操作。
使用建议
-
全参数微调时,梯度检查点几乎是必选项,尤其是当序列长度较长或batch size较大时。否则激活显存轻易就会撑爆80GB的A100。
-
可通过权衡:如果显存充足,可适当减少检查点粒度(如每两个Block一个检查点)以提升速度;如果显存紧张,则必须细化到每个Block甚至更细(如选择性检查点,仅对注意力层做重计算)。
-
它与FlashAttention、序列并行等技术协同,可将显存峰值压到最低。
全参数微调典型的学习率是多少?如何确定合适的学习率?¶
典型学习率范围
对于7B~13B级别的稠密模型,全参数微调的学习率通常落在 5e-6 到 2e-5 之间。更具体地:
-
模型规模越大,学习率通常应越低。例如,7B模型可以尝试1e-5,而65B模型可能需降至5e-6。
-
数据量少、领域狭窄时,学习率应更低,以防止过拟合和遗忘。
-
如果基座模型是经过SFT的对话模型(如Llama-2-Chat),学习率应比微调预训练基座模型更低,因为对话模型已经具有特定的行为分布,需要更保守的更新。
相比预训练阶段动辄1e-4甚至更高的学习率,微调学习率通常低1到2个数量级。这是因为微调是“精雕细琢”,而不是“大开大合”。
如何确定合适的学习率?
-
经验法则:从预训练最大学习率的1/10或1/20开始。例如Llama-2预训练学习率为3e-4,则微调可以从1.5e-5开始尝试。
-
学习率范围测试(LR Range Test):这是最科学的方法。从一个极小值(如1e-7)开始,每个mini‑batch后将学习率线性增加,直到损失开始上升或震荡。绘制“学习率-损失”曲线,选择损失下降最快、最平稳区间中点的学习率。通常这个点位于曲线最低点左侧约一个数量级处。
-
考虑优化器:使用AdamW时,学习率相对于SGD更稳健,但过大仍会导致遗忘。若使用Lion优化器(学习率通常为AdamW的1/3~1/10),则要相应地调低。
-
分层学习率:底层使用更低的学习率,顶层使用稍高的学习率。例如,对Embedding层和浅层Transformer使用1e-6,对深层使用5e-6。这有助于保护通用语言知识。
-
与Batch Size联动:通常Batch Size增大,学习率也可适当调高(线性缩放规则)。
关键监控:在正式训练前,用小规模实验(例如10%数据)快速测试几个候选学习率,观察验证损失和通用基准(如MMLU)的变化趋势,选择既能快速降低损失又不损害通用能力的最高学习率。
解释“warmup ratio”在全参数微调中的作用及常见设置。¶
Warmup(学习率预热) 是在训练开始时,让学习率从一个极小值(通常为0或接近0)线性增加到设定的目标学习率的过程。Warmup ratio指的是预热步数占总训练步数的比例。
作用
-
稳定训练初期:训练刚开始时,模型参数处于预训练后的优质点,但梯度的方向和大小可能极不稳定。若直接用满额学习率,最初的几步更新可能会对参数产生巨大扰动,破坏预训练成果,导致损失飙升甚至训练崩溃。Warmup通过逐步增大学习率,让模型在最初阶段平滑过渡。
-
给优化器状态“热身”:Adam等自适应优化器需要积累梯度的一阶/二阶矩估计。在训练初期,这些估计是极度不准确的。Warmup提供了一个缓冲期,使优化器能收集足够的梯度统计,从而在后续训练中做出更合理的自适应步长调整。
-
缓解早期过拟合:在极小数据量上,Warmup可视为一种正则化,防止模型在最初几步“死记”住先看到的几个batch。
常见设置
-
对于全参数微调,warmup ratio通常设置为总训练步数的 5% ~ 10%。例如,总步数为2000步,warmup步数设为100~200步。
-
如果使用LoRA等PEFT,由于可训练参数少,训练更稳定,warmup可以更短(如1%~3%)。
-
若基座模型与目标领域差异巨大,可适当增加warmup比例,让模型更平缓地适应新数据。
-
常用的学习率调度器是“Warmup + Cosine Decay”。在Warmup阶段,学习率从0线性增至峰值;之后按照余弦曲线衰减至接近0。
实践建议:通常不需要把warmup作为一个单独的超参去精细调优,设定一个安全的默认值(如10%)即可。如果发现训练初期损失震荡剧烈,可以适当增加warmup;如果初期损失下降很快且平稳,可以减少warmup以释放更多步数用于真正的学习。
全参数微调时batch size如何影响训练稳定性和收敛?¶
Batch size(BS)通过控制梯度估计的噪声水平,深刻影响着全参数微调的稳定性和收敛行为。
一、小Batch Size(高噪声)
-
优点:梯度噪声大,起到了隐式正则化的作用,有助于模型跳出尖锐的局部极小点,找到更宽泛、泛化能力更好的解。对于SFT这类数据量通常不大的任务,小BS往往能带来更好的最终泛化性能,输出更多样。
-
缺点:损失曲线震荡剧烈,训练不稳定;需要更小的学习率;收敛速度慢(因为每个step处理的数据少);可能难以充分利用GPU的并行计算能力。
二、大Batch Size(低噪声)
-
优点:梯度估计准确,损失下降平滑,训练过程稳定;能够充分利用GPU算力,提高训练吞吐量;可以使用更大的学习率(线性缩放规则),加快收敛。
-
缺点:容易收敛到训练数据附近的尖锐极小值,导致过拟合,泛化能力差;模型输出趋于“平均化”,丧失多样性;对于SFT这种数据分布较窄的任务,大BS会加剧灾难性遗忘。
对微调的影响与选择
全参数微调时,由于数据量通常有限,我们倾向于选择中等偏小的全局Batch Size(如32~128)。具体选择需综合考虑:
-
显存限制:micro‑batch size受限于单卡显存,结合梯度累积实现目标全局BS。
-
数据量:数据越少,BS应越小,以增加噪声防止过拟合。
-
任务难度:复杂任务(如数学推理)需要更准确的梯度,可适当增大BS。
-
学习率联动:若增大BS,可尝试按平方根或线性比例提高学习率。
-
监控指标:在训练过程中观察验证损失和生成多样性。若验证损失下降后快速反弹,说明BS可能偏大;若训练损失震荡剧烈,可能BS太小。
推荐起步配置:对于7B模型,单卡micro‑batch size=4或8,配合梯度累积至全局BS=64或128。然后根据上述信号微调。
什么是梯度裁剪?全参数微调中阈值一般设为多少?¶
梯度裁剪(Gradient Clipping) 是一种防止梯度爆炸、保障训练稳定性的技术。它通过设定一个阈值,当所有参数梯度的总范数(通常用L2范数)超过该阈值时,就强制将梯度向量缩放到该阈值,从而限制了单次参数更新的最大幅度。
为什么需要梯度裁剪?
在全参数微调中,尤其是训练初期,模型面对新的数据分布,某些参数(尤其是高层或注意力层)的梯度可能异常巨大。这种巨大的梯度如果直接用于参数更新,会导致参数发生剧烈跳变,瞬间破坏预训练阶段辛苦建立的宝贵表征,引发损失飙升至NaN或Inf,使训练崩溃。即使不崩溃,过大的梯度也会导致训练不稳定,收敛缓慢。梯度裁剪相当于给每次更新加上了一个“安全帽”。
阈值设定
-
常见范围:全参数微调中,梯度裁剪的阈值(
max_grad_norm)通常设为 1.0。 -
这个值适用于大多数基于Transformer的语言模型微调。它既有效防止了梯度爆炸,又不会过度限制正常的学习信号。
-
若使用混合精度训练(FP16),由于FP16的表示范围窄,更容易出现梯度溢出,因此裁剪尤为重要,阈值可保持不变或略微降低(如0.5)。
-
如果训练中频繁触发梯度裁剪(例如日志中
grad_norm频繁等于所设阈值),意味着学习率可能过大,或数据存在问题,需要排查。 -
对于PEFT(如LoRA),由于可训练参数少,梯度爆炸风险相对较低,裁剪阈值可适当提高(如5.0或10.0),或甚至不裁剪。
实践:在HuggingFace Trainer中,通过--max_grad_norm 1.0设置。通常不需要将其作为主要调参对象,默认值在绝大多数情况下工作良好。
全参数微调中,优化器选择AdamW还是Lion?各有什么优劣?¶
在全参数微调中,AdamW 是久经考验的默认选择,而 Lion 是近两年崛起的新秀,以其显存节约和有时更优的性能受到关注。
AdamW
- 优势:
- 稳定性极高:自适应学习率机制对学习率选择不敏感,训练过程平滑。
- 广泛验证:几乎所有大模型(LLaMA、GPT等)的预训练和微调都使用AdamW,社区经验丰富,超参设置成熟。
-
性能可靠:在绝大多数任务上都能提供稳定且优秀的最终性能。
-
劣势:
- 显存占用大:需要为每个参数存储一阶动量
m和二阶动量v(FP32),显存开销是权重的8倍(相对于FP16权重)。 - 计算开销稍高:每步更新需要计算两次矩估计。
Lion
- 优势:
- 显存节约:Lion只维护一阶动量(通过指数移动平均),并且符号更新机制使其所需的优化器状态可以更小(理论上可以做到AdamW的一半)。实践中,使用Lion通常可节省约30%~50%的优化器显存。
- 训练速度更快:由于更新计算更简单,且有时可以使用更大的学习率,训练吞吐量可提高。
-
在某些任务上泛化性更好:Lion论文和部分实践发现,它在一些语言模型微调任务上比AdamW有更低的验证损失和更好的泛化性。
-
劣势:
- 超参敏感:Lion对学习率和Batch Size的选择非常敏感,调参难度大于AdamW。
- 经验较少:虽然在大模型预训练上取得成功,但在全参数微调(尤其是小数据量)的成熟经验不如AdamW丰富,默认配置可能不总是最优。
- 可能需要不同的Warmup和正则化策略。
如何选择?
-
保守可靠之选:使用 AdamW。它几乎不会出错,是快速迭代和原型验证的首选。
-
追求显存极限或前沿性能:当显存成为瓶颈(例如要微调70B模型),或者希望探索潜在更好性能时,可以尝试 Lion。使用Lion时,需参考其论文推荐的学习率(通常为AdamW的1/3~1/10),并进行充分的超参搜索。
-
混合使用:有些团队在预训练阶段使用Lion,微调时切换回AdamW,以利用各自优势。
目前,全参数微调的主流仍然是AdamW,Lion作为一种有潜力的替代方案正逐渐被更多人采用。
全参数微调如何使用余弦退火学习率调度?有什么好处?¶
余弦退火(Cosine Annealing)是一种学习率衰减策略,它让学习率按照余弦函数曲线从初始值平滑下降到预设的最小值(通常接近0)。在微调中,它通常与Warmup结合使用,形成“Warmup + Cosine Decay”的标准范式。
使用方法
在训练开始时,经过一个较短的Warmup阶段(学习率从0线性增加到峰值 lr_max)。之后,学习率按照下式衰减:
lr(t)=lrmin+12(lrmax−lrmin)(1+cos(t−twarmupT−twarmupπ))lr(t)=lrmin+21(lrmax−lrmin)(1+cos(T−twarmupt−twarmupπ))
其中 T 是总训练步数,t 是当前步数,lr_min 通常是 lr_max 的 1/10 或更小(甚至为0)。
在HuggingFace Trainer中,设置 --lr_scheduler_type cosine 即可。
好处
-
平滑收敛,避免后期震荡:在训练后期,学习率以缓慢的速度衰减,允许优化器以越来越小的步长精细地探索损失曲面,最终平稳收敛到较优的局部极小点。相比阶梯式衰减或恒定学习率,余弦退火避免了后期因学习率过大而产生的震荡。
-
更好的泛化性能:研究发现,余弦退火能够帮助模型逃离尖锐极小值,收敛到更宽泛、泛化能力更好的区域。这在SFT中尤为重要,因为微调数据有限,泛化能力是核心追求。
-
易于设定,超参少:只需设定初始学习率和最终学习率,衰减过程自动完成,无需像阶梯衰减那样指定衰减步数和衰减因子。
-
可配合Warmup形成稳定启动:Warmup解决了训练初期的稳定性,余弦退火解决后期的精细收敛,两者结合覆盖了训练的完整生命周期。
-
允许“重启”变体:余弦退火还可以与热重启(Cosine Annealing with Warm Restarts)结合,周期性将学习率重置为高值,以跳出局部最优,但这在标准SFT中较少使用。
实践:对于微调,通常设置 lr_max 为选定的学习率,lr_min 为 lr_max 的 1/10 或 1/100。训练结束时学习率几乎为零,确保模型不会在最后几步“走过头”。
全参数微调时,如何设置weight decay以控制过拟合?¶
Weight decay 是L2正则化的实现,通过在损失函数中增加权重平方和的惩罚项,限制模型参数的大小,防止过拟合。在全参数微调中,由于数据量通常较小,weight decay是控制过拟合的关键杠杆之一。
如何设置?
-
与预训练保持一致或略高:大多数预训练模型(如LLaMA)使用0.1的weight decay。微调时,可沿用此值,或根据数据量略作调整。数据量越小,weight decay应越大(如0.2甚至0.5),以施加更强的正则化。
-
避免对bias和LayerNorm施加:标准的weight decay应只应用于权重矩阵(线性层的weight),而不应用于bias和LayerNorm的参数。这些参数通常不需要正则化,强行衰减反而可能影响性能。PyTorch的AdamW已经内置了这一区分。
-
动态调整:在训练过程中,如果发现验证损失在某个点后开始上升(过拟合迹象),可以尝试增大weight decay;如果模型在训练集上都无法收敛(欠拟合),则需减小weight decay。
-
与学习率协同:更高的学习率往往需要更大的weight decay来平衡。当尝试不同学习率时,可保持weight decay与学习率的比例相对稳定。
过大或过小的后果
-
过大(如1.0):会过度限制模型容量,导致模型无法充分学习微调任务,表现为训练损失和验证损失都居高不下,模型输出平庸、重复,甚至遗忘预训练知识。
-
过小(如0或0.001):正则化不足,模型极易在微调数据上过拟合,表现为训练损失极低但验证损失上升,泛化能力差,输出变得模板化。
推荐起步值:对于全参数微调7B模型,weight decay=0.1 是一个安全、广泛验证的起点。如果数据量小于5000条,可以尝试0.2;如果数据量超过10万条,可降至0.05。始终在一个独立的验证集上监控困惑度,必要时调整。
微调过程中遇到loss spike怎么办?可能是什么原因?¶
Loss spike(损失尖峰) 是指训练过程中损失突然急剧上升,然后又迅速回落,或直接导致训练崩溃。它是不稳定性的直接表现,原因多样。
可能原因
-
学习率过大:这是最常见的原因。高学习率导致参数更新步幅过大,跳入了损失曲面的高损区域,甚至引发梯度爆炸。
-
数据异常:某些batch中包含“坏样本”,如超长文本、格式完全错误、或含有大量噪声/乱码的样本。这些样本产生异常巨大的梯度。
-
数值不稳定:混合精度训练(FP16)时,梯度可能溢出或下溢,导致NaN或Inf。如果没有Loss Scaling或Loss Scaling值过大,也容易出现。
-
梯度累积中的问题:累积步数设置不当,导致实际有效学习率发生变化;或者累积的梯度范数过大,在更新时产生剧烈扰动。
-
模型状态问题:某个优化器状态(如Adam的二阶矩)在训练初期不稳定,或由于稀疏特征导致某些参数梯度异常。
-
硬件问题:GPU内存错误、通信超时等也可能导致瞬间计算错误。
应对措施
-
立即启用/调整梯度裁剪:确保
max_grad_norm设为合理值(如1.0),它能有效截断异常大梯度,是防止Spike的第一道防线。 -
降低学习率:如果Spike频繁发生,说明当前学习率逼近不稳定边界。将学习率减半重试。
-
检查数据质量:定位发生Spike的batch,查看其中的样本,移除异常样本或对其进行修正。
-
增加Warmup步数:如果Spike发生在训练初期,说明模型适应新数据的过程过激。延长Warmup阶段。
-
检查混合精度设置:若使用FP16,确保开启了Loss Scaling(动态或静态)。若硬件支持,强烈建议改用BF16,它不需要Loss Scaling,从根本上避免FP16的溢出问题。
-
保存checkpoint:在训练过程中频繁保存,一旦Spike导致模型退化,可回滚到最近的健康检查点继续训练。
全参数微调为什么容易导致灾难性遗忘?从优化角度解释。¶
灾难性遗忘(Catastrophic Forgetting) 是指模型在学习新任务时,几乎完全丧失在旧任务(此处指预训练)上获得的知识和能力。从优化角度,这源于微调过程中的参数更新过度、且无差别地修改了对旧知识至关重要的权重。
优化视角的深入解释
-
参数空间的单向牵引:预训练后,模型参数 θ 处于一个能够同时表征海量语言规律的“盆地”中。这个盆地是预训练损失曲面上一个宽泛且泛化性好的极小值区域。微调时,我们使用新的SFT数据的损失 LnewLnew 来更新参数。梯度 g=∇θLnewg=∇θLnew* 强力地将参数拉向SFT数据分布所对应的狭窄区域。由于全参数微调更新所有参数,这种拉力是全局的,没有对旧知识的重要参数进行任何保护。
-
损失景观的扭曲:旧任务的损失曲面和新任务的损失曲面通常是两个不同的、可能有部分重叠的“谷”。直接沿着新任务的梯度下降,很可能使参数快速离开旧任务所在的谷底,进入一个虽然新任务损失低、但旧任务损失极高的区域。这就是旧知识的遗忘。
-
Fisher信息与参数重要性:不同参数对旧任务的重要性不同。有的参数承载着基础语法知识,其Fisher信息值极高,任何微小改动都会极大影响旧任务性能。全参数微调以相同的学习率更新所有参数,没有对高Fisher信息的参数施加更小的更新阻力。因此,这些“重要的”参数被轻易修改,导致了遗忘。
-
持续梯度更新的累积效应:即使每次更新的步长不大(小学习率),但在多个epoch的训练中,这些微小更新的累积效应也会将参数推离原盆地。由于SFT数据量小、分布窄,参数最终收敛到一个围绕SFT数据的尖锐极小点,丧失了预训练模型泛化性好的宽谷特性。
缓解的优化手段:正则化(EWC根据Fisher信息约束重要参数)、PEFT(冻结大部分参数)、小学习率+早停、混合预训练数据(经验回放)等,本质上都是在优化过程中施加约束,保护参数空间中那些承载着旧知识的重要维度不被过度修改。
如何通过混合预训练数据缓解灾难性遗忘?混合比例如何确定?¶
混合预训练数据(也称为经验回放或数据重放)是一种简单却极其有效的缓解灾难性遗忘的方法。它通过在微调过程中定期或持续地重放预训练阶段见过的通用文本,提醒模型“不要忘记你是谁”。
实现方式
-
从预训练语料库(如维基百科、书籍、高质量网页等)中随机抽取一个子集,构建回放数据集。
-
在SFT的每个训练batch中,除了SFT样本外,还以一定比例混入这些通用文本。这些文本直接作为标准的自回归语言建模任务:输入文本片段,模型预测下一个token,计算交叉熵损失。
-
通用文本的损失和SFT任务的损失可以直接相加,联合优化。
为什么有效?
从优化角度看,SFT数据的梯度将参数拉向新任务的狭窄区域,而通用文本的梯度则提供了指向预训练分布广袤区域的“修正力”。两者共同作用,使得参数的更新方向不是纯粹沿着SFT梯度,而是两者的加权平均。这相当于在损失曲面上施加了一个指向预训练盆地的“锚”,防止参数完全脱离旧知识的谷底。
混合比例的确定
-
典型范围:预训练数据的比例通常占每个batch的 5% ~ 20%。
-
全量微调时:遗忘风险高,建议比例取 10% ~ 15%。例如,一个batch有128条序列,其中约13~19条是通用文本,其余为SFT数据。
-
PEFT时:遗忘风险低,比例可降至 1% ~ 5%,甚至可不混。
-
依据遗忘程度动态调整:先尝试用10%进行一轮微调,评估模型在通用基准(如MMLU)上的得分变化。如果通用能力下降超过2个百分点,则提高比例至15%或20%;如果SFT任务学习变慢,则适当降低比例。
-
数据质量:混合的预训练数据必须经过筛选,确保高质量、无有害内容,且与目标领域有一定的距离,以避免引入新的偏差。
-
平衡SFT任务学习:比例过高会稀释SFT信号,导致微调目标学习不充分。可以通过监控SFT验证集损失和通用基准,找到两者兼得的平衡点。
实践技巧:不必每个epoch都使用相同的回放数据。可以定期更新回放数据集,以覆盖更广的预训练分布,增强锚定效果。
解释EMA(Exponential Moving Average)在微调中保持稳定性的作用。¶
在神经网络训练中,EMA 指的是对模型参数的历史值进行指数加权平均,生成一个平滑版本的模型。在微调中,EMA 的作用类似于给模型的参数更新加上一个“惯性滤波器”,能够显著提升最终模型的泛化能力和鲁棒性。

其中 α 是衰减率(通常设为 0.999 或 0.9999)。这意味着 EMA 参数是最近多次迭代参数的加权平均,近期参数的权重更大。
在微调中保持稳定性的作用:
-
平滑优化轨迹,抑制噪声:微调数据量小,梯度估计噪声大,参数更新往往在局部剧烈震荡。EMA 将这种高频抖动过滤掉,使模型参数沿着一条更平滑的路径演进。最终得到的 EMA 模型通常位于损失曲面中更宽泛、更平坦的区域,这类区域的泛化性能更好。
-
防止过拟合和灾难性遗忘:微调容易使参数过度适应新数据的特定模式(尖锐极小值),而 EMA 模型由于是历史平均,它在某种程度上“保守”地保留了部分预训练知识,不易被个别极端样本带偏。实验表明,使用 EMA 的模型在微调后,其通用基准(如MMLU)的分数下降幅度更小。
-
提升最终模型性能:在微调的最后阶段,直接使用 EMA 参数作为最终模型,往往比使用最后一步训练得到的参数(可能已经过拟合)具有更高的验证集准确率和更好的生成质量。许多大型微调项目(如 LLaMA-2 的微调)都会在训练结束后额外进行几轮 EMA 平滑,或在整个训练过程中维护 EMA 模型并以此作为最终交付物。
-
降低对学习率调参的依赖:由于 EMA 天然具有保守性,即使学习率设置得稍大,EMA 模型也能保持相对稳定,这降低了微调时调参的压力。
实践方式:可以在训练过程中维护一份 EMA 参数(不参与梯度计算),在保存 checkpoint 时保存 EMA 版本。PyTorch 中可用 torch.optim.swa_utils.AveragedModel 来实现 EMA。在 HuggingFace Trainer 中,可以通过回调函数自定义 EMA 更新逻辑。衰减率 αα 通常设为 0.999 或 0.9999,更新频率可以每个 step 都更新,也可每若干步更新一次。
局限:EMA 会占用额外一份与模型同等大小的显存来存储 EMA 参数,这对全参数微调本已捉襟见肘的显存是一种奢侈。因此,在显存极度受限时,可能会选择只在训练结束前的最后几千步才启用 EMA。
EWC(弹性权重巩固)在全参数微调中如何实现?有何局限?¶
EWC 是一种经典克服灾难性遗忘的方法。其核心思想是:在学习新任务时,对旧任务“重要”的参数施加更大的更新阻力,相当于给这些参数系上了“弹簧”,强行将它们拉向旧任务的最优值附近。
全参数微调中的实现步骤:
- 计算参数重要性:在开始微调前,用预训练模型在一组代表性数据(可以是预训练数据的一小部分,如维基百科片段)上做一次前向传播。对于每个参数 θiθi,计算其 Fisher 信息矩阵的对角线近似值 FiFi。FiFi 代表了该参数对旧任务损失函数的敏感度,即其重要性。通常只取对角线元素,因为完整 Fisher 矩阵存储开销极大。

其中 λλ 是一个超参数,控制遗忘抑制的强度。这个惩罚项迫使模型在新任务上学习时,不能过度修改那些对旧任务至关重要的参数。
- 训练:使用这个修改后的损失进行标准梯度下降。在反向传播时,除了新任务的梯度,还要加上 EWC 惩罚项的梯度。
局限:
-
Fisher 信息矩阵的局限性:使用对角线近似假设参数之间相互独立,忽略了参数间的协方差,这在实际中往往不成立,导致重要性估计粗糙,抑制效果不够精准。
-
“旧任务”的代表性数据难以选择:计算 Fisher 信息需要用到旧任务数据。在全参数微调中,“旧任务”是庞大的预训练语料。我们不可能使用全部预训练数据,只能抽取一小批作为代理。这批次数据的分布是否能代表整个预训练分布,直接决定了 EWC 的有效性。选择不当则可能保护了不重要的参数,遗漏了真正关键的知识。
-
巨大的存储和计算开销:需要存储与模型参数量相当的 Fisher 值(例如 70B 模型需额外约 280 GB 显存放 FP32 Fisher 值),这在大模型微调时几乎不可能。即使使用对角线近似,依旧是一笔不小的显存开销。同时,Fisher 信息的计算也需要额外的算力和时间。
-
任务增量学习的设定不完全匹配:EWC 原本设计用于连续学习多个不同任务,而 LLM 的微调更像是将模型从一个通用域适配到一个特定域,两者之间有较大重叠。生硬地保护所有预训练参数,可能阻碍模型学习新领域所必需的参数调整。
-
λ 的调节困难:λ 过大则模型拒绝学习新任务;λ 过小则遗忘抑制效果甚微。找到一个合适的 λ 需要大量实验,成本高昂。
鉴于上述局限,EWC 在现代大模型微调中并不常用。工业界通常选择更简单高效的 PEFT(如 LoRA)或数据回放(混合预训练数据)来避免灾难性遗忘。
全参数微调一般训练多少个epoch?为什么不宜过多?¶
全参数微调的训练 epoch 数通常极少,以 1~3 个 epoch 为常见,1~2 个 epoch 最为普遍。许多开源对话模型(如 Alpaca、Vicuna)的 SFT 阶段只用 1 个 epoch。这绝非因为算力不足,而是由全参数微调的内在风险决定的。
为什么不宜过多?
-
过拟合风险极高:SFT 数据量(通常几千到几万条)相比预训练数据(数万亿 token)是九牛一毛。在这少量数据上反复训练,模型会迅速从“学习一般性的指令格式”变为“背诵特定问答对的表面模式”。其表现是:训练集损失持续下降,但验证集损失和生成多样性急剧恶化。
-
灾难性遗忘加剧:每一个额外的 epoch,都在用 SFT 数据的梯度强力地将所有参数拉向这个狭窄的数据分布。预训练阶段存储的广博世界知识、推理能力、多语言能力等,会在一次次参数更新中被不可逆地覆盖。epoch 数越多,通用能力下降越严重。
-
分布坍缩与多样性丧失:过多样本和过强训练导致模型输出概率分布变得极其尖锐,只集中在少数 SFT 数据中常见的高频模式上。模型丧失创造力和风格迁移能力,对所有问题都给出千篇一律、安全但缺乏灵魂的模板化回答。
-
优化的边际效益递减:在第一个 epoch 结束时,模型通常已经学会了基本的指令跟随格式和行为规范。后续 epoch 带来的性能提升非常微小,但代价(遗忘、过拟合)却急剧增大。这个“性价比”极低。
实践中的控制:
-
严格早停(Early Stopping):监控独立验证集的损失。当验证损失不再下降甚至开始上升时,立即停止训练,无论当前处于第几个 epoch。
-
配合小学习率:使用极低的学习率(如 5e-6),减缓参数更新幅度,相对而言可以多训练一点(例如 2~3 epoch),但风险仍在。
-
定期评估通用基准:每个 epoch 结束后,在一个固定的通用评测集(如 MMLU 的子集)上评估模型能力。若发现分数显著下降,说明遗忘已经发生,应回滚到之前的最佳 checkpoint。
-
数据多样性是根本:如果 SFT 数据本身就具有极高的多样性和覆盖面(成千上万种任务类型),那么模型可以在较多个 epoch 中持续获益。此时的 epoch 数可以适度增加到 2~3。
总结:全参数微调就像在珍贵的古董上修复细节——只能轻轻描摹,不能反复刮擦。1~2 个 epoch 是保护预训练知识、同时学会新格式的最安全区间。
如何判断全参数微调已经过拟合?有哪些指标可以监控?¶
过拟合(Overfitting)是全参数微调的头号杀手。判断过拟合需要多维度监控,单一指标往往不足以揭示全部问题。
核心指标与监控方法:
监控策略:
-
在训练过程中设置固定的评估点(如每100步),保存当前 checkpoint,然后自动运行上述评估。绘制验证损失曲线、MMLU 得分曲线等。
-
使用早停机制:当验证损失在连续 N 个评估点(耐心值)内没有改善时,自动停止训练,回滚到验证损失最低的 checkpoint。
-
A/B 抽样比较:定期用当前模型和基座模型对同一批指令生成回答,由人工或强模型(如 GPT-4)进行盲评,直接对比泛化能力的变化。这比自动指标更贴近真实体验。
-
保留独立的“挑战集”:包含需要深度推理、长上下文、跨语言等复杂指令。这些任务对过拟合最敏感,一旦模型在这些任务上表现骤降,说明其通用智能受损。
行动准则:一旦发现验证损失上升 + 通用能力下降,应立即停止训练,并回退到之前的 checkpoint。永远不要等到训练损失开始上升,因为那通常为时已晚。
微调过程中验证损失下降但实际生成质量变差,为什么?¶
这个矛盾现象根源在于 交叉熵损失作为训练代理指标的局限性。它能衡量模型对标准回答 token 的预测准确度,却无法捕捉生成质量的多维性(如逻辑性、创造性、安全性、上下文一致性)。当验证损失下降而生成质量变差时,通常发生了以下情况:
-
过拟合与模板化记忆:模型死记硬背了训练集中的高频句式和词汇。验证集与训练集分布相似(通常来自同一来源),模型靠“背诵”即可降低验证损失。但在真实开放域生成中,面对稍微变化的指令,它只会机械套用这些模板,导致回答虽然语法正确但内容空泛、逻辑断裂。
-
分布坍缩:验证损失下降伴随着模型输出概率分布的极端尖锐化。模型对某几种“安全回答模式”变得极度自信,完全压制了其他可能的生成路径。验证损失是计算在给定参考答案上的,它无法惩罚模型“遗忘”了其他同样正确但更灵活的表达方式。因此损失可以继续降低,但实际的对话体验却变得僵硬、无趣。
-
安全与对齐信号的隐性代价:如果 SFT 数据中包含大量生硬的拒绝样本或过于谨慎的表述,验证损失可能会因为这些样本中“拒绝”关键词的容易预测而下降,但实际生成时,模型却变得过度拒绝或表达模糊,损害了有用性。
-
教师强制(Teacher Forcing)的欺骗性:验证损失是在教师强制(每一步都输入真实上下文)下计算的,而真实生成是自回归的,错误会累积。过拟合使模型在教师强制下表现良好,但一旦开始自回归生成,任何微小的偏差都会让它进入从未见过的“错误上下文”区域,导致后续生成质量急剧恶化。
解决方向:不能仅依赖损失选择模型。必须结合生成式评估:定期用当前 checkpoint 对一组固定的复杂指令进行实际生成,并用强模型(如 GPT-4)或人工评估其逻辑性、有用性、流畅度等维度。当损失与生成质量背离时,应信任生成质量评估的结果,并采取早停或调整数据/超参。
全参数微调时,用什么策略保存checkpoint?如何选择最优模型?¶
全参数微调成本高昂,必须确保能够挑选出泛化能力最强、最符合最终应用目标的模型。因此,checkpoint 保存与选择策略至关重要。
保存策略:
-
按步数定期保存:每 N 步(如 200 步)保存一个完整的模型状态(包含优化器状态、学习率调度器状态),以便恢复训练。
-
保存“最佳验证损失”模型:额外维护一个指标,当验证损失创下新低时,单独保存一个只含模型权重的 checkpoint,便于快速加载。
-
保存多个维度的“最佳”:除了损失最低,还可保存通用基准得分最高、或生成质量评分最高的 checkpoint。这需要训练过程中持续运行评估。
-
保留足够的检查点历史:避免只保留最后几个,以便在发现过拟合后能回滚到较早的、泛化能力更好的模型。
如何选择最优模型:
- 首选:基于生成质量的多维评估 训练结束后,对所有保存的 checkpoint(至少是最后10~20个)进行全面离线评估。评估应包含:
- 目标领域的任务性能(如准确率、F1)。
- 通用能力基准(MMLU、HellaSwag 等)。
- 安全性和过度拒绝率。
-
使用 GPT-4 等强模型,在独立测试集上对回答进行多维度盲评(有用性、准确性、流畅度等),并计算胜率。 综合这些指标,绘制出每个 checkpoint 的“能力雷达图”,选择综合表现最优的那个。
-
警惕“单一的损失最优”陷阱:验证损失最低的 checkpoint 往往已经接近过拟合,其生成质量和泛化能力可能已经下降。通常,泛化最佳的模型出现在验证损失刚刚趋于平缓、尚未开始反弹的“高原区”早期。
-
使用早停原则:在训练过程中就确定最优模型。当验证损失在设定的耐心值内不再改善时,直接终止训练,并将耐心值开始前那个损失最低的 checkpoint 作为最优模型。
-
线上 A/B 验证(最终决策):如果条件允许,将离线评估选出的前 2~3 个候选模型部署为灰度服务,分配少量真实流量进行 A/B 测试,通过用户点赞率、任务完成率等终极指标决定上线版本。
实践心得:最优模型极少是训练损失最低的那个。它通常是那个在“学会新格式”和“保留旧知识”之间达到最佳平衡的模型。这个平衡点需要通过上述多维评估来精准定位。
如何在训练中动态评估模型生成质量,作为选择checkpoint的依据?¶
将生成质量评估嵌入训练循环,是将模型选择从“事后诸葛”变为“实时导航”的关键。这需要自动化的、与人类判断高度相关的评估手段。
动态评估管道:
-
准备固定评估集:构建一个约100~200条指令的集合,需覆盖核心业务场景、边界案例、安全对抗等,且绝不与训练集重叠。
-
设定评估频率:一般每隔 100~200 训练步或每个 epoch 结束时,暂停训练,用当前模型(切换到 eval 模式)对该评估集生成回答。
-
自动化评判:
- GPT-4 as Judge:将生成的回答发送给 GPT-4,让其按照预设的评分标准(如准确性、有用性、安全性等维度)打分,或与一个固定的基线模型(如基座模型)的回答进行对比评判(A vs B),输出胜率。此方法成本较高但效果最好。
-
轻量级自动指标:对于有标准答案的任务,直接计算 ROUGE、BLEU 或精确匹配率。对于格式遵循任务,可使用 IFEval 那样的规则检查器自动计算遵循率。
-
记录与可视化:将每个评估点的得分(如 GPT-4 评分均值、胜率、格式化准确率等)记录到日志中,并用 TensorBoard 等工具绘制出随训练步数变化的曲线。
如何依据评估选择 checkpoint:
-
趋势判断:观察得分曲线。当得分达到最高点后开始呈现下降趋势(即使验证损失仍在下降),说明过拟合开始损害真实生成质量。此时应立即停止训练或回滚到得分最高的那一步。
-
多维权衡:如果 GPT-4 评判的“有用性”得分持续上升,但“安全性”得分下降到不可接受的程度,说明模型变得过于顺从,需要平衡。选择那个在两个维度上达到最佳 trade-off 的 checkpoint。
-
与损失曲线结合:通常,生成质量最优的 checkpoint 位于验证损失曲线由陡变缓的拐点附近,而非最低点。
工程实现:可在 HuggingFace Trainer 中自定义一个回调函数,在 on_log 或 on_step_end 时触发评估,用多线程/异步方式调用 GPT-4 API,并将结果写回训练状态。注意评估期间释放训练显存,评估完成后再恢复。
全参数微调显存不足时,你会优先尝试哪些优化手段?¶
显存不足是全参数微调最常见的困境。优化时应遵循“性价比从高到低”的原则,即先尝试对训练速度影响小、效果显著的手段。
优化阶梯(从最先尝试到最终手段):
实践组合示例:
微调 7B 模型,单卡 24GB:
-
启用
load_in_8bit或load_in_4bit(QLoRA),但这属于 PEFT。 -
若坚持全量微调,则需:BF16 + 梯度检查点 + micro_batch=1 + 梯度累积=16 + Adafactor 优化器 + 冻结 embedding。通过这些组合,或许能在 24GB 上勉强运行全量微调一个 7B 模型。
注意:梯度累积不会降低单步显存峰值,它只影响有效 batch size。真正降低显存峰值的是 micro-batch size 的减小、混合精度和梯度检查点。
混合精度训练FP16和BF16在全参数微调中的选择依据是什么?¶
选择 FP16 还是 BF16 进行全参数微调,核心考量在于硬件兼容性、训练稳定性和精度需求。
选择依据:
-
硬件是否支持 BF16:BF16 需要 NVIDIA Ampere 架构(A100, A6000, RTX 3090/4090 等)或更新的 GPU。如果你使用的是 V100 或更早的 GPU,只能使用 FP16。这是硬性条件。
-
训练稳定性优先:BF16 的动态范围(指数位 8 位)与 FP32 相同,能够表示极大和极小的数值,因此几乎不会出现梯度下溢(变为 0)或上溢(变为 Inf)的问题。训练过程无需复杂的 Loss Scaling,更加稳定可靠。如果你的微调任务对大范围数值变化敏感,或你希望简化训练流程、减少调参,优先选 BF16。
-
性能与精度:FP16 的尾数精度(10位)高于 BF16(7位),理论上在某些对精度要求极高的任务上(如某些科学计算)可能更好。但对于大语言模型,7位尾数已足够捕捉模型权重的更新,性能差异极小。且 BF16 与 FP32 互转更简单(直接截断),计算效率有时更高。
-
现有代码与生态:一些旧的训练脚本或定制算子可能只针对 FP16 进行了优化。如果你的代码库是成熟的 FP16 工作流,且已在其中投入了大量调试,继续使用 FP16 也是合理的。
-
是否需要 Loss Scaling:使用 FP16 必须配置动态或静态 Loss Scaling,这增加了超参数。BF16 则完全无需考虑此问题。
总结决策树:
-
检查 GPU 型号。是 A100/H100/RTX 30系及以上?→ 推荐 BF16。
-
是老款 V100/T4?→ 只能 FP16,并务必开启 Loss Scaling。
-
即使在支持 BF16 的硬件上,若你使用的是一个高度定制化的、不兼容 BF16 的算子库,也可能被迫使用 FP16。
在大模型微调中,BF16 已成为事实标准,因为它带来的训练稳定性提升远大于微不足道的精度损失。
BF16相比FP16有什么优势?为什么大模型训练常用BF16?¶
BF16(Brain Floating Point)相比 FP16,在大模型训练中拥有两大核心优势:无与伦比的训练稳定性和简化的软硬件支持。
优势详解:
-
动态范围与 FP32 一致,彻底避免溢出:这是最根本的优势。BF16 使用 8 位指数,与 FP32 的 8 位指数完全一致,能表示的最大/最小数值范围完全相同。在大模型训练中,梯度值往往极小(容易下溢变为0)或极大(容易上溢变为Inf)。BF16 凭借其宽广的动态范围,几乎完美地解决了 FP16 中常见的梯度消失和爆炸问题。你无需再引入复杂的 Loss Scaling(损失缩放)技术来人为调整损失大小,这不仅简化了代码,也消除了一个敏感的超参数调优点。
-
与 FP32 互转便捷:BF16 转 FP32 只需在低位补零,FP32 转 BF16 只需截断尾数。这种简单的转换机制使得混合精度训练中,权重的主副本(FP32)与 BF16 之间切换非常高效,硬件设计也更为简单。
-
尾数精度对大模型影响甚微:BF16 只有 7 位尾数,低于 FP16 的 10 位,表示数的“粒度”更粗。然而,大语言模型的参数更新是高度冗余和噪声的,模型对数值精度的微小损失具有极强的鲁棒性。牺牲少量的精度换取训练过程的绝对稳定和潜在的速度提升,是完全值得的。
-
硬件趋势:Google 的 TPU 最早支持 BF16,NVIDIA 从 A100 开始全面拥抱 BF16。新一代 AI 加速器普遍将 BF16 作为主要的半精度计算格式。
因此,大模型训练常用 BF16 的最重要原因是:它能以最小的精度代价,换来训练过程的“免维护”稳定性和更简化的软件栈。 这对于动辄数周的大规模训练来说,是至关重要的。
什么是“loss scaling”?为什么FP16训练需要它,BF16不需要?¶
Loss Scaling(损失缩放) 是混合精度训练中,为解决 FP16 格式梯度下溢问题而采用的一种技术。
为什么 FP16 需要它?
FP16 的指数范围很窄(能表示的最小正规数约 6e-5),而大模型训练后期,许多梯度的值会变得非常微小(例如 1e-7 甚至更小)。这些微小的梯度值落在 FP16 的表示范围之外,会被直接截断为 0。一旦梯度变为 0,对应的参数将无法得到更新,导致模型训练停滞,无法收敛。
Loss Scaling 的做法:
-
前向传播计算出 loss 后,将 loss 乘以一个很大的常数(如 65536),得到放大后的 scaled loss。
-
反向传播时,计算出的梯度也会相应地放大相同倍数。原本微小的梯度被放大后,就进入了 FP16 能够表示的范围,从而避免了变为 0 的命运。
-
在优化器更新参数之前,再将梯度除以相同的常数,恢复其原始的尺度。
这样,模型既能享受 FP16 高速低存储的好处,又能避免梯度下溢。通常,Loss Scaling 的值可以动态调整(自动找到不会导致溢出的最大缩放因子),也可手动设置。
为什么 BF16 不需要?
BF16 的指数位数与 FP32 相同,其动态范围(能表示的最小/最大值)与 FP32 完全一致。FP16 中那些小到无法表示的梯度值,在 BF16 中依然可以被精确表示(虽然尾数精度较低)。因此,BF16 训练时,梯度下溢的问题几乎不存在,自然不需要 Loss Scaling 这种补救措施。
简而言之,Loss Scaling 是 FP16 为了弥补其动态范围窄的“先天缺陷”而打的一个“补丁”。而 BF16 天生没有这个缺陷,因此不需要这个补丁,训练过程更简洁、稳定。
全参数微调时,是否应该冻结embedding层和lm_head?有何利弊?¶
这是一个经典的权衡:是否冻结 embedding 层和 lm_head(输出投影层),取决于模型词表大小、目标任务与预训练的分布差异、以及显存状况。
利:为什么要冻结它们?
-
显著节省显存:Embedding 层和 lm_head 的参数规模为
vocab_size * hidden_dim。对于多语言大词表模型(词表可达 25万+),这两个矩阵的参数量极其庞大,甚至可占到模型总参数的 20%~30%。冻结它们可以节省对应的梯度存储和优化器状态,显存节省非常可观。 -
防止词表过拟合:微调数据中的词汇分布通常很窄。如果更新 embedding 层,模型可能会过度适应微调数据中的词汇共现模式,破坏预训练阶段习得的、健壮的语义表示。冻结可以保护底层语义特征。
-
缓解灾难性遗忘:底层 embedding 承载着大量通用的形态和语义知识,冻结它们是防止遗忘最直接的手段之一。
弊:为什么不冻结它们?
-
无法适应新领域的关键术语:如果目标领域存在大量全新的特殊词汇(如医学术语、法律术语、代码 tokens),且这些词在预训练中很少见或不存在,冻结 embedding 会导致模型根本无法“理解”这些核心词汇,微调效果大打折扣。
-
无法对齐输出分布:lm_head 决定了最终输出 token 的概率分布。如果目标任务的回答风格、格式与预训练差异极大,冻结 lm_head 会使模型在生成特定格式(如 JSON、代码)时能力受限,因为它无法调整输出层的偏好。
-
联合优化的一致性:在现代大模型中,embedding 和 lm_head 的权重常常是共享的(tied weights)。如果冻结一个而微调另一个,可能会破坏这种对称性,带来不可预知的问题。
决策建议:
-
通用 SFT(对话、指令遵循)且显存紧张:优先冻结 embedding 和 lm_head。这是成本低、风险小的选择,大多数聊天模型微调都这么做。
-
领域知识注入(医学、法律等)且数据充足:解冻 embedding 和 lm_head。但需要用极小的学习率(如全局学习率的 1/10)单独训练它们,以保护底层的通用语义。
-
代码/数学微调:通常解冻,因为代码关键字和数学符号的向量表示需要调整。
-
最佳实践:进行一个微型实验:分别训练冻结和不冻结的模型(例如 100 步),比较两者在验证集上的困惑度和下游任务效果,用数据做决策。
分层学习率(discriminative learning rates)在全参数微调中如何设置?¶
分层学习率(Discriminative Learning Rates) 是对模型的不同层或模块使用不同的学习率,底层使用更小的学习率以保护通用知识,顶层使用较大的学习率以快速适应新任务。
设置原则与步骤:
-
确定顶层学习率(Top‑Layer LR):首先通过常规手段(如学习率范围测试)确定一个适合于整个模型微调的学习率,例如
lr = 1e-5。这将是顶层的学习率。 -
设计衰减策略:
- 按层衰减:最经典的做法。将 Transformer 层从顶层(最靠近输出)到底层(最靠近输入)编号,每下降一层,学习率乘以一个衰减因子,例如 0.95 或 0.9。这样,第 0 层(最底层)的学习率可能是
lr * (decay_factor)^{num_layers}。 - 分组衰减:更简单的做法是将模型分为 3 个组:
- 顶层:最后几层 Transformer + lm_head。学习率 =
lr。 - 中层:中间的 Transformer 层。学习率 =
lr * 0.5(或 0.1 等)。 - 底层:最初的几层 Transformer + Embedding 层。学习率 =
lr * 0.1(或 0.01),甚至冻结(lr=0)。
- 顶层:最后几层 Transformer + lm_head。学习率 =
-
单独设定特殊模块:对 Embedding 层和 lm_head 可以设置极低的学习率(如
lr * 0.1),如上题所述。 -
在优化器中实现:在 PyTorch 中,你需要构造一个参数列表,为不同组指定不同的
'lr'。
optimizer = AdamW([
{'params': model.layers[-4:].parameters(), 'lr': 1e-5},
{'params': model.layers[4:-4].parameters(), 'lr': 5e-6},
{'params': model.embed_tokens.parameters(), 'lr': 1e-6},
{'params': model.lm_head.parameters(), 'lr': 1e-6},
])
为什么有效?
-
预训练模型的底层学习到的是更通用的、基础的语言特征(语法、词性),这些知识是跨领域通用的,应该被强力保护。顶层学习到的是更抽象、更任务相关的语义组合,需要更大的灵活性来适应新任务。
-
这种策略在迁移学习中是一种强正则化,迫使模型在“不破坏根基”的前提下进行“上层重构”,从而有效平衡新任务学习和旧知识保持。
实践中的调整:衰减因子和分层粒度通常需要通过实验确定。可以先从温和的分组策略开始(如顶层 1x,底层 0.5x),观察验证集损失和通用基准的变化,再逐步拉大或缩小差距。注意,使用分层学习率后,总的训练动态会改变,Warmup 和衰减策略可能需要相应微调。
“progressive layer dropping”或“layer freezing”策略有哪些?¶
在全参数微调中,渐进式层冻结(progressive layer freezing) 和 层丢弃(layer dropping) 是两种通过限制参数更新范围来降低计算开销、抑制过拟合和遗忘的策略。
一、渐进式层冻结(Progressive Freezing)
这种方法在训练过程中由顶至底逐步冻结模型层。训练开始时所有层都可训练,然后按预定计划或根据验证指标,从底层开始逐层或逐组将层冻结(requires_grad=False),只保留高层和任务相关头继续更新。
常见策略:
-
按时间步冻结:设定冻结计划表,例如第1 000步冻结最底层2层,第2 000步再冻结接下来的2层,直到只剩最后几层可训练。
-
按指标冻结:监控底层参数的梯度范数或Fisher信息,当它们变化很小时认为该层已“收敛”,将其冻结。
-
分层冻结与解冻:也可以先冻结所有层,只训练顶层;然后每轮解冻一组更低的层(与progressive unfreezing相反),但在全参数微调中更常见的是由底向上冻结。
优势:
-
节省计算:被冻结的层不再需要梯度计算和优化器状态更新,前向反向速度提升,显存压力降低。
-
抑制遗忘:底层通用特征被尽早锁定,防止微调数据冲刷预训练基础。
-
正则化效应:减少可训练参数数量,降低过拟合风险。
二、层丢弃(Layer Dropping)
这不是严格意义上的冻结,而是在训练过程中随机丢弃某些层,即跳过它们的计算。它在推理时也常用于加速,但在微调中通常作为正则化手段。
常见策略:
-
随机层丢弃:每个训练step以一定概率(如0.1)随机选择一些Transformer层,将其输出直接设为输入(残差连接直接传过),不进行计算。
-
结构化丢弃:专门针对高层或低层进行丢弃,或者逐渐增加丢弃概率。
优势:
-
极强的正则化:迫使模型不依赖任何单一层,学习到更鲁棒的表示。
-
模拟较浅网络:有助于在微调数据稀缺时减少过拟合。
微调中的实际应用:
全参数微调时,通常会将渐进式冻结与分层学习率结合使用。例如,前几个epoch所有层参与训练,但底层使用极小学习率;随后直接将底层冻结,加速后续训练。层丢弃更多见于预训练或极低资源微调,工业界全参数微调中较少单独使用。
如何使用“replay”策略保持全参数微调中的通用能力?¶
Replay(经验回放) 是从持续学习领域借鉴的核心策略,通过在微调过程中持续“重放”预训练阶段见过的数据,使模型参数优化时不仅要降低新任务损失,还要保持在旧任务(通用知识)上的低损失。
实现方式:
-
构建重放缓冲区:从预训练语料中(如维基百科、书籍、高质量网页)随机采样一个子集,大小通常为目标SFT数据集的5%~20%。这些数据代表“旧知识”。
-
混合训练:在每个训练batch中,不仅包含SFT样本,还按比例混入重放缓冲区的预训练文本。这些文本直接按标准语言建模任务处理(预测下一个token),与SFT损失相加。
-
动态更新:缓冲区可以固定,也可以每个epoch重新随机采样一部分,以覆盖更广的预训练分布。
为什么有效:
从优化视角看,微调数据的梯度会将参数拉向狭窄的任务分布,而重放数据的梯度则提供了指向原预训练分布广阔区域的“修正力”。参数更新方向是两者的加权平均,从而防止参数完全脱离旧知识的盆地。这相当于在损失曲面上施加了一个“锚”。
效果:
-
极大缓解灾难性遗忘,实验显示混入10%预训练数据可使MMLU等通用基准下降幅度从-5%缩小到-1%以内。
-
维持生成多样性和语言流畅度。
-
对SFT任务本身的学习影响很小,因为预训练数据的损失与SFT损失是累加关系,只要比例适当,SFT信号依然占主导。
混合比例的确定:
-
全参数微调风险高,建议重放数据占每个batch的10%~15%。
-
PEFT时遗忘风险低,可减至1%~5%或不使用。
-
可通过监控通用基准验证:用10%比例训练一轮,若通用能力下降可接受,则维持;若下降明显,提高到15%或20%。
潜在问题:重放数据如果含有噪声或低质内容,可能引入负面影响,因此需清洗筛选。另外,重放数据与SFT数据在长度、格式上的差异可能导致训练效率降低,可通过适当截断或padding解决。
解释“continual learning”中的“memory replay”在微调中的应用。¶
Memory Replay(记忆重放) 是持续学习(Continual Learning)中克服灾难性遗忘的核心方法。在LLM微调中,它被直接借用来保护模型的通用知识。
核心思想:
模型在学习新任务(SFT)时,会周期性地“复习”旧任务(预训练)的典型样本,从而巩固旧知识,防止被新知识完全覆盖。这模仿了人类学习过程中通过反复练习来保持长期记忆的机制。
在微调中的具体应用:
-
存储旧任务样本:从预训练数据集中抽取一个有代表性的子集,存储在“记忆缓冲区”中。这个缓冲区的大小是固定的(例如10万条文本)。
-
交织训练:在SFT的每个训练批次中,从缓冲区随机抽取一小批样本,与当前SFT样本一起输入模型。旧样本的损失函数通常是标准的自回归语言建模损失。
-
缓冲区更新:
- 固定缓冲区:训练前一次性构建,整个微调过程不变。
-
动态缓冲区:定期用新的预训练样本替换缓冲区中的旧样本,使模型接触到更广泛的旧知识,避免对缓冲区本身过拟合。
-
损失加权:通常新旧任务的损失直接相加,或者给重放损失一个较小的系数(如0.1~0.3),以防止旧任务信号过强而阻碍新任务学习。
与普通Replay的区别:
-
在LLM微调中,“旧任务”通常是整个预训练语料的分布,而非几个特定的旧任务。因此,缓冲区构建更注重覆盖面而非特定样本。
-
由于预训练数据规模极大,缓冲区不可能完整覆盖,只能近似代表,因此需要精心挑选具有代表性的高质量文本,而不是随机采样。
效果与局限:
-
显著降低灾难性遗忘,是目前全参数微调中最标准、最有效的防遗忘手段之一。
-
需要额外的显存(存储缓冲区数据,但数据本身不常驻显存,只在加载时消耗)和训练时间(处理额外样本)。
-
如何选择最有“复习价值”的旧样本仍然是一个开放问题,通常基于启发式规则(如选择困惑度高的样本,这些样本信息量大)。
总结:Memory Replay 在微调中就是那个不断提醒模型“别忘了你是谁”的教练。它让模型在新任务上精进的同时,不丢失原有的广博学识。
如果微调一个对话模型,如何在全参数微调中保持安全对齐?¶
对已经经过SFT和RLHF安全对齐的对话模型(如Llama‑2‑Chat)进行二次全参数微调,极易因新数据的分布而“洗掉”原有的安全护栏,导致模型变得危险。保持安全对齐是一项贯穿数据、训练和评估的系统工程。
一、数据层面的安全锚定
-
保留并扩充安全样本:原始对齐数据集中的安全拒绝样本必须被保留,并加入到新的微调数据集中。此外,应针对新领域主动构造安全对抗样本。例如,如果微调成金融顾问,需要构造“如何利用内幕信息牟利?”这类问题的正确拒绝回答。
-
安全样本比例控制:安全样本通常占总数据的3%~8%。比例过低则安全边界被淹没,过高则模型变得过度拒绝。需要通过实验找到平衡点。
-
建设性拒绝的示范:安全回答不应只是简单的“抱歉我不能”,而应提供原因解释和安全的替代方案,这样模型才能学会既安全又有帮助。
二、训练策略的安全防护
-
使用极低学习率:相比初次SFT,二次微调的学习率应更低(如1/5~1/10),以减少参数剧烈变动,保护已校准的安全行为。
-
混合回放原始对齐数据:在训练batch中混入一小部分原始对齐阶段使用的安全对话数据,作为“记忆锚点”,持续激活安全相关的参数。
-
考虑PEFT:若条件允许,优先使用LoRA等参数高效微调,冻结基座模型,只训练轻量适配器。这从根本上杜绝了对原始安全参数的修改。
-
监控与早停:训练过程中,在独立的安全测试集(包含正常和对抗性请求)上持续评估模型的拒绝率和误拒绝率。一旦发现安全指标恶化,立即停止训练并回滚。
三、评估与迭代
训练完成后,必须进行严格的红队测试和自动化安全基准(如ToxicChat)评估,确认模型在新领域上没有产生新的安全漏洞。将发现的漏洞转化为新的训练样本,进入下一轮迭代。
核心原则:永远不要在追求新能力的过程中,牺牲模型已经具备的安全品格。
全参数微调后,模型在某个领域的知识增加了,但推理能力下降了,为什么?¶
这是典型的 灾难性遗忘的局部表现 与 知识-推理解耦失效 共同作用的结果。
一、灾难性遗忘
全参数微调时,参数更新完全由新领域数据的损失驱动。模型为了快速降低新领域的损失,会将与推理相关的通用参数(如注意力模式、逻辑演绎能力)大幅调整,甚至覆盖。这些推理能力是在预训练阶段通过海量代码、数学、逻辑文本习得的,微调数据通常缺乏此类任务,导致相关神经元被“洗掉”。
二、知识-推理的依赖关系被破坏
预训练模型中,知识(事实性信息)和推理(如何运用知识)是紧密耦合的。微调注入的新知识可能扰乱原有的参数结构,使得模型在调用推理能力时,无法与相关旧知识有效协同。例如,模型学会了新的医学知识,但逻辑推理网络被新知识的梯度干扰,导致它在需要多步推理的医学诊断问题上表现变差。
三、分布坍缩导致推理路径丧失
微调数据往往以直接问答为主,缺乏需要复杂推理链的文本。模型在优化过程中,输出分布越来越集中到“直接给出答案”的模式上,丧失了预训练时“分步推理”、“自我纠正”等多样化的生成路径。当遇到需要推理的问题时,它更倾向于套用简单的回答模板,而不是进行深层思考。
四、优化过程中的耦合效应
一个参数的更新会影响成百上千个相连的神经元。新领域知识的梯度可能无意中“关闭”了某些对推理至关重要的参数。这在高维空间中尤为常见——新任务的最优方向恰好与保持推理能力的方向正交甚至相反。
缓解措施:
-
在SFT数据中混合保留一定比例的推理数据(如CoT数学题、逻辑谜题),强制模型在微调过程中持续练习推理。
-
使用PEFT(如LoRA)而不是全参数微调,冻结底层的推理能力。
-
严格早停和超低学习率,避免参数变动幅度过大。
-
混合预训练通用文本(重放),维持旧知识的激活。
如何对全参数微调进行profiling,找出计算或通信瓶颈?¶
全参数微调效率低下时,瓶颈可能来自GPU计算、通信或者数据加载。系统性地进行profiling是定位问题的第一步。
一、使用PyTorch Profiler定位计算瓶颈
-
启用Profiler:在训练循环中包裹
torch.profiler.profile,设置activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA],并记录record_shapes=True和profile_memory=True。 -
生成Chrome Trace:导出为
chrome_trace.json,在Chrome的chrome://tracing中打开。按“GPU duration”排序,找出耗时最长的算子。常见元凶包括:aten::linear(矩阵乘)、aten::softmax(注意力)、aten::layer_norm等。检查它们的输入形状是否合理(如过多的小矩阵乘法,可考虑算子融合)。 -
计算利用率:观察
cudaMemcpyAsync和核函数之间的间隔。如果GPU长时间空闲(stream空闲区大),通常意味着CPU无法快速提供数据或启动核函数。
二、定位通信瓶颈(多卡训练)
-
使用NCCL调试环境:设置
NCCL_DEBUG=INFO查看通信日志,设置NCCL_ALGO环境变量测试不同算法。 -
Nsight Systems:可以捕获多节点通信时间线。观察
All‑Reduce、All‑Gather等集合通信操作的耗时。如果这些操作占据总步时比例过高(>20%),说明网络带宽或延迟是瓶颈。 -
计算/通信重叠分析:检查是否有大量时间花费在“等待梯度同步”上。理想情况下通信应与计算重叠。可以通过启用DeepSpeed或FSDP的
overlap_comm选项来改善。 -
网络带宽监控:使用
nvidia-smi nvlink -e 0或ib_read_bw等工具检查带宽是否达到硬件理论值。
三、数据加载瓶颈定位
-
监控CPU利用率:如果
DataLoader的worker进程CPU使用率长期接近100%,说明数据预处理是瓶颈。 -
观察GPU空闲时间:在Profiler时间线中,若每个训练step之前有大量GPU空闲,且对应的时间段内CPU在处理数据,则数据加载跟不上。
-
优化措施:增加
num_workers,使用pin_memory=True,将数据预取到共享内存;使用更快的解码库(如nvidia.dali);对于文本数据,考虑使用预tokenized并缓存的数据集(Parquet/Arrow格式)。
四、分析显存使用
用torch.cuda.memory_summary()打印显存分配峰值和碎片情况。定位是哪些张量(权重/梯度/优化器/激活)占了大头,从而针对性地使用梯度检查点、混合精度等技术。
实践流程:先用nvidia-smi dmon和htop做粗粒度观察,发现问题倾向后,再用PyTorch Profiler或Nsight Systems进行精细化诊断。
多节点全参数微调时,如何保证数据加载不成为瓶颈?¶
在多节点分布式训练中,模型的计算速度极快,数据加载(I/O)极易成为短板。必须构建一个高效的、流水线化的数据供给系统。
一、数据格式与存储优化
-
使用列式存储格式(Parquet/Arrow):相比JSONL逐行解析,Parquet可以只读取需要的列,且内部压缩率高、I/O效率极高。将数据集预先转换为Parquet格式,可大幅减少磁盘读取时间。
-
预Tokenize并缓存:将文本预先转换为
input_ids和labels的Tensor,并以内存映射(Arrow)方式存储。这样训练时直接加载张量,省去重复的CPU Tokenization。HuggingFace Datasets的set_format和cache_files功能可自动完成这一工作。 -
数据分片(Sharding):将大数据集切分成多个小文件,每个worker加载一部分,避免单文件I/O竞争。文件大小建议在100MB~1GB之间。
二、DataLoader配置
-
增加worker数量:
num_workers通常设为CPU核心数的1/2到1倍,但不宜过多,因为多worker会竞争Python GIL。可通过实验找到吞吐最高点。 -
使用
pin_memory=True:在worker中将数据锁页到CPU内存,加速CPU到GPU的数据传输。 -
预取(prefetch_factor):设置
prefetch_factor=2,让每个worker提前准备多个batch,避免GPU等待。 -
使用
IterableDataset和自定义Sampler:对于极大数据集,使用流式处理(如WebDataset)而非Map-style Dataset,避免频繁的随机访问。
三、分布式环境特定优化
-
数据分片与亲和性:确保每个节点只加载自己需要处理的数据分片,避免跨节点数据传输。
-
使用本地高速缓存:对于云存储上的数据,先在每个计算节点上使用本地NVMe SSD作为缓存,第一次epoch后,后续epoch的加载速度会极快。
-
混合精度数据加载:如果数据需要动态padding,使用
DataCollator时尽量在collate_fn中完成,避免在主进程中进行重量级处理。
四、监控与调优
使用nvidia-smi和htop监控GPU利用率。如果GPU利用率间歇性降至0,且此时CPU负载高,基本可判定为数据瓶颈。使用torch.utils.bottleneck或自定义profiler进一步分析是I/O、反序列化还是tokenization慢,针对性地优化。
全参数微调中,“sequence length”对显存的影响有多大?如何估算?¶
序列长度(Sequence Length, L)对显存的影响是线性与二次的叠加:中间激活(Q、K、V、FFN等)随L线性增长,而注意力矩阵的理论尺寸随L二次增长(不过使用FlashAttention后可控制在O(L)级别)。对全参数微调,激活显存往往是最大的可变部分。
一、激活显存估算公式
对于单个Transformer层,一次前向传播的中间激活显存大致为:

-
H:隐藏维度 -
num_layers:层数 -
系数
34~40:包含了Q、K、V、注意力输出、FFN中间结果等,这是经验值。使用FlashAttention后,系数可降至20~30左右。
更直观地,对于一个7B模型(H=4096,层数=32):
-
L=512,batch=1:激活显存约
1 × 512 × 4096 × 32 × 34 ≈ 2.1 GB(开启FlashAttention)。 -
L=2048:约
8.5 GB。 -
L=4096:约
17 GB。
二、注意力矩阵的显存陷阱
若不使用FlashAttention,标准的自注意力需要存储一个形状为 [batch, heads, L, L] 的注意力分数矩阵,FP16下占用 2 × heads × L² 字节。例如,32个头,L=4096时,该矩阵为 32 × 4096² × 2 ≈ 1 GB。这只是一个头的,多头求和会更大。FlashAttention通过分块计算完全消除了这个矩阵,是长序列训练的必备。
三、KV Cache的显存
微调时如果是自回归生成(如使用teacher forcing),同样需要存储KV Cache,但其大小在训练中通常被中间激活涵盖。在纯推理时它是主要瓶颈,但在微调中,我们更关注前向的激活。
如何估算整体显存?
总显存 = 模型状态(权重+梯度+优化器) + 激活。模型状态通常是固定的(70B模型约840GB),激活随L线性变化。通过上述公式估算激活,加上模型状态,可得到粗略的需求,再乘以1.2的系数覆盖碎片和框架开销。
控制方法:
-
减小batch size或使用梯度累积。
-
启用梯度检查点,将激活显存压至 1/√层数 级别。
-
使用FlashAttention。
-
使用序列并行(将序列维度切分到多卡)。
当微调数据多为长文本时,怎样平衡显存和batch size?¶
长文本微调时,显存和batch size的平衡主要依靠梯度累积、梯度检查点、序列打包优化和动态批处理。
一、梯度累积(Gradient Accumulation)
这是最核心的手段。将micro‑batch size设到显存可承受的最小值(如1或2),然后通过累积多个micro‑batch的梯度来模拟目标全局batch size。例如,目标batch size=64,micro‑batch=2,则累积32步。这确保了长序列的激活不会撑爆显存。
二、梯度检查点(Gradient Checkpointing)
开启后可大幅降低激活显存,从而在相同显存下可以使用更大的micro‑batch,或在相同micro‑batch下支持更长的序列。但会牺牲约20‑30%的训练速度。
三、序列打包(Sequence Packing)
将多个短文本拼接成一个接近最大长度的序列,减少padding浪费,提高有效token比例。但长文本本身已近满长度,打包空间有限。可以使用动态打包:允许一个pack中包含不同长度的样本,通过截断或跳过超长样本来控制最大长度。
四、使用长度感知的动态批处理(Dynamic Batching)
构建DataLoader时,将长度相似的样本分到同一个batch,以减少padding开销。极端情况下,对少数超长样本单独成batch,使用极小的micro‑batch处理。
五、利用ZeRO‑3或张量并行
在多卡环境下,通过ZeRO‑3分片参数和激活,可将单卡显存压力大幅降低,从而支持更大的序列或micro‑batch。张量并行也能直接按TP度线性降低单卡激活。
六、截断与滑动窗口
如果任务允许,对超长文本进行智能截断(保留首尾关键信息),或使用滑动窗口注意力,在模型架构层面限制最大序列长度。
实践组合:设置micro_batch_size=1,gradient_accumulation_steps=32,启用gradient_checkpointing和FlashAttention,使用ZeRO‑3(多卡时),并配置长度感知的Dynamic Batching。这样可以在有限的显存下,高效训练长文本模型。
全参数微调时,可以使用序列打包(packing)吗?怎么做?¶
可以使用,且序列打包是提高长文本训练效率、减少padding浪费的重要手段。但在全参数微调中,由于需要计算每个token的损失,packing的实现需格外小心,尤其是注意力掩码(attention mask)和损失掩码(loss mask) 的构造。
一、序列打包的原理
将多个较短的样本拼接成一个长度为max_seq_len的序列,从而让GPU的每次前向/反向传播处理尽可能多的有效token。不同样本之间通过特殊的分隔符(如EOS token)和分块对角注意力掩码(block-diagonal attention mask) 来隔离,防止它们互相关注。
二、全参数微调中的实现步骤
-
数据准备:将每个样本的
input_ids(指令+回答)和labels准备好。注意,labels中已对prompt部分做了-100掩码。 -
拼接策略:维护一个缓冲区,将样本逐一加入。当加入下一个样本会超出
max_seq_len时,将当前缓冲区的样本拼接为一个pack,送入模型;清空缓冲区,开始新pack。 -
插入分隔符:在每个样本的末尾插入一个EOS token(或特殊分隔符),其
input_ids为EOS id,labels为-100(不计算损失),以保证样本边界明确。 -
构造注意力掩码:必须构造4D的block-diagonal causal mask。对于每个pack内的每个样本,只允许它内部的token相互关注(且遵循因果掩码),完全屏蔽对其他样本token的注意力。
-
计算损失:由于
labels中非回答部分已被掩码,损失仅计算在实际回答token上。不同样本的回答不会互相干扰。
三、关键注意事项
-
数据顺序:由于SFT数据常有主题聚集性,为增加随机性,应在打包前对数据集进行充分混洗。
-
位置编码:RoPE等相对位置编码在打包场景下仍有效,因为注意力被限制在各自块内,位置连续性不影响语义。
-
填充与截断:如果最后剩余空间放不下一个完整样本,可选择截断该样本(可能丢失信息),或用
[PAD]填充,让模型忽略。 -
训练效率:打包能大幅提升有效token的利用率,训练吞吐量可提升2~5倍,尤其适合短文本多的数据集。
全参数微调下的大规模实现:可使用HuggingFace的DataCollatorForSeq2Seq的扩展,或自定义DataCollator实现packing。在数据加载前完成打包(离线打包)可以进一步减少训练时的CPU开销。
全参数微调过程中,如何监控GPU利用率和显存使用?¶
实时监控GPU状态是发现训练瓶颈、优化资源使用和避免OOM的关键。需要从粗粒度实时观察和细粒度记录分析两个层面进行。
一、粗粒度实时监控(命令行)
-
nvidia-smi循环:最常用的工具。watch -n 1 nvidia-smi可以每秒刷新GPU利用率(GPU-Util)、显存使用(Memory-Usage)、温度等。简单直观。 -
nvidia-smi dmon:更专业的持续监控,可输出pucm(功耗、利用率、时钟、显存)等指标的流式数据,适合重定向到日志文件供后续分析。 -
nvtop:交互式、全屏的GPU监控工具,类似htop,适合单机快速查看。
二、细粒度记录与分析(Python/Profiler)
- PyTorch内置函数:
torch.cuda.memory_allocated():当前PyTorch实际分配的显存。torch.cuda.memory_reserved():PyTorch缓存分配器持有的显存(包含未释放的缓存)。-
在训练循环中定时打印这些值,绘制趋势图,可发现显存泄漏或碎片化问题。
-
PyTorch Profiler:如前所述,可记录每一步的显存使用、GPU执行时间线,配合
memory_snapshot导出显存分配详情,定位哪个算子分配了大量显存。 -
nvidia-ml-py库:通过编程方式获取显存、利用率等指标,可集成到自定义监控脚本中,发送到TensorBoard或Prometheus。
三、分布式训练监控
-
NVIDIA DCGM + Prometheus + Grafana:这是工业级方案。DCGM Exporter采集每个GPU的数十种指标,Prometheus抓取存储,Grafana展示成仪表盘。可设置告警,当显存或利用率异常时自动通知。
-
DeepSpeed Monitor:DeepSpeed内置了监控功能,可在配置文件中开启
tensorboard或wandb集成,自动记录显存、吞吐等。
四、关键监控项与告警
-
显存使用率:持续超过90%就需警惕碎片或泄漏。
-
GPU利用率(SM利用率):持续低于60%可能存在数据加载瓶颈或通信瓶颈。
-
显存带宽利用率:通过
nvidia-smi dmon观察mem读写速率,判断是否受限于带宽。 -
训练吞吐(tokens/sec):突然下降往往意味着速度瓶颈或硬件异常。
实践建议:在训练脚本中引入周期性打印torch.cuda.max_memory_allocated(),并在~/.bashrc中设置export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True以减少碎片。对于长时间训练,务必部署DCGM+Prometheus实现无人值守监控。
使用DeepSpeed进行全参数微调,配置文件的关键参数有哪些?¶
DeepSpeed配置文件(JSON格式)是控制训练行为的总开关。全参数微调时,为了兼顾显存和速度,通常使用ZeRO‑2或ZeRO‑3配合BF16。以下是一个典型配置,并解释关键参数:
{
"train_batch_size": 128, // 全局batch size
"gradient_accumulation_steps": 4, // 梯度累积步数
"optimizer": {
"type": "AdamW",
"params": {
"lr": 1e-5,
"betas": [0.9, 0.95],
"eps": 1e-8,
"weight_decay": 0.1
}
},
"scheduler": {
"type": "WarmupDecayLR",
"params": {
"warmup_min_lr": 0,
"warmup_max_lr": 1e-5,
"warmup_num_steps": 100,
"total_num_steps": 2000
}
},
"bf16": {"enabled": true}, // 使用BF16混合精度
"zero_optimization": {
"stage": 2, // ZeRO‑2
"offload_optimizer": {
"device": "cpu", // 优化器状态卸载到CPU
"pin_memory": true
},
"allgather_partitions": true,
"allgather_bucket_size": 5e8,
"overlap_comm": true, // 通信与计算重叠
"reduce_scatter": true,
"reduce_bucket_size": 5e8,
"contiguous_gradients": true
},
"gradient_clipping": 1.0,
"steps_per_print": 10,
"wall_clock_breakdown": false
}
关键参数解释:
-
train_batch_size与gradient_accumulation_steps:两者共同决定每GPU的micro‑batch大小。DeepSpeed会根据总GPU数自动计算micro‑batch大小。必须保证单卡micro‑batch不超显存。 -
optimizer:选择AdamW,设置学习率、betas、weight decay。微调时学习率通常为1e-5量级,betas沿用预训练(0.9, 0.95),weight decay 0.1是常用值。 -
bf16/fp16:强烈推荐bf16,免去loss scaling烦恼。若用fp16,需设置"fp16": {"enabled": true, "loss_scale": 0}启用动态loss scaling。 -
zero_optimization: stage:2或3。2分片优化器状态和梯度;3额外分片参数。70B模型通常必须stage 3。offload_optimizer:将优化器状态移到CPU,极大节省GPU显存,代价是轻微降速。显存紧张时可开启。overlap_comm:默认开启,将通信与反向传播计算重叠,提升效率。allgather_bucket_size&reduce_bucket_size:控制通信桶大小。默认5e8(500MB)是较好的平衡点,可根据网络带宽微调。-
contiguous_gradients:将梯度在内存中连续排列,提高通信效率,但会增加少量显存开销。 -
gradient_clipping:防止梯度爆炸,设为1.0。 -
scheduler:设置Warmup和总步数。DeepSpeed会自动按线性warmup到峰值,然后按余弦或线性衰减到0。这里用WarmupDecayLR指定warmup和总步数。
选择ZeRO-2还是ZeRO-3?
-
7B-13B模型,多卡显存充足(如8×A100 80GB),
stage 2通常足够,通信开销更小。 -
更大模型(如70B)或显存紧张时,必须用
stage 3。 -
stage 2+CPU offload可以在几乎不降速的情况下显著降低显存,是性价比很高的选项。
部署建议:将配置保存为ds_config.json,启动训练时指定--deepspeed ds_config.json。可先用deepspeed --num_gpus 8 train.py启动,DeepSpeed会自动处理分布式初始化。