Hinweis:

OpenAI vLLM-Produktionsstack auf Oracle Kubernetes Engine (OKE) bereitstellen

Einführung

Unternehmen, die große Sprachmodelle (LLMs) für Produktions-Workloads einsetzen, stehen vor einer kritischen Infrastrukturentscheidung: Verlassen Sie sich auf Inferenz-APIs von Drittanbietern oder stellen Sie einen selbst gehosteten Inferenzstack bereit. Selbst gehostete Bereitstellungen bieten erhebliche Vorteile: vollständige Datenschutz- und Compliance-Kontrolle, Inferenzlatenz von unter 100 Millisekunden, indem Netzwerk-Roundtrips eliminiert werden, vorhersehbare Kosten in großem Maßstab und die Freiheit, jedes Open-Source-Modell ohne Anbieterbindung zu optimieren und zu bedienen.

Die Erstellung eines LLM-Inferenzstacks in Produktionsqualität ist jedoch komplex. Sie erfordert eine GPU-fähige Containerorchestrierung, intelligentes Anforderungsrouting über mehrere Modellreplikate hinweg, persistenten Speicher für große Modellgewichtungen und kontinuierliche Überwachung – alles integriert und zuverlässig ausgeführt.

Oracle Cloud Infrastructure bietet mehrere Pfade für KI-Inferenz. Der OCI Generative AI-Service bietet eine vollständig verwaltete Erfahrung mit dedizierten KI-Clustern, die in Ihrem Mandanten isoliert sind. Ideal für Teams, die schnell mit unterstützten Modellen beginnen möchten. Dieses Tutorial verfolgt den alternativen Ansatz: Bereitstellung eines eigenen Inferenzstacks auf OKE. Dieser Pfad richtet sich an Teams, die präzise Kontrolle über GPU-Treiber, CUDA-Versionen, Modellkonfigurationen und Serviceparameter benötigen, oder an Teams, die benutzerdefinierte Modelle trainieren und optimieren und direkt bedienen möchten. OCI bietet Bare-Metal-GPU-Instanzen mit NVIDIA-GPUs A10, A100 und H100, die über RDMA-Clusternetzwerke mit extrem geringer Latenz verbunden sind. So erhalten Sie die gleiche Hardwarekontrolle wie On-Premises, während Sie von der Cloud-Elastizität profitieren.

Der vLLM-Produktionsstack löst die Komplexität der selbst gehosteten Inferenz, indem er eine Open-Source-Kubernetes-native Plattform bereitstellt, die auf vLLM basiert, der Inferenz-Engine mit hohem Durchsatz, die in der Produktion von Organisationen wie Meta, Mistral AI und IBM verwendet wird. Es bietet einen bis zu 24x höheren Durchsatz im Vergleich zu Standard-Service-Frameworks durch effiziente GPU-Speicherverwaltung und KV-Cacheoptimierung. In Kombination mit OKE- und OCI-GPU-Ausprägungen erhalten Sie eine produktionsfähige Inferenzplattform mit Netzwerken, Speicher und Sicherheit der Unternehmensklasse. Die in diesem Tutorial verwendeten OCI-Deployment-Skripte werden im offiziellen vLLM-Produktions-Stack-Repository beigetragen und verwaltet.

In diesem Tutorial erfahren Sie, wie Sie den vLLM-Produktionsstack auf OKE bereitstellen, vom Infrastruktur-Provisioning bis zur Ausführung Ihrer ersten Inferenzanforderung.

Hinweis: In diesem Tutorial werden Ressourcen Schritt für Schritt mit der OCI-CLI bereitgestellt, damit Sie den vollständigen Ablauf der OCI-Cloud-Ressourcen verstehen, die für ein GPU-Inferenz-Deployment erforderlich sind. Für Produktionsumgebungen wird empfohlen, diese Infrastruktur mit Terraform oder OCI Resource Manager (Shepherd) für wiederholbare, versionsgesteuerte Deployments zu kodifizieren.

Architekturdiagramm mit VCN mit Subnetzen, OKE-Cluster, GPU-Knotenpool, vLLM-Pods und Load Balancer

In diesem Tutorial werden die folgenden OCI-Services verwendet:

Service Zweck
Oracle Kubernetes-Engine (OKE) Verwaltetes Kubernetes-Cluster für Containerorchestrierung und GPU-Workload-Planung
OCI Compute (GPU-Ausprägungen) NVIDIA A10-(24 GB-) und A100-(80 GB-)GPU-Instanzen für Modellinferenz
OCI-Block-Volumes Persistenter Speicher für Modellgewichte mit konfigurierbaren Performance-Tiers
Virtuelles OCI-Cloud-Netzwerk (VCN) Netzwerkinfrastruktur, einschließlich Subnetze, Gateways und Sicherheitslisten
OCI Load Balancer Externer Zugriff auf Inferenzendpunkte
OCI Bastion Verwaltete SSH-Tunnel für den Zugriff auf private Cluster
OCI Object Storage Alternative Modellquelle mit Pre-Authenticated Request-(PAR-)URLs

Ziele

In diesem Tutorial führen Sie folgende Schritte aus:

Voraussetzungen

Hinweis: In den Beispielausgaben und Screenshots in diesem Tutorial wird us-chicago-1 verwendet. Sie können das Deployment in jeder unterstützten Region durchführen, indem Sie OCI_REGION festlegen. Die GPU-Kapazität variiert je nach Region und Availability-Domain. Stellen Sie daher sicher, dass Ihre Ziel-GPU-Ausprägung verfügbar ist, bevor Sie sie bereitstellen. Prüfen Sie die Verfügbarkeit der GPU-Ausprägung nach Region, und testen Sie eine andere Availability-Domain (GPU_AD_INDEX), wenn Sie Kapazitätsfehler feststellen.

Hinweis: In diesem Tutorial werden bezahlte GPU-Ressourcen bereitgestellt (z.B. VM.GPU.A10.1). Es handelt sich nicht um eine Workload vom Typ "OCI Always Free". Führen Sie die Bereinigungsschritte immer aus, wenn Sie fertig sind, um laufende Gebühren zu vermeiden.

Hinweis: In diesem Tutorial wird openai/gpt-oss-20b bereitgestellt, ein lizenziertes Apache 2.0-Modell aus OpenAI. Es ist kein Hugging Face-Token erforderlich. Wenn Sie Gated-Modelle wie Meta Llama 3.1 bereitstellen möchten, benötigen Sie einen Hugging Face-Account mit einem API-Token.

Aufgabe 1: Umgebungsvariablen konfigurieren

Legen Sie die erforderliche OCI-Konfiguration fest, bevor Sie die Infrastruktur bereitstellen.

  1. Suchen Sie die Compartment-OCID in der OCI-Konsole. Navigieren Sie zu Identität und Sicherheit > Compartments, klicken Sie auf das Ziel-Compartment, und kopieren Sie die OCID.

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

    OCI-CLI-Ausgabe mit Compartment-OCID

  2. Exportieren Sie die erforderliche Umgebungsvariable.

    export OCI_COMPARTMENT_ID="ocid1.compartment.oc1..xxxxx"
  3. Setzen Sie optional die Standardkonfiguration außer Kraft, indem Sie eine der folgenden Umgebungsvariablen festlegen.

    Variable Standard Beschreibung
    OCI_REGION us-ashburn-1 OCI-Region für das Deployment
    OCI_PROFILE DEFAULT OCI-CLI-Konfigurationsprofil
    CLUSTER_NAME production-stack Name der OKE-Cluster
    GPU_SHAPE VM.GPU.A10.1 GPU-Compute-Ausprägung für den Knotenpool
    GPU_NODE_COUNT 1 Anzahl der GPU-Knoten im Pool
    GPU_BOOT_VOLUME_GB 200 Boot-Volume-Größe in GB für GPU-Knoten
    CPU_BOOT_VOLUME_GB 100 Boot-Volume-Größe in GB für CPU-Knoten
    GPU_AD_INDEX 1 Availability-Domainindex (0-basiert) für GPU-Platzierung
    PRIVATE_CLUSTER true Für einen öffentlichen Kubernetes-API-Endpunkt auf false setzen
    KUBERNETES_VERSION v1.31.10 Kubernetes-Version für das OKE-Cluster

    Beispiel: Für das Deployment mit zwei A100-GPU-Knoten:

    export OCI_COMPARTMENT_ID="ocid1.compartment.oc1..xxxxx"
    export GPU_SHAPE="BM.GPU.A100-v2.8"
    export GPU_NODE_COUNT="2"
  4. Prüfen Sie die verfügbaren GPU-Ausprägungen, und wählen Sie je nach Modellgrößenanforderungen eine aus.

    Form GPUs GPU-Typ GPU-Arbeitsspeicher Empfohlen Für
    VM.GPU.A10.1 1 NVIDIA A10 24 GB 7B–13B-Parametermodelle
    VM.GPU.A10.2 2 NVIDIA A10 48 GB Tensor parallel zu kleinen Modellen
    BM.GPU4.8 8 NVIDIA A100 40 GB 320 GB 70B Modelle, kostengünstig
    BM.GPU.A100-v2.8 8 NVIDIA A100 80 GB 640 GB 70B+ Parametermodelle
    BM.GPU.H100.8 8 NVIDIA H100 640 GB Größte Modelle, RDMA-Unterstützung

    Hinweis: Bare-Metal-Ausprägungen (BM.*) stellen dedizierte Hardware ohne Virtualisierungsoverhead bereit und unterstützen MultigPU-Tensorparallelität. Virtual-Machine-Ausprägungen (VM.*) sind für kleinere Modelle kostengünstiger.

    Hinweis: In diesem Tutorial wird VM.GPU.A10.1 (einzelne NVIDIA A10 mit 24 GB GPU-Speicher) verwendet, um openai/gpt-oss-20b bereitzustellen, ein Modell von Mixture of Experts (MoE) mit aktiven 3.6B-Parametern, das normalerweise zu einer einzelnen A10-GPU passt. Die erweiterten Abschnitte zeigen Multi-GPU-Konfigurationen, die BM.GPU.H100.8 für größere Modelle wie Llama 3.1 70B verwenden.

Aufgabe 2: Mit dem automatisierten Skript bereitstellen (Schnellstart)

Der vLLM-Produktionsstack enthält ein automatisiertes Deployment-Skript, das alle OCI-Ressourcen bereitstellt und den Inferenzstack mit einem einzigen Befehl bereitstellt. Verwenden Sie diesen Ansatz für ein schnelles Deployment. Die Aufgaben 3 bis 10 decken jeden Schritt einzeln für Benutzer ab, die den Prozess anpassen möchten.

  1. Klonen Sie das vLLM-Produktionsstack-Repository.

    git clone https://github.com/vllm-project/production-stack.git
    cd production-stack/deployment_on_cloud/oci
  2. Exportieren Sie die Compartment-OCID.

    export OCI_COMPARTMENT_ID="ocid1.compartment.oc1..xxxxx"
  3. Führen Sie das Deployment-Skript aus.

    ./entry_point.sh setup

    Terminalausgabe des entry_point.sh-Setups mit VCN-, Cluster-, Bastion- und Knotenpoolerstellung

    Bei öffentlichen Clustern (PRIVATE_CLUSTER=false) erstellt das Setup die gesamte Infrastruktur und stellt den vLLM-Stack mit einem einzigen Befehl bereit. Übergeben Sie die Helm-Wertedatei als zweites Argument:

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

    Bei privaten Clustern (Standard) erstellt das Setup die Infrastruktur, kann die Kubernetes-API jedoch nicht direkt erreichen. Öffnen Sie ein separates Terminal, und starten Sie den Tunnel. Stellen Sie dann Folgendes bereit:

    # 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. Prüfen Sie, ob das Deployment ausgeführt wird.

    kubectl get pods

    Erwartete Ausgaben:

    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

Hinweis: Wenn beide Pods den Status Running anzeigen, ist Ihr Deployment bereit. Fahren Sie mit Aufgabe 10: Inferenzendpunkt testen fort.

Hinweis: GPU-Instanzen unterliegen OCI-Kapazitätsbeschränkungen. Wenn das Skript länger als 15 Minuten in der Schleife "Warten auf GPU-Knoten" bleibt, ist die GPU-Ausprägung möglicherweise nicht in der ausgewählten Availability-Domain verfügbar. Prüfen Sie den Knotenpoolstatus mit oci ce node-pool get, und suchen Sie nach Fehlern bei "Nicht genügend Hostkapazität". Bereinigen Sie das Problem mit ./entry_point.sh cleanup, und stellen Sie es erneut mit einer anderen Availability-Domain (z.B. GPU_AD_INDEX=0 oder GPU_AD_INDEX=2) oder einer anderen GPU-Ausprägung (z.B. GPU_SHAPE=VM.GPU.A10.2) bereit.

Hinweis: Das Deployment-Skript verwendet GPU-Instanzen, für die erhebliche Kosten anfallen (ca. 50 US-Dollar pro Tag für eine einzelne A10-GPU). Führen Sie immer ./entry_point.sh cleanup aus, wenn Sie fertig sind, um laufende Gebühren zu vermeiden.

Aufgabe 3: VCN und Networking erstellen

Erstellen Sie die für das OKE-Cluster erforderliche OCI-Netzwerkinfrastruktur. Dazu gehören ein virtuelles Cloud-Netzwerk (VCN), Gateways, Routentabellen, Sicherheitslisten und Subnetze. Jede Netzwerkressource wird in wenigen Sekunden erstellt. Der vollständige Befehlssatz wird in weniger als 2 Minuten abgeschlossen.

  1. Erstellen Sie ein VCN mit einem 10.0.0.0/16-CIDR-Block.

    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. Internetgateway für öffentliches Subnetzrouting erstellen.

    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. Erstellen Sie ein NAT-Gateway für ausgehenden Datenverkehr aus privaten Subnetzen.

    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. Servicegateway für den Zugriff auf Oracle Services Network erstellen. Der OKE-Cloud-Controller initialisiert Worker-Knoten mit Oracle Services (Festlegen von Availability-Domainlabels, Entfernen von Initialisierungstaints). Ohne Servicegateway verbleiben GPU-Knoten möglicherweise in einem nicht initialisierten Status, und das Provisioning von Block-Volumes verläuft nicht erfolgreich.

    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. Erstellen Sie Routentabellen für private und öffentliche Subnetze.

    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)

    Hinweis: Die private Routentabelle enthält zwei Regeln: eine NAT-Gatewayroute für den allgemeinen Internetzugriff (Containerimages abrufen, Modelle herunterladen) und eine Servicegatewayroute für den direkten Zugriff auf Oracle Services Network. Die Servicegatewayroute ist kritisch. Ohne sie kann der OKE-Cloudcontroller keine Worker-Knoten initialisieren, was das Block-Volume-Provisioning verhindert. Die öffentliche Routentabelle verwendet das Internetgateway für den Load-Balancer-Zugriff.

  6. Erstellen Sie eine Sicherheitsliste mit den erforderlichen Ingress- und Egress-Regeln für 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)

    Sicherheitshinweis: Diese Beispielsicherheitsliste ist aus Gründen der Einfachheit absichtlich breit gefächert. Beschränken Sie für die Produktion SSH auf das Bastionsubnetz und Ihren IP-Bereich, und bevorzugen Sie separate Sicherheitslisten oder NSGs pro Subnetz, damit das Load-Balancer-Subnetz keine SSH von 0.0.0.0/0 zulässt.

    Sicherer Standardwert: Begrenzen Sie zunächst SSH auf Ihre öffentliche IP, und hängen Sie SSH-Regeln nur an das Bastionsubnetz an. Sie können die Kubernetes-Pod-/Service-CIDRs im Worker-Subnetz beibehalten und SSH vollständig aus dem Load-Balancer-Subnetz auslassen.

    Optionale (empfohlene) Aufteilung: Erstellen Sie eine kleine Nur-SSH-Sicherheitsliste für das Bastionsubnetz und eine separate Liste für Worker-/LB-Subnetze.

    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)

    Hinweis: Wenn Sie nur einen internen Load Balancer verwenden, ersetzen Sie die 0.0.0.0/0-Quellen oben durch 10.0.0.0/16 (oder das VCN-CIDR). Verwendung: Hängen Sie BASTION_SL_ID an das Bastionsubnetz, WORKER_SL_ID an die API-/Workersubnetze und LB_SL_ID an das Load-Balancer-Subnetz an.

    Hinweis: Die CIDR-Regeln für Kubernetes-Pods (10.244.0.0/16) und Service-CIDR-Regeln (10.96.0.0/16) sind erforderlich, damit sich GPU-Worker-Knoten beim Cluster registrieren können. Die ICMP-Regel vom Typ 3 Code 4 aktiviert die Pfad-MTU-Erkennung, wodurch Probleme bei der Paketfragmentierung vermieden werden.

  7. Erstellen der Subnetze. Das Cluster erfordert vier Subnetze: eines für den Kubernetes-API-Endpunkt, eines für Worker-Knoten, eines für Load Balancer und eines für den Bastionhost, der für den Zugriff auf das private Cluster verwendet wird.

    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)
    Subnetz CIDR Sichtbarkeit Zweck
    API-Endpunkt 10.0.0.0/28 Privat Kubernetes-API-Server
    Worker-Knoten 10.0.10.0/24 Privat GPU-Compute-Knoten
    Load Balancer 10.0.20.0/24 Öffentlich Externer Servicezugriff
    Bastion 10.0.30.0/24 Öffentlich SSH-Tunnel für privaten Clusterzugriff

Aufgabe 4: OKE-Cluster erstellen

Stellen Sie ein verwaltetes Kubernetes-Cluster auf OKE mit den in Aufgabe 3 erstellten Netzwerkressourcen bereit. Das Provisioning des Clusters dauert etwa 10 Minuten. In diesem Tutorial wird ein privates Cluster (Skriptstandard) erstellt, das keine reservierte öffentliche IP für den Kubernetes-API-Endpunkt konsumiert. Private Cluster sind der empfohlene Ansatz für Produktions-Workloads, da der API-Server nicht im öffentlichen Internet verfügbar ist.

  1. OKE-Cluster mit einem privaten Endpunkt erstellen.

    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

    Der Befehl gibt eine Arbeitsanforderungs-ID zurück. Rufen Sie die Cluster-ID aus der Clusterliste ab.

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

    Hinweis: Private Cluster erfordern keine reservierte öffentliche IP. Worker-Knoten greifen weiterhin über das NAT-Gateway auf das Internet zu, um Containerimages abzurufen und Modelle herunterzuladen. Nur der kubectl-Zugriff erfordert einen SSH-Tunnel durch die Bastion (in den nächsten Schritten konfiguriert).

  2. Warten Sie, bis das Cluster ACTIVE wird. Dieser Schritt dauert ca. 10 Minuten.

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

    Fragen Sie den Befehl ab, bis die Ausgabe ACTIVE zurückgibt.

    Optional: Zeigen Sie eine präzise Statusübersicht an (einschließlich des privaten API-Endpunkts).

    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

    OCI-CLI-Ausgabe mit OKE-Cluster im Status ACTIVE

    kubectl get nodes -o wide

    kubectl-Ausgabe zum Abrufen von Knoten mit zwei Bereitschaftsknoten

  3. OCI-Bastion für den Zugriff auf das private Cluster erstellen. Die Bastion stellt einen verwalteten SSH-Tunnel zum privaten Kubernetes-API-Endpunkt bereit.

    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)

    Hinweis: Ersetzen Sie YOUR_PUBLIC_IP/32 durch Ihre aktuelle öffentliche IP. Verwenden Sie bei gemeinsam genutzten Netzwerken stattdessen den CIDR-Block Ihres Unternehmens.

    Warten Sie, bis die Bastion ACTIVE wird (ca. 1 Minute).

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

    Sicherheitshinweis: Verwenden Sie für die Produktion nicht 0.0.0.0/0. Schränken Sie --client-cidr-list auf Ihre öffentliche IP oder das CIDR Ihres Unternehmens ein (z.B. "YOUR_PUBLIC_IP/32"). Andernfalls kann jeder im Internet eine Bastionsession versuchen.

  4. Laden Sie die kubeconfig mit dem privaten Endpunkt herunter.

    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. Rufen Sie die IP-Adresse des privaten Endpunkts für den SSH-Tunnel ab.

    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. Bastion-Portweiterleitungssession erstellen Sie benötigen eine SSH-Public Key-Datei.

    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)

    Warten Sie, bis die Session ACTIVE wird, und rufen Sie dann den SSH-Befehl ab.

    oci bastion session get \
        --session-id "${SESSION_ID}" \
        --query "data.{state:\"lifecycle-state\", ssh:\"ssh-metadata\".command}" 2>&1
  7. Öffnen Sie ein separates Terminal, und starten Sie den SSH-Tunnel mit dem Befehl aus dem vorherigen Schritt. Der Tunnel leitet den lokalen Port 6443 an die private Kubernetes-API weiter.

    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

    Hinweis: Ersetzen Sie <PRIVATE_IP>, <SESSION_OCID> und <REGION> durch die Werte aus dem vorherigen Schritt. Halten Sie dieses Terminal für die Dauer Ihrer Sitzung geöffnet. Das Flag -o IdentitiesOnly=yes verhindert "zu viele Authentifizierungsfehler", wenn Ihr SSH-Agent mehrere Schlüssel geladen hat.

  8. Aktualisieren Sie kubeconfig, um eine Verbindung über den lokalen Tunnel herzustellen.

    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

    Hinweis: Das Kennzeichen --insecure-skip-tls-verify ist erforderlich, weil das Clusterzertifikat für die IP des privaten Endpunkts und nicht für 127.0.0.1 ausgestellt wurde. Dies ist sicher, da der Datenverkehr über den SSH-Tunnel verschlüsselt wird.

  9. Wenn Sie ein nicht standardmäßiges OCI-CLI-Profil (z.B. API_KEY_AUTH) verwenden, aktualisieren Sie die kubeconfig, um es zu verwenden. Die generierte kubeconfig-Datei ist standardmäßig das Profil DEFAULT für die Tokengenerierung.

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

    Tipp: Die Schritte 6 bis 9 werden von ./entry_point.sh tunnel automatisiert. Dadurch wird auch automatisch eine Verbindung hergestellt, wenn der SSH-Tunnel bei Vorgängen mit langer Ausführungszeit wie der Datenträgererweiterung unterbrochen wird. Führen Sie sie in einem separaten Terminal aus, und lassen Sie sie für die Dauer der Session laufen.

  10. Prüfen Sie den Clusterzugriff.

kubectl get nodes

An dieser Stelle zeigt die Ausgabe keine Knoten an, da der GPU-Knotenpool noch nicht hinzugefügt wurde.

No resources found

Aufgabe 5: GPU-Knotenpool hinzufügen

Fügen Sie dem OKE-Cluster einen Knotenpool mit GPU-Compute-Instanzen hinzu.

  1. Suchen Sie das neueste GPU-kompatible OKE-Knotenimage. OKE erfordert bestimmte Images, bei denen Kubelet- und Knotenregistrierungskomponenten vorinstalliert sind. Verwenden Sie die API node-pool-options, um das richtige Image für Ihre Kubernetes-Version zu finden.

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

    Hinweis: Die Abfragefilter für Oracle Linux 8.10-GPU-Images, die mit Ihrer Kubernetes-Version übereinstimmen (Beispiel: OKE-1.31.10). Wenn Sie ARM-basierte Images benötigen, ersetzen Sie 8.10 durch den entsprechenden Filter.

  2. Bestimmen Sie die Availability-Domain mit GPU-Ausprägungen. Nicht alle Availability-Domains verfügen über GPU-Kapazität.

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

    Hinweis: Die GPU-Kapazität variiert je nach Region und Availability-Domain. Wenn die Knotenpoolerstellung mit dem Fehler "Nicht genügend Hostkapazität" nicht erfolgreich verläuft, versuchen Sie es mit einer anderen Availability-Domain (GPU_AD_INDEX) oder GPU-Ausprägung, oder fordern Sie Kapazität über Ihren normalen OCI-Prozess an.

  3. Erstellen Sie den GPU-Knotenpool mit einem Boot-Volume von 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"}]'

    Hinweis: Die Knotenlabels app=gpu und nvidia.com/gpu=true werden später vom vLLM-Helm-Diagramm verwendet, um Inferenzpods auf GPU-Knoten zu planen. Das 200-GB-Boot-Volume bietet Speicherplatz für das vLLM-Containerimage (~10 GB) und Modellgewichtungen. Das Dateisystem muss jedoch vor der Verwendung erweitert werden (siehe Aufgabe 8).

  4. Warten Sie, bis die GPU-Knoten bereit sind. Dieser Vorgang dauert in der Regel 5 bis 10 Minuten, während der Knoten GPU-Treiber bereitstellt, startet, installiert und beim Cluster registriert.

    Hinweis: GPU-Instanzen unterliegen Kapazitätsbeschränkungen. Wenn der Knotenpool den Status CREATING aufweist, prüfen Sie den Knotenstatus in der OCI-Konsole oder mit oci ce node-pool get. Ein Fehler "Nicht genügend Hostkapazität" bedeutet, dass keine GPU-Instanzen in dieser Availability-Domain verfügbar sind. Um dies zu beheben, versuchen Sie eine andere Availability-Domain (GPU_AD_INDEX=0 oder GPU_AD_INDEX=2), versuchen Sie eine andere GPU-Ausprägung, oder fordern Sie eine Kapazitätsreservierung über die OCI-Konsole oder ein Supportticket an.

    kubectl get nodes -w

    Erwartete Ausgabe, sobald der Knoten bereit ist:

    NAME          STATUS   ROLES   AGE   VERSION
    10.0.10.x     Ready    node    5m    v1.31.10
  5. Prüfen Sie, ob die GPU auf dem Knoten erkannt wurde.

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

    Erwartete Ausgaben:

    NAME          GPUs
    10.0.10.x     1
  6. Patch für CoreDNS zur Planung auf GPU-Knoten. OKE-GPU-Knoten haben einen nvidia.com/gpu=present:NoSchedule-Taint. In Clustern, die nur GPU-Knoten enthalten, können Systempods wie CoreDNS nicht ohne Toleranz für diesen Taint geplant werden. Ohne DNS können Pods keine externen Hostnamen auflösen, um Modelle herunterzuladen.

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

    Stellen Sie sicher, dass CoreDNS ausgeführt wird.

    kubectl get pods -n kube-system | grep coredns

    Hinweis: Wenn Ihr Cluster über einen dedizierten CPU-Knotenpool für System-Workloads verfügt, ist dieser Schritt nicht erforderlich. Dieser Patch wird nur benötigt, wenn GPU-Knoten die einzigen Knoten im Cluster sind.

    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

    OCI-CLI-Ausgabe mit cpu-pool und gpu-pool beide im Status ACTIVE

Aufgabe 6: NVIDIA Device Plugin installieren

Installieren Sie das NVIDIA-Geräte-Plug-in, damit Kubernetes Workloads auf der GPU erkennen und planen kann.

  1. Wenden Sie das NVIDIA-Geräte-Plug-in DaemonSet an.

    kubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.14.1/nvidia-device-plugin.yml
  2. Warten Sie, bis die Plugin-Pods bereit sind.

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

    Hinweis: Einige OKE-GPU-Knotenimages enthalten ein vorinstalliertes NVIDIA-Geräte-Plug-in (nvidia-gpu-device-plugin). Wenn das Image es bereits enthält, erstellt das Anwenden des Upstream-DaemonSet eine zweite Instanz, die keine Konflikte verursacht. Das automatisierte Skript (entry_point.sh deploy-vllm) installiert es immer, um sicherzustellen, dass die GPU-Erkennung unabhängig von der Knotenimageversion funktioniert.

  3. Vergewissern Sie sich, dass die GPU von Kubernetes zugewiesen werden kann.

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

    Erwartete Ausgaben:

    NAME          GPUs
    10.0.10.x     1
  4. Patchen Sie CoreDNS, um GPU-Knoten-Taints zu tolerieren. In Clustern, in denen GPU-Knoten die einzigen Worker-Knoten sind, können CoreDNS-Pods nicht planen, da OKE-GPU-Knoten einen nvidia.com/gpu=present:NoSchedule-Taint aufweisen. Ohne DNS können Pods keine Image-Registrys oder Modelldownload-URLs auflösen.

    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

    Hinweis: Dieser Schritt ist nur erforderlich, wenn GPU-Knoten die einzigen Worker-Knoten im Cluster sind. Wenn Sie über einen dedizierten CPU-Knotenpool für System-Workloads verfügen, plant CoreDNS diese standardmäßig, und dieser Patch ist nicht erforderlich.

Aufgabe 7: Speicher konfigurieren

Wenden Sie das OCI-Block-Volume StorageClass an, um persistenten Speicher für Modellgewichtungen bereitzustellen.

  1. Wenden Sie die StorageClass-Definition an.

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

    Die Datei definiert zwei Performance-Tiers:

    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 Performance Anwendungsfall
    oci-block-storage-enc Ausgewogen (vpusPerGB: 10) Standard, kostengünstig für die meisten Modelle
    oci-block-storage-hp Hohe Leistung (vpusPerGB: 20) Schnelleres Laden von Modellen für größere Modelle
  2. Stellen Sie sicher, dass StorageClasses verfügbar ist.

    kubectl get storageclass

Hinweis: Verwenden Sie für Deployments mit mehreren Knoten, für die Shared Storage über mehrere Pods hinweg erforderlich ist, OCI File Storage Service (NFS) mit dem Zugriffsmodus ReadWriteMany anstelle von Block Volumes.

Aufgabe 8: GPU-Knotendateisystem erweitern

OCI-Boot-Volumes haben unabhängig von der angegebenen Boot-Volume-Größe eine feste Partition mit ~47 GB. Allein das vLLM-Containerimage beträgt etwa 10 GB, und Modellgewichte erfordern zusätzlichen Speicherplatz. Sie müssen das Dateisystem erweitern, bevor Sie vLLM bereitstellen, um DiskPressure-Entnahmen zu vermeiden.

Hinweis: Dies ist eine OCI-spezifische Anforderung. Das Boot-Volume wird mit 200 GB bereitgestellt, das Betriebssystem partitioniert jedoch standardmäßig nur ~47 GB. Der verbleibende Speicherplatz muss manuell beansprucht werden.

  1. Prüfen Sie die aktuelle Dateisystemgröße auf dem GPU-Knoten.

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

    Die Ausgabe wird ungefähr 47 GB insgesamt zeigen, was bestätigt, dass eine Erweiterung erforderlich ist.

  2. Erstellen Sie einen privilegierten Pod auf dem GPU-Knoten, um die Erweiterungsbefehle auszuführen.

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

    Warten Sie, bis der Pod gestartet wird.

    kubectl wait --for=condition=Ready pod/expand-disk --timeout=60s
  3. Führen Sie alle vier Erweiterungsschritte in einem einzigen kubectl exec-Befehl aus. Wenn Sie sie zusammen ausführen, wird das Risiko vermieden, dass kubectl exec den Exitcode 137 (SIGKILL) zwischen den Schritten zurückgibt, die bei einem starken Datenträger-I/O auf dem Host auftreten können.

    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 /
    '
    Schritt Befehl Zweck
    1 growpart /dev/sda 3 Erweitern Sie Partition 3, um die vollständige Festplatte zu verwenden
    2 pvresize /dev/sda3 Größe des physischen LVM-Volumes ändern
    3 lvextend -l +100%FREE /dev/ocivolume/root Logisches Volume erweitern
    4 xfs_growfs / Vergrößern Sie das XFS-Dateisystem, um das Volume zu füllen

    Hinweis: Alle vier Vorgänge sind idempotent. Wenn die Ausführung Exit-Code 137 zurückgibt, können Sie den gesamten Block sicher erneut ausführen. Suchen Sie in der Ausgabe nach EXPANSION_COMPLETE, um den Erfolg zu bestätigen.

  4. Starten Sie das Kubelet neu, damit der Knoten den aktualisierten zuweisbaren Speicher meldet, und prüfen und bereinigen Sie ihn.

    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

    Hinweis: Der Befehl nsenter gibt den PID-Namespace des Hosts ein, um auf systemd zuzugreifen. Ein einfacher chroot /host systemctl restart kubelet-Befehl ist nicht erfolgreich, da er keine Verbindung mit dem systemd-Bus innerhalb eines chroot-Verzeichnisses herstellen kann.

    Die erwartete Ausgabe sollte ungefähr 189 GB insgesamt zeigen.

Aufgabe 9: vLLM-Produktionsstack bereitstellen

Installieren Sie den vLLM-Inferenzstack mit Helm.

  1. Fügen Sie das vLLM-Helm-Repository hinzu.

    helm repo add vllm https://vllm-project.github.io/production-stack
    helm repo update
  2. Prüfen Sie die Helm-Wertedatei. Die production_stack_specification.yaml konfiguriert das Modell, die Ressourcen und den Speicher für 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"

    Hinweis: Das Modell openai/gpt-oss-20b ist ein Mixture of Experts-(MoE-)Modell mit 20B-Gesamtparametern und 3.6B-aktiven Parametern pro Forward-Pass. Es wird unter der Apache 2.0-Lizenz veröffentlicht, so dass kein Hugging Face-Token erforderlich ist. Das vllm/vllm-openai-Containerimage stellt einen OpenAI-kompatiblen API-Server bereit, mit dem Clients standardmäßige OpenAI-SDK-Aufrufe für Ihren selbst gehosteten Endpunkt verwenden können.

  3. Stellen Sie den Stack bereit. Verwenden Sie hier nicht --wait, da der Router-Pod CrashLoop erhält, bis er im nächsten Schritt gepatcht wird.

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

    Warten Sie, bis der vLLM-Engine-Pod gestartet ist (der Router wird als Nächstes gepatcht).

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

    Hinweis: Der Engine-Pod benötigt einige Minuten, um Ready zu werden, da beim ersten Start die Modellgewichtungen heruntergeladen werden. Wenn der Pod in ContainerCreating bleibt, wird das Containerimage (~10 GB) weiterhin abgerufen. Verwenden Sie kubectl describe pod <pod-name>, um den Fortschritt zu prüfen.

  4. Patch für die Router-Bereitstellung. Der Router benötigt eine GPU-Toleranz (damit er planen kann, wenn GPU-Knoten die einzigen Knoten mit Kapazität sind) und erhöhte Speicherlimits (die Standard-500-Mi-Werte können OOMKill verursachen).

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

    Hinweis: Die GPU-Toleranz ist erforderlich, da OKE-GPU-Knoten einen nvidia.com/gpu=present:NoSchedule-Taint aufweisen, der verhindert, dass Nicht-GPU-Workloads geplant werden. Da der Router keine GPU verwendet, sondern irgendwo ausgeführt werden muss, kann er mit dieser Toleranz auf GPU-Knoten planen. In Clustern mit dedizierten CPU-Knotenpools ist diese Toleranz nicht erforderlich.

  5. Vergewissern Sie sich, dass das Helm-Release bereitgestellt ist.

    helm list

    Terminalausgabe der Helm-Liste mit bereitgestelltem vllm-Stack

  6. Prüfen Sie, ob die Pods ausgeführt werden.

    kubectl get pods

    Erwartete Ausgaben:

    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 get-Pods mit Router- und Engine-Pods, die beide ausgeführt werden 1/1

  7. Prüfen Sie den Ladefortschritt des Modells in den Podlogs.

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

    Warten Sie, bis eine Meldung angezeigt wird, dass das Modell geladen wurde und der Server bereit ist, Anforderungen zu akzeptieren.

Aufgabe 10: Inferenzendpunkt testen

Stellen Sie sicher, dass das Deployment Inferenzanforderungen verarbeitet. Der vLLM-Produktionsstack stellt eine OpenAI-kompatible API über den Routerservice bereit, sodass jeder OpenAI-SDK-Client oder curl-Befehl damit interagieren kann.

Das folgende Diagramm zeigt den Lebenszyklus der Inferenzanforderung: von der Clientanforderung über die Engine-Auswahllogik des Routers bis hin zu den Vorfüll- und Decodierungsphasen der vLLM-Engine und zurück als gestreamte Antwort.

Sequenzdiagramm mit Inferenzanforderungslebenszyklus von Client über Router zu vLLM-Engine und GPU

  1. Listen Sie die verfügbaren Modelle auf, um zu bestätigen, dass das Deployment fehlerfrei ist.

    kubectl get svc vllm-router-service

    Der Routerservice stellt das API-Gateway für alle bereitgestellten Modelle bereit. Da das Cluster einen privaten Endpunkt verwendet, greifen Sie über kubectl port-forward auf den Service zu.

  2. Starten Sie einen Port-Forward vom lokalen Rechner zum Routerservice. Öffnen Sie ein neues Terminal (halten Sie die Ausführung des SSH-Tunnels im anderen fest), und führen Sie Folgendes aus:

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

    Dadurch wird localhost:8080 auf Ihrem Rechner Port 80 auf dem Routerservice im Cluster zugeordnet.

    Sicherheitshinweis: kubectl port-forward bindet lokal und stellt den Service nicht öffentlich zur Verfügung. Dies ist der sicherste Weg, um zu testen, wenn ein privates Cluster über einen Bastiontunnel verwendet wird.

    Hinweis: Der Befehl "port-forward" wird im Vordergrund ausgeführt. Halten Sie dieses Terminal während des Tests offen. Drücken Sie Ctrl+C, um es zu stoppen, wenn Sie fertig sind.

  3. Prüfen Sie in einem anderen Terminal, ob das Modell verfügbar ist, indem Sie den Modellendpunkt abfragen.

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

    Erwartete Ausgaben:

    {
        "object": "list",
        "data": [
            {
                "id": "openai/gpt-oss-20b",
                "object": "model",
                "created": 1234567890,
                "owned_by": "vllm"
            }
        ]
    }
  4. Senden Sie eine Textabschlusungsanforderung.

    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

    Erwartete Ausgabe (abgekürzt):

    {
        "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. Senden Sie eine Chatabschlussanforderung. Dies ist das gleiche Format, das vom Python-SDK OpenAI verwendet wird, und ist die häufigste Möglichkeit, programmgesteuert mit LLMs zu interagieren.

    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

    Erwartete Ausgabe (abgekürzt):

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

    Beide Antworten umfassen den generierten Text des Modells im choices-Array, Tokenverwendungsstatistiken und eine finish_reason von stop (das Modell wurde natürlich beendet) oder length (Ausgabe wurde unter max_tokens abgeschnitten).

    Hinweis: Der Modellname in der API-Anforderung ist der vollständige Modellpfad (openai/gpt-oss-20b), der mit dem Feld modelURL in den Helm-Werten übereinstimmt. Jeder OpenAI-kompatible Client kann diesen Endpunkt verwenden, indem er base_url auf http://localhost:8080/v1 setzt.

    Terminalausgabe mit Lock-Chat-Abschlussanforderung und JSON-Antwort von openai/gpt-oss-20b

  6. Messen Sie die End-to-End-Latenz für eine kurze Anforderung.

    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

    Erwarten Sie auf einer einzelnen A10-GPU 1-3 Sekunden End-to-End für kurze Abschlüsse. Die Zeit bis zum ersten Token (TTFT) beträgt in der Regel 50-200 ms, abhängig von der Prompt-Länge. Für einen höheren Durchsatz erhöhen Sie replicaCount in den Helm-Werten, um weitere Engine-Replikate hinter dem Router hinzuzufügen.

Aufgabe 11: Über OCI Load Balancer bereitstellen (optional)

Machen Sie den Inferenzendpunkt extern über einen OCI Load Balancer zugänglich.

Sicherheitshinweis: Dadurch wird die Inferenz-API standardmäßig für das öffentliche Internet verfügbar gemacht. Aktivieren Sie dies in der Produktion nicht ohne TLS-, Authentifizierungs-(API-Schlüssel/JWT/mTLS-) und IP-Ausnahmelisten oder WAF-Steuerelemente. Wenn Sie es bereitstellen müssen, stellen Sie ihm einen Ingress-Controller oder ein API-Gateway vor, das Authentifizierungs- und Ratenlimits durchsetzt, oder verwenden Sie einen internen Load Balancer.

  1. Patchen Sie den Routerservice, um einen LoadBalancer-Typ zu verwenden.

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

    Hinweis: Wenn Sie einen internen Load Balancer benötigen, fügen Sie die OCI-Annotation dem Service hinzu (Beispiel unten). Dadurch bleibt der Endpunkt im VCN privat.

    kubectl annotate svc vllm-router-service \
      "service.beta.kubernetes.io/oci-load-balancer-internal"="true"
  2. Warten Sie, bis die externe IP zugewiesen wurde.

    kubectl get svc vllm-router-service -w

    Erwartete Ausgabe nach Provisioning des Load Balancers:

    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 get svc-Ausgabe mit Router- und Engine-Services

  3. Testen Sie den externen Endpunkt.

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

Aufgabe 12: Multi-GPU-Tensorparallelität konfigurieren (erweitert)

Stellen Sie größere Modelle über mehrere GPUs auf Bare-Metal-Ausprägungen bereit.

Die Tensor-Parallelität teilt ein Modell auf mehrere GPUs auf einem einzelnen Knoten auf. Dies ist erforderlich, wenn die Speicheranforderungen eines Modells eine einzelne GPU überschreiten. Beispiel: Meta Llama 3.1 70B benötigt etwa 140 GB GPU-Speicher, was die Kapazität einer einzelnen GPU überschreitet, jedoch auf 8x A100 80 GB oder 8x H100 GPUs passt.

  1. Erstellen Sie mit Ihrem Hugging Face-Token ein Kubernetes-Secret. Gated-Modelle wie Llama 3.1 70B erfordern eine Authentifizierung.

    kubectl create secret generic hf-token-secret \
      --from-literal=token=YOUR_HUGGINGFACE_TOKEN
  2. Aktualisieren Sie production_stack_specification.yaml mit einer Konfiguration mit mehreren GPUs.

    servingEngineSpec:
      modelSpec:
      - name: "llama70b"
        repository: "vllm/vllm-openai"
        tag: "latest"
        modelURL: "meta-llama/Llama-3.1-70B-Instruct"
    
        replicaCount: 1
        tensorParallelSize: 8
    
        requestCPU: 32
        requestMemory: "256Gi"
        requestGPU: 8
    
        hf_token:
          secretName: "hf-token-secret"
          secretKey: "token"
    
        pvcStorage: "500Gi"
        pvcAccessMode:
          - ReadWriteOnce
        storageClass: "oci-block-storage-enc"
    
        nodeSelector:
          node.kubernetes.io/instance-type: "BM.GPU.H100.8"
        tolerations:
          - key: "nvidia.com/gpu"
            operator: "Exists"
            effect: "NoSchedule"
    
        extraArgs:
          - "--max-model-len=8192"
          - "--gpu-memory-utilization=0.95"
          - "--tensor-parallel-size=8"
  3. Mit den aktualisierten Werten bereitstellen. Verwenden Sie wie bei Aufgabe 9 nicht --wait. Der Router CrashLoop, bis er gepatcht wird.

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

    Patchen Sie anschließend den Router (entspricht Aufgabe 9, Schritt 4), und prüfen Sie:

    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. Stellen Sie sicher, dass der Pod ausgeführt wird und alle GPUs verwendet werden.

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

Hinweis: Stellen Sie sicher, dass das OKE-Cluster einen Knotenpool mit der entsprechenden Bare-Metal-GPU-Ausprägung (z.B. BM.GPU.H100.8) enthält, bevor Sie eine Konfiguration mit mehreren GPUs bereitstellen.

Aufgabe 13: OCI Object Storage für Modelle verwenden (erweitert)

Laden Sie Modellgewichte aus OCI Object Storage, anstatt sie von Hugging Face herunterzuladen. Dies ist nützlich für private Modelle, schnellere Downloads innerhalb von OCI oder Umgebungen ohne externen Internetzugriff.

  1. Laden Sie Ihre Modellgewichtungen in einen OCI Object Storage-Bucket hoch. Navigieren Sie in der OCI-Konsole zu Speicher > Object Storage, und erstellen Sie einen Bucket, falls noch keiner vorhanden ist.

  2. Erstellen Sie eine URL für die vorab authentifizierte Anforderung (PAR) für Ihren Bucket. Wählen Sie in der OCI-Konsole Ihren Bucket aus, klicken Sie auf Vorab authentifizierte Anforderungen, und erstellen Sie eine neue Anforderung mit Lesezugriff.

  3. Aktualisieren Sie production_stack_specification.yaml, um die PAR-URL zu verwenden.

    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. Mit den aktualisierten Werten bereitstellen (ohne --wait). Siehe Aufgabe 9 für warum).

    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. Prüfen Sie die Modellladevorgänge aus Object Storage, indem Sie die Podlogs prüfen.

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

Aufgabe 14: Ressourcen bereinigen

Entfernen Sie alle bereitgestellten Ressourcen, um laufende Gebühren zu vermeiden.

  1. Entfernen Sie die Kubernetes-Ressourcen mit dem Bereinigungsskript.

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

    Dadurch wird das Helm-Release deinstalliert, alle PersistentVolumeClaims-, PersistentVolumes- und benutzerdefinierten vLLM-Ressourcen werden gelöscht.

  2. Löschen Sie das OKE-Cluster und alle OCI-Netzwerkressourcen.

    ./entry_point.sh cleanup

    Dadurch werden die folgenden Ressourcen gelöscht:

    • GPU-Knotenpool
    • OKE-Cluster
    • Bastionhost (falls erstellt)
    • Subnetze (API, Worker, Load Balancer, Bastion)
    • Sicherheitslisten
    • Servicegateway, NAT-Gateway und Internetgateway
    • Routentabellen
    • VCN
  3. Prüfen Sie, ob alle Ressourcen in der OCI-Konsole unter Entwicklerservices > Kubernetes-Cluster und Networking > Virtuelle Cloud-Netzwerke entfernt wurden.

    ./entry_point.sh cleanup

    Terminalausgabe der Bereinigung entry_point.sh mit vollständigem Ressourcen-Teardown

Hinweis: Stellen Sie sicher, dass alle Ressourcen gelöscht werden, um laufende Gebühren zu vermeiden. GPU-Instanzen und Block-Volumes verursachen auch im Leerlauf Kosten.

Weitere Schritte

In diesem Tutorial wurde ein funktionaler Inferenzstack bereitgestellt. Berücksichtigen Sie bei Produktions-Workloads die folgenden Verbesserungen:

Bestätigungen

Weitere Lernressourcen

Sehen Sie sich weitere Übungen zu docs.oracle.com/learn an, oder greifen Sie auf weitere kostenlose Lerninhalte im Oracle Learning YouTube-Kanal zu. Besuchen Sie außerdem education.oracle.com/learning-explorer, um ein Oracle Learning Explorer zu werden.

Die Produktdokumentation finden Sie im Oracle Help Center.