刚开始接触大模型微调时,很多人都会听到一句话:

显存不够就用 LoRA,再不够就用 QLoRA。

这句话没错,但它说得太粗了。

LoRA 到底省了什么?
QLoRA 又是在 LoRA 的基础上继续省了哪一块?
为什么同样是微调,有的人 24GB 显存能跑起来,有的人换了 LoRA 还是 OOM?

如果只停留在“LoRA 省显存”“QLoRA 更省显存”这个层面,其实很容易理解偏。

我刚开始看 LoRA 的时候,也以为它是把模型变小了。后来真正看参数、梯度和优化器状态之后,才发现不是这样。

LoRA 省的不是原始模型本身,而是减少了需要训练和保存的参数。
QLoRA 则进一步通过 4-bit 量化基础模型权重,降低大模型加载和训练时的显存占用。

理解 LoRA 和 QLoRA,不能只看参数量,还要看训练时显存到底被哪些东西占掉了。

这篇文章就从全量微调讲起,把 LoRA、QLoRA 的核心原理、参数量计算、显存占用和工程选型一次讲清楚。


1. 为什么全量微调大模型这么贵?

先从最直接的问题开始:为什么大模型全量微调这么吃显存?

很多人会简单理解为:

因为模型参数太多。

这只说对了一部分。

比如一个 7B 模型,如果用 FP16 加载,单看模型权重大概是:

7B × 2 bytes ≈ 14GB

看起来 24GB 显卡好像还能放得下。

但问题是,训练不是只加载模型权重。

全量微调时,模型参数不仅要参与前向计算,还要参与反向传播和优化器更新。也就是说,训练时显存里通常不只是模型权重,还包括:

模型权重
梯度
优化器状态
激活值
临时 buffer

如果使用 AdamW 这类优化器,每个参数还需要额外维护一阶矩和二阶矩。
这部分显存非常容易被忽略,但实际训练时它往往是大头之一。

所以全量微调 7B 模型,并不是简单的“14GB 显存就够了”。
如果考虑梯度、优化器状态、激活值,显存需求会远远超过模型权重本身。

这也是为什么全量微调大模型对普通开发者并不友好。
不是代码难写,而是硬件成本太高。


2. 训练显存到底由哪些部分组成?

想真正理解 LoRA 和 QLoRA 省在哪里,先要把训练显存拆开看。

一个比较粗略但很有用的公式是:

训练显存 ≈ 模型权重 + 梯度 + 优化器状态 + 激活值 + 临时 buffer

这几个部分分别看一下。


2.1 模型权重

模型权重就是 Transformer 里的各种参数,比如:

  • Attention 里的 q_proj、k_proj、v_proj、o_proj;

  • MLP 里的 gate_proj、up_proj、down_proj;

  • LayerNorm 参数;

  • Embedding 参数;

  • LM Head 参数。

如果是 FP16 或 BF16,每个参数通常占 2 bytes。
如果是 FP32,每个参数通常占 4 bytes。

模型越大,这部分显存越高。


2.2 梯度

训练时,可训练参数需要保存梯度。

全量微调时,模型的大部分参数都需要更新,所以这些参数都要保存梯度。

但 LoRA 不一样。
LoRA 会冻结基础模型参数,只训练额外注入的小矩阵。

所以基础模型虽然还在参与前向计算,但它的参数通常不需要保存梯度。

这就是 LoRA 省显存的一个关键点。


2.3 优化器状态

以 AdamW 为例,除了参数和梯度,还要为每个可训练参数维护:

一阶矩 m
二阶矩 v

这两个状态是优化器用来稳定更新方向和步长的。

全量微调时,几乎所有参数都要进入 optimizer。
这意味着优化器状态会非常大。

LoRA 因为只训练少量 adapter 参数,所以 optimizer 只需要维护 LoRA 参数对应的状态。

这也是 LoRA 显存下降明显的原因之一。


2.4 激活值

激活值是前向传播过程中保存下来的中间结果,用于反向传播。

它和下面这些因素关系很大:

  • batch size;

  • sequence length;

  • hidden size;

  • 模型层数;

  • attention 实现方式;

  • 是否开启 gradient checkpointing。

很多时候,即使用了 LoRA 或 QLoRA,训练长上下文时还是会 OOM,原因就是激活值太大。

所以如果你的显存瓶颈主要来自长序列训练,只靠 LoRA / QLoRA 不一定够,还要配合:

  • 减小 sequence length;

  • 减小 batch size;

  • 使用 gradient accumulation;

  • 开启 gradient checkpointing;

  • 使用 flash attention。


2.5 临时 buffer

这部分包括 CUDA kernel、attention 计算、中间张量、通信缓存等。

它不是最容易估算的一部分,但实际训练时经常存在。

所以有时候你按公式算显存觉得够,实际一跑还是爆了。
这并不奇怪,因为框架和算子本身也会有额外开销。


3. LoRA 的核心思想:不要更新整个 W,只学习一个低秩增量

现在进入 LoRA。

普通线性层可以写成:

y = Wx

其中:

  • x 是输入;

  • W 是原始权重矩阵;

  • y 是输出。

全量微调时,我们直接更新 W

LoRA 的思路不一样。
它不直接更新原始权重 W,而是冻结 W,额外学习一个低秩增量。

LoRA 修改后的形式可以写成:

y = Wx + BAx

也可以理解为:

W' = W + ΔW
ΔW = BA

其中:

  • W:原始预训练权重,训练时冻结;

  • A:低秩矩阵,通常负责降维;

  • B:低秩矩阵,通常负责升维;

  • r:rank,也就是低秩维度;

  • BA:对原始权重更新量的近似。

LoRA 的核心思想就是:

不训练完整的巨大矩阵 W,而是训练两个很小的矩阵 A 和 B,用它们组合出一个低秩更新 ΔW。

这有点像在原模型旁边加了一个“小旁路”。
原来的模型能力还在,LoRA 只学习当前任务需要调整的那部分变化。

这里一定要注意一个点:

LoRA 不是把原始模型变小了。

基础模型仍然要加载到显存里,也仍然参与前向计算。
LoRA 省的是:

  • 可训练参数;

  • 这些参数对应的梯度;

  • 这些参数对应的优化器状态;

  • checkpoint 保存体积。

这和“模型本身变小”不是一回事。


4. 从 4096 × 4096 的例子看 LoRA 省了多少参数

只讲低秩分解可能还是有点抽象,我们用一个具体例子看。

假设原始线性层权重矩阵 W 的维度是:

4096 × 4096

全量微调时,这一层需要训练的参数量是:

4096 × 4096 = 16,777,216

也就是大约 1677 万个参数。

LoRA 会把更新量写成两个小矩阵:

A: r × d_in
B: d_out × r

如果:

d_in = 4096
d_out = 4096
r = 8

那么 LoRA 需要训练的参数量是:

r × d_in + d_out × r
= 8 × 4096 + 4096 × 8
= 32,768 + 32,768
= 65,536

对比一下:

全量微调参数量:16,777,216
LoRA 参数量:65,536

比例是:

65,536 / 16,777,216 ≈ 0.39%

也就是说,对于这个线性层来说,LoRA 只训练不到 0.4% 的参数。

这就是 LoRA 参数效率高的原因。

当然,这只是单个线性层的例子。
真实 Transformer 里通常会对多个模块注入 LoRA,比如:

  • q_proj;

  • k_proj;

  • v_proj;

  • o_proj;

  • gate_proj;

  • up_proj;

  • down_proj。

实际训练参数量取决于:

  • 模型层数;

  • hidden size;

  • rank;

  • target_modules;

  • 是否训练 bias;

  • 是否对 MLP 也注入 LoRA。

所以不要只看 rank 一个参数。
同样是 r=8,只注入 q_proj / v_proj,和注入所有 Linear,参数量和显存都不是一个级别。


5. LoRA 为什么常加在 q_proj、v_proj 上?

很多 Hugging Face / PEFT 的 LoRA 配置里,会看到类似这样的设置:

target_modules=["q_proj", "v_proj"]

很多人直接复制,但不一定知道为什么。

Transformer 的 Attention 里通常有几个关键线性层:

q_proj:生成 Query
k_proj:生成 Key
v_proj:生成 Value
o_proj:输出投影

简单理解:

  • q_proj 影响模型“关注什么”;

  • k_proj 影响内容如何被匹配;

  • v_proj 影响被聚合的信息内容;

  • o_proj 影响 attention 输出如何回到 hidden state。

