Qwen3-30B-A3B in der GKE mit Multi-Host-vLLM-TorchTPU und PD-Disaggregation auf TPU v6e-Beschleunigern bereitstellen

In dieser Anleitung wird gezeigt, wie Sie Qwen3-30B-A3B (Qwen/Qwen3-30B-A3B-FP8), ein Mixture of Experts-Modell, in der Google Kubernetes Engine (GKE) bereitstellen und bereitstellen. Für die Bereitstellung werden Multi-Host-TPU v6e (Trillium)-Beschleuniger und das vLLM TPU-Bereitstellungs-Framework mit TorchTPU (torchtpu) und Prefill-Decode-Disaggregation (PD) verwendet.

In der in dieser Anleitung beschriebenen Bereitstellungskonfiguration stellen Sie zwei ct6e-standard-4t-Knotenpools mit mehreren Hosts bereit, die jeweils in einer 2x4-Topologie angeordnet sind (acht TPU v6e-Chips auf zwei Knoten pro Knotenpool, also insgesamt 16 TPU v6e-Chips). Sie verwenden die Kubernetes JobSet API und Ray für Folgendes:

  • Führen Sie disaggregierte Prefill- und Decodierungsserver mit mehreren Hosts auf separaten TPU v6e-Slices aus, wobei Datenparallelität und Expertenparallelität aktiviert sind.
  • Anfragen über einen Disaggregations-Proxy weiterleiten.

Informationen zum Ausführen eines ähnlichen vLLM-TorchTPU-Bereitstellungs-Workflows mit mehreren Hosts auf TPU7x (Ironwood)-Beschleunigern mit dem größeren Qwen3.5-397B-Modell finden Sie unter Qwen3.5-397B in der GKE mit vLLM-TorchTPU mit mehreren Hosts auf TPU7x-Beschleunigern bereitstellen.

Ziele

  • Richten Sie Ihren GKE-Cluster und Ihre TPU v6e-Knotenpools mit mehreren Hosts ein.
  • Stellen Sie die Modellgewichtungen in einem Cloud Storage-Bucket bereit.
  • Stellen Sie die vLLM-TorchTPU-Prefill-, Decodierungs- und Proxyserver mit mehreren Hosts mit JobSet bereit.
  • Führen Sie eine Benchmark-Arbeitslast für die Bereitstellung aus und analysieren Sie die Ergebnisse.
  • bereinigen.

Kosten

In diesem Dokument verwenden Sie die folgenden kostenpflichtigen Komponenten vonGoogle Cloud:

Mit dem Preisrechner können Sie eine Kostenschätzung Ihrer voraussichtlichen Nutzung vornehmen.

Nach Abschluss der in diesem Dokument beschriebenen Aufgaben können Sie weitere Kosten vermeiden, indem Sie die erstellten Ressourcen löschen. Weitere Informationen finden Sie unter Bereinigen.

Hinweis

Richten Sie die Umgebung für die Anleitung ein:

Hugging Face-Zugriffstoken abrufen

Für diese Anleitung benötigen Sie ein Hugging Face-Zugriffstoken. Sie können sich bei Hugging Face für ein kostenloses Konto registrieren. Nachdem Sie ein Konto erstellt haben, generieren Sie ein Zugriffstoken:

  1. Klicken Sie auf der Seite Willkommen bei Hugging Face auf Ihren Konto-Avatar und wählen Sie Zugriffstokens aus.
  2. Klicken Sie auf der Seite Zugriffstokens auf Neues Token erstellen.
  3. Wählen Sie den Tokentyp Lesen aus und geben Sie einen Namen für das Token ein.
  4. Ihr Zugriffstoken wird angezeigt. Speichern Sie das Token an einem sicheren Ort.

Erforderliche Rollen

Bitten Sie Ihren Administrator, Ihnen die folgenden IAM-Rollen für Ihr Projekt zuzuweisen, um die Berechtigungen zu erhalten, die Sie zum Ausführen dieser Anleitung benötigen:

Weitere Informationen zum Zuweisen von Rollen finden Sie unter Zugriff auf Projekte, Ordner und Organisationen verwalten.

Sie können die erforderlichen Berechtigungen auch über benutzerdefinierte Rollen oder andere vordefinierte Rollen erhalten.

Umgebungsvariablen einrichten

So richten Sie Ihre Umgebungsvariablen ein:

  1. Richten Sie die Authentifizierung ein:

    gcloud auth login
    gcloud auth application-default login
  2. Exportieren Sie die Umgebungsvariablen, die Sie in dieser Anleitung verwenden:

    export PROJECT_ID="YOUR_PROJECT_ID"
    export REGION="YOUR_REGION"
    export ZONE="YOUR_ZONE"
    export CLUSTER_NAME="YOUR_CLUSTER_NAME"
    export MACHINE_TYPE="ct6e-standard-4t"
    export TOPOLOGY="2x4"
    export RESERVATION_NAME="YOUR_RESERVATION_NAME"
    export GCS_BUCKET="YOUR_GCS_BUCKET"
    export HF_TOKEN="YOUR_HF_TOKEN"
    export VLLM_IMAGE="vllm/vllm-torchtpu:v0.29.0@sha256:3976ccc1a69ffd361e0a7d2831fe317f6b685636014f426321a39ae02f8da0dc"
    
    gcloud config set project "${PROJECT_ID}"
    gcloud config set billing/quota_project "${PROJECT_ID}"

    Ersetzen Sie Folgendes:

    • YOUR_PROJECT_ID: die ID IhresGoogle Cloud -Projekts.
    • YOUR_REGION: die Region, in der Sie Ihren Cluster bereitstellen möchten, z. B. us-central1.
    • YOUR_ZONE: Die Zone, in der Sie Ihre TPU v6e-Knotenpools bereitstellen möchten, z. B. us-central1-c.
    • YOUR_CLUSTER_NAME: Name Ihres GKE-Clusters.
    • YOUR_RESERVATION_NAME: Wenn Sie eine Reservierung verwenden, der Name Ihrer TPU v6e-Reservierung.
    • YOUR_GCS_BUCKET: der Name des Cloud Storage-Bucket, den Sie zum Staging der Modellgewichtungen verwenden.
    • YOUR_HF_TOKEN: Ihr Hugging Face-Zugriffstoken.

    Wenn Sie ein benutzerdefiniertes vLLM-TorchTPU-Container-Image verwenden, ersetzen Sie den Standard-Image-URI vllm/vllm-torchtpu in der Variablen VLLM_IMAGE durch den URI Ihres Images. Öffentliche Images finden Sie im vLLM TorchTPU-Repository.

GKE-Cluster erstellen

So erstellen Sie einen GKE-Cluster im Standardmodus und hängen zwei TPU v6e-Knotenpools mit mehreren Hosts für die Disaggregation von Vorbefüllung und Decodierung an:

  1. Erstellen Sie einen regionalen GKE-Cluster mit aktivierter Workload Identity-Föderation für GKE:

    gcloud container clusters create "${CLUSTER_NAME}" \
        --project="${PROJECT_ID}" \
        --location="${REGION}" \
        --node-locations="${ZONE}" \
        --machine-type="e2-standard-4" \
        --workload-pool="${PROJECT_ID}.svc.id.goog"

    Die Ausgabe sieht etwa so aus:

    NAME     LOCATION       MASTER_VERSION      MASTER_IP      MACHINE_TYPE   NODE_VERSION        NUM_NODES  STATUS   STACK_TYPE
    asq-v6e  us-central1    1.35.7-gke.1027000  35.226.172.84  e2-standard-4  1.35.7-gke.1027000  3          RUNNING  IPV4
    
  2. Erstellen Sie zwei TPU v6e-Knotenpools mit mehreren Hosts, die jeweils zwei ct6e-standard-4t-Knoten enthalten (acht TPU v6e-Chips in einer 2x4-Topologie pro Knotenpool, insgesamt 16 TPU v6e-Chips):

    for NODE_POOL_NAME in np-0 np-1; do
      gcloud container node-pools create "${NODE_POOL_NAME}" \
          --cluster="${CLUSTER_NAME}" \
          --machine-type="${MACHINE_TYPE}" \
          --tpu-topology=2x4 \
          --location="${REGION}" \
          --node-locations="${ZONE}" \
          --project="${PROJECT_ID}" \
          --num-nodes=2 \
          --spot
    done

    In dieser Anleitung wird Spot-Kapazität (--spot) verwendet. Es kann einige Zeit dauern, bis Instanzen mit Spot-Kapazität nach dem Erstellen des Knotenpools erstellt werden. Außerdem können sie während der Ausführung von Arbeitslasten vorzeitig beendet werden. Sie können die Knotenpools auch mit einer Reservierung oder Flex-Start-VMs erstellen, indem Sie die folgenden Flags übergeben:

    • Spezifische Reservierung: Erstellen Sie den Knotenpool mit einer bestimmten Reservierung:

      for NODE_POOL_NAME in np-0 np-1; do
        gcloud container node-pools create "${NODE_POOL_NAME}" \
            --cluster="${CLUSTER_NAME}" \
            --machine-type="${MACHINE_TYPE}" \
            --placement-policy="${WORKLOAD_POLICY_NAME}" \
            --location="${REGION}" \
            --node-locations="${ZONE}" \
            --project="${PROJECT_ID}" \
            --num-nodes=2 \
            --reservation-affinity=specific \
            --reservation="${RESERVATION_NAME}"
      done

      Weitere Informationen zu TPU-Reservierungen finden Sie unter Cloud TPU-Reservierungen.

    • Flex-Start-VMs: Stellen Sie den Knotenpool mit der Verbrauchsoption Flex-Start bereit:

      for NODE_POOL_NAME in np-0 np-1; do
        gcloud container node-pools create "${NODE_POOL_NAME}" \
            --cluster="${CLUSTER_NAME}" \
            --machine-type="${MACHINE_TYPE}" \
            --placement-policy="${WORKLOAD_POLICY_NAME}" \
            --location="${REGION}" \
            --node-locations="${ZONE}" \
            --project="${PROJECT_ID}" \
            --reservation-affinity=none \
            --enable-autoscaling \
            --num-nodes=0 \
            --min-nodes=0 \
            --max-nodes=2 \
            --flex-start
      done

      Weitere Informationen finden Sie unter Kleine Batcharbeitslast mit TPUs und Flex-Start-VMs ausführen.

    Die Ausgabe für jeden Knotenpool sieht in etwa so aus:

    NAME  MACHINE_TYPE      DISK_SIZE_GB  NODE_VERSION
    np-0  ct6e-standard-4t  100           1.35.7-gke.1027000
    
  3. Konfigurieren Sie kubectl-Anmeldedaten für die Kommunikation mit Ihrem Cluster:

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

Modellgewichtungen in Cloud Storage bereitstellen

Stellen Sie die Modellgewichtungen in einem Cloud Storage-Bucket für Ihre Arbeitslastausführungen bereit:

  1. Erstellen Sie einen Cloud Storage-Bucket mit einheitlichem Zugriff auf Bucket-Ebene:

    gcloud storage buckets create "gs://${GCS_BUCKET}" \
        --project="${PROJECT_ID}" \
        --location="${REGION}" \
        --uniform-bucket-level-access
  2. Gewähren Sie dem Compute Engine-Standarddienstkonto und dem Kubernetes-Standarddienstkonto IAM-Berechtigungen (Identity and Access Management) für den Bucket:

    PROJECT_NUMBER=$(gcloud projects describe "${PROJECT_ID}" --format="value(projectNumber)")
    
    gcloud storage buckets add-iam-policy-binding "gs://${GCS_BUCKET}" \
        --member="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \
        --role="roles/storage.objectAdmin"
    
    gcloud storage buckets add-iam-policy-binding "gs://${GCS_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.admin"
  3. Erstellen Sie ein Kubernetes-Secret mit dem Namen hf-secret, um Ihr Hugging Face-Zugriffstoken zu speichern:

    kubectl create secret generic hf-secret \
        --from-literal=hf_api_token="${HF_TOKEN}" \
        --dry-run=client -o yaml | kubectl apply -f -
  4. Rufen Sie das Manifest download_qwen3_30b.yaml ab:

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

    Alternativ können Sie eine Datei mit dem Namen download_qwen3_30b.yaml und folgendem Inhalt erstellen:

    apiVersion: batch/v1
    kind: Job
    metadata:
      name: download-qwen3
      namespace: default
    spec:
      backoffLimit: 1
      template:
        spec:
          restartPolicy: OnFailure
          # Schedule onto a TPU node (requests 0 TPUs so it can run alongside or before workloads)
          nodeSelector:
            cloud.google.com/gke-tpu-accelerator: tpu-v6e-slice
          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 the staging volume so the 100GB 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 Qwen/Qwen3-30B-A3B-FP8 to Local Staging Volume ==="
              hf download Qwen/Qwen3-30B-A3B-FP8 --local-dir /local-ssd/model/Qwen/Qwen3-30B-A3B-FP8
              rm -rf /local-ssd/model/Qwen/Qwen3-30B-A3B-FP8/.cache
    
              echo "=== 3. Uploading from Local Staging Volume to GCS ==="
              gcloud config set storage/parallel_composite_upload_compatibility_check False
              gcloud storage cp -r /local-ssd/model/Qwen gs://${GCS_BUCKET}/
    
              echo "=== 4. Cleaning up Local Staging Volume ==="
              rm -rf /local-ssd/model /local-ssd/hf-cache
            resources:
              requests:
                cpu: "32"
                memory: "64Gi"
            volumeMounts:
            - name: local-ssd
              mountPath: /local-ssd
          volumes:
          - name: local-ssd
            # If the TPU node pool was created with --ephemeral-storage-local-ssd, use `emptyDir: {}`.
            # On standard ct6e-standard-4t node pools without Local SSDs attached,
            # `emptyDir: {}` uses the 100GB boot disk; use `medium: Memory` (RAM disk) instead:
            emptyDir:
              medium: Memory
              sizeLimit: 64Gi
  5. Laden Sie die Qwen/Qwen3-30B-A3B-FP8-Modellgewichtungen auf das Staging-Volume herunter und übertragen Sie sie in Ihren Cloud Storage-Bucket:

    envsubst '$GCS_BUCKET' < download_qwen3_30b.yaml | kubectl apply -f -

    Dieser Schritt dauert etwa 5 bis 10 Minuten.

  6. Warten Sie, bis der Pod gestartet wurde, und überwachen Sie die Logs des Downloadjobs:

    kubectl wait --for=condition=Ready pod -l job-name=download-qwen3 --timeout=120s
    kubectl logs -f job/download-qwen3
  7. Prüfen Sie nach Abschluss des Jobs, ob alle 16 .safetensors-Shards in Ihrem Cloud Storage-Bucket vorhanden sind:

    gcloud storage ls "gs://${GCS_BUCKET}/Qwen/Qwen3-30B-A3B-FP8/*.safetensors" | wc -l
  8. Löschen Sie den abgeschlossenen Staging-Job:

    kubectl delete job download-qwen3

Multi-Host-vLLM-TorchTPU-Server bereitstellen

Das Arbeitslastmanifest (v6e_qwen3_server_multihost_slice.yaml) definiert eine Kubernetes-JobSet-Ressource (vllm-qwen3-30b-pd-disagg-v6e) mit drei replizierten Jobs, die die disaggregierte Prefill- und Decode-Bereitstellung über die beiden 2x4-TPU-v6e-Knotenpools koordinieren:

  • Prefill-Server (prefill): Führt zwei Replikate in einem 2x4-TPU-v6e-Knotenpool aus (zwei ct6e-standard-4t-Knoten mit vier Chips pro Host, insgesamt acht TPU-Kerne). Die Pods bilden einen Ray-Cluster mit zwei Knoten und starten vLLM TorchTPU an Port 8000 für Qwen/Qwen3-30B-A3B-FP8. Der Server verwendet TPURaidenConnector, konfiguriert als kv_producer mit den folgenden Flags:

    • --data-parallel-size=8
    • --tensor-parallel-size=1
    • --enable-expert-parallel
    • --load-format=runai_streamer
  • Decodierungsserver (decode): Führt zwei Replikate im zweiten 2x4-TPU-v6e-Knotenpool aus (insgesamt acht TPU-Kerne). Das JobSet verwendet Pod-Affinitäts- und Anti-Affinitätsregeln für das Label cloud.google.com/gke-nodepool, um das Vorabfüllen und Decodieren auf separaten Knotenpools zu isolieren. Die Pods bilden einen Ray-Cluster mit zwei Knoten und starten vLLM TorchTPU auf Port 7200, wobei TPURaidenConnector als kv_consumer konfiguriert ist.

  • Disaggregations-Proxy (proxy): Führt das Skript toy_proxy_server.py auf Port 10000 aus, um das Anfrage-Routing und die KV-Cache-Übertragung zwischen dem Prefill-Server (Port 8000) und dem Decodierungsserver (Port 7200) zu koordinieren.

Führen Sie die folgenden Schritte aus, um die disaggregierten vLLM-Server bereitzustellen und zu überwachen:

  1. Installieren Sie die Kubernetes-JobSet-CustomResourceDefinition und den Controller auf Ihrem Cluster:

    kubectl apply --server-side -f https://github.com/kubernetes-sigs/jobset/releases/download/v0.12.0/manifests.yaml
    kubectl wait --for=condition=Available --timeout=300s deployment/jobset-controller-manager -n jobset-system
  2. Rufen Sie das Servermanifest ab:

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

    Alternativ können Sie eine Datei mit dem Namen v6e_qwen3_server_multihost_slice.yaml und folgendem Inhalt erstellen:

    apiVersion: jobset.x-k8s.io/v1alpha2
    kind: JobSet
    metadata:
      name: vllm-qwen3-30b-pd-disagg-v6e
    spec:
      failurePolicy:
        restartStrategy: BlockingRecreate
        maxRestarts: 5
      ttlSecondsAfterFinished: 86400
      network:
        enableDNSHostnames: true
      replicatedJobs:
      # ---------------------------------------------------------
      # PREFILL SERVERS (2 Replicas, 1 Node each = 4 chips on 2x4 v6e slice)
      # ---------------------------------------------------------
      - name: prefill
        replicas: 2
        template:
          spec:
            parallelism: 1
            completions: 1
            backoffLimit: 0
            template:
              metadata:
                labels:
                  app: vllm-tpu-prefill
              spec:
                dnsPolicy: ClusterFirstWithHostNet
                nodeSelector:
                  cloud.google.com/gke-tpu-topology: 2x4
                  cloud.google.com/gke-tpu-accelerator: tpu-v6e-slice
                tolerations:
                - key: "google.com/tpu"
                  operator: "Exists"
                  effect: "NoSchedule"
                - key: "cloud.google.com/gke-spot"
                  operator: "Equal"
                  value: "true"
                  effect: "NoSchedule"
                affinity:
                  podAntiAffinity:
                    requiredDuringSchedulingIgnoredDuringExecution:
                    - labelSelector:
                        matchExpressions:
                        - key: app
                          operator: In
                          values:
                          - vllm-tpu-decode
                      topologyKey: "cloud.google.com/gke-nodepool"
                  podAffinity:
                    requiredDuringSchedulingIgnoredDuringExecution:
                    - labelSelector:
                        matchExpressions:
                        - key: app
                          operator: In
                          values:
                          - vllm-tpu-prefill
                      topologyKey: "cloud.google.com/gke-nodepool"
                initContainers:
                - name: tpu-node-setup
                  image: busybox
                  securityContext:
                    privileged: true
                  command:
                  - /bin/sh
                  - -c
                  args:
                  - |
                    sysctl -w vm.max_map_count=8388608
                    if [ -f /sys/module/vfio_iommu_type1/parameters/dma_entry_limit ]; then
                      echo 2000000 > /sys/module/vfio_iommu_type1/parameters/dma_entry_limit
                    fi
                containers:
                - name: vllm-tpu-prefill
                  image: ${VLLM_IMAGE}
                  imagePullPolicy: IfNotPresent
                  command:
                  - /bin/bash
                  - -c
                  args:
                    - |
                      set -e
                      export TPU_WORKER_ID=$JOBSET_JOB_INDEX
                      unset TPU_VISIBLE_CHIPS
    
                      echo "=== Installing runai-model-streamer[gcs] and pre-caching metadata from GCS on $HOSTNAME ==="
                      pip install -q "runai-model-streamer[gcs]==0.16.1"
                      python3 -c "from vllm.transformers_utils.runai_utils import ObjectStorageModel; uri = 'gs://${GCS_BUCKET}/Qwen/Qwen3-30B-A3B-FP8'; m = ObjectStorageModel(uri); m.pull_files(uri, ignore_pattern=['*.pt', '*.safetensors', '*.bin', '*.tensors', '*.pth']); print('Pulled metadata/tokenizer to', m.dir)"
    
                      HEAD_ADDR="${JS_NAME}-prefill-0-0.${JS_NAME}"
    
                      if [ "$JOBSET_JOB_INDEX" = "0" ]; then
                        echo "=== Node 0: Starting Ray Head on $HOSTNAME (port 6379, IP $POD_IP) ==="
                        ray start --head --port=6379 --node-ip-address=$POD_IP --resources='{"TPU": 4}' --block &
    
                        echo "Waiting for worker node to join Ray cluster (expecting 8.0 TPU total across 2 nodes)..."
                        until ray status 2>/dev/null | grep -E -q "8(\.0)? TPU"; do
                          echo "Waiting for worker to connect to Ray Head..."
                          sleep 2
                        done
                        echo "Worker joined! Ray cluster status:"
                        ray status
    
                        echo "=== Starting vLLM Prefill Server (Qwen3 30B FP8, DP=8, TP=1, Port 8000) ==="
                        PREFILL_ARGS=(
                          gs://${GCS_BUCKET}/Qwen/Qwen3-30B-A3B-FP8
                          --served-model-name=Qwen/Qwen3-30B-A3B-FP8
                          --api-server-count=1
                          --load-format=runai_streamer
                          --enable-ep-weight-filter
                          --tensor-parallel-size=1
                          --data-parallel-size=8
                          --data-parallel-size-local=4
                          --data-parallel-backend=ray
                          --data-parallel-address=$POD_IP
                          --distributed-executor-backend=ray
                          --enable-expert-parallel
                          --max-model-len=18432
                          --max-num-batched-tokens=8192
                          --max-num-seqs=64
                          --num-gpu-blocks-override=128
                          --host=0.0.0.0
                          --port=8000
                          --async-scheduling
                          --no-enable-prefix-caching
                          --kv-cache-dtype=fp8
                          --attention-backend=CUSTOM
                          --block-size=1024
                          --return-tokens-as-token-ids
                          --hf-overrides '{"linear_num_key_heads":1,"linear_num_value_heads":1}'
                          --compilation-config '{"backend":"vllm_torchtpu.compilation.tpu_compiler.TpuCompilerAdaptor","compile_sizes":[4096,8192],"inductor_compile_config":{"enable_auto_functionalized_v2":false,"size_asserts":false,"alignment_asserts":false,"scalar_asserts":false}}'
                          --kv-transfer-config '{"kv_connector":"TPURaidenConnector","kv_connector_module_path":"vllm_torchtpu.distributed.kv_transfer.tpu_connector","kv_role":"kv_producer","kv_port":14579}'
                        )
                        exec vllm serve "${PREFILL_ARGS[@]}"
                      else
                        echo "=== Node 1: Starting Ray Worker on $HOSTNAME (IP $POD_IP) ==="
                        until timeout 1 bash -c "cat < /dev/null > /dev/tcp/${HEAD_ADDR}/6379" 2>/dev/null; do
                          echo "Waiting for Ray head at ${HEAD_ADDR}:6379..."
                          sleep 2
                        done
                        echo "Ray head is reachable! Starting Ray worker..."
                        sleep 3
                        exec ray start --address="${HEAD_ADDR}:6379" --node-ip-address=$POD_IP --resources='{"TPU": 4}' --block
                      fi
    
                  env:
                  - name: JS_NAME
                    valueFrom:
                      fieldRef:
                        fieldPath: metadata.labels['jobset.sigs.k8s.io/jobset-name']
                  - name: JOBSET_JOB_INDEX
                    valueFrom:
                      fieldRef:
                        fieldPath: metadata.labels['jobset.sigs.k8s.io/job-index']
                  - name: POD_IP
                    valueFrom:
                      fieldRef:
                        fieldPath: status.podIP
                  - name: TPU_MULTIHOST_BACKEND
                    value: "ray"
                  - name: RAY_EXPERIMENTAL_NOSET_TPU_VISIBLE_CHIPS
                    value: "1"
                  - name: RAY_ADDRESS
                    value: "127.0.0.1:6379"
                  - name: VLLM_ENGINE_READY_TIMEOUT_S
                    value: "14400"
                  - name: SKIP_MEGASCALE_PJRT_CLIENT
                    value: "true"
                  - name: USE_MOE_FUSED_EP_KERNEL
                    value: "1"
                  - name: RAGGED_GATHER_REDUCE_VERSION
                    value: "v3"
                  - name: LIBTPU_INIT_ARGS
                    value: "--xla_tpu_use_minor_sharding_for_major_trivial_input=true --xla_tpu_enable_sparse_core_collective_offload_reduce_scatter=false --xla_tpu_ars_combiner_threshold_in_bytes=0 --xla_tpu_enable_async_collective_merger=false --xla_tpu_enable_diagnostic_coordinator=false --xla_enable_async_all_gather=true --xla_tpu_enable_sparse_core_collective_offload_all_gather=true --xla_tpu_sparse_core_all_gather_offload_min_size_in_bytes=262144"
                  # Raiden KV-transfer configuration (Prefill / Producer)
                  - name: TPU_KV_RESHARD_TRANSPORT
                    value: "raiden"
                  - name: TPU_RAIDEN_RESHARD_IMPL
                    value: "store"
                  - name: TPU_RAIDEN_ADVERTISE_HOST
                    valueFrom:
                      fieldRef:
                        fieldPath: status.podIP
                  - name: TPU_USE_RAIDEN_KV_CACHE_MANAGER
                    value: "1"
                  - name: TPU_RAIDEN_QWEN35_ADMISSION
                    value: "1"
                  - name: TPU_RAIDEN_TRANSFER_PARALLELISM
                    value: "1"
                  - name: TPU_VLLM_ENABLE_UNIFIED_BLOCK_POOL
                    value: "1"
                  volumeMounts:
                  - name: dshm
                    mountPath: /dev/shm
                  securityContext:
                    privileged: true
                    capabilities:
                      add:
                      - IPC_LOCK
                  ports:
                  - name: http
                    containerPort: 8000
                  - name: ray-gcs
                    containerPort: 6379
                  resources:
                    limits:
                      google.com/tpu: "4"
                    requests:
                      google.com/tpu: "4"
                volumes:
                - name: dshm
                  emptyDir:
                    medium: Memory
    
      # ---------------------------------------------------------
      # DECODE SERVERS (2 Replicas, 1 Node each = 4 chips on 2x4 v6e slice)
      # ---------------------------------------------------------
      - name: decode
        replicas: 2
        template:
          spec:
            parallelism: 1
            completions: 1
            backoffLimit: 0
            template:
              metadata:
                labels:
                  app: vllm-tpu-decode
              spec:
                dnsPolicy: ClusterFirstWithHostNet
                nodeSelector:
                  cloud.google.com/gke-tpu-topology: 2x4
                  cloud.google.com/gke-tpu-accelerator: tpu-v6e-slice
                tolerations:
                - key: "google.com/tpu"
                  operator: "Exists"
                  effect: "NoSchedule"
                - key: "cloud.google.com/gke-spot"
                  operator: "Equal"
                  value: "true"
                  effect: "NoSchedule"
                affinity:
                  podAntiAffinity:
                    requiredDuringSchedulingIgnoredDuringExecution:
                    - labelSelector:
                        matchExpressions:
                        - key: app
                          operator: In
                          values:
                          - vllm-tpu-prefill
                      topologyKey: "cloud.google.com/gke-nodepool"
                  podAffinity:
                    requiredDuringSchedulingIgnoredDuringExecution:
                    - labelSelector:
                        matchExpressions:
                        - key: app
                          operator: In
                          values:
                          - vllm-tpu-decode
                      topologyKey: "cloud.google.com/gke-nodepool"
                initContainers:
                - name: tpu-node-setup
                  image: busybox
                  securityContext:
                    privileged: true
                  command:
                  - /bin/sh
                  - -c
                  args:
                  - |
                    sysctl -w vm.max_map_count=8388608
                    if [ -f /sys/module/vfio_iommu_type1/parameters/dma_entry_limit ]; then
                      echo 2000000 > /sys/module/vfio_iommu_type1/parameters/dma_entry_limit
                    fi
                containers:
                - name: vllm-tpu-decode
                  image: ${VLLM_IMAGE}
                  imagePullPolicy: IfNotPresent
                  command:
                  - /bin/bash
                  - -c
                  args:
                    - |
                      set -e
                      export TPU_WORKER_ID=$JOBSET_JOB_INDEX
                      unset TPU_VISIBLE_CHIPS
    
                      echo "=== Installing runai-model-streamer[gcs] and pre-caching metadata from GCS on $HOSTNAME ==="
                      pip install -q "runai-model-streamer[gcs]==0.16.1"
                      python3 -c "from vllm.transformers_utils.runai_utils import ObjectStorageModel; uri = 'gs://${GCS_BUCKET}/Qwen/Qwen3-30B-A3B-FP8'; m = ObjectStorageModel(uri); m.pull_files(uri, ignore_pattern=['*.pt', '*.safetensors', '*.bin', '*.tensors', '*.pth']); print('Pulled metadata/tokenizer to', m.dir)"
    
                      HEAD_ADDR="${JS_NAME}-decode-0-0.${JS_NAME}"
    
                      if [ "$JOBSET_JOB_INDEX" = "0" ]; then
                        echo "=== Node 0: Starting Ray Head on $HOSTNAME (port 6379, IP $POD_IP) ==="
                        ray start --head --port=6379 --node-ip-address=$POD_IP --resources='{"TPU": 4}' --block &
    
                        echo "Waiting for worker node to join Ray cluster (expecting 8.0 TPU total across 2 nodes)..."
                        until ray status 2>/dev/null | grep -E -q "8(\.0)? TPU"; do
                          echo "Waiting for worker to connect to Ray Head..."
                          sleep 2
                        done
                        echo "Worker joined! Ray cluster status:"
                        ray status
    
                        echo "=== Starting vLLM Decode Server (Qwen3 30B FP8, DP=8, TP=1, Port 7200) ==="
                        DECODE_ARGS=(
                          gs://${GCS_BUCKET}/Qwen/Qwen3-30B-A3B-FP8
                          --served-model-name=Qwen/Qwen3-30B-A3B-FP8
                          --api-server-count=1
                          --load-format=runai_streamer
                          --enable-ep-weight-filter
                          --tensor-parallel-size=1
                          --data-parallel-size=8
                          --data-parallel-size-local=4
                          --data-parallel-backend=ray
                          --data-parallel-address=$POD_IP
                          --distributed-executor-backend=ray
                          --enable-expert-parallel
                          --max-model-len=18432
                          --max-num-batched-tokens=2048
                          --max-num-seqs=64
                          --num-gpu-blocks-override=32
                          --host=0.0.0.0
                          --port=7200
                          --async-scheduling
                          --no-enable-prefix-caching
                          --kv-cache-dtype=fp8
                          --attention-backend=CUSTOM
                          --block-size=8192
                          --hf-overrides '{"linear_num_key_heads":1,"linear_num_value_heads":1}'
                          --compilation-config '{"backend":"vllm_torchtpu.compilation.tpu_compiler.TpuCompilerAdaptor","compile_sizes":[8,16,32,64,2048],"inductor_compile_config":{"enable_auto_functionalized_v2":false,"size_asserts":false,"alignment_asserts":false,"scalar_asserts":false}}'
                          --kv-transfer-config '{"kv_connector":"TPURaidenConnector","kv_connector_module_path":"vllm_torchtpu.distributed.kv_transfer.tpu_connector","kv_role":"kv_consumer","kv_port":14579}'
                        )
                        exec vllm serve "${DECODE_ARGS[@]}"
                      else
                        echo "=== Node 1: Starting Ray Worker on $HOSTNAME (IP $POD_IP) ==="
                        until timeout 1 bash -c "cat < /dev/null > /dev/tcp/${HEAD_ADDR}/6379" 2>/dev/null; do
                          echo "Waiting for Ray head at ${HEAD_ADDR}:6379..."
                          sleep 2
                        done
                        echo "Ray head is reachable! Starting Ray worker..."
                        sleep 3
                        exec ray start --address="${HEAD_ADDR}:6379" --node-ip-address=$POD_IP --resources='{"TPU": 4}' --block
                      fi
    
                  env:
                  - name: JS_NAME
                    valueFrom:
                      fieldRef:
                        fieldPath: metadata.labels['jobset.sigs.k8s.io/jobset-name']
                  - name: JOBSET_JOB_INDEX
                    valueFrom:
                      fieldRef:
                        fieldPath: metadata.labels['jobset.sigs.k8s.io/job-index']
                  - name: POD_IP
                    valueFrom:
                      fieldRef:
                        fieldPath: status.podIP
                  - name: TPU_MULTIHOST_BACKEND
                    value: "ray"
                  - name: RAY_EXPERIMENTAL_NOSET_TPU_VISIBLE_CHIPS
                    value: "1"
                  - name: RAY_ADDRESS
                    value: "127.0.0.1:6379"
                  - name: VLLM_ENGINE_READY_TIMEOUT_S
                    value: "14400"
                  - name: SKIP_MEGASCALE_PJRT_CLIENT
                    value: "true"
                  - name: USE_MOE_FUSED_EP_KERNEL
                    value: "1"
                  - name: RAGGED_GATHER_REDUCE_VERSION
                    value: "v3"
                  - name: USE_BATCHED_RPA_LONGCTX
                    value: "1"
                  - name: LIBTPU_INIT_ARGS
                    value: "--xla_tpu_use_minor_sharding_for_major_trivial_input=true --xla_tpu_enable_sparse_core_collective_offload_reduce_scatter=false --xla_tpu_ars_combiner_threshold_in_bytes=0 --xla_tpu_enable_async_collective_merger=false --xla_tpu_enable_diagnostic_coordinator=false --xla_enable_async_all_gather=true --xla_tpu_enable_sparse_core_collective_offload_all_gather=true --xla_tpu_sparse_core_all_gather_offload_min_size_in_bytes=262144"
                  # Raiden KV-transfer configuration (Decode / Consumer)
                  - name: TPU_KV_RESHARD_TRANSPORT
                    value: "raiden"
                  - name: TPU_RAIDEN_RESHARD_IMPL
                    value: "store"
                  - name: TPU_RAIDEN_ADVERTISE_HOST
                    valueFrom:
                      fieldRef:
                        fieldPath: status.podIP
                  - name: TPU_USE_RAIDEN_KV_CACHE_MANAGER
                    value: "1"
                  - name: TPU_RAIDEN_QWEN35_ADMISSION
                    value: "1"
                  - name: TPU_RAIDEN_TRANSFER_PARALLELISM
                    value: "1"
                  - name: TPU_VLLM_ENABLE_UNIFIED_BLOCK_POOL
                    value: "1"
                  volumeMounts:
                  - name: dshm
                    mountPath: /dev/shm
                  securityContext:
                    privileged: true
                    capabilities:
                      add:
                      - IPC_LOCK
                  ports:
                  - name: http
                    containerPort: 7200
                  - name: ray-gcs
                    containerPort: 6379
                  resources:
                    limits:
                      google.com/tpu: "4"
                    requests:
                      google.com/tpu: "4"
                volumes:
                - name: dshm
                  emptyDir:
                    medium: Memory
    
      # ---------------------------------------------------------
      # DISAGGREGATION PROXY (Coordinates Prefill -> Decode)
      # ---------------------------------------------------------
      - name: proxy
        replicas: 1
        template:
          spec:
            parallelism: 1
            completions: 1
            backoffLimit: 2
            template:
              spec:
                containers:
                - name: vllm-tpu-proxy
                  image: ${VLLM_IMAGE}
                  imagePullPolicy: IfNotPresent
                  command:
                  - /bin/bash
                  - -c
                  args:
                  - |
                    set -e
                    exec python3 /root/torchtpu-vllm/examples/disagg/toy_proxy_server.py \
                      --host 0.0.0.0 \
                      --port 10000 \
                      --prefiller-host "${JS_NAME}-prefill-0-0.${JS_NAME}" \
                      --prefiller-port 8000 \
                      --decoder-host "${JS_NAME}-decode-0-0.${JS_NAME}" \
                      --decoder-port 7200
                  env:
                  - name: JS_NAME
                    valueFrom:
                      fieldRef:
                        fieldPath: metadata.labels['jobset.sigs.k8s.io/jobset-name']
                  ports:
                  - name: http
                    containerPort: 10000
                  resources:
                    requests: { cpu: "2", memory: "4Gi" }
                    limits: { cpu: "8", memory: "16Gi" }
  3. Ersetzen Sie den Namen Ihres Cloud Storage-Bucket und das Container-Image in der Datei v6e_qwen3_server_multihost_slice.yaml und wenden Sie das Manifest an:

    envsubst '${GCS_BUCKET} ${VLLM_IMAGE}' < "v6e_qwen3_server_multihost_slice.yaml" | kubectl apply -f -

    In dieser Konfiguration wird ein 2x4-TPU-v6e-Slice (zwei Knoten) für die Vorbefüllung und ein 2x4-TPU-v6e-Slice (zwei Knoten) für die Decodierung verwendet. Wenn Sie die Anzahl der Knoten in einem der Pools skalieren möchten, aktualisieren Sie das Feld replicas (replicas: NUM_NODES). Ersetzen Sie NUM_NODES durch die Anzahl der Knoten, die Sie verwenden möchten.

  4. Beobachten Sie die Pod-Logs für das Vorabfüllen und Decodieren, bis der Anwendungsstart abgeschlossen ist:

    # Prefill logs
    kubectl logs -l jobset.sigs.k8s.io/replicatedjob-name=prefill -f --tail=-1
    
    # Decode logs
    kubectl logs -l jobset.sigs.k8s.io/replicatedjob-name=decode -f --tail=-1

    Das erste Laden und Kompilieren des Modells dauert beim ersten Durchlauf etwa 15 bis 25 Minuten.

    Wenn Sie Kapazität auf Abruf wie Spot-Kapazität oder Flex-Start-VMs verwenden, kann es nach dem Erstellen des Knotenpools länger dauern, bis Ihre TPU-Slice-Knoten erstellt werden. Außerdem können sie während der Ausführung von Arbeitslasten vorzeitig beendet werden. Weitere Informationen finden Sie unter VM-Instanzen auf Abruf. Informationen zum Prüfen, ob Ihre TPU-Slice-Knoten erstellt wurden, finden Sie unter Status und Bedingungen des Knotens prüfen.

    Warten Sie, bis in den Server-Logs für die Vorbefüllung und die Decodierung Application startup complete angezeigt wird, bevor Sie die Benchmark-Arbeitslast ausführen:

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

Benchmark-Arbeitslast ausführen

Führen Sie die folgenden Schritte aus, um das bereitgestellte Qwen3-30B-A3B-Modell mit einer Arbeitslast von 8.192 Token Eingabe und 1.024 Token Ausgabe (8k/1k) mit dem Disaggregations-Proxy zu vergleichen:

  1. Rufen Sie das Benchmark-Manifest ab:

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

    Alternativ können Sie eine Datei mit dem Namen v6e_qwen3_benchmark_multihost_slice.yaml und folgendem Inhalt erstellen:

    apiVersion: batch/v1
    kind: Job
    metadata:
      name: vllm-qwen3-30b-pd-disagg-v6e-tester
    spec:
      parallelism: 1
      completions: 1
      backoffLimit: 0
      template:
        spec:
          restartPolicy: Never
          containers:
          - name: vllm-test
            image: ${VLLM_IMAGE}
            imagePullPolicy: IfNotPresent
            resources:
              requests: { cpu: "1", memory: "4Gi" }
              limits: { cpu: "4", memory: "8Gi" }
            command:
            - /bin/bash
            - -c
            args:
            - |
              set -e
              PREFILLER_DNS="${JS_NAME}-prefill-0-0.${JS_NAME}"
              DECODER_DNS="${JS_NAME}-decode-0-0.${JS_NAME}"
              PROXY_DNS="${JS_NAME}-proxy-0-0.${JS_NAME}"
              MODEL="Qwen/Qwen3-30B-A3B-FP8"
    
              echo 'import torch; torch.utils.rename_privateuse1_backend("tpu")' > /tmp/sitecustomize.py
              export PYTHONPATH="/tmp:${PYTHONPATH:-}"
    
              wait_for() {
                local name="$1" url="$2" timeout=18000 elapsed=0
                echo "Waiting for $name ($url)..."
                until curl -sf "$url" > /dev/null; do
                  [ "$elapsed" -ge "$timeout" ] && { echo "ERROR: $name timeout" >&2; exit 1; }
                  sleep 10; elapsed=$((elapsed + 10))
                done
                echo "$name is up after ${elapsed}s!"
              }
              wait_for prefill "http://$PREFILLER_DNS:8000/health"
              wait_for decode  "http://$DECODER_DNS:7200/health"
              wait_for proxy   "http://$PROXY_DNS:10000/healthcheck"
    
              echo "=== BENCHMARK: 8K/1K c=128 n=1280 ==="
              BENCH_ARGS=(
                --backend openai-chat
                --base-url "http://$PROXY_DNS:10000" --endpoint /v1/chat/completions
                --model "$MODEL" --dataset-name random
                --random-input-len 8192 --random-output-len 1024
                --ignore-eos
                --max-concurrency 128 --num-prompts 1280
              )
              if vllm bench serve "${BENCH_ARGS[@]}"; then
                echo "=== BENCHMARK COMPLETED SUCCESSFULLY ==="
              else
                echo "ERROR: BENCHMARK FAILED"
                exit 1
              fi
            env:
            - name: JS_NAME
              value: "vllm-qwen3-30b-pd-disagg-v6e"
  2. Wenden Sie das Benchmark-Jobmanifest an:

    envsubst '${VLLM_IMAGE}' < v6e_qwen3_benchmark_multihost_slice.yaml | kubectl apply -f -
  3. Warten Sie, bis der Benchmark-Pod bereit ist, und überwachen Sie die Benchmark-Joblogs:

    kubectl wait --for=condition=Ready pod -l job-name=vllm-qwen3-30b-pd-disagg-v6e-tester --timeout=300s
    kubectl logs -f job/vllm-qwen3-30b-pd-disagg-v6e-tester
  4. Suchen Sie nach Abschluss des Benchmarks nach einer Ausgabe, die Messwerte wie die folgenden enthält:

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

Bereinigen

Damit Ihrem Google Cloud -Konto die in dieser Anleitung verwendeten Ressourcen nicht in Rechnung gestellt werden, löschen Sie die Kubernetes-Arbeitslasten, die TPU v6e-Knotenpools, den GKE-Cluster und Ihren Cloud Storage-Bucket:

  1. Löschen Sie die Kubernetes-Arbeitslasten:

    kubectl delete -f v6e_qwen3_server_multihost_slice.yaml --ignore-not-found || true
    kubectl delete secret hf-secret --ignore-not-found || true
  2. Löschen Sie die TPU v6e-Knotenpools und den GKE-Cluster:

    for NODE_POOL_NAME in np-0 np-1; do
      gcloud container node-pools delete "${NODE_POOL_NAME}" \
          --cluster="${CLUSTER_NAME}" \
          --location="${REGION}" \
          --project="${PROJECT_ID}" \
          --quiet || true
    done
    
    gcloud container clusters delete "${CLUSTER_NAME}" \
        --location="${REGION}" \
        --project="${PROJECT_ID}" \
        --quiet || true
  3. Löschen Sie den Cloud Storage-Bucket, in dem die bereitgestellten Modellgewichtungen gespeichert sind. Wenn Sie die Modellgewichtungen beibehalten möchten, überspringen Sie diesen Schritt oder kopieren Sie sie an den gewünschten Speicherort:

    gcloud storage rm --recursive "gs://${GCS_BUCKET}" --project="${PROJECT_ID}" --quiet || true

Nächste Schritte