一、为什么需要 LoRA

1.1 全量微调的困境

全量微调(Full Fine-tuning)需要更新模型所有参数,对于大模型来说显存和计算成本极高:

LLaMA-70B 全量微调:
  模型参数: 70B × 4 bytes (FP32) = 280 GB
  优化器状态 (Adam): 280 GB × 2 = 560 GB
  梯度: 280 GB
  激活值: ~100 GB (视 batch size)
  总显存需求: ~1220 GB

  → 需要 20 张昇腾 910B (64GB) 才能跑

1.2 LoRA 的核心思想

LoRA(Low-Rank Adaptation)的核心思想是:大模型微调时的权重更新矩阵是低秩的。既然如此,我们不需要更新全部参数,只需要训练两个小矩阵 A 和 B 即可。

原始权重更新: ΔW = W_fine - W_pre (d × d 矩阵)

LoRA 分解: ΔW = B × A
  A: d × r 矩阵 (r << d)
  B: r × d 矩阵 (r << d)

  参数量: 2 × d × r (远小于 d × d)

示例 (d=4096, r=8):
  全量参数: 4096 × 4096 = 16,777,216
  LoRA 参数: 2 × 4096 × 8 = 65,536
  压缩比: 256x

二、LoRA 数学原理

2.1 前向传播

原始: y = Wx
LoRA: y = Wx + (B × A) × x × α/r

其中:
  W: 冻结的原始权重 (d × d)
  A: 可训练矩阵 (d × r), 用高斯初始化
  B: 可训练矩阵 (r × d), 用零初始化
  r: 秩 (rank), 通常 4-64
  α: 缩放系数, 通常 16-32
  α/r: 归一化因子

2.2 参数效率

方法 可训练参数 显存占用 效果
全量微调 100% 极高 最佳
LoRA (r=8) ~0.1% 极低 接近全量
LoRA (r=32) ~0.4% 非常接近全量
Adapter ~1-3% 略低于 LoRA
Prefix Tuning ~0.1% 较低

2.3 秩的选择

def choose_rank(model_size, task_complexity):
    """根据模型大小和任务复杂度选择秩"""
    
    # 模型越大,需要的秩越小 (因为已有足够表示能力)
    size_factor = {
        '7B': 1.0,
        '13B': 0.8,
        '30B': 0.6,
        '70B': 0.5
    }
    
    # 任务越复杂,需要的秩越大
    task_factor = {
        'classification': 0.5,
        'summarization': 0.8,
        'translation': 1.0,
        'code_generation': 1.2,
        'math_reasoning': 1.5
    }
    
    base_rank = 8
    rank = int(base_rank * size_factor.get(model_size, 1.0) * task_factor.get(task_complexity, 1.0))
    
    return max(4, min(rank, 64))  # 限制在 4-64

# 示例
rank = choose_rank('7B', 'translation')
print(f"推荐 rank: {rank}")  # → 8

三、昇腾 NPU 上的 LoRA 实现

3.1 基础 LoRA 层

import torch
import torch.nn as nn
import torch.npu

class LoRALayer(nn.Module):
    def __init__(self, original_layer, r=8, alpha=16):
        super().__init__()
        
        self.original_layer = original_layer
        self.r = r
        self.alpha = alpha
        self.scaling = alpha / r
        
        # 冻结原始权重
        for param in self.original_layer.parameters():
            param.requires_grad = False
        
        # LoRA 矩阵
        d_in = original_layer.in_features
        d_out = original_layer.out_features
        
        # A: 用高斯初始化
        self.lora_A = nn.Parameter(torch.randn(d_in, r) * 0.01)
        
        # B: 用零初始化 (确保初始时 LoRA 输出为 0)
        self.lora_B = nn.Parameter(torch.zeros(r, d_out))
    
    def forward(self, x):
        # 原始前向
        result = self.original_layer(x)
        
        # LoRA 增量
        lora_out = (x @ self.lora_A @ self.lora_B) * self.scaling
        
        return result + lora_out
    
    def merge_weights(self):
        """合并 LoRA 权重到原始层 (推理时使用)"""
        with torch.no_grad():
            self.original_layer.weight += (self.lora_B @ self.lora_A).t() * self.scaling

3.2 注入 LoRA 到模型

def inject_lora(model, target_modules=['q_proj', 'v_proj'], r=8, alpha=16):
    """向模型注入 LoRA 层"""
    
    lora_params = 0
    
    for name, module in model.named_modules():
        if any(target in name for target in target_modules):
            # 替换目标层为 LoRA 层
            lora_layer = LoRALayer(module, r=r, alpha=alpha)
            
            # 设置到父模块
            parent_name = '.'.join(name.split('.')[:-1])
            child_name = name.split('.')[-1]
            parent = dict(model.named_modules())[parent_name]
            setattr(parent, child_name, lora_layer)
            
            # 统计 LoRA 参数
            lora_params += lora_layer.lora_A.numel() + lora_layer.lora_B.numel()
    
    # 统计
    total_params = sum(p.numel() for p in model.parameters())
    print(f"LoRA 参数: {lora_params:,} ({lora_params/total_params*100:.2f}%)")
    print(f"总参数: {total_params:,}")
    
    return model

# 使用示例
model = LLaMAForCausalLM.from_pretrained("llama-7b")
model = inject_lora(
    model,
    target_modules=['q_proj', 'k_proj', 'v_proj', 'o_proj'],
    r=16,
    alpha=32
)
model = model.npu()

3.3 LoRA 训练流程

import torch.optim as optim

def train_lora(model, dataset, num_epochs=3, lr=2e-4):
    """LoRA 微调训练"""
    
    # 只优化 LoRA 参数
    lora_params = []
    for param in model.parameters():
        if param.requires_grad:
            lora_params.append(param)
    
    optimizer = optim.AdamW(lora_params, lr=lr, weight_decay=0.01)
    
    # 学习率调度
    scheduler = optim.lr_scheduler.CosineAnnealingLR(
        optimizer, T_max=num_epochs, eta_min=1e-6
    )
    
    for epoch in range(num_epochs):
        model.train()
        total_loss = 0
        
        for batch in dataloader:
            input_ids = batch['input_ids'].npu()
            labels = batch['labels'].npu()
            
            # 前向
            outputs = model(input_ids, labels=labels)
            loss = outputs.loss
            
            # 反向
            loss.backward()
            
            # 梯度裁剪
            torch.nn.utils.clip_grad_norm_(lora_params, max_norm=1.0)
            
            # 更新
            optimizer.step()
            optimizer.zero_grad()
            
            total_loss += loss.item()
        
        scheduler.step()
        
        avg_loss = total_loss / len(dataloader)
        print(f"Epoch {epoch+1}/{num_epochs}, Loss: {avg_loss:.4f}")
    
    return model

四、推理阶段:合并 LoRA 权重

4.1 权重合并

推理时可以将 LoRA 权重合并到原始层,消除额外计算开销:

def merge_lora_weights(model):
    """合并所有 LoRA 权重"""
    
    for name, module in model.named_modules():
        if isinstance(module, LoRALayer):
            # 合并权重
            module.merge_weights()
            
            # 替换为原始层
            parent_name = '.'.join(name.split('.')[:-1])
            child_name = name.split('.')[-1]
            parent = dict(model.named_modules())[parent_name]
            setattr(parent, child_name, module.original_layer)
    
    print("LoRA 权重已合并")
    return model

# 合并后推理
model = merge_lora_weights(model)

# 推理性能与全量微调模型相同
output = model.generate(input_ids, max_new_tokens=100)

4.2 多 LoRA 切换

同一基础模型可以加载不同的 LoRA 适配器,实现多任务切换:

class MultiLoRAModel:
    def __init__(self, base_model_path):
        self.base_model = load_model(base_model_path)
        self.lora_adapters = {}
    
    def load_lora(self, task_name, lora_path):
        """加载 LoRA 适配器"""
        adapter = torch.load(lora_path)
        self.lora_adapters[task_name] = adapter
    
    def switch_adapter(self, task_name):
        """切换 LoRA 适配器"""
        adapter = self.lora_adapters[task_name]
        
        for name, param in self.base_model.named_parameters():
            if name in adapter:
                param.data = adapter[name]
    
    def inference(self, input_text, task_name):
        """使用指定任务的 LoRA 推理"""
        self.switch_adapter(task_name)
        return self.base_model.generate(input_text)

# 使用示例
multi_model = MultiLoRAModel("llama-7b")
multi_model.load_lora("chat", "chat_lora.pt")
multi_model.load_lora("code", "code_lora.pt")
multi_model.load_lora("math", "math_lora.pt")

# 快速切换
result_chat = multi_model.inference("Hello!", task_name="chat")
result_code = multi_model.inference("Write a function", task_name="code")
result_math = multi_model.inference("Solve equation", task_name="math")

五、LoRA 超参数调优

5.1 关键超参数

超参数 推荐值 说明
rank ® 8-32 越大表达能力越强,但参数越多
alpha 16-64 通常设为 2×rank
target_modules q,v,k,o_proj 通常只调注意力层
learning_rate 1e-4 - 3e-4 比全量微调高
batch_size 16-64 LoRA 显存占用小,可增大
num_epochs 2-5 通常 3 epoch 足够

5.2 推荐配置

# 7B 模型 LoRA 配置
lora_config_7b = {
    'r': 16,
    'alpha': 32,
    'target_modules': ['q_proj', 'v_proj', 'k_proj', 'o_proj'],
    'learning_rate': 2e-4,
    'batch_size': 32,
    'num_epochs': 3,
    'warmup_ratio': 0.03,
    'weight_decay': 0.01,
    'max_grad_norm': 1.0,
}

# 70B 模型 LoRA 配置
lora_config_70b = {
    'r': 8,  # 模型更大,rank 可以更小
    'alpha': 16,
    'target_modules': ['q_proj', 'v_proj'],  # 只调 Q 和 V
    'learning_rate': 1e-4,
    'batch_size': 16,
    'num_epochs': 2,
    'warmup_ratio': 0.03,
    'weight_decay': 0.01,
    'max_grad_norm': 1.0,
}

六、LoRA vs 其他 PEFT 方法

方法 参数量 显存 训练速度 推理速度 效果
全量微调 100% 极高 最快 最佳
LoRA 0.1-1% 与全量相同 接近最佳
QLoRA 0.1-1% 更低 略慢 接近 LoRA
Adapter 1-3% 略慢 略低于 LoRA
Prefix Tuning 0.1% 略慢 较低
Prompt Tuning <0.1% 最低 最快 最快 最低

QLoRA(量化 LoRA)

# 8.2 新增:QLoRA 支持
from torch.quantization import quantize_dynamic

class QLoRALayer(nn.Module):
    def __init__(self, original_layer, r=8, alpha=16):
        super().__init__()
        
        # 量化原始权重到 INT4
        self.original_layer = quantize_dynamic(
            original_layer,
            {nn.Linear},
            dtype=torch.qint4
        )
        
        # LoRA 保持 FP16
        self.lora_A = nn.Parameter(torch.randn(d_in, r) * 0.01)
        self.lora_B = nn.Parameter(torch.zeros(r, d_out))
    
    def forward(self, x):
        # 量化层前向
        result = self.original_layer(x)
        
        # LoRA 增量 (FP16)
        lora_out = (x.half() @ self.lora_A @ self.lora_B) * self.scaling
        
        return result + lora_out

# QLoRA 显存对比
# LoRA (FP16): ~14 GB (7B 模型)
# QLoRA (INT4): ~4 GB (7B 模型)
# 节省: ~70%

七、完整训练脚本

import torch
import torch.npu
from transformers import AutoTokenizer, AutoModelForCausalLM

def main():
    # 1. 加载模型
    model = AutoModelForCausalLM.from_pretrained(
        "meta-llama/Llama-2-7b-hf",
        torch_dtype=torch.float16
    )
    
    # 2. 注入 LoRA
    model = inject_lora(
        model,
        target_modules=['q_proj', 'v_proj', 'k_proj', 'o_proj'],
        r=16,
        alpha=32
    )
    
    # 3. 移到 NPU
    model = model.npu()
    
    # 4. 加载数据
    dataset = load_dataset("your_dataset")
    
    # 5. 训练
    trained_model = train_lora(
        model,
        dataset,
        num_epochs=3,
        lr=2e-4
    )
    
    # 6. 合并权重
    merged_model = merge_lora_weights(trained_model)
    
    # 7. 保存
    merged_model.save_pretrained("./lora_merged_model")
    
    # 8. 推理测试
    tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-hf")
    inputs = tokenizer("Hello, how are you?", return_tensors="pt").to("npu:0")
    
    outputs = merged_model.generate(**inputs, max_new_tokens=50)
    print(tokenizer.decode(outputs[0]))

if __name__ == "__main__":
    main()

八、常见问题

问题 原因 解决方案
微调效果差 rank 太小 增大 rank 到 16-32
训练不稳定 学习率太高 降低到 1e-4
显存 OOM batch size 太大 减小 batch 或用 QLoRA
推理速度慢 未合并权重 推理前合并 LoRA
多任务切换慢 频繁加载/卸载 使用 MultiLoRA 管理

相关仓库

  • torch_npu - 昇腾 LoRA 支持 https://atomgit.com/cann/ops-nn
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