Skip to main content

Cyber AI România

Joi, 22 octombrie 2026

TorchAO int4 pe laptop: quantizare locală pentru un LLM cu OPT-125M

Pe un laptop AI PC, quantizarea int4 nu înseamnă automat că modelul rulează pe NPU. În acest tutorial folosim o variantă sigură și reproductibilă: CPU local, TorchAO, Hugging Face Transformers și un model mic, public, facebook/opt-125m. Rezultatul final este un director artifacts/opt-125m-torchao-int4-cpu/ cu modelul quantizat, un fișier artifacts/report.json cu loss-ul comparativ și un test de inferență determinist.

Dacă ai lucrat deja cu AWQ, GPTQ sau FP8, vezi și ghidul despre cum măsori compromisul dintre memorie, viteză și calitatea răspunsurilor. Aici rămânem strict pe TorchAO int4 weight-only, pe un singur flux local.

Ce construim

Construim un mic pipeline local pentru inferență LLM:

  • descarcă facebook/opt-125m;
  • aplică TorchAO int4 weight-only pe CPU, cu Int4WeightOnlyConfig;
  • salvează modelul quantizat în format compatibil save_pretrained;
  • reîncarcă modelul quantizat de pe disc;
  • calculează loss pe un set mic de evaluare local;
  • rulează o inferență scurtă și scrie totul în artifacts/report.json.

Atenție: exemplul este pentru laborator local, nu pentru producție. Pentru servire cu mai mulți utilizatori, ai nevoie de altă arhitectură, iar pentru asta e relevant ghidul despre deploy cu vLLM, PagedAttention și KV cache.

Mediul exact

Mediu folosit în tutorial:

  • Ubuntu 24.04 LTS;
  • Python 3.11;
  • pip într-un virtual environment;
  • PyTorch instalat din indexul oficial CPU;
  • torchao versiunea 0.15.0 sau mai nouă, deoarece documentația Transformers menționează suportul save_pretrained pentru TorchAO de la 0.15;
  • transformers, accelerate, safetensors, psutil.

Resurse minime realiste pentru acest exemplu:

  • 8 GB RAM;
  • 4 GB spațiu liber;
  • internet pentru descărcarea modelului;
  • 15-30 de minute la prima rulare, în funcție de conexiune și CPU.

Modelul folosit este facebook/opt-125m, ales pentru că este suficient de mic pentru test local. Nu folosi acest tutorial ca evaluare de calitate pentru modele mari; aici verificăm fluxul tehnic TorchAO int4.

Ce instalezi

Creează directorul proiectului:

mkdir -p torchao-int4-local
cd torchao-int4-local
python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip

Instalează PyTorch CPU din indexul oficial PyTorch:

pip install torch --index-url https://download.pytorch.org/whl/cpu

Instalează pachetele pentru quantizare și rulare:

pip install "torchao>=0.15.0" "transformers" "accelerate" "safetensors" "psutil"

Ce trebuie să vezi:

Successfully installed ...

Verifică versiunile:

python - <<'PY'
import torch
import torchao
import transformers
print("torch:", torch.__version__)
print("torchao:", torchao.__version__)
print("transformers:", transformers.__version__)
print("cuda available:", torch.cuda.is_available())
PY

Exemplu de output:

torch: 2.x.x+cpu
torchao: 0.15.0 sau mai nou
transformers: 4.x.x sau mai nou
cuda available: False

Pasul 1: creezi setul local de evaluare

Facem un set mic, local, fără date personale. Nu este un benchmark public, ci o verificare reproductibilă că modelul base și modelul int4 pot calcula loss pe aceleași texte.

mkdir -p data artifacts
cat > data/eval_prompts.jsonl <<'EOF'
{"text":"Cybersecurity teams should keep offline backups and test restore procedures."}
{"text":"A small business can reduce account takeover risk with passkeys and multi-factor authentication."}
{"text":"Local language models can be useful for drafting, but sensitive data still needs governance."}
EOF

Ce trebuie să vezi după verificare:

wc -l data/eval_prompts.jsonl

Output așteptat:

3 data/eval_prompts.jsonl

Pasul 2: scrii scriptul complet

Fișierul de mai jos face tot fluxul: încarcă modelul base, calculează loss, încarcă modelul quantizat int4, îl salvează, îl reîncarcă și scrie raportul.

cat > run_torchao_int4_cpu.py <<'PY'
import json
import os
import time
from pathlib import Path

import psutil
import torch
import torchao
import transformers
from packaging.version import Version
from torchao.quantization import Int4WeightOnlyConfig
from torchao.quantization.quantize_.workflows import Int4PackingFormat
from transformers import AutoModelForCausalLM, AutoTokenizer, TorchAoConfig

