Gestisci un LLM con più GPU in GKE


Questo tutorial mostra come gestire un modello linguistico di grandi dimensioni (LLM) con GPU in Google Kubernetes Engine (GKE) utilizzando più GPU per un'inferenza efficiente e scalabile. Questo tutorial crea un cluster GKE che utilizza più GPU L4 e prepara l'infrastruttura GKE a gestire uno dei seguenti modelli:

Il numero di GPU varia a seconda del formato dei dati del modello. In questo tutorial, ogni modello utilizza due GPU L4. Per scoprire di più, consulta la sezione Calcolo del numero di GPU.

Prima di completare questo tutorial in GKE, ti consigliamo di approfondire Informazioni sulle GPU in GKE.

Obiettivi

Questo tutorial è destinato agli ingegneri MLOps o DevOps o agli amministratori di piattaforma che vogliono utilizzare funzionalità di orchestrazione GKE per gestire gli LLM.

Questo tutorial illustra i seguenti passaggi:

  1. Creare un cluster e i pool di nodi.
  2. Prepara il carico di lavoro.
  3. Esegui il deployment del carico di lavoro.
  4. Interagire con l'interfaccia LLM.

Prima di iniziare

Prima di iniziare, assicurati di aver eseguito le seguenti attività:

  • Abilita l'API Google Kubernetes Engine.
  • Abilita l'API Google Kubernetes Engine
  • Se vuoi utilizzare Google Cloud CLI per questa attività, installa e quindi initialize gcloud CLI. Se hai già installato gcloud CLI, ottieni la versione più recente eseguendo gcloud components update.
  • Alcuni modelli hanno requisiti aggiuntivi. Assicurati di soddisfare questi requisiti:

prepara l'ambiente

  1. Nella console Google Cloud, avvia un'istanza di Cloud Shell:
    Apri Cloud Shell

  2. Imposta le variabili di ambiente predefinite:

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

    Sostituisci PROJECT_ID con il tuo ID progetto Google Cloud.

Crea un cluster GKE e un pool di nodi

Puoi gestire LLM su GPU in un cluster GKE Autopilot o Standard. Ti consigliamo di utilizzare un cluster Autopilot per un'esperienza Kubernetes completamente gestita. Per scegliere la modalità operativa GKE più adatta ai tuoi carichi di lavoro, vedi Scegliere una modalità operativa di GKE.

Autopilot

  1. In Cloud Shell, esegui questo comando:

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

    GKE crea un cluster Autopilot con nodi di CPU e GPU, come richiesto dai carichi di lavoro di cui è stato eseguito il deployment.

  2. Configura kubectl per comunicare con il tuo cluster:

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

Standard

  1. In Cloud Shell, esegui questo comando per creare un cluster standard che utilizzi la Federazione delle identità per i carichi di lavoro per 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 creazione del cluster potrebbe richiedere diversi minuti.

  2. Esegui questo comando per creare un pool di nodi per il tuo 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 crea le seguenti risorse per l'LLM:

    • Un cluster pubblico di Google Kubernetes Engine (GKE) Standard.
    • Un pool di nodi con tipo di macchina g2-standard-24 fatto lo scale down a 0 nodi. Non ti viene addebitato alcun costo per le GPU finché non avvii i pod che richiedono GPU. Questo pool di nodi esegue il provisioning delle VM spot, che hanno un prezzo inferiore rispetto alle VM standard predefinite di Compute Engine e non forniscono alcuna garanzia di disponibilità. Puoi rimuovere il flag --spot da questo comando e il selettore di nodi cloud.google.com/gke-spot nella configurazione text-generation-inference.yaml per utilizzare le VM on demand.
  3. Configura kubectl per comunicare con il tuo cluster:

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

Prepara il carico di lavoro

La sezione seguente mostra come configurare il carico di lavoro in base al modello che vuoi utilizzare:

Lama 3 70b

  1. Imposta le variabili di ambiente predefinite:

    export HF_TOKEN=HUGGING_FACE_TOKEN
    

    Sostituisci HUGGING_FACE_TOKEN con il token HuggingFace.

  2. Crea un secret Kubernetes per il token HuggingFace:

    kubectl create secret generic l4-demo \
        --from-literal=HUGGING_FACE_TOKEN=${HF_TOKEN} \
        --dry-run=client -o yaml | kubectl apply -f -
    
  3. Crea il seguente manifest text-generation-inference.yaml:

    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:2.0.4
            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/Meta-Llama-3-70B-Instruct
            - name: NUM_SHARD
              value: "2"
            - name: MAX_INPUT_TOKENS
              value: "2048"
            - 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"
    
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: llm-service
    spec:
      selector:
        app: llm
      type: ClusterIP
      ports:
        - protocol: TCP
          port: 80
          targetPort: 8080

    In questo manifest:

    • NUM_SHARD deve essere 2 perché il modello richiede due GPU NVIDIA L4.
    • QUANTIZE è impostato su bitsandbytes-nf4, il che significa che il modello viene caricato a 4 bit anziché a 32 bit. Ciò consente a GKE di ridurre la quantità di memoria GPU necessaria e migliora la velocità di inferenza. Tuttavia, l'accuratezza del modello può diminuire. Per scoprire come calcolare le GPU da richiedere, consulta Calcolo del numero di GPU.
  4. Applica il manifest:

    kubectl apply -f text-generation-inference.yaml
    

    L'output è simile al seguente:

    deployment.apps/llm created
    
  5. Verifica lo stato del modello:

    kubectl get deploy
    

    L'output è simile al seguente:

    NAME          READY   UP-TO-DATE   AVAILABLE   AGE
    llm           1/1     1            1           20m
    
  6. Visualizza i log del deployment in esecuzione:

    kubectl logs -l app=llm
    

    L'output è simile al seguente:

    {"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. Imposta le variabili di ambiente predefinite:

    export HF_TOKEN=HUGGING_FACE_TOKEN
    

    Sostituisci HUGGING_FACE_TOKEN con il token HuggingFace.

  2. Crea un secret Kubernetes per il token HuggingFace:

    kubectl create secret generic l4-demo \
        --from-literal=HUGGING_FACE_TOKEN=${HF_TOKEN} \
        --dry-run=client -o yaml | kubectl apply -f -
    
  3. Crea il seguente manifest text-generation-inference.yaml:

    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"

    In questo manifest:

    • NUM_SHARD deve essere 2 perché il modello richiede due GPU NVIDIA L4.
    • QUANTIZE è impostato su bitsandbytes-nf4, il che significa che il modello viene caricato a 4 bit anziché a 32 bit. Ciò consente a GKE di ridurre la quantità di memoria GPU necessaria e migliora la velocità di inferenza. Tuttavia, ciò potrebbe ridurre l'accuratezza del modello. Per scoprire come calcolare le GPU da richiedere, consulta Calcolo del numero di GPU.
  4. Applica il manifest:

    kubectl apply -f text-generation-inference.yaml
    

    L'output è simile al seguente:

    deployment.apps/llm created
    
  5. Verifica lo stato del modello:

    watch kubectl get deploy
    

    Quando il deployment è pronto, l'output è simile al seguente. Per uscire dallo smartwatch, digita CTRL + C:

    NAME          READY   UP-TO-DATE   AVAILABLE   AGE
    llm           1/1     1            1           10m
    
  6. Visualizza i log del deployment in esecuzione:

    kubectl logs -l app=llm
    

    L'output è simile al seguente:

    {"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. Crea il seguente manifest text-generation-inference.yaml:

    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"

    In questo manifest:

    • NUM_SHARD deve essere 2 perché il modello richiede due GPU NVIDIA L4.
    • QUANTIZE è impostato su bitsandbytes-nf4, il che significa che il modello viene caricato a 4 bit anziché a 32 bit. Ciò consente a GKE di ridurre la quantità di memoria GPU necessaria e migliora la velocità di inferenza. Tuttavia, l'accuratezza del modello può diminuire. Per scoprire come calcolare le GPU da richiedere, consulta Calcolo del numero di GPU.
  2. Applica il manifest:

    kubectl apply -f text-generation-inference.yaml
    

    L'output è simile al seguente:

    deployment.apps/llm created
    
  3. Verifica lo stato del modello:

    watch kubectl get deploy
    

    Quando il deployment è pronto, l'output è simile al seguente. Per uscire dallo smartwatch, digita CTRL + C:

    NAME          READY   UP-TO-DATE   AVAILABLE   AGE
    llm           1/1     1            1           10m
    
  4. Visualizza i log del deployment in esecuzione:

    kubectl logs -l app=llm
    

    L'output è simile al seguente:

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

Crea un servizio di tipo ClusterIP

  1. Crea il seguente manifest llm-service.yaml:

    apiVersion: v1
    kind: Service
    metadata:
      name: llm-service
    spec:
      selector:
        app: llm
      type: ClusterIP
      ports:
        - protocol: TCP
          port: 80
          targetPort: 8080
    
  2. Applica il manifest:

    kubectl apply -f llm-service.yaml
    

Esegui il deployment di un'interfaccia di chat

Usa Gradio per creare un'applicazione web che ti consenta di interagire con il tuo modello. Gradio è una libreria Python che ha un wrapper ChatInterface che crea le interfacce utente per i chatbot.

Lama 3 70b

  1. Crea un file denominato 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.3
            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: "meta-llama/Meta-Llama-3-70B-Instruct"
            - name: USER_PROMPT
              value: "<|begin_of_text|><|start_header_id|>user<|end_header_id|> prompt <|eot_id|><|start_header_id|>assistant<|end_header_id|>"
            - name: SYSTEM_PROMPT
              value: "prompt <|eot_id|>"
            ports:
            - containerPort: 7860
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: gradio-service
    spec:
      type: LoadBalancer
      selector:
        app: gradio
      ports:
      - port: 80
        targetPort: 7860
  2. Applica il manifest:

    kubectl apply -f gradio.yaml
    
  3. Trova l'indirizzo IP esterno del servizio:

    kubectl get svc
    

    L'output è simile al seguente:

    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. Copia l'indirizzo IP esterno dalla colonna EXTERNAL-IP.

  5. Visualizza l'interfaccia del modello nel browser web utilizzando l'indirizzo IP esterno con la porta esposta:

    http://EXTERNAL_IP
    

Mixtral 8x7b

  1. Crea un file denominato 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. Applica il manifest:

    kubectl apply -f gradio.yaml
    
  3. Trova l'indirizzo IP esterno del servizio:

    kubectl get svc
    

    L'output è simile al seguente:

    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. Copia l'indirizzo IP esterno dalla colonna EXTERNAL-IP.

  5. Visualizza l'interfaccia del modello nel browser web utilizzando l'indirizzo IP esterno con la porta esposta:

    http://EXTERNAL_IP
    

Falcon 40B

  1. Crea un file denominato 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. Applica il manifest:

    kubectl apply -f gradio.yaml
    
  3. Trova l'indirizzo IP esterno del servizio:

    kubectl get svc
    

    L'output è simile al seguente:

    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. Copia l'indirizzo IP esterno dalla colonna EXTERNAL-IP.

  5. Visualizza l'interfaccia del modello nel browser web utilizzando l'indirizzo IP esterno con la porta esposta:

    http://EXTERNAL_IP
    

Calcolo della quantità di GPU

La quantità di GPU dipende dal valore del flag QUANTIZE. In questo tutorial, QUANTIZE è impostato su bitsandbytes-nf4, il che significa che il modello viene caricato in 4 bit.

Un modello da 70 miliardi di parametri richiederebbe un minimo di 40 GB di memoria GPU, pari a 70 miliardi di volte a 4 bit (70 miliardi x 4 bit= 35 GB) e considera un overhead di 5 GB. In questo caso, una singola GPU L4 non avrà memoria sufficiente. Pertanto, gli esempi in questo tutorial utilizzano due GPU L4 di memoria (2 x 24 = 48 GB). Questa configurazione è sufficiente per eseguire Falcon 40b o Llama 3 70b nelle GPU L4.

Esegui la pulizia

Per evitare che al tuo Account Google Cloud vengano addebitati costi relativi alle risorse utilizzate in questo tutorial, elimina il progetto che contiene le risorse oppure mantieni il progetto ed elimina le singole risorse.

Elimina il cluster

Per evitare che al tuo account Google Cloud vengano addebitati costi relativi alle risorse create in questa guida, elimina il cluster GKE:

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

Passaggi successivi