Ciclo de vida de visão computacional em frotas de câmeras na borda com SageMaker SDK v3

O problema que aparece depois que o piloto funciona

Uma câmera, um modelo, uma linha de inspeção — o piloto vai bem. Aí a fábrica pede o mesmo resultado em 40 linhas, cada uma com várias câmeras. E cada câmera enxerga o mundo de um jeito diferente: ângulo distinto, reflexo diferente na superfície de aço inox, iluminação que muda dependendo de qual porta do galpão está aberta.

Na prática, um único modelo treinado com imagens misturadas de todas as câmeras performa mal o suficiente para que cada câmera acabe precisando do seu próprio modelo ajustado. O problema real deixa de ser “treinar um bom modelo” e passa a ser: treinar, avaliar, versionar e distribuir milhares deles sem intervenção humana em cada um.

A AWS publicou uma arquitetura de referência que endereça exatamente esse cenário, agora atualizada para o Amazon SageMaker Python SDK v3. O post original de 2023 Build an end-to-end MLOps pipeline for visual quality inspection at the edge é o ponto de partida, e esta versão substitui o SageMaker Edge Manager (descontinuado) pelo SageMaker Neo para otimização de modelos para dispositivos de borda. O código completo está disponível no repositório aws-samples no GitHub.

Visão geral da solução

A arquitetura se divide em duas metades: uma camada de orquestração de frota e um pipeline de treinamento por câmera.

O pipeline de treinamento é parametrizado por câmera e executa 13 etapas. Três etapas iniciais rodam em paralelo: pré-processamento das imagens, busca do modelo de anomalia mais recente aprovado e busca do classificador mais recente aprovado para aquela câmera. A partir daí, dois ramos de treinamento seguem em paralelo:

  • Ramo de anomalia: um autoencoder convolucional pequeno que aprende como é um produto normal e detecta anomalias pelo erro de reconstrução.
  • Ramo de classificação: um ResNet18 ajustado fino para confirmar o tipo de defeito.

Cada ramo exporta o modelo para o formato ONNX, avalia o artefato exportado contra dados reservados e passa por um gate de qualidade. Se o gate for aprovado, o modelo é compilado com o SageMaker Neo para o hardware-alvo configurado e só então registrado no SageMaker Model Registry. O resultado: todo pacote aprovado já chega ao registry com um artefato compilado e pronto para rodar no dispositivo.

A camada de orquestração controla o ritmo da frota. Requisições de re-treinamento chegam em uma fila Serviço de Filas Simples da Amazon (Amazon SQS) do tipo FIFO. Uma função AWS Lambda inicia uma execução do Step Functions por requisição. Antes de disparar o pipeline, a execução precisa adquirir um slot via semáforo no DynamoDB — o que garante que uma rajada de 500 requisições não vire 500 pipelines simultâneos. Conclusões são publicadas no Serviço de Notificação Simples da Amazon (Amazon SNS), e falhas operacionais chegam ao mesmo tópico via Amazon EventBridge, com o erro original preservado.

Definindo o pipeline com o SDK v3

O SDK v3 reorganizou o SageMaker Python SDK em pacotes modulares. Os imports mostram a nova estrutura:

from sagemaker.train import ModelTrainer
from sagemaker.train.configs import InputData, Compute, OutputDataConfig
from sagemaker.core.training.configs import MetricDefinition
from sagemaker.serve.model_builder import ModelBuilder
from sagemaker.mlops.workflow.pipeline import Pipeline
from sagemaker.mlops.workflow.steps import ProcessingStep, TrainingStep
from sagemaker.mlops.workflow.model_step import ModelStep
from sagemaker.mlops.workflow.condition_step import ConditionStep
from sagemaker.core.workflow.conditions import ConditionGreaterThanOrEqualTo
from sagemaker.core.workflow.functions import JsonGet

