הצגת Qwen3.8-2.4T ב-GKE עם פירוק של דיסק קבוע (PD) בכמה מארחים באמצעות Dynamo ו-SGLang במכונות וירטואליות מסוג A4

במדריך הזה מוסבר איך לפרוס ולהציג את Qwen3.8-2.4T (nvidia/Qwen3.8-2.4T-A95B-NVFP4), מודל שילוב מומחים (MoE) עם 2.4 טריליון פרמטרים ו-95 מיליארד פרמטרים פעילים שעברו קוונטיזציה ל-NVFP4, ב-Google Kubernetes Engine (GKE) ב-Google Cloud. הפריסה משתמשת ב-Cluster Toolkit, בארבע מכונות וירטואליות (VM) מסוג A4 (a4-highgpu-8g) עם מספר מארחים ו-32 מעבדי GPU מסוג NVIDIA B200, ב-NVIDIA Dynamo ובמסגרת להצגת מודעות SGLang עם הפרדה של מילוי מראש ופענוח (PD).

בהגדרת הפריסה שמתוארת במדריך הזה, משתמשים ב-Cluster Toolkit כדי להקצות אשכול GKE במצב Standard. האשכול מכיל ארבעה צומתי a4-highgpu-8g שמחוברים באמצעות רשת GPUDirect Remote Direct Memory Access ‏ (RDMA). אחרי זה מתקינים את KAI Scheduler ואת פלטפורמת NVIDIA Dynamo עם Grove gang scheduling, ומעבירים את משקלי המודל ל-Cloud Storage. בשלב הבא, פורסים משאב מותאם אישית של DynamoGraphDeployment שמחבר את הקטגוריה באמצעות Cloud Storage FUSE ומפריד בין המילוי מראש לבין הפענוח בשני מאגרי Worker עם מספר מארחים ו-16 יחידות GPU, עם מקביליות של טנזורים, מקביליות של מומחים ומטמון KV של FP8.

מטרות

  • מתקינים את Cluster Toolkit ומקצים אשכול GKE עם כמה מארחים מסוג A4 במצב רגיל.
  • מתקינים באשכול את פלטפורמת KAI Scheduler ו-NVIDIA Dynamo עם תזמון Grove gang.
  • מעבירים את משקלי המודל Qwen3.8-2.4T לקטגוריה של Cloud Storage.
  • פריסת שרת להסקת מסקנות של SGLang עם כמה מארחים ו-PD מבוזר בארבעה צומתי A4, עם 32 מעבדי GPU מסוג NVIDIA B200 בסך הכול.
  • שליחת בקשה להשלמת צ'אט לנתב החזיתי של Dynamo.
  • מריצים עומס עבודה להערכת ביצועים של מילוי בקשות.
  • מחיקת המשאבים שנפרסו.

עלויות

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

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

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

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

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

    הפעלת ממשקי API

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

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

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

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

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

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

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

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

מייצאים את משתני הסביבה שבהם משתמשים במדריך הזה:

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 GCS_BUCKET="YOUR_BUCKET_NAME"
export HF_TOKEN="YOUR_HF_TOKEN"
export CLUSTER_TOOLKIT_VERSION="v1.104.0"
export DYNAMO_RELEASE_VERSION="1.5.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_BUCKET_NAME: שם ייחודי גלובלי לקטגוריה של Cloud Storage שבה מאוחסנים המצב של Terraform ל-Cluster Toolkit ומשקלי המודל של Qwen3.8-2.4T בשלב הביניים.
  • ‫YOUR_HF_TOKEN: טוקן הגישה שלכם ל-Hugging Face.

יצירת אשכול GKE

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

  1. נכנסים ל-Google Cloud CLI ויוצרים Application Default Credentials ל-Terraform:

    gcloud auth login
    gcloud auth application-default login
  2. מורידים ומחלצים את חבילת Cluster Toolkit שנוצרה מראש לגרסה שצוינה ב-CLUSTER_TOOLKIT_VERSION (v1.104.0):

    TAG="${CLUSTER_TOOLKIT_VERSION:-v1.104.0}"
    OS="linux"
    ARCH="amd64"
    mkdir -p cluster-toolkit && curl -L "https://github.com/GoogleCloudPlatform/cluster-toolkit/releases/download/${TAG}/gcluster_bundle_${OS}_${ARCH}.tgz" | tar -xz -C cluster-toolkit

    אם אתם מריצים את הפקודה הזו ב-macOS או בתחנת עבודה של arm64, צריך להגדיר את OS="mac" או את ARCH="arm64" בהתאם לפלטפורמה.

  3. כדי לאחסן את המצב של Terraform לפריסה של Cluster Toolkit ואת משקלי המודל בשלבים, יוצרים קטגוריה של Cloud Storage באותו אזור של המקום השמור של A4, עם הרשאות גישה מאוחדות ברמת הקטגוריה (UBLA) וניהול גרסאות של אובייקטים:

    gcloud storage buckets create "gs://${GCS_BUCKET}" \
        --default-storage-class=STANDARD \
        --project="${PROJECT_ID}" \
        --location="${REGION}" \
        --uniform-bucket-level-access
    gcloud storage buckets update "gs://${GCS_BUCKET}" --versioning
  4. יוצרים את קובץ התצורה של הפריסה cluster-toolkit/examples/gke-a4/gke-a4-deployment.yaml לארבעה צומתי a4-highgpu-8g:

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

    כברירת מחדל, קובץ הפריסה מוגדר לצרוך הזמנה ספציפית. אם אין לכם מקום שמור ספציפי, לפני פריסת האשכול אתם יכולים לעדכן את cluster-toolkit/examples/gke-a4/gke-a4-deployment.yaml כדי להשתמש באפשרות צריכה חלופית, כמו VM במודל Spot או Flex-start VM. הוראות להגדרה מופיעות במאמר אפשרויות צריכה של GKE.

  5. עוברים לספרייה cluster-toolkit, פורסים את אשכול GKE באמצעות הפקודה gcluster deploy וחוזרים לספריית העבודה:

    cd cluster-toolkit
    ./gcluster deploy \
        -d examples/gke-a4/gke-a4-deployment.yaml \
        examples/gke-a4/gke-a4.yaml \
        --auto-approve -w
    cd ..

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

  6. מגדירים את פרטי הכניסה של kubectl כדי לתקשר עם האשכול:

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

התקנת KAI Scheduler ו-NVIDIA Dynamo

מכינים את האשכול על ידי יצירת סוד של Hugging Face והתקנת KAI Scheduler ופלטפורמת NVIDIA Dynamo עם תזמון קבוצתי של Grove:

  1. יוצרים סוד של 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 -
  2. מורידים את תרשים ה-Helm של פלטפורמת NVIDIA Dynamo‏ (v1.5.0), מתקינים את KAI Scheduler ומתקינים את פלטפורמת Dynamo עם Grove:

    RELEASE_VERSION="${DYNAMO_RELEASE_VERSION:-1.5.0}"
    helm pull "https://helm.ngc.nvidia.com/nvidia/ai-dynamo/charts/dynamo-platform-${RELEASE_VERSION}.tgz"
    tar -zxf "dynamo-platform-${RELEASE_VERSION}.tgz"
    
    helm upgrade --install kai-scheduler ./dynamo-platform/charts/kai-scheduler \
        --set scheduler.args.default-staleness-grace-period="-1s" \
        --set topologyMigration.enabled=false
    
    kubectl wait --for=condition=Available deployment/kai-operator --timeout=120s
    until kubectl get deployment/queue-controller >/dev/null 2>&1; do sleep 2; done
    kubectl wait --for=condition=Available deployment/queue-controller --timeout=120s
    
    helm upgrade --install dynamo-platform ./dynamo-platform \
        --set global.grove.install=true \
        --set global.grove.enabled=true \
        --set global.kai-scheduler.enabled=true

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

מעבירים את נקודת הבדיקה של nvidia/Qwen3.8-2.4T-A95B-NVFP4 לקטגוריה של Cloud Storage באזור (gs://${GCS_BUCKET}), כדי ש-Pods של ה-worker להסקת מסקנות יוכלו לטעון את המשקלים באמצעות Cloud Storage FUSE במקום להוריד אותם מ-Hugging Face בכל הפעלה קרה:

  1. מקצים את התפקיד Storage Object User‏ (roles/storage.objectUser) בקטגוריה gs://${GCS_BUCKET} שיצרתם ביצירת אשכול GKE לחשבון השירות של Google שמקושר לחשבון השירות workload-identity-k8s-sa של Kubernetes:

    GSA=$(kubectl get sa workload-identity-k8s-sa -o jsonpath='{.metadata.annotations.iam\.gke\.io/gcp-service-account}')
    gcloud storage buckets add-iam-policy-binding "gs://${GCS_BUCKET}" \
        --member="serviceAccount:${GSA}" \
        --role="roles/storage.objectUser"
  2. מורידים את המניפסטים של qwen3.8-2.4t-nvfp4-stage-weights-job.yaml ו-qwen3.8-2.4t-nvfp4-multihost-a4-gcsfuse.yaml:

    curl -O https://raw.githubusercontent.com/GoogleCloudSamples/AIHypercomputerSamples/refs/heads/main/gpu/inference/qwen38-2t/qwen3.8-2.4t-nvfp4-stage-weights-job.yaml
    curl -O https://raw.githubusercontent.com/GoogleCloudSamples/AIHypercomputerSamples/refs/heads/main/gpu/inference/qwen38-2t/qwen3.8-2.4t-nvfp4-multihost-a4-gcsfuse.yaml

    אפשר גם ליצור קובץ בשם qwen3.8-2.4t-nvfp4-stage-weights-job.yaml עם התוכן הבא. ה-Indexed Job הזה מריץ ארבעה Pods במקביל בארבעת צומתי ה-A4. כל Pod מוריד חלק בגודל מאוזן של נקודת הבדיקה של nvidia/Qwen3.8-2.4T-A95B-NVFP4 ל-NVMe SSD המקומי של הצומת, ומעלה אותו ל-gs://${GCS_BUCKET}/hf_models/models--nvidia--qwen3_8-2_4t-a95b-nvfp4 באמצעות gcloud storage rsync:

    # ==============================================================================
    # Stage nvidia/Qwen3.8-2.4T-A95B-NVFP4 (~1.44 TB, 210 files) from Hugging Face Hub
    # into an in-region Cloud Storage bucket for qwen3.8-2.4t-nvfp4-multihost-a4-gcsfuse.yaml
    #
    # Each Indexed Job pod downloads a size-balanced slice of the repo onto the node's
    # local NVMe SSD, then uploads that slice with `gcloud storage rsync`.
    # ==============================================================================
    apiVersion: batch/v1
    kind: Job
    metadata:
      name: qwen3-8-2-4t-stage-weights
    spec:
      completionMode: Indexed
      completions: 4
      parallelism: 4
      backoffLimitPerIndex: 2
      template:
        metadata:
          labels:
            app: qwen3-8-2-4t-stage-weights
        spec:
          restartPolicy: Never
          serviceAccountName: workload-identity-k8s-sa
          nodeSelector:
            cloud.google.com/gke-accelerator: nvidia-b200
          tolerations:
            - key: "nvidia.com/gpu"
              operator: "Exists"
              effect: "NoSchedule"
          topologySpreadConstraints:
            - maxSkew: 1
              topologyKey: kubernetes.io/hostname
              whenUnsatisfiable: DoNotSchedule
              labelSelector:
                matchLabels:
                  app: qwen3-8-2-4t-stage-weights
          initContainers:
            - name: download
              image: nvcr.io/nvidia/ai-dynamo/sglang-runtime:1.5.0
              command: ["python3", "-c"]
              args:
                - |
                  import os, time
                  from huggingface_hub import HfApi, snapshot_download
                  repo = os.environ["REPO_ID"]
                  n = int(os.environ["NUM_SHARDS"])
                  i = int(os.environ["JOB_COMPLETION_INDEX"])
                  info = HfApi().model_info(repo, files_metadata=True)
                  files = sorted(((s.size or 0, s.rfilename) for s in info.siblings), reverse=True)
                  bins = [[0, []] for _ in range(n)]
                  for size, name in files:
                      b = min(bins, key=lambda x: x[0])
                      b[0] += size
                      b[1].append(name)
                  size, mine = bins[i]
                  print(f"shard {i}/{n}: {len(mine)} files, {size / 1e9:.1f} GB", flush=True)
                  for attempt in range(1, 7):
                      try:
                          snapshot_download(repo, local_dir="/ssd/model", allow_patterns=mine, max_workers=16)
                          break
                      except Exception as e:
                          print(f"shard {i}/{n}: attempt {attempt} failed: {e!r}", flush=True)
                          if attempt == 6:
                              raise
                          time.sleep(30)
                  print(f"shard {i}/{n}: download complete", flush=True)
              env:
                - name: REPO_ID
                  value: "nvidia/Qwen3.8-2.4T-A95B-NVFP4"
                - name: NUM_SHARDS
                  value: "4"
                - name: HF_HOME
                  value: "/ssd/hf"
                - name: HF_HUB_DISABLE_XET
                  value: "1"
                - name: HF_TOKEN
                  valueFrom:
                    secretKeyRef:
                      name: hf-secret
                      key: hf_api_token
                      optional: true
              resources:
                requests:
                  cpu: "16"
                  memory: "64Gi"
                  ephemeral-storage: "450Gi"
                limits:
                  cpu: "16"
                  memory: "64Gi"
                  ephemeral-storage: "700Gi"
              volumeMounts:
                - name: ssd
                  mountPath: /ssd
          containers:
            - name: upload
              image: gcr.io/google.com/cloudsdktool/google-cloud-cli:slim
              command: ["/bin/bash", "-c"]
              args:
                - |
                  set -euo pipefail
                  gcloud storage rsync --recursive --exclude='^\.cache/.*' /ssd/model "${DEST}"
                  echo "shard ${JOB_COMPLETION_INDEX}: upload complete"
              env:
                - name: DEST
                  value: "gs://${GCS_BUCKET}/hf_models/models--nvidia--qwen3_8-2_4t-a95b-nvfp4"
              resources:
                requests:
                  cpu: "16"
                  memory: "32Gi"
                limits:
                  cpu: "16"
                  memory: "32Gi"
              volumeMounts:
                - name: ssd
                  mountPath: /ssd
          volumes:
            - name: ssd
              emptyDir: {}
  3. מחליפים את שם הקטגוריה של Cloud Storage ב-qwen3.8-2.4t-nvfp4-stage-weights-job.yaml, מפעילים את משימת ה-staging, מחכים שכל ארבעת ה-Pods יסיימו להעלות את המשקלים ואז מוחקים את המשימה שהושלמה:

    envsubst '${GCS_BUCKET}' < qwen3.8-2.4t-nvfp4-stage-weights-job.yaml | kubectl apply -f -
    
    kubectl wait --for=condition=complete job/qwen3-8-2-4t-stage-weights --timeout=2h
    
    kubectl delete job qwen3-8-2-4t-stage-weights

    הורדת משקלי המודל ל-SSD מקומי של NVMe והעלאה שלהם ל-Cloud Storage נמשכת כ-25 דקות.

  4. אופציונלי: בזמן שה-Job של ה-Staging פועל, אפשר להעביר בסטרימינג את היומנים של Pod של Staging בסשן טרמינל שני כדי לעקוב אחרי התקדמות ההורדה:

    POD_NAME="YOUR_POD_NAME"
    kubectl logs -f "${POD_NAME}"

    מחליפים את YOUR_POD_NAME בשם של אחד מהפודים של qwen3-8-2-4t-stage-weights מ-kubectl get pods.

פריסת שרת SGLang מבוזר עם כמה מארחים

המניפסט של עומס העבודה (qwen3.8-2.4t-nvfp4-multihost-a4-gcsfuse.yaml) מגדיר את Kubernetes ResourceClaimTemplate‏ (all-mrdma), שמקצה את מכשירי ה-RDMA עם multi-NIC של הצומת (mrdma.google.com) דרך DRA, משאבי Role ו-RoleBinding לחשבון השירות workload-identity-k8s-sa, ו-NVIDIA Dynamo DynamoGraphDeployment‏ (qwen3-8-2-4t-a4-disagg) עם תזמון קבוצתי של Grove מופעל. הפריסה כוללת את הרכיבים הבאים:

  • רכיב קצה קדמי (Frontend): מריץ רפליקה אחת של נתב שמקבלת בקשות מהלקוח, מגלה את ה-workers הרשומים למילוי מראש ולפענוח, ומתאמת את הניתוב של הבקשות המפורקות.

  • ‫Prefill worker (Prefill): מפעיל מאגר של שני צומתי וורקרים בשני צומתי a4-highgpu-8g עם 16 מעבדי NVIDIA B200 GPU בסך הכול. כל Pod טוען את המשקולות של nvidia/Qwen3.8-2.4T-A95B-NVFP4 שהועלו לקטגוריה של Cloud Storage ב-/model-weights באמצעות מנהל ההתקן של ה-CSI של Cloud Storage FUSE‏ (gcsfuse.csi.storage.gke.io) עם מטמון קבצים מקומי של NVMe SSD, מקורות לסביבת NCCL/gIB ומפעיל את python3 -m dynamo.sglang ב---disaggregation-mode prefill עם הדגלים הבאים:

    • --served-model-name "nvidia/Qwen3.8-2.4T-A95B-NVFP4"
    • --tensor-parallel-size 16
    • --ep-size 16
    • --nnodes 2
    • --moe-runner-backend flashinfer_trtllm
    • --attention-backend triton
    • --kv-cache-dtype fp8_e4m3
    • --disable-flashinfer-autotune
  • ‫Decode worker (Decode): מריץ מאגר של שני צומתי וורקר בשני הצמתים הנותרים של a4-highgpu-8g, עם 16 מעבדי NVIDIA B200 GPU בסך הכול. כל Pod טוען את משקולות המודל שהועלו מ-Cloud Storage ב-/model-weights דרך מנהל התקן ה-CSI של Cloud Storage FUSE, ומפעיל את python3 -m dynamo.sglang ב---disaggregation-mode decode עם הדגלים הבאים:

    • --served-model-name "nvidia/Qwen3.8-2.4T-A95B-NVFP4"
    • --tensor-parallel-size 16
    • --ep-size 16
    • --nnodes 2
    • --moe-runner-backend flashinfer_trtllm
    • --attention-backend triton
    • --kv-cache-dtype fp8_e4m3

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

  1. בודקים את מניפסט עומס העבודה qwen3.8-2.4t-nvfp4-multihost-a4-gcsfuse.yaml שהורדתם קודם, או יוצרים קובץ בשם qwen3.8-2.4t-nvfp4-multihost-a4-gcsfuse.yaml עם התוכן הבא:

    # ==============================================================================
    # Multihost Disaggregated Serving of Qwen3.8-2.4T-A95B (NVFP4 MoE) on GKE
    # using NVIDIA Dynamo Platform (v1.5.0) and SGLang on GPU A4 (4 x a4-highgpu-8g)
    # ==============================================================================
    apiVersion: resource.k8s.io/v1
    kind: ResourceClaimTemplate
    metadata:
      name: all-mrdma
    spec:
      spec:
        devices:
          requests:
            - name: req-mrdma
              exactly:
                deviceClassName: mrdma.google.com
                allocationMode: All
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: Role
    metadata:
      name: dynamo-workload-identity-discovery-role
      namespace: default
    rules:
      - apiGroups: [""]
        resources: ["endpoints", "pods"]
        verbs: ["get", "list", "watch"]
      - apiGroups: ["discovery.k8s.io"]
        resources: ["endpointslices"]
        verbs: ["get", "list", "watch"]
      - apiGroups: ["nvidia.com"]
        resources: ["dynamoworkermetadatas"]
        verbs: ["create", "get", "list", "watch", "update", "patch", "delete"]
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: RoleBinding
    metadata:
      name: dynamo-workload-identity-discovery-binding
      namespace: default
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: Role
      name: dynamo-workload-identity-discovery-role
    subjects:
      - kind: ServiceAccount
        name: workload-identity-k8s-sa
        namespace: default
    ---
    apiVersion: nvidia.com/v1alpha1
    kind: DynamoGraphDeployment
    metadata:
      name: qwen3-8-2-4t-a4-disagg
      annotations:
        nvidia.com/enable-grove: "true"
    spec:
      backendFramework: sglang
      services:
        # --------------------------------------------------------------------------
        # FRONTEND: Dynamo OpenAI-Compatible Router (`python3 -m dynamo.frontend`)
        # --------------------------------------------------------------------------
        Frontend:
          componentType: frontend
          replicas: 1
          resources:
            requests:
              cpu: "8"
              memory: "32Gi"
            limits:
              cpu: "8"
              memory: "32Gi"
          extraPodSpec:
            tolerations:
              - key: "nvidia.com/gpu"
                operator: "Exists"
                effect: "NoSchedule"
            mainContainer:
              image: nvcr.io/nvidia/ai-dynamo/sglang-runtime:1.5.0
              command: ["/bin/bash", "-c"]
              args:
                - |
                  set -ex
                  exec python3 -m dynamo.frontend --http-port 8000
    
        # --------------------------------------------------------------------------
        # PREFILL WORKER: 2 x GPU A4 (`a4-highgpu-8g`, 16 x B200 GPUs, TP=16, EP=16)
        # --------------------------------------------------------------------------
        Prefill:
          componentType: worker
          subComponentType: prefill
          replicas: 1
          multinode:
            nodeCount: 2
          extraPodMetadata:
            annotations:
              gke-gcsfuse/volumes: "true"
          livenessProbe:
            httpGet:
              path: /live
              port: system
            periodSeconds: 60
            timeoutSeconds: 30
            failureThreshold: 60
          readinessProbe:
            httpGet:
              path: /health
              port: system
            initialDelaySeconds: 120
            periodSeconds: 30
            timeoutSeconds: 30
            failureThreshold: 60
          sharedMemory:
            size: 128Gi
          resources:
            requests:
              gpu: "8"
              cpu: "128"
              memory: "1500Gi"
            limits:
              gpu: "8"
              cpu: "128"
              memory: "2000Gi"
          envs:
            - name: MODEL_PATH
              value: "/model-weights/hf_models/models--nvidia--qwen3_8-2_4t-a95b-nvfp4"
            - name: DYN_SYSTEM_PORT
              value: "9090"
            - name: LD_LIBRARY_PATH
              value: "/usr/local/gib/lib64:/usr/local/nvidia/lib64"
            - name: GLOO_SOCKET_IFNAME
              value: "eth0"
            - name: NCCL_ALGO
              value: "Ring,Tree"
            - name: WITH_NVIDIA_PEERMEM
              value: "0"
          extraPodSpec:
            serviceAccountName: workload-identity-k8s-sa
            automountServiceAccountToken: true
            resourceClaims:
              - name: rdma
                resourceClaimTemplateName: all-mrdma
            nodeSelector:
              cloud.google.com/gke-accelerator: nvidia-b200
            tolerations:
              - key: "nvidia.com/gpu"
                operator: "Exists"
                effect: "NoSchedule"
            mainContainer:
              image: nvcr.io/nvidia/ai-dynamo/sglang-runtime:1.5.0
              resources:
                claims:
                  - name: rdma
              startupProbe:
                httpGet:
                  path: /live
                  port: system
                initialDelaySeconds: 120
                periodSeconds: 30
                timeoutSeconds: 10
                failureThreshold: 120
              command: ["/bin/bash", "-c"]
              args:
                - |
                  set -ex
                  if [ -f /usr/local/gib/scripts/set_nccl_env.sh ]; then
                    . /usr/local/gib/scripts/set_nccl_env.sh
                  fi
    
                  echo "=== [Dynamo Prefill Worker] Launching dynamo.sglang ==="
                  exec python3 -m dynamo.sglang \
                    --dist-init-addr "${GROVE_PCSG_NAME}-${GROVE_PCSG_INDEX}-prefill-ldr-0.${GROVE_HEADLESS_SERVICE}.:29500" \
                    --model-path "${MODEL_PATH}" \
                    --served-model-name "nvidia/Qwen3.8-2.4T-A95B-NVFP4" \
                    --host 0.0.0.0 \
                    --disaggregation-mode prefill \
                    --disaggregation-bootstrap-port 30001 \
                    --tensor-parallel-size 16 \
                    --ep-size 16 \
                    --moe-runner-backend flashinfer_trtllm \
                    --attention-backend triton \
                    --disable-flashinfer-autotune \
                    --nnodes 2 \
                    --kv-cache-dtype fp8_e4m3 \
                    --context-length 16384 \
                    --mem-fraction-static 0.85 \
                    --enable-metrics
              volumeMounts:
                - name: library-dir-host
                  mountPath: /usr/local/nvidia
                - name: gib
                  mountPath: /usr/local/gib
                - name: gcs-model-dir
                  mountPath: /model-weights
                  readOnly: true
            volumes:
              - name: library-dir-host
                hostPath:
                  path: /home/kubernetes/bin/nvidia
              - name: gib
                hostPath:
                  path: /home/kubernetes/bin/gib
              - name: gcs-model-dir
                csi:
                  driver: gcsfuse.csi.storage.gke.io
                  readOnly: true
                  volumeAttributes:
                    bucketName: ${GCS_BUCKET}
                    mountOptions: "implicit-dirs,file-cache:max-size-mb:-1"
    
        # --------------------------------------------------------------------------
        # DECODE WORKER: 2 x GPU A4 (`a4-highgpu-8g`, 16 x B200 GPUs, TP=16, EP=16)
        # --------------------------------------------------------------------------
        Decode:
          componentType: worker
          subComponentType: decode
          replicas: 1
          multinode:
            nodeCount: 2
          extraPodMetadata:
            annotations:
              gke-gcsfuse/volumes: "true"
          livenessProbe:
            httpGet:
              path: /live
              port: system
            periodSeconds: 60
            timeoutSeconds: 30
            failureThreshold: 60
          readinessProbe:
            httpGet:
              path: /health
              port: system
            initialDelaySeconds: 120
            periodSeconds: 30
            timeoutSeconds: 30
            failureThreshold: 60
          sharedMemory:
            size: 128Gi
          resources:
            requests:
              gpu: "8"
              cpu: "128"
              memory: "1500Gi"
            limits:
              gpu: "8"
              cpu: "128"
              memory: "2000Gi"
          envs:
            - name: MODEL_PATH
              value: "/model-weights/hf_models/models--nvidia--qwen3_8-2_4t-a95b-nvfp4"
            - name: DYN_SYSTEM_PORT
              value: "9090"
            - name: LD_LIBRARY_PATH
              value: "/usr/local/gib/lib64:/usr/local/nvidia/lib64"
            - name: GLOO_SOCKET_IFNAME
              value: "eth0"
            - name: NCCL_ALGO
              value: "Ring,Tree"
            - name: WITH_NVIDIA_PEERMEM
              value: "0"
          extraPodSpec:
            serviceAccountName: workload-identity-k8s-sa
            automountServiceAccountToken: true
            resourceClaims:
              - name: rdma
                resourceClaimTemplateName: all-mrdma
            nodeSelector:
              cloud.google.com/gke-accelerator: nvidia-b200
            tolerations:
              - key: "nvidia.com/gpu"
                operator: "Exists"
                effect: "NoSchedule"
            mainContainer:
              image: nvcr.io/nvidia/ai-dynamo/sglang-runtime:1.5.0
              resources:
                claims:
                  - name: rdma
              startupProbe:
                httpGet:
                  path: /live
                  port: system
                initialDelaySeconds: 120
                periodSeconds: 30
                timeoutSeconds: 10
                failureThreshold: 120
              command: ["/bin/bash", "-c"]
              args:
                - |
                  set -ex
                  if [ -f /usr/local/gib/scripts/set_nccl_env.sh ]; then
                    . /usr/local/gib/scripts/set_nccl_env.sh
                  fi
    
                  echo "=== [Dynamo Decode Worker] Launching dynamo.sglang ==="
                  exec python3 -m dynamo.sglang \
                    --dist-init-addr "${GROVE_PCSG_NAME}-${GROVE_PCSG_INDEX}-decode-ldr-0.${GROVE_HEADLESS_SERVICE}.:29500" \
                    --model-path "${MODEL_PATH}" \
                    --served-model-name "nvidia/Qwen3.8-2.4T-A95B-NVFP4" \
                    --host 0.0.0.0 \
                    --disaggregation-mode decode \
                    --disaggregation-bootstrap-port 30001 \
                    --tensor-parallel-size 16 \
                    --ep-size 16 \
                    --moe-runner-backend flashinfer_trtllm \
                    --attention-backend triton \
                    --nnodes 2 \
                    --kv-cache-dtype fp8_e4m3 \
                    --context-length 16384 \
                    --mem-fraction-static 0.85 \
                    --enable-metrics
              volumeMounts:
                - name: library-dir-host
                  mountPath: /usr/local/nvidia
                - name: gib
                  mountPath: /usr/local/gib
                - name: gcs-model-dir
                  mountPath: /model-weights
                  readOnly: true
            volumes:
              - name: library-dir-host
                hostPath:
                  path: /home/kubernetes/bin/nvidia
              - name: gib
                hostPath:
                  path: /home/kubernetes/bin/gib
              - name: gcs-model-dir
                csi:
                  driver: gcsfuse.csi.storage.gke.io
                  readOnly: true
                  volumeAttributes:
                    bucketName: ${GCS_BUCKET}
                    mountOptions: "implicit-dirs,file-cache:max-size-mb:-1"
  2. מחליפים את שם הקטגוריה של Cloud Storage ב-qwen3.8-2.4t-nvfp4-multihost-a4-gcsfuse.yaml ומחילים את המניפסט של עומס העבודה על האשכול:

    envsubst '${GCS_BUCKET}' < qwen3.8-2.4t-nvfp4-multihost-a4-gcsfuse.yaml | kubectl apply -f -
  3. כדי לעקוב אחרי טעינת המשקלים והאתחול של השרת, מציגים את ה-Pods באשכול ומעבירים בסטרימינג את היומנים של ה-Pod הראשי למילוי מראש ושל ה-Pod הראשי לפענוח:

    kubectl get pods
    POD_NAME="YOUR_POD_NAME"
    kubectl logs -f "${POD_NAME}"

    מחליפים את YOUR_POD_NAME בשם של pod המילוי מראש הראשי או pod הפענוח הראשי מהפלט של kubectl get pods.

    בהפעלה קרה, נדרשות כ-4 עד 5 דקות כדי להעביר בסטרימינג את משקלי המודל של 2,400,000,000,000 NVFP4 (כ-1.44TB ב-200 פיצולים) מ-Cloud Storage דרך Cloud Storage FUSE, ולהפעיל את שרתי SGLang עם כמה מארחים. לפני ששולחים בקשות, צריך לחכות עד שיופיע הערך 1/1 ב-Pods של ממשק המשתמש, והערך 2/2 ב-Pods של המילוי מראש והפענוח בעמודה READY של kubectl get pods:

    NAME                                                     READY   STATUS    RESTARTS   AGE
    qwen3-8-2-4t-a4-disagg-0-decode-0-decode-ldr-wz92h       2/2     Running   0          5m
    qwen3-8-2-4t-a4-disagg-0-decode-0-decode-wkr-fb8g6       2/2     Running   0          5m
    qwen3-8-2-4t-a4-disagg-0-frontend-jrx4s                  1/1     Running   0          5m
    qwen3-8-2-4t-a4-disagg-0-prefill-0-prefill-ldr-7nz25     2/2     Running   0          5m
    qwen3-8-2-4t-a4-disagg-0-prefill-0-prefill-wkr-7d5cw     2/2     Running   0          5m
    

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

שולחים בקשה להשלמת צ'אט לשירות Qwen3.8-2.4T שנפרס:

  1. מעבירים את היציאה המקומית 8000 לשירות qwen3-8-2-4t-a4-disagg-frontend:

    kubectl port-forward svc/qwen3-8-2-4t-a4-disagg-frontend 8000:8000

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

  2. בסשן שני של הטרמינל, שולחים בקשה להשלמת צ'אט לנקודת הקצה (endpoint) של /v1/chat/completions:

    curl -s http://127.0.0.1:8000/v1/chat/completions \
      -H "Content-Type: application/json" \
      -d '{
        "model": "nvidia/Qwen3.8-2.4T-A95B-NVFP4",
        "messages": [{"role": "user", "content": "Explain KV-cache disaggregation in one sentence."}],
        "max_tokens": 64
      }' | jq .

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

מריצים את sglang.bench_serving עם הגדרה של 1,024 אסימוני קלט, 1,024 אסימוני פלט (1k/1k), 256 הנחיות ו-128 בקשות בו-זמניות לכל היותר, כדי לבדוק את הביצועים של שרת Qwen3.8-2.4T שנפרס מ-Dynamo frontend Pod:

ROUTER_POD=$(kubectl get pod -l 'nvidia.com/dynamo-component-type=frontend' -o jsonpath='{.items[0].metadata.name}')

kubectl exec "${ROUTER_POD}" -- python3 -m sglang.bench_serving \
  --backend sglang-oai-chat \
  --base-url http://localhost:8000 \
  --model nvidia/Qwen3.8-2.4T-A95B-NVFP4 \
  --dataset-name random \
  --num-prompts 256 \
  --random-input-len 1024 \
  --random-output-len 1024 \
  --random-range-ratio 1.0 \
  --max-concurrency 128 | tee benchmark.log

אחרי שהבדיקה מסתיימת, מחפשים פלט שמכיל מדדים כמו:

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

הסרת המשאבים

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

  1. מוחקים את המשאבים DynamoGraphDeployment ו-ResourceClaimTemplate:

    kubectl delete -f qwen3.8-2.4t-nvfp4-multihost-a4-gcsfuse.yaml --ignore-not-found || true
  2. מסירים את גרסאות ה-Helm של פלטפורמת NVIDIA Dynamo ושל KAI Scheduler:

    helm uninstall dynamo-platform kai-scheduler || true
  3. מוחקים את הסוד של Hugging Face ב-Kubernetes:

    kubectl delete secret hf-secret --ignore-not-found || true
  4. בספרייה cluster-toolkit, משמידים את אשכול GKE ואת התשתית שמשויכת אליו באמצעות הפקודה gcluster destroy:

    cd cluster-toolkit
    ./gcluster destroy "${CLUSTER_NAME}" --auto-approve || true
    cd ..
  5. מוחקים את הקטגוריה של Cloud Storage שבה מאוחסנים משקלי המודל שהועברו ל-Staging ומצב Terraform:

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

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