· llm / fine-tuning / lora

Cách fine-tune LLM nhỏ năm 2026 (LoRA trên laptop)

Fine-tune Llama 3.1 8B QLoRA trên GPU consumer — cài đặt ghim phiên bản, cấu hình training chính xác, xuất GGUF sang Ollama, và tám trường hợp lỗi.

Bởi

2.051 từ · 11 phút đọc

Fine-tune Llama 3.1 8B trên phần cứng consumer là hoàn toàn khả thi trong năm 2026. Với Unsloth v0.1.39-beta và QLoRA 4-bit quantization, một chiếc RTX 3080 (10 GB VRAM) là đủ — cùng thao tác đó sẽ OOM trên vanilla HuggingFace PEFT. Số tham số trainable là 42M trong tổng số 8B (0.52%), và với dataset 500–1.000 ví dụ, bạn sẽ mất khoảng 20–60 phút trên Colab T4 miễn phí.

Bài viết này dành cho ai

Dành cho các developer cần model đưa ra output đúng định dạng, tuân theo phong cách riêng, hoặc xử lý một lĩnh vực hẹp tốt hơn base model. Bạn cần GPU ít nhất 6 GB VRAM, Python 3.10+, và một dataset nhỏ. Nếu bạn dùng macOS Apple Silicon mà không có discrete GPU, Unsloth chưa hỗ trợ training tính đến tháng 5 năm 2026 — inference thì vẫn chạy được, training thì không.

Yêu cầu cài đặt

Cấu hình tối thiểu (QLoRA 4-bit, Unsloth)

ModelVRAM cần thiết
Llama 3.2 3B3.5 GB
Mistral 7B5 GB
Llama 3.1 8B6 GB
Llama 3.3 70B41 GB

Bài hướng dẫn này dùng Llama 3.1 8B. Nếu VRAM của bạn dưới 6 GB, hãy xuống 3B (unsloth/Llama-3.2-3B-bnb-4bit). Nếu không có GPU, Lambda Labs cho thuê RTX 3090 với giá khoảng $0.50/giờ — đủ cho một lần training ngắn.

Nếu bạn đang cân nhắc giữa fine-tune và gọi hosted API, chi phí thực sự khi chạy đội AI agent năm 2026 phân tích TCO chi tiết bao gồm cả chi phí thuê GPU.

Cài đặt toolchain

Trình cài đặt Unsloth tự động ghim các phiên bản tương thích của transformers, trl, peftdatasets — một số bản vá nhỏ trong dải 4.x gây ra recursion error hoặc lỗi training, và installer sẽ loại chúng ra.

curl -fsSL https://unsloth.ai/install.sh | sh

Kiểm tra các phiên bản đã được resolve:

pip show unsloth peft trl transformers
# unsloth           0.1.39b0
# peft              0.19.1
# trl               0.18.2   (or 0.23+ — see §6 Troubleshooting)
# transformers      4.51.3   (or a later compatible point release)

Trường hợp lỗi: Nếu bạn cài trl riêng và nó resolve sang v0.23 trở lên, SFTConfig (từ trl) là config class được khuyến nghị cho SFTTrainer. Đoạn code training ở §3 dùng transformers.TrainingArguments — vẫn chạy được nhưng bỏ qua một số cấu hình riêng của SFT. Hãy ghim trl<0.23 hoặc cập nhật import (xem §6).

Tải base model

Checkpoint đã được quantize 4-bit có dung lượng ~5.4 GB, trong khi phiên bản 16-bit chiếm ~16 GB. Luôn dùng biến thể bnb-4bit:

from unsloth import FastLanguageModel

model, tokenizer = FastLanguageModel.from_pretrained(
    model_name="unsloth/Meta-Llama-3.1-8B-bnb-4bit",
    max_seq_length=2048,
    load_in_4bit=True,
    dtype=None,  # auto-detects BF16 on Ampere and later
)