很多任务中,只对 q_projv_proj 注入 LoRA,就能让模型获得不错的任务适配能力。

如果任务更复杂,比如希望模型在推理风格、指令跟随、领域表达上变化更明显,也可以考虑扩展到:

q_proj
k_proj
v_proj
o_proj
gate_proj
up_proj
down_proj

但 target_modules 不是越多越好。

模块越多,意味着:

  • 可训练参数更多;

  • 显存占用更高;

  • 训练时间更长;

  • 小数据集上更容易过拟合。

我的经验是,如果数据量不大,或者只是做格式、风格、简单任务适配,可以先从 q_projv_projq_projk_projv_projo_proj 开始。

不要一上来就所有 Linear 都加 LoRA。
那样看起来很强,但实际可能只是更容易把训练集记住。


6. LoRA 训练时冻结什么、更新什么?

LoRA 训练时最关键的一点是:

基础模型冻结,只训练 LoRA adapter。

伪代码大概是这样:

# 方法:只让 LoRA 参数参与训练
# 思想:冻结基础模型,减少梯度和优化器状态,只更新 adapter 参数
# 循环终止条件:遍历完模型所有参数后停止
for name, param in model.named_parameters():
    if "lora_" in name:
        param.requires_grad = True
    else:
        param.requires_grad = False

这段逻辑背后的含义很重要。

全量微调时:

基础模型参数:更新
基础模型梯度:需要保存
优化器状态:需要维护
checkpoint:保存完整模型或大量参数

LoRA 微调时:

基础模型参数:冻结
基础模型梯度:通常不保存
优化器状态:只维护 LoRA 参数
checkpoint:只保存 adapter

这就是 LoRA 为什么适合参数高效微调,也就是 PEFT。

但这里还有一个容易误解的点:

冻结参数不代表这些参数不参与计算。

基础模型仍然要参与前向传播。
输入仍然要经过 Transformer 的每一层。
只是反向传播时,不对基础权重做更新。

所以 LoRA 不是让训练完全变轻,而是让“需要更新的部分”大幅变小。


7. LoRA 推理时如何合并权重?

LoRA 训练完成后,我们学到的是一个增量:

ΔW = BA

原始权重是:

W

所以可以合并成:

W_merged = W + BA

也就是说,推理时可以有两种方式。


方式一:base model + adapter 动态加载

这种方式不合并权重,而是在推理时加载基础模型和 LoRA adapter。

优点是灵活:

  • 一个基础模型可以挂多个 adapter;

  • 不同业务可以切换不同 adapter;

  • adapter 文件很小,保存和分发方便。

比如一个通用基础模型,分别挂:

客服 adapter
法律 adapter
代码 adapter
医疗问答 adapter

这样比每个任务保存一份完整模型更省空间。


方式二:合并权重后部署

另一种方式是把 LoRA 权重合并进基础模型:

W_merged = W + BA

合并后推理路径更简单,不需要额外处理 LoRA 分支。

适合:

  • 固定任务;

  • 不需要频繁切换 adapter;

  • 希望部署链路更简单;

  • 推理服务不想额外依赖 PEFT adapter 加载逻辑。

但要注意,如果基础模型本身是量化模型,合并权重时要看框架是否支持,以及合并后的精度是否符合预期。

尤其是 QLoRA 训练出来的 adapter,实际部署时经常需要根据推理框架决定是单独加载 adapter,还是在合适精度下合并。


8. LoRA 不是免费午餐:rank、alpha、dropout 怎么看?

LoRA 确实省参数、省显存,但它不是免费午餐。

最常见的几个参数是:

rank r
lora_alpha
lora_dropout
target_modules

8.1 rank 不是越大越好

rank 决定 LoRA 分支的表达能力。

rank 太小,可能学不动。
表现为:

  • loss 降得慢;

  • 输出仍然像 base model;

  • 任务格式学不好;

  • 复杂任务适配不够。

rank 太大,也不一定好。
尤其在小数据集上,rank 太大可能更容易过拟合。

很多人微调效果不好,第一反应是把 rank 从 8 调到 16、32、64。
但我的经验是,数据质量、Prompt 模板、学习率、训练轮数往往比盲目调大 rank 更关键。


8.2 alpha 影响 LoRA 分支缩放

LoRA 里通常会有一个缩放系数,常见形式类似:

scaling = alpha / r

它会影响 LoRA 分支对最终输出的贡献。

alpha 太小,LoRA 分支影响不明显。
alpha 太大,训练可能不稳定,输出风格也可能被过度拉偏。

常见配置里,alpha 会设置为 r 的 2 倍或 4 倍,但这不是绝对规则。


8.3 dropout 可以缓解过拟合

lora_dropout 是 LoRA 分支上的 dropout。

如果数据量比较小,或者样本重复度高,可以适当加一点 dropout。
比如:

0.05
0.1

但 dropout 也不是越大越好。
太大可能导致模型学不到稳定模式。


9. QLoRA 和 LoRA 的关系:不是替代,而是进一步压缩基础模型

讲完 LoRA,再看 QLoRA。

QLoRA 不是推翻 LoRA。
它是在 LoRA 的基础上,进一步把冻结的基础模型用 4-bit 量化加载。

普通 LoRA 通常是:

基础模型用 FP16 / BF16 加载
基础模型冻结
训练 LoRA adapter

QLoRA 通常是:

基础模型用 4-bit 量化加载
基础模型冻结
训练 LoRA adapter
计算时按需要反量化

也就是说:

LoRA 主要省的是可训练参数、梯度和优化器状态。
QLoRA 在此基础上,进一步节省基础模型权重的显存。

这两者省的重点不是完全一样。

很多人说 QLoRA 更省显存,这没错。
但更准确的说法是:

QLoRA 让基础模型权重以更低 bit 加载,从而降低大模型本体占用的显存;同时仍然通过 LoRA adapter 做参数高效微调。


10. QLoRA 的 4-bit 量化到底省在哪里?

如果基础模型用 FP16 加载,每个参数通常占 2 bytes。

如果用 4-bit 加载,理论上每个参数占:

4 bit = 0.5 byte

以 7B 模型为例:

FP16 权重:7B × 2 bytes ≈ 14GB
4-bit 权重:7B × 0.5 bytes ≈ 3.5GB

当然,真实情况不会这么理想。
因为还会有:

  • 量化 scale;

  • metadata;

  • 计算缓存;

  • LoRA 参数;

  • 激活值;

  • 框架额外开销。

但即使如此,4-bit 量化对模型权重显存的降低仍然非常明显。

这也是为什么 QLoRA 能让很多人用单卡微调更大的模型。

不过要注意:

QLoRA 不是把所有训练都变成 4-bit。

通常是基础模型权重量化存储,计算时会涉及反量化。
LoRA adapter 仍然以较高精度训练,比如 FP16、BF16。

所以 QLoRA 不是无损魔法。
它是在显存、速度、数值稳定性之间做折中。


11. NF4、Double Quantization、Paged Optimizer 分别解决什么问题?

QLoRA 里经常会提到三个关键词:

NF4
Double Quantization
Paged Optimizer

这几个名字看起来有点论文味,但用工程语言解释并不复杂。


11.1 NF4:更适合神经网络权重的 4-bit 表示

NF4 全称是 NormalFloat 4-bit。

神经网络权重通常不是均匀分布的,而是更接近某种正态分布。
如果随便用普通 4-bit 量化,可能会损失比较多信息。

NF4 的想法是:

既然权重分布有特点,那就设计一种更适合这种分布的 4-bit 表示方式。

所以 NF4 解决的是:

如何让 4-bit 量化更适合神经网络权重分布

它不是简单地把数字粗暴压成 16 个等级,而是尽量让低 bit 表示更贴合权重分布。


11.2 Double Quantization:量化参数也要省

量化不是只保存 4-bit 权重就完了。

通常还要保存一些量化相关参数,比如 scale。
这些参数本身也会占空间。

Double Quantization 的思路是:

对量化常数再做一次量化,进一步减少额外存储开销。

它解决的是:

量化权重省了,但量化参数本身也占显存

虽然这部分不像模型权重那么大,但在大模型规模下,能省一点是一点。


11.3 Paged Optimizer:缓解训练时显存峰值

训练过程中,显存不是一直平稳的。

有些阶段显存会突然升高,比如:

  • 长序列 batch;

  • 反向传播某些阶段;

  • optimizer step;

  • 临时张量分配。

Paged Optimizer 借助类似分页的机制,在 GPU 和 CPU 之间管理优化器状态,从而缓解显存峰值问题。

它解决的是:

训练过程中的显存峰值和不稳定 OOM

这对显存卡得很紧的训练特别有用。


12. 实际训练时如何估算显存?

现在把 LoRA 和 QLoRA 放回训练显存公式里看。

训练显存可以粗略拆成:

模型权重 + 梯度 + 优化器状态 + 激活值 + 临时 buffer

12.1 全量微调

全量微调需要保存:

基础模型权重
基础模型梯度
Adam 一阶矩
Adam 二阶矩
激活值
临时 buffer

如果模型很大,这几部分叠加起来非常夸张。

尤其是 Adam 优化器状态。
它不是一个小开销,而是和可训练参数规模强相关。


12.2 LoRA

LoRA 里,基础模型仍然要加载。

但是:

基础模型被冻结
基础模型参数不进入 optimizer
基础模型不保存对应优化器状态
只训练 LoRA 参数
只保存 LoRA 参数的梯度和优化器状态

所以 LoRA 主要减少的是:

可训练参数
梯度
优化器状态
checkpoint 大小

但基础模型权重仍然在。
激活值也仍然在。

所以如果你用 LoRA 训练时仍然 OOM,不一定是 LoRA 没生效,可能是:

  • sequence length 太长;

  • batch size 太大;

  • 没开 gradient checkpointing;

  • target_modules 太多;

  • 不小心把全量参数也设成可训练;

  • optimizer 接收了不该训练的参数。


12.3 QLoRA

QLoRA 在 LoRA 的基础上进一步做了:

基础模型权重 4-bit 加载
基础模型冻结
训练 LoRA adapter

所以 QLoRA 进一步减少的是:

基础模型权重显存

但它仍然需要:

LoRA 参数
LoRA 梯度
LoRA 优化器状态
激活值
attention 计算开销
临时 buffer

所以判断显存问题时,可以这样看:

如果模型加载阶段就 OOM,QLoRA 帮助很大。
如果反向传播阶段 OOM,还要继续看 batch size、sequence length、gradient checkpointing 和 attention 实现。

这也是为什么有些人用了 QLoRA,模型能加载了,但一训练还是 OOM。


13. 一个最小 PEFT / bitsandbytes 配置示例

CSDN 上很多文章会直接贴一大段训练脚本,但我觉得更重要的是看懂配置。

一个最小的 LoRA 配置大概是这样:

from peft import LoraConfig

lora_config = LoraConfig(
    r=8,
    lora_alpha=16,
    target_modules=["q_proj", "v_proj"],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM"
)

几个参数解释一下:

r:低秩维度,控制 LoRA 分支表达能力
lora_alpha:缩放系数,影响 LoRA 分支强度
target_modules:注入 LoRA 的模块
lora_dropout:LoRA 分支 dropout,用于缓解过拟合
bias:是否训练 bias
task_type:任务类型,例如因果语言模型

如果是 QLoRA,还会涉及 bitsandbytes 的 4-bit 加载配置:

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
)

这里几个参数也很关键:

load_in_4bit=True:使用 4-bit 加载基础模型
bnb_4bit_quant_type="nf4":使用 NF4 量化类型
bnb_4bit_use_double_quant=True:启用 Double Quantization
bnb_4bit_compute_dtype=torch.bfloat16:计算时使用 bfloat16

配置不是固定模板。
rtarget_moduleslearning ratebatch sizesequence length 都要结合任务和数据来调。

不要只复制别人的配置。
同样是 r=8,数据质量不同、任务难度不同、模型规模不同,效果会差很多。


14. LoRA 和 QLoRA 优缺点对比

下面用一个表格总结一下。

对比项 LoRA QLoRA
基础模型加载 通常 FP16 / BF16 4-bit 量化
训练参数 只训练 adapter 只训练 adapter
主要节省部分 训练参数、梯度、优化器状态 在 LoRA 基础上进一步节省基础模型权重
显存占用 比全量微调低很多 通常比 LoRA 更低
训练速度 通常更简单稳定 可能受量化和反量化影响
数值稳定性 相对更稳 需要关注量化配置
硬件门槛 中等 更低
配置复杂度 较低 稍高
适合场景 显存相对够、追求稳定 单卡显存有限、模型较大
部署复杂度 较低 稍高

这里不要简单理解成:

QLoRA 一定比 LoRA 好。

更准确的说法是:

  • 显存够,LoRA 更简单、更稳;

  • 显存不够,QLoRA 能把门槛降下来;

  • QLoRA 更省显存,但可能带来速度、数值稳定性、框架兼容性方面的问题。

工程上没有绝对最优,只有适合当前资源和任务的方案。


15. 实际项目里怎么选 LoRA 还是 QLoRA?

我一般会按下面这个思路判断。


15.1 显存够,优先 LoRA

如果你的显存足够,比如模型能用 FP16 / BF16 正常加载和训练 LoRA,那优先用 LoRA。

原因很简单:

  • 配置更简单;

  • 训练更稳定;

  • 排查问题更容易;

  • 部署链路更清晰。

在工程项目里,稳定性很重要。
不是越极限越好。


15.2 显存不够,再考虑 QLoRA

如果模型刚加载就爆显存,或者 LoRA 训练显存压力很大,可以考虑 QLoRA。

典型场景:

  • 单卡 16GB / 24GB;

  • 想微调 7B、13B 模型;

  • 预算有限;

  • 可以接受训练速度稍慢;

  • 能处理 bitsandbytes、量化配置、环境兼容问题。

QLoRA 的价值在于降低硬件门槛。
但你要接受它带来的额外复杂度。


15.3 数据少,不要盲目加大 rank

如果数据集只有几百条、几千条,rank 拉太大不一定有帮助。

小数据集上更常见的问题是过拟合,而不是模型容量不够。

这时更应该关注:

  • 数据是否干净;

  • 样本格式是否统一;

  • 是否有重复样本;

  • Prompt 模板是否一致;

  • 验证集是否能真实反映任务;

  • epoch 是否太多。

很多时候,清洗数据比调 rank 更有用。


15.4 不是所有问题都该微调

这一点很重要。

如果问题本质是知识更新,比如:

公司制度变化
产品文档更新
业务资料查询
FAQ 经常变化

那优先考虑 RAG,而不是微调。

如果问题只是输出格式不稳定,可能 Prompt 或 few-shot 就能解决。

可以用一个简单判断:

知识缺失?优先 RAG
格式不稳定?先调 Prompt / few-shot
风格迁移?可以 LoRA
任务模式固定?可以 LoRA / QLoRA
显存不够?考虑 QLoRA
数据很少?先别急着调大 rank

微调不是万能的。
有些问题用微调解决,反而会增加维护成本。


16. 常见踩坑:rank、数据、学习率和过拟合

这一节讲几个真实训练里很常见的坑。


16.1 rank 太小,模型学不动

表现可能是:

  • loss 降得很慢;

  • 输出格式没变化;

  • 指令跟随能力没明显提升;

  • 任务特征学不到。

这种情况可以适当提高 rank,比如从 8 调到 16。

但不要只看训练 loss。
还要看验证集和真实样例效果。


16.2 rank 太大,小数据集容易过拟合

rank 大,表达能力更强,但也更容易记住训练集。

表现可能是:

  • 训练集回答很好;

  • 验证集效果下降;

  • 输出开始套训练数据里的固定表达;

  • 泛化能力变差。

这时可以考虑:

  • 降低 rank;

  • 减少 epoch;

  • 加大 dropout;

  • 清洗重复数据;

  • 增加验证集;

  • 降低 learning rate。


16.3 学习率不合适

LoRA 训练参数少,但不代表学习率可以随便设。

学习率太大,可能导致:

  • 输出格式崩;

  • 模型变得啰嗦;

  • 学到错误风格;

  • loss 波动明显。

学习率太小,可能导致:

  • 学不动;

  • 收敛很慢;

  • 微调前后差异不明显。

所以学习率要结合任务、数据量、batch size 和训练轮数一起看。


16.4 数据质量差,比参数设置更致命

微调里最容易被低估的是数据。

如果训练数据里有:

  • 错误答案;

  • 格式混乱;

  • 重复样本;

  • 低质量合成数据;

  • 用户问题和回答不匹配;

  • Prompt 模板不统一;

