Note:

Despliegue de la pila de producción de vLLM OpenAI en Oracle Kubernetes Engine (OKE)

Introducción

Las organizaciones que adoptan grandes modelos de lenguaje (LLM) para cargas de trabajo de producción se enfrentan a una decisión crítica de infraestructura: confiar en API de inferencia de terceros o desplegar una pila de inferencia autoalojada. Las implementaciones autoalojadas ofrecen ventajas significativas: privacidad de datos completa y control de cumplimiento, latencia de inferencia de menos de 100 milisegundos al eliminar viajes en red, costos predecibles a escala y la libertad de ajustar y servir cualquier modelo de código abierto sin dependencia del proveedor.

Sin embargo, la creación de una pila de inferencia de LLM de nivel de producción desde cero es compleja. Requiere orquestación de contenedores con GPU, enrutamiento de solicitudes inteligente en múltiples réplicas de modelos, almacenamiento persistente para grandes pesos de modelos y monitoreo continuo, todo integrado y en ejecución confiable.

Oracle Cloud Infrastructure ofrece varias rutas para la inferencia de IA. El servicio OCI Generative AI proporciona una experiencia totalmente gestionada con clusters de IA dedicados aislados en su arrendamiento, ideal para equipos que desean comenzar rápidamente con modelos soportados. En este tutorial se adopta el enfoque alternativo: desplegar su propia pila de inferencia en OKE. Esta ruta está diseñada para equipos que necesitan un control preciso sobre los controladores de GPU, las versiones de CUDA, las configuraciones de modelos y los parámetros de servicio, o equipos que entrenan y ajustan modelos personalizados y desean servirlos directamente. OCI proporciona instancias de GPU con hardware dedicado con GPU NVIDIA A10, A100 y H100, conectadas por redes de clústeres RDMA de latencia ultrabaja, lo que le brinda el mismo nivel de control de hardware que tendría en las instalaciones y se beneficia de la flexibilidad de la nube.

La pila de producción de vLLM resuelve la complejidad de la inferencia autoalojada al proporcionar una plataforma de código abierto nativa de Kubernetes basada en vLLM, el motor de inferencia de alto rendimiento utilizado en producción por organizaciones como Meta, Mistral AI e IBM. Ofrece hasta 24x mayor rendimiento en comparación con los marcos de servicio estándar a través de una gestión eficiente de la memoria GPU y la optimización de la caché de KV. En combinación con las unidades de GPU de OKE y OCI, obtienes una plataforma de inferencia lista para producción con redes, almacenamiento y seguridad de nivel empresarial. Los scripts de despliegue de OCI utilizados en este tutorial se añaden y mantienen en el repositorio oficial de pila de producción de vLLM.

Este tutorial le guiará a través del despliegue de la pila de producción de vLLM en OKE, desde el aprovisionamiento de infraestructura hasta la ejecución de su primera solicitud de inferencia.

Nota: En este tutorial se aprovisionan recursos paso a paso mediante la CLI de OCI para ayudarle a comprender el flujo completo de recursos en la nube de OCI necesarios para un despliegue de inferencia de GPU. Para los entornos de producción, se recomienda codificar esta infraestructura mediante Terraform o OCI Resource Manager (Shepherd) para despliegues repetibles y controlados por versiones.

Diagrama de arquitectura que muestra la VCN con subredes, cluster de OKE, pool de nodos de GPU, pods vLLM y equilibrador de carga

En este tutorial se utilizan los siguientes servicios de OCI:

Servicio Finalidad
Motor Kubernetes de Oracle (OKE) Cluster de Kubernetes gestionado para orquestación de contenedores y programación de cargas de trabajo de GPU
OCI Compute (unidades de GPU) Instancias de GPU NVIDIA A10 (24 GB) y A100 (80 GB) para inferencia de modelos
Volúmenes en bloque de OCI Almacenamiento persistente para pesos de modelo con niveles de rendimiento configurables
Red virtual en la nube (VCN) de OCI Infraestructura de red, incluidas subredes, gateways y listas de seguridad
Equilibrador de carga de OCI Acceso externo a puntos finales de inferencia
OCI Bastion Túneles SSH gestionados para el acceso al cluster privado
OCI Object Storage Origen de modelo alternativo mediante URL de solicitud autenticada previamente (PAR)

Objetivos

En este tutorial, realizará lo siguiente:

Requisitos

Nota: Las salidas y capturas de pantalla de ejemplo de este tutorial utilizan us-chicago-1. Puede desplegar en cualquier región soportada definiendo OCI_REGION. La capacidad de la GPU varía según la región y el dominio de disponibilidad, por lo que debe confirmar que la unidad de GPU de destino está disponible antes del despliegue. Compruebe la disponibilidad de la unidad de GPU por región y prepárese para probar un dominio de disponibilidad diferente (GPU_AD_INDEX) si detecta errores de capacidad.

Nota: En este tutorial se aprovisionan recursos de GPU pagados (por ejemplo, VM.GPU.A10.1). No es una carga de trabajo Siempre gratis de OCI. Ejecute siempre los pasos de limpieza cuando haya terminado para evitar cargos continuos.

Nota: Este tutorial despliega openai/gpt-oss-20b, un modelo con licencia de Apache 2.0 de OpenAI. No se requiere ningún token Hugging Face. Si desea desplegar modelos cerrados como Meta Llama 3.1, necesitará una cuenta de Hugging Face con un token de API.

Tarea 1: Configuración de variables de entorno

Defina la configuración de OCI necesaria antes de desplegar la infraestructura.

  1. Busque el OCID del compartimento en la consola de OCI. Vaya a Identity & Security > Compartments y, a continuación, haga clic en el compartimento de destino y copie el OCID.

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

    Salida de la CLI de OCI que muestra el OCID del compartimento

  2. Exporte la variable de entorno necesaria.

    export OCI_COMPARTMENT_ID="ocid1.compartment.oc1..xxxxx"
  3. Opcionalmente, sustituya la configuración por defecto definiendo cualquiera de las siguientes variables de entorno.

    Variable Por defecto Descripción
    OCI_REGION us-ashburn-1 Región de OCI para el despliegue
    OCI_PROFILE DEFAULT Perfil de configuración de CLI de OCI
    CLUSTER_NAME production-stack Nombre del cluster de OKE
    GPU_SHAPE VM.GPU.A10.1 Unidad de computación de GPU para el pool de nodos
    GPU_NODE_COUNT 1 Número de nodos de GPU en el pool
    GPU_BOOT_VOLUME_GB 200 Tamaño del volumen de inicio en GB para los nodos de GPU
    CPU_BOOT_VOLUME_GB 100 Tamaño del volumen de inicio en GB para los nodos de CPU
    GPU_AD_INDEX 1 Índice de dominio de disponibilidad (basado en 0) para colocación de GPU
    PRIVATE_CLUSTER true Definido en false para un punto final de API de Kubernetes público
    KUBERNETES_VERSION v1.31.10 Versión de Kubernetes para el cluster de OKE

    Por ejemplo, para desplegar con dos nodos 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 las unidades de GPU disponibles y seleccione una según los requisitos de tamaño del modelo.

    Forma GPU Tipo de GPU Memoria de GPU Recomendada 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 con modelos pequeños
    BM.GPU4.8 8 NVIDIA A100 40 GB 320 GB modelos 70B, rentables
    BM.GPU.A100-v2.8 8 NVIDIA A100 80 GB 640 GB Modelos de parámetros 70B+
    BM.GPU.H100.8 8 NVIDIA H100 640 GB Modelos más grandes, soporte RDMA

    Nota: Las unidades con hardware dedicado (BM.*) proporcionan hardware dedicado sin sobrecarga de virtualización y soportan el paralelismo de tensor de varias GPU. Las unidades de máquina virtual (VM.*) son más rentables para modelos más pequeños.

    Nota: En este tutorial se utiliza VM.GPU.A10.1 (NVIDIA única A10 con memoria de GPU de 24 GB) para desplegar openai/gpt-oss-20b, un modelo de mezcla de expertos (MoE) con parámetros activos 3.6B que normalmente se ajusta a una única GPU A10. En las secciones avanzadas se muestran configuraciones de varias GPU mediante BM.GPU.H100.8 para modelos más grandes, como Llama 3.1 70B.

Tarea 2: Despliegue mediante el script automatizado (inicio rápido)

La pila de producción de vLLM incluye un script de despliegue automatizado que aprovisiona todos los recursos de OCI y despliega la pila de inferencia con un único comando. Utilice este enfoque para un despliegue rápido. Las tareas 3 a 10 cubren cada paso individualmente para los usuarios que desean personalizar el proceso.

  1. Clone el repositorio de la pila de producción de vLLM.

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

    export OCI_COMPARTMENT_ID="ocid1.compartment.oc1..xxxxx"
  3. Ejecute el script de despliegue.

    ./entry_point.sh setup

    Salida de terminal de la configuración entry_point.sh que muestra la creación de VCN, cluster, bastión y pool de nodos

    Para clusters públicos (PRIVATE_CLUSTER=false), la configuración crea toda la infraestructura y despliega la pila de vLLM en un único comando. Transfiera el archivo de valores de Helm como un segundo argumento:

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

    Para los clusters privados (valor por defecto), la configuración crea la infraestructura, pero no puede acceder directamente a la API de Kubernetes. Abra un terminal independiente e inicie el túnel y, a continuación, despliegue:

    # 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 que el despliegue se está ejecutando.

    kubectl get pods

    Salida 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

