LLaMA、Mistral、Qwen 这类大语言模型动辄拥有数十亿参数,在自定义数据上进行全量微调的代价极高:一个 65B 的模型,仅以 float16 精度加载就需要消耗约 130GB 的显存,使用顶配硬件跑上几天几周是常事。然而,对于大多数下游任务来说,我们其实并不需要更新模型的所有参数。
于是,研究者们开始思考:能否只微调一小部分参数,同时又能达到不错的效果呢?答案是肯定的。这类方法被统称为参数高效微调。其中,LoRA、QLoRA 和 DoRA 各自从不同的角度切入,解决了这个问题。

LoRA——低秩自适应
论文:LoRA: Low-Rank Adaptation of Large Language Models, Hu et al. (ICLR 2022)
核心思想
LoRA 的思路非常巧妙且简洁:它冻结了预训练模型原始的权重矩阵 W,然后在其旁边挂载两个小的、可训练的矩阵 A 和 B。前向传播时的计算表达式为:
output = W·x + (B·A)·x × (alpha/r)
在整个训练过程中,只更新 A 和 B,而 W 保持冻结。这里的秩 r 是分解的瓶颈维度,常见的取值为 4、8 或 16。alpha 是一个缩放因子,用于控制 LoRA 更新的强度。
参数量的减少效果极其显著。以一个 (4096, 4096) 的权重矩阵为例,原始参数量是 1670 万。如果换成秩 r=8 的 LoRA,矩阵 A 的维度是 (8, 4096),包含 32,768 个参数;矩阵 B 的维度是 (4096, 8),同样包含 32,768 个参数。两者相加总计 65,536 个参数——这比原始参数少了 99.6%。
LoRA 论文中的一个关键发现是,在微调过程中,大部分有意义的权重更新本来就集中在低维子空间里。因此,将更新约束在低秩矩阵上,并不会造成太大的性能损失。
微软官方仓库中 Linear 层的实现清晰地展示了这一点:
# Source: github.com/microsoft/LoRA — loralib/layers.py
class Linear(nn.Linear, LoRALayer):
def __init__(
self,
in_features: int,
out_features: int,
r: int = 0,
lora_alpha: int = 1,
lora_dropout: float = 0.,
merge_weights: bool = True,
**kwargs
):
nn.Linear.__init__(self, in_features, out_features, **kwargs)
LoRALayer.__init__(self, r=r, lora_alpha=lora_alpha,
lora_dropout=lora_dropout,
merge_weights=merge_weights)
if r > 0:
self.lora_A = nn.Parameter(
self.weight.new_zeros((r, in_features))
)
self.lora_B = nn.Parameter(
self.weight.new_zeros((out_features, r))
)
self.scaling = self.lora_alpha / self.r
# Freeze the pretrained weights
self.weight.requires_grad = False
def forward(self, x: torch.Tensor):
if self.r > 0 and not self.merged:
# Original frozen weights + low-rank update
result = F.linear(x, self.weight, bias=self.bias)
result += (
self.lora_dropout(x)
@ self.lora_A.transpose(0, 1)
@ self.lora_B.transpose(0, 1)
) * self.scaling
return result
else:
return F.linear(x, self.weight, bias=self.bias)
在实际应用中,例如替换注意力投影层,代码会像这样写:
# Source: github.com/microsoft/LoRA — README [1]
import loralib as lora
# Before: standard attention projection
# qkv_proj = nn.Linear(d_model, 3*d_model)
# After: apply LoRA to Q and V, freeze K
qkv_proj = lora.MergedLinear(
d_model, 3*d_model,
r=8,
enable_lora=[True, False, True] # Q=LoRA, K=frozen, V=LoRA
)
# Mark only LoRA parameters as trainable
lora.mark_only_lora_as_trainable(model)
基准测试结果(来自 LoRA 论文)
论文在 GPT-2(自然语言生成)和 RoBERTa/DeBERTa(GLUE 基准)上进行了评测。下表展示了 GPT-2 在 E2E NLG Challenge 上的表现,对比了 LoRA 与全量微调及其他 PEFT 方法:
| Method | Trainable Params | BLEU | NIST | MET | ROUGE-L | CIDEr |
|-------------------|-----------------|------|------|------|----------|-------|
| Full Fine-Tuning | 117M | 68.2 | 8.62 | 46.2 | 71.0 | 2.47 |
| Adapter (Houlsby) | 1.0M | 66.3 | 8.41 | 45.0 | 69.8 | 2.40 |
| Prefix Tuning | 0.35M | 68.1 | 8.59 | 46.3 | 70.8 | 2.47 |
| LoRA (r=4) | 0.77M | 70.4 | 8.85 | 46.8 | 71.8 | 2.53 |
令人惊讶的是,LoRA 不仅追平了全量微调,在仅训练不到 1% 参数的情况下,其性能甚至实现了反超。论文将这一现象归因于正则化效应:低秩约束本身起到了防止过拟合的作用。
关键超参数
- 秩 (r):定义了瓶颈维度。
r 越大,模型的容量越高,可训练参数也越多。通常,4 到 16 的范围能覆盖绝大多数场景。
- Alpha (lora_alpha):缩放因子。LoRA 更新的有效学习率大致等于
alpha / r。通常设置 alpha = 2r 是一个不错的起点。
- 目标模块 (Target Modules):决定 LoRA 适配器挂载到哪些层。注意力机制中的
q_proj 和 v_proj 是最常见的选择。
QLoRA——量化 LoRA
论文:QLoRA: Efficient Finetuning of Quantized LLMs, Dettmers et al. (NeurIPS 2023)
LoRA 未彻底解决的问题
LoRA 虽然大幅减少了需要训练的参数数量,但基础模型仍然需要完整地加载到显存中。一个 65B 的模型以 float16 精度加载需要 130GB 显存,通常需要多块 A100 这样的高端 GPU,这对于大多数研究者来说门槛过高。
QLoRA 的核心思路是:先将基础模型量化为 4 位精度,然后在其上以 16 位精度运行 LoRA 适配器进行微调。
三大技术创新(来自 QLoRA 论文)
QLoRA 论文同时引入了三项相互配合的技术,以实现高效且低损失的量化微调。
- NF4 (4-bit NormalFloat):这是一种专门为正态分布权重设计的 4 位数据类型。神经网络权重天然服从近似正态分布,从信息论角度看,NF4 是最优的编码方式,其存储效率比 INT4 和 FP4 都要高。
- 双重量化:量化过程中用于将 4 位值转换回浮点数的“量化常数”本身也会占用内存。QLoRA 进一步将这些常数也进行量化,在 65B 模型上大约能节省 3GB 内存。
- 分页优化器:针对使用梯度检查点技术时可能出现的内存峰值问题,QLoRA 利用了 NVIDIA 的统一内存机制。当某条序列的计算即将触发 GPU 内存不足(OOM)时,系统会自动将优化器状态卸载到 CPU 内存中,待需要时再换回,从而避免了训练中断。
代码(来自 artidoro/qlora)
以下是使用 QLoRA 进行微调的典型代码流程:
# Source: github.com/artidoro/qlora — qlora.py [2]
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
import torch
# Step 1: Define 4-bit quantization config (NF4)
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4", # NormalFloat4
bnb_4bit_compute_dtype=torch.bfloat16, # Compute in bf16
bnb_4bit_use_double_quant=True, # Double quantization
)
# Step 2: Load base model in 4-bit
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b-hf",
quantization_config=bnb_config,
device_map="auto",
)
# Step 3: Prepare for k-bit training (handles frozen layer casting)
model = prepare_model_for_kbit_training(model)
# Step 4: Apply LoRA adapters in 16-bit on top
lora_config = LoraConfig(
r=64,
lora_alpha=16,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
)
model = get_peft_model(model, lora_config)
在整个过程中,基础模型的权重始终保持为 NF4(4位、冻结状态),只有 LoRA 适配器的权重以 bf16 精度参与训练和更新。
基准测试结果(来自 QLoRA 论文)
QLoRA 被用于训练出了著名的 Guanaco 系列模型。在由 GPT-4 进行评估的 Vicuna 基准测试中,Guanaco 65B 模型在单块 48GB 显存的 GPU 上训练了 24 小时,就达到了 ChatGPT 性能的 99.3%。
更关键的一点是,4 位量化相比 16 位的 LoRA 几乎没有造成性能损失:
| Method | Model | Memory Usage | Vicuna Score vs ChatGPT |
|----------------------|-------|--------------|--------------------------|
| Full Fine-Tuning (fp16) | 65B | >780 GB | — |
| LoRA (fp16) | 65B | ~130 GB | — |
| QLoRA (NF4) | 65B | ~48 GB | 99.3% |
| QLoRA (NF4) | 33B | ~24 GB | 97.8% |
| QLoRA (NF4) | 7B | ~5 GB | ~87% |
以 7B 的 Guanaco 模型为例,它仅占用约 5GB 显存,却在 Vicuna 基准上以超过 20 个百分点的巨大优势碾压了 Alpaca 65B。这正是 QLoRA 革命性的意义所在:它使得使用一块消费级 GPU 来微调大规模大语言模型成为可能,极大地降低了技术门槛。
DoRA——权重分解低秩自适应
论文:DoRA: Weight-Decomposed Low-Rank Adaptation, Liu et al. (ICML 2024, Oral)
尽管 LoRA 表现优异,但其与全量微调之间始终存在一道精度鸿沟,尤其是在秩 r 取值较低时更为明显。LoRA 似乎无法完全复现全参数更新时的梯度学习行为。
问题出在哪里?DoRA 论文通过一种新颖的权重分解分析给出了答案。
核心洞察
任何一个权重矩阵 W 都可以被分解为两个分量:
W = m × (V / ||V||_c)
其中,m 是幅度分量,它是一个标量(对于矩阵的每一行),反映了每个输出神经元权重的“大小”;V / ||V||_c 是方向分量,即权重的单位方向向量。
DoRA 论文发现,在全量微调过程中,幅度和方向是以一种灵活且耦合的方式同步更新的。然而,LoRA 的更新方式将两者绑定在一起,主要只改变了权重的朝向(方向),这反而在一定程度上限制了模型的学习能力。
DoRA 的解决方案是将两者拆开:在冻结分解结构后,只让 LoRA 作用于方向分量,同时将幅度 m 当作独立的可学习标量进行自由更新。
W' = (m + Δm) × ((V + ΔV_LoRA) / ||V + ΔV_LoRA||_c)
这样一来,LoRA 的学习模式就更接近全量微调了。并且,由于幅度和方向在部署前可以合并回单个权重矩阵,因此在推理阶段不会产生任何额外的开销。
代码(来自 NVlabs/DoRA)
以下是 DoRA 层前向传播逻辑的简化实现,展示了其核心的分解机制:
# Source: github.com/NVlabs/DoRA — adapted from DoRA paper implementation [3]
import torch
import torch.nn as nn
import torch.nn.functional as F
class DoRALayer(nn.Module):
def __init__(self, d_in, d_out, rank, lora_alpha):
super().__init__()
# Frozen pretrained weight
self.weight = nn.Parameter(
torch.randn(d_out, d_in), requires_grad=False
)
# Learnable magnitude (one scalar per output neuron)
self.m = nn.Parameter(
self.weight.norm(p=2, dim=1, keepdim=True)
)
# LoRA matrices for directional updates (trainable)
std = 1 / torch.sqrt(torch.tensor(rank).float())
self.lora_A = nn.Parameter(torch.randn(d_in, rank) * std)
self.lora_B = nn.Parameter(torch.zeros(rank, d_out))
self.rank = rank
self.scaling = lora_alpha / rank
def forward(self, x):
# Compute the directional update from LoRA
lora_update = (self.lora_A @ self.lora_B).T * self.scaling
# Adapted weight = base weight + LoRA update
adapted = self.weight + lora_update
# Column-wise normalization → unit direction vectors
column_norms = adapted.norm(p=2, dim=1, keepdim=True)
V_normalized = adapted / column_norms
# Scale by learned magnitude
effective_weight = self.m * V_normalized
return F.linear(x, effective_weight)
更便捷的是,通过 HuggingFace PEFT 库(peft>=0.9.0 已支持),启用 DoRA 仅需一个标志位:
# Source: HuggingFace PEFT documentation — DoRA is supported from peft>=0.9.0 [3]
from peft import LoraConfig, get_peft_model
lora_config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
use_dora=True, # ← This single flag enables DoRA
)
model = get_peft_model(model, lora_config)
基准测试结果(来自 DoRA 论文 [3])
DoRA 论文在 8 个常识推理数据集(BoolQ、PIQA、HellaSwag、WinoGrande、ARC-e、ARC-c、OBQA 及综合平均)上进行了评测。在 LLaMA-7B 和 LLaMA2-7B 模型上,与相同秩、相同可训练参数量的 LoRA 进行公平对比:
| Method | BoolQ | PIQA | HellaSwag | WinoGrande | ARC-e | ARC-c | OBQA | Avg |
|--------------|-------|------|-----------|------------|-------|-------|------|-------|
| Full FT | 69.4 | 82.3 | 89.7 | 82.4 | 79.8 | 60.7 | 81.6 | 77.99 |
| LoRA (r=32) | 68.9 | 80.9 | 90.0 | 82.1 | 78.2 | 59.8 | 80.0 | 77.13 |
| DoRA (r=32) | 70.0 | 83.6 | 91.0 | 83.0 | 81.4 | 65.8 | 83.4 | 79.75 |
结果显示,DoRA 在全部 8 个数据集上全面胜出。差距在复杂推理任务上尤其明显,例如 ARC-c 数据集上的分数从 LoRA 的 59.8 跃升至 65.8,提升幅度相当可观。论文还在 LLaVA-1.5-7B 的视觉指令调优和 VL-BART 的图像/视频-文本理解任务上进行了实验,DoRA 同样全面超越了 LoRA。
综合对比
内存需求
| Method | LLaMA 7B | LLaMA 13B | LLaMA 33B | LLaMA 65B |
|-------------------------|----------|-----------|-----------|-----------|
| Full Fine-Tuning (fp16) | ~28 GB | ~52 GB | ~130 GB | ~260 GB |
| LoRA (fp16) | ~14 GB | ~26 GB | ~65 GB | ~130 GB |
| QLoRA (NF4) | ~5 GB | ~8 GB | ~20 GB | ~48 GB |
| DoRA (fp16) | ~14 GB | ~26 GB | ~65 GB | ~130 GB |
DoRA 由于多了一个幅度向量(每个目标层的每个输出神经元对应一个标量),会有极少量额外开销,但在实际应用中通常可以忽略不计。
与全量微调的性能对比
| Method | Commonsense Reasoning | Instruction Tuning | Memory |
|-------------------|-----------------------|------------------------|------------|
| Full Fine-Tuning | Baseline | Baseline | Very High |
| LoRA | -0.86 avg | Comparable | Medium |
| QLoRA | ~Same as LoRA | 99.3% of ChatGPT | Low |
| DoRA | +2.62 avg over LoRA | Better than LoRA | Medium |
训练速度(相对值)
| Method | Speed |
|--------|--------------------------------------------------|
| LoRA | Fast |
| DoRA | Fast (near identical to LoRA) |
| QLoRA | Moderate (quantize/dequantize overhead) |
各场景下的最佳选择
该用 LoRA 的场景
当你拥有 16GB 以上显存,需要微调的模型不超过 13B 左右,并且希望使用一个稳定可靠、经过大量实战验证的方案时,LoRA 是理想选择。它是所有后续方法的基础,HuggingFace 的 PEFT 生态主要围绕它构建,工具链最为成熟。对于初次接触 PEFT 的开发者来说,从这里入手最为省心。
该用 QLoRA 的场景
当你的显存资源有限,但又希望尝试微调 30B 以上的大模型时,QLoRA 几乎是唯一的选择。它让使用单块 48GB GPU 微调 LLaMA-65B 成为现实,24GB 显存就能应对 LLaMA-33B。如果你在使用免费的 Colab T4(约15GB显存)并想动一动 20B+ 的模型,QLoRA 是你的不二之选。
该用 DoRA 的场景
当你在与 LoRA 相同的参数预算下,追求最高的微调精度时,DoRA 是 LoRA 的完美即插即用替代品。只需在配置中添加 use_dora=True 即可,推理零成本,并且在复杂推理任务上的提升尤为突出。
该用 QLoRA + DoRA(QDoRA)的场景
当你既希望节省显存,又渴望获得高精度时,可以尝试这种组合。该组合已获得官方支持,实验数据证明其效果优于单纯的 QLoRA。部分早期实验甚至表明 QDoRA 能够持平乃至超过全量微调的效果。
总结
2025年的大语言模型微调生态可以清晰地分为三个层次:LoRA 是坚实底座,简单、快速、生态成熟,足以应对多数场景;QLoRA 则打开了使用消费级显卡微调大模型的大门,在显著降低显存需求的同时,精度损失微乎其微;DoRA 则是一次“免费升级”——在与 LoRA 成本几乎相同的前提下,提供了更好的性能,只需一行 use_dora=True 代码即可启用。
这三种方法各司其职,互为补充。作为开发者和研究者,你唯一需要权衡的是,哪种 PEFT 方案最契合你手头的硬件条件和对模型精度的具体要求。想了解更多此类技术实践与深度讨论,欢迎访问云栈社区。