Disponibilizar o DeepSeek-R1 671B no GKE com vLLM de vários hosts em VMs A4

Neste tutorial, mostramos como implantar e disponibilizar o DeepSeek-R1 (deepseek-ai/DeepSeek-R1), um modelo de Mix de Especialistas de 671 bilhões de parâmetros com 37 bilhões de parâmetros ativos, no Google Kubernetes Engine (GKE) em Google Cloud. A implantação usa o Cluster Toolkit, duas instâncias de máquina virtual (VM) A4 (a4-highgpu-8g) com vários hosts e um total de 16 GPUs NVIDIA B200, além do framework de exibição vLLM.

Na configuração de implantação descrita neste tutorial, use o Cluster Toolkit para provisionar um cluster do GKE no modo Standard. O cluster tem dois nós a4-highgpu-8g conectados pela rede de acesso direto à memória remota (RDMA) do GPUDirect. Em seguida, você prepara os pesos do modelo no Cloud Storage e usa a API JobSet do Kubernetes e o Ray para disponibilizar o modelo em todas as 16 GPUs NVIDIA B200 com paralelismo de tensor, paralelismo de especialista e um cache KV FP8.

Objetivos

  • Instale o Cluster Toolkit e provisione um cluster do GKE A4 de vários hosts no modo padrão.
  • Armazene os pesos do modelo DeepSeek-R1 em um bucket do Cloud Storage.
  • Implantar um servidor de inferência vLLM de vários hosts com JobSet e Ray em dois nós A4, com um total de 16 GPUs NVIDIA B200.
  • Envie uma solicitação de conclusão de chat para o servidor vLLM.
  • Executar uma carga de trabalho de comparativo de mercado de exibição.
  • Limpe os recursos implantados.

Custos

Neste documento, você usará os seguintes componentes faturáveis do Google Cloud:

Para gerar uma estimativa de custo baseada na projeção de uso deste tutorial, use a calculadora de preços.

Antes de começar

Configure o ambiente para o tutorial:

  1. Instale e inicialize a Google Cloud CLI e instale kubectl. Se você usa o Cloud Shell, as duas ferramentas já estão pré-instaladas.
  2. Se você estiver executando este tutorial em uma estação de trabalho Linux ou macOS em vez do Cloud Shell, instale as dependências do Cluster Toolkit necessárias (como Terraform e Git).
  3. Ative as API Kubernetes Engine, Compute Engine, Cloud Storage, API Artifact Registry e API Service Usage no projeto do Google Cloud :

    Ativar APIs

Receber um token de acesso do Hugging Face

Você precisa de um token de acesso do Hugging Face para usar este tutorial. Você pode se inscrever para uma conta sem custo financeiro no Hugging Face. Depois de criar uma conta, gere um token de acesso:

  1. Na página Welcome to Hugging Face, clique no avatar da sua conta e selecione Tokens de acesso.
  2. Na página Tokens de acesso, clique em Criar novo token.
  3. Selecione o tipo de token Read e insira um nome para ele.
  4. Seu token de acesso será exibido. Salve o token em um local seguro.

Funções exigidas

Para conseguir as permissões necessárias para concluir o tutorial, peça ao administrador para conceder a você os seguintes papéis do IAM no seu projeto:

Para mais informações sobre a concessão de papéis, consulte Gerenciar o acesso a projetos, pastas e organizações.

Também é possível conseguir as permissões necessárias usando papéis personalizados ou outros papéis predefinidos.

Configurar as variáveis de ambiente

