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ă.
Cuprinsul lecției
- Obiective de învățare
- Problema
- Conceptul
- Cele trei bugete
- Disciplina măsurării
- FLOPs ca aproximare
- Cuantizarea într-un paragraf
- Pruning și distilare
- Runtime-urile de inferență
- Selectorul de arhitecturi edge
- Construiți
- Pasul 1: Măsurați corect latența
- Pasul 2: Numărul de parametri și FLOP-uri
- Pasul 3: Cuantizarea statică post-antrenare
- Pasul 4: Exportați în ONNX
- Pasul 5: Comparați regimurile prin benchmark
- Utilizați
- Livrați
- Exerciții
- Termeni-cheie
- 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
- 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:
- Încălziți modelul cu 5–10 treceri înainte fictive înainte de măsurare. Cache-urile reci și compilarea JIT produc primele valori nereprezentative.
- 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. - 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ă lann.Linearșinn.LSTM; pentrunn.Conv2dse 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ă
.mlmodelsau.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.compileoptimizează 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.ptespecializat 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;converttransformă 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 fluxurilortorch.ao.quantizationcătretorchao.
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_axesdin exemplu face dinamică numai dimensiunea lotului, nu și dimensiunile spațiale ale imaginii; în exporterul actual cudynamo=True, PyTorch recomandădynamic_shapesîn locul luidynamic_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
- (Ușor) Măsurați latența p50 pentru
resnet18,mobilenet_v3_small,efficientnet_v2_sșiconvnext_tinyla 224x224 pe CPU. Raportați tabelul și identificați arhitectura cu cea mai bună acuratețe per ms. - (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. - (Dificil) Exportați
convnext_tinyîn ONNX, rulați-l prinonnxruntimecuCPUExecutionProviderș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
- EfficientNet (Tan & Le, 2019) — scalarea compusă pentru arhitecturi eficiente
- MobileNetV3 (Howard et al., 2019) — arhitectură pentru dispozitive mobile cu h-swish și squeeze-excite
- A Practical Guide to TensorRT Optimization (NVIDIA) — cum să obțineți în practică valorile de debit din lucrare
- ONNX Runtime docs — cuantizare, optimizarea grafului, selectarea providerilor
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