Disponibilizar um LLM com várias GPUs no GKE


Neste tutorial, você vai aprender a disponibilizar um modelo de linguagem grande (LLM) com GPUs no Google Kubernetes Engine (GKE). Além disso, você vai criar um cluster do GKE que usa várias GPUs L4 e preparar a infraestrutura do GKE para disponibilizar qualquer um dos seguintes modelos:

O número de GPUs varia de acordo com o formato de dados do modelo. Neste tutorial, cada modelo usa duas GPUs L4. Para saber mais, consulte Como calcular a quantidade de GPUs.

Antes de concluir este tutorial no GKE, recomendamos que você leia Sobre as GPUs no GKE.

Objetivos

Este tutorial é destinado a engenheiros de MLOps ou DevOps ou administradores de plataforma que querem usar os recursos de orquestração do GKE para exibir LLMs.

Este tutorial inclui as etapas a seguir:

  1. Crie um cluster e pools de nós.
  2. Prepare a carga de trabalho.
  3. Implante a carga de trabalho.
  4. Interaja com a interface do LLM.

Antes de começar

Antes de começar, verifique se você realizou as tarefas a seguir:

  • Ativar a API Google Kubernetes Engine.
  • Ativar a API Google Kubernetes Engine
  • Se você quiser usar a Google Cloud CLI para essa tarefa, instale e, em seguida, inicialize a CLI gcloud. Se você instalou a CLI gcloud anteriormente, instale a versão mais recente executando gcloud components update.
  • Alguns modelos têm requisitos adicionais. Verifique se você cumpre estes requisitos:

prepare o ambiente

  1. No console do Google Cloud, inicie uma instância do Cloud Shell:
    Abrir o Cloud Shell

  2. Defina as variáveis de ambiente padrão:

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

    Substitua PROJECT_ID pelo ID do projeto do Google Cloud.

Criar um cluster do GKE e um pool de nós

É possível disponibilizar os LLMs no GPUs em um cluster do GKE Autopilot ou Standard. Recomendamos que você use um cluster do Autopilot para ter uma experiência totalmente gerenciada do Kubernetes. Para escolher o modo de operação do GKE mais adequado para suas cargas de trabalho, consulte Escolher um modo de operação do GKE.

Autopilot

  1. No Cloud Shell, execute este comando:

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

    O GKE cria um cluster do Autopilot com nós de CPU e GPU conforme solicitado pelas cargas de trabalho implantadas.

  2. Configure kubectl para se comunicar com o cluster:

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

Standard

  1. No Cloud Shell, execute o seguinte comando para criar um cluster padrão que use a federação de identidade da carga de trabalho para o 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
    

    A criação do cluster pode levar vários minutos.

  2. Execute o seguinte comando para criar um pool de nós para o 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
    

    O GKE cria os seguintes recursos para o LLM:

    • Um cluster público da edição Standard do Google Kubernetes Engine (GKE).
    • Um pool de nós com o tipo de máquina g2-standard-24 escalonado para zero nós. Nenhuma GPU será cobrada até que você inicie pods que solicitam GPUs. Este pool de nós provisiona VMs spot, que têm um preço menor do que as VMs Standard padrão do Compute Engine e não oferecem garantia de disponibilidade. É possível remover a flag --spot desse comando e o seletor de nós cloud.google.com/gke-spot na configuração text-generation-inference.yaml para usar VMs sob demanda.
  3. Configure kubectl para se comunicar com o cluster:

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

Preparar sua carga de trabalho

Na seção a seguir, mostramos como configurar sua carga de trabalho, dependendo do modelo que você quer usar:

Llama 3 70b

  1. Defina as variáveis de ambiente padrão:

    export HF_TOKEN=HUGGING_FACE_TOKEN
    

    Substitua HUGGING_FACE_TOKEN pelo token HuggingFace.

  2. Crie um secret do Kubernetes para o token HuggingFace:

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

    Nesse manifesto:

    • NUM_SHARD precisa ser 2 porque o modelo requer duas GPUs NVIDIA L4.
    • QUANTIZE está definido como bitsandbytes-nf4, o que significa que o modelo é carregado em 4 bits em vez de em 32 bits. Isso permite que o GKE reduza a quantidade de memória de GPU necessária e melhore a velocidade de inferência. No entanto, a acurácia do modelo pode diminuir. Para saber como calcular as GPUs a serem solicitadas, consulte Como calcular a quantidade de GPUs.
  4. Aplique o manifesto:

    kubectl apply -f text-generation-inference.yaml
    

    O resultado será assim:

    deployment.apps/llm created
    
  5. Verifique o status do modelo:

    kubectl get deploy
    

    O resultado será assim:

    NAME          READY   UP-TO-DATE   AVAILABLE   AGE
    llm           1/1     1            1           20m
    
  6. Confira os registros da implantação em execução:

    kubectl logs -l app=llm
    

    O resultado será assim:

    {"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. Defina as variáveis de ambiente padrão:

    export HF_TOKEN=HUGGING_FACE_TOKEN
    

    Substitua HUGGING_FACE_TOKEN pelo token HuggingFace.

  2. Crie um secret do Kubernetes para o token HuggingFace:

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

    Nesse manifesto:

    • NUM_SHARD precisa ser 2 porque o modelo requer duas GPUs NVIDIA L4.
    • QUANTIZE está definido como bitsandbytes-nf4, o que significa que o modelo é carregado em 4 bits em vez de em 32 bits. Isso permite que o GKE reduza a quantidade de memória de GPU necessária e melhore a velocidade de inferência. No entanto, isso pode reduzir a acurácia do modelo. Para saber como calcular as GPUs a serem solicitadas, consulte Como calcular a quantidade de GPUs.
  4. Aplique o manifesto:

    kubectl apply -f text-generation-inference.yaml
    

    O resultado será assim:

    deployment.apps/llm created
    
  5. Verifique o status do modelo:

    watch kubectl get deploy
    

    A saída será semelhante ao exemplo abaixo quando a implantação estiver pronta. Para sair da exibição, digite CTRL + C:

    NAME          READY   UP-TO-DATE   AVAILABLE   AGE
    llm           1/1     1            1           10m
    
  6. Confira os registros da implantação em execução:

    kubectl logs -l app=llm
    

    O resultado será assim:

    {"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. Crie o seguinte manifesto 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"

    Nesse manifesto:

    • NUM_SHARD precisa ser 2 porque o modelo requer duas GPUs NVIDIA L4.
    • QUANTIZE está definido como bitsandbytes-nf4, o que significa que o modelo é carregado em 4 bits em vez de em 32 bits. Isso permite que o GKE reduza a quantidade de memória de GPU necessária e melhore a velocidade de inferência. No entanto, a acurácia do modelo pode diminuir. Para saber como calcular as GPUs a serem solicitadas, consulte Como calcular a quantidade de GPUs.
  2. Aplique o manifesto:

    kubectl apply -f text-generation-inference.yaml
    

    O resultado será assim:

    deployment.apps/llm created
    
  3. Verifique o status do modelo:

    watch kubectl get deploy
    

    A saída será semelhante ao exemplo abaixo quando a implantação estiver pronta. Para sair da exibição, digite CTRL + C:

    NAME          READY   UP-TO-DATE   AVAILABLE   AGE
    llm           1/1     1            1           10m
    
  4. Confira os registros da implantação em execução:

    kubectl logs -l app=llm
    

    O resultado será assim:

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

Criar um serviço do tipo ClusterIP

  1. Crie o seguinte manifesto 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. Aplique o manifesto:

    kubectl apply -f llm-service.yaml
    

Implantar uma interface de chat

Use o Gradio para criar um aplicativo da Web que permita a interação com o modelo. O Gradio é uma biblioteca Python que tem um wrapper ChatInterface que cria interfaces de usuário para chatbots.

Llama 3 70b

  1. Crie um arquivo chamado 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. Aplique o manifesto:

    kubectl apply -f gradio.yaml
    
  3. Encontre o endereço IP externo do serviço:

    kubectl get svc
    

    O resultado será assim:

    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. Copie o endereço IP externo da coluna EXTERNAL-IP.

  5. Confira a interface do modelo no navegador da Web usando o endereço IP externo com a porta exposta:

    http://EXTERNAL_IP
    

Mixtral 8x7b

  1. Crie um arquivo chamado 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. Aplique o manifesto:

    kubectl apply -f gradio.yaml
    
  3. Encontre o endereço IP externo do serviço:

    kubectl get svc
    

    O resultado será assim:

    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. Copie o endereço IP externo da coluna EXTERNAL-IP.

  5. Confira a interface do modelo no navegador da Web usando o endereço IP externo com a porta exposta:

    http://EXTERNAL_IP
    

Falcon 40b

  1. Crie um arquivo chamado 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. Aplique o manifesto:

    kubectl apply -f gradio.yaml
    
  3. Encontre o endereço IP externo do serviço:

    kubectl get svc
    

    O resultado será assim:

    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. Copie o endereço IP externo da coluna EXTERNAL-IP.

  5. Confira a interface do modelo no navegador da Web usando o endereço IP externo com a porta exposta:

    http://EXTERNAL_IP
    

Como calcular a quantidade de GPUs

A quantidade de GPUs depende do valor da flag QUANTIZE. Neste tutorial, QUANTIZE está definido como bitsandbytes-nf4, o que significa que o modelo é carregado em 4 bits.

Um modelo de 70 bilhões de parâmetros exigiria no mínimo 40 GB de memória de GPU, o que equivale a 70 bilhões vezes 4 bits (70 bilhões x 4 bits = 35 GB) e considera 5 GB de sobrecarga. Nesse caso, uma única GPU L4 não teria memória suficiente. Portanto, os exemplos neste tutorial usam duas GPUs L4 de memória (2 x 24 = 48 GB). Essa configuração é suficiente para executar o Falcon 40b ou o Llama 3 70b em GPUs L4.

Limpar

Para evitar cobranças na sua conta do Google Cloud pelos recursos usados no tutorial, exclua o projeto que os contém ou mantenha o projeto e exclua os recursos individuais.

Exclua o cluster

Para evitar cobranças na sua conta do Google Cloud pelos recursos criados neste guia, exclua o cluster do GKE:

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

A seguir