从 SFT 到 LoRA
预训练让大模型变成"博学的实习生"——它懂很多,却不知道在你这儿该怎么干活。 监督微调(SFT)教它怎么干, LoRA 则让你用一张消费级显卡就能完成这件事。
预设读者:基础 ML、基础高数、基础线代。完全没碰过微调也行——我们从动机讲起。
为什么要再微调
把训练一个 LLM 类比成培养一个员工,大致长这样:
- 预训练 (Pretraining) — 像让 TA 读完整个互联网:百科、小说、代码、论文。得到一个什么都懂一点的通才。
- 监督微调 (SFT) — 像入职培训:用"问题 → 标准答案"的对子告诉 TA「我们公司客服就是这么说话的」。
- 偏好对齐 (RLHF / DPO) — 像绩效反馈:在多个答案里告诉 TA「这条比那条好」,磨细风格与安全性。
- Pretrain
tokens - SFT
对 - RLHF / DPO
偏好
本文关心第 2 步。LoRA 不是另一种新的微调流派——它是让"第 2 步"变得便宜的实现技巧。
§1.1SFT 到底是什么
SFT 用的损失函数和预训练一模一样——都是 next-token 交叉熵:
关键差别只有两点:
- 数据少而精:从万亿 token 的网络爬虫,缩到几千到几十万对人工写好的
(指令, 答案)。 - 只对答案算 loss:把 prompt 部分的 label 设成 (PyTorch 的
ignore_index),让模型学怎么答,而不是学怎么问。
鼠标悬停每个 token 看注释。灰色 + 删除线= 被 mask(不计 loss); 绿色 = 参与 cross-entropy。
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]
§1.2把全部参数都改一遍?显存先崩
给 参数的模型做全参数 SFT,每个可训练参数大约需要:
- 字节存权重(fp16 / bf16)
- 字节存梯度
- 字节存 Adam 的两个动量 (fp32)
合计 约 字节 / 参数。再加上前向激活、KV cache、梯度累积,实际显存往往是参数量的 倍。
结果: 模型全参 SFT 已经吃掉一张 A100 80G, 直接劝退个人玩家。
预估峰值显存:— GB
§2.1关键直觉 · 微调改的东西本来就"很瘦"
线性代数里,一个 矩阵的秩 (rank) 等于它的列向量张成空间的维度。秩越低,这个矩阵"压缩"得越狠——把整个空间压成一个低维子空间。
论文实验显示,在 的 GPT-3 上,把 约束到 秩 或 都几乎不掉点。 [Hu et al. 2021]
§2.2LoRA 的数学 · 把 拆成两小块
LoRA 把前向公式改写成:
它在做什么
原本你要训练一个 的"修改量" ,这是 个参数。LoRA 说:
参数量从 变成 。当 时:
- 原本:
- LoRA:
- 缩减 (仅一层;模型有上百层,整体缩减更夸张)
节省倍数:—
形状一眼看懂
又高又瘦, 又扁又长——它们的乘积是一个秩最多为 的"瘦扁"矩阵 。
§2.3训练时冻结,推理时合并
LoRA 与 Adapter 类方法最大的区别在于推理时可以合并权重,不引入任何额外延迟。
① 训练阶段
- 梯度只流过
- 优化器只为 维护动量
- 显存峰值 小于全参 SFT
② 部署 · 保留 adapter
- 不同任务 → 不同 LoRA 文件( MB)
- 一个 base + N 个 adapter
- 切任务零成本(换文件即可)
③ 部署 · 合并权重
- 把 加回 ,得到普通的 Transformer
- 推理延迟和原模型完全一样
- 代价:失去多任务切换的灵活性
§3.1动手 · 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.§3.2超参数 · 七寸在哪
α alpha
默认规则:。
控制 LoRA 对原模型的"影响强度"。把 翻倍 把学习率翻倍。 想要不依赖 的稳定尺度,可用 RSLoRA 的 。
target 目标层
原论文只动 。 实践派推荐 all-linear: 加 MLP 全套。
覆盖全部线性层效果最佳,代价是 LoRA 参数 。
lr 学习率
LoRA 通常 (比全参 SFT 高 倍)。
因为只有 在更新,原模型不会被冲坏,可以"放心上大 lr"。RL 阶段(DPO/GRPO)需降到 。
dropout
典型 ,常见 。
小数据集多加一点防过拟合,大数据集设 也行。
🍳 推荐入门配方
r: 16
lora_alpha: 32
lora_dropout: 0.05
target: all-linear
lr: 2e-4
epochs: 1-3
batch: 越大越好(用 grad-accum)§4.1LoRA 大家族
从 2021 的 LoRA 出发,一个迅速膨胀的工具谱系。每张卡一句话讲清卖点,详细看论文。
§4.2一句话总结
🧠 该带走的 5 个事实
- SFT 与预训练共享同一个损失,但只在 response token 上算。
- 全参数 SFT 显存 参数量 字节,根本吃不消。
- LoRA 假设 是低秩的,所以分解为 ,只训 个数。
- 零初始化保证训练起点等价于原模型,安全无扰动。
- 推理时可以合并权重,零延迟代价。