Entrega un LLM con varias GPUs en GKE


En este instructivo, se muestra cómo entregar un modelo grande de lenguaje (LLM) con GPU en Google Kubernetes Engine (GKE). En este instructivo, se crea un clúster de GKE que usa varias GPU L4 y prepara la infraestructura de GKE para entregar cualquiera de los siguientes modelos:

Según el formato de datos del modelo, la cantidad de GPU varía. En este instructivo, cada modelo usa dos GPU L4. Para obtener más información, consulta Calcula la cantidad de GPU.

Antes de completar este instructivo en GKE, te recomendamos que aprendas Información sobre las GPU en GKE

Objetivos

Este instructivo está dirigido a administradores de plataforma o ingenieros de MLOps o DevOps que deseen usar las funciones de organización de GKE para entregar LLM.

En este instructivo, se abarcan los siguientes pasos:

  1. Crea un clúster y grupos de nodos.
  2. Prepara tu carga de trabajo.
  3. Implementa tu carga de trabajo.
  4. Interactúa con la interfaz de LLM.

Antes de comenzar

Antes de comenzar, asegúrate de haber realizado las siguientes tareas:

  • Habilita la API de Google Kubernetes Engine.
  • Habilitar la API de Google Kubernetes Engine
  • Si deseas usar Google Cloud CLI para esta tarea, instala y, luego, inicializa gcloud CLI. Si ya instalaste gcloud CLI, ejecuta gcloud components update para obtener la versión más reciente.
  • Algunos modelos tienen requisitos adicionales. Asegúrate de cumplir con los siguientes requisitos:

Prepare el entorno

  1. En la consola de Google Cloud, inicia una instancia de Cloud Shell:
    Abrir Cloud Shell

  2. Configura las variables de entorno predeterminadas:

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

    Reemplaza PROJECT_ID por el ID del proyecto de Google Cloud.

Crea un clúster de GKE y un grupo de nodos

Puedes entregar LLM en GPU en un clúster de GKE Autopilot o Standard. Te recomendamos que uses un clúster de Autopilot para una experiencia de Kubernetes completamente administrada. Para elegir el modo de operación de GKE que se adapte mejor a tus cargas de trabajo, consulta Elige un modo de operación de GKE.

Autopilot

  1. En Cloud Shell, ejecuta el siguiente comando:

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

    GKE crea un clúster de Autopilot con nodos de CPU y GPU según lo solicitan las cargas de trabajo implementadas.

  2. Configura kubectl para comunicarse con tu clúster:

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

Estándar

  1. En Cloud Shell, ejecuta el siguiente comando para crear un clúster estándar que use la federación de identidades para cargas de trabajo para 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 creación del clúster puede tomar varios minutos.

  2. Ejecuta el siguiente comando para crear un grupo de nodos para el clúster:

    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 los siguientes recursos para el LLM:

    • Un clúster público de Google Kubernetes Engine (GKE) de edición Standard
    • Un grupo de nodos con el tipo de máquina g2-standard-24 en el que se redujo la escala verticalmente a 0 nodos. No se te cobrará por ninguna GPU hasta que inicies Pods que soliciten GPU. Este grupo de nodos aprovisiona VMs Spot, que tienen un precio menor que las VMs estándar de Compute Engine predeterminadas y no proporcionan garantía de disponibilidad. Puedes quitar la marca --spot de este comando y el selector de nodos cloud.google.com/gke-spot en la configuración text-generation-inference.yaml para usar VMs a pedido.
  3. Configura kubectl para comunicarse con tu clúster:

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

Prepara tu carga de trabajo

En la siguiente sección, se muestra cómo configurar la carga de trabajo según el modelo que deseas usar:

Llama 3 70b

  1. Configura las variables de entorno predeterminadas:

    export HF_TOKEN=HUGGING_FACE_TOKEN
    

    Reemplaza HUGGING_FACE_TOKEN por tu token de HuggingFace.

  2. Crea un Secret de Kubernetes para el token de HuggingFace:

    kubectl create secret generic l4-demo \
        --from-literal=HUGGING_FACE_TOKEN=${HF_TOKEN} \
        --dry-run=client -o yaml | kubectl apply -f -
    
  3. Crea el siguiente manifiesto 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

    En el manifiesto se muestra lo siguiente:

    • NUM_SHARD debe ser 2 porque el modelo requiere dos GPU NVIDIA L4.
    • QUANTIZE se configura como bitsandbytes-nf4, lo que significa que el modelo se carga en 4 bits en lugar de 32 bits. Esto permite que GKE reduzca la cantidad de memoria de GPU necesaria y mejora la velocidad de inferencia. Sin embargo, la exactitud del modelo puede disminuir. Para aprender a calcular las GPU que se solicitarán, consulta Calcula la cantidad de GPU.
  4. Aplica el manifiesto

    kubectl apply -f text-generation-inference.yaml
    

    El resultado es similar al siguiente:

    deployment.apps/llm created
    
  5. Verifica el estado del modelo:

    kubectl get deploy
    

    El resultado es similar al siguiente:

    NAME          READY   UP-TO-DATE   AVAILABLE   AGE
    llm           1/1     1            1           20m
    
  6. Observa los registros de la implementación en ejecución:

    kubectl logs -l app=llm
    

    El resultado es similar al siguiente:

    {"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. Configura las variables de entorno predeterminadas:

    export HF_TOKEN=HUGGING_FACE_TOKEN
    

    Reemplaza HUGGING_FACE_TOKEN por tu token de HuggingFace.

  2. Crea un Secret de Kubernetes para el token de HuggingFace:

    kubectl create secret generic l4-demo \
        --from-literal=HUGGING_FACE_TOKEN=${HF_TOKEN} \
        --dry-run=client -o yaml | kubectl apply -f -
    
  3. Crea el siguiente manifiesto 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"

    En el manifiesto se muestra lo siguiente:

    • NUM_SHARD debe ser 2 porque el modelo requiere dos GPU NVIDIA L4.
    • QUANTIZE se configura como bitsandbytes-nf4, lo que significa que el modelo se carga en 4 bits en lugar de 32 bits. Esto permite que GKE reduzca la cantidad de memoria de GPU necesaria y mejora la velocidad de inferencia. Sin embargo, esto puede reducir la exactitud del modelo. Para aprender a calcular las GPU que se solicitarán, consulta Calcula la cantidad de GPU.
  4. Aplica el manifiesto

    kubectl apply -f text-generation-inference.yaml
    

    El resultado es similar al siguiente:

    deployment.apps/llm created
    
  5. Verifica el estado del modelo:

    watch kubectl get deploy
    

    El resultado es similar al siguiente cuando la implementación está lista. Para salir de la visualización, escribe CTRL + C:

    NAME          READY   UP-TO-DATE   AVAILABLE   AGE
    llm           1/1     1            1           10m
    
  6. Observa los registros de la implementación en ejecución:

    kubectl logs -l app=llm
    

    El resultado es similar al siguiente:

    {"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 el siguiente manifiesto 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"

    En el manifiesto se muestra lo siguiente:

    • NUM_SHARD debe ser 2 porque el modelo requiere dos GPU NVIDIA L4.
    • QUANTIZE se configura como bitsandbytes-nf4, lo que significa que el modelo se carga en 4 bits en lugar de 32 bits. Esto permite que GKE reduzca la cantidad de memoria de GPU necesaria y mejora la velocidad de inferencia. Sin embargo, la exactitud del modelo puede disminuir. Para aprender a calcular las GPU que se solicitarán, consulta Calcula la cantidad de GPU.
  2. Aplica el manifiesto

    kubectl apply -f text-generation-inference.yaml
    

    El resultado es similar al siguiente:

    deployment.apps/llm created
    
  3. Verifica el estado del modelo:

    watch kubectl get deploy
    

    El resultado es similar al siguiente cuando la implementación está lista. Para salir de la visualización, escribe CTRL + C:

    NAME          READY   UP-TO-DATE   AVAILABLE   AGE
    llm           1/1     1            1           10m
    
  4. Observa los registros de la implementación en ejecución:

    kubectl logs -l app=llm
    

    El resultado es similar al siguiente:

    {"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 Service de tipo ClusterIP

  1. Crea el siguiente manifiesto 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. Aplica el manifiesto

    kubectl apply -f llm-service.yaml
    

Implementa una interfaz de chat

Usa Gradio para crear una aplicación web que te permita interactuar con el modelo. Gradio es una biblioteca de Python que tiene un wrapper de ChatInterface que crea interfaces de usuario para chatbots.

Llama 3 70b

  1. Crea un archivo llamado 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. Aplica el manifiesto

    kubectl apply -f gradio.yaml
    
  3. Busca la dirección IP externa del Service:

    kubectl get svc
    

    El resultado es similar al siguiente:

    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 la dirección IP externa de la columna EXTERNAL-IP.

  5. Para ver la interfaz del modelo desde tu navegador web, usa la dirección IP externa con el puerto expuesto:

    http://EXTERNAL_IP
    

Mixtral 8x7b

  1. Crea un archivo llamado 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. Aplica el manifiesto

    kubectl apply -f gradio.yaml
    
  3. Busca la dirección IP externa del Service:

    kubectl get svc
    

    El resultado es similar al siguiente:

    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 la dirección IP externa de la columna EXTERNAL-IP.

  5. Para ver la interfaz del modelo desde tu navegador web, usa la dirección IP externa con el puerto expuesto:

    http://EXTERNAL_IP
    

Falcon 40b

  1. Crea un archivo llamado 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. Aplica el manifiesto

    kubectl apply -f gradio.yaml
    
  3. Busca la dirección IP externa del Service:

    kubectl get svc
    

    El resultado es similar al siguiente:

    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 la dirección IP externa de la columna EXTERNAL-IP.

  5. Para ver la interfaz del modelo desde tu navegador web, usa la dirección IP externa con el puerto expuesto:

    http://EXTERNAL_IP
    

Calcula la cantidad de GPU

La cantidad de GPU depende del valor de la marca QUANTIZE. En este instructivo, QUANTIZE se configura como bitsandbytes-nf4, lo que significa que el modelo se carga en 4 bits.

Un modelo de 70,000 millones de parámetros requeriría un mínimo de 40 GB de memoria de GPU, que equivale a 70,000 millones de veces 4 bits (70,000 millones x 4 bits = 35 GB) y considera una sobrecarga de 5 GB. En este caso, una sola GPU L4 no tendría suficiente memoria. Por lo tanto, en los ejemplos de este instructivo, se usan dos GPU L4 de memoria (2 x 24 = 48 GB). Esta configuración es suficiente para ejecutar Falcon 40b o Llama 3 70b en las GPU L4.

Limpia

Para evitar que se apliquen cargos a tu cuenta de Google Cloud por los recursos usados en este instructivo, borra el proyecto que contiene los recursos o conserva el proyecto y borra los recursos individuales.

Borre el clúster

Para evitar que se apliquen cargos a tu cuenta de Google Cloud por los recursos que creaste en esta guía, borra el clúster de GKE:

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

¿Qué sigue?