Diffuser Qwen3-Coder-30B-A3B sur GKE avec SGLang TorchTPU multi-hôte et la désagrégation des disques persistants sur des accélérateurs TPU v6e

Ce tutoriel vous explique comment déployer et diffuser Qwen3-Coder-30B-A3B (Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8), un modèle de codage Mixture of Experts, sur Google Kubernetes Engine (GKE). Le déploiement utilise des accélérateurs TPU v6e (Trillium) multi-hôtes et le framework de diffusion SGLang avec TorchTPU (torchtpu) et la désagrégation préremplissage-décodage (PD).

Dans la configuration de déploiement décrite dans ce tutoriel, vous provisionnez deux pools de nœuds ct6e-standard-4t multi-hôtes, chacun organisé selon une topologie 2x4 (huit puces TPU v6e sur deux nœuds par pool de nœuds, pour un total de 16 puces TPU v6e). Vous utilisez l'API Kubernetes LeaderWorkerSet pour effectuer les opérations suivantes :

  • Exécutez des serveurs de préremplissage et de décodage multi-hôtes désagrégés sur des tranches TPU v6e distinctes avec le parallélisme Tensor et le parallélisme expert activés.
  • Acheminez les requêtes via un routeur de désagrégation SGLang.

Pour exécuter un workflow de diffusion SGLang TorchTPU multi-hôte similaire sur des accélérateurs TPU7x (Ironwood) avec le modèle Qwen3-Coder-480B plus volumineux, consultez Diffuser Qwen3-Coder-480B sur GKE avec SGLang TorchTPU multi-hôte et désagrégation de PD sur des accélérateurs TPU7x.

Objectifs

  • Configurez votre cluster GKE et vos pools de nœuds TPU v6e multi-hôtes.
  • Configurez les identifiants et installez le contrôleur LeaderWorkerSet.
  • Déployez les charges de travail de préremplissage, de décodage et de routeur SGLang multihôtes.
  • Envoyez une requête d'inférence au service de routeur SGLang.
  • Exécutez une charge de travail de benchmark de diffusion.
  • Effectuer un nettoyage.

Coûts

Dans ce document, vous utilisez les composants facturables suivants deGoogle Cloud :

Obtenez une estimation des coûts en fonction de votre utilisation prévue à l'aide du simulateur de coût.

Une fois que vous avez terminé les tâches décrites dans ce document, vous pouvez éviter de continuer à payer des frais en supprimant les ressources que vous avez créées. Pour en savoir plus, consultez la section Effectuer un nettoyage.

Avant de commencer

Configurez votre environnement pour le tutoriel :

  • Installez et initialisez la Google Cloud CLI, puis installez kubectl. Si vous utilisez Cloud Shell, les deux outils sont préinstallés.

  • Activez l'API Kubernetes Engine, l'API Compute Engine, l'API Cloud TPU et l'API Artifact Registry dans votre projet Google Cloud  :

    Activer les API

  • Assurez-vous que votre projet dispose d'un quota de disque et d'un quota d'adresses IP externes suffisants dans la région où se trouve votre capacité TPU :

    1. Dans la console Google Cloud , accédez à la page Quotas et limites du système.

      Accéder à la page Quotas et limites du système

    2. Vérifiez que vous disposez d'au moins 400 Gio de quota Google Cloud Hyperdisk Balanced disponible sur les quatre volumes éphémères de 100 Gio utilisés pour la mise en cache du modèle par les pods de préremplissage et de décodage.

    3. Filtrez par Adresses IPv4 externes régionales en cours d'utilisation et vérifiez que vous disposez d'un quota d'adresses IP suffisant dans votre région cible.

Obtenir un jeton d'accès Hugging Face

Vous avez besoin d'un jeton d'accès Hugging Face pour suivre ce tutoriel. Vous pouvez créer un compte sans frais sur Hugging Face. Une fois que vous disposez d'un compte, générez un jeton d'accès :

  1. Sur la page Welcome to Hugging Face (Bienvenue sur Hugging Face), cliquez sur l'avatar de votre compte, puis sélectionnez Access tokens (Jetons d'accès).
  2. Sur la page Access tokens (Jetons d'accès), cliquez sur Create new token (Créer un jeton).
  3. Sélectionnez le type de jeton Read (Lecture) et saisissez un nom pour votre jeton.
  4. Votre jeton d'accès s'affiche. Enregistrez le jeton dans un endroit sûr.

Rôles requis

Pour obtenir les autorisations nécessaires pour suivre ce tutoriel, demandez à votre administrateur de vous accorder les rôles IAM suivants sur votre projet :

Pour en savoir plus sur l'attribution de rôles, consultez la page Gérer l'accès aux projets, aux dossiers et aux organisations.

Vous pouvez également obtenir les autorisations requises avec des rôles personnalisés ou d'autres rôles prédéfinis.

Assurez-vous que votre compte de service Compute Engine par défaut dispose des rôles IAM (Identity and Access Management) suivants sur votre projet :

Configurer vos variables d'environnement

Exportez les variables d'environnement que vous utiliserez tout au long de ce tutoriel :

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"

Remplacez les éléments suivants :

  • YOUR_CLUSTER_NAME : nom de votre cluster GKE.
  • YOUR_PROJECT_ID : ID de votre projet Google Cloud.
  • YOUR_REGION : région dans laquelle vous souhaitez déployer votre cluster, par exemple us-central1.
  • YOUR_ZONE : zone dans laquelle vous souhaitez déployer vos pools de nœuds TPU v6e, par exemple us-central1-b.
  • YOUR_RESERVATION_NAME : si vous utilisez une réservation, nom de votre réservation de TPU v6e.
  • YOUR_HF_TOKEN : votre jeton d'accès Hugging Face.

Si vous utilisez une image de conteneur SGLang TorchTPU personnalisée, remplacez lmsysorg/sglang:v0.1.0-tpu dans la variable SGLANG_IMAGE par l'URI de votre image.

Créer le cluster GKE

Pour créer un cluster GKE en mode Standard et associer deux pools de nœuds TPU v6e multi-hôtes pour la désagrégation du préremplissage et du décodage, procédez comme suit :

  1. Créez un cluster GKE régional qui utilise des nœuds e2-standard-4 pour le pool de nœuds par défaut, qui fournit suffisamment de ressources de processeur et de mémoire pour le contrôleur LeaderWorkerSet et le routeur de désagrégation SGLang :

    gcloud container clusters create "${CLUSTER_NAME}" \
      --project="${PROJECT_ID}" \
      --location="${REGION}" \
      --machine-type=e2-standard-4
  2. Créez deux pools de nœuds TPU v6e multi-hôtes, chacun contenant deux nœuds ct6e-standard-4t (huit puces TPU v6e dans une topologie 2x4 par pool de nœuds, pour un total de 16 puces 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}"

    Au lieu d'utiliser une réservation spécifique, vous pouvez également créer les pools de nœuds à l'aide de la capacité Spot ou de VM à démarrage flexible :

    VM Spot

    Provisionnez les pools de nœuds à l'aide de la capacité Spot en remplaçant les options --reservation-affinity=specific et --reservation="${RESERVATION}" par l'option --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

    La création d'instances utilisant la capacité Spot peut prendre du temps après la création du pool de nœuds, et elles peuvent être préemptées pendant l'exécution de la charge de travail.

    VM à démarrage flexible

    Provisionnez les pools de nœuds à l'aide de l'option de consommation Démarrage flexible :

    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

    Pour en savoir plus, consultez Exécuter une petite charge de travail par lot avec des TPU et des VM à démarrage flexible.

  3. Configurez les identifiants kubectl pour communiquer avec votre cluster :

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

Configurer les identifiants et installer LeaderWorkerSet

Pour préparer votre cluster au déploiement SGLang multi-hôte, procédez comme suit :

  1. Créez un secret Kubernetes nommé hf-secret pour stocker votre jeton d'accès Hugging Face :

    kubectl create secret generic hf-secret \
        --from-literal=hf_api_token="${HF_TOKEN}"
  2. Installez la ressource CustomResourceDefinition et le contrôleur LeaderWorkerSet sur votre cluster :

    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

Déployer le serveur SGLang multi-hôte

Le fichier manifeste de la charge de travail (qwen3-coder-30b-multihost-disagg-v6e-2x4.yaml) définit les ressources suivantes pour coordonner la mise en service désagrégée du préremplissage et du décodage sur les deux pools de nœuds TPU v6e 2x4 :

  • StorageClass (hyperdisk-balanced-tpu) : provisionne des volumes ReadWriteOnce éphémères de 100 Gio, basés sur le stockage Google Cloud Hyperdisk Balanced. Chacun des quatre pods de préremplissage et de décodage installe son propre volume sur /data pour stocker localement les points de contrôle Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8 téléchargés.

  • Pool de préremplissage (sglang-prefill-service et sglang-tpu-prefill) : exécute un LeaderWorkerSet, qui contient un pod leader et un pod de nœud de calcul, épinglés exclusivement à un pool de nœuds TPU v6e 2x4. Ce pool de nœuds comporte huit puces TPU, qui fournissent huit TensorCores. LeaderWorkerSet utilise l'annotation leaderworkerset.sigs.k8s.io/exclusive-topology définie sur "cloud.google.com/gke-nodepool". Les pods lancent sglang.launch_server avec les options suivantes, en exposant le port 8000 et le port d'amorçage 8998 :

    • --disaggregation-mode=prefill
    • --tensor-parallel-size=8
    • --ep-size=8
  • Pool de décodage (sglang-decode-service et sglang-tpu-decode) : exécute un LeaderWorkerSet de size: 2 épinglé au deuxième pool de nœuds TPU v6e 2x4 (huit puces TPU, qui fournissent huit TensorCores). LeaderWorkerSet lance sglang.launch_server avec les options suivantes :

    • --disaggregation-mode=decode
    • --tensor-parallel-size=8
    • --ep-size=8
  • Routeur de désagrégation (sglang-router et sglang-service) : exécute un déploiement de processeur qui exécute le module sglang_router.launch_router avec l'option --pd-disaggregation et expose un service ClusterIP sur le port 8000. Le routeur dirige les requêtes et coordonne le transfert du cache KV entre les services sglang-prefill-service et sglang-decode-service.

Pour déployer et surveiller les serveurs SGLang multi-hôtes, procédez comme suit :

  1. Téléchargez le fichier manifeste de la charge de travail :

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

    Vous pouvez également créer un fichier nommé qwen3-coder-30b-multihost-disagg-v6e-2x4.yaml avec le contenu suivant :

    # ==============================================================================
    # 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. Appliquez le fichier manifeste de la charge de travail à votre cluster :

    envsubst '$SGLANG_IMAGE' < qwen3-coder-30b-multihost-disagg-v6e-2x4.yaml | kubectl apply -f -
  3. Vérifiez que les pods de routeur, de préremplissage et de décodage ont été créés :

    kubectl get pods

    Lorsque tous les pods sont prêts, la sortie ressemble à ce qui suit :

    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. Surveillez les journaux du pod leader de préremplissage et de décodage jusqu'à la fin du démarrage :

    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

    Le téléchargement initial du modèle, le chargement des pondérations et la compilation prennent environ sept à dix minutes lors de la première exécution.

    Si vous utilisez une capacité préemptive telle que la capacité Spot ou des VM Flex-start, la création de vos nœuds de tranche TPU peut prendre plus de temps après la création du pool de nœuds, et ils peuvent être préemptés pendant l'exécution des charges de travail. Pour en savoir plus, consultez Instances de VM préemptives. Pour vérifier si vos nœuds de tranche TPU ont été créés, consultez Vérifier l'état et les conditions du nœud.

    Attendez que les journaux des pods leaders de préremplissage et de décodage affichent le message suivant et que kubectl get pods affiche 1/1 dans la colonne READY pour tous les pods :

    The server is fired up and ready to roll!
    
  5. Ouvrez une deuxième session de terminal et transférez le port local 8000 vers sglang-service :

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

    Laissez le transfert de port s'exécuter dans cette session de terminal.

Envoyer des requêtes d'inférence

Revenez à votre première session de terminal et envoyez une requête de complétion de chat au point de terminaison /v1/chat/completions exposé par le routeur 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
  }'

Exécuter une charge de travail de benchmark

Pour évaluer le modèle Qwen3-Coder-30B-A3B déployé à l'aide d'une charge de travail d'entrée de 1 024 jetons et de sortie de 1 024 jetons (1k/1k) à partir du pod sglang-router, exécutez la commande suivante :

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

Une fois le benchmark terminé, recherchez un résultat contenant des métriques telles que les suivantes :

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

Effectuer un nettoyage

Pour éviter que des frais supplémentaires ne soient facturés sur votre compte Google Cloud pour les ressources utilisées dans ce tutoriel, supprimez les ressources suivantes :

  1. Supprimez les charges de travail de préremplissage, de décodage et de routeur SGLang :

    kubectl delete -f qwen3-coder-30b-multihost-disagg-v6e-2x4.yaml --ignore-not-found
  2. Supprimez les pools de nœuds TPU v6e et le cluster 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

Étapes suivantes