Disponibilizar um LLM com várias GPUs no GKE


Este tutorial demonstra como implantar e disponibilizar um modelo de linguagem grande (LLM) usando várias GPUs no GKE para inferência eficiente e escalonável. Você cria um cluster do GKE que usa várias GPUs L4 e prepara a infraestrutura para disponibilizar qualquer um dos seguintes modelos:

O número de GPUs necessárias 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.

Este tutorial é destinado a engenheiros de machine learning (ML), administradores e operadores de plataforma e especialistas em dados e IA que têm interesse em usar os recursos de orquestração de contêineres do Kubernetes para veiculação de LLMs. Para saber mais sobre papéis comuns e exemplos de tarefas referenciados no conteúdo do Google Cloud, consulte Tarefas e funções de usuário comuns do GKE Enterprise.

Antes de ler esta página, confira se você conhece os seguintes conceitos:

Objetivos

Neste tutorial, você aprenderá a:

  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, veja se você realizou as seguintes tarefas:

  • Ative a API Google Kubernetes Engine.
  • Ativar a API Google Kubernetes Engine
  • Se você quiser usar a CLI do Google Cloud para essa tarefa, instale e, em seguida, inicialize a CLI gcloud. Se você instalou a gcloud CLI 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 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.

Piloto automático

  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}
    

Padrão

  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 solicitem 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

Esta seção mostra como configurar sua carga de trabalho, dependendo do modelo que você quer usar. Neste tutorial, usamos implantações do Kubernetes para implantar o modelo. Uma implantação é um objeto da API do Kubernetes que permite executar várias réplicas de pods distribuídos entre os nós de um cluster.

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 de implantação 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: us-docker.pkg.dev/deeplearning-platform-release/gcr.io/huggingface-text-generation-inference-cu121.2-1.ubuntu2204.py310
            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 is set to /tmp as it's the path where the HUGGINGFACE_HUB_CACHE environment
              # variable in the TGI DLCs is set to instead of the default /data set within the TGI default image.
              # i.e. where the downloaded model from the Hub will be stored
              - mountPath: /tmp
                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"

    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 de implantação 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: us-docker.pkg.dev/deeplearning-platform-release/gcr.io/huggingface-text-generation-inference-cu124.2-3.ubuntu2204.py311
            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 is set to /tmp as it's the path where the HF_HOME environment
              # variable in the TGI DLCs is set to instead of the default /data set within the TGI default image.
              # i.e. where the downloaded model from the Hub will be stored
              - mountPath: /tmp
                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
    

    Quando a implantação estiver pronta, a saída será semelhante a esta:

    NAME          READY   UP-TO-DATE   AVAILABLE   AGE
    llm           1/1     1            1           10m
    

    Para sair da exibição, digite CTRL + C.

  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 de implantação 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: us-docker.pkg.dev/deeplearning-platform-release/gcr.io/huggingface-text-generation-inference-cu121.1-4.ubuntu2204.py310
            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 is set to /data as it's the path where the HUGGINGFACE_HUB_CACHE environment
              # variable points to in the TGI container image i.e. where the downloaded model from the Hub will be
              # stored
              - 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
    

    Quando a implantação estiver pronta, a saída será semelhante a esta:

    NAME          READY   UP-TO-DATE   AVAILABLE   AGE
    llm           1/1     1            1           10m
    

    Para sair da exibição, digite CTRL + C.

  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

Exponha seus pods internamente no cluster para que eles possam ser descobertos e acessados por outros aplicativos.

  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.4
            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.4
            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.4
            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
    

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.

Excluir 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