Diffuser DeepSeek-R1 671B sur GKE avec vLLM multi-hôte sur des VM A4

Ce tutoriel vous explique comment déployer et diffuser DeepSeek-R1 (deepseek-ai/DeepSeek-R1), un modèle Mixture-of-Experts de 671 milliards de paramètres avec 37 milliards de paramètres actifs, sur Google Kubernetes Engine (GKE) surGoogle Cloud. Le déploiement utilise Cluster Toolkit, deux instances de machines virtuelles (VM) A4 (a4-highgpu-8g) multi-hôtes avec un total de 16 GPU NVIDIA B200, et le framework de diffusion vLLM.

Dans la configuration de déploiement décrite dans ce tutoriel, vous allez utiliser Cluster Toolkit pour provisionner un cluster GKE en mode Standard. Le cluster comporte deux nœuds a4-highgpu-8g connectés via un réseau GPUDirect RDMA (Remote Direct Memory Access). Vous préparez ensuite les pondérations du modèle dans Cloud Storage et utilisez l'API Kubernetes JobSet et Ray pour diffuser le modèle sur les 16 GPU NVIDIA B200 avec le parallélisme de tenseur, le parallélisme d'expert et un cache KV FP8.

Objectifs

  • Installez Cluster Toolkit et provisionnez un cluster GKE A4 multi-hôte en mode Standard.
  • Préparez les pondérations du modèle DeepSeek-R1 dans un bucket Cloud Storage.
  • Déployez un serveur d'inférence vLLM multi-hôte avec JobSet et Ray sur deux nœuds A4, avec un total de 16 GPU NVIDIA B200.
  • Envoyez une requête de complétion de chat au serveur vLLM.
  • Exécutez une charge de travail de benchmark de diffusion.
  • Nettoyez les ressources déployées.

Coûts

Dans ce document, vous utilisez les composants facturables suivants deGoogle Cloud :

Obtenez une estimation des coûts en fonction de votre utilisation prévue à l'aide du simulateur de coût.

Avant de commencer

Configurez votre environnement pour le tutoriel :

  1. Installez et initialisez la Google Cloud CLI, puis installez kubectl. Si vous utilisez Cloud Shell, les deux outils sont préinstallés.
  2. Si vous suivez ce tutoriel depuis un poste de travail Linux ou macOS plutôt que depuis Cloud Shell, installez les dépendances Cluster Toolkit requises (telles que Terraform et Git).
  3. Activez l'API Kubernetes Engine, l'API Compute Engine, l'API Cloud Storage, l'API Artifact Registry et l'API Service Usage dans votre projet Google Cloud  :

    Activer les API

Obtenir un jeton d'accès Hugging Face

Vous avez besoin d'un jeton d'accès Hugging Face pour suivre ce tutoriel. Vous pouvez créer un compte sans frais sur Hugging Face. Une fois que vous disposez d'un compte, générez un jeton d'accès :

  1. Sur la page Welcome to Hugging Face (Bienvenue sur Hugging Face), cliquez sur l'avatar de votre compte, puis sélectionnez Access tokens (Jetons d'accès).
  2. Sur la page Access tokens (Jetons d'accès), cliquez sur Create new token (Créer un jeton).
  3. Sélectionnez le type de jeton Read (Lecture) et saisissez un nom pour votre jeton.
  4. Votre jeton d'accès s'affiche. Enregistrez le jeton dans un endroit sûr.

Rôles requis

Pour obtenir les autorisations nécessaires pour suivre ce tutoriel, demandez à votre administrateur de vous accorder les rôles IAM suivants sur votre projet :

Pour en savoir plus sur l'attribution de rôles, consultez la page Gérer l'accès aux projets, aux dossiers et aux organisations.

Vous pouvez également obtenir les autorisations requises avec des rôles personnalisés ou d'autres rôles prédéfinis.

Configurer vos variables d'environnement

Configurez l'authentification et les variables d'environnement pour le tutoriel :

  1. Configurez votre authentification :

    gcloud auth login
    gcloud auth application-default login
  2. Exportez les variables d'environnement que vous utiliserez tout au long de ce tutoriel :

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

    Remplacez les éléments suivants :

    • YOUR_PROJECT_ID : ID de votre projetGoogle Cloud .
    • YOUR_REGION : région de calcul où se trouve votre capacité A4 et où vous souhaitez déployer votre cluster, par exemple us-central1.
    • YOUR_ZONE : zone de calcul où se trouve votre capacité A4, par exemple us-central1-b.
    • YOUR_CLUSTER_NAME : nom de votre cluster GKE et de votre déploiement Cluster Toolkit. Utilisez un nom de 20 caractères ou moins pour que l'ID de compte de service du pool de nœuds généré (YOUR_CLUSTER_NAME-gke-np-sa) ne dépasse pas la limite de 30 caractères d'Identity and Access Management (IAM).
    • YOUR_RESERVATION_NAME : nom de votre réservation A4 ou chemin d'accès au bloc de réservation étendu au format RESERVATION_NAME/reservationBlocks/RESERVATION_BLOCK_NAME.
    • YOUR_HF_TOKEN : votre jeton d'accès Hugging Face.
    • YOUR_GCS_BUCKET : nom unique du bucket Cloud Storage dans lequel vous préparez les pondérations du modèle. Le script définit également TERRAFORM_BUCKET sur YOUR_GCS_BUCKET-tf-state pour stocker l'état Terraform pour Cluster Toolkit.

    Le script définit également CLUSTER_TOOLKIT_VERSION sur v1.104.0, ce qui spécifie la version de Cluster Toolkit à installer. Cluster Toolkit version v1.104.0 et ultérieures incluent des options de consommation de capacité intégrées pour examples/gke-a4/gke-a4-deployment.yaml. Dans les versions antérieures à v1.104.0, vous devez ajouter ces options de capacité manuellement.

Créer le cluster GKE

Téléchargez Cluster Toolkit et provisionnez un cluster GKE en mode Standard avec deux nœuds a4-highgpu-8g, la mise en réseau GPUDirect RDMA et le contrôleur JobSet :

  1. Téléchargez et extrayez le bundle Cluster Toolkit prédéfini pour la version spécifiée dans CLUSTER_TOOLKIT_VERSION (v1.104.0), remplacez votre répertoire de travail par le répertoire cluster-toolkit, puis vérifiez la version binaire :

    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. Vous pouvez éventuellement afficher les commandes et les options disponibles dans Cluster Toolkit :

    ./gcluster --help
  3. Créez le bucket Cloud Storage qui stocke l'état Terraform pour votre déploiement Cluster Toolkit :

    gcloud storage buckets create "gs://${TERRAFORM_BUCKET}" \
        --default-storage-class=STANDARD \
        --project="${PROJECT_ID}" \
        --location="${REGION}" \
        --uniform-bucket-level-access
  4. Créez le fichier de configuration de déploiement examples/gke-a4/gke-a4-deployment.yaml pour deux nœuds 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

    Par défaut, le plan examples/gke-a4/gke-a4.yaml est configuré pour utiliser une réservation spécifique. Si vous n'avez pas de réservation spécifique, vous pouvez modifier examples/gke-a4/gke-a4.yaml avant de déployer le cluster pour utiliser l'une des options de consommation suivantes :

    • Capacité Spot : assurez-vous que votre projet dispose d'un quota préemptif suffisant pour vos GPU, disques et processeurs. En bas de examples/gke-a4/gke-a4.yaml, mettez en commentaire la section --- Option 1: Specific Reservation --- et annulez la mise en commentaire de la section --- Option 4: Spot ---.
    • VM à démarrage flexible : assurez-vous que votre projet dispose d'un quota préemptif suffisant pour vos GPU, disques et processeurs. En bas de examples/gke-a4/gke-a4.yaml, mettez en commentaire la section --- Option 1: Specific Reservation --- et annulez la mise en commentaire de la section --- Option 2: DWS Flex Start ---.
  5. Déployez le cluster GKE à l'aide de la commande gcluster deploy :

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

    La création du cluster prend environ 30 à 45 minutes. Si vous exécutez ./gcluster deploy sans l'option --auto-approve, saisissez a lorsque vous y êtes invité pour appliquer les modifications proposées. Une fois le déploiement terminé, le résultat se termine par un message semblable à celui-ci :

    Apply complete! Resources: 80 added, 0 changed, 0 destroyed.
    
  6. Configurez les identifiants kubectl pour communiquer avec votre cluster :

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

Stocker les pondérations du modèle dans Cloud Storage

Téléchargez les pondérations DeepSeek-R1 sur le disque SSD local de l'un de vos nœuds A4 et transférez-les vers un bucket Cloud Storage pour un streaming rapide entre les nœuds :

  1. Téléchargez les fichiers manifestes Kubernetes pour le job de téléchargement du modèle, le serveur d'inférence vLLM et les pods de benchmark :

    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. Créez le bucket Cloud Storage avec un accès uniforme au niveau du bucket pour stocker les pondérations du modèle :

    gcloud storage buckets create "gs://${MODEL_BUCKET}" \
        --project="${PROJECT_ID}" \
        --location="${REGION}" \
        --uniform-bucket-level-access
  3. Accordez des autorisations IAM sur le bucket au compte de service Kubernetes par défaut, au compte de service Compute Engine par défaut et au compte de service du pool de nœuds du 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"

    Les autorisations IAM accordées par le script précédent incluent le rôle "Lecteur des anciens buckets Storage" (roles/storage.legacyBucketReader), qui est requis pour --load-format runai_streamer.

  4. Créez un secret Kubernetes nommé hf-secret pour stocker votre jeton d'accès Hugging Face :

    kubectl create secret generic hf-secret \
        --from-literal=hf_api_token="${HF_TOKEN}" \
        --dry-run=client -o yaml | kubectl apply -f -
  5. Examinez le fichier manifeste download_model.yaml ou créez-le localement si vous ne l'avez pas téléchargé précédemment :

    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. Remplacez le nom de votre bucket Cloud Storage dans le fichier manifeste download_model.yaml et lancez le job de téléchargement du modèle :

    envsubst '$MODEL_BUCKET' < download_model.yaml | kubectl apply -f -
  7. Attendez que le pod de téléchargement démarre, puis diffusez les journaux du job :

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

    Le téléchargement du modèle sur le disque SSD NVMe local du nœud et l'importation des pondérations dans votre bucket Cloud Storage prennent environ 30 à 40 minutes.

  8. Une fois le job terminé, vérifiez que les 163 segments .safetensors sont présents dans votre bucket Cloud Storage :

    gcloud storage ls "gs://${MODEL_BUCKET}/DeepSeek-R1/*.safetensors" | wc -l
  9. Supprimez le job de préproduction terminé :

    kubectl delete job download-deepseek-r1

Déployer le serveur vLLM multi-hôte

Le fichier manifeste de la charge de travail (deepseek_r1_server.yaml) définit un service ClusterIP (vllm-r1-leader) et un JobSet Kubernetes (vllm-r1) qui coordonnent la diffusion multi-hôte sur les deux nœuds a4-highgpu-8g :

  • Leader (leader) : exécute une instance répliquée sur un nœud A4 avec huit GPU NVIDIA B200, source l'environnement NCCL/gIB (/usr/local/gib/scripts/set_nccl_env.sh), démarre le nœud principal Ray sur le port 6379 pour un cluster à deux nœuds et lance vllm serve sur le port HTTP 8080. Le serveur diffuse les pondérations directement depuis gs://$MODEL_BUCKET/DeepSeek-R1 à l'aide de --load-format runai_streamer et charge le tokeniseur à partir du volume Cloud Storage FUSE installé sur /models/DeepSeek-R1. Il s'exécute avec --tensor-parallel-size 16, --pipeline-parallel-size 1, --distributed-executor-backend ray, --enable-expert-parallel, --kv-cache-dtype fp8_e4m3, --async-scheduling et les graphiques CUDA (FULL_AND_PIECEWISE).
  • Nœud de calcul (worker) : exécute une instance répliquée sans interface graphique sur le deuxième nœud A4 avec huit GPU NVIDIA B200, configure NCCL/gIB et rejoint le cluster Ray du leader à l'adresse vllm-r1-leader:6379.

Déployez et surveillez le serveur vLLM multihôte :

  1. Examinez le fichier manifeste deepseek_r1_server.yaml ou créez-le localement si vous ne l'avez pas téléchargé précédemment :

    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

    Pour effectuer un scaling de ce déploiement sur davantage d'hôtes, mettez à jour les paramètres dans deepseek_r1_server.yaml. Par exemple, pour effectuer un scaling jusqu'à quatre nœuds A4, avec un total de 32 GPU NVIDIA B200, définissez replicas: 3 sous le job répliqué worker et mettez à jour les arguments du leader comme suit :

    • --ray_cluster_size=4
    • --tensor-parallel-size=16
    • --pipeline-parallel-size=2
  2. Remplacez deepseek_r1_server.yaml par le nom de votre bucket Cloud Storage et appliquez le fichier manifeste à votre cluster :

    envsubst '$MODEL_BUCKET' < deepseek_r1_server.yaml | kubectl apply -f -
  3. Surveillez les journaux des pods leader et worker jusqu'à ce que le démarrage du serveur soit terminé :

    # 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

    Étant donné que kubectl logs -f diffuse les journaux au premier plan, exécutez la commande de journaux du nœud de calcul dans une session de terminal distincte si vous souhaitez surveiller les deux pods simultanément. Pour arrêter le streaming des journaux à tout moment, appuyez sur Ctrl+C.

    La diffusion initiale des pondérations, le chargement du modèle et la capture du graphe CUDA prennent environ 15 à 20 minutes. Avant d'envoyer des requêtes, attendez que les journaux du pod leader affichent les messages suivants :

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

Envoyer des requêtes d'inférence

Envoyez une requête de complétion de chat au serveur vLLM déployé :

  1. Transférez le port local 8080 vers le service vllm-r1-leader en arrière-plan :

    kubectl port-forward service/vllm-r1-leader 8080:8080 &
  2. Envoyez une requête de complétion de chat au point de terminaison /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
        }'

Exécuter des charges de travail de benchmark

Évaluez le serveur DeepSeek-R1 déployé à l'aide de vllm bench serve avec une configuration de 8 192 jetons d'entrée et 1 024 jetons de sortie (8k/1k) :

  1. Examinez le fichier manifeste deepseek_r1_8k_1k.yaml ou créez-le localement si vous ne l'avez pas téléchargé précédemment :

    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

    Le fichier manifeste spécifie 8 192 jetons d'entrée, 1 024 jetons de sortie, 1 280 requêtes et une simultanéité maximale de 112.

  2. Appliquez le fichier manifeste deepseek_r1_8k_1k.yaml :

    envsubst '$MODEL_BUCKET' < deepseek_r1_8k_1k.yaml | kubectl apply -f -
  3. Attendez que le pod vllm-bench-8k-1k démarre, puis diffusez les journaux de benchmark :

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

    L'exécution du benchmark prend environ 15 minutes. Une fois l'analyse comparative terminée, recherchez un résultat contenant des métriques telles que les suivantes :

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

Effectuer un nettoyage

Pour éviter que des frais supplémentaires ne soient facturés sur votre compte Google Cloud pour les ressources utilisées dans ce tutoriel, supprimez les charges de travail Kubernetes, le cluster GKE et vos buckets Cloud Storage :

  1. Supprimez les pods de benchmark, le JobSet et le service du serveur vLLM, le job de téléchargement du modèle et le secret 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. Supprimez le bucket Cloud Storage qui stocke les pondérations du modèle préproduit :

    gcloud storage rm --recursive "gs://${MODEL_BUCKET}"
  3. À partir du répertoire cluster-toolkit, détruisez le cluster GKE et l'infrastructure associée à l'aide de la commande gcluster destroy :

    ./gcluster destroy "${CLUSTER_NAME}" --robust --auto-approve
  4. Supprimez le bucket Cloud Storage qui stocke l'état Terraform :

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

Étapes suivantes