digest:低秩分解原理、target_modules 选择、4-bit 量化配置,附 tiny 模型可跑代码
上一篇写完 SFT 全流程后,有读者问:公司只有 2 张 4090,24G 显存,能不能跑 7B 的全参 SFT。答案很直接:跑不动。7B 模型 fp16 权重就占 14G,加上梯度、优化器状态(AdamW 每个参数要存 m 和 v)、激活值,全参 SFT 至少要 60G 显存,得 A100 80G 才顶得住。
这就是为什么大多数团队实际做微调都在用 LoRA 或者 QLoRA。参数量降到千分之一,显存需求从 60G 砍到 10G,训练完的 adapter 只有几十兆,甚至可以给每个业务客户维护一份独立 adapter,运行时按需加载。
这篇讲清楚 LoRA/QLoRA 的机制、参数选择的经验、几个真实踩坑点,最后给一份能在 CPU 上跑通的完整示例。
一、LoRA 的核心机制
LoRA 的思路一句话概括:不训练原始权重 W,而是训练一个低秩的增量 ΔW = BA,让最终权重变成 W’ = W + BA。
假设原始权重矩阵 W 的形状是 d×k(比如 4096×4096),全参训练要更新 1600 万参数。LoRA 引入两个小矩阵:B 的形状是 d×r,A 的形状是 r×k,r 通常取 8、16、32。r=8 时,可训练参数变成 4096×8 + 8×4096 ≈ 6.5 万,参数量降到千分之四。
为什么这样能工作。LoRA 论文的观察是:微调过程中权重的实际变化 ΔW 是低秩的,也就是说大模型在下游任务上真正需要调整的"信息维度"远小于权重矩阵本身的维度。r 就是这个内在维度的估计。
几个参数怎么选:
r(秩):任务越接近预训练分布,r 可以越小。指令跟随、简单风格迁移 r=8 够用;领域知识注入、复杂任务 r=16 或 32;如果发现 loss 降不下去,先加 r 再考虑其他。r=64 以上收益就很有限了,还不如去调数据。
lora_alpha:实际的缩放系数是 alpha/r。这个参数控制 LoRA 分支对原始输出的影响力。经验做法是 alpha 设为 r 的 2 倍(r=8 时 alpha=16),或者干脆设成一样。alpha 越大 LoRA 的影响越强,训练也越不稳定。
lora_dropout:0.05 到 0.1 之间,防止 adapter 过拟合。数据量大时可以设 0。
初始化上有个细节:B 初始化为零矩阵,A 用高斯初始化。这样训练开始时 BA=0,模型行为完全等同于原始 W,训练过程是从原点开始逐步偏离,不会一上来就破坏预训练能力。
二、target_modules:哪些层加 adapter
LoRA 只在指定的线性层上加旁路,target_modules 决定加在哪些层。这个参数比 r 更影响最终效果,也是最容易踩坑的地方。
不同模型的 attention 层命名不一样,抄配置抄错模型就会静默失败(PEFT 找不到匹配的层,adapter 参数是 0)。常见几家的命名:
- LLaMA / Mistral / Qwen 系列:
q_proj、k_proj、v_proj、o_proj、gate_proj、up_proj、down_proj - GPT-2 / GPT-Neo:
c_attn、c_proj(q/k/v 合并在 c_attn 里) - ChatGLM:
query_key_value、dense - Falcon:
query_key_value、dense_4h_to_h
怎么查一个新模型有哪些线性层,最省事的方法是打印所有 nn.Linear 的名字:
from transformers import AutoModelForCausalLM
import torch.nn as nn
model = AutoModelForCausalLM.from_pretrained(“your-model”)
for name, module in model.named_modules():
if isinstance(module, nn.Linear):
print(name)
加哪些层的选择:
只加 q_proj 和 v_proj:LoRA 原论文的做法,参数最少,训练最快,效果对多数任务够用。适合数据量小、任务简单的场景。
加所有 attention 投影(q/k/v/o):更全面,参数量翻倍。适合任务需要调整注意力模式的场景,比如长上下文适配。
加 attention + MLP(q/k/v/o/gate/up/down):QLoRA 论文推荐的配置。参数量再翻一倍,但也真的能吃到更好的效果,尤其是需要注入新知识时。现在 7B 以上模型的 QLoRA 基本都这么配。
一个经常被忽略的细节:modules_to_save参数。如果你换了 tokenizer(比如加了特殊 token),embedding 和 lm_head 需要完整训练,不能只加 LoRA,要通过 modules_to_save=[“embed_tokens”, “lm_head”] 让它们参与全参数更新。忘了这一步,新加的 token 永远学不好。
三、QLoRA:4-bit 量化 + LoRA
QLoRA 在 LoRA 基础上做了一件事:把冻结的原始权重 W 量化到 4-bit,只有 LoRA 的 A、B 保持 bf16 参与训练。这样 7B 模型的权重从 14G 降到 3.5G 左右,加上梯度和优化器状态(只针对 LoRA 参数,占用极小),总显存能压到 10G 以内。
关键的三个技术点:
NF4 量化:普通的 4-bit 量化是均匀分布的(16 个等距的 quantization level),但神经网络权重实际是近似正态分布,均匀量化在中间密集区域太粗糙、两端稀疏区域太浪费。NF4(NormalFloat 4-bit)把 16 个 level 按正态分布的分位数放置,中间密、两端疏,量化误差比 int4 小很多。配置里写bnb_4bit_quant_type="nf4"就是启用这个。
Double Quantization:量化本身要存 scale 参数(每个 block 一个 fp32 scale)。double quant 把这些 scale 参数再量化一次,每个参数省 0.37 bit。7B 模型能再省几百 MB。配置里bnb_4bit_use_double_quant=True。
Paged Optimizer:训练时如果显存尖峰超了会 OOM,paged optimizer 借用 CUDA 的 unified memory,把优化器状态在 GPU 显存和 CPU 内存之间自动分页交换。峰值一过就换回来,训练过程不中断。TrainingArguments 里设optim="paged_adamw_8bit"。
实际显存对比(7B 模型,seq_len=2048,batch=1):
| 方案 | 显存占用 | 训练速度 |
|---|---|---|
| 全参 SFT (bf16) | ~60G | 1x |
| LoRA (bf16) | ~20G | 0.9x |
| QLoRA (4-bit) | ~10G | 0.7x |
QLoRA 训练速度比 LoRA 慢一些,因为 forward 时要反量化,但显存优势太明显,24G 消费卡跑 13B 都可以。
四、常见坑
坑一:target_modules 拼错,adapter 是空的
配置里写了 target_modules=[“query”, “value”],但模型里叫 q_proj、v_proj,PEFT 不会报错,只是静默地什么也不加。训练后你会发现 loss 完全不动,或者动一点点纯粹是 embedding 层的贡献。检查方法:model.print_trainable_parameters(),正常应该看到 trainable params 占几百万到几千万,如果只有几万那基本就是这个问题。
坑二:QLoRA 训练时用了错误的 dtype
BitsAndBytesConfig 里的bnb_4bit_compute_dtype必须是 bf16 或 fp16,而且要和 TrainingArguments 里的 bf16/fp16 设置一致。混用会报 “Input type and weight type should be the same” 或者训练 loss 是 nan。经验做法:Ampere 及以上架构(A100、3090、4090)统一用 bf16,老卡用 fp16。
坑三:合并 adapter 后精度掉了
训练完想把 LoRA 权重合回原模型(model.merge_and_unload())方便部署。QLoRA 训练的 adapter 直接 merge 会有精度损失,因为合并时原始权重是 4-bit 反量化出来的,不是 fp16 原值。正确做法:先用 fp16 加载原始模型,再加载 adapter,再 merge。或者干脆不 merge,推理时用 PEFT 直接加载 adapter。
坑四:保存的 checkpoint 巨大
Trainer 默认会保存完整模型 checkpoint,QLoRA 训练时也是。每个 checkpoint 都是十几 G,很快把磁盘塞满。解决方案:用save_only_model=True或者写个 callback,在 on_save 时只保存 adapter(model.save_pretrained(path)只会保存 LoRA 部分,几十 M)。
五、完整可运行示例
下面这份代码用sshleifer/tiny-gpt2(2.5MB 的 mock 模型)演示 LoRA 训练全流程,不需要 GPU,几秒钟跑完。生产上换成 Qwen2.5-7B 或 Llama-3-8B,把 target_modules 改成对应名字就能直接用。
from dataclasses import dataclass
import torch
import torch.nn as nn
from datasets import Dataset
from peft import LoraConfig, TaskType, get_peft_model
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
DataCollatorForLanguageModeling,
Trainer,
TrainingArguments,
)
MODEL_NAME: str = “sshleifer/tiny-gpt2”
OUTPUT_DIR: str = “./lora_demo_output”
@dataclass
class LoRARecipe:
r: int = 8
lora_alpha: int = 16
lora_dropout: float = 0.05
target_modules: list[str] | None = None
def to\_config(self) -> LoraConfig: return LoraConfig( task\_type=TaskType.CAUSAL\_LM, r=self.r, lora\_alpha=self.lora\_alpha, lora\_dropout=self.lora\_dropout, target\_modules=self.target\_modules or ["c\_attn"], bias="none", )def inspect_linear_layers(model: nn.Module) -> list[str]:
return [
name
for name, module in model.named_modules()
if isinstance(module, nn.Linear)
]
def build_dummy_dataset(tokenizer: AutoTokenizer, n_samples: int = 32) -> Dataset:
prompts = [
f"Question {i}: what is {i} plus {i}? Answer: {2 * i}."
for i in range(n_samples)
]
def tokenize(batch: dict) -> dict: return tokenizer( batch["text"], truncation=True, padding="max\_length", max\_length=32, ) ds = Dataset.from\_dict({"text": prompts}) return ds.map(tokenize, batched=True, remove\_columns=["text"])def build_lora_model(
model_name: str, recipe: LoRARecipe
) -> tuple[AutoModelForCausalLM, AutoTokenizer]:
tokenizer = AutoTokenizer.from_pretrained(model_name)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
base\_model = AutoModelForCausalLM.from\_pretrained(model\_name) linear\_names = inspect\_linear\_layers(base\_model) print(f"[info] linear layers: {linear\_names[:5]} ...") peft\_model = get\_peft\_model(base\_model, recipe.to\_config()) peft\_model.print\_trainable\_parameters() return peft\_model, tokenizerdef run_training(model: AutoModelForCausalLM, tokenizer: AutoTokenizer, dataset: Dataset) -> None:
collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)
args = TrainingArguments(
output_dir=OUTPUT_DIR,
num_train_epochs=1,
per_device_train_batch_size=4,
learning_rate=1e-3,
logging_steps=2,
save_strategy=“no”,
report_to=[],
no_cuda=not torch.cuda.is_available(),
)
trainer = Trainer(
model=model,
args=args,
train_dataset=dataset,
data_collator=collator,
)
trainer.train()
def qlora_config_snippet() -> str:
“”“QLoRA 配置模板,需要 GPU + bitsandbytes 环境运行。”“”
return “”"
from transformers import BitsAndBytesConfig
import torch
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type=“nf4”,
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.bfloat16,
)
base = AutoModelForCausalLM.from_pretrained(
“Qwen/Qwen2.5-7B”,
quantization_config=bnb_config,
device_map=“auto”,
)
Qwen 系列的 target_modules
recipe = LoRARecipe(
r=16,
lora_alpha=32,
target_modules=[
“q_proj”, “k_proj”, “v_proj”, “o_proj”,
“gate_proj”, “up_proj”, “down_proj”,
],
)
“”"
def main() -> None:
recipe = LoRARecipe(r=8, lora_alpha=16, target_modules=[“c_attn”])
model, tokenizer = build_lora_model(MODEL_NAME, recipe)
dataset = build_dummy_dataset(tokenizer, n_samples=32)
run_training(model, tokenizer, dataset)
model.save_pretrained(f"{OUTPUT_DIR}/adapter")
print(“[info] adapter saved, QLoRA snippet:”)
print(qlora_config_snippet())
if __name__ == “__main__”:
main()
跑一遍你会看到trainable params: 6,144 || all params: 1,564,800 || trainable%: 0.39这样的输出,adapter 只占千分之四的参数。生产环境换 7B 模型时,主要改三个地方:MODEL_NAME、target_modules(按前面第二节的模型对照表选)、加上 BitsAndBytesConfig 走 QLoRA 路径。
到这里 SFT + LoRA/QLoRA 的技术栈就凑齐了:数据准备、chat template、loss masking、adapter 训练,能覆盖 80% 的工程微调场景。剩下的 20% 是模型学会做事之后,怎么让它做的事符合业务偏好、拒绝该拒绝的、优先该优先的——这是 RLHF 和 DPO 的地盘。
DPO 相比 RLHF 简单很多,不需要单独训 reward model,直接用偏好对数据做对比学习。下篇讲清楚这两条路线怎么选、DPO 的数据格式怎么准备、以及为什么大多数团队最终选 DPO 而不是走完整的 PPO 流程。
学AI大模型的正确顺序,千万不要搞错了
🤔2026年AI风口已来!各行各业的AI渗透肉眼可见,超多公司要么转型做AI相关产品,要么高薪挖AI技术人才,机遇直接摆在眼前!
有往AI方向发展,或者本身有后端编程基础的朋友,直接冲AI大模型应用开发转岗超合适!
就算暂时不打算转岗,了解大模型、RAG、Prompt、Agent这些热门概念,能上手做简单项目,也绝对是求职加分王🔋
📝给大家整理了超全最新的AI大模型应用开发学习清单和资料,手把手帮你快速入门!👇👇
学习路线:
✅大模型基础认知—大模型核心原理、发展历程、主流模型(GPT、文心一言等)特点解析
✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑
✅开发基础能力—Python进阶、API接口调用、大模型开发框架(LangChain等)实操
✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用
✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代
✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经
以上6大模块,看似清晰好上手,实则每个部分都有扎实的核心内容需要吃透!
我把大模型的学习全流程已经整理📚好了!抓住AI时代风口,轻松解锁职业新可能,希望大家都能把握机遇,实现薪资/职业跃迁~