Configure as variáveis de ambiente e de autenticação para o tutorial:

  1. Configure sua autenticação:

    gcloud auth login
    gcloud auth application-default login
  2. Exporte as variáveis de ambiente que você vai usar neste tutorial:

    export PROJECT_ID="YOUR_PROJECT_ID"
    export REGION="YOUR_REGION"
    export ZONE="YOUR_ZONE"
    export CLUSTER_NAME="YOUR_CLUSTER_NAME"
    export RESERVATION_NAME="YOUR_RESERVATION_NAME"
    export HF_TOKEN="YOUR_HF_TOKEN"
    export MODEL_BUCKET="YOUR_GCS_BUCKET"
    export TERRAFORM_BUCKET="${MODEL_BUCKET}-tf-state"
    export CLUSTER_TOOLKIT_VERSION="v1.104.0"
    
    gcloud config set project "${PROJECT_ID}"
    gcloud config set billing/quota_project "${PROJECT_ID}"

    Substitua:

    • YOUR_PROJECT_ID: o ID do seu projeto do Google Cloud .
    • YOUR_REGION: a região de computação em que sua capacidade A4 está localizada e em que você quer implantar o cluster, como us-central1.
    • YOUR_ZONE: a zona do Compute em que sua capacidade A4 está localizada, como us-central1-b.
    • YOUR_CLUSTER_NAME: o nome do cluster do GKE e da implantação do Cluster Toolkit. Use um nome com até 20 caracteres para que o ID da conta de serviço do pool de nós gerado (YOUR_CLUSTER_NAME-gke-np-sa) permaneça dentro do limite de 30 caracteres do Identity and Access Management (IAM).
    • YOUR_RESERVATION_NAME: o nome da sua reserva de A4 ou o caminho do bloco de reserva estendido no formato RESERVATION_NAME/reservationBlocks/RESERVATION_BLOCK_NAME.
    • YOUR_HF_TOKEN: seu token de acesso do Hugging Face.
    • YOUR_GCS_BUCKET: um nome globalmente exclusivo para o bucket do Cloud Storage em que você prepara os pesos do modelo. O script também define TERRAFORM_BUCKET como YOUR_GCS_BUCKET-tf-state para armazenar o estado do Terraform para o Cluster Toolkit.

    O script também define CLUSTER_TOOLKIT_VERSION como v1.104.0, que especifica a versão do Cluster Toolkit a ser instalada. A versão v1.104.0 e mais recentes do Cluster Toolkit incluem opções integradas de consumo de capacidade para examples/gke-a4/gke-a4-deployment.yaml. Em versões anteriores à v1.104.0, é necessário adicionar essas opções de capacidade manualmente.

Criar o cluster do GKE

Faça o download do Cluster Toolkit e provisione um cluster do GKE no modo Standard com dois nós a4-highgpu-8g, rede GPUDirect RDMA e o controlador JobSet:

  1. Faça o download e extraia o pacote pré-criado do Cluster Toolkit para a versão especificada em CLUSTER_TOOLKIT_VERSION (v1.104.0), mude o diretório de trabalho para o diretório cluster-toolkit e verifique a versão binária:

    case "$(uname -s)-$(uname -m)" in
      Linux-x86_64)              BUNDLE="gcluster_bundle_linux_amd64.tgz" ;;
      Linux-aarch64|Linux-arm64) BUNDLE="gcluster_bundle_linux_arm64.tgz" ;;
      Darwin-x86_64)             BUNDLE="gcluster_bundle_mac_amd64.tgz" ;;
      Darwin-arm64)              BUNDLE="gcluster_bundle_mac_arm64.tgz" ;;
      *) BUNDLE=""; echo "Unsupported platform: $(uname -s)-$(uname -m)" >&2 ;;
    esac
    mkdir -p cluster-toolkit
    curl -fsSL "https://github.com/GoogleCloudPlatform/cluster-toolkit/releases/download/${CLUSTER_TOOLKIT_VERSION}/${BUNDLE:?}" \
      | tar -xz -C cluster-toolkit
    cd cluster-toolkit
    ./gcluster --version
  2. Se quiser, confira os comandos e as flags disponíveis no Cluster Toolkit:

    ./gcluster --help
  3. Crie o bucket do Cloud Storage que armazena o estado do Terraform para sua implantação do Cluster Toolkit:

    gcloud storage buckets create "gs://${TERRAFORM_BUCKET}" \
        --default-storage-class=STANDARD \
        --project="${PROJECT_ID}" \
        --location="${REGION}" \
        --uniform-bucket-level-access
  4. Crie o arquivo de configuração de implantação examples/gke-a4/gke-a4-deployment.yaml para dois nós a4-highgpu-8g:

    cat <<EOF > examples/gke-a4/gke-a4-deployment.yaml
    terraform_backend_defaults:
      type: gcs
      configuration:
        bucket: ${TERRAFORM_BUCKET}
    
    vars:
      project_id: ${PROJECT_ID}
      deployment_name: ${CLUSTER_NAME}
      region: ${REGION}
      zone: ${ZONE}
      static_node_count: 2
      authorized_cidr: 0.0.0.0/0
      reservation_affinity:
        consume_reservation_type: SPECIFIC_RESERVATION
        specific_reservations:
        - name: ${RESERVATION_NAME}
    EOF

    Por padrão, o blueprint examples/gke-a4/gke-a4.yaml é configurado para usar uma reserva específica. Se você não tiver uma reserva específica, poderá editar examples/gke-a4/gke-a4.yaml antes de implantar o cluster para usar uma das seguintes opções de consumo:

    • Capacidade spot: verifique se o projeto tem cota preemptiva suficiente para as GPUs, os discos e as CPUs. Na parte de baixo de examples/gke-a4/gke-a4.yaml, comente a seção --- Option 1: Specific Reservation --- e remova a marca de comentário da seção --- Option 4: Spot ---.
    • VMs flex-start: verifique se o projeto tem cota preemptiva suficiente para as GPUs, os discos e as CPUs. Na parte de baixo de examples/gke-a4/gke-a4.yaml, comente a seção --- Option 1: Specific Reservation --- e remova a marca de comentário da seção --- Option 2: DWS Flex Start ---.
  5. Implante o cluster do GKE usando o comando gcluster deploy:

    ./gcluster deploy -d examples/gke-a4/gke-a4-deployment.yaml examples/gke-a4/gke-a4.yaml \
        --download-dependencies \
        --auto-approve -w

    A criação do cluster leva aproximadamente de 30 a 45 minutos. Se você executar ./gcluster deploy sem a flag --auto-approve, para aplicar as mudanças propostas, insira a quando solicitado. Quando a implantação terminar, a saída vai terminar com uma mensagem semelhante a esta:

    Apply complete! Resources: 80 added, 0 changed, 0 destroyed.
    
  6. Configure as credenciais de kubectl para se comunicar com o cluster:

    gcloud container clusters get-credentials "${CLUSTER_NAME}" \
        --project="${PROJECT_ID}" \
        --location="${REGION}"

