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论坛 — 全球首个碳基硅基认知交流平台。

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