A pedagogical primer · 2026 · 05

从 SFT 到 LoRA

预训练让大模型变成"博学的实习生"——它懂很多,却不知道在你这儿该怎么干活。 监督微调(SFT)教它怎么干, LoRA 则让你用一张消费级显卡就能完成这件事。

预设读者:基础 ML、基础高数、基础线代。完全没碰过微调也行——我们从动机讲起。

10 节 · 5 个手写交互演示 · 全部数据点附原始来源 · 最后更新 2026-05-19

$10\,000\times$ 可训练参数减少
$3\times$ GPU 显存节省
35 MB 取代 350 GB 的 checkpoint
$0$ ms 推理延迟(可合并)
阅读路径建议

没碰过微调:从 §1 顺读到 §4,把每个 demo 玩一遍; 只想跑代码:直接跳 §6; 只想知道选什么 $r$ 和 $\alpha$:去 §7

为什么要再微调

把训练一个 LLM 类比成培养一个员工,大致长这样:

  1. 预训练 (Pretraining) — 像让 TA 读完整个互联网:百科、小说、代码、论文。得到一个什么都懂一点的通才。
  2. 监督微调 (SFT) — 像入职培训:用"问题 → 标准答案"的对子告诉 TA「我们公司客服就是这么说话的」。
  3. 偏好对齐 (RLHF / DPO) — 像绩效反馈:在多个答案里告诉 TA「这条比那条好」,磨细风格与安全性。
Pretrain
$\sim 10^{12}$ tokens
SFT
$\sim 10^{4}\!-\!10^{6}$ 对
RLHF / DPO
$\sim 10^{4}\!-\!10^{5}$ 偏好

本文关心第 2 步。LoRA 不是另一种新的微调流派——它是让"第 2 步"变得便宜的实现技巧

§1SFT 到底是什么

SFT 用的损失函数和预训练一模一样——都是 next-token 交叉熵:

$$ \mathcal{L}_{\text{SFT}} \;=\; - \sum_{t \,\in\, \text{response}} \log P_\theta\!\left(y_t \,\middle|\, y_{\lt t},\; x\right) $$

关键差别只有两点:

  • 数据少而精:从万亿 token 的网络爬虫,缩到几千到几十万对人工写好的 (指令, 答案)
  • 只对答案算 loss:把 prompt 部分的 label 设成 $-100$(PyTorch 的 ignore_index),让模型学怎么答,而不是学怎么问
Demo 1 · Loss-mask 可视化

鼠标悬停每个 token 看注释。灰色 + 删除线= 被 mask(不计 loss); 绿色 = 参与 cross-entropy。

参与 loss:0 tokens
被 mask:0 tokens
labels = input_ids.clone()
# Mask the prompt portion so CE loss skips it
labels[: prompt_len] = -100

常见 SFT 数据格式 · Alpaca 风

{
  "instruction": "用一句话解释什么是低秩矩阵。",
  "input": "",
  "output": "低秩矩阵的列空间维度远小于其形状所允许的最大维度。"
}

Stanford Alpaca 用 52K 条这样的样本就把 LLaMA 7B 调出了类 ChatGPT 风格。 [source]

§2把全部参数都改一遍?显存先崩

给 $N$ 参数的模型做全参数 SFT,每个可训练参数大约需要:

  • $2$ 字节存权重(fp16 / bf16)
  • $2$ 字节存梯度
  • $8$ 字节存 Adam 的两个动量 $m, v$(fp32)

合计 约 $16$ 字节 / 参数。再加上前向激活、KV cache、梯度累积,实际显存往往是参数量的 $20$ 倍

结果:$7\mathrm{B}$ 模型全参 SFT 已经吃掉一张 A100 80G,$175\mathrm{B}$ 直接劝退个人玩家

Demo 2 · 显存估算器

滑动模型规模与优化器选项,看四类显存如何堆起来。

权重
梯度
优化器
激活/其他

预估峰值显存: GB

§3关键直觉 · 微调改的东西本来就"很瘦"

线性代数里,一个 $d \times k$ 矩阵的秩 (rank) 等于它的列向量张成空间的维度。秩越低,这个矩阵"压缩"得越狠——把整个空间压成一个低维子空间。

LoRA 论文的大胆假设

预训练已经给了模型一个非常好的起点。SFT 让权重的变化量 $\Delta W$ "落在一个内在维度极低的子空间"—— 也就是说,$\Delta W$ 本身就接近一个低秩矩阵

论文实验显示,在 $175\mathrm{B}$ 的 GPT-3 上,把 $\Delta W$ 约束到 秩 $1$ 或 $2$ 都几乎不掉点。 [Hu et al. 2021]

Demo 3 · 低秩重构

滑动秩 $r$,左边是原始的 $64 \times 64$ "权重图",右边是只保留前 $r$ 个奇异值后的重构。

原始 $W$
秩-$r$ 近似
压缩率 重构误差

§4LoRA 的数学 · 把 $\Delta W$ 拆成两小块

LoRA 把前向公式改写成:

$$ h \;=\; W_0\, x \;+\; \Delta W\, x \;=\; W_0\, x \;+\; \frac{\alpha}{r}\, B\,A\, x $$
$W_0 \in \mathbb{R}^{d\times k}$ 预训练权重,训练时冻结 $A \in \mathbb{R}^{r\times k}$ 高斯初始化 $B \in \mathbb{R}^{d\times r}$ 零初始化 $\Rightarrow$ 起步时 $BA = 0$,不扰动原模型 $r \ll \min(d,\, k)$ 通常 $4 \sim 64$ $\alpha / r$ 缩放因子,作用类似学习率

它在做什么

原本你要训练一个 $d \times k$ 的"修改量" $\Delta W$,这是 $dk$ 个参数。LoRA 说:

核心 trick

既然 $\Delta W$ 反正是低秩的,那我干脆只让你训两个低秩因子 $B$ 与 $A$,乘起来的 $BA$ 自动是秩 $\le r$ 的矩阵。

参数量从 $dk$ 变成 $r(d+k)$。当 $d = k = 4096,\; r = 8$ 时:

  • 原本:$4096 \times 4096 \,=\, 16\,777\,216$
  • LoRA:$8 \times (4096 + 4096) \,=\, 65\,536$
  • 缩减 $256\times$(仅一层;模型有上百层,整体缩减更夸张)
Demo 4 · 参数节省计算器

滑动 $d, k, r$,实时看 LoRA 占了全参的多大比例。

全参 $\Delta W$
LoRA $BA$

节省倍数:

形状一眼看懂

$W_0$$d \times k$
$+$
$B$$d \times r$
$\times$
$A$$r \times k$

$B$ 又高又瘦,$A$ 又扁又长——它们的乘积是一个秩最多为 $r$ 的"瘦扁"矩阵 $\Delta W$。

§5训练时冻结,推理时合并

LoRA 与 Adapter 类方法最大的区别在于推理时可以合并权重,不引入任何额外延迟。

① 训练阶段

$W_0$
冻结
$B$
$A$
  • 梯度只流过 $A, B$
  • 优化器只为 $A, B$ 维护动量
  • 显存峰值 $\approx 3 \sim 5\times$ 小于全参 SFT

② 部署 · 保留 adapter

$W_0$
$+$
$\frac{\alpha}{r}BA$
adapter
  • 不同任务 → 不同 LoRA 文件($\sim 10\!-\!100$ MB)
  • 一个 base + N 个 adapter
  • 切任务零成本(换文件即可)

③ 部署 · 合并权重

$W' = W_0 + \frac{\alpha}{r}BA$
  • 把 $BA$ 加回 $W_0$,得到普通的 Transformer
  • 推理延迟和原模型完全一样
  • 代价:失去多任务切换的灵活性

§6动手 · 20 行 PEFT + TRL

来源:HF PEFT 文档 · TRL SFTTrainer · QLoRA

from datasets import load_dataset
from peft import LoraConfig
from trl import SFTTrainer, SFTConfig

dataset = load_dataset("tatsu-lab/alpaca", split="train")

peft_config = LoraConfig(
    r=8,                                  # 秩,常用 8/16/32
    lora_alpha=16,                        # alpha = 2r 经验法则
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
    target_modules=["q_proj", "v_proj"],  # 或 "all-linear"
)

trainer = SFTTrainer(
    model="meta-llama/Llama-3.2-1B",
    train_dataset=dataset,
    args=SFTConfig(
        output_dir="lora-alpaca",
        num_train_epochs=1,
        per_device_train_batch_size=2,
        learning_rate=2e-4,               # LoRA 用 ~10x 于全参微调
    ),
    peft_config=peft_config,
)
trainer.train()
import torch
import torch.nn as nn

class LoRALinear(nn.Module):
    def __init__(self, base: nn.Linear, r=8, alpha=16):
        super().__init__()
        self.base = base
        for p in self.base.parameters():
            p.requires_grad = False            # freeze W0
        d, k = base.out_features, base.in_features
        self.A = nn.Parameter(torch.randn(r, k) * 0.01)  # Gaussian init
        self.B = nn.Parameter(torch.zeros(d, r))         # zero init -> BA = 0
        self.scale = alpha / r

    def forward(self, x):
        return self.base(x) + self.scale * (x @ self.A.T @ self.B.T)
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
from peft import prepare_model_for_kbit_training, LoraConfig, get_peft_model
import torch

bnb = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",                # NF4 quantization
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=True,           # extra savings
)

model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-3.2-8B", quantization_config=bnb)
model = prepare_model_for_kbit_training(model)
model = get_peft_model(model, LoraConfig(
    r=16, lora_alpha=32,
    target_modules="all-linear",
    task_type="CAUSAL_LM"))

# 8B model + QLoRA fits on a single 16 GB consumer card.

§7超参数 · 七寸在哪

r

最重要的旋钮。$8 / 16 / 32$ 是 90% 任务的最佳点。

$r$ 不是越大越好——研究发现 $r = 256$ 偶尔最优,但 $r = 8$ 已经能拿到 $\ge 95\%$ 的性能。

Raschka 实验

α alpha

默认规则:$\alpha = 2r$

$\alpha/r$ 控制 LoRA 对原模型的"影响强度"。把 $\alpha$ 翻倍 $\approx$ 把学习率翻倍。 想要不依赖 $r$ 的稳定尺度,可用 RSLoRA 的 $\alpha/\sqrt{r}$。

target 目标层

原论文只动 $W_q, W_v$。 实践派推荐 all-linear:$q, k, v, o$ 加 MLP 全套。

覆盖全部线性层效果最佳,代价是 LoRA 参数 $\times 4 \sim 5$。

lr 学习率

LoRA 通常 $1\mathrm{e}{-4} \sim 3\mathrm{e}{-4}$(比全参 SFT 高 $10$ 倍)。

因为只有 $BA$ 在更新,原模型不会被冲坏,可以"放心上大 lr"。RL 阶段(DPO/GRPO)需降到 $5\mathrm{e}{-6}$。

dropout

典型 $0.0 \sim 0.1$,常见 $0.05$。

小数据集多加一点防过拟合,大数据集设 $0$ 也行。

🍳 推荐入门配方

r:            16
lora_alpha:   32
lora_dropout: 0.05
target:       all-linear
lr:           2e-4
epochs:       1-3
batch:        越大越好(用 grad-accum)

§8LoRA 大家族

从 2021 的 LoRA 出发,一个迅速膨胀的工具谱系。每张卡一句话讲清卖点,详细看论文。

QLoRA

2023 · Dettmers

把 base 模型量化到 $4$-bit NF4,再在上面跑 LoRA。$65$B 模型塞进单张 $48$GB 显卡。

论文 →

DoRA

2024 · Liu (ICML Oral)

把权重拆成 方向 $+$ 幅度,只对方向做 LoRA。准确率显著高于 LoRA,零推理开销。

论文 →

LoRA+

2024 · Hayou

给 $B$ 用 比 $A$ 大 $16 \sim 24$ 倍的学习率,理论推导支持,约 $2\times$ 训练速度。

论文 →

VeRA

2024 · Kopiczko

$A, B$ 全部 冻结成随机矩阵,只训两个小缩放向量。参数量再减一个数量级。

论文 →

AdaLoRA

2023 · Zhang

用 SVD 形式参数化,按重要性自适应分配 $r$ 给不同层,预算相同时质量更高。

论文 →

RSLoRA

2023 · Kalajdzievski

把缩放因子从 $\alpha/r$ 改成 $\alpha/\sqrt{r}$,使大 $r$ 时训练更稳定。

PEFT 实现 →

§9一句话总结

SFT 是用问答对教大模型干活;
LoRA 让你只训两小块矩阵就完成这件事—— 参数省 $10\,000$ 倍,效果几乎一样。

🧠 该带走的 5 个事实

  1. SFT 与预训练共享同一个损失,但只在 response token 上算。
  2. 全参数 SFT 显存 $\approx$ 参数量 $\times\, 16 \sim 20$ 字节,根本吃不消。
  3. LoRA 假设 $\Delta W$ 是低秩的,所以分解为 $BA$,只训 $r(d+k)$ 个数。
  4. $B$ 零初始化保证训练起点等价于原模型,安全无扰动。
  5. 推理时可以合并权重,零延迟代价

📚 想再深入