Observação:

Implantar Pilha de Produção do vLLM OpenAI no Oracle Kubernetes Engine (OKE)

Introdução

As organizações que adotam grandes modelos de linguagem (LLMs) para cargas de trabalho de produção enfrentam uma decisão crítica de infraestrutura: confie em APIs de inferência de terceiros ou implante uma pilha de inferência auto-hospedada. As implementações auto-hospedadas oferecem vantagens significativas: total privacidade de dados e controle de conformidade, latência de inferência de menos de 100 milissegundos, eliminando viagens de ida e volta da rede, custo previsível em escala e a liberdade de ajustar e atender a qualquer modelo de código aberto sem restrição de fornecedor.

No entanto, a criação de uma pilha de inferência de LLM de nível de produção do zero é complexa. Ele requer orquestração de contêineres com reconhecimento de GPU, roteamento inteligente de solicitações em várias réplicas de modelo, armazenamento persistente para grandes pesos de modelo e monitoramento contínuo — tudo integrado e executado de forma confiável.

A Oracle Cloud Infrastructure oferece vários caminhos para inferência de IA. O Serviço OCI Generative AI fornece uma experiência totalmente gerenciada com clusters de IA dedicados isolados para sua tenancy, ideal para equipes que desejam começar rapidamente com modelos suportados. Este tutorial adota a abordagem alternativa: implantar sua própria pilha de inferência no OKE. Esse caminho é projetado para equipes que precisam de controle preciso sobre drivers de GPU, versões de CUDA, configurações de modelos e parâmetros de atendimento, ou equipes que estão treinando e ajustando modelos personalizados e desejam atendê-los diretamente. A OCI fornece instâncias de GPU bare metal com GPUs NVIDIA A10, A100 e H100, conectadas por rede de cluster RDMA de latência ultrabaixa, oferecendo o mesmo nível de controle de hardware que você teria on-premises e se beneficiando da elasticidade da nuvem.

A pilha de produção vLLM resolve a complexidade da inferência auto-hospedada, fornecendo uma plataforma nativa de Kubernetes de código aberto criada no vLLM, o mecanismo de inferência de alto rendimento usado na produção por organizações como Meta, Mistral AI e IBM. Ele oferece uma taxa de transferência até 24x maior em comparação com estruturas de serviço padrão por meio de gerenciamento eficiente de memória de GPU e otimização de cache KV. Combinado com formas de OKE e OCI GPU, você obtém uma plataforma de inferência pronta para produção com rede, armazenamento e segurança de nível empresarial. Os scripts de implantação do OCI usados neste tutorial são contribuídos e mantidos no repositório oficial de pilha de produção vLLM.

Este tutorial orienta você na implantação da Pilha de Produção vLLM no OKE, desde o provisionamento da infraestrutura até a execução da sua primeira solicitação de inferência.

Observação: Este tutorial provisiona recursos passo a passo usando a CLI do OCI para ajudar você a entender o fluxo completo de recursos de nuvem do OCI necessários para uma implantação de inferência de GPU. Para ambientes de produção, é recomendável codificar essa infraestrutura usando o Terraform ou o OCI Resource Manager (Shepherd) para implantações repetíveis e controladas por versão.

Diagrama de arquitetura mostrando a VCN com sub-redes, cluster do OKE, pool de nós de GPU, pods de vLLM e balanceador de carga

Os seguintes serviços do OCI são usados neste tutorial:

Serviço Objetivo
Oracle Kubernetes Engine (OKE) Cluster Kubernetes gerenciado para orquestração de contêineres e programação de carga de trabalho de GPU
OCI Compute (Formas GPU) Instâncias de GPU NVIDIA A10 (24 GB) e A100 (80 GB) para inferência de modelo
Volumes em Blocos do OCI Armazenamento persistente para pesos de modelo com camadas de desempenho configuráveis
VCN (Rede Virtual na Nuvem) do OCI Infraestrutura de rede, incluindo sub-redes, gateways e listas de segurança
Balanceador de Carga do OCI Acesso externo a pontos finais de inferência
OCI Bastion Túneis SSH gerenciados para acesso ao cluster privado
OCI Object Storage Origem de modelo alternativa usando URLs de Solicitação Pré-Autenticada (PAR)

Objetivos

Neste tutorial, você:

Pré-requisitos

Observação: os exemplos de saídas e capturas de tela neste tutorial usam us-chicago-1. Você pode implantar em qualquer região suportada definindo OCI_REGION. A capacidade da GPU varia de acordo com a região e o domínio de disponibilidade; portanto, confirme se a sua forma de GPU de destino está disponível antes da implantação. Verifique a disponibilidade da forma de GPU por região e prepare-se para experimentar outro domínio de disponibilidade (GPU_AD_INDEX) se você encontrar erros de capacidade.

Observação: Este tutorial provisiona recursos de GPU pagos (por exemplo, VM.GPU.A10.1). Não é uma carga de trabalho Always Free da OCI. Sempre execute as etapas de limpeza quando terminar para evitar cobranças contínuas.

Observação: Este tutorial implanta o openai/gpt-oss-20b, um modelo licenciado do Apache 2.0 do OpenAI. Nenhum token Hugging Face é necessário. Se você quiser implantar modelos fechados, como o Meta Llama 3.1, precisará de uma conta do Hugging Face com um token de API.

Tarefa 1: Configurar Variáveis de Ambiente

Defina a configuração do OCI necessária antes de implantar a infraestrutura.

  1. Localize o OCID do compartimento na Console do OCI. Navegue até Identity & Security > Compartments e clique no compartimento de destino e copie o OCID.

    oci iam compartment list --query 'data[].{name:name,id:id}' --output table

    Saída da CLI do OCI mostrando o OCID do compartimento

  2. Exporte a variável de ambiente necessária.

    export OCI_COMPARTMENT_ID="ocid1.compartment.oc1..xxxxx"
  3. Opcionalmente, substitua a configuração padrão definindo qualquer uma das variáveis de ambiente a seguir.

    Variável Padrão Descrição
    OCI_REGION us-ashburn-1 Região da OCI para implantação
    OCI_PROFILE DEFAULT Perfil de configuração da CLI do OCI
    CLUSTER_NAME production-stack Nome do cluster do OKE
    GPU_SHAPE VM.GPU.A10.1 Forma de computação GPU para o pool de nós
    GPU_NODE_COUNT 1 Número de nós de GPU no pool
    GPU_BOOT_VOLUME_GB 200 Tamanho do volume de inicialização em GB para nós de GPU
    CPU_BOOT_VOLUME_GB 100 Tamanho do volume de inicialização em GB para nós da CPU
    GPU_AD_INDEX 1 Índice de domínio de disponibilidade (com base em 0) para posicionamento de GPU
    PRIVATE_CLUSTER true Definir como false para um ponto final público da API do Kubernetes
    KUBERNETES_VERSION v1.31.10 Versão do Kubernetes para o cluster do OKE

    Por exemplo, para implantar com dois nós de GPU A100:

    export OCI_COMPARTMENT_ID="ocid1.compartment.oc1..xxxxx"
    export GPU_SHAPE="BM.GPU.A100-v2.8"
    export GPU_NODE_COUNT="2"
  4. Revise as formas de GPU disponíveis e selecione uma com base nos requisitos de tamanho do modelo.

    Forma GPUs Tipo de GPU Memória de GPU Recomendado Para
    VM.GPU.A10.1 1 NVIDIA A10 24 GB Modelos de parâmetros 7B–13B
    VM.GPU.A10.2 2 NVIDIA A10 48 GB Tensor paralelo com modelos pequenos
    BM.GPU4.8 8 NVIDIA A100 40 GB 320 GB Modelos 70B, econômicos
    BM.GPU.A100-v2.8 8 NVIDIA A100 80 GB 640 GB Mais de 70B modelos de parâmetros
    BM.GPU.H100.8 8 NVIDIA H100 640 GB Maiores modelos, suporte a RDMA

    Observação: As formas bare metal (BM.*) fornecem hardware dedicado sem sobrecarga de virtualização e suportam paralelismo de tensor de vários GPUs. As formas de máquina virtual (VM.*) são mais econômicas para modelos menores.

    Observação: Este tutorial usa o VM.GPU.A10.1 (único NVIDIA A10 com memória de GPU de 24 GB) para implantar o openai/gpt-oss-20b, um modelo de Mistura de Especialistas (MoE) com parâmetros ativos 3.6B que geralmente se encaixa em uma única GPU A10. As seções avançadas demonstram configurações de vários GPUs usando BM.GPU.H100.8 para modelos maiores, como Llama 3.1 70B.

Tarefa 2: Implantar Usando o Script Automatizado (Início Rápido)

A Pilha de Produção vLLM inclui um script de implantação automatizada que provisiona todos os recursos da OCI e implanta a pilha de inferência com um único comando. Use essa abordagem para uma implantação rápida. As tarefas de 3 a 10 abrangem cada etapa individualmente para os usuários que desejam personalizar o processo.

  1. Clone o repositório da Pilha de Produção vLLM.

    git clone https://github.com/vllm-project/production-stack.git
    cd production-stack/deployment_on_cloud/oci
  2. Exporte o OCID do compartimento.

    export OCI_COMPARTMENT_ID="ocid1.compartment.oc1..xxxxx"
  3. Execute o script de implantação.

    ./entry_point.sh setup

    Saída de terminal da configuração entry_point.sh mostrando a criação de VCN, cluster, bastion e pool de nós

    Para clusters públicos (PRIVATE_CLUSTER=false), a configuração cria toda a infraestrutura e implanta a pilha vLLM em um único comando. Informe o arquivo de valores do Helm como segundo argumento:

    PRIVATE_CLUSTER=false ./entry_point.sh setup ./production_stack_specification.yaml

    Para clusters privados (o padrão), a configuração cria a infraestrutura, mas não pode acessar a API do Kubernetes diretamente. Abra um terminal separado e inicie o túnel; em seguida, implante:

    # In a separate terminal, start the SSH tunnel (auto-reconnects on drops):
    ./entry_point.sh tunnel
    
    # Back in the first terminal, deploy vLLM:
    ./entry_point.sh deploy-vllm ./production_stack_specification.yaml
  4. Verifique se a implantação está em execução.

    kubectl get pods

    Saída esperada:

    NAME                                            READY   STATUS    RESTARTS   AGE
    vllm-deployment-router-xxxxxxxxxx-xxxxx          1/1     Running   0          5m
    vllm-gpt-oss-deployment-vllm-xxxxxxxxxx-xxxxx    1/1     Running   0          5m

Observação: Se ambos os pods mostrarem o status Running, sua implantação estará pronta. Vá para a Tarefa 10: Testar o Ponto Final de Inferência.

Observação: as instâncias de GPU estão sujeitas a restrições de capacidade do OCI. Se o script permanecer no loop "Aguardando o nó da GPU" por mais de 15 minutos, a forma da GPU poderá não estar disponível no domínio de disponibilidade selecionado. Verifique o status do pool de nós com oci ce node-pool get e procure erros de "Capacidade insuficiente do host". Para resolver isso, faça a limpeza com ./entry_point.sh cleanup e reimplante com outro domínio de disponibilidade (por exemplo, GPU_AD_INDEX=0 ou GPU_AD_INDEX=2) ou outra forma de GPU (por exemplo, GPU_SHAPE=VM.GPU.A10.2).

Observação: O script de implantação usa instâncias de GPU que incorrem em custos significativos (aproximadamente US$ 50/dia para uma única GPU A10). Sempre execute ./entry_point.sh cleanup quando terminar de evitar cobranças contínuas.

Tarefa 3: Criar VCN e Rede

Criar a infraestrutura de rede do OCI necessária para o cluster do OKE. Isso inclui uma VCN (Rede Virtual na Nuvem), gateways, tabelas de roteamento, listas de segurança e sub-redes. Cada recurso de rede é criado em alguns segundos; o conjunto completo de comandos é concluído em menos de 2 minutos.

  1. Crie uma VCN com um bloco CIDR 10.0.0.0/16.

    VCN_ID=$(oci network vcn create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --display-name "${CLUSTER_NAME}-vcn" \
        --cidr-blocks '["10.0.0.0/16"]' \
        --dns-label "prodstack" \
        --query "data.id" \
        --raw-output)
  2. Criar um Gateway de Internet para roteamento de sub-rede pública.

    IGW_ID=$(oci network internet-gateway create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-igw" \
        --is-enabled true \
        --query "data.id" \
        --raw-output)
  3. Criar um Gateway NAT para tráfego de saída de sub-redes privadas.

    NAT_ID=$(oci network nat-gateway create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-nat" \
        --query "data.id" \
        --raw-output)
  4. Criar um Gateway de Serviço para acesso ao Oracle Services Network. O controlador de nuvem do OKE usa o Oracle Services para inicializar nós de trabalho (definir labels de domínio de disponibilidade, remover taints de inicialização). Sem um Gateway de Serviço, os nós de GPU podem permanecer em um estado não inicializado e o provisionamento do volume em blocos falhará.

    SGW_SERVICE_ID=$(oci network service list \
        --query "data[?contains(name, 'All') && contains(name, 'Services')].id | [0]" \
        --raw-output)
    
    SGW_SERVICE_NAME=$(oci network service list \
        --query "data[?contains(name, 'All') && contains(name, 'Services')].\"cidr-block\" | [0]" \
        --raw-output)
    
    SGW_ID=$(oci network service-gateway create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-sgw" \
        --services "[{\"serviceId\": \"${SGW_SERVICE_ID}\"}]" \
        --query "data.id" \
        --raw-output)
  5. Crie tabelas de roteamento para sub-redes públicas e privadas.

    PRIVATE_RT_ID=$(oci network route-table create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-private-rt" \
        --route-rules "[
            {\"cidrBlock\": \"0.0.0.0/0\", \"networkEntityId\": \"${NAT_ID}\"},
            {\"destination\": \"${SGW_SERVICE_NAME}\", \"destinationType\": \"SERVICE_CIDR_BLOCK\", \"networkEntityId\": \"${SGW_ID}\"}
        ]" \
        --query "data.id" \
        --raw-output)
    
    PUBLIC_RT_ID=$(oci network route-table create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-public-rt" \
        --route-rules "[{\"cidrBlock\": \"0.0.0.0/0\", \"networkEntityId\": \"${IGW_ID}\"}]" \
        --query "data.id" \
        --raw-output)

    Observação: A tabela de roteamento privada tem duas regras: uma rota do Gateway NAT para acesso geral à internet (extraindo imagens de contêiner, fazendo download de modelos) e uma rota do Gateway de Serviço para acesso direto ao Oracle Services Network. A rota do Gateway de Serviço é crítica. Sem ela, o controlador de nuvem do OKE não poderá inicializar nós de trabalho, o que impede o provisionamento do volume em blocos. A tabela de roteamento pública usa o Gateway de Internet para acesso do balanceador de carga.

  6. Crie uma lista de segurança com as regras de entrada e saída necessárias para o OKE.

    SL_ID=$(oci network security-list create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-sl" \
        --egress-security-rules '[{"destination": "0.0.0.0/0", "protocol": "all", "isStateless": false}]' \
        --ingress-security-rules '[
            {"source": "0.0.0.0/0", "protocol": "6", "isStateless": false, "tcpOptions": {"destinationPortRange": {"min": 22, "max": 22}}, "description": "SSH access"},
            {"source": "10.0.0.0/16", "protocol": "all", "isStateless": false, "description": "VCN internal traffic"},
            {"source": "10.244.0.0/16", "protocol": "all", "isStateless": false, "description": "Kubernetes pods CIDR"},
            {"source": "10.96.0.0/16", "protocol": "all", "isStateless": false, "description": "Kubernetes services CIDR"},
            {"source": "0.0.0.0/0", "protocol": "1", "isStateless": false, "icmpOptions": {"type": 3, "code": 4}, "description": "Path MTU discovery"}
        ]' \
        --query "data.id" \
        --raw-output)

    Nota de Segurança: Este exemplo de lista de segurança é intencionalmente amplo para simplificar. Para produção, restrinja o SSH à sub-rede bastion e à sua faixa de IPs e prefira listas de segurança separadas ou NSGs por sub-rede para que a sub-rede do balanceador de carga não permita o SSH de 0.0.0.0/0.

    Padrão seguro: Comece limitando o SSH ao seu IP público e anexando regras SSH somente à sub-rede bastion. Você pode manter os CIDRs de pod/serviço do Kubernetes na sub-rede de trabalho e omitir o SSH inteiramente da sub-rede do balanceador de carga.

    Divisão opcional (recomendada): Crie uma pequena lista de segurança somente SSH para a sub-rede bastion e uma lista separada para sub-redes worker/LB.

    BASTION_SL_ID=$(oci network security-list create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-bastion-sl" \
        --egress-security-rules '[{"destination": "0.0.0.0/0", "protocol": "all", "isStateless": false}]' \
        --ingress-security-rules '[
            {"source": "YOUR_PUBLIC_IP/32", "protocol": "6", "isStateless": false, "tcpOptions": {"destinationPortRange": {"min": 22, "max": 22}}, "description": "SSH from your IP"}
        ]' \
        --query "data.id" \
        --raw-output)
    
    WORKER_SL_ID=$(oci network security-list create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-worker-sl" \
        --egress-security-rules '[{"destination": "0.0.0.0/0", "protocol": "all", "isStateless": false}]' \
        --ingress-security-rules '[
            {"source": "10.0.0.0/16", "protocol": "all", "isStateless": false, "description": "VCN internal traffic"},
            {"source": "10.244.0.0/16", "protocol": "all", "isStateless": false, "description": "Kubernetes pods CIDR"},
            {"source": "10.96.0.0/16", "protocol": "all", "isStateless": false, "description": "Kubernetes services CIDR"},
            {"source": "0.0.0.0/0", "protocol": "1", "isStateless": false, "icmpOptions": {"type": 3, "code": 4}, "description": "Path MTU discovery"}
        ]' \
        --query "data.id" \
        --raw-output)
    
    LB_SL_ID=$(oci network security-list create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-lb-sl" \
        --egress-security-rules '[{"destination": "0.0.0.0/0", "protocol": "all", "isStateless": false}]' \
        --ingress-security-rules '[
            {"source": "10.0.0.0/16", "protocol": "all", "isStateless": false, "description": "VCN internal traffic"},
            {"source": "0.0.0.0/0", "protocol": "6", "isStateless": false, "tcpOptions": {"destinationPortRange": {"min": 80, "max": 80}}, "description": "HTTP (public LB)"},
            {"source": "0.0.0.0/0", "protocol": "6", "isStateless": false, "tcpOptions": {"destinationPortRange": {"min": 443, "max": 443}}, "description": "HTTPS (public LB)"}
        ]' \
        --query "data.id" \
        --raw-output)

    Observação: Se você usar apenas um balanceador de carga interno, substitua as origens 0.0.0.0/0 acima por 10.0.0.0/16 (ou seu CIDR da VCN). Uso: Anexe BASTION_SL_ID à sub-rede bastion, WORKER_SL_ID às sub-redes de API/trabalhador e LB_SL_ID à sub-rede do balanceador de carga.

    Observação: As regras CIDR de pods do Kubernetes (10.244.0.0/16) e CIDR de serviços (10.96.0.0/16) são necessárias para que os nós de trabalho de GPU se registrem no cluster. A regra ICMP tipo 3 código 4 permite a descoberta de MTU do caminho, o que impede problemas de fragmentação do pacote.

  7. Crie as sub-redes. O cluster requer quatro sub-redes: uma para o ponto final da API do Kubernetes, uma para nós de trabalho, uma para balanceadores de carga e outra para o bastion host usado para acessar o cluster privado.

    API_SUBNET_ID=$(oci network subnet create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-api-subnet" \
        --cidr-block "10.0.0.0/28" \
        --route-table-id "${PRIVATE_RT_ID}" \
        --security-list-ids "[\"${SL_ID}\"]" \
        --dns-label "kubeapi" \
        --prohibit-public-ip-on-vnic true \
        --query "data.id" \
        --raw-output)
    
    WORKER_SUBNET_ID=$(oci network subnet create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-worker-subnet" \
        --cidr-block "10.0.10.0/24" \
        --route-table-id "${PRIVATE_RT_ID}" \
        --security-list-ids "[\"${SL_ID}\"]" \
        --dns-label "workers" \
        --prohibit-public-ip-on-vnic true \
        --query "data.id" \
        --raw-output)
    
    LB_SUBNET_ID=$(oci network subnet create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-lb-subnet" \
        --cidr-block "10.0.20.0/24" \
        --route-table-id "${PUBLIC_RT_ID}" \
        --security-list-ids "[\"${SL_ID}\"]" \
        --dns-label "loadbalancers" \
        --query "data.id" \
        --raw-output)
    
    BASTION_SUBNET_ID=$(oci network subnet create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-bastion-subnet" \
        --cidr-block "10.0.30.0/24" \
        --route-table-id "${PUBLIC_RT_ID}" \
        --security-list-ids "[\"${SL_ID}\"]" \
        --dns-label "bastion" \
        --query "data.id" \
        --raw-output)
    Sub-rede CIDR Visibilidade Objetivo
    Ponto final da API 10.0.0.0/28 Privado(a) Servidor de API do Kubernetes
    nós de trabalho 10.0.10.0/24 Privado(a) Nós de computação GPU
    Balanceadores de carga 10.0.20.0/24 Público(a) Acesso ao serviço externo
    Bastion 10.0.30.0/24 Público(a) Túnel SSH para acesso ao cluster privado

Tarefa 4: Criar o Cluster do OKE

Implantar um cluster gerenciado do Kubernetes no OKE usando os recursos de rede criados na Tarefa 3. O provisionamento do cluster leva aproximadamente 10 minutos. Este tutorial cria um cluster privado (o padrão de script), que não consome um IP público reservado para o ponto final da API do Kubernetes. Os clusters privados são a abordagem recomendada para cargas de trabalho de produção porque o servidor de API não está exposto à internet pública.

  1. Crie o cluster do OKE com um ponto final privado.

    oci ce cluster create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --name "${CLUSTER_NAME}" \
        --vcn-id "${VCN_ID}" \
        --kubernetes-version "${KUBERNETES_VERSION}" \
        --endpoint-subnet-id "${API_SUBNET_ID}" \
        --service-lb-subnet-ids "[\"${LB_SUBNET_ID}\"]" \
        --endpoint-public-ip-enabled false

    O comando retorna um ID de solicitação de serviço. Obtenha o ID do cluster na lista de clusters.

    CLUSTER_ID=$(oci ce cluster list \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --lifecycle-state CREATING \
        --query 'data[0].id' \
        --raw-output)
    echo "CLUSTER_ID=${CLUSTER_ID}"

    Observação: Os clusters privados não exigem um IP público reservado. Os nós de trabalho ainda acessam a internet por meio do Gateway NAT para extrair imagens de contêiner e fazer download de modelos. Somente o acesso kubectl requer um túnel SSH por meio do bastion (configurado nas próximas etapas).

  2. Aguarde o cluster se tornar ATIVO. Esta etapa leva aproximadamente 10 minutos.

    oci ce cluster get \
        --cluster-id "${CLUSTER_ID}" \
        --query "data.\"lifecycle-state\"" \
        --raw-output

    Verifique o comando até que a saída retorne ACTIVE.

    Opcional: exiba um resumo conciso do status (incluindo o ponto final da API privada).

    oci ce cluster get \
        --cluster-id "${CLUSTER_ID}" \
        --query 'data.{name:name,state:"lifecycle-state","k8s-version":"kubernetes-version",endpoint:endpoints."private-endpoint"}' \
        --output table

    Saída da CLI do OCI mostrando o cluster do OKE no estado ACTIVE

    kubectl get nodes -o wide

    Saída de obter nós do kubectl mostrando dois nós Prontos

  3. Crie um Bastion do OCI para acessar o cluster privado. O bastion fornece um túnel SSH gerenciado para o ponto final privado da API do Kubernetes.

    BASTION_ID=$(oci bastion bastion create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --bastion-type STANDARD \
        --target-subnet-id "${BASTION_SUBNET_ID}" \
        --name "${CLUSTER_NAME}-bastion" \
        --client-cidr-list '["YOUR_PUBLIC_IP/32"]' \
        --query "data.id" \
        --raw-output)

    Observação: Substitua YOUR_PUBLIC_IP/32 pelo seu IP público atual. Para redes compartilhadas, use seu bloco CIDR corporativo.

    Espere que o bastion se torne ATIVO (aproximadamente 1 minuto).

    oci bastion bastion get \
        --bastion-id "${BASTION_ID}" \
        --query "data.\"lifecycle-state\"" \
        --raw-output

    Nota de Segurança: Para produção, não use 0.0.0.0/0. Restrinja o --client-cidr-list ao seu IP público ou CIDR corporativo (por exemplo, "YOUR_PUBLIC_IP/32"); caso contrário, qualquer pessoa na internet poderá tentar uma sessão bastion.

  4. Faça download do kubeconfig usando o ponto final privado.

    oci ce cluster create-kubeconfig \
        --cluster-id "${CLUSTER_ID}" \
        --file "${HOME}/.kube/config" \
        --region "${OCI_REGION}" \
        --token-version 2.0.0 \
        --kube-endpoint PRIVATE_ENDPOINT
  5. Obtenha o endereço IP do ponto final privado para o túnel SSH.

    PRIVATE_ENDPOINT=$(oci ce cluster get \
        --cluster-id "${CLUSTER_ID}" \
        --query "data.endpoints.\"private-endpoint\"" \
        --raw-output)
    PRIVATE_IP="${PRIVATE_ENDPOINT%:*}"
    echo "Private endpoint IP: ${PRIVATE_IP}"
  6. Criar uma sessão de encaminhamento de porta bastion. Você precisará de um arquivo de chave pública SSH.

    SESSION_ID=$(oci bastion session create-port-forwarding \
        --bastion-id "${BASTION_ID}" \
        --target-private-ip "${PRIVATE_IP}" \
        --target-port 6443 \
        --session-ttl 10800 \
        --display-name "kubectl-tunnel" \
        --ssh-public-key-file ~/.ssh/id_rsa.pub \
        --query "data.id" \
        --raw-output)

    Aguarde a sessão se tornar ACTIVE e, em seguida, obtenha o comando SSH.

    oci bastion session get \
        --session-id "${SESSION_ID}" \
        --query "data.{state:\"lifecycle-state\", ssh:\"ssh-metadata\".command}" 2>&1
  7. Abra um terminal separado e inicie o túnel SSH usando o comando da etapa anterior. O túnel encaminha a porta local 6443 para a API privada do Kubernetes.

    ssh -i ~/.ssh/id_rsa -o IdentitiesOnly=yes -N -L 6443:<PRIVATE_IP>:6443 \
        -p 22 -o ServerAliveInterval=30 \
        <SESSION_OCID>@host.bastion.<REGION>.oci.oraclecloud.com

    Observação: Substitua <PRIVATE_IP>, <SESSION_OCID> e <REGION> pelos valores da etapa anterior. Mantenha este terminal aberto durante a sessão. O flag -o IdentitiesOnly=yes impede erros de "muitas falhas de autenticação" quando seu agente SSH tem várias chaves carregadas.

  8. Atualize o kubeconfig para estabelecer conexão por meio do túnel local.

    CLUSTER_NAME_KUBE=$(kubectl config view --minify -o jsonpath='{.clusters[0].name}')
    kubectl config set-cluster "${CLUSTER_NAME_KUBE}" \
        --server=https://127.0.0.1:6443 \
        --insecure-skip-tls-verify=true

    Observação: O flag --insecure-skip-tls-verify é obrigatório porque o certificado do cluster foi emitido para o IP do ponto final privado, não para 127.0.0.1. Isso é seguro porque o tráfego é criptografado por meio do túnel SSH.

  9. Se você estiver usando um perfil da CLI do OCI não padrão (por exemplo, API_KEY_AUTH), atualize o kubeconfig para usá-lo. O kubeconfig gerado usa como padrão o perfil DEFAULT para geração de token.

    kubectl config set-credentials \
        $(kubectl config view --minify -o jsonpath='{.users[0].name}') \
        --exec-env=OCI_CLI_PROFILE=${OCI_PROFILE}

    Dica: As etapas de 6 a 9 são automatizadas pelo ./entry_point.sh tunnel, que também se conecta automaticamente se o túnel SSH cair durante operações de longa execução, como expansão de disco. Execute-o em um terminal separado e deixe-o em execução durante a sessão.

  10. Verifique o acesso ao cluster.

kubectl get nodes

Neste ponto, a saída não mostrará nós, já que o pool de nós de GPU ainda não foi adicionado.

No resources found

Tarefa 5: Adicionar Pool de Nós de GPU

Adicione um pool de nós com instâncias de computação de GPU ao cluster do OKE.

  1. Localize a imagem mais recente do nó do OKE compatível com GPU. O OKE requer imagens específicas com componentes de registro de nó e kubelet pré-instalados. Use a API node-pool-options para localizar a imagem correta para sua versão do Kubernetes.

    GPU_IMAGE_ID=$(oci ce node-pool-options get \
        --node-pool-option-id all \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --query "data.sources[?contains(\"source-name\", 'GPU') && contains(\"source-name\", 'OKE-${KUBERNETES_VERSION#v}') && contains(\"source-name\", '8.10')].\"image-id\" | [0]" \
        --raw-output)
    echo "GPU Image: ${GPU_IMAGE_ID}"

    Observação: Os filtros de consulta para imagens de GPU do Oracle Linux 8.10 correspondentes à sua versão do Kubernetes (por exemplo, OKE-1.31.10). Se você precisar de imagens baseadas no ARM, substitua 8.10 pelo filtro apropriado.

  2. Determine o domínio de disponibilidade que tem formas de GPU. Nem todos os domínios de disponibilidade têm capacidade de GPU.

    AD=$(oci iam availability-domain list \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --query "data[${GPU_AD_INDEX}].name" \
        --raw-output)
    echo "Availability Domain: ${AD}"

    Observação: A capacidade da GPU varia de acordo com a região e o domínio de disponibilidade. Se a criação do pool de nós falhar com um erro "Fora da capacidade do host", tente outra forma de domínio de disponibilidade (GPU_AD_INDEX) ou GPU ou solicite a capacidade por meio do seu processo normal do OCI.

  3. Crie o pool de nós de GPU com um volume de inicialização de 200 GB.

    oci ce node-pool create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --cluster-id "${CLUSTER_ID}" \
        --name "${GPU_NODE_POOL_NAME:-gpu-pool}" \
        --kubernetes-version "${KUBERNETES_VERSION}" \
        --node-shape "${GPU_SHAPE}" \
        --node-image-id "${GPU_IMAGE_ID}" \
        --node-boot-volume-size-in-gbs "${GPU_BOOT_VOLUME_GB:-200}" \
        --size "${GPU_NODE_COUNT}" \
        --placement-configs "[{\"availabilityDomain\": \"${AD}\", \"subnetId\": \"${WORKER_SUBNET_ID}\"}]" \
        --initial-node-labels '[{"key": "app", "value": "gpu"}, {"key": "nvidia.com/gpu", "value": "true"}]'

    Observação: Os labels de nó app=gpu e nvidia.com/gpu=true são usados posteriormente pelo gráfico vLLM Helm para programar pods de inferência em nós de GPU. O volume de inicialização de 200 GB fornece espaço para a imagem do contêiner vLLM (~10 GB) e pesos do modelo, mas o sistema de arquivos deve ser expandido antes do uso (consulte a Tarefa 8).

  4. Aguarde até que os nós de GPU se tornem Prontos. Isso normalmente leva de 5 a 10 minutos, enquanto o nó provisiona, inicializa, instala drivers de GPU e se registra no cluster.

    Observação: as instâncias de GPU estão sujeitas a restrições de capacidade. Se o pool de nós permanecer no estado CREATING, verifique o status do nó na Console do OCI ou com o oci ce node-pool get. Um erro de "Capacidade insuficiente do host" significa que não há instâncias de GPU disponíveis nesse domínio de disponibilidade. Para resolver isso, tente outro domínio de disponibilidade (GPU_AD_INDEX=0 ou GPU_AD_INDEX=2), tente outra forma de GPU ou solicite uma reserva de capacidade por meio da Console do OCI ou de um ticket de suporte.

    kubectl get nodes -w

    Saída esperada quando o nó estiver pronto:

    NAME          STATUS   ROLES   AGE   VERSION
    10.0.10.x     Ready    node    5m    v1.31.10
  5. Verifique se a GPU foi detectada no nó.

    kubectl get nodes -o=custom-columns=NAME:.metadata.name,GPUs:.status.capacity.'nvidia\.com/gpu'

    Saída esperada:

    NAME          GPUs
    10.0.10.x     1
  6. Aplique o patch CoreDNS para programar em nós de GPU. Os nós de GPU do OKE têm uma taint nvidia.com/gpu=present:NoSchedule. Em clusters que só têm nós de GPU, os pods do sistema como CoreDNS não podem ser programados sem uma tolerância para essa mancha. Sem o DNS, os pods não podem resolver nomes de host externos para fazer download de modelos.

    kubectl patch deployment coredns -n kube-system --type='json' \
      -p='[{"op": "add", "path": "/spec/template/spec/tolerations/-", "value": {"key": "nvidia.com/gpu", "operator": "Exists", "effect": "NoSchedule"}}]'
    
    kubectl patch deployment kube-dns-autoscaler -n kube-system --type='json' \
      -p='[{"op": "add", "path": "/spec/template/spec/tolerations/-", "value": {"key": "nvidia.com/gpu", "operator": "Exists", "effect": "NoSchedule"}}]'

    Verifique se o CoreDNS está em execução.

    kubectl get pods -n kube-system | grep coredns

    Observação: Se o cluster tiver um pool de nós de CPU dedicado para cargas de trabalho do sistema, essa etapa não será necessária. Este patch só é necessário quando os nós de GPU são os únicos nós do cluster.

    oci ce node-pool list --compartment-id "${OCI_COMPARTMENT_ID}" --cluster-id "${CLUSTER_ID}" \
        --query 'data[].{name:name,state:"lifecycle-state",shape:"node-shape",size:"node-config-details".size}' --output table

    Saída da CLI do OCI mostrando cpu-pool e gpu-pool no estado ACTIVE

Tarefa 6: Instalar Plug-in do Dispositivo NVIDIA

Instale o plug-in de dispositivo NVIDIA para que o Kubernetes possa detectar e programar cargas de trabalho na GPU.

  1. Aplique o plug-in de dispositivo NVIDIA DaemonSet.

    kubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.14.1/nvidia-device-plugin.yml
  2. Aguarde que os pods de plug-in estejam prontos.

    kubectl wait --for=condition=Ready pods -l name=nvidia-device-plugin-ds -n kube-system --timeout=300s

    Observação: Algumas imagens de nó de GPU do OKE incluem um plug-in de dispositivo NVIDIA pré-instalado (nvidia-gpu-device-plugin). Se a imagem já a incluir, a aplicação do DaemonSet upstream criará uma segunda instância que não causará conflitos. O script automatizado (entry_point.sh deploy-vllm) sempre o instala para garantir que a detecção de GPU funcione, independentemente da versão da imagem do nó.

  3. Confirme se a GPU é alocável pelo Kubernetes.

    kubectl get nodes -o=custom-columns=NAME:.metadata.name,GPUs:.status.allocatable.'nvidia\.com/gpu'

    Saída esperada:

    NAME          GPUs
    10.0.10.x     1
  4. Aplique o patch CoreDNS para tolerar manchas de nó de GPU. Em clusters em que os nós de GPU são os únicos nós de trabalho, os pods CoreDNS não podem ser programados porque os nós de GPU do OKE têm uma mancha nvidia.com/gpu=present:NoSchedule. Sem o DNS, os pods não podem resolver registros de imagem ou URLs de download de modelo.

    kubectl patch deployment coredns -n kube-system --type='json' -p='[
      {"op": "add", "path": "/spec/template/spec/tolerations/-", "value": {"key": "nvidia.com/gpu", "operator": "Exists", "effect": "NoSchedule"}}
    ]'
    kubectl rollout status deployment/coredns -n kube-system --timeout=60s

    Observação: Esta etapa só é necessária quando os nós de GPU são os únicos nós de trabalho no cluster. Se você tiver um pool de nós de CPU dedicado para cargas de trabalho do sistema, o CoreDNS o programará por padrão e esse patch será desnecessário.

Tarefa 7: Configurar Armazenamento

Aplique o OCI Block Volume StorageClass para fornecer armazenamento persistente para pesos de modelo.

  1. Aplique a definição StorageClass.

    kubectl apply -f oci-block-storage-sc.yaml

    O arquivo define duas camadas de desempenho:

    apiVersion: storage.k8s.io/v1
    kind: StorageClass
    metadata:
      name: oci-block-storage-enc
    provisioner: blockvolume.csi.oraclecloud.com
    parameters:
      vpusPerGB: "10"
    reclaimPolicy: Delete
    volumeBindingMode: WaitForFirstConsumer
    allowVolumeExpansion: true
    StorageClass Desempenho Caso de Uso
    oci-block-storage-enc Balanceado (vpusPerGB: 10) Padrão, econômico para a maioria dos modelos
    oci-block-storage-hp Alto desempenho (vpusPerGB: 20) Carregamento de modelos mais rápido para modelos maiores
  2. Verifique se o StorageClasses está disponível.

    kubectl get storageclass

Observação: Para implantações com vários nós que exigem armazenamento compartilhado em vários pods, use o OCI File Storage Service (NFS) com o modo de acesso ReadWriteMany em vez de Block Volumes.

Tarefa 8: Expandir Sistema de Arquivos de Nó de GPU

Os volumes de inicialização do OCI têm uma partição fixa de ~47 GB, independentemente do tamanho do volume de inicialização especificado. A imagem do contêiner vLLM sozinha é de aproximadamente 10 GB, e os pesos do modelo exigem espaço adicional. Você deve expandir o sistema de arquivos antes de implantar o vLLM para evitar remoções de DiskPressure.

Observação: Este é um requisito específico do OCI. O volume de inicialização é provisionado em 200 GB, mas o sistema operacional só faz a partição de ~47 GB por padrão. O espaço restante deve ser reivindicado manualmente.

  1. Verifique o tamanho atual do sistema de arquivos no nó GPU.

    GPU_NODE=$(kubectl get nodes -l app=gpu -o jsonpath='{.items[0].metadata.name}')
    kubectl run check-disk --rm -i --restart=Never \
      --image=busybox:latest \
      --overrides="{\"spec\":{\"nodeName\":\"${GPU_NODE}\",\"tolerations\":[{\"operator\":\"Exists\"}],\"containers\":[{\"name\":\"check\",\"image\":\"busybox:latest\",\"command\":[\"sh\",\"-c\",\"chroot /host df -h / | tail -1\"],\"securityContext\":{\"privileged\":true},\"volumeMounts\":[{\"name\":\"host\",\"mountPath\":\"/host\"}]}],\"volumes\":[{\"name\":\"host\",\"hostPath\":{\"path\":\"/\"}}]}}"

    A saída mostrará aproximadamente 47 GB no total, confirmando que a expansão é necessária.

  2. Crie um pod privilegiado no nó da GPU para executar os comandos de expansão.

    kubectl run expand-disk --restart=Never \
      --image=busybox:latest \
      --overrides="{\"spec\":{\"nodeName\":\"${GPU_NODE}\",\"tolerations\":[{\"operator\":\"Exists\"}],\"containers\":[{\"name\":\"expand\",\"image\":\"busybox:latest\",\"command\":[\"sleep\",\"600\"],\"securityContext\":{\"privileged\":true},\"volumeMounts\":[{\"name\":\"host\",\"mountPath\":\"/host\"}]}],\"volumes\":[{\"name\":\"host\",\"hostPath\":{\"path\":\"/\"}}]}}"

    Aguarde a inicialização do pod.

    kubectl wait --for=condition=Ready pod/expand-disk --timeout=60s
  3. Execute todas as quatro etapas de expansão em um único comando kubectl exec. Executá-los juntos evita o risco de kubectl exec retornar o código de saída 137 (SIGKILL) entre as etapas, o que pode acontecer durante a E/S de disco pesado no host.

    kubectl exec expand-disk -- chroot /host bash -c '
      set -x
      growpart /dev/sda 3 || echo "growpart: partition may already be expanded"
      sleep 3
      pvresize /dev/sda3
      lvextend -l +100%FREE /dev/ocivolume/root || echo "lvextend: may already be extended"
      xfs_growfs /
      echo "EXPANSION_COMPLETE"
      df -h /
    '
    Etapa Comando Objetivo
    1 growpart /dev/sda 3 Expanda a partição 3 para usar o disco completo
    2 pvresize /dev/sda3 Redimensionar volume físico de LVM
    3 lvextend -l +100%FREE /dev/ocivolume/root Estender volume lógico
    4 xfs_growfs / Aumente o sistema de arquivos XFS para preencher o volume

    Observação: todas as quatro operações são idempotentes. Se a execução retornar o código de saída 137, você poderá executar novamente todo o bloco com segurança. Procure EXPANSION_COMPLETE na saída para confirmar o sucesso.

  4. Reinicie o kubelet para que o nó reporte o armazenamento alocável atualizado e, em seguida, verifique e limpe.

    kubectl exec expand-disk -- nsenter -t 1 -m -p -- systemctl restart kubelet 2>/dev/null \
        || echo "Warning: kubelet restart returned non-zero (non-critical)"
    kubectl exec expand-disk -- chroot /host df -h /
    kubectl delete pod expand-disk --force

    Observação: O comando nsenter informa o namespace PID do host para acessar systemd. Um chroot /host systemctl restart kubelet simples falha porque não pode se conectar ao barramento systemd de dentro de um chroot.

    A saída esperada deve mostrar aproximadamente 189 GB no total.

Tarefa 9: Implantar a Pilha de Produção vLLM

Instale a pilha de inferência de vLLM usando o Helm.

  1. Adicione o repositório vLLM Helm.

    helm repo add vllm https://vllm-project.github.io/production-stack
    helm repo update
  2. Verifique o arquivo de valores do Helm. O production_stack_specification.yaml configura o modelo, os recursos e o armazenamento para o OCI.

    servingEngineSpec:
      runtimeClassName: ""
      modelSpec:
      - name: "gpt-oss"
        repository: "vllm/vllm-openai"
        tag: "latest"
        modelURL: "openai/gpt-oss-20b"
    
        replicaCount: 1
        requestCPU: 4
        requestMemory: "24Gi"
        requestGPU: 1
    
        # No HF token needed - Apache 2.0 licensed OpenAI open source model
    
        pvcStorage: "100Gi"
        pvcAccessMode:
          - ReadWriteOnce
        storageClass: "oci-block-storage-enc"
    
        nodeSelector:
          app: gpu
        tolerations:
          - key: "nvidia.com/gpu"
            operator: "Exists"
            effect: "NoSchedule"
    
        extraArgs:
          - "--max-model-len=8192"
          - "--gpu-memory-utilization=0.90"

    Observação: O modelo openai/gpt-oss-20b é um modelo de Mistura de Especialistas (MoE) com parâmetros totais 20B e parâmetros ativos 3.6B por avanço. Ele é lançado sob a licença Apache 2.0, portanto, nenhum token Hugging Face é necessário. A imagem do contêiner vllm/vllm-openai fornece um servidor de API compatível com OpenAI, permitindo que os clientes usem chamadas padrão do SDK OpenAI em seu ponto final auto-hospedado.

  3. Implante a pilha. Não use --wait aqui porque o pod do roteador será CrashLoop até ser corrigido na próxima etapa.

    helm upgrade -i \
        vllm vllm/vllm-stack \
        -f production_stack_specification.yaml

    Aguarde o pod do mecanismo vLLM ser iniciado (o roteador será corrigido em seguida).

    kubectl wait --for=condition=Ready pods -l model=gpt-oss --timeout=600s

    Observação: O pod do mecanismo leva vários minutos para se tornar Pronto porque faz download dos pesos do modelo no primeiro início. Se o pod permanecer em ContainerCreating, a imagem do contêiner (~10 GB) ainda estará sendo extraída. Use kubectl describe pod <pod-name> para verificar o andamento.

  4. Corrigir a implantação do roteador. O roteador precisa de uma tolerância de GPU (para que possa ser agendado quando os nós de GPU forem os únicos com capacidade) e limites de memória aumentados (o padrão 500 Mi pode causar OOMKill).

    kubectl patch deployment vllm-deployment-router --type='json' -p='[
      {"op": "add", "path": "/spec/template/spec/tolerations", "value": [{"key": "nvidia.com/gpu", "operator": "Exists", "effect": "NoSchedule"}]},
      {"op": "replace", "path": "/spec/template/spec/containers/0/resources/requests/memory", "value": "512Mi"},
      {"op": "replace", "path": "/spec/template/spec/containers/0/resources/limits/memory", "value": "1Gi"}
    ]'

    Observação: A tolerância de GPU é necessária porque os nós de GPU do OKE têm uma taint nvidia.com/gpu=present:NoSchedule que impede a programação de cargas de trabalho não GPU. Como o roteador não usa uma GPU, mas precisa ser executado em algum lugar, essa tolerância permite que ele seja programado em nós de GPU. Em clusters com pools de nós de CPU dedicados, essa tolerância não é necessária.

  5. Confirme se a release do Helm foi implantada.

    helm list

    Saída de terminal da lista de helm mostrando a pilha de vllm implantada

  6. Verifique se os pods estão em execução.

    kubectl get pods

    Saída esperada:

    NAME                                            READY   STATUS    RESTARTS   AGE
    vllm-deployment-router-xxxxxxxxxx-xxxxx          1/1     Running   0          5m
    vllm-gpt-oss-deployment-vllm-xxxxxxxxxx-xxxxx    1/1     Running   0          5m
    kubectl get pods

    kubectl obtém pods mostrando pods de roteador e motor ambos Executando 1/1

  7. Verifique o andamento do carregamento do modelo nos logs de pod.

    kubectl logs -f deployment/vllm-gpt-oss-deployment-vllm

    Aguarde até que você veja uma mensagem indicando que o modelo foi carregado e que o servidor está pronto para aceitar solicitações.

Tarefa 10: Testar o Ponto Final de Inferência

Valide se a implantação está atendendo a solicitações de inferência. A Pilha de Produção vLLM expõe uma API compatível com OpenAI por meio do serviço do roteador, para que qualquer cliente SDK OpenAI ou comando curl possa interagir com ele.

O diagrama a seguir mostra o ciclo de vida da solicitação de inferência: desde a solicitação do cliente até a lógica de seleção do mecanismo do roteador, até as fases de pré-preenchimento e decodificação do mecanismo vLLM e de volta como uma resposta transmitida.

Diagrama de sequência mostrando o ciclo de vida da solicitação de inferência do cliente por meio do roteador para o mecanismo vLLM e GPU

  1. Liste os modelos disponíveis para confirmar se a implantação está íntegra.

    kubectl get svc vllm-router-service

    O serviço de roteador fornece o gateway de API para todos os modelos implantados. Como o cluster usa um ponto final privado, você acessa o serviço por meio do kubectl port-forward.

  2. Inicie uma transferência de porta da sua máquina local para o serviço do roteador. Abra um novo terminal (mantenha o túnel SSH em execução no outro) e execute:

    kubectl port-forward svc/vllm-router-service 8080:80

    Isso mapeia localhost:8080 na sua máquina para a porta 80 no serviço do roteador dentro do cluster.

    Observação de Segurança: O kubectl port-forward é vinculado localmente e não expõe o serviço publicamente. Esta é a maneira mais segura de testar ao usar um cluster privado em um túnel bastion.

    Observação: O comando port-forward é executado em primeiro plano. Mantenha este terminal aberto durante o teste. Pressione Ctrl+C para interrompê-lo quando terminar.

  3. Em outro terminal, verifique se o modelo está disponível consultando o ponto final dos modelos.

    curl -s http://localhost:8080/v1/models | python3 -m json.tool

    Saída esperada:

    {
        "object": "list",
        "data": [
            {
                "id": "openai/gpt-oss-20b",
                "object": "model",
                "created": 1234567890,
                "owned_by": "vllm"
            }
        ]
    }
  4. Enviar uma solicitação de conclusão de texto.

    curl -s http://localhost:8080/v1/completions \
      -H "Content-Type: application/json" \
      -d '{
        "model": "openai/gpt-oss-20b",
        "prompt": "Oracle Cloud Infrastructure is",
        "max_tokens": 50
      }' | python3 -m json.tool

    Saída esperada (abreviada):

    {
        "id": "cmpl-xxxxxxxxxxxx",
        "object": "text_completion",
        "model": "openai/gpt-oss-20b",
        "choices": [
            {
                "index": 0,
                "text": " a cloud computing platform that provides ...",
                "finish_reason": "length"
            }
        ],
        "usage": {
            "prompt_tokens": 5,
            "completion_tokens": 50,
            "total_tokens": 55
        }
    }
  5. Envie uma solicitação de conclusão de chat. Esse é o mesmo formato usado pelo OpenAI Python SDK e é a maneira mais comum de interagir com LLMs de forma programática.

    curl -s http://localhost:8080/v1/chat/completions \
      -H "Content-Type: application/json" \
      -d '{
        "model": "openai/gpt-oss-20b",
        "messages": [{"role": "user", "content": "What is Oracle Cloud Infrastructure in one sentence?"}],
        "max_tokens": 100
      }' | python3 -m json.tool

    Saída esperada (abreviada):

    {
        "id": "chatcmpl-xxxxxxxxxxxx",
        "object": "chat.completion",
        "model": "openai/gpt-oss-20b",
        "choices": [
            {
                "index": 0,
                "message": {
                    "role": "assistant",
                    "content": "Oracle Cloud Infrastructure (OCI) is Oracle's enterprise-grade cloud platform that provides a full range of services for building, deploying, and managing applications and workloads..."
                },
                "finish_reason": "length"
            }
        ],
        "usage": {
            "prompt_tokens": 71,
            "completion_tokens": 100,
            "total_tokens": 171
        }
    }

    Ambas as respostas incluem o texto gerado do modelo no array choices, as estatísticas de uso do token e um finish_reason de stop (o modelo foi concluído naturalmente) ou length (a saída foi truncada em max_tokens).

    Observação: O nome do modelo na solicitação de API é o caminho completo do modelo (openai/gpt-oss-20b), que corresponde ao campo modelURL nos valores do Helm. Qualquer cliente compatível com OpenAI pode usar esse ponto final definindo base_url como http://localhost:8080/v1.

    Saída de terminal mostrando a solicitação de conclusão de chat curl e a resposta JSON de openai/gpt-oss-20b

  6. Meça a latência de ponta a ponta para uma solicitação curta.

    time curl -s http://localhost:8080/v1/chat/completions \
        -H "Content-Type: application/json" \
        -d '{"model":"openai/gpt-oss-20b","messages":[{"role":"user","content":"What is Kubernetes?"}],"max_tokens":50}' > /dev/null

    Em uma única GPU A10, espere de 1 a 3 segundos de ponta a ponta para conclusões curtas. O tempo para o primeiro token (TTFT) geralmente é de 50 a 200 ms, dependendo do tamanho do prompt. Para um throughput mais alto, aumente replicaCount nos valores do Helm para adicionar mais réplicas de mecanismo atrás do roteador.

Tarefa 11: Expor por meio do OCI Load Balancer (Opcional)

Torne o ponto final de inferência acessível externamente por meio de um Balanceador de Carga do OCI.

Observação de Segurança: Isso expõe a API de inferência à internet pública por padrão. Não ative isso na produção sem TLS, autenticação (chave de API/JWT/mTLS) e lista de permissões de IP ou controles de WAF. Se você precisar expô-lo, use um controlador de entrada ou um gateway de API que impõe limites de taxa e autenticação ou use um balanceador de carga interno.

  1. Aplique patch no serviço do roteador para usar um tipo LoadBalancer.

    kubectl patch svc vllm-router-service \
      -p '{"spec": {"type": "LoadBalancer"}}'

    Observação: se você quiser um balanceador de carga interno, adicione a anotação do OCI ao serviço (exemplo abaixo). Isso mantém o ponto final privado dentro da VCN.

    kubectl annotate svc vllm-router-service \
      "service.beta.kubernetes.io/oci-load-balancer-internal"="true"
  2. Aguarde a designação do IP externo.

    kubectl get svc vllm-router-service -w

    Saída esperada depois que o balanceador de carga for provisionado:

    NAME                   TYPE           CLUSTER-IP     EXTERNAL-IP      PORT(S)
    vllm-router-service    LoadBalancer   10.96.x.x      129.xxx.xxx.xx   80:xxxxx/TCP
    kubectl get svc

    kubectl obtém saída de svc mostrando serviços de roteador e mecanismo

  3. Teste o ponto final externo.

    curl http://<EXTERNAL-IP>/v1/completions \
      -H "Content-Type: application/json" \
      -d '{
        "model": "openai/gpt-oss-20b",
        "prompt": "Hello from OCI!",
        "max_tokens": 50
      }'

Tarefa 12: Configurar Paralelismo de Tensor de Várias GPUs (Avançado)

Implante modelos maiores em várias GPUs em formas bare metal.

O paralelismo de tensores divide um modelo em várias GPUs em um único nó. Isso é necessário quando os requisitos de memória de um modelo excedem uma única GPU. Por exemplo, o Meta Llama 3.1 70B requer aproximadamente 140 GB de memória GPU, o que excede a capacidade de qualquer GPU única, mas se encaixa em 8x A100 80 GB ou 8x H100 GPUs.

  1. Crie um segredo do Kubernetes com seu token Hugging Face. Modelos fechados, como o Llama 3.1 70B, exigem autenticação.

    kubectl create secret generic hf-token-secret \
      --from-literal=token=YOUR_HUGGINGFACE_TOKEN
  2. Atualize o production_stack_specification.yaml com uma configuração de vários GPUs.

    servingEngineSpec:
      modelSpec:
      - name: "llama70b"
        repository: "vllm/vllm-openai"
        tag: "latest"
        modelURL: "meta-llama/Llama-3.1-70B-Instruct"
    
        replicaCount: 1
        tensorParallelSize: 8
    
        requestCPU: 32
        requestMemory: "256Gi"
        requestGPU: 8
    
        hf_token:
          secretName: "hf-token-secret"
          secretKey: "token"
    
        pvcStorage: "500Gi"
        pvcAccessMode:
          - ReadWriteOnce
        storageClass: "oci-block-storage-enc"
    
        nodeSelector:
          node.kubernetes.io/instance-type: "BM.GPU.H100.8"
        tolerations:
          - key: "nvidia.com/gpu"
            operator: "Exists"
            effect: "NoSchedule"
    
        extraArgs:
          - "--max-model-len=8192"
          - "--gpu-memory-utilization=0.95"
          - "--tensor-parallel-size=8"
  3. Implante com os valores atualizados. Assim como na Tarefa 9, não use --wait. O roteador será CrashLoop até ser corrigido.

    helm upgrade -i \
        vllm vllm/vllm-stack \
        -f production_stack_specification.yaml
    kubectl wait --for=condition=Ready pods -l model=llama70b --timeout=900s

    Em seguida, corrija o roteador (o mesmo que Tarefa 9, etapa 4) e verifique:

    kubectl patch deployment vllm-deployment-router --type='json' -p='[
      {"op": "add", "path": "/spec/template/spec/tolerations", "value": [{"key": "nvidia.com/gpu", "operator": "Exists", "effect": "NoSchedule"}]},
      {"op": "replace", "path": "/spec/template/spec/containers/0/resources/requests/memory", "value": "512Mi"},
      {"op": "replace", "path": "/spec/template/spec/containers/0/resources/limits/memory", "value": "1Gi"}
    ]'
  4. Verifique se o pod está em execução e se todas as GPUs estão em uso.

    kubectl get pods
    kubectl logs -f deployment/vllm-llama70b-deployment-vllm

Observação: Certifique-se de que o cluster do OKE tenha um pool de nós com a forma de GPU bare metal apropriada (por exemplo, BM.GPU.H100.8) antes de implantar uma configuração de vários GPUs.

Tarefa 13: Usar o OCI Object Storage para Modelos (Avançado)

Carregue pesos de modelo do OCI Object Storage em vez de fazer download do Hugging Face. Isso é útil para modelos privados, downloads mais rápidos na OCI ou ambientes sem acesso externo à internet.

  1. Faça upload dos pesos do modelo para um bucket do OCI Object Storage. Navegue até Armazenamento > Object Storage na Console do OCI e crie um bucket se ainda não tiver um.

  2. Crie um URL de PAR (Solicitação Pré-Autenticada) para seu bucket. Na Console do OCI, selecione seu bucket, clique em Solicitações Pré-Autenticadas e crie uma nova solicitação com acesso de leitura.

  3. Atualize o production_stack_specification.yaml para usar o URL da PAR.

    servingEngineSpec:
      modelSpec:
      - name: "custom-model"
        repository: "iad.ocir.io/YOUR_TENANCY/vllm-custom:latest"
        modelURL: "/models/custom-model"
    
        env:
          - name: BUCKET_PAR_URL
            value: "https://objectstorage.us-ashburn-1.oraclecloud.com/p/xxx/n/namespace/b/bucket/o"
          - name: MODEL_NAME
            value: "custom-model"
  4. Implante com os valores atualizados (Sem --wait. Consulte a Tarefa 9 para saber por quê).

    helm upgrade -i \
        vllm vllm/vllm-stack \
        -f production_stack_specification.yaml
    kubectl wait --for=condition=Ready pods -l model=custom-model --timeout=600s
  5. Verifique os carregamentos de modelo do serviço Object Storage verificando os logs de pod.

    kubectl logs -f deployment/vllm-custom-model-deployment-vllm

Tarefa 14: Limpar Recursos

Remova todos os recursos implantados para evitar cobranças contínuas.

  1. Remova os recursos do Kubernetes usando o script de limpeza.

    cd production-stack/deployment_on_cloud/oci
    ./clean_up.sh

    Isso desinstala a release do Helm, exclui todos os recursos PersistentVolumeClaims, PersistentVolumes e vLLM personalizados.

  2. Excluir o cluster do OKE e todos os recursos de rede do OCI.

    ./entry_point.sh cleanup

    Isso exclui os seguintes recursos na ordem:

    • Pool de nós de GPU
    • Cluster do OKE
    • Bastion host (se criado)
    • Sub-redes (API, worker, load balancer, bastion)
    • Listas de segurança
    • Gateway de Serviço, Gateway NAT e Gateway de Internet
    • Tabelas de roteamento
    • VCN
  3. Verifique se todos os recursos foram removidos na Console do OCI em Developer Services > Clusters do Kubernetes e Networking > Redes Virtuais na Nuvem.

    ./entry_point.sh cleanup

    Saída terminal da limpeza entry_point.sh mostrando a desativação completa do recurso

Observação: certifique-se de que todos os recursos sejam excluídos para evitar cobranças contínuas. As instâncias de GPU e os volumes em blocos incorrem em custos mesmo quando ociosos.

O Que Vem a Seguir

Este tutorial implantou uma pilha de inferência funcional. Para cargas de trabalho de produção, considere os seguintes aprimoramentos:

Confirmações

Mais Recursos de Aprendizado

Explore outros laboratórios em docs.oracle.com/learn ou acesse mais conteúdo de aprendizado gratuito no canal do Oracle Learning YouTube. Além disso, acesse education.oracle.com/learning-explorer para se tornar um Oracle Learning Explorer.

Para obter a documentação do produto, visite o Oracle Help Center.