那 LoRA / QLoRA 都救不了。

大模型微调不是把数据丢进去就行。
数据质量通常比盲目调 rank 更重要。


16.5 sequence length 设置太大

很多人用了 QLoRA 之后,以为显存问题就解决了,于是把 sequence length 拉得很长。

结果一训练还是 OOM。

原因前面说过:
长序列会显著增加激活值和 attention 计算开销。

所以如果显存紧张,优先检查:

max_seq_length
per_device_train_batch_size
gradient_accumulation_steps
gradient_checkpointing
flash attention

不是所有 OOM 都是模型权重导致的。


17. LoRA / QLoRA 选型建议和排查清单

最后给一份比较实用的清单。


17.1 选型建议

显存够,优先 LoRA,简单稳定。
显存不够,再考虑 QLoRA。
数据少,不要盲目加大 rank。
知识更新类问题,优先 RAG。
格式控制类问题,先试 Prompt / few-shot。
需要多任务复用,可以保留多个 adapter。
固定任务部署,可以考虑 merge 权重。
任务复杂度高,可以适当扩大 target_modules。
小数据集训练,重点关注过拟合。

17.2 显存排查清单

如果模型加载阶段就 OOM:

检查基础模型加载精度
考虑 8-bit / 4-bit 加载
考虑 QLoRA
检查 device_map 配置
检查是否加载了不必要的模型副本

如果训练反向传播阶段 OOM:

降低 batch size
降低 sequence length
开启 gradient checkpointing
使用 flash attention
检查是否错误训练了全量参数
检查 target_modules 是否过多
检查是否保存了过多中间结果

如果用了 LoRA 显存没有明显下降:

检查 requires_grad 是否正确
检查 optimizer 是否只接收 LoRA 参数
检查是否误把 base model 参数加入 optimizer
检查 target_modules 是否设置过多
检查是否保存了全量 checkpoint

17.3 效果排查清单

如果模型学不会:

检查训练数据格式
检查 Prompt 模板是否一致
检查 learning rate 是否太低
检查 rank 是否太小
检查 target_modules 是否太少
检查训练轮数是否不够

如果模型过拟合:

减少 epoch
降低 rank
增加 lora_dropout
降低 learning rate
清洗重复样本
增加验证集
检查训练集和测试集是否泄漏

如果输出格式混乱:

检查训练样本格式是否统一
检查 system prompt / user prompt 是否一致
检查推理时模板是否和训练时一致
检查是否混入不同风格数据
检查 stop words 和生成参数

18. 总结:LoRA 和 QLoRA 省的不是同一块

最后回到标题:

LoRA 和 QLoRA 到底省在哪里?

可以简单总结成几句话。

全量微调贵,是因为训练时不只要保存模型权重,还要保存梯度、优化器状态、激活值和临时 buffer。

LoRA 的核心是冻结基础模型,只训练两个低秩矩阵 A 和 B。
它主要省的是:

可训练参数
梯度
优化器状态
checkpoint 体积

但 LoRA 并没有让基础模型消失。
基础模型仍然要加载,仍然参与前向计算。

QLoRA 则是在 LoRA 的基础上,把冻结的基础模型用 4-bit 量化加载。
它进一步省的是:

基础模型权重显存

所以更准确地说:

LoRA:减少要训练的东西
QLoRA:进一步压缩要加载的基础模型

LoRA 不是免费午餐,rank 太小可能表达能力不够,rank 太大又可能过拟合。
QLoRA 能让单卡微调大模型更现实,但它也会带来量化、速度、数值稳定性和环境兼容问题。

真正做大模型微调时,不要只问:

LoRA 和 QLoRA 哪个更好?

更应该先问:

我的瓶颈到底是显存、数据、任务定义,还是评测体系?

如果瓶颈是显存,可以考虑 LoRA / QLoRA。
如果瓶颈是知识更新,可能 RAG 更合适。
如果只是输出格式不稳定,Prompt 或 few-shot 可能就够了。
如果数据质量很差,换什么微调方法都不会有太好结果。

大模型微调的难点,不只是把脚本跑起来,而是知道每个技术选择背后到底在省什么、牺牲什么,以及它是否真的适合当前业务场景。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