Chuyển đến nội dung chính

第 12 課:PEFT — LoRA、QLoRA 和適配器方法

學習使用 LoRA 和 QLoRA 進行參數高效微調 (PEFT),這種技術只需使用消費性 GPU 即可對包含數十億個參數的 LLM 進行微調。掌握 LoRA 數學、4 位元 BitsAndBytes 配置,並比較方法之間的效率。

🧠 人工智慧與機器學習 — 第 11 課 第 12 課:PEFT — LoRA、QLoRA 和適配器 方法

人工智慧和法學碩士:從基礎到高級

第 3 部分:法學碩士培訓和微調

亞洲開發網

第 12 課:PEFT — LoRA、QLoRA 和適配器方法

1. Full Fine-Tuning 的問題

微調大型法學碩士的整個參數帶來了巨大的挑戰:

需要記憶體

型號參數數顯存 (FP32)顯存 (BF16)顯存 + Adam (BF16)
GPT-2117M0.5GB0.25 GB0.25 GB 〜1 GB
LLaMA-7B7B28GB14GB〜56 GB
LLaMA-13B13B13B 52GB26GB〜104 GB
LLaMA-70B70B70B 280GB140 GB140 GB 〜560 GB

Adam 優化器為每個參數保存動量和方差——模型權重記憶體的 3 倍。 LLaMA-7B 需要 4× A100 80GB 才能進行全面微調!

其他問題

  • 災難性遺忘:模型「忘記」預訓練知識
  • 儲存:每個微調版本 = 完整模型副本(7B 為 14GB)
  • 花費:一次訓練數千美元

2. 參數高效率微調 (PEFT) 概述

PEFT 是一系列技術,僅更新參數的小子集,其餘部分保持不變:

Full Fine-Tuning:  cập nhật 100% params  → VRAM cao, chậm, tốn kém
PEFT:              cập nhật 0.1% - 5%    → VRAM thấp, nhanh, tiết kiệm

主要PEFT方法:

  1. Adapter Layers:在transformer圖層之間新增小圖層
  2. 前綴調整:將可訓練的標記加入到序列的開頭
  3. Prompt Tuning:僅微調軟提示嵌入
  4. LoRA:將權重更新矩陣分解為兩個低秩矩陣的乘積

3. LoRA:低階適應

LoRA 的數學

核心思想:微調中的權重更新往往排名較低。

而不是直接學習:

W_new = W_original + ΔW   (ΔW có kích thước d×d, rất lớn)

LoRA 將 ΔW 分解為:

ΔW = B × A

其中:

  • W_original ε ℝ^(d×d):凍結,不更新
  • A ε ℝ^(r×d):可訓練、隨機高斯初始化
  • B ε ℝ^(d×r):可訓練,初始化為0(使得最初ΔW = 0)
  • r 是等級(通常為4-64),r << d
Forward pass: h = W_original × x + (B × A) × x × (alpha/r)

參數的好處:

d = 4096 (LLaMA-7B hidden dim)

Full ΔW:   4096 × 4096 = 16,777,216 tham số
LoRA r=16: (4096×16) + (16×4096) = 131,072 tham số
→ Giảm 128 lần!

排名和阿爾法

lora_config = LoraConfig(
    r=16,          # Rank: càng cao → càng nhiều params nhưng expressive hơn
    lora_alpha=32, # Scaling factor: alpha/r = scale của LoRA
    # Thường đặt alpha = 2*r hoặc alpha = r
    lora_dropout=0.05,  # Regularization
)

選擇等級說明:

排名 (r)使用案例可訓練參數的數量(7B 模型)
4任務簡單,數據少~4M (~0.06%)
8良好的平衡性~8M (~0.12%)
1616更複雜的任務約 1700 萬 (約 0.24%)
6464大域適配~67M (~0.95%)

4. 選擇目標模組

LoRA 應用於注意力機制中的線性層:

# Với LLaMA/Mistral architecture:
target_modules = [
    "q_proj",   # Query projection
    "v_proj",   # Value projection
    "k_proj",   # Key projection
    "o_proj",   # Output projection
    # Tùy chọn thêm:
    "gate_proj", # MLP gate
    "up_proj",   # MLP up
    "down_proj", # MLP down
]

策略:

# Minimal (nhanh, ít VRAM):
target_modules = ["q_proj", "v_proj"]

# Standard (cân bằng tốt):
target_modules = ["q_proj", "k_proj", "v_proj", "o_proj"]

# Full attention + MLP (tốt nhất, nhưng chậm hơn):
target_modules = ["q_proj", "k_proj", "v_proj", "o_proj",
                  "gate_proj", "up_proj", "down_proj"]

5. QLoRA:4 位元量化 + LoRA

QLoRA(Dettmers 等人,2023)結合了兩種技術:

  1. NF4(NormalFloat 4 位):將模型權重量化為 4 位
  2. 雙量化:對兩個量化常數進行量化
  3. 分頁優化器:具有CPU卸載的優化器記憶體管理

為什麼 NF4 比 INT4 更好?

NF4 專為模型權重的常態分佈設計:

  • LLM權重呈常態分佈(高斯)
  • NF4 在高密度區域分配更多垃圾箱
  • 與 INT4 相比,量化誤差顯著降低
FP16 → NF4: giảm 75% bộ nhớ, chất lượng gần như giữ nguyên

VRAM 比較 (LLaMA-7B)

方法顯存訓練品質
完整金融時報 (FP16)〜56 GB基線
洛拉 (FP16)〜28 GB~98% 基線
QLoRA (NF4)〜10 GB~97% 基線

6. BitsAndBytes:載入模型 4 位

import torch
from transformers import AutoModelForCausalLM, BitsAndBytesConfig

# Cấu hình 4-bit quantization
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,              # Bật 4-bit loading
    bnb_4bit_quant_type="nf4",      # Dùng NF4 (tốt hơn fp4)
    bnb_4bit_compute_dtype=torch.bfloat16,  # Dtype cho compute
    bnb_4bit_use_double_quant=True, # Double quantization (tiết kiệm thêm ~0.4 bit/param)
)

model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Meta-Llama-3-8B",
    quantization_config=bnb_config,
    device_map="auto",  # Tự động phân bổ layers lên GPU/CPU
)

# Kiểm tra bộ nhớ
print(f"Model footprint: {model.get_memory_footprint() / 1e9:.2f} GB")
# → ~4.5 GB thay vì ~16 GB (BF16)

7. 前綴調優與提示調優

及時調整

只需微調 軟提示嵌入 (虛擬嵌入標記將添加到輸入中):

from peft import PromptTuningConfig, TaskType, get_peft_model

config = PromptTuningConfig(
    task_type=TaskType.CAUSAL_LM,
    num_virtual_tokens=20,  # Số soft prompt tokens
    tokenizer_name_or_path="gpt2",
)
model = get_peft_model(model, config)
# trainable params: 15,360 / 124,475,648 → 0.01%!

前綴調整

將可訓練的「前綴」加到每個注意力層的鍵和值:

from peft import PrefixTuningConfig

config = PrefixTuningConfig(
    task_type=TaskType.CAUSAL_LM,
    num_virtual_tokens=30,
    encoder_hidden_size=512,  # MLP để tạo prefix
)

在許多任務中,前綴調優比提示調優好,但比 LoRA 差。當需要很少的可訓練參數時使用。


8. 完整程式碼:QLoRA 使用 PEFT + TRL 微調 LLaMA

import torch
from datasets import load_dataset
from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    BitsAndBytesConfig,
)
from peft import (
    LoraConfig,
    get_peft_model,
    prepare_model_for_kbit_training,
    TaskType,
)
from trl import SFTTrainer, SFTConfig

# ============================================================
# 1. Cấu hình
# ============================================================
MODEL_ID = "meta-llama/Meta-Llama-3-8B"
OUTPUT_DIR = "./llama3-8b-qlora-vi"
MAX_SEQ_LENGTH = 2048

# ============================================================
# 2. Load Tokenizer
# ============================================================
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"

# ============================================================
# 3. Load Model với 4-bit Quantization
# ============================================================
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=True,
)

model = AutoModelForCausalLM.from_pretrained(
    MODEL_ID,
    quantization_config=bnb_config,
    device_map="auto",
    attn_implementation="flash_attention_2",  # Faster attention
)

