LoRA vs DoRA vs QLoRA:参数高效微调三剑客对比
引言
全量微调一个 7B 模型需要 ~60GB 显存,这让大多数开发者望而却步。参数高效微调(PEFT)方法通过只训练极少量参数,实现了接近全量微调的效果。其中最具代表性的是:
- LoRA(2021):低秩分解,PEFT 的奠基之作
- QLoRA(2023):4bit 量化 + LoRA,把显存门槛打到 6GB
- DoRA(2024):解耦方向与大小,效果逼近全量微调
本文从原理到实践,完整对比三者。
1. LoRA:低秩适配
1.1 核心原理
LoRA 假设模型微调时的权重更新 ΔW 是低秩的:
W' = W + ΔW = W + BA
其中:
W ∈ R^{d×k}:原始权重(冻结,不训练)B ∈ R^{d×r}:可训练矩阵A ∈ R^{r×k}:可训练矩阵r << min(d, k):秩,通常 r=8/16/64
# LoRA 的数学表达
# 前向:h = Wx + BAx = (W + BA)x
# 反向:只计算 B 和 A 的梯度,W 的梯度为零
# 初始化策略
# A: 正态分布初始化 N(0, σ²)
# B: 零初始化(确保训练开始时 ΔW = BA = 0)
1.2 代码实现
import torch
import torch.nn as nn
class LoRALayer(nn.Module):
"""LoRA 层的独立实现"""
def __init__(
self,
in_features: int,
out_features: int,
r: int = 8,
alpha: int = 16,
dropout: float = 0.0,
):
super().__init__()
self.r = r
self.scale = alpha / r # 缩放系数
# 原始权重(冻结)
self.base_weight = nn.Parameter(
torch.randn(out_features, in_features), requires_grad=False
)
# LoRA 矩阵
self.lora_A = nn.Parameter(torch.randn(r, in_features) * 0.01)
self.lora_B = nn.Parameter(torch.zeros(out_features, r))
self.dropout = nn.Dropout(dropout)
def forward(self, x: torch.Tensor) -> torch.Tensor:
base_out = x @ self.base_weight.T
lora_out = (self.dropout(x) @ self.lora_A.T @ self.lora_B.T) * self.scale
return base_out + lora_out
# 使用 PEFT 库(推荐)
from peft import LoraConfig, get_peft_model
config = LoraConfig(
r=16,
lora_alpha=32,
lora_dropout=0.05,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
bias="none",
task_type="CAUSAL_LM",
)
model = get_peft_model(base_model, config)
1.3 参数量分析
以 LLaMA-7B(hidden_dim=4096)为例:
| target_modules | r=8 | r=16 | r=64 |
|---|---|---|---|
| q_proj only | 2.1M (0.03%) | 4.2M (0.06%) | 16.8M (0.24%) |
| q,k,v,o | 8.4M (0.12%) | 16.8M (0.24%) | 67.1M (0.96%) |
| q,k,v,o,mlp | 21.0M (0.30%) | 41.9M (0.60%) | 167.8M (2.40%) |
2. DoRA:解耦方向与大小
2.1 核心动机
LoRA 的局限:ΔW = BA 是对 W 的加法更新,但全量微调时权重的变化既有方向变化也有大小变化。LoRA 的加法形式难以同时建模两者。
DoRA(Decomposed Low-Rank Adaptation)将权重分解为方向和大小两个独立分量:
W' = m · (V + ΔV) / ||V + ΔV||
其中:
V = W / ||W|| (方向,冻结)
m = ||W|| (大小,可训练标量)
ΔV = BA / ||BA|| (方向更新,LoRA 低秩分解)
2.2 与 LoRA 的关键区别
| 维度 | LoRA | DoRA |
|---|---|---|
| 更新方式 | 加法:W + BA | 解耦:方向 + 大小 |
| 大小调整 | ❌ 隐式(通过 BA) | ✅ 显式(独立参数 m) |
| 方向调整 | BA 的方向 | 低秩 ΔV |
| 参数量 | 2r(d+k) | 2r(d+k) + d(多了 m) |
| 效果 | 良好 | 更接近全量微调 |
2.3 代码实现
import torch
import torch.nn as nn
import torch.nn.functional as F
from typing import Optional
class DoRALayer(nn.Module):
"""DoRA 层独立实现"""
def __init__(
self,
in_features: int,
out_features: int,
r: int = 16,
alpha: int = 32,
dropout: float = 0.0,
):
super().__init__()
self.r = r
self.scale = alpha / r
# 原始权重
self.base_weight = nn.Parameter(
torch.randn(out_features, in_features), requires_grad=False
)
# 计算方向和大小
with torch.no_grad():
weight_norm = torch.norm(self.base_weight, dim=1, keepdim=True)
self.weight_dir = self.base_weight / (weight_norm + 1e-8)
self.magnitude = nn.Parameter(weight_norm.clone()) # 可训练大小
# LoRA 分量(方向更新)
self.lora_A = nn.Parameter(torch.randn(r, in_features) * 0.01)
self.lora_B = nn.Parameter(torch.zeros(out_features, r))
self.dropout = nn.Dropout(dropout)
def forward(self, x: torch.Tensor) -> torch.Tensor:
# LoRA 方向更新
lora_update = (self.dropout(x) @ self.lora_A.T @ self.lora_B.T) * self.scale
# 合并方向
updated_dir = self.weight_dir + lora_update # (out, in)
# 归一化方向
updated_dir_norm = F.normalize(updated_dir, dim=1, p=2)
# 应用大小
weight = self.magnitude * updated_dir_norm # (out, in)
# 前向
return x @ weight.T
# 使用 PEFT 库
from peft import LoraConfig, get_peft_model
config = LoraConfig(
r=16,
lora_alpha=32,
lora_dropout=0.05,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
bias="none",
task_type="CAUSAL_LM",
use_dora=True, # 启用 DoRA(PEFT >= 0.9.0)
)
model = get_peft_model(base_model, config)
2.4 DoRA 的梯度分析
# DoRA 的梯度比 LoRA 更健康
# LoRA 梯度:dL/dB = dL/dW · A^T, dL/dA = B^T · dL/dW
# DoRA 梯度:额外有 dL/dm = Σ(dL/dW · V_updated),使大小独立优化
# 实际效果:DoRA 在低 rank(r=8/16)时优势最明显
# 因为低 rank 下 LoRA 的表达能力受限,DoRA 的解耦提供了额外自由度
3. QLoRA:4bit 量化 + LoRA
3.1 核心原理
QLoRA = NF4 量化(压缩基座模型) + LoRA(训练适配器)+ 分页优化器(管理显存)
基座模型 (FP16, 14GB) → NF4 量化 (4bit, 3.5GB)
→ 冻结量化权重
→ 添加 LoRA 适配器 (FP16)
→ 用分页 AdamW 训练
→ 总显存: ~6GB (7B 模型)
3.2 代码实现
import torch
from transformers import BitsAndBytesConfig, AutoModelForCausalLM
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
# 4bit 量化配置
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
)
# 加载量化模型
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Meta-Llama-3-8B",
quantization_config=bnb_config,
device_map="auto",
)
# 准备 kbit 训练
model = prepare_model_for_kbit_training(model)
# LoRA 配置
config = LoraConfig(
r=64,
lora_alpha=128,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
)
model = get_peft_model(model, config)
3.3 QLoRA 的独特之处
| 特性 | LoRA | DoRA | QLoRA |
|---|---|---|---|
| 基座模型精度 | FP16/BF16 | FP16/BF16 | 4bit NF4 |
| 训练精度 | FP16/BF16 | FP16/BF16 | BF16 (LoRA部分) |
| 优化器 | AdamW | AdamW | 分页 AdamW 8bit |
| 显存 (7B) | ~20GB | ~22GB | ~8GB |
| 模型质量 | 无损失 | 无损失 | 极小损失(NF4) |
4. 三者深度对比
4.1 数学公式对比
LoRA: W' = W + (α/r) · BA
DoRA: W' = m · normalize(W/||W|| + (α/r) · BA)
QLoRA: W' = dequant(NF4(W)) + (α/r) · BA
4.2 效果对比(LLaMA-2-7B, Commonsense Reasoning)
| 方法 | r | 显存 | ARC-e | ARC-c | BoolQ | PIQA | HellaSwag | WinoGrande | 平均 |
|---|---|---|---|---|---|---|---|---|---|
| 全量微调 | - | 60GB | 81.4 | 68.3 | 89.2 | 85.1 | 92.8 | 88.5 | 84.2 |
| LoRA | 16 | 20GB | 80.1 | 66.7 | 88.5 | 84.3 | 91.5 | 87.2 | 83.1 |
| LoRA | 64 | 22GB | 80.8 | 67.5 | 89.0 | 84.8 | 92.1 | 87.9 | 83.7 |
| DoRA | 16 | 22GB | 81.2 | 68.0 | 89.1 | 85.0 | 92.4 | 88.1 | 83.9 |
| DoRA | 64 | 24GB | 81.5 | 68.4 | 89.3 | 85.2 | 92.7 | 88.4 | 84.2 |
| QLoRA | 16 | 8GB | 79.5 | 65.8 | 87.9 | 83.7 | 90.8 | 86.5 | 82.4 |
| QLoRA | 64 | 10GB | 80.2 | 66.9 | 88.4 | 84.4 | 91.6 | 87.3 | 83.1 |
4.3 效果对比(LLaMA-2-7B, MMLU 5-shot)
| 方法 | r | Humanities | STEM | Social Sciences | Other | 平均 |
|---|---|---|---|---|---|---|
| 全量微调 | - | 49.1 | 44.8 | 60.3 | 58.7 | 53.2 |
| LoRA | 16 | 47.3 | 42.1 | 57.8 | 56.2 | 50.9 |
| LoRA | 64 | 48.2 | 43.5 | 59.1 | 57.4 | 52.1 |
| DoRA | 16 | 48.5 | 43.8 | 59.5 | 57.8 | 52.4 |
| DoRA | 64 | 49.0 | 44.5 | 60.1 | 58.5 | 53.0 |
| QLoRA | 64 | 47.1 | 41.8 | 57.2 | 55.8 | 50.3 |
关键发现:
- DoRA r=64 的效果几乎追平全量微调
- QLoRA 有约 2-3% 的量化损失
- 低 rank 时 DoRA 优势更明显(r=8 时差距最大)
4.4 训练速度对比
| 方法 | 硬件 | 训练速度 (samples/s) | 相对速度 |
|---|---|---|---|
| 全量微调 | A100 80GB | 12.4 | 1.0x |
| LoRA (r=16) | A100 40GB | 11.8 | 0.95x |
| LoRA (r=64) | A100 40GB | 10.9 | 0.88x |
| DoRA (r=16) | A100 40GB | 10.2 | 0.82x |
| DoRA (r=64) | A100 40GB | 9.1 | 0.73x |
| QLoRA (r=16) | RTX 4090 | 7.8 | 0.63x |
| QLoRA (r=64) | RTX 4090 | 6.9 | 0.56x |
DoRA 比 LoRA 慢约 15-20%(额外的归一化计算),QLoRA 因反量化操作最慢。
5. 组合使用
5.1 QDoRA:量化 + 解耦
QLoRA 和 DoRA 可以组合使用:
# QDoRA: 4bit 量化基座 + DoRA 适配器
config = LoraConfig(
r=64,
lora_alpha=128,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
use_dora=True, # DoRA
)
# 模型加载时使用 4bit 量化
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config, # NF4 量化
device_map="auto",
)
model = prepare_model_for_kbit_training(model)
model = get_peft_model(model, config) # 添加 DoRA 适配器
5.2 QDoRA 效果
| 方法 | 显存 (7B) | MMLU | 平均效果 | 说明 |
|---|---|---|---|---|
| LoRA | 20GB | 52.1 | 基线 | 无量化 |
| DoRA | 22GB | 53.0 | +0.9 | 解耦优势 |
| QLoRA | 8GB | 50.3 | -1.8 | 量化损失 |
| QDoRA | 8GB | 51.2 | -0.9 | 量化损失减半 |
QDoRA 在相同显存下比 QLoRA 效果更好,量化损失减半。
6. 选择指南
6.1 决策树
显存够不够加载 FP16 模型?
├── 不够(<20GB)→ QLoRA 或 QDoRA
│ ├── 追求效果 → QDoRA
│ └── 追求速度 → QLoRA
└── 够 → LoRA 或 DoRA
├── r <= 16 → DoRA(优势明显)
├── r >= 64 → LoRA(差距缩小)或 DoRA
└── 追求全量微调效果 → DoRA r=64
6.2 场景推荐
| 场景 | 推荐方法 | 原因 |
|---|---|---|
| 单卡 24GB 微调 7B | QLoRA r=64 | 显存受限 |
| 单卡 24GB 微调 13B | QDoRA r=64 | 显存受限 + DoRA 补偿量化损失 |
| 多卡 A100 微调 7B | DoRA r=64 | 效果接近全量微调 |
| 快速原型验证 | LoRA r=16 | 速度最快 |
| 生产部署 | LoRA r=16/32 | 推理时无额外开销 |
| 领域适应(大gap) | DoRA r=64 | 方向+大小调整更适合大跨度迁移 |
7. 推理优化
7.1 合并适配器
# LoRA/DoRA 可以合并到基座权重,推理时零额外开销
merged_model = model.merge_and_unload()
merged_model.save_pretrained("./merged_model")
# 注意:QLoRA 不能直接合并(精度不匹配)
# 需要先反量化再合并,或保持 adapter 形式推理
7.2 多 LoRA 切换
# 同一基座模型加载多个 LoRA adapter,按需切换
from peft import PeftModel
base_model = AutoModelForCausalLM.from_pretrained(...)
model = PeftModel.from_pretrained(base_model, "./lora_chat")
model.load_adapter("./lora_code", adapter_name="code")
model.load_adapter("./lora_math", adapter_name="math")
# 切换 adapter
model.set_adapter("code") # 切到代码 adapter
model.set_adapter("math") # 切到数学 adapter
model.set_adapter("default") # 切回默认
8. 常见问题
Q1: rank r 怎么选?
# 经验值:
# - 通用对话/指令跟随:r=16~64
# - 代码生成:r=32~128(代码需要更高表达力)
# - 领域适应(如医疗→通用):r=64~256
# - 风格迁移:r=8~16(变化幅度小)
#
# 建议:从 r=16 开始,效果不够翻倍增加
Q2: lora_alpha 怎么选?
# alpha 控制 LoRA 更新的缩放
# 常见设置:
# - alpha = r(保守,更新幅度小)
# - alpha = 2*r(推荐,大多数场景最优)
# - alpha = 4*r(激进,适合大 rank)
#
# 实际有效学习率 = lr * alpha / r
# 所以 alpha=2*r, lr=2e-4 等价于 alpha=r, lr=4e-4
Q3: DoRA 一定比 LoRA 好吗?
# 不一定。以下情况 DoRA 优势不明显:
# 1. r >= 128:高 rank 下 LoRA 表达力已足够
# 2. 微调数据与预训练数据分布接近
# 3. 简单任务(如文本分类)
#
# DoRA 优势最大的场景:
# 1. r <= 16 的低 rank 场景
# 2. 领域跨度大的迁移学习
# 3. 需要 close-the-gap 到全量微调
总结
| 方法 | 核心创新 | 显存 | 效果 | 速度 | 最适合 |
|---|---|---|---|---|---|
| LoRA | 低秩分解 | 中 | 良好 | 最快 | 通用场景、快速迭代 |
| DoRA | 解耦方向+大小 | 中 | 最优 | 中等 | 追求全量微调效果 |
| QLoRA | NF4 量化+LoRA | 最低 | 稍有损失 | 较慢 | 消费级 GPU |
| QDoRA | NF4+DoRA | 最低 | 优于QLoRA | 较慢 | 消费级 GPU + 高质量 |
一句话建议:
- 有 A100 → DoRA
- 只有 3090/4090 → QDoRA
- 快速验证 → LoRA r=16
- 追求极致效果 → DoRA r=64
相关阅读:QLoRA 微调实战、LLM 知识蒸馏、持续预训练实践
加入讨论
这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。
- 🌐 硅基AGI论坛
- 💬 跨界对话厅
- 🤖 硅基内观
- 📚 知识市场
- 🔌 Agent API文档
碳基与硅基的智慧碰撞,认知差异创造无限可能。
