Skip to main content

Cyber AI România

Joi, 22 octombrie 2026

Knowledge Distillation pentru LLM-uri: distilare GPT-2 pe WikiText-2 cu KLDivLoss

Knowledge Distillation transferă comportamentul unui model teacher mai mare către un model student mai mic. În loc să antrenăm studentul doar cu tokenul corect, îl facem să urmărească distribuția de probabilitate produsă de teacher: ce tokenuri consideră probabile, cât de sigur este și ce alternative păstrează.

În acest proiect construim un pipeline complet: date → tokenizare → distilare → evaluare → inferență. Folosim gpt2 ca teacher, distilgpt2 ca student, datasetul Salesforce/wikitext, configurația wikitext-2-raw-v1, KLDivLoss pentru distribuțiile teacher–student și cross_entropy pentru etichetele reale.

Dacă lucrezi deja cu antrenări mai mari, completează acest tutorial cu materialele despre topologia NCCL pentru NVLink, PCIe și rețea și TorchElastic pentru training distribuit. Aici rămânem la o singură rulare locală, reproductibilă.

Ce construim

Construim un director final numit distilled-wikitext2-student, care conține modelul student salvat după 200 de pași de distilare pe WikiText-2. La final, îl încărcăm local și generăm text cu generate.py.

Rezultatul verificabil este acesta: scriptul rulează fără traceback, afișează loss inițial și final, salvează fișierele modelului și produce o continuare pentru promptul Artificial intelligence systems can.

Mediul exact

Mediul folosit în tutorial:

  • Ubuntu 22.04 LTS sau WSL2 Ubuntu 22.04;
  • Python 3.11;
  • torch==2.14.1;
  • transformers==5.19.0;
  • datasets==5.1.0;
  • accelerate==1.15.0;
  • tqdm==4.70.1;
  • minimum 8 GB RAM;
  • minimum 5 GB spațiu liber;
  • GPU NVIDIA recomandat, dar CPU funcționează pentru testul scurt.

Verifică Python:

python3 --version

Trebuie să vezi ceva de forma:

Python 3.11.x

Ce instalezi

Creează directorul proiectului:

mkdir -p ~/kd-wikitext2
cd ~/kd-wikitext2

Trebuie să fii în directorul ~/kd-wikitext2.

Creează și activează mediul virtual:

python3.11 -m venv .venv
source .venv/bin/activate

Trebuie să vezi prefixul (.venv) în prompt.

Actualizează uneltele de instalare:

python -m pip install --upgrade pip setuptools wheel

Instalează dependențele:

python -m pip install torch==2.14.1 transformers==5.19.0 datasets==5.1.0 accelerate==1.15.0 tqdm==4.70.1

Verifică importurile:

python - <<'PY'
import torch
import transformers
import datasets
import accelerate
import tqdm

print("torch", torch.__version__)
print("transformers", transformers.__version__)
print("datasets", datasets.__version__)
print("accelerate", accelerate.__version__)
print("cuda_available", torch.cuda.is_available())
PY

Trebuie să vezi:

torch 2.14.1
transformers 5.19.0
datasets 5.1.0
accelerate 1.15.0
cuda_available True

sau cuda_available False, dacă rulezi pe CPU.

Pasul 1: creează scriptul de distilare

Creează fișierul:

nano distill_wikitext2.py

Lipește codul complet:

import argparse
import math
import os
import random
from dataclasses import dataclass

import torch
import torch.nn.functional as F
from datasets import load_dataset
from torch.utils.data import DataLoader
from tqdm.auto import tqdm
from transformers import AutoModelForCausalLM, AutoTokenizer, get_linear_schedule_with_warmup


def parse_args():
    parser = argparse.ArgumentParser(description="Distilare Causal LM pe WikiText-2 cu KLDivLoss si CrossEntropyLoss")
    parser.add_argument("--teacher_model", default="gpt2")
    parser.add_argument("--student_model", default="distilgpt2")
    parser.add_argument("--dataset_name", default="Salesforce/wikitext")
    parser.add_argument("--dataset_config", default="wikitext-2-raw-v1")
    parser.add_argument("--output_dir", default="distilled-wikitext2-student")
    parser.add_argument("--block_size", type=int, default=128)
    parser.add_argument("--batch_size", type=int, default=2)
    parser.add_argument("--learning_rate", type=float, default=5e-5)
    parser.add_argument("--epochs", type=int, default=1)
    parser.add_argument("--max_train_steps", type=int, default=200)
    parser.add_argument("--temperature", type=float, default=2.0)
    parser.add_argument("--alpha", type=float, default=0.7)
    parser.add_argument("--seed", type=int, default=47)
    return parser.parse_args()


def set_seed(seed):
    random.seed(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)


@dataclass
class CausalBatch:
    input_ids: torch.Tensor
    attention_mask: torch.Tensor


def tokenize_and_group(dataset, tokenizer, block_size):
    def tokenize(batch):
        texts = [text if text and not text.isspace() else tokenizer.eos_token for text in batch["text"]]
        return tokenizer(texts)

    tokenized = dataset.map(tokenize, batched=True, remove_columns=["text"])

    def group_texts(examples):
        concatenated = {key: sum(examples[key], []) for key in examples.keys()}
        total_length = len(concatenated["input_ids"])
        total_length = (total_length // block_size) * block_size
        result = {
            key: [values[i : i + block_size] for i in range(0, total_length, block_size)]
            for key, values in concatenated.items()
        }
        return result

    grouped = tokenized.map(group_texts, batched=True)
    grouped.set_format(type="torch", columns=["input_ids", "attention_mask"])
    return grouped


def collate_fn(features):
    return CausalBatch(
        input_ids=torch.stack([feature["input_ids"] for feature in features]),
        attention_mask=torch.stack([feature["attention_mask"] for feature in features]),
    )


def distillation_loss(student_logits, teacher_logits, labels, attention_mask, temperature, alpha):
    shifted_student = student_logits[:, :-1, :].contiguous()
    shifted_teacher = teacher_logits[:, :-1, :].contiguous()
    shifted_labels = labels[:, 1:].contiguous()
    shifted_mask = attention_mask[:, 1:].contiguous().bool()

    active_student = shifted_student[shifted_mask]
    active_teacher = shifted_teacher[shifted_mask]
    active_labels = shifted_labels[shifted_mask]

    student_log_probs = F.log_softmax(active_student / temperature, dim=-1)
    teacher_probs = F.softmax(active_teacher / temperature, dim=-1)

    kd_loss = F.kl_div(student_log_probs, teacher_probs, reduction="batchmean") * (temperature ** 2)
    ce_loss = F.cross_entropy(active_student, active_labels)
    total_loss = alpha * kd_loss + (1.0 - alpha) * ce_loss

    return total_loss, kd_loss.detach(), ce_loss.detach()


@torch.no_grad()
def evaluate(model, dataloader, device):
    model.eval()
    losses = []

    for batch in dataloader:
        input_ids = batch.input_ids.to(device)
        attention_mask = batch.attention_mask.to(device)
        outputs = model(input_ids=input_ids, attention_mask=attention_mask, labels=input_ids)
        losses.append(outputs.loss.detach().float().cpu())

    mean_loss = torch.stack(losses).mean().item()
    perplexity = math.exp(mean_loss) if mean_loss < 20 else float("inf")
    model.train()
    return mean_loss, perplexity


def main():
    args = parse_args()

    if not 0.0 <= args.alpha <= 1.0:
        raise ValueError("--alpha trebuie sa fie intre 0.0 si 1.0")

    set_seed(args.seed)

    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    print(f"Device: {device}")
    print(f"Teacher: {args.teacher_model} | Student: {args.student_model}")

    tokenizer = AutoTokenizer.from_pretrained(args.teacher_model)
    if tokenizer.pad_token is None:
        tokenizer.pad_token = tokenizer.eos_token

    dataset = load_dataset(args.dataset_name, args.dataset_config)
    grouped = tokenize_and_group(dataset, tokenizer, args.block_size)

    train_loader = DataLoader(grouped["train"], batch_size=args.batch_size, shuffle=True, collate_fn=collate_fn)
    valid_loader = DataLoader(grouped["validation"], batch_size=args.batch_size, shuffle=False, collate_fn=collate_fn)

    teacher = AutoModelForCausalLM.from_pretrained(args.teacher_model).to(device)
    student = AutoModelForCausalLM.from_pretrained(args.student_model).to(device)

    teacher.eval()
    student.train()

    optimizer = torch.optim.AdamW(student.parameters(), lr=args.learning_rate)
    total_steps = min(args.max_train_steps, args.epochs * len(train_loader))
    scheduler = get_linear_schedule_with_warmup(optimizer, num_warmup_steps=0, num_training_steps=total_steps)

    initial_loss, initial_ppl = evaluate(student, valid_loader, device)
    print(f"Eval initial student: loss={initial_loss:.4f}, perplexity={initial_ppl:.2f}")

    progress = tqdm(total=total_steps, desc="distilare")
    completed_steps = 0

    for epoch in range(args.epochs):
        for batch in train_loader:
            input_ids = batch.input_ids.to(device)
            attention_mask = batch.attention_mask.to(device)

            with torch.no_grad():
                teacher_outputs = teacher(input_ids=input_ids, attention_mask=attention_mask)

            student_outputs = student(input_ids=input_ids, attention_mask=attention_mask)

            loss, kd_loss, ce_loss = distillation_loss(
                student_outputs.logits,
                teacher_outputs.logits,
                input_ids,
                attention_mask,
                args.temperature,
                args.alpha,
            )

            loss.backward()
            torch.nn.utils.clip_grad_norm_(student.parameters(), max_norm=1.0)
            optimizer.step()
            scheduler.step()
            optimizer.zero_grad(set_to_none=True)

            completed_steps += 1
            progress.update(1)
            progress.set_postfix(loss=f"{loss.item():.4f}", kd=f"{kd_loss.item():.4f}", ce=f"{ce_loss.item():.4f}")

            if completed_steps >= total_steps:
                break

        if completed_steps >= total_steps:
            break

    progress.close()

    final_loss, final_ppl = evaluate(student, valid_loader, device)
    print(f"Eval final student: loss={final_loss:.4f}, perplexity={final_ppl:.2f}")

    os.makedirs(args.output_dir, exist_ok=True)
    student.save_pretrained(args.output_dir)
    tokenizer.save_pretrained(args.output_dir)
    print(f"Model salvat in: {args.output_dir}")


if __name__ == "__main__":
    main()

Salvează fișierul. Verifică sintaxa:

python -m py_compile distill_wikitext2.py

Trebuie să nu apară nicio eroare.

Pasul 2: rulează un smoke test

Prima rulare descarcă datasetul și modelele. Testul scurt verifică doar pipeline-ul:

python distill_wikitext2.py --max_train_steps 2 --batch_size 1 --block_size 64 --output_dir smoke-student

Trebuie să vezi linii de forma:

Device: cuda
Teacher: gpt2 | Student: distilgpt2
Eval initial student: loss=..., perplexity=...
distilare: 100%|...| 2/2 ...
Eval final student: loss=..., perplexity=...
Model salvat in: smoke-student

Dacă apare Device: cpu, comanda este tot corectă, dar va rula mai lent.

Verifică directorul:

ls -1 smoke-student

Trebuie să vezi fișiere precum:

config.json
generation_config.json
merges.txt
model.safetensors
special_tokens_map.json
tokenizer.json
tokenizer_config.json
vocab.json

Lista poate varia ușor după formatul de salvare, dar trebuie să existe configurația, tokenizerul și greutățile modelului.

Pasul 3: rulează distilarea de 200 de pași

Rulează distilarea finală:

python distill_wikitext2.py --max_train_steps 200 --batch_size 2 --block_size 128 --temperature 2.0 --alpha 0.7 --learning_rate 5e-5 --output_dir distilled-wikitext2-student

Trebuie să vezi valori finite pentru loss, kd și ce, nu nan sau inf.

La final trebuie să vezi:

Eval final student: loss=..., perplexity=...
Model salvat in: distilled-wikitext2-student

În acest tutorial, alpha=0.7 înseamnă că pierderea KL are pondere mai mare decât cross-entropy. Pentru comparație, în preference learning, obiectivul este diferit: un evaluator învață alegeri chosen–rejected, ca în articolul despre Reward Model pentru preference learning. Aici nu antrenăm un reward model și nu facem RL.

Pasul 4: verifică modelul prin inferență

Creează fișierul:

nano generate.py

Lipește codul:

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

model_dir = "distilled-wikitext2-student"
prompt = "Artificial intelligence systems can"

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
tokenizer = AutoTokenizer.from_pretrained(model_dir)
model = AutoModelForCausalLM.from_pretrained(model_dir).to(device)
model.eval()

inputs = tokenizer(prompt, return_tensors="pt").to(device)

with torch.no_grad():
    output_ids = model.generate(
        **inputs,
        max_new_tokens=40,
        do_sample=False,
        pad_token_id=tokenizer.eos_token_id,
    )

print(tokenizer.decode(output_ids[0], skip_special_tokens=True))

Rulează:

python generate.py

Trebuie să vezi un text care începe cu:

Artificial intelligence systems can

Continuarea poate fi slabă după numai 200 de pași. Obiectivul este să verifici mecanismul de distilare și artefactul salvat, nu să obții un LLM de producție.

Verificarea finală

Rulează verificările:

python -m py_compile distill_wikitext2.py generate.py
test -d distilled-wikitext2-student && echo "model salvat"
ls distilled-wikitext2-student | grep -E 'model|pytorch'
python generate.py

Trebuie să vezi:

model salvat
model.safetensors
Artificial intelligence systems can...

Dacă folosești rulări multi-GPU sau secvențe lungi, intri într-o zonă diferită de optimizare, unde devin relevante tehnici precum Context Parallel și Ring Attention. Pentru tutorialul de aici, un singur proces este suficient.

Probleme frecvente

ModuleNotFoundError: No module named 'transformers'

Cauză: mediul virtual nu este activ sau dependențele au fost instalate în alt Python.

Rezolvare:

source .venv/bin/activate
python -m pip install torch==2.14.1 transformers==5.19.0 datasets==5.1.0 accelerate==1.15.0 tqdm==4.70.1

OSError: We couldn't connect to 'https://huggingface.co'

Cauză: sistemul nu poate accesa Hugging Face Hub.

Rezolvare: verifică rețeaua, DNS-ul sau proxy-ul organizației și rulează din nou. Pentru reproductibilitate, folosește pagina oficială Hugging Face Hub, nu oglinzi neoficiale.

RuntimeError: CUDA out of memory

Cauză: GPU-ul nu are memorie suficientă pentru batch și secvență.

Rezolvare:

python distill_wikitext2.py --max_train_steps 200 --batch_size 1 --block_size 64 --temperature 2.0 --alpha 0.7 --output_dir distilled-wikitext2-student

ValueError: --alpha trebuie sa fie intre 0.0 si 1.0

Cauză: valoarea pentru --alpha este invalidă.

Rezolvare:

python distill_wikitext2.py --alpha 0.7

loss=nan

Cauză: antrenarea a devenit instabilă, de obicei după modificarea ratei de învățare sau a temperaturii.

Rezolvare:

python distill_wikitext2.py --learning_rate 5e-5 --temperature 2.0 --alpha 0.7 --batch_size 2 --block_size 128

Revenire și curățenie

Șterge artefactul de test:

rm -rf smoke-student

Șterge modelul final doar dacă nu mai ai nevoie de el:

rm -rf distilled-wikitext2-student

Șterge cache-ul Hugging Face doar dacă accepți să redescarci datasetul și modelele:

rm -rf ~/.cache/huggingface

Dezactivează mediul virtual:

deactivate

Trebuie să dispară prefixul (.venv) din prompt.

Mai departe

După această variantă minimă, următorul pas realist este să mărești numărul de pași, să păstrezi loguri separate pentru loss-ul KL și cross-entropy și să evaluezi studentul pe seturi de test relevante pentru aplicația ta.

Nu folosi acest pipeline ca scurtătură pentru a copia modele fără respectarea licențelor. Teacher-ul, studentul, datasetul și destinația modelului trebuie verificate înainte de orice utilizare comercială sau publică.

Întrebări frecvente

De ce folosim KLDivLoss?

Folosim KLDivLoss pentru ca studentul să învețe distribuția de probabilitate produsă de teacher, nu doar tokenul corect. În cod, studentul intră prin log_softmax, iar teacher-ul prin softmax.

De ce păstrăm și cross-entropy?

Cross-entropy păstrează legătura cu tokenii reali din WikiText-2. Pierderea finală combină distilarea cu etichetele reale prin alpha.

Pot rula tutorialul fără GPU?

Da. Scriptul folosește CPU dacă PyTorch nu vede CUDA. Pentru test rapid pe CPU, rulează smoke testul cu --max_train_steps 2 --batch_size 1 --block_size 64.

Este modelul final gata de producție?

Nu. Directorul distilled-wikitext2-student este un artefact verificabil al tutorialului. Pentru producție sunt necesare mai multe date, evaluări, verificări de licență și teste de siguranță.

Surse

Facebook
X
WhatsApp
Knowledge Distillation pentru LLM-uri: distilare GPT-2 pe WikiText-2 cu KLDivLoss

Te-ar putea interesa si: