参数高效微调深度解析:从 LoRA 到 QLoRA 的全面指南

5209 字
26 分钟
参数高效微调深度解析:从 LoRA 到 QLoRA 的全面指南

1. 引言:为什么需要参数高效微调?#

1.1 全参数微调的困境#

想象一下你要微调一个拥有 700 亿参数的 LLaMA-2 模型:

全参数微调的资源消耗:
模型: LLaMA-2 70B
参数: 70,000,000,000 (700 亿)
存储:
- 模型权重: 140 GB (FP16)
- 优化器状态: 280 GB (Adam 状态)
- 梯度: 140 GB
- 总计: ~560 GB 显存
成本:
- A100 80GB × 8 = ~$40,000/月(云端)
- 或购买 8 张 A100 40GB = ~$160,000
现实:
→ 个人开发者几乎不可能
→ 中小企业也难以承受
→ 每训练一个任务都需要复制全量权重

1.2 PEFT 的核心思想#

参数高效微调(PEFT) 的核心洞察是:微调时不需要更新所有参数,只需要更新一小部分参数。

class PEFTCoreIdea:
"""
PEFT 的核心思想
全参数微调:
θ_new = θ_old - lr * ∇L(θ_old)
PEFT (LoRA):
W_new = W_old + ΔW
ΔW = B @ A (低秩矩阵)
关键:只训练 A 和 B,W_old 冻结
"""
def full_finetuning(self):
"""
全参数微调
所有参数都是可训练的:
- 70B 参数 → 70B 可训练
优点: 表达能力最强
缺点: 资源消耗巨大
"""
return {
"trainable_params": "100%",
"显存需求": "~560GB (70B 模型)",
"适用场景": "有充足资源 + 最高性能要求",
}
def lora(self):
"""
LoRA
只训练低秩适配矩阵:
- W ∈ R^(d×k)
- A ∈ R^(r×k), B ∈ R^(d×r)
- 只训练 r×(d+k) << d×k 参数
70B 模型示例:
- r=8, d=4096, k=4096
- 可训练参数: 8 × (4096+4096) = ~65K
- 仅为原来的 0.0001%
"""
return {
"trainable_params": "~0.01% - 0.1%",
"显存需求": "~few GB",
"适用场景": "资源受限 + 快速实验",
}

1.3 PEFT 方法全景#

┌─────────────────────────────────────────────────────────────┐
│ PEFT 方法全景 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 适配器方法 (Adapter-based) │
│ ├─ Adapter (Houlsby et al., 2019) │
│ ├─ Compacter (Mahabadi et al., 2021) │
│ └─ AdapterDrop (Rücklé et al., 2020) │
│ │
│ 2. 增量表示方法 (Additive) │
│ ├─ Prefix Tuning (Li & Liang, 2021) │
│ ├─ Prompt Tuning (Lester et al., 2021) │
│ └─ P-Tuning (Liu et al., 2021) │
│ │
│ 3. 低秩分解方法 (Low-rank) │
│ ├─ LoRA (Hu et al., 2021) ← 主流 │
│ ├─ QLoRA (Dettmers et al., 2023) │
│ ├─ DoRA (Liu et al., 2024) │
│ ├─ LoRA+ (Hayou et al., 2024) │
│ └─ PiSSA (Meng et al., 2024) │
│ │
│ 4. 混合方法 │
│ └─ MoL (Mix-of-LoRA) │
│ │
└─────────────────────────────────────────────────────────────┘

1.4 本系列文章关联#

文章关联
后训练深度解析PEFT 在后训练中的位置
数据构建深度解析微调数据的构建方法

2. LoRA:低秩自适应#

2.1 LoRA 的核心原理#

LoRA(Low-Rank Adaptation,Hu et al., 2021)的核心发现是:预训练语言模型的权重更新具有低内在秩

class LoRA原理:
"""
LoRA 原理
假设:对于预训练模型权重 W₀ ∈ R^(d×k),
微调后的权重更新 ΔW 也可以用低秩矩阵近似。
W_new = W₀ + ΔW
= W₀ + BA
其中:
- B ∈ R^(d×r)
- A ∈ R^(r×k)
- r << min(d, k)
关键洞察:
ΔW 是低秩的 → 用 BA 近似 ΔW
"""
def __init__(self, d, k, r):
self.d = d # 输出维度
self.k = k # 输入维度
self.r = r # 秩
# 冻结原始权重
self.W0 = None
# 可训练的低秩矩阵
self.A = nn.Parameter(torch.randn(r, k) * 0.01) # 用小随机值初始化
self.B = nn.Parameter(torch.zeros(d, r)) # 用零初始化
def forward(self, x):
"""
前向传播
h = W₀x + BAx
= W₀x + ΔWx
"""
return self.W0 @ x + (self.B @ self.A) @ x
def compute_trainable_params(self):
"""
计算可训练参数数量
"""
trainable = self.r * (self.d + self.k)
total = self.d * self.k
ratio = trainable / total * 100
print(f"Total: {total:,} params")
print(f"Trainable: {trainable:,} params")
print(f"Ratio: {ratio:.4f}%")
return trainable

2.2 LoRA 的数学推导#

LoRA 的数学推导:
Step 1: 问题形式化
给定预训练权重 W₀ ∈ R^(d×k),
我们想学习一个更新 ΔW,使得:
W_new = W₀ + ΔW
直接学习 ΔW 需要优化 d×k 个参数。
Step 2: 低秩假设
假设 ΔW 的秩 r << min(d, k),
则存在分解:
ΔW = BA, 其中 B ∈ R^(d×r), A ∈ R^(r×k)
可训练参数: r×(d+k) << d×k
Step 3: 为什么低秩有效?
预训练语言模型的知识是"过参数化"的。
微调时,重要的知识改变发生在低秩子空间中。
数学解释:
- W₀ 捕获通用语言知识
- ΔW 捕获特定任务知识
- 任务特定知识可以用低秩表示
Step 4: 初始化策略
A 的初始化: 随机小值(~N(0, 0.02))
B 的初始化: 零矩阵
这样初始化确保 ΔW = BA = 0 初始状态。
训练从 W₀ 开始,逐渐学习 ΔW。

2.3 LoRA 的实现#

class LoRALinear(nn.Module):
"""
LoRA 线性层实现
"""
def __init__(self, in_features, out_features, rank=4, alpha=1.0, dropout=0.0):
super().__init__()
self.in_features = in_features
self.out_features = out_features
self.rank = rank
self.alpha = alpha
self.scaling = alpha / rank # 缩放因子
# 原始权重(冻结)
self.weight = nn.Parameter(
torch.randn(out_features, in_features),
requires_grad=False
)
self.bias = nn.Parameter(
torch.zeros(out_features),
requires_grad=False
)
# LoRA 参数
self.lora_A = nn.Parameter(torch.randn(rank, in_features) * 0.01)
self.lora_B = nn.Parameter(torch.zeros(out_features, rank))
# Dropout(可选)
self.dropout = nn.Dropout(p=dropout)
# 标记哪些参数是 LoRA 的
self.lora_dropout = nn.Identity()
def forward(self, x):
"""
前向传播
"""
# 原始输出
output = F.linear(x, self.weight, self.bias)
# LoRA 输出
# h = Wx + (α/r) * BAx
lora_output = (self.dropout(x) @ self.lora_A.T @ self.lora_B.T) * self.scaling
return output + lora_output
def merge_weights(self):
"""
合并权重
将 LoRA 权重合并到原始权重中
用于推理阶段,消除额外计算
"""
# W_new = W + (α/r) * BA
merged_weight = self.weight + (self.alpha / self.rank) * (self.lora_B @ self.lora_A)
return merged_weight

2.4 哪些层应该应用 LoRA?#

class LoRALayerSelection:
"""
LoRA 层选择策略
"""
# 不同配置的实验结果(来自论文)
EXPERIMENTS = {
"llama_7b": {
# 只有 Query 注意力
"q": {"trainable": 0.09, "average": 91.2},
# Query + Value
"qkv": {"trainable": 0.27, "average": 93.1},
# 所有注意力层
"qkv_proj": {"trainable": 0.39, "average": 93.5},
# 包含 MLP
"all": {"trainable": 1.07, "average": 93.9},
# 包含所有线性层
"all_linear": {"trainable": "3.2%", "average": 94.1},
},
"llama_65b": {
"qkv": {"trainable": 0.06, "average": 92.7},
"all_linear": {"trainable": "0.8%", "average": 94.1},
},
}
def recommended_config(self, model_size="7b"):
"""
推荐配置
"""
configs = {
"7b": {
"target_modules": ["q_proj", "v_proj", "k_proj", "o_proj"],
"rank": 8,
"alpha": 16,
"dropout": 0.05,
},
"13b": {
"target_modules": ["q_proj", "v_proj", "k_proj", "o_proj"],
"rank": 8,
"alpha": 16,
"dropout": 0.05,
},
"70b": {
"target_modules": ["q_proj", "v_proj"],
"rank": 8,
"alpha": 16,
"dropout": 0.05,
},
}
return configs.get(model_size, configs["7b"])

层选择策略对比

配置可训练参数性能建议
仅 Q~0.03%中等极端资源受限
Q, V~0.09%良好常用推荐
Q, K, V, O~0.39%更好推荐
Q, K, V, O + MLP~1.07%最好有条件时使用

3. QLoRA:量化感知的 LoRA#

3.1 QLoRA 的核心思想#

QLoRA(Quantized LoRA,Dettmers et al., 2023)将量化与 LoRA 结合,在 4-bit 量化基础上微调。

class QLoRA:
"""
QLoRA: Quantized Low-Rank Adaptation
核心创新:
1. 4-bit NormalFloat (NF4) 量化
2. 双重量化(对量化常数也量化)
3. 分页优化器(处理内存峰值)
"""
def __init__(self, model, rank=64, alpha=16):
self.model = model
self.rank = rank
self.alpha = alpha
# Step 1: 4-bit 量化基础模型
self.quantized_base = self.quantize_model(model)
# Step 2: 添加 LoRA 适配器(16-bit,精度)
self.lora_adapters = self.add_lora_adapters()
def quantize_model(self, model):
"""
量化模型
方法:NF4 (4-bit NormalFloat)
"""
from bitsandbytes import BitsAndBytesConfig
quantization_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True, # 双重量化
bnb_4bit_quant_type="nf4", # NormalFloat4
)
quantized_model = model.from_pretrained(
model_path,
quantization_config=quantization_config,
)
return quantized_model
def add_lora_adapters(self):
"""
添加 LoRA 适配器
LoRA 参数保持 16-bit,不量化
"""
from peft import get_peft_model, LoraConfig
lora_config = LoraConfig(
r=self.rank,
lora_alpha=self.alpha,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
)
model = get_peft_model(self.quantized_base, lora_config)
model.print_trainable_parameters()
return model

3.2 NF4 量化原理#

class NF4Quantization:
"""
NF4 (4-bit NormalFloat) 量化
关键思想:
1. 对权重按 abs 值分组
2. 用归一化的分位数量化
3. 保持重要权重(大幅值)的精度
"""
def __init__(self, n_bits=4):
self.n_bits = n_bits
self.num_values = 2 ** n_bits # 16 个离散值
def quantize(self, weights):
"""
NF4 量化
"""
# Step 1: 计算绝对值
abs_weights = torch.abs(weights)
# Step 2: 确定量化常数(分位数)
# NF4 使用特殊的分位数量化表
quantiles = self.compute_quantiles(abs_weights, self.num_values)
# Step 3: 量化
quantized = self.assign_quantized_values(weights, quantiles)
return quantized, quantiles
def compute_quantiles(self, values, num_bins):
"""
计算分位数
对于 k-bit 量化,我们需要 2^k 个量化值。
NF4 使用归一化的分位数,确保每个 bin 有相同数量的权重。
"""
# 展平
flat = values.flatten().float()
# 计算分位数
# 使用特殊的 NF4 分位数量化表
# 这里简化实现
probs = torch.linspace(0, 1, num_bins + 1)[1:-1]
quantiles = torch.quantile(flat, probs)
return quantiles
def dequantize(self, quantized, quantiles):
"""
反量化
"""
# 找到最近的量化值
indices = self.find_nearest_indices(quantized, quantiles)
# 重建
reconstructed = quantiles[indices]
return reconstructed

3.3 QLoRA vs LoRA 显存对比#

QLoRA 显存节省:
模型: LLaMA-2 65B
全参数微调:
- 权重: 130 GB (FP16)
- 梯度: 130 GB
- 优化器: 260 GB
- 总计: 520 GB
LoRA (FP16):
- 权重: 130 GB (冻结)
- LoRA 参数: ~150 MB
- 梯度: ~500 MB
- 优化器: ~1 GB
- 总计: ~132 GB
QLoRA (4-bit):
- 量化权重: 32 GB (NF4)
- LoRA 参数: ~150 MB (BF16)
- 梯度: ~200 MB
- 优化器: ~400 MB
- 总计: ~33 GB
节省: 520GB → 33GB (94% 减少!)
可用硬件:
- 全参数: 需要 A100 80GB × 8
- LoRA: 需要 A100 40GB × 1
- QLoRA: 需要 A100 40GB × 1 (甚至 3090!)

4. LoRA 的变体#

4.1 DoRA:权重分解自适应#

class DoRA:
"""
DoRA: Weight-Decomposed Low-Rank Adaptation (Liu et al., 2024)
核心思想:
将权重更新分解为幅度和方向两部分。
原始 LoRA:
W_new = W₀ + BA
DoRA:
W_new = W₀ + m·(BA / ||BA||_F)
其中 m 是可学习的幅度,BA/||BA||_F 是方向。
"""
def __init__(self, in_features, out_features, rank=4):
self.in_features = in_features
self.out_features = out_features
self.rank = rank
# 原始权重(冻结)
self.W0 = None
# 方向(LoRA)
self.A = nn.Parameter(torch.randn(rank, in_features) * 0.01)
self.B = nn.Parameter(torch.zeros(out_features, rank))
# 幅度
self.m = nn.Parameter(torch.ones(out_features, in_features))
def forward(self, x):
"""
DoRA 前向传播
"""
# 原始输出
base_output = F.linear(x, self.W0)
# LoRA 方向
delta_W = self.B @ self.A
# 幅度归一化
normalized_delta = delta_W / (torch.norm(delta_W, p='fro') + 1e-8)
# 加幅度
weighted_delta = self.m * normalized_delta
return base_output + weighted_delta * x

DoRA vs LoRA 对比

方面LoRADoRA
更新方式联合更新 BA分解为幅度+方向
可解释性
性能基准更高
收敛速度中等更快

4.2 PiSSA:主成分初始化#

class PiSSA:
"""
PiSSA: Principal Singular Values Adaptation (Meng et al., 2024)
核心思想:
用 SVD 对权重矩阵进行分解,
只对最重要的奇异分量应用 LoRA。
优势:
1. 更好的初始化
2. 更快的收敛
3. 更好的性能
"""
def __init__(self, weight, rank=4):
self.weight = weight # 原始权重
self.rank = rank
# Step 1: SVD 分解
U, S, Vh = torch.linalg.svd(weight.float(), full_matrices=False)
# Step 2: 选择主成分
# 只对前 r 个奇异分量应用 LoRA
self.U_main = U[:, :rank] # 保留主要方向
self.S_main = S[:rank]
self.Vh_main = Vh[:rank, :]
# Step 3: 初始化 LoRA
# 用主成分的权重初始化
self.A = nn.Parameter(self.Vh_main)
self.B = nn.Parameter(self.U_main @ torch.diag(self.S_main))
# 剩余奇异值作为偏置
self.remaining = U[:, rank:] @ torch.diag(S[rank:]) @ Vh[rank:, :]
def forward(self, x):
"""
PiSSA 前向传播
"""
# 主成分部分用 LoRA
lora_part = (self.B @ self.A) @ x
# 剩余部分(冻结)
remaining_part = self.remaining @ x
# 偏置部分
bias_part = self.weight.bias @ x if hasattr(self.weight, 'bias') else 0
return lora_part + remaining_part + bias_part

4.3 LoRA+#

class LoRAPlus:
"""
LoRA+: Different Learning Rates for A and B (Hayou et al., 2024)
核心思想:
A 和 B 应该有不同的学习率。
原始 LoRA:
lr_A = lr_B = lr
LoRA+:
lr_A = lr / λ_A
lr_B = lr * λ_B
推荐: λ_A = r, λ_B = 1
"""
def configure_optimizers(self, model, base_lr=1e-4, r=8):
"""
配置优化器
"""
# A 的学习率(通常较小)
lr_A = base_lr / r
# B 的学习率(标准)
lr_B = base_lr
# 其他参数(LoRA 以外)
lr_other = base_lr * 0.1
optimizer_grouped_parameters = [
# LoRA A
{"params": model.lora_A, "lr": lr_A},
# LoRA B
{"params": model.lora_B, "lr": lr_B},
# 其他(冻结)
{"params": model.other_params, "lr": 0},
]
return torch.optim.AdamW(optimizer_grouped_parameters)

4.4 LoRA 变体对比#

方法核心创新性能提升计算开销
LoRA低秩分解基准
DoRA幅度+方向分解+2-5%极低
PiSSASVD 初始化+3-8%
LoRA+自适应学习率+2-5%极低
QLoRA4-bit 量化接近 FP16中等

5. 其他 PEFT 方法#

5.1 Adapter#

class Adapter:
"""
Adapter (Houlsby et al., 2019)
结构:逐层添加瓶颈网络
h = W₀x + adapter(W₀x)
adapter: d → r → d (bottleneck: r << d)
"""
def __init__(self, d_model, r=64, alpha=16):
self.d_model = d_model
self.r = r # bottleneck 维度
# 下投影
self.down = nn.Linear(d_model, r)
# 上投影
self.up = nn.Linear(r, d_model)
# 非线性
self.act = nn.GELU()
# LayerNorm
self.layer_norm = nn.LayerNorm(d_model)
# 残差缩放
self.alpha = alpha
def forward(self, x):
"""
Adapter 前向传播
"""
# 原始残差
residual = x
# Adapter
adapter_output = self.up(self.act(self.down(x)))
# 残差连接(带缩放)
output = self.layer_norm(residual + (self.alpha / self.r) * adapter_output)
return output

5.2 Prefix Tuning#

class PrefixTuning:
"""
Prefix Tuning (Li & Liang, 2021)
核心思想:
在每层注意力之前添加可学习的前缀。
不修改模型权重,只添加前缀参数。
"""
def __init__(self, num_layers, num_heads, head_dim, prefix_len=10):
self.num_layers = num_layers
self.num_heads = num_heads
self.head_dim = head_dim
self.prefix_len = prefix_len
# 可学习的前缀
# 每层有独立的 prefix
self.prefixes = nn.ParameterList([
nn.Parameter(torch.randn(prefix_len, num_heads * head_dim) * 0.01)
for _ in range(num_layers)
])
def forward(self, layer_idx, q, k, v):
"""
应用 Prefix Tuning
在 Key 和 Value 前面拼接可学习的 prefix
"""
# 获取该层的前缀
prefix = self.prefixes[layer_idx]
# 分割 head
prefix_q = prefix[:, :self.num_heads * self.head_dim]
prefix_k = prefix[:, :self.num_heads * self.head_dim]
prefix_v = prefix[:, :self.num_heads * self.head_dim]
# Reshape
batch_size = q.shape[0]
prefix_len = prefix.shape[0]
prefix_q = prefix_q.unsqueeze(0).expand(batch_size, -1, -1)
prefix_k = prefix_k.unsqueeze(0).expand(batch_size, -1, -1)
prefix_v = prefix_v.unsqueeze(0).expand(batch_size, -1, -1)
# 拼接
k_full = torch.cat([prefix_k, k], dim=1)
v_full = torch.cat([prefix_v, v], dim=1)
return k_full, v_full

5.3 Prompt Tuning#

class PromptTuning:
"""
Prompt Tuning (Lester et al., 2021)
核心思想:
只添加可学习的 soft prompt,完全不修改模型。
参数量:prefix_len × embedding_dim
"""
def __init__(self, vocab_size, embed_dim, num_tokens=20):
self.vocab_size = vocab_size
self.embed_dim = embed_dim
self.num_tokens = num_tokens
# 可学习的 soft prompt
self.prompt = nn.Parameter(
torch.randn(1, num_tokens, embed_dim) * 0.01
)
def forward(self, input_ids, embedding_layer):
"""
获取带 prompt 的 embedding
"""
# 输入 embedding
inputs_embeds = embedding_layer(input_ids)
# 拼接 prompt
batch_size = inputs_embeds.shape[0]
prompt = self.prompt.expand(batch_size, -1, -1)
full_embeds = torch.cat([prompt, inputs_embeds], dim=1)
# 记录 prompt 长度(用于 mask)
prompt_length = self.num_tokens
return full_embeds, prompt_length

5.4 方法对比#

方法引入参数位置推理开销性能
LoRA~0.1-1%权重矩阵旁低(可合并)
Adapter~0.5-5%残差连接中
Prefix Tuning~0.1%输入前
Prompt Tuning~0.01%嵌入层前较低
QLoRA~0.1%权重矩阵旁接近全参数

6. 秩的选择与调优#

6.1 秩的理论分析#

class RankSelection:
"""
LoRA 秩的选择
"""
# 不同秩的实验结果(来自论文)
EXPERIMENTS = {
"WikiSQL": {
"r=1": 74.2,
"r=2": 75.6,
"r=4": 77.2,
"r=8": 78.5,
"r=16": 79.3,
"r=32": 79.8,
"r=64": 80.1,
"r=128": 80.2,
},
"MNLI": {
"r=1": 82.6,
"r=4": 84.1,
"r=8": 84.8,
"r=16": 85.3,
"r=64": 85.9,
},
}
def analyze_scaling(self):
"""
性能-秩关系分析
观察:
1. 性能随秩增加而提升(边际递减)
2. r=8 是一个常用的"甜点"
3. r=64 之后提升很小
"""
return {
"conclusion": "边际收益递减",
"recommended_range": "r ∈ [4, 64]",
"sweet_spot": "r=8 for most tasks",
"rule_of_thumb": "r = α/2, where α is typical attention dimension ratio",
}
def adaptive_rank(self, model, task_complexity):
"""
自适应秩选择
"""
rank_map = {
"simple_classification": 4,
"ner": 8,
"question_answering": 16,
"summarization": 32,
"code_generation": 64,
"complex_reasoning": 128,
}
return rank_map.get(task_complexity, 8)

6.2 秩与任务复杂度的关系#

秩选择指南:
简单任务(分类、NER):
建议 r = 4-8
理由:任务简单,低秩足以捕捉更新
中等任务(QA、翻译):
建议 r = 8-16
理由:需要更多能力
复杂任务(代码生成、复杂推理):
建议 r = 32-64
理由:任务复杂,需要更高表达能力
极端情况(多任务、微调整个模型):
建议 r = 64-128
理由:最大程度保持性能

6.3 缩放因子 α#

class AlphaScaling:
"""
缩放因子 α 的作用
实际缩放 = α / r
"""
def __init__(self, rank, alpha=16):
self.scaling = alpha / rank
def experiment_results(self):
"""
实验结果(来自论文)
"""
return {
"alpha=r": "平衡设置",
"alpha=2r": "更保守的更新",
"alpha=r/2": "更激进的更新(不推荐)",
"recommendation": "α = 2r 是一个好的起点",
}

7. 实战指南#

7.1 使用 Hugging Face PEFT#

from peft import (
get_peft_model,
LoraConfig,
TaskType,
PeftType,
prepare_model_for_kbit_training,
)
def create_lora_model(model, config=None):
"""
创建 LoRA 模型
"""
# 默认配置
if config is None:
config = {
"r": 8,
"lora_alpha": 16,
"target_modules": ["q_proj", "v_proj"],
"lora_dropout": 0.05,
"bias": "none",
"task_type": TaskType.CAUSAL_LM,
}
# 创建配置
lora_config = LoraConfig(**config)
# 应用 LoRA
model = get_peft_model(model, lora_config)
# 打印可训练参数
model.print_trainable_parameters()
return model
def train_with_peft(model, train_dataset, eval_dataset):
"""
PEFT 训练
"""
from transformers import Trainer, TrainingArguments
training_args = TrainingArguments(
output_dir="./output",
num_train_epochs=3,
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=1e-4,
warmup_ratio=0.03,
lr_scheduler_type="cosine",
logging_steps=10,
save_steps=500,
eval_steps=500,
evaluation_strategy="steps",
fp16=True,
optim="paged_adamw_32bit", # 页面优化器
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
data_collator=data_collator,
)
trainer.train()
return model
def merge_and_save(model, output_path):
"""
合并权重并保存
"""
# 合并 LoRA 权重到基础模型
merged_model = model.merge_and_unload()
# 保存
merged_model.save_pretrained(output_path)

7.2 QLoRA 完整示例#

from transformers import AutoModelForCausalLM, BitsAndBytesConfig
from peft import prepare_model_for_kbit_training, get_peft_model, LoraConfig
def load_qloRA_model(model_name):
"""
加载 QLoRA 模型
"""
# Step 1: 量化配置
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
)
# Step 2: 加载量化模型
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config,
device_map="auto",
trust_remote_code=True,
)
# Step 3: 准备训练
model = prepare_model_for_kbit_training(model)
# Step 4: 应用 LoRA
lora_config = LoraConfig(
r=64,
lora_alpha=16,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
lora_dropout=0.05,
bias="none",
task_type=TaskType.CAUSAL_LM,
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# 输出: "trainable params: 83,886,080 || all params: 33,杂,杂,杂 || trainable%: 0.2414%"
return model
# 使用示例
model = load_qloRA_model("meta-llama/Llama-2-70b-hf")
trainer = Trainer(model=model, ...)
trainer.train()

7.3 多 LoRA 组合#

class MultiLoRA:
"""
多 LoRA 组合
"""
def __init__(self, base_model):
self.base_model = base_model
self.lora_weights = {}
def add_task_lora(self, task_name, lora_path):
"""
添加任务的 LoRA
"""
from peft import PeftModel
# 加载 LoRA
lora_model = PeftModel.from_pretrained(
self.base_model,
lora_path,
adapter_name=task_name,
)
self.lora_weights[task_name] = lora_model
def switch_lora(self, task_name):
"""
切换到指定任务的 LoRA
"""
for name, model in self.lora_weights.items():
if name == task_name:
model.set_adapter(task_name)
else:
model.disable_adapter()
def weighted_merge(self, weights):
"""
加权合并多个 LoRA
用于多任务学习
"""
# 获取所有 LoRA 权重
lora_weights = []
for name in weights.keys():
lora_weights.append(self.lora_weights[name])
# 加权平均
# 实际实现需要更复杂的逻辑
pass

8. 高级主题#

8.1 权重合并与卸载#

class WeightMerging:
"""
权重合并与卸载
"""
def merge_weights(self, model):
"""
合并 LoRA 权重
合并后:
- 推理速度更快(无需额外的矩阵乘法)
- 模型大小不变
"""
# 合并所有 LoRA 适配器
merged_model = model.merge_and_unload()
return merged_model
def merge_and_quantize(self, model):
"""
合并后量化
将合并的模型量化以节省空间
"""
# 合并
merged = model.merge_and_unload()
# 量化
quantized = self.quantize(merged, bits=4)
return quantized
def unload_and_reload(self, model):
"""
卸载 LoRA,保留原始模型
"""
# 卸载所有 LoRA 适配器
model.unload()
return model

8.2 持续预训练与微调的结合#

class ContinualPretraining:
"""
持续预训练 + 微调
"""
def combined_training(self, base_model, pretrain_data, finetune_data):
"""
组合训练策略
阶段 1: 持续预训练(领域适应)
阶段 2: 指令微调(任务适应)
"""
# 阶段 1: 预训练
pretrain_model = self.pretrain(base_model, pretrain_data)
# 阶段 2: 微调
# 复用同一个 LoRA 或创建新的
finetune_model = self.finetune(pretrain_model, finetune_data)
return finetune_model
def pretrain(self, model, data):
"""
持续预训练
"""
# LoRA 配置(可以是不同的模块)
config = LoraConfig(
r=16,
target_modules=["q_proj", "v_proj", "k_proj", "mlp"],
)
model = get_peft_model(model, config)
# 训练
trainer = Trainer(model=model, data=data, ...)
trainer.train()
return model
def finetune(self, model, data):
"""
指令微调
"""
# 冻结预训练 LoRA
for param in model.parameters():
if "lora_" not in param.name:
param.requires_grad = False
# 添加新的 LoRA 或继续训练现有 LoRA
# ...

8.3 分布式训练#

class DistributedLoRA:
"""
分布式 LoRA 训练
"""
def train_distributed(self, model, data):
"""
分布式训练
"""
# FSDP (Fully Sharded Data Parallel)
from torch.distributed.fsdp import (
FullyShardedDataParallel as FSDP,
MixedPrecision,
ShardingStrategy,
)
# 包装模型
model = FSDP(
model,
sharding_strategy=ShardingStrategy.FULL_SHARD,
mixed_precision=MixedPrecision(
param_dtype=torch.bfloat16,
reduce_dtype=torch.float32,
),
)
# 训练
trainer = Trainer(model=model, ...)
trainer.train()
def ZeRO_LoRA(self, model, data):
"""
ZeRO 优化
"""
# DeepSpeed ZeRO
from transformers import DeepSpeedConfig
ds_config = {
"zero_optimization": {
"stage": 3,
"offload_optimizer": {
"device": "cpu",
},
},
"bf16": {"enabled": True},
}
# 训练
trainer = Trainer(
model=model,
args=training_args,
ds_config=ds_config,
...
)

9. 核心公式汇总#

9.1 LoRA 权重更新#

Wnew=W0+αrBA\mathbf{W}_{\text{new}} = \mathbf{W}_0 + \frac{\alpha}{r} \mathbf{B}\mathbf{A}

其中 ARr×k\mathbf{A} \in \mathbb{R}^{r \times k}BRd×r\mathbf{B} \in \mathbb{R}^{d \times r}

9.2 可训练参数比例#

trainabletotal=r(d+k)dkrmin(d,k)\frac{\text{trainable}}{\text{total}} = \frac{r(d + k)}{dk} \approx \frac{r}{\min(d, k)}

9.3 LoRA 梯度#

LA=αrBLWnew\frac{\partial \mathcal{L}}{\partial \mathbf{A}} = \frac{\alpha}{r} \mathbf{B}^\top \frac{\partial \mathcal{L}}{\partial \mathbf{W}_{\text{new}}}LB=αrLWnewA\frac{\partial \mathcal{L}}{\partial \mathbf{B}} = \frac{\alpha}{r} \frac{\partial \mathcal{L}}{\partial \mathbf{W}_{\text{new}}} \mathbf{A}^\top

9.4 NF4 量化#

WQ=quantize(W,Q)=Wcs\mathbf{W}_Q = \text{quantize}(\mathbf{W}, Q) = \left\lfloor \frac{\mathbf{W} - c}{s} \right\rceil

其中 cc 是偏移,ss 是缩放因子。


10. 总结#

10.1 方法选择指南#

┌─────────────────────────────────────────────────────────────┐
│ PEFT 方法选择指南 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 资源情况 │
│ │ │
│ ├── 消费级 GPU (RTX 3090/4090, 24GB) │
│ │ └── QLoRA (4-bit) + r=32-64 │
│ │ │
│ ├── 单卡 A100 (40-80GB) │
│ │ ├── 小模型 (<13B): LoRA + r=8-16 │
│ │ └── 大模型 (>=13B): QLoRA + r=64 │
│ │ │
│ └── 多卡 A100 │
│ └── 全参数微调 或 LoRA + r=64 │
│ │
│ 任务复杂度 │
│ │ │
│ ├── 简单分类 │
│ │ └── LoRA r=4-8 │
│ │ │
│ ├── NER/QA │
│ │ └── LoRA r=8-16 │
│ │ │
│ ├── 代码生成 │
│ │ └── LoRA r=32-64 │
│ │ │
│ └── 多任务/复杂推理 │
│ └── LoRA r=64-128 或 全参数微调 │
│ │
└─────────────────────────────────────────────────────────────┘

10.2 最佳实践#

┌─────────────────────────────────────────────────────────────┐
│ LoRA 最佳实践 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 模块选择 │
│ → 默认: q_proj, v_proj │
│ → 更好: + k_proj, o_proj │
│ → 最好: + mlp.gate_proj, mlp.up_proj, mlp.down_proj │
│ │
│ 2. 秩选择 │
│ → 常用: r=8 (简单任务), r=64 (复杂任务) │
│ → α 通常设为 2r │
│ │
│ 3. Dropout │
│ → 训练时: lora_dropout=0.05 │
│ → 推理时: 不使用 dropout │
│ │
│ 4. 优化器 │
│ → AdamW (paged 版本防止峰值) │
│ → 学习率: 1e-4 到 3e-4 │
│ → Warmup: 3% 到 6% │
│ │
│ 5. 权重合并 │
│ → 推理前合并权重以提高速度 │
│ → 如果需要切换 LoRA,保持分开 │
│ │
│ 6. 持久化 │
│ → 保存: 只保存 LoRA 权重 (~10-100MB) │
│ → 加载: 需要基础模型 + LoRA 权重 │
│ │
└─────────────────────────────────────────────────────────────┘

10.3 性能对比#

┌─────────────────────────────────────────────────────────────┐
│ 65B 模型性能对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 方法 可训练参数 MMLU HumanEval │
│ ───────────────────────────────────────────────────────── │
│ 全参数微调 65B 68.0 62.0 │
│ LoRA (r=64, all) 666M 67.5 61.5 │
│ LoRA (r=8, qkv) 83M 66.8 60.2 │
│ QLoRA (r=64, NF4) 666M 67.6 61.8 │
│ Adapter (r=64) 666M 67.2 61.0 │
│ Prefix Tuning 19M 63.5 58.0 │
│ ───────────────────────────────────────────────────────── │
│ │
│ 结论: LoRA/QLoRA 接近全参数微调性能 │
│ │
└─────────────────────────────────────────────────────────────┘
推荐阅读
  1. LoRA(Hu et al., 2021)—— 低秩自适应原论文
  2. QLoRA(Dettmers et al., 2023)—— 量化感知 LoRA
  3. DoRA(Liu et al., 2024)—— 权重分解自适应
  4. Adapter(Houlsby et al., 2019)—— 适配器方法
  5. Prefix Tuning(Li & Liang, 2021)—— 前缀调优

参考资料#

  1. Hu, E. J., et al. (2021). “LoRA: Low-Rank Adaptation of Large Language Models.” ICLR.
  2. Dettmers, T., et al. (2023). “QLoRA: Efficient Finetuning of Quantized LLMs.” NeurIPS.
  3. Liu, S., et al. (2024). “DoRA: Weight-Decomposed Low-Rank Adaptation.” arXiv.
  4. Meng, F., et al. (2024). “PiSSA: Principal Singular Values Adaptation.” arXiv.
  5. Hayou, S., et al. (2024). “LoRA+: Efficient Low Rank Adaptation of LLMs.” arXiv.
  6. Houlsby, N., et al. (2019). “Parameter-Efficient Transfer Learning for NLP.” ICML.
  7. Li, X. L., & Liang, P. (2021). “Prefix-Tuning: Optimizing Continuous Prompts.” ACL.
  8. Lester, B., et al. (2021). “The Power of Scale for Parameter-Efficient Prompt Tuning.” EMNLP.
  9. Mahabadi, R. K., et al. (2021). “Compacter: Efficient Low-Rank Hypercomplex Adapter Layers.” NeurIPS.
  10. Rücklé, A., et al. (2020). “AdapterDrop: On the Efficiency of Adapters in Transformers.” EMNLP.
  11. Aghajanyan, A., et al. (2020). “Intrinsic Dimensionality Explains the Effectiveness of Language Model Fine-Tuning.” ACL.
  12. Lialin, V., et al. (2023). “Hierarchical LoRA for Multi-task Fine-tuning.” arXiv.

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

参数高效微调深度解析:从 LoRA 到 QLoRA 的全面指南
https://aiattnstudio.link/posts/peft-lora/
作者
Federico
发布于
2026-07-16
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author

Federico

AI Research Lab

Hello, I'm Federico.

关于实验室 / About
公告

欢迎来到Federico的个人博客

分类
标签
站点统计
57文章
7分类
404标签