Faza 04 · lecția 15

Viziune în timp real — implementare la marginea rețelei

Scopul lecției: Inferența la marginea rețelei este disciplina de a face ca un model cu acuratețe de 90 să ruleze la 30 fps pe un dispozitiv cu 2 GB de RAM. Fiecare punct procentual de acuratețe este negociat în raport cu milisecundele de latență.

Versiunea curentă AlexBred.com: primele 100 de lecții ale programului în limba română.

Curs
AI Engineering from Scratch
Fază
Viziune computerizată
Lectură
15 min.
Verificat
Cuprinsul lecției
  1. Obiective de învățare
  2. Problema
  3. Conceptul
  4. Cele trei bugete
  5. Disciplina măsurării
  6. FLOPs ca aproximare
  7. Cuantizarea într-un paragraf
  8. Pruning și distilare
  9. Runtime-urile de inferență
  10. Selectorul de arhitecturi edge
  11. Construiți
  12. Pasul 1: Măsurați corect latența
  13. Pasul 2: Numărul de parametri și FLOP-uri
  14. Pasul 3: Cuantizarea statică post-antrenare
  15. Pasul 4: Exportați în ONNX
  16. Pasul 5: Comparați regimurile prin benchmark
  17. Utilizați
  18. Livrați
  19. Exerciții
  20. Termeni-cheie
  21. Lecturi suplimentare

Inferența la marginea rețelei este disciplina de a face ca un model cu acuratețe de 90 să ruleze la 30 fps pe un dispozitiv cu 2 GB de RAM. Fiecare punct procentual de acuratețe este negociat în raport cu milisecundele de latență.

Tip: Învățați + construiți Limbaje: Python Cerințe prealabile: Faza 4 Lecția 04 (Clasificarea imaginilor), Faza 10 Lecția 11 (Cuantizare) Timp: ~75 de minute

Obiective de învățare

  • Măsurați latența de inferență, memoria maximă și debitul pentru orice model PyTorch și interpretați compromisul dintre FLOPs / parametri / latență
  • Cuantizați un model de viziune la INT8 folosind cuantizarea post-antrenare a PyTorch și verificați o pierdere de acuratețe < 1%
  • Exportați în ONNX și compilați cu ONNX Runtime sau TensorRT; numiți cele mai frecvente trei eșecuri de export și corecțiile lor
  • Explicați când să alegeți MobileNetV3, EfficientNet-Lite, ConvNeXt-Tiny sau MobileViT pentru o constrângere edge

Problema

Un model de viziune din timpul antrenării este un monstru în virgulă mobilă: 100M de parametri, 10 GFLOPs pentru fiecare trecere înainte, 2 GB de VRAM. Nimic din toate acestea nu încape pe un telefon, unitatea de infotainment a unei mașini, o cameră industrială sau o dronă. Livrarea unui sistem de viziune înseamnă încadrarea acelorași predicții într-un buget de 100x mai mic.

Trei reglaje fac cea mai mare parte a muncii: alegerea modelului (o arhitectură mai mică, cu aceeași rețetă), cuantizarea (INT8 în loc de FP32) și runtime-ul de inferență (ONNX Runtime, TensorRT, Core ML, TFLite). Alegerea lor corectă face diferența între un demo care rulează pe o stație de lucru și un produs livrat pe un modul de cameră de 30 de dolari.

Această lecție stabilește mai întâi disciplina măsurării (nu puteți optimiza ceea ce nu puteți măsura), apoi parcurge cele trei reglaje. Obiectivul nu este învățarea fiecărui runtime edge, ci cunoașterea pârghiilor disponibile și verificarea faptului că fiecare face ceea ce credeți că face.

Conceptul

Cele trei bugete

Диаграмма к уроку «Viziune în timp real — implementare la marginea rețelei»

  • Latență: p50, p95, p99. Calcularea mediei numai pentru p50 ascunde comportamentul de coadă care contează în sistemele în timp real.
  • Memorie maximă: maximul văzut vreodată de dispozitiv, nu media în regim staționar. Contează deoarece erorile OOM sunt fatale pe ținte embedded.
  • Putere / energie: milijouli per inferență pe un dispozitiv alimentat de baterie. Este aproximată adesea prin utilizarea CPU/GPU * timp.

Un tabel cu (model, latență, memorie, acuratețe) stă la baza unei decizii edge. Fiecare celulă este măsurată pe dispozitivul-țintă, nu pe stația de lucru.

Disciplina măsurării

Trei reguli pe care ar trebui să le urmeze orice profil edge:

  1. Încălziți modelul cu 5–10 treceri înainte fictive înainte de măsurare. Cache-urile reci și compilarea JIT produc primele valori nereprezentative.
  2. Sincronizați sarcinile GPU cu torch.cuda.synchronize() înainte și după blocul cronometrat. Fără aceasta măsurați dispecerizarea kernelului, nu execuția kernelului.
  3. Fixați dimensiunile de intrare la rezoluția din producție. Latența la 224x224 nu este latența la 512x512.

FLOPs ca aproximare

FLOPs (operații în virgulă mobilă per inferență) reprezintă o aproximare ieftină, independentă de dispozitiv, a latenței. Sunt utile pentru compararea arhitecturilor, dar induc în eroare ca timp absolut de ceas. Un model cu 10% mai multe FLOPs poate fi de 2x mai rapid în practică deoarece folosește operații favorabile hardware-ului (convoluțiile depthwise se compilează bine, convoluțiile mari 7x7 nu).

Regulă: utilizați FLOPs pentru căutarea arhitecturii și latența pe dispozitiv pentru deciziile de implementare.

Notă tehnică a traducerii: FLOPs nu stabilesc universal latența: aceasta depinde de operatori, formele tensorilor, memorie, compilator, runtime și dispozitiv. Afirmațiile despre operații care „se compilează bine” sunt euristici dependente de hardware; măsurarea pe ținta reală rămâne criteriul de decizie.

Cuantizarea într-un paragraf

Înlocuiți ponderile și activările FP32 cu INT8. Dimensiunea modelului scade de 4x, lățimea de bandă a memoriei scade de 4x, iar calculul scade de 2–4x pe hardware cu kerneluri INT8 (fiecare SoC mobil modern, fiecare GPU NVIDIA cu Tensor Cores). Pierderea de acuratețe în sarcinile de viziune este în mod tipic de 0,1–1 puncte procentuale cu cuantizarea statică post-antrenare.

Tipuri:

  • Dinamică — cuantizați ponderile la INT8, iar activările sunt calculate în FP. Ușor de aplicat, cu accelerare redusă.
  • Statică (post-antrenare) — cuantizați ponderile și calibrați intervalele activărilor pe un set mic de calibrare. Mult mai rapidă decât cea dinamică.
  • Antrenare conștientă de cuantizare (quantisation-aware training, QAT) — simulați cuantizarea în timpul antrenării, astfel încât modelul să învețe să o compenseze. Oferă cea mai bună acuratețe, dar necesită date etichetate.

Notă tehnică a traducerii: În cuantizarea dinamică, parametrii de cuantizare ai activărilor, precum scala și punctul zero, sunt calculați la rulare, iar activarea este cuantizată chiar înaintea operației. În fluxul clasic PyTorch quantize_dynamic, tutorialul oficial limitează PTQ dinamică generală la nn.Linear și nn.LSTM; pentru nn.Conv2d se folosesc în mod obișnuit cuantizarea statică sau QAT.

Pentru viziune, cuantizarea statică post-antrenare oferă 95% din beneficiu cu 5% din efort. Utilizați QAT numai atunci când pierderea de acuratețe cauzată de PTQ este inacceptabilă.

Notă tehnică a traducerii: Reducerea cu aproximativ 4x a stocării tensorilor cuantizați rezultă din trecerea de la 32 la 8 biți, dar accelerarea și pierderea de acuratețe nu sunt garanții. Ele depind de model, operatori, setul de calibrare, backend și hardware; suportul INT8 nu implică automat o latență mai mică pentru fiecare graf de calcul.

