LoRA
LoRA - Tinh chỉnh hạng thấp
Mô hình Llama 3 có 8 tỷ tham số. Bạn muốn fine-tune nó trên laptop GPU 8GB. Cần tối thiểu bao nhiêu tham số phải huấn luyện để đạt chất lượng gần full fine-tune?
Hình minh họa
Trực quan hóa adapter LoRA
Kéo các thanh trượt để thấy rank r, chiều d và chế độ QLoRA ảnh hưởng đến số tham số, bộ nhớ và cấu trúc ma trận.
Chế độ QLoRA (base 4-bit NF4)
Lượng tử hóa W gốc xuống 4-bit để fit model 65B trên 1 GPU.
1.07B
Tham số full FT
4.19M
Tham số LoRA (A+B)
0.3906%
Tỷ lệ trainable
99.61%
Tiết kiệm
Ước lượng VRAM cần thiết (rough)
2.15 GB
Base (fp16)
8.4 MB
Adapter fp16
33.6 MB
Optimizer Adam
2.20 GB
Tổng cộng
Chưa tính activations và gradient checkpointing — thực tế có thể tiết kiệm thêm 30-50%.
Ma trận gốc W có kích thước 4096 x 4096 (16.7M tham số). Với LoRA rank r = 8, tổng tham số của A và B là bao nhiêu?
Giải thích
LoRA (Low-Rank Adaptation) dựa trên giả thuyết rằng sự thay đổi trọng số khi fine-tune có intrinsic rank thấp. Thay vì cập nhật toàn bộ ma trận W, LoRA phân tích delta thành tích của hai ma trận mỏng:
Trong đó đóng băng, và là hai ma trận huấn luyện được, với . Hệ số kiểm soát độ lớn của đóng góp adapter — chia cho r để tổng tác động không phụ thuộc nhiều vào r.
Quy trình LoRA gồm ba bước cốt lõi:
- Đóng băng W gốc: Ma trận pre-train không bị thay đổi — bảo toàn toàn bộ kiến thức nền đã học được.
- Thêm adapter: Khởi tạo A ngẫu nhiên (Gaussian), B = 0. Tại bước 0, ΔW = BA = 0 — mô hình vẫn hoạt động hệt như cũ. Khi gradient chảy vào B, nó học hướng cần thiết.
- Gộp khi triển khai:Sau huấn luyện, tính W' = W + (α/r)·BA một lần và lưu. Không tăng chi phí inference so với mô hình gốc.
from peft import LoraConfig, get_peft_model, TaskType
from transformers import AutoModelForCausalLM
# 1. Load base model (fp16 trên GPU)
base_model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-3-8B",
torch_dtype="float16",
device_map="auto",
)
# 2. Cấu hình LoRA
config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=8, # Rank — trade-off giữa chất lượng/chi phí
lora_alpha=16, # Scaling: W' = W + (16/8)·BA = W + 2·BA
target_modules=[ # Áp dụng cho attention projections
"q_proj", "v_proj", # Tối thiểu: Q và V
# "k_proj", "o_proj" # Tùy chọn: K và O cho biểu diễn mạnh hơn
],
lora_dropout=0.05, # Regularization
bias="none", # Không huấn luyện bias
modules_to_save=None, # Không có module full-trainable
)
# 3. Gắn adapter
model = get_peft_model(base_model, config)
model.print_trainable_parameters()
# → trainable params: 4,194,304 || all params: 8,034,078,720
# → trainable%: 0.0522
# 4. Huấn luyện như bình thường
# trainer = Trainer(model=model, ...)
# trainer.train()
# 5. Lưu adapter (chỉ vài MB, không phải 14GB mô hình gốc!)
model.save_pretrained("./my-lora-adapter")
# 6. Nạp lại khi inference
from peft import PeftModel
base = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3-8B")
model = PeftModel.from_pretrained(base, "./my-lora-adapter")
# 7. (Tùy chọn) Gộp để tăng tốc inference
model = model.merge_and_unload() # W' = W + BA, xoá adapterGiải thích
LoRA vs QLoRA: khi nào dùng cái nào?
Nếu LoRA đã tiết kiệm 99% tham số huấn luyện, tại sao vẫn cần QLoRA? Câu trả lời: LoRA vẫn phải giữ W gốc ở fp16 trên GPU. Với mô hình 65B, bạn cần 130GB VRAM chỉ để load W₀ — vẫn quá tải cho consumer GPU. QLoRA = LoRA + quantization 4-bit để cắt 4× footprint của base model.
| Tiêu chí | LoRA | QLoRA |
|---|---|---|
| Base model | fp16 / bf16 (2 bytes/param) | 4-bit NF4 (0.5 bytes/param) |
| Adapter A, B | fp16 | fp16 (không quantize) |
| Optimizer state | Adam fp32 trên GPU | Paged Adam (offload CPU) |
| VRAM cho Llama 65B | ~130GB (8× A100) | ~48GB (1× A100) |
| Tốc độ | Nhanh nhất | Chậm hơn ~25% (dequantize on-the-fly) |
| Chất lượng | Gần full FT | 99%+ so với LoRA (NF4 hầu như không mất) |
- 4-bit NormalFloat (NF4): kiểu lượng tử hóa phù hợp phân phối chuẩn của trọng số pre-train. Hiệu quả hơn int4 đều.
- Double quantization: lượng tử hóa chính scale factor của từng block, tiết kiệm thêm ~0.37 bit/param.
- Paged optimizer: khi VRAM đầy, tự động offload Adam state sang RAM CPU qua NVIDIA unified memory — tránh OOM khi gặp peak activation.
Giải thích
Dưới đây là cấu hình QLoRA đầy đủ, sẵn sàng copy-paste. Lưu ý 3 chỗ khác biệt so với LoRA thuần: BitsAndBytesConfig, prepare_model_for_kbit_training, và optimizer paged_adamw_32bit.
import torch
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
TrainingArguments,
Trainer,
)
from peft import (
LoraConfig,
get_peft_model,
prepare_model_for_kbit_training,
TaskType,
)
# ===== 1. 4-bit quantization config =====
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4", # NormalFloat 4-bit
bnb_4bit_compute_dtype=torch.bfloat16, # Tính toán ở bf16
bnb_4bit_use_double_quant=True, # Double quantization
)
# ===== 2. Load model 4-bit =====
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-3-70B",
quantization_config=bnb_config,
device_map="auto",
trust_remote_code=True,
)
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3-70B")
# ===== 3. Chuẩn bị cho k-bit training =====
# Bật gradient checkpointing, cast layer norm về fp32,
# đóng băng mọi tham số non-LoRA
model = prepare_model_for_kbit_training(
model,
use_gradient_checkpointing=True,
)
# ===== 4. Gắn LoRA adapter =====
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=16, # Rank cao hơn LoRA thường
lora_alpha=32, # α = 2r
target_modules=[ # Tất cả linear layer trong attention
"q_proj", "k_proj",
"v_proj", "o_proj",
# "gate_proj", "up_proj", # Cũng có thể thêm MLP
# "down_proj",
],
lora_dropout=0.05,
bias="none",
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# → trainable: 40M / 70B = 0.057%
# ===== 5. Training với paged optimizer =====
args = TrainingArguments(
output_dir="./qlora-llama3-70b",
num_train_epochs=3,
per_device_train_batch_size=1,
gradient_accumulation_steps=16,
optim="paged_adamw_32bit", # Offload optimizer state
learning_rate=2e-4,
bf16=True,
logging_steps=10,
save_strategy="epoch",
warmup_ratio=0.03,
lr_scheduler_type="cosine",
)
# trainer = Trainer(model=model, args=args, ...)
# trainer.train()
# ===== 6. Lưu adapter (~100MB thay vì 140GB!) =====
model.save_pretrained("./qlora-llama3-adapter")Bạn có GPU 24GB VRAM. Muốn fine-tune Llama-3 13B (base fp16 ≈ 26GB). Cách nào hợp lý nhất?
- LoRA đóng băng W gốc, chỉ học hai ma trận hạng thấp A (r × d_in) và B (d_out × r) — giảm >99% tham số huấn luyện.
- Rank r là tham số quan trọng nhất: r = 4-16 đủ cho hầu hết tác vụ; r = 32-64 khi cần biểu diễn mạnh hơn.
- Khởi tạo B = 0, A ~ Gaussian → mô hình ban đầu hoạt động như cũ; scale α/r giúp ổn định khi đổi r.
- Sau huấn luyện gộp W' = W + (α/r)·BA — không tăng latency khi triển khai.
- QLoRA = LoRA + 4-bit NF4 base + double quant + paged optimizer → fit được model 65B trên 1 GPU 48GB.
- Cho phép multi-tenant serving: 1 base model + N adapter (vài MB/cái) thay cho N bản sao model 14GB.
Kiểm tra hiểu biết
LoRA tiết kiệm bộ nhớ bằng cách nào?