【Bug已解决】Inquiry About Two-Stage QLoRA Fine-Tuning 解决方案一、现象长什么样你在做 QLoRA 微调时看到别人提到Two-Stage QLoRA两阶段 QLoRA 微调但不清楚它到底是什么、和单次 QLoRA 有何区别、怎么落地。常见困惑“两阶段”是哪两阶段先全量量化训练一次、再 LoRA 一次还是先大学习率后小学习率还是先用 4-bit 训、再换更高质量精度两阶段会不会比单次 QLoRA 更好还是纯属多此一举第二阶段要不要重新量化、要不要换 adapter、会不会丢第一阶段成果显存/时间成本怎么算本文把 Two-Stage QLoRA 的几种常见含义、适用场景、正确落地流程讲清并给可运行示例。二、背景QLoRA 4-bitNF4量化基础模型 LoRA 微调省显存。所谓 “Two-Stage QLoRA” 在社区里主要指两种实践含义 A先“预热/对齐”再“精调”训练策略两阶段阶段一较大学习率、较短步数让 LoRA 快速对齐到目标分布粗略适配阶段二较小学习率、较长步数精细打磨类似 warmup decay但跨两次训练会话。 这样两阶段比单次固定 lr 往往更稳。含义 B先 4-bit QLoRA再“回蒸”到更高精度精度两阶段阶段一在 4-bit 量化模型上加 LoRA 训练得到 adapter阶段二把阶段一的 LoRA 合并merge_and_unload回权重再把这个“已适配的权重”用更高精度如 bf16重新量化/加载做第二阶段更高质量的微调或把 adapter 迁移到 fp16 模型上再训。 这种适合“4-bit 训不动、但想先用 4-bit 找方向再用更高质量收尾”。无论哪种关键都是第二阶段要正确承接第一阶段的成果保存/加载 adapter 或合并后的权重否则两阶段退化成两次独立训练甚至互相覆盖。三、根因为什么容易做错根因 A第二阶段没承接第一阶段成果最常见。两阶段各自从头训等于两次独立 QLoRA浪费且可能互相覆盖。必须 save/load adapter 或合并后承接。根因 B混淆“两阶段训练”和“两次独立训练”两阶段应是同一适配任务的连续两阶段共享方向不是换任务重训。根因 C第二阶段重新量化丢信息若阶段二用新量化没把阶段一的 adapter 合并进去原适配丢失。根因 Dlr/步数规划不当两阶段 lr 都设一样等于单次或阶段二 lr 太大破坏阶段一成果。根因小结Two-Stage QLoRA 阶段一粗适配 阶段二精调或 4-bit→高精度回蒸关键第二阶段必须承接第一阶段成果save/load adapter 或合并权重修复正确规划 lr/步数 用 state_dict 承接 避免重新量化丢信息。四、最小可运行复现下面脚本演示“两阶段 QLoRA训练策略两阶段 正确承接 adapter”import torch import torch.nn as nn from peft import get_peft_model, LoraConfig, get_peft_model_state_dict class Tiny(nn.Module): def __init__(self): super().__init__() self.lin nn.Linear(16, 8) def forward(self, x): return self.lin(x) def train_stage(model, lr, steps): opt torch.optim.AdamW(model.parameters(), lrlr) x torch.randn(8, 16) y torch.randn(8, 8) for _ in range(steps): opt.zero_grad() ((model(x) - y) ** 2).mean().backward() opt.step() def demo(): torch.manual_seed(0) base Tiny() cfg LoraConfig(r4, target_modules[lin]) model get_peft_model(base, cfg) # 阶段一大 lr 粗适配 train_stage(model, lr1e-3, steps20) # 承接保存 adapter sd1 get_peft_model_state_dict(model) # 阶段二小 lr 精调承接同一 adapter train_stage(model, lr1e-4, steps20) sd2 get_peft_model_state_dict(model) # 验证两阶段确实在阶段一基础上继续参数变化了 diff sum((a - b).abs().sum().item() for a, b in zip(sd1.values(), sd2.values())) print(两阶段间 adapter 参数变化量:, round(diff, 4), (应0, 说明承接并继续训练)) if __name__ __main__: demo()运行后阶段二在阶段一 adapter 基础上继续更新参数变化0证明“承接”正确。五、解决方案第一层最小直接修复两阶段核心阶段一训练 → 保存 adapter → 阶段二加载同一 adapter 继续训练更小 lrfrom peft import get_peft_model, LoraConfig, set_peft_model_state_dict # 阶段一 model get_peft_model(base, LoraConfig(r4, target_modules[lin])) train(model, lr1e-3, epochs1) adapter_sd get_peft_model_state_dict(model) torch.save(adapter_sd, stage1_adapter.pt) # 阶段二重新加载 base 同一 adapter换小 lr model2 get_peft_model(base, LoraConfig(r4, target_modules[lin])) set_peft_model_state_dict(model2, torch.load(stage1_adapter.pt)) train(model2, lr1e-4, epochs2) # 承接阶段一成果精调若是“精度两阶段”4-bit→高精度回蒸# 阶段一4-bit QLoRA 训得到 adapter stage1 get_peft_model(base_4bit, LoraConfig(r4, target_modules[lin])) train(stage1, lr1e-3) stage1.merge_and_unload() # 合并回 4-bit 权重 # 阶段二把合并后的权重以 bf16 重新加载再做第二阶段或把 adapter 应用到 fp16 模型 base_bf16 base.half() # 更高质量 stage2 get_peft_model(base_bf16, LoraConfig(r4, target_modules[lin])) # 可选把阶段一 adapter 迁移需同结构 train(stage2, lr1e-4)六、解决方案第二层结构性改进6.1 lr 规划阶段一大、阶段二小STAGE_PLAN [ {lr: 1e-3, epochs: 1}, # 粗适配 {lr: 1e-4, epochs: 2}, # 精调 ]6.2 用adapter_name区分阶段model.add_adapter(stage1, LoraConfig(r4)) model.set_adapter(stage1) train(model, lr1e-3) model.add_adapter(stage2, LoraConfig(r4)) model.set_adapter(stage2) # 阶段二可继承阶段一权重初始化set_peft_model_state_dict6.3 避免重复量化丢信息# 阶段二若换精度先 merge 阶段一 adapter 再重新加载确保承接 stage1.merge_and_unload()七、解决方案第三层断言 / CI 守护import torch import pytest from peft import get_peft_model, LoraConfig, get_peft_model_state_dict def test_two_stage_inherits(tmp_path): model get_peft_model(Tiny(), LoraConfig(r4, target_modules[lin])) train_stage(model, 1e-3, 10) sd1 get_peft_model_state_dict(model) torch.save(sd1, tmp_path / s1.pt) model2 get_peft_model(Tiny(), LoraConfig(r4, target_modules[lin])) set_peft_model_state_dict(model2, torch.load(tmp_path / s1.pt)) train_stage(model2, 1e-4, 10) sd2 get_peft_model_state_dict(model2) diff sum((a - b).abs().sum().item() for a, b in zip(sd1.values(), sd2.values())) assert diff 0, 阶段二应在阶段一基础上继续 def test_stage2_lr_smaller(): assert STAGE_PLAN[1][lr] STAGE_PLAN[0][lr], 阶段二 lr 应更小 def test_merged_weights_finite(model): model.merge_and_unload() assert torch.isfinite(model.base_layer.weight if hasattr(model, base_layer) else model.lin.weight).all()CI 跑这三条两阶段承接正确性被守住。八、排查清单Two-Stage QLoRA 做错时查阶段二承接阶段一成果了吗save/load adapter 或 merge 后承接别各自从头。是同任务连续两阶段吗不是换任务重训。lr 阶段一大、阶段二小吗否则等于单次或破坏阶段一。精度两阶段丢信息了吗4-bit→高精度先 merge 再重载。重新量化覆盖了吗阶段二换精度前先 merge 阶段一 adapter。adapter 参数阶段二变化了吗断言 0 确认承接并继续。CI 测了承接吗守护两阶段不退化成两次独立训练。九、小结“Inquiry About Two-Stage QLoRA Fine-Tuning” 的答案是Two-Stage QLoRA 阶段一粗适配大 lr 阶段二精调小 lr或 4-bit 训完 merge 后换更高精度回蒸。关键阶段二必须承接阶段一成果save/load adapter 或合并权重否则退化为两次独立训练训练策略两阶段用不同 lr大→小精度两阶段先 merge 再换精度重载避免丢信息用“阶段二在阶段一基础上继续更新参数变化0 阶段二 lr 更小 合并有限”的断言守护。一句话Two-Stage QLoRA 的核心是“承接”阶段一训完保存 adapter或 merge阶段二加载同一 adapter 用小 lr 精调精度两阶段则先 merge 再换更高质量重载避免成果丢失。