全部参数都训练
AdamW 训练 7B 模型,权重、梯度和优化器状态可能需要远超 56 GB 显存。
EXPENSIVE / FLEXIBLEInteractive field guide · 2026
冻结巨人,训练一条轻巧的支流。
从低秩矩阵的数学直觉,到用 PEFT、TRL、LLaMA-Factory 与 Unsloth 完成一次可复现的微调。
Learning map
建议按顺序阅读;如果你已经熟悉 Transformer,可以直接从第 03 章开始。
The motivation
全量微调会更新模型的每一个权重。LoRA 的关键发现是:面向具体任务时,真正需要学习的“变化”通常落在一个低维子空间里。
AdamW 训练 7B 模型,权重、梯度和优化器状态可能需要远超 56 GB 显存。
EXPENSIVE / FLEXIBLE冻结原模型,仅保存几 MB 到几百 MB 的适配器;同一基础模型可以挂载多个能力。
LIGHT / MODULAR你并不是重新教育这位专家,而是交给他一份很薄的“工作手册”:面对客服问题用何种语气、面对医疗文本输出什么格式、如何模仿一种画风。LoRA adapter 就是这份可插拔的手册。
Inside the matrix
这一章只需要基础线性代数。先弄懂“秩”代表多少个独立方向,再看 LoRA 如何用一个窄瓶颈描述模型真正需要学习的变化。
First: what is rank?
想象一群人在平面上移动:如果所有人都只能沿同一条直线前后走,虽然每个人走的距离不同,但变化只有 1 个独立方向,秩就是 1;如果他们既能左右走、又能上下走,就需要 2 个独立方向,秩就是 2。
矩阵也是一样。它可能有很多行和列,但其中一些只是另一些的缩放或组合。把这些重复关系剥掉后,剩下多少种无法互相替代的变化方式,就是这个矩阵的秩。
第二行只是第一行的 2 倍。看似有两行,实际上只提供一个独立方向。
两行无法由彼此缩放得到,分别代表两个独立方向。
Then: why low rank?
一个预训练模型已经学会语言、知识和推理。微调客服语气或固定输出格式时,我们通常只需推动其中少量能力方向。于是 LoRA 不直接训练巨大的权重更新矩阵 ΔW,而让数据先经过一个降维矩阵 A,挤进只有 r 个维度的瓶颈,再由矩阵 B 展开回原空间。
W₀ 冻结;只训练 A 与 B。其中 r 远小于输入、输出维度。
Rank laboratory
秩越高,适配器表达能力越强,但参数、显存与过拟合风险也会上升。
预训练模型已经处于一个很好的解附近。下游任务所需的更新 ΔW 往往具有较低的“内在秩”,因此可以近似为 B · A。
α 控制 LoRA 支路的整体强度。传统 LoRA 使用 α/r;Rank-Stabilized LoRA(rsLoRA)常用 α/√r,在高秩时更稳定。
常见做法是随机初始化 A、将 B 初始化为零。训练开始时 BA = 0,模型行为与原模型完全一致,再逐步学出增量。
lora_dropout 只作用于 LoRA 分支。目标层通常从 Attention 的 q_proj、v_proj 开始;数据充足时可扩展到所有线性层。
d × k 的线性层,原参数量为 d·k,LoRA 参数量为 r·(d+k)。例如 4096×4096、r=8 时,从 16,777,216 降为 65,536 个可训练参数。The pipeline
微调效果大多在启动训练之前就已经被决定:目标是否清晰、数据是否一致、模板是否匹配、评测是否可信。
先写 50–200 条“模型应该如何回答”的独立测试样本。不要用训练 loss 代替真实任务评测。
统一角色、语气和答案粒度;清理重复、冲突、隐私与低质量样本。几百条精心策划的数据常常胜过几万条噪声数据。
选择许可证、语言和能力匹配的 Instruct 模型,并严格使用其官方 chat template。模板错位是“训练成功但推理失效”的常见原因。
LoRA 冻结半精度基础模型;QLoRA 进一步把基础模型量化到 4-bit,LoRA 参数仍以 BF16/FP16 训练,从而显著降低显存。
监控训练 / 验证 loss、梯度范数、学习率和任务指标。每隔固定 steps 保存 adapter,不要默认最后一个 checkpoint 最好。
可单独保存 adapter 动态挂载,也可用 merge_and_unload() 合并进基础权重以减少推理开销。合并前必须确保模型版本一致。
Choose your stack
点击切换。它们不是完全互斥:常见组合是 Transformers + PEFT + TRL;LLaMA-Factory 和 Unsloth 则提供更高层的训练体验。
最适合:想理解并精确控制 LoRA 的开发者
参数高效微调的事实标准库,提供 LoraConfig、adapter 注入、保存、加载、切换与合并能力。它负责“改造模型”,训练循环通常交给 Transformers Trainer 或 TRL。
最适合:SFT、DPO、GRPO 等后训练流程
在 Transformers 与 PEFT 之上提供 SFTTrainer、DPOTrainer 等高级训练器。它会处理对话数据、packing、loss mask 和训练配置。
最适合:快速实验、低代码与多模型统一配置
通过 YAML / CLI / WebUI 统一数百种模型的预训练、SFT、DPO 和量化。对初学者友好,也适合批量实验,但深度定制时仍需理解底层机制。
最适合:显存紧张、追求单卡训练速度
通过定制 kernel 和计算图优化加速 LoRA / QLoRA,并降低显存占用。常与 TRL 配合,API 接近 Hugging Face 生态。
最适合:Stable Diffusion / FLUX 等生成图像模型
图像生成模型的 LoRA 训练与加载生态。文本模型 LoRA 多作用于线性层;扩散模型则常训练 UNet / Transformer 与文本编码器中的注意力层。
最适合:Apple Silicon 本地实验
利用 Apple Silicon 统一内存,在 Mac 上完成量化推理与 LoRA 微调。适合学习和小规模实验,但部署生态与 CUDA 路线不同。
| 你的场景 | 推荐起点 | 理由 |
|---|---|---|
| 第一次学习 LoRA | PEFT + TRL | 概念与代码映射最清楚 |
| 24 GB 单卡训练 7B/8B | QLoRA + Unsloth | 显存和速度更友好 |
| 需要大量 YAML 实验 | LLaMA-Factory | 统一配置、低代码复现 |
| 训练人物 / 画风 | Diffusers | 针对扩散模型的完整工具链 |
| M 系列 Mac 本地学习 | MLX-LM | 原生利用 Apple Silicon |
Hands on
下面以 Transformers + PEFT + TRL 为主线。示例结构适用于现代 Causal LM;实际运行时请按显卡和模型许可证替换模型名。
Single GPU field demo
目标:在单张 NVIDIA GeForce RTX 4090 上,用 4-bit QLoRA 对 Qwen2.5-7B-Instruct 做中文客服 SFT。
先让 PyTorch 看到正确的显卡。RTX 4090 支持 BF16;若输出不是 4090 或 CUDA 为 false,先修复驱动 / PyTorch CUDA 版本,不要开始训练。
nvidia-smi
python - <<'PY'
import torch
print("GPU :", torch.cuda.get_device_name(0))
print("CUDA:", torch.cuda.is_available())
print("BF16:", torch.cuda.is_bf16_supported())
PY在 Linux / WSL2 中创建独立环境。PyTorch 的 CUDA wheel 要和驱动兼容;若 cu128 不适合你的驱动,请在 PyTorch 安装选择器中换成匹配版本。
python -m venv .venv
source .venv/bin/activate
pip install -U pip
pip install torch --index-url https://download.pytorch.org/whl/cu128
pip install -U transformers datasets peft trl accelerate bitsandbytes这个配置把 micro batch 固定为 1,再用 16 次梯度累积得到有效 batch 16;2048 token 是 24 GB 显存上相对稳妥的起点。
import torch
from datasets import load_dataset
from transformers import (
AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig,
)
from peft import LoraConfig, prepare_model_for_kbit_training
from trl import SFTConfig, SFTTrainer
MODEL_ID = "Qwen/Qwen2.5-7B-Instruct"
OUTPUT_DIR = "outputs/qwen2.5-7b-4090-lora"
if not torch.cuda.is_available():
raise RuntimeError("本 demo 需要 NVIDIA CUDA GPU")
print("GPU:", torch.cuda.get_device_name(0))
quant_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.bfloat16,
)
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, use_fast=True)
model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
quantization_config=quant_config,
torch_dtype=torch.bfloat16,
device_map={"": 0},
attn_implementation="sdpa",
)
model.config.use_cache = False
model = prepare_model_for_kbit_training(
model, use_gradient_checkpointing=True
)
dataset = load_dataset(
"json", data_files="train.jsonl", split="train"
)
splits = dataset.train_test_split(test_size=0.1, seed=42)
lora_config = 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",
],
bias="none",
task_type="CAUSAL_LM",
)
train_config = SFTConfig(
output_dir=OUTPUT_DIR,
num_train_epochs=2,
per_device_train_batch_size=1,
per_device_eval_batch_size=1,
gradient_accumulation_steps=16,
gradient_checkpointing=True,
gradient_checkpointing_kwargs={"use_reentrant": False},
learning_rate=2e-4,
lr_scheduler_type="cosine",
warmup_ratio=0.03,
max_grad_norm=0.3,
bf16=True,
tf32=True,
optim="paged_adamw_8bit",
max_length=2048,
packing=False,
eval_strategy="steps",
eval_steps=50,
save_steps=50,
save_total_limit=2,
logging_steps=5,
report_to="none",
seed=42,
)
trainer = SFTTrainer(
model=model,
args=train_config,
train_dataset=splits["train"],
eval_dataset=splits["test"],
processing_class=tokenizer,
peft_config=lora_config,
)
trainer.model.print_trainable_parameters()
trainer.train()
trainer.save_model(OUTPUT_DIR)
tokenizer.save_pretrained(OUTPUT_DIR)PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True \
CUDA_VISIBLE_DEVICES=0 python train_4090.py
# 在另一个终端观察显存、功耗和利用率
watch -n 1 nvidia-smi先把 max_length 从 2048 降到 1536 或 1024;确认 batch size 已是 1 和 gradient checkpointing 已开启;再将目标层缩到 q/k/v/o;最后才降低 rank。序列激活通常比 LoRA 参数更占显存。
nvidia-smi 才是你的实际答案。python -m venv .venv
source .venv/bin/activate
pip install -U torch transformers datasets peft trl accelerate bitsandbytesWindows 将激活命令换成 .venv\Scripts\activate。Apple Silicon 通常不使用 bitsandbytes;可走 MLX-LM 或非 4-bit 路线。
推荐保存为 JSONL,每行一个样本。训练前再通过模型自带的 chat template 转成 token;不要手写 <|assistant|> 等特殊符号。
{"messages":[{"role":"system","content":"你是严谨的产品客服。"},{"role":"user","content":"退款多久到账?"},{"role":"assistant","content":"原路退款通常需要 3–5 个工作日。"}]}
{"messages":[{"role":"system","content":"你是严谨的产品客服。"},{"role":"user","content":"如何修改收货地址?"},{"role":"assistant","content":"订单发货前,可在订单详情页选择「修改地址」。"}]}import torch
from datasets import load_dataset
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import LoraConfig
from trl import SFTConfig, SFTTrainer
model_id = "Qwen/Qwen2.5-1.5B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_id, use_fast=True)
model = AutoModelForCausalLM.from_pretrained(
model_id,
torch_dtype=torch.bfloat16,
device_map="auto",
)
dataset = load_dataset("json", data_files="train.jsonl", split="train")
peft_config = LoraConfig(
r=16,
lora_alpha=32,
lora_dropout=0.05,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
bias="none",
task_type="CAUSAL_LM",
)
args = SFTConfig(
output_dir="outputs/customer-service-lora",
num_train_epochs=3,
per_device_train_batch_size=2,
gradient_accumulation_steps=8,
learning_rate=2e-4,
logging_steps=5,
save_strategy="epoch",
bf16=True,
max_length=1024,
packing=False,
report_to="none",
)
trainer = SFTTrainer(
model=model,
args=args,
train_dataset=dataset,
processing_class=tokenizer,
peft_config=peft_config,
)
trainer.train()
trainer.save_model(args.output_dir)SFTConfig 与 SFTTrainer 签名。from transformers import BitsAndBytesConfig
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
)
model = AutoModelForCausalLM.from_pretrained(
model_id, quantization_config=bnb_config, device_map="auto"
)from peft import PeftModel
base = AutoModelForCausalLM.from_pretrained(model_id, device_map="auto")
model = PeftModel.from_pretrained(base, "outputs/customer-service-lora")
messages = [{"role": "user", "content": "退款多久到账?"}]
inputs = tokenizer.apply_chat_template(
messages, add_generation_prompt=True, return_tensors="pt"
).to(model.device)
output = model.generate(inputs, max_new_tokens=128, do_sample=False)
print(tokenizer.decode(output[0][inputs.shape[-1]:], skip_special_tokens=True))Parameter calculator
仅权重约 56.0 MiB(FP16 / BF16)
假设每个目标层都是 d × d;实际值以model.print_trainable_parameters() 为准。Tune & debug
从稳定的保守配置开始,每次只改变一类变量,并保留固定的基础模型对照组。
格式 / 风格任务的可靠起点。复杂迁移可试 32–64。
CAPACITY常见经验起点,不是定律。观察输出偏移与训练稳定性。
STRENGTH通常从 1e-4~2e-4 起步;数据小或不稳定时降低。
STEP SIZE大数据可为 0;小数据或过拟合时尝试 0.05–0.1。
REGULARIZE先看验证集与真实任务曲线,不要机械追求更多轮。
DURATION稳妥起点。追求容量时可加入 MLP,代价是更多参数。
LOCATION| 现象 | 优先检查 | 行动 |
|---|---|---|
| loss 不下降 | target_modules / 数据 mask | 打印可训练参数;抽查 token 与 labels |
| 训练后像没变化 | adapter 是否真正加载 | 对比启用 / 禁用 adapter;检查路径和名称 |
| 输出乱码或不停重复 | chat template / EOS | 使用 tokenizer 官方模板;确认终止 token |
| 训练集很好,真实问题很差 | 过拟合 / 分布偏差 | 早停、降 rank、加数据多样性 |
| CUDA OOM | 序列长度与激活 | 降 batch / length;梯度检查点;QLoRA |
| 合并后效果不同 | 基础模型版本 / dtype | 锁定 revision;高精度合并后再量化 |
Knowledge check
Your next experiment
用 1–3B 模型、500 条高质量样本、r=16、固定的 100 条测试集开始。先跑通闭环,再扩大规模。
让基础模型直接回答评测集,保存原始输出和指标。这是判断 LoRA 是否真的创造价值的起点。
只选择 q/k/v/o,训练 1–3 epochs;抽查数据模板与实际生成,不盯着 loss 自我感动。
把失败样本分组:知识缺失、指令误解、格式错误、幻觉、风格偏差。不同失败类型需要不同解法;下一轮实验只改一个变量。