SFT 微调实战:LoRA / QLoRA / 全参微调对比
SFT 微调实战:LoRA / QLoRA / 全参微调对比
《大模型知识与部署》系列 · No.07 / 35
适合人群:AI 工程师、后端开发
阅读时间:约 28 分钟

写在前面
上一篇我们看了预训练这件"上层精英玩"的事。从这一篇开始,我们进入大多数 AI 工程师真正会亲自上手的领域——SFT(Supervised Fine-Tuning,监督微调)。
预训练造就了一个"通才大脑",但它有两个问题:
- 只会接话,不会对话——Base 模型听到"你好",可能回一段"你好的英文是 hello…“,而不是"你好,请问有什么可以帮您?”
- 不懂你的业务——它不知道你公司的客服流程、API 规范、术语表
SFT 就是解决这两件事的关键工程动作。
如果你做过相关工作,下面这些问题应该不陌生:
- 用 LoRA 微调,效果比全参差多少?什么时候必须上全参?
- 单卡 24GB 显存能不能微调 70B 模型?
- 训练数据要多少?10 条够吗?1 万条?
- 微调后模型"忘了"原来的能力,怎么办?
- LoRA 训完一堆,能不能像插件一样动态切换?
读完本文你将能:
- 选对微调方法(全参 / LoRA / QLoRA / 哪个)
- 估算微调一个 N B 模型的资源需求
- 上手运行一个完整的 QLoRA 训练脚本
- 避免 5 个最常见的微调坑
- 决策"是否需要 SFT"——很多时候 RAG 或 Prompt 就够
我们开始。
一、SFT 在 LLM 工程栈中的位置
1.1 Base → Chat:从「会接话」到「会对话」
回忆第 1 篇我们画的全链路:
预训练 (Base) → SFT (Instruct) → 对齐 (DPO/RLHF) → 可用模型
每个阶段干一件事:
- Base 模型:知道"语言"是什么。喂"今天天气",会接"很好"或者"如何?"
- SFT (Instruct) 模型:知道"指令"是什么。喂"总结这段话",会给出总结而不是接话
- 对齐模型:知道"什么回答更好"。会拒绝有害请求、更礼貌、更有用
HuggingFace 上模型名常见后缀:
| 后缀 | 含义 |
|---|---|
-Base / 无后缀 |
仅做了预训练 |
-Instruct / -Chat / -SFT |
做过 SFT |
-DPO / -RLHF / -Aligned |
做过对齐 |
生产环境用模型,至少要 Instruct 版本——Base 模型直接上线就是个灾难。
1.2 工程师做 SFT 的常见场景
| 场景 | 目标 | 典型数据量 |
|---|---|---|
| 垂直领域适配 | 法律/医疗/金融术语认知 | 1-10 万条 |
| 业务规则注入 | 客服回复风格、产品 FAQ | 5K-5 万条 |
| 格式约束 | 强制 JSON 输出、特定模板 | 1K-1 万条 |
| Tool Use 训练 | 教模型用特定工具 | 几千-几万条 |
| 代码助手 | 内部代码风格、私有 API | 1 万+ |
| 私有数据吸收 | 让模型"记住"知识 | 看场景 |
注意第 6 个场景——让模型记住知识——这是新手最容易踩的坑。SFT 不擅长让模型"学新知识",只擅长让模型"学新格式/风格"。需要让模型"知道某些信息",应该用 RAG。
1.3 SFT vs RAG vs Prompt:三选一的决策
这是一个被无数团队踩坑后才搞清楚的决策矩阵:
| 需求 | 推荐 | 理由 |
|---|---|---|
| 输出格式特定(JSON / XML) | Prompt → SFT | Prompt 不够稳定上 SFT |
| 让模型用特定语气/风格 | SFT | Prompt 难以稳定 |
| 给模型注入新知识 | RAG | SFT 学知识效率低、易遗忘 |
| 模型理解新术语 | SFT + RAG | 先 SFT 理解、再 RAG 查询 |
| 复杂工具调用 | SFT (Function Calling 数据) | Prompt 不够稳 |
| 内部工作流编排 | SFT | Prompt 链条太长 |
| 单次复杂任务 | Prompt + Chain of Thought | 不需要训练 |
一个简化的优先级:
Prompt → RAG → SFT → 对齐微调
能用 Prompt 解决,不要上 RAG;能 RAG 解决,不要 SFT;能 SFT 解决,不要 RLHF。降维使用,工程上最省事。
二、SFT 原理速通
2.1 SFT 与预训练的本质区别
| 维度 | 预训练 | SFT |
|---|---|---|
| 数据量 | 万亿 token | 千-百万 sample |
| 数据格式 | 纯文本 | (指令, 响应) 对 |
| 训练目标 | 下一个 token 预测 | 仅响应部分的 token 预测 |
| 学习率 | ~3e-4 | ~1e-5 ~ 2e-4(取决于方法) |
| 训练时长 | 数月 | 数小时-数天 |
| 算力规模 | 千-万卡 | 单卡-数十卡 |
最关键的区别:
SFT 的 loss 只算"响应"部分的 token,不算"指令"部分——因为我们想让模型学会"如何回答",而不是"如何提问"。
这是新手容易写错的细节,下面代码会讲到。
2.2 训练数据格式
主流 SFT 数据格式两类:
Alpaca 风格(早期):
{
"instruction": "把下面文本翻译成英文",
"input": "你好,世界",
"output": "Hello, world"
}
ChatML / Multi-turn 风格(当下主流):
{
"messages": [
{"role": "system", "content": "你是一个翻译助手"},
{"role": "user", "content": "把'你好,世界'翻译成英文"},
{"role": "assistant", "content": "Hello, world"}
]
}
当下生产环境几乎都用 ChatML 格式——因为支持多轮、支持 system prompt、和推理时的 prompt 格式完全一致。
2.3 Chat Template:让模型学会对话格式
不同模型有不同的对话模板。比如 Qwen3 的模板:
<|im_start|>system
{system}<|im_end|>
<|im_start|>user
{user_msg}<|im_end|>
<|im_start|>assistant
{assistant_msg}<|im_end|>
Llama 3:
<|begin_of_text|><|start_header_id|>system<|end_header_id|>
{system}<|eot_id|><|start_header_id|>user<|end_header_id|>
{user_msg}<|eot_id|>...
SFT 训练时必须用和推理时完全一致的模板——否则上线效果会"莫名其妙差"。
直接用 tokenizer.apply_chat_template() 是最稳的做法,不要手拼字符串。
三、三种微调方法:原理与对比
3.1 全参数微调(Full Fine-Tuning)
做法:解锁所有模型参数,照常用 SGD/AdamW 更新。
显存需求(第 3 篇推导过的训练显存公式):
显存 ≈ 12 × N bytes(BF16 + AdamW)
对 70B 模型:12 × 70 = 840 GB——单 H100 80GB 装不下,需要 8 张 H100 起。
优点:
- 效果上限最高
- 适合大幅改变模型行为
- 适合预训练后再次精炼
缺点:
- 显存成本极高
- 容易"灾难遗忘"(catastrophic forgetting)—— 学新东西忘老东西
- 训练完产物是个完整模型,存储/传输不便
实操场景:
- 准备发布新版本基座模型
- 算力充足且需要极致效果
- 大数据量(10 万+)领域适配
3.2 LoRA(Low-Rank Adaptation)
LoRA 是 2021 年微软提出的方法,当下 80% 的微调任务都用 LoRA 系列。
核心思路:冻结原模型权重,只在每一层旁边加一个"低秩适配器"。
数学表达:
原模型权重:W (d × d)
新增适配器:B × A,其中 B 是 d×r,A 是 r×d
推理时实际权重:W + α/r × B × A
其中 r 是 LoRA 秩(典型值 8、16、64),α 是缩放因子。
为什么有效:研究表明,微调阶段权重的"实际变化"是低秩的——可以用小矩阵很好近似。
视觉上:
原始 forward
input ──→ [W: d×d] ──→ output ← 冻结,不训练
+
input ──→ [A: r×d] → [B: d×r] ──→ output_delta ← 训练
↑ ↑
低秩 r=8/16/64
显存收益:
- 训练参数从 N 降到 ~0.1-1% × N(取决于 r)
- 优化器状态减少同等比例
- 显存降到 2-4 × N bytes(约全参的 1/3-1/6)
对比 Llama-3-70B 训练显存:
| 方法 | 显存 |
|---|---|
| 全参(BF16+Adam) | ~840 GB |
| LoRA r=64 | ~280 GB |
| QLoRA r=64 | ~70 GB |
LoRA 的工程红利:
- 可叠加:训出多个 LoRA,可以加权合并
- 可热切换:推理时一个基座 + 多个 LoRA,按业务切换
- 轻量传输:一个 LoRA 通常只有几十-几百 MB
- 多任务共存:不同业务不同 LoRA,互不干扰
这就是为什么 vLLM、SGLang 都支持多 LoRA 热加载——一个基座撑 N 个业务。
3.3 QLoRA(Quantized LoRA)
问题:即便 LoRA 把训练显存降到 1/3,70B 模型还是要 280GB——单卡跑不动。
QLoRA 的妙招:把冻结的基座模型量化到 4-bit,LoRA 适配器仍用 BF16 训练。
基座 W:NF4 量化 (4-bit) ← 冻结
LoRA A, B:BF16 ← 训练
反向传播:用 BF16 精度计算梯度
显存进一步降到 N bytes 量级——70B 模型只需 ~70GB,单 H100 80G 完全可以。
关键技术细节:
- NF4 量化:专为正态分布权重设计的 4-bit 量化,精度损失极小
- Double Quantization:连量化常数本身也量化,再省 10% 显存
- Paged Optimizer:用 CUDA 统一内存,OOM 时自动换到 CPU
代价:
- 训练速度比 LoRA 慢约 20-40%(量化/反量化开销)
- 极少数极端任务效果略差
结论:QLoRA 是单卡微调的事实标准。
3.4 一张表总结
| 维度 | 全参 | LoRA | QLoRA |
|---|---|---|---|
| 训练显存 (70B) | ~840 GB | ~280 GB | ~70 GB |
| 单卡可训?(80G H100) | ❌ | ❌ | ✅ |
| 双卡可训? | ❌ | ✅(紧张) | ✅(轻松) |
| 训练速度 | 1× | 1.5× | 1× |
| 效果上限 | 100% | 95-98% | 92-96% |
| 产物大小 | 140 GB | 100-500 MB | 100-500 MB |
| 适合多任务部署 | ❌ | ✅ | ✅ |
3.5 选择决策树
是否准备发布新基座 / 极致效果?
├─ 是 → 全参(多卡)
└─ 否 → 用 LoRA 系列
│
├─ 显存充足(数十 GB+)?
│ ├─ 是 → LoRA(更快)
│ └─ 否 → QLoRA(单卡也能跑)
经验法则:从 QLoRA 开始。除非证明 QLoRA 效果不够,否则不要折腾全参。
四、实战:用 QLoRA 微调 Qwen3-7B
下面是一个可直接运行的完整 QLoRA 微调脚本。
4.1 环境准备
pip install transformers==4.46.0 peft==0.13.0 bitsandbytes==0.44.0 \
accelerate==1.0.0 trl==0.12.0 datasets==3.0.0
4.2 数据准备
假设我们要训一个「翻译助手」,数据格式:
[
{"messages": [
{"role": "system", "content": "你是翻译助手"},
{"role": "user", "content": "把'你好,世界'翻译成英文"},
{"role": "assistant", "content": "Hello, world"}
]},
...
]
数据准备代码:
import json
from datasets import Dataset
# 假设你的训练数据在 train.jsonl
with open("train.jsonl") as f:
data = [json.loads(l) for l in f]
dataset = Dataset.from_list(data)
print(f"训练样本数: {len(dataset)}")
4.3 加载模型 + QLoRA 配置
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
MODEL_NAME = "Qwen/Qwen3-7B-Instruct"
# 1. 4-bit 量化配置
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True, # Double Quantization
)
# 2. 加载量化基座
model = AutoModelForCausalLM.from_pretrained(
MODEL_NAME,
quantization_config=bnb_config,
device_map="auto",
trust_remote_code=True,
)
model = prepare_model_for_kbit_training(model)
# 3. LoRA 配置
lora_config = LoraConfig(
r=64, # 秩
lora_alpha=128, # 缩放因子,通常 = 2r
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
target_modules=[ # 注入 LoRA 的层
"q_proj", "k_proj", "v_proj", "o_proj", # attention
"gate_proj", "up_proj", "down_proj", # FFN
],
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# 输出: trainable params: 80,478,208 || all params: 7,696,150,528 || trainable%: 1.04%
几个关键参数解读:
r=64:rank。经验值:小任务 8-16,中型 32-64,大型 64-128lora_alpha = 2r:缩放系数标准做法target_modules:决定 LoRA 注入的层。注入 attention + FFN 是当前最优配置
4.4 数据处理(关键:只对 response 算 loss)
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True)
def format_and_tokenize(example):
"""关键点:分离 prompt 和 response,loss 只算 response 部分"""
messages = example["messages"]
# 用 chat template 构造完整文本
full_text = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=False
)
# 构造 prompt(不含 assistant 部分)
prompt_messages = messages[:-1] # 去掉最后一条 assistant
prompt_text = tokenizer.apply_chat_template(
prompt_messages, tokenize=False, add_generation_prompt=True
)
# tokenize
full_ids = tokenizer(full_text, truncation=True, max_length=2048)["input_ids"]
prompt_ids = tokenizer(prompt_text, truncation=True, max_length=2048)["input_ids"]
# labels: prompt 部分用 -100 mask(不计 loss)
labels = [-100] * len(prompt_ids) + full_ids[len(prompt_ids):]
return {
"input_ids": full_ids,
"labels": labels[:len(full_ids)],
"attention_mask": [1] * len(full_ids),
}
dataset = dataset.map(format_and_tokenize, remove_columns=dataset.column_names)
⚠️ 新手最常踩的坑:直接把整条对话拿来训,导致 loss 也算了"用户提问"部分——模型会学到"如何生成用户提问",效果显著变差。
4.5 训练
from transformers import TrainingArguments, Trainer
from transformers.data.data_collator import DataCollatorForSeq2Seq
training_args = TrainingArguments(
output_dir="./qlora-qwen3-7b",
num_train_epochs=3,
per_device_train_batch_size=4,
gradient_accumulation_steps=4, # 等效 batch = 16
learning_rate=2e-4, # QLoRA 通常比全参高
lr_scheduler_type="cosine",
warmup_ratio=0.03,
logging_steps=20,
save_steps=500,
save_total_limit=3,
bf16=True,
optim="paged_adamw_8bit", # Paged Optimizer
gradient_checkpointing=True,
report_to="tensorboard",
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=dataset,
data_collator=DataCollatorForSeq2Seq(
tokenizer=tokenizer, padding=True
),
)
trainer.train()
model.save_pretrained("./qlora-qwen3-7b-final")
4.6 合并 LoRA 权重 + 推理
训完后有两种部署方式:
方式 1:保留 LoRA,动态加载(推荐生产)
from peft import PeftModel
from transformers import AutoModelForCausalLM
base = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-7B-Instruct")
model = PeftModel.from_pretrained(base, "./qlora-qwen3-7b-final")
# 可以在推理时切换不同 LoRA
方式 2:合并权重(用于发布完整模型)
merged = model.merge_and_unload()
merged.save_pretrained("./qwen3-7b-translate")
合并后的模型和原模型一样可以用 vLLM 等推理框架部署。
4.7 vLLM 部署(多 LoRA 共存)
vllm serve Qwen/Qwen3-7B-Instruct \
--enable-lora \
--lora-modules \
translate=./qlora-qwen3-7b-translate \
customer-service=./qlora-qwen3-7b-cs \
code-review=./qlora-qwen3-7b-code \
--max-loras 4
请求时指定要用哪个 LoRA:
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "translate",
"messages": [{"role":"user","content":"翻译: hello"}]
}'
单基座 + 多 LoRA 是当下生产部署的最经济方案——一份 GPU 资源服务多业务。
五、避坑清单:5 个最常见的微调陷阱
坑 1:数据质量 > 数据量
很多人觉得"我训数据越多越好",结果训出来效果反而退化。
反例:一个团队拿 100 万条客服对话训,结果模型学了大量"嗯嗯啊啊"等口语化噪声。
对策:
- 严格清洗数据,宁少勿滥
- 1000 条高质量数据 > 10 万条噪声数据——Anthropic 在 Constitutional AI 中验证过
- 数据要覆盖业务的关键场景,不要全是简单 case
坑 2:学习率配错
不同微调方法学习率差异很大:
| 方法 | 推荐 LR |
|---|---|
| 全参微调 | 1e-5 ~ 5e-5 |
| LoRA | 1e-4 ~ 3e-4 |
| QLoRA | 1e-4 ~ 3e-4 |
| Embedding 微调 | 1e-3 |
错配 LR 的症状:
- LR 过大 → loss 飞起,胡言乱语
- LR 过小 → 训练无进展,效果同未训
坑 3:灾难遗忘
微调后模型在原能力上"退化"。例如训一个法律助手,结果模型连日常对话都答不好了。
对策:
- 混入 10-20% 的通用指令数据
- 用 LoRA 而非全参(自带"保护"机制)
- 学习率小一些、训练 epoch 少一些
- DPO 阶段可以部分修复
坑 4:Chat Template 用错
症状:训练 loss 看着正常,部署后效果差。
根本原因:训练用的 template 和推理用的 template 不一致。
对策:
- 训练和推理必须用同一个
tokenizer.apply_chat_template() - 检查 EOS token 是否正确(很多模型有多个 EOS)
- 如果加了自定义 system prompt,推理时也要加
坑 5:评估太草率
反例:训完用 5 个测试样例看一眼,效果好就上线。结果用户场景一来全崩。
对策:
- 准备至少 100 条 hold-out 测试集(不要混进训练集)
- 用业务真实分布的数据评估,不要用通用 benchmark
- 同时跑通用 benchmark(MMLU/CEval)确认没有灾难遗忘
- 加入对抗性 case(prompt injection、边缘场景)
六、进阶话题与下一篇预告
6.1 LoRA 的进化变种
LoRA 之后还出了一系列改进:
| 方法 | 改进点 | 当下地位 |
|---|---|---|
| LoRA | 低秩适配 | 主流 |
| AdaLoRA | 自适应分配 rank | 部分场景 |
| DoRA | 分解为方向 + 大小 | 论文热门 |
| PiSSA | 用主成分初始化 | 新兴 |
| GaLore | 梯度低秩投影 | 接近全参效果 |
工程上:LoRA 仍然是首选,其他方法在 5% 边际增益场景考虑。
6.2 何时 SFT 完不够、要上 DPO/RLHF?
SFT 解决"会做",不解决"做得好"。下列情况要继续做对齐:
- 安全性场景(医疗、法律建议)
- 用户体验场景(语气、有用性、诚实性)
- 偏好选择("哪个回答更好"是模糊判断)
👉 详见 系列第 8 篇:RLHF 与 DPO。
6.3 多 LoRA 合并
如果你训了多个 LoRA,可以加权合并:
from peft import PeftModel
model = PeftModel.from_pretrained(base, lora_a)
model.load_adapter(lora_b, adapter_name="b")
model.add_weighted_adapter(
adapters=["default", "b"], weights=[0.7, 0.3],
adapter_name="merged"
)
适用于:把"格式 LoRA"和"领域 LoRA"组合使用。
6.4 SFT 数据自动化
工业上越来越多用 大模型生成 SFT 数据:
- Self-Instruct:模型自己生成 instruction
- Evol-Instruct:让 GPT-4 把简单 instruction 改写得更复杂
- 蒸馏:用闭源大模型回答,作为开源小模型的 SFT 数据
👉 详见 系列第 10 篇:训练数据工程。
结语:80% 的 AI 工程师会与 SFT 朝夕相处
读完本文你应该明白:
- QLoRA 是单卡微调的事实标准,从这里开始
- SFT 不擅长学知识,擅长学格式/风格——知识用 RAG
- 数据质量 >> 数据量,1000 条精品 > 10 万条噪声
- 多 LoRA + 单基座 是生产部署的经济选择
- Prompt → RAG → SFT → 对齐,按优先级降维使用
下一篇我们继续训练与微调线:
- 第 8 篇:RLHF 与 DPO —— 在 SFT 已经做完之后,怎么让模型"更好"?我们会讲透 RLHF 的三阶段流程、DPO 的"一步到位"思路、以及 2024-2025 年 DeepSeek R1 带来的 GRPO 新范式。
之后是垂直领域大模型(第 9 篇)、训练数据工程(第 10 篇)。微调篇全套讲完后,我们就进入推理优化篇(第 11-15 篇)—— 这是部署工程师真正的主战场。
我们下篇见。
📮 关于「码海寻道」
这里是一个聚焦 AI 工程化、大模型部署、后端架构实战的技术专栏。
写最一线的踩坑经验,做最务实的技术拆解。如果这篇文章对你有启发,欢迎点赞、转发、关注。我们下篇见。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)