Skip to main content

Distillazione modelli: -90% energia, -10% qualità

Articolo della serie Sustainable AI / Green Software 2027. Pubblicazione prevista 14 September 2027.

What do you think about this article?

Distillazione modelli: -90% energia, -10% qualità
Sustainable AIPhotos:Tara WinsteadPexels

At a Glance

Articolo della serie Sustainable AI / Green Software 2027. Pubblicazione prevista 14 September 2027.

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

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects