Note :

Déployer l'infrastructure de production vLLM OpenAI sur Oracle Kubernetes Engine (OKE)

Présentation

Les entreprises qui adoptent de grands modèles de langage (LLM) pour des 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 d'importants avantages : un contrôle complet de la conformité et de la confidentialité des données, une latence d'inférence de moins de 100 millisecondes en éliminant les allers-retours réseau, des coûts prévisibles à grande échelle et la liberté d'ajuster et de servir n'importe quel modèle à code source libre sans être lié à un fournisseur.

Toutefois, la création d'une pile d'inférence de grand modèle de production à partir de zéro est complexe. Il nécessite une orchestration de conteneurs sensible au GPU, un acheminement intelligent des demandes sur plusieurs répliques de modèle, un stockage persistant pour les poids importants du modèle et une surveillance continue, le tout intégré et fonctionnant de manière fiable.

Oracle Cloud Infrastructure offre plusieurs chemins pour l'inférence liée à l'IA. Le service IA générative d'OCI offre une expérience entièrement gérée avec des grappes d'IA dédiées isolées de votre location, idéale pour les équipes qui souhaitent démarrer rapidement avec les modèles pris en charge. Ce tutoriel adopte l'approche alternative suivante : 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èles et les paramètres de service, ou les équipes qui forment et peaufinent des modèles personnalisés et veulent les servir directement. OCI fournit des instances de processeur graphique sans système d'exploitation avec des processeurs graphiques NVIDIA A10, A100 et H100, connectés par un réseau en grappe RDMA à très faible latence, ce qui vous donne le même niveau de contrôle matériel que celui sur place tout en bénéficiant de l'élasticité du nuage.

La pile de production vLLM résout la complexité de l'inférence auto-hébergée en fournissant une plate-forme à code source libre 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 plus élevé que les cadres de service standard grâce à une gestion efficace de la mémoire GPU et à l'optimisation de la mémoire cache KV. Combiné aux formes OKE et OCI GPU, vous obtenez une plate-forme d'inférence prête pour la production 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 pile de production vLLM.

Ce tutoriel 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.

Note : Ce tutoriel provisionne les ressources étape par étape à l'aide de l'interface de ligne de commande OCI pour vous aider à comprendre le flux complet des ressources en nuage OCI requises pour un déploiement d'inférence de processeur graphique. Pour les environnements de production, il est recommandé de codifier cette infrastructure à l'aide de Terraform ou du gestionnaire de ressources OCI (Shepherd) pour les déploiements reproductibles contrôlés par version.

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

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

Service Objet
Oracle Kubernetes Engine (OKE) Grappe Kubernetes gérée pour l'orchestration de conteneurs et la programmation de charges de travail GPU
OCI Compute (formes de processeur graphique) Instances de processeur graphique NVIDIA A10 (24 Go) et A100 (80 Go) pour l'inférence de modèle
Volumes par blocs OCI Stockage persistant pour les pondérations de modèle avec niveaux de performance configurables
Réseau en nuage virtuel (VCN) OCI Infrastructure réseau, y compris les sous-réseaux, les passerelles et les listes de sécurité
Équilibreur de charge OCI Accès externe aux points d'extrémité d'inférence
Service d'hôte bastion pour OCI Tunnels SSH gérés pour l'accès à une grappe privée
Service de stockage d'objets pour OCI Autre source de modèle utilisant des URL de demande préauthentifiée

Objectifs

Dans ce tutoriel, vous allez :

Conditions requises

Note : Les exemples de sorties et de captures d'écran de ce tutoriel utilisent us-chicago-1. Vous pouvez effectuer un déploiement dans n'importe quelle région prise en charge en définissant OCI_REGION. La capacité du processeur graphique varie selon la région et le domaine de disponibilité. Vérifiez donc que la forme du processeur graphique cible est disponible avant le déploiement. Vérifiez la disponibilité des formes de processeur graphique par région et soyez prêt à essayer un autre domaine de disponibilité (GPU_AD_INDEX) si vous rencontrez des erreurs de capacité.

Note : Ce tutoriel provisionne des ressources GPU payantes (par exemple, VM.GPU.A10.1). Il ne s'agit pas d'une charge de travail OCI de type Toujours gratuit. Exécutez toujours les étapes de nettoyage lorsque vous avez terminé pour éviter les frais en cours.

Note : Ce tutoriel déploie openai/gpt-oss-20b, un modèle sous licence Apache 2.0 à partir de OpenAI. Aucun jeton Face Hugging n'est requis. Si vous voulez 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 votre compartiment dans la console OCI. Naviguez jusqu'à 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 montrant l'OCID du compartiment

  2. Exportez la variable d'environnement requise.

    export OCI_COMPARTMENT_ID="ocid1.compartment.oc1..xxxxx"
  3. Facultativement, remplacez la configuration par défaut en définissant l'une des variables d'environnement suivantes.

    Variable Valeur par défaut Description
    OCI_REGION us-ashburn-1 Région OCI pour déploiement
    OCI_PROFILE DEFAULT Profil de configuration de l'interface de ligne de commande OCI
    CLUSTER_NAME production-stack Nom de la grappe OKE
    GPU_SHAPE VM.GPU.A10.1 Forme de calcul GPU pour le groupe de noeuds
    GPU_NODE_COUNT 1 Nombre de noeuds GPU dans le groupe
    GPU_BOOT_VOLUME_GB 200 Taille du volume de démarrage en Go pour les noeuds GPU
    CPU_BOOT_VOLUME_GB 100 Taille du volume de démarrage en Go pour les noeuds d'UC
    GPU_AD_INDEX 1 Index de domaine de disponibilité (basé sur 0) pour le positionnement de GPU
    PRIVATE_CLUSTER true Régler à false pour un point d'extrémité d'API Kubernetes public
    KUBERNETES_VERSION v1.31.10 Version de Kubernetes pour la grappe OKE

    Par exemple, pour 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. Vérifiez les formes de processeur graphique disponibles et sélectionnez-en une en fonction de vos exigences de taille de modèle.

    Forme Processeurs graphiques Type de GPU Mémoire du processeur graphique Recommandé pour
    VM.GPU.A10.1 1 NVIDIA A10 24 Go Modèles de paramètres 7B–13B
    VM.GPU.A10.2 2 NVIDIA A10 48 Go Tensor parallèle aux petits modèles
    BM.GPU4.8 8 NVIDIA A100 40 GB 320 Go Modèles 70B, économiques
    BM.GPU.A100-v2.8 8 NVIDIA A100 80 GB 640 Go 70B+ modèles de paramètre
    BM.GPU.H100.8 8 NVIDIA H100 640 Go Les plus grands modèles, prise en charge RDMA

    Note : Les formes sans système d'exploitation (BM.*) fournissent du matériel dédié sans frais généraux de virtualisation et prennent en charge le parallélisme de tenseur multi-GPU. Les formes de machine virtuelle (VM.*) sont plus rentables pour les modèles plus petits.

    Note : 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 s'adaptent généralement à 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éployer à l'aide du script automatisé (démarrage rapide)

La pile de production vLLM comprend un script de déploiement automatisé qui provisionne toutes les ressources OCI et qui 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. Cloner 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 votre 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 montrant la création du VCN, de la grappe, de l'hôte bastion et du groupe de noeuds

    Pour les grappes publiques (PRIVATE_CLUSTER=false), la configuration crée toute l'infrastructure et déploie la pile vLLM dans 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 grappes privées (par défaut), la configuration crée l'infrastructure mais ne peut pas accéder 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

    Résultat attendu :

    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

Note : Si les deux pods affichent le statut Running, votre déploiement est prêt. Passez à Tâche 10 : Tester le point d'extrémité d'inférence.

Note : Les instances de GPU sont soumises à des contraintes de capacité OCI. Si le script reste dans la boucle "En attente du noeud GPU" pendant plus de 15 minutes, la forme GPU peut ne pas être disponible dans le domaine de disponibilité sélectionné. Vérifiez le statut du groupe de noeuds avec oci ce node-pool get et recherchez les erreurs de capacité d'hôte insuffisante. Pour résoudre ce problème, nettoyez-le avec ./entry_point.sh cleanup et redéployez-le avec un domaine de disponibilité différent (par exemple, GPU_AD_INDEX=0 ou GPU_AD_INDEX=2) ou une forme de GPU différente (par exemple, GPU_SHAPE=VM.GPU.A10.2).

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

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

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

  1. Créez un réseau 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éer une passerelle de service pour accéder à Oracle Services Network. Le contrôleur de nuage OKE utilise Oracle Services pour initialiser les noeuds de travail (définir les étiquettes de domaine de disponibilité, supprimer les tintes d'initialisation). Sans passerelle de service, les noeuds GPU peuvent rester à l'état non initialisé et le provisionnement des volumes par blocs échoue.

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

    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)

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

  6. Créez une liste de sécurité avec les règles de trafic entrant et sortant 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)

    Note de sécurité : Cet exemple de liste de sécurité est volontairement large pour simplifier. Pour la production, limitez SSH au sous-réseau de l'hôte bastion et à votre intervalle d'adresses IP, et préférez des listes de sécurité ou des groupes de sécurité de réseau distincts par sous-réseau afin que le sous-réseau de l'é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 en attachant des règles SSH uniquement au sous-réseau de l'hôte bastion. Vous pouvez conserver les blocs CIDR de pod/service Kubernetes sur le sous-réseau de traitement et omettre SSH entièrement à partir du sous-réseau de l'équilibreur de charge.

    Fractionnement facultatif (recommandé) : Créez une petite liste de sécurité SSH seulement pour le sous-réseau d'hôte bastion et une liste distincte pour les sous-réseaux de travailleurs/équilibreurs de charge.

    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)

    Note : 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 bloc CIDR de VCN). Utilisation : Attachez BASTION_SL_ID au sous-réseau de l'hôte bastion, WORKER_SL_ID aux sous-réseaux de l'API/du travailleur et LB_SL_ID au sous-réseau de l'équilibreur de charge.

    Note : Les règles CIDR des pods Kubernetes (10.244.0.0/16) et CIDR des services (10.96.0.0/16) sont requises pour que les noeuds de travail GPU puissent s'enregistrer auprès de la grappe. La règle ICMP de type 3 code 4 permet la détection de MTU de chemin, ce qui évite les problèmes de fragmentation des paquets.

  7. Créez des sous-réseaux. La grappe nécessite quatre sous-réseaux : un pour le point d'extrémité de l'API Kubernetes, un pour les noeuds de travail, un pour les équilibreurs de charge et un pour l'hôte bastion utilisé pour accéder à la grappe privée.

    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é Objet
    Point d'extrémité d'API 10.0.0.0/28 Privée Serveur d'API Kubernetes
    Noeuds de travail 10.0.10.0/24 Privée Noeuds de calcul GPU
    Équilibreurs de charge 10.0.20.0/24 Publique Accès au service externe
    Hôte bastion 10.0.30.0/24 Publique Tunnel SSH pour l'accès à la grappe privée

Tâche 4 : Créer la grappe OKE

Déployer une grappe Kubernetes gérée sur OKE à l'aide des ressources de réseau créées dans la tâche 3. Le provisionnement de la grappe prend environ 10 minutes. Ce tutoriel crée une grappe privée (la valeur par défaut du script), qui ne consomme pas d'adresse IP publique réservée pour le point d'extrémité de l'API Kubernetes. Les grappes privées 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 la grappe OKE avec un point d'extrémité privé.

    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 retourne un ID demande de travail. Obtenez l'ID grappe à partir de la liste des grappes.

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

    Note : Les grappes privées ne requièrent pas d'adresse IP publique réservée. Les noeuds de travail accèdent toujours à Internet au moyen de 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 au moyen de l'hôte bastion (configuré dans les étapes suivantes).

  2. Attendez que le cluster devienne actif. Cette étape dure 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 retourne ACTIVE.

    Facultatif : Affichez un sommaire de statut concis (y compris le point d'extrémité de l'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 montrant la grappe OKE à l'état ACTIVE

    kubectl get nodes -o wide

    Sortie d'obtention de noeuds kubectl montrant deux noeuds prêts

  3. Créez un hôte bastion OCI pour accéder à la grappe privée. L'hôte bastion fournit un tunnel SSH géré vers le point d'extrémité d'API Kubernetes privé.

    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)

    Note : Remplacez YOUR_PUBLIC_IP/32 par votre adresse IP publique courante. 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

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

  4. Téléchargez le fichier kubeconfig à l'aide du point d'extrémité privé.

    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 du point d'extrémité privé 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 d'hôte bastion. Vous aurez besoin d'un fichier de clé publique 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 transmet le port local 6443 à 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

    Note : 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 votre agent SSH.

  8. Mettez à jour le fichier kubeconfig pour vous connecter au moyen du 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

    Note : L'indicateur --insecure-skip-tls-verify est requis car le certificat de grappe a été émis pour l'adresse IP du point d'extrémité privé, et non 127.0.0.1. Cela 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 non par défaut (par exemple, API_KEY_AUTH), mettez à jour le fichier kubeconfig pour l'utiliser. Par défaut, le kubeconfig généré est le profil DEFAULT pour la génération de jetons.

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

    Conseil : Les étapes 6 à 9 sont automatisées par ./entry_point.sh tunnel, qui s'enregistre également automatiquement si le tunnel SSH tombe en panne lors d'opérations de longue durée telles que l'expansion de disque. Exécutez-le dans un terminal distinct et laissez-le en cours d'exécution pendant toute la durée de votre session.

  10. Vérifiez l'accès aux grappes.

kubectl get nodes

À ce stade, la sortie n'affiche aucun noeud, car le groupe de noeuds GPU n'a pas encore été ajouté.

No resources found

Tâche 5 : Ajouter un groupe de noeuds GPU

Ajoutez un groupe de noeuds avec des instances de calcul GPU à la grappe OKE.

  1. Recherchez la dernière image de noeud OKE compatible GPU. OKE nécessite 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 appropriée 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}"

    Note : Les filtres d'interrogation pour les images de processeur graphique 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é comportant 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}"

    Note : La capacité du processeur graphique varie selon la région et le domaine de disponibilité. Si la création du groupe de noeuds échoue avec une erreur de capacité d'hôte insuffisante, essayez un autre domaine de disponibilité (GPU_AD_INDEX) ou une autre forme de GPU, ou demandez de la capacité au moyen de votre processus OCI normal.

  3. Créez le groupe de noeuds GPU avec un volume de démarrage 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"}]'

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

  4. Attendez que les noeuds GPU deviennent prêts. Cela prend généralement 5 à 10 minutes pendant que le noeud provisionne, démarre, installe les pilotes GPU et s'enregistre auprès du cluster.

    Note : Les instances de GPU sont soumises à des contraintes de capacité. Si le groupe 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 de 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é au moyen de la console OCI ou d'un ticket de soutien.

    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'

    Résultat attendu :

    NAME          GPUs
    10.0.10.x     1
  6. Appliquez le correctif CoreDNS pour programmer sur les noeuds GPU. Les noeuds GPU OKE ont une teinte nvidia.com/gpu=present:NoSchedule. Dans les grappes qui n'ont que des noeuds GPU, les pods système tels que CoreDNS ne peuvent pas être programmés 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

    Note : Si votre grappe comporte un groupe de noeuds d'UC dédié pour les charges de travail du système, cette étape n'est pas nécessaire. Ce correctif n'est nécessaire que lorsque les noeuds GPU sont les seuls noeuds de la grappe.

    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 montrant le groupe d'UC et le groupe d'UC gpu à l'état ACTIF

Tâche 6 : Installer le plugiciel d'appareil NVIDIA

Installez le plugiciel d'appareil NVIDIA afin que Kubernetes puisse détecter et programmer les charges de travail sur le processeur graphique.

  1. Appliquez le plugiciel 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 plugiciel soient prêts.

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

    Note : Certaines images de noeud de processeur graphique OKE incluent un plugiciel d'appareil NVIDIA préinstallé (nvidia-gpu-device-plugin). Si l'image l'inclut déjà, l'application de la commande DaemonSet en amont crée une deuxième instance qui n'entraîne pas de conflit. 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 de noeud.

  3. Vérifiez que le processeur graphique peut être alloué par Kubernetes.

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

    Résultat attendu :

    NAME          GPUs
    10.0.10.x     1
  4. Appliquez le correctif CoreDNS pour tolérer les tintes de noeud GPU. Dans les grappes où les noeuds GPU sont les seuls noeuds de travail, les pods CoreDNS ne peuvent pas programmer car les noeuds GPU OKE portent une teinte 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

    Note : Cette étape n'est nécessaire que lorsque les noeuds GPU sont les seuls noeuds de travail de la grappe. Si vous disposez d'un groupe de noeuds d'UC dédié pour les charges de travail du système, CoreDNS y est programmé par défaut et ce correctif n'est pas nécessaire.

Tâche 7 : Configurer le stockage

Appliquez le volume par blocs OCI StorageClass pour fournir un stockage persistant pour les pondérations 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 Performance Cas d'utilisation
    oci-block-storage-enc Équilibré (vpusPerGB: 10) Par défaut, rentable pour la plupart des modèles
    oci-block-storage-hp Haute performance (vpusPerGB: 20) Chargement plus rapide des modèles plus volumineux
  2. Vérifiez que StorageClasses est disponible.

    kubectl get storageclass

Note : Pour les déploiements à plusieurs noeuds nécessitant un stockage partagé dans plusieurs pods, utilisez le service de stockage de fichiers (NFS) OCI avec le mode d'accès ReadWriteMany au lieu du service de volumes par blocs.

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

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

Note : Il s'agit d'une exigence propre à OCI. Le volume de démarrage 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 courante 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 affichera un total d'environ 47 Go, confirmant que l'expansion est nécessaire.

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

    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'expansion dans une seule commande kubectl exec. Les exécuter ensemble évite le risque que kubectl exec retourne le code de sortie 137 (SIGKILL) entre les étapes, ce qui peut se produire pendant les 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 Objet
    1 growpart /dev/sda 3 Développez 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 Étendre le volume logique
    4 xfs_growfs / Développer le système de fichiers XFS pour remplir le volume

    Note : Les quatre opérations sont idempotentes. Si l'exécute 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 afin 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

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

    La sortie attendue doit 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. Vérifiez 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"

    Note : Le modèle openai/gpt-oss-20b est un modèle Mixture of Experts (MoE) avec 20B paramètres totaux et 3.6B paramètres actifs par transmission. Il est publié sous la licence Apache 2.0, donc aucun jeton Hugging Face n'est requis. L'image de conteneur vllm/vllm-openai fournit un serveur d'API compatible avec OpenAI, permettant aux clients d'utiliser des appels de trousse SDK OpenAI standard pour votre point d'extrémité auto-hébergé.

  3. Déployez la pile. N'utilisez pas --wait ici, car le module de réseautage du routeur CrashLoop jusqu'à ce qu'il soit corrigé à 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 corrigé ensuite).

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

    Note : Le pod du moteur prend plusieurs minutes pour devenir Prêt, car il télécharge les pondérations 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. Corriger le déploiement du routeur. Le routeur a besoin d'une tolérance de GPU (de sorte qu'il peut programmer lorsque les nœuds GPU sont les seuls nœuds avec capacité) et des limites de mémoire accrues (la valeur par défaut de 500 Mi peut causer 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"}
    ]'

    Note : La tolérance de GPU est nécessaire, car les noeuds GPU OKE ont une teinte nvidia.com/gpu=present:NoSchedule qui empêche la programmation des charges de travail autres que GPU. Comme le routeur n'utilise pas de GPU mais doit s'exécuter quelque part, cette tolérance lui permet de planifier sur des noeuds GPU. Dans les grappes avec des groupes de noeuds d'UC dédiés, cette tolérance n'est pas nécessaire.

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

    helm list

    Sortie de terminal de la liste d'arrivées montrant la pile vllm déployée

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

    kubectl get pods

    Résultat attendu :

    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 montrant les 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 du 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 : Tester le point d'extrémité d'inférence

Vérifiez que le déploiement répond aux demandes d'inférence. La pile de production vLLM expose une API compatible avec OpenAI au moyen du service de routeur, de sorte que toute commande client de trousse SDK OpenAI ou curl peut interagir avec celle-ci.

Le diagramme suivant présente 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 retour en tant que réponse en continu.

Diagramme de séquence montrant le cycle de vie de la demande d'inférence du client au moyen du routeur vers le moteur vLLM et le GPU

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

    kubectl get svc vllm-router-service

    Le service de routeur fournit la passerelle d'API à tous les modèles déployés. Comme la grappe utilise un point d'extrémité privé, vous accédez au service au moyen de kubectl port-forward.

  2. Démarrez un port-forward 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

    Cela mappe localhost:8080 sur votre machine au port 80 sur le service de routeur dans la grappe.

    Note de sécurité : kubectl port-forward lie localement et n'expose pas le service publiquement. Il s'agit du moyen le plus sûr de le tester lors de l'utilisation d'une grappe privée sur un tunnel d'hôte bastion.

    Note : 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 le point d'extrémité des modèles.

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

    Résultat attendu :

    {
        "object": "list",
        "data": [
            {
                "id": "openai/gpt-oss-20b",
                "object": "model",
                "created": 1234567890,
                "owned_by": "vllm"
            }
        ]
    }
  4. Envoyer une demande de saisie semi-automatique.

    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

    Sortie attendue (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 d'achèvement de clavardage. Il s'agit du même format utilisé par la trousse SDK Python OpenAI et il s'agit du moyen le plus courant d'interagir avec les LLM par programmation.

    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

    Sortie attendue (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 du 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).

    Note : Le nom du modèle dans la demande d'API est le chemin complet du modèle (openai/gpt-oss-20b), qui correspond au champ modelURL dans les valeurs Helm. Tout client compatible OpenAI peut utiliser ce point d'extrémité en réglant base_url à http://localhost:8080/v1.

    Sortie de terminal affichant la demande d'achèvement de clavardage curl et la réponse JSON d'openai/gpt-oss-20b

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

    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 1 à 3 secondes de bout en bout pour des achèvements courts. Le délai pour le premier jeton (TTFT) est généralement de 50 à 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 : Présenter au moyen de l'équilibreur de charge OCI (facultatif)

Rendez le point d'extrémité d'inférence accessible à l'externe au moyen d'un équilibreur de charge OCI.

Note de sécurité : L'API d'inférence est exposée à l'Internet public par défaut. Ne l'activez pas en production sans TLS, authentification (clé d'API/JWT/mTLS) et liste d'autorisation IP ou contrôles WAF. Si vous devez l'exposer, vous pouvez le présenter avec un contrôleur de trafic entrant ou une passerelle d'API qui applique des limites d'authentification et de débit, ou utiliser un équilibreur de charge interne.

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

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

    Note : Si vous voulez un équilibreur de charge interne, ajoutez l'annotation OCI au service (exemple ci-dessous). Le point d'extrémité reste privé 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

    Sortie attendue 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 le point d'extrémité 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 Tensor Multi-GPU (avancé)

Déployez des modèles plus volumineux sur plusieurs processeurs graphiques sur des formes sans système d'exploitation.

Le parallélisme de tenseur divise un modèle en plusieurs GPU sur un seul noeud. Cela est requis 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é de n'importe quel GPU, mais s'adapte sur 8x A100 80 Go ou 8x H100 GPU.

  1. Créez une clé secrète Kubernetes avec votre jeton Face Hugging. 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, --waitn'utilisez pas . Le routeur CrashLoop jusqu'à ce qu'il soit corrigé.

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

    Appliquez ensuite un correctif 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 utilisés.

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

Note : Assurez-vous que votre grappe OKE comporte un groupe de noeuds avec la forme GPU sans système d'exploitation appropriée (par exemple, BM.GPU.H100.8) avant de déployer une configuration multi-GPU.

Tâche 13 : Utiliser le stockage d'objets OCI pour les modèles (avancé)

Chargez les pondérations de modèle à partir du stockage d'objets OCI au lieu du téléchargement à partir du visage Hugging. Cette fonction 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. Chargez vos pondérations de modèle dans un seau de stockage d'objets OCI. Naviguez jusqu'à Stockage > Stockage d'objets dans la console OCI et créez un seau si vous n'en avez pas déjà un.

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

  3. Mettez à jour production_stack_specification.yaml pour utiliser l'URL de la 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 les chargements de modèle à partir du stockage d'objets 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. Supprimez 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 de Helm, supprime toutes les ressources PersistentVolumeClaims, PersistentVolumes et vLLM personnalisées.

  2. Supprimez la grappe OKE et toutes les ressources de réseau OCI.

    ./entry_point.sh cleanup

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

    • Groupe de noeuds GPU
    • Grappe OKE
    • Hôte bastion (si créé)
    • Sous-réseaux (API, programme, équilibreur de charge, hôte 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é supprimées dans la console OCI sous Services de développement > Grappes Kubernetes et Réseau > Réseaux en nuage virtuels.

    ./entry_point.sh cleanup

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

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

Étape suivante

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

Remerciements

Ressources d'apprentissage supplémentaires

Explorez d'autres laboratoires sur le site docs.oracle.com/learn ou accédez à plus de contenu d'apprentissage gratuit sur le canal Oracle Learning YouTube. De plus, visitez education.oracle.com/learning-explorer pour devenir un explorateur Oracle Learning.

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