云镜收藏

稍后阅读

清单保存在当前浏览器,方便下次回来继续阅读。

清单还是空的

在笔记卡片或正文页点击“加入稍后阅读”即可收藏。

卷2 · LLM 内功

2.6 参数高效微调:LoRA 与 QLoRA

约 7 分钟阅读
全书目录

展开本章目录与同卷章节1. 显存对账:为什么需要 PEFT2. LoRA 原理(低秩假设的胜利)3. 手写 LoRA 层(peft 的内核就这么多)4. QLoRA:4bit 底座上的 LoRA5. 训练完之后:merge 还是留 adapter6. 其他 PEFT 一句话扫盲7. 自测题8. 动手作业(寒假里程碑第二段)案例 · 数值:算清一次 LoRA 的参数账9. 延伸资源同卷章节2.1 语言模型前史:n-gram → RNN → 注意力(20 分钟走完 30 年)2.2 BPE tokenizer 原理与实现2.3 GPT 架构工程版:从 TinyGPT 到 nanoGPT2.4 预训练:数据、Scaling Law 与训练稳定性2.5 SFT 与指令微调实战2.6 参数高效微调:LoRA 与 QLoRA2.7 对齐技术:RLHF → DPO → GRPO2.8 推理优化与 vLLM:把模型变成服务2.9 架构前沿:MoE、长上下文与注意力变体2.10 小模型时代的科研机会:1B–7B 甜点区

本章目标:彻底吃透 LoRA——你未来两年用得最多的训练技术。学完标准:能手写 LoRA 层,会算它的显存账,知道 rank/alpha/target_modules 怎么选,能用 QLoRA 在单张 24G 卡上微调 7B。

1. 显存对账:为什么需要 PEFT

0.2 章的公式:全参微调显存 ≈ 16 字节/参数(bf16 权重 2 + 梯度 2 + AdamW 状态 8 + fp32 主权重 4)。7B 模型全参 ≈ 112GB——单卡无解。

LoRA 的账:冻结权重(只存 2 字节/参数,bf16),训练的只有低秩增量(参数量 ~0.1–1%)。7B 模型:权重 14GB + 少量梯度和优化器状态 ≈ 24GB 卡能跑。这就是消费级卡微调时代开启的原因。

2. LoRA 原理(低秩假设的胜利)

核心假设(0.1 章 SVD 的直接应用):微调引起的权重变化 ΔW\Delta W 近似低秩——不需要动整个 d×kd\times k 矩阵,用两个瘦矩阵的乘积表达增量:

h=Wx+ΔWx=Wx+BAx,B∈Rd×r, A∈Rr×k, r≪min⁡(d,k)h = Wx + \Delta W x = Wx + BAx, \quad B \in \mathbb{R}^{d \times r},\ A \in \mathbb{R}^{r \times k},\ r \ll \min(d,k)

参数量从 d×kd\times k 降到 r(d+k)r(d+k)。例:4096×4096 的矩阵,r=16 → 从 16.8M 降到 131K(0.8%)。

初始化的讲究:AA 高斯初始化、BB 置零——保证训练起点 ΔW=0\Delta W = 0,模型从预训练状态平滑出发(不破坏已学能力),这是”微调不该踢翻原解”哲学(0.1 章 §4.3)的架构化实现。

缩放系数 α:实际增量是 αrBA\frac{\alpha}{r} BA。α 固定(常取 2r),调 r 时学习率尺度自动稳定——你只需记配比 α = 2r。

3. 手写 LoRA 层(peft 的内核就这么多)

class LoRALinear(nn.Module):
    def __init__(self, base: nn.Linear, r=16, alpha=32):
        super().__init__()
        self.base = base                     # 冻结的原层
        for p in self.base.parameters(): p.requires_grad = False
        self.A = nn.Parameter(torch.randn(r, base.in_features) * 0.01)
        self.B = nn.Parameter(torch.zeros(base.out_features, r))   # B 置零!
        self.scale = alpha / r
    def forward(self, x):
        return self.base(x) + self.scale * (x @ self.A.T @ self.B.T)

实战用 peft 库:

from peft import LoraConfig, get_peft_model
cfg = LoraConfig(r=16, lora_alpha=32, lora_dropout=0.05,
                 target_modules=["q_proj","k_proj","v_proj","o_proj","gate_proj","up_proj","down_proj"])
model = get_peft_model(model, cfg)
model.print_trainable_parameters()   # trainable params: ~0.5% || 100%

选择指南:

  • target_modules:现代默认全挂(attention 四件套 + MLP 三件套)。早期只挂 q/v,实验证明 MLP 也很重要;
  • rank:数据少/任务简单 r=8–16;任务复杂/数据多 r=32–64;r 加大要配合数据量,否则过拟合更快;
  • 学习率:LoRA 比全参大 10 倍左右(1e-4 ~ 2e-4)——只有增量在学习,敢迈大步。

4. QLoRA:4bit 底座上的 LoRA

LoRA 之后显存大头是冻结的权重本体(7B bf16 = 14GB)。QLoRA(2023)把它也压下去:

  1. NF4 量化:4bit 存储底座权重(NF4 是为正态分布权重定制的 4bit 数据类型,比均匀 int4 更贴分布);
  2. 反量化计算:前向时按需反量化到 bf16 做矩阵乘(存储 4bit,计算 bf16);
  3. 双重量化 + 分页优化器:量化常数本身再量化;显存峰值时优化器状态分页到内存——防 OOM 的保险。

结果:7B 模型 4bit 权重 ≈ 3.5GB + LoRA 参数与状态 + 激活 → 单张 24G 卡舒服微调,甚至 16G 卡能跑 7B。代价:训练速度慢 20–30%(反量化开销),精度损失实验上可忽略。LLaMA-Factory 里就是 quantization_bit: 4 一行配置。

5. 训练完之后:merge 还是留 adapter

  • merge_and_unload():把 BA 乘积加回 W,得到独立模型——部署简单、推理零开销;但每个任务一个完整副本(7B bf16 = 14GB/个);
  • 保留 adapter 文件(几十 MB):同一底座动态挂载多任务 adapter(多租户服务的做法);
  • 上传 HF/ModelScope:LoRA 权重 + base 模型名 + 配置即可复现——寒假里程碑的交付形态。

常见坑清单:① 训练后忘 merge 或忘写 base model 名,加载直接报错;② dtype 混用(4bit 底座 + fp16 LoRA 会静默精度错乱,统一 bf16);③ target_modules 拼错名字静默不生效——print_trainable_parameters() 是 0 时及时发现;④ merge 时忘了 eval() 和dtype转换,输出乱码。

6. 其他 PEFT 一句话扫盲

Adapter(串联小瓶颈层,推理有额外延迟,已过气)|Prefix-tuning / P-tuning v2(在每层前加可学习软 token,全量推理开销,已边缘化)|DoRA(把权重分解为幅度+方向,方向上做 LoRA,小幅提升,可关注)。LoRA 家族是唯一的主流——理解它就理解了整个类别。

7. 自测题

  1. LoRA 的低秩假设是什么?为什么 B 初始化为零?(ΔW≈低秩;保证起点等于原模型)
  2. α/r 的作用?(学习率尺度的稳定器;α=2r 是惯例)
  3. 算账:Qwen2.5-7B(d=3584, 28 层)全挂 r=16 的 LoRA,可训练参数约多少?(7 个模块 × 2×16×(3584+3584)×28 ≈ 22M,约 0.3%——量级对即可)
  4. QLoRA 三件套?(NF4 量化底座 / bf16 反量化计算 / 双重量化+分页优化器)
  5. LoRA 学习率为什么比全参大 10 倍?(训练参数少一个数量级、B 起点为零——梯度小步长小,需放大)
  6. 多任务部署为什么留 adapter 不 merge?(几十 MB/任务 vs 14GB/任务;动态挂载)
  7. r 加大但不加数据的后果?(可训练容量↑ + 数据不变 → 过拟合加快;配合早停或降 r)

8. 动手作业(寒假里程碑第二段)

  1. 用第 3 节手写 LoRA 给一个预训练 ViT 的 attention 层加增量,在 CIFAR 微调,与全参微调对比精度和可训练参数量;
  2. LLaMA-Factory 用 QLoRA 微调 Qwen2.5-7B(租一张 4090):接续 2.5 的数据集,跑通 4bit 训练;
  3. 合并 adapter 并用 vLLM 起服务(2.8 预告,先跑通再说);
  4. 上传 HF/ModelScope:README 写清 base 模型、数据构造、训练配置、复现命令——这是你作品集的第一个大件;
  5. 实验记录:r=8/16/32 三档的验证 loss 曲线——亲手看到 rank 与过拟合的关系。

案例 · 数值:算清一次 LoRA 的参数账

案例:7B 模型全挂 r=16 的 LoRA,可训练参数是多少

LLaMA 式 7B:L=32,d=4096,每层挂 7 个线性层(q/k/v/o + gate/up/down),r=16。

每个模块:A(16×4096)+B(4096×16)=131,072A(16\times4096) + B(4096\times16) = 131{,}072 参数。

全模型:131,072×7×32≈29.4M≈131{,}072 \times 7 \times 32 \approx 29.4\text{M} \approx 7B 的 0.42%。

训练状态(bf16 梯度 + fp32 AdamW 双动量):29.4M×(2+8) B≈0.29 GB29.4\text{M} \times (2+8)\,\text{B} \approx 0.29\ \text{GB}——对照全参微调约 70 GB 的梯度+优化器状态(7.1 的账),这就是 24G 卡能跑 7B 的完整解释。

验证手段:model.print_trainable_parameters() 应显示 ~29.4M / 0.42%——显示 0% 说明 target_modules 拼错了(7.2 案例群的经典静默失败)。

9. 延伸资源

  • LoRA 论文(Hu et al., 2021)/ QLoRA 论文(Dettmers et al., 2023)——必读,各精读一遍
  • peft 官方文档——API 速查
  • Sebastian Raschka《LoRA 实操指南》博客系列——超参选择的工程视角