Sau 20 bài lý thuyết, đã đến lúc làm tay. Bài này là capstone của Part 5: bạn sẽ thực sự fine-tune Llama-3-8B trên dataset tiếng Việt, từ setup GPU đến deploy adapter.

Tổng cost ước tính: $15-25 cho GPU rental, $0 cho phần còn lại. Thời gian: 4-6 giờ thực hành (bao gồm download model, tokenize, train, eval).

Sau bài này bạn có:

  • Một adapter LoRA file ~200MB chứa fine-tune Vietnamese
  • Hiểu rõ pipeline thực tế từ raw dataset đến trained model
  • Một template script có thể tái sử dụng cho dataset khác

Bài này không phải copy-paste. Mỗi bước có lý do, có pitfall, có cách verify. Đọc kỹ, làm theo, đừng skip.

Mental model: pipeline tổng thể

[Pretrained Llama-3-8B]
        |
        v
[1. Setup GPU cloud rental]
        |
        v
[2. Download model + Vietnamese tokenizer test]
        |
        v
[3. Load dataset VN]
        |
        v
[4. Tokenize + apply chat template]
        |
        v
[5. Configure QLoRA + SFT]
        |
        v
[6. Train 1-2 giờ]
        |
        v
[7. Eval và iterate]
        |
        v
[Output: LoRA adapter ~200MB]

Mỗi bước có thể fail. Bài này list cụ thể cách verify trước khi qua bước sau.

Phần 1: Setup GPU cloud rental

Để fine-tune Llama-3-8B với QLoRA, ta cần ~12GB VRAM. Có nhiều provider:

ProviderGPUGiá ~Setup
Vast.aiRTX 3090 24GB$0.20-0.40/hSelf-service, marketplace
RunPodA40 48GB$0.39/hSelf-service, easy
RunPodA100 40GB$1.10/hFaster
LambdaA100 40GB$1.10/hReliable, hơi chậm setup
Colab Pro+A100 40GB$50/thángTime limited, có disconnect

Cho beginner, RunPod hoặc Vast.ai là lựa chọn tốt. Chọn template “PyTorch 2.x” có sẵn CUDA, drivers.

Setup workflow (Vast.ai, similar cho RunPod):

1. Tạo account, deposit $30
2. Search GPU: filter "RTX 3090" hoặc "A40", "Storage > 100GB"
3. Pick instance giá ~$0.30/h, status "Verified"
4. "Rent" -> chọn image "pytorch/pytorch:2.1.0-cuda12.1-cudnn8-devel"
5. Wait 1-2 phút cho instance khởi động
6. SSH vào: ssh root@<ip> -p <port>
   (host và port lấy từ dashboard)

Verify GPU sau khi SSH:

nvidia-smi

Expected output: GPU listed, CUDA version, VRAM available. Nếu không, instance broken, terminate và rent cái khác.

Tip: pick instance “Verified” thay vì “Unverified”, tránh trường hợp GPU thật sự không có hoặc unstable.

Phần 2: Setup environment

Trên remote instance:

apt update && apt install -y git wget tmux htop

# Tạo workspace
mkdir -p /workspace && cd /workspace

# Setup conda/python env
python -m pip install --upgrade pip

pip install torch transformers accelerate datasets peft trl bitsandbytes wandb sentencepiece protobuf

Verify:

python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name())"

Expected: True RTX 3090 (hoặc tương đương).

Pip install xong, tạo tmux session để training không bị mất khi SSH disconnect:

tmux new -s train

Sau này, attach lại bằng tmux attach -t train.

Phần 3: Download model và dataset

Llama-3-8B là gated model. Cần HuggingFace account và accept license:

huggingface-cli login
# Paste token từ huggingface.co/settings/tokens

Sau đó download bằng Python:

from transformers import AutoModelForCausalLM, AutoTokenizer
import torch

model_id = "meta-llama/Meta-Llama-3-8B"

tokenizer = AutoTokenizer.from_pretrained(model_id)
if tokenizer.pad_token is None:
    tokenizer.pad_token = tokenizer.eos_token

model = AutoModelForCausalLM.from_pretrained(
    model_id,
    torch_dtype=torch.bfloat16,
)

Download mất 5-10 phút tùy bandwidth. Output ~16GB.

Test tokenizer với tiếng Việt:

text = "Xin chào, hôm nay trời đẹp quá!"
tokens = tokenizer.tokenize(text)
print(tokens)
print(f"Số token: {len(tokens)}")

Llama-3 tokenizer dùng tiktoken base, support tiếng Việt khá tốt (mỗi từ 1-2 tokens). Tốt hơn nhiều Llama-2 (mỗi chữ cái thường thành 1 token).

Dataset: dùng dataset VN public từ HuggingFace.

Một dataset open source phổ biến cho instruction VN: vilm/vi-alpaca (~50K samples, Alpaca format tiếng Việt). Hoặc bkai-foundation-models/vi-alpaca-train (~80K).

from datasets import load_dataset

dataset = load_dataset("vilm/vi-alpaca", split="train")
print(dataset[0])

Output mẫu:

{
  "instruction": "Hãy viết một đoạn văn ngắn về Hà Nội.",
  "input": "",
  "output": "Hà Nội là thủ đô của Việt Nam, một thành phố cổ kính với hơn nghìn năm lịch sử..."
}

Filter các sample quá dài hoặc quá ngắn:

def filter_length(example):
    text_len = len(example["instruction"]) + len(example.get("input", "")) + len(example["output"])
    return 50 < text_len < 3000

dataset = dataset.filter(filter_length)
print(f"After filter: {len(dataset)} samples")

Subset nhỏ để chạy nhanh lần đầu:

dataset = dataset.shuffle(seed=42).select(range(5000))

5000 sample đủ để verify pipeline. Train production nên dùng 30K+.

Phần 4: Tokenize và format

Convert Alpaca format sang chat template:

def to_chat_format(example):
    instruction = example["instruction"]
    inp = example.get("input", "").strip()
    if inp:
        user_msg = f"{instruction}\n\n{inp}"
    else:
        user_msg = instruction

    return {
        "messages": [
            {"role": "user", "content": user_msg},
            {"role": "assistant", "content": example["output"]},
        ]
    }

dataset = dataset.map(to_chat_format).remove_columns(["instruction", "input", "output"])

Verify:

print(dataset[0])
print(tokenizer.apply_chat_template(dataset[0]["messages"], tokenize=False))

Expected output: full template với <|begin_of_text|>, <|start_header_id|>user, etc.

Phần 5: Configure QLoRA

Setup QLoRA dùng bitsandbytes cho 4-bit và peft cho LoRA:

from transformers import BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training

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",
)
model.config.use_cache = False
model.gradient_checkpointing_enable()
model = prepare_model_for_kbit_training(model)

lora_config = LoraConfig(
    r=32,
    lora_alpha=64,
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
                    "gate_proj", "up_proj", "down_proj"],
)

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

Expected output:

trainable params: 83,886,080 || all params: 8,116,553,728 || trainable%: 1.0335

83M trainable, 8B total. 1% trainable.

Verify GPU memory:

nvidia-smi --query-gpu=memory.used,memory.total --format=csv

Expected: ~7-9GB used (Llama-3-8B NF4 + adapter + activation buffer).

Phần 6: Configure trainer và train

Setup SFTTrainer:

from trl import SFTTrainer, SFTConfig

training_config = SFTConfig(
    output_dir="/workspace/llama3-vi-sft",
    per_device_train_batch_size=2,
    gradient_accumulation_steps=8,
    num_train_epochs=2,
    learning_rate=2e-4,
    warmup_ratio=0.03,
    lr_scheduler_type="cosine",
    bf16=True,
    logging_steps=20,
    save_steps=200,
    save_total_limit=2,
    max_seq_length=2048,
    packing=True,
    report_to="none",
)

trainer = SFTTrainer(
    model=model,
    train_dataset=dataset,
    args=training_config,
    tokenizer=tokenizer,
)

trainer.train()

Tham số quan trọng:

  • per_device_train_batch_size=2: 4-bit NF4 còn tốn memory, batch lớn dễ OOM.
  • gradient_accumulation_steps=8: effective batch = 2 * 8 = 16 samples / step. Tốt cho gradient stability.
  • num_train_epochs=2: 1 epoch là minimum, 2 thường tốt nhất. Quá nhiều dễ overfit.
  • learning_rate=2e-4: cao điển hình cho LoRA. Full FT thường 1e-5.
  • packing=True: gộp nhiều short example vào 1 sequence dài. Tăng throughput 2-3x.