MODEL_ID = "facebook/opt-125m"
EVAL_FILE = Path("data/eval_prompts.jsonl")
OUT_DIR = Path("artifacts/opt-125m-torchao-int4-cpu")
REPORT_FILE = Path("artifacts/report.json")
GROUP_SIZE = 128
MAX_NEW_TOKENS = 24

def rss_mb():
    return round(psutil.Process(os.getpid()).memory_info().rss / 1024 / 1024, 2)

def read_eval_texts():
    texts = []
    with EVAL_FILE.open("r", encoding="utf-8") as handle:
        for line in handle:
            item = json.loads(line)
            texts.append(item["text"])
    return texts

def mean_loss(model, tokenizer, texts):
    losses = []
    model.eval()
    with torch.inference_mode():
        for text in texts:
            encoded = tokenizer(text, return_tensors="pt")
            encoded = {key: value.to(model.device) for key, value in encoded.items()}
            output = model(**encoded, labels=encoded["input_ids"])
            losses.append(float(output.loss.detach().cpu()))
    return sum(losses) / len(losses)

def generate_text(model, tokenizer):
    prompt = "Local AI inference helps teams"
    encoded = tokenizer(prompt, return_tensors="pt")
    encoded = {key: value.to(model.device) for key, value in encoded.items()}
    with torch.inference_mode():
        generated = model.generate(
            **encoded,
            max_new_tokens=MAX_NEW_TOKENS,
            do_sample=False,
            pad_token_id=tokenizer.eos_token_id,
        )
    return tokenizer.decode(generated[0], skip_special_tokens=True)

def dir_size_mb(path):
    total = 0
    for file_path in path.rglob("*"):
        if file_path.is_file():
            total += file_path.stat().st_size
    return round(total / 1024 / 1024, 2)

def main():
    if Version(torchao.__version__) < Version("0.15.0"):
        raise RuntimeError("TorchAO 0.15.0 sau mai nou este necesar pentru acest flux.")

    OUT_DIR.mkdir(parents=True, exist_ok=True)
    REPORT_FILE.parent.mkdir(parents=True, exist_ok=True)

    texts = read_eval_texts()
    tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)

    started = time.time()
    mem_before_base = rss_mb()
    base_model = AutoModelForCausalLM.from_pretrained(
        MODEL_ID,
        device_map="cpu",
        dtype="auto",
    )
    base_loss = mean_loss(base_model, tokenizer, texts)
    base_generation = generate_text(base_model, tokenizer)
    mem_after_base = rss_mb()
    del base_model

    quant_config = Int4WeightOnlyConfig(
        group_size=GROUP_SIZE,
        int4_packing_format=Int4PackingFormat.PLAIN_INT32,
    )
    quantization_config = TorchAoConfig(quant_type=quant_config)

    mem_before_quant = rss_mb()
    quantized_model = AutoModelForCausalLM.from_pretrained(
        MODEL_ID,
        device_map="cpu",
        dtype="auto",
        quantization_config=quantization_config,
    )
    quant_loss = mean_loss(quantized_model, tokenizer, texts)
    quant_generation = generate_text(quantized_model, tokenizer)
    mem_after_quant = rss_mb()

    quantized_model.save_pretrained(OUT_DIR)
    tokenizer.save_pretrained(OUT_DIR)
    saved_size = dir_size_mb(OUT_DIR)
    del quantized_model

    reloaded_model = AutoModelForCausalLM.from_pretrained(
        OUT_DIR,
        device_map="cpu",
        dtype=torch.bfloat16,
    )
    reloaded_loss = mean_loss(reloaded_model, tokenizer, texts)
    reloaded_generation = generate_text(reloaded_model, tokenizer)

    report = {
        "model_id": MODEL_ID,
        "device": "cpu",
        "torch": torch.__version__,
        "torchao": torchao.__version__,
        "transformers": transformers.__version__,
        "group_size": GROUP_SIZE,
        "int4_packing_format": "PLAIN_INT32",
        "eval_items": len(texts),
        "base_loss": round(base_loss, 6),
        "quantized_loss": round(quant_loss, 6),
        "reloaded_loss": round(reloaded_loss, 6),
        "base_generation": base_generation,
        "quantized_generation": quant_generation,
        "reloaded_generation": reloaded_generation,
        "rss_mb": {
            "before_base": mem_before_base,
            "after_base": mem_after_base,
            "before_quant": mem_before_quant,
            "after_quant": mem_after_quant,
        },
        "saved_model_dir": str(OUT_DIR),
        "saved_model_size_mb": saved_size,
        "elapsed_seconds": round(time.time() - started, 2),
    }

    REPORT_FILE.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
    print(json.dumps(report, indent=2, ensure_ascii=False))

if __name__ == "__main__":
    main()
PY

Ce trebuie să vezi:

test -f run_torchao_int4_cpu.py && echo "script creat"

Output așteptat:

script creat

Pasul 3: rulezi quantizarea int4

Prima rulare descarcă modelul de pe Hugging Face Hub. Nu întrerupe procesul în timpul salvării.

python run_torchao_int4_cpu.py

Ce trebuie să vezi la final: un JSON tipărit în terminal, cu câmpuri ca acestea:

"model_id": "facebook/opt-125m"
"device": "cpu"
"group_size": 128
"int4_packing_format": "PLAIN_INT32"
"eval_items": 3
"saved_model_dir": "artifacts/opt-125m-torchao-int4-cpu"

Valorile pentru base_loss, quantized_loss, reloaded_loss, elapsed_seconds și rss_mb pot diferi între laptopuri. Important este ca scriptul să ajungă la final și să scrie artifacts/report.json.

Dacă vrei o evaluare mai serioasă a răspunsurilor, nu te baza pe o singură generare. Pentru partea de evaluare cu arbitri LLM, vezi ghidul despre LLM-as-a-Judge fără rezultate înșelătoare.

Pasul 4: verifici fișierele produse

Verifică directorul modelului quantizat:

find artifacts/opt-125m-torchao-int4-cpu -maxdepth 1 -type f | sort

Ce trebuie să vezi: fișiere de configurare, tokenizer și greutățile modelului. Numele exact al fișierului de greutăți poate depinde de versiunea Transformers, dar directorul trebuie să conțină modelul salvat cu save_pretrained.

Verifică raportul:

python - <<'PY'
import json
from pathlib import Path

report = json.loads(Path("artifacts/report.json").read_text(encoding="utf-8"))

required = [
    "model_id",
    "torch",
    "torchao",
    "transformers",
    "base_loss",
    "quantized_loss",
    "reloaded_loss",
    "saved_model_dir",
    "saved_model_size_mb",
]

missing = [key for key in required if key not in report]
if missing:
    raise SystemExit(f"Lipsesc câmpuri: {missing}")

print("OK: raport complet")
print("Model:", report["model_id"])
print("TorchAO:", report["torchao"])
print("Loss base:", report["base_loss"])
print("Loss int4:", report["quantized_loss"])
print("Loss reîncărcat:", report["reloaded_loss"])
print("Director:", report["saved_model_dir"])
PY

Output așteptat:

OK: raport complet
Model: facebook/opt-125m
TorchAO: 0.15.0 sau mai nou
Loss base: valoare numerică
Loss int4: valoare numerică
Loss reîncărcat: valoare numerică
Director: artifacts/opt-125m-torchao-int4-cpu

Pasul 5: rulezi inferență doar din modelul reîncărcat

Acum confirmăm că nu depindem de modelul base din memorie. Folosim doar directorul salvat.

cat > infer_from_saved_int4.py <<'PY'
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

MODEL_DIR = "artifacts/opt-125m-torchao-int4-cpu"
PROMPT = "Local AI inference is useful because"

tokenizer = AutoTokenizer.from_pretrained(MODEL_DIR)
model = AutoModelForCausalLM.from_pretrained(
    MODEL_DIR,
    device_map="cpu",
    dtype=torch.bfloat16,
)

encoded = tokenizer(PROMPT, return_tensors="pt")
encoded = {key: value.to(model.device) for key, value in encoded.items()}

with torch.inference_mode():
    generated = model.generate(
        **encoded,
        max_new_tokens=24,
        do_sample=False,
        pad_token_id=tokenizer.eos_token_id,
    )

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

python infer_from_saved_int4.py

Ce trebuie să vezi:

Local AI inference is useful because ...

Textul complet poate varia între versiuni de biblioteci. Verificarea importantă este că scriptul încarcă modelul din artifacts/opt-125m-torchao-int4-cpu și produce text fără să redescarce modelul base.

Dacă folosești modelul într-un classifier, partea de quantizare nu rezolvă singură calibrarea probabilităților. Pentru asta, vezi tutorialul despre ECE și temperature scaling pentru probabilități LLM.

Verificarea finală

Rulează toate verificările într-o singură comandă:

python run_torchao_int4_cpu.py
python infer_from_saved_int4.py
python - <<'PY'
import json
from pathlib import Path

model_dir = Path("artifacts/opt-125m-torchao-int4-cpu")
report_file = Path("artifacts/report.json")

assert model_dir.exists(), "Directorul modelului lipsește"
assert report_file.exists(), "Raportul lipsește"

report = json.loads(report_file.read_text(encoding="utf-8"))
assert report["model_id"] == "facebook/opt-125m"
assert report["device"] == "cpu"
assert report["group_size"] == 128
assert report["int4_packing_format"] == "PLAIN_INT32"
assert report["eval_items"] == 3

for key in ["base_loss", "quantized_loss", "reloaded_loss"]:
    assert isinstance(report[key], float), f"{key} nu este numeric"

print("VERIFICARE COMPLETĂ: model int4 salvat, reîncărcat și evaluat.")
PY

Output așteptat:

VERIFICARE COMPLETĂ: model int4 salvat, reîncărcat și evaluat.

Dacă vezi acest mesaj, ai un flux local complet: date de evaluare, model base, quantizare int4 TorchAO, salvare, reîncărcare, loss și inferență.

Probleme frecvente

RuntimeError: TorchAO 0.15.0 sau mai nou este necesar pentru acest flux.

Cauza: ai o versiune veche de TorchAO. Rezolvare:

pip install --upgrade "torchao>=0.15.0"

ModuleNotFoundError: No module named 'torchao'

Cauza: mediul virtual nu este activ sau pachetul nu a fost instalat. Rezolvare:

source .venv/bin/activate
pip install "torchao>=0.15.0"

OSError: facebook/opt-125m is not a local folder and is not a valid model identifier

Cauza: nu ai conexiune la Hugging Face Hub sau există o problemă de acces la rețea. Rezolvare: verifică internetul și rulează din nou aceeași comandă.

python run_torchao_int4_cpu.py

RuntimeError la încărcarea modelului int4 pe alt dispozitiv

Cauza: documentația Transformers precizează că pentru int4 modelul trebuie încărcat pe același tip de dispozitiv pe care a fost quantizat, deoarece layout-ul este specific dispozitivului. În acest tutorial quantizăm și reîncărcăm pe CPU. Rezolvare: păstrează device_map="cpu" în scripturile de quantizare și inferență.

Procesul se oprește din lipsă de memorie

Cauza: deși OPT-125M este mic, în timpul fluxului există momente în care modelul base și modelul quantizat sunt încărcate succesiv. Rezolvare: închide aplicațiile mari și rulează din nou într-un terminal curat.

python run_torchao_int4_cpu.py

Revenire și curățenie

Dacă vrei să revii la starea inițială, șterge doar artefactele create de tutorial și mediul virtual:

rm -rf artifacts data run_torchao_int4_cpu.py infer_from_saved_int4.py .venv

Ce trebuie să vezi:

test ! -d artifacts && test ! -d .venv && echo "curățenie completă"

Output așteptat:

curățenie completă

Comanda nu atinge alte directoare. Nu rula variante cu rm -rf / sau căi absolute pe care nu le-ai verificat.

Mai departe

Următorul pas practic este să înlocuiești facebook/opt-125m cu un model mai apropiat de cazul tău, dar să păstrezi aceeași structură: evaluare înainte, quantizare, salvare, reîncărcare, evaluare după. Al doilea pas este să extinzi data/eval_prompts.jsonl cu exemple reale, fără date personale sau secrete. Al treilea pas este să compari TorchAO int4 cu o altă metodă de quantizare, folosind aceeași evaluare, nu impresii dintr-un singur prompt.

Întrebări frecvente

Rulează acest tutorial pe NPU-ul unui AI PC?

Nu. Tutorialul folosește CPU local cu TorchAO și Transformers. „AI PC” înseamnă aici rulare locală pe laptop, nu accelerare NPU.

De ce folosim facebook/opt-125m și nu un model de 7B?

Pentru reproductibilitate pe laptopuri obișnuite. Modelul OPT-125M permite testarea fluxului TorchAO int4 fără cerințe mari de memorie.

Pot încărca modelul int4 salvat pe GPU după ce l-am quantizat pe CPU?

Pentru int4, documentația Transformers precizează că modelul trebuie încărcat pe același tip de dispozitiv pe care a fost quantizat. În tutorial, salvarea și reîncărcarea sunt pe CPU.

Loss-ul quantizat trebuie să fie identic cu loss-ul modelului base?

Nu. Tutorialul verifică faptul că fluxul rulează complet și că loss-ul este numeric pentru același set local. Quantizarea poate schimba loss-ul și textul generat.

Surse

Facebook
X
WhatsApp
TorchAO int4 pe laptop: quantizare locală pentru un LLM cu OPT-125M

Te-ar putea interesa si: