Files
2026-05-16 17:16:51 +08:00

7.8 KiB
Raw Permalink Blame History

QLoRAQuantized Low-Rank Adaptation

概述

QLoRAQuantized Low-Rank Adaptation是由Tim Dettmers等人于2023年提出的高效微调方法结合了量化技术与低秩适配LoRA使得在单张48GB GPU上微调65B参数模型成为可能。

核心思想将预训练模型量化到4-bit NF4存储但在计算时反量化为BF16进行训练同时使用LoRA适配器学习任务相关知识。

NF4数据类型4-bit NormalFloat

问题背景

标准INT4量化对于正态分布的权重不是最优的因为正态分布的权重理论上应该均匀分布在量化网格上。

NF4原理

NF44-bit NormalFloat是一种针对正态分布数据优化的4位数值格式

NF4量化网格: [-1, -0.696, -0.525, -0.351, -0.175, 0, 0.175, 0.351, 0.525, 0.696, 1]
             (共11个值点用于量化16个4-bit表示)

设计原则

  • 量化边界基于正态分布的分位数确定
  • 保证每个量化值承载的信息量相等
  • 减少小值区域的量化误差

与INT4对比

INT4: 均匀分布 [-7, -5, -3, -1, 1, 3, 5, 7]对称8值
NF4:  非均匀分布,基于正态分布的分位数

PyTorch实现

# NF4 量化网格(预计算)
NF4_GRID = [-1.0, -0.69619224, -0.52507305, -0.35093744, 
            -0.17546872, 0.0, 0.17546872, 0.35093744, 
            0.52507305, 0.69619224, 1.0]

def quantize_nf4(tensor):
    # 计算每个值最近的NF4量化点
    scale = tensor.abs().max()
    tensor_normalized = tensor / scale
    quantized = torch.zeros_like(tensor, dtype=torch.uint8)
    for i, grid_val in enumerate(NF4_GRID):
        quantized[tensor_normalized == grid_val] = i
    return quantized, scale

def dequantize_nf4(quantized, scale):
    # 反量化,使用查找表
    values = torch.tensor(NF4_GRID, device=quantized.device)
    tensor_normalized = values[quantized]
    return tensor_normalized * scale

双重量化Double Quantization

问题

在QLoRA中量化权重需要存储量化参数scale、zero_point。对于NF4每个参数块block需要一个FP32 scale。

Block size = 64 参数 → 1个FP32 scale
模型参数量65B → 约1B个Block → 1B × 4 bytes = 4GB仅scale

解决方案:双重量化

对量化参数本身再进行量化:

# 第一层对模型权重进行NF4量化
W_quant, W_scale = quantize_nf4(W)  # W_scale: FP32, block_size=64

# 第二层对scale进行INT8量化
W_scale_quant, W_scale_scale = quantize_int8(W_scale)
# W_scale_scale: FP32, block_size=256更大的block因为scale分布更集中

显存节省

量化方式 scale显存
无双重量化FP32 4GB65B参数模型
双重量化INT8 scale ~0.5GB
节省 ~3.5GB

梯度分页优化器Paged Optimizer

问题

在多GPU训练时梯度累积需要存储大量优化器状态。当显存不足时通常需要CPU卸载但CPU-GPU数据传输成为瓶颈。

分页优化器原理

借鉴操作系统虚拟内存的页面调度思想:

class PagedOptimizer:
    """梯度分页优化器"""
    
    def __init__(self, model_params, optimizer_class, block_size=1024):
        self.blocks = {}  # device_id -> list of blocks
        self.block_size = block_size
        
    def step(self, gradients):
        # 检查是否需要页面调度
        for param in gradients:
            if self._need_offload(param):
                # 将不活跃的优化器状态换出到CPU
                self._page_out(self._find_inactive_block())
        
        # 正常优化器步骤
        self.optimizer.step()
        
    def _need_offload(self, param):
        # 判断是否需要换页
        return self._gpu_memory_pressure() > self._threshold

在QLoRA中的应用

# QLoRA训练配置
config = {
    "lora_r": 64,
    "lora_alpha": 16,
    "lora_dropout": 0.05,
    "weight_loader": {
        "nf4": True,
        "double_quant": True,
        "quantization_storage_type": "nf4"
    },
    "paged_optimizer": {
        "enabled": True,
        "block_size": 1024,
        "cpu_offload": True
    }
}

LoRA与量化的结合

架构设计

          输入X
            ↓
    ┌───────────────────────┐
    │   冻结的4-bit权重 W   │  ← NF4量化存储反量化后用于计算
    └───────┬───────────────┘
            ↓
    ┌───────────────────────┐
    │   添加LoRA适配器      │  ← BF16训练
    │   ΔW = A × B          │
    │   A: (in_features, r) │
    │   B: (r, out_features)│
    └───────────────────────┘
            ↓
         输出Y = X @ (W + ΔW)

量化权重前向传播

def quantized_linear(x, weight, lora_a, lora_b, scale):
    """
    x: 输入 (batch, seq, in_features)
    weight: NF4量化权重 (存储用)
    lora_a, lora_b: LoRA参数 (BF16)
    """
    # 反量化权重到BF16进行计算
    w_bf16 = dequantize_nf4(weight)  # (out_features, in_features)
    
    # 计算量化部分贡献
    y_quant = F.linear(x, w_bf16)
    
    # 计算LoRA部分贡献BF16
    y_lora = F.linear(F.linear(x, lora_a), lora_b)
    
    return y_quant + y_lora

训练流程

# QLoRA训练循环
for batch in dataloader:
    optimizer.zero_grad()
    
    # 1. 前向传播:反量化权重,计算损失
    x = batch["input_ids"]
    outputs = model(x, use_qlora=True)
    loss = criterion(outputs, batch["labels"])
    
    # 2. 反向传播只更新LoRA参数和LayerNorm
    loss.backward()
    
    # 梯度裁剪
    torch.nn.utils.clip_grad_norm_(model.lora_params(), max_norm=0.5)
    
    # 3. 优化器步骤只在GPU上更新LoRA参数
    optimizer.step()
    
    # 4. 页面调度(如需要)
    paged_optimizer.check_memory_pressure()

可训练参数占比

模型规模 总参数量 LoRA参数量r=64 训练比例
7B 7,000M ~50M7B × 64 × 2 0.7%
13B 13,000M ~50M 0.4%
65B 65,000M ~50M 0.08%

极低训练参数量使得单卡微调超大模型成为可能。

量化参数配置

NF4参数块大小

# 量化参数
BITS = 4
BLOCK_SIZE = 64  # 每64个参数共用一个scale

# 双重量化参数
DQ_BLOCK_SIZE = 256  # 每256个scale共用一个量化scale

量化存储类型

配置 存储精度 计算精度 适用场景
NF4 + BF16 NF4 BF16 标准QLoRA
INT8 + BF16 INT8 BF16 精度要求高
NF4 + FP16 NF4 FP16 硬件限制

实验结果

模型 量化精度 任务表现 单GPU显存
LLaMA-65B QLoRA (NF4) 接近FP16基线 48GB
LLaMA-33B QLoRA (NF4) 接近FP16基线 24GB
LLaMA-7B QLoRA (NF4) 超过FP16 6GB

实现框架

# transformers + peft
from peft import LoraConfig, get_peft_model, prepare_model_for_kint4_training

model = AutoModelForCausalLM.from_pretrained(
    "model_name",
    load_in_4bit=True,
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=True,
    bnb_4bit_quant_type="nf4"
)

model = prepare_model_for_kint4_training(model)
config = LoraConfig(r=64, lora_alpha=16, target_modules=["q_proj", "v_proj"])
model = get_peft_model(model, config)

注意事项

  1. 量化校准:需要代表性数据集估计量化范围
  2. 量化误差:累积误差可能导致训练不稳定
  3. LoRA秩选择过大r可能导致过拟合过小r表达能力不足
  4. 混合精度部分层如Embedding保持FP16以保证精度