Instalar sagemaker>=3.0.0,<4 puxa automaticamente os pacotes sagemaker-core, sagemaker-train, sagemaker-serve e sagemaker-mlops. As configurações de treinamento que no v2 ficavam espalhadas nos argumentos do construtor Estimator agora vivem em objetos de configuração tipados passados ao ModelTrainer. Este é o trainer do ramo de anomalia, exatamente como aparece no exemplo:

anomalib_trainer = ModelTrainer(
    training_image=custom_training_image_uri,
    compute=Compute(
        instance_type=training_instance_type.to_string(),
        instance_count=1,
    ),
    base_job_name="cv-pipeline-anomalib-train",
    sagemaker_session=pipeline_session,
    role=role_arn,
    hyperparameters={
        "program": "train_anomalib.py",
        "model_name": "autoencoder",
        "image_size": "256",
        "num_epochs": "3",
        "batch_size": "16",
        "learning_rate": "0.001",
    },
    input_data_config=anomalib_input_data,
    output_data_config=OutputDataConfig(
        s3_output_path=run_prefix("anomalib-training")
    ),
).with_metric_definitions(
    [
        MetricDefinition(
            name="image_auroc",
            regex=r"validation:image_auroc=([0-9]*\.?[0-9]+)",
        ),
    ]
)

Dois comportamentos do SDK v3 merecem atenção especial ao usar o pipeline dentro de uma aplicação AWS CDK. Primeiro, o SDK v3 valida o papel de execução do pipeline contra o Gerenciamento de Identidade e Acesso (IAM) em tempo de síntese — não em tempo de execução. Isso significa que uma aplicação CDK não consegue criar o papel e o pipeline no mesmo deploy, porque o papel ainda não existe quando o SDK tenta validá-lo. A solução do exemplo usa um deploy em duas fases controlado por um flag de contexto: a primeira passagem cria o papel, o bucket e o repositório do Registro de Contêineres Elástico da Amazon (Amazon ECR); a segunda sintetiza o pipeline contra o papel já existente. Segundo, o validador de papéis do v3 simula permissões de pull no ECR sem um ARN de recurso específico, então um papel com escopo restrito a um único repositório ECR falha na validação mesmo que funcionasse em runtime — o exemplo usa uma concessão ECR mais ampla por isso.

Gates de qualidade pensados para dados de defeitos

Todo modelo treinado é avaliado antes do registro, e a avaliação roda contra o artefato ONNX exportado — não contra o modelo PyTorch em memória. Isso importa: o artefato que passa pelo gate é exatamente o artefato que vai para o dispositivo, então um problema de exportação não consegue escorregar por uma métrica de treinamento saudável.

O ramo de anomalia exige AUROC de pelo menos 0,8. O ramo de classificação exige dois critérios simultaneamente, e a escolha dessas métricas reflete uma propriedade fundamental dos dados de defeitos: desbalanceamento de classes. No exemplo, cerca de 90% das imagens são de produtos bons — o que é realista para uma linha de produção. Nessa distribuição, acurácia simples não diz nada: um modelo que classifica tudo como "bom" acerta 90% das vezes sem detectar um único defeito.

A acurácia balanceada corrige isso, mas tem seu próprio ponto cego: um modelo com recall perfeito em peças boas e apenas 20% de recall em defeitos ainda pontua (1,0 + 0,2) / 2 = 0,6. Por isso, o gate exige acurácia balanceada de pelo menos 0,6 e recall de defeitos de pelo menos 0,5, juntos:

step_check_classifier = ConditionStep(
    name="CheckClassifierQuality",
    conditions=[
        ConditionGreaterThanOrEqualTo(
            left=JsonGet(
                step_name=step_eval_classifier.name,
                property_file="evaluation",
                json_path="balanced_accuracy",
            ),
            right=min_accuracy,
        ),
        ConditionGreaterThanOrEqualTo(
            left=JsonGet(
                step_name=step_eval_classifier.name,
                property_file="evaluation",
                json_path="defect_recall",
            ),
            right=min_defect_recall,
        ),
    ],
    if_steps=[step_compile_classifier, step_register_classifier],
    else_steps=[step_fail_classifier],
    depends_on=[step_eval_classifier],
)

