Tôi đã dành 3 tháng nghiên cứu và thực chiến việc fine-tuning các mô hình Qwen 3 trên GPU consumer, từ RTX 3060 12GB đến RTX 4090 24GB. Trong bài viết này, tôi sẽ chia sẻ kinh nghiệm thực tế về việc chọn giữa LoRA và QLoRA, so sánh chi phí thực tế, và những bài học xương máu khi triển khai trên phần cứng giới hạn. Nếu bạn là người mới hoàn toàn không có kinh nghiệm với fine-tuning, đây chính xác là bài viết dành cho bạn.

Mở đầu: Tại sao nên fine-tune Qwen 3 thay vì dùng API?

Khi tôi bắt đầu, câu hỏi đầu tiên là: Tại sao không đơn giản gọi API từ OpenAI hoặc Anthropic? Câu trả lời nằm ở 3 lý do chính:

Qwen 3 của Alibaba là lựa chọn tuyệt vời vì:

LoRA vs QLoRA: Hiểu để chọn đúng

Đây là hai kỹ thuật fine-tuning phổ biến nhất hiện nay. Tôi sẽ giải thích bằng ngôn ngữ đơn giản nhất.

LoRA (Low-Rank Adaptation) là gì?

Hãy tưởng tượng mô hình ngôn ngữ là một thư viện khổng lồ với 7 tỷ từ (7B model). Nếu muốn thay đổi cách thư viện này hoạt động, bạn có hai cách:

LoRA hoạt động bằng cách tạo ra các ma trận nhỏ (low-rank matrices) và "dán" chúng vào các điểm quan trọng của model. Khi inference, kết quả = model gốc + adjustments từ sticky notes.

QLoRA (Quantized LoRA) là gì?

QLoRA = Quantization + LoRA. Thay vì chỉ dán sticky notes, QLoRA còn "nén" toàn bộ thư viện xuống định dạng nhỏ hơn trước khi fine-tune.

Cụ thể:

Bảng so sánh chi tiết: LoRA vs QLoRA

Tiêu chí LoRA (FP16) QLoRA (4-bit NF4)
VRAM tối thiểu 14-16GB 6-8GB
Thời gian train Nhanh hơn 20-30% Chậm hơn 1.2-1.5x
Chất lượng output Tuyệt đối, giữ nguyên 100% 99.2-99.8% (pratically identical)
File size (adapter) 50-200MB 50-200MB
GPU phù hợp RTX 3090, A6000, A100 RTX 3060, RTX 4060, M1 Mac
Độ phức tạp setup Cao hơn Thấp hơn
Chi phí điện (estimate) $2-5/session $0.8-2/session

Phù hợp / Không phù hợp với ai

Nên dùng QLoRA nếu bạn là:

Nên dùng LoRA (FP16) nếu bạn là:

Hướng dẫn từng bước: Setup môi trường

Bước 1: Cài đặt Python và conda

Tải Miniconda từ trang chính thức. Sau đó tạo environment mới:

# Tải Miniconda
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
bash Miniconda3-latest-Linux-x86_64.sh -b -p ~/miniconda

Khởi tạo conda

source ~/miniconda/etc/profile.d/conda.sh

Tạo environment mới với Python 3.11

conda create -n qwen-finetune python=3.11 -y conda activate qwen-finetune

Cài đặt PyTorch với CUDA 12.1

pip install torch==2.2.0 torchvision==0.17.0 --index-url https://download.pytorch.org/whl/cu121

Verify CUDA

python -c "import torch; print(f'CUDA available: {torch.cuda.is_available()}'); print(f'GPU: {torch.cuda.get_device_name(0) if torch.cuda.is_available() else None}')"

Bước 2: Cài đặt các thư viện cần thiết

# Cài đặt transformers và các dependencies
pip install transformers==4.39.0
pip install accelerate==0.27.0
pip install peft==0.8.2
pip install bitsandbytes==0.41.3
pip install datasets==2.16.1
pip install trl==0.7.10
pip install tensorboard==2.16.0

Cài đặt flash-attn (tăng tốc attention)

pip install flash-attn --no-build-isolation

Verify tất cả packages

python -c " import transformers import accelerate import peft import datasets import trl print(f'Transformers: {transformers.__version__}') print(f'Accelerate: {accelerate.__version__}') print(f'PEFT: {peft.__version__}') print(f'Datasets: {datasets.__version__}') print(f'TRL: {trl.__version__}') "

Bước 3: Chuẩn bị dataset

Tôi khuyên bạn nên bắt đầu với dataset nhỏ để test pipeline trước. Format chuẩn cho instruction fine-tuning:

[
  {
    "instruction": "Giải thích khái niệm machine learning",
    "input": "",
    "output": "Machine learning là một nhánh của trí tuệ nhân tạo..."
  },
  {
    "instruction": "Viết code Python sắp xếp mảng",
    "input": "Mảng: [5, 2, 8, 1, 9]",
    "output": "``python\narr = [5, 2, 8, 1, 9]\narr_sorted = sorted(arr)\nprint(arr_sorted)  # Output: [1, 2, 5, 8, 9]\n``"
  }
]

Lưu file này thành train.json. Dataset nên có ít nhất 500-1000 samples để có kết quả meaningful.

Code thực chiến: Training script

Script QLoRA Training (cho GPU 6-8GB)

"""
QLoRA Fine-tuning Qwen 3 cho GPU consumer
Chạy được trên RTX 3060 12GB hoặc RTX 4060 8GB
"""

import torch
from datasets import load_dataset
from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    BitsAndBytesConfig,
    TrainingArguments,
    Trainer,
    DataCollatorForLanguageModeling
)
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
import os

Cấu hình

MODEL_NAME = "Qwen/Qwen2.5-7B-Instruct" # Hoặc Qwen/Qwen2.5-3B-Instruct cho GPU yếu hơn OUTPUT_DIR = "./qwen-qlora-finetuned" DATASET_PATH = "./train.json"

Bật gradient checkpointing tiết kiệm RAM

os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "max_split_size_mb:512"

===== QUAN TRỌNG: Cấu hình QLoRA 4-bit =====

bnb_config = BitsAndBytesConfig( load_in_4bit=True, # Quantize xuống 4-bit bnb_4bit_quant_type="nf4", # Normal Float 4-bit (tốt hơn fp4) bnb_4bit_compute_dtype=torch.float16, bnb_4bit_use_double_quant=True, # Nested quantization, tiết kiệm thêm 0.4 bit )

Load model với quantization

print("Đang tải model...") model = AutoModelForCausalLM.from_pretrained( MODEL_NAME, quantization_config=bnb_config, device_map="auto", trust_remote_code=True, )

Chuẩn bị model cho kbit training

model = prepare_model_for_kbit_training(model)

Load tokenizer

tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True) tokenizer.pad_token = tokenizer.eos_token

===== Cấu hình LoRA =====

lora_config = LoraConfig( r=16, # Rank - cao hơn cho quality tốt hơn (8-64, mặc định 8) lora_alpha=32, # Scaling factor (thường = 2*r) target_modules=[ # Modules quan trọng nhất để apply LoRA "q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj", ], lora_dropout=0.05, bias="none", task_type="CAUSAL_LM", )

Apply LoRA vào model

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

Output sẽ show: "trainable params: 83,886,080 || all params: 6,610,000,000 || trainable%: 1.268%"

===== Load và format dataset =====

def format_instruction(example): """Format dữ liệu thành chat template của Qwen""" text = f"<|im_start|>user\n{example['instruction']}{example.get('input', '')}<|im_end|>\n" text += f"<|im_start|>assistant\n{example['output']}<|im_end|>\n" return {"text": text}

Load dataset

dataset = load_dataset("json", data_files=DATASET_PATH, split="train")

Map format function

dataset = dataset.map(format_instruction, remove_columns=dataset.column_names)

Tokenize

def tokenize(example): result = tokenizer( example["text"], truncation=True, max_length=512, padding="max_length", ) result["labels"] = result["input_ids"].copy() return result dataset = dataset.map(tokenize, batched=False, remove_columns=["text"])

Split train/validation

dataset = dataset.train_test_split(test_size=0.1) train_dataset = dataset["train"] eval_dataset = dataset["test"] print(f"Train samples: {len(train_dataset)}") print(f"Eval samples: {len(eval_dataset)}")

===== Training Arguments =====

training_args = TrainingArguments( output_dir=OUTPUT_DIR, num_train_epochs=3, per_device_train_batch_size=2, # RTX 3060: 2, RTX 4090: 4 gradient_accumulation_steps=4, # Effective batch size = 2*4 = 8 optim="paged_adamw_32bit", # Optimizer tối ưu cho QLoRA save_strategy="epoch", logging_steps=10, learning_rate=2e-4, weight_decay=0.001, fp16=False, # Không cần vì đã dùng 4-bit bf16=True, # Dùng BF16 nếu GPU hỗ trợ (Ampere trở lên) max_grad_norm=0.3, warmup_ratio=0.03, lr_scheduler_type="cosine", report_to="tensorboard", )

Data collator

data_collator = DataCollatorForLanguageModeling(tokenizer, mlm=False)

Khởi tạo Trainer

trainer = Trainer( model=model, args=training_args, train_dataset=train_dataset, eval_dataset=eval_dataset, data_collator=data_collator, )

Bắt đầu training!

print("\n🚀 Bắt đầu training...") trainer.train()

Save adapter cuối cùng

print("\n💾 Lưu adapter...") trainer.save_model(f"{OUTPUT_DIR}/final") print(f"✅ Hoàn tất! Model được lưu tại: {OUTPUT_DIR}/final")

Script Inference với Adapter

"""
Inference với QLoRA Adapter đã train
"""

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import PeftModel
import os

Đường dẫn model và adapter

BASE_MODEL = "Qwen/Qwen2.5-7B-Instruct" ADAPTER_PATH = "./qwen-qlora-finetuned/final" OUTPUT_DIR = "./inference_output" os.makedirs(OUTPUT_DIR, exist_ok=True)

Cấu hình quantization (phải giống lúc train)

bnb_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=torch.float16, bnb_4bit_use_double_quant=True, )

Load base model

print("Đang tải base model...") base_model = AutoModelForCausalLM.from_pretrained( BASE_MODEL, quantization_config=bnb_config, device_map="auto", trust_remote_code=True, )

Load tokenizer

tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL, trust_remote_code=True) tokenizer.pad_token = tokenizer.eos_token

Load adapter vào model

print("Đang load adapter...") model = PeftModel.from_pretrained(base_model, ADAPTER_PATH) model.eval() def generate_response(instruction: str, input_text: str = "") -> str: """Generate response từ fine-tuned model""" # Format prompt giống training prompt = f"<|im_start|>user\n{instruction}{input_text}<|im_end|>\n" if input_text: prompt = f"<|im_start|>user\n{instruction}\n{input_text}<|im_end|>\n" # Tokenize inputs = tokenizer(prompt, return_tensors="pt").to(model.device) # Generate with torch.no_grad(): outputs = model.generate( **inputs, max_new_tokens=512, temperature=0.7, top_p=0.9, do_sample=True, repetition_penalty=1.1, pad_token_id=tokenizer.pad_token_id, eos_token_id=tokenizer.eos_token_id, ) # Decode và extract response response = tokenizer.decode(outputs[0], skip_special_tokens=True) # Remove prompt từ response if "<|im_start|>user" in response: response = response.split("<|im_start|>assistant\n")[-1] if "<|im_end|>" in response: response = response.split("<|im_end|>")[0] return response.strip()

Test với vài examples

test_cases = [ { "instruction": "Giải thích khái niệm", "input": "machine learning là gì?", }, { "instruction": "Viết code Python", "input": "Hàm tính Fibonacci", } ] print("\n" + "="*60) print("🧪 Testing Fine-tuned Model") print("="*60) for i, test in enumerate(test_cases, 1): print(f"\n📝 Test {i}:") print(f" Instruction: {test['instruction']}") print(f" Input: {test['input']}") response = generate_response(test['instruction'], test['input']) print(f"\n 🤖 Response:") print(f" {response[:200]}..." if len(response) > 200 else f" {response}") print("-"*60)

Bảng so sánh chi phí thực tế theo GPU

GPU VRAM Phương pháp Batch size Thời gian train (1 epoch) Chi phí điện estimate Ghi chú
RTX 3060 12GB QLoRA 4-bit 2 45-60 phút $0.15-0.25 Entry-level, chạy được
RTX 4060 Ti 16GB QLoRA 4-bit 4 30-40 phút $0.10-0.18 Sweet spot cho beginners
RTX 4070 Super 12GB QLoRA 4-bit 3 35-45 phút $0.12-0.20 Cân bằng giá/hiệu
RTX 4090 24GB LoRA FP16 4 15-20 phút $0.20-0.35 Top choice cho professionals
RTX 4090 24GB QLoRA 4-bit 8 10-15 phút $0.08-0.15 Fastest setup

Chi phí điện tính theo giá $0.12/kWh (trung bình US). Thực tế có thể khác tùy region.

Giá và ROI: Tính toán khi nào nên fine-tune vs dùng API

Dựa trên kinh nghiệm thực tế, đây là calculation để bạn quyết định:

Scenario 1: Startup nhỏ, 1,000 inference/day

Scenario 2: Side project, 100 inference/day

Scenario 3: Heavy usage, 50,000 inference/day

Vì sao chọn HolySheep AI cho Inference sau khi Fine-tune?

Sau khi đã fine-tune model thành công, bạn cần infrastructure để deploy. Đây là lý do HolySheep AI là lựa chọn tối ưu:

Tiêu chí HolySheep AI AWS/GCP Vercel AI SDK
Chi phí inference Từ $0.42/MTok (DeepSeek) $3.5-15/MTok Tùy provider
Độ trễ trung bình <50ms (Asia-Pacific) 150-300ms 150-500ms
Hỗ trợ thanh toán WeChat/Alipay/Visa Chỉ Visa Chỉ Visa
Tín dụng miễn phí Có, khi đăng ký Không Không
Tỷ giá ¥1 = $1 $1 = $1 $1 = $1
API tương thích OpenAI-compatible Native only OpenAI-compatible

Với mức giá DeepSeek V3.2 chỉ $0.42/MTok (rẻ hơn 85% so với GPT-4.1), HolySheep AI cho phép bạn:

# Ví dụ: Tích hợp HolySheep AI sau khi fine-tune Qwen

Chỉ cần thay đổi base_url!

import openai

Cấu hình HolySheep - KHÔNG dùng api.openai.com

client = openai.OpenAI( base_url="https://api.holysheep.ai/v1", # ✅ ĐÚNG api_key="YOUR_HOLYSHEEP_API_KEY" # ✅ Thay thế bằng key của bạn )

Sử dụng như OpenAI API thông thường

response = client.chat.completions.create( model="deepseek-chat", # Hoặc "qwen-3" nếu có sẵn messages=[ {"role": "system", "content": "Bạn là trợ lý AI được fine-tune riêng cho..."}, {"role": "user", "content": "Câu hỏi của user ở đây"} ], temperature=0.7, max_tokens=500 ) print(response.choices[0].message.content)

Troubleshooting: Giải quyết vấn đề thường gặp

Qua quá trình fine-tune Qwen 3 trên nhiều GPU khác nhau, tôi đã gặp và giải quyết rất nhiều lỗi. Dưới đây là những lỗi phổ biến nhất và cách fix nhanh nhất.

Lỗi 1: CUDA Out of Memory khi load model

Mô tả: Khi chạy training script, bạn thấy lỗi "CUDA out of memory. Tried to allocate..." ngay sau khi load model.

# ❌ LỖI THƯỜNG GẶP: Không enable gradient checkpointing
model = AutoModelForCausalLM.from_pretrained(MODEL_NAME, ...)

✅ FIX: Thêm gradient checkpointing trước khi apply LoRA

model.gradient_checkpointing_enable() model = prepare_model_for_kbit_training(model)

✅ FIX 2: Giảm batch size và enable gradient checkpointing

training_args = TrainingArguments( per_device_train_batch_size=1, # Giảm từ 2 xuống 1 gradient_accumulation_steps=8, # Tăng để compensate ... )

✅ FIX 3: Dùng model nhỏ hơn

MODEL_NAME = "Qwen/Qwen2.5-3B-Instruct" # Thay vì 7B

✅ FIX 4: Xóa cache CUDA trước khi load

import torch torch.cuda.empty_cache() model = AutoModelForCausalLM.from_pretrained(MODEL_NAME, ...)

Lỗi 2: Loss không giảm hoặc tăng đột ngột

Mô tả: Training loss dao động wildy hoặc không convergence.

# ❌ LỖI THƯỜNG GẶP: Learning rate quá cao
training_args = TrainingArguments(
    learning_rate=1e-3,  # QUÁ CAO cho QLoRA
    ...
)

✅ FIX 1: Giảm learning rate

training_args = TrainingArguments( learning_rate=2e-4, # Standard cho LoRA warmup_ratio=0.03, # Warmup để tránh shock ... )

✅ FIX 2: Kiểm tra data format

def format_instruction(example): # Đảm bảo không có trailing spaces gây confusion instruction = example['instruction'].strip() output = example['output'].strip() return {"text": f"..."}

✅ FIX 3: Tăng rank nếu model underfitting

lora_config = LoraConfig( r=32, # Thử tăng từ 16 lên 32 lora_alpha=64, # = 2*r ... )

✅ FIX 4: Kiểm tra tokenizer

Đảm bảo pad_token và eos_token được set đúng

tokenizer.pad_token = tokenizer.eos_token tokenizer.pad_token_id = tokenizer.eos_token_id

Lỗi 3: Adapter weights không load đúng khi inference

Mô tả: Sau khi train xong, model output không khác gì base model.

# ❌ LỖI THƯỜNG GẶP: Load model trước, adapter sau nhưng không merge
base_model = AutoModelForCausalLM.from_pretrained(BASE_MODEL, ...)
model = PeftModel.from_pretrained(base_model, ADAPTER_PATH)

Inference mà không merge → adapter không apply!

outputs = model.generate(...) # Vẫn như base model

✅ FIX 1: Merge adapter vào base model

model = model.merge_and_unload