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_modulesr=8r=16r=64
q_proj only2.1M (0.03%)4.2M (0.06%)16.8M (0.24%)
q,k,v,o8.4M (0.12%)16.8M (0.24%)67.1M (0.96%)
q,k,v,o,mlp21.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 的关键区别

维度LoRADoRA
更新方式加法: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 的独特之处

特性LoRADoRAQLoRA
基座模型精度FP16/BF16FP16/BF164bit NF4
训练精度FP16/BF16FP16/BF16BF16 (LoRA部分)
优化器AdamWAdamW分页 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-eARC-cBoolQPIQAHellaSwagWinoGrande平均
全量微调-60GB81.468.389.285.192.888.584.2
LoRA1620GB80.166.788.584.391.587.283.1
LoRA6422GB80.867.589.084.892.187.983.7
DoRA1622GB81.268.089.185.092.488.183.9
DoRA6424GB81.568.489.385.292.788.484.2
QLoRA168GB79.565.887.983.790.886.582.4
QLoRA6410GB80.266.988.484.491.687.383.1

4.3 效果对比(LLaMA-2-7B, MMLU 5-shot)

方法rHumanitiesSTEMSocial SciencesOther平均
全量微调-49.144.860.358.753.2
LoRA1647.342.157.856.250.9
LoRA6448.243.559.157.452.1
DoRA1648.543.859.557.852.4
DoRA6449.044.560.158.553.0
QLoRA6447.141.857.255.850.3

关键发现

  • DoRA r=64 的效果几乎追平全量微调
  • QLoRA 有约 2-3% 的量化损失
  • 低 rank 时 DoRA 优势更明显(r=8 时差距最大)

4.4 训练速度对比

方法硬件训练速度 (samples/s)相对速度
全量微调A100 80GB12.41.0x
LoRA (r=16)A100 40GB11.80.95x
LoRA (r=64)A100 40GB10.90.88x
DoRA (r=16)A100 40GB10.20.82x
DoRA (r=64)A100 40GB9.10.73x
QLoRA (r=16)RTX 40907.80.63x
QLoRA (r=64)RTX 40906.90.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平均效果说明
LoRA20GB52.1基线无量化
DoRA22GB53.0+0.9解耦优势
QLoRA8GB50.3-1.8量化损失
QDoRA8GB51.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 微调 7BQLoRA r=64显存受限
单卡 24GB 微调 13BQDoRA r=64显存受限 + DoRA 补偿量化损失
多卡 A100 微调 7BDoRA 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解耦方向+大小最优中等追求全量微调效果
QLoRANF4 量化+LoRA最低稍有损失较慢消费级 GPU
QDoRANF4+DoRA最低优于QLoRA较慢消费级 GPU + 高质量

一句话建议

  • 有 A100 → DoRA
  • 只有 3090/4090 → QDoRA
  • 快速验证 → LoRA r=16
  • 追求极致效果 → DoRA r=64

相关阅读:QLoRA 微调实战、LLM 知识蒸馏、持续预训练实践

加入讨论

这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。

碳基与硅基的智慧碰撞,认知差异创造无限可能。