הצגת Qwen3-30B-A3B ב-GKE עם vLLM TorchTPU ב-TPU v6e

במדריך הזה נסביר איך לפרוס ולהפעיל את מודל שילוב המומחים (MoE) Qwen3-30B-A3B (Qwen/Qwen3-30B-A3B-FP8) ב-Google Kubernetes Engine ‏(GKE) באמצעות מאיץ TPU v6e (Trillium) במארח יחיד, ומסגרת vLLM TPU להפעלת מודלים עם TorchTPU (torchtpu). עומס העבודה פועל בצומת ct6e-standard-4t יחיד שמכיל ארבעה שבבי TPU v6e בטופולוגיה 2x2, עם מקבילוּת נתונים ומקבילוּת מומחים.

כדי להריץ תהליך עבודה דומה של הצגת מודל vLLM TorchTPU במארח יחיד במאיצי TPU7x (Ironwood) עם המודל הגדול יותר Qwen3.5-397B, קראו את המאמר הצגת המודל Qwen3.5-397B ב-GKE באמצעות vLLM TorchTPU במאיצי TPU7x.

מטרות

  • מגדירים את אשכול ה-GKE ואת מאגר הצמתים של TPU v6e במארח יחיד.
  • פורסים את שרת vLLM TorchTPU במארח יחיד ב-GKE.
  • שולחים בקשת הסקה לשרת vLLM TorchTPU.
  • מריצים עומס עבודה להערכת ביצועים של מילוי בקשות.
  • מנקים.

עלויות

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

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

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

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

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

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

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

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

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

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

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

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

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

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

gcloud auth login
gcloud auth application-default login

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

export PROJECT_ID="YOUR_PROJECT_ID"
export REGION="YOUR_REGION"
export ZONE="YOUR_ZONE"
export CLUSTER_NAME="YOUR_CLUSTER_NAME"
export MACHINE_TYPE="tpu7x-standard-4t"
export NODE_POOL_NAME="np-0"
export RESERVATION_NAME="YOUR_RESERVATION_NAME"
export HF_TOKEN="YOUR_HF_TOKEN"
export NETWORK="YOUR_NETWORK_NAME"
export SUBNETWORK="YOUR_SUBNETWORK_NAME"
export VLLM_IMAGE="YOUR_VLLM_TPU_IMAGE"

gcloud config set project "${PROJECT_ID}"
gcloud config set billing/quota_project "${PROJECT_ID}"

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

  • ‫YOUR_PROJECT_ID: מזהה הפרויקט ב- Google Cloud.
  • ‫YOUR_REGION: האזור שבו רוצים לפרוס את האשכול, למשל us-central1.
  • ‫YOUR_ZONE: האזור שבו רוצים לפרוס את מאגר הצמתים של TPU v6e, למשל us-central1-c.
  • ‫YOUR_CLUSTER_NAME: השם של אשכול GKE, למשל tpu-v6e-tutorial-cluster.
  • ‫YOUR_RESERVATION_NAME: אם אתם משתמשים בהזמנה, השם של ההזמנה של TPU v6e.
  • ‫YOUR_HF_TOKEN: טוקן הגישה שלכם ל-Hugging Face.
  • ‫YOUR_NETWORK_NAME: השם של רשת ה-Google Cloud . אם יצרתם רשת בהתאמה אישית, ציינו את הרשת הזו. אחרת, צריך לציין את default.
  • ‫YOUR_SUBNETWORK_NAME: השם של רשת המשנה ב-Google Cloud . אם יצרתם רשת משנה בהתאמה אישית, ציינו אותה. אחרת, צריך לציין את default.
  • ‫YOUR_VLLM_TPU_IMAGE: קובץ האימג' של vLLM TorchTPU שבו משתמשים. לגבי קובצי אימג' ציבוריים, עיינו במאגר vLLM TorchTPU.

יצירת אשכול GKE

כדי ליצור אשכול GKE במצב Standard ולצרף מאגר של צומתי TPU v6e במארח יחיד:

  1. יוצרים אשכול GKE אזורי:

    gcloud container clusters create "${CLUSTER_NAME}" \
        --project="${PROJECT_ID}" \
        --location="${REGION}" \
        --node-locations="${ZONE}" \
        --network="${NETWORK}" \
        --subnetwork="${SUBNETWORK}"

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

    הפלט אמור להיראות כך:

    NAME                      LOCATION     MASTER_VERSION      MASTER_IP      MACHINE_TYPE   NODE_VERSION        NUM_NODES  STATUS
    tpu-v6e-tutorial-cluster  us-central1  1.35.7-gke.1027000  34.63.222.246  e2-standard-4  1.35.7-gke.1027000  3          RUNNING
    
  2. יוצרים מאגר צמתים של TPU v6e במארח יחיד עם צומת ct6e-standard-4t אחד (ארבעה שבבי TPU v6e בטופולוגיה 2x2) באמצעות ההזמנה:

    gcloud container node-pools create "${NODE_POOL_NAME}" \
        --cluster="${CLUSTER_NAME}" \
        --machine-type="${MACHINE_TYPE}" \
        --location="${REGION}" \
        --node-locations="${ZONE}" \
        --project="${PROJECT_ID}" \
        --num-nodes=1 \
        --reservation-affinity=specific \
        --reservation="${RESERVATION_NAME}"

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

    • ‫Spot VMs: מחליפים את הדגלים --reservation-affinity ו---reservation בדגל --spot כדי להשתמש בקיבולת של Spot. יכול להיות שיעבור זמן מהרגע שיוצרים את מאגר הצמתים ועד שנוצרים המופעים שמשתמשים בקיבולת של Spot, ויכול להיות שהם יופסקו בזמן שעומסי העבודה פועלים. אם לוקח זמן ליצור את ה-Pods, או שעומסי העבודה נעצרים באמצע הריצה, יכול להיות שזו הסיבה.
    • ‫Flex-start VMs: כדי להקצות את מאגר הצמתים באמצעות אפשרות הצריכה Flex-start, מעבירים את הדגל --flex-start. מידע נוסף זמין במאמר הרצת עומס עבודה קטן של אצווה באמצעות יחידות TPU ומכונות וירטואליות מסוג Flex-start.

    הפלט אמור להיראות כך:

    NAME  MACHINE_TYPE      DISK_SIZE_GB  NODE_VERSION
    np-0  ct6e-standard-4t  100           1.35.7-gke.1027000
    
  3. מגדירים את פרטי הכניסה של kubectl כדי לתקשר עם האשכול:

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

הגדרת פרטי כניסה

יוצרים סוד של 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 -

פריסת שרת vLLM TorchTPU

במניפסט של עומס העבודה (qwen3_5_server.yaml) מוגדרים המשאבים הבאים:

  • ‫StorageClass‏ (hyperdisk-balanced-qwen3) ו-PersistentVolumeClaim‏ (hd-claim-qwen3) בנפח 500GiB שמגובה באחסון Google Cloud Hyperdisk Balanced, כדי לאחסן את משקלי המודל שהורדו, את מטמון הקימפול של XLA ואת מטמון הקימפול של PyTorch.

  • פריסה (vllm-qwen3) שמתוזמנת בצומת TPU v6e יחיד עם טופולוגיה של 2x2 (google.com/tpu: '4'). הקונטיינר מריץ את vllm serve בשביל Qwen/Qwen3-30B-A3B-FP8 עם הדגלים הבאים:

    • --data-parallel-size=4
    • --tensor-parallel-size=1
    • --enable-expert-parallel
    • --kv-cache-dtype=fp8
    • --max-model-len=9236
    • --max-num-seqs=64.
  • שירות ClusterIP (vllm-qwen3-service) שחושף את יציאה 8000.

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

  1. אחזור המניפסט של השרת:

    curl -O https://raw.githubusercontent.com/GoogleCloudSamples/AIHypercomputerSamples/refs/heads/main/tpu/inference/qwen3-5-397b/qwen3_5_server.yaml

    אפשר גם ליצור קובץ בשם qwen3_5_server.yaml עם התוכן הבא:

    apiVersion: storage.k8s.io/v1
    kind: StorageClass
    metadata:
      name: hyperdisk-balanced-qwen3-5
    provisioner: pd.csi.storage.gke.io
    parameters:
      type: hyperdisk-balanced
    reclaimPolicy: Delete
    volumeBindingMode: WaitForFirstConsumer
    allowVolumeExpansion: true
    ---
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: hd-claim-qwen3-5
      namespace: default
    spec:
      storageClassName: hyperdisk-balanced-qwen3-5
      accessModes:
        - ReadWriteOnce
      resources:
        requests:
          storage: 1500Gi
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: vllm-qwen3-5
      namespace: default
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: vllm-qwen3-5
      template:
        metadata:
          labels:
            app: vllm-qwen3-5
        spec:
          nodeSelector:
            cloud.google.com/gke-tpu-accelerator: tpu7x
            cloud.google.com/gke-tpu-topology: 2x2x1
          tolerations:
          - key: "cloud.google.com/gke-spot"
            operator: "Equal"
            value: "true"
            effect: "NoSchedule"
          containers:
          - name: vllm-tpu
            image: $VLLM_IMAGE
            command: ["vllm", "serve"]
            args:
            - --host=0.0.0.0
            - --port=8000
            - --model=Qwen/Qwen3.5-397B-A17B-FP8
            - --tokenizer=Qwen/Qwen3.5-397B-A17B-FP8
            - --max-model-len=9236
            - --max-num-batched-tokens=1024
            - --max-num-seqs=64
            - --no-enable-prefix-caching
            - --gpu-memory-utilization=0.9
            - --data-parallel-size=8
            - --tensor-parallel-size=1
            - --async-scheduling
            - --language-model-only
            - --enable-auto-tool-choice
            - --tool-call-parser=qwen3_coder
            - --reasoning-parser=qwen3
            - '--limit-mm-per-prompt={"image": 0, "video": 0}'
            - --kv-cache-dtype=fp8
            - --enable-expert-parallel
            - --download-dir=/data
            - --block-size=128
            env:
            - name: MODEL_IMPL_TYPE
              value: "vllm"
            - name: DP_SCHED_ENABLED
              value: "1"
            - name: ONEHOT_MOE_PERMUTE_THRESHOLD
              value: "32768"
            - name: VLLM_ENGINE_READY_TIMEOUT_S
              value: "9600"
            - name: VLLM_ENGINE_ITERATION_TIMEOUT_S
              value: "1800"
            - name: USE_MOE_SPARSE_CORE
              value: "1"
            - name: TPU_MOE_SKIP_PADDED_TOKENS
              value: "1"
            - name: VLLM_CACHE_ROOT
              value: "/data/vllm_cache"
            - name: VLLM_TORCH_COMPILE_CACHE_DIR
              value: "/data/torch_compile_cache"
            - name: XLA_CACHE_DIR
              value: "/data/xla_cache"
            - name: HF_HOME
              value: /data
            - name: HF_TOKEN
              valueFrom:
                secretKeyRef:
                  name: hf-secret
                  key: hf_api_token
            ports:
            - containerPort: 8000
            resources:
              limits:
                google.com/tpu: '4'
              requests:
                google.com/tpu: '4'
            readinessProbe:
              tcpSocket:
                port: 8000
              initialDelaySeconds: 15
              periodSeconds: 10
            volumeMounts:
            - mountPath: "/data"
              name: data-volume
            - mountPath: /dev/shm
              name: dshm
          volumes:
          - emptyDir:
              medium: Memory
            name: dshm
          - name: data-volume
            persistentVolumeClaim:
              claimName: hd-claim-qwen3-5
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: vllm-qwen3-5-service
      namespace: default
    spec:
      selector:
        app: vllm-qwen3-5
      type: ClusterIP
      ports:
      - name: http
        protocol: TCP
        port: 8000
        targetPort: 8000
  2. מחילים את המניפסט של שרת vLLM TorchTPU על האשכול:

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

    kubectl wait --for=jsonpath='{.status.phase}'=Running pod -l app=vllm-qwen3-5 --timeout=900s
    kubectl logs deployment/vllm-qwen3-5 -c vllm-tpu -f

    ההפעלה הראשונית של השירות עשויה להימשך כ-15 עד 30 דקות, בזמן שה-Pod מוריד את משקלי המודל ומקמפל את המודל.

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

    לפני ששולחים תנועה, צריך להמתין עד שמופיעה ההודעה Application startup complete ביומנים:

    (APIServer pid=1) INFO:     Started server process [1]
    (APIServer pid=1) INFO:     Waiting for application startup.
    (APIServer pid=1) INFO:     Application startup complete.
    
  4. פותחים סשן שני בטרמינל.

  5. בסשן השני של הטרמינל, מעבירים את היציאה המקומית 8000 אל vllm-qwen3-service:

    kubectl port-forward service/vllm-qwen3-5-service 8000:8000

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

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

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

curl http://localhost:8000/v1/chat/completions \
    -H "Content-Type: application/json" \
    -d '{
        "model": "Qwen/Qwen3.5-397B-A17B-FP8",
        "messages": [
            {"role": "user", "content": "Hello! Tell me a joke."}
        ],
        "max_tokens": 512,
        "temperature": 0.7
    }' | jq .

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

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

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

    curl -O https://raw.githubusercontent.com/GoogleCloudSamples/AIHypercomputerSamples/refs/heads/main/tpu/inference/qwen3-5-397b/qwen3_5-benchmark-8k1k.yaml

    אפשר גם ליצור קובץ בשם qwen3_5-benchmark-8k1k.yaml עם התוכן הבא:

    apiVersion: v1
    kind: Pod
    metadata:
      name: qwen3-5-bench-8k1k
      namespace: default
    spec:
      restartPolicy: OnFailure
      terminationGracePeriodSeconds: 60
      containers:
      - name: vllm-bench
        image: $VLLM_IMAGE
        command: ["/bin/bash", "-c"]
        args:
        - |
          while ! curl http://vllm-qwen3-5-service:8000/ping; do sleep 30 && echo 'Waiting for server...'; done
          apt-get update && apt-get install -y git && \
          git clone https://github.com/SemiAnalysisAI/InferenceX.git /ubench/inferencex && \
          cd /ubench/inferencex && \
          git checkout 89ce6098ef2bc4576a735c43f39c7d972b091cfc && \
          python3 /ubench/inferencex/utils/bench_serving/benchmark_serving.py \
            --backend=vllm \
            --percentile-metrics='ttft,tpot,itl,e2el' \
            --host=vllm-qwen3-5-service \
            --port=8000 \
            --model=Qwen/Qwen3.5-397B-A17B-FP8 \
            --tokenizer=Qwen/Qwen3.5-397B-A17B-FP8 \
            --dataset-name=random \
            --random-input-len=8192 \
            --random-output-len=1024 \
            --random-range-ratio=1.0 \
            --num-prompts=2560 \
            --num-warmups=512 \
            --max-concurrency=256 \
            --ignore-eos
        env:
        - name: HUGGING_FACE_HUB_TOKEN
          valueFrom:
            secretKeyRef:
              key: hf_api_token
              name: hf-secret
  2. מפעילים את המניפסט של Pod להערכת ביצועים:

    envsubst '$VLLM_IMAGE' < "qwen3_5-benchmark-8k1k.yaml" | kubectl apply -f -
  3. עוקבים אחרי יומני הערכת הביצועים:

    kubectl wait --for=jsonpath='{.status.phase}'=Running pod --field-selector=metadata.name=qwen3-5-bench-8k1k --timeout=600s
    kubectl logs -f qwen3-5-bench-8k1k

    השלמת הבדיקה נמשכת כ-25 עד 30 דקות.

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

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

הסרת המשאבים

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

  1. מוחקים את הפריסה, השירות, PersistentVolumeClaim והסוד של Hugging Face בשרת vLLM TorchTPU:

    kubectl delete -f qwen3_5_server.yaml --ignore-not-found || true
    kubectl delete secret hf-secret --ignore-not-found || true
  2. מוחקים את מאגר הצמתים של TPU v6e ואת אשכול GKE:

    gcloud container node-pools delete "${NODE_POOL_NAME}" \
        --cluster="${CLUSTER_NAME}" \
        --location="${REGION}" \
        --project="${PROJECT_ID}" \
        --quiet || true
    
    gcloud container clusters delete "${CLUSTER_NAME}" \
        --location="${REGION}" \
        --project="${PROJECT_ID}" \
        --quiet || true

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