איך מפעילים את DeepSeek-R1 671B ב-GKE עם vLLM בכמה מארחים במכונות וירטואליות מסוג A4

במדריך הזה נסביר איך לפרוס את המודל DeepSeek-R1 (deepseek-ai/DeepSeek-R1) ולהשתמש בו ב-Google Kubernetes Engine (GKE) ב-Google Cloud. זהו מודל שילוב מומחים (MoE) עם 671 מיליארד פרמטרים, מתוכם 37 מיליארד פרמטרים פעילים. הפריסה משתמשת ב-Cluster Toolkit, בשני מכונות וירטואליות (VM) מסוג A4 (a4-highgpu-8g) עם מספר מארחים ו-16 מעבדי GPU מסוג NVIDIA B200, ובמסגרת להצגת מודעות vLLM.

בהגדרת הפריסה שמתוארת במדריך הזה, משתמשים ב-Cluster Toolkit כדי להקצות אשכול GKE במצב Standard. באשכול יש שני צומתי a4-highgpu-8g שמחוברים באמצעות רשת GPUDirect Remote Direct Memory Access ‏ (RDMA). לאחר מכן, אתם מעבירים את משקלי המודל ל-Cloud Storage ומשתמשים ב-Kubernetes JobSet API וב-Ray כדי להציג את המודל בכל 16 יחידות ה-GPU מסוג NVIDIA B200 עם מקביליות של טנזורים, מקביליות של מומחים ומטמון FP8 KV.

מטרות

  • מתקינים את Cluster Toolkit ומקצים אשכול GKE עם כמה מארחים מסוג A4 במצב רגיל.
  • מעבירים את משקלי המודל של DeepSeek-R1 לקטגוריה של Cloud Storage.
  • פורסים שרת היקשים של vLLM עם מספר מארחים באמצעות JobSet ו-Ray בשני צומתי A4, עם 16 מעבדי NVIDIA B200 GPU בסך הכול.
  • שולחים בקשה להשלמת צ'אט לשרת vLLM.
  • מריצים עומס עבודה להערכת ביצועים של מילוי בקשות.
  • מחיקת המשאבים שנפרסו.

עלויות

במסמך הזה משתמשים ברכיבים הבאים שלGoogle Cloud, והשימוש בהם כרוך בתשלום:

כדי להעריך את העלויות בהתאם לשימוש הצפוי, אתם יכולים להשתמש במחשבון העלויות.

לפני שמתחילים

מגדירים את הסביבה למדריך:

  1. מתקינים ומפעילים את Google Cloud CLI, ומתקינים את kubectl. אם אתם משתמשים ב-Cloud Shell, שני הכלים מותקנים מראש.
  2. אם אתם מריצים את המדריך הזה מתחנת עבודה של Linux או macOS ולא מ-Cloud Shell, התקינו את יחסי התלות הנדרשים של Cluster Toolkit (כמו Terraform ו-Git).
  3. מפעילים את ממשקי ה-API הבאים בפרויקט ב- Google Cloud : ‏Kubernetes Engine API, ‏Compute Engine API, ‏Cloud Storage API, ‏Artifact Registry API ו-Service Usage API.

    הפעלת ממשקי API

איך מקבלים טוקן גישה ל-Hugging Face

כדי להשתמש במדריך הזה, צריך טוקן גישה ל-Hugging Face. אפשר להירשם לחשבון בחינם ב-Hugging Face. אחרי שיוצרים חשבון, יוצרים אסימון גישה:

  1. בדף Welcome to Hugging Face, לוחצים על הדמות של החשבון ובוחרים באפשרות טוקן גישה.
  2. בדף טוקני גישה, לוחצים על יצירת טוקן חדש.
  3. בוחרים את סוג הטוקן Read ומזינים שם לטוקן.
  4. טוקן הגישה שלכם יוצג. שומרים את הטוקן במקום בטוח.

התפקידים הנדרשים

כדי לקבל את ההרשאות שדרושות להשלמת המדריך, בקשו מהאדמין להקצות לכם את התפקידים הבאים ב-IAM בפרויקט:

להסבר על מתן תפקידים, ראו איך מנהלים את הגישה ברמת הפרויקט, התיקייה והארגון.

יכול להיות שאפשר לקבל את ההרשאות הנדרשות גם באמצעות תפקידים בהתאמה אישית או תפקידים מוגדרים מראש.

הגדרת משתני הסביבה

מגדירים את משתני האימות והסביבה של המדריך:

  1. מגדירים את האימות:

    gcloud auth login
    gcloud auth application-default login
  2. מייצאים את משתני הסביבה שבהם משתמשים במדריך הזה:

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

    מחליפים את מה שכתוב בשדות הבאים:

    • ‫YOUR_PROJECT_ID: מזהה הפרויקט ב-Google Cloud .
    • ‫YOUR_REGION: אזור המחשוב שבו נמצאת קיבולת ה-A4 ושבו רוצים לפרוס את האשכול, למשל us-central1.
    • ‫YOUR_ZONE: אזור המחשוב שבו נמצאת הקיבולת של A4, למשל us-central1-b.
    • ‫YOUR_CLUSTER_NAME: השם של אשכול GKE ושל פריסת Cluster Toolkit. השתמשו בשם באורך של עד 20 תווים, כדי שמזהה חשבון השירות שנוצר למאגר הצמתים (YOUR_CLUSTER_NAME-gke-np-sa) לא יחרוג מהמגבלה של 30 תווים בניהול הזהויות והרשאות הגישה (IAM).
    • ‫YOUR_RESERVATION_NAME: השם של ההזמנה שלכם ל-A4, או הנתיב של בלוק ההזמנה המורחב בפורמט RESERVATION_NAME/reservationBlocks/RESERVATION_BLOCK_NAME.
    • ‫YOUR_HF_TOKEN: טוקן הגישה שלכם ל-Hugging Face.
    • ‫YOUR_GCS_BUCKET: שם ייחודי גלובלי לקטגוריה של Cloud Storage שבה אתם מעבירים את משקלי המודל לשלב ביניים. הסקריפט גם מגדיר את TERRAFORM_BUCKET לערך YOUR_GCS_BUCKET-tf-state כדי לאחסן את המצב של Terraform ל-Cluster Toolkit.

    הסקריפט גם מגדיר את CLUSTER_TOOLKIT_VERSION ל-v1.104.0, שמציין את גרסת Cluster Toolkit שצריך להתקין. גרסה v1.104.0 של Cluster Toolkit ואילך כוללת אפשרויות מובנות לצריכת קיבולת של examples/gke-a4/gke-a4-deployment.yaml. בגרסאות שקודמות ל-v1.104.0, צריך להוסיף את אפשרויות הקיבולת האלה באופן ידני.

יצירת אשכול GKE

מורידים את Cluster Toolkit ומקצים אשכול GKE במצב רגיל עם שני צומתי a4-highgpu-8g, רשת GPUDirect RDMA ובקר JobSet:

  1. מורידים ומחלצים את חבילת Cluster Toolkit שנוצרה מראש לגרסה שצוינה ב-CLUSTER_TOOLKIT_VERSION (v1.104.0), משנים את ספריית העבודה לספרייה cluster-toolkit ומאמתים את הגרסה הבינארית:

    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. אם רוצים, אפשר לראות את הפקודות והדגלים שזמינים ב-Cluster Toolkit:

    ./gcluster --help
  3. יוצרים את הקטגוריה של Cloud Storage שבה מאוחסן המצב של Terraform לפריסה של Cluster Toolkit:

    gcloud storage buckets create "gs://${TERRAFORM_BUCKET}" \
        --default-storage-class=STANDARD \
        --project="${PROJECT_ID}" \
        --location="${REGION}" \
        --uniform-bucket-level-access
  4. יוצרים את קובץ התצורה של הפריסה examples/gke-a4/gke-a4-deployment.yaml לשני צומתי 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

    כברירת מחדל, התוכנית examples/gke-a4/gke-a4.yaml מוגדרת להשתמש בהזמנה ספציפית. אם אין לכם הזמנה ספציפית, אתם יכולים לערוך את examples/gke-a4/gke-a4.yaml לפני פריסת האשכול כדי להשתמש באחת מאפשרויות הצריכה הבאות:

    • קיבולת Spot: ודאו שיש לפרויקט מספיק מכסה של מכונות שניתן להפסיק לפני הזמן בשביל ה-GPU, הדיסקים וה-CPU. בחלק התחתון של examples/gke-a4/gke-a4.yaml, מסמנים כהערה את הקטע --- Option 1: Specific Reservation --- ומבטלים את הסימון כהערה של הקטע --- Option 4: Spot ---.
    • מכונות Flex-start VM: ודאו שיש לפרויקט מספיק מכסה של מכונות שניתנות להפסקה לפני הזמן בשביל ה-GPU, הדיסקים וה-CPU. בתחתית הקובץ examples/gke-a4/gke-a4.yaml, מסמנים כהערה את הקטע --- Option 1: Specific Reservation --- ומבטלים את הסימון כהערה של הקטע --- Option 2: DWS Flex Start ---.
  5. פורסים את אשכול GKE באמצעות הפקודה gcluster deploy:

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

    יצירת האשכול נמשכת בערך 30 עד 45 דקות. אם מריצים את ./gcluster deploy בלי הדגל --auto-approve, כדי להחיל את השינויים המוצעים צריך להזין a כשמוצגת בקשה לעשות זאת. בסיום הפריסה, הפלט מסתיים בהודעה שדומה לזו:

    Apply complete! Resources: 80 added, 0 changed, 0 destroyed.
    
  6. מגדירים את פרטי הכניסה של kubectl כדי לתקשר עם האשכול:

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

העברת משקלי המודל ל-Cloud Storage

מורידים את המשקלים של DeepSeek-R1 ל-SSD המקומי של אחד מצומתי A4, ומעבירים אותם לקטגוריה של Cloud Storage כדי להעביר אותם בסטרימינג מהר בין הצמתים:

  1. מורידים את קובצי המניפסט של Kubernetes בשביל משימת הורדת המודל, שרת ההסקה של vLLM וקבוצות ה-Pod להשוואה:

    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. יוצרים קטגוריה ב-Cloud Storage עם הרשאות גישה אחידות ברמת הקטגוריה כדי לאחסן את משקלי המודל:

    gcloud storage buckets create "gs://${MODEL_BUCKET}" \
        --project="${PROJECT_ID}" \
        --location="${REGION}" \
        --uniform-bucket-level-access
  3. נותנים הרשאות IAM בקטגוריה לחשבון השירות שמוגדר כברירת מחדל ב-Kubernetes, לחשבון השירות שמוגדר כברירת מחדל ב-Compute Engine ולחשבון השירות של מאגר הצמתים באשכול (${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"

    הרשאות ה-IAM שניתנות על ידי הסקריפט הקודם כוללות את התפקיד "קריאה בקטגוריה באחסון מדור קודם" (roles/storage.legacyBucketReader), שנדרש ל---load-format runai_streamer.

  4. יוצרים סוד של Kubernetes בשם hf-secret כדי לאחסן את טוקן הגישה של Hugging Face:

    kubectl create secret generic hf-secret \
        --from-literal=hf_api_token="${HF_TOKEN}" \
        --dry-run=client -o yaml | kubectl apply -f -
  5. בודקים את המניפסט download_model.yaml, או יוצרים את הקובץ באופן מקומי אם לא הורדתם אותו קודם:

    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. מחליפים את שם הקטגוריה של Cloud Storage במניפסט download_model.yaml ומפעילים את משימת הורדת המודל:

    envsubst '$MODEL_BUCKET' < download_model.yaml | kubectl apply -f -
  7. מחכים שה-Pod של ההורדה יתחיל, ואז מעבירים בסטרימינג את יומני המשימה:

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

    הורדת המודל ל-SSD המקומי של NVMe בצומת והעלאת המשקלים לקטגוריה של Cloud Storage נמשכות כ-30 עד 40 דקות.

  8. אחרי שהמשימה מסתיימת, מוודאים שכל 163 המקטעים של .safetensors נמצאים בקטגוריה של Cloud Storage:

    gcloud storage ls "gs://${MODEL_BUCKET}/DeepSeek-R1/*.safetensors" | wc -l
  9. מוחקים את משימת ההעברה שהושלמה:

    kubectl delete job download-deepseek-r1

פריסת שרת vLLM עם כמה מארחים

המניפסט של עומס העבודה (deepseek_r1_server.yaml) מגדיר שירות ClusterIP‏ (vllm-r1-leader) ו-Kubernetes JobSet‏ (vllm-r1) שמתאמים את ההצגה בכמה מארחים בשני הצמתים של a4-highgpu-8g:

  • ‫Leader (leader): מריץ רפליקה אחת בצומת A4 עם שמונה מעבדי NVIDIA B200 GPU, מקבל את סביבת NCCL/gIB‏ (/usr/local/gib/scripts/set_nccl_env.sh), מפעיל את צומת ה-head של Ray ביציאה 6379 לאשכול עם שני צמתים, ומפעיל את vllm serve ביציאת HTTP‏ 8080. השרת מעביר משקלים בסטרימינג ישירות מ-gs://$MODEL_BUCKET/DeepSeek-R1 באמצעות --load-format runai_streamer, וטוען את ה-tokenizer מנפח ה-Cloud Storage FUSE שנטען ב-/models/DeepSeek-R1. הוא פועל עם --tensor-parallel-size 16,‏ --pipeline-parallel-size 1,‏ --distributed-executor-backend ray,‏ --enable-expert-parallel,‏ --kv-cache-dtype fp8_e4m3,‏ --async-scheduling ותרשימי CUDA‏ (FULL_AND_PIECEWISE).
  • ‫Worker (worker): מריץ רפליקה אחת ללא ממשק משתמש בצומת A4 השני עם שמונה מעבדי NVIDIA B200 GPU, מגדיר את NCCL/gIB ומצטרף לאשכול Ray של ה-Leader ב-vllm-r1-leader:6379.

פורסים את שרת ה-vLLM עם כמה מארחים ועוקבים אחריו:

  1. בודקים את המניפסט deepseek_r1_server.yaml, או יוצרים את הקובץ באופן מקומי אם לא הורדתם אותו קודם:

    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

    כדי להרחיב את הפריסה הזו למארחים נוספים, צריך לעדכן את הפרמטרים בקובץ deepseek_r1_server.yaml. לדוגמה, כדי להגדיל את קנה המידה ל-4 צומתי A4, עם 32 יחידות GPU מסוג NVIDIA B200 בסך הכול, מגדירים את replicas: 3 ב-Job המשוכפל worker ומעדכנים את הארגומנטים של ה-leader באופן הבא:

    • --ray_cluster_size=4
    • --tensor-parallel-size=16
    • --pipeline-parallel-size=2
  2. מחליפים את deepseek_r1_server.yaml בשם הקטגוריה של Cloud Storage ומחילים את המניפסט על האשכול:

    envsubst '$MODEL_BUCKET' < deepseek_r1_server.yaml | kubectl apply -f -
  3. עוקבים אחרי היומנים של ה-Pod הראשי וה-Pod המשני עד שהשרת מסיים את ההפעלה:

    # 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

    מכיוון ש-kubectl logs -f מעביר יומנים בחזית, צריך להריץ את הפקודה worker logs בסשן נפרד של הטרמינל אם רוצים לעקוב אחרי שני ה-Pods בו-זמנית. כדי להפסיק את הסטרימינג של היומנים בכל שלב, מקישים על Control+C.

    השלבים הראשוניים של העברת המשקלים, טעינת המודל וצילום תרשים CUDA נמשכים בערך 15 עד 20 דקות. לפני ששולחים בקשות, ממתינים עד שיופיעו ההודעות הבאות ביומני הפלט של ה-Pod הראשי:

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

שליחת בקשות להסקת מסקנות

שולחים בקשה להשלמת צ'אט לשרת ה-vLLM שנפרס:

  1. מעבירים את היציאה המקומית 8080 לשירות vllm-r1-leader ברקע:

    kubectl port-forward service/vllm-r1-leader 8080:8080 &
  2. שולחים בקשה להשלמת צ'אט לנקודת הקצה (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
        }'

הרצת עומסי עבודה להערכת ביצועים

מריצים בדיקת ביצועים של שרת DeepSeek-R1 שנפרס באמצעות vllm bench serve עם הגדרה של 8,192 טוקנים של קלט ו-1,024 טוקנים של פלט (8k/1k):

  1. בודקים את המניפסט deepseek_r1_8k_1k.yaml, או יוצרים את הקובץ באופן מקומי אם לא הורדתם אותו קודם:

    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

    במניפסט מצוינים 8,192 טוקנים של קלט, 1,024 טוקנים של פלט, 1,280 הנחיות ורמת בו-זמניות מקסימלית של 112.

  2. מחילים את המניפסט deepseek_r1_8k_1k.yaml:

    envsubst '$MODEL_BUCKET' < deepseek_r1_8k_1k.yaml | kubectl apply -f -
  3. מחכים שה-Pod של vllm-bench-8k-1k יתחיל, ואז מעבירים בסטרימינג את יומני ההשוואה:

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

    הבדיקה נמשכת כ-15 דקות. בסיום הבדיקה, חפשו פלט שמכיל מדדים כמו:

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

הסרת המשאבים

כדי להימנע מחיובים נוספים בחשבון Google Cloud על המשאבים שבהם השתמשתם במדריך הזה, אתם צריכים למחוק את עומסי העבודה ב-Kubernetes, את אשכול GKE ואת הקטגוריות של Cloud Storage:

  1. מוחקים את הפודים של נקודות ההשוואה, את ה-JobSet והשירות של שרת ה-vLLM, את משימת הורדת המודל ואת הסוד של 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. מוחקים את הקטגוריה של Cloud Storage שבה מאוחסנים משקלי המודל שהועלו:

    gcloud storage rm --recursive "gs://${MODEL_BUCKET}"
  3. בספרייה cluster-toolkit, משמידים את אשכול GKE ואת התשתית שמשויכת אליו באמצעות הפקודה gcluster destroy:

    ./gcluster destroy "${CLUSTER_NAME}" --robust --auto-approve
  4. מוחקים את הקטגוריה של Cloud Storage שבה מאוחסן המצב של Terraform:

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

המאמרים הבאים