Training output mẫu:

{'loss': 1.8421, 'grad_norm': 0.523, 'learning_rate': 0.00018, 'epoch': 0.05}
{'loss': 1.5234, 'grad_norm': 0.412, 'learning_rate': 0.00019, 'epoch': 0.16}
{'loss': 1.3851, 'grad_norm': 0.367, 'learning_rate': 0.00018, 'epoch': 0.32}
{'loss': 1.2745, 'grad_norm': 0.341, 'learning_rate': 0.00016, 'epoch': 0.55}
...
{'loss': 0.9821, 'grad_norm': 0.298, 'learning_rate': 0.00002, 'epoch': 1.95}

Pattern healthy: loss giảm smooth, grad norm < 1.0, learning rate warmup rồi cosine decay.

Wall-clock estimate:

  • 5000 samples * 2 epochs = 10000 samples
  • Effective batch 16 => 625 steps
  • RTX 3090: ~5 sec/step
  • Tổng: ~52 phút

Trên A40 hoặc A100, nhanh hơn 1.5-2x.

Cost: 1h * $0.30/h = $0.30 cho 5K samples. 50K samples ~$3. 200K samples ~$12. Vẫn under $20.

Phần 7: Save và test adapter

Sau training:

trainer.save_model("/workspace/llama3-vi-sft/final")

Adapter folder ~250MB. Có thể download về local hoặc upload lên HuggingFace Hub.

Test:

from peft import PeftModel

base_model = AutoModelForCausalLM.from_pretrained(
    model_id,
    quantization_config=bnb_config,
    device_map="auto",
)
model = PeftModel.from_pretrained(base_model, "/workspace/llama3-vi-sft/final")

def chat(user_msg):
    messages = [{"role": "user", "content": user_msg}]
    text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
    inputs = tokenizer(text, return_tensors="pt").to("cuda")
    outputs = model.generate(**inputs, max_new_tokens=300, do_sample=True, temperature=0.7, top_p=0.9)
    return tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)

print(chat("Viết một bài thơ ngắn về mùa thu Hà Nội"))
print("---")
print(chat("Giải thích blockchain bằng tiếng Việt đơn giản"))
print("---")
print(chat("Hai cộng hai bằng mấy?"))

Compare với base model (chưa fine-tune):

base_only = AutoModelForCausalLM.from_pretrained(model_id, quantization_config=bnb_config, device_map="auto")
# Test same prompts với base_only, compare output

Expected difference:

  • Base model thường mix English/Vietnamese, hoặc continue text style (không answer).
  • Fine-tuned model trả lời thuần tiếng Việt, follow instruction.

Nếu fine-tuned model output kém hơn base, có vấn đề:

  1. Overfit (learning rate quá cao, epochs quá nhiều)
  2. Dataset quality kém
  3. Target modules sai

Phần 8: Pitfall thực tế

Tôi từng chạy pipeline này trên một dataset 10K VN samples, kết quả model “quên” English. Khi hỏi tiếng Anh, generate broken English mixed Vietnamese.

Lý do: catastrophic forgetting. Fine-tune mạnh trên một ngôn ngữ làm degrade ngôn ngữ khác. Đây là vấn đề kinh điển của continual learning.

Fix:

  1. Mix dataset: trộn 30% English instruction vào dataset VN. Llama-3-8B-Instruct có một template phong phú, học VN không cần overwrite English.
  2. Lower learning rate: 1e-4 thay vì 2e-4. Slower adaptation, less forgetting.
  3. Lower LoRA rank: r=16 thay vì r=32. Adapter nhỏ hơn, ít overwrite hơn.
  4. Higher beta nếu DPO (next step): keep model gần reference hơn.

Pattern: forgetting tăng tỷ lệ với “magnitude của update”. Giảm magnitude là giảm forgetting, đổi lại là kết quả VN cũng nhẹ hơn. Trade-off.

Phần 9: Iteration plan

Lần chạy đầu thường không hoàn hảo. Plan iterate:

Iteration 1 (5K samples, 2 epochs):
  Verify: pipeline chạy không lỗi, loss giảm, output ok
  Khoảng 1 giờ, $0.30

Iteration 2 (filter + 20K samples, 2 epochs):
  Verify: quality better, không catastrophic forget
  ~3 giờ, $1

Iteration 3 (50K samples, mixed VN + EN, 1 epoch):
  Production-ish quality
  ~5 giờ, $2

Iteration 4 (DPO trên preference data VN):
  Polish quality cuối
  ~2 giờ, $0.60

Tổng: ~10 giờ training, ~$5-10 GPU

Hoặc nếu thuê A100 cho tốc độ:

  • A100 40GB: $1.10/h
  • Pipeline cùng vậy, nhanh hơn 2x
  • Tổng: $5-6 GPU

Đó là lý do title bài là “$20 GPU”. Có buffer cho mistake.

Phần 10: Deploy adapter

Có 2 cách deploy:

A. Keep adapter tách (flexible):

base = AutoModelForCausalLM.from_pretrained(model_id, torch_dtype=torch.bfloat16, device_map="auto")
model = PeftModel.from_pretrained(base, "/path/to/adapter")

Memory cao hơn (load base BF16), nhưng có thể swap adapter cho task khác.

B. Merge và deploy (single artifact):

model = AutoModelForCausalLM.from_pretrained(model_id, torch_dtype=torch.bfloat16)
model = PeftModel.from_pretrained(model, "/path/to/adapter")
model = model.merge_and_unload()
model.save_pretrained("/path/to/merged-model")
tokenizer.save_pretrained("/path/to/merged-model")

Output: merged model ~16GB. Serve bằng vllm, llama.cpp, hoặc ollama (sau khi convert GGUF). Sẽ bàn ở Part 6.

Cheatsheet: full script tóm tắt

BướcCommand / Code
Setup GPURunPod / Vast.ai instance + tmux
Installpip install torch transformers peft trl bitsandbytes datasets accelerate
Login HFhuggingface-cli login
Load modelAutoModelForCausalLM.from_pretrained(..., quantization_config=bnb)
Load datasetload_dataset("vilm/vi-alpaca")
Formatapply_chat_template()
LoRA configLoraConfig(r=32, target_modules="all-linear")
TrainSFTTrainer(...).train()
Savetrainer.save_model("./final")
TestPeftModel.from_pretrained(base, "./final")
Mergemodel.merge_and_unload()
Budget
GPU rental (RTX 3090 1-2h training): $0.50-1
GPU rental (test + iterate): $3-5
Optional: HuggingFace Hub storage (free for public)
Total: under $20 cho full pipeline

Lời kết

Đây là kết thúc Part 5. Bạn đã đi từ “training là gì” (bài 14) đến “có model fine-tune VN” (bài 21). Trên đường có scaling laws, mixed precision, distributed training, LoRA, QLoRA, SFT, DPO. Mỗi khái niệm rời rạc tự nó khó tiếp cận, nhưng đặt liên tiếp thì pipeline trở nên tự nhiên.

Hands-on cuối: thực sự làm bài này từng bước. Đừng đọc xong và đóng tab. Setup GPU, chạy script, save adapter của riêng bạn. Khoảnh khắc khi adapter đầu tiên generate đoạn văn tiếng Việt theo style của dataset bạn, là khoảnh khắc bạn từ “đọc về LLM” thành “biết train LLM”. Khác biệt là vô giá.

Một số mở rộng đáng làm:

  1. Thay dataset VN bằng dataset domain-specific bạn quan tâm (code, legal, medical, finance). Process tương tự, chỉ khác data.
  2. Continue pretrain trước SFT: nếu domain corpus đủ lớn (1B+ tokens), train một stage continue pretrain trước SFT. Cải thiện quality đáng kể.
  3. DPO trên adapter sau SFT: tạo 1K-5K cặp preference (chosen, rejected) cho domain của bạn, train DPO 1 epoch. Polish quality cuối.
  4. Deploy lên Ollama hoặc llama.cpp local: convert merged model sang GGUF (quantize Q4_K_M), serve trên laptop. Sẽ học chi tiết ở Part 6.

Part 6 sẽ vào inference và production: quantization (INT8, INT4, NF4, BitNet), inference frameworks (vLLM, llama.cpp, Ollama), KV cache, PagedAttention, RAG, agent. Đây là phần làm cho model “useful trong app thật”, không chỉ trong Jupyter notebook. Bài 22 mở đầu với quantization advanced.

Hẹn gặp ở Part 6.