Pruning și distilare

  • Pruning — eliminați ponderile neimportante (bazat pe magnitudine) sau canalele (structurat). Funcționează bine pe arhitecturi supraparametrizate; este mai puțin util pe arhitecturi deja compacte.
  • Distilare — antrenați un student mic să imite logiturile unui profesor mare. Recuperează adesea cea mai mare parte a acurateții pierdute prin micșorarea modelului. Este standard pentru modelele edge de producție.

Runtime-urile de inferență

  • PyTorch eager — lent, nu pentru implementare. Folosiți-l doar pentru dezvoltare.
  • TorchScript — moștenit. A fost înlocuit de torch.compile și de exportul ONNX.
  • ONNX Runtime — runtime-ul neutru. CPU, CUDA, CoreML, TensorRT, OpenVINO au toate provideri ONNX. Începeți de aici.
  • TensorRT — compilatorul NVIDIA. Oferă cea mai bună latență pe GPU-urile NVIDIA (stație de lucru și Jetson). Se integrează cu ONNX Runtime sau rulează independent.
  • Core ML — runtime-ul Apple pentru iOS/macOS. Necesită .mlmodel sau .mlpackage.
  • TFLite — runtime-ul Google pentru Android/ARM. Necesită .tflite.
  • OpenVINO — runtime-ul Intel pentru CPU/VPU. Necesită .xml + .bin.

În practică: exportați PyTorch -> ONNX -> alegeți runtime-ul pentru țintă. ONNX este lingua franca.

Notă tehnică a traducerii: ONNX nu este o cale de conversie universală: suportul operatorilor, formele dinamice, versiunile opset și back-endurile diferă între runtime-uri. Pentru Core ML și TFLite sunt necesare conversii și validări specifice țintei; exportul ONNX reușit nu garantează rularea sau performanța pe fiecare runtime.

Notă tehnică a traducerii: TorchScript este depreciat, însă torch.compile optimizează execuția în PyTorch și nu produce singur un format portabil pentru implementarea embedded. Pentru export, PyTorch recomandă torch.export; pentru ținte edge, ExecuTorch îl coboară într-un fișier .pte specializat pentru backend. ONNX rămâne o rută separată de schimb între framework-uri și runtime-uri.

Selectorul de arhitecturi edge

Buget Model De ce
< 3M parametri MobileNetV3-Small Se compilează peste tot, bază de comparație bună
3–10M EfficientNet-Lite-B0 Cea mai bună acuratețe per parametru în TFLite
10–20M ConvNeXt-Tiny Cea mai bună acuratețe per parametru, favorabilă CPU
20–30M MobileViT-S sau EfficientViT Transformer cu acuratețe ImageNet
30–80M Swin-V2-Tiny Dacă stack-ul acceptă atenția pe ferestre

Cuantizați toate aceste modele la INT8 dacă nu aveți un motiv specific să procedați altfel.

Notă tehnică a traducerii: Tabelul este un punct de plecare, nu un clasament universal. Acuratețea, memoria și latența depind de varianta exactă a modelului, rezoluție, setul de date, compilator, runtime și dispozitiv; comparați modelele cu același protocol pe ținta de implementare.

cnn-param-count

Construiți

Pasul 1: Măsurați corect latența

import time
import torch

def measure_latency(model, input_shape, device="cpu", warmup=10, iters=50):
    model = model.to(device).eval()
    x = torch.randn(input_shape, device=device)
    with torch.no_grad():
        for _ in range(warmup):
            model(x)
        if device == "cuda":
            torch.cuda.synchronize()
        times = []
        for _ in range(iters):
            if device == "cuda":
                torch.cuda.synchronize()
            t0 = time.perf_counter()
            model(x)
            if device == "cuda":
                torch.cuda.synchronize()
            times.append((time.perf_counter() - t0) * 1000)
    times.sort()
    return {
        "p50_ms": times[len(times) // 2],
        "p95_ms": times[int(len(times) * 0.95)],
        "p99_ms": times[int(len(times) * 0.99)],
        "mean_ms": sum(times) / len(times),
    }

Încălziți, sincronizați, utilizați time.perf_counter(). Raportați percentilele, nu doar media.

Pasul 2: Numărul de parametri și FLOP-uri

def parameter_count(model):
    return sum(p.numel() for p in model.parameters())

def flops_estimate(model, input_shape):
    """
    Rough FLOP count for a conv/linear-only model. For production use `fvcore` or `ptflops`.
    """
    total = 0
    def conv_hook(m, inp, out):
        nonlocal total
        c_out, c_in, kh, kw = m.weight.shape
        h, w = out.shape[-2:]
        total += 2 * c_in * c_out * kh * kw * h * w
    def linear_hook(m, inp, out):
        nonlocal total
        total += 2 * m.in_features * m.out_features
    hooks = []
    for m in model.modules():
        if isinstance(m, torch.nn.Conv2d):
            hooks.append(m.register_forward_hook(conv_hook))
        elif isinstance(m, torch.nn.Linear):
            hooks.append(m.register_forward_hook(linear_hook))
    model.eval()
    with torch.no_grad():
        model(torch.randn(input_shape))
    for h in hooks:
        h.remove()
    return total

Pentru proiecte reale utilizați fvcore.nn.FlopCountAnalysis sau ptflops; acestea tratează corect fiecare tip de modul.

Notă tehnică a traducerii: Estimatorul din cod este numai orientativ. Nu multiplică după dimensiunea lotului și calculează incorect convoluțiile grupate (inclusiv depthwise), deoarece nu ia în calcul groups; de asemenea, nu numără toate operațiile unui model real. Folosiți un profiler validat sau o analiză FLOP care acoperă arhitectura efectivă.

Pasul 3: Cuantizarea statică post-antrenare

def quantise_ptq(model, calibration_loader, backend="x86"):
    import torch.ao.quantization as tq
    model = model.eval().cpu()
    model.qconfig = tq.get_default_qconfig(backend)
    tq.prepare(model, inplace=True)
    with torch.no_grad():
        for x, _ in calibration_loader:
            model(x)
    tq.convert(model, inplace=True)
    return model

Patru pași: configurați, pregătiți (inserați observatori), calibrați cu date reale, convertiți (fuzionați + cuantizați). Necesită ca modelul să fie fuzionat (Conv -> BN -> ReLU -> ConvBnReLU), operație pe care o gestionează torch.ao.quantization.fuse_modules.

Notă tehnică a traducerii: În eager mode, fuziunea trebuie făcută înainte de prepare; convert transformă modulele pregătite, nu fuzionează automat orice secvență. Backend-ul trebuie să fie disponibil pentru platforma țintă și, după caz, trebuie selectat și motorul de cuantizare PyTorch; acest exemplu este o schiță, nu o rețetă completă pentru fiecare arhitectură. Documentația PyTorch curentă recomandă migrarea fluxurilor torch.ao.quantization către torchao.

Pasul 4: Exportați în ONNX

def export_onnx(model, sample_input, path="model.onnx"):
    model = model.eval()
    torch.onnx.export(
        model,
        sample_input,
        path,
        input_names=["input"],
        output_names=["output"],
        dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}},
        opset_version=17,
    )
    return path

opset_version=17 este valoarea implicită sigură în 2026. dynamic_axes vă permite să rulați modelul ONNX cu o dimensiune arbitrară a lotului.

Notă tehnică a traducerii: Nu există o valoare opset „sigură” pentru toate țintele: alegeți versiunea compatibilă cu exporterul PyTorch, operatorii modelului și runtime-ul de implementare. dynamic_axes din exemplu face dinamică numai dimensiunea lotului, nu și dimensiunile spațiale ale imaginii; în exporterul actual cu dynamo=True, PyTorch recomandă dynamic_shapes în locul lui dynamic_axes.

Pasul 5: Comparați regimurile prin benchmark

import torch.nn as nn
from torchvision.models import mobilenet_v3_small

def compare_regimes():
    model = mobilenet_v3_small(weights=None, num_classes=10)
    params = parameter_count(model)
    flops = flops_estimate(model, (1, 3, 224, 224))
    lat_fp32 = measure_latency(model, (1, 3, 224, 224), device="cpu")
    print(f"FP32 MobileNetV3-Small: {params:,} params  {flops/1e9:.2f} GFLOPs  "
          f"p50={lat_fp32['p50_ms']:.2f}ms  p95={lat_fp32['p95_ms']:.2f}ms")

Rulați aceeași funcție pentru resnet50, efficientnet_v2_s și convnext_tiny și obțineți tabelul de comparație necesar pentru o decizie de implementare.

Utilizați

Stack-urile de producție converg către una dintre trei căi:

  • Web / serverless: PyTorch -> ONNX -> ONNX Runtime (provider CPU sau CUDA). Cea mai simplă, suficient de bună pentru majoritatea cazurilor.
  • Edge NVIDIA (Jetson, server GPU): PyTorch -> ONNX -> TensorRT. Cea mai bună latență, cel mai mare efort de inginerie.
  • Mobil: PyTorch -> ONNX -> Core ML (iOS) sau TFLite (Android). Cuantizați înainte de export.

Pentru măsurare, torch-tb-profiler, nvprof / nsys și Instruments pe macOS oferă o detaliere pe straturi. benchmark_app (OpenVINO) și trtexec (TensorRT) oferă valori CLI independente.

Livrați

Această lecție produce:

  • outputs/prompt-edge-deployment-planner.md — un prompt care alege backbone-ul, strategia de cuantizare și runtime-ul date fiind dispozitivul-țintă și SLA-ul de latență.
  • outputs/skill-latency-profiler.md — o competență care scrie un script complet de benchmarking al latenței, cu încălzire, sincronizare, percentile și urmărirea memoriei.

Exerciții

  1. (Ușor) Măsurați latența p50 pentru resnet18, mobilenet_v3_small, efficientnet_v2_s și convnext_tiny la 224x224 pe CPU. Raportați tabelul și identificați arhitectura cu cea mai bună acuratețe per ms.
  2. (Mediu) Aplicați cuantizarea statică post-antrenare la mobilenet_v3_small. Raportați latența FP32 față de INT8 și pierderea de acuratețe pe un subset de testare CIFAR-10 sau similar.
  3. (Dificil) Exportați convnext_tiny în ONNX, rulați-l prin onnxruntime cu CPUExecutionProvider și comparați latența cu baza PyTorch eager. Identificați primul strat la care ONNX Runtime este mai rapid și explicați de ce.

Termeni-cheie

Termen Cum este numit în practică Ce înseamnă de fapt
Latență „Cât de repede” Timpul de la intrare la ieșire; percentile p50/p95/p99, nu media
FLOPs „Dimensiunea modelului” Operații în virgulă mobilă per trecere înainte; aproximare grosieră a costului de calcul
Cuantizare INT8 „8 biți” Înlocuiește ponderile/activările FP32 cu întregi pe 8 biți; cu ~4x mai mică, de 2–4x mai rapidă
PTQ „Cuantizare post-antrenare” Cuantizează un model antrenat fără reantrenare; ușor, de obicei suficient
QAT „Antrenare conștientă de cuantizare” Simulează cuantizarea în timpul antrenării; cea mai bună acuratețe, necesită date etichetate
ONNX „Formatul neutru” Format de schimb al modelelor, sprijinit de fiecare runtime principal de inferență
TensorRT „Compilatorul NVIDIA” Compilează ONNX într-un engine optimizat pentru GPU-urile NVIDIA
Distilare „Profesor -> student” Antrenează un model mic să imite logiturile unui model mare; recuperează cea mai mare parte a acurateții pierdute

Lecturi suplimentare

Sursă: Originalul în limba engleză

Navigare: ← Lecția 04.14 — Transformere vizuale (ViT) · Faza 4 — Viziune computerizată · Lecția 04.16 — Construiți un flux complet de viziune — proiect integrator → · Catalog complet