Entrega DeepSeek-R1 671B en GKE con vLLM de varios hosts en VMs A4

En este instructivo, se muestra cómo implementar y entregar DeepSeek-R1 (deepseek-ai/DeepSeek-R1), un modelo de mezcla de expertos de 671,000 millones de parámetros con 37,000 millones de parámetros activos, en Google Kubernetes Engine (GKE) en Google Cloud. La implementación usa Cluster Toolkit, dos instancias de máquina virtual (VM) A4 (a4-highgpu-8g) multihost con un total de 16 GPUs NVIDIA B200 y el framework de entrega de vLLM.

En la configuración de implementación que se describe en este instructivo, usarás Cluster Toolkit para aprovisionar un clúster de GKE en modo estándar. El clúster tiene dos nodos a4-highgpu-8g conectados a través de la red de acceso remoto directo a memoria (RDMA) de GPUDirect. Luego, almacena en etapa intermedia los pesos del modelo en Cloud Storage y usa la API de JobSet de Kubernetes y Ray para entregar el modelo en las 16 GPU NVIDIA B200 con paralelismo de tensores, paralelismo de expertos y una caché de KV de FP8.

Objetivos

  • Instala Cluster Toolkit y aprovisiona un clúster de GKE de A4 con varios hosts en el modo estándar.
  • Almacena en etapa intermedia las ponderaciones del modelo DeepSeek-R1 en un bucket de Cloud Storage.
  • Implementa un servidor de inferencia de vLLM de varios hosts con JobSet y Ray en dos nodos A4, con un total de 16 GPUs NVIDIA B200.
  • Envía una solicitud de finalización de chat al servidor de vLLM.
  • Ejecuta una carga de trabajo de comparativas de entrega.
  • Limpia los recursos implementados.

Costos

En este documento, usarás los siguientes componentes facturables de Google Cloud:

Para generar una estimación de costos en función del uso previsto, usa la calculadora de precios.

Antes de comenzar

Configura tu entorno para el instructivo:

  1. Instala y, luego, inicializa Google Cloud CLI, y, luego, instala kubectl. Si usas Cloud Shell, ambas herramientas están preinstaladas.
  2. Si ejecutas este instructivo desde una estación de trabajo de Linux o macOS en lugar de Cloud Shell, instala las dependencias de Cluster Toolkit necesarias (como Terraform y Git).
  3. Habilita la API de Kubernetes Engine, la API de Compute Engine, la API de Cloud Storage, la API de Artifact Registry y la API de Service Usage en tu proyecto de Google Cloud :

    Habilita las API

Obtén un token de acceso de Hugging Face

Necesitas un token de acceso de Hugging Face para usar este instructivo. Puedes registrarte para obtener una cuenta gratuita en Hugging Face. Después de tener una cuenta, genera un token de acceso:

  1. En la página Welcome to Hugging Face, haz clic en el avatar de tu cuenta y selecciona Tokens de acceso.
  2. En la página Tokens de acceso, haz clic en Crear token nuevo.
  3. Selecciona el tipo de token Lectura y, luego, ingresa un nombre para el token.
  4. Se mostrará tu token de acceso. Guarda el token en un lugar seguro.

Roles obligatorios

Si quieres obtener los permisos que necesitas para completar este instructivo, pídele a tu administrador que te otorgue los siguientes roles de IAM en tu proyecto:

Para obtener más información sobre cómo otorgar roles, consulta Administra el acceso a proyectos, carpetas y organizaciones.

También puedes obtener los permisos necesarios a través de roles personalizados o cualquier otro rol predefinido.

Configura tus variables de entorno

Configura la autenticación y las variables de entorno para el instructivo:

  1. Configura la autenticación:

    gcloud auth login
    gcloud auth application-default login
  2. Exporta las variables de entorno que usarás en este instructivo:

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

    Reemplaza lo siguiente:

    • YOUR_PROJECT_ID: Es el ID de tu proyecto de Google Cloud .
    • YOUR_REGION: Es la región de procesamiento en la que se encuentra tu capacidad de A4 y en la que deseas implementar tu clúster, como us-central1.
    • YOUR_ZONE: Es la zona de procesamiento en la que se encuentra tu capacidad de A4, como us-central1-b.
    • YOUR_CLUSTER_NAME: Es el nombre de tu clúster de GKE y de la implementación de Cluster Toolkit. Usa un nombre de 20 caracteres o menos para que el ID de cuenta de servicio del grupo de nodos generado (YOUR_CLUSTER_NAME-gke-np-sa) se mantenga dentro del límite de 30 caracteres de Identity and Access Management (IAM).
    • YOUR_RESERVATION_NAME: Es el nombre de tu reserva de A4 o la ruta de acceso del bloque de reserva extendido en el formato RESERVATION_NAME/reservationBlocks/RESERVATION_BLOCK_NAME.
    • YOUR_HF_TOKEN: Es tu token de acceso de Hugging Face.
    • YOUR_GCS_BUCKET: un nombre único a nivel global para el bucket de Cloud Storage en el que almacenas en etapa intermedia los pesos del modelo. La secuencia de comandos también establece TERRAFORM_BUCKET en YOUR_GCS_BUCKET-tf-state para almacenar el estado de Terraform para Cluster Toolkit.

    La secuencia de comandos también establece CLUSTER_TOOLKIT_VERSION en v1.104.0, lo que especifica la versión de Cluster Toolkit que se instalará. La versión v1.104.0 y las posteriores de Cluster Toolkit incluyen opciones integradas de consumo de capacidad para examples/gke-a4/gke-a4-deployment.yaml. En las versiones anteriores a la v1.104.0, debes agregar estas opciones de capacidad de forma manual.

Crea el clúster de GKE

Descarga Cluster Toolkit y aprovisiona un clúster de GKE en modo estándar con dos nodos a4-highgpu-8g, redes de RDMA de GPUDirect y el controlador de JobSet:

  1. Descarga y extrae el paquete de Cluster Toolkit compilado previamente para la versión especificada en CLUSTER_TOOLKIT_VERSION (v1.104.0), cambia tu directorio de trabajo al directorio cluster-toolkit y verifica la versión del objeto binario:

    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. De manera opcional, consulta los comandos y las marcas disponibles en Cluster Toolkit:

    ./gcluster --help
  3. Crea el bucket de Cloud Storage que almacena el estado de Terraform para tu implementación de Cluster Toolkit:

    gcloud storage buckets create "gs://${TERRAFORM_BUCKET}" \
        --default-storage-class=STANDARD \
        --project="${PROJECT_ID}" \
        --location="${REGION}" \
        --uniform-bucket-level-access
  4. Crea el archivo de configuración de implementación de examples/gke-a4/gke-a4-deployment.yaml para dos nodos de 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

    De forma predeterminada, el plano examples/gke-a4/gke-a4.yaml está configurado para usar una reserva específica. Si no tienes una reserva específica, puedes editar examples/gke-a4/gke-a4.yaml antes de implementar el clúster para usar una de las siguientes opciones de consumo:

    • Capacidad de Spot: Asegúrate de que tu proyecto tenga suficiente cuota interrumpible para tus GPU, discos y CPU. En la parte inferior de examples/gke-a4/gke-a4.yaml, marca como comentario la sección --- Option 1: Specific Reservation --- y quita el comentario de la sección --- Option 4: Spot ---.
    • VMs de inicio flexible: Asegúrate de que tu proyecto tenga suficiente cuota interrumpible para tus GPUs, discos y CPUs. En la parte inferior de examples/gke-a4/gke-a4.yaml, marca como comentario la sección --- Option 1: Specific Reservation --- y quita el comentario de la sección --- Option 2: DWS Flex Start ---.
  5. Implementa el clúster de GKE con el comando gcluster deploy:

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

    La creación de clústeres tarda entre 30 y 45 minutos aproximadamente. Si ejecutas ./gcluster deploy sin la marca --auto-approve, para aplicar los cambios propuestos, ingresa a cuando se te solicite. Cuando finaliza la implementación, el resultado termina con un mensaje similar al siguiente:

    Apply complete! Resources: 80 added, 0 changed, 0 destroyed.
    
  6. Configura las credenciales de kubectl para comunicarte con tu clúster:

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

Almacena las ponderaciones del modelo en Cloud Storage

Descarga los pesos de DeepSeek-R1 en la SSD local de uno de tus nodos A4 y transfiérelos a un bucket de Cloud Storage para transmitirlos con rapidez entre nodos:

  1. Descarga los archivos de manifiesto de Kubernetes para el trabajo de descarga del modelo, el servidor de inferencia de vLLM y los Pods de comparativas:

    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. Crea el bucket de Cloud Storage con acceso uniforme a nivel de bucket para almacenar las ponderaciones del modelo:

    gcloud storage buckets create "gs://${MODEL_BUCKET}" \
        --project="${PROJECT_ID}" \
        --location="${REGION}" \
        --uniform-bucket-level-access
  3. Otorga permisos de IAM en el bucket a la cuenta de servicio predeterminada de Kubernetes, la cuenta de servicio predeterminada de Compute Engine y la cuenta de servicio del grupo de nodos del clúster (${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"

    Los permisos de IAM que otorga la secuencia de comandos anterior incluyen el rol de lector de buckets heredados de Storage (roles/storage.legacyBucketReader), que es necesario para --load-format runai_streamer.

  4. Crea un Secret de Kubernetes llamado hf-secret para almacenar tu token de acceso de Hugging Face:

    kubectl create secret generic hf-secret \
        --from-literal=hf_api_token="${HF_TOKEN}" \
        --dry-run=client -o yaml | kubectl apply -f -
  5. Revisa el manifiesto download_model.yaml o crea el archivo de forma local si no lo descargaste antes:

    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. Sustituye el nombre de tu bucket de Cloud Storage en el manifiesto download_model.yaml y, luego, inicia el trabajo de descarga del modelo:

    envsubst '$MODEL_BUCKET' < download_model.yaml | kubectl apply -f -
  7. Espera a que se inicie el Pod de descarga y, luego, transmite los registros del trabajo:

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

    La descarga del modelo en la SSD de NVMe local del nodo y la carga de las ponderaciones en tu bucket de Cloud Storage tardan entre 30 y 40 minutos aproximadamente.

  8. Una vez que se haya completado el trabajo, verifica que los 163 fragmentos de .safetensors estén presentes en tu bucket de Cloud Storage:

    gcloud storage ls "gs://${MODEL_BUCKET}/DeepSeek-R1/*.safetensors" | wc -l
  9. Borra el trabajo de etapa de pruebas completado:

    kubectl delete job download-deepseek-r1

Implementa el servidor de vLLM de varios hosts

El manifiesto de la carga de trabajo (deepseek_r1_server.yaml) define un Service ClusterIP (vllm-r1-leader) y un JobSet de Kubernetes (vllm-r1) que coordinan la entrega de varios hosts en los dos nodos a4-highgpu-8g:

  • Líder (leader): Ejecuta una réplica en un nodo A4 con ocho GPU NVIDIA B200, obtiene el entorno de NCCL/gIB (/usr/local/gib/scripts/set_nccl_env.sh), inicia el nodo principal de Ray en el puerto 6379 para un clúster de dos nodos y lanza vllm serve en el puerto HTTP 8080. El servidor transmite los pesos directamente desde gs://$MODEL_BUCKET/DeepSeek-R1 con --load-format runai_streamer y carga el asignador de token desde el volumen de Cloud Storage FUSE activado en /models/DeepSeek-R1. Se ejecuta con --tensor-parallel-size 16, --pipeline-parallel-size 1, --distributed-executor-backend ray, --enable-expert-parallel, --kv-cache-dtype fp8_e4m3, --async-scheduling y gráficos de CUDA (FULL_AND_PIECEWISE).
  • Trabajador (worker): Ejecuta una réplica sin interfaz gráfica en el segundo nodo A4 con ocho GPUs NVIDIA B200, configura NCCL/gIB y se une al clúster de Ray del líder en vllm-r1-leader:6379.

Implementa y supervisa el servidor de vLLM de varios hosts:

  1. Revisa el manifiesto deepseek_r1_server.yaml o crea el archivo de forma local si no lo descargaste 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 escalar esta implementación a más hosts, actualiza los parámetros en deepseek_r1_server.yaml. Por ejemplo, para escalar a cuatro nodos A4, con un total de 32 GPU NVIDIA B200, establece replicas: 3 en el trabajo replicado worker y actualiza los argumentos del líder de la siguiente manera:

    • --ray_cluster_size=4
    • --tensor-parallel-size=16
    • --pipeline-parallel-size=2
  2. Sustituye el nombre de tu bucket de Cloud Storage en deepseek_r1_server.yaml y aplica el manifiesto a tu clúster:

    envsubst '$MODEL_BUCKET' < deepseek_r1_server.yaml | kubectl apply -f -
  3. Supervisa los registros de los Pods líder y trabajador hasta que se complete el inicio del servidor:

    # 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

    Debido a que kubectl logs -f transmite registros en primer plano, ejecuta el comando de registros del trabajador en una sesión de terminal independiente si deseas supervisar ambos Pods de forma simultánea. Para detener la transmisión de registros en cualquier momento, presiona Control + C.

    La transmisión de pesos inicial, la carga del modelo y la captura de grafos de CUDA tardan aproximadamente entre 15 y 20 minutos. Antes de enviar solicitudes, espera hasta que los registros del Pod líder muestren los siguientes mensajes:

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

Envía solicitudes de inferencia

Envía una solicitud de finalización de chat al servidor de vLLM implementado:

  1. Redirecciona el puerto local 8080 al Service vllm-r1-leader en segundo plano:

    kubectl port-forward service/vllm-r1-leader 8080:8080 &
  2. Envía una solicitud de finalización de chat al extremo /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
        }'

Ejecuta cargas de trabajo de comparativas

Compara el servidor de DeepSeek-R1 implementado con vllm bench serve con una configuración de 8,192 tokens de entrada y 1,024 tokens de salida (8k/1k):

  1. Revisa el manifiesto deepseek_r1_8k_1k.yaml o crea el archivo de forma local si no lo descargaste 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

    El manifiesto especifica 8,192 tokens de entrada, 1,024 tokens de salida, 1,280 instrucciones y una simultaneidad máxima de 112.

  2. Aplica el manifiesto deepseek_r1_8k_1k.yaml:

    envsubst '$MODEL_BUCKET' < deepseek_r1_8k_1k.yaml | kubectl apply -f -
  3. Espera a que se inicie el Pod vllm-bench-8k-1k y, luego, transmite los registros de la comparativa:

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

    La comparativa tarda aproximadamente 15 minutos en completarse. Una vez que se complete la comparativa, busca un resultado que contenga métricas como las siguientes:

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

Realiza una limpieza

Para evitar que se apliquen cargos adicionales a tu cuenta de Google Cloud por los recursos que se usaron en este instructivo, borra las cargas de trabajo de Kubernetes, el clúster de GKE y tus buckets de Cloud Storage:

  1. Borra los Pods de comparativas, el JobSet y el Service del servidor de vLLM, el Job de descarga del modelo y el Secret de 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. Borra el bucket de Cloud Storage que almacena los pesos del modelo en etapa de pruebas:

    gcloud storage rm --recursive "gs://${MODEL_BUCKET}"
  3. En el directorio cluster-toolkit, destruye el clúster de GKE y su infraestructura asociada con el comando gcluster destroy:

    ./gcluster destroy "${CLUSTER_NAME}" --robust --auto-approve
  4. Borra el bucket de Cloud Storage que almacena el estado de Terraform:

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

¿Qué sigue?