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)

ModelGiá/MTok10M 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(