Trên GPU 8 GB, Unsloth chạy được 2.972 token context với Llama 3.1 8B. Vanilla HuggingFace với Flash Attention 2 sẽ OOM trên cùng phần cứng đó.

Chuẩn bị dữ liệu

Định dạng

Dùng ShareGPT JSONL với danh sách conversations. Đây là định dạng mà notebook chuẩn của mlabonne sử dụng và helper train_on_responses_only của Unsloth mong đợi:

{"conversations": [{"from": "human", "value": "Summarize this contract clause in plain English: ..."}, {"from": "gpt", "value": "This clause means the vendor can ..."}]}
{"conversations": [{"from": "human", "value": "..."}, {"from": "gpt", "value": "..."}]}

Mỗi dòng là một ví dụ training. Trường from phải là "human""gpt" — các giá trị khác sẽ làm hỏng mapping của template ChatML.

Cần bao nhiêu ví dụ

Nhiệm vụTối thiểu
Phân loại (mỗi class)100–300
Trích xuất có cấu trúc200–500
Instruction-following tổng quát500–1.000
Sinh nội dung500–2.000
Domain phức tạp (pháp lý, y tế)1.000–5.000

Chất lượng quan trọng hơn số lượng. 200 ví dụ được chọn lọc kỹ luôn cho kết quả tốt hơn 2.000 ví dụ thu thập vội. Một ví dụ sai định dạng duy nhất có thể dạy model một pattern sai xuyên suốt quá trình training.

Tải dữ liệu với datasets

from datasets import load_dataset

dataset = load_dataset("json", data_files="my_data.jsonl", split="train")

Trường hợp lỗi: Một số bản vá nhỏ của datasets 4.x gây ra recursion error — installer của Unsloth sẽ tự loại chúng. Nếu bạn đang dùng môi trường cũ, hãy chạy pip show datasets và cài lại qua script của Unsloth để lấy phiên bản tương thích.

Chạy fine-tune

Gắn LoRA adapter vào rồi chạy trainer. Cấu hình này được lấy từ notebook chuẩn của mlabonne và đã được xác nhận hoạt động — điều chỉnh cho dataset nhỏ ở local.

from unsloth import FastLanguageModel, is_bfloat16_supported
from unsloth.chat_templates import get_chat_template
from trl import SFTTrainer
from transformers import TrainingArguments
from datasets import Dataset

# (assuming model, tokenizer, dataset from previous steps)

# Apply ChatML template
tokenizer = get_chat_template(tokenizer, chat_template="chatml")

def format_conversations(examples):
    texts = [
        tokenizer.apply_chat_template(conv, tokenize=False, add_generation_prompt=False)
        for conv in examples["conversations"]
    ]
    return {"text": texts}

dataset = dataset.map(format_conversations, batched=True)

# Attach LoRA adapter — trains 42M of 8B params (0.52%)
model = FastLanguageModel.get_peft_model(
    model,
    r=16,
    lora_alpha=16,
    lora_dropout=0,
    target_modules=["q_proj", "k_proj", "v_proj", "up_proj", "down_proj", "o_proj", "gate_proj"],
    use_rslora=True,                        # Rank-Stabilized LoRA — more stable at r=16
    use_gradient_checkpointing="unsloth",   # Unsloth's custom gradient checkpointing — extra VRAM savings
)

trainer = SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    train_dataset=dataset,
    dataset_text_field="text",
    max_seq_length=2048,
    packing=True,
    args=TrainingArguments(
        learning_rate=3e-4,
        per_device_train_batch_size=2,
        gradient_accumulation_steps=4,      # effective batch = 8
        num_train_epochs=1,
        optim="adamw_8bit",
        weight_decay=0.01,
        warmup_steps=10,
        fp16=not is_bfloat16_supported(),
        bf16=is_bfloat16_supported(),
        output_dir="outputs",
        logging_steps=10,
    ),
)

trainer.train()

Kết quả có thể mong đợi