O ConditionStep aplica um AND entre as condições — as duas precisam passar. Os limiares são parâmetros do pipeline, ajustáveis por execução sem necessidade de redeploy. Se um gate falha, um FailStep marca a execução como falha e o modelo daquele ramo nunca chega ao registry. Os dois ramos são independentes: se um falha, o outro pode ainda registrar seu modelo. A frota continua servindo o modelo aprovado anteriormente para aquela câmera.

Compilação para a borda com SageMaker Neo dentro do pipeline

Após o gate de qualidade, a etapa de compilação com o SageMaker Neo recebe o artefato ONNX aprovado. A invariante é clara: gate → compilação → registro. Um modelo só se torna um pacote aprovado no registry se também compilou com sucesso para o hardware-alvo da frota.

A etapa de compilação encapsula três detalhes propensos a erros. O Neo exige que o tarball de entrada contenha apenas o arquivo do modelo, mas os jobs de treinamento emitem um model.tar.gz com checkpoint e métricas junto ao ONNX — a etapa reempacota um tarball só com o ONNX antes de enviar. O Neo também exige formas de entrada concretas, enquanto as exportações declaram eixo de batch dinâmico — então o batch size 1 é fixado na requisição de compilação. Por fim, como etapas de pipeline podem sofrer retry, o nome do job de compilação é derivado deterministicamente do ID de execução, evitando jobs órfãos em caso de reprocessamento.

O target é um parâmetro do pipeline: NeoTargetDevice seleciona um dispositivo nomeado como jetson_xavier ou rasp4b, e o padrão compila para a plataforma NeoTargetOs/NeoTargetArch (LINUX/X86_64 por padrão). O pacote compilado contém o compiled.so otimizado mais o runtime DLR para carregá-lo. A compilação em si não tem custo — você paga pela instância de processamento que a executa e pelo armazenamento S3 dos outputs.

Modelos por câmera no Model Registry

Cada ramo registra em seu próprio grupo de pacotes de modelos, e todo pacote carrega o ID da câmera e suas métricas de avaliação como metadados. Do exemplo:

classifier_model_builder = ModelBuilder(
    s3_model_data_url=classifier_model_data,
    image_uri=custom_training_image_uri,
    sagemaker_session=pipeline_session,
    role_arn=role_arn,
)
step_register_classifier = ModelStep(
    name="RegisterClassifierModel",
    depends_on=[step_compile_classifier],
    step_args=classifier_model_builder.register(
        model_package_group_name="sm1k-classifier-model-group",
        approval_status="Approved",
        model_metrics=classifier_metrics,
        customer_metadata_properties={"CameraId": camera_id},
    ),
)

A identidade da câmera em customer_metadata_properties é o que transforma dois grupos de pacotes compartilhados em um catálogo de frota. Pacotes de todas as câmeras convivem nos mesmos grupos, e o histórico por câmera é recuperado filtrando pelo metadado. Cada ramo tem uma etapa de busca que percorre os pacotes aprovados do mais recente para o mais antigo até encontrar o modelo mais recente daquela câmera — porque ListModelPackages não filtra por metadados do lado do servidor.

Na primeira execução de uma câmera, a busca não encontra nada e o treinamento usa inicialização fria (pesos ImageNet para o classificador, inicialização aleatória para o autoencoder). Nas execuções seguintes, o treinamento retoma do modelo anterior daquela câmera — o que torna o re-treinamento incremental, frequente e barato viável em escala de frota.

Orquestrando o treinamento da frota

Toda conta AWS tem cotas de serviço para jobs de treinamento simultâneos no SageMaker, e cada execução de pipeline dispara vários jobs. Algo precisa impedir que uma rajada de 500 requisições vire 500 pipelines simultâneos.

O exemplo usa um semáforo — um contador de slots disponíveis que funciona como um estacionamento com cinco vagas. Antes de a máquina de estados do Step Functions iniciar um pipeline, ela tenta adquirir um slot incrementando atomicamente um contador no DynamoDB, com uma condição que recusa o incremento quando o limite é atingido:

acquire_semaphore = tasks.DynamoUpdateItem(
    self,
    "AcquireSemaphore",
    table=semaphore_table,
    key={"LockName": tasks.DynamoAttributeValue.from_string("pipeline-concurrency")},
    update_expression="SET ActiveCount = if_not_exists(ActiveCount, :zero) + :one",
    condition_expression="attribute_not_exists(ActiveCount) OR ActiveCount < :limit",
)

Se todos os slots estão ocupados, a execução espera e tenta novamente. Quando o pipeline termina, o contador é decrementado para liberar o slot. O ponto-chave do design: a expressão de condição do DynamoDB aplica o limite em uma única operação atômica, eliminando a janela de corrida que existiria se a máquina de estados lesse o contador e depois decidisse. O cap padrão é cinco — baixo o suficiente para que o exemplo funcione em qualquer conta padrão sem solicitar aumento de cotas.

Implantando e executando o exemplo

O ponto de partida é um checkout limpo do repositório:

git clone https://github.com/aws-samples/sample-sagemaker-fleet-cv-pipeline
cd sample-sagemaker-fleet-cv-pipeline && python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cdk bootstrap                                        # uma vez por conta/região
cdk deploy SageMakerCVPipelineStack -c bootstrap=true  # papéis, bucket, ECR
cdk deploy --all                                     # pipeline + orquestração
./scripts/push_ecr_image.sh                          # build e push da imagem de treinamento

Por padrão, o pipeline gera imagens sintéticas de linha de produção, então é possível validar o sistema completo sem nenhum dataset real. Uma execução completa roda todos os 13 passos, incluindo as duas compilações Neo, em cerca de 15 minutos e custa aproximadamente US$ 0,07 em tempo de computação (instâncias ml.m5.xlarge, região US East Ohio, julho de 2026, sob demanda). Os recursos de orquestração juntos custam bem menos de US$ 0,10 por mês na escala do exemplo.

Para testar com imagens reais, o repositório inclui um downloader para o dataset BCCD — uma coleção de 364 imagens anotadas de esfregaços de sangue, originalmente publicada no GitHub e também distribuída pela Roboflow como domínio público. O script mapeia a presença de plaquetas para a classe de defeito, opcionalmente divide as imagens por câmera para simular uma frota e faz upload dos tarballs de treinamento para o bucket do pipeline.

Para limpar o ambiente, pacotes de modelos são criados em tempo de execução do pipeline e o AWS CloudFormation não consegue deletá-los automaticamente. É necessário removê-los com o AWS CLI primeiro e depois destruir as stacks com cdk destroy --all -c bootstrap=true --force. O README lista os comandos exatos.

Conclusão

A arquitetura publicada pela AWS resolve o problema que aparece depois que um piloto de visão computacional industrial funciona: escalar de uma câmera para uma frota inteira sem intervenção humana em cada modelo. Os princípios de design transferem para além de visão computacional: avalie e compile o artefato que você vai distribuir (não o modelo em memória), use acurácia balanceada mais recall da classe minoritária em vez de acurácia simples, e faça falhas operacionais notificarem em vez de terminarem silenciosamente.

Para equipes cujas necessidades são atendidas por opções gerenciadas como o Amazon Lookout for Vision, esse é o caminho mais simples. Este padrão existe para o cenário específico de restrições por câmera em escala de frota que opções gerenciadas não cobrem. O exemplo implanta em minutos, uma execução completa de validação custa cerca de sete centavos de dólar em computação — um ponto de partida de baixo risco para clonar o repositório e testar. Para a migração do SDK v3 em si, a documentação do SageMaker Python SDK é a referência recomendada.

Fonte

Fleet-wide computer vision lifecycle at the edge with SageMaker SDK v3 (https://aws.amazon.com/blogs/machine-learning/fleet-wide-computer-vision-lifecycle-at-the-edge-with-sagemaker-sdk-v3/)

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *