Gestisci DeepSeek-R1 671B su GKE con vLLM multi-host su VM A4

Questo tutorial mostra come eseguire il deployment e l'addestramento di DeepSeek-R1 (deepseek-ai/DeepSeek-R1), un modello Mixture of Experts con 671 miliardi di parametri e 37 miliardi di parametri attivi, su Google Kubernetes Engine (GKE) suGoogle Cloud. Il deployment utilizza Cluster Toolkit, due istanze di macchine virtuali (VM) A4 (a4-highgpu-8g) multi-host con un totale di 16 GPU NVIDIA B200 e il framework di distribuzione vLLM.

Nella configurazione del deployment descritta in questo tutorial, utilizzi Cluster Toolkit per eseguire il provisioning di un cluster GKE in modalità Standard. Il cluster ha due nodi a4-highgpu-8g connessi tramite networking GPUDirect accesso diretto alla memoria da remoto (RDMA). Quindi, esegui lo stage dei pesi del modello in Cloud Storage e utilizzi l'API JobSet di Kubernetes e Ray per gestire il modello su tutte le 16 GPU NVIDIA B200 con parallelismo dei tensori, parallelismo degli esperti e una cache KV FP8.

Obiettivi

  • Installa Cluster Toolkit ed esegui il provisioning di un cluster GKE A4 multi-host in modalità Standard.
  • Esegui lo stage dei pesi del modello DeepSeek-R1 in un bucket Cloud Storage.
  • Esegui il deployment di un server di inferenza vLLM multi-host con JobSet e Ray su due nodi A4, con un totale di 16 GPU NVIDIA B200.
  • Invia una richiesta di completamento della chat al server vLLM.
  • Esegui un workload di benchmark di gestione.
  • Esegui la pulizia delle risorse di cui hai eseguito il deployment.

Costi

In questo documento vengono utilizzati i seguenti componenti fatturabili diGoogle Cloud:

Per generare una stima dei costi in base all'utilizzo previsto, utilizza il calcolatore prezzi.

Prima di iniziare

Configura l'ambiente per il tutorial:

  1. Installa e inizializza Google Cloud CLI e installa kubectl. Se utilizzi Cloud Shell, entrambi gli strumenti sono preinstallati.
  2. Se esegui questo tutorial da una workstation Linux o macOS anziché da Cloud Shell, installa le dipendenze di Cluster Toolkit richieste (come Terraform e Git).
  3. Abilita l'API Kubernetes Engine, l'API Compute Engine, l'API Cloud Storage, l'API Artifact Registry e l'API Service Usage nel tuo progetto Google Cloud :

    Abilita le API

Ottieni un token di accesso a Hugging Face

Per utilizzare questo tutorial, devi disporre di un token di accesso a Hugging Face. Puoi registrarti per un account senza costi su Hugging Face. Dopo aver creato un account, genera un token di accesso:

  1. Nella pagina Welcome to Hugging Face, fai clic sull'avatar del tuo account e seleziona Token di accesso.
  2. Nella pagina Token di accesso, fai clic su Crea nuovo token.
  3. Seleziona il tipo di token Lettura e inserisci un nome per il token.
  4. Viene visualizzato il token di accesso. Salva il token in un luogo sicuro.

Ruoli obbligatori

Per ottenere le autorizzazioni necessarie per completare questo tutorial, chiedi all'amministratore di concederti i seguenti ruoli IAM nel progetto:

Per saperne di più sulla concessione dei ruoli, consulta Gestisci l'accesso a progetti, cartelle e organizzazioni.

Potresti anche riuscire a ottenere le autorizzazioni richieste tramite i ruoli personalizzati o altri ruoli predefiniti.

Imposta le variabili di ambiente

Configura l'autenticazione e le variabili di ambiente per il tutorial:

  1. Configura l'autenticazione:

    gcloud auth login
    gcloud auth application-default login
  2. Esporta le variabili di ambiente che utilizzi in questo 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}"

    Sostituisci quanto segue:

    • YOUR_PROJECT_ID: l'ID del tuo progettoGoogle Cloud .
    • YOUR_REGION: la regione di computing in cui si trova la tua capacità A4 e in cui vuoi eseguire il deployment del cluster, ad esempio us-central1.
    • YOUR_ZONE: la zona di computing in cui si trova la tua capacità A4, ad esempio us-central1-b.
    • YOUR_CLUSTER_NAME: il nome del tuo cluster GKE e del deployment di Cluster Toolkit. Utilizza un nome di massimo 20 caratteri in modo che l'ID account del service account del pool di nodi generato (YOUR_CLUSTER_NAME-gke-np-sa) rimanga entro il limite di 30 caratteri di Identity and Access Management (IAM).
    • YOUR_RESERVATION_NAME: il nome della prenotazione A4 o il percorso del blocco di prenotazione esteso nel formato RESERVATION_NAME/reservationBlocks/RESERVATION_BLOCK_NAME.
    • YOUR_HF_TOKEN: il tuo token di accesso a Hugging Face.
    • YOUR_GCS_BUCKET: un nome univoco a livello globale per il bucket Cloud Storage in cui eseguire l'addestramento dei pesi del modello. Lo script imposta anche TERRAFORM_BUCKET su YOUR_GCS_BUCKET-tf-state per archiviare lo stato di Terraform per Cluster Toolkit.

    Lo script imposta anche CLUSTER_TOOLKIT_VERSION su v1.104.0, che specifica la versione di Cluster Toolkit da installare. Cluster Toolkit versione v1.104.0 e successive include opzioni integrate per il consumo della capacità per examples/gke-a4/gke-a4-deployment.yaml. Nelle versioni precedenti alla v1.104.0, devi aggiungere queste opzioni di capacità manualmente.

Crea il cluster GKE

Scarica Cluster Toolkit ed esegui il provisioning di un cluster GKE in modalità Standard con due nodi a4-highgpu-8g, networking GPUDirect RDMA e il controller JobSet:

  1. Scarica ed estrai il bundle Cluster Toolkit predefinito per la versione specificata in CLUSTER_TOOLKIT_VERSION (v1.104.0), cambia la directory di lavoro in cluster-toolkit e verifica la versione binaria:

    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. Facoltativamente, visualizza i comandi e i flag disponibili in Cluster Toolkit:

    ./gcluster --help
  3. Crea il bucket Cloud Storage che archivia lo stato di Terraform per il deployment di Cluster Toolkit:

    gcloud storage buckets create "gs://${TERRAFORM_BUCKET}" \
        --default-storage-class=STANDARD \
        --project="${PROJECT_ID}" \
        --location="${REGION}" \
        --uniform-bucket-level-access
  4. Crea il file di configurazione del deployment examples/gke-a4/gke-a4-deployment.yaml per due nodi 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

    Per impostazione predefinita, il progetto examples/gke-a4/gke-a4.yaml è configurato per utilizzare una prenotazione specifica. Se non hai una prenotazione specifica, puoi modificare examples/gke-a4/gke-a4.yaml prima di eseguire il deployment del cluster per utilizzare una delle seguenti opzioni di consumo:

    • Capacità spot: assicurati che il tuo progetto disponga di una quota preemptible sufficiente per GPU, dischi e CPU. Nella parte inferiore di examples/gke-a4/gke-a4.yaml, commenta la sezione --- Option 1: Specific Reservation --- e rimuovi il commento dalla sezione --- Option 4: Spot ---.
    • VM Flex-Start: assicurati che il tuo progetto disponga di una quota preemptible sufficiente per GPU, dischi e CPU. Nella parte inferiore di examples/gke-a4/gke-a4.yaml, commenta la sezione --- Option 1: Specific Reservation --- e rimuovi il commento dalla sezione --- Option 2: DWS Flex Start ---.
  5. Esegui il deployment del cluster GKE utilizzando il 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 creazione del cluster richiede circa 30-45 minuti. Se esegui ./gcluster deploy senza il flag --auto-approve, per applicare le modifiche proposte, inserisci a quando richiesto. Al termine del deployment, l'output termina con un messaggio simile al seguente:

    Apply complete! Resources: 80 added, 0 changed, 0 destroyed.
    
  6. Configura le credenziali kubectl per comunicare con il tuo cluster:

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

Esegui lo stage dei pesi del modello in Cloud Storage

Scarica i pesi di DeepSeek-R1 sull'SSD locale di uno dei tuoi nodi A4 e trasferiscili in un bucket Cloud Storage per lo streaming rapido tra i nodi:

  1. Scarica i file manifest di Kubernetes per il job di download del modello, il server di inferenza vLLM e i pod di 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. Crea il bucket Cloud Storage con accesso uniforme a livello di bucket per archiviare i pesi del modello:

    gcloud storage buckets create "gs://${MODEL_BUCKET}" \
        --project="${PROJECT_ID}" \
        --location="${REGION}" \
        --uniform-bucket-level-access
  3. Concedi le autorizzazioni IAM sul bucket al account di servizio Kubernetes predefinito, al account di servizio Compute Engine predefinito e al account di servizio del pool di nodi del 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"

    Le autorizzazioni IAM concesse dallo script precedente includono il ruolo Storage Legacy Bucket Reader (roles/storage.legacyBucketReader), richiesto per --load-format runai_streamer.

  4. Crea un secret di Kubernetes denominato hf-secret per archiviare il tuo token di accesso Hugging Face:

    kubectl create secret generic hf-secret \
        --from-literal=hf_api_token="${HF_TOKEN}" \
        --dry-run=client -o yaml | kubectl apply -f -
  5. Esamina il manifest download_model.yaml o crea il file localmente se non l'hai scaricato in precedenza:

    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. Sostituisci il nome del tuo bucket Cloud Storage nel manifest download_model.yaml e avvia il job di download del modello:

    envsubst '$MODEL_BUCKET' < download_model.yaml | kubectl apply -f -
  7. Attendi l'avvio del pod di download, quindi trasmetti in streaming i log del job:

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

    Il download del modello nell'SSD NVMe locale del nodo e il caricamento dei pesi nel bucket Cloud Storage richiedono circa 30-40 minuti.

  8. Una volta completato il job, verifica che tutti i 163 shard .safetensors siano presenti nel bucket Cloud Storage:

    gcloud storage ls "gs://${MODEL_BUCKET}/DeepSeek-R1/*.safetensors" | wc -l
  9. Elimina il job di gestione temporanea completato:

    kubectl delete job download-deepseek-r1

Esegui il deployment del server vLLM multi-host

Il manifest del workload (deepseek_r1_server.yaml) definisce un servizio ClusterIP (vllm-r1-leader) e un JobSet Kubernetes (vllm-r1) che coordinano la distribuzione multi-host tra i due nodi a4-highgpu-8g:

  • Leader (leader): esegue una replica su un nodo A4 con otto GPU NVIDIA B200, genera l'ambiente NCCL/gIB (/usr/local/gib/scripts/set_nccl_env.sh), avvia il nodo head Ray sulla porta 6379 per un cluster a due nodi e avvia vllm serve sulla porta HTTP 8080. Il server trasmette i pesi direttamente da gs://$MODEL_BUCKET/DeepSeek-R1 utilizzando --load-format runai_streamer e carica il tokenizer dal volume Cloud Storage FUSE montato in /models/DeepSeek-R1. Funziona con --tensor-parallel-size 16, --pipeline-parallel-size 1, --distributed-executor-backend ray, --enable-expert-parallel, --kv-cache-dtype fp8_e4m3, --async-scheduling e grafici CUDA (FULL_AND_PIECEWISE).
  • Worker (worker): esegue una replica headless sul secondo nodo A4 con otto GPU NVIDIA B200, configura NCCL/gIB e si unisce al cluster Ray del leader in vllm-r1-leader:6379.

Esegui il deployment e monitora il server vLLM multi-host:

  1. Esamina il manifest deepseek_r1_server.yaml o crea il file localmente se non l'hai scaricato in precedenza:

    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

    Per scalare questo deployment su più host, aggiorna i parametri in deepseek_r1_server.yaml. Ad esempio, per scalare a quattro nodi A4, con un totale di 32 GPU NVIDIA B200, imposta replicas: 3 in worker del job replicato e aggiorna gli argomenti del leader come segue:

    • --ray_cluster_size=4
    • --tensor-parallel-size=16
    • --pipeline-parallel-size=2
  2. Sostituisci il nome del tuo bucket Cloud Storage in deepseek_r1_server.yaml e applica il manifest al tuo cluster:

    envsubst '$MODEL_BUCKET' < deepseek_r1_server.yaml | kubectl apply -f -
  3. Monitora i log dei pod leader e worker fino al completamento dell'avvio del server:

    # 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

    Poiché kubectl logs -f trasmette i log in primo piano, esegui il comando dei log del worker in una sessione del terminale separata se vuoi monitorare entrambi i pod contemporaneamente. Per interrompere lo streaming dei log in qualsiasi momento, premi Ctrl+C.

    Lo streaming iniziale dei pesi, il caricamento del modello e l'acquisizione del grafico CUDA richiedono circa 15-20 minuti. Prima di inviare richieste, attendi che i log del pod leader restituiscano i seguenti messaggi:

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

Invia richieste di inferenza

Invia una richiesta di completamento della chat al server vLLM di cui è stato eseguito il deployment:

  1. Inoltra la porta locale 8080 al servizio vllm-r1-leader in background:

    kubectl port-forward service/vllm-r1-leader 8080:8080 &
  2. Invia una richiesta di completamento della chat all'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
        }'

Esegui workload di benchmark

Esegui il benchmark del server DeepSeek-R1 di cui è stato eseguito il deployment utilizzando vllm bench serve con una configurazione di 8192 token di input e 1024 token di output (8k/1k):

  1. Esamina il manifest deepseek_r1_8k_1k.yaml o crea il file localmente se non l'hai scaricato in precedenza:

    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

    Il manifest specifica 8192 token di input, 1024 token di output, 1280 prompt e una concorrenza massima di 112.

  2. Applica il manifest deepseek_r1_8k_1k.yaml:

    envsubst '$MODEL_BUCKET' < deepseek_r1_8k_1k.yaml | kubectl apply -f -
  3. Attendi l'avvio del pod vllm-bench-8k-1k, quindi trasmetti in streaming i log di benchmark:

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

    Il completamento del benchmark richiede circa 15 minuti. Al termine del benchmark, cerca un output contenente metriche come le seguenti:

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

Esegui la pulizia

Per evitare che al tuo account Google Cloud vengano addebitati costi aggiuntivi per le risorse utilizzate in questo tutorial, elimina i workload Kubernetes, il cluster GKE e i bucket Cloud Storage:

  1. Elimina i pod di benchmark, il JobSet e il Service del server vLLM, il job di download del modello e il secret di 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. Elimina il bucket Cloud Storage in cui sono archiviati i pesi del modello di gestione temporanea:

    gcloud storage rm --recursive "gs://${MODEL_BUCKET}"
  3. Dalla directory cluster-toolkit, elimina il cluster GKE e l'infrastruttura associata utilizzando il comando gcluster destroy:

    ./gcluster destroy "${CLUSTER_NAME}" --robust --auto-approve
  4. Elimina il bucket Cloud Storage in cui è archiviato lo stato di Terraform:

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

Passaggi successivi