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:

  1. 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.
  2. 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.
  3. 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.compile e 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 .mlmodel ou .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

  1. (Fácil) Meça a latência p50 para resnet18, mobilenet_v3_small, efficientnet_v2_s e convnext_tiny em 224x224 na CPU. Reporte a tabela e identifique qual arquitetura tem a melhor acurácia-por-ms.
  2. (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.
  3. (Difícil) Exporte convnext_tiny para ONNX, rode-o através do onnxruntime com o CPUExecutionProvider e 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

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).