真正让微调变得普及的,不是把训练变简单了,而是把显存门槛降到了更多团队够得着的范围
全量微调为什么会先把显存打爆
一提到“给模型加业务知识”,很多人的第一反应仍然是微调。
这个方向没错。
但如果默认把微调理解成“把所有参数都更新一遍”,部署成本马上就会变得很重。
原因不只是权重本身大。
训练时你还要为梯度、优化器状态和中间激活付账。
这和只做推理时的内存结构完全不同。
一个 7B 模型推理也许还能勉强塞进一张普通卡。
全量微调时却可能需要数倍以上的显存预算。
因此很多团队卡住的不是“不会训练”。
而是“硬件根本负担不起”。
显存膨胀到底来自哪里
只看模型权重,会误以为事情还不算太糟。
例如 BF16 权重大致是每参数 2 字节。
但训练时至少还有三类额外成本会叠上来。
- 梯度副本。
- 优化器状态,例如 Adam 的一阶与二阶矩。
- 前向中间激活与反向传播缓存。
如果所有参数都参与训练,这三类成本都会随着模型规模线性放大。
于是一个看起来“只是 14GB 权重”的模型,训练时的总占用可能远高于新手直觉。
这正是参数高效微调会流行起来的现实背景。
LoRA 的关键不是“少改一点”,而是改法不同
LoRALoRALoRA stands for Low-Rank Adaptation. Instead of updating the full weight matrix, it learns a low-rank update that is added on top of the frozen pretrained weights.LoRA 的核心思路不是把原模型胡乱冻结一部分。
而是承认一个事实。
很多任务适配并不需要把完整权重矩阵都重新学一遍。
与其更新原矩阵 W 的全部参数,不如学习一个低秩增量 ΔW。
最经典的写法就是:
ΔW = B A
W' = W + ΔW
这里 A 和 B 的秩远小于原矩阵维度。
于是可训练参数量会大幅下降。
这不是数学上的小把戏。
它直接决定了显存、训练时间和存储成本都能一起下降。
低秩分解低秩分解低秩分解的直觉是:很多有效的任务适配方向,未必需要在完整高维空间里自由移动,而可以被更小的子空间近似表达。为什么 LoRA 往往挂在注意力投影层上
LoRA 并不要求你给模型的每一层都加适配器。
在大语言模型里,一个非常常见的选择,是把 LoRA 挂到注意力投影层。
例如 q_proj、k_proj、v_proj、o_proj。
原因很实际。
这些层对模型行为影响大。
同时参数规模也很可观。
如果在这些位置上学习低秩增量,通常已经足够让模型获得明显的任务适配能力。
当然,也有人会继续扩展到 MLP 投影层。
但代价和收益要一起看。
并不是挂得越多越划算。
LoRA 真正省下来的是什么
很多介绍会说 LoRA “只训练很少参数”。
这句话没错,但还不够到位。
LoRA 真正省下来的,是一整条训练链路里的高成本部分。
因为基础权重被冻结,梯度和优化器状态主要集中在新增的低秩矩阵上。
于是:
- 可训练参数量显著下降。
- 优化器状态体积显著下降。
- Checkpoint 体积通常只保存 adapter,而不是整模型。
这使得“做很多个任务版本”成为可能。
你不需要为每个任务复制一份完整基座模型。
只要维护一套共享基座,再配若干 LoRA adapter 即可。
QLoRA 为什么又往前走了一步
LoRA 已经显著降低了微调成本。
但基础模型权重本身还是要加载进显存。
如果基座已经很大,这一步仍然昂贵。
于是 QLoRA 把问题继续往前推。
QLoRAQLoRAQLoRA combines a 4-bit quantized base model with trainable LoRA adapters, making finetuning feasible on much smaller hardware budgets.它的关键做法,是把基础模型以 4bit 形式加载。
基础权重保持冻结。
真正训练的仍然是 LoRA adapter。
这样一来,基座占用显存更少。
LoRA 又避免了全量梯度和优化器状态的爆炸。
两者叠加,就把很多本来只能在更大硬件上做的微调,拉回到了更普通的机器上。
QLoRA 不是“把模型量化后继续乱训”
初学者容易有个误解。
既然基础模型是 4bit,那训练时是不是就在极低精度权重上直接瞎改。
不是。
QLoRA 的关键是:
- 基础模型以低比特形式存储,降低显存。
- 前向和反向里仍通过合适的计算精度执行。
- 训练更新主要落在 LoRA adapter 上,而不是原始量化权重。
这和“对整个量化模型做全量训练”是两件不同的事。
也因此 QLoRA 在工程上更稳。
什么时候你该选 LoRA,什么时候该选 QLoRA
如果你的基座模型并不大。
或者手上有相对宽裕的 GPU。
普通 LoRA 已经可能足够。
它的路径更简单。
兼容性问题也通常更少。
如果你的硬件预算更紧。
或者想在单卡上微调更大的模型。
QLoRA 会更有吸引力。
代价是量化加载、算子支持和训练稳定性要更仔细地核对。
所以这不是“谁先进就选谁”。
而是看你的硬件边界在哪里。
LoRA adapter 为什么对部署也友好
用 PEFT 做最基本的 LoRA 包装
Hugging Face 的 PEFT 已经把 LoRA 用法标准化得很清楚。
下面先看最基础的包装方式。
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import LoraConfig, TaskType, get_peft_model
model_id = "Qwen/Qwen2.5-1.5B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(model_id, device_map="auto")
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
inference_mode=False,
r=16,
lora_alpha=32,
lora_dropout=0.05,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
这段代码做了两件事。
加载基座模型。
再把 LoRA adapter 挂到指定模块上。
真正值得你看的,是 print_trainable_parameters() 的输出。
它会直观告诉你,可训练参数量已经缩到什么程度。
一个可运行的最小训练脚本
如果要把它往前推进一步,可以直接配一个极小的数据集做验证。
下面这个例子使用 datasets 和 Trainer。
from datasets import Dataset
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
DataCollatorForLanguageModeling,
Trainer,
TrainingArguments,
)
from peft import LoraConfig, TaskType, get_peft_model
model_id = "Qwen/Qwen2.5-1.5B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(model_id, device_map="auto")
model.config.use_cache = False
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
inference_mode=False,
r=16,
lora_alpha=32,
lora_dropout=0.05,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
)
model = get_peft_model(model, lora_config)
samples = [
{"text": "### 指令\n解释什么是 KV cache。\n### 回答\nKV cache 用于缓存历史 token 的 key 和 value。"},
{"text": "### 指令\n解释什么是 LoRA。\n### 回答\nLoRA 通过低秩矩阵学习权重增量,而不更新全部参数。"},
]
dataset = Dataset.from_list(samples)
def tokenize_fn(batch):
out = tokenizer(
batch["text"],
truncation=True,
padding="max_length",
max_length=256,
)
out["labels"] = out["input_ids"].copy()
return out
tokenized = dataset.map(tokenize_fn, batched=True, remove_columns=["text"])
training_args = TrainingArguments(
output_dir="./outputs/lora-demo",
per_device_train_batch_size=1,
gradient_accumulation_steps=8,
learning_rate=2e-4,
num_train_epochs=3,
logging_steps=1,
save_steps=20,
bf16=True,
report_to="none",
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized,
data_collator=DataCollatorForLanguageModeling(tokenizer, mlm=False),
)
trainer.train()
model.save_pretrained("./outputs/lora-demo/final")
tokenizer.save_pretrained("./outputs/lora-demo/final")
这个脚本不是为了训出一个业务可用模型。
而是为了让你把 LoRA 的训练链路完整跑通。
QLoRA 的加载方式长什么样
如果要进一步走到 QLoRA,常见写法是结合 bitsandbytes 与 PEFT。
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig, TaskType, get_peft_model, prepare_model_for_kbit_training
model_id = "Qwen/Qwen2.5-1.5B-Instruct"
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
)
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=bnb_config,
device_map="auto",
)
model.config.use_cache = False
model = prepare_model_for_kbit_training(model)
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
inference_mode=False,
r=16,
lora_alpha=32,
lora_dropout=0.05,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
这段代码的关键点有两个。
第一,基座模型是 4bit 加载。
第二,prepare_model_for_kbit_training() 会把一些训练所需准备工作做好。
在这个基础上,你再接 Trainer 或自己的训练循环即可。
LoRA/QLoRA 训练时最常见的坑
这类微调路线虽然门槛低了很多,但并不等于完全无脑。
最常见的问题通常集中在下面几类。
- target modules 选错,结果根本没挂上 adapter。
use_cache没关,训练时显存和行为异常。- 数据模板不稳定,导致模型学到的是脏格式而不是任务模式。
- batch 太小又没有梯度累积,更新非常抖。
- 量化路径和硬件后端不匹配,训练过程频繁报错或退回慢路径。
所以真正可复用的经验,不是盲背一份脚本。
而是理解每个组件在整个训练链路里的角色。
从部署视角看,LoRA 其实很像“可插拔能力层”
很多团队把 LoRA 只当作训练技巧。
这也太保守了。
从部署角度看,它其实很像一层可插拔能力。
共享基座模型不动。
不同业务、不同租户或不同领域,加载不同 adapter。
这样一来:
- 基座权重只需维护一份。
- 新版本交付更轻。
- 灰度与回滚更简单。
- 多个任务版本可以共存。
这就是为什么 LoRA 在训练端和部署端都会变得重要。
它既降低了训练门槛,也改写了模型版本管理方式。
本篇要点
- 全量微调显存昂贵,不只是因为权重大,还因为梯度、优化器状态和激活都会叠加。
- LoRA 用低秩增量
ΔW = BA替代全量权重更新,大幅减少可训练参数。 - QLoRA 进一步把冻结的基座模型压到 4bit,再配合 LoRA adapter 完成低显存微调。
- PEFT 已经把 LoRA/QLoRA 的核心工作流标准化,适合快速搭建可运行训练链路。
- LoRA adapter 体积小、版本轻,不只是训练技巧,也非常适合后续部署与灰度管理。
下一篇
上一篇把模型搬到了更轻的执行形态,这一篇则把训练本身压到了更小的硬件预算里。下一篇会换一个角度,不再直接改大模型,而是讲知识蒸馏:怎样把强教师模型的行为压进一个更小、更便宜的学生模型。
参考资料
版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。
(采用 CC BY-NC-SA 4.0 许可协议进行授权)
本文标题:LoRA与QLoRA微调
本文链接:https://www.sshipanoo.com/blog/ai/inference-opt/04-LoRA与QLoRA微调/
本文最后一次更新为 天前,文章中的某些内容可能已过时!