Nota: Si ambos pods muestran el estado Running, el despliegue está listo. Vaya a Tarea 10: probar el punto final de inferencia.

Nota: Las instancias de GPU están sujetas a restricciones de capacidad de OCI. Si el script permanece en el bucle "Waiting for GPU node" (Esperando nodo de GPU) durante más de 15 minutos, es posible que la unidad de GPU no esté disponible en el dominio de disponibilidad seleccionado. Compruebe el estado del pool de nodos con oci ce node-pool get y busque los errores "Fuera de la capacidad del host". Para resolverlo, limpie con ./entry_point.sh cleanup y vuelva a desplegar con un dominio de disponibilidad diferente (por ejemplo, GPU_AD_INDEX=0 o GPU_AD_INDEX=2) o una unidad de GPU diferente (por ejemplo, GPU_SHAPE=VM.GPU.A10.2).

Nota: El script de despliegue utiliza instancias de GPU que incurren en costos significativos (~50 $/día para una única GPU A10). Ejecute siempre ./entry_point.sh cleanup cuando haya terminado para evitar cargos continuos.

Tarea 3: Creación de VCN y redes

Cree la infraestructura de red de OCI necesaria para el cluster de OKE. Esto incluye una red virtual en la nube (VCN), gateways, tablas de rutas, listas de seguridad y subredes. Cada recurso de red se crea en unos segundos; el conjunto completo de comandos se completa en menos de 2 minutos.

  1. Cree una VCN con un bloque de 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. Cree un gateway de Internet para el enrutamiento de subred 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. Cree un gateway de NAT para el tráfico saliente desde subredes 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. Cree un gateway de servicio para acceder a Oracle Services Network. El controlador en la nube de OKE utiliza los servicios de Oracle para inicializar los nodos de trabajador (definir etiquetas de dominio de disponibilidad y eliminar las manchas de inicialización). Sin un gateway de servicio, los nodos de GPU pueden permanecer en un estado no inicializado y el aprovisionamiento de volúmenes en bloque fallará.

    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. Cree tablas de rutas para subredes públicas y 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)

    Nota: La tabla de rutas privada tiene dos reglas: una ruta de gateway de NAT para el acceso general a Internet (extracción de imágenes de contenedor, descarga de modelos) y una ruta de gateway de servicio para el acceso directo a Oracle Services Network. La ruta del gateway de servicios es fundamental. Sin él, el controlador de nube de OKE no puede inicializar nodos de trabajador, lo que impide el aprovisionamiento de volúmenes en bloque. La tabla de rutas pública utiliza el gateway de Internet para el acceso al equilibrador de carga.

  6. Cree una lista de seguridad con las reglas de entrada y salida necesarias para 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 seguridad: esta lista de seguridad de ejemplo es intencionalmente amplia por simplicidad. Para la producción, restrinja SSH a la subred bastión y al rango de IP, y prefiera listas de seguridad o NSG independientes por subred para que la subred del equilibrador de carga no permita SSH desde 0.0.0.0/0.

    Valor por defecto seguro: comience limitando SSH a su IP pública y asociando reglas SSH solo a la subred del bastión. Puede mantener los CIDR de pod/servicio de Kubernetes en la subred de trabajador y omitir SSH por completo de la subred del equilibrador de carga.

    Dividido opcional (recomendado): cree una pequeña lista de seguridad solo SSH para la subred de bastión y una lista independiente para subredes de trabajador/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)

    Nota: Si solo utiliza un equilibrador de carga interno, sustituya los orígenes 0.0.0.0/0 anteriores por 10.0.0.0/16 (o el CIDR de la VCN). Uso: asocie BASTION_SL_ID a la subred del bastión, WORKER_SL_ID a las subredes API/worker y LB_SL_ID a la subred del equilibrador de carga.

    Nota: Las reglas CIDR de los pods de Kubernetes (10.244.0.0/16) y CIDR de servicios (10.96.0.0/16) son necesarias para que los nodos de trabajador de GPU se registren en el cluster. La regla de código 4 ICMP tipo 3 permite la detección de MTU de ruta, lo que evita problemas de fragmentación de paquetes.

  7. Cree las subredes. El cluster necesita cuatro subredes: una para el punto final de API de Kubernetes, otra para los nodos de trabajador, otra para los equilibradores de carga y otra para el host bastión utilizado para acceder al 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)
    Subred CIDR Visibilidad Finalidad
    Punto final de API 10.0.0.0/28 Privada Servidor Kubernetes de API
    Nodos de trabajador 10.0.10.0/24 Privada Nodos de cálculo de GPU
    Equilibradores de carga 10.0.20.0/24 Pública Acceso a servicios externos
    Bastion 10.0.30.0/24 Pública Túnel SSH para acceso de cluster privado

Tarea 4: Creación del cluster de OKE

