Jemné vyladění SLM pomocí LoRA a QLoRA: Kompletní praktický průvodce
V roce 2021 vyžadovalo jemné vyladění modelu s parametry 7B 8 GPU po 80 GB a týdny času. S QLoRA (2023) a vyspělými nástroji z roku 2026 je stejného výsledku dosaženo na jediný spotřebitelský GPU s 8–12 GB VRAM za 2–4 hodiny. Phi-4-mini nebo Qwen 3 přizpůsobené vašim vlastním doména — obchodní dokumentace, lékařská terminologie, starší kód — přesahuje GPT-4 sui specifické úkoly za zlomek nákladů. Tato příručka pokrývá celý pracovní postup: příprava datové sady, školení s QLoRA, vyhodnocení a nasazení.
Co se naučíte
- LoRA: Jak funguje rozklad matice nízké úrovně
- QLoRA: Kombinujte 4bitovou kvantizaci s LoRA pro 8-12GB GPU
- Připravte datovou sadu ve správném formátu pro SFT (Supervised Fine-Tuning)
- Kompletní pracovní postup s datovými sadami PEFT + TRL + Hugging Face
- Vyhodnoťte vyladěný model a slučujte jej na základě modelu
Jak funguje LoRA: Teorie za 5 minut
Model transformátoru má miliony hmotnostních matic. Vylaďte všechny miliony parametrů aktualizovat – na spotřebitelských GPU nemožné. LoRA (Low-Rank Adaptation) uvádí, že Aktualizace potřebné k přizpůsobení předem řízenému modelu mají ze své podstaty nízké hodnocení: lze aproximovat dvěma malými maticemi A a B, kde hodnost r (typicky 4-64) a mnohem menší než původní velikost.
# 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
Nastavení prostředí pro QLoRA
# 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")
Připravte datovou sadu pro SFT
# 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"])
Konfigurace QLoRA
# 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
Školení s TRL SFTTrainer
# --- 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.")
Hodnocení a sloučení modelů
# --- 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")
Pro jemné doladění je vyžadována paměť GPU
- Phi-4-mini (3,8B) se 4bitovou QLoRA: ~8-10 GB VRAM (RTX 4070 OK)
- Qwen 3 7B se 4bitovou QLoRA: ~12-14 GB VRAM (RTX 4070 12GB hranice)
- Phi-4-mini s LoRA fp16: ~16 GB VRAM (RTX 4090 nebo A10G)
- Con
gradient_checkpointing=Trueušetříte ~40 % paměti VRAM navíc
Příspěvek na Hugging Face Hub
# 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")
Závěry
QLoRA zpřístupnila jemné ladění každému, kdo má spotřebitelský GPU: 8–12 GB VRAM stačí k přizpůsobení 3-7B modelů vaší doméně během pár hodin. Pracovní postup s PEFT + TRL a standardizované a dobře zdokumentované. Výsledek – model, který ví terminologie specifická pro vaši doménu – často však předčí GPT-4 v konkrétních úkolech stojí zlomek v závěru.
Další článek se zabývá kvantizací pro nasazení hran: jak převést jemně vyladěný model v GGUF pro Ollama, ONNX pro běhové prostředí pro více platforem a INT4 pro NPU NVIDIA a Qualcomm.
Řada: Small Language Models
- Článek 1: SLM v roce 2026 – Přehled a benchmark
- Článek 2: Phi-4-mini vs Gemma 3n – podrobné srovnání
- článek 3 (tento): Jemné doladění pomocí LoRA a QLoRA
- Článek 4: Kvantování pro Edge – GGUF, ONNX, INT4
- Článek 5: Ollama - SLM lokálně za 5 minut







