Entrega Qwen3-Coder-30B-A3B en GKE con SGLang TorchTPU de varios hosts y desagregación de PD en aceleradores TPU v6e

En este instructivo, se muestra cómo implementar y entregar Qwen3-Coder-30B-A3B (Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8), una combinación de modelos de codificación de expertos, en Google Kubernetes Engine (GKE). La implementación usa aceleradores TPU v6e (Trillium) de varios hosts y el framework de entrega de SGLang con TorchTPU (torchtpu) y desagregación de decodificación de relleno previo (PD).

En la configuración de implementación que se describe en este instructivo, aprovisionas dos grupos de nodos ct6e-standard-4t de varios hosts, cada uno organizado en una topología 2x4 (ocho chips TPU v6e en dos nodos por grupo de nodos, para un total de 16 chips TPU v6e). Usa la API de LeaderWorkerSet de Kubernetes para hacer lo siguiente:

  • Ejecuta servidores de decodificación y precompletado de varios hosts desagregados en porciones de TPU v6e independientes con el paralelismo tensorial y el paralelismo de expertos habilitados.
  • Enruta las solicitudes a través de un router de desagregación de SGLang.

Para ejecutar un flujo de trabajo similar de entrega de TorchTPU de SGLang de varios hosts en aceleradores TPU7x (Ironwood) con el modelo Qwen3-Coder-480B más grande, consulta Entrega Qwen3-Coder-480B en GKE con TorchTPU de SGLang de varios hosts y desagregación de PD en aceleradores TPU7x.

Objetivos

  • Configura tu clúster de GKE y los grupos de nodos de TPU v6e de varios hosts.
  • Configura las credenciales y, luego, instala el controlador de LeaderWorkerSet.
  • Implementa las cargas de trabajo de precompletado, decodificación y router de SGLang de varios hosts.
  • Envía una solicitud de inferencia al servicio de router de SGLang.
  • Ejecuta una carga de trabajo de comparativas de entrega.
  • Realizar una limpieza

Costos

En este documento, usarás los siguientes componentes facturables de Google Cloud:

Para generar una estimación de costos en función del uso previsto, usa la calculadora de precios.

Cuando finalices las tareas que se describen en este documento, puedes borrar los recursos que creaste para evitar que continúe la facturación. Para obtener más información, consulta Cómo realizar una limpieza.

Antes de comenzar

Configura tu entorno para el instructivo:

  • Instala y, luego, inicializa Google Cloud CLI, y, luego, instala kubectl. Si usas Cloud Shell, ambas herramientas están preinstaladas.

  • Habilita la API de Kubernetes Engine, la API de Compute Engine, la API de Cloud TPU y la API de Artifact Registry en tu proyecto de Google Cloud :

    Habilita las API

  • Asegúrate de que tu proyecto tenga suficiente cuota de disco y de dirección IP externa en la región en la que se encuentra tu capacidad de TPU:

    1. En la consola de Google Cloud , ve a la página Cuotas y límites del sistema.

      Ir a Cuotas y límites del sistema

    2. Verifica que tengas al menos 400 GiB de cuota disponible de Google Cloud Hyperdisk Balanced en los cuatro volúmenes efímeros de 100 GiB que usan los Pods de precompletado y decodificación para el almacenamiento en caché de modelos.

    3. Filtra por Direcciones IPv4 externas regionales en uso y verifica que tengas suficiente cuota de direcciones IP en tu región de destino.

Obtén un token de acceso de Hugging Face

Necesitas un token de acceso de Hugging Face para usar este instructivo. Puedes registrarte para obtener una cuenta gratuita en Hugging Face. Después de tener una cuenta, genera un token de acceso:

  1. En la página Welcome to Hugging Face, haz clic en el avatar de tu cuenta y selecciona Tokens de acceso.
  2. En la página Tokens de acceso, haz clic en Crear token nuevo.
  3. Selecciona el tipo de token Lectura y, luego, ingresa un nombre para el token.
  4. Se mostrará tu token de acceso. Guarda el token en un lugar seguro.

Roles obligatorios

Si quieres obtener los permisos que necesitas para completar este instructivo, pídele a tu administrador que te otorgue los siguientes roles de IAM en tu proyecto:

Para obtener más información sobre cómo otorgar roles, consulta Administra el acceso a proyectos, carpetas y organizaciones.

También puedes obtener los permisos necesarios a través de roles personalizados o cualquier otro rol predefinido.

Asegúrate de que tu cuenta de servicio predeterminada de Compute Engine tenga los siguientes roles de Identity and Access Management (IAM) en tu proyecto:

Configura tus variables de entorno

Exporta las variables de entorno que usarás en este instructivo:

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"

Reemplaza lo siguiente:

  • YOUR_CLUSTER_NAME: el nombre del clúster de GKE.
  • YOUR_PROJECT_ID: Es el ID de tu proyecto de Google Cloud.
  • YOUR_REGION: Es la región en la que deseas implementar tu clúster, como us-central1.
  • YOUR_ZONE: Es la zona en la que deseas implementar tus grupos de nodos de TPU v6e, como us-central1-b.
  • YOUR_RESERVATION_NAME: Si usas una reserva, es el nombre de tu reserva de TPU v6e.
  • YOUR_HF_TOKEN: Es tu token de acceso de Hugging Face.

Si usas una imagen de contenedor personalizada de SGLang TorchTPU, reemplaza lmsysorg/sglang:v0.1.0-tpu en la variable SGLANG_IMAGE por el URI de tu imagen.

Crea el clúster de GKE

Para crear un clúster de GKE en modo estándar y conectar dos grupos de nodos TPU v6e de varios hosts para la desagregación de precompletado y decodificación, completa los siguientes pasos:

  1. Crea un clúster de GKE regional que use nodos e2-standard-4 para el grupo de nodos predeterminado, que proporciona suficientes recursos de CPU y memoria para el controlador LeaderWorkerSet y el router de desagregación de SGLang:

    gcloud container clusters create "${CLUSTER_NAME}" \
      --project="${PROJECT_ID}" \
      --location="${REGION}" \
      --machine-type=e2-standard-4
  2. Crea dos grupos de nodos de TPU v6e de varios hosts, cada uno con dos nodos ct6e-standard-4t (ocho chips TPU v6e en una topología 2x4 por grupo de nodos, para un total de 16 chips 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}"

    En lugar de usar una reserva específica, también puedes crear los grupos de nodos con capacidad Spot o VMs de inicio flexible:

    VMs Spot

    Aprovisiona los grupos de nodos con la capacidad de Spot reemplazando las marcas --reservation-affinity=specific y --reservation="${RESERVATION}" por la marca --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

    Las instancias que usan la capacidad de Spot pueden tardar en crearse después de que se crea el grupo de nodos, y se pueden interrumpir durante las ejecuciones de las cargas de trabajo.

    VMs de inicio flexible

    Aprovisiona los grupos de nodos con la opción de consumo inicio 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

    Para obtener más información, consulta Ejecuta una carga de trabajo por lotes pequeña con TPU y VMs de inicio flexible.

  3. Configura las credenciales de kubectl para comunicarte con tu clúster:

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

Configura las credenciales e instala LeaderWorkerSet

Para preparar tu clúster para la implementación de SGLang de varios hosts, completa los siguientes pasos:

  1. Crea un Secret de Kubernetes llamado hf-secret para almacenar tu token de acceso de Hugging Face:

    kubectl create secret generic hf-secret \
        --from-literal=hf_api_token="${HF_TOKEN}"
  2. Instala la CustomResourceDefinition y el controlador de LeaderWorkerSet en tu clúster:

    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

Implementa el servidor de SGLang de varios hosts

El manifiesto de la carga de trabajo (qwen3-coder-30b-multihost-disagg-v6e-2x4.yaml) define los siguientes recursos para coordinar la entrega de precompletado y decodificación desagregados en los dos grupos de nodos de TPU v6e de 2x4:

  • StorageClass (hyperdisk-balanced-tpu): Aprovisiona volúmenes efímeros de 100 GiB ReadWriteOnce respaldados por el almacenamiento de Google Cloud Hyperdisk Balanced. Cada uno de los cuatro Pods de precompletado y decodificación activa su propio volumen en /data para almacenar los puntos de control de Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8 descargados de forma local.

  • Grupo de relleno previo (sglang-prefill-service y sglang-tpu-prefill): Ejecuta un LeaderWorkerSet, que contiene un Pod líder y un Pod trabajador, fijados exclusivamente a un grupo de nodos TPU v6e 2x4. Este grupo de nodos tiene ocho chips TPU, que proporcionan ocho TensorCores. LeaderWorkerSet usa la anotación leaderworkerset.sigs.k8s.io/exclusive-topology establecida en "cloud.google.com/gke-nodepool". Los Pods inician sglang.launch_server con las siguientes marcas, lo que expone el puerto 8000 y el puerto de arranque 8998:

    • --disaggregation-mode=prefill
    • --tensor-parallel-size=8
    • --ep-size=8
  • Grupo de decodificación (sglang-decode-service y sglang-tpu-decode): Ejecuta un LeaderWorkerSet de size: 2 fijado al segundo grupo de nodos de TPU v6e 2x4 (ocho chips TPU, que proporcionan ocho TensorCores). LeaderWorkerSet inicia sglang.launch_server con las siguientes marcas:

    • --disaggregation-mode=decode
    • --tensor-parallel-size=8
    • --ep-size=8
  • Router de desagregación (sglang-router y sglang-service): Ejecuta una implementación de CPU que ejecuta el módulo sglang_router.launch_router con la marca --pd-disaggregation y expone un Service ClusterIP en el puerto 8000. El router dirige los mensajes y coordina la transferencia de la caché de KV entre los servicios sglang-prefill-service y sglang-decode-service.

Para implementar y supervisar los servidores de SGLang de varios hosts, completa los siguientes pasos:

  1. Descarga el manifiesto de la carga de trabajo:

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

    Como alternativa, crea un archivo llamado qwen3-coder-30b-multihost-disagg-v6e-2x4.yaml con el siguiente contenido:

    # ==============================================================================
    # 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. Aplica el manifiesto de la carga de trabajo al clúster:

    envsubst '$SGLANG_IMAGE' < qwen3-coder-30b-multihost-disagg-v6e-2x4.yaml | kubectl apply -f -
  3. Verifica que se hayan creado los Pods de router, prefill y decode:

    kubectl get pods

    Cuando todos los Pods están listos, el resultado es similar al siguiente:

    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. Supervisa los registros del Pod líder de precompletado y decodificación hasta que se complete el inicio:

    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

    La descarga inicial del modelo, la carga de pesos y la compilación tardan aproximadamente entre siete y diez minutos en la primera ejecución.

    Si usas capacidad interrumpible, como la capacidad de Spot o las VMs de inicio flexible, es posible que los nodos de porción de TPU tarden más en crearse después de la creación del grupo de nodos y que se interrumpan durante las ejecuciones de las cargas de trabajo. Para obtener más información, consulta Instancias de VMs interrumpibles. Para verificar si se crearon los nodos de porción de TPU, consulta Verifica el estado y las condiciones del nodo.

    Espera hasta que los registros de los Pods líderes de precompletado y decodificación muestren el siguiente mensaje y kubectl get pods muestre 1/1 en la columna READY para todos los Pods:

    The server is fired up and ready to roll!
    
  5. Abre una segunda sesión de terminal y reenvía el puerto local 8000 a sglang-service:

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

    Deja la redirección de puertos en ejecución en esta sesión de terminal.

Envía solicitudes de inferencia

Regresa a tu primera sesión de terminal y envía una solicitud de finalización de chat al extremo /v1/chat/completions expuesto por el router de 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
  }'

Ejecuta una carga de trabajo de comparativas

Para comparar el modelo Qwen3-Coder-30B-A3B implementado con una carga de trabajo de entrada de 1,024 tokens y salida de 1,024 tokens (1k/1k) desde el Pod sglang-router, ejecuta el siguiente comando:

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

Una vez que se complete la comparativa, busca un resultado que contenga métricas como las siguientes:

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

Realiza una limpieza

Para evitar que se apliquen cargos adicionales continuos a tu cuenta de Google Cloud por los recursos que usaste en este instructivo, borra los siguientes recursos:

  1. Borra las cargas de trabajo de precompletado, decodificación y router de SGLang:

    kubectl delete -f qwen3-coder-30b-multihost-disagg-v6e-2x4.yaml --ignore-not-found
  2. Borra los grupos de nodos de TPU v6e y el clúster de 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

¿Qué sigue?