Danh sách bài viết

Bài 49: LoRA (Low-Rank Adaptation) — fine-tune chỉ vài % tham số

LoRA (Hu et al. 2021, arXiv 2106.09685) là phương pháp PEFT phổ biến nhất hiện nay. Thay vì update toàn bộ ma trận trọng số \( W \), LoRA học một low-rank update dưới dạng tích hai ma trận nhỏ \( \Delta W = BA \) với rank \( r \) thấp (8, 16, 32, 64). Với một layer \( 4096 \times 4096 \), full update cần ~16.7M tham số; LoRA với \( r=16 \) chỉ cần ~131K — khoảng 0.8%. Bài này đi từ ý tưởng phân rã low-rank, công thức forward kèm scaling \( \alpha / r \), cách init \( A \) Kaiming và \( B = 0 \) để adapter khởi đầu là identity, các hyperparameter quan trọng (rank, alpha, dropout, target_modules), code Python với thư viện peft, save / load / merge adapter, multi-LoRA swap runtime, các variants gần đây (LoRA+, AdaLoRA, DoRA, rsLoRA), VRAM và latency thực tế, ứng dụng cho Stable Diffusion, integration với trl SFTTrainer. Đây là nền cho bài 50 (QLoRA) và bài 52 (fine-tune SLM end-to-end).

25/05/2026
14 phút đọc
0 lượt xem
1

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).

2

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.

3

Ý 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ả.

4

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.

5

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.

6

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 \).

7

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.

8

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.
9

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.

10

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.

11

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õ.

12

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.

13

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
14

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.

15

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).

16

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.

17

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.

18

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.

19

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ả.
20

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.

21

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