Distillazione modelli: -90% energia, -10% qualità
Answer-first: La distillazione di modelli trasferisce la conoscenza da un LLM grande (teacher, 7B+ parametri) a uno piccolo (student, 1B-3B), riducendo consumo energetico di 90% durante inferenza. Il costo: perdita di accuracy del 2-10% a seconda della qualità della distillazione e del task. Nel 2027, è la tecnica più impactful per edge deployment, mobile inference, e on-prem con risorse limitate. Questo articolo copre teoria fondamentale, implementazione end-to-end in PyTorch, strategie avanzate (layer-wise distillation, weight distillation), e case study pratico su Ollama on-prem.
Il problema: costi computazionali del deployment LLM
Un modello LLaMA 3.1 7B richiede ~14GB memoria (float16). Su edge device (smartphone, Raspberry Pi, VPS a budget), non è fattibile. Su cloud, il costo raggiunge $50-100/mese per baseline inference. Nel contesto aziendale, ogni 10ms di latency perso in produzione costa feedback quality e user experience.
Soluzioni tradizionali (quantization, pruning) riducono di 2-4x. Distillazione va oltre: uno student model 1.5B distillato da teacher 7B consuma 75-90% meno energia, con accuracy drop minimale se trainato bene. Non è una sostituzione della quantization—è complementare. Le combine: distillation + INT4 quantization = deployment edge-native.
- Edge deployment: modelli <3B su smartphone/IoT con latency <500ms
- Latency vincolo: inference in <100ms per applicazioni interattive (chat, completamento)
- Privacy + cost: on-prem su VPS €5-15/mese vs cloud €50-200/mese
- Offline-first: applicazioni che funzionano senza internet (no roundtrip cloud)
Knowledge Distillation: fondamenti teorici
Hinton et al. (2015) introdussero distillation come trasferimento di "soft targets" (probabilità) da teacher a student. L'intuizione: il teacher non solo dice la risposta corretta, ma anche quanto è "confidenza" in risposte sbagliate. Questo contesto di confidenza è informazione preziosa per lo student.
Due komponenti chiave:
- Distillation loss: KL divergence tra distribuzioni soft (probabilità scalate da temperature)
- Task loss: classification/generation loss standard (cross-entropy per LLM)
Ibrido α-weighted: Loss_total = α * L_task + (1-α) * L_distill. Tipicamente α=0.3-0.5 per LLM (più peso a distillation rispetto a task diretta).
Implementazione: training loop distillazione
Setup: teacher model (llama2:7b, frozen), student model (custom 1.5B), dataset di istruzioni (es. Alpaca-cleaned, ~10K sample).
Step 1: carica modelli e configura training
import torch
import torch.nn.functional as F
from transformers import AutoModelForCausalLM, AutoTokenizer
# Carica teacher (frozen)
teacher_name = "meta-llama/Llama-2-7b"
teacher_model = AutoModelForCausalLM.from_pretrained(teacher_name, torch_dtype=torch.float16)
teacher_model.eval()
for param in teacher_model.parameters():
param.requires_grad = False
# Carica student (trainabile)
student_config = AutoConfig.from_pretrained("meta-llama/Llama-2-7b")
student_config.num_hidden_layers = 12 # Riduci da 32 a 12 layer
student_config.num_attention_heads = 8 # Riduci da 32 a 8
student_model = AutoModelForCausalLM.from_config(student_config, torch_dtype=torch.float16)
tokenizer = AutoTokenizer.from_pretrained(teacher_name)
tokenizer.pad_token = tokenizer.eos_token
# Setup optimizer
optimizer = torch.optim.AdamW(student_model.parameters(), lr=5e-5)
Step 2: distillation loss function
def distillation_loss(student_logits, teacher_logits, labels, temperature=3.0, alpha=0.3):
"""
Combina classification loss + KL divergence distillation.
Args:
student_logits: output logits dello student (batch, seq_len, vocab)
teacher_logits: output logits del teacher (detach!)
labels: target token IDs (batch, seq_len)
temperature: scaling per soft targets (default 3.0 per LLM)
alpha: weight distillation vs task (default 0.3)
"""
# Classification loss standard
ce_loss = F.cross_entropy(
student_logits.view(-1, student_logits.size(-1)),
labels.view(-1),
reduction='mean'
)
# Distillation loss: KL divergence
student_probs = F.log_softmax(student_logits / temperature, dim=-1)
teacher_probs = F.softmax(teacher_logits / temperature, dim=-1)
kl_loss = F.kl_div(
student_probs.view(-1, student_probs.size(-1)),
teacher_probs.view(-1, teacher_probs.size(-1)),
reduction='batchmean'
)
# Combined loss
loss = alpha * ce_loss + (1 - alpha) * kl_loss * (temperature ** 2)
return loss, ce_loss, kl_loss
# Parametri consigliati per LLM 2027
temperature = 3.0 # Soft targets (no troppo morbide, no troppo dure)
alpha = 0.3 # 30% task, 70% distillation (per LLM funziona meglio)
Step 3: training loop
from torch.utils.data import DataLoader, Dataset
class DistillationDataset(Dataset):
def __init__(self, data, tokenizer, max_len=512):
self.tokenizer = tokenizer
self.data = data
self.max_len = max_len
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
text = self.data[idx]
encoding = self.tokenizer(
text,
max_length=self.max_len,
padding='max_length',
truncation=True,
return_tensors='pt'
)
return {
'input_ids': encoding['input_ids'].squeeze(),
'attention_mask': encoding['attention_mask'].squeeze()
}
# Carica dati (es. da Hugging Face)
from datasets import load_dataset
dataset = load_dataset("tatsu-lab/alpaca", split="train[:10000]") # Prime 10K
texts = [d["instruction"] + " " + d["output"] for d in dataset]
train_dataset = DistillationDataset(texts, tokenizer)
train_loader = DataLoader(train_dataset, batch_size=4, shuffle=True)
# Training
num_epochs = 3
for epoch in range(num_epochs):
total_loss = 0
for batch in train_loader:
input_ids = batch['input_ids'].to("cuda")
attention_mask = batch['attention_mask'].to("cuda")
labels = input_ids.clone()
labels[labels == tokenizer.pad_token_id] = -100
# Forward pass teacher (no grad)
with torch.no_grad():
teacher_output = teacher_model(
input_ids=input_ids,
attention_mask=attention_mask,
output_hidden_states=False
)
teacher_logits = teacher_output.logits
# Forward pass student
student_output = student_model(
input_ids=input_ids,
attention_mask=attention_mask
)
student_logits = student_output.logits
# Loss
loss, ce_l, kl_l = distillation_loss(
student_logits, teacher_logits, labels,
temperature=temperature, alpha=alpha
)
# Backward
optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(student_model.parameters(), 1.0)
optimizer.step()
total_loss += loss.item()
avg_loss = total_loss / len(train_loader)
print(f"Epoch {epoch+1}/{num_epochs} | Loss: {avg_loss:.4f}")
print("Training completato!")
Strategie avanzate: layer-wise e weight distillation
Basic distillation usa solo output logits. Versioni sofisticate distillano layer intermedi, activation, e weight attention.
Layer-wise distillation (intermediate layers)
Forza lo student a imitare hidden states del teacher a ogni layer, no solo l'output finale. Migliora convergenza e accuracy finale di 1-3%:
def layer_wise_distillation_loss(student_hiddens, teacher_hiddens, temperature=1.0):
"""
student_hiddens, teacher_hiddens: lista di (batch, seq_len, hidden_dim)
Distilla ogni layer intermedio con MSE loss.
"""
assert len(student_hiddens) == len(teacher_hiddens)
loss_total = 0
for s_hidden, t_hidden in zip(student_hiddens, teacher_hiddens):
# Project student to teacher dimension se diversi
if s_hidden.shape[-1] != t_hidden.shape[-1]:
s_hidden = torch.nn.functional.linear(
s_hidden,
torch.randn(t_hidden.shape[-1], s_hidden.shape[-1], device=s_hidden.device)
)
# MSE tra hidden states
loss_total += F.mse_loss(s_hidden, t_hidden, reduction='mean')
return loss_total / len(student_hiddens)
# In training loop:
# teacher_output = teacher_model(..., output_hidden_states=True)
# student_output = student_model(..., output_hidden_states=True)
# loss_hidden = layer_wise_distillation_loss(
# student_output.hidden_states, teacher_output.hidden_states
# )
# loss_total = loss_logits + 0.5 * loss_hidden
Attention distillation (teacher attention copy)
Copia attention patterns del teacher. Utile se student ha meno attention heads:
def attention_distillation_loss(student_attentions, teacher_attentions, temperature=3.0):
"""
student_attentions, teacher_attentions: (batch, heads, seq_len, seq_len)
KL divergence su attention weights (già normalizzate).
"""
loss_total = 0
for s_attn, t_attn in zip(student_attentions, teacher_attentions):
# Riduci teacher heads se troppi
if s_attn.shape[1] != t_attn.shape[1]:
t_attn = t_attn.mean(dim=1, keepdim=True) # Average heads
s_log_attn = F.log_softmax(s_attn / temperature, dim=-1)
t_attn_soft = F.softmax(t_attn / temperature, dim=-1)
loss_total += F.kl_div(s_log_attn.view(-1, s_log_attn.size(-1)),
t_attn_soft.view(-1, t_attn_soft.size(-1)),
reduction='batchmean')
return loss_total / len(student_attentions)
Architettura student ottimale
Come progettare il modello student? Trade-off tra parametri e accuracy:
- Depth (num_hidden_layers): Riduci da 32 (teacher 7B) a 12-18 (student 1.5B). Depth influenza accuracy più di width
- Width (hidden_size): 1024-2048 (vs 4096 del teacher). Width ridotta = memoria bassa
- Attention heads: 8-16 (vs 32). Riduce complessità quadratica attention
- Vocab retention: Mantieni stesso vocab size del teacher (evita bottleneck embedding)
Configurazione consigliata per student 1.5B da teacher 7B:
from transformers import AutoConfig, AutoModelForCausalLM
teacher_config = AutoConfig.from_pretrained("meta-llama/Llama-2-7b")
student_config = AutoConfig.from_pretrained("meta-llama/Llama-2-7b")
student_config.num_hidden_layers = 12 # 32 → 12 (62% riduzione)
student_config.num_attention_heads = 8 # 32 → 8
student_config.hidden_size = 1536 # 4096 → 1536 (62% riduzione)
student_config.intermediate_size = 6144 # 11008 → 6144
student_config.num_key_value_heads = 8
# Costruisci student da zero
student = AutoModelForCausalLM.from_config(student_config)
# Parametri student: ~1.5B vs 7B teacher = 78% meno memoria, 90% meno FLOPs
total_params = sum(p.numel() for p in student.parameters())
print(f"Student parametri: {total_params/1e9:.1f}B") # ~1.5B
Validation e testing della distillazione
Come validi che la distillazione funziona? Metriche chiave:
Metriche di accuracy
- Perplexity: PPL su validation set (lower is better). Target: student PPL entro 10-15% da teacher
- Task-specific: BLEU (translation), exact match (QA), ROUGE (summarization)
- Human eval: annotatori che giudicano output qualità 1-5. Non è automatico ma è gold standard
Script di validation
def evaluate_distillation(student_model, teacher_model, val_dataset, tokenizer):
"""Compara student vs teacher su validation set."""
student_model.eval()
teacher_model.eval()
student_loss = 0
teacher_loss = 0
num_batches = 0
with torch.no_grad():
for batch in val_dataset:
input_ids = batch['input_ids'].to("cuda")
labels = input_ids.clone()
labels[labels == tokenizer.pad_token_id] = -100
# Student eval
s_out = student_model(input_ids=input_ids, labels=labels)
student_loss += s_out.loss.item()
# Teacher eval
t_out = teacher_model(input_ids=input_ids, labels=labels)
teacher_loss += t_out.loss.item()
num_batches += 1
student_avg_loss = student_loss / num_batches
teacher_avg_loss = teacher_loss / num_batches
# Compute perplexity
student_ppl = torch.exp(torch.tensor(student_avg_loss))
teacher_ppl = torch.exp(torch.tensor(teacher_avg_loss))
print(f"Teacher PPL: {teacher_ppl:.2f}")
print(f"Student PPL: {student_ppl:.2f}")
print(f"Accuracy gap: {((student_ppl/teacher_ppl - 1) * 100):.1f}%")
return student_ppl, teacher_ppl
# Usa durante training ogni N epoch
evaluate_distillation(student_model, teacher_model, val_loader, tokenizer)
Caso studio: Ollama on-prem con distilled model
Setup pratico su VPS Hetzner CCX13 (4 vCPU, 8GB RAM). Distilled Mistral 1.5B + quantization INT4 + Ollama.
Carica e serve distilled model
# Esporta student model in GGUF format (compatibile Ollama)
python -m ollama.convert --model-dir ./student_model_output
# O copia manualmente in Ollama models
cp student_model.gguf ~/.ollama/models/
# Avvia Ollama
ollama serve
# Prova inference
curl http://localhost:11434/api/generate -d '{
"model": "student_model",
"prompt": "Spiega il machine learning brevemente",
"stream": false
}'
Benchmark: student vs teacher su Ollama
- Teacher (7B float16): 16GB memoria, ~2 sec/prompt, ~30W power
- Teacher (7B INT4): 4GB memoria, ~1.5 sec/prompt, ~25W power
- Student (1.5B INT4): 1GB memoria, ~0.4 sec/prompt, ~10W power
- Rapporto: 16x meno memoria, 5x più veloce, 3x meno energia
Inferenza via client Python
import requests
def infer_student(prompt, temperature=0.7):
"""Inferenza da student model su Ollama."""
response = requests.post(
"http://localhost:11434/api/generate",
json={
"model": "student_model",
"prompt": prompt,
"temperature": temperature,
"stream": False
}
)
return response.json()["response"]
# Test
result = infer_student("Che cos'è la distillazione?")
print(result)
# Latency: ~400ms vs ~2000ms teacher
Ottimizzazione: fine-tuning e curriculum learning
Se accuracy student cala troppo, applica tecniche avanzate:
Curriculum distillation
Inizia con task facile, incrementa difficoltà. Esempio: addestra su Q&A semplici prima di complesse:
# Fase 1: Simple facts (1000 sample)
simple_data = load_data("simple_facts.json")
train_one_epoch(student, teacher, simple_data, epochs=1)
# Fase 2: Complex reasoning (5000 sample)
complex_data = load_data("complex_qa.json")
train_one_epoch(student, teacher, complex_data, epochs=2)
# Fase 3: Edge cases e adversarial (2000 sample)
edge_data = load_data("edge_cases.json")
train_one_epoch(student, teacher, edge_data, epochs=1)
# Risultato: accuracy finale +2-3% rispetto single-phase
Adapter fine-tuning post-distillation
Congela student, aggiungi adapter layer per task specifico:
from peft import get_peft_model, LoraConfig, TaskType
# Freeze student base
for param in student.parameters():
param.requires_grad = False
# Aggiungi LoRA adapter
lora_config = LoraConfig(
r=8,
lora_alpha=16,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.05,
bias="none",
task_type=TaskType.CAUSAL_LM
)
student_lora = get_peft_model(student, lora_config)
# Fine-tune su task specifico (es. IT customer support)
optimizer = torch.optim.AdamW(student_lora.parameters(), lr=2e-4)
# ... training loop per 1-2 epoch su in-domain data
Problemi comuni e debug
Problema 1: Accuracy student crolla (PPL student >> teacher)
Causa: temperature troppo bassa (soft targets troppo dure), o student architettura troppo piccola. Soluzione: incrementa temperature a 4-5, riduci α a 0.2, aumenta layer student da 12 a 18.
Problema 2: Distillation loss non diminuisce
Causa: learning rate troppo alto, o gradienti vanishing. Soluzione: abbassa LR a 1e-5, attiva gradient clipping (max norm 1.0), usa warmup 500 step.
Problema 3: Student output è copia esatta di teacher (overfitting a distillation)
Causa: α (weight task loss) troppo basso (<0.2). Soluzione: incrementa α a 0.4-0.5, aggiungi data augmentation, usa dropout variabile.
Problema 4: Inferenza student lentissima su CPU
Causa: student ancora in float32 o float16 without quantization. Soluzione: applica GPTQ INT4 post-distillation (AutoGPTQ library), esporta in GGUF per Ollama, prova CPU inference con batch size=1.
Roadmap distillazione 2027-2028
- Multi-task distillation: uno student impara da multiple teacher specializzati. Output eterogeni ma robustezza superiore
- Cross-lingual distillation: distilla modello IT in modello EN usando parallel corpus. Efficace per espansione linguistica
- Retrieval-augmented distillation: student impara a recuperare documenti rilevanti (come teacher) prima di generare. Migliora accuracy long-context
- Quantization-aware distillation: durante training, applica quantization (INT4) online su student. Post-training quantization più accurata
- Parameter-efficient distillation: LoRA/Adapter distillation rende adattabile su nuovi task senza ritraining base
FAQ
1. Distillazione è sempre meglio di quantization?
No, sono complementari. Quantization riduce memoria di 4x (float32 → INT8). Distillation riduce parametri di 4-6x (7B → 1.5B). Combinate: 16-24x meno memoria. Distillation è più impegnativa (richiede training), quantization è plug-and-play.
2. Posso distillare un modello fine-tuned specifico per dominio?
Sì, perfetto. Se il teacher è stato fine-tuned su customer support IT, distilla quello specializzato. Risultato: student che capisce dominio specifico in 1.5B parametri vs 7B del teacher. Richiede distillation dataset in-domain (~5K sample sufficienti).
3. Quanto tempo impiega training distillazione?
3 epoch su 10K sample con GPU A100: ~4 ore. Con GPU consumer (RTX 4090): ~16 ore. Su CPU VPS Hetzner: ~3-4 giorni. Consiglio: prepara dataset offline, valida ogni epoch su V100 cloud (cheap), salva best student.
4. L'output student è sempre deterministico?
Sì, dato seed fisso. Ma dropout aggiunte durante training introduce variazione controllata. Se serve riproducibilità assoluta, disabilita dropout (set p=0) e usa temperature=0 (greedy decoding).
5. Come monitorare la qualità della distillazione in produzione?
Log: accuracy metric (PPL, BLEU) su sample batch periodico. Alerta: se student accuracy cala >5% da baseline, attiva fallback a teacher. Pattern: raccogli user feedback (thumbs up/down), retrain student se drift rilevato.
Conclusione
Distillazione modelli è il ponte tra deployment edge e accuracy cloud. Nel 2027, è standard industry per mobile app, IoT, e on-prem inference. Implementa a step: basic distillation (loss funzione + training loop) → validation (compare PPL) → deployment (Ollama + quantization INT4) → iterate (curriculum learning se accuracy scende). Il codice di questa guida scala da prototipo a production con poche modifiche.
Prossimo articolo della serie "Sustainable AI": implementazione multi-task distillation e orchestrazione su Kubernetes.

Commenti
Caricamento commenti...
Accedi per commentare