Pular para o conteúdo

O patch que logava “patched” e nunca rodou

Dois incidentes. Mesma causa raiz. O primeiro derrubou a empresa por três horas em julho. O segundo revelou que o fix instalado era um placebo com log reconfortante: o código estava lá, logava confirmação, e a arena de memória seguia crescendo sem teto.

Conto aqui como a BFCArena do ONNX Runtime cresceu em silêncio até travar tudo com OOM, por que o primeiro patch não funcionou apesar de imprimir “patched” nos logs, e qual régua de medição prova que o fix correto funcionou.

Sintoma medido

Em 16 de julho de 2026, às 18h, o serviço de orquestração de agentes de IA começou a vazar memória. A taxa chegou a 737 MB por minuto.

O serviço é a pipeline que roda na porta 4090: recebe requisições de despacho, executa os agentes, gerencia o estado de cada run em andamento. O ponto de esgotamento foi a rota memory_recall_v2, responsável pela busca semântica no índice de fatos da empresa. Cada chamada a essa rota acionava a biblioteca fastembed para gerar o vetor de consulta e comparar com o índice.

Em menos de 20 minutos desde os primeiros alertas, o kernel ativou o OOM killer. O processo da pipeline morreu. Todos os agentes com tasks abertas perderam a conexão com o dispatcher. O sistema de despacho ficou cego: sem o processo, nenhum agente conseguia devolver resultado, atualizar estado ou criar tasks filhas. A empresa parou.

A hipótese errada

A hipótese operacional era clara e estava errada: o log de startup dizia "[brain_server] onnxruntime patched: BFCArena bounded", então a arena estava desabilitada. O servidor tinha reiniciado limpo e ficado estável nos primeiros testes.

A hipótese estava errada porque o patch verificava if so is None antes de criar um SessionOptions com arena desabilitada. O fastembed nunca passa sess_options=None: ele entrega o próprio SessionOptions com arena habilitada. A condição nunca era verdadeira, o _Bounded era instanciado, o log aparecia, e o SessionOptions do fastembed chegava intacto ao runtime. A arena crescia normalmente.

Causa raiz

O onnxruntime usa uma arena de alocação interna chamada BFCArena (Best-Fit with Coalescing). O comportamento padrão tem dois aspectos que, combinados, são fatais em processos de longa duração:

Primeiro, a arena cresce em potências de dois conforme a demanda aumenta: 256 MB, 512 MB, 1 GB, 2 GB. Cada salto é irreversível.

Segundo, a arena nunca devolve ao sistema operacional o que alocou. Se uma inferência precisa de 1 GB de arena, esse gigabyte fica reservado para o processo pelo resto da vida dele, independente de quantas outras inferências precisarem de menos.

Com chamadas contínuas de memory_recall_v2, a arena crescia em degraus exponenciais. Cada chamada que precisava de um bloco maior forçava um novo salto de potência de dois. O bloco anterior ficava reservado. O próximo salto reservava o dobro. Em menos de 20 minutos, 12 GB esgotados.

O patch que logava “patched” e nunca rodava

Identificada a BFCArena como culpada, apliquei um monkey-patch no onnxruntime antes do import do fastembed. A ideia era substituir a InferenceSession por uma subclasse que forçasse o SessionOptions com arena desabilitada:

import onnxruntime as _ort
_orig = _ort.InferenceSession
class _Bounded(_orig):
    def __init__(self, *args, **kwargs):
        so = kwargs.get("sess_options")
        if so is None:   # o bug estava aqui
            so = _ort.SessionOptions()
        so.enable_cpu_mem_arena = False
        so.enable_mem_pattern = False
        kwargs["sess_options"] = so
        super().__init__(*args, **kwargs)
_ort.InferenceSession = _Bounded

O log no startup confirmava: "[brain_server] onnxruntime patched: BFCArena bounded". O servidor reiniciou limpo. O serviço ficou estável nos primeiros testes.

Sete dias depois, o vazamento voltou.

Medi o VmRSS do processo rodando embeddings sem carga externa: 902 MB crescendo para 2,57 GB em 5 minutos. O patch estava instalado, logava “patched”, e a arena nunca tinha sido desabilitada em nenhuma inferência real. Ao longo desse período, o serviço acumulou 308 restarts.

O motivo era a condição if so is None. O fastembed nunca passa sess_options=None: ele entrega o próprio SessionOptions com arena habilitada. O patch interceptava a chamada, via que so não era None, e pulava a lógica de desabilitação completamente.

Veja também: Sobre Igor Caique.

O fix correto

O fix correto remove a condição e força o desligamento da arena independente do que o caller passou:

class _Bounded(_orig):
    def __init__(self, *args, **kwargs):
        so = kwargs.get("sess_options")
        if so is None:
            so = _ort.SessionOptions()
        # força SEMPRE, ignorando o que o fastembed passou
        so.enable_cpu_mem_arena = False
        so.enable_mem_pattern = False
        so.intra_op_num_threads = 1
        so.inter_op_num_threads = 1
        kwargs["sess_options"] = so
        super().__init__(*args, **kwargs)

Junto com o patch corrigido, ativei um kill-switch via variável de ambiente: PEI_DISABLE_LOCAL_EMBED=1 configurado no serviço systemd. Com o kill-switch ativo, a rota de recall desabilita o embedding local e degrada para busca FTS5 pura. Temos 316 mil fatos indexados em FTS5 e a busca textual é suficiente para operação normal enquanto o embedding remoto não está disponível.

A régua que prova

O sinal é o VmRSS do processo. Sem medir isso em runtime, o patch “funciona” no papel e nenhum teste detecta a falha. O log "patched" não é sinal: é texto que confirma que uma função rodou, não que o efeito foi produzido.

Régua de aceite: rodar ao menos 200 embeddings reais e medir o VmRSS em três pontos espaçados no tempo. Critério de aprovação: o delta entre o ponto do meio e o ponto final deve ser menor que 100 MB. RSS crescente com o patch instalado significa que a condição não está executando.

for i in 1 2 3; do
  grep VmRSS /proc/$(pgrep -f brain_server)/status
  sleep 30
done

Com o patch errado: crescimento linear nos três pontos, de 902 MB para 2,57 GB.

Com o patch correto: estabilização entre o segundo e o terceiro ponto, delta abaixo de 80 MB.

A pergunta que fecha qualquer prova: “se o patch estivesse quebrado, o meu teste teria acusado?” Com VmRSS em três pontos e 200 embeddings reais, a resposta é sim. Com apenas o log “patched”, a resposta é não.

Assine a newsletter. Um e-mail por post. Só o próximo incidente quando sair.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

error: