QLoRA 微调:4GB 显存也能跑大模型训练
上篇文章我们聊了 LoRA——用两张小矩阵代替全量参数更新,把 7B 模型的微调显存需求从 60GB 打到了 12GB 左右。这个门槛对一张 RTX 4070 或 3080 来说已经可以接受了。
但问题来了:如果你只有一张老旧的 RTX 2060(6GB),或者一台 MacBook(统一内存 8GB 共享),甚至一张 4GB 显存的入门卡呢?传统 LoRA 虽然省了很多,但 12GB 对它们来说还是遥不可及。
这就是 QLoRA 要解决的问题。2023 年,华盛顿大学 Tim Dettmers 团队在 LoRA 的基础上,引入了一项关键改造——先把模型压缩到 4 位精度,再挂 LoRA 微调。结果:7B 模型微调只需要 6~8GB 显存,3B 模型甚至只要 4GB,效果和 16 位全量微调几乎持平。
一、QLoRA 到底做了啥?
QLoRA 的全称是 Quantized LoRA(量化 LoRA)。核心思路用一句话说就是:先把大模型用 4 位整数存起来,省掉 75% 显存,然后在上面挂 LoRA 做微调。具体来讲,论文做了三个创新:
1. 4-bit NormalFloat(NF4)
普通的 4 位量化就是把 16 位浮点数直接截断成 4 位,丢失精度。NF4 做的更聪明:它根据神经网络权重的分布(零均值、正态分布形状)设计了一种最优的量化映射,把信息密度最高的区间分配最多的比特位。结果是:NF4 的精度损失远小于普通 4 位量化,接近 8 位量化的水平。
2. 双重量化(Double Quantization)
量化本身需要一组缩放因子(quantization constants)来还原数值。在 NF4 里,每 64 个参数共享一个 32 位浮点数的缩放因子。这在 7B 模型上大约多占 350MB。双重量化再对这些缩放因子做一次 8 位量化,把这 350MB 又压缩到约 85MB。蚊子腿也是肉。
3. 分页优化器(Paged Optimizers)
微调过程中,优化器状态(Adam 算法的动量、方差)会占用大量显存。分页优化器利用了 NVIDIA CUDA 的统一内存特性——当显存不够时,自动把优化器状态换到 CPU 内存,训练完了再换回来。相当于给显存装了一个”虚拟内存”。
把这三个加起来:NF4 把权重压缩到 1/4,双重量化再省一点,分页优化器兜底防止 OOM。7B 模型的 QLoRA 微调显存从 ~56GB(全量)→ ~12GB(LoRA)→ ~7GB(QLoRA)。
二、实战:用 QLoRA 微调 Qwen2.5-7B
下面这段代码基于 HuggingFace transformers + bitsandbytes + PEFT 实现,是 QLoRA 的标准写法。
第一步:安装依赖
pip install transformers peft accelerate datasets bitsandbytes torch
第二步:用 4 位精度加载模型
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, TaskType, prepare_model_for_kbit_training
import torch
model_id = "Qwen/Qwen2.5-7B-Instruct"
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
)
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=bnb_config,
device_map="auto",
)
# 4bit 模型需要做一些准备才能训练
model = prepare_model_for_kbit_training(model)
注意 load_in_4bit=True——这一行就是整个 QLoRA 最大的魔法。模型加载后,7B 的权重只占 ~3.5GB 显存(7B × 0.5 bytes/parameter)。
第三步:挂 LoRA 适配器
lora_config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
lora_dropout=0.05,
bias="none",
task_type=TaskType.CAUSAL_LM,
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# 输出: trainable params: ~7.4M / 7,619M = 0.097%
看到没?70 多亿参数的大模型,我们只训练 740 万个参数——占比不到千分之一。
第四步:准备数据并训练
训练数据的格式和上一篇文章完全一样。假设我们准备了一份 QA 数据集:
from datasets import Dataset
data = [
{"input": "用 Python 实现一个二分查找", "output": "def binary_search(arr, target):\n left, right = 0, len(arr)-1\n while left <= right:\n mid = (left + right) // 2\n if arr[mid] == target: return mid\n elif arr[mid] < target: left = mid + 1\n else: right = mid - 1\n return -1"},
{"input": "解释一下 HTTP 状态码 301 和 302 的区别", "output": "301 是永久重定向(Moved Permanently),浏览器会缓存结果,下次直接访问新地址。302 是临时重定向(Found),每次都要先请求原地址再跳转。对 SEO 来说,网站换域名用 301,活动页面临时跳转用 302。"},
]
def format_func(example):
return {"text": f"用户:{example['input']}\n助手:{example['output']}"}
dataset = Dataset.from_list(data).map(format_func)
from transformers import TrainingArguments, Trainer
training_args = TrainingArguments(
output_dir="./qwen-qlora",
per_device_train_batch_size=2, # QLoRA 批次可以小一点
gradient_accumulation_steps=4,
num_train_epochs=3,
learning_rate=2e-4,
logging_steps=10,
save_strategy="epoch",
bf16=True,
gradient_checkpointing=True, # 进一步省显存
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=dataset,
)
trainer.train()
model.save_pretrained("./qwen-qlora-final")
这里有两个 QLoRA 特有的关键设置:gradient_checkpointing=True 通过不存中间激活值来节省显存(大概再省 2GB),per_device_train_batch_size=2 用更小的批次让显存不爆。
第五步:加载 LoRA 做推理
from peft import PeftModel
base_model = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=bnb_config,
device_map="auto",
)
lora_model = PeftModel.from_pretrained(base_model, "./qwen-qlora-final")
inputs = tokenizer("用户:用 Python 实现一个二分查找\n助手:", return_tensors="pt").to("cuda")
outputs = lora_model.generate(**inputs, max_new_tokens=200)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
三、QLoRA vs LoRA:差别大吗?
很多人担心的核心问题是:
| 维度 | LoRA(16-bit) | QLoRA(4-bit) |
|---|---|---|
| 7B 模型显存需求 | ~12GB | ~7GB |
| 3B 模型显存需求 | ~6GB | ~4GB |
| 训练速度(7B) | 基准 1x | 约慢 15~25% |
| 推理速度(7B) | 基准 1x | 约慢 5~10% |
| 微调效果 | 基准 | 论文报告 99.3% 等效 |
| 支持的最低显卡 | RTX 3060 12GB | GTX 1660 6GB / 笔记本 |
论文(Dettmers et al., 2023)在 Vicuna 评测基准上对比了 QLoRA 和全量微调:QLoRA 训练出来的 Guanaco 模型达到了 ChatGPT 性能的 99.3%,与 16 位全量微调几乎无差距。也就是说,用 4 位量化训练的效果和 16 位微调几乎没区别。
代价是什么?训练时间多了 15~25%。纯从算账角度:时间是免费的(等一晚上就好),显存是要钱的(每多 8GB 多花一千块钱)。对个人开发者来说,QLoRA 几乎是唯一现实的选择。
四、实用建议
显存不够怎么办?
如果你只有 4GB 显存(比如 GTX 1650、MX450),还是可以玩 QLoRA——换 3B 参数级别的模型,比如 Qwen2.5-3B-Instruct 或 LLaMA-3.2-3B-Instruct。把上面代码里的 model_id 换成对应模型,batch_size 设成 1,就能跑起来。
数据量少怎么办?
QLoRA 因为基座模型被压缩到 4 位,信息容量理论上比 16 位 LoRA 小一些。如果数据量超过 5000 条,可以考虑先 QLoRA 训练收敛,然后切回 16 位 LoRA 再做几轮精调(refinement)。不过对大多数 200~2000 条数据的场景,纯 QLoRA 完全够用。
NF4 vs FP4?
从 bitsandbytes 0.41 开始,NF4 是默认推荐选项。FP4 是另一种 4 位量化格式,精度略低但计算更快。除非对速度有极致要求,否则选 NF4 就好。
总结
QLoRA 把大模型微调的硬件门槛从"必须有一张高端显卡"降到了"手头有什么都能试一试"——7B 模型只需要 7GB 显存,3B 模型 4GB 就能跑。三个关键技术(NF4 量化 + 双重量化 + 分页优化器)的组合拳,让个人开发者终于有了平价调大模型的可能。
第 3 篇的 LoRA 让你学会微调,第 4 篇的 QLoRA 让任何人都能微调。这是"从零学大模型"系列的第 4 篇,下一篇我们聊模型评测:怎么知道你微调的模型到底好不好——从自动评估指标到人工评测,给你一套完整的评估工具箱。
参考文献
- Dettmers, T., Pagnoni, A., Holtzman, A., & Zettlemoyer, L. "QLoRA: Efficient Finetuning of Quantized LLMs." arXiv:2305.14314, 2023. https://arxiv.org/abs/2305.14314
- HuggingFace PEFT 官方仓库. https://github.com/huggingface/peft
- bitsandbytes 官方仓库. https://github.com/bitsandbytes-foundation/bitsandbytes
- HuggingFace Transformers 量化文档. https://huggingface.co/docs/transformers/en/quantization
- Qwen2.5 官方模型仓库. https://huggingface.co/Qwen/Qwen2.5-7B-Instruct



