Eu rodo um VPS próprio com dezenas de containers e gosto disso. Gosto de saber onde as coisas estão, gosto de docker compose up, gosto de não descobrir uma fatura surpresa. Então quando a conversa virou "precisamos rodar modelo nosso", meu instinto foi o de sempre: aluga uma máquina com GPU e sobe lá.
Foi o que fizemos. E foi caro do jeito mais bobo possível.
O problema não era o preço da GPU
A carga que a gente tinha era clássica de B2B brasileiro: pico de manhã, pico no fim da tarde, silêncio no resto. Documento entrando pra extração, embedding de lote novo, uma classificação aqui e ali. Somando tudo com honestidade, dava umas três horas de trabalho real por dia.
A máquina ficava ligada 24. Isso é 87% de ociosidade e todo mundo sabe fazer essa conta.
O que ninguém conta é a segunda parte: como a máquina era cara, ela virou preciosa. Ninguém queria testar um modelo maior porque "aí precisa trocar a instância". Ninguém queria rodar reprocessamento em lote porque ia competir com a fila de produção. Um experimento que devia custar quarenta minutos de curiosidade custava uma reunião.
O custo em real era chato. O custo em não-mexer foi o que doeu.
O que o Modal muda
A ideia central é simples: você escreve uma função Python, diz de qual GPU ela precisa, e ela sobe quando alguém chama e morre quando ninguém chama. Sem cluster pra manter, sem instância parada.
Isso é o arquivo inteiro. Não tem Dockerfile separado, não tem manifesto de Kubernetes, não tem registry pra empurrar imagem. A imagem é declarada no mesmo lugar que a função, e o deploy é um comando.
Três coisas me convenceram de verdade:
Escala a zero é real. Sem requisição, sem cobrança de GPU. Pra carga em rajada isso não é otimização, é mudança de categoria.
Paralelismo sai de graça. extrair.map(lista_de_pdfs) sobe dezenas de containers de uma vez. Reprocessar seis mil documentos deixou de ser projeto de fim de semana e virou almoço.
O ambiente é código. O .pip_install está do lado da função. Aquela dança de "funciona na minha máquina, quebra no servidor" praticamente sumiu, porque não existe "servidor" com estado próprio pra divergir.
O que dói
Cold start existe. Container novo com modelo grande pra carregar leva de alguns segundos a bem mais, dependendo do tamanho dos pesos. Pra job assíncrono é irrelevante. Pra endpoint síncrono que um humano está esperando, é o problema central.
Dá pra domar de três jeitos, e usamos os três:
- Snapshot de memória, que congela o estado depois do modelo carregado e restaura muito mais rápido do que carregar de novo.
- Container quente durante a janela conhecida de pico, aceitando pagar por ociosidade das 8h às 11h porque ali a latência importa.
- Empurrar o que dá pra assíncrono. Boa parte do que parecia síncrono não era; era só interface mal desenhada. Documento que entra pode virar notificação em vez de spinner.
Segundo ponto honesto: é lock-in. O decorator é deles. Migrar depois significa reescrever a camada de orquestração — não o pipeline em si, que é Python normal, mas a casca. Aceitei conscientemente porque a alternativa era manter Kubernetes com nó de GPU, e eu já fiz isso o suficiente pra saber o que custa.
Terceiro: o custo por segundo de GPU não é barato quando comparado a alugar bruto. Se sua carga for constante, 24 por 7, o Modal provavelmente sai mais caro que uma máquina dedicada. A economia vem do padrão de uso, não da tabela de preço. Vale fazer a conta com o seu perfil real de tráfego antes de acreditar em qualquer post — inclusive neste.
Como ficou dividido
Não migrei tudo, e acho que ninguém deveria.
O que continua no VPS: Postgres, aplicações web, jobs de negócio, tudo que é leve e vive ligado. Isso é barato e previsível onde está, e mexer seria trocar sossego por nada.
O que foi pro Modal: qualquer coisa que precise de GPU, qualquer coisa em lote grande, e experimento. Especialmente experimento.
Essa última parte foi a surpresa. Quando testar um modelo diferente custa "escrever outra função e chamar", você testa. Trocamos o modelo de extração duas vezes em dois meses, não porque o anterior estava quebrado, mas porque a comparação ficou barata o bastante pra ser feita direito, com dado real, em vez de discutida na reunião.
Se eu fosse recomeçar
Começaria pelo job mais chato que você tem — aquele reprocessamento que ninguém roda porque atrapalha produção. É a menor superfície possível, não tem usuário esperando, e mostra o comportamento de escala já na primeira execução.
E não migraria o endpoint síncrono antes de medir cold start com o seu modelo. O número que vale é o seu; qualquer benchmark de blog, incluindo os deste post, está medindo outra coisa.
