הצגת Qwen3-Coder-30B-A3B ב-GKE עם SGLang TorchTPU בכמה מארחים ופירוק PD במאיצי TPU v6e

במדריך הזה נסביר איך לפרוס את מודל ה-AI המשולב Qwen3-Coder-30B-A3B (Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8) ב-Google Kubernetes Engine ‏(GKE) ולהגיש ממנו תשובות. הפריסה משתמשת במאיצי TPU v6e (Trillium) עם כמה מארחים ובמסגרת להצגת מודעות SGLang עם TorchTPU (torchtpu) ופירוק של מילוי מראש ופענוח (PD).

בהגדרת הפריסה שמתוארת במדריך הזה, אתם מקצים שני מאגרי צמתים של ct6e-standard-4t עם כמה מארחים, שכל אחד מהם מסודר בטופולוגיה של 2x4 (שמונה שבבי TPU v6e בשני צמתים לכל מאגר צמתים, סה"כ 16 שבבי TPU v6e). אתם יכולים להשתמש ב-Kubernetes LeaderWorkerSet API כדי:

  • מריצים שרתי מילוי מראש ופענוח מרובי-מארחים מפורקים בפרוסות נפרדות של TPU v6e, כשהאפשרויות "מקביליות של טנזורים" ו"מקביליות של מומחים" מופעלות.
  • ניתוב בקשות דרך נתב SGLang disaggregation.

כדי להריץ תהליך עבודה דומה של מילוי בקשות ב-SGLang TorchTPU עם מספר מארחים במאיצי TPU7x (Ironwood) עם המודל הגדול יותר Qwen3-Coder-480B, קראו את המאמר מילוי בקשות ב-Qwen3-Coder-480B ב-GKE עם SGLang TorchTPU עם מספר מארחים והפרדת PD במאיצי TPU7x.

מטרות

  • מגדירים את אשכול GKE ואת מאגרי הצמתים של TPU v6e עם כמה מארחים.
  • מגדירים את פרטי הכניסה ומתקינים את בקר LeaderWorkerSet.
  • פורסים את עומסי העבודה של מילוי מראש, פענוח ונתב של SGLang בכמה מארחים.
  • שולחים בקשת הסקה לשירות הנתב של SGLang.
  • מריצים עומס עבודה להערכת ביצועים של מילוי בקשות.
  • מנקים.

עלויות

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

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

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

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

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

  • מתקינים ומפעילים את Google Cloud CLI, ומתקינים את kubectl. אם אתם משתמשים ב-Cloud Shell, שני הכלים מותקנים מראש.

  • מפעילים את ממשקי ה-API הבאים בפרויקט ב- Google Cloud : ‏Kubernetes Engine API, ‏Compute Engine API, ‏Cloud TPU API ו-Artifact Registry API.

    הפעלת ממשקי API

  • מוודאים שבפרויקט יש מספיק מכסת דיסק ומכסת כתובות IP חיצוניות באזור שבו נמצאת קיבולת ה-TPU:

    1. במסוף Google Cloud , נכנסים לדף Quotas & System Limits.

      כניסה לדף Quotas & System Limits

    2. מוודאים שיש לכם מכסה פנויה של לפחות 400GiB ב-Google Cloud Hyperdisk Balanced, בארבעת הנפחים הזמניים של 100GiB שמשמשים לשמירת המודל במטמון על ידי ה-Pods של המילוי מראש ופענוח הקוד.

    3. מסננים לפי In-use regional external IPv4 addresses ומוודאים שיש לכם מספיק מכסות של כתובות IP באזור היעד.

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

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

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

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

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

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

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

מוודאים שלחשבון השירות שמוגדר כברירת מחדל ב-Compute Engine יש את התפקידים הבאים של ניהול זהויות והרשאות גישה (IAM) בפרויקט:

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

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

export CLUSTER_NAME="YOUR_CLUSTER_NAME"
export PROJECT_ID="YOUR_PROJECT_ID"
export REGION="YOUR_REGION"
export ZONE="YOUR_ZONE"
export RESERVATION="YOUR_RESERVATION_NAME"
export HF_TOKEN="YOUR_HF_TOKEN"
export SGLANG_IMAGE="lmsysorg/sglang:v0.1.0-tpu"

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

  • ‫YOUR_CLUSTER_NAME: השם של אשכול GKE.
  • ‫YOUR_PROJECT_ID: מזהה הפרויקט ב- Google Cloud.
  • ‫YOUR_REGION: האזור שבו רוצים לפרוס את האשכול, למשל us-central1.
  • ‫YOUR_ZONE: האזור שבו רוצים לפרוס את מאגרי הצמתים של TPU v6e, למשל us-central1-b.
  • ‫YOUR_RESERVATION_NAME: אם אתם משתמשים בהזמנה, השם של ההזמנה של TPU v6e.
  • ‫YOUR_HF_TOKEN: טוקן הגישה שלכם ל-Hugging Face.

אם אתם משתמשים בקובץ אימג' של קונטיינר SGLang TorchTPU בהתאמה אישית, החליפו את lmsysorg/sglang:v0.1.0-tpu במשתנה SGLANG_IMAGE ב-URI של קובץ האימג'.

יצירת אשכול GKE

כדי ליצור אשכול GKE במצב Standard ולצרף שני מאגרי צמתים של TPU v6e עם כמה מארחים להפרדה של מילוי מראש ופענוח, מבצעים את השלבים הבאים:

  1. יוצרים אשכול GKE אזורי שמשתמש בצומתי e2-standard-4 למאגר הצמתים שמוגדר כברירת מחדל, שמספק מספיק משאבי מעבד (CPU) וזיכרון לבקר LeaderWorkerSet ולנתב הפירוק של SGLang:

    gcloud container clusters create "${CLUSTER_NAME}" \
      --project="${PROJECT_ID}" \
      --location="${REGION}" \
      --machine-type=e2-standard-4
  2. יוצרים שני מאגרי צמתים של TPU v6e עם כמה מארחים, שכל אחד מהם מכיל שני צמתים של ct6e-standard-4t (שמונה שבבי TPU v6e בטופולוגיה של 2x4 לכל מאגר צמתים, ובסך הכול 16 שבבי TPU v6e):

    gcloud container node-pools create tpu-v6e-pool-0 \
        --cluster="${CLUSTER_NAME}" \
        --location="${REGION}" \
        --node-locations="${ZONE}" \
        --machine-type=ct6e-standard-4t \
        --tpu-topology=2x4 \
        --num-nodes=2 \
        --reservation-affinity=specific \
        --reservation="${RESERVATION}" \
        --project="${PROJECT_ID}"
    
    gcloud container node-pools create tpu-v6e-pool-1 \
        --cluster="${CLUSTER_NAME}" \
        --location="${REGION}" \
        --node-locations="${ZONE}" \
        --machine-type=ct6e-standard-4t \
        --tpu-topology=2x4 \
        --num-nodes=2 \
        --reservation-affinity=specific \
        --reservation="${RESERVATION}" \
        --project="${PROJECT_ID}"

    במקום להשתמש במקום שמור ספציפי, אתם יכולים גם ליצור את מאגרי הצמתים באמצעות קיבולת Spot או Flex-start VMs:

    ‏VMs במודל Spot

    מקצים את מאגרי הצמתים באמצעות קיבולת Spot, על ידי החלפת הדגלים --reservation-affinity=specific ו---reservation="${RESERVATION}" בדגל --spot:

    gcloud container node-pools create tpu-v6e-pool-0 \
        --cluster="${CLUSTER_NAME}" \
        --location="${REGION}" \
        --node-locations="${ZONE}" \
        --machine-type=ct6e-standard-4t \
        --tpu-topology=2x4 \
        --num-nodes=2 \
        --project="${PROJECT_ID}" \
        --spot
    
    gcloud container node-pools create tpu-v6e-pool-1 \
        --cluster="${CLUSTER_NAME}" \
        --location="${REGION}" \
        --node-locations="${ZONE}" \
        --machine-type=ct6e-standard-4t \
        --tpu-topology=2x4 \
        --num-nodes=2 \
        --project="${PROJECT_ID}" \
        --spot

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

    מכונות וירטואליות מסוג Flex-start

    מקצים את מאגרי הצמתים באמצעות אפשרות הצריכה Flex-start:

    gcloud container node-pools create tpu-v6e-pool-0 \
        --cluster="${CLUSTER_NAME}" \
        --location="${REGION}" \
        --node-locations="${ZONE}" \
        --machine-type=ct6e-standard-4t \
        --tpu-topology=2x4 \
        --project="${PROJECT_ID}" \
        --reservation-affinity=none \
        --enable-autoscaling \
        --num-nodes=0 \
        --min-nodes=0 \
        --max-nodes=2 \
        --flex-start
    
    gcloud container node-pools create tpu-v6e-pool-1 \
        --cluster="${CLUSTER_NAME}" \
        --location="${REGION}" \
        --node-locations="${ZONE}" \
        --machine-type=ct6e-standard-4t \
        --tpu-topology=2x4 \
        --project="${PROJECT_ID}" \
        --reservation-affinity=none \
        --enable-autoscaling \
        --num-nodes=0 \
        --min-nodes=0 \
        --max-nodes=2 \
        --flex-start

    מידע נוסף זמין במאמר הרצת עומס עבודה קטן של אצווה באמצעות יחידות TPU ומכונות וירטואליות מסוג Flex-start.

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

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

הגדרת פרטי כניסה והתקנת LeaderWorkerSet

כדי להכין את האשכול לפריסת SGLang בכמה מארחים, מבצעים את הפעולות הבאות:

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

    kubectl create secret generic hf-secret \
        --from-literal=hf_api_token="${HF_TOKEN}"
  2. מתקינים את ה-CustomResourceDefinition והבקר של LeaderWorkerSet באשכול:

    kubectl apply --server-side -k github.com/kubernetes-sigs/lws/config/default?ref=main
    echo "Waiting for LWS controller to be ready..."
    kubectl wait --for=condition=Available --timeout=300s deployment/lws-controller-manager -n lws-system

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

במניפסט של עומס העבודה (qwen3-coder-30b-multihost-disagg-v6e-2x4.yaml) מוגדרים המשאבים הבאים, כדי לתאם בין מילוי מראש מבוזר לבין פענוח של הצגת מודעות בשני מאגרי הצמתים של 2x4 TPU v6e:

  • <!--StorageClass (): provisions ephemeral 100 GiBReadWriteOnce volumes backed by Google Cloud Hyperdisk Balanced storage.-->StorageClass (hyperdisk-balanced-tpu): מקצה נפחי אחסון זמניים של ‎100 GiB ReadWriteOnce שמגובים באחסון של Google Cloud Hyperdisk Balanced. כל אחד מארבעת ה-Pods למילוי מראש ולפענוח טוען את אמצעי האחסון שלו ב-/data כדי לאחסן באופן מקומי את נקודות הבדיקה של Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8 שהורדו.

  • ‫מאגר למילוי מראש (sglang-prefill-service ו-sglang-tpu-prefill): מריץ LeaderWorkerSet, שמכיל Pod אחד של Leader ו-Pod אחד של Worker, שמוצמדים באופן בלעדי למאגר אחד של צומתי 2x4 TPU v6e. במאגר הצמתים הזה יש שמונה שבבי TPU, שמספקים שמונה ליבות Tensor. ה-LeaderWorkerSet משתמש בהערה leaderworkerset.sigs.k8s.io/exclusive-topology שמוגדרת ל-"cloud.google.com/gke-nodepool". ה-Pods מפעילים את sglang.launch_server עם הדגלים הבאים, וחושפים את היציאה 8000 ואת יציאת האתחול 8998:

    • --disaggregation-mode=prefill
    • --tensor-parallel-size=8
    • --ep-size=8
  • ‫Decode pool (sglang-decode-service ו-sglang-tpu-decode): מריץ LeaderWorkerSet של size: 2 שמוצמד למאגר הצמתים השני של 2x4 TPU v6e (שמונה שבבי TPU שמספקים שמונה TensorCores). ‫LeaderWorkerSet מפעיל את sglang.launch_server עם הדגלים הבאים:

    • --disaggregation-mode=decode
    • --tensor-parallel-size=8
    • --ep-size=8
  • ‫Disaggregation router (sglang-router ו-sglang-service): מריץ פריסת מעבד (CPU) שמפעילה את המודול sglang_router.launch_router עם הדגל --pd-disaggregation וחושפת ClusterIP Service ביציאה 8000. הנתב מפנה את ההנחיות ומתאם את העברת המטמון של KV בין השירותים sglang-prefill-service ו-sglang-decode-service.

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

  1. מורידים את המניפסט של עומס העבודה:

    curl -LO https://raw.githubusercontent.com/GoogleCloudSamples/AIHypercomputerSamples/main/tpu/inference/qwen3-coder/qwen3-coder-30b-multihost-disagg-v6e-2x4.yaml

    אפשר גם ליצור קובץ בשם qwen3-coder-30b-multihost-disagg-v6e-2x4.yaml עם התוכן הבא:

    # ==============================================================================
    # Serve Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8 on GKE / Trillium (TPU v6e)
    # using SGLang TorchTPU with Prefill/Decode Disaggregation, multi-host (2x4)
    # ==============================================================================
    apiVersion: storage.k8s.io/v1
    kind: StorageClass
    metadata:
      name: hyperdisk-balanced-tpu
    provisioner: pd.csi.storage.gke.io
    volumeBindingMode: WaitForFirstConsumer
    allowVolumeExpansion: true
    parameters:
      type: hyperdisk-balanced
    ---
    # ==============================================================================
    # PREFILL POOL: Internal ClusterIP Service exposing SGLang Prefill Endpoint
    # ==============================================================================
    apiVersion: v1
    kind: Service
    metadata:
      name: sglang-prefill-service
      labels:
        app: sglang-prefill
    spec:
      type: ClusterIP
      selector:
        app: sglang-prefill
        role: leader
      ports:
        - name: http
          protocol: TCP
          port: 8000
          targetPort: 8000
        - name: bootstrap
          protocol: TCP
          port: 8998
          targetPort: 8998
    ---
    # ==============================================================================
    # PREFILL POOL: LeaderWorkerSet (TPU v6e, 1 slice = 2 nodes, 8 chips / 8 TensorCores, 2x4)
    # ==============================================================================
    apiVersion: leaderworkerset.x-k8s.io/v1
    kind: LeaderWorkerSet
    metadata:
      name: sglang-tpu-prefill
      annotations:
        leaderworkerset.sigs.k8s.io/exclusive-topology: "cloud.google.com/gke-nodepool"
      labels:
        app: sglang-prefill
    spec:
      replicas: 1
      rolloutStrategy:
        type: RollingUpdate
      leaderWorkerTemplate:
        size: 2
        restartPolicy: RecreateGroupOnPodRestart
        leaderTemplate:
          metadata:
            labels:
              app: sglang-prefill
              role: leader
          spec: &prefill-pod-spec
            securityContext:
              fsGroup: 65534
            nodeSelector:
              cloud.google.com/gke-tpu-accelerator: tpu-v6e-slice
              cloud.google.com/gke-tpu-topology: 2x4
            tolerations:
              - key: "google.com/tpu"
                operator: "Exists"
                effect: "NoSchedule"
            containers:
              - name: sglang
                image: $SGLANG_IMAGE
                command:
                  - /bin/bash
                  - -c
                  - |
                    set -ex
                    ulimit -c 0
                    LEADER_POD="${LWS_LEADER_ADDRESS%%.*}"
                    DNS_SUFFIX="${LWS_LEADER_ADDRESS#*.}"
                    HOSTS="${LWS_LEADER_ADDRESS}"
                    for ((i = 1; i < LWS_GROUP_SIZE; i++)); do
                      HOSTS="${HOSTS},${LEADER_POD}-${i}.${DNS_SUFFIX}"
                    done
                    export TPU_WORKER_HOSTNAMES="${HOSTS}"
                    export TPU_WORKER_ID="${LWS_WORKER_INDEX}"
                    export TORCH_TPU_DEVICES_PER_HOST=4
                    export TPU_TOPOLOGY="2x4x1"
                    echo "=== [prefill rank ${LWS_WORKER_INDEX}/${LWS_GROUP_SIZE}] hosts=${TPU_WORKER_HOSTNAMES} ==="
                    exec python3 -m sglang.launch_server \
                      --device=tpu \
                      --host=0.0.0.0 \
                      --port=8000 \
                      --model-path=Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8 \
                      --tensor-parallel-size=8 \
                      --ep-size=8 \
                      --nnodes="${LWS_GROUP_SIZE}" \
                      --node-rank="${LWS_WORKER_INDEX}" \
                      --dist-init-addr="${LWS_LEADER_ADDRESS}:20000" \
                      --attention-backend=pallas_tpu_rpa_batched \
                      --mem-fraction-static=0.8 \
                      --max-running-requests=256 \
                      --chunked-prefill-size=8192 \
                      --max-total-tokens=524288 \
                      --context-length=9216 \
                      --enable-torch-compile \
                      --torch-compile-max-bs=256 \
                      --tpu-compile-tracer=direct \
                      --sampling-defaults=openai \
                      --enable-fused-qk-norm-rope \
                      --disaggregation-mode=prefill \
                      --disaggregation-transfer-backend=tpu \
                      --download-dir=/data \
                      --tpu-compile-tokens=8192 \
                      --disable-radix-cache
                env:
                  - name: HF_TOKEN
                    valueFrom:
                      secretKeyRef:
                        name: hf-secret
                        key: hf_api_token
                        optional: true
                resources:
                  requests:
                    cpu: "16"
                    memory: 128Gi
                    google.com/tpu: "4"
                  limits:
                    memory: 280Gi
                    google.com/tpu: "4"
                ports:
                  - containerPort: 8000
                    name: http
                  - containerPort: 8998
                    name: bootstrap
                startupProbe:
                  httpGet:
                    path: /health
                    port: 8000
                  initialDelaySeconds: 60
                  periodSeconds: 15
                  timeoutSeconds: 10
                  failureThreshold: 236
                readinessProbe:
                  httpGet:
                    path: /health
                    port: 8000
                  periodSeconds: 10
                  timeoutSeconds: 5
                  failureThreshold: 3
                volumeMounts:
                  - name: model-cache
                    mountPath: /data
                  - name: dshm
                    mountPath: /dev/shm
            volumes:
              - name: dshm
                emptyDir:
                  medium: Memory
                  sizeLimit: 32Gi
              - name: model-cache
                ephemeral:
                  volumeClaimTemplate:
                    spec:
                      accessModes: ["ReadWriteOnce"]
                      storageClassName: hyperdisk-balanced-tpu
                      resources:
                        requests:
                          storage: 100Gi
        workerTemplate:
          metadata:
            labels:
              app: sglang-prefill
              role: worker
          spec: *prefill-pod-spec
    ---
    # ==============================================================================
    # DECODE POOL: Internal ClusterIP Service exposing SGLang Decode Endpoint
    # ==============================================================================
    apiVersion: v1
    kind: Service
    metadata:
      name: sglang-decode-service
      labels:
        app: sglang-decode
    spec:
      type: ClusterIP
      selector:
        app: sglang-decode
        role: leader
      ports:
        - name: http
          protocol: TCP
          port: 8000
          targetPort: 8000
        - name: bootstrap
          protocol: TCP
          port: 8998
          targetPort: 8998
    ---
    # ==============================================================================
    # DECODE POOL: LeaderWorkerSet (TPU v6e, 1 slice = 2 nodes, 8 chips / 8 TensorCores, 2x4)
    # ==============================================================================
    apiVersion: leaderworkerset.x-k8s.io/v1
    kind: LeaderWorkerSet
    metadata:
      name: sglang-tpu-decode
      annotations:
        leaderworkerset.sigs.k8s.io/exclusive-topology: "cloud.google.com/gke-nodepool"
      labels:
        app: sglang-decode
    spec:
      replicas: 1
      rolloutStrategy:
        type: RollingUpdate
      leaderWorkerTemplate:
        size: 2
        restartPolicy: RecreateGroupOnPodRestart
        leaderTemplate:
          metadata:
            labels:
              app: sglang-decode
              role: leader
          spec: &decode-pod-spec
            securityContext:
              fsGroup: 65534
            nodeSelector:
              cloud.google.com/gke-tpu-accelerator: tpu-v6e-slice
              cloud.google.com/gke-tpu-topology: 2x4
            tolerations:
              - key: "google.com/tpu"
                operator: "Exists"
                effect: "NoSchedule"
            containers:
              - name: sglang
                image: $SGLANG_IMAGE
                command:
                  - /bin/bash
                  - -c
                  - |
                    set -ex
                    ulimit -c 0
                    LEADER_POD="${LWS_LEADER_ADDRESS%%.*}"
                    DNS_SUFFIX="${LWS_LEADER_ADDRESS#*.}"
                    HOSTS="${LWS_LEADER_ADDRESS}"
                    for ((i = 1; i < LWS_GROUP_SIZE; i++)); do
                      HOSTS="${HOSTS},${LEADER_POD}-${i}.${DNS_SUFFIX}"
                    done
                    export TPU_WORKER_HOSTNAMES="${HOSTS}"
                    export TPU_WORKER_ID="${LWS_WORKER_INDEX}"
                    export TORCH_TPU_DEVICES_PER_HOST=4
                    export TPU_TOPOLOGY="2x4x1"
                    echo "=== [decode rank ${LWS_WORKER_INDEX}/${LWS_GROUP_SIZE}] hosts=${TPU_WORKER_HOSTNAMES} ==="
                    exec python3 -m sglang.launch_server \
                      --device=tpu \
                      --host=0.0.0.0 \
                      --port=8000 \
                      --model-path=Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8 \
                      --tensor-parallel-size=8 \
                      --ep-size=8 \
                      --nnodes="${LWS_GROUP_SIZE}" \
                      --node-rank="${LWS_WORKER_INDEX}" \
                      --dist-init-addr="${LWS_LEADER_ADDRESS}:20000" \
                      --attention-backend=pallas_tpu_rpa_batched \
                      --mem-fraction-static=0.8 \
                      --max-running-requests=256 \
                      --chunked-prefill-size=8192 \
                      --max-total-tokens=524288 \
                      --context-length=9216 \
                      --enable-torch-compile \
                      --torch-compile-max-bs=256 \
                      --tpu-compile-tracer=direct \
                      --sampling-defaults=openai \
                      --enable-fused-qk-norm-rope \
                      --disaggregation-mode=decode \
                      --disaggregation-transfer-backend=tpu \
                      --download-dir=/data
                env:
                  - name: HF_TOKEN
                    valueFrom:
                      secretKeyRef:
                        name: hf-secret
                        key: hf_api_token
                        optional: true
                resources:
                  requests:
                    cpu: "16"
                    memory: 128Gi
                    google.com/tpu: "4"
                  limits:
                    memory: 280Gi
                    google.com/tpu: "4"
                ports:
                  - containerPort: 8000
                    name: http
                  - containerPort: 8998
                    name: bootstrap
                startupProbe:
                  httpGet:
                    path: /health
                    port: 8000
                  initialDelaySeconds: 60
                  periodSeconds: 15
                  timeoutSeconds: 10
                  failureThreshold: 236
                readinessProbe:
                  httpGet:
                    path: /health
                    port: 8000
                  periodSeconds: 10
                  timeoutSeconds: 5
                  failureThreshold: 3
                volumeMounts:
                  - name: model-cache
                    mountPath: /data
                  - name: dshm
                    mountPath: /dev/shm
            volumes:
              - name: dshm
                emptyDir:
                  medium: Memory
                  sizeLimit: 32Gi
              - name: model-cache
                ephemeral:
                  volumeClaimTemplate:
                    spec:
                      accessModes: ["ReadWriteOnce"]
                      storageClassName: hyperdisk-balanced-tpu
                      resources:
                        requests:
                          storage: 100Gi
        workerTemplate:
          metadata:
            labels:
              app: sglang-decode
              role: worker
          spec: *decode-pod-spec
    ---
    # ==============================================================================
    # SGLANG DISAGGREGATION ROUTER / PROXY DEPLOYMENT
    # ==============================================================================
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: sglang-router
      labels:
        app: sglang-router
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: sglang-router
      template:
        metadata:
          labels:
            app: sglang-router
        spec:
          containers:
            - name: router
              image: $SGLANG_IMAGE
              command:
                - python3
                - -m
                - sglang_router.launch_router
              args:
                - --pd-disaggregation
                - --prefill
                - http://sglang-prefill-service:8000
                - "8998"
                - --decode
                - http://sglang-decode-service:8000
                - --host
                - 0.0.0.0
                - --port
                - "8000"
                - --model-path
                - Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8
                - --request-timeout-secs
                - "3600"
                - --worker-startup-timeout-secs
                - "5400"
              env:
                - name: HF_TOKEN
                  valueFrom:
                    secretKeyRef:
                      name: hf-secret
                      key: hf_api_token
                      optional: true
              ports:
                - containerPort: 8000
                  name: http
              readinessProbe:
                httpGet:
                  path: /health_generate
                  port: 8000
                initialDelaySeconds: 15
                periodSeconds: 30
                timeoutSeconds: 10
                failureThreshold: 3
              resources:
                requests:
                  cpu: "1"
                  memory: 4Gi
                limits:
                  cpu: "4"
                  memory: 12Gi
    ---
    # ==============================================================================
    # SGLANG EXTERNAL SERVICE: Public / Client Endpoint for OpenAI-compatible Serving
    # ==============================================================================
    apiVersion: v1
    kind: Service
    metadata:
      name: sglang-service
      labels:
        app: sglang-router
    spec:
      type: ClusterIP
      selector:
        app: sglang-router
      ports:
        - name: http
          protocol: TCP
          port: 8000
          targetPort: 8000
    
  2. מחילים את המניפסט של עומס העבודה על האשכול:

    envsubst '$SGLANG_IMAGE' < qwen3-coder-30b-multihost-disagg-v6e-2x4.yaml | kubectl apply -f -
  3. מוודאים שהפודים של הנתב, המילוי מראש והפענוח נוצרו:

    kubectl get pods

    כשהפודים מוכנים, הפלט אמור להיראות כך:

    NAME                             READY   STATUS    RESTARTS   AGE
    sglang-router-6d87cb8c7d-gv4wr   1/1     Running   0          39m
    sglang-tpu-decode-0              1/1     Running   0          40m
    sglang-tpu-decode-0-1            1/1     Running   0          40m
    sglang-tpu-prefill-0             1/1     Running   0          40m
    sglang-tpu-prefill-0-1           1/1     Running   0          40m
    
  4. עוקבים אחרי היומנים של Pod ה-Leader למילוי מראש ולפענוח עד לסיום ההפעלה:

    kubectl logs -f -l role=leader --prefix=true &
    LOG_PID=$!
    kubectl wait --for=condition=Ready --timeout=3600s pod -l role=leader
    kill "${LOG_PID}" 2>/dev/null || true

    ההורדה הראשונית של המודל, טעינת המשקלים וההידור נמשכים בערך 7 עד 10 דקות בהפעלה הראשונה.

    אם אתם משתמשים בקיבולת שניתנת להפסקה לפני הזמן, כמו קיבולת של Spot או מכונות וירטואליות מסוג Flex-start, יכול להיות שייקח יותר זמן ליצור את הצמתים של פרוסות ה-TPU אחרי שתיצרו את מאגר הצמתים, ויכול להיות שהם יופסקו לפני הזמן במהלך הרצת עומסי העבודה. מידע נוסף מופיע במאמר מכונות VM זמניות. כדי לבדוק אם נוצרו צומתי פרוסות ה-TPU, עיינו בקטע בדיקת הסטטוס והתנאים של הצומת.

    מחכים עד שביומני ה-Pod הראשיים של המילוי מראש והפענוח תופיע ההודעה הבאה, ובעמודה READY של כל ה-Pods הערך של kubectl get pods יהיה 1/1:

    The server is fired up and ready to roll!
    
  5. פותחים סשן שני של טרמינל ומעבירים את היציאה המקומית 8000 אל sglang-service:

    kubectl port-forward service/sglang-service 8000:8000

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

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

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








  
  














  
  
  
  



curl -s -X POST http://localhost:8000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8",
    "messages": [
      {"role": "user", "content": "Explain quicksort in two sentences."}
    ],
    "max_tokens": 60,
    "temperature": 0.0
  }'

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

כדי להשוות את המודל Qwen3-Coder-30B-A3B שנפרס באמצעות עומס עבודה של קלט של 1,024 טוקנים ופלט של 1,024 טוקנים (1k/1k) מ-Pod של sglang-router, מריצים את הפקודה הבאה:

kubectl exec deployment/sglang-router -- python3 -m sglang.bench_serving \
  --backend sglang \
  --base-url http://localhost:8000 \
  --model Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8 \
  --dataset-name random \
  --random-input-len 1024 \
  --random-output-len 1024 \
  --random-range-ratio 1.0 \
  --num-prompts 256 \
  --max-concurrency 256

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

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

הסרת המשאבים

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

  1. מוחקים את עומסי העבודה של SGLang למילוי מראש, לפענוח ולניתוב:

    kubectl delete -f qwen3-coder-30b-multihost-disagg-v6e-2x4.yaml --ignore-not-found
  2. מוחקים את מאגרי הצמתים של TPU v6e ואת אשכול GKE:

    gcloud container node-pools delete tpu-v6e-pool-0 \
        --cluster="${CLUSTER_NAME}" \
        --location="${REGION}" \
        --project="${PROJECT_ID}" \
        --quiet
    
    gcloud container node-pools delete tpu-v6e-pool-1 \
        --cluster="${CLUSTER_NAME}" \
        --location="${REGION}" \
        --project="${PROJECT_ID}" \
        --quiet
    
    gcloud container clusters delete "${CLUSTER_NAME}" \
        --location="${REGION}" \
        --project="${PROJECT_ID}" \
        --quiet

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