【Bug已解决】Inquiry About Two-Stage QLoRA Fine-Tuning 解决方案
【Bug已解决】Inquiry About Two-Stage QLoRA Fine-Tuning 解决方案
一、现象长什么样
你在做 QLoRA 微调时,看到别人提到Two-Stage QLoRA(两阶段 QLoRA 微调),但不清楚它到底是什么、和单次 QLoRA 有何区别、怎么落地。常见困惑:
- “两阶段”是哪两阶段?先全量量化训练一次、再 LoRA 一次?还是先大学习率后小学习率?还是先用 4-bit 训、再换更高质量精度?
- 两阶段会不会比单次 QLoRA 更好,还是纯属多此一举?
- 第二阶段要不要重新量化、要不要换 adapter、会不会丢第一阶段成果?
- 显存/时间成本怎么算?
本文把 Two-Stage QLoRA 的几种常见含义、适用场景、正确落地流程讲清,并给可运行示例。
二、背景
QLoRA = 4-bit(NF4)量化基础模型 + 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 合并进去,原适配丢失。
根因 D:lr/步数规划不当
两阶段 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(), lr=lr) 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(r=4, target_modules=["lin"]) model = get_peft_model(base, cfg) # 阶段一:大 lr 粗适配 train_stage(model, lr=1e-3, steps=20) # 承接:保存 adapter sd1 = get_peft_model_state_dict(model) # 阶段二:小 lr 精调(承接同一 adapter) train_stage(model, lr=1e-4, steps=20) 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 继续训练(更小 lr):
from peft import get_peft_model, LoraConfig, set_peft_model_state_dict # 阶段一 model = get_peft_model(base, LoraConfig(r=4, target_modules=["lin"])) train(model, lr=1e-3, epochs=1) adapter_sd = get_peft_model_state_dict(model) torch.save(adapter_sd, "stage1_adapter.pt") # 阶段二:重新加载 base + 同一 adapter,换小 lr model2 = get_peft_model(base, LoraConfig(r=4, target_modules=["lin"])) set_peft_model_state_dict(model2, torch.load("stage1_adapter.pt")) train(model2, lr=1e-4, epochs=2) # 承接阶段一成果精调若是“精度两阶段”(4-bit→高精度回蒸):
# 阶段一:4-bit QLoRA 训,得到 adapter stage1 = get_peft_model(base_4bit, LoraConfig(r=4, target_modules=["lin"])) train(stage1, lr=1e-3) stage1.merge_and_unload() # 合并回 4-bit 权重 # 阶段二:把合并后的权重以 bf16 重新加载,再做第二阶段(或把 adapter 应用到 fp16 模型) base_bf16 = base.half() # 更高质量 stage2 = get_peft_model(base_bf16, LoraConfig(r=4, target_modules=["lin"])) # 可选:把阶段一 adapter 迁移(需同结构) train(stage2, lr=1e-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(r=4)) model.set_adapter("stage1") train(model, lr=1e-3) model.add_adapter("stage2", LoraConfig(r=4)) model.set_adapter("stage2") # 阶段二可继承阶段一权重初始化(set_peft_model_state_dict)6.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(r=4, 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(r=4, 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 再换更高质量重载,避免成果丢失。
