Mục lục
- Mục tiêu bài học
- LoRA — gốc paper
- Ý tưởng phân rã low-rank
- Công thức toán
- Tiết kiệm tham số
- Forward pass và scaling
- Khởi tạo A và B
- Hyperparameter quan trọng
- Target modules
- Code — LoraConfig và get_peft_model
- Trade-off rank
- Save adapter
- Load adapter
- Merge adapter
- Multi-LoRA và swap runtime
- LoRA merging và mixing
- Chất lượng so với full fine-tune
- VRAM khi train LoRA
- Latency inference
- Variants gần đây
- LoRA cho diffusion / vision
- trl SFTTrainer + LoRA
- Pitfall hay gặp
- Bài tập
Mục tiêu bài học
Sau bài này, bạn cần:
- Giải thích được ý tưởng low-rank update \( \Delta W = BA \) và lý do nó tiết kiệm tham số khi \( r \ll d, k \).
- Viết được công thức forward \( h = Wx + \frac{\alpha}{r} BA x \) và biết vai trò của \( \alpha \) (scaling) so với \( r \) (capacity).
- Hiểu vì sao init \( A \) Kaiming và \( B = 0 \) để adapter ban đầu không thay đổi output.
- Biết các hyperparameter chính:
r,lora_alpha,lora_dropout,target_modules. - Viết được code
LoraConfig+get_peft_model, kiểm tra số trainable params. - Save adapter (~50-200 MB), load lại trên base model, merge để inference không có overhead.
- Biết các variants gần đây (LoRA+, AdaLoRA, DoRA, rsLoRA) đang giải quyết vấn đề gì.
- Ước lượng VRAM và latency khi dùng LoRA so với full fine-tune.
Bài này dựa trên bài 48 (Full vs PEFT) và là nền trực tiếp cho bài 50 (QLoRA — kết hợp LoRA với 4-bit quantization) và bài 52 (fine-tune SLM end-to-end với trl).
LoRA — gốc paper
LoRA được Edward Hu và đồng nghiệp ở Microsoft đề xuất trong paper LoRA: Low-Rank Adaptation of Large Language Models (arXiv 2106.09685, 2021). Lý do ra đời: GPT-3 175B fine-tune full quá đắt (cần TB VRAM, hàng trăm GPU). Adapter cũ (Houlsby 2019) thêm module nối tiếp gây overhead inference. Prefix tuning khó train.
LoRA chỉ ra: update ma trận trong fine-tuning có intrinsic rank thấp — không cần update đầy đủ \( d \times k \) tham số. Học hai ma trận nhỏ tích thành update là đủ. Trong vài năm kế tiếp, LoRA trở thành PEFT method mặc định, được tích hợp vào peft, diffusers, trl và mọi training framework lớn.
Ý tưởng phân rã low-rank
Trong full fine-tuning, ta cập nhật \( W \rightarrow W + \Delta W \), với \( \Delta W \in \mathbb{R}^{d \times k} \) có cùng kích thước \( W \). LoRA giả thiết \( \Delta W \) có rank thấp và phân rã được thành tích hai ma trận:
W (frozen) ΔW = BA (trainable)
───────────── ─────────────────────
d × k d × r r × k
┌────┐ ┌──────────┐
│ B │ │ A │
│ │ └──────────┘
│ │
└────┘
r là rank, r << min(d, k)
\( W \) bị đóng băng (không train). Chỉ \( A \) và \( B \) học được. Output cuối cùng của layer trở thành \( W + BA \). Vì rank của tích \( BA \) tối đa là \( r \), update bị "ép" nằm trong không gian rank-\( r \) — chính là restriction tạo ra hiệu quả.
Công thức toán
Ma trận trọng số mới:
\[ W_{\text{new}} = W + \Delta W = W + BA \]Với:
- \( W \in \mathbb{R}^{d \times k} \): trọng số gốc, frozen.
- \( A \in \mathbb{R}^{r \times k} \): down-projection, ánh xạ \( k \) chiều xuống \( r \).
- \( B \in \mathbb{R}^{d \times r} \): up-projection, ánh xạ \( r \) chiều lên \( d \).
- \( r \ll \min(d, k) \): rank, thường 8, 16, 32, 64.
Tích \( BA \in \mathbb{R}^{d \times k} \) cùng shape với \( W \) nên cộng được trực tiếp. Rank của tích chính xác bằng \( r \) (nếu \( A, B \) full rank), nên không gian các update có thể biểu diễn được nằm trong manifold rank-\( r \) — đây là inductive bias của LoRA.
Tiết kiệm tham số
So sánh số tham số trainable:
- Full update: \( d \times k \) tham số.
- LoRA update: \( d \times r + r \times k = r(d + k) \) tham số.
Ví dụ một layer attention projection \( d = k = 4096 \) (Llama-3 8B):
Full : 4096 × 4096 = 16,777,216 (~16.7M)
LoRA r=8 : 8 × (4096 + 4096) = 65,536 (~0.4%)
LoRA r=16 : 16 × (4096 + 4096) = 131,072 (~0.8%)
LoRA r=32 : 32 × (4096 + 4096) = 262,144 (~1.6%)
LoRA r=64 : 64 × (4096 + 4096) = 524,288 (~3.1%)
Nhân với số layer × số projection áp dụng LoRA, tổng trainable params với Llama-3 8B thường nằm trong khoảng 4-40M (0.05-0.5% của 8B). Lý do quan trọng: optimizer state (Adam: 2 moment, mỗi cái cùng shape gradient) và gradient buffer cũng chỉ phải lưu cho chừng đó tham số, không phải cho toàn bộ 8B.
Forward pass và scaling
Trong thực tế, LoRA thêm một hệ số scaling \( \alpha / r \) vào nhánh update:
\[ h = W x + \frac{\alpha}{r} \, B A x \]Trong đó \( \alpha \) (thường gọi lora_alpha) là hyperparameter, không train. Vai trò:
- \( \alpha \) điều khiển độ lớn đóng góp của adapter so với base.
- Chia cho \( r \) giúp magnitude của update không đổi khi tăng \( r \) — bạn có thể thay rank mà không cần tune lại learning rate.
- Convention phổ biến: \( \alpha = 2r \) (ví dụ \( r=16, \alpha=32 \)) — nhánh LoRA scaled với hệ số 2.
Lưu ý: trong implementation PEFT, scaling áp dụng khi forward, không bake vào ma trận. Khi merge adapter (xem bước 14) thì scaling mới được áp vào để tạo \( W' = W + (\alpha/r) BA \).
Khởi tạo A và B
LoRA dùng init bất đối xứng:
- \( A \): khởi tạo Kaiming uniform (random nhỏ).
- \( B \): khởi tạo bằng zero.
Hệ quả: lúc bắt đầu train, \( \Delta W = B A = 0 \), nên \( W_{\text{new}} = W \). Model output không đổi so với base — bằng đúng base model. Đây là một điểm quan trọng cho ổn định: khi gradient bắt đầu cập nhật, \( B \) "lớn dần" từ 0, không gây cú sốc cho hành vi mạng.
Nếu init cả \( A \) và \( B \) random thì \( BA \neq 0 \) ngay từ đầu — output mạng sẽ lệch khỏi base ngay step 0, làm loss tăng vọt và training kém ổn định.
Hyperparameter quan trọng
r(rank): 8, 16, 32, 64. Cao hơn = capacity hơn, nhưng tham số nhiều hơn. Default trong nhiều recipe: 16.lora_alpha: thường \( 2r \). Vài recipe modern (rsLoRA) dùng scaling theo \( \sqrt{r} \) thay vì \( 1/r \).lora_dropout: 0.05-0.1. Dropout áp lên input của nhánh LoRA, giúp regularize trên dataset nhỏ.target_modules: list tên layer áp dụng LoRA (xem bước 9).bias:"none"(default, không train bias),"all", hoặc"lora_only".task_type:"CAUSAL_LM"cho LLM decoder-only,"SEQ_CLS"cho classification,"SEQ_2_SEQ_LM"cho encoder-decoder.
Target modules
LoRA áp dụng cho linear layer nào trong block transformer? Lựa chọn phổ biến (theo thứ tự "ít → nhiều"):
PAPER GỐC (Hu 2021)
─────────────────────────────────────────────────────────
["q_proj", "v_proj"]
Chỉ Q và V trong attention. Tham số rất ít, đủ cho GLUE / GPT-3.
MODERN RECIPE (tốt cho instruction tuning)
─────────────────────────────────────────────────────────
["q_proj", "k_proj", "v_proj", "o_proj"]
Toàn bộ attention projections. Thêm K và O cải thiện thường gặp.
FULL LINEAR (chất lượng tốt nhất, recipe của Unsloth / Axolotl)
─────────────────────────────────────────────────────────
["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"]
Toàn bộ attention + MLP (gate / up / down trong SwiGLU).
Số trainable params tăng nhưng vẫn rất nhỏ so với full FT.
Tên layer phụ thuộc kiến trúc. Llama / Mistral / Qwen dùng tên trên. Một số kiến trúc cũ dùng query, value (BERT) hoặc c_attn (GPT-2). PEFT hỗ trợ regex và "all-linear" để target toàn bộ linear tự động.
Code — LoraConfig và get_peft_model
from transformers import AutoModelForCausalLM
from peft import LoraConfig, get_peft_model
base = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-3.2-1B",
torch_dtype="bfloat16",
)
config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
)
model = get_peft_model(base, config)
model.print_trainable_parameters()
# trainable params: 4,505,600 || all params: 1,240,319,488
# || trainable%: 0.3633
Sau get_peft_model, base bị đóng băng và mỗi target_module được wrap bằng LoraLayer chứa \( A \), \( B \), scaling. Optimizer chỉ thấy tham số của LoRA, không thấy base — đó là lý do VRAM cho optimizer state giảm mạnh.
Trade-off rank
r = 4, 8 Capacity thấp. Đủ cho task đơn giản, dataset nhỏ.
Trainable ~0.1-0.2%. Nhanh, ít risk overfit.
r = 16, 32 Default range. Balance giữa capacity và param.
Phù hợp instruction tuning, domain adaptation.
r = 64, 128 Capacity cao. Cần dataset lớn, dễ overfit nhỏ hơn.
Đôi khi bắt đầu trùng quality với full FT.
r = 256+ Hiếm dùng cho LLM text. Hay gặp trong SD LoRA
khi muốn học style phức tạp.
Khuyến nghị: bắt đầu \( r = 16 \), tăng dần nếu metric chưa đủ. Nhân đôi \( r \) gấp đôi tham số nhưng không nhất thiết gấp đôi chất lượng — diminishing return rõ.
Save adapter
model.save_pretrained("./lora_adapter")
# Output:
# lora_adapter/
# adapter_config.json # config: r, alpha, target_modules
# adapter_model.safetensors # A, B của tất cả target layer
# README.md
# Size: ~10-200 MB (tùy r và số target_modules)
Chỉ A và B được lưu, không lưu base weights. Một LoRA adapter cho Llama-3 8B với \( r=16 \) và 4 target modules thường ~20-50 MB. Compare với base 8B FP16 = 16 GB. Có thể train hàng chục adapter cho nhiều task khác nhau, share chung 1 base.
Load adapter
from transformers import AutoModelForCausalLM
from peft import PeftModel
base = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-3.2-1B",
torch_dtype="bfloat16",
)
model = PeftModel.from_pretrained(base, "./lora_adapter")
model.eval()
# Inference như model bình thường
out = model.generate(...)
Khi inference, output đi qua nhánh \( W x \) (frozen) cộng với \( (\alpha/r) BA x \) (adapter). PEFT tự lo việc forward kép này. Adapter cũng có thể bật/tắt:
model.disable_adapter_layers() # tạm tắt → base behavior
model.enable_adapter_layers() # bật lại
Merge adapter
merged = model.merge_and_unload()
# merged là một model thường (không phải PeftModel)
# Trong mỗi target layer: W ← W + (α/r) * B @ A
merged.save_pretrained("./merged_model")
Sau merge, model không còn nhánh LoRA nữa — chỉ có 1 ma trận \( W' \) trong mỗi layer. Lợi:
- Inference không có overhead so với base.
- Triển khai như model thường (vLLM, llama.cpp, TGI).
- Có thể quantize (GGUF, AWQ) sau merge.
Bất lợi: mất khả năng swap adapter, mất khả năng kết hợp nhiều adapter runtime. Một workflow phổ biến: lưu cả 2 phiên bản — adapter để giữ flexibility, merged để serve sản xuất.
Multi-LoRA và swap runtime
from peft import PeftModel
model = PeftModel.from_pretrained(base, "./adapter_legal", adapter_name="legal")
model.load_adapter("./adapter_medical", adapter_name="medical")
model.load_adapter("./adapter_sql", adapter_name="sql")
# Chuyển task runtime
model.set_adapter("legal") # forward dùng adapter "legal"
out = model.generate(...)
model.set_adapter("medical") # đổi sang adapter "medical"
out = model.generate(...)
Pattern này rất tiết kiệm cho serving nhiều task: 1 base 8B chiếm 16 GB VRAM, mỗi adapter chỉ thêm vài chục MB. Có thể serve hàng chục task khác nhau trên cùng GPU. Server như vLLM, SGLang, LoRAX hỗ trợ pattern này native (load nhiều adapter, batch request theo adapter).
LoRA merging và mixing
Có thể combine nhiều adapter thành một bằng linear weight:
\[ \Delta W_{\text{mix}} = w_1 \cdot B_1 A_1 + w_2 \cdot B_2 A_2 + \dots \]PEFT cung cấp add_weighted_adapter:
model.add_weighted_adapter(
adapters=["legal", "medical"],
weights=[0.7, 0.3],
adapter_name="legal_medical_mix",
combination_type="linear",
)
model.set_adapter("legal_medical_mix")
Naive linear combine đôi khi gây interference giữa các adapter. Kỹ thuật gần đây:
- TIES-Merging (Yadav et al. 2023, arXiv 2306.01708): trim các update nhỏ, resolve sign conflict, chỉ merge phần đồng thuận.
- DARE (Yu et al. 2023, arXiv 2311.03099): drop random rồi rescale trước khi merge để giảm conflict.
Use case: tạo model "đa nhiệm" từ nhiều adapter chuyên biệt mà không cần train lại.
Chất lượng so với full fine-tune
Hu et al. 2021 báo cáo LoRA đạt ngang full fine-tune trên GLUE và một số benchmark NLU/NLG. Kinh nghiệm cộng đồng từ 2023-2025:
- Với instruction tuning và domain adaptation, LoRA \( r=16-32 \) target full linear thường gần full FT về quality, đôi khi không phân biệt được trong human eval.
- Với task đòi hỏi học kiến thức mới (continual pretraining), LoRA thua full FT rõ — nên dùng QLoRA rank lớn hoặc full FT từng layer.
- Tăng rank không phải lúc nào cũng tăng quality; đôi khi \( r=8 \) đã đủ cho task nhỏ.
- Mở rộng
target_modules(include MLP) thường có lợi hơn tăng rank trong cùng budget tham số.
Luôn cần đo bằng eval thực tế cho task của mình — không có một config "best" universal.
VRAM khi train LoRA
Thành phần VRAM khi train (Llama-3 8B, BF16, batch 1)
─────────────────────────────────────────────────────────
1. Base weights (frozen)
8B × 2 bytes = 16 GB
2. Gradient buffer
- Full FT: 16 GB
- LoRA: ~80 MB (chỉ cho A, B)
3. Optimizer state (Adam, FP32 master + 2 moment)
- Full FT: 8B × 12 bytes = 96 GB
- LoRA: ~250 MB (chỉ cho A, B)
4. Activation (phụ thuộc seq_len, batch)
~vài GB, gradient checkpoint giảm được
TỔNG (training, batch 1, seq 1024):
Full FT: ~120 GB → cần A100 80GB × 2+
LoRA: ~22 GB → 1 GPU 24-32 GB là đủ
Bài 50 (QLoRA) sẽ giảm tiếp item 1 từ 16 GB xuống ~4-5 GB bằng 4-bit quantization base, đưa total xuống ~12 GB → fit GPU consumer 16 GB.
Latency inference
LoRA thêm 2 matmul cho mỗi target layer ở mỗi forward step:
\[ x \rightarrow A x \rightarrow B (A x) \rightarrow \frac{\alpha}{r} B A x \]Cost: \( O(r(d + k)) \) thêm so với \( O(dk) \) của \( W x \). Vì \( r \ll d, k \), overhead nhỏ — đo thực tế thường 5-10% chậm hơn base trên cùng GPU.
Khi serve sản xuất:
- 1 adapter cố định: merge thành \( W' \) → 0 overhead, serve như model thường.
- Multi-LoRA serving: giữ adapter unmerged, chấp nhận 5-10% latency để đổi lấy khả năng swap. vLLM / LoRAX có optimization riêng (Punica, S-LoRA) để serve nhiều adapter song song hiệu quả.
Variants gần đây
- LoRA+ (Hayou et al. 2024, arXiv 2402.12354): dùng learning rate khác nhau cho \( A \) và \( B \) (B lớn hơn A nhiều lần). Cải thiện training speed và final quality.
- AdaLoRA (Zhang et al. 2023, arXiv 2303.10512): phân bổ rank không đều giữa các layer — layer quan trọng dùng rank cao, layer ít quan trọng dùng rank thấp. Cùng budget tham số nhưng quality tốt hơn.
- DoRA (Liu et al. 2024, arXiv 2402.09353): tách weight thành magnitude và direction, chỉ áp LoRA cho direction. Quality gần full FT hơn ở cùng rank.
- rsLoRA (Kalajdzievski 2023, arXiv 2312.03732): đổi scaling từ \( \alpha/r \) sang \( \alpha/\sqrt{r} \). Giúp tăng rank lớn (256+) mà không phá ổn định.
- VeRA, LoHa, LoKr: các phân rã thay thế (Hadamard, Kronecker) — giảm tham số hơn LoRA nguyên bản, ít phổ biến hơn nhưng có support trong
peft.
PEFT v0.10+ tích hợp hầu hết: LoraConfig(use_dora=True), LoraConfig(use_rslora=True), AdaLoraConfig, v.v. Khuyến nghị: thử LoRA chuẩn trước, tune đủ recipe, rồi mới thử variants.
LoRA cho diffusion / vision
LoRA không chỉ dùng cho LLM. Cộng đồng Stable Diffusion áp dụng LoRA rộng rãi để dạy model phong cách hoặc nhân vật mới:
- Style LoRA: train trên ~30-200 ảnh cùng phong cách, adapter ~50-150 MB.
- Character LoRA: train trên 10-50 ảnh một nhân vật / người cụ thể.
- Civitai và Hugging Face chứa hàng chục nghìn SD LoRA của cộng đồng.
from diffusers import StableDiffusionXLPipeline
pipe = StableDiffusionXLPipeline.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0",
torch_dtype="float16",
).to("cuda")
pipe.load_lora_weights("./my_style_lora")
image = pipe("a cat in 