Distillazione modelli: -90% energia, -10% qualità
Answer-first: Model distillation transfers knowledge from a large LLM (teacher, 7B+ parameters) to a smaller one (student, 1B-3B), reducing energy consumption by 90% during inference. The cost: accuracy loss of 2-10% depending on distillation quality and task. In 2027, it's the most impactful technique for edge deployment, mobile inference, and on-prem with limited resources. This article covers foundational theory, end-to-end PyTorch implementation, advanced strategies (layer-wise distillation, weight distillation), and practical case study on Ollama on-prem.
Il problema: costi computazionali del deployment LLM
A LLaMA 3.1 7B model requires ~14GB memory (float16). On edge devices (smartphone, Raspberry Pi, budget VPS), it's not feasible. On cloud, cost reaches $50-100/month for baseline inference. In enterprise context, every 10ms of latency lost in production costs feedback quality and user experience.
Traditional solutions (quantization, pruning) reduce by 2-4x. Distillation goes beyond: a 1.5B student distilled from 7B teacher consumes 75-90% less energy, with minimal accuracy drop if trained well. It's not a replacement for quantization—it's complementary. Combine them: distillation + INT4 quantization = native edge deployment.
- Edge deployment: models <3B on smartphone/IoT with latency <500ms
- Latency constraint: inference in <100ms for interactive applications (chat, completion)
- Privacy + cost: on-prem on €5-15/month VPS vs cloud €50-200/month
- Offline-first: applications working without internet (no cloud roundtrip)
Knowledge Distillation: fondamenti teorici
Hinton et al. (2015) introduced distillation as transfer of "soft targets" (probabilities) from teacher to student. The intuition: the teacher not only gives the correct answer, but also how "confident" it is in wrong answers. This confidence context is valuable information for the student.
Two key components:
- Distillation loss: KL divergence between soft distributions (probabilities scaled by temperature)
- Task loss: standard classification/generation loss (cross-entropy for LLM)
Hybrid α-weighted: Loss_total = α * L_task + (1-α) * L_distill. Typically α=0.3-0.5 for LLM (more weight on distillation vs direct task).
Implementazione: training loop distillazione
Setup: teacher model (llama2:7b, frozen), student model (custom 1.5B), instruction dataset (e.g., Alpaca-cleaned, ~10K samples).
Step 1: carica modelli e configura training
import torch
import torch.nn.functional as F
from transformers import AutoModelForCausalLM, AutoTokenizer
# Load 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
# Load student (trainable)
student_config = AutoConfig.from_pretrained("meta-llama/Llama-2-7b")
student_config.num_hidden_layers = 12 # Reduce from 32 to 12 layers
student_config.num_attention_heads = 8 # Reduce from 32 to 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):
"""
Combines classification loss + KL divergence distillation.
Args:
student_logits: student output logits (batch, seq_len, vocab)
teacher_logits: teacher output logits (detach!)
labels: target token IDs (batch, seq_len)
temperature: scaling for soft targets (default 3.0 for LLM)
alpha: weight distillation vs task (default 0.3)
"""
# Standard classification loss
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
# Recommended parameters for LLM 2027
temperature = 3.0 # Soft targets (not too soft, not too hard)
alpha = 0.3 # 30% task, 70% distillation (works better for LLM)
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()
}
# Load data (e.g., from Hugging Face)
from datasets import load_dataset
dataset = load_dataset("tatsu-lab/alpaca", split="train[:10000]") # First 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 completed!")
Strategie avanzate: layer-wise e weight distillation
Basic distillation uses only output logits. Sophisticated versions distill intermediate layers, activations, and attention weights.
Layer-wise distillation (intermediate layers)
Force the student to mimic teacher hidden states at every layer, not just final output. Improves convergence and final accuracy by 1-3%:
def layer_wise_distillation_loss(student_hiddens, teacher_hiddens, temperature=1.0):
"""
student_hiddens, teacher_hiddens: list of (batch, seq_len, hidden_dim)
Distill each intermediate layer with 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 if different
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 between 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)
Copy teacher attention patterns. Useful if student has fewer 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 on attention weights (already normalized).
"""
loss_total = 0
for s_attn, t_attn in zip(student_attentions, teacher_attentions):
# Reduce teacher heads if too many
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
How to design the student model? Trade-off between parameters and accuracy:
- Depth (num_hidden_layers): Reduce from 32 (teacher 7B) to 12-18 (student 1.5B). Depth influences accuracy more than width
- Width (hidden_size): 1024-2048 (vs 4096 of teacher). Reduced width = low memory
- Attention heads: 8-16 (vs 32). Reduces quadratic complexity of attention
- Vocab retention: Keep same vocab size as teacher (avoid embedding bottleneck)
Recommended configuration for 1.5B student from 7B teacher:
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% reduction)
student_config.num_attention_heads = 8 # 32 → 8
student_config.hidden_size = 1536 # 4096 → 1536 (62% reduction)
student_config.intermediate_size = 6144 # 11008 → 6144
student_config.num_key_value_heads = 8
# Build student from scratch
student = AutoModelForCausalLM.from_config(student_config)
# Student parameters: ~1.5B vs 7B teacher = 78% less memory, 90% fewer FLOPs
total_params = sum(p.numel() for p in student.parameters())
print(f"Student parameters: {total_params/1e9:.1f}B") # ~1.5B
Validation e testing della distillazione
How do you validate that distillation works? Key metrics:
Metriche di accuracy
- Perplexity: PPL on validation set (lower is better). Target: student PPL within 10-15% of teacher
- Task-specific: BLEU (translation), exact match (QA), ROUGE (summarization)
- Human eval: annotators judge output quality 1-5. Not automatic but gold standard
Script di validation
def evaluate_distillation(student_model, teacher_model, val_dataset, tokenizer):
"""Compare student vs teacher on 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
# Use during training every N epochs
evaluate_distillation(student_model, teacher_model, val_loader, tokenizer)
Caso studio: Ollama on-prem con distilled model
Practical setup on Hetzner CCX13 VPS (4 vCPU, 8GB RAM). Distilled Mistral 1.5B + INT4 quantization + Ollama.
Carica e serve distilled model
# Export student model in GGUF format (Ollama compatible)
python -m ollama.convert --model-dir ./student_model_output
# Or copy manually into Ollama models
cp student_model.gguf ~/.ollama/models/
# Start Ollama
ollama serve
# Test inference
curl http://localhost:11434/api/generate -d '{
"model": "student_model",
"prompt": "Explain machine learning briefly",
"stream": false
}'
Benchmark: student vs teacher on Ollama
- Teacher (7B float16): 16GB memory, ~2 sec/prompt, ~30W power
- Teacher (7B INT4): 4GB memory, ~1.5 sec/prompt, ~25W power
- Student (1.5B INT4): 1GB memory, ~0.4 sec/prompt, ~10W power
- Ratio: 16x less memory, 5x faster, 3x less energy
Inferenza via client Python
import requests
def infer_student(prompt, temperature=0.7):
"""Run inference on student model on 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("What is distillation?")
print(result)
# Latency: ~400ms vs ~2000ms teacher
Ottimizzazione: fine-tuning e curriculum learning
If student accuracy drops too much, apply advanced techniques:
Curriculum distillation
Start with easy task, increase difficulty. Example: train on simple Q&A before complex ones:
# Phase 1: Simple facts (1000 samples)
simple_data = load_data("simple_facts.json")
train_one_epoch(student, teacher, simple_data, epochs=1)
# Phase 2: Complex reasoning (5000 samples)
complex_data = load_data("complex_qa.json")
train_one_epoch(student, teacher, complex_data, epochs=2)
# Phase 3: Edge cases and adversarial (2000 samples)
edge_data = load_data("edge_cases.json")
train_one_epoch(student, teacher, edge_data, epochs=1)
# Result: final accuracy +2-3% vs single-phase
Adapter fine-tuning post-distillation
Freeze student, add adapter layer for task-specific:
from peft import get_peft_model, LoraConfig, TaskType
# Freeze student base
for param in student.parameters():
param.requires_grad = False
# Add 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 on task-specific (e.g., IT customer support)
optimizer = torch.optim.AdamW(student_lora.parameters(), lr=2e-4)
# ... training loop for 1-2 epochs on in-domain data
Problemi comuni e debug
Problema 1: Accuracy student crolla (PPL student >> teacher)
Cause: temperature too low (soft targets too hard), or student architecture too small. Solution: increase temperature to 4-5, reduce α to 0.2, increase student layers from 12 to 18.
Problema 2: Distillation loss non diminuisce
Cause: learning rate too high, or vanishing gradients. Solution: lower LR to 1e-5, enable gradient clipping (max norm 1.0), use warmup 500 steps.
Problema 3: Student output è copia esatta di teacher (overfitting a distillation)
Cause: α (weight task loss) too low (<0.2). Solution: increase α to 0.4-0.5, add data augmentation, use variable dropout.
Problema 4: Inferenza student lentissima su CPU
Cause: student still in float32 or float16 without quantization. Solution: apply GPTQ INT4 post-distillation (AutoGPTQ library), export to GGUF for Ollama, try CPU inference with batch size=1.
Roadmap distillazione 2027-2028
- Multi-task distillation: one student learns from multiple specialized teachers. Heterogeneous outputs but superior robustness
- Cross-lingual distillation: distill IT model into EN model using parallel corpus. Effective for language expansion
- Retrieval-augmented distillation: student learns to retrieve relevant documents (like teacher) before generating. Improves long-context accuracy
- Quantization-aware distillation: during training, apply quantization (INT4) online on student. More accurate than post-training quantization
- Parameter-efficient distillation: LoRA/Adapter distillation makes it adaptable to new tasks without base retraining
FAQ
1. Distillazione è sempre meglio di quantization?
No, they're complementary. Quantization reduces memory 4x (float32 → INT8). Distillation reduces parameters 4-6x (7B → 1.5B). Combined: 16-24x less memory. Distillation is more demanding (requires training), quantization is plug-and-play.
2. Posso distillare un modello fine-tuned specifico per dominio?
Yes, perfect. If the teacher is fine-tuned on IT customer support, distill that specialized one. Result: student understanding domain-specific in 1.5B parameters vs 7B teacher. Requires in-domain distillation dataset (~5K samples sufficient).
3. Quanto tempo impiega training distillazione?
3 epochs on 10K samples with A100 GPU: ~4 hours. With consumer GPU (RTX 4090): ~16 hours. On CPU Hetzner VPS: ~3-4 days. Recommendation: prepare dataset offline, validate every epoch on V100 cloud (cheap), save best student.
4. L'output student è sempre deterministico?
Yes, with fixed seed. But dropout added during training introduces controlled variation. If absolute reproducibility needed, disable dropout (set p=0) and use temperature=0 (greedy decoding).
5. Come monitorare la qualità della distillazione in produzione?
Log: accuracy metric (PPL, BLEU) on periodic sample batch. Alert: if student accuracy drops >5% from baseline, trigger fallback to teacher. Pattern: collect user feedback (thumbs up/down), retrain student if drift detected.
Conclusione
Model distillation is the bridge between edge deployment and cloud accuracy. In 2027, it's industry standard for mobile app, IoT, and on-prem inference. Implement step by step: basic distillation (loss function + training loop) → validation (compare PPL) → deployment (Ollama + INT4 quantization) → iterate (curriculum learning if accuracy drops). The code in this guide scales from prototype to production with minimal changes.
Next article in the "Sustainable AI" series: implementing multi-task distillation and orchestration on Kubernetes.

Commenti
Caricamento commenti...
Sign in to comment