Entrega Qwen3.8-2.4T en GKE con desagregación de PD de varios hosts usando Dynamo y SGLang en VMs A4

En este instructivo, se muestra cómo implementar y entregar Qwen3.8-2.4T (nvidia/Qwen3.8-2.4T-A95B-NVFP4), un modelo de mezcla de expertos de 2.4 billones de parámetros con 95,000 millones de parámetros activos cuantificados a NVFP4, en Google Kubernetes Engine (GKE) en Google Cloud. La implementación usa Cluster Toolkit, cuatro instancias de máquina virtual (VM) A4 (a4-highgpu-8g) multihost con un total de 32 GPUs NVIDIA B200, NVIDIA Dynamo y el framework de entrega de SGLang con desagregación de precompletado y decodificación (PD).

En la configuración de implementación que se describe en este instructivo, usarás Cluster Toolkit para aprovisionar un clúster de GKE en modo estándar. El clúster contiene cuatro nodos a4-highgpu-8g conectados a través de la red de acceso remoto directo a memoria (RDMA) de GPUDirect. Luego, instalas el programador de KAI y la plataforma NVIDIA Dynamo con la programación de Grove, y almacenas los pesos del modelo en Cloud Storage. A continuación, implementarás un recurso personalizado DynamoGraphDeployment que activa el bucket con Cloud Storage FUSE y separa el llenado previo y la decodificación en dos grupos de trabajadores de varios hosts de 16 GPU con paralelismo de tensores, paralelismo de expertos y una caché de KV de FP8.

Objetivos

  • Instala Cluster Toolkit y aprovisiona un clúster de GKE de A4 con varios hosts en el modo estándar.
  • Instala el programador de KAI y la plataforma NVIDIA Dynamo con la programación de Grove gang en tu clúster.
  • Almacena los pesos del modelo Qwen3.8-2.4T en un bucket de Cloud Storage.
  • Implementar un servidor de inferencia de SGLang desagregado de PD de varios hosts en cuatro nodos A4, con un total de 32 GPUs NVIDIA B200
  • Envía una solicitud de finalización de chat al router de frontend de Dynamo.
  • Ejecuta una carga de trabajo de comparativas de entrega.
  • Limpia los recursos implementados.

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.

Antes de comenzar

Configura tu entorno para el instructivo:

  1. Instala e inicializa Google Cloud CLI, e instala kubectl y Helm (versión 3.10 o posterior). Si usas Cloud Shell, estas herramientas están preinstaladas.
  2. Si ejecutas este instructivo desde una estación de trabajo de Linux o macOS en lugar de Cloud Shell, instala las dependencias de Cluster Toolkit necesarias, como Terraform y Git.
  3. Habilita la API de Kubernetes Engine, la API de Compute Engine, la API de Cloud Storage, la API de Identity and Access Management, la API de Cloud Resource Manager y la API de Service Usage en tu proyecto de Google Cloud :

    Habilita las API

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.

Configura tus variables de entorno

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

export PROJECT_ID="YOUR_PROJECT_ID"
export REGION="YOUR_REGION"
export ZONE="YOUR_ZONE"
export CLUSTER_NAME="YOUR_CLUSTER_NAME"
export RESERVATION_NAME="YOUR_RESERVATION_NAME"
export GCS_BUCKET="YOUR_BUCKET_NAME"
export HF_TOKEN="YOUR_HF_TOKEN"
export CLUSTER_TOOLKIT_VERSION="v1.104.0"
export DYNAMO_RELEASE_VERSION="1.5.0"

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

Reemplaza lo siguiente:

  • YOUR_PROJECT_ID: Es el ID de tu proyecto de Google Cloud.
  • YOUR_REGION: Es la región en la que se encuentra tu capacidad de A4 y en la que deseas implementar tu clúster, como us-central1.
  • YOUR_ZONE: Es la zona en la que se encuentra tu capacidad de A4, como us-central1-b.
  • YOUR_CLUSTER_NAME: Es el nombre de tu clúster de GKE y de la implementación de Cluster Toolkit. Usa un nombre de 20 caracteres o menos para que el ID de cuenta de servicio del grupo de nodos generado (YOUR_CLUSTER_NAME-gke-np-sa) se mantenga dentro del límite de 30 caracteres de Identity and Access Management (IAM).
  • YOUR_RESERVATION_NAME: Es el nombre de tu reserva de A4 o la ruta de acceso del bloque de reserva extendido en el formato RESERVATION_NAME/reservationBlocks/RESERVATION_BLOCK_NAME.
  • YOUR_BUCKET_NAME: un nombre único a nivel global para el bucket de Cloud Storage que almacena el estado de Terraform para Cluster Toolkit y los pesos del modelo Qwen3.8-2.4T en etapa de pruebas.
  • YOUR_HF_TOKEN: Es tu token de acceso de Hugging Face.

Crea el clúster de GKE

Descarga Cluster Toolkit y aprovisiona un clúster de GKE en modo estándar con cuatro nodos a4-highgpu-8g, redes de RDMA de GPUDirect y compatibilidad con DRA:

  1. Accede a Google Cloud CLI y genera credenciales predeterminadas de la aplicación para Terraform:

    gcloud auth login
    gcloud auth application-default login
  2. Descarga y extrae el paquete de Cluster Toolkit compilado previamente para la versión especificada en CLUSTER_TOOLKIT_VERSION (v1.104.0):

    TAG="${CLUSTER_TOOLKIT_VERSION:-v1.104.0}"
    OS="linux"
    ARCH="amd64"
    mkdir -p cluster-toolkit && curl -L "https://github.com/GoogleCloudPlatform/cluster-toolkit/releases/download/${TAG}/gcluster_bundle_${OS}_${ARCH}.tgz" | tar -xz -C cluster-toolkit

    Si ejecutas este comando en macOS o en una estación de trabajo arm64, configura OS="mac" o ARCH="arm64" para que coincida con tu plataforma.

  3. Para almacenar el estado de Terraform de tu implementación de Cluster Toolkit y los pesos del modelo en etapa de pruebas, crea un bucket de Cloud Storage en la misma región que tu reserva de A4 con acceso uniforme a nivel de bucket y control de versiones de objetos habilitado:

    gcloud storage buckets create "gs://${GCS_BUCKET}" \
        --default-storage-class=STANDARD \
        --project="${PROJECT_ID}" \
        --location="${REGION}" \
        --uniform-bucket-level-access
    gcloud storage buckets update "gs://${GCS_BUCKET}" --versioning
  4. Crea el archivo de configuración de implementación cluster-toolkit/examples/gke-a4/gke-a4-deployment.yaml para cuatro nodos a4-highgpu-8g:

    cat <<EOF > cluster-toolkit/examples/gke-a4/gke-a4-deployment.yaml
    terraform_backend_defaults:
      type: gcs
      configuration:
        bucket: ${GCS_BUCKET}
    
    vars:
      project_id: ${PROJECT_ID}
      deployment_name: ${CLUSTER_NAME}
      region: ${REGION}
      zone: ${ZONE}
      static_node_count: 4
      authorized_cidr: 0.0.0.0/0
      reservation_affinity:
        consume_reservation_type: SPECIFIC_RESERVATION
        specific_reservations:
        - name: ${RESERVATION_NAME}
    EOF

    De forma predeterminada, el archivo de implementación está configurado para consumir una reserva específica. Si no tienes una reserva específica, antes de implementar el clúster, puedes actualizar cluster-toolkit/examples/gke-a4/gke-a4-deployment.yaml para usar una opción de consumo alternativa, como VMs Spot o VMs de inicio flexible. Para obtener instrucciones de configuración, consulta Opciones de consumo de GKE.

  5. Cambia al directorio cluster-toolkit, implementa el clúster de GKE con el comando gcluster deploy y, luego, regresa a tu directorio de trabajo:

    cd cluster-toolkit
    ./gcluster deploy \
        -d examples/gke-a4/gke-a4-deployment.yaml \
        examples/gke-a4/gke-a4.yaml \
        --auto-approve -w
    cd ..

    La creación de clústeres tarda entre 20 y 30 minutos aproximadamente. Si ejecutas ./gcluster deploy sin la marca --auto-approve, para aplicar los cambios propuestos, ingresa a cuando se te solicite.

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

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

Instala KAI Scheduler y NVIDIA Dynamo

Prepara tu clúster creando un Secret de Hugging Face y, luego, instalando el programador de KAI y la plataforma NVIDIA Dynamo con la programación de Grove Gang:

  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}" \
        --dry-run=client -o yaml | kubectl apply -f -
  2. Descarga el gráfico de Helm de la plataforma NVIDIA Dynamo (v1.5.0), instala el programador de KAI y, luego, instala la plataforma Dynamo con Grove habilitado:

    RELEASE_VERSION="${DYNAMO_RELEASE_VERSION:-1.5.0}"
    helm pull "https://helm.ngc.nvidia.com/nvidia/ai-dynamo/charts/dynamo-platform-${RELEASE_VERSION}.tgz"
    tar -zxf "dynamo-platform-${RELEASE_VERSION}.tgz"
    
    helm upgrade --install kai-scheduler ./dynamo-platform/charts/kai-scheduler \
        --set scheduler.args.default-staleness-grace-period="-1s" \
        --set topologyMigration.enabled=false
    
    kubectl wait --for=condition=Available deployment/kai-operator --timeout=120s
    until kubectl get deployment/queue-controller >/dev/null 2>&1; do sleep 2; done
    kubectl wait --for=condition=Available deployment/queue-controller --timeout=120s
    
    helm upgrade --install dynamo-platform ./dynamo-platform \
        --set global.grove.install=true \
        --set global.grove.enabled=true \
        --set global.kai-scheduler.enabled=true

Almacena las ponderaciones del modelo en Cloud Storage

Almacena en etapa intermedia el punto de control nvidia/Qwen3.8-2.4T-A95B-NVFP4 en tu bucket de Cloud Storage en la región (gs://${GCS_BUCKET}) para que los Pods de trabajadores de inferencia puedan activar las ponderaciones con Cloud Storage FUSE en lugar de descargarlas de Hugging Face en cada inicio en frío:

  1. Otorga el rol de usuario de objetos de almacenamiento (roles/storage.objectUser) en el bucket de gs://${GCS_BUCKET} que creaste en Crea el clúster de GKE a la cuenta de servicio de Google vinculada a la cuenta de servicio de Kubernetes workload-identity-k8s-sa:

    GSA=$(kubectl get sa workload-identity-k8s-sa -o jsonpath='{.metadata.annotations.iam\.gke\.io/gcp-service-account}')
    gcloud storage buckets add-iam-policy-binding "gs://${GCS_BUCKET}" \
        --member="serviceAccount:${GSA}" \
        --role="roles/storage.objectUser"
  2. Descarga los manifiestos qwen3.8-2.4t-nvfp4-stage-weights-job.yaml y qwen3.8-2.4t-nvfp4-multihost-a4-gcsfuse.yaml:

    curl -O https://raw.githubusercontent.com/GoogleCloudSamples/AIHypercomputerSamples/refs/heads/main/gpu/inference/qwen38-2t/qwen3.8-2.4t-nvfp4-stage-weights-job.yaml
    curl -O https://raw.githubusercontent.com/GoogleCloudSamples/AIHypercomputerSamples/refs/heads/main/gpu/inference/qwen38-2t/qwen3.8-2.4t-nvfp4-multihost-a4-gcsfuse.yaml

    Como alternativa, crea un archivo llamado qwen3.8-2.4t-nvfp4-stage-weights-job.yaml con el siguiente contenido. Este Job indexado ejecuta cuatro Pods paralelos en tus cuatro nodos A4. Cada Pod descarga una porción de tamaño equilibrado del punto de control de nvidia/Qwen3.8-2.4T-A95B-NVFP4 en la SSD de NVMe local del nodo y la sube a gs://${GCS_BUCKET}/hf_models/models--nvidia--qwen3_8-2_4t-a95b-nvfp4 con gcloud storage rsync:

    # ==============================================================================
    # Stage nvidia/Qwen3.8-2.4T-A95B-NVFP4 (~1.44 TB, 210 files) from Hugging Face Hub
    # into an in-region Cloud Storage bucket for qwen3.8-2.4t-nvfp4-multihost-a4-gcsfuse.yaml
    #
    # Each Indexed Job pod downloads a size-balanced slice of the repo onto the node's
    # local NVMe SSD, then uploads that slice with `gcloud storage rsync`.
    # ==============================================================================
    apiVersion: batch/v1
    kind: Job
    metadata:
      name: qwen3-8-2-4t-stage-weights
    spec:
      completionMode: Indexed
      completions: 4
      parallelism: 4
      backoffLimitPerIndex: 2
      template:
        metadata:
          labels:
            app: qwen3-8-2-4t-stage-weights
        spec:
          restartPolicy: Never
          serviceAccountName: workload-identity-k8s-sa
          nodeSelector:
            cloud.google.com/gke-accelerator: nvidia-b200
          tolerations:
            - key: "nvidia.com/gpu"
              operator: "Exists"
              effect: "NoSchedule"
          topologySpreadConstraints:
            - maxSkew: 1
              topologyKey: kubernetes.io/hostname
              whenUnsatisfiable: DoNotSchedule
              labelSelector:
                matchLabels:
                  app: qwen3-8-2-4t-stage-weights
          initContainers:
            - name: download
              image: nvcr.io/nvidia/ai-dynamo/sglang-runtime:1.5.0
              command: ["python3", "-c"]
              args:
                - |
                  import os, time
                  from huggingface_hub import HfApi, snapshot_download
                  repo = os.environ["REPO_ID"]
                  n = int(os.environ["NUM_SHARDS"])
                  i = int(os.environ["JOB_COMPLETION_INDEX"])
                  info = HfApi().model_info(repo, files_metadata=True)
                  files = sorted(((s.size or 0, s.rfilename) for s in info.siblings), reverse=True)
                  bins = [[0, []] for _ in range(n)]
                  for size, name in files:
                      b = min(bins, key=lambda x: x[0])
                      b[0] += size
                      b[1].append(name)
                  size, mine = bins[i]
                  print(f"shard {i}/{n}: {len(mine)} files, {size / 1e9:.1f} GB", flush=True)
                  for attempt in range(1, 7):
                      try:
                          snapshot_download(repo, local_dir="/ssd/model", allow_patterns=mine, max_workers=16)
                          break
                      except Exception as e:
                          print(f"shard {i}/{n}: attempt {attempt} failed: {e!r}", flush=True)
                          if attempt == 6:
                              raise
                          time.sleep(30)
                  print(f"shard {i}/{n}: download complete", flush=True)
              env:
                - name: REPO_ID
                  value: "nvidia/Qwen3.8-2.4T-A95B-NVFP4"
                - name: NUM_SHARDS
                  value: "4"
                - name: HF_HOME
                  value: "/ssd/hf"
                - name: HF_HUB_DISABLE_XET
                  value: "1"
                - name: HF_TOKEN
                  valueFrom:
                    secretKeyRef:
                      name: hf-secret
                      key: hf_api_token
                      optional: true
              resources:
                requests:
                  cpu: "16"
                  memory: "64Gi"
                  ephemeral-storage: "450Gi"
                limits:
                  cpu: "16"
                  memory: "64Gi"
                  ephemeral-storage: "700Gi"
              volumeMounts:
                - name: ssd
                  mountPath: /ssd
          containers:
            - name: upload
              image: gcr.io/google.com/cloudsdktool/google-cloud-cli:slim
              command: ["/bin/bash", "-c"]
              args:
                - |
                  set -euo pipefail
                  gcloud storage rsync --recursive --exclude='^\.cache/.*' /ssd/model "${DEST}"
                  echo "shard ${JOB_COMPLETION_INDEX}: upload complete"
              env:
                - name: DEST
                  value: "gs://${GCS_BUCKET}/hf_models/models--nvidia--qwen3_8-2_4t-a95b-nvfp4"
              resources:
                requests:
                  cpu: "16"
                  memory: "32Gi"
                limits:
                  cpu: "16"
                  memory: "32Gi"
              volumeMounts:
                - name: ssd
                  mountPath: /ssd
          volumes:
            - name: ssd
              emptyDir: {}
  3. Sustituye el nombre de tu bucket de Cloud Storage en qwen3.8-2.4t-nvfp4-stage-weights-job.yaml, inicia el trabajo de etapa de pruebas, espera a que los cuatro Pods terminen de subir los pesos y, luego, borra el trabajo completado:

    envsubst '${GCS_BUCKET}' < qwen3.8-2.4t-nvfp4-stage-weights-job.yaml | kubectl apply -f -
    
    kubectl wait --for=condition=complete job/qwen3-8-2-4t-stage-weights --timeout=2h
    
    kubectl delete job qwen3-8-2-4t-stage-weights

    Descargar las ponderaciones del modelo en la SSD de NVMe local y subirlas a Cloud Storage tarda aproximadamente 25 minutos.

  4. Opcional: Mientras se ejecuta el trabajo de etapa de pruebas, transmite los registros de un Pod de etapa de pruebas en una segunda sesión de terminal para supervisar el progreso de la descarga:

    POD_NAME="YOUR_POD_NAME"
    kubectl logs -f "${POD_NAME}"

    Reemplaza YOUR_POD_NAME por el nombre de uno de los Pods de qwen3-8-2-4t-stage-weights de kubectl get pods.

Implementa el servidor de SGLang desagregado de varios hosts

El manifiesto de la carga de trabajo (qwen3.8-2.4t-nvfp4-multihost-a4-gcsfuse.yaml) define un ResourceClaimTemplate de Kubernetes (all-mrdma), que asigna los dispositivos RDMA multi-NIC del nodo (mrdma.google.com) a través de DRA, recursos de Role y RoleBinding para la cuenta de servicio workload-identity-k8s-sa y un DynamoGraphDeployment de NVIDIA Dynamo (qwen3-8-2-4t-a4-disagg) con la programación de grupos de Grove habilitada. La implementación consiste en lo siguiente:

  • Frontend (Frontend): Ejecuta una réplica de router que acepta solicitudes de clientes, descubre trabajadores de precompletado y decodificación registrados, y coordina el enrutamiento de solicitudes desagregadas.

  • Trabajador de precompletado (Prefill): Ejecuta un grupo de trabajadores de dos nodos en dos nodos a4-highgpu-8g con un total de 16 GPU NVIDIA B200. Cada Pod activa los pesos de nvidia/Qwen3.8-2.4T-A95B-NVFP4 de etapa de pruebas desde tu bucket de Cloud Storage en /model-weights con el controlador de CSI de Cloud Storage FUSE (gcsfuse.csi.storage.gke.io) con el almacenamiento en caché de archivos de SSD de NVMe local, obtiene el entorno de NCCL/gIB y, luego, inicia python3 -m dynamo.sglang en --disaggregation-mode prefill con las siguientes marcas:

    • --served-model-name "nvidia/Qwen3.8-2.4T-A95B-NVFP4"
    • --tensor-parallel-size 16
    • --ep-size 16
    • --nnodes 2
    • --moe-runner-backend flashinfer_trtllm
    • --attention-backend triton
    • --kv-cache-dtype fp8_e4m3
    • --disable-flashinfer-autotune
  • Trabajador de decodificación (Decode): Ejecuta un grupo de trabajadores de dos nodos en los dos nodos a4-highgpu-8g restantes, con un total de 16 GPUs NVIDIA B200. Cada Pod activa los pesos del modelo de etapa de pruebas desde Cloud Storage en /model-weights a través del controlador de CSI de Cloud Storage FUSE y inicia python3 -m dynamo.sglang en --disaggregation-mode decode con las siguientes marcas:

    • --served-model-name "nvidia/Qwen3.8-2.4T-A95B-NVFP4"
    • --tensor-parallel-size 16
    • --ep-size 16
    • --nnodes 2
    • --moe-runner-backend flashinfer_trtllm
    • --attention-backend triton
    • --kv-cache-dtype fp8_e4m3

Implementa y supervisa el servidor de inferencia desagregado:

  1. Revisa el manifiesto de la carga de trabajo qwen3.8-2.4t-nvfp4-multihost-a4-gcsfuse.yaml que descargaste antes o crea un archivo llamado qwen3.8-2.4t-nvfp4-multihost-a4-gcsfuse.yaml con el siguiente contenido:

    # ==============================================================================
    # Multihost Disaggregated Serving of Qwen3.8-2.4T-A95B (NVFP4 MoE) on GKE
    # using NVIDIA Dynamo Platform (v1.5.0) and SGLang on GPU A4 (4 x a4-highgpu-8g)
    # ==============================================================================
    apiVersion: resource.k8s.io/v1
    kind: ResourceClaimTemplate
    metadata:
      name: all-mrdma
    spec:
      spec:
        devices:
          requests:
            - name: req-mrdma
              exactly:
                deviceClassName: mrdma.google.com
                allocationMode: All
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: Role
    metadata:
      name: dynamo-workload-identity-discovery-role
      namespace: default
    rules:
      - apiGroups: [""]
        resources: ["endpoints", "pods"]
        verbs: ["get", "list", "watch"]
      - apiGroups: ["discovery.k8s.io"]
        resources: ["endpointslices"]
        verbs: ["get", "list", "watch"]
      - apiGroups: ["nvidia.com"]
        resources: ["dynamoworkermetadatas"]
        verbs: ["create", "get", "list", "watch", "update", "patch", "delete"]
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: RoleBinding
    metadata:
      name: dynamo-workload-identity-discovery-binding
      namespace: default
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: Role
      name: dynamo-workload-identity-discovery-role
    subjects:
      - kind: ServiceAccount
        name: workload-identity-k8s-sa
        namespace: default
    ---
    apiVersion: nvidia.com/v1alpha1
    kind: DynamoGraphDeployment
    metadata:
      name: qwen3-8-2-4t-a4-disagg
      annotations:
        nvidia.com/enable-grove: "true"
    spec:
      backendFramework: sglang
      services:
        # --------------------------------------------------------------------------
        # FRONTEND: Dynamo OpenAI-Compatible Router (`python3 -m dynamo.frontend`)
        # --------------------------------------------------------------------------
        Frontend:
          componentType: frontend
          replicas: 1
          resources:
            requests:
              cpu: "8"
              memory: "32Gi"
            limits:
              cpu: "8"
              memory: "32Gi"
          extraPodSpec:
            tolerations:
              - key: "nvidia.com/gpu"
                operator: "Exists"
                effect: "NoSchedule"
            mainContainer:
              image: nvcr.io/nvidia/ai-dynamo/sglang-runtime:1.5.0
              command: ["/bin/bash", "-c"]
              args:
                - |
                  set -ex
                  exec python3 -m dynamo.frontend --http-port 8000
    
        # --------------------------------------------------------------------------
        # PREFILL WORKER: 2 x GPU A4 (`a4-highgpu-8g`, 16 x B200 GPUs, TP=16, EP=16)
        # --------------------------------------------------------------------------
        Prefill:
          componentType: worker
          subComponentType: prefill
          replicas: 1
          multinode:
            nodeCount: 2
          extraPodMetadata:
            annotations:
              gke-gcsfuse/volumes: "true"
          livenessProbe:
            httpGet:
              path: /live
              port: system
            periodSeconds: 60
            timeoutSeconds: 30
            failureThreshold: 60
          readinessProbe:
            httpGet:
              path: /health
              port: system
            initialDelaySeconds: 120
            periodSeconds: 30
            timeoutSeconds: 30
            failureThreshold: 60
          sharedMemory:
            size: 128Gi
          resources:
            requests:
              gpu: "8"
              cpu: "128"
              memory: "1500Gi"
            limits:
              gpu: "8"
              cpu: "128"
              memory: "2000Gi"
          envs:
            - name: MODEL_PATH
              value: "/model-weights/hf_models/models--nvidia--qwen3_8-2_4t-a95b-nvfp4"
            - name: DYN_SYSTEM_PORT
              value: "9090"
            - name: LD_LIBRARY_PATH
              value: "/usr/local/gib/lib64:/usr/local/nvidia/lib64"
            - name: GLOO_SOCKET_IFNAME
              value: "eth0"
            - name: NCCL_ALGO
              value: "Ring,Tree"
            - name: WITH_NVIDIA_PEERMEM
              value: "0"
          extraPodSpec:
            serviceAccountName: workload-identity-k8s-sa
            automountServiceAccountToken: true
            resourceClaims:
              - name: rdma
                resourceClaimTemplateName: all-mrdma
            nodeSelector:
              cloud.google.com/gke-accelerator: nvidia-b200
            tolerations:
              - key: "nvidia.com/gpu"
                operator: "Exists"
                effect: "NoSchedule"
            mainContainer:
              image: nvcr.io/nvidia/ai-dynamo/sglang-runtime:1.5.0
              resources:
                claims:
                  - name: rdma
              startupProbe:
                httpGet:
                  path: /live
                  port: system
                initialDelaySeconds: 120
                periodSeconds: 30
                timeoutSeconds: 10
                failureThreshold: 120
              command: ["/bin/bash", "-c"]
              args:
                - |
                  set -ex
                  if [ -f /usr/local/gib/scripts/set_nccl_env.sh ]; then
                    . /usr/local/gib/scripts/set_nccl_env.sh
                  fi
    
                  echo "=== [Dynamo Prefill Worker] Launching dynamo.sglang ==="
                  exec python3 -m dynamo.sglang \
                    --dist-init-addr "${GROVE_PCSG_NAME}-${GROVE_PCSG_INDEX}-prefill-ldr-0.${GROVE_HEADLESS_SERVICE}.:29500" \
                    --model-path "${MODEL_PATH}" \
                    --served-model-name "nvidia/Qwen3.8-2.4T-A95B-NVFP4" \
                    --host 0.0.0.0 \
                    --disaggregation-mode prefill \
                    --disaggregation-bootstrap-port 30001 \
                    --tensor-parallel-size 16 \
                    --ep-size 16 \
                    --moe-runner-backend flashinfer_trtllm \
                    --attention-backend triton \
                    --disable-flashinfer-autotune \
                    --nnodes 2 \
                    --kv-cache-dtype fp8_e4m3 \
                    --context-length 16384 \
                    --mem-fraction-static 0.85 \
                    --enable-metrics
              volumeMounts:
                - name: library-dir-host
                  mountPath: /usr/local/nvidia
                - name: gib
                  mountPath: /usr/local/gib
                - name: gcs-model-dir
                  mountPath: /model-weights
                  readOnly: true
            volumes:
              - name: library-dir-host
                hostPath:
                  path: /home/kubernetes/bin/nvidia
              - name: gib
                hostPath:
                  path: /home/kubernetes/bin/gib
              - name: gcs-model-dir
                csi:
                  driver: gcsfuse.csi.storage.gke.io
                  readOnly: true
                  volumeAttributes:
                    bucketName: ${GCS_BUCKET}
                    mountOptions: "implicit-dirs,file-cache:max-size-mb:-1"
    
        # --------------------------------------------------------------------------
        # DECODE WORKER: 2 x GPU A4 (`a4-highgpu-8g`, 16 x B200 GPUs, TP=16, EP=16)
        # --------------------------------------------------------------------------
        Decode:
          componentType: worker
          subComponentType: decode
          replicas: 1
          multinode:
            nodeCount: 2
          extraPodMetadata:
            annotations:
              gke-gcsfuse/volumes: "true"
          livenessProbe:
            httpGet:
              path: /live
              port: system
            periodSeconds: 60
            timeoutSeconds: 30
            failureThreshold: 60
          readinessProbe:
            httpGet:
              path: /health
              port: system
            initialDelaySeconds: 120
            periodSeconds: 30
            timeoutSeconds: 30
            failureThreshold: 60
          sharedMemory:
            size: 128Gi
          resources:
            requests:
              gpu: "8"
              cpu: "128"
              memory: "1500Gi"
            limits:
              gpu: "8"
              cpu: "128"
              memory: "2000Gi"
          envs:
            - name: MODEL_PATH
              value: "/model-weights/hf_models/models--nvidia--qwen3_8-2_4t-a95b-nvfp4"
            - name: DYN_SYSTEM_PORT
              value: "9090"
            - name: LD_LIBRARY_PATH
              value: "/usr/local/gib/lib64:/usr/local/nvidia/lib64"
            - name: GLOO_SOCKET_IFNAME
              value: "eth0"
            - name: NCCL_ALGO
              value: "Ring,Tree"
            - name: WITH_NVIDIA_PEERMEM
              value: "0"
          extraPodSpec:
            serviceAccountName: workload-identity-k8s-sa
            automountServiceAccountToken: true
            resourceClaims:
              - name: rdma
                resourceClaimTemplateName: all-mrdma
            nodeSelector:
              cloud.google.com/gke-accelerator: nvidia-b200
            tolerations:
              - key: "nvidia.com/gpu"
                operator: "Exists"
                effect: "NoSchedule"
            mainContainer:
              image: nvcr.io/nvidia/ai-dynamo/sglang-runtime:1.5.0
              resources:
                claims:
                  - name: rdma
              startupProbe:
                httpGet:
                  path: /live
                  port: system
                initialDelaySeconds: 120
                periodSeconds: 30
                timeoutSeconds: 10
                failureThreshold: 120
              command: ["/bin/bash", "-c"]
              args:
                - |
                  set -ex
                  if [ -f /usr/local/gib/scripts/set_nccl_env.sh ]; then
                    . /usr/local/gib/scripts/set_nccl_env.sh
                  fi
    
                  echo "=== [Dynamo Decode Worker] Launching dynamo.sglang ==="
                  exec python3 -m dynamo.sglang \
                    --dist-init-addr "${GROVE_PCSG_NAME}-${GROVE_PCSG_INDEX}-decode-ldr-0.${GROVE_HEADLESS_SERVICE}.:29500" \
                    --model-path "${MODEL_PATH}" \
                    --served-model-name "nvidia/Qwen3.8-2.4T-A95B-NVFP4" \
                    --host 0.0.0.0 \
                    --disaggregation-mode decode \
                    --disaggregation-bootstrap-port 30001 \
                    --tensor-parallel-size 16 \
                    --ep-size 16 \
                    --moe-runner-backend flashinfer_trtllm \
                    --attention-backend triton \
                    --nnodes 2 \
                    --kv-cache-dtype fp8_e4m3 \
                    --context-length 16384 \
                    --mem-fraction-static 0.85 \
                    --enable-metrics
              volumeMounts:
                - name: library-dir-host
                  mountPath: /usr/local/nvidia
                - name: gib
                  mountPath: /usr/local/gib
                - name: gcs-model-dir
                  mountPath: /model-weights
                  readOnly: true
            volumes:
              - name: library-dir-host
                hostPath:
                  path: /home/kubernetes/bin/nvidia
              - name: gib
                hostPath:
                  path: /home/kubernetes/bin/gib
              - name: gcs-model-dir
                csi:
                  driver: gcsfuse.csi.storage.gke.io
                  readOnly: true
                  volumeAttributes:
                    bucketName: ${GCS_BUCKET}
                    mountOptions: "implicit-dirs,file-cache:max-size-mb:-1"
  2. Sustituye el nombre de tu bucket de Cloud Storage en qwen3.8-2.4t-nvfp4-multihost-a4-gcsfuse.yaml y aplica el manifiesto de la carga de trabajo a tu clúster:

    envsubst '${GCS_BUCKET}' < qwen3.8-2.4t-nvfp4-multihost-a4-gcsfuse.yaml | kubectl apply -f -
  3. Para supervisar la carga de pesos y la inicialización del servidor, enumera los Pods en tu clúster y transmite los registros del Pod líder de precompletado y del Pod líder de decodificación:

    kubectl get pods
    POD_NAME="YOUR_POD_NAME"
    kubectl logs -f "${POD_NAME}"

    Reemplaza YOUR_POD_NAME por el nombre del Pod líder de precompletado o el Pod líder de decodificación del resultado de kubectl get pods.

    En un inicio en frío, se necesitan aproximadamente entre 4 y 5 minutos para transmitir los 2,400,000,000,000 de pesos del modelo NVFP4 (~1.44 TB en 200 fragmentos) desde Cloud Storage a través de Cloud Storage FUSE y, luego, inicializar los servidores de SGLang de varios hosts. Antes de enviar cualquier solicitud, espera hasta que los Pods de frontend muestren 1/1 y los Pods de precompletado y decodificación muestren 2/2 en la columna READY de kubectl get pods:

    NAME                                                     READY   STATUS    RESTARTS   AGE
    qwen3-8-2-4t-a4-disagg-0-decode-0-decode-ldr-wz92h       2/2     Running   0          5m
    qwen3-8-2-4t-a4-disagg-0-decode-0-decode-wkr-fb8g6       2/2     Running   0          5m
    qwen3-8-2-4t-a4-disagg-0-frontend-jrx4s                  1/1     Running   0          5m
    qwen3-8-2-4t-a4-disagg-0-prefill-0-prefill-ldr-7nz25     2/2     Running   0          5m
    qwen3-8-2-4t-a4-disagg-0-prefill-0-prefill-wkr-7d5cw     2/2     Running   0          5m
    

Envía solicitudes de inferencia

Envía una solicitud de finalización de chat al servicio Qwen3.8-2.4T implementado:

  1. Redirecciona el puerto local 8000 al servicio qwen3-8-2-4t-a4-disagg-frontend:

    kubectl port-forward svc/qwen3-8-2-4t-a4-disagg-frontend 8000:8000

    Deja la redirección de puertos en ejecución en esta sesión de terminal o ejecuta el comando en segundo plano.

  2. En una segunda sesión de terminal, envía una solicitud de finalización de chat al extremo /v1/chat/completions:

    curl -s http://127.0.0.1:8000/v1/chat/completions \
      -H "Content-Type: application/json" \
      -d '{
        "model": "nvidia/Qwen3.8-2.4T-A95B-NVFP4",
        "messages": [{"role": "user", "content": "Explain KV-cache disaggregation in one sentence."}],
        "max_tokens": 64
      }' | jq .

Ejecuta una carga de trabajo de comparativas

Compara el servidor Qwen3.8-2.4T implementado desde el Pod de frontend de Dynamo con sglang.bench_serving con una configuración de 1,024 tokens de entrada, 1,024 tokens de salida (1k/1k), 256 instrucciones y una simultaneidad máxima de 128:

ROUTER_POD=$(kubectl get pod -l 'nvidia.com/dynamo-component-type=frontend' -o jsonpath='{.items[0].metadata.name}')

kubectl exec "${ROUTER_POD}" -- python3 -m sglang.bench_serving \
  --backend sglang-oai-chat \
  --base-url http://localhost:8000 \
  --model nvidia/Qwen3.8-2.4T-A95B-NVFP4 \
  --dataset-name random \
  --num-prompts 256 \
  --random-input-len 1024 \
  --random-output-len 1024 \
  --random-range-ratio 1.0 \
  --max-concurrency 128 | tee benchmark.log

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 las cargas de trabajo de Kubernetes, el clúster de GKE y el bucket de Cloud Storage:

  1. Borra los recursos DynamoGraphDeployment y ResourceClaimTemplate:

    kubectl delete -f qwen3.8-2.4t-nvfp4-multihost-a4-gcsfuse.yaml --ignore-not-found || true
  2. Desinstala las versiones de Helm de la plataforma NVIDIA Dynamo y KAI Scheduler:

    helm uninstall dynamo-platform kai-scheduler || true
  3. Borra el Secret de Kubernetes de Hugging Face:

    kubectl delete secret hf-secret --ignore-not-found || true
  4. Desde el directorio cluster-toolkit, destruye el clúster de GKE y su infraestructura asociada con el comando gcluster destroy:

    cd cluster-toolkit
    ./gcluster destroy "${CLUSTER_NAME}" --auto-approve || true
    cd ..
  5. Borra el bucket de Cloud Storage que almacena los pesos del modelo en etapa de pruebas y el estado de Terraform:

    gcloud storage rm --recursive "gs://${GCS_BUCKET}"
    

¿Qué sigue?