Nota
- Questa esercitazione richiede l'accesso a Oracle Cloud. Per iscriversi a un account gratuito, consulta Inizia a utilizzare Oracle Cloud Infrastructure Free Tier.
- Utilizza valori di esempio per le credenziali, la tenancy e i compartimenti di Oracle Cloud Infrastructure. Quando completi il tuo laboratorio, sostituisci questi valori con quelli specifici del tuo ambiente cloud.
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.

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:
- Distribuire un cluster OKE con pool di nodi abilitati per GPU utilizzando OCI CLI
- Configura il networking OCI (VCN, subnet e gateway) per i carichi di lavoro Kubernetes
- Installare e configurare il plugin del dispositivo NVIDIA per la pianificazione della GPU
- Espandere il file system del nodo GPU per utilizzare la capacità completa del volume di avvio
- Distribuire lo stack di produzione vLLM con un endpoint di inferenza compatibile con OpenAI
- Test dell'inferenza LLM con richieste API rispetto al modello GPT-OSS-20B OpenAI
- Configurare il parallelismo del tensore multi-GPU per modelli più grandi su forme Bare Metal
- Usa OCI Object Storage come origine modello alternativa
- Esegui il cleanup di tutte le risorse OCI per evitare addebiti continui
Prerequisiti
- Un account Oracle Cloud Infrastructure con quanto segue:
- Compartimento con le autorizzazioni per creare e gestire i cluster OKE
- Quota di computazione GPU per la forma desiderata (ad esempio,
VM.GPU.A10.1oBM.GPU.A100-v2.8). Se non si dispone di una quota GPU, richiederla tramite un ticket di supporto - Autorizzazioni per creare VCN, subnet, gateway Internet e load balancer
- Accesso ai volumi a blocchi OCI per lo storage persistente
- Accesso allo storage degli oggetti OCI (per la sezione di caricamento del modello avanzato)
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.
-
OCI CLI installato e configurato con
oci setup config -
jq installato per l'analisi JSON
-
kubectl installato
-
Helm installato
-
Coppia di chiavi SSH (ad esempio,
~/.ssh/id_rsae~/.ssh/id_rsa.pub) per l'accesso al bastion. Generarne uno conssh-keygen -t rsa -b 4096se necessario -
Familiarità con i concetti di Kubernetes (pod, servizi, distribuzioni, pool di nodi)
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.
-
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
-
Esportare la variabile di ambiente richiesta.
export OCI_COMPARTMENT_ID="ocid1.compartment.oc1..xxxxx" -
Facoltativamente, eseguire l'override della configurazione predefinita impostando una qualsiasi delle variabili di ambiente indicate di seguito.
Variabile Predefinita Descrizione OCI_REGIONus-ashburn-1Area OCI per la distribuzione OCI_PROFILEDEFAULTProfilo di configurazione OCI CLI CLUSTER_NAMEproduction-stackNome del cluster OKE GPU_SHAPEVM.GPU.A10.1Forma di computazione GPU per il pool di nodi GPU_NODE_COUNT1Numero di nodi GPU nel pool GPU_BOOT_VOLUME_GB200Dimensione del volumi di avvio in GB per i nodi GPU CPU_BOOT_VOLUME_GB100Dimensione del volumi di avvio in GB per i nodi CPU GPU_AD_INDEX1Indice del dominio di disponibilità (basato su 0) per il posizionamento delle GPU PRIVATE_CLUSTERtrueImpostare su falseper un endpoint API Kubernetes pubblicoKUBERNETES_VERSIONv1.31.10Versione 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" -
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.11 NVIDIA A10 24 GB Modelli di parametri 7B–13B VM.GPU.A10.22 NVIDIA A10 48 GB Tensore parallelo con piccoli modelli BM.GPU4.88 NVIDIA A100 40 GB 320 GB Modelli 70B a costi contenuti BM.GPU.A100-v2.88 NVIDIA A100 80 GB 640 GB 70B+ modelli di parametri BM.GPU.H100.88 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 distribuireopenai/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 utilizzanoBM.GPU.H100.8per 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.
-
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 -
Esportare l'OCID compartimento.
export OCI_COMPARTMENT_ID="ocid1.compartment.oc1..xxxxx" -
Eseguire lo script di distribuzione.
./entry_point.sh setup
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.yamlPer 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 -
Verificare che la distribuzione sia in esecuzione.
kubectl get podsOutput 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 gete cercare gli errori "Capacità host esaurita". Per risolvere questo problema, eseguire il cleanup con./entry_point.sh cleanupe la ridistribuzione con un dominio di disponibilità diverso (ad esempio,GPU_AD_INDEX=0oGPU_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 cleanupquando 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.
-
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) -
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) -
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) -
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) -
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.
-
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/0precedenti con10.0.0.0/16(o il CIDR della VCN). Uso: collegaBASTION_SL_IDalla subnet del bastion,WORKER_SL_IDalle subnet API/worker eLB_SL_IDalla 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. -
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/28Privata Server API Kubernetes Nodi di lavoro 10.0.10.0/24Privata Nodi di computazione GPU Load balancer 10.0.20.0/24Pubblica Accesso al servizio esterno Bastion 10.0.30.0/24Pubblica 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.
-
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 falseIl 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
kubectlrichiede un tunnel SSH tramite il bastion (configurato nei passi successivi). -
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-outputEseguire 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
kubectl get nodes -o wide
-
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/32con 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-outputNota di sicurezza: per la produzione, non utilizzare
0.0.0.0/0. Limitare--client-cidr-listall'IP pubblico o al CIDR aziendale (ad esempio,"YOUR_PUBLIC_IP/32"), altrimenti qualsiasi utente su Internet può tentare una sessione bastion. -
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 -
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}" -
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 -
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.comNota: 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=yesimpedisce errori di "troppi errori di autenticazione" quando l'agente SSH contiene più chiavi caricate. -
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=trueNota: il flag
--insecure-skip-tls-verifyè obbligatorio perché il certificato cluster è stato emesso per l'IP dell'endpoint privato, non per127.0.0.1. Questo è sicuro perché il traffico è crittografato attraverso il tunnel SSH. -
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 profiloDEFAULTper 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. -
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.
-
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-optionsper 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, sostituire8.10con il filtro appropriato. -
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. -
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=gpuenvidia.com/gpu=truevengono 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). -
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=0oGPU_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 -wOutput previsto quando il nodo è pronto:
NAME STATUS ROLES AGE VERSION 10.0.10.x Ready node 5m v1.31.10 -
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 -
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 corednsNota: 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
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.
-
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 -
Attendere che i pod del plugin siano pronti.
kubectl wait --for=condition=Ready pods -l name=nvidia-device-plugin-ds -n kube-system --timeout=300sNota: 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. -
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 -
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=60sNota: 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.
-
Applicare la definizione StorageClass.
kubectl apply -f oci-block-storage-sc.yamlIl 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: trueStorageClass Prestazioni Caso d'uso oci-block-storage-encEquilibrato ( vpusPerGB: 10)Predefinito, conveniente per la maggior parte dei modelli oci-block-storage-hpPrestazioni elevate ( vpusPerGB: 20)Caricamento del modello più rapido per modelli più grandi -
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
ReadWriteManyanziché 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.
-
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.
-
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 -
Eseguire tutti e quattro i passi di espansione in un singolo comando
kubectl exec. Eseguirli insieme evita il rischio chekubectl execrestituisca 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 3Espandere la partizione 3 per utilizzare il disco completo 2 pvresize /dev/sda3Ridimensiona volume fisico LVM 3 lvextend -l +100%FREE /dev/ocivolume/rootEstendi 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_COMPLETEnell'output per confermare il successo. -
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 --forceNota: il comando
nsenterimmette lo spazio di nomi PID dell'host per accedere a systemd. Unchroot /host systemctl restart kubeletnormale 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.
-
Aggiungere il repository Helm vLLM.
helm repo add vllm https://vllm-project.github.io/production-stack helm repo update -
Rivedere il file dei valori Helm.
production_stack_specification.yamlconfigura 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 contenitorevllm/vllm-openaifornisce un server API compatibile con OpenAI che consente ai client di utilizzare chiamate SDK OpenAI standard per l'endpoint self-hosted. -
Distribuire lo stack. Non utilizzare
--waitqui 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.yamlAttendere l'avvio del pod del motore vLLM (il router verrà aggiornato successivamente).
kubectl wait --for=condition=Ready pods -l model=gpt-oss --timeout=600sNota: 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. Utilizzarekubectl describe pod <pod-name>per controllare l'avanzamento. -
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:NoScheduleche 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. -
Confermare la distribuzione della release Helm.
helm list
-
Verificare che i pod siano in esecuzione.
kubectl get podsOutput 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 5mkubectl get pods
-
Controllare l'avanzamento del caricamento del modello nei pod log.
kubectl logs -f deployment/vllm-gpt-oss-deployment-vllmAttendere 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.

-
Elencare i modelli disponibili per verificare che la distribuzione sia in buono stato.
kubectl get svc vllm-router-serviceIl 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. -
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:80Questo mappa
localhost:8080sul computer alla porta 80 sul servizio router all'interno del cluster.Nota di sicurezza:
kubectl port-forwardsi 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.
-
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.toolOutput previsto:
{ "object": "list", "data": [ { "id": "openai/gpt-oss-20b", "object": "model", "created": 1234567890, "owned_by": "vllm" } ] } -
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.toolUscita 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 } } -
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.toolUscita 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 unfinish_reasondistop(il modello è stato completato in modo naturale) olength(l'output è stato troncato inmax_tokens).Nota: il nome del modello nella richiesta API è il percorso completo del modello (
openai/gpt-oss-20b), che corrisponde al campomodelURLnei valori Helm. Qualsiasi client compatibile con OpenAI può utilizzare questo endpoint impostandobase_urlsuhttp://localhost:8080/v1.
-
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/nullSu 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
replicaCountnei 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.
-
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" -
Attendere l'assegnazione dell'IP esterno.
kubectl get svc vllm-router-service -wOutput 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/TCPkubectl get svc
-
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.
-
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 -
Aggiornare
production_stack_specification.yamlcon 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" -
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=900sQuindi 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"} ]' -
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.
-
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.
-
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.
-
Aggiornare
production_stack_specification.yamlper 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" -
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 -
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.
-
Rimuovere le risorse Kubernetes utilizzando lo script di cleanup.
cd production-stack/deployment_on_cloud/oci ./clean_up.shIn questo modo viene disinstallata la release Helm, vengono eliminate tutte le risorse PersistentVolumeClaims, PersistentVolumes e vLLM personalizzate.
-
Eliminare il cluster OKE e tutte le risorse di rete OCI.
./entry_point.sh cleanupIn 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
-
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
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:
- Monitoraggio: vLLM espone le metriche Prometheus all'indirizzo
/metricssu ogni pod del motore. Connetti uno stack Prometheus + Grafana per tenere traccia del tempo al primo token, ai token al secondo, all'utilizzo della cache KV e alla profondità della coda di richieste. - Ridimensionamento automatico: configura Kubernetes Horizontal Pod Autoscaler con metriche personalizzate (profondità della coda di richieste o utilizzo della GPU tramite DCGM) per ridimensionare automaticamente le repliche del motore sotto carico.
- Protezione avanzata: limita
BASTION_CLIENT_CIDRall'intervallo IP, aggiunge criteri di rete Kubernetes per isolare lo spazio di nomi dell'inferenza e memorizza le credenziali del modello in OCI Vault anziché nei segreti Kubernetes. - Alta disponibilità: distribuisce i nodi GPU in più domini di disponibilità e aumenta il valore
replicaCountnei valori Helm in modo che il router possa eseguire il failover tra le repliche del motore. - Ottimizzazione dei costi: utilizzare
./entry_point.sh cleanupquando non è in uso. Per i carichi di lavoro di sviluppo/test, considera le istanze GPU prerilasciabili. Monitora l'utilizzo della GPU per ridimensionare correttamente il pool di nodi. - Infrastructure as Code: modifica questa distribuzione con il provider Terraform OCI o OCI Resource Manager per un'infrastruttura ripetibile e controllata dalle versioni.
Collegamenti correlati
Conferme
- Autore - Federico Kamelhar (Senior Principal Architect, Agentic AI)
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.
Deploy OpenAI vLLM Production Stack on Oracle Kubernetes Engine (OKE)
G50929-01