Remarques :

Déployer la pile de production OpenAI vLLM sur Oracle Kubernetes Engine (OKE)

Introduction

Les entreprises qui adoptent de grands modèles de langage (LLM) pour les charges de travail de production sont confrontées à une décision d'infrastructure critique : faites confiance à des API d'inférence tierces ou déployez une pile d'inférence auto-hébergée. Les déploiements auto-hébergés offrent des avantages significatifs : un contrôle complet de la conformité et de la confidentialité des données, une latence d'inférence inférieure à 100 millisecondes en éliminant les allers-retours réseau, un coût prévisible à grande échelle et la liberté d'affiner et de servir tout modèle open source sans dépendance vis-à-vis d'un fournisseur.

Cependant, la création d'une pile d'inférence LLM de qualité production à partir de zéro est complexe. Elle nécessite une orchestration de conteneurs prenant en charge les GPU, un routage intelligent des demandes sur plusieurs répliques de modèles, un stockage persistant pour de grands poids de modèle et une surveillance continue, le tout intégré et exécuté de manière fiable.

Oracle Cloud Infrastructure propose plusieurs chemins pour l'inférence en IA. Le service OCI Generative AI offre une expérience entièrement gérée avec des clusters d'IA dédiés isolés de votre location, idéal pour les équipes qui souhaitent se lancer rapidement avec des modèles pris en charge. Ce tutoriel adopte une autre approche : déployer votre propre pile d'inférence sur OKE. Ce chemin est conçu pour les équipes qui ont besoin d'un contrôle précis sur les pilotes GPU, les versions CUDA, les configurations de modèle et les paramètres de service, ou les équipes qui entraînent et affinent les modèles personnalisés et souhaitent les servir directement. OCI fournit des instances de GPU Bare Metal avec des GPU NVIDIA A10, A100 et H100, connectés par un réseau de cluster RDMA à très faible latence, vous donnant le même niveau de contrôle matériel que sur site tout en bénéficiant de l'élasticité du cloud.

La pile de production vLLM résout la complexité de l'inférence auto-hébergée en fournissant une plate-forme open source native de Kubernetes basée sur vLLM, le moteur d'inférence à haut débit utilisé en production par des organisations telles que Meta, Mistral AI et IBM. Il offre un débit jusqu'à 24x supérieur à celui des structures de service standard grâce à une gestion efficace de la mémoire GPU et à une optimisation du cache KV. Associée aux formes de GPU OKE et OCI, vous bénéficiez d'une plate-forme d'inférence prête à l'emploi avec un réseau, un stockage et une sécurité de niveau entreprise. Les scripts de déploiement OCI utilisés dans ce tutoriel sont fournis et gérés dans le référentiel officiel de la pile de production vLLM.

Ce tutoriel vous explique comment déployer la pile de production vLLM sur OKE, du provisionnement de l'infrastructure à l'exécution de votre première demande d'inférence.

Remarque : ce tutoriel provisionne des ressources étape par étape à l'aide de l'interface de ligne de commande OCI pour vous aider à comprendre le flux complet des ressources cloud OCI requises pour un déploiement d'inférence de GPU. Pour les environnements de production, il est recommandé de codifier cette infrastructure à l'aide de Terraform ou d'OCI Resource Manager (Shepherd) pour les déploiements reproductibles et contrôlés par version.

Diagramme d'architecture montrant le VCN avec des sous-réseaux, un cluster OKE, un pool de noeuds GPU, des pods vLLM et un équilibreur de charge

Les services OCI suivants sont utilisés dans ce tutoriel :

Service Description
Moteur Oracle Kubernetes (OKE) Cluster Kubernetes géré pour l'orchestration de conteneurs et la programmation de charges de travail GPU
OCI Compute (formes GPU) Instances de GPU NVIDIA A10 (24 Go) et A100 (80 Go) pour l'inférence de modèle
Volumes de blocs OCI Stockage persistant pour les poids de modèle avec des niveaux de performances configurables
Réseau cloud virtuel OCI Infrastructure réseau comprenant des sous-réseaux, des passerelles et des listes de sécurité
Equilibreur de charge OCI Accès externe aux adresses d'inférence
OCI Bastion Tunnels SSH gérés pour l'accès au cluster privé
OCI Object Storage Autre source de modèle utilisant des URL de demande pré-authentifiée

Objectifs

Dans ce tutoriel, vous allez :

Prérequis

Remarque : les exemples de sortie et de capture d'écran de ce tutoriel utilisent us-chicago-1. Vous pouvez effectuer le déploiement dans n'importe quelle région prise en charge en définissant OCI_REGION. La capacité des GPU varie en fonction de la région et du domaine de disponibilité. Vérifiez donc que la forme de GPU cible est disponible avant le déploiement. Vérifiez la disponibilité de la forme GPU par région et préparez-vous à essayer un autre domaine de disponibilité (GPU_AD_INDEX) si vous rencontrez des erreurs de capacité.

Remarque : ce tutoriel provisionne les ressources de GPU payantes (par exemple, VM.GPU.A10.1). Il ne s'agit pas d'une charge globale OCI Always Free. Exécutez toujours les étapes de nettoyage lorsque vous avez terminé pour éviter les frais en cours.

Remarque : ce tutoriel déploie openai/gpt-oss-20b, un modèle sous licence Apache 2.0 à partir de OpenAI. Aucun jeton Hugging Face n'est requis. Si vous souhaitez déployer des modèles sécurisés tels que Meta Llama 3.1, vous aurez besoin d'un compte Hugging Face avec un jeton d'API.

Tâche 1 : configurer les variables d'environnement

Définissez la configuration OCI requise avant de déployer l'infrastructure.

  1. Recherchez l'OCID de compartiment dans la console OCI. Accédez à Identité et sécurité > Compartiments, puis cliquez sur le compartiment cible et copiez l'OCID.

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

    Sortie de l'interface de ligne de commande OCI affichant l'OCID de compartiment

  2. Exportez la variable d'environnement requise.

    export OCI_COMPARTMENT_ID="ocid1.compartment.oc1..xxxxx"
  3. Vous pouvez éventuellement remplacer la configuration par défaut en définissant l'une des variables d'environnement suivantes.

    Variable Par défaut Description
    OCI_REGION us-ashburn-1 Région OCI pour le déploiement
    OCI_PROFILE DEFAULT Profil de configuration OCI CLI
    CLUSTER_NAME production-stack Nom du cluster OKE
    GPU_SHAPE VM.GPU.A10.1 Forme de calcul GPU pour le pool de noeuds
    GPU_NODE_COUNT 1 Nombre de noeuds GPU dans le pool
    GPU_BOOT_VOLUME_GB 200 Taille de volume d'initialisation (en Go) pour les noeuds GPU
    CPU_BOOT_VOLUME_GB 100 Taille de volume d'initialisation en Go pour les noeuds d'UC
    GPU_AD_INDEX 1 Index de domaine de disponibilité (basé sur 0) pour le placement de GPU
    PRIVATE_CLUSTER true Définir sur false pour une adresse d'API Kubernetes publique
    KUBERNETES_VERSION v1.31.10 Version de Kubernetes pour le cluster OKE

    Par exemple, pour effectuer un déploiement avec deux noeuds GPU A100 :

    export OCI_COMPARTMENT_ID="ocid1.compartment.oc1..xxxxx"
    export GPU_SHAPE="BM.GPU.A100-v2.8"
    export GPU_NODE_COUNT="2"
  4. Passez en revue les formes de GPU disponibles et sélectionnez-en une en fonction des exigences de taille de modèle.

    Forme GPU Type de GPU Mémoire GPU Recommandée pour
    VM.GPU.A10.1 1 NVIDIA A10 24 GB Modèles de paramètre 7B–13B
    VM.GPU.A10.2 2 NVIDIA A10 48 GB Tenseur parallèle avec petits modèles
    BM.GPU4.8 8 NVIDIA A100 40 GB 320 GB 70B modèles, rentables
    BM.GPU.A100-v2.8 8 NVIDIA A100 80 GB 640 GB Modèles de paramètre 70B+
    BM.GPU.H100.8 8 NVIDIA H100 640 GB Les plus grands modèles, prise en charge RDMA

    Remarque : les formes Bare Metal (BM.*) fournissent du matériel dédié sans surcharge de virtualisation et prennent en charge le parallélisme des tenseurs multi-GPU. Les formes de machine virtuelle (VM.*) sont plus économiques pour les petits modèles.

    Remarque : ce tutoriel utilise VM.GPU.A10.1 (un seul NVIDIA A10 avec 24 Go de mémoire GPU) pour déployer openai/gpt-oss-20b, un modèle Mixture of Experts (MoE) avec des paramètres actifs 3.6B qui tient généralement sur un seul GPU A10. Les sections avancées présentent les configurations multi-GPU à l'aide de BM.GPU.H100.8 pour les modèles plus volumineux tels que Llama 3.1 70B.

Tâche 2 : déploiement à l'aide du script automatisé (démarrage rapide)

La pile de production vLLM inclut un script de déploiement automatisé qui provisionne toutes les ressources OCI et déploie la pile d'inférence avec une seule commande. Utilisez cette approche pour un déploiement rapide. Les tâches 3 à 10 couvrent chaque étape individuellement pour les utilisateurs qui souhaitent personnaliser le processus.

  1. Clonez le référentiel vLLM Production Stack.

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

    export OCI_COMPARTMENT_ID="ocid1.compartment.oc1..xxxxx"
  3. Exécutez le script de déploiement.

    ./entry_point.sh setup

    Sortie de terminal de la configuration entry_point.sh indiquant la création du VCN, du cluster, du bastion et du pool de noeuds

    Pour les clusters publics (PRIVATE_CLUSTER=false), la configuration crée toute l'infrastructure et déploie la pile vLLM en une seule commande. Transmettez le fichier de valeurs Helm en tant que deuxième argument :

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

    Pour les clusters privés (valeur par défaut), la configuration crée l'infrastructure mais ne peut pas atteindre directement l'API Kubernetes. Ouvrez un terminal distinct et démarrez le tunnel, puis déployez :

    # 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. Vérifiez que le déploiement est en cours d'exécution.

    kubectl get pods

    Sortie attendue :

    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

Remarque : si les deux pods affichent le statut Running, le déploiement est prêt. Passez à la tâche 10 : test de l'adresse d'inférence.

Remarque : les instances de GPU sont soumises à des contraintes de capacité OCI. Si le script reste dans la boucle "Waiting for GPU node" pendant plus de 15 minutes, la forme de GPU peut ne pas être disponible dans le domaine de disponibilité sélectionné. Vérifiez le statut du pool de noeuds avec oci ce node-pool get et recherchez les erreurs "Out of host capacity". Pour résoudre ce problème, nettoyez-le avec ./entry_point.sh cleanup et redéployez-le avec un autre domaine de disponibilité (par exemple, GPU_AD_INDEX=0 ou GPU_AD_INDEX=2) ou une autre forme de GPU (par exemple, GPU_SHAPE=VM.GPU.A10.2).

Remarque : le script de déploiement utilise des instances de GPU qui entraînent des coûts importants (~50 $/jour pour un seul GPU A10). Exécutez toujours ./entry_point.sh cleanup lorsque vous avez terminé pour éviter les frais permanents.

Tâche 3 : créer un VCN et un réseau

Créez l'infrastructure réseau OCI requise pour le cluster OKE. Cela inclut un réseau cloud virtuel (VCN), des passerelles, des tables de routage, des listes de sécurité et des sous-réseaux. Chaque ressource réseau est créée en quelques secondes ; l'ensemble complet des commandes se termine en moins de 2 minutes.

  1. Créez un VCN avec un bloc CIDR 10.0.0.0/16.

    VCN_ID=$(oci network vcn create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --display-name "${CLUSTER_NAME}-vcn" \
        --cidr-blocks '["10.0.0.0/16"]' \
        --dns-label "prodstack" \
        --query "data.id" \
        --raw-output)
  2. Créez une passerelle Internet pour le routage de sous-réseau public.

    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. Créez une passerelle NAT pour le trafic sortant à partir de sous-réseaux privés.

    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. Créez une passerelle de service pour l'accès à Oracle Services Network. Le contrôleur de cloud OKE utilise les services Oracle pour initialiser les noeuds de processus actifs (définir des étiquettes de domaine de disponibilité, supprimer les taches d'initialisation). Sans passerelle de service, les noeuds GPU peuvent rester dans un état non initialisé et le provisionnement de volume de blocs échouera.

    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. Créez des tables de routage pour les sous-réseaux publics et privés.

    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)

    Remarque : la table de routage privée comporte deux règles : un routage de passerelle NAT pour un accès Internet général (extraction d'images de conteneur, téléchargement de modèles) et un routage de passerelle de service pour un accès direct à Oracle Services Network. Le routage de la passerelle de service est critique. Sans cela, le contrôleur de cloud OKE ne peut pas initialiser les noeuds de processus actifs, ce qui empêche le provisionnement des volumes de blocs. La table de routage publique utilise la passerelle Internet pour l'accès à l'équilibreur de charge.

  6. Créez une liste de sécurité avec les règles entrantes et sortantes requises pour 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)

    Remarque sur la sécurité : cet exemple de liste de sécurité est intentionnellement étendu pour plus de simplicité. Pour la production, limitez SSH au sous-réseau de bastion et à votre plage d'adresses IP, et préférez des listes de sécurité ou des groupes de sécurité réseau distincts par sous-réseau afin que le sous-réseau d'équilibreur de charge n'autorise pas SSH à partir de 0.0.0.0/0.

    Valeur par défaut sécurisée : commencez par limiter SSH à votre adresse IP publique et par attacher des règles SSH uniquement au sous-réseau de bastion. Vous pouvez conserver les CIDR de pod/service Kubernetes sur le sous-réseau de processus actif et omettre entièrement SSH du sous-réseau d'équilibreur de charge.

    Fractionnement facultatif (recommandé) : créez une petite liste de sécurité SSH uniquement pour le sous-réseau de bastion et une liste distincte pour les sous-réseaux de processus actifs/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)

    Remarque : si vous utilisez uniquement un équilibreur de charge interne, remplacez les sources 0.0.0.0/0 ci-dessus par 10.0.0.0/16 (ou votre CIDR VCN). Utilisation : attachez BASTION_SL_ID au sous-réseau de bastion, WORKER_SL_ID aux sous-réseaux d'API/de travail et LB_SL_ID au sous-réseau d'équilibreur de charge.

    Remarque : les règles CIDR de pods Kubernetes (10.244.0.0/16) et CIDR de services (10.96.0.0/16) sont requises pour que les noeuds de processus actif GPU s'inscrivent dans le cluster. La règle ICMP type 3 code 4 permet la découverte de MTU de chemin, ce qui empêche les problèmes de fragmentation des paquets.

  7. Créez les sous-réseaux. Le cluster requiert quatre sous-réseaux : un pour l'adresse d'API Kubernetes, un pour les noeuds de processus actif, un pour les équilibreurs de charge et un pour l'hôte de bastion utilisé pour accéder au cluster privé.

    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)
    Sous-réseau CIDR Visibilité Description
    Adresse d'API 10.0.0.0/28 Privé Serveur d'API Kubernetes
    Noeuds de processus actif 10.0.10.0/24 Privé Noeuds de calcul GPU
    Equilibreurs de charge 10.0.20.0/24 Public Accès au service externe
    Bastion 10.0.30.0/24 Public Tunnel SSH pour l'accès au cluster privé

Tâche 4 : créer le cluster OKE

Déployez un cluster Kubernetes géré sur OKE à l'aide des ressources réseau créées dans la tâche 3. Le provisionnement du cluster prend environ 10 minutes. Ce tutoriel crée un cluster privé (valeur par défaut du script), qui n'utilise pas d'adresse IP publique réservée pour l'adresse d'API Kubernetes. Les clusters privés sont l'approche recommandée pour les charges de travail de production car le serveur d'API n'est pas exposé au réseau Internet public.

  1. Créez le cluster OKE avec une adresse privée.

    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

    La commande renvoie un ID de demande de travail. Obtenez l'ID de cluster dans la liste des clusters.

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

    Remarque : les clusters privés ne nécessitent pas d'adresse IP publique réservée. Les noeuds de processus actif accèdent toujours à Internet via la passerelle NAT pour extraire des images de conteneur et télécharger des modèles. Seul l'accès kubectl nécessite un tunnel SSH via le bastion (configuré dans les étapes suivantes).

  2. Attendez que le cluster devienne actif. Cette étape prend environ 10 minutes.

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

    Interrogez la commande jusqu'à ce que la sortie renvoie ACTIVE.

    Facultatif : affichez un récapitulatif concis des statuts (y compris l'adresse d'API privée).

    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

    Sortie de l'interface de ligne de commande OCI affichant le cluster OKE à l'état ACTIVE

    kubectl get nodes -o wide

    Sortie des noeuds get kubectl affichant deux noeuds prêts

  3. Créez un bastion OCI pour accéder au cluster privé. Le bastion fournit un tunnel SSH géré vers l'adresse d'API Kubernetes privée.

    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)

    Remarque : remplacez YOUR_PUBLIC_IP/32 par l'adresse IP publique en cours. Pour les réseaux partagés, utilisez plutôt le bloc CIDR de votre entreprise.

    Attendez que le bastion devienne ACTIF (environ 1 minute).

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

    Remarque relative à la sécurité : pour la production, n'utilisez pas 0.0.0.0/0. Restreignez --client-cidr-list à votre adresse IP publique ou à votre CIDR d'entreprise (par exemple, "YOUR_PUBLIC_IP/32"), sinon toute personne sur Internet peut tenter une session de bastion.

  4. Téléchargez kubeconfig à l'aide de l'adresse privée.

    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. Obtenez l'adresse IP de l'adresse privée pour le tunnel SSH.

    PRIVATE_ENDPOINT=$(oci ce cluster get \
        --cluster-id "${CLUSTER_ID}" \
        --query "data.endpoints.\"private-endpoint\"" \
        --raw-output)
    PRIVATE_IP="${PRIVATE_ENDPOINT%:*}"
    echo "Private endpoint IP: ${PRIVATE_IP}"
  6. Créez une session de transfert de port de bastion. Vous aurez besoin d'un fichier de clés publiques 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)

    Attendez que la session devienne ACTIVE, puis obtenez la commande SSH.

    oci bastion session get \
        --session-id "${SESSION_ID}" \
        --query "data.{state:\"lifecycle-state\", ssh:\"ssh-metadata\".command}" 2>&1
  7. Ouvrez un terminal distinct et démarrez le tunnel SSH à l'aide de la commande de l'étape précédente. Le tunnel transfère le port local 6443 vers l'API Kubernetes privée.

    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

    Remarque : remplacez <PRIVATE_IP>, <SESSION_OCID> et <REGION> par les valeurs de l'étape précédente. Gardez ce terminal ouvert pendant toute la durée de votre session. L'indicateur -o IdentitiesOnly=yes empêche les erreurs "trop d'échecs d'authentification" lorsque plusieurs clés sont chargées pour l'agent SSH.

  8. Mettez à jour le kubeconfig pour qu'il se connecte via le tunnel local.

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

    Remarque : l'indicateur --insecure-skip-tls-verify est requis car le certificat de cluster a été émis pour l'adresse IP de l'adresse privée, et non pour 127.0.0.1. Ceci est sûr car le trafic est chiffré via le tunnel SSH.

  9. Si vous utilisez un profil d'interface de ligne de commande OCI autre que celui par défaut (par exemple, API_KEY_AUTH), mettez à jour le fichier kubeconfig pour l'utiliser. Le profil kubeconfig généré est par défaut le profil DEFAULT pour la génération de jeton.

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

    A savoir : Les étapes 6 à 9 sont automatisées par ./entry_point.sh tunnel, qui se connecte également automatiquement si le tunnel SSH est abandonné lors d'opérations à longue durée d'exécution telles que l'extension de disque. Exécutez-le dans un terminal distinct et laissez-le fonctionner pendant toute la durée de votre session.

  10. Vérifiez l'accès au cluster.

kubectl get nodes

A ce stade, la sortie n'affichera aucun noeud, car le pool de noeuds GPU n'a pas encore été ajouté.

No resources found

Tâche 5 : ajouter un pool de noeuds GPU

Ajoutez un pool de noeuds avec des instances de calcul GPU au cluster OKE.

  1. Recherchez la dernière image de noeud OKE compatible GPU. OKE requiert des images spécifiques avec des composants d'enregistrement de kubelet et de noeud préinstallés. Utilisez l'API node-pool-options pour trouver l'image correcte pour votre version de Kubernetes.

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

    Remarque : les filtres de requête pour les images de GPU Oracle Linux 8.10 correspondant à votre version de Kubernetes (par exemple, OKE-1.31.10). Si vous avez besoin d'images basées sur ARM, remplacez 8.10 par le filtre approprié.

  2. Déterminez le domaine de disponibilité qui comporte des formes GPU. Tous les domaines de disponibilité n'ont pas de 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}"

    Remarque : la capacité des GPU varie selon la région et le domaine de disponibilité. Si la création du pool de noeuds échoue avec une erreur "Out of host capacity", essayez un autre domaine de disponibilité (GPU_AD_INDEX) ou une autre forme de GPU, ou demandez de la capacité via le processus OCI normal.

  3. Créez le pool de noeuds GPU avec un volume d'initialisation de 200 Go.

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

    Remarque : les libellés de noeud app=gpu et nvidia.com/gpu=true sont utilisés ultérieurement par le graphique Helm vLLM pour programmer des pods d'inférence sur des noeuds GPU. Le volume d'initialisation de 200 Go fournit de l'espace pour l'image de conteneur vLLM (~10 Go) et les poids de modèle, mais le système de fichiers doit être étendu avant utilisation (voir la tâche 8).

  4. Attendez que les noeuds GPU deviennent prêts. Cela prend généralement entre 5 et 10 minutes lorsque le noeud provisionne, initialise, installe les pilotes GPU et s'inscrit dans le cluster.

    Remarque : les instances de GPU sont soumises à des contraintes de capacité. Si le pool de noeuds reste à l'état CREATING, vérifiez le statut du noeud dans la console OCI ou avec oci ce node-pool get. Une erreur "Out of host capacity" signifie qu'aucune instance GPU n'est disponible dans ce domaine de disponibilité. Pour résoudre ce problème, essayez un autre domaine de disponibilité (GPU_AD_INDEX=0 ou GPU_AD_INDEX=2), essayez une autre forme de GPU ou demandez une réservation de capacité via la console OCI ou un ticket d'assistance.

    kubectl get nodes -w

    Sortie attendue une fois le noeud prêt :

    NAME          STATUS   ROLES   AGE   VERSION
    10.0.10.x     Ready    node    5m    v1.31.10
  5. Vérifiez que le GPU est détecté sur le noeud.

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

    Sortie attendue :

    NAME          GPUs
    10.0.10.x     1
  6. Appliquez un patch à CoreDNS pour programmer les noeuds GPU. Les noeuds de GPU OKE ont une tache nvidia.com/gpu=present:NoSchedule. Dans les clusters qui n'ont que des noeuds GPU, les pods système tels que CoreDNS ne peuvent pas programmer sans tolérance pour cette tache. Sans DNS, les pods ne peuvent pas résoudre les noms d'hôte externes pour télécharger des modèles.

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

    Vérifiez que CoreDNS est en cours d'exécution.

    kubectl get pods -n kube-system | grep coredns

    Remarque : si votre cluster dispose d'un pool de noeuds CPU dédié pour les charges globales système, cette étape n'est pas nécessaire. Ce patch n'est nécessaire que lorsque les noeuds GPU sont les seuls noeuds du 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

    Sortie de l'interface de ligne de commande OCI affichant cpu-pool et gpu-pool à l'état ACTIVE

Tâche 6 : installation du plug-in de périphérique NVIDIA

Installez le module d'extension de périphérique NVIDIA afin que Kubernetes puisse détecter et programmer des charges de travail sur le GPU.

  1. Appliquez le plug-in de périphérique NVIDIA DaemonSet.

    kubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.14.1/nvidia-device-plugin.yml
  2. Attendez que les pods de plugin soient prêts.

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

    Remarque : certaines images de noeud de GPU OKE incluent un module d'extension de périphérique NVIDIA préinstallé (nvidia-gpu-device-plugin). Si l'image l'inclut déjà, l'application de l'élément DaemonSet en amont crée une deuxième instance qui n'entraîne pas de conflits. Le script automatisé (entry_point.sh deploy-vllm) l'installe toujours pour s'assurer que la détection de GPU fonctionne quelle que soit la version de l'image du noeud.

  3. Vérifiez que le GPU est allouable par Kubernetes.

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

    Sortie attendue :

    NAME          GPUs
    10.0.10.x     1
  4. Appliquez un patch à CoreDNS pour tolérer les taches de noeud de GPU. Dans les clusters où les noeuds de GPU sont les seuls noeuds de processus actif, les pods CoreDNS ne peuvent pas programmer car les noeuds de GPU OKE comportent une tache nvidia.com/gpu=present:NoSchedule. Sans DNS, les pods ne peuvent pas résoudre les registres d'images ou les URL de téléchargement de modèle.

    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

    Remarque : cette étape n'est nécessaire que lorsque les noeuds GPU sont les seuls noeuds de processus actif du cluster. Si vous disposez d'un pool de noeuds de CPU dédié pour les charges globales système, CoreDNS y planifie par défaut et ce patch n'est pas nécessaire.

Tâche 7 : configurer le stockage

Appliquez le volume de blocs OCI StorageClass pour fournir un stockage persistant pour les poids de modèle.

  1. Appliquez la définition StorageClass.

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

    Le fichier définit deux niveaux de performances :

    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 Les performances Cas d'emploi
    oci-block-storage-enc Equilibré (vpusPerGB: 10) Par défaut, économique pour la plupart des modèles
    oci-block-storage-hp Hautes performances (vpusPerGB: 20) Chargement plus rapide des modèles pour les modèles plus grands
  2. Vérifiez que StorageClasses est disponible.

    kubectl get storageclass

Remarque : pour les déploiements à plusieurs noeuds nécessitant un stockage partagé sur plusieurs pods, utilisez OCI File Storage Service (NFS) avec le mode d'accès ReadWriteMany au lieu de Block Volumes.

Tâche 8 : développer le système de fichiers du noeud GPU

Les volumes d'initialisation OCI disposent d'une partition fixe d'environ 47 Go, quelle que soit la taille de volume d'initialisation indiquée. L'image de conteneur vLLM seule est d'environ 10 Go et le poids du modèle nécessite de l'espace supplémentaire. Vous devez développer le système de fichiers avant de déployer vLLM pour éviter les expulsions DiskPressure.

Remarque : il s'agit d'une exigence propre à OCI. Le volume d'initialisation est provisionné à 200 Go, mais le système d'exploitation ne partitionne que ~47 Go par défaut. L'espace restant doit être réclamé manuellement.

  1. Vérifiez la taille actuelle du système de fichiers sur le noeud GPU.

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

    La sortie affiche environ 47 Go au total, confirmant que l'extension est nécessaire.

  2. Créez un pod privilégié sur le noeud GPU pour exécuter les commandes d'extension.

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

    Attendez que le pod démarre.

    kubectl wait --for=condition=Ready pod/expand-disk --timeout=60s
  3. Exécutez les quatre étapes d'extension en une seule commande kubectl exec. Les exécuter ensemble évite le risque que kubectl exec renvoie le code de sortie 137 (SIGKILL) entre les étapes, ce qui peut se produire lors des E/S de disque lourdes sur l'hôte.

    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 /
    '
    Étape Commande Description
    1 growpart /dev/sda 3 Développer la partition 3 pour utiliser le disque complet
    2 pvresize /dev/sda3 Redimensionner le volume physique LVM
    3 lvextend -l +100%FREE /dev/ocivolume/root Etendre le volume logique
    4 xfs_growfs / Développer le système de fichiers XFS pour remplir le volume

    Remarque : les quatre opérations sont idempotentes. Si l'exécutable renvoie le code de sortie 137, vous pouvez réexécuter le bloc entier en toute sécurité. Recherchez EXPANSION_COMPLETE dans la sortie pour confirmer la réussite.

  4. Redémarrez le kubelet pour que le noeud signale le stockage allouable mis à jour, puis vérifiez et nettoyez.

    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

    Remarque : la commande nsenter entre l'espace de noms PID de l'hôte pour accéder à systemd. Un élément chroot /host systemctl restart kubelet brut échoue car il ne peut pas se connecter au bus systemd à partir d'un chroot.

    La sortie attendue devrait afficher un total d'environ 189 Go.

Tâche 9 : déployer la pile de production vLLM

Installez la pile d'inférence vLLM à l'aide de Helm.

  1. Ajoutez le référentiel Helm vLLM.

    helm repo add vllm https://vllm-project.github.io/production-stack
    helm repo update
  2. Consultez le fichier de valeurs Helm. production_stack_specification.yaml configure le modèle, les ressources et le stockage pour 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"

    Remarque : le modèle openai/gpt-oss-20b est un modèle de mélange d'experts (MoE) avec des paramètres totaux 20B et des paramètres actifs 3.6B par passe aval. Il est publié sous la licence Apache 2.0, de sorte qu'aucun jeton Hugging Face n'est requis. L'image de conteneur vllm/vllm-openai fournit un serveur d'API compatible OpenAI, permettant aux clients d'utiliser des appels de kit SDK OpenAI standard sur l'adresse auto-hébergée.

  3. Déployez la pile. N'utilisez pas --wait ici car le pod de routeur CrashLoop jusqu'à ce que des patches soient appliqués à l'étape suivante.

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

    Attendez que le pod du moteur vLLM démarre (le routeur sera ensuite corrigé).

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

    Remarque : le pod de moteur met plusieurs minutes à être prêt car il télécharge les poids du modèle au premier démarrage. Si le pod reste dans ContainerCreating, l'image de conteneur (~10 Go) est toujours extraite. Utilisez kubectl describe pod <pod-name> pour vérifier la progression.

  4. Appliquez un patch au déploiement du routeur. Le routeur a besoin d'une tolérance GPU (de sorte qu'il peut planifier lorsque les noeuds GPU sont les seuls noeuds ayant une capacité) et d'une augmentation des limites de mémoire (la valeur par défaut de 500 Mi peut entraîner 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"}
    ]'

    Remarque : la tolérance de GPU est nécessaire car les noeuds de GPU OKE ont une tache nvidia.com/gpu=present:NoSchedule qui empêche la planification des charges globales non GPU. Comme le routeur n'utilise pas de GPU mais doit s'exécuter quelque part, cette tolérance lui permet de programmer sur les noeuds de GPU. Dans les clusters avec des pools de noeuds de CPU dédiés, cette tolérance n'est pas nécessaire.

  5. Vérifiez que la version Helm est déployée.

    helm list

    Sortie de terminal de la liste helm affichant la pile vllm déployée

  6. Vérifiez que les pods sont en cours d'exécution.

    kubectl get pods

    Sortie attendue :

    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 obtient des pods affichant des pods de routeur et de moteur en cours d'exécution 1/1

  7. Vérifiez la progression du chargement du modèle dans les journaux de pod.

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

    Attendez qu'un message indiquant que le modèle a été chargé et que le serveur est prêt à accepter les demandes s'affiche.

Tâche 10 : test de l'adresse d'inférence

Vérifiez que le déploiement traite les demandes d'inférence. La pile de production vLLM expose une API compatible OpenAI via le service de routeur, de sorte que tout client SDK OpenAI ou toute commande curl peut interagir avec elle.

Le diagramme suivant illustre le cycle de vie de la demande d'inférence : de la demande client à la logique de sélection du moteur du routeur, en passant par les phases de préremplissage et de décodage du moteur vLLM et en retour en tant que réponse en flux.

Diagramme de séquence présentant le cycle de vie des demandes d'inférence du client au routeur vers le moteur vLLM et le GPU

  1. Répertoriez les modèles disponibles pour vérifier que le déploiement est en bon état.

    kubectl get svc vllm-router-service

    Le service de routeur fournit la passerelle d'API à tous les modèles déployés. Le cluster utilisant une adresse privée, vous accédez au service via kubectl port-forward.

  2. Démarrez un transfert de port de votre machine locale vers le service de routeur. Ouvrez un nouveau terminal (gardez le tunnel SSH en cours d'exécution dans l'autre) et exécutez :

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

    Cette opération met en correspondance localhost:8080 sur votre ordinateur avec le port 80 sur le service de routeur à l'intérieur du cluster.

    Remarque de sécurité : kubectl port-forward établit une liaison locale et n'expose pas le service publiquement. Il s'agit du moyen le plus sûr de tester l'utilisation d'un cluster privé sur un tunnel de bastion.

    Remarque : la commande port-forward s'exécute au premier plan. Gardez ce terminal ouvert pendant le test. Appuyez sur Ctrl+C pour l'arrêter lorsque vous avez terminé.

  3. Dans un autre terminal, vérifiez que le modèle est disponible en interrogeant l'adresse des modèles.

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

    Sortie attendue :

    {
        "object": "list",
        "data": [
            {
                "id": "openai/gpt-oss-20b",
                "object": "model",
                "created": 1234567890,
                "owned_by": "vllm"
            }
        ]
    }
  4. Envoyer une demande d'achèvement de texte.

    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

    Résultat attendu (abrégé) :

    {
        "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. Envoyer une demande de fin de discussion. Il s'agit du même format que celui utilisé par le kit SDK Python OpenAI. Il s'agit du moyen le plus courant d'interagir par programmation avec les LLM.

    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

    Résultat attendu (abrégé) :

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

    Les deux réponses incluent le texte généré par le modèle dans le tableau choices, les statistiques d'utilisation de jeton et une valeur finish_reason de stop (le modèle s'est terminé naturellement) ou length (la sortie a été tronquée à max_tokens).

    Remarque : le nom de modèle dans la demande d'API est le chemin de modèle complet (openai/gpt-oss-20b), qui correspond au champ modelURL dans les valeurs Helm. Tout client compatible OpenAI peut utiliser cette adresse en définissant base_url sur http://localhost:8080/v1.

    Sortie de terminal affichant la demande de fin de discussion en boucle et la réponse JSON d'openai/gpt-oss-20b

  6. Mesurez la latence de bout en bout pour une courte demande.

    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

    Sur un seul GPU A10, attendez-vous à 1 à 3 secondes de bout en bout pour les fins de production courtes. Le délai de réception du premier jeton (TTFT) est généralement compris entre 50 et 200 ms en fonction de la longueur de l'invite. Pour un débit plus élevé, augmentez replicaCount dans les valeurs Helm pour ajouter d'autres répliques de moteur derrière le routeur.

Tâche 11 : affichage via l'équilibreur de charge OCI (facultatif)

Rendez l'adresse d'inférence accessible en externe via un équilibreur de charge OCI.

Remarque sur la sécurité : par défaut, l'API d'inférence est exposée au réseau Internet public. Ne l'activez pas en production sans contrôle TLS, authentification (clé d'API/JWT/mTLS) et liste d'autorisation IP ou WAF. Si vous devez l'exposer, placez-le devant un contrôleur entrant ou une passerelle d'API qui applique des limites d'authentification et de débit, ou utilisez un équilibreur de charge interne.

  1. Appliquez un patch au service de routeur pour qu'il utilise un type LoadBalancer.

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

    Remarque : si vous voulez un équilibreur de charge interne, ajoutez l'annotation OCI au service (exemple ci-dessous). L'adresse reste ainsi privée dans le VCN.

    kubectl annotate svc vllm-router-service \
      "service.beta.kubernetes.io/oci-load-balancer-internal"="true"
  2. Attendez que l'adresse IP externe soit affectée.

    kubectl get svc vllm-router-service -w

    Résultat attendu une fois l'équilibreur de charge provisionné :

    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 obtenir la sortie svc montrant les services de routeur et de moteur

  3. Testez l'adresse externe.

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

Tâche 12 : configurer le parallélisme de capteur multi-GPU (avancé)

Déployez des modèles plus importants sur plusieurs GPU sur des formes Bare Metal.

Le parallélisme de Tensor divise un modèle en plusieurs GPU sur un seul noeud. Cela est nécessaire lorsque les besoins en mémoire d'un modèle dépassent un seul GPU. Par exemple, Meta Llama 3.1 70B nécessite environ 140 Go de mémoire GPU, ce qui dépasse la capacité d'un seul GPU, mais s'adapte aux GPU 8x A100 80 Go ou 8x H100.

  1. Créez une clé secrète Kubernetes avec votre jeton Hugging Face. Les modèles fermés tels que Llama 3.1 70B nécessitent une authentification.

    kubectl create secret generic hf-token-secret \
      --from-literal=token=YOUR_HUGGINGFACE_TOKEN
  2. Mettez à jour production_stack_specification.yaml avec une configuration multi-GPU.

    servingEngineSpec:
      modelSpec:
      - name: "llama70b"
        repository: "vllm/vllm-openai"
        tag: "latest"
        modelURL: "meta-llama/Llama-3.1-70B-Instruct"
    
        replicaCount: 1
        tensorParallelSize: 8
    
        requestCPU: 32
        requestMemory: "256Gi"
        requestGPU: 8
    
        hf_token:
          secretName: "hf-token-secret"
          secretKey: "token"
    
        pvcStorage: "500Gi"
        pvcAccessMode:
          - ReadWriteOnce
        storageClass: "oci-block-storage-enc"
    
        nodeSelector:
          node.kubernetes.io/instance-type: "BM.GPU.H100.8"
        tolerations:
          - key: "nvidia.com/gpu"
            operator: "Exists"
            effect: "NoSchedule"
    
        extraArgs:
          - "--max-model-len=8192"
          - "--gpu-memory-utilization=0.95"
          - "--tensor-parallel-size=8"
  3. Déployer avec les valeurs mises à jour. Comme pour la tâche 9, n'utilisez pas --wait. Le routeur CrashLoop jusqu'à l'application du patch.

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

    Ensuite, appliquez un patch au routeur (identique à la tâche 9, étape 4) et vérifiez :

    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. Vérifiez que le pod est en cours d'exécution et que tous les GPU sont en cours d'utilisation.

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

Remarque : assurez-vous que votre cluster OKE dispose d'un pool de noeuds avec la forme de GPU Bare Metal appropriée (par exemple, BM.GPU.H100.8) avant de déployer une configuration multiGPU.

Tâche 13 : utiliser OCI Object Storage pour les modèles (avancé)

Chargez les poids des modèles à partir d'OCI Object Storage au lieu de les télécharger à partir de Hugging Face. Cela est utile pour les modèles privés, les téléchargements plus rapides dans OCI ou les environnements sans accès Internet externe.

  1. Téléchargez vos poids de modèle vers un bucket OCI Object Storage. Accédez à Stockage > Object Storage dans la console OCI et créez un bucket si vous n'en disposez pas déjà.

  2. Créez une URL de demande pré-authentifiée pour votre bucket. Dans la console OCI, sélectionnez votre bucket, cliquez sur Demandes pré-authentifiées et créez une demande avec accès en lecture.

  3. Mettez à jour production_stack_specification.yaml pour utiliser l'URL de demande pré-authentifiée.

    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. Déployez avec les valeurs mises à jour (sans --wait). Voir la tâche 9 pour savoir pourquoi).

    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. Vérifiez que le modèle est chargé à partir d'Object Storage en consultant les journaux de pod.

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

Tâche 14 : Nettoyer les ressources

Supprimez toutes les ressources déployées pour éviter les frais continus.

  1. Enlevez les ressources Kubernetes à l'aide du script de nettoyage.

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

    Cette action désinstalle la version Helm, supprime toutes les ressources PersistentVolumeClaims, PersistentVolumes et vLLM personnalisées.

  2. Supprimez le cluster OKE et toutes les ressources réseau OCI.

    ./entry_point.sh cleanup

    Les ressources suivantes sont supprimées dans l'ordre :

    • Pool de noeuds GPU
    • Cluster OKE
    • Hôte bastion (si créé)
    • Sous-réseaux (API, processus actif, équilibreur de charge, bastion)
    • Listes de sécurité
    • Passerelle de service, passerelle NAT et passerelle Internet
    • Tables de routage
    • VCN
  3. Vérifiez que toutes les ressources ont été enlevées dans la console OCI sous Services de développeur > Clusters Kubernetes et Networking > Réseaux cloud virtuels.

    ./entry_point.sh cleanup

    Sortie de terminal du nettoyage entry_point.sh affichant le démontage complet des ressources

Remarque : assurez-vous que toutes les ressources sont supprimées pour éviter les frais continus. Les instances de GPU et les volumes de blocs entraînent des coûts, même en cas d'inactivité.

Etapes suivantes

Ce tutoriel a déployé une pile d'inférence fonctionnelle. Pour les charges de travail de production, envisagez les améliorations suivantes :

Accusés de réception

Ressources de formation supplémentaires

Explorez d'autres ateliers sur le site docs.oracle.com/learn ou accédez à d'autres contenus d'apprentissage gratuits sur le canal Oracle Learning YouTube. En outre, visitez le site education.oracle.com/learning-explorer pour devenir un explorateur Oracle Learning.

Pour obtenir de la documentation sur le produit, consultez Oracle Help Center.