Despliegue un cluster de Kubernetes gestionado en OKE mediante los recursos de red creados en la tarea 3. El cluster tarda aproximadamente 10 minutos en aprovisionarse. Este tutorial crea un cluster privado (valor por defecto del script), que no consume una IP pública reservada para el punto final de API de Kubernetes. Los clusters privados son el enfoque recomendado para las cargas de trabajo de producción porque el servidor de API no está expuesto a la red pública de Internet.

  1. Cree el cluster de OKE con un punto 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

    El comando devuelve un ID de solicitud de trabajo. Obtenga el ID de cluster de la 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}"

    Nota: Los clusters privados no necesitan una IP pública reservada. Los nodos de trabajador siguen accediendo a Internet a través del gateway de NAT para extraer imágenes de contenedor y descargar modelos. Solo el acceso kubectl requiere un túnel SSH a través del bastión (configurado en los pasos siguientes).

  2. Espere a que el cluster se ACTIVE. Este paso tarda aproximadamente 10 minutos.

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

    Sondee el comando hasta que la salida devuelva ACTIVE.

    Opcional: mostrar un resumen de estado conciso (incluido el punto final de API privado).

    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

    Salida de la CLI de OCI que muestra el cluster de OKE en estado ACTIVE

    kubectl get nodes -o wide

    Salida de nodos get de kubectl que muestra dos nodos listos

  3. Cree un bastión de OCI para acceder al cluster privado. El bastión proporciona un túnel SSH gestionado al punto final de API de Kubernetes privado.

    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)

    Nota: Sustituya YOUR_PUBLIC_IP/32 por la IP pública actual. Para redes compartidas, utilice su bloque CIDR corporativo en su lugar.

    Espere a que el bastión se ACTIVE (aproximadamente 1 minuto).

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

    Nota de seguridad: para producción, no utilice 0.0.0.0/0. Restrinja --client-cidr-list a su IP pública o CIDR corporativo (por ejemplo, "YOUR_PUBLIC_IP/32"); de lo contrario, cualquier usuario de Internet puede intentar una sesión de bastión.

  4. Descargue el kubeconfig mediante el punto 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. Obtenga la dirección IP de punto final privado para el 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. Cree una sesión de reenvío de puerto de bastión. Necesitará un archivo de clave 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)

    Espere a que la sesión se ACTIVE y, a continuación, obtenga el comando SSH.

    oci bastion session get \
        --session-id "${SESSION_ID}" \
        --query "data.{state:\"lifecycle-state\", ssh:\"ssh-metadata\".command}" 2>&1
  7. Abra un terminal independiente e inicie el túnel SSH con el comando del paso anterior. El túnel reenvía el puerto local 6443 a la API de Kubernetes privada.

    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

    Nota: Sustituya <PRIVATE_IP>, <SESSION_OCID> y <REGION> por los valores del paso anterior. Mantenga este terminal abierto durante la sesión. El indicador -o IdentitiesOnly=yes evita errores de "demasiados fallos de autenticación" cuando el agente SSH tiene varias claves cargadas.

  8. Actualice el kubeconfig para conectarse a través del 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

    Nota: El indicador --insecure-skip-tls-verify es necesario porque el certificado de cluster se ha emitido para la IP de punto final privado, no 127.0.0.1. Esto es seguro porque el tráfico se cifra a través del túnel SSH.

  9. Si utiliza un perfil de la CLI de OCI no predeterminado (por ejemplo, API_KEY_AUTH), actualice kubeconfig para utilizarlo. El valor por defecto de kubeconfig generado es el perfil DEFAULT para la generación de token.

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

    Consejo: los pasos 6 a 9 son automatizados por ./entry_point.sh tunnel, que también se vuelve a conectar automáticamente si el túnel SSH se cae durante operaciones de larga ejecución como la expansión del disco. Ejecútelo en un terminal independiente y déjelo en ejecución mientras dure la sesión.

  10. Verifique el acceso al cluster.

kubectl get nodes

En este punto, la salida no mostrará ningún nodo, ya que el pool de nodos de GPU aún no se ha agregado.

No resources found

Tarea 5: Agregar pool de nodos de GPU

