全参微调的痛点

全参数微调一个 7B 模型需要:

  • 显存:~80GB(模型权重 14GB + 梯度 14GB + 优化器状态 56GB)
  • 硬件:1×A100 80GB 或 2×A100 40GB
  • 成本:每小时 ¥10-30

LoRA(Low-Rank Adaptation)将这个数字降到 ~8GB,QLoRA 进一步降到 ~5GB

LoRA 原理:低秩分解

核心数学

LoRA 假设模型微调时的权重更新 ΔW 是低秩的。它将 ΔW 分解为两个小矩阵的乘积:

原始:h = W·x          W ∈ R^(d×k),参数量 d×k
LoRA:h = W·x + B·A·x   A ∈ R^(r×k),B ∈ R^(d×r),参数量 r×(d+k)

当 r << min(d, k) 时,参数量大幅减少
import torch
import torch.nn as nn

class LoRALayer(nn.Module):
    def __init__(self, original_layer, rank=8, alpha=16):
        super().__init__()
        self.original = original_layer  # 冻结的原始权重
        self.rank = rank
        self.alpha = alpha
        self.scaling = alpha / rank
        
        d_out, d_in = original_layer.weight.shape
        
        # 低秩矩阵 A 和 B
        self.lora_A = nn.Parameter(torch.zeros(rank, d_in))
        self.lora_B = nn.Parameter(torch.zeros(d_out, rank))
        
        # A 用 Kaiming 初始化,B 用零初始化
        nn.init.kaiming_uniform_(self.lora_A, a=5**0.5)
        # B 初始为 0,所以训练开始时 ΔW = 0,不改变原模型行为
        
        # 冻结原始权重
        for param in self.original.parameters():
            param.requires_grad = False
    
    def forward(self, x):
        original_output = self.original(x)
        lora_output = (x @ self.lora_A.T) @ self.lora_B * self.scaling
        return original_output + lora_output

参数量对比

以 7B 模型为例(隐藏层 4096):

方法可训练参数总参数占比显存(训练)
全参微调7B100%~80GB
LoRA (r=8)~20M0.3%~10GB
LoRA (r=16)~40M0.6%~12GB
LoRA (r=64)~160M2.3%~18GB
QLoRA (r=8)~20M0.3%~5GB

QLoRA:4-bit 量化 + LoRA

核心创新

QLoRA 在 LoRA 基础上,将冻结的原始权重量化为 4-bit,大幅减少显存:

# QLoRA 的三重创新:
# 1. NF4 (NormalFloat 4-bit):专为正态分布权重设计的量化格式
# 2. Double Quantization:对量化常数本身再量化,省额外显存
# 3. Paged Optimizer:用 CPU RAM 溢出处理 GPU 显存峰值

import bitsandbytes as bnb

def create_qlora_model(model_name="meta-llama/Llama-2-7b-hf"):
    from transformers import AutoModelForCausalLM, BitsAndBytesConfig
    
    bnb_config = BitsAndBytesConfig(
        load_in_4bit=True,
        bnb_4bit_quant_type="nf4",           # NormalFloat 4-bit
        bnb_4bit_compute_dtype=torch.bfloat16, # 计算精度
        bnb_4bit_use_double_quant=True,       # 双重量化
    )
    
    model = AutoModelForCausalLM.from_pretrained(
        model_name,
        quantization_config=bnb_config,
        device_map="auto"
    )
    
    # 准备模型以使用梯度检查点
    model.gradient_checkpointing_enable()
    model = prepare_model_for_kbit_training(model)
    
    return model

NF4 量化原理

标准权重服从正态分布,NF4 直接使用正态分布的分位点作为量化级别:

NF4 的 16 个量化级别(对称):
  [-1.0, -0.6962, -0.5251, -0.3949, -0.2844, -0.1848, -0.0911, 0.0,
   0.0796, 0.1609, 0.2461, 0.3379, 0.4407, 0.5626, 0.7230, 1.0]

相比均匀量化,NF4 在正态分布密集区域有更高精度

实战:使用 PEFT 进行 QLoRA 微调

from peft import LoraConfig, get_peft_model, TaskType

def setup_qlora_training(model):
    # LoRA 配置
    config = LoraConfig(
        r=16,                          # Rank
        lora_alpha=32,                 # 缩放因子
        target_modules=[               # 应用 LoRA 的模块
            "q_proj", "k_proj", "v_proj", "o_proj",  # Attention
            "gate_proj", "up_proj", "down_proj",      # MLP
        ],
        lora_dropout=0.05,
        bias="none",
        task_type=TaskType.CAUSAL_LM,
    )
    
    model = get_peft_model(model, config)
    model.print_trainable_parameters()
    # 输出: trainable params: 39,616,512 || all params: 3,540,389,888 || trainable%: 1.12%
    
    return model

Rank 选择指南

Rank(r)是最关键的超参数:

Rank可训练参数适用场景风险
r=4极少简单任务、小数据集欠拟合
r=8通用任务(推荐起点)-
r=16领域适配、多任务-
r=32复杂任务、大量数据略过拟合
r=64+很多大规模领域微调过拟合、失去 PEFT 优势
# 经验法则:r 的选择
def recommend_rank(task_type, dataset_size, model_size):
    base = {
        "style_transfer": 4,      # 风格迁移,低秩足够
        "domain_adapt": 16,       # 领域适配
        "code_generation": 32,    # 代码生成
        "math_reasoning": 64,     # 数学推理
    }.get(task_type, 8)
    
    # 数据集越大,rank 可以越大
    if dataset_size > 100000:
        base = min(base * 2, 64)
    elif dataset_size < 1000:
        base = min(base, 8)
    
    return base

Target Modules 配置

选择哪些模块应用 LoRA 影响巨大:

# 最小配置:仅 Attention 的 Q/V(原论文默认)
target_modules_minimal = ["q_proj", "v_proj"]

# 推荐配置:全部 Attention 投影
target_modules_recommended = ["q_proj", "k_proj", "v_proj", "o_proj"]

# 激进配置:Attention + MLP
target_modules_full = [
    "q_proj", "k_proj", "v_proj", "o_proj",
    "gate_proj", "up_proj", "down_proj"
]

# 效果对比(7B 模型,r=16):
# minimal:     ~10M params, 效果尚可
# recommended: ~20M params, 效果好(推荐)
# full:        ~40M params, 效果最好,显存略增

Alpha 和学习率

# LoRA Alpha:控制 LoRA 更新的幅度
# 经验:alpha = 2 * r 或 alpha = r
# alpha 越大,LoRA 对模型的影响越大

# 学习率:LoRA 通常需要比全参微调更大的学习率
training_configs = {
    "conservative": {"lr": 1e-4, "alpha": 16, "r": 8},
    "balanced":     {"lr": 3e-4, "alpha": 32, "r": 16},  # 推荐
    "aggressive":   {"lr": 5e-4, "alpha": 64, "r": 32},
}

完整训练脚本

import torch
from transformers import (
    AutoModelForCausalLM, AutoTokenizer,
    TrainingArguments, Trainer, BitsAndBytesConfig
)
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from datasets import Dataset

def qlora_finetune(
    model_name="Qwen/Qwen2-7B",
    train_data=None,
    output_dir="./qlora-output"
):
    # 1. 量化配置
    bnb_config = BitsAndBytesConfig(
        load_in_4bit=True,
        bnb_4bit_quant_type="nf4",
        bnb_4bit_compute_dtype=torch.bfloat16,
        bnb_4bit_use_double_quant=True,
    )
    
    # 2. 加载模型
    model = AutoModelForCausalLM.from_pretrained(
        model_name,
        quantization_config=bnb_config,
        device_map="auto",
        torch_dtype=torch.bfloat16,
    )
    model = prepare_model_for_kbit_training(model)
    
    # 3. LoRA 配置
    lora_config = LoraConfig(
        r=16,
        lora_alpha=32,
        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, lora_config)
    
    # 4. 训练参数
    training_args = TrainingArguments(
        output_dir=output_dir,
        num_train_epochs=3,
        per_device_train_batch_size=4,
        gradient_accumulation_steps=4,  # 等效 batch_size=16
        learning_rate=3e-4,
        lr_scheduler_type="cosine",
        warmup_ratio=0.03,
        logging_steps=10,
        save_strategy="epoch",
        bf16=True,
        optim="paged_adamw_8bit",      # QLoRA 专用优化器
        gradient_checkpointing=True,
    )
    
    # 5. 训练
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    tokenizer.pad_token = tokenizer.eos_token
    
    trainer = Trainer(
        model=model,
        args=training_args,
        train_dataset=train_data,
        tokenizer=tokenizer,
    )
    
    trainer.train()
    model.save_pretrained(output_dir)
    
    return model, tokenizer

Unsloth 加速

Unsloth 将 LoRA 训练速度提升 2-5 倍,显存再降 30%:

# pip install unsloth
from unsloth import FastLanguageModel

def unsloth_qlora_train():
    # 1. 加载模型(Unsloth 优化版)
    model, tokenizer = FastLanguageModel.from_pretrained(
        model_name="unsloth/Qwen2-7B-bnb-4bit",
        max_seq_length=2048,
        dtype=None,  # 自动选择
        load_in_4bit=True,
    )
    
    # 2. 添加 LoRA(Unsloth 优化)
    model = FastLanguageModel.get_peft_model(
        model,
        r=16,
        target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
                       "gate_proj", "up_proj", "down_proj"],
        lora_alpha=32,
        lora_dropout=0.05,
        bias="none",
        use_gradient_checkpointing="unsloth",  # Unsloth 专用
        random_state=42,
    )
    
    # 3. 训练(使用标准 Trainer 或 SFTTrainer)
    from trl import SFTTrainer
    trainer = SFTTrainer(
        model=model,
        tokenizer=tokenizer,
        train_dataset=train_data,
        dataset_text_field="text",
        max_seq_length=2048,
        args=TrainingArguments(
            per_device_train_batch_size=4,
            gradient_accumulation_steps=4,
            warmup_steps=50,
            num_train_epochs=3,
            learning_rate=3e-4,
            bf16=True,
            logging_steps=10,
            optim="adamw_8bit",
            seed=42,
            output_dir="unsloth-output",
        ),
    )
    
    trainer.train()

Unsloth 性能对比

指标标准 PEFTUnsloth提升
训练速度1x2-5x2-5 倍
显存(7B)~10GB~7GB-30%
显存(70B)~40GB~28GB-30%
序列长度 8KOOM on 24GB可运行-

合并策略

训练完成后,LoRA 权重需要合并回基座模型才能部署:

# 方式一:合并后保存(推荐部署用)
merged_model = model.merge_and_unload()
merged_model.save_pretrained("./merged-model")

# 方式二:保存 LoRA 适配器(灵活,可随时切换)
model.save_pretrained("./lora-adapter")  # 只保存 LoRA 权重(~50MB)
# 部署时加载
base_model = AutoModelForCausalLM.from_pretrained("base-model")
model = PeftModel.from_pretrained(base_model, "./lora-adapter")
# 多 LoRA 切换:一个基座模型 + 多个 LoRA 适配器
class MultiLoRAManager:
    def __init__(self, base_model_path):
        self.base = AutoModelForCausalLM.from_pretrained(base_model_path)
        self.adapters = {}
    
    def load_adapter(self, name, adapter_path):
        self.adapters[name] = PeftModel.from_pretrained(
            self.base, adapter_path
        )
    
    def generate(self, prompt, adapter_name="default"):
        model = self.adapters[adapter_name]
        return model.generate(prompt)

总结

维度全参微调LoRAQLoRA
显存(7B)~80GB~10GB~5GB
训练速度1x1.1x0.8x
效果100%~98%~97%
硬件要求A100 80GBRTX 3090RTX 3060
部署直接需合并/加载需合并/加载

最佳实践:QLoRA + Unsloth + r=16 + 全模块覆盖 + 3e-4 学习率。这是 2025 年性价比最高的微调方案。

加入讨论

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

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