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.