Phase 04 - Lesson 15
Visão em Tempo Real — Deploy na Borda (Edge)
Inferência na borda é a disciplina de fazer um modelo com 90 de acurácia rodar a 30 fps em um dispositivo com 2 GB de RAM. Cada ponto percentual de acurácia é trocado por milissegundos de latência.
Tipo: Aprender + Construir Linguagens: Python Pré-requisitos: Fase 4 Lição 04 (Classificação de Imagens), Fase 10 Lição 11 (Quantização) Tempo: ~75 minutos
Objetivos de Aprendizagem
- Medir latência de inferência, pico de memória e throughput para qualquer modelo PyTorch, e ler o trade-off entre FLOPs / parâmetros / latência
- Quantizar um modelo de visão para INT8 usando a quantização pós-treino do PyTorch e verificar perda de acurácia < 1%
- Exportar para ONNX e compilar com ONNX Runtime ou TensorRT; nomear as três falhas de exportação mais comuns e suas correções
- Explicar quando escolher MobileNetV3, EfficientNet-Lite, ConvNeXt-Tiny ou MobileViT para uma restrição de borda
O Problema
Um modelo de visão em tempo de treino é um monstro de ponto flutuante. 100M de parâmetros, 10 GFLOPs por passagem direta, 2 GB de VRAM. Nada disso cabe em um celular, na central de entretenimento de um carro, em uma câmera industrial ou em um drone. Entregar um sistema de visão significa encaixar as mesmas predições em um orçamento 100x menor.
Três botões fazem a maior parte do trabalho: escolha do modelo (uma arquitetura menor com a mesma receita), quantização (INT8 em vez de FP32) e o runtime de inferência (ONNX Runtime, TensorRT, Core ML, TFLite). Acertá-los é a diferença entre um demo que roda numa estação de trabalho e um produto que sai num módulo de câmera de $30.
Esta lição estabelece primeiro a disciplina de medição (você não pode otimizar o que não consegue medir), depois percorre os três botões. O objetivo não é aprender todo runtime de borda, mas saber quais alavancas existem e como verificar que cada uma faz o que você imagina.
O Conceito
Os três orçamentos
flowchart LR
M["Modelo"] --> LAT["Latência<br/>ms por imagem"]
M --> MEM["Memória<br/>pico MB"]
M --> PWR["Energia<br/>mJ por inferência"]
LAT --> SHIP["Decisão de<br/>entregar / não entregar"]
MEM --> SHIP
PWR --> SHIP
style LAT fill:#fecaca,stroke:#dc2626
style MEM fill:#fef3c7,stroke:#d97706
style PWR fill:#dbeafe,stroke:#2563eb
- Latência: p50, p95, p99. Tirar média apenas do p50 esconde o comportamento de cauda que importa para sistemas em tempo real.
- Pico de memória: o máximo que o dispositivo chega a ver, não a média em regime estacionário. Importa porque OOMs são fatais em alvos embarcados.
- Energia / consumo: milijoules por inferência em um dispositivo alimentado por bateria. Frequentemente aproximado por utilização de CPU/GPU * tempo.
Uma tabela de (modelo, latência, memória, acurácia) é a base de uma decisão de borda. Cada célula é medida no dispositivo alvo, não na estação de trabalho.
Disciplina de medição
Três regras que todo perfil de borda deve seguir:
- Aqueça o modelo com 5-10 passagens diretas fictícias antes de medir. Caches frios e compilação JIT produzem primeiros números não representativos.
- Sincronize cargas de trabalho de GPU com
torch.cuda.synchronize()antes e depois do bloco cronometrado. Sem isso você mede o despacho do kernel, não sua execução. - Fixe os tamanhos de entrada na resolução de produção. Latência em 224x224 não é latência em 512x512.
FLOPs como proxy
FLOPs (operações de ponto flutuante por inferência) é um proxy barato e independente de dispositivo para latência. Útil para comparação de arquiteturas, enganoso como wall-clock absoluto. Um modelo com 10% mais FLOPs pode ser 2x mais rápido na prática porque usa operações amigáveis ao hardware (convoluções depthwise compilam bem, convoluções grandes 7x7 não).
Regra: use FLOPs para busca de arquitetura, use latência no dispositivo para decisões de deploy.
Quantização em um parágrafo
Substitua pesos e ativações FP32 por INT8. O tamanho do modelo cai 4x, a largura de banda de memória cai 4x, o compute cai 2-4x em hardware que tem kernels INT8 (todo SoC móvel moderno, toda GPU NVIDIA com Tensor Cores). A perda de acurácia em tarefas de visão é tipicamente de 0,1-1 ponto percentual com quantização estática pós-treino.
Tipos:
- Dinâmica — quantiza pesos para INT8, ativações computadas em FP. Fácil, ganho de velocidade pequeno.
- Estática (pós-treino) — quantiza pesos + calibra faixas de ativação em um pequeno conjunto de calibração. Muito mais rápida que a dinâmica.
- Treino consciente de quantização (QAT) — simula a quantização durante o treino para que o modelo aprenda em torno dela. Melhor acurácia, precisa de dados rotulados.
Para visão, a quantização estática pós-treino dá 95% do benefício com 5% do esforço. Use QAT apenas quando a perda de acurácia do PTQ for inaceitável.
Pruning e destilação
- Pruning — remove pesos sem importância (baseado em magnitude) ou canais (estruturado). Funciona bem em modelos superparametrizados; menos útil em arquiteturas já compactas.
- Destilação — treina um aluno pequeno para imitar os logits de um professor grande. Frequentemente recupera a maior parte da acurácia perdida ao encolher o modelo. Padrão para modelos de borda em produção.
Os runtimes de inferência
- PyTorch eager — lento, não serve para deploy. Use apenas para desenvolvimento.
- TorchScript — legado. Superado por
torch.compilee exportação ONNX. - ONNX Runtime — o runtime neutro. CPU, CUDA, CoreML, TensorRT, OpenVINO têm todos providers ONNX. Comece por aqui.
- TensorRT — o compilador da NVIDIA. Melhor latência em GPUs NVIDIA (estação de trabalho e Jetson). Integra com ONNX Runtime ou standalone.
- Core ML — o runtime da Apple para iOS/macOS. Precisa de
.mlmodelou.mlpackage. - TFLite — o runtime do Google para Android/ARM. Precisa de
.tflite. - OpenVINO — o runtime da Intel para CPU/VPU. Precisa de
.xml+.bin.
Na prática: exporte PyTorch -> ONNX -> escolha o runtime para o alvo. ONNX é a língua franca.
Seletor de arquitetura de borda
| Orçamento | Modelo | Por quê |
|---|---|---|
| < 3M params | MobileNetV3-Small | Compila em todo lugar, boa baseline |
| 3-10M | EfficientNet-Lite-B0 | Melhor acurácia por param no TFLite |
| 10-20M | ConvNeXt-Tiny | Melhor acurácia-por-param, amigável a CPU |
| 20-30M | MobileViT-S ou EfficientViT | Transformer com acurácia ImageNet |
| 30-80M | Swin-V2-Tiny | Se o stack suportar window attention |
Quantize todos estes para INT8 a menos que tenha uma razão específica para não fazer.
Construa
Passo 1: Meça a latência corretamente
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),
}
Aqueça, sincronize, use time.perf_counter(). Reporte percentis, não apenas a média.
Passo 2: Contagem de parâmetros e FLOPs
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
Para projetos reais use fvcore.nn.FlopCountAnalysis ou ptflops; eles tratam corretamente cada tipo de módulo.
Passo 3: Quantização estática pós-treino
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
Três passos: configurar, preparar (inserir observers), calibrar com dados reais, converter (fundir + quantizar). Requer que o modelo esteja fundido (Conv -> BN -> ReLU -> ConvBnReLU), o que torch.ao.quantization.fuse_modules resolve.
Passo 4: Exportar para 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 é o padrão seguro em 2026. dynamic_axes permite rodar o modelo ONNX com tamanho de batch arbitrário.
Passo 5: Faça benchmark e compare os regimes
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")
Rode a mesma função para resnet50, efficientnet_v2_s e convnext_tiny e você terá a tabela comparativa que precisa para uma decisão de deploy.
Use
Stacks de produção convergem para um de três caminhos:
- Web / serverless: PyTorch -> ONNX -> ONNX Runtime (provider de CPU ou CUDA). Mais fácil, bom o suficiente para a maioria.
- Borda NVIDIA (Jetson, servidor GPU): PyTorch -> ONNX -> TensorRT. Melhor latência, maior esforço de engenharia.
- Mobile: PyTorch -> ONNX -> Core ML (iOS) ou TFLite (Android). Quantize antes de exportar.
Para medição, torch-tb-profiler, nvprof / nsys e o Instruments no macOS dão breakdowns camada a camada. benchmark_app (OpenVINO) e trtexec (TensorRT) dão números standalone via CLI.
Entregue
Esta lição produz:
outputs/prompt-edge-deployment-planner.md— um prompt que escolhe backbone, estratégia de quantização e runtime dado o dispositivo alvo e o SLA de latência.outputs/skill-latency-profiler.md— uma skill que escreve um script completo de benchmark de latência com warmup, sincronização, percentis e rastreamento de memória.
Exercícios
- (Fácil) Meça a latência p50 para
resnet18,mobilenet_v3_small,efficientnet_v2_seconvnext_tinyem 224x224 na CPU. Reporte a tabela e identifique qual arquitetura tem a melhor acurácia-por-ms. - (Médio) Aplique quantização estática pós-treino ao
mobilenet_v3_small. Reporte a latência FP32 vs INT8 e a perda de acurácia em um subconjunto de validação do CIFAR-10 ou similar. - (Difícil) Exporte
convnext_tinypara ONNX, rode-o através doonnxruntimecom oCPUExecutionProvidere compare a latência com a baseline eager do PyTorch. Identifique a primeira camada em que o ONNX Runtime é mais rápido e explique por quê.
Termos-Chave
| Termo | O que as pessoas dizem | O que realmente significa |
|---|---|---|
| Latência | "Quão rápido" | Tempo da entrada à saída; percentis p50/p95/p99, não a média |
| FLOPs | "Tamanho do modelo" | Operações de ponto flutuante por passagem direta; proxy aproximado do custo de compute |
| Quantização INT8 | "8 bits" | Substituir pesos/ativações FP32 por inteiros de 8 bits; ~4x menor, 2-4x mais rápido |
| PTQ | "Quantização pós-treino" | Quantizar um modelo treinado sem retreinar; fácil, geralmente suficiente |
| QAT | "Treino consciente de quantização" | Simular a quantização durante o treino; melhor acurácia, requer dados rotulados |
| ONNX | "O formato neutro" | Formato de troca de modelos suportado por todo runtime de inferência mainstream |
| TensorRT | "Compilador da NVIDIA" | Compila ONNX em um engine otimizado para GPUs NVIDIA |
| Destilação | "Professor -> aluno" | Treinar um modelo pequeno para imitar os logits de um modelo grande; recupera a maior parte da acurácia perdida |
Leitura Adicional
- EfficientNet (Tan & Le, 2019) — escalonamento composto para arquiteturas eficientes
- MobileNetV3 (Howard et al., 2019) — arquitetura mobile-first com h-swish e squeeze-excite
- A Practical Guide to TensorRT Optimization (NVIDIA) — como de fato obter os números de throughput do paper
- ONNX Runtime docs — quantização, otimização de grafo, seleção de provider