Agregue un pool de nodos con instancias informáticas de GPU al cluster de OKE.

  1. Busque la última imagen de nodo de OKE compatible con GPU. OKE requiere imágenes específicas con componentes de registro de nodos y kubelet preinstalados. Utilice la API node-pool-options para buscar la imagen correcta para su versión de 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}"

    Nota: Los filtros de consulta para las imágenes de GPU de Oracle Linux 8.10 que coincidan con la versión de Kubernetes (por ejemplo, OKE-1.31.10). Si necesita imágenes basadas en ARM, sustituya 8.10 por el filtro adecuado.

  2. Determine el dominio de disponibilidad que tiene unidades de GPU. No todos los dominios de disponibilidad tienen capacidad 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}"

    Nota: La capacidad de la GPU varía según la región y el dominio de disponibilidad. Si la creación del pool de nodos falla con un error de "Capacidad insuficiente del host", pruebe con un dominio de disponibilidad diferente (GPU_AD_INDEX) o una unidad de GPU, o solicite capacidad mediante el proceso normal de OCI.

  3. Cree el pool de nodos de GPU con un volumen de inicio 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"}]'

    Nota: El gráfico de Helm de vLLM utiliza las etiquetas de nodo app=gpu y nvidia.com/gpu=true más adelante para programar pods de inferencia en nodos de GPU. El volumen de inicio de 200 GB proporciona espacio para la imagen de contenedor vLLM (~10 GB) y los pesos del modelo, pero el sistema de archivos se debe ampliar antes de su uso (consulte la tarea 8).

  4. Espere a que los nodos de GPU estén listos. Esto suele tardar entre 5 y 10 minutos mientras el nodo aprovisiona, inicia, instala controladores de GPU y se registra con el cluster.

    Nota: Las instancias de GPU están sujetas a restricciones de capacidad. Si el pool de nodos permanece en estado CREATING, compruebe el estado del nodo en la consola de OCI o con oci ce node-pool get. Un error de "Capacidad insuficiente del host" significa que no hay instancias de GPU disponibles en ese dominio de disponibilidad. Para resolverlo, pruebe con un dominio de disponibilidad diferente (GPU_AD_INDEX=0 o GPU_AD_INDEX=2), pruebe con una unidad de GPU diferente o solicite una reserva de capacidad a través de la consola de OCI o un ticket de soporte.

    kubectl get nodes -w

    Salida esperada una vez que el nodo está listo:

    NAME          STATUS   ROLES   AGE   VERSION
    10.0.10.x     Ready    node    5m    v1.31.10
  5. Verifique que la GPU se haya detectado en el nodo.

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

    Salida esperada:

    NAME          GPUs
    10.0.10.x     1
  6. Parche CoreDNS para programar en nodos de GPU. Los nodos de GPU de OKE tienen un tinte nvidia.com/gpu=present:NoSchedule. En los clusters que solo tienen nodos de GPU, los pods del sistema como CoreDNS no se pueden programar sin una tolerancia para este mantenimiento. Sin DNS, los pods no pueden resolver nombres de host externos para descargar 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 que CoreDNS se está ejecutando.

    kubectl get pods -n kube-system | grep coredns

    Nota: Si el cluster tiene un pool de nodos de CPU dedicado para las cargas de trabajo del sistema, este paso no es necesario. Este parche solo es necesario cuando los nodos de GPU son los únicos nodos del 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

    Salida de la CLI de OCI que muestra la agrupación de CPU y la agrupación de gpu en estado ACTIVE

Tarea 6: Instalación del plugin de dispositivo NVIDIA

Instale el plugin del dispositivo NVIDIA para que Kubernetes pueda detectar y programar cargas de trabajo en la GPU.

  1. Aplique el plugin de dispositivo NVIDIA DaemonSet.

    kubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.14.1/nvidia-device-plugin.yml
  2. Espere a que los pods del plugin estén listos.

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

    Nota: Algunas imágenes de nodo de GPU de OKE incluyen un plugin de dispositivo NVIDIA preinstalado (nvidia-gpu-device-plugin). Si la imagen ya la incluye, al aplicar el flujo ascendente DaemonSet se crea una segunda instancia que no causa conflictos. El script automatizado (entry_point.sh deploy-vllm) siempre lo instala para garantizar que la detección de GPU funcione independientemente de la versión de la imagen del nodo.

  3. Confirme que Kubernetes puede asignar la GPU.

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

    Salida esperada:

    NAME          GPUs
    10.0.10.x     1
  4. Aplique el parche CoreDNS para tolerar los contaminantes del nodo de GPU. En los clusters en los que los nodos de GPU son los únicos nodos de trabajador, los pods CoreDNS no se pueden programar porque los nodos de GPU de OKE llevan un mantenimiento de nvidia.com/gpu=present:NoSchedule. Sin DNS, los pods no pueden resolver los registros de imágenes ni las URL de descarga 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 rollout status deployment/coredns -n kube-system --timeout=60s

    Nota: Este paso solo es necesario cuando los nodos de GPU son los únicos nodos de trabajador del cluster. Si tiene un pool de nodos de CPU dedicado para las cargas de trabajo del sistema, CoreDNS programa allí por defecto y este parche no es necesario.

Tarea 7: Configuración del almacenamiento

Aplique el volumen en bloque de OCI StorageClass para proporcionar almacenamiento persistente para los pesos del modelo.

  1. Aplique la definición StorageClass.

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

    El archivo define dos niveles de rendimiento:

    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 Rendimiento Caso de uso
    oci-block-storage-enc Equilibrado (vpusPerGB: 10) Predeterminado y rentable para la mayoría de los modelos
    oci-block-storage-hp Alto rendimiento (vpusPerGB: 20) Carga de modelos más rápida para modelos más grandes
  2. Verifique que StorageClasses esté disponible.

    kubectl get storageclass

Nota: Para despliegues de varios nodos que requieren almacenamiento compartido en varios pods, utilice el servicio OCI File Storage (NFS) con el modo de acceso ReadWriteMany en lugar de Block Volumes.

Tarea 8: Expansión del sistema de archivos del nodo de GPU

Los volúmenes de inicio de OCI tienen una partición fija de ~47 GB independientemente del tamaño del volumen de inicio que especifique. La imagen del contenedor vLLM es de aproximadamente 10 GB, y los pesos del modelo requieren espacio adicional. Debe ampliar el sistema de archivos antes de desplegar vLLM para evitar expulsiones de DiskPressure.

Nota: Este es un requisito específico de OCI. El volumen de inicio se aprovisiona a 200 GB, pero el sistema operativo solo particiona ~47 GB por defecto. El espacio restante se debe reclamar manualmente.

  1. Verifique el tamaño actual del sistema de archivos en el nodo de 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\":\"/\"}}]}}"

    La salida mostrará aproximadamente 47 GB en total, lo que confirma que se necesita expansión.

  2. Cree un pod con privilegios en el nodo de GPU para ejecutar los comandos de expansión.

    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\":\"/\"}}]}}"

    Espere a que se inicie el pod.

    kubectl wait --for=condition=Ready pod/expand-disk --timeout=60s
  3. Ejecute los cuatro pasos de expansión en un único comando kubectl exec. Al ejecutarlos juntos, se evita el riesgo de que kubectl exec devuelva el código de salida 137 (SIGKILL) entre los pasos, lo que puede ocurrir durante la E/S de disco pesado en el 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 /
    '
    Paso Comando Finalidad
    1 growpart /dev/sda 3 Amplíe la partición 3 para utilizar el disco completo
    2 pvresize /dev/sda3 Cambiar tamaño de volumen físico de LVM
    3 lvextend -l +100%FREE /dev/ocivolume/root Ampliar volumen lógico
    4 xfs_growfs / Ampliar el sistema de archivos XFS para llenar el volumen

    Nota: Las cuatro operaciones son idempotentes. Si el exec devuelve el código de salida 137, puede volver a ejecutar el bloque completo de forma segura. Busque EXPANSION_COMPLETE en la salida para confirmar que se ha realizado correctamente.

  4. Reinicie kubelet para que el nodo informe el almacenamiento asignable actualizado y, a continuación, verifique y limpie.

    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

    Nota: El comando nsenter introduce el espacio de nombres PID del host para acceder a systemd. Un archivo chroot /host systemctl restart kubelet sin formato falla porque no se puede conectar al bus systemd desde un chroot.

    La salida esperada debe mostrar aproximadamente 189 GB en total.

Tarea 9: Despliegue de la pila de producción de vLLM

Instale la pila de inferencia de vLLM mediante Helm.

  1. Agregue el repositorio de vLLM Helm.

    helm repo add vllm https://vllm-project.github.io/production-stack
    helm repo update
  2. Revise el archivo de valores de Helm. production_stack_specification.yaml configura el modelo, los recursos y el almacenamiento para 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"

    Nota: El modelo openai/gpt-oss-20b es un modelo de mezcla de expertos (MoE) con parámetros totales 20B y parámetros activos 3.6B por transferencia directa. Se publica bajo la licencia Apache 2.0, por lo que no se requiere ningún token de Hugging Face. La imagen de contenedor vllm/vllm-openai proporciona un servidor de API compatible con OpenAI, lo que permite a los clientes utilizar llamadas estándar del SDK OpenAI en el punto final autoalojado.

  3. Despliegue la pila. No utilice --wait aquí porque el pod de enrutador CrashLoop hasta que se aplique el parche en el siguiente paso.

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

    Espere a que se inicie el pod del motor vLLM (el enrutador se aplicará a continuación).

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

    Nota: El pod del motor tarda varios minutos en estar listo porque descarga los pesos del modelo en el primer inicio. Si el pod permanece en ContainerCreating, la imagen de contenedor (~10 GB) se sigue extrayendo. Utilice kubectl describe pod <pod-name> para comprobar el progreso.

  4. Aplique parches al despliegue del enrutador. El enrutador necesita una tolerancia de GPU (para que pueda programar cuando los nodos de GPU sean los únicos nodos con capacidad) y un aumento de los límites de memoria (los 500 Mi por defecto pueden 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"}
    ]'

    Nota: La tolerancia de GPU es necesaria porque los nodos de GPU de OKE tienen un mantenimiento nvidia.com/gpu=present:NoSchedule que evita que las cargas de trabajo que no son de GPU se programen. Dado que el enrutador no utiliza una GPU, pero necesita ejecutarse en algún lugar, esta tolerancia le permite programar en nodos de GPU. En clusters con pools de nodos de CPU dedicados, esta tolerancia no es necesaria.

  5. Confirme que se ha desplegado la versión de Helm.

    helm list

    Salida de terminal de la lista de timbres que muestra la pila de vllm desplegada

  6. Verifique que los pods se están ejecutando.

    kubectl get pods

    Salida 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 obtiene pods que muestran los pods del enrutador y del motor en ejecución 1/1

  7. Compruebe el progreso de carga del modelo en los logs de pod.

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

    Espere hasta que vea un mensaje que indique que el modelo se ha cargado y que el servidor está listo para aceptar solicitudes.

Tarea 10: Prueba del punto final de inferencia

Valide que el despliegue sirve solicitudes de inferencia. La pila de producción de vLLM expone una API compatible con OpenAI a través del servicio de enrutador, por lo que cualquier cliente SDK OpenAI o comando curl puede interactuar con ella.

El siguiente diagrama muestra el ciclo de vida de la solicitud de inferencia: desde la solicitud del cliente hasta la lógica de selección del motor del enrutador, hasta las fases de prelleno y descodificación del motor vLLM y viceversa como respuesta transmitida.

Diagrama de secuencia que muestra el ciclo de vida de la solicitud de inferencia desde el cliente hasta el enrutador y el motor vLLM y la GPU

  1. Muestre los modelos disponibles para confirmar que el despliegue está en buen estado.

    kubectl get svc vllm-router-service

    El servicio de enrutador proporciona el gateway de API para todos los modelos desplegados. Dado que el cluster utiliza un punto final privado, puede acceder al servicio mediante kubectl port-forward.

  2. Inicie un reenvío de puerto desde la máquina local al servicio de enrutador. Abra un nuevo terminal (mantenga el túnel SSH en ejecución en el otro) y ejecute:

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

    Esto asigna localhost:8080 en el equipo al puerto 80 en el servicio de enrutador dentro del cluster.

    Nota de seguridad: kubectl port-forward se enlaza localmente y no expone el servicio públicamente. Esta es la forma más segura de realizar pruebas cuando se utiliza un cluster privado en un túnel bastión.

    Nota: El comando port-forward se ejecuta en primer plano. Mantenga este terminal abierto durante las pruebas. Pulse Ctrl+C para detenerlo cuando haya terminado.

  3. En otro terminal, verifique que el modelo está disponible consultando el punto final de los modelos.

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

    Salida esperada:

    {
        "object": "list",
        "data": [
            {
                "id": "openai/gpt-oss-20b",
                "object": "model",
                "created": 1234567890,
                "owned_by": "vllm"
            }
        ]
    }
  4. Enviar una solicitud de finalización 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

    Salida esperada (abreviado):

    {
        "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. Enviar una solicitud de finalización de chat. Este es el mismo formato que utiliza el SDK de Python OpenAI y es la forma más común de interactuar con los LLM mediante programación.

    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

    Salida esperada (abreviado):

    {
        "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 respuestas incluyen el texto generado por el modelo en la matriz choices, las estadísticas de uso de token y un finish_reason de stop (el modelo finalizó naturalmente) o length (la salida se truncó en max_tokens).

    Nota: El nombre de modelo de la solicitud de API es la ruta de modelo completa (openai/gpt-oss-20b), que coincide con el campo modelURL en los valores de Helm. Cualquier cliente compatible con OpenAI puede utilizar este punto final definiendo base_url en http://localhost:8080/v1.

    Salida de terminal que muestra la solicitud de finalización de chat curl y la respuesta JSON de openai/gpt-oss-20b

  6. Mida la latencia de extremo a extremo para una solicitud corta.

    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

    En una única GPU A10, espere de 1 a 3 segundos de principio a fin para finalizaciones cortas. El tiempo hasta el primer token (TTFT) suele ser de 50-200 ms en función de la longitud de la petición de datos. Para obtener un mayor rendimiento, aumente replicaCount en los valores de Helm para agregar más réplicas del motor detrás del enrutador.

Tarea 11: Exponer a través de OCI Load Balancer (opcional)

Haga que el punto final de inferencia sea accesible externamente a través de un equilibrador de carga de OCI.

Nota de seguridad: esto expone la API de inferencia a la red pública de Internet por defecto. No lo active en producción sin TLS, autenticación (clave de API/JWT/mTLS) y controles de lista de permitidos de IP o WAF. Si debe exponerlo, avíselo con un controlador de entrada o gateway de API que aplique límites de autenticación y velocidad, o bien utilice un equilibrador de carga interno.

  1. Aplique parches al servicio de enrutador para que utilice un tipo LoadBalancer.

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

    Nota: Si desea un equilibrador de carga interno, agregue la anotación de OCI al servicio (ejemplo a continuación). Esto mantiene el punto final privado dentro de la VCN.

    kubectl annotate svc vllm-router-service \
      "service.beta.kubernetes.io/oci-load-balancer-internal"="true"
  2. Espere a que se asigne la IP externa.

    kubectl get svc vllm-router-service -w

    Salida esperada una vez aprovisionado el equilibrador de carga:

    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 obtiene la salida svc que muestra los servicios de enrutador y motor

  3. Pruebe el punto 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
      }'

Tarea 12: Configuración del paralelismo de tensores de varias GPU (avanzado)

Despliegue modelos más grandes en varias GPU en unidades con hardware dedicado.

El paralelismo de sensores divide un modelo en varias GPU en un solo nodo. Esto es necesario cuando los requisitos de memoria de un modelo superan una única GPU. Por ejemplo, Meta Llama 3.1 70B requiere aproximadamente 140 GB de memoria de GPU, lo que excede la capacidad de cualquier GPU única, pero cabe en GPU 8x A100 de 80 GB o 8x H100.

  1. Cree un secreto de Kubernetes con su token Hugging Face. Los modelos cerrados como Llama 3.1 70B requieren autenticación.

    kubectl create secret generic hf-token-secret \
      --from-literal=token=YOUR_HUGGINGFACE_TOKEN
  2. Actualice production_stack_specification.yaml con una configuración de varias GPU.

    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. Desplegar con los valores actualizados. Al igual que con la tarea 9, no utilice --wait. El enrutador CrashLoop hasta que se aplique el parche.

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

    A continuación, aplique el parche al enrutador (igual que la tarea 9, paso 4) y 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 que el pod se está ejecutando y que todas las GPU están en uso.

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

Nota: Asegúrese de que el cluster de OKE tiene un pool de nodos con la unidad de GPU con hardware dedicado adecuada (por ejemplo, BM.GPU.H100.8) antes de desplegar una configuración de varias GPU.

Tarea 13: Uso de OCI Object Storage para modelos (avanzado)

Cargue los pesos del modelo de OCI Object Storage en lugar de descargarlos de Hugging Face. Esto es útil para modelos privados, descargas más rápidas dentro de OCI o entornos sin acceso a Internet externo.

  1. Cargue los pesos del modelo en un cubo de OCI Object Storage. Vaya a Almacenamiento > Almacenamiento de objetos en la consola de OCI y cree un cubo si aún no lo tiene.

  2. Cree una URL de solicitud autenticada previamente (SAP) para el cubo. En la consola de OCI, seleccione el cubo, haga clic en Solicitudes autenticadas previamente y cree una nueva solicitud con acceso de lectura.

  3. Actualice production_stack_specification.yaml para utilizar la URL de 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. Despliegue con los valores actualizados (sin --wait). Consulte la tarea 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 las cargas del modelo desde Object Storage comprobando los logs de pod.

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

Tarea 14: Limpiar recursos

Elimine todos los recursos desplegados para evitar cargos continuos.

  1. Elimine los recursos de Kubernetes mediante el script de limpieza.

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

    Esto desinstala la versión de Helm, suprime todos los recursos PersistentVolumeClaims, PersistentVolumes y vLLM personalizados.

  2. Suprima el cluster de OKE y todos los recursos de red de OCI.

    ./entry_point.sh cleanup

    Esto suprime los siguientes recursos en orden:

    • Pool de nodos de GPU
    • Cluster de OKE
    • Host bastión (si se crea)
    • Subredes (API, trabajador, equilibrador de carga, bastión)
    • Listas de seguridad
    • Gateway de servicios, gateway de NAT y gateway de Internet
    • Tablas de rutas
    • VCN
  3. Verifique que todos los recursos se hayan eliminado en la consola de OCI en Developer Services > Kubernetes Clusters y Networking > Virtual Cloud Networks.

    ./entry_point.sh cleanup

    Salida de terminal de limpieza entry_point.sh que muestra el desglose completo de recursos

Nota: Asegúrese de suprimir todos los recursos para evitar cargos en curso. Las instancias de GPU y los volúmenes en bloque generan costos incluso cuando están inactivos.

Siguiente paso

En este tutorial se desplegó una pila de inferencia funcional. Para las cargas de trabajo de producción, tenga en cuenta las siguientes mejoras:

Acuses de recibo

Más recursos de aprendizaje

Explore otros laboratorios en docs.oracle.com/learn o acceda a más contenido de aprendizaje gratuito en el canal YouTube de Oracle Learning. Además, visite education.oracle.com/learning-explorer para convertirse en un explorador de Oracle Learning.

Para obtener documentación sobre el producto, visite Oracle Help Center.