Tôi đã thực hiện hơn 200 lần fine-tune các mô hình open-source trong 3 năm qua, từ LLaMA 2 đến Mistral, và giờ là Llama 4. Điều tôi nhận ra sớm nhất: 80% thất bại của các dự án fine-tuning đến từ khâu chuẩn bị dữ liệu, không phải từ cấu hình model hay hyperparameters.
Trong bài viết này, tôi sẽ chia sẻ toàn bộ quy trình chuẩn bị dataset cho Llama 4 LoRA fine-tuning, kèm theo code thực chiến và những lỗi phổ biến nhất mà tôi đã gặp phải.
Tại sao Chi phí Fine-tuning là Vấn đề Cấp bách
Trước khi đi vào kỹ thuật, hãy xem xét bối cảnh chi phí 2026 đã thay đổi hoàn toàn cán cân:
So sánh Chi phí API 2026 (Output Tokens)
| Model | Giá/MTok | 10M tokens/tháng |
|---|---|---|
| GPT-4.1 | $8.00 | $80 |
| Claude Sonnet 4.5 | $15.00 | $150 |
| Gemini 2.5 Flash | $2.50 | $25 |
| DeepSeek V3.2 | $0.42 | $4.20 |
| Llama 4 (Fine-tuned) | ~$0.10* | ~$1.00 |
*Chi phí ước tính khi self-host trên GPU V100, bao gồm infrastructure.
Việc fine-tune Llama 4 để tạo specialized assistant có thể tiết kiệm 85-95% chi phí so với việc sử dụng GPT-4.1 hoặc Claude cho các tác vụ lặp đi lặp lại. Đây là lý do chính khiến tôi đầu tư thời gian vào việc chuẩn bị dataset chuẩn xác.
Dataset Preparation: Từ Zero đến Hero
Bước 1: Cài đặt Môi trường
# Tạo virtual environment
python -m venv llama4_env
source llama4_env/bin/activate
Cài đặt các thư viện cần thiết
pip install transformers datasets peft accelerate bitsandbytes
pip install sentencepiece protobuf scipy
pip install wandb tensorboard
Kiểm tra GPU
nvidia-smi
python -c "import torch; print(f'CUDA: {torch.cuda.is_available()}, Device: {torch.cuda.get_device_name(0)}')"
Bước 2: Format Dataset theo ChatML
Llama 4 sử dụng ChatML format. Đây là format chuẩn mà tôi luôn tuân thủ:
import json
from datasets import Dataset
def format_conversation(messages):
"""
Format conversation theo ChatML standard cho Llama 4
messages: list of dict với keys: 'role', 'content'
"""
system_prompt = messages[0]['content'] if messages[0]['role'] == 'system' else "Bạn là một trợ lý AI hữu ích."
formatted = f"<|im_start|>system\n{system_prompt}<|im_end|>\n"
for msg in messages:
if msg['role'] != 'system':
formatted += f"<|im_start|>{msg['role']}\n{msg['content']}<|im_end|>\n"
formatted += "<|im_start|>assistant\n"
return formatted
def create_training_sample(conversation, target_response):
"""
Tạo sample cho supervised fine-tuning
Input: formatted conversation
Output: text với target_response được thêm vào
"""
return {
"text": conversation + target_response + "<|im_end|>"
}
Ví dụ thực tế
sample_messages = [
{"role": "system", "content": "Bạn là chuyên gia phân tích dữ liệu Python."},
{"role": "user", "content": "Viết code để đọc file CSV và tính mean của cột 'price'"},
{"role": "assistant", "content": "import pandas as pd\n\ndf = pd.read_csv('data.csv')\nmean_price = df['price'].mean()\nprint(f'Mean price: {mean_price}')"}
]
formatted = format_conversation(sample_messages)
print(formatted)
Bước 3: Xây dựng Dataset Class Từ Đầu
Đây là class mà tôi đã tối ưu qua nhiều dự án, xử lý được cả CSV, JSON, và parquet:
import pandas as pd
import json
from typing import List, Dict, Optional
from datasets import Dataset
class DatasetBuilder:
"""Dataset Builder cho Llama 4 LoRA Fine-tuning"""
def __init__(self,
base_url: str = "https://api.holysheep.ai/v1",
api_key: str = "YOUR_HOLYSHEEP_API_KEY"):
self.base_url = base_url
self.api_key = api_key
self.client = OpenAI(api_key=api_key, base_url=base_url)
def load_raw_data(self,
file_path: str,
file_type: str = 'json') -> List[Dict]:
"""Load data từ various file formats"""
if file_type == 'json':
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
return data if isinstance(data, list) else [data]
elif file_type == 'csv':
df = pd.read_csv(file_path)
return df.to_dict('records')
elif file_type == 'parquet':
df = pd.read_parquet(file_path)
return df.to_dict('records')
else:
raise ValueError(f"Unsupported file type: {file_type}")
def augment_with_ai(self,
samples: List[Dict],
field_to_augment: str,
num_variations: int = 3) -> List[Dict]:
"""
Sử dụng AI để tạo thêm variations cho dataset
Tiết kiệm 85%+ chi phí với HolySheep AI
"""
augmented = []
for sample in samples:
original_content = sample[field_to_augment]
variations_prompt = f"""Tạo {num_variations} biến thể khác nhau cho câu sau,
giữ nguyên ý nghĩa nhưng thay đổi cách diễn đạt:
Original: {original_content}
Format output JSON array với keys: 'variation_1', 'variation_2', ..."""
response = self.client.chat.completions.create(
model="gpt-4.1",
messages=[{"role": "user", "content": variations_prompt}],
temperature=0.7,
max_tokens=500
)
try:
variations = json.loads(response.choices[0].message.content)
for var_key, var_value in variations.items():
new_sample = sample.copy()
new_sample[field_to_augment] = var_value
new_sample['augmented'] = True
augmented.append(new_sample)
except:
augmented.append(sample)
return augmented
def build_sft_dataset(self,
conversations: List[Dict],
tokenizer,
max_length: int = 2048) -> Dataset:
"""
Build dataset cho Supervised Fine-Tuning
"""
def tokenize_function(examples):
# Format conversation
texts = [format_conversation(conv['messages']) +
conv.get('target', '') +
"<|im_end|>"
for conv in examples['conversation']]
# Tokenize với labels
model_inputs = tokenizer(
texts,
max_length=max_length,
truncation=True,
padding='max_length',
return_tensors=None
)
# Tạo labels: chỉ tính loss cho phần assistant
labels = model_inputs['input_ids'].copy()
# Mask system và user tokens (loss = -100)
for i, text in enumerate(texts):
user_part = text.split("<|im_start|>assistant\n")[0]
user_tokens = len(tokenizer(user_part)['input_ids'])
for j in range(user_tokens):
if j < max_length:
labels[i][j] = -100
model_inputs['labels'] = labels
return model_inputs
dataset = Dataset.from_list(conversations)
dataset = dataset.map(
tokenize_function,
batched=True,
remove_columns=dataset.column_names,
desc="Tokenizing dataset"
)
return dataset
def validate_dataset(self, dataset: Dataset) -> Dict:
"""Validate dataset quality"""
stats = {
'total_samples': len(dataset),
'avg_length': 0,
'max_length': 0,
'min_length': float('inf'),
'empty_samples': 0
}
total_len = 0
for sample in dataset:
length = len(sample['input_ids'])
total_len += length
stats['max_length'] = max(stats['max_length'], length)
stats['min_length'] = min(stats['min_length'], length)
if length == 0:
stats['empty_samples'] += 1
stats['avg_length'] = total_len / len(dataset) if len(dataset) > 0 else 0
return stats
Sử dụng
builder = DatasetBuilder()
raw_data = builder.load_raw_data('data/conversations.json', 'json')
print(f"Loaded {len(raw_data)} samples")
Bước 4: Cấu hình LoRA Training
from peft import LoraConfig, get_peft_model, TaskType
from transformers import TrainingArguments, Trainer
import torch
def setup_lora_config(
model,
lora_r: int = 16,
lora_alpha: int = 32,
lora_dropout: float = 0.05,
target_modules: List[str] = None
):
"""Cấu hình LoRA cho Llama 4"""
if target_modules is None:
# Llama 4 default target modules
target_modules = [
"q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"
]
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=lora_r,
lora_alpha=lora_alpha,
lora_dropout=lora_dropout,
target_modules=target_modules,
bias="none",
modules_to_save=None
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
return model
def create_training_arguments(
output_dir: str = "./llama4_lora_output",
learning_rate: float = 2e-4,
batch_size: int = 4,
gradient_accumulation_steps: int = 4,
num_train_epochs: int = 3,
warmup_ratio: float = 0.03,
logging_steps: int = 10,
save_steps: int = 100,
eval_steps: int = 100,
save_total_limit: int = 3
):
"""Tạo TrainingArguments"""
training_args = TrainingArguments(
output_dir=output_dir,
per_device_train_batch_size=batch_size,
per_device_eval_batch_size=batch_size,
gradient_accumulation_steps=gradient_accumulation_steps,
learning_rate=learning_rate,
num_train_epochs=num_train_epochs,
lr_scheduler_type="cosine",
warmup_ratio=warmup_ratio,
logging_dir="./logs",
logging_steps=logging_steps,
save_steps=save_steps,
eval_strategy="steps",
eval_steps=eval_steps,
save_total_limit=save_total_limit,
fp16=True,
dataloader_num_workers=4,
remove_unused_columns=False,
optim="adamw_torch",
report_to=["wandb"],
gradient_checkpointing=True,
gradient_checkpointing_kwargs={"use_reentrant": False}
)
return training_args
Full training pipeline
def train_llama4_lora(
model,
tokenizer,
train_dataset,
eval_dataset=None,
output_dir: str = "./llama4_finetuned"
):
"""Complete training pipeline"""
# Setup LoRA
model = setup_lora_config(model)
# Training arguments
training_args = create_training_arguments(
output_dir=output_dir,
batch_size=4,
num_train_epochs=3
)
# Initialize trainer
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
tokenizer=tokenizer,
data_collator=lambda data: {
'input_ids': torch.stack([f['input_ids'] for f in data]),
'attention_mask': torch.stack([f['attention_mask'] for f in data]),
'labels': torch.stack([f['labels'] for f in data])
}
)
# Start training
trainer.train()
# Save final model
trainer.save_model(f"{output_dir}/final")
return trainer
Bước 5: Inference với Model đã Fine-tune
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
def load_finetuned_model(
base_model_path: str = "meta-llama/Llama-4-Maverick-17B-128E",
lora_path: str = "./llama4_finetuned/final",
device: str = "cuda"
):
"""Load model đã fine-tune"""
tokenizer = AutoTokenizer.from_pretrained(base_model_path)
tokenizer.pad_token = tokenizer.eos_token
base_model = AutoModelForCausalLM.from_pretrained(
base_model_path,
torch_dtype=torch.float16,
device_map="auto",
load_in_4bit=True
)
# Merge LoRA weights
model = PeftModel.from_pretrained(base_model, lora_path)
model = model.merge_and_unload()
return model, tokenizer
def generate_response(
model,
tokenizer,
prompt: str,
system_prompt: str = "Bạn là trợ lý AI hữu ích.",
max_new_tokens: int = 512,
temperature: float = 0.7,
top_p: float = 0.9
):
"""Generate response từ fine-tuned model"""
formatted_prompt = f"<|im_start|>system\n{system_prompt}<|im_end|>\n<|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant\n"
inputs = tokenizer(formatted_prompt, return_tensors="pt").to(model.device)
outputs = model.generate(
**inputs,
max_new_tokens=max_new_tokens,
temperature=temperature,
top_p=top_p,
do_sample=True,
repetition_penalty=1.1
)
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
# Extract assistant response
response = response.split("<|im_start|>assistant\n")[-1]
response = response.replace("<|im_end|>", "").strip()
return response
Sử dụng
model, tokenizer = load_finetuned_model(
base_model_path="meta-llama/Llama-4-Maverick-17B-128E",
lora_path="./llama4_finetuned/final"
)
response = generate_response(
model, tokenizer,
prompt="Phân tích đoạn code Python sau và đề xuất cải thiện"
)
print(response)
Chiến lược Dataset Quality Assurance
Qua kinh nghiệm thực chiến, tôi áp dụng 3 tầng QA cho dataset:
1. Automated Quality Checks
import re
from collections import Counter
class DatasetQA:
"""Quality Assurance cho training dataset"""
def __init__(self):
self.issues = []
def check_length_distribution(self, dataset, min_len=10, max_len=4096):
"""Kiểm tra phân bố độ dài samples"""
lengths = [len(sample['input_ids']) for sample in dataset]
too_short = sum(1 for l in lengths if l < min_len)
too_long = sum(1 for l in lengths if l > max_len)
if too_short > len(lengths) * 0.1:
self.issues.append(f"Cảnh báo: {too_short} samples quá ngắn (<{min_len} tokens)")
if too_long > 0:
self.issues.append(f"Cảnh báo: {too_long} samples bị truncated (>{max_len} tokens)")
return {
'too_short': too_short,
'too_long': too_long,
'avg_length': sum(lengths) / len(lengths)
}
def check_special_tokens(self, texts):
"""Đảm bảo format ChatML đúng"""
required_tokens = ["<|im_start|>", "<|im_end|>"]
issues = []
for i, text in enumerate(texts):
for token in required_tokens:
if token not in text:
issues.append(f"Sample {i}: Thiếu {token}")
if issues:
self.issues.extend(issues)
return len(issues) == 0
def check_response_quality(self, conversations):
"""Kiểm tra chất lượng response"""
low_quality = []
for i, conv in enumerate(conversations):
messages = conv.get('messages', [])
assistant_msgs = [m for m in messages if m.get('role') == 'assistant']
for msg in assistant_msgs:
content = msg.get('content', '')
# Check for very short responses
if len(content) < 20:
low_quality.append((i, 'too_short'))
# Check for placeholder text
if re.search(r'(xxx|placeholder|todo|fill in)', content.lower()):
low_quality.append((i, 'placeholder'))
# Check for repetition
words = content.split()
if len(words) > 10:
word_counts = Counter(words)
most_common_ratio = word_counts.most_common(1)[0][1] / len(words)
if most_common_ratio > 0.5:
low_quality.append((i, 'repetition'))
return low_quality
def generate_report(self):
"""Generate QA report"""
report = {
'total_issues': len(self.issues),
'issues': self.issues,
'passed': len(self.issues) == 0
}
return report
Sử dụng QA
qa = DatasetQA()
length_stats = qa.check_length_distribution(dataset)
qa.check_response_quality(conversations)
report = qa.generate_report()
print(f"QA Report: {'PASSED' if report['passed'] else 'FAILED'}")
print(f"Total issues: {report['total_issues']}")
Lỗi thường gặp và cách khắc phục
Lỗi 1: CUDA Out of Memory khi Load Model
Mô tả: Khi chạy fine-tuning trên GPU có bộ nhớ hạn chế, model Llama 4 17B có thể chiếm toàn bộ VRAM.
# ❌ Sai: Load full model
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-4-Maverick-17B-128E")
✅ Đúng: Load với quantization 4-bit
from transformers import BitsAndBytesConfig
quantization_config = BitsAndBytesConfig(