Diffuser un LLM avec plusieurs GPU dans GKE


Ce tutoriel explique comment diffuser un grand modèle de langage (LLM) avec des GPU en mode Google Kubernetes Engine (GKE). Ce tutoriel crée un cluster GKE qui utilise plusieurs GPU L4 et prépare l'infrastructure GKE pour diffuser l'un des modèles suivants :

Selon le format de données du modèle, le nombre de GPU varie. Dans ce tutoriel, chaque modèle utilise deux GPU L4. Pour en savoir plus, consultez la page Calculer la quantité de GPU.

Avant de suivre ce tutoriel sur GKE, nous vous recommandons de vous familiariser avec la Présentation des GPU dans GKE.

Objectifs

Ce tutoriel est destiné aux ingénieurs MLOps ou DevOps ou aux administrateurs de plate-forme qui souhaitent utiliser les fonctionnalités d'orchestration GKE pour obtenir des inférences à partir d'un LLM.

Ce tutoriel couvre les étapes suivantes :

  1. Créer un cluster et des pools de nœuds
  2. Préparer votre charge de travail.
  3. Déployer votre charge de travail.
  4. Interagir avec l'interface du LLM.

Avant de commencer

Avant de commencer, effectuez les tâches suivantes :

  • Activez l'API Google Kubernetes Engine.
  • Activer l'API Google Kubernetes Engine
  • Si vous souhaitez utiliser Google Cloud CLI pour cette tâche, installez puis initialisez gcloud CLI. Si vous avez déjà installé gcloud CLI, assurez-vous de disposer de la dernière version en exécutant la commande gcloud components update.
  • Certains modèles ont des exigences supplémentaires. Assurez-vous de remplir les conditions suivantes:

Préparer votre environnement

  1. Dans la console Google Cloud, démarrez une instance Cloud Shell :
    Ouvrir Cloud Shell

  2. Définissez les variables d'environnement par défaut :

    gcloud config set project PROJECT_ID
    export PROJECT_ID=$(gcloud config get project)
    export REGION=us-central1
    

    Remplacez PROJECT_ID par l'ID du projet Google Cloud.

Créer un cluster GKE et un pool de nœuds

Vous pouvez diffuser les LLM sur des GPU dans un cluster GKE Autopilot ou GKE Standard. Nous vous recommandons d'utiliser un cluster GKE Autopilot pour une expérience Kubernetes entièrement gérée. Pour choisir le mode de fonctionnement GKE le mieux adapté à vos charges de travail, consultez la section Choisir un mode de fonctionnement GKE.

Autopilot

  1. Dans Cloud Shell, exécutez la commande suivante :

    gcloud container clusters create-auto l4-demo \
      --project=${PROJECT_ID} \
      --region=${REGION} \
      --release-channel=rapid
    

    GKE crée un cluster Autopilot avec des nœuds de processeur et de GPU, à la demande des charges de travail déployées.

  2. Configurez kubectl de manière à communiquer avec votre cluster :

    gcloud container clusters get-credentials l4-demo --region=${REGION}
    

Standard

  1. Dans Cloud Shell, exécutez la commande suivante pour créer un cluster standard qui utilise la fédération d'identité de charge de travail pour GKE :

    gcloud container clusters create l4-demo --location ${REGION} \
      --workload-pool ${PROJECT_ID}.svc.id.goog \
      --enable-image-streaming \
      --node-locations=$REGION-a \
      --workload-pool=${PROJECT_ID}.svc.id.goog \
      --machine-type n2d-standard-4 \
      --num-nodes 1 --min-nodes 1 --max-nodes 5 \
      --release-channel=rapid
    

    La création du cluster peut prendre plusieurs minutes.

  2. Exécutez la commande suivante pour créer un pool de nœuds pour votre cluster :

    gcloud container node-pools create g2-standard-24 --cluster l4-demo \
      --accelerator type=nvidia-l4,count=2,gpu-driver-version=latest \
      --machine-type g2-standard-24 \
      --enable-autoscaling --enable-image-streaming \
      --num-nodes=0 --min-nodes=0 --max-nodes=3 \
      --node-locations $REGION-a,$REGION-c --region $REGION --spot
    

    GKE crée les ressources suivantes pour le LLM :

    • Un cluster Google Kubernetes Engine (GKE) standard public.
    • Un pool de nœuds avec le type de machine g2-standard-24 réduit à 0 nœud. Vous n'êtes facturé pour aucun GPU tant que vous n'avez pas lancé de pods qui demandent des GPU. Ce pool de nœuds provisionne des VM Spot, dont la tarification est inférieure à celle des VM Compute Engine standard par défaut mais qui n'offrent aucune garantie de disponibilité. Vous pouvez supprimer l'option --spot de cette commande et le sélecteur de nœud cloud.google.com/gke-spot dans la configuration text-generation-inference.yaml pour utiliser des VM à la demande.
  3. Configurez kubectl de manière à communiquer avec votre cluster :

    gcloud container clusters get-credentials l4-demo --region=${REGION}
    

Préparer votre charge de travail

La section suivante montre comment configurer votre charge de travail en fonction du modèle que vous souhaitez utiliser :

Llama 2 70b

  1. Définissez les variables d'environnement par défaut :

    export HF_TOKEN=HUGGING_FACE_TOKEN
    

    Remplacez HUGGING_FACE_TOKEN par votre jeton HuggingFace.

  2. Créez un secret Kubernetes pour le jeton HuggingFace :

    kubectl create secret generic l4-demo \
        --from-literal=HUGGING_FACE_TOKEN=${HF_TOKEN} \
        --dry-run=client -o yaml | kubectl apply -f -
    
  3. Créez le fichier manifeste text-generation-inference.yaml suivant :

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: llm
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: llm
      template:
        metadata:
          labels:
            app: llm
        spec:
          containers:
          - name: llm
            image: ghcr.io/huggingface/text-generation-inference:1.4.3
            resources:
              requests:
                cpu: "10"
                memory: "60Gi"
                nvidia.com/gpu: "2"
              limits:
                cpu: "10"
                memory: "60Gi"
                nvidia.com/gpu: "2"
            env:
            - name: MODEL_ID
              value: meta-llama/Llama-2-70b-chat-hf
            - name: NUM_SHARD
              value: "2"
            - name: PORT
              value: "8080"
            - name: QUANTIZE
              value: bitsandbytes-nf4
            - name: HUGGING_FACE_HUB_TOKEN
              valueFrom:
                secretKeyRef:
                  name: l4-demo
                  key: HUGGING_FACE_TOKEN
            volumeMounts:
              - mountPath: /dev/shm
                name: dshm
              - mountPath: /data
                name: ephemeral-volume
          volumes:
            - name: dshm
              emptyDir:
                  medium: Memory
            - name: ephemeral-volume
              ephemeral:
                volumeClaimTemplate:
                  metadata:
                    labels:
                      type: ephemeral
                  spec:
                    accessModes: ["ReadWriteOnce"]
                    storageClassName: "premium-rwo"
                    resources:
                      requests:
                        storage: 150Gi
          nodeSelector:
            cloud.google.com/gke-accelerator: "nvidia-l4"
            cloud.google.com/gke-spot: "true"

    Dans le fichier manifeste :

    • NUM_SHARD doit être défini sur 2, car le modèle nécessite deux GPU NVIDIA L4.
    • QUANTIZE est défini sur bitsandbytes-nf4, ce qui signifie que le modèle est chargé en 4 bits au lieu de 32 bits. Cela permet à GKE de réduire la quantité de mémoire GPU nécessaire et d'améliorer la vitesse d'inférence. Cependant, la justesse du modèle peut diminuer. Pour savoir comment calculer le nombre de GPU à demander, consultez la section Calculer le nombre de GPU.
  4. Appliquez le fichier manifeste :

    kubectl apply -f text-generation-inference.yaml
    

    Le résultat ressemble à ce qui suit :

    deployment.apps/llm created
    
  5. Vérifiez l'état du modèle :

    kubectl get deploy
    

    Le résultat ressemble à ce qui suit :

    NAME          READY   UP-TO-DATE   AVAILABLE   AGE
    llm           1/1     1            1           20m
    
  6. Affichez les journaux du déploiement en cours d'exécution :

    kubectl logs -l app=llm
    

    Le résultat ressemble à ce qui suit :

    {"timestamp":"2024-03-09T05:08:14.751646Z","level":"INFO","message":"Warming up model","target":"text_generation_router","filename":"router/src/main.rs","line_number":291}
    {"timestamp":"2024-03-09T05:08:19.961136Z","level":"INFO","message":"Setting max batch total tokens to 133696","target":"text_generation_router","filename":"router/src/main.rs","line_number":328}
    {"timestamp":"2024-03-09T05:08:19.961164Z","level":"INFO","message":"Connected","target":"text_generation_router","filename":"router/src/main.rs","line_number":329}
    {"timestamp":"2024-03-09T05:08:19.961171Z","level":"WARN","message":"Invalid hostname, defaulting to 0.0.0.0","target":"text_generation_router","filename":"router/src/main.rs","line_number":343}
    

Mixtral 8x7b

  1. Définissez les variables d'environnement par défaut :

    export HF_TOKEN=HUGGING_FACE_TOKEN
    

    Remplacez HUGGING_FACE_TOKEN par votre jeton HuggingFace.

  2. Créez un secret Kubernetes pour le jeton HuggingFace :

    kubectl create secret generic l4-demo \
        --from-literal=HUGGING_FACE_TOKEN=${HF_TOKEN} \
        --dry-run=client -o yaml | kubectl apply -f -
    
  3. Créez le fichier manifeste text-generation-inference.yaml suivant :

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: llm
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: llm
      template:
        metadata:
          labels:
            app: llm
        spec:
          containers:
          - name: llm
            image: ghcr.io/huggingface/text-generation-inference:1.4.3
            resources:
              requests:
                cpu: "5"
                memory: "40Gi"
                nvidia.com/gpu: "2"
              limits:
                cpu: "5"
                memory: "40Gi"
                nvidia.com/gpu: "2"
            env:
            - name: MODEL_ID
              value: mistralai/Mixtral-8x7B-Instruct-v0.1
            - name: NUM_SHARD
              value: "2"
            - name: PORT
              value: "8080"
            - name: QUANTIZE
              value: bitsandbytes-nf4
            - name: HUGGING_FACE_HUB_TOKEN
              valueFrom:
                secretKeyRef:
                  name: l4-demo
                  key: HUGGING_FACE_TOKEN
            volumeMounts:
              - mountPath: /dev/shm
                name: dshm
              - mountPath: /data
                name: ephemeral-volume
          volumes:
            - name: dshm
              emptyDir:
                  medium: Memory
            - name: ephemeral-volume
              ephemeral:
                volumeClaimTemplate:
                  metadata:
                    labels:
                      type: ephemeral
                  spec:
                    accessModes: ["ReadWriteOnce"]
                    storageClassName: "premium-rwo"
                    resources:
                      requests:
                        storage: 100Gi
          nodeSelector:
            cloud.google.com/gke-accelerator: "nvidia-l4"
            cloud.google.com/gke-spot: "true"

    Dans le fichier manifeste :

    • NUM_SHARD doit être défini sur 2, car le modèle nécessite deux GPU NVIDIA L4.
    • QUANTIZE est défini sur bitsandbytes-nf4, ce qui signifie que le modèle est chargé en 4 bits au lieu de 32 bits. Cela permet à GKE de réduire la quantité de mémoire GPU nécessaire et d'améliorer la vitesse d'inférence. Toutefois, cela peut réduire la précision du modèle. Pour savoir comment calculer le nombre de GPU à demander, consultez la section Calculer la quantité de GPU.
  4. Appliquez le fichier manifeste :

    kubectl apply -f text-generation-inference.yaml
    

    Le résultat ressemble à ce qui suit :

    deployment.apps/llm created
    
  5. Vérifiez l'état du modèle :

    watch kubectl get deploy
    

    Une fois le déploiement prêt, le résultat ressemble à ce qui suit. Pour quitter la lecture, appuyez sur CTRL + C :

    NAME          READY   UP-TO-DATE   AVAILABLE   AGE
    llm           1/1     1            1           10m
    
  6. Affichez les journaux du déploiement en cours d'exécution :

    kubectl logs -l app=llm
    

    Le résultat ressemble à ce qui suit :

    {"timestamp":"2024-03-09T05:08:14.751646Z","level":"INFO","message":"Warming up model","target":"text_generation_router","filename":"router/src/main.rs","line_number":291}
    {"timestamp":"2024-03-09T05:08:19.961136Z","level":"INFO","message":"Setting max batch total tokens to 133696","target":"text_generation_router","filename":"router/src/main.rs","line_number":328}
    {"timestamp":"2024-03-09T05:08:19.961164Z","level":"INFO","message":"Connected","target":"text_generation_router","filename":"router/src/main.rs","line_number":329}
    {"timestamp":"2024-03-09T05:08:19.961171Z","level":"WARN","message":"Invalid hostname, defaulting to 0.0.0.0","target":"text_generation_router","filename":"router/src/main.rs","line_number":343}
    

Falcon 40b

  1. Créez le fichier manifeste text-generation-inference.yaml suivant :

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: llm
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: llm
      template:
        metadata:
          labels:
            app: llm
        spec:
          containers:
          - name: llm
            image: ghcr.io/huggingface/text-generation-inference:1.4.3
            resources:
              requests:
                cpu: "10"
                memory: "60Gi"
                nvidia.com/gpu: "2"
              limits:
                cpu: "10"
                memory: "60Gi"
                nvidia.com/gpu: "2"
            env:
            - name: MODEL_ID
              value: tiiuae/falcon-40b-instruct
            - name: NUM_SHARD
              value: "2"
            - name: PORT
              value: "8080"
            - name: QUANTIZE
              value: bitsandbytes-nf4
            volumeMounts:
              - mountPath: /dev/shm
                name: dshm
              - mountPath: /data
                name: ephemeral-volume
          volumes:
            - name: dshm
              emptyDir:
                  medium: Memory
            - name: ephemeral-volume
              ephemeral:
                volumeClaimTemplate:
                  metadata:
                    labels:
                      type: ephemeral
                  spec:
                    accessModes: ["ReadWriteOnce"]
                    storageClassName: "premium-rwo"
                    resources:
                      requests:
                        storage: 175Gi
          nodeSelector:
            cloud.google.com/gke-accelerator: "nvidia-l4"
            cloud.google.com/gke-spot: "true"

    Dans le fichier manifeste :

    • NUM_SHARD doit être défini sur 2, car le modèle nécessite deux GPU NVIDIA L4.
    • QUANTIZE est défini sur bitsandbytes-nf4, ce qui signifie que le modèle est chargé en 4 bits au lieu de 32 bits. Cela permet à GKE de réduire la quantité de mémoire GPU nécessaire et d'améliorer la vitesse d'inférence. Cependant, la justesse du modèle peut diminuer. Pour savoir comment calculer le nombre de GPU à demander, consultez la section Calculer la quantité de GPU.
  2. Appliquez le fichier manifeste :

    kubectl apply -f text-generation-inference.yaml
    

    Le résultat ressemble à ce qui suit :

    deployment.apps/llm created
    
  3. Vérifiez l'état du modèle :

    watch kubectl get deploy
    

    Une fois le déploiement prêt, le résultat ressemble à ce qui suit. Pour quitter la lecture, appuyez sur CTRL + C :

    NAME          READY   UP-TO-DATE   AVAILABLE   AGE
    llm           1/1     1            1           10m
    
  4. Affichez les journaux du déploiement en cours d'exécution :

    kubectl logs -l app=llm
    

    Le résultat ressemble à ce qui suit :

    {"timestamp":"2024-03-09T05:08:14.751646Z","level":"INFO","message":"Warming up model","target":"text_generation_router","filename":"router/src/main.rs","line_number":291}
    {"timestamp":"2024-03-09T05:08:19.961136Z","level":"INFO","message":"Setting max batch total tokens to 133696","target":"text_generation_router","filename":"router/src/main.rs","line_number":328}
    {"timestamp":"2024-03-09T05:08:19.961164Z","level":"INFO","message":"Connected","target":"text_generation_router","filename":"router/src/main.rs","line_number":329}
    {"timestamp":"2024-03-09T05:08:19.961171Z","level":"WARN","message":"Invalid hostname, defaulting to 0.0.0.0","target":"text_generation_router","filename":"router/src/main.rs","line_number":343}
    

Créer un service de type ClusterIP

  1. Créez le fichier manifeste llm-service.yaml suivant :

    apiVersion: v1
    kind: Service
    metadata:
      name: llm-service
    spec:
      selector:
        app: llm
      type: ClusterIP
      ports:
        - protocol: TCP
          port: 80
          targetPort: 8080
    
  2. Appliquez le fichier manifeste :

    kubectl apply -f llm-service.yaml
    

Déployer une interface de chat

Utilisez Gradio pour créer une application Web qui vous permet d'interagir avec votre modèle. Gradio est une bibliothèque Python dotée d'un wrapper ChatInterface qui crée des interfaces utilisateur pour les chatbots.

Llama 2 70b

  1. Créez un fichier nommé gradio.yaml :

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: gradio
      labels:
        app: gradio
    spec:
      strategy:
        type: Recreate
      replicas: 1
      selector:
        matchLabels:
          app: gradio
      template:
        metadata:
          labels:
            app: gradio
        spec:
          containers:
          - name: gradio
            image: us-docker.pkg.dev/google-samples/containers/gke/gradio-app:v1.0.0
            resources:
              requests:
                cpu: "512m"
                memory: "512Mi"
              limits:
                cpu: "1"
                memory: "512Mi"
            env:
            - name: CONTEXT_PATH
              value: "/generate"
            - name: HOST
              value: "http://llm-service"
            - name: LLM_ENGINE
              value: "tgi"
            - name: MODEL_ID
              value: "llama-2-70b"
            - name: USER_PROMPT
              value: "[INST] prompt [/INST]"
            - name: SYSTEM_PROMPT
              value: "prompt"
            ports:
            - containerPort: 7860
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: gradio-service
    spec:
      type: LoadBalancer
      selector:
        app: gradio
      ports:
      - port: 80
        targetPort: 7860
  2. Appliquez le fichier manifeste :

    kubectl apply -f gradio.yaml
    
  3. Recherchez l'adresse IP externe du service :

    kubectl get svc
    

    Le résultat ressemble à ce qui suit :

    NAME             TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)        AGE
    gradio-service   LoadBalancer   10.24.29.197   34.172.115.35   80:30952/TCP   125m
    
  4. Copiez l'adresse IP externe présente dans la colonne EXTERNAL-IP.

  5. Consultez l'interface du modèle depuis votre navigateur Web en utilisant l'adresse IP externe avec le port exposé :

    http://EXTERNAL_IP
    

Mixtral 8x7b

  1. Créez un fichier nommé gradio.yaml :

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: gradio
      labels:
        app: gradio
    spec:
      strategy:
        type: Recreate
      replicas: 1
      selector:
        matchLabels:
          app: gradio
      template:
        metadata:
          labels:
            app: gradio
        spec:
          containers:
          - name: gradio
            image: us-docker.pkg.dev/google-samples/containers/gke/gradio-app:v1.0.0
            resources:
              requests:
                cpu: "512m"
                memory: "512Mi"
              limits:
                cpu: "1"
                memory: "512Mi"
            env:
            - name: CONTEXT_PATH
              value: "/generate"
            - name: HOST
              value: "http://llm-service"
            - name: LLM_ENGINE
              value: "tgi"
            - name: MODEL_ID
              value: "mixtral-8x7b"
            - name: USER_PROMPT
              value: "[INST] prompt [/INST]"
            - name: SYSTEM_PROMPT
              value: "prompt"
            ports:
            - containerPort: 7860
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: gradio-service
    spec:
      type: LoadBalancer
      selector:
        app: gradio
      ports:
      - port: 80
        targetPort: 7860
  2. Appliquez le fichier manifeste :

    kubectl apply -f gradio.yaml
    
  3. Recherchez l'adresse IP externe du service :

    kubectl get svc
    

    Le résultat ressemble à ce qui suit :

    NAME             TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)        AGE
    gradio-service   LoadBalancer   10.24.29.197   34.172.115.35   80:30952/TCP   125m
    
  4. Copiez l'adresse IP externe présente dans la colonne EXTERNAL-IP.

  5. Consultez l'interface du modèle depuis votre navigateur Web en utilisant l'adresse IP externe avec le port exposé :

    http://EXTERNAL_IP
    

Falcon 40b

  1. Créez un fichier nommé gradio.yaml :

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: gradio
      labels:
        app: gradio
    spec:
      strategy:
        type: Recreate
      replicas: 1
      selector:
        matchLabels:
          app: gradio
      template:
        metadata:
          labels:
            app: gradio
        spec:
          containers:
          - name: gradio
            image: us-docker.pkg.dev/google-samples/containers/gke/gradio-app:v1.0.0
            resources:
              requests:
                cpu: "512m"
                memory: "512Mi"
              limits:
                cpu: "1"
                memory: "512Mi"
            env:
            - name: CONTEXT_PATH
              value: "/generate"
            - name: HOST
              value: "http://llm-service"
            - name: LLM_ENGINE
              value: "tgi"
            - name: MODEL_ID
              value: "falcon-40b-instruct"
            - name: USER_PROMPT
              value: "User: prompt"
            - name: SYSTEM_PROMPT
              value: "Assistant: prompt"
            ports:
            - containerPort: 7860
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: gradio-service
    spec:
      type: LoadBalancer
      selector:
        app: gradio
      ports:
      - port: 80
        targetPort: 7860
  2. Appliquez le fichier manifeste :

    kubectl apply -f gradio.yaml
    
  3. Recherchez l'adresse IP externe du service :

    kubectl get svc
    

    Le résultat ressemble à ce qui suit :

    NAME             TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)        AGE
    gradio-service   LoadBalancer   10.24.29.197   34.172.115.35   80:30952/TCP   125m
    
  4. Copiez l'adresse IP externe présente dans la colonne EXTERNAL-IP.

  5. Consultez l'interface du modèle depuis votre navigateur Web en utilisant l'adresse IP externe avec le port exposé :

    http://EXTERNAL_IP
    

Calculer le nombre de GPU

Le nombre de GPU dépend de la valeur de l'option QUANTIZE. Dans ce tutoriel, QUANTIZE est défini sur bitsandbytes-nf4, ce qui signifie que le modèle est chargé en 4 bits.

Un modèle avec 70 milliards de paramètres nécessiterait au moins 40 Go de mémoire GPU, à raison de 70 milliards fois 4 bits (70 milliards x 4 bits= 35 Go) en prenant en compte une surcharge de 5 Go. Dans ce cas, un GPU L4 ne disposerait pas de suffisamment de mémoire. Par conséquent, les exemples de ce tutoriel utilisent la mémoire de deux GPU L4 de mémoire (2 x 24 = 48 Go). Cette configuration est suffisante pour exécuter Falcon 40b ou Llama 2 70b sur les GPU L4.

Effectuer un nettoyage

Pour éviter que les ressources utilisées lors de ce tutoriel soient facturées sur votre compte Google Cloud, supprimez le projet contenant les ressources, ou conservez le projet et supprimez les ressources individuelles.

Supprimer le cluster

Pour éviter que les ressources que vous avez créées dans ce guide ne soient facturées sur votre compte Google Cloud, supprimez le cluster GKE :

gcloud container clusters delete l4-demo --region ${REGION}

Étapes suivantes