Phần cứngKích thước datasetThời gian ước tính
Colab T4 (16 GB)500–2.000 ví dụ20–60 phút
Colab T4 (16 GB)100k ví dụ~47 giờ
A100 40 GB100k ví dụ~4 giờ 45 phút
RTX 3080 (10 GB)500–2.000 ví dụ30–90 phút

Lần chạy đầu tiên sẽ chậm — quá trình warmup của torch.compile mất tới 5 phút. Chỉ đo throughput sau khi vài bước đầu đã ổn định.

Trường hợp lỗi — OOM: Giảm per_device_train_batch_size xuống 1 và tăng gradient_accumulation_steps để giữ nguyên effective batch. Thử thêm maximum_memory_usage=0.5 nếu OOM xảy ra trong lúc evaluation.

Đánh giá kết quả

Theo dõi training loss trong logs. Các mức bình thường:

Giá trị lossÝ nghĩa
1.5–2.5Quá cao — model chưa học được; kiểm tra lại định dạng dữ liệu
0.5–1.0Bình thường
0.1–0.4Đang xuống thấp — một epoch thường là đủ
Gần 0.0Overfit — giảm số epoch hoặc tăng kích thước dataset

Sau khi training xong, hãy kiểm tra nhanh kết quả trước khi xuất:

FastLanguageModel.for_inference(model)

messages = [{"role": "user", "content": "Your test prompt here"}]
inputs = tokenizer.apply_chat_template(
    messages, tokenize=True, add_generation_prompt=True, return_tensors="pt"
).to("cuda")

outputs = model.generate(inputs, max_new_tokens=256, temperature=0.7)
print(tokenizer.decode(outputs[0][inputs.shape[1]:], skip_special_tokens=True))

Nếu output mạch lạc và đúng nhiệm vụ, model đã sẵn sàng để xuất. Nếu output bị lặp hoặc vô nghĩa, hãy kiểm tra chat template và đường loss trước khi kết luận training thất bại.

Quantize sang GGUF để inference local

Unsloth xuất trực tiếp sang GGUF và tự động tạo Ollama Modelfile, xử lý chat template mapping cho các họ model được hỗ trợ gồm Llama-3, Mistral, Phi-3 và Gemma. Đừng tự viết Modelfile — sai template là nguyên nhân phổ biến nhất sau khi xuất.

# Q4_K_M: good quality/size balance, ~4.5 GB output
model.save_pretrained_gguf("model_gguf", tokenizer, quantization_method="q4_k_m")

Các tùy chọn quantization:

Phương phápDung lượngChất lượng
q8_0~8 GBGần lossless — dùng để kiểm tra chất lượng
q5_k_m~5.5 GBĐiểm cân bằng tốt
q4_k_m~4.5 GBKhuyến nghị cho production

Trường hợp lỗi — OOM khi lưu: Truyền maximum_memory_usage=0.5 vào save_pretrained_gguf. Giá trị mặc định là 0.75, đủ để đẩy card 8 GB vượt ngưỡng.

Sau khi file GGUF được tạo:

# Unsloth wrote model_gguf/Modelfile — use it directly
ollama create my-fine-tuned-model -f model_gguf/Modelfile
ollama run my-fine-tuned-model

Thử với cùng prompt bạn đã dùng lúc kiểm tra kết quả. Nếu output khớp với những gì bạn thấy trong Python, việc xuất file thành công.

Để so sánh chi tiết Ollama với LM Studio — throughput, hiệu quả bộ nhớ và API compatibility — xem Ollama vs LM Studio trên Mac: cái nào dùng được hàng ngày?.

Xử lý khi gặp sự cố

OOM trong lúc training

  1. Đặt per_device_train_batch_size=1, tăng gradient_accumulation_steps để bù lại.
  2. Giảm max_seq_length xuống 1024.
  3. Đặt fp16_full_eval=True và thêm eval_accumulation_steps=4.

Loss bị kẹt ở 0 (không phải NaN — chỉ đứng ở 0.0)

Nguyên nhân: train_on_responses_only đang mask toàn bộ vì delimiter không khớp với token boundaries của Llama 3.1.

Cách sửa — dùng đúng delimiter của Llama 3.1:

from unsloth.chat_templates import train_on_responses_only

trainer = train_on_responses_only(
    trainer,
    instruction_part="<|start_header_id|>user<|end_header_id|>\n\n",
    response_part="<|start_header_id|>assistant<|end_header_id|>\n\n",
)

Nếu bạn không dùng train_on_responses_only, hãy kiểm tra JSONL có "from": "gpt" (không phải "from": "assistant") — template ShareGPT ánh xạ gpt sang vai trò assistant, không phải chuỗi literal assistant.

Loss NaN

  1. Learning rate quá cao — giảm learning_rate từ 3e-4 xuống 1e-4.
  2. JSONL sai định dạng — một dòng có cấu trúc sai (thiếu key conversations, trộn lẫn template) có thể gây NaN loss. Validate dataset: assert all("conversations" in ex for ex in dataset).
  3. Gradient explosion — thêm max_grad_norm=0.3 vào TrainingArguments (mặc định là 1.0).

Catastrophic forgetting

Triệu chứng: model chỉ sinh ra text trông giống data training của bạn, mất khả năng xử lý ngôn ngữ tổng quát.

Cách khắc phục:

  • Giảm rank: r=8 thay vì r=16.
  • Ít epoch hơn: 1 epoch thường là đủ.
  • Trộn thêm 10–20% ví dụ từ domain tổng quát như mlabonne/FineTome-100k.
  • Thêm dropout nhỏ: lora_dropout=0.05.

LoRA vốn kháng catastrophic forgetting tốt hơn full fine-tuning, nhưng không hoàn toàn miễn nhiễm khi dataset nhỏ và đồng nhất.

TRL 0.23+ — import TrainingArguments thất bại

SFTConfig (từ trl) là config class được khuyến nghị cho SFTTrainer từ TRL 0.9+. Nó xử lý các argument riêng của SFT vốn trước đây được truyền trực tiếp vào SFTTrainer.__init__. Dùng transformers.TrainingArguments vẫn chạy được nhưng bỏ qua các cấu hình đó. Nếu bạn đang dùng TRL 0.23 trở lên, hãy chuyển sang:

# trl >= 0.23
from trl import SFTConfig

args = SFTConfig(
    learning_rate=3e-4,
    per_device_train_batch_size=2,
    # ... rest of args unchanged
    output_dir="outputs",
)

Phần còn lại của trainer setup không thay đổi. Nếu bạn muốn ghim API cũ: pip install "trl<0.23".

Sai template sau khi xuất GGUF

Triệu chứng: model sinh ra lặp vô hạn hoặc vô nghĩa sau khi chạy ollama run.

Cách sửa: luôn dùng Modelfile mà Unsloth tạo ra tại model_gguf/Modelfile. Đừng tự tạo file riêng. Nếu bạn đã tự tạo một cái, hãy xóa nó đi và chạy lại save_pretrained_gguf.

Lỗi biên dịch CUDA

Đặt UNSLOTH_COMPILE_DISABLE=1 trước khi bắt đầu training. Download chậm và bị treo ở 90–95%: đặt UNSLOTH_STABLE_DOWNLOADS=1. Cả hai đều là biến môi trường — đặt trong shell hoặc ở đầu script với os.environ["UNSLOTH_COMPILE_DISABLE"] = "1".

macOS Apple Silicon

API training Python của Unsloth chưa hỗ trợ MLX (tính đến tháng 5 năm 2026). Inference trên model GGUF vẫn chạy tốt qua Ollama. Để training trên Mac dòng M, dùng Axolotl với MPS backend trực tiếp — chậm hơn và tốn RAM hơn CUDA, nhưng hoạt động được.

Tham khảo