View a markdown version of this page

Acelerar o carregamento de modelos no Amazon EKS - Amazon EKS

Ajudar a melhorar esta página

Para contribuir com este guia de usuário, escolha o link Editar esta página no GitHub, disponível no painel direito de cada página.

Acelerar o carregamento de modelos no Amazon EKS

Quando você implanta grandes modelo de linguagem (LLMs) no Amazon EKS, o tempo de carregamento do modelo afeta diretamente a rapidez com que os pods podem começar a atender às requisições de inferência. Isso é especialmente verdadeiro durante eventos de aumento vertical da escala, quando novos pods ou nós precisam carregar o modelo antes de lidar com o tráfego. A inicialização do modelo tem duas fases que você pode melhorar com cache de artefatos de compilação e ajuste:

  • Carregamento de pesos: transferência via streaming dos arquivos de pesos do modelo do Amazon S3 para a memória da GPU utilizando Run:ai Model Streamer.

  • torch.compile: compilação do grafo de computação do modelo em kernels otimizados e fundidos do CUDA/Triton. Essa compilação é executada na primeira inicialização e pode afetar significativamente o tempo de início, dependendo do tamanho do modelo.

Este tópico mostra como você pode otimizar o desempenho do Run:ai Model Streamer e o armazenamento em cache do torch.compile para reduzir as duas fases do processo de carregamento do modelo. Para obter o procedimento completo de implantação do vLLM no Amazon EKS para inferência, consulte Carregar e servir modelos no Amazon EKS.

Otimizar o caminho de rede do S3 no Modo Automático do EKS

Se você estiver executando no Modo Automático do EKS com nós de GPU em sub-redes privadas, recomendamos usar um endpoint da VPC de gateway para S3 para otimizar o caminho de rede entre seus nós e o S3. Com o endpoint da VPC de gateway, o tráfego para o S3 permanece na rede da AWS e ignora totalmente o gateway NAT, portanto, não há limite de largura de banda compartilhada nem cobrança de processamento de dados NAT por GB. Sem o endpoint da VPC de gateway, quando o tráfego passa por um gateway NAT, este torna-se um gargalo compartilhado durante a expansão quando vários nós baixam o modelo ao mesmo tempo.

O Modo Automático do EKS geralmente coloca nós em sub-redes privadas, portanto, o tráfego para o S3 flui por meio de um gateway NAT por padrão. Um gateway NAT fornece até 100 Gbps de largura de banda e 55 mil conexões simultâneas por destino. No entanto, essa largura de banda é compartilhada entre todos os nós da sub-rede privada. Durante um evento de aumento vertical da escala, vários nós que baixam o modelo completo ao mesmo tempo disputam a mesma largura de banda do gateway NAT, o que pode desacelerar o carregamento dos pesos do modelo em todos eles.

Ajuste de desempenho do Run:ai Model Streamer

Mecanismos de inferência como o vLLM e SGLang usam o Run:ai Model Streamer como um mecanismo alternativo para carregar pesos durante a inicialização da inferência.

Por padrão, o Run:ai Model Streamer usa configurações conservadoras de simultaneidade ao baixar arquivos de pesos de modelos do S3. Aumentar a simultaneidade do download e o tamanho do fragmento reduz o tempo de carregamento dos pesos dos modelos ao baixar mais dados em paralelo.

Calcular a simultaneidade ideal

Calcule o valor de simultaneidade ideal como:

concurrency = ceil(total_model_size_gb / chunk_size_gb)

Substitua o valor de concurrency que você usa com base no tamanho do modelo e no tamanho do fragmento. Consulte a tabela a seguir para ver alguns exemplos. Por exemplo, com um modelo de 67 GB e um tamanho de fragmento de 4 GB: ceil(67 / 4) = 17.

Tamanho do modelo Tamanho do bloco Simultaneidade

10 GB

4 GB

3

67 GB

4 GB

17

140 GB

4 GB

35

Aplicar a configuração

Adicione os seguintes argumentos e variáveis de ambiente à especificação do seu contêiner de inferência:

  • --tensor-parallel-size: o grau de paralelismo de tensores (TP) geralmente é o número mínimo de GPUs necessárias para comportar o modelo na memória da GPU com base no tamanho do modelo. Por exemplo, um modelo de 67 GB em um tipo de instância p5.48xlarge requer pelo menos 2 GPUs, então defina-o como 2.

  • concurrency e distributed (em --model-loader-extra-config): defina concurrency como o valor calculado para seu modelo. Defina distributed como true somente ao usar o paralelismo de tensores (TP > 1). Quando habilitado, cada classificação de paralelismo de tensor transmite seu próprio fragmento de peso diretamente do Amazon S3, em vez de a classificação 0 carregar todos os pesos e transmiti-los para as outras classificações. Isso melhora significativamente o desempenho de carregamento para implantações com várias GPUs. Deixe sem definição (ou false) para TP=1, pois não há nenhum benefício nessa opção. Essa opção requer a arquitetura vLLM V1. É incompatível com --enforce-eager, o que força o caminho V0; usar os dois juntos causará um erro ou retornará silenciosamente ao carregamento não distribuído.

  • RUNAI_STREAMER_CHUNK_BYTESIZE: tamanho do bloco de 4 GB. Esse valor mostra de forma consistente o melhor desempenho em todas as avaliações comparativas. Blocos maiores reduzem o número de requisições do S3 e melhoram o throughput em instâncias de alta largura de banda.

  • RUNAI_STREAMER_S3_REQUEST_TIMEOUT_MS: o tempo limite por requisição em milissegundos. Permite uma nova tentativa mais rápida em respostas lentas do S3.

  • RUNAI_STREAMER_S3_LOW_SPEED_LIMIT: velocidade mínima de transferência em bytes por segundo antes que uma requisição seja considerada lenta e executada novamente.

containers: - name: vllm-inference image: vllm/vllm-openai:v0.21.0 command: - python3 - -m - vllm.entrypoints.openai.api_server args: # ... your existing args ... - --model=s3://<MODEL_PATH> - --tensor-parallel-size=2 - --load-format=runai_streamer - --model-loader-extra-config={"concurrency":17,"distributed":true} env: - name: RUNAI_STREAMER_CHUNK_BYTESIZE value: "4294967296" - name: RUNAI_STREAMER_S3_REQUEST_TIMEOUT_MS value: "3000" - name: RUNAI_STREAMER_S3_LOW_SPEED_LIMIT value: "1048576"

Otimizar a inicialização a frio de torch.compile

A etapa torch.compile do processo de disponibilização de inferência traça o grafo computacional de um modelo (a sequência de operações matemáticas) e o compila em kernels CUDA/Triton fundidos otimizados. Ele não compila os pesos dos modelos, apenas as operações que os transformam.

Os mecanismos de inferência usam o torch.compile porque ele fornece melhorias significativas no throughput automaticamente, sem engenharia de kernel personalizada por arquitetura de modelo:

  • Fusão de kernel: várias pequenas operações (soma residual, normalização de camada, ativação) são fundidas em um único kernel, reduzindo as idas e vindas à memória da GPU.

  • Menos inicializações de kernel: uma camada de transformador reduz de aproximadamente 15 a 30 kernels CUDA individuais para cerca de 10 fundidos, diminuindo a sobrecarga da CPU a cada inicialização.

  • Python removido do caminho dinâmico: toda a passagem direta torna-se um plano de execução em C++, eliminando a sobrecarga do interpretador Python entre as operações.

  • Melhor compatibilidade com o CUDA Graph: grafos estáticos compilados são capturados e reproduzidos com quase zero de sobrecarga de CPU.

  • Otimização automática: funciona para qualquer arquitetura de modelo. O throughput melhora de 5 a 30% em relação ao modo eager.

A tabela a seguir mostra os mecanismos comuns de serviço de inferência que usam o torch.compile.

Mecanismo uso de torch.compile

vLLM

Habilitado por padrão (arquitetura V1)

SGLang

Opcional via --enable-torch-compile

TensorRT-LLM

O novo caminho é compatível com essa opção junto com a compilação do mecanismo legado

O problema de inicialização a frio do torch.compile

A desvantagem do torch.compile é que o primeiro pod de inferência deve compilar antes de poder atender às requisições. Essa compilação pode levar vários minutos, dependendo do tamanho do modelo. Os artefatos compilados são pequenos (~15 MB para um modelo de 60 GB), mas aumentam significativamente o tempo de inicialização a frio. Como os artefatos são pequenos e determinísticos para uma configuração específica, você pode armazená-los em cache e reutilizá-los para eliminar a penalidade de inicialização a frio nas inicializações subsequentes de pods e nós.

Os artefatos consistem em:

  • Arquivos-fonte de kernel Triton gerados

  • Binários do kernel compilados (.cubin)

  • Estrutura de grafo que sequencia as chamadas do kernel

