Nota

Distribuisci lo stack di produzione vLLM OpenAI sul motore Oracle Kubernetes (OKE)

Introduzione

Le organizzazioni che adottano modelli linguistici di grandi dimensioni (LLM, large language model) per i carichi di lavoro di produzione devono affrontare una decisione fondamentale sull'infrastruttura: affidarsi a API di inferenza di terze parti o distribuire uno stack di inferenza self-hosted. Le implementazioni self-hosted offrono vantaggi significativi: privacy completa dei dati e controllo della compliance, latenza di inferenza inferiore a 100 millisecondi eliminando i round-trip della rete, costo prevedibile su larga scala e libertà di perfezionare e servire qualsiasi modello open-source senza accordi esclusivi con i fornitori.

Tuttavia, la creazione da zero di uno stack di inferenza LLM di livello produttivo è complessa. Richiede l'orchestrazione dei container basata su GPU, l'instradamento intelligente delle richieste su più repliche di modelli, lo storage persistente per pesi di modelli di grandi dimensioni e il monitoraggio continuo, il tutto integrato e in esecuzione in modo affidabile.

Oracle Cloud Infrastructure offre diversi percorsi per l'inferenza AI. Il servizio di AI generativa OCI offre un'esperienza completamente gestita con cluster AI dedicati isolati nella tua tenancy, ideale per i team che desiderano iniziare rapidamente con i modelli supportati. Questo tutorial adotta l'approccio alternativo: distribuire il proprio stack di inferenze su OKE. Questo percorso è progettato per i team che hanno bisogno di un controllo preciso sui driver GPU, sulle versioni CUDA, sulle configurazioni dei modelli e sui parametri di servizio o per i team che stanno addestrando e perfezionando modelli personalizzati e vogliono servirli direttamente. OCI fornisce istanze GPU Bare Metal con GPU NVIDIA A10, A100 e H100, connesse tramite una rete di cluster RDMA a bassissima latenza, offrendoti lo stesso livello di controllo hardware che avresti on-premise, beneficiando al contempo dell'elasticità del cloud.

Lo stack di produzione vLLM risolve la complessità dell'inferenza self-hosted fornendo una piattaforma open source e nativa per Kubernetes basata su vLLM, il motore di inferenza ad alto throughput utilizzato nella produzione da organizzazioni come Meta, Mistral AI e IBM. Offre un throughput fino a 24x più elevato rispetto ai framework di servizio standard attraverso una gestione efficiente della memoria GPU e l'ottimizzazione della cache KV. In combinazione con le forme OKE e OCI GPU, ottieni una piattaforma di inferenza pronta per la produzione con networking, storage e sicurezza di livello Enterprise. Gli script di distribuzione OCI utilizzati in questa esercitazione vengono forniti e gestiti nel repository ufficiale dello stack di produzione vLLM.

In questa esercitazione viene illustrato come distribuire lo stack di produzione vLLM su OKE, dal provisioning dell'infrastruttura all'esecuzione della prima richiesta di inferenza.

Nota: questa esercitazione esegue il provisioning delle risorse passo-passo utilizzando l'interfaccia CLI OCI per comprendere il flusso completo delle risorse cloud OCI necessarie per una distribuzione dell'inferenza GPU. Per gli ambienti di produzione, si consiglia di codificare questa infrastruttura utilizzando Terraform o OCI Resource Manager (Shepherd) per distribuzioni ripetibili e controllate a livello di versione.

Diagramma dell'architettura che mostra la VCN con subnet, cluster OKE, pool di nodi GPU, pod vLLM e load balancer

In questa esercitazione vengono utilizzati i seguenti servizi OCI:

Servizio Scopo
Motore Oracle Kubernetes (OKE) Cluster Kubernetes gestito per orchestrazione dei container e pianificazione dei carichi di lavoro GPU
OCI Compute (forme GPU) Istanze GPU NVIDIA A10 (24 GB) e A100 (80 GB) per l'inferenza del modello
Volumi a blocchi OCI Storage persistente per i pesi dei modelli con livelli di prestazioni configurabili
Rete Cloud virtuale (VCN) OCI Infrastruttura di rete che include subnet, gateway ed elenchi di sicurezza
Load balancer OCI Accesso esterno agli endpoint di inferenza
OCI Bastion Tunnel SSH gestiti per l'accesso al cluster privato
Memorizzazione degli oggetti OCI Origine del modello alternativo che utilizza URL di richiesta preautenticata (PAR, PreAuthenticated Request)

Obiettivi

In questa esercitazione:

Prerequisiti

Nota: gli output e gli screenshot di esempio in questa esercitazione utilizzano us-chicago-1. È possibile distribuire in qualsiasi area supportata impostando OCI_REGION. La capacità della GPU varia in base all'area e al dominio di disponibilità, pertanto conferma che la forma della GPU di destinazione è disponibile prima della distribuzione. Controlla la disponibilità della forma GPU per area ed essere pronto a provare un dominio di disponibilità diverso (GPU_AD_INDEX) se riscontri errori di capacità.

Nota: questa esercitazione esegue il provisioning delle risorse GPU a pagamento (ad esempio, VM.GPU.A10.1). Non si tratta di un carico di lavoro OCI Sempre gratis. Eseguire sempre i passaggi di pulizia al termine per evitare addebiti continui.

Nota: questa esercitazione distribuisce openai/gpt-oss-20b, un modello con licenza Apache 2.0 di OpenAI. Non sono richiesti token Hugging Face. Se desideri distribuire modelli con accesso controllato come Meta Llama 3.1, avrai bisogno di un account Hugging Face con un token API.

Task 1: Configurazione delle variabili di ambiente

Impostare la configurazione OCI richiesta prima di distribuire l'infrastruttura.

  1. Trovare l'OCID del compartimento in OCI Console. Andare a Identità e sicurezza > Compartimenti, quindi fare clic sul compartimento di destinazione e copiare l'OCID.

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

    Output CLI OCI che mostra l'OCID del compartimento

  2. Esportare la variabile di ambiente richiesta.

    export OCI_COMPARTMENT_ID="ocid1.compartment.oc1..xxxxx"
  3. Facoltativamente, eseguire l'override della configurazione predefinita impostando una qualsiasi delle variabili di ambiente indicate di seguito.

    Variabile Predefinita Descrizione
    OCI_REGION us-ashburn-1 Area OCI per la distribuzione
    OCI_PROFILE DEFAULT Profilo di configurazione OCI CLI
    CLUSTER_NAME production-stack Nome del cluster OKE
    GPU_SHAPE VM.GPU.A10.1 Forma di computazione GPU per il pool di nodi
    GPU_NODE_COUNT 1 Numero di nodi GPU nel pool
    GPU_BOOT_VOLUME_GB 200 Dimensione del volumi di avvio in GB per i nodi GPU
    CPU_BOOT_VOLUME_GB 100 Dimensione del volumi di avvio in GB per i nodi CPU
    GPU_AD_INDEX 1 Indice del dominio di disponibilità (basato su 0) per il posizionamento delle GPU
    PRIVATE_CLUSTER true Impostare su false per un endpoint API Kubernetes pubblico
    KUBERNETES_VERSION v1.31.10 Versione Kubernetes per il cluster OKE

    Ad esempio, per eseguire la distribuzione con due nodi GPU A100:

    export OCI_COMPARTMENT_ID="ocid1.compartment.oc1..xxxxx"
    export GPU_SHAPE="BM.GPU.A100-v2.8"
    export GPU_NODE_COUNT="2"
  4. Esamina le forme GPU disponibili e selezionane una in base ai requisiti delle dimensioni del modello.

    Forma GPU Tipo GPU Memoria GPU Consigliato
    VM.GPU.A10.1 1 NVIDIA A10 24 GB Modelli di parametri 7B–13B
    VM.GPU.A10.2 2 NVIDIA A10 48 GB Tensore parallelo con piccoli modelli
    BM.GPU4.8 8 NVIDIA A100 40 GB 320 GB Modelli 70B a costi contenuti
    BM.GPU.A100-v2.8 8 NVIDIA A100 80 GB 640 GB 70B+ modelli di parametri
    BM.GPU.H100.8 8 NVIDIA H100 640 GB Modelli più grandi, supporto RDMA

    Nota: le forme Bare Metal (BM.*) forniscono hardware dedicato senza sovraccarico di virtualizzazione e supportano il parallelismo del tensore multi-GPU. Le forme delle virtual machine (VM.*) sono più convenienti per i modelli più piccoli.

    Nota: questa esercitazione utilizza VM.GPU.A10.1 (singola NVIDIA A10 con memoria GPU da 24 GB) per distribuire openai/gpt-oss-20b, un modello Mixture of Experts (MoE) con parametri attivi 3.6B che in genere si adatta a una singola GPU A10. Le sezioni avanzate mostrano configurazioni multi-GPU che utilizzano BM.GPU.H100.8 per modelli più grandi come Llama 3.1 70B.

Task 2: Distribuzione mediante script automatizzato (avvio rapido)

Lo stack di produzione vLLM include uno script di distribuzione automatizzato che esegue il provisioning di tutte le risorse OCI e distribuisce lo stack di inferenza con un singolo comando. Utilizzare questo approccio per una distribuzione rapida. I task da 3 a 10 coprono ogni passo singolarmente per gli utenti che desiderano personalizzare il processo.

  1. Duplicare il repository dello stack di produzione vLLM.

    git clone https://github.com/vllm-project/production-stack.git
    cd production-stack/deployment_on_cloud/oci
  2. Esportare l'OCID compartimento.

    export OCI_COMPARTMENT_ID="ocid1.compartment.oc1..xxxxx"
  3. Eseguire lo script di distribuzione.

    ./entry_point.sh setup

    Output del terminale dell'impostazione entry_point.sh che mostra la creazione di VCN, cluster, bastion e pool di nodi

    Per i cluster pubblici (PRIVATE_CLUSTER=false), l'impostazione crea tutta l'infrastruttura e distribuisce lo stack vLLM in un singolo comando. Passare il file dei valori Helm come secondo argomento:

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

    Per i cluster privati (impostazione predefinita), l'impostazione crea l'infrastruttura ma non può raggiungere direttamente l'API Kubernetes. Aprire un terminale separato e avviare il tunnel, quindi distribuire:

    # 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. Verificare che la distribuzione sia in esecuzione.

    kubectl get pods

    Output previsto:

    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: se entrambi i pod mostrano lo stato Running, la distribuzione è pronta. Passare a Task 10: eseguire il test dell'endpoint di inferenza.

Nota: le istanze GPU sono soggette ai vincoli di capacità OCI. Se lo script rimane nel loop "In attesa di nodo GPU" per più di 15 minuti, la forma GPU potrebbe non essere disponibile nel dominio di disponibilità selezionato. Controllare lo stato del pool di nodi con oci ce node-pool get e cercare gli errori "Capacità host esaurita". Per risolvere questo problema, eseguire il cleanup con ./entry_point.sh cleanup e la ridistribuzione con un dominio di disponibilità diverso (ad esempio, GPU_AD_INDEX=0 o GPU_AD_INDEX=2) o con una forma GPU diversa (ad esempio, GPU_SHAPE=VM.GPU.A10.2).

Nota: lo script di distribuzione utilizza istanze GPU che comportano costi significativi (circa 50 dollari al giorno per una singola GPU A10). Esegui sempre ./entry_point.sh cleanup quando hai finito per evitare costi continui.

Task 3: Creare VCN e networking

Creare l'infrastruttura di rete OCI necessaria per il cluster OKE. Sono incluse una rete cloud virtuale (VCN), gateway, tabelle di instradamento, liste di sicurezza e subnet. Ogni risorsa di rete viene creata in pochi secondi; il set completo di comandi viene completato in meno di 2 minuti.

  1. Creare una VCN con un blocco 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. Creare un gateway Internet per l'instradamento della subnet pubblica.

    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. Creare un gateway NAT per il traffico in uscita dalle subnet private.

    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. Creare un gateway di servizi per l'accesso a Oracle Services Network. Il controller cloud OKE utilizza i servizi Oracle per inizializzare i nodi di lavoro (impostare le etichette del dominio di disponibilità, rimuovere i contratti di inizializzazione). Senza un gateway di servizio, i nodi GPU potrebbero rimanere in uno stato non inizializzato e il provisioning dei volumi a blocchi non riuscirà.

    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. Creare tabelle di instradamento per le subnet private e pubbliche.

    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 tabella di instradamento privato prevede due regole: un instradamento del gateway NAT per l'accesso a Internet generale (estrazione di immagini del contenitore, download di modelli) e un instradamento del gateway di servizio per l'accesso diretto a Oracle Services Network. L'instradamento del gateway del servizio è critico. Senza di esso, il controller cloud OKE non può inizializzare i nodi di lavoro, il che impedisce il provisioning dei volumi a blocchi. La tabella di instradamento pubblico utilizza il gateway Internet per l'accesso al load balancer.

  6. Creare una lista di sicurezza con le regole di entrata e uscita necessarie per 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 di sicurezza: questa lista di sicurezza di esempio è intenzionalmente ampia per semplicità. Per la produzione, limitare l'accesso SSH alla subnet del bastion e all'intervallo IP in uso e preferire liste di sicurezza separate o gruppi NSG per subnet in modo che la subnet del load balancer non consenta l'accesso SSH a 0.0.0.0/0.

    Impostazione predefinita sicura: iniziare limitando SSH all'IP pubblico e collegando le regole SSH solo alla subnet bastion. Puoi mantenere i CIDR pod/servizio Kubernetes nella subnet del worker e omettere completamente SSH dalla subnet del load balancer.

    Divisione facoltativa (consigliata): creare una piccola lista di sicurezza solo SSH per la subnet del bastion e una lista separata per le subnet di 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)

    Nota: se si utilizza solo un load balancer interno, sostituire le origini 0.0.0.0/0 precedenti con 10.0.0.0/16 (o il CIDR della VCN). Uso: collega BASTION_SL_ID alla subnet del bastion, WORKER_SL_ID alle subnet API/worker e LB_SL_ID alla subnet del load balancer.

    Nota: le regole CIDR (10.244.0.0/16) dei pod Kubernetes e CIDR (10.96.0.0/16) dei servizi sono necessarie per la registrazione dei nodi di lavoro GPU nel cluster. La regola ICMP di tipo 3 code 4 consente la ricerca automatica MTU del percorso, che impedisce problemi di frammentazione dei pacchetti.

  7. Creare le subnet. Il cluster richiede quattro subnet: una per l'endpoint API Kubernetes, una per i nodi di lavoro, una per i load balancer e una per l'host bastion utilizzato per accedere al cluster privato.

    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)
    Subnet CIDR Visibilità Scopo
    Endpoint API 10.0.0.0/28 Privata Server API Kubernetes
    Nodi di lavoro 10.0.10.0/24 Privata Nodi di computazione GPU
    Load balancer 10.0.20.0/24 Pubblica Accesso al servizio esterno
    Bastion 10.0.30.0/24 Pubblica Tunnel SSH per l'accesso al cluster privato

Task 4: Creazione del cluster OKE

Distribuire un cluster Kubernetes gestito su OKE utilizzando le risorse di rete create nel task 3. Il provisioning del cluster richiede circa 10 minuti. Questa esercitazione crea un cluster privato (impostazione predefinita dello script), che non utilizza un IP pubblico riservato per l'endpoint API Kubernetes. I cluster privati sono l'approccio consigliato per i carichi di lavoro di produzione perché il server API non è esposto alla rete Internet pubblica.

  1. Creare il cluster OKE con un endpoint privato.

    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

    Il comando restituisce un ID di richiesta di lavoro. Recupera l'ID cluster dalla lista di cluster.

    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: i cluster privati non richiedono un IP pubblico riservato. I nodi di lavoro accedono ancora a Internet tramite il gateway NAT per estrarre le immagini dei container e scaricare i modelli. Solo l'accesso kubectl richiede un tunnel SSH tramite il bastion (configurato nei passi successivi).

  2. Attendere che il cluster diventi attivo. Questo passaggio richiede circa 10 minuti.

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

    Eseguire il polling del comando finché l'output non restituisce ACTIVE.

    Facoltativo: visualizza un riepilogo conciso dello stato (incluso l'endpoint API privato).

    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

    Output dell'interfaccia CLI OCI che mostra il cluster OKE in stato ACTIVE

    kubectl get nodes -o wide

    Output di recupero nodi kubectl con due nodi pronti

  3. Creare un bastion OCI per accedere al cluster privato. Il bastion fornisce un tunnel SSH gestito all'endpoint API Kubernetes privato.

    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: sostituire YOUR_PUBLIC_IP/32 con l'IP pubblico corrente. Per le reti condivise, utilizzare invece il blocco CIDR aziendale.

    Attendere che il bastione diventi ATTIVO (circa 1 minuto).

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

    Nota di sicurezza: per la produzione, non utilizzare 0.0.0.0/0. Limitare --client-cidr-list all'IP pubblico o al CIDR aziendale (ad esempio, "YOUR_PUBLIC_IP/32"), altrimenti qualsiasi utente su Internet può tentare una sessione bastion.

  4. Scaricare kubeconfig utilizzando l'endpoint privato.

    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. Recupera l'indirizzo IP dell'endpoint privato per il tunnel 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. Creare una sessione di inoltro della porta bastion. Sarà necessario un file di chiave pubblica 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)

    Attendere che la sessione diventi ACTIVE, quindi ottenere il comando SSH.

    oci bastion session get \
        --session-id "${SESSION_ID}" \
        --query "data.{state:\"lifecycle-state\", ssh:\"ssh-metadata\".command}" 2>&1
  7. Aprire un terminale separato e avviare il tunnel SSH utilizzando il comando del passo precedente. Il tunnel inoltra la porta locale 6443 all'API Kubernetes privata.

    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: sostituire <PRIVATE_IP>, <SESSION_OCID> e <REGION> con i valori del passo precedente. Mantieni questo terminale aperto per tutta la durata della sessione. Il flag -o IdentitiesOnly=yes impedisce errori di "troppi errori di autenticazione" quando l'agente SSH contiene più chiavi caricate.

  8. Aggiornare la configurazione kubeconfig per connettersi tramite il tunnel locale.

    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: il flag --insecure-skip-tls-verify è obbligatorio perché il certificato cluster è stato emesso per l'IP dell'endpoint privato, non per 127.0.0.1. Questo è sicuro perché il traffico è crittografato attraverso il tunnel SSH.

  9. Se si utilizza un profilo CLI OCI non predefinito (ad esempio, API_KEY_AUTH), aggiornare kubeconfig per utilizzarlo. Il valore predefinito di kubeconfig generato è il profilo DEFAULT per la generazione del token.

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

    Suggerimento: i passi da 6 a 9 sono automatizzati da ./entry_point.sh tunnel, che si connette automaticamente anche se il tunnel SSH viene eliminato durante operazioni con tempi di esecuzione lunghi come l'espansione del disco. Eseguire in un terminale separato e lasciarlo in esecuzione per tutta la durata della sessione.

  10. Verificare l'accesso al cluster.

kubectl get nodes

A questo punto l'output non mostrerà nodi, poiché il pool di nodi GPU non è stato ancora aggiunto.

No resources found

Task 5: Aggiungi pool di nodi GPU

Aggiungere un pool di nodi con istanze di computazione GPU al cluster OKE.

  1. Trova l'immagine del nodo OKE più recente compatibile con GPU. OKE richiede immagini specifiche con componenti di registrazione kubelet e nodo preinstallati. Utilizzare l'API node-pool-options per trovare l'immagine corretta per la versione di 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: i filtri di query per le immagini GPU Oracle Linux 8.10 corrispondenti alla versione di Kubernetes (ad esempio, OKE-1.31.10). Se sono necessarie immagini basate su ARM, sostituire 8.10 con il filtro appropriato.

  2. Determinare il dominio di disponibilità con forme GPU. Non tutti i domini di disponibilità hanno capacità 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 capacità della GPU varia in base all'area e al dominio di disponibilità. Se la creazione del pool di nodi non riesce con un errore "Capacità host esaurita", provare a usare un dominio di disponibilità (GPU_AD_INDEX) o una forma GPU diversa oppure richiedere capacità tramite il normale processo OCI.

  3. Creare il pool di nodi GPU con un volume di avvio da 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: le etichette dei nodi app=gpu e nvidia.com/gpu=true vengono utilizzate in un secondo momento dal grafico Helm vLLM per pianificare i pod di inferenza sui nodi GPU. Il volume di avvio da 200 GB fornisce spazio per l'immagine del contenitore vLLM (~10 GB) e i pesi del modello, ma il file system deve essere espanso prima dell'uso (vedere il task 8).

  4. Attendere che i nodi GPU diventino pronti. In genere sono necessari da 5 a 10 minuti mentre il provisioning del nodo, l'avvio, l'installazione dei driver GPU e la registrazione con il cluster.

    Nota: le istanze GPU sono soggette a vincoli di capacità. Se il pool di nodi rimane in stato CREATING, controllare lo stato del nodo in OCI Console o con oci ce node-pool get. Un errore "Capacità host insufficiente" indica che non sono disponibili istanze GPU in tale dominio di disponibilità. Per risolvere questo problema, provare a utilizzare un dominio di disponibilità diverso (GPU_AD_INDEX=0 o GPU_AD_INDEX=2), provare una forma GPU diversa o richiedere una riserva di capacità tramite OCI Console o un ticket di supporto.

    kubectl get nodes -w

    Output previsto quando il nodo è pronto:

    NAME          STATUS   ROLES   AGE   VERSION
    10.0.10.x     Ready    node    5m    v1.31.10
  5. Verificare che la GPU sia stata rilevata sul nodo.

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

    Output previsto:

    NAME          GPUs
    10.0.10.x     1
  6. Applicare la patch CoreDNS per pianificare i nodi GPU. I nodi GPU OKE hanno un taint nvidia.com/gpu=present:NoSchedule. Nei cluster che dispongono solo di nodi GPU, i pod di sistema come CoreDNS non possono essere pianificati senza una tolleranza per questa caratteristica. Senza DNS, i pod non possono risolvere i nomi host esterni per scaricare i modelli.

    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"}}]'

    Verificare che CoreDNS sia in esecuzione.

    kubectl get pods -n kube-system | grep coredns

    Nota: se il cluster dispone di un pool di nodi CPU dedicato per i carichi di lavoro del sistema, questo passo non è necessario. Questa patch è necessaria solo quando i nodi GPU sono gli unici nodi 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

    Output dell'interfaccia CLI OCI che mostra cpu-pool e gpu-pool sia in stato ACTIVE

Task 6: Installa plugin dispositivo NVIDIA

Installare il plugin del dispositivo NVIDIA in modo che Kubernetes possa rilevare e pianificare i carichi di lavoro sulla GPU.

  1. Applicare il plugin del dispositivo NVIDIA DaemonSet.

    kubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.14.1/nvidia-device-plugin.yml
  2. Attendere che i pod del plugin siano pronti.

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

    Nota: alcune immagini dei nodi GPU OKE includono un plugin del dispositivo NVIDIA (nvidia-gpu-device-plugin) preinstallato. Se l'immagine lo include già, l'applicazione del valore DaemonSet a monte crea una seconda istanza che non causa conflitti. Lo script automatico (entry_point.sh deploy-vllm) lo installa sempre per garantire che il rilevamento della GPU funzioni indipendentemente dalla versione dell'immagine del nodo.

  3. Verificare che la GPU sia allocabile da Kubernetes.

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

    Output previsto:

    NAME          GPUs
    10.0.10.x     1
  4. Applicare la patch CoreDNS per tollerare i tendini dei nodi GPU. Nei cluster in cui i nodi GPU sono gli unici nodi di lavoro, i pod CoreDNS non possono essere pianificati perché i nodi GPU OKE hanno una caratteristica nvidia.com/gpu=present:NoSchedule. Senza DNS, i pod non possono risolvere i registri di immagini o gli URL di download dei modelli.

    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: questo passo è necessario solo quando i nodi GPU sono gli unici nodi di lavoro nel cluster. Se si dispone di un pool di nodi CPU dedicato per i carichi di lavoro del sistema, CoreDNS li pianifica per impostazione predefinita e questa patch non è necessaria.

Task 7: Configurare la memorizzazione

Applica il volume a blocchi OCI StorageClass per fornire storage persistente per i pesi del modello.

  1. Applicare la definizione StorageClass.

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

    Il file definisce due livelli di prestazioni:

    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 Prestazioni Caso d'uso
    oci-block-storage-enc Equilibrato (vpusPerGB: 10) Predefinito, conveniente per la maggior parte dei modelli
    oci-block-storage-hp Prestazioni elevate (vpusPerGB: 20) Caricamento del modello più rapido per modelli più grandi
  2. Verificare che StorageClasses sia disponibile.

    kubectl get storageclass

Nota: per le distribuzioni multi-nodo che richiedono storage condiviso su più pod, utilizzare il servizio NFS (File Storage Service) OCI con la modalità di accesso ReadWriteMany anziché i volumi a blocchi.

Task 8: Espandi file system nodo GPU

I volumi di avvio OCI hanno una partizione fissa di ~47 GB, indipendentemente dalla dimensione del volume di avvio specificata. La sola immagine del contenitore vLLM è di circa 10 GB e i pesi del modello richiedono spazio aggiuntivo. È necessario espandere il file system prima di distribuire vLLM per evitare le eliminazioni DiskPressure.

Nota: si tratta di un requisito specifico di OCI. Il provisioning del volume di avvio viene eseguito a 200 GB, ma per impostazione predefinita il sistema operativo esegue solo partizioni di circa 47 GB. Lo spazio rimanente deve essere richiesto manualmente.

  1. Verificare la dimensione del file system corrente nel nodo 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\":\"/\"}}]}}"

    L'output mostrerà circa 47 GB totali, confermando che è necessaria l'espansione.

  2. Creare un pod con privilegi sul nodo GPU per eseguire i comandi di espansione.

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

    Attendere l'avvio del pod.

    kubectl wait --for=condition=Ready pod/expand-disk --timeout=60s
  3. Eseguire tutti e quattro i passi di espansione in un singolo comando kubectl exec. Eseguirli insieme evita il rischio che kubectl exec restituisca il codice di uscita 137 (SIGKILL) tra i passaggi, che può verificarsi durante l'I/O su disco pesante sull'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 /
    '
    A punti Comando Scopo
    1 growpart /dev/sda 3 Espandere la partizione 3 per utilizzare il disco completo
    2 pvresize /dev/sda3 Ridimensiona volume fisico LVM
    3 lvextend -l +100%FREE /dev/ocivolume/root Estendi volume logico
    4 xfs_growfs / Fai crescere il file system XFS per riempire il volume

    Nota: tutte e quattro le operazioni sono idempotenti. Se l'esec restituisce il codice di uscita 137, è possibile rieseguire l'intero blocco in tutta sicurezza. Cercare EXPANSION_COMPLETE nell'output per confermare il successo.

  4. Riavviare il kubelet in modo che il nodo riporti lo storage allocabile aggiornato, quindi verificare ed eseguire il cleanup.

    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: il comando nsenter immette lo spazio di nomi PID dell'host per accedere a systemd. Un chroot /host systemctl restart kubelet normale non riesce perché non è in grado di connettersi al bus systemd da una chroot.

    L'output previsto dovrebbe mostrare circa 189 GB in totale.

Task 9: Distribuire lo stack di produzione vLLM

Installare lo stack di inferenza vLLM utilizzando Helm.

  1. Aggiungere il repository Helm vLLM.

    helm repo add vllm https://vllm-project.github.io/production-stack
    helm repo update
  2. Rivedere il file dei valori Helm. production_stack_specification.yaml configura il modello, le risorse e lo storage per 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: il modello openai/gpt-oss-20b è un modello Mixture of Experts (MoE) con parametri totali 20B e parametri attivi 3.6B per passaggio in avanti. Viene rilasciato sotto la licenza Apache 2.0, quindi non è richiesto alcun token Hugging Face. L'immagine contenitore vllm/vllm-openai fornisce un server API compatibile con OpenAI che consente ai client di utilizzare chiamate SDK OpenAI standard per l'endpoint self-hosted.

  3. Distribuire lo stack. Non utilizzare --wait qui perché il pod del router CrashLoop finché non vengono applicate le patch nel passo successivo.

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

    Attendere l'avvio del pod del motore vLLM (il router verrà aggiornato successivamente).

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

    Nota: il pod del motore impiega diversi minuti per diventare pronto perché scarica i pesi del modello al primo avvio. Se il pod rimane in ContainerCreating, l'immagine del contenitore (~10 GB) viene ancora estratta. Utilizzare kubectl describe pod <pod-name> per controllare l'avanzamento.

  4. Applicare le patch all'implementazione del router. Il router ha bisogno di una tolleranza GPU (in modo che possa pianificare quando i nodi GPU sono gli unici nodi con capacità) e limiti di memoria aumentati (il valore predefinito 500 Mi può causare 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 tolleranza GPU è necessaria perché i nodi GPU OKE dispongono di un taint nvidia.com/gpu=present:NoSchedule che impedisce la pianificazione dei carichi di lavoro non GPU. Poiché il router non utilizza una GPU ma deve essere eseguito da qualche parte, questa tolleranza consente di pianificare sui nodi GPU. Nei cluster con pool di nodi CPU dedicati, questa tolleranza non è necessaria.

  5. Confermare la distribuzione della release Helm.

    helm list

    Output del terminale della lista di helm che mostra la distribuzione vllm-stack

  6. Verificare che i pod siano in esecuzione.

    kubectl get pods

    Output previsto:

    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 ottiene pod che mostrano router e pod motore entrambi in esecuzione 1/1

  7. Controllare l'avanzamento del caricamento del modello nei pod log.

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

    Attendere che venga visualizzato un messaggio che indica che il modello è stato caricato e che il server è pronto ad accettare le richieste.

Task 10: Test dell'endpoint inferenziale

Verificare che la distribuzione stia fornendo richieste di inferenza. Lo stack di produzione vLLM espone un'API compatibile con OpenAI tramite il servizio router, in modo che qualsiasi client SDK OpenAI o comando curl possa interagire con esso.

Il seguente diagramma mostra il ciclo di vita della richiesta di inferenza: dalla richiesta del client alla logica di selezione del motore del router, alle fasi di precompilazione e decodifica del motore vLLM e viceversa come risposta in streaming.

Diagramma di sequenza che mostra il ciclo di vita delle richieste di inferenza dal client al router al motore vLLM e alla GPU

  1. Elencare i modelli disponibili per verificare che la distribuzione sia in buono stato.

    kubectl get svc vllm-router-service

    Il servizio router fornisce il gateway API a tutti i modelli distribuiti. Poiché il cluster utilizza un endpoint privato, è possibile accedere al servizio tramite kubectl port-forward.

  2. Avviare un port-forward dal computer locale al servizio router. Aprire un nuovo terminale (mantenere il tunnel SSH in esecuzione nell'altro) ed eseguire:

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

    Questo mappa localhost:8080 sul computer alla porta 80 sul servizio router all'interno del cluster.

    Nota di sicurezza: kubectl port-forward si associa a livello locale e non espone il servizio pubblicamente. Questo è il modo più sicuro per testare quando si utilizza un cluster privato su un tunnel bastion.

    Nota: il comando port-forward viene eseguito in primo piano. Tenere questo terminale aperto durante il test. Al termine, premere Ctrl+C per interromperlo.

  3. In un altro terminale, verificare che il modello sia disponibile eseguendo una query sull'endpoint dei modelli.

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

    Output previsto:

    {
        "object": "list",
        "data": [
            {
                "id": "openai/gpt-oss-20b",
                "object": "model",
                "created": 1234567890,
                "owned_by": "vllm"
            }
        ]
    }
  4. Invia una richiesta di completamento testo.

    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

    Uscita prevista (abbreviato):

    {
        "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. Invia una richiesta di completamento della chat. Questo è lo stesso formato utilizzato dall'SDK Python OpenAI ed è il modo più comune per interagire con i LLM a livello di programmazione.

    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

    Uscita prevista (abbreviato):

    {
        "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
        }
    }

    Entrambe le risposte includono il testo generato dal modello nell'array choices, le statistiche sull'uso del token e un finish_reason di stop (il modello è stato completato in modo naturale) o length (l'output è stato troncato in max_tokens).

    Nota: il nome del modello nella richiesta API è il percorso completo del modello (openai/gpt-oss-20b), che corrisponde al campo modelURL nei valori Helm. Qualsiasi client compatibile con OpenAI può utilizzare questo endpoint impostando base_url su http://localhost:8080/v1.

    Output del terminale che mostra la richiesta di completamento della chat corrente e la risposta JSON da openai/gpt-oss-20b

  6. Misura la latenza end-to-end per una breve richiesta.

    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

    Su un'unica GPU A10, prevedi 1-3 secondi end-to-end per completamenti brevi. Il time to first token (TTFT) è in genere di 50-200 ms a seconda della lunghezza del prompt. Per un throughput più elevato, aumentare replicaCount nei valori di Helm per aggiungere più repliche del motore dietro il router.

Task 11: Esponi tramite OCI Load Balancer (facoltativo)

Rendi l'endpoint di inferenza accessibile esternamente tramite un load balancer OCI.

Nota di sicurezza: espone l'API di inferenza alla rete Internet pubblica per impostazione predefinita. Non abilitare questa opzione in produzione senza controlli TLS, autenticazione (chiave API/JWT/mTLS) e lista di inclusione IP o WAF. Se è necessario esporlo, affrontarlo con un controller in entrata o un gateway API che applica i limiti di autenticazione e frequenza o utilizzare un load balancer interno.

  1. Applicare una patch al servizio del router per usare un tipo LoadBalancer.

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

    Nota: se si desidera un load balancer interno, aggiungere l'annotazione OCI al servizio (esempio riportato di seguito). In questo modo, l'endpoint rimane privato all'interno della VCN.

    kubectl annotate svc vllm-router-service \
      "service.beta.kubernetes.io/oci-load-balancer-internal"="true"
  2. Attendere l'assegnazione dell'IP esterno.

    kubectl get svc vllm-router-service -w

    Output previsto dopo il provisioning del load balancer:

    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 ottiene l'output svc che mostra i servizi del router e del motore

  3. Eseguire il test dell'endpoint esterno.

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

Task 12: Configurare il parallelismo dei sensori multi-GPU (avanzato)

Distribuisci modelli più grandi su più GPU su forme Bare Metal.

Il parallelismo di trazione divide un modello in più GPU su un singolo nodo. Ciò è necessario quando i requisiti di memoria di un modello superano una singola GPU. Ad esempio, Meta Llama 3.1 70B richiede circa 140 GB di memoria GPU, che supera la capacità di qualsiasi singola GPU, ma si adatta alle GPU 8x A100 80 GB o 8x H100.

  1. Crea un segreto Kubernetes con il token Hugging Face. I modelli con accesso controllato come Llama 3.1 70B richiedono l'autenticazione.

    kubectl create secret generic hf-token-secret \
      --from-literal=token=YOUR_HUGGINGFACE_TOKEN
  2. Aggiornare production_stack_specification.yaml con una configurazione multi-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. Distribuisci con i valori aggiornati. Come per il task 9, non utilizzare --wait. Il router sarà CrashLoop fino a quando non verrà applicata la patch.

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

    Quindi applicare le patch al router (come il task 9, punto 4) e verificare:

    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. Verificare che il pod sia in esecuzione e che tutte le GPU siano in uso.

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

Nota: prima di distribuire una configurazione con più GPU, assicurarsi che il cluster OKE disponga di un pool di nodi con la forma GPU Bare Metal appropriata (ad esempio, BM.GPU.H100.8).

Task 13: Uso dello storage degli oggetti OCI per i modelli (avanzato)

Carica i pesi dei modelli dallo storage degli oggetti OCI invece di scaricarli da Hugging Face. Questa funzione è utile per i modelli privati, per i download più rapidi all'interno di OCI o per gli ambienti senza accesso a Internet esterno.

  1. Carica i pesi del modello in un bucket di storage degli oggetti OCI. Passare a Memorizzazione > Storage degli oggetti in OCI Console e creare un bucket se non ne è già disponibile uno.

  2. Creare un URL PAR (Pre-Authenticated Request) per il bucket. In OCI Console, selezionare il bucket, fare clic su Richieste preautenticate e creare una nuova richiesta con accesso in lettura.

  3. Aggiornare production_stack_specification.yaml per utilizzare l'URL 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. Distribuire con i valori aggiornati (senza --wait). Vedere il task 9 per il motivo).

    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. Verificare i caricamenti del modello dallo storage degli oggetti controllando i pod log.

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

Task 14: Cleanup delle risorse

Rimuovere tutte le risorse distribuite per evitare addebiti continui.

  1. Rimuovere le risorse Kubernetes utilizzando lo script di cleanup.

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

    In questo modo viene disinstallata la release Helm, vengono eliminate tutte le risorse PersistentVolumeClaims, PersistentVolumes e vLLM personalizzate.

  2. Eliminare il cluster OKE e tutte le risorse di rete OCI.

    ./entry_point.sh cleanup

    In questo modo vengono eliminate le risorse seguenti in ordine:

    • Pool di nodi GPU
    • Cluster OKE
    • Host bastion (se creato)
    • Subnet (API, worker, load balancer, bastion)
    • Liste di sicurezza
    • Gateway del servizio, gateway NAT e gateway Internet
    • Tabelle di instradamento
    • VCN
  3. Verificare che tutte le risorse siano state rimosse nella console OCI in Servizi per sviluppatori > Cluster Kubernetes e Networking > Reti cloud virtuali.

    ./entry_point.sh cleanup

    Output del terminale del cleanup entry_point.sh che mostra il teardown completo delle risorse

Nota: assicurarsi che tutte le risorse vengano eliminate per evitare addebiti continui. Le istanze GPU e i volumi a blocchi comportano costi anche in caso di inattività.

Operazione successiva

Questa esercitazione ha distribuito uno stack di inferenza funzionale. Per i carichi di lavoro di produzione, considerare i seguenti miglioramenti:

Conferme

Altre risorse di apprendimento

Esplora altri laboratori su docs.oracle.com/learn o accedi a più contenuti di formazione gratuiti sul canale YouTube di Oracle Learning. Inoltre, visitare education.oracle.com/learning-explorer per diventare Oracle Learning Explorer.

Per la documentazione del prodotto, visitare Oracle Help Center.