Pare de deixar NaNs chegarem à produção: o playbook de higiene numérica para CTOs

Por Diogo Hudson Dias
Senior backend engineer reviewing live system metrics on multiple monitors in a dimly lit operations room.

NaNs não são dramáticos. Eles não dão tela azul no seu servidor nem explodem os logs. Eles escorregam silenciosamente pelo JSON, envenenam seus rankings e fazem suas estatísticas de A/B mentirem. Em 2026 — quando você está misturando inferência FP8/BF16, embeddings quantizados e chamadas de modelos de terceiros — NaNs são a classe de bugs que vai degradar seu produto sem ninguém perceber até a receita se mover.

Recentemente, o Hacker News ressuscitou um ótimo lembrete com “Two Case Studies of NaN”. A essência: NaNs são contagiosos. Eles se propagam pelas computações e raramente aparecem como exceção. Se você não desenhar para higiene numérica, eles vão escorregar pela sua IA, seus serviços e seu data warehouse. Vamos consertar isso.

Por que você quase nunca vê NaNs (mesmo quando eles estão lá)

  • JSON os esconde. A especificação do JSON proíbe NaN e Infinity. Muitos encoders serializam NaN como null; alguns emitem o literal NaN e rezam; muitos decoders rejeitam o payload. De um jeito ou de outro, você perde sinal ou higieniza silenciosamente.
  • A maioria das linguagens não intercepta exceções IEEE‑754 por padrão. Divisão por zero, sqrt inválido, exp catastrófico — nada disso levanta exceção a menos que você peça. Em GPUs, você não consegue interceptar nada em runtimes de produção.
  • Seu pipeline de métricas os achata. Camadas de visualização coercem implicitamente NaN para zero ou descartam o ponto. Seu dashboard bonito está mentindo para você.

Enquanto isso, um único NaN em um embedding de 768 dimensões costuma tornar a similaridade cosseno NaN, que muitas bibliotecas de ANN tratam como a pior pontuação possível ou pulam completamente. Isso não é um crash; é uma queda sutil de recall. Já vimos equipes recuperar 5–10% de recall de recuperação eliminando uma taxa de NaN <0,1% em embeddings.

De onde vêm os NaNs nos stacks modernos

  • Inferência/treinamento em precisão mista. FP16/FP8 saturam rápido. Softmax em logits grandes estoura; layer norm com variância quase zero divide por ~0; o cast de volta para FP32 gera Inf ou NaN.
  • Normalização e estatística. Batches de variância zero, log de valores não positivos, sqrt de números negativos ou soma de arrays vazios.
  • Resultados de APIs de terceiros. Provedores de modelos às vezes retornam probabilidades ou scores NaN em inputs de borda. Seu SDK provavelmente coerça ou descarta sem avisar.
  • Deriva em parser e ETL. CSVs com "" mapeado para float; NumPy e Go podem ambos produzir NaN em parses ruins se você for permissivo. Features downstream herdam o veneno.
  • Compiladores fast‑math. Flags como -ffast-math e equivalentes prometem velocidade relaxando garantias IEEE. Eles também permitem transformações algébricas que amplificam operações inválidas.
  • Erros de quantização/desquantização. Escalas assimétricas perto de zero somadas a clipping agressivo podem tornar denominadores posteriores praticamente zero.

O custo real de um NaN no lugar errado

  • Rankeadores e recuperação. Um NaN dentro de um vetor torna a similaridade cosseno NaN; o candidato pode cair fora ou ranquear por último. Se 0,05% das consultas carregarem um vetor com NaN, isso é 500 resultados ruins por milhão de consultas. Com 3% de lift de CTR por top‑3 correto, você está abrindo mão de dinheiro de verdade.
  • Chamada de ferramentas para LLMs. Um NaN em um score de função pode curto‑circuitar o seletor; seu agente “decide” pular uma ferramenta, ou o planner degenera. Isso não aparece como 5xx — parece que o modelo ficou “burro”.
  • Bancos de dados e analytics. No Postgres, NaN é um valor float válido. Ele ordena de forma esquisita e pode quebrar dedup/agregações de maneiras surpreendentes. Se você não filtrar com isfinite(x), vai entregar dashboards sem sentido.
  • Preços e lances. Qualquer modelo de scoring que alimenta um multiplicador de lance ou um motor de descontos vai propagar NaN para um default; se seu default é leniente, parabéns — você está pagando a mais ou descontando demais.

Um playbook para CTOs: detectar, conter e depois eliminar

Não comece reescrevendo kernels. Comece transformando NaNs de fantasmas em eventos que sua organização consegue ver e contornar. Aqui vai um plano em camadas com passos concretos e de baixo overhead.

Camada 1: higiene do tráfego e do armazenamento

  • Use protocolos binários que preservem os bits de float. Prefira Protobuf ou Arrow Flight para data planes de IA. JSON torna NaNs invisíveis — ou problema de alguém. Se você precisar usar JSON no browser, faça down‑cast ou codifique floats em base64 quando a precisão importar e valide nas bordas do servidor.
  • Interceptors gRPC/HTTP que escaneiem payloads. Para vetores e arrays de scores, uma única varredura é barata. Um core moderno de CPU consegue streamar 20–40 GB/s; escanear um embedding de 3 KB a 1.000 RPS é ~3 MB/s — 0,01 core. Ao encontrar, incremente um contador, anexe tags de modelo/versão e sanitize (nan_to_num) ou faça fail fast por rota.
  • Restrições no banco. No Postgres, adicione CHECK (isfinite(x)) em colunas float que nunca devem armazenar NaN/Inf. Para arrays, imponha em triggers ou nos serviços do caminho de escrita. Rejeitar linhas ruins cedo é mais barato do que reparar warehouses depois.

Camada 2: estabilidade em nível de modelo

  • Softmax e normalizações estáveis. Sempre subtraia max(logits) antes de exp; acumule em FP32 mesmo quando os pesos são FP16/FP8; adicione eps (por exemplo, 1e‑5) aos denominadores em layer/batch norm.
  • Clamp e sanitização nas saídas do modelo. Use funções como torch.nan_to_num(x, nan=0.0, posinf=1e9, neginf=-1e9) na etapa final para embeddings e scores. Fazer clamp no limite é melhor do que propagar NaN para sistemas de ranking.
  • Desabilite fast‑math inseguro nos hot paths. Compiladores e engines de grafo oferecem flags que sacrificam segurança de NaN por velocidade. Mantenha‑os desligados para ops numericamente sensíveis; o ganho de velocidade costuma ser 0–3% e não vale o risco.
  • Quantização com guarda‑corpos. Calibre com inputs reais de cauda; habilite escalas por canal; verifique que o dequant produz valores finitos sob inputs extremos. Rode testes de unidade com faixas adversariais.

Camada 3: guardas de runtime nos seus serviços

  • Checagens no nível da linguagem. Em Python/NumPy: np.isfinite faz asserts nas bordas; configure numpy.seterr(all='raise') no CI e nos testes para forçar falhas. Em Go: math.IsNaN, math.IsInf em desserialização e antes de persistir. Em Java/Kotlin: Double.isFinite.
  • Gates de debug no ONNX Runtime/TensorRT (não‑prod primeiro). Habilite checagens de propagação de NaN no nível do modelo em staging para encontrar a camada onde os NaNs nascem. Para treinamento em PyTorch, torch.autograd.set_detect_anomaly(True) em builds de debug vai capturar a origem; para inferência, envolva ops críticas e faça assert de tensores finitos.
  • Contratos, não comentários. Adicione contratos no nível de schema ao Protobuf: dedique um tipo de mensagem para Embedding que seja validado na construção; não passe float32 repetido cru pela sua frota.

Camada 4: observabilidade e políticas

  • Defina um SLO de NaN. Exemplo: “Menos de 1e‑7 valores não finitos por elemento de saída, por modelo, por dia.” Acompanhe como um contador com labels: modelo, versão, camada, feature, rota.
  • Fail‑closed vs sanitize — decida explicitamente. Para qualquer coisa que toque dinheiro, identidade ou políticas de segurança, falhe a requisição e exponha um erro retentável. Para ranking/recuperação, sanitize para defaults seguros e registre uma amostra de alta cardinalidade para replay.
  • Fallbacks automatizados. Se a taxa de NaN exceder o limiar para uma versão de modelo ou rota, faça circuit‑break para um caminho mais lento porém estável (por exemplo, inferência em CPU ou a última versão boa) até um humano revisar.
  • Guarde amostras de cauda por 30 dias. Mantenha 100–1.000 exemplares reproduzíveis por modelo/versão com inputs e seeds exatos para reproduzir e corrigir. Eles são pequenos (<100 KB cada) e inestimáveis durante incidentes.

Padrões de implementação concretos (por stack)

Serviços em Python/PyTorch

  • Envolva todas as saídas do modelo com um utilitário pequeno que faça assert de finitude, aplique clamps de faixa e emita uma métrica. Mantenha o overhead por requisição em microssegundos vetorizando as checagens.
  • No pré‑processamento, converta inputs de usuário para float com parsing estrito. Rejeite ou impute; nunca deixe um parse inválido virar NaN silenciosamente.
  • No treinamento e na avaliação, rode um job noturno de “fuzz numérico”: amostre inputs extremos, adicione pequenos ruídos, varra tamanhos de batch; falhe o job se qualquer tensor reportar valores não finitos após layer norm/softmax/attention.

Microsserviços em Go

  • Em fronteiras de API, escaneie slices de float com math.IsNaN/math.IsInf. Para embeddings: faça pré‑alocação e varredura sem desvios com SIMD quando disponível; o gargalo é a largura de banda de memória, não CPU.
  • Em interceptors gRPC, adicione uma única passada sobre campos float conhecidos. Ao detectar, anote o contexto com o motivo e direcione para um fallback de sanitização.
  • Ao fazer marshalling para JSON (clientes de browser), mapeie explicitamente NaN/Inf para null ou sentinelas seguros e defina um header de indicação. Serviços downstream devem tratar “sanitized” como QoS degradada.

Postgres, warehouses e analytics

  • Use CHECK (isfinite(col)) em colunas float. Para arrays, considere uma função no caminho de escrita que rejeite qualquer elemento não finito.
  • Queries: sempre filtre métricas com isfinite(value) e decida como tratar não finitos — excluir das agregações ou colocar em bucket separado.
  • Se você precisar armazenar saídas brutas do modelo com NaNs para forense, faça isso em uma tabela de quarentena com retenção e sem joins com analytics core.

ONNX Runtime e TensorRT

  • Prefira FP16 com acumulação em FP32 em ops sensíveis (softmax, layer norm). Valide seu engine varrendo faixas de input ao redor do seu percentil 99,9 real.
  • Adicione um plugin pós‑engine que escaneie as saídas por finitude e emita um log estruturado em violações. Mantenha desligado no hot path para SLAs de ultra‑baixa latência; mantenha ligado em canários e jobs em lote.
  • Evite fundir kernels que escondam a origem de NaNs até você validar a numérica; depois reative as fusões seletivamente.

Governança: transforme higiene numérica em contrato, não em boa prática

  • Testes de aceitação numérica no CI. Envie vetores sintéticos de teste que exercitem zeros, desnormalizados, logits extremos e tensores vazios. Asserte “nenhum não finito em lugar nenhum”. Isso leva minutos e evita semanas caçando incidentes.
  • Testes baseados em propriedades para helpers matemáticos. Para qualquer função que faz log/exp/divisão/sqrt, gere inputs adversariais e asserte finitude e propriedades de monotonicidade.
  • Checklists de promoção de modelos. Antes da promoção, exija: (a) varredura de finitude sobre 1M saídas representativas; (b) histogramas de faixa para tensores‑chave; (c) plano para sanitize vs fail‑closed por call site.
  • Mapas de ownership. Nomeie uma única equipe dona dos “SLOs numéricos” através de modelos e serviços. Se todo mundo é dono, ninguém é.

Trade‑offs de performance e como manter o overhead mínimo

Sim, checar floats custa alguma coisa. Mas é barato se você colocar no lugar certo e vetorizar.

  • Custo do wire‑scan. Escanear um embedding de 3 KB a 1.000 RPS é ~3 MB/s. Um único core faz stream de 20–40 GB/s. Isso dá ~0,01 core para garantias no fio — erro de arredondamento.
  • Clamp em nível de modelo. torch.nan_to_num é limitado por largura de banda de memória; em um tensor de 768 dimensões, fica em poucos microssegundos em CPU/GPU. Faça isso uma vez na folha.
  • Cardinalidade de métricas. Contadores de NaN são naturalmente de alta cardinalidade. Limite labels a modelo/versão/rota/camada. Faça amostragem dos violadores para logs com detalhes.
  • Fail‑fast vs sanitize. Isso é uma decisão de produto. Para dinheiro/segurança, falhe. Para ranking, sanitize para defaults estáveis e degrade graciosamente. Documente o que se aplica onde.

Seu plano de 90 minutos, 2 dias e 30 dias

Nos próximos 90 minutos

  • Adicione uma única varredura de finitude à sua principal resposta de embedding/ranking e emita uma métrica (modelo, versão, rota). Acenda um painel no dashboard que mostre a taxa diária de NaN.
  • No Postgres, adicione filtros isfinite às suas queries de analytics mais importantes. Você vai se surpreender.

Nos próximos 2 dias

  • Introduza CHECK (isfinite(x)) em pelo menos uma coluna float de alto valor e corrija as primeiras violações na fonte.
  • Envolva saídas de modelos com nan_to_num ou equivalente e defina clamps sensatos. Reexecute A/Bs de recuperação ou ranking — observe recall e CTR se moverem.
  • Adicione um interceptor gRPC/HTTP que escaneie payloads float conhecidos e conte violações. Direcione respostas “ruins” por um sanitizador.

Nos próximos 30 dias

  • Codifique um SLO de NaN, integre ao on‑call e dê à equipe dona um orçamento para corrigir causas‑raiz.
  • Coloque de pé um job noturno de fuzz numérico contra cada modelo promovido com inputs e tamanhos de batch adversariais. A promoção bloqueia se aparecer qualquer valor não finito.
  • Decida, documente e implemente políticas de fail‑closed vs sanitize rota por rota. Audite call sites que toquem preços, identidade e segurança.

Por que isso importa agora

Stacks de IA em 2026 são numericamente agressivos por design: dtypes menores, orçamentos de latência mais apertados, tráfego vetorial mais pesado. NaNs são o subproduto previsível — e são a classe de bugs mais fácil de erradicar com wins rápidos. O overhead é desprezível; o payoff é imediato: rankings mais limpos, analytics confiáveis, menos chamados de “o modelo ficou mais burro”.

Em um mundo em que reguladores observam segurança e confiabilidade, um programa de higiene de NaN também é uma história de governança. Você pode encarar o board — e seus clientes — e dizer: não temos apenas experimentos; temos contratos numéricos. Esse é o novo patamar.

Pontos‑chave

  • NaNs raramente derrubam seus sistemas; eles os corrompem silenciosamente — especialmente em recuperação, ranking e analytics.
  • JSON esconde NaNs; use Protobuf/Arrow para data planes de IA e escaneie payloads nas fronteiras.
  • Adicione CHECK (isfinite(x)) no Postgres e clamps de faixa nas saídas do modelo para ganhos imediatos.
  • Defina um SLO de NaN, decida onde falhar fechado vs sanitizar e automatize fallbacks quando as taxas subirem.
  • O custo de varreduras de finitude é minúsculo (≈0,01 core por 1.000 RPS de vetores de 3 KB); o ROI é grande.

Ready to scale your engineering team?

Tell us about your project and we'll get back to you within 24 hours.

Start a conversation