前言:微调的"贫富差距"
你想微调一个 LLaMA-7B 模型,让它学会你的业务知识。
全量微调需要什么?
# 7B 模型参数量
params = 7 * 1e9
# 训练时的显存占用(Adam 优化器)
# = 模型参数 + 梯度 + 优化器状态(2个动量)
memory_bytes = params * (2 + 2 + 4 + 4) # fp16 模型 + fp16 梯度 + fp32 优化器
memory_gb = memory_bytes / 1e9
print(f"全量微调显存需求: {memory_gb:.1f} GB")
# 输出: 全量微调显存需求: 84.0 GB84GB 显存! 一张 A100 80GB 都不够用!
这就是为什么需要 PEFT(Parameter-Efficient Fine-Tuning,参数高效微调)。
一、为什么全量微调这么贵?
1.1 显存占用分析
训练时,显存里需要存储:
| 组件 | 精度 | 大小(7B 模型) |
|---|---|---|
| 模型参数 | FP16 | 14 GB |
| 梯度 | FP16 | 14 GB |
| 优化器状态(Adam) | FP32 | 56 GB |
| 激活值(取决于 batch) | FP16 | 可变 |
| 总计 | - | 84+ GB |
优化器状态是大头! Adam 需要为每个参数存储两个动量(m 和 v),而且必须是 FP32。
1.2 穷人的困境
| GPU | 显存 | 能全量微调? |
|---|---|---|
| RTX 3090 | 24 GB | ❌ 不行 |
| RTX 4090 | 24 GB | ❌ 不行 |
| A100 40GB | 40 GB | ❌ 不行 |
| A100 80GB | 80 GB | ⚠️ 勉强 |
| 8×A100 80GB | 640 GB | ✅ 可以 |
大部分人根本没有条件全量微调!
1.3 PEFT 的思路
核心洞察:微调时,模型的大部分参数其实不需要更新。
预训练模型已经学到了丰富的知识,微调只是"微调",不是从头学习。
PEFT 的策略:
- 冻结大部分参数
- 只训练少量新增参数
- 显存占用大幅下降
二、LoRA:低秩适配器
2.1 核心思想
LoRA(Low-Rank Adaptation) 的核心假设:
微调时的权重变化 ΔW 是低秩的。
什么意思?
一个 4096×4096 的矩阵有 1600 万个参数。但微调时,这个矩阵的"有效变化"可能只需要用一个 4096×16 和 16×4096 的矩阵来表示(只有 13 万参数)。
2.2 数学表达
原始的线性层:
$$ h = Wx $$
LoRA 修改后:
$$ h = Wx + \frac{\alpha}{r}BAx $$
其中:
- $W \in \mathbb{R}^{d \times k}$:原始权重(冻结)
- $B \in \mathbb{R}^{d \times r}$:LoRA 矩阵 B
- $A \in \mathbb{R}^{r \times k}$:LoRA 矩阵 A
- $r$:秩(rank),通常 4~64
- $\alpha$:缩放因子
参数量对比:
- 原始:$d \times k$
- LoRA:$d \times r + r \times k = r(d+k)$
当 $r << d, k$ 时,参数量大幅减少!
# 以 4096×4096 矩阵为例
d, k = 4096, 4096
r = 16 # LoRA 秩
original_params = d * k # 16,777,216
lora_params = r * (d + k) # 131,072
reduction = original_params / lora_params
print(f"原始参数: {original_params:,}")
print(f"LoRA 参数: {lora_params:,}")
print(f"参数减少: {reduction:.0f}x")
# 输出:
# 原始参数: 16,777,216
# LoRA 参数: 131,072
# 参数减少: 128x2.3 LoRA 的初始化
关键技巧:让 LoRA 在训练开始时对输出没有影响。
# A 用高斯初始化
A = torch.randn(r, k) * 0.01
# B 用零初始化
B = torch.zeros(d, r)
# 训练开始时: BA = 0,不影响原始输出这样,模型在训练开始时的行为和原始模型完全一致。
2.4 LoRA 代码实现
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
class LoRALinear(nn.Module):
"""带 LoRA 的线性层"""
def __init__(
self,
in_features: int,
out_features: int,
r: int = 8, # LoRA 秩
lora_alpha: int = 16, # 缩放因子
lora_dropout: float = 0.1,
):
super().__init__()
self.in_features = in_features
self.out_features = out_features
self.r = r
self.lora_alpha = lora_alpha
self.scaling = lora_alpha / r
# 原始权重(冻结)
self.weight = nn.Parameter(torch.empty(out_features, in_features))
self.bias = nn.Parameter(torch.zeros(out_features))
# LoRA 参数
self.lora_A = nn.Parameter(torch.empty(r, in_features))
self.lora_B = nn.Parameter(torch.zeros(out_features, r))
# Dropout
self.lora_dropout = nn.Dropout(lora_dropout) if lora_dropout > 0 else nn.Identity()
# 初始化
self.reset_parameters()
# 冻结原始权重
self.weight.requires_grad = False
self.bias.requires_grad = False
def reset_parameters(self):
# 原始权重初始化
nn.init.kaiming_uniform_(self.weight, a=math.sqrt(5))
# LoRA A: 高斯初始化
nn.init.kaiming_uniform_(self.lora_A, a=math.sqrt(5))
# LoRA B: 零初始化(确保初始时 LoRA 无影响)
nn.init.zeros_(self.lora_B)
def forward(self, x: torch.Tensor) -> torch.Tensor:
# 原始线性变换
result = F.linear(x, self.weight, self.bias)
# LoRA 增量
lora_output = self.lora_dropout(x) @ self.lora_A.T @ self.lora_B.T
result = result + lora_output * self.scaling
return result
def merge_weights(self):
"""将 LoRA 权重合并到原始权重(推理时用)"""
self.weight.data += (self.lora_B @ self.lora_A) * self.scaling
# 合并后可以删除 LoRA 参数
del self.lora_A
del self.lora_B
class LoRALayer(nn.Module):
"""将 LoRA 应用到现有的 nn.Linear"""
def __init__(
self,
original_layer: nn.Linear,
r: int = 8,
lora_alpha: int = 16,
lora_dropout: float = 0.1,
):
super().__init__()
self.original_layer = original_layer
self.r = r
self.lora_alpha = lora_alpha
self.scaling = lora_alpha / r
in_features = original_layer.in_features
out_features = original_layer.out_features
# 冻结原始层
for param in original_layer.parameters():
param.requires_grad = False
# LoRA 参数
self.lora_A = nn.Parameter(torch.zeros(r, in_features))
self.lora_B = nn.Parameter(torch.zeros(out_features, r))
# 初始化 A
nn.init.kaiming_uniform_(self.lora_A, a=math.sqrt(5))
# Dropout
self.lora_dropout = nn.Dropout(lora_dropout) if lora_dropout > 0 else nn.Identity()
def forward(self, x: torch.Tensor) -> torch.Tensor:
# 原始输出
result = self.original_layer(x)
# LoRA 增量
lora_output = self.lora_dropout(x) @ self.lora_A.T @ self.lora_B.T
result = result + lora_output * self.scaling
return result
def apply_lora_to_model(model, r=8, lora_alpha=16, target_modules=None):
"""给模型添加 LoRA"""
if target_modules is None:
# 默认只对 attention 的 q, v 投影加 LoRA
target_modules = ['q_proj', 'v_proj']
for name, module in model.named_modules():
if any(target in name for target in target_modules):
if isinstance(module, nn.Linear):
# 获取父模块
parent_name = '.'.join(name.split('.')[:-1])
child_name = name.split('.')[-1]
parent = model.get_submodule(parent_name) if parent_name else model
# 替换为 LoRA 层
lora_layer = LoRALayer(
original_layer=module,
r=r,
lora_alpha=lora_alpha,
)
setattr(parent, child_name, lora_layer)
return model
# 使用示例
def count_trainable_params(model):
"""统计可训练参数"""
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
total = sum(p.numel() for p in model.parameters())
return trainable, total
# 演示
if __name__ == "__main__":
# 模拟一个简单模型
class SimpleModel(nn.Module):
def __init__(self):
super().__init__()
self.q_proj = nn.Linear(4096, 4096)
self.k_proj = nn.Linear(4096, 4096)
self.v_proj = nn.Linear(4096, 4096)
self.o_proj = nn.Linear(4096, 4096)
def forward(self, x):
q = self.q_proj(x)
k = self.k_proj(x)
v = self.v_proj(x)
return self.o_proj(q + k + v)
model = SimpleModel()
# 应用 LoRA 前
trainable, total = count_trainable_params(model)
print(f"LoRA 前: {trainable:,} / {total:,} 可训练 ({100*trainable/total:.2f}%)")
# 应用 LoRA
model = apply_lora_to_model(model, r=16, target_modules=['q_proj', 'v_proj'])
# 应用 LoRA 后
trainable, total = count_trainable_params(model)
print(f"LoRA 后: {trainable:,} / {total:,} 可训练 ({100*trainable/total:.4f}%)")
# 输出:
# LoRA 前: 67,108,864 / 67,108,864 可训练 (100.00%)
# LoRA 后: 262,144 / 67,371,008 可训练 (0.3891%)2.5 LoRA 应用在哪些层?
通常对 Transformer 的 Attention 层应用 LoRA:
| 目标模块 | 效果 | 参数增量 |
|---|---|---|
| q_proj, v_proj | 基础效果 | 少 |
| q_proj, k_proj, v_proj, o_proj | 更好效果 | 中等 |
| + gate_proj, up_proj, down_proj | 最佳效果 | 较多 |
推荐配置:至少包含 q_proj 和 v_proj,资源充足时可以加更多。
三、QLoRA:4-bit 量化 + LoRA
3.1 为什么需要 QLoRA?
LoRA 只减少了可训练参数,但模型本身还是要完整加载到显存。
7B 模型 FP16 需要 14GB,加上激活值,还是需要 20GB+ 显存。
QLoRA 的思路:用 4-bit 量化存储基础模型!
FP16: 7B × 2 bytes = 14 GB
INT4: 7B × 0.5 bytes = 3.5 GB3.2 QLoRA 的三大创新
1. NF4(4-bit NormalFloat)
专门为神经网络权重设计的 4-bit 数据类型。
神经网络权重通常服从正态分布,NF4 的量化点针对这个分布优化,比普通 INT4 更精确。
2. 双重量化(Double Quantization)
量化需要存储缩放因子(scale),这部分也占显存。
双重量化:对缩放因子也进行量化,进一步节省显存。
3. 分页优化器(Paged Optimizer)
当显存不足时,自动将优化器状态卸载到 CPU 内存。
3.3 QLoRA 显存对比
| 方法 | 7B 模型显存 | 能在什么 GPU 上跑 |
|---|---|---|
| 全量微调 FP16 | 84 GB | 8×A100 |
| LoRA FP16 | 20 GB | A100 40GB |
| QLoRA NF4 | 6 GB | RTX 3060 12GB |
6GB 显存就能微调 7B 模型!
3.4 QLoRA 代码实践
import torch
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
TrainingArguments,
)
from peft import (
LoraConfig,
get_peft_model,
prepare_model_for_kbit_training,
)
from trl import SFTTrainer
def train_with_qlora(
model_name: str,
dataset,
output_dir: str,
r: int = 64,
lora_alpha: int = 16,
target_modules: list = None,
):
"""使用 QLoRA 微调模型"""
# 1. 4-bit 量化配置
bnb_config = BitsAndBytesConfig(
load_in_4bit=True, # 4-bit 加载
bnb_4bit_quant_type="nf4", # NF4 量化
bnb_4bit_compute_dtype=torch.bfloat16, # 计算时用 bf16
bnb_4bit_use_double_quant=True, # 双重量化
)
# 2. 加载量化模型
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config,
device_map="auto",
trust_remote_code=True,
)
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
# 3. 准备模型用于 k-bit 训练
model = prepare_model_for_kbit_training(model)
# 4. LoRA 配置
if target_modules is None:
target_modules = [
"q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj",
]
lora_config = LoraConfig(
r=r,
lora_alpha=lora_alpha,
lora_dropout=0.05,
target_modules=target_modules,
bias="none",
task_type="CAUSAL_LM",
)
# 5. 应用 LoRA
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# 6. 训练配置
training_args = TrainingArguments(
output_dir=output_dir,
num_train_epochs=3,
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=2e-4,
weight_decay=0.001,
warmup_ratio=0.03,
lr_scheduler_type="cosine",
logging_steps=10,
save_steps=500,
save_total_limit=3,
bf16=True,
gradient_checkpointing=True,
optim="paged_adamw_8bit", # 分页优化器
max_grad_norm=0.3,
)
# 7. 创建 Trainer
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=dataset,
tokenizer=tokenizer,
max_seq_length=2048,
)
# 8. 训练
trainer.train()
# 9. 保存 LoRA 权重
model.save_pretrained(output_dir)
tokenizer.save_pretrained(output_dir)
return model, tokenizer
# 合并 LoRA 权重到基础模型
def merge_lora_weights(
base_model_name: str,
lora_weights_path: str,
output_path: str,
):
"""将 LoRA 权重合并到基础模型"""
from peft import PeftModel
# 加载基础模型(这次用 FP16)
base_model = AutoModelForCausalLM.from_pretrained(
base_model_name,
torch_dtype=torch.float16,
device_map="auto",
)
# 加载 LoRA 权重
model = PeftModel.from_pretrained(base_model, lora_weights_path)
# 合并权重
merged_model = model.merge_and_unload()
# 保存合并后的模型
merged_model.save_pretrained(output_path)
return merged_model
# 使用示例
if __name__ == "__main__":
from datasets import load_dataset
# 加载数据集
dataset = load_dataset("tatsu-lab/alpaca", split="train[:1000]")
# QLoRA 训练
model, tokenizer = train_with_qlora(
model_name="meta-llama/Llama-2-7b-hf",
dataset=dataset,
output_dir="./qlora_output",
r=64,
lora_alpha=16,
)3.5 QLoRA 显存监控
import torch
def print_gpu_memory():
"""打印 GPU 显存使用情况"""
if torch.cuda.is_available():
for i in range(torch.cuda.device_count()):
allocated = torch.cuda.memory_allocated(i) / 1024**3
reserved = torch.cuda.memory_reserved(i) / 1024**3
total = torch.cuda.get_device_properties(i).total_memory / 1024**3
print(f"GPU {i}: {allocated:.1f}GB allocated, "
f"{reserved:.1f}GB reserved, {total:.1f}GB total")
# 在训练的关键节点调用
print("加载模型后:")
print_gpu_memory()
print("开始训练后:")
print_gpu_memory()四、其他 PEFT 方法
4.1 PEFT 方法全景
4.2 Adapter:插入小模块
Adapter 在 Transformer 层中间插入小型可训练模块:
class Adapter(nn.Module):
"""Adapter 模块"""
def __init__(self, hidden_size, adapter_size=64):
super().__init__()
self.down_proj = nn.Linear(hidden_size, adapter_size)
self.up_proj = nn.Linear(adapter_size, hidden_size)
self.act = nn.GELU()
def forward(self, x):
# 残差连接
return x + self.up_proj(self.act(self.down_proj(x)))Adapter vs LoRA:
- Adapter 增加了推理延迟(串行)
- LoRA 可以合并权重(无延迟)
- LoRA 更主流
4.3 Prefix-Tuning:可学习前缀
在每一层的 attention 前添加可学习的"虚拟 token":
class PrefixTuning(nn.Module):
"""Prefix-Tuning"""
def __init__(self, num_layers, hidden_size, prefix_length=10):
super().__init__()
# 每层的 key 和 value 前缀
self.prefix_key = nn.Parameter(
torch.randn(num_layers, prefix_length, hidden_size)
)
self.prefix_value = nn.Parameter(
torch.randn(num_layers, prefix_length, hidden_size)
)
def get_prefix(self, layer_idx, batch_size):
"""获取某一层的前缀"""
key = self.prefix_key[layer_idx].unsqueeze(0).expand(batch_size, -1, -1)
value = self.prefix_value[layer_idx].unsqueeze(0).expand(batch_size, -1, -1)
return key, value原始 attention:
Q @ K^T → Attention → @ V
Prefix-Tuning:
Q @ [prefix_K; K]^T → Attention → @ [prefix_V; V]4.4 Prompt Tuning:软提示
只在输入层添加可学习的 embedding:
class PromptTuning(nn.Module):
"""Prompt Tuning: 最简单的 PEFT"""
def __init__(self, embedding_dim, prompt_length=20):
super().__init__()
# 可学习的软提示
self.soft_prompt = nn.Parameter(
torch.randn(prompt_length, embedding_dim)
)
def forward(self, input_embeddings):
batch_size = input_embeddings.shape[0]
# 扩展到 batch
prompt = self.soft_prompt.unsqueeze(0).expand(batch_size, -1, -1)
# 拼接: [soft_prompt, input_embeddings]
return torch.cat([prompt, input_embeddings], dim=1)优点:参数量极少
缺点:效果通常不如 LoRA
4.5 方法对比
| 方法 | 参数量 | 效果 | 推理开销 | 复杂度 |
|---|---|---|---|---|
| LoRA | 0.1-1% | ⭐⭐⭐⭐ | 无(可合并) | 低 |
| QLoRA | 0.1-1% | ⭐⭐⭐⭐ | 无 | 低 |
| Adapter | 0.5-2% | ⭐⭐⭐ | 有 | 中 |
| Prefix-Tuning | 0.1% | ⭐⭐⭐ | 有 | 中 |
| Prompt Tuning | <0.1% | ⭐⭐ | 无 | 最低 |
| BitFit | 0.05% | ⭐⭐ | 无 | 最低 |
结论:LoRA/QLoRA 是目前的最佳实践!
五、使用 PEFT 库
5.1 PEFT 库基础用法
from peft import (
LoraConfig,
get_peft_model,
TaskType,
PeftModel,
)
from transformers import AutoModelForCausalLM, AutoTokenizer
# 1. 加载基础模型
base_model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b-hf",
torch_dtype=torch.bfloat16,
device_map="auto",
)
# 2. 配置 LoRA
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=16, # 秩
lora_alpha=32, # 缩放因子
lora_dropout=0.05, # Dropout
target_modules=[ # 目标模块
"q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"
],
bias="none",
)
# 3. 创建 PEFT 模型
peft_model = get_peft_model(base_model, lora_config)
# 4. 查看可训练参数
peft_model.print_trainable_parameters()
# 输出类似:trainable params: 4,194,304 || all params: 6,742,609,920 || trainable%: 0.0622
# 5. 正常训练...
# trainer.train()
# 6. 保存 LoRA 权重(只保存增量)
peft_model.save_pretrained("./lora_weights")
# 只有几十 MB!
# 7. 加载 LoRA 权重
loaded_model = PeftModel.from_pretrained(
base_model,
"./lora_weights"
)
# 8. 合并权重(推理时可选)
merged_model = loaded_model.merge_and_unload()5.2 不同任务的配置
from peft import TaskType
# 因果语言模型(GPT 类)
config_clm = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=16,
lora_alpha=32,
target_modules=["q_proj", "v_proj"],
)
# 序列分类(BERT 类)
config_seq_cls = LoraConfig(
task_type=TaskType.SEQ_CLS,
r=8,
lora_alpha=16,
target_modules=["query", "value"],
)
# Seq2Seq(T5 类)
config_seq2seq = LoraConfig(
task_type=TaskType.SEQ_2_SEQ_LM,
r=16,
lora_alpha=32,
target_modules=["q", "v"],
)
# Token 分类(NER)
config_token_cls = LoraConfig(
task_type=TaskType.TOKEN_CLS,
r=8,
lora_alpha=16,
target_modules=["query", "value"],
)5.3 多个 LoRA 适配器
from peft import PeftModel
# 基础模型
base_model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-hf")
# 加载第一个 LoRA(中文能力)
model = PeftModel.from_pretrained(base_model, "./lora_chinese", adapter_name="chinese")
# 加载第二个 LoRA(代码能力)
model.load_adapter("./lora_code", adapter_name="code")
# 切换适配器
model.set_adapter("chinese") # 使用中文 LoRA
output_zh = model.generate(...)
model.set_adapter("code") # 切换到代码 LoRA
output_code = model.generate(...)
# 列出所有适配器
print(model.peft_config)
# 禁用 LoRA(使用原始模型)
model.disable_adapter()
# 重新启用
model.enable_adapter()5.4 推理优化:合并权重
def optimize_for_inference(peft_model_path, base_model_name, output_path):
"""将 LoRA 合并到基础模型,优化推理"""
# 加载基础模型
base_model = AutoModelForCausalLM.from_pretrained(
base_model_name,
torch_dtype=torch.float16,
)
# 加载 PEFT 模型
peft_model = PeftModel.from_pretrained(base_model, peft_model_path)
# 合并权重
merged_model = peft_model.merge_and_unload()
# 保存
merged_model.save_pretrained(output_path)
print(f"合并后的模型已保存到 {output_path}")
return merged_model
# 合并后的模型可以直接用,没有额外开销
merged_model = AutoModelForCausalLM.from_pretrained("./merged_model")六、LoRA 超参数调优
6.1 关键超参数
| 参数 | 说明 | 推荐值 |
|---|---|---|
r | 秩(rank) | 8-64 |
lora_alpha | 缩放因子 | 通常 = 2×r |
lora_dropout | Dropout | 0-0.1 |
target_modules | 目标层 | 至少 q_proj, v_proj |
bias | 是否训练 bias | "none" |
6.2 秩(r)的选择
参数少
欠拟合风险"] R2["r=16
平衡选择"] R3["r=64
参数多
效果更好"] R4["r=256
接近全量微调"] end style R2 fill:#4ecdc4
经验法则:
- 简单任务(分类):r=8 足够
- 复杂任务(指令遵循):r=16-64
- 资源充足:r=64 效果最好
6.3 缩放因子(lora_alpha)
lora_alpha / r 决定了 LoRA 更新的"强度"。
# 常见配置
config_1 = LoraConfig(r=8, lora_alpha=16) # scale = 2
config_2 = LoraConfig(r=16, lora_alpha=32) # scale = 2
config_3 = LoraConfig(r=64, lora_alpha=128) # scale = 2
# 一般 lora_alpha = 2 * r,即 scale = 26.4 目标模块的选择
# 最小配置(效果一般)
target_modules = ["q_proj", "v_proj"]
# 推荐配置
target_modules = ["q_proj", "k_proj", "v_proj", "o_proj"]
# 完整配置(效果最好)
target_modules = [
"q_proj", "k_proj", "v_proj", "o_proj", # Attention
"gate_proj", "up_proj", "down_proj", # FFN
]
# 不同模型的模块名可能不同
# LLaMA: q_proj, k_proj, v_proj, o_proj
# GPT-2: c_attn, c_proj
# BERT: query, key, value6.5 学习率
LoRA 通常需要比全量微调更高的学习率:
# 全量微调
learning_rate = 2e-5
# LoRA 微调
learning_rate = 1e-4 ~ 3e-4 # 高 5-10 倍七、实战:完整训练流程
7.1 使用 LLaMA-Factory
# 安装
git clone https://github.com/hiyouga/LLaMA-Factory.git
cd LLaMA-Factory
pip install -e .
# 启动 WebUI
python src/webui.py配置示例(yaml):
# llama2_lora_sft.yaml
model_name_or_path: meta-llama/Llama-2-7b-hf
stage: sft
do_train: true
finetuning_type: lora
# LoRA 配置
lora_rank: 64
lora_alpha: 128
lora_dropout: 0.05
lora_target: q_proj,v_proj,k_proj,o_proj,gate_proj,up_proj,down_proj
# 数据
dataset: alpaca_zh
template: llama2
cutoff_len: 2048
# 训练
per_device_train_batch_size: 4
gradient_accumulation_steps: 4
learning_rate: 2e-4
num_train_epochs: 3
lr_scheduler_type: cosine
warmup_ratio: 0.1
# 量化(QLoRA)
quantization_bit: 4
# 输出
output_dir: ./output/llama2-7b-lora# 命令行训练
llamafactory-cli train llama2_lora_sft.yaml7.2 完整训练脚本
import torch
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
TrainingArguments,
)
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from trl import SFTTrainer
from datasets import load_dataset
def main():
# ============ 配置 ============
MODEL_NAME = "meta-llama/Llama-2-7b-hf"
OUTPUT_DIR = "./output/llama2-7b-qlora"
# ============ 量化配置 ============
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
)
# ============ 加载模型 ============
print("加载模型...")
model = AutoModelForCausalLM.from_pretrained(
MODEL_NAME,
quantization_config=bnb_config,
device_map="auto",
trust_remote_code=True,
)
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
# ============ 准备 LoRA ============
model = prepare_model_for_kbit_training(model)
lora_config = LoraConfig(
r=64,
lora_alpha=128,
lora_dropout=0.05,
target_modules=[
"q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj",
],
bias="none",
task_type="CAUSAL_LM",
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# ============ 加载数据 ============
print("加载数据...")
dataset = load_dataset("tatsu-lab/alpaca", split="train")
def format_instruction(example):
if example.get("input"):
text = f"""Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.
### Instruction:
{example['instruction']}
### Input:
{example['input']}
### Response:
{example['output']}"""
else:
text = f"""Below is an instruction that describes a task. Write a response that appropriately completes the request.
### Instruction:
{example['instruction']}
### Response:
{example['output']}"""
return text
# ============ 训练配置 ============
training_args = TrainingArguments(
output_dir=OUTPUT_DIR,
num_train_epochs=3,
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=2e-4,
weight_decay=0.001,
warmup_ratio=0.03,
lr_scheduler_type="cosine",
logging_steps=10,
save_steps=500,
save_total_limit=3,
bf16=True,
gradient_checkpointing=True,
optim="paged_adamw_8bit",
max_grad_norm=0.3,
report_to="none",
)
# ============ 训练 ============
print("开始训练...")
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=dataset,
tokenizer=tokenizer,
formatting_func=format_instruction,
max_seq_length=2048,
)
trainer.train()
# ============ 保存 ============
print("保存模型...")
trainer.model.save_pretrained(OUTPUT_DIR)
tokenizer.save_pretrained(OUTPUT_DIR)
print(f"训练完成!模型保存在 {OUTPUT_DIR}")
if __name__ == "__main__":
main()八、总结
LoRA 核心要点
关键 Takeaway
- LoRA 的核心是低秩分解:用两个小矩阵近似权重更新
- QLoRA = 4-bit 量化 + LoRA:7B 模型只需 6GB 显存
推荐配置:
- r=64, lora_alpha=128
- 目标模块:q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj
- 学习率:2e-4
- LoRA 可以合并:推理时无额外开销
工具推荐:
- PEFT 库:官方实现
- TRL:配合 SFTTrainer
- LLaMA-Factory:中文友好,一站式
显存需求速查
| 模型 | 全量微调 | LoRA (FP16) | QLoRA (4-bit) |
|---|---|---|---|
| 7B | 84 GB | 20 GB | 6 GB |
| 13B | 156 GB | 36 GB | 10 GB |
| 70B | 840 GB | 160 GB | 48 GB |
下一步学习
- [ ] 模型量化:让大模型更轻量
- [ ] vLLM 推理加速:高效部署
- [ ] 本地部署:Ollama 实战
参考资料
- LoRA Paper - LoRA: Low-Rank Adaptation of Large Language Models
- QLoRA Paper - QLoRA: Efficient Finetuning of Quantized LLMs
- PEFT Library - HuggingFace PEFT
- LLaMA-Factory - 一站式微调框架
- Adapter Paper - Parameter-Efficient Transfer Learning