A relação de custo-benefício da opção --enforce-eager

O vLLM habilita o torch.compile e a captura de grafos do CUDA por padrão. O sinalizador --enforce-eager desativa os dois e executa o modelo no modo eager, em que cada operação é executada imediatamente por meio do interpretador Python. Como o modo eager ignora a compilação e a captura de grafos, alguns guias de início rápido, incluindo a implantação base em Carregar e servir modelos no Amazon EKS, usam --enforce-eager para iniciar os pods mais rapidamente.

O --enforce-eager é uma opção válida para depuração, implantações com restrição de memória ou arquiteturas de modelo que não são compiladas de forma limpa, e você pode usá-lo na produção por esses motivos. Embora isso possa mitigar a penalidade de inicialização a frio do torch.compile, recomendamos outras abordagens, como as detalhadas nas seções subsequentes desta página para manter o desempenho do runtime na produção.

Entenda a compensação entre o tempo de inicialização e o desempenho no runtime de --enforce-eager antes de implantar na produção.

Aspecto Com --enforce-eager (modo eager) Padrão (torch.compile + grafos do CUDA)

Horário de início

Rápido: sem etapa de compilação ou captura de grafos

Inicialização a frio lenta (o problema resolvido por Armazenar em cache os artefatos do torch.compile no mesmo nó e Pré-aqueça o cache torch.compile em novos nós)

Throughput em estado estável

Referência

~5 a 30% maior em relação à fusão do kernel

Latência por token (lote pequeno)

Maior sobrecarga de lançamento da CPU

Muito mais baixo: os grafos do CUDA reproduzem os lançamentos do kernel como uma unidade

Memória da GPU

Menor e mais previsível

Superior: a captura de grafos pré-aloca grupos de buffers

Capacidade de depuração

Rastreamentos de pilha limpos por operação

Erros surgem dentro dos kernels gerados

Armazenar em cache os artefatos do torch.compile no mesmo nó

Essa técnica se aplica quando você usa um mecanismo de inferência compatível com o torch.compile, como o vLLM (habilitado por padrão) ou o SGLang (habilitado via --enable-torch-compile). Funciona somente quando torch.compile está ativo, ou seja, quando --enforce-eager não está configurado.

Importante

Essa melhoria não terá efeito se torch.compile estiver desabilitado. No vLLM, o sinalizador --enforce-eager desabilita o torch.compile totalmente, portanto, nenhum artefato é compilado ou armazenado em cache. Se você acompanhou a implantação de base em Carregar e servir modelos no Amazon EKS com --enforce-eager, o vLLM cria o diretório de cache, mas nunca grava nele. Remova --enforce-eager antes de aplicar essa técnica.

Quando um mecanismo de inferência compila o grafo computacional do modelo na primeira inicialização, você pode armazenar em cache os kernels otimizados resultantes no armazenamento local do nó. Os pods subsequentes no mesmo nó reutilizam os artefatos em cache e ignoram totalmente a etapa de compilação, o que pode reduzir significativamente o tempo de inicialização.

Adicionar variáveis de ambiente de cache

Adicione as variáveis de ambiente a seguir à especificação do seu contêiner de inferência para direcionar o torch.compile e o cache do Triton para um caminho de host persistente. Recomendamos usar o armazenamento de instâncias NVMe local do nó e não o volume raiz do Amazon Elastic Block Store (Amazon EBS) para o hostPath. Para ver exemplos, consulte a seção a seguir.

containers: - name: vllm-inference env: # torch.compile cache - name: XDG_CACHE_HOME value: "/compile-cache" - name: TORCHINDUCTOR_CACHE_DIR value: "/compile-cache/inductor" - name: TRITON_CACHE_DIR value: "/compile-cache/triton" volumeMounts: - name: compile-cache mountPath: /compile-cache volumes: - name: compile-cache hostPath: path: /mnt/k8s-disks/0/compile-cache type: DirectoryOrCreate

Defina o caminho do cache para o armazenamento de instâncias NVMe

Artefatos compilados e pesos via streaming se beneficiam do rápido armazenamento local. Em instâncias de GPU com armazenamento de instâncias NVMe (como instâncias da família G e da família P), o armazenamento de instâncias fornece aproximadamente 30 GB/s, em comparação com aproximadamente 1 GB/s para o volume raiz do Amazon EBS. Direcione o hostPath do cache para o ponto de montagem do NVMe para otimizar o throughput.

Importante

O volume do hostPath usa type: DirectoryOrCreate. Se você apontar para um caminho que não seja apoiado pelo armazenamento de instâncias NVMe, o Kubernetes criará silenciosamente o diretório no volume raiz do Amazon EBS. O cache ainda funciona, mas você perde o benefício de desempenho do NVMe sem nenhum erro ou aviso.

O ponto de montagem do NVMe e a forma como você o habilita diferem entre o Modo Automático do EKS e os nós autogerenciados:

Computação Ponto de montagem do NVMe Como habilitar o armazenamento de instâncias NVMe

Modo Automático do EKS

/mnt/.ephemeral

Habilitado dinamicamente com base no armazenamento temporário solicitado. O Modo Automático do EKS formata e monta o armazenamento de instâncias NVMe como uma matriz RAID 0 quando a instância tem várias unidades NVMe somente quando o ephemeralStorage.size solicitado no NodeClass é menor que a capacidade NVMe disponível da instância. Se o ephemeralStorage.size requisitado for igual ou maior que a capacidade do NVMe, o Modo Automático do EKS não usará o armazenamento de instância e o caminho será apoiado pelo volume raiz do EBS.

Karpenter autogerenciado

/mnt/k8s-disks/0

Defina instanceStorePolicy: RAID0 no EC2NodeClass do Karpenter. Sem isso, o Karpenter ignora os volumes de armazenamento de instâncias e o caminho não é apoiado pelo NVMe.

Em Modo Automático do EKS, defina o hostPath como /mnt/.ephemeral/compile-cache na especificação do seu contêiner:

volumes: - name: compile-cache hostPath: path: /mnt/.ephemeral/compile-cache type: DirectoryOrCreate

Para o Karpenter autogerenciado, defina o hostPath como /mnt/k8s-disks/0/compile-cache na especificação do seu contêiner:

volumes: - name: compile-cache hostPath: path: /mnt/k8s-disks/0/compile-cache type: DirectoryOrCreate

Exemplos de resultados

  1. O primeiro pod em um nó executa torch.compile e grava os kernels compilados em /compile-cache no host.

  2. Os pods subsequentes no mesmo nó montam o cache existente e ignoram totalmente a compilação, o que reduz a inicialização a frio do torch.compile de ~50 a 80s para ~4 a 6s.

A tabela a seguir mostra a melhoria com o armazenamento em cache do mesmo nó para diferentes tamanhos de modelo:

Tamanho do modelo Primeiro pod torch.compile (sem cache) Pods subsequentes torch.compile (mesmo nó)

60 GB

~53s

~6s

140 GB

~60s

~6s

640 GB

~80s

~6s

Pré-aqueça o cache torch.compile em novos nós

Essa técnica se aplica quando você está executando uma inferência de vários nós com GPUs homogêneas, paralelismo de tensores, modelos e versões do PyTorch em todos os nós. A técnica em Armazenar em cache os artefatos do torch.compile no mesmo nó foca o caso de um único nó, mas os novos nós adicionados durante eventos de aumento vertical da escala começam com um cache torch.compile vazio. Para reduzir ainda mais o tempo de inicialização a frio em nós recém-inicializados, você pode implementar um mecanismo de cache que armazena os artefatos torch.compile compilados no S3 e os pré-baixa para novos nós quando eles ingressam no cluster.

A abordagem geral é:

  1. Depois que o primeiro pod compilar o modelo no primeiro nó, faça o upload dos artefatos torch.compile (aproximadamente 15 MB) para um bucket do S3.

  2. Quando novos nós ingressarem no cluster, baixe os artefatos em cache para o armazenamento local do nó antes que os pods de inferência sejam agendados.

Por exemplo, você pode implementar um DaemonSet que seja executado em nós de GPU que empacota e faz upload do cache torch.compile para o S3. Ele também pode sincronizar esse cache com o armazenamento local do nó antes que os pods de inferência sejam agendados em novos nós.

Considerações sobre o armazenamento em cache entre nós

Quando você armazena em cache artefatos torch.compile entre nós, os kernels compilados são válidos somente quando esses parâmetros coincidem entre o nó que gerou o cache e o nó que o consome. Seu mecanismo de armazenamento em cache deve considerar tudo isso. Uma incompatibilidade em qualquer parâmetro produz um cache inválido que força a recompilação ou causa erros de runtime. Sua ferramenta de armazenamento em cache deve diferenciar os artefatos por esses parâmetros, por exemplo, incorporando-os à chave de objeto do S3 ou à estrutura do diretório de cache.

Parâmetro Por que isso importa?

Tipo de GPU

Os kernels compilados são específicos da arquitetura da GPU (por exemplo, sm_90 para H100 vs sm_89 para L4).

Paralelismo de tensores (TP)

Diferentes graus de TP produzem diferentes partições do grafo de computação.

Modelo

Cada arquitetura e tamanho do modelo é compilado em diferentes kernels.

Versão PyTorch

As estruturas internas dos compiladores torch.compile e Triton podem introduzir alterações incompatíveis entre versões.

Exemplos de resultados

Em testes direcionais com pré-aquecimento de cache entre nós (Qwen3-6-35B-A3B, 67 GB, 2x GPU com TP=2 em p5.48xlarge), o primeiro pod em nós recém-escalados alcançou o mesmo tempo de inicialização dos pods subsequentes em um nó já aquecido:

Cenário Primeiro pod Segundo pod

Sem cache entre nós (novo nó)

65s

16s

Com cache entre nós (novo nó)

16s

16s

Exemplo de implantação

O exemplo a seguir combina o ajuste de desempenho do Run:AI Model Streamer e um cache torch.compile em um único manifesto de implantação do vLLM. Substitua os valores dos espaços reservados pela sua própria configuração:

  • serviceAccountName: conta de serviço com um perfil do IAM que tem acesso de leitura do Amazon S3 ao seu bucket de modelos.

  • nodeSelector (karpenter.sh/nodepool): o nome do seu grupo de nós da GPU (por exemplo, gpu-nodepool-g6e-12xlarge).

  • --model: o caminho do Amazon S3 até os pesos do seu modelo.

  • --model-loader-extra-config: defina concurrency com base no tamanho do seu modelo: ceil(total_model_size_gb / chunk_size_gb). Por exemplo, um modelo de 67 GB com um tamanho de bloco de 4 GB oferece ceil(67 / 4) = 17.

  • --tensor-parallel-size: defina o grau de paralelismo de tensores (TP) para o número mínimo de GPUs necessárias para comportar o modelo na memória.

  • hostPath path: ponto de montagem do armazenamento de instâncias NVMe para nós autogerenciados com o Karpenter. No Modo Automático do EKS, use /mnt/.ephemeral/compile-cache. Para mais detalhes, consulte Armazenar em cache os artefatos do torch.compile no mesmo nó.

apiVersion: apps/v1 kind: Deployment metadata: name: vllm-inference namespace: default spec: replicas: 1 selector: matchLabels: app: vllm-inference template: metadata: labels: app: vllm-inference spec: serviceAccountName: <SERVICE_ACCOUNT_NAME> nodeSelector: karpenter.sh/nodepool: <GPU_NODEPOOL> tolerations: - key: nvidia.com/gpu operator: Exists effect: NoSchedule containers: - name: vllm image: vllm/vllm-openai:v0.21.0 command: - python3 - -m - vllm.entrypoints.openai.api_server args: - --model=s3://<BUCKET_NAME>/<MODEL_PATH> - --load-format=runai_streamer - --model-loader-extra-config={"concurrency":17,"distributed":true} - --tensor-parallel-size=2 - --max-model-len=8192 - --host=0.0.0.0 - --port=8000 ports: - containerPort: 8000 name: http env: # Run:ai streamer tuning - name: RUNAI_STREAMER_CHUNK_BYTESIZE value: "4294967296" - name: RUNAI_STREAMER_S3_REQUEST_TIMEOUT_MS value: "3000" - name: RUNAI_STREAMER_S3_LOW_SPEED_LIMIT value: "1048576" # torch.compile cache - name: XDG_CACHE_HOME value: "/compile-cache" - name: TORCHINDUCTOR_CACHE_DIR value: "/compile-cache/inductor" - name: TRITON_CACHE_DIR value: "/compile-cache/triton" resources: requests: cpu: "12" memory: 80Gi nvidia.com/gpu: "2" limits: nvidia.com/gpu: "2" volumeMounts: - name: compile-cache mountPath: /compile-cache startupProbe: httpGet: path: /health port: 8000 periodSeconds: 10 failureThreshold: 60 initialDelaySeconds: 30 readinessProbe: httpGet: path: /health port: 8000 periodSeconds: 5 timeoutSeconds: 3 volumes: - name: compile-cache hostPath: path: /mnt/k8s-disks/0/compile-cache type: DirectoryOrCreate