LoRA ve QLoRA ile SLM İnce Ayarı: Tam Pratik Kılavuz
2021'de 7B parametre modelinde ince ayar yapmak, her biri 80 GB'lık 8 GPU gerektirdi ve haftalarca sürdü zamanın. QLoRA (2023) ve 2026'nın gelişmiş araçlarıyla aynı sonuç elde ediliyor 2-4 saatte 8-12 GB VRAM'e sahip tek bir tüketici GPU'su. Phi-4-mini veya Qwen 3 kendinize göre uyarlanmış etki alanı — iş belgeleri, tıbbi terminoloji, eski kod — GPT-4 sui'yi aşıyor maliyetin çok altında belirli görevler. Bu kılavuz tüm iş akışını kapsar: hazırlık veri kümesinin geliştirilmesi, QLoRA ile eğitim, değerlendirme ve dağıtım.
Ne Öğreneceksiniz
- LoRA: Düşük dereceli matris ayrıştırması nasıl çalışır?
- QLoRA: 8-12 GB GPU'lar için 4 bit nicelemeyi LoRA ile birleştirin
- Veri kümesini SFT (Denetimli İnce Ayar) için doğru formatta hazırlayın
- PEFT + TRL + Hugging Face Veri Kümeleri ile iş akışını tamamlayın
- İnce ayarlı modeli değerlendirin ve model bazında birleştirin
LoRA Nasıl Çalışır: 5 Dakikada Teori
Bir transformatör modelinin milyonlarca ağırlık matrisi vardır. Milyonlarca parametrenin tamamına ince ayar yapın güncellemek — tüketici GPU'larında imkansızdır. LoRA (Düşük Sıralı Uyarlama), şunu gözlemliyor: Önceden yönlendirilmiş bir modele uyum sağlamak için gereken güncellemeler doğası gereği düşük sıralamaya sahiptir: iki küçük matris A ve B ile yaklaşık olarak tahmin edilebilir; burada rütbe r (tipik olarak 4-64) ve orijinal boyutundan çok daha küçüktür.
# Concetto di LoRA spiegato con codice
import torch
import torch.nn as nn
class LoRALayer(nn.Module):
"""
Implementazione concettuale di uno strato LoRA.
In pratica si usa la libreria PEFT che lo fa automaticamente.
"""
def __init__(self, original_layer: nn.Linear, rank: int = 16, alpha: float = 32):
super().__init__()
d_in, d_out = original_layer.weight.shape
# Pesi originali: CONGELATI (no gradients)
self.original_weight = original_layer.weight
self.original_weight.requires_grad_(False)
# Matrici LoRA: piccole e trainabili
# A: forma (rank, d_in) - inizializzata con random gaussiana
# B: forma (d_out, rank) - inizializzata a ZERO (output = 0 all'inizio)
self.lora_A = nn.Parameter(torch.randn(rank, d_in) * 0.02)
self.lora_B = nn.Parameter(torch.zeros(d_out, rank))
# Scaling factor: alpha/rank (controlla la magnitudine dell'update)
self.scaling = alpha / rank
# Parametri trainabili: rank * (d_in + d_out) invece di d_in * d_out
# Esempio: d_in=4096, d_out=4096, rank=16
# Originale: 16,777,216 parametri
# LoRA: 16 * (4096 + 4096) = 131,072 parametri (128x meno!)
def forward(self, x: torch.Tensor) -> torch.Tensor:
# Output = Wx + (BA)x * scaling
original_out = x @ self.original_weight.T
# Update LoRA: passare per A poi B
lora_out = (x @ self.lora_A.T) @ self.lora_B.T
return original_out + self.scaling * lora_out
# In pratica: PEFT gestisce tutto questo automaticamente
# Si specificano solo rank, alpha e quali moduli applicare LoRA
QLoRA için Ortam Kurulumu
# Installazione dell'ambiente (Python 3.11, CUDA 12.4)
# pip install torch==2.5.1 torchvision --index-url https://download.pytorch.org/whl/cu124
# pip install transformers==4.47.0 peft==0.13.2 trl==0.12.2
# pip install bitsandbytes==0.44.1 datasets==3.1.0 evaluate==0.4.3
# pip install accelerate==1.2.1 wandb sentencepiece
import os
import torch
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
TrainingArguments
)
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from trl import SFTTrainer, DataCollatorForCompletionOnlyLM
from datasets import load_dataset
# Configurare wandb per il tracking (opzionale ma raccomandato)
os.environ["WANDB_PROJECT"] = "slm-fine-tuning"
# Verificare hardware
print(f"CUDA disponibile: {torch.cuda.is_available()}")
if torch.cuda.is_available():
print(f"GPU: {torch.cuda.get_device_name(0)}")
print(f"VRAM: {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB")
Veri Kümesini SFT için Hazırlama
# Il formato piu comune per SFT (Supervised Fine-Tuning) e il formato "chat"
# con messaggi strutturati in ruoli: system, user, assistant
from datasets import Dataset
import json
def prepare_sft_dataset(raw_examples: list[dict]) -> Dataset:
"""
Converte esempi grezzi nel formato chat per SFT.
raw_examples: lista di {"question": str, "answer": str, "context": str}
"""
formatted = []
for ex in raw_examples:
messages = [
{
"role": "system",
"content": "Sei un esperto di database PostgreSQL. "
"Rispondi in italiano con esempi pratici e codice SQL."
},
{
"role": "user",
"content": ex["question"]
},
{
"role": "assistant",
"content": ex["answer"]
}
]
formatted.append({"messages": messages})
return Dataset.from_list(formatted)
# Esempio di dataset per fine-tuning su dominio PostgreSQL
raw_data = [
{
"question": "Quando devo usare un partial index invece di un indice normale?",
"answer": """Usa un partial index quando:
1. Solo una piccola percentuale di righe e "attiva" nelle query
2. Esempio classico: ordini con stato='pendente' (1% del totale)
```sql
CREATE INDEX idx_ordini_pendenti ON ordini (creato_il)
WHERE stato = 'pendente';
```
Vantaggi: l'indice e 100x piu piccolo, piu veloce da aggiornare."""
},
# ... 500-1000 esempi per risultati buoni ...
]
train_dataset = prepare_sft_dataset(raw_data[:800])
eval_dataset = prepare_sft_dataset(raw_data[800:])
print(f"Train: {len(train_dataset)} esempi")
print(f"Eval: {len(eval_dataset)} esempi")
print("Esempio:", train_dataset[0]["messages"][1]["content"])
QLoRA yapılandırması
# Modello base: Phi-4-mini
BASE_MODEL = "microsoft/Phi-4-mini-instruct"
# --- Configurazione BitsAndBytes per quantizzazione 4-bit ---
bnb_config = BitsAndBytesConfig(
load_in_4bit=True, # quantizzazione 4-bit NF4
bnb_4bit_use_double_quant=True, # double quantization: -15% memoria extra
bnb_4bit_quant_type="nf4", # NF4 > fp4 per LLM
bnb_4bit_compute_dtype=torch.bfloat16 # bf16 per calcoli (piu stabile di fp16)
)
# Caricare il modello con quantizzazione 4-bit
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL, padding_side="right")
tokenizer.pad_token = tokenizer.eos_token # Phi non ha pad token
model = AutoModelForCausalLM.from_pretrained(
BASE_MODEL,
quantization_config=bnb_config,
device_map="auto",
trust_remote_code=True,
torch_dtype=torch.bfloat16
)
# Preparare il modello per training in kbit (necessario con bitsandbytes)
model = prepare_model_for_kbit_training(model)
# --- Configurazione LoRA ---
lora_config = LoraConfig(
r=16, # rank: 8-64, piu alto = piu parametri trainabili
lora_alpha=32, # scaling: tipicamente = 2*r
target_modules=[ # moduli su cui applicare LoRA
"q_proj", # Query projection
"k_proj", # Key projection
"v_proj", # Value projection
"o_proj", # Output projection
"gate_proj", # MLP gate
"up_proj", # MLP up
"down_proj", # MLP down
],
lora_dropout=0.05, # regularization
bias="none", # non trainare i bias
task_type="CAUSAL_LM"
)
# Applicare LoRA al modello
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# Output: trainable params: 20,971,520 || all params: 3,841,593,344 || trainable%: 0.55
TRL SFTTrainer ile Eğitim
# --- Argomenti di training ---
training_args = TrainingArguments(
output_dir="./phi4-mini-postgres-finetuned",
num_train_epochs=3, # 3 epoch per dataset piccolo
per_device_train_batch_size=2, # batch size: aumentare se hai piu VRAM
gradient_accumulation_steps=4, # effective batch = 8 (2*4)
gradient_checkpointing=True, # risparmia ~40% VRAM, +20% tempo
learning_rate=2e-4, # tipico range: 1e-4 a 5e-4 per LoRA
lr_scheduler_type="cosine", # cosine decay
warmup_ratio=0.05, # 5% del training per warmup
bf16=True, # bfloat16 per training
optim="paged_adamw_8bit", # AdamW quantizzato: risparmia VRAM
logging_steps=10,
evaluation_strategy="steps",
eval_steps=50,
save_strategy="steps",
save_steps=100,
load_best_model_at_end=True,
metric_for_best_model="eval_loss",
report_to="wandb", # o "none" se non usi wandb
push_to_hub=False, # True per uploadare su HF Hub
)
# --- DataCollator: ottimizza il training per SFT ---
# Solo l'output del assistant contribuisce alla loss (non l'input)
response_template = "<|assistant|>" # token che indica l'inizio della risposta
collator = DataCollatorForCompletionOnlyLM(
response_template=response_template,
tokenizer=tokenizer
)
# --- SFTTrainer ---
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
args=training_args,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
data_collator=collator,
max_seq_length=2048, # lunghezza massima sequenza
dataset_text_field=None, # usiamo il campo "messages"
packing=False, # non unire sequenze corte (meglio per chat)
)
# Avviare il training
print("Avvio training...")
trainer.train()
# Salvare il modello (solo i pesi LoRA, non il modello base)
trainer.save_model("./phi4-mini-postgres-lora")
print("Training completato. Pesi LoRA salvati.")
Model Değerlendirme ve Birleştirme
# --- Valutazione qualitativa ---
from peft import PeftModel
def evaluate_finetuned_model(base_model_id: str, lora_path: str, test_prompts: list[str]):
"""Caricare e valutare il modello fine-tunato."""
# Caricare base model + LoRA adapter
tokenizer = AutoTokenizer.from_pretrained(base_model_id)
base_model = AutoModelForCausalLM.from_pretrained(
base_model_id,
torch_dtype=torch.float16,
device_map="auto",
trust_remote_code=True
)
ft_model = PeftModel.from_pretrained(base_model, lora_path)
ft_model.eval()
for prompt in test_prompts:
messages = [
{"role": "system", "content": "Sei un esperto di PostgreSQL."},
{"role": "user", "content": prompt}
]
input_text = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
inputs = tokenizer(input_text, return_tensors="pt").to(ft_model.device)
with torch.no_grad():
outputs = ft_model.generate(
**inputs,
max_new_tokens=300,
temperature=0.3,
do_sample=True
)
response = tokenizer.decode(
outputs[0][inputs.input_ids.shape[1]:],
skip_special_tokens=True
)
print(f"Q: {prompt}")
print(f"A: {response[:200]}")
print("---")
test_prompts = [
"Qual e la differenza tra EXPLAIN e EXPLAIN ANALYZE?",
"Come creo un partial index su ordini non completati?",
"Perche la mia query usa Sequential Scan invece di Index Scan?"
]
evaluate_finetuned_model(BASE_MODEL, "./phi4-mini-postgres-lora", test_prompts)
# --- Merge LoRA nel modello base (per deployment) ---
def merge_and_save(base_model_id: str, lora_path: str, output_path: str):
"""Fonde i pesi LoRA nel modello base per deployment efficiente."""
tokenizer = AutoTokenizer.from_pretrained(base_model_id)
base_model = AutoModelForCausalLM.from_pretrained(
base_model_id,
torch_dtype=torch.float16,
device_map="cpu", # CPU per il merge (piu stabile)
trust_remote_code=True
)
# Applicare i pesi LoRA
ft_model = PeftModel.from_pretrained(base_model, lora_path)
# Merge: fonde A*B nel peso originale
merged_model = ft_model.merge_and_unload()
# Salvare il modello merged
merged_model.save_pretrained(output_path, safe_serialization=True) # usa safetensors
tokenizer.save_pretrained(output_path)
print(f"Modello merged salvato in {output_path}")
print("Ora puoi convertirlo in GGUF con llama.cpp per Ollama!")
merge_and_save(BASE_MODEL, "./phi4-mini-postgres-lora", "./phi4-mini-postgres-merged")
İnce Ayar İçin Gereken GPU Belleği
- 4 bit QLoRA ile Phi-4-mini (3,8B): ~8-10 GB VRAM (RTX 4070 TAMAM)
- QLoRA 4 bitli Qwen 3 7B: ~12-14 GB VRAM (RTX 4070 12GB sınırda)
- LoRA FP16 ile Phi-4-mini: ~16 GB VRAM (RTX 4090 veya A10G)
- İle
gradient_checkpointing=True~%40 ekstra VRAM tasarrufu sağlarsınız
Hugging Face Hub'da yayınlayın
# Pubblicare il modello fine-tunato (opzionale)
from huggingface_hub import login
login() # autentica con token HF
# Pushare solo l'adapter LoRA (leggero, ~50MB)
trainer.model.push_to_hub(
"tuo-username/phi4-mini-postgresql-expert",
private=True, # private=False per rendere pubblico
safe_serialization=True
)
tokenizer.push_to_hub("tuo-username/phi4-mini-postgresql-expert")
# README automatico con informazioni sul training
from huggingface_hub import ModelCard
card_content = """
---
base_model: microsoft/Phi-4-mini-instruct
language: it
tags:
- lora
- postgresql
- database
- fine-tuned
---
# Phi-4-mini fine-tuned su dominio PostgreSQL (italiano)
## Utilizzo
```python
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
model = PeftModel.from_pretrained("tuo-username/phi4-mini-postgresql-expert")
```
"""
card = ModelCard(card_content)
card.push_to_hub("tuo-username/phi4-mini-postgresql-expert")
Sonuçlar
QLoRA, tüketici GPU'su olan herkesin ince ayar yapmasını sağladı: 8-12 GB VRAM 3-7B modellerini birkaç saat içinde alanınıza uyarlamanız yeterlidir. iş akışı PEFT + TRL ile standartlaştırılmış ve iyi belgelenmiş. Sonuç: bilen bir model alana özgü terminolojiniz — belirli görevlerde genellikle GPT-4'ten daha iyi performans gösterir çıkarımda bir kesir maliyeti.
Sonraki makale uç konuşlandırma için nicelemeyi ele alıyor: nasıl dönüştürülür Ollama için GGUF, platformlar arası çalışma süreleri için ONNX ve INT4'te ince ayarlı model NVIDIA ve Qualcomm NPU'ları için.
Seri: Küçük Dil Modelleri
- Makale 1: 2026'da SLM - Genel Bakış ve Karşılaştırma
- Makale 2: Phi-4-mini ve Gemma 3n - Ayrıntılı Karşılaştırma
- Madde 3 (bu): LoRA ve QLoRA ile ince ayar
- Madde 4: Kenar için Niceleme - GGUF, ONNX, INT4
- Madde 5: Ollama - 5 Dakikada Yerel Olarak SLM







