A Noite em que Meu Computador Reiniciou Sozinho
Uma noite, no meio de um trabalho longo, meu computador inteiro se desligou e voltou. Não o programa — a máquina. Em um momento havia um processo rodando; no seguinte eu estava olhando para uma tela de login zerada, como se alguém tivesse puxado o cabo de força e o colocado de volta.
Eu havia passado dias sendo cuidadoso com o único recurso que tinha certeza de que importava. A queda veio daquele que eu nunca havia pensado em vigiar.
A queda sem mensagem
A parte mais perturbadora foi o quão pouco havia para ler. Nenhum stack trace, nenhuma exceção, nenhuma linha de log marcando o fim. Apenas uma reinicialização abrupta e, enterrado nos próprios registros do sistema operacional, o equivalente digital de um dar de ombros: o sistema reiniciou sem ter sido desligado corretamente.
Um bug normal deixa um corpo. Esse deixou um quarto vazio. E um erro sem mensagem é, por si só, um tipo de pista — geralmente significa que a falha aconteceu em algum lugar abaixo do seu programa, em uma camada que não para para se explicar antes de derrubar tudo junto.
Eu havia orçado a memória errada
Eu estava rodando um lote de trabalho muito grande na GPU, e havia dimensionado esse lote com cuidado — contra a memória da GPU. Eu sabia quanta VRAM a placa tinha, sabia mais ou menos quanto cada unidade de trabalho consumiria, e havia feito a aritmética para ficar com folga abaixo do teto. No que me dizia respeito, memória era um problema resolvido.
Estava resolvido para a memória errada. A VRAM da placa esteve bem o tempo todo; se eu estivesse observando o medidor dela no momento da queda, ele teria parecido perfeitamente calmo. O teto que eu de fato bati foi a RAM do sistema da máquina hospedeira — a memória comum em que o resto do computador roda — e eu não havia orçado aquilo de jeito nenhum, porque no meu modelo mental simplesmente não era a restrição.
Para onde foi a RAM do host
Enviar um lote enorme de trabalho para uma GPU não sai de graça do lado do host. Antes que a GPU possa rodar qualquer coisa, o trabalho precisa ser preparado, configurado e entregue do outro lado — e essa preparação vive na RAM do sistema. Cada peça dessa configuração era modesta por si só. Multiplicada por um lote grande o suficiente para saturar a GPU, as peças modestas se empilharam em algo enorme.
A RAM do sistema encheu. O sistema operacional fez o que faz quando a memória fica escassa — transbordou para o disco, e então ficou sem disco também. Pressão severa de memória nem sempre se anuncia com uma exceção capturável. Ela pode significar uma alocação falha, um processo morto, ou uma máquina inteira ficando instável. A minha ficou instável o suficiente para reiniciar.
Por que eu nunca vi isso chegando
O frustrante, em retrospecto, é que eu estava monitorando com cuidado — só estava monitorando o medidor errado. Meus olhos estavam na GPU, o recurso que eu havia otimizado, com o qual me preocupei e em relação ao qual me senti esperto. O tempo todo, a pressão estava se acumulando em um lugar que eu não tinha motivo para olhar, porque eu já havia decidido que não era ali que o problema estaria.
Esse é um padrão que já vi vezes demais para chamar de coincidência: você instrumenta a restrição em que está pensando, e fica cego para aquela em que não está. O bug não se esconde. Ele se senta em um ponto cego que você mesmo construiu, ao ter confiança sobre onde estava o limite.
A correção: espere o muro, e ricocheteie nele
O reparo não foi um cálculo melhor. Foi desistir da ideia de que eu poderia calcular o tamanho seguro com antecedência. Em vez de prever exatamente quão grande um lote a máquina poderia sobreviver, deixei a máquina me dizer: tente o lote, e se ele esbarrar em um erro de memória recuperável, trate isso como um sinal para recuar — reduza a carga de trabalho de forma conservadora e tente de novo, encolhendo até caber.
É um loopzinho humilde, e é muito mais robusto do que minha aritmética cuidadosa jamais foi, justamente porque não depende de eu ter entendido cada custo oculto com antecedência. Não preciso mais saber exatamente onde está o muro; para erros de memória recuperáveis, o processo pode tatear seu caminho até um tamanho viável em qualquer hardware em que rode. A exaustão no nível do host, como a daquela noite, é o caso mais difícil — ela ainda precisa de limites externos e monitoramento, não apenas de um loop de repetição.
Suposições sobre recursos são silenciosas até deixarem de ser
A lição que guardei tem pouco a ver com GPUs especificamente. É que suposições sobre recursos são invisíveis até exatamente o momento em que se tornam catastróficas. Você pode estar perfeitamente certo sobre o limite que está imaginando e ainda assim ser derrubado por aquele que não está — e a falha, quando vem, muitas vezes chega sem explicação, porque a camada que quebra está abaixo da camada onde vivem as suas mensagens de erro.
Então parei de tentar ser preciso sobre recursos e comecei a tentar ser defensivo em relação a eles. Precisão pressupõe que você conhece todos os custos. Defensividade pressupõe que você deixou passar pelo menos um. Em um sistema feito para rodar por horas, sem supervisão, sobre uma memória que você não controla por completo, a segunda suposição é a única segura.
Um programa que trava deixa uma mensagem para você. Uma máquina que reinicia deixa um silêncio. Passei a tratar esse silêncio como uma das coisas mais honestas que meu hardware já me disse: você estava orçando a coisa errada.
— Sem sinais, sem retornos, isto não é conselho de investimento.