Organizar os pesos do modelo no Cloud Storage

Faça o download dos pesos do DeepSeek-R1 no SSD local de um dos nós A4 e transfira-os para um bucket do Cloud Storage para streaming rápido entre nós:

  1. Faça o download dos arquivos de manifesto do Kubernetes para o job de download do modelo, o servidor de inferência vLLM e os pods de comparativo de mercado:

    curl -LO https://raw.githubusercontent.com/GoogleCloudSamples/AIHypercomputerSamples/main/gpu/inference/deepseek-r1-gcluster/download_model.yaml
    curl -LO https://raw.githubusercontent.com/GoogleCloudSamples/AIHypercomputerSamples/main/gpu/inference/deepseek-r1-gcluster/deepseek_r1_server.yaml
    curl -LO https://raw.githubusercontent.com/GoogleCloudSamples/AIHypercomputerSamples/main/gpu/inference/deepseek-r1-gcluster/deepseek_r1_8k_1k.yaml
  2. Crie o bucket do Cloud Storage com acesso uniforme no nível do bucket para armazenar os pesos do modelo:

    gcloud storage buckets create "gs://${MODEL_BUCKET}" \
        --project="${PROJECT_ID}" \
        --location="${REGION}" \
        --uniform-bucket-level-access
  3. Conceda permissões do IAM no bucket à conta de serviço padrão do Kubernetes, à conta de serviço padrão do Compute Engine e à conta de serviço do pool de nós do cluster (${CLUSTER_NAME}-gke-np-sa):

    PROJECT_NUMBER=$(gcloud projects describe "${PROJECT_ID}" --format="value(projectNumber)")
    
    gcloud storage buckets add-iam-policy-binding "gs://${MODEL_BUCKET}" \
        --member="principal://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${PROJECT_ID}.svc.id.goog/subject/ns/default/sa/default" \
        --role="roles/storage.objectAdmin"
    
    gcloud storage buckets add-iam-policy-binding "gs://${MODEL_BUCKET}" \
        --member="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \
        --role="roles/storage.objectAdmin"
    
    gcloud storage buckets add-iam-policy-binding "gs://${MODEL_BUCKET}" \
        --member="serviceAccount:${CLUSTER_NAME}-gke-np-sa@${PROJECT_ID}.iam.gserviceaccount.com" \
        --role="roles/storage.objectAdmin"
    
    # legacyBucketReader is required to load the model with --load-format runai_streamer
    gcloud storage buckets add-iam-policy-binding "gs://${MODEL_BUCKET}" \
        --member="serviceAccount:${CLUSTER_NAME}-gke-np-sa@${PROJECT_ID}.iam.gserviceaccount.com" \
        --role="roles/storage.legacyBucketReader"

    As permissões do IAM concedidas pelo script anterior incluem o papel de Leitor de bucket legado do Storage (roles/storage.legacyBucketReader), que é necessário para --load-format runai_streamer.

  4. Crie um Secret do Kubernetes chamado hf-secret para armazenar o token de acesso do Hugging Face:

    kubectl create secret generic hf-secret \
        --from-literal=hf_api_token="${HF_TOKEN}" \
        --dry-run=client -o yaml | kubectl apply -f -
  5. Revise o manifesto download_model.yaml ou crie o arquivo localmente se você não o tiver baixado anteriormente:

    apiVersion: batch/v1
    kind: Job
    metadata:
      name: download-deepseek-r1
      namespace: default
    spec:
      backoffLimit: 1
      template:
        spec:
          restartPolicy: OnFailure
          # Schedule onto an A4 node with Local SSD
          nodeSelector:
            cloud.google.com/gke-accelerator: nvidia-b200
          tolerations:
          - operator: Exists
          containers:
          - name: model-downloader
            image: gcr.io/google.com/cloudsdktool/google-cloud-cli:slim
            env:
            - name: HF_TOKEN
              valueFrom:
                secretKeyRef:
                  name: hf-secret
                  key: hf_api_token
            # Enable high-performance concurrent chunked downloads in huggingface_hub
            - name: HF_XET_HIGH_PERFORMANCE
              value: "1"
            # Place Hugging Face cache on Local SSD so the boot disk never fills up
            - name: HF_HOME
              value: "/local-ssd/hf-cache"
            command: ["/bin/bash", "-c"]
            args:
            - |
              set -euo pipefail
    
              echo "=== 1. Installing huggingface_hub ==="
              pip3 install --break-system-packages -q -U "huggingface_hub"
    
              echo "=== 2. Downloading deepseek-ai/DeepSeek-R1 to Local NVMe SSD ==="
              hf download deepseek-ai/DeepSeek-R1 --local-dir /local-ssd/DeepSeek-R1
              rm -rf /local-ssd/DeepSeek-R1/.cache
    
              echo "=== 3. Uploading from Local SSD to GCS ==="
              gcloud config set storage/parallel_composite_upload_compatibility_check False
              gcloud storage cp -r /local-ssd/DeepSeek-R1 gs://$MODEL_BUCKET/
    
              echo "=== 4. Cleaning up Local SSD ==="
              rm -rf /local-ssd/DeepSeek-R1 /local-ssd/hf-cache
            resources:
              requests:
                cpu: "64"
                memory: "128Gi"
            volumeMounts:
            - name: local-ssd
              mountPath: /local-ssd
          volumes:
          - name: local-ssd
            emptyDir: {}
  6. Substitua o nome do bucket do Cloud Storage no manifesto download_model.yaml e inicie o job de download do modelo:

    envsubst '$MODEL_BUCKET' < download_model.yaml | kubectl apply -f -
  7. Aguarde o início do pod de download e transmita os registros do job:

    kubectl wait --for=condition=Ready pod -l job-name=download-deepseek-r1 --timeout=120s
    kubectl logs -f job/download-deepseek-r1

    Leva de 30 a 40 minutos para fazer o download do modelo para o SSD NVMe local do nó e fazer upload dos pesos para o bucket do Cloud Storage.

  8. Depois que o job for concluído, verifique se todos os 163 fragmentos .safetensors estão presentes no bucket do Cloud Storage:

    gcloud storage ls "gs://${MODEL_BUCKET}/DeepSeek-R1/*.safetensors" | wc -l
  9. Exclua o job de preparo concluído:

    kubectl delete job download-deepseek-r1

Implantar o servidor vLLM de vários hosts

O manifesto da carga de trabalho (deepseek_r1_server.yaml) define um serviço ClusterIP (vllm-r1-leader) e um JobSet do Kubernetes (vllm-r1) que coordenam a exibição de vários hosts nos dois nós a4-highgpu-8g:

  • Líder (leader): executa uma réplica em um nó A4 com oito GPUs NVIDIA B200, origina o ambiente NCCL/gIB (/usr/local/gib/scripts/set_nccl_env.sh), inicia o nó principal do Ray na porta 6379 para um cluster de dois nós e inicia vllm serve na porta HTTP 8080. O servidor transmite pesos diretamente do gs://$MODEL_BUCKET/DeepSeek-R1 usando --load-format runai_streamer e carrega o tokenizador do volume montado do Cloud Storage FUSE em /models/DeepSeek-R1. Ele é executado com --tensor-parallel-size 16, --pipeline-parallel-size 1, --distributed-executor-backend ray, --enable-expert-parallel, --kv-cache-dtype fp8_e4m3, --async-scheduling e gráficos CUDA (FULL_AND_PIECEWISE).
  • Worker (worker): executa uma réplica headless no segundo nó A4 com oito GPUs NVIDIA B200, configura o NCCL/gIB e se junta ao cluster do Ray do líder em vllm-r1-leader:6379.

Implante e monitore o servidor vLLM de vários hosts:

  1. Revise o manifesto deepseek_r1_server.yaml ou crie o arquivo localmente se você não tiver feito o download dele antes:

    apiVersion: v1
    kind: Service
    metadata:
      name: vllm-r1-leader
      namespace: default
      labels:
        app: vllm-r1
    spec:
      ports:
      - name: http
        port: 8080
        targetPort: 8080
      - name: ray-head
        port: 6379
        targetPort: 6379
      selector:
        app: vllm-r1
        role: leader
      type: ClusterIP
    ---
    apiVersion: jobset.x-k8s.io/v1alpha2
    kind: JobSet
    metadata:
      name: vllm-r1
      namespace: default
      labels:
        app: vllm-r1
    spec:
      failurePolicy:
        maxRestarts: 3
        restartStrategy: Recreate
      network:
        enableDNSHostnames: true
        publishNotReadyAddresses: true
      replicatedJobs:
      # -------------------- LEADER --------------------
      - name: leader
        replicas: 1
        template:
          spec:
            parallelism: 1
            completions: 1
            backoffLimit: 1
            template:
              metadata:
                labels:
                  app: vllm-r1
                  role: leader
                annotations:
                  gke-gcsfuse/volumes: "true"
              spec:
                hostNetwork: true
                dnsPolicy: ClusterFirstWithHostNet
                hostIPC: true
                nodeSelector:
                  cloud.google.com/gke-accelerator: nvidia-b200
                tolerations:
                - key: nvidia.com/gpu
                  operator: Exists
                - key: cloud.google.com/impending-node-termination
                  operator: Exists
                - effect: NoSchedule
                  key: cloud.google.com/gke-queued
                  value: "true"
                containers:
                - name: vllm-leader
                  image: docker.io/vllm/vllm-openai@sha256:61fc8a896b0a4fbbbdc063bc4b0dbc25ce98e02b5050c24aeb7830ac02039b14
                  imagePullPolicy: IfNotPresent
                  securityContext:
                    privileged: true
                  env:
                  - name: PYTHONUNBUFFERED
                    value: "1"
                  - name: NCCL_DEBUG
                    value: "INFO"
                  - name: VLLM_ENGINE_READY_TIMEOUT_S
                    value: "1800"
                  - name: VLLM_RPC_TIMEOUT
                    value: "1800000"
                  - name: HF_TOKEN
                    valueFrom:
                      secretKeyRef:
                        name: hf-secret
                        key: hf_api_token
                  command: ["/bin/bash", "-c"]
                  args:
                  - |
                    set -e
                    echo "precedence ::ffff:0:0/96  100" >> /etc/gai.conf 2>/dev/null || true
                    ulimit -l unlimited || true
                    export LD_LIBRARY_PATH=/usr/local/gib/lib64:/usr/local/nvidia/lib64:${LD_LIBRARY_PATH:-}
                    source /usr/local/gib/scripts/set_nccl_env.sh
                    export GLOO_SOCKET_IFNAME=eth0
                    export NCCL_SOCKET_IFNAME=eth0
                    export VLLM_HOST_IP=$(hostname -I | awk '{print $1}')
                    echo "=== Starting Optimized vLLM Server with Ray (Leader, rank 0) ==="
                    echo "Host IP: ${VLLM_HOST_IP}"
                    # 0. Install Ray with Compiled Graph support (not pre-installed in vLLM 0.28.0 image)
                    uv pip install --system "ray[cgraph]"
                    # 1. Start Ray head on port 6379 and wait until all 2 nodes (16 GPUs) join
                    bash /vllm-workspace/examples/ray_serving/multi-node-serving.sh leader \
                      --ray_cluster_size=2 \
                      --ray_port=6379 \
                      --node-ip-address="${VLLM_HOST_IP}"
                    # 2. Launch vLLM on the Leader using the Ray cluster
                    exec vllm serve \
                      --host 0.0.0.0 \
                      --port 8080 \
                      --model gs://$MODEL_BUCKET/DeepSeek-R1 \
                      --tokenizer /models/DeepSeek-R1 \
                      --load-format runai_streamer \
                      --model-loader-extra-config '{"distributed": true, "concurrency": 16, "memory_limit": 34359738368}' \
                      --served-model-name deepseek-ai/DeepSeek-R1 \
                      --tensor-parallel-size 16 \
                      --pipeline-parallel-size 1 \
                      --distributed-executor-backend ray \
                      --async-scheduling \
                      --disable-custom-all-reduce \
                      --kv-cache-dtype fp8_e4m3 \
                      --block-size 128 \
                      --enable-expert-parallel \
                      --max-num-batched-tokens 8192 \
                      --max-num-seqs 160 \
                      --compilation-config '{"mode": 3, "cudagraph_mode":"FULL_AND_PIECEWISE", "custom_ops":["none"], "pass_config":{"fuse_allreduce_rms":false}}' \
                      --trust-remote-code \
                      --max-model-len 16384 \
                      --gpu-memory-utilization 0.90
                  resources:
                    requests:
                      nvidia.com/gpu: "8"
                    limits:
                      nvidia.com/gpu: "8"
                  ports:
                  - containerPort: 8080
                    name: http
                  - containerPort: 6379
                    name: ray-head
                  volumeMounts:
                  - mountPath: /dev/shm
                    name: dshm
                  - mountPath: /models
                    name: model-volume
                    readOnly: true
                  - mountPath: /usr/local/gib
                    name: gib
                  - mountPath: /usr/local/nvidia/lib64
                    name: nvidia-lib64
                volumes:
                - name: dshm
                  emptyDir:
                    medium: Memory
                    sizeLimit: 64Gi
                - name: model-volume
                  csi:
                    driver: gcsfuse.csi.storage.gke.io
                    volumeAttributes:
                      bucketName: $MODEL_BUCKET
                      mountOptions: implicit-dirs
                - name: gib
                  hostPath:
                    path: /home/kubernetes/bin/gib
                - name: nvidia-lib64
                  hostPath:
                    path: /home/kubernetes/bin/nvidia/lib64
      # -------------------- WORKER --------------------
      - name: worker
        replicas: 1
        template:
          spec:
            parallelism: 1
            completions: 1
            backoffLimit: 1
            template:
              metadata:
                labels:
                  app: vllm-r1
                  role: worker
                annotations:
                  gke-gcsfuse/volumes: "true"
              spec:
                hostNetwork: true
                dnsPolicy: ClusterFirstWithHostNet
                hostIPC: true
                nodeSelector:
                  cloud.google.com/gke-accelerator: nvidia-b200
                tolerations:
                - key: nvidia.com/gpu
                  operator: Exists
                - key: cloud.google.com/impending-node-termination
                  operator: Exists
                - effect: NoSchedule
                  key: cloud.google.com/gke-queued
                  value: "true"
                containers:
                - name: vllm-worker
                  image: docker.io/vllm/vllm-openai@sha256:61fc8a896b0a4fbbbdc063bc4b0dbc25ce98e02b5050c24aeb7830ac02039b14
                  imagePullPolicy: IfNotPresent
                  securityContext:
                    privileged: true
                  env:
                  - name: PYTHONUNBUFFERED
                    value: "1"
                  - name: NCCL_DEBUG
                    value: "INFO"
                  - name: VLLM_ENGINE_READY_TIMEOUT_S
                    value: "1800"
                  - name: VLLM_RPC_TIMEOUT
                    value: "1800000"
                  - name: HF_TOKEN
                    valueFrom:
                      secretKeyRef:
                        name: hf-secret
                        key: hf_api_token
                  command: ["/bin/bash", "-c"]
                  args:
                  - |
                    set -e
                    echo "precedence ::ffff:0:0/96  100" >> /etc/gai.conf 2>/dev/null || true
                    ulimit -l unlimited || true
                    export LD_LIBRARY_PATH=/usr/local/gib/lib64:/usr/local/nvidia/lib64:${LD_LIBRARY_PATH:-}
                    source /usr/local/gib/scripts/set_nccl_env.sh
                    export GLOO_SOCKET_IFNAME=eth0
                    export NCCL_SOCKET_IFNAME=eth0
                    export VLLM_HOST_IP=$(hostname -I | awk '{print $1}')
                    echo "=== Starting Optimized vLLM Ray Worker (rank 1) ==="
                    echo "Host IP: ${VLLM_HOST_IP}"
                    # 0. Install Ray with Compiled Graph support (not pre-installed in vLLM 0.28.0 image)
                    uv pip install --system "ray[cgraph]"
                    # Join the Leader's Ray cluster and block so the container stays alive
                    exec bash /vllm-workspace/examples/ray_serving/multi-node-serving.sh worker \
                      --ray_address=vllm-r1-leader \
                      --ray_port=6379 \
                      --node-ip-address="${VLLM_HOST_IP}"
                  resources:
                    requests:
                      nvidia.com/gpu: "8"
                    limits:
                      nvidia.com/gpu: "8"
                  ports:
                  - containerPort: 8080
                    name: http
                  volumeMounts:
                  - mountPath: /dev/shm
                    name: dshm
                  - mountPath: /models
                    name: model-volume
                    readOnly: true
                  - mountPath: /usr/local/gib
                    name: gib
                  - mountPath: /usr/local/nvidia/lib64
                    name: nvidia-lib64
                volumes:
                - name: dshm
                  emptyDir:
                    medium: Memory
                    sizeLimit: 64Gi
                - name: model-volume
                  csi:
                    driver: gcsfuse.csi.storage.gke.io
                    volumeAttributes:
                      bucketName: $MODEL_BUCKET
                      mountOptions: implicit-dirs
                - name: gib
                  hostPath:
                    path: /home/kubernetes/bin/gib
                - name: nvidia-lib64
                  hostPath:
                    path: /home/kubernetes/bin/nvidia/lib64

    Para escalonar essa implantação para mais hosts, atualize os parâmetros em deepseek_r1_server.yaml. Por exemplo, para escalonar para quatro nós A4, com um total de 32 GPUs NVIDIA B200, defina replicas: 3 no job replicado worker e atualize os argumentos do líder da seguinte maneira:

    • --ray_cluster_size=4
    • --tensor-parallel-size=16
    • --pipeline-parallel-size=2
  2. Substitua o nome do bucket do Cloud Storage em deepseek_r1_server.yaml e aplique o manifesto ao cluster:

    envsubst '$MODEL_BUCKET' < deepseek_r1_server.yaml | kubectl apply -f -
  3. Monitore os registros dos pods líder e worker até que a inicialização do servidor seja concluída:

    # monitor leader
    kubectl logs -l jobset.sigs.k8s.io/replicatedjob-name=leader -c vllm-leader -f --tail=-1
    # monitor worker
    kubectl logs -l jobset.sigs.k8s.io/replicatedjob-name=worker -c vllm-worker -f --tail=-1

    Como o kubectl logs -f transmite registros em primeiro plano, execute o comando de registros do worker em uma sessão de terminal separada se quiser monitorar os dois pods simultaneamente. Para interromper o streaming de registros a qualquer momento, pressione Control+C.

    O streaming de peso inicial, o carregamento do modelo e a captura de gráficos CUDA levam aproximadamente de 15 a 20 minutos. Antes de enviar solicitações, aguarde até que os registros do pod líder mostrem as seguintes mensagens:

    (APIServer pid=1) INFO:     Started server process [1]
    (APIServer pid=1) INFO:     Waiting for application startup.
    (APIServer pid=1) INFO:     Application startup complete.
    

Enviar solicitações de inferência

Envie uma solicitação de conclusão de chat para o servidor vLLM implantado:

  1. Encaminhe a porta local 8080 para o Serviço vllm-r1-leader em segundo plano:

    kubectl port-forward service/vllm-r1-leader 8080:8080 &
  2. Envie uma solicitação de conclusão de chat para o endpoint /v1/chat/completions:

    curl http://localhost:8080/v1/chat/completions \
        -H "Content-Type: application/json" \
        -d '{
            "model": "deepseek-ai/DeepSeek-R1",
            "messages": [
                {"role": "user", "content": "Explain tensor parallelism in one sentence."}
            ],
            "max_tokens": 100,
            "temperature": 0.6
        }'

Executar cargas de trabalho de comparativo de mercado

Faça um comparativo do servidor DeepSeek-R1 implantado usando vllm bench serve com uma configuração de 8.192 tokens de entrada e 1.024 tokens de saída (8k/1k):

  1. Revise o manifesto deepseek_r1_8k_1k.yaml ou crie o arquivo localmente se você não tiver feito o download dele antes:

    apiVersion: v1
    kind: Pod
    metadata:
      name: vllm-bench-8k-1k
      namespace: default
      labels:
        app: vllm-bench
      annotations:
        gke-gcsfuse/volumes: "true"
    spec:
      restartPolicy: Never
      tolerations:
      - operator: Exists
      containers:
      - name: benchmark
        image: docker.io/vllm/vllm-openai@sha256:61fc8a896b0a4fbbbdc063bc4b0dbc25ce98e02b5050c24aeb7830ac02039b14
        imagePullPolicy: IfNotPresent
        env:
        - name: PYTHONUNBUFFERED
          value: "1"
        - name: HF_TOKEN
          valueFrom:
            secretKeyRef:
              name: hf-secret
              key: hf_api_token
        - name: HUGGING_FACE_HUB_TOKEN
          valueFrom:
            secretKeyRef:
              name: hf-secret
              key: hf_api_token
        command: ["/bin/bash", "-c"]
        args:
        - |
          set -e
          echo "=== Benchmark: Input 8k (8192) / Output 1k (1024) [1280 Prompts, Concurrency 112] ==="
          vllm bench serve \
            --backend openai \
            --base-url http://vllm-r1-leader:8080 \
            --model deepseek-ai/DeepSeek-R1 \
            --tokenizer /models/DeepSeek-R1 \
            --dataset-name random \
            --random-input-len 8192 \
            --random-output-len 1024 \
            --random-range-ratio 0.0 \
            --num-prompts 1280 \
            --max-concurrency 112 \
            --request-rate inf \
            --ready-check-timeout-sec 0 \
            --trust-remote-code
        resources:
          requests:
            cpu: "8"
            memory: 16Gi
        volumeMounts:
        - mountPath: /models
          name: model-volume
          readOnly: true
      volumes:
      - name: model-volume
        csi:
          driver: gcsfuse.csi.storage.gke.io
          volumeAttributes:
            bucketName: $MODEL_BUCKET
            mountOptions: implicit-dirs

    O manifesto especifica 8.192 tokens de entrada, 1.024 tokens de saída, 1.280 comandos e uma simultaneidade máxima de 112.

  2. Aplique o manifesto deepseek_r1_8k_1k.yaml:

    envsubst '$MODEL_BUCKET' < deepseek_r1_8k_1k.yaml | kubectl apply -f -
  3. Aguarde o pod vllm-bench-8k-1k ser iniciado e, em seguida, transmita os registros de comparação:

    kubectl wait --for=condition=Ready pod/vllm-bench-8k-1k --timeout=120s
    kubectl logs -f vllm-bench-8k-1k -c benchmark

    O comparativo de mercado leva cerca de 15 minutos para ser concluído. Depois que o comparativo de mercado for concluído, procure uma saída que contenha métricas como as seguintes:

    • Output token throughput
    • Total token throughput
    • TTFT
    • TPOT

Limpar

Para evitar cobranças contínuas adicionais na sua conta do Google Cloud pelos recursos usados neste tutorial, exclua as cargas de trabalho do Kubernetes, o cluster do GKE e os buckets do Cloud Storage:

  1. Exclua os pods de comparativo de mercado, o JobSet e o serviço do servidor vLLM, o job de download do modelo e o secret do Hugging Face:

    envsubst '$MODEL_BUCKET' < deepseek_r1_8k_1k.yaml | kubectl delete -f - --ignore-not-found || true
    envsubst '$MODEL_BUCKET' < deepseek_r1_server.yaml | kubectl delete -f - --ignore-not-found || true
    kubectl delete job download-deepseek-r1 --ignore-not-found || true
    kubectl wait --for=delete pod -l app=vllm-r1 --timeout=120s || true
    kubectl delete secret hf-secret --ignore-not-found || true
  2. Exclua o bucket do Cloud Storage que armazena os pesos do modelo em etapas:

    gcloud storage rm --recursive "gs://${MODEL_BUCKET}"
  3. No diretório cluster-toolkit, destrua o cluster do GKE e a infraestrutura associada usando o comando gcluster destroy:

    ./gcluster destroy "${CLUSTER_NAME}" --robust --auto-approve
  4. Exclua o bucket do Cloud Storage que armazena o estado do Terraform:

    gcloud storage rm --recursive "gs://${TERRAFORM_BUCKET}"

A seguir