Cold start de LLM no HyperPod: de 27 minutos a segundos com model caching
Um pod de inferência com um modelo de 145 GB levava mais de 20 minutos entre o scheduler aceitar o pod e o primeiro token sair, e o autoscaler só pedia esse pod quando a fila já estava cheia. Este artigo conta a migração de um endpoint LLM no SageMaker HyperPod para o model caching que a AWS liberou em 11 de setembro de 2026: pesos em NVMe local, imagem pré-puxada, e a parte que nenhum anúncio menciona, reescrever a política de autoscaling que tinha sido calibrada para conviver com o cold start.
O ponto de partida: 27 minutos até o primeiro token
O endpoint que serve de fio condutor aqui é um pipeline de análise de documentos, contratos e extratos, tráfego em rajada na abertura do expediente e no fechamento do dia. Modelo com cerca de 145 GB de pesos no S3, vLLM num contêiner próprio de 12 GB no ECR, instâncias p5.48xlarge, InferenceEndpointConfig com autoScalingSpec disparado por invocações no CloudWatch.
Medido no kubectl describe pod, o tempo entre Scheduled e Ready se dividia em três fatias: 5 a 7 minutos puxando a imagem do ECR, mais de 20 minutos baixando os pesos do S3 para o disco do nó, e cerca de 90 segundos carregando pesos na HBM. As duas primeiras fatias são rede e não têm nada a ver com o modelo, são o mesmo download repetido em cada pod, em cada scale-out, em cada reposição de nó pelo health check do HyperPod.
A consequência operacional não era o tempo em si; era o que fizemos para conviver com ele. Mantínhamos duas réplicas de folga permanentes, scaleUpStabilizationTime em zero e targetValue baixo para o autoscaler antecipar a rajada. Duas p5.48xlarge paradas 24 horas por dia são 1.440 instance-hours por mês compradas só para esconder um download. A pergunta certa não era "como fazer o pod subir mais rápido?", era "quanto da folga existe só porque o pod demora?".
A jornada, na ordem em que foi feita
- 1
Medir o baseline por fatia
Antes de mudar qualquer coisa, gravamos por pod o tempo de pull de imagem, de download de pesos e de load em GPU, a partir dos eventos do pod e dos logs do vLLM. Sem essa divisão não dá para saber se o ganho veio do cache ou do acaso.
- 2
Confirmar NVMe no tipo de instância
weightsCacheexige NVMe local nohostPath(padrão/opt/dlami/nvme). Instância só com EBS não avisa: o cache nunca aquece e o pod cai no fallback para o S3 em silêncio. Ap5.48xlargetraz 8 × 3,84 TB. - 3
Atualizar o add-on do Inference Operator
Se o
modelCacheConfignão é reconhecido, o operator é antigo. Atualizamos o add-on do EKS num cluster de homologação primeiro, porque o mesmo add-on traz o KEDA e o ALB controller, a atualização mexe em mais do que o cache. - 4
Ligar só o
imageCachePrimeira mudança em produção:
imageCache.enabled: true. Não altera o pod de inferência, só cria um DaemonSet que pré-puxa a imagem. Ganho medido: os 5 a 7 minutos de pull viraram segundos, na linha dos 97% que a AWS reporta. - 5
Ligar o
weightsCachee observar os rótulosweightsCache.enabled: true, mesmohostPath. Acompanhamoskubectl get nodes --show-labels | grep cache-readyaté todos os nós do grupo estarem rotulados antes de tocar no autoscaler. - 6
Versionar o caminho do modelo
O cache não detecta mudança na fonte. Passamos a publicar pesos em
s3://.../modelo/v2026-09-11/e a alterar ospeca cada release, a mudança de caminho é o que força um novo aquecimento. - 7
Reescrever o autoscaling
Com scale-out em segundos, retiramos as duas réplicas de folga, subimos
targetValuee mantivemosscaleUpStabilizationTime: 0. O cold start deixou de ser um parâmetro escondido dentro da política.
Dois caches, um rótulo de nó e uma afinidade preferida
O desenho tem três peças que valem entender antes de confiar nele.
Aquecimento por nó: o operator baixa os pesos da fonte (S3, FSx for Lustre, Hugging Face Hub ou JumpStart) para um diretório isolado por deployment sob o hostPath, em cada nó que satisfaz as restrições de scheduling do deployment. Quando termina, aplica ao nó um rótulo com prefixo inference.sagemaker.aws.amazon.com/weights-cache-ready. seguido do UID da configuração; o cache de imagem faz o mesmo com image-cache-ready. via DaemonSet.
Afinidade preferida, não obrigatória: o deployment usa preferred node affinity nesses rótulos. Um pod vai para um nó quente quando existe um; se não existe, é agendado do mesmo jeito e baixa do S3 e do ECR como sempre fez. Isso é o que torna a migração reversível: o pior caso é o comportamento antigo, nunca um pod preso em Pending.
Montagem read-only: o pod monta o diretório do cache como volume hostPath somente leitura e o vLLM lê do NVMe local, a AWS cita cerca de 7 GB/s, em vez de percorrer a rede. O mesmo modelo de 145 GB que levava mais de 20 minutos para chegar ao disco agora está lá antes de o pod existir.
O que isso não faz: não replica cache entre nós, não detecta pesos novos na mesma chave e não sobrevive à troca do nó. Nó reposto pelo health check nasce frio, e o operator precisa aquecê-lo de novo enquanto os nós quentes seguem servindo.
Ciclo de vida do model caching no HyperPod Inference
Do apply do spec ao pod pronto: quem aquece, quem rotula, onde o fallback entra e por que um nó novo do Karpenter continua frio.
- Controller · reads InferenceEndpointConfig
- Weights warmer · per-node download
- Image DaemonSet · pre-pull per node
- S3 / FSx for Lustre · weights v2026-09-11
- ECR · vLLM image 12 GB
- Local NVMe · /opt/dlami/nvme (8 × 3.84 TB)
- Node label · cache-ready.<uid>
- vLLM pod · mounts cache read-only
- KEDA · autoScalingSpec (pods)
- Karpenter · new node = cold cache
O que muda no autoscaling quando o cold start some
O autoscaling no HyperPod tem duas camadas, e o cache só age numa delas.
Camada de pod (KEDA): o autoScalingSpec lê CloudWatch ou Amazon Managed Prometheus a cada pollingInterval (padrão 30 s; o HPA consulta o KEDA a cada 15 s) e decide réplicas. Com cold start de 27 minutos, uma política honesta precisa disparar cedo e manter folga; por isso tínhamos targetValue conservador e réplicas paradas. Com scale-out em segundos num nó quente, a mesma política passa a oscilar, sobe pods que ficam prontos antes de a janela padrão de 300 s de metricCollectionPeriod refletir a rajada. Reduzimos metricCollectionPeriod para 60 s, subimos targetValue e deixamos scaleDownStabilizationTime em 300 s para não devolver capacidade no primeiro vale.
Camada de nó (Karpenter): aqui o cache não ajuda. Um nó que o Karpenter acabou de provisionar não tem pesos nem imagem; o primeiro pod nele cai no fallback e paga os 27 minutos inteiros. Se o seu pico exige nós novos, o model caching encurta o segundo pod naquele nó, não o primeiro. A decisão que tomamos foi manter um piso de nós GPU quentes, o cache aquece neles fora do horário de pico, e usar o Karpenter só para o excedente raro.
Scale-to-zero no nível de nó e model caching resolvem problemas diferentes. Escolher entre eles é escolher o que você prefere pagar: instância ociosa com cache pronto, ou download completo no minuto de maior demanda.
Antes e depois, nos números que a AWS publicou e nos que medimos
O custo que fica: disco, versão e vizinhança de nó
Ligar o cache leva cinco linhas de YAML. Mantê-lo é o que custa, e três coisas viraram rotina de operação.
Capacidade de NVMe: cada deployment com cache ocupa o próprio diretório no mesmo disco local do nó. Dois modelos de 145 GB mais um de 57 GB num p5.48xlarge cabem folgados em 30 TB, mas o time que consolida seis endpoints num grupo de instâncias menor descobre a pressão de disco só quando o aquecimento para de acontecer. A documentação é explícita: separe grupos de instâncias ou reduza deployments cacheados por nó.
Invalidação por versão: como a fonte não é monitorada, a chave do S3 é a versão. Publicar pesos novos sobrescrevendo latest/ deixa o nó quente servindo o modelo antigo por tempo indefinido, sem erro algum. Um sufixo de versão no caminho e um kubectl apply novo é o único gatilho confiável; o operator limpa os arquivos antigos quando o deployment antigo é deletado.
Sinal de fallback: um pod que sobe sem o volume hostPath está no caminho lento e ninguém é avisado. Criamos um alerta simples: contagem de nós com rótulo cache-ready menor que o número de nós do grupo por mais de 10 minutos, mais o tempo Scheduled → Ready por pod exportado para o Prometheus. Sem isso, a regressão para o comportamento antigo passa despercebida até o próximo pico, e o próximo pico é exatamente quando você não quer descobrir.
Os riscos que gerenciamos no caminho
Três falhas silenciosas concentram o risco desta migração. Instância sem NVMe no hostPath: o cache nunca aquece, nada quebra e o cold start continua igual. Pesos sobrescritos na mesma chave: nós quentes servem o modelo antigo até alguém mudar o spec. Nó novo do Karpenter no pico: o primeiro pod paga o download inteiro; o cache só protege o segundo. Nenhuma das três gera erro, todas geram latência que parece normal.
Anti-padrões que vi surgir na primeira semana
- Ligar os dois caches e o autoscaler novo no mesmo apply: quando o tempo de
Readycai e a política oscila no mesmo dia, ninguém sabe qual mudança causou o quê. Uma mudança por vez, com o baseline medido. - Tratar o cache como réplica: ele é por nó, não por cluster. Consolidar deployments num grupo pequeno de instâncias para 'compartilhar' o cache só compartilha o disco, e a pressão dele.
- Scale-to-zero de nó com model caching como se fossem complementares: quando o Karpenter remove o último nó, remove o cache junto. O primeiro pod do dia volta aos 27 minutos.
Eu ligaria o imageCache em toda deployment de inferência no HyperPod já na próxima janela de mudança, é um DaemonSet, não toca no pod, e devolve dois minutos por scale-out sem pedir nada em troca. O weightsCache eu só ligo depois de confirmar NVMe no tipo de instância e de colocar versão no caminho do S3, porque as duas falhas que ele introduz são mudas. A lição que fica de 16 anos operando plataforma: toda otimização de cold start muda a política de autoscaling que foi escrita para conviver com o cold start antigo, se você não reescreve a política, fica pagando a folga velha com o problema novo resolvido.
Veredito
Use o model caching do HyperPod quando: o modelo passa de 50 GB, o tráfego tem rajada previsível, a frota tem NVMe local e você consegue manter um piso de nós quentes fora do pico. Nesse cenário ele retira do autoscaler a folga que existia só para esconder o download, e é aí que o dinheiro aparece, não no benchmark. Não espere ganho quando o pico exige nós novos do Karpenter, quando a frota é só EBS, ou quando o modelo cabe em um pull de poucos minutos: nesses três casos o custo de manter versão de caminho e alerta de fallback supera o que o cache devolve. Ligue o imageCache sempre; ligue o weightsCache com as condições acima.
Referências
Continue lendo
Deep dives de arquitetura, AWS, IA e mercado: direto no seu email. Grátis.
Sem spam · cancele quando quiser
Pergunte ao Fernando sobre isto
Uma resposta focada sobre este artigo, do meu assistente de IA, baseada no meu trabalho.
Participe da conversa
Entre para comentar
Confirme seu e-mail para participar, você também recebe a newsletter. Sem senha.