# Chuẩn bị model cho k-bit training
model = prepare_model_for_kbit_training(model)

# ============================================================
# 4. Cấu hình 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=TaskType.CAUSAL_LM,
)

model = get_peft_model(model, lora_config)
model.print_trainable_parameters()

# ============================================================
# 5. Dataset: Vietnamese instruction data
# ============================================================
dataset = load_dataset("tatsu-lab/alpaca", split="train[:10000]")

def format_chat(example):
    messages = [
        {"role": "system", "content": "Bạn là trợ lý AI hữu ích, trả lời bằng tiếng Việt."},
        {"role": "user", "content": example["instruction"]
                                    + (f"\n\n{example['input']}" if example["input"] else "")},
        {"role": "assistant", "content": example["output"]},
    ]
    text = tokenizer.apply_chat_template(messages, tokenize=False)
    return {"text": text}

dataset = dataset.map(format_chat, remove_columns=dataset.column_names)

# ============================================================
# 6. Training Configuration
# ============================================================
training_args = SFTConfig(
    output_dir=OUTPUT_DIR,
    num_train_epochs=2,
    per_device_train_batch_size=2,
    gradient_accumulation_steps=8,      # Effective batch = 16
    learning_rate=2e-4,
    lr_scheduler_type="cosine",
    warmup_ratio=0.05,
    bf16=True,
    gradient_checkpointing=True,
    gradient_checkpointing_kwargs={"use_reentrant": False},
    max_seq_length=MAX_SEQ_LENGTH,
    dataset_text_field="text",
    logging_steps=10,
    save_strategy="epoch",
    optim="paged_adamw_8bit",           # Paged optimizer cho QLoRA
    report_to="tensorboard",
)

# ============================================================
# 7. Train
# ============================================================
trainer = SFTTrainer(
    model=model,
    args=training_args,
    train_dataset=dataset,
    tokenizer=tokenizer,
)

trainer.train()

# ============================================================
# 8. Lưu LoRA weights (chỉ ~80MB, không phải toàn bộ model!)
# ============================================================
trainer.save_model(OUTPUT_DIR + "/lora-weights")
tokenizer.save_pretrained(OUTPUT_DIR + "/lora-weights")

# ============================================================
# 9. Merge LoRA vào base model (cho inference)
# ============================================================
from peft import PeftModel

base_model = AutoModelForCausalLM.from_pretrained(
    MODEL_ID,
    torch_dtype=torch.bfloat16,
    device_map="auto",
)
merged_model = PeftModel.from_pretrained(base_model, OUTPUT_DIR + "/lora-weights")
merged_model = merged_model.merge_and_unload()
merged_model.save_pretrained(OUTPUT_DIR + "/merged")
print("Done! Merged model saved.")

9. 比較:Full FT、LoRA 與 QLoRA

標準全面微調洛拉 (BF16)QLoRA (NF4)
VRAM(7B 型)〜56 GB〜28 GB〜10 GB
所需 GPU4× A1002× A1001× RTX 3090
訓練速度最快快慢約 30%
品質100%(基線)~98-99%〜96-98%
檢查點尺寸14GB〜80 MB〜80 MB
易於測試困難簡單最簡單
災難性遺忘曹低低

什麼時候使用什麼?

  • 完整 FT:擁有許多 GPU、大數據集、需要最高品質
  • LoRA:A100/H100 GPU,品質與資源之間的良好平衡
  • QLoRA:消費級 GPU (RTX 3090/4090),個人研究,快速原型

總結

  • 完全微調對於常規 GPU 上的模型 7B+ 來說不切實際
  • LoRA 將權重更新分解為 B × A 低等級 — 減少 99% 以上的可訓練參數
  • QLoRA 將 NF4 量化與 LoRA 結合 — 在 2× RTX 3090 上微調 70B 模型
  • target_modules 重要:從 q_proj, v_proj,必要時展開
  • 訓練後,將 LoRA 合併到基礎模型中進行推理,無需額外開銷

下一篇文章將討論 RLHF 和對齊 — 訓練模型的過程,使其不僅遵循指令,而且安全且符合人類價值。