Ajusta y escala el aprendizaje por refuerzo con Verl en GKE

En este instructivo, se muestra cómo organizar un entorno de entrenamiento distribuido para el aprendizaje por refuerzo en Google Kubernetes Engine (GKE). Usarás Ray y el framework de verl (Volcano Engine Reinforcement Learning) para configurar un entorno de entrenamiento distribuido y ajustar un modelo de Qwen2.5-32B-Instruct en el conjunto de datos de GSM8K.

Este instructivo se centra en la canalización de entrenamiento de la optimización de políticas relativas de grupos (GRPO) en GKE con Ray y verl. GRPO es un algoritmo de aprendizaje por refuerzo diseñado para mejorar la capacidad de razonamiento de un modelo. Este algoritmo con uso eficiente de la memoria simplifica el proceso de aprendizaje por refuerzo (RL) eliminando el crítico, o modelo de valor, y usando un cálculo relativo basado en grupos en su lugar.

Este instructivo es un buen punto de partida si necesitas configurar un entorno de entrenamiento distribuido en el que los datos, los pesos del modelo y el motor de entrenamiento estén desvinculados para mejorar la eficiencia.

Este instructivo admite las siguientes arquitecturas de GPU:

  • Nodos de GPU basados en Intel o AMD: Configura y escala con GPUs NVIDIA B200 o H200, usando la Asignación dinámica de recursos (DRA) de GKE para la ruta de Autopilot.
  • Nodos A4X (GB200) basados en Arm: Configura y escala con los superchips NVIDIA GB200 Grace Blackwell, usando la asignación dinámica de recursos (DRA) de GKE y NVLink de varios nodos (IMEX).

Fondo

En las siguientes secciones, se proporciona una breve descripción general de los conceptos que se usan en este instructivo.

Aprendizaje por refuerzo (RL)

El RL enseña a los modelos a través de la experiencia, la exploración y la retroalimentación, en lugar de la imitación estática. Aunque el entrenamiento previo le enseña a un modelo qué decir, el aprendizaje por refuerzo con retroalimentación humana (RLHF) le enseña a ser útil, seguro y lógico. El RL sirve como puente entre un modelo base y un modelo ajustado para un caso de uso especializado.

Para obtener más información, consulta ¿Qué es el aprendizaje por refuerzo?

Optimización de políticas relativas al grupo (GRPO)

GRPO, un algoritmo popularizado por DeepSeek, ofrece una alternativa con uso eficiente de la memoria a la optimización de políticas proximales (PPO) para la alineación de LLM, ya que quita el modelo de crítico. En lugar de una red de críticos, el GRPO genera un grupo de respuestas para la misma instrucción y usa la recompensa promedio de ese grupo como referencia.

Para obtener más información, consulta GRPO.

Volcano Engine Reinforcement Learning (verl)

verl es un framework de alto rendimiento diseñado para controlar los patrones complejos de memoria y procesamiento del RL basado en LLM.

Para obtener más información, consulta verl.

Objetivos

En este instructivo, se muestra cómo configurar el aprendizaje por refuerzo en GKE con verl. Para ello, completa los siguientes pasos:

  1. Configura un clúster de GKE con A4X (Superchips GB200), A4 (GPU B200) o A3 Ultra (GPU H200).
  2. Configurar KubeRay para administrar un clúster de Ray distribuido
  3. Usa Cloud Storage FUSE para activar un bucket de Cloud Storage en todos los nodos.
  4. Ejecuta un trabajo de entrenamiento de GRPO con verl para alinear el modelo Qwen2.5-32B-Instruct con el conjunto de datos GSM8K.

Antes de comenzar

  • Accede a tu cuenta de Google Cloud . Si eres nuevo en Google Cloud, crea una cuenta para evaluar el rendimiento de nuestros productos en situaciones reales. Los clientes nuevos también obtienen $300 en créditos gratuitos para ejecutar, probar y, además, implementar cargas de trabajo.
  • Instala Google Cloud CLI.

  • Si usas un proveedor de identidad externo (IdP), primero debes acceder a la gcloud CLI con tu identidad federada.

  • Para inicializar gcloud CLI, ejecuta el siguiente comando:

    gcloud init
  • Crea o selecciona un Google Cloud proyecto.

    Roles necesarios para seleccionar o crear un proyecto

    • Selecciona un proyecto: Para seleccionar un proyecto, no se requiere un rol de IAM específico. Puedes seleccionar cualquier proyecto en el que se te haya otorgado un rol.
    • Crea un proyecto: Para crear un proyecto, necesitas el rol de Creador de proyectos (roles/resourcemanager.projectCreator), que contiene el permiso resourcemanager.projects.create. Obtén más información para otorgar roles.
    • Crea un proyecto de Google Cloud :

      gcloud projects create PROJECT_ID

      Reemplaza PROJECT_ID por un nombre para el proyecto de Google Cloud que estás creando.

    • Selecciona el proyecto de Google Cloud que creaste:

      gcloud config set project PROJECT_ID

      Reemplaza PROJECT_ID por el nombre de tu proyecto de Google Cloud .

  • Verifica que la facturación esté habilitada para tu proyecto de Google Cloud .

  • Habilita las APIs necesarias, si aún no están habilitadas:

    Roles necesarios para habilitar las APIs

    Para habilitar APIs, necesitas el permiso serviceusage.services.enable. Si creaste el proyecto, es probable que ya tengas este permiso a través del rol de propietario (roles/owner). De lo contrario, puedes obtenerlo a través del rol de administrador de Service Usage (roles/serviceusage.serviceUsageAdmin). Obtén más información para otorgar roles.

    gcloud services enable container.googleapis.com storage.googleapis.com compute.googleapis.com
  • Instala Google Cloud CLI.

  • Si usas un proveedor de identidad externo (IdP), primero debes acceder a la gcloud CLI con tu identidad federada.

  • Para inicializar gcloud CLI, ejecuta el siguiente comando:

    gcloud init
  • Crea o selecciona un Google Cloud proyecto.

    Roles necesarios para seleccionar o crear un proyecto

    • Selecciona un proyecto: Para seleccionar un proyecto, no se requiere un rol de IAM específico. Puedes seleccionar cualquier proyecto en el que se te haya otorgado un rol.
    • Crea un proyecto: Para crear un proyecto, necesitas el rol de Creador de proyectos (roles/resourcemanager.projectCreator), que contiene el permiso resourcemanager.projects.create. Obtén más información para otorgar roles.
    • Crea un proyecto de Google Cloud :

      gcloud projects create PROJECT_ID

      Reemplaza PROJECT_ID por un nombre para el proyecto de Google Cloud que estás creando.

    • Selecciona el proyecto de Google Cloud que creaste:

      gcloud config set project PROJECT_ID

      Reemplaza PROJECT_ID por el nombre de tu proyecto de Google Cloud .

  • Verifica que la facturación esté habilitada para tu proyecto de Google Cloud .

  • Habilita las APIs necesarias, si aún no están habilitadas:

    Roles necesarios para habilitar las APIs

    Para habilitar APIs, necesitas el permiso serviceusage.services.enable. Si creaste el proyecto, es probable que ya tengas este permiso a través del rol de propietario (roles/owner). De lo contrario, puedes obtenerlo a través del rol de administrador de Service Usage (roles/serviceusage.serviceUsageAdmin). Obtén más información para otorgar roles.

    gcloud services enable container.googleapis.com storage.googleapis.com compute.googleapis.com
  • Otorga roles a tu cuenta de usuario. Ejecuta el siguiente comando una vez para cada uno de los siguientes roles de IAM: roles/container.admin, roles/iam.serviceAccountAdmin, roles/storage.admin, roles/iam.serviceAccountUser

    gcloud projects add-iam-policy-binding PROJECT_ID --member="user:USER_IDENTIFIER" --role=ROLE

    Reemplaza lo siguiente:

    • PROJECT_ID: ID del proyecto
    • USER_IDENTIFIER: Es el identificador de tu cuenta de usuario de . Por ejemplo, myemail@example.com.
    • ROLE: Es el rol de IAM que otorgas a tu cuenta de usuario.

Prepara el entorno

En este instructivo, usarás Cloud Shell.

  1. Ve a la consola deGoogle Cloud .

  2. En la parte superior de la ventana de la Google Cloud consola, haz clic en el botón Activar Cloud Shell.

  3. Configura las variables de entorno:

    A4 y A3 Ultra

    Autopilot

    export PROJECT_ID=$(gcloud config get project)
    export PROJECT_NUMBER=$(gcloud projects describe ${PROJECT_ID} --format="value(projectNumber)")
    export CONTROL_PLANE_REGION="YOUR_REGION"
    export NODE_ZONE="YOUR_ZONE"
    export CLUSTER_NAME="YOUR_CLUSTER_NAME"
    export KSA_NAME="YOUR_KSA_NAME"
    export GS_BUCKET="YOUR_GCS_BUCKET"
    export NAMESPACE="default"
    export GPU_TYPE="YOUR_GPU_TYPE"
    export MACHINE_TYPE="YOUR_MACHINE_TYPE"
    export RESERVATION="YOUR_RESERVATION_NAME"
    export HF_TOKEN="YOUR_HF_TOKEN"

    Estándar

    export PROJECT_ID=$(gcloud config get project)
    export PROJECT_NUMBER=$(gcloud projects describe ${PROJECT_ID} --format="value(projectNumber)")
    export CONTROL_PLANE_REGION="YOUR_REGION"
    export NODE_ZONE="YOUR_ZONE"
    export CLUSTER_NAME="YOUR_CLUSTER_NAME"
    export KSA_NAME="YOUR_KSA_NAME"
    export GS_BUCKET="YOUR_GCS_BUCKET"
    export NAMESPACE="default"
    export GPU_TYPE="YOUR_GPU_TYPE"
    export MACHINE_TYPE="YOUR_MACHINE_TYPE"
    export CPU_MACHINE_TYPE="YOUR_CPU_MACHINE_TYPE"
    export RESERVATION="YOUR_RESERVATION_NAME"
    export HF_TOKEN="YOUR_HF_TOKEN"
    
    export GVNIC_NETWORK_PREFIX="YOUR_GVNIC_NAME"
    export RDMA_NETWORK_PREFIX="YOUR_RDMA_NAME"

    A4X

    export PROJECT_ID=$(gcloud config get project)
    export PROJECT_NUMBER=$(gcloud projects describe ${PROJECT_ID} --format="value(projectNumber)")
    export CONTROL_PLANE_REGION=YOUR_REGION
    export NODE_ZONE=YOUR_ZONE
    export CLUSTER_NAME=YOUR_CLUSTER_NAME
    export KSA_NAME=YOUR_KSA_NAME
    export GS_BUCKET=YOUR_GCS_BUCKET-${PROJECT_ID}
    export NAMESPACE=default
    export GPU_TYPE=YOUR_GPU_TYPE
    export MACHINE_TYPE=YOUR_MACINE_TYPE
    export RESERVATION=YOUR_RESERVATION_NAME
    export HF_TOKEN=YOUR_HF_TOKEN
    
    # A4X (GB200 Superchips) only variables
    export NUM_GPU_NODES=4
    export VERL_IMAGE=verlai/verl:vllm023.aarch64.dev1
    export VERL_REF=ddbcdb7
    

    Reemplaza los siguientes valores:

    • YOUR_REGION: Es la región de Compute Engine para el plano de control del clúster de GKE.
    • YOUR_ZONE: Es la zona en la que se reservan los nodos. Para obtener más información, consulta Disponibilidad de GPU.
    • YOUR_CLUSTER_NAME: el nombre del clúster de GKE. Este nombre debe tener 40 caracteres o menos y estar compuesto por caracteres alfanuméricos en minúscula y guiones.
    • YOUR_KSA_NAME: Es el nombre de tu cuenta de servicio de Kubernetes.
    • YOUR_GCS_BUCKET: Es el nombre base de tu bucket de Cloud Storage. No es necesario especificar el prefijo gs://.
    • YOUR_GPU_TYPE: es el acelerador que reservaste en la reserva de capacidad de Compute Engine. Debe ser uno de los siguientes valores:
      • nvidia-gb200: A4X (superchips GB200)
      • nvidia-b200: A4 (GPU B200)
      • nvidia-h200-141gb: A3 Ultra (GPU H200)
    • YOUR_MACHINE_TYPE: Es el tipo de máquina que se usará:
      • Para A4X (superchips GB200), usa a4x-highgpu-4g.
      • Para A4 (GPU B200), usa a4-highgpu-8g o versiones posteriores.
      • Para A3 Ultra (GPU H200), usa a3-ultragpu-8g o una versión posterior.
    • YOUR_CPU_MACHINE_TYPE: Es el tipo de máquina de Compute Engine para el grupo de nodos predeterminado de la CPU. Usa un tipo de máquina highmem con al menos 16 CPUs virtuales que sea compatible con la zona de tu clúster, como n4-highmem-16 o n2d-highmem-16.
    • YOUR_RESERVATION_NAME: Es el nombre de tu reserva de capacidad.
    • YOUR_HF_TOKEN: Es tu token de Hugging Face.
    • Usa las siguientes variables cuando configures tu entorno en clústeres de GKE Standard:
      • YOUR_GVNIC_NAME (solo para A4 o A3 Ultra): Es el prefijo del nombre de la red gVNIC. Puedes usar cualquier prefijo que desees.
      • YOUR_RDMA_NAME (solo A4 o A3 Ultra): Es el prefijo de la red de acceso directo a la memoria remota (RDMA). Puedes usar cualquier prefijo que desees.
  4. Clona el repositorio de ejemplo:

    git clone https://github.com/GoogleCloudSamples/AIHypercomputerSamples.git

    En el resto del instructivo, busca en tu clon del repositorio de muestra los archivos específicos que debes inspeccionar o aplicar.

  5. Navega al directorio de trabajo del modo de GKE que elegiste:

    A4 y A3 Ultra

    Autopilot

    cd AIHypercomputerSamples/gpu/tuning/verl_rl_autopilot

    Estándar

    cd AIHypercomputerSamples/gpu/tuning/verl_rl_standard

    A4X

    No es necesario cambiar el directorio. Puedes continuar directamente con la siguiente sección.

Configura la infraestructura

En esta sección, crearás redes de VPC estándar y el clúster de GKE.

Crea una red y subredes de RDMA (solo GKE Standard: A4 y A3 Ultra)

A4 y A3 Ultra

Autopilot

Esta sección solo es obligatoria para las GPU A4 y A3 Ultra de GKE Standard.

Si usas Autopilot, omite esta sección y continúa directamente con Crea un clúster de GKE. GKE aprovisiona automáticamente las redes y subredes de VPC necesarias, y usa DRANET administrado por GKE para asignar estos recursos a tus Pods. No es necesario que crees manualmente ninguna infraestructura de red.

Estándar

  1. Crea una red de VPC para la interfaz de gVNIC:

    gcloud compute networks create ${GVNIC_NETWORK_PREFIX}-net \
      --subnet-mode=custom \
      --project=${PROJECT_ID}
    
    gcloud compute networks subnets create ${GVNIC_NETWORK_PREFIX}-sub \
      --network=${GVNIC_NETWORK_PREFIX}-net \
      --region=${CONTROL_PLANE_REGION} \
      --range=192.168.0.0/24 \
      --project=${PROJECT_ID}
    
    gcloud compute firewall-rules create ${GVNIC_NETWORK_PREFIX}-internal \
      --network=${GVNIC_NETWORK_PREFIX}-net \
      --action=ALLOW \
      --rules=tcp:0-65535,udp:0-65535,icmp \
      --source-ranges=192.168.0.0/16 \
      --project=${PROJECT_ID}
  2. Crea una red de VPC para RDMA:

    gcloud compute networks create ${RDMA_NETWORK_PREFIX}-net \
      --network-profile=${NODE_ZONE}-vpc-roce \
      --subnet-mode=custom \
      --project=${PROJECT_ID}
  3. Crea las 8 subredes de RDMA para las 8 GPUs:

    for N in $(seq 0 7); do
      if ! gcloud compute networks subnets describe ${RDMA_NETWORK_PREFIX}-sub-$N --region=${CONTROL_PLANE_REGION} --project=${PROJECT_ID} >/dev/null 2>&1; then
        gcloud compute networks subnets create ${RDMA_NETWORK_PREFIX}-sub-$N \
          --network=${RDMA_NETWORK_PREFIX}-net \
          --region=${CONTROL_PLANE_REGION} \
          --range=192.168.$((N+1)).0/24 \
          --project=${PROJECT_ID} &
      else
        echo "Subnet ${RDMA_NETWORK_PREFIX}-sub-$N already exists."
      fi
    done
    wait

A4X

Esta sección solo es obligatoria para las GPU A4 y A3 Ultra de GKE Standard.

Si usas GPUs A4X (GB200), omite esta sección y continúa directamente con Crea un clúster de GKE. En el caso de las GPU A4X (GB200) o Autopilot, GKE crea las redes automáticamente cuando el grupo de nodos usa el perfil de red de aceleradores auto. El plano de Cluster Toolkit habilita este perfil con la marca enable_dranet:true.

Crea un clúster de GKE

Crea un clúster de GKE que corresponda a tu arquitectura de GPU:

A4 y A3 Ultra

Selecciona el modo de clúster de GKE que quieres usar:

Autopilot

  1. Crea un clúster de Autopilot:

    gcloud container clusters create-auto ${CLUSTER_NAME} \
        --location=${CONTROL_PLANE_REGION} \
        --release-channel=rapid \
        --enable-ray-operator
  2. Obtén credenciales para el clúster:

    gcloud container clusters get-credentials ${CLUSTER_NAME} \
        --location=${CONTROL_PLANE_REGION}

Estándar

  1. Crea un clúster estándar:

    gcloud container clusters create ${CLUSTER_NAME} \
        --location=${CONTROL_PLANE_REGION} \
        --node-locations=${NODE_ZONE} \
        --enable-dataplane-v2 \
        --workload-pool=${PROJECT_ID}.svc.id.goog \
        --enable-ip-alias \
        --enable-multi-networking \
        --addons=RayOperator,GcsFuseCsiDriver \
        --machine-type=${CPU_MACHINE_TYPE} \
        --num-nodes=1 \
        --min-nodes=1 \
        --max-nodes=5 \
        --enable-autoscaling \
        --project=${PROJECT_ID}
  2. Obtén credenciales para el clúster:

    gcloud container clusters get-credentials ${CLUSTER_NAME} \
        --location=${CONTROL_PLANE_REGION} \
        --project=${PROJECT_ID}
  3. Crea el grupo de nodos de GPU. Estos grupos de nodos usan tu reserva para garantizar la disponibilidad. Comienzas con dos nodos:

    RESERVATION_ARGS=("--reservation-affinity=none")
    if [ -n "${RESERVATION:-}" ]; then
      RESERVATION_ARGS=("--reservation-affinity=specific" "--reservation=${RESERVATION}")
    fi
    gcloud container node-pools create gpu-pool \
        --cluster="${CLUSTER_NAME}" \
        --location="${CONTROL_PLANE_REGION}" \
        --node-locations="${NODE_ZONE}" \
        --machine-type="${MACHINE_TYPE}" \
        --accelerator="type=${GPU_TYPE},count=8,gpu-driver-version=DEFAULT" \
        --num-nodes=2 \
        "${RESERVATION_ARGS[@]}" \
        --additional-node-network="network=${GVNIC_NETWORK_PREFIX}-net,subnetwork=${GVNIC_NETWORK_PREFIX}-sub" \
        --additional-node-network="network=${RDMA_NETWORK_PREFIX}-net,subnetwork=${RDMA_NETWORK_PREFIX}-sub-0" \
        --additional-node-network="network=${RDMA_NETWORK_PREFIX}-net,subnetwork=${RDMA_NETWORK_PREFIX}-sub-1" \
        --additional-node-network="network=${RDMA_NETWORK_PREFIX}-net,subnetwork=${RDMA_NETWORK_PREFIX}-sub-2" \
        --additional-node-network="network=${RDMA_NETWORK_PREFIX}-net,subnetwork=${RDMA_NETWORK_PREFIX}-sub-3" \
        --additional-node-network="network=${RDMA_NETWORK_PREFIX}-net,subnetwork=${RDMA_NETWORK_PREFIX}-sub-4" \
        --additional-node-network="network=${RDMA_NETWORK_PREFIX}-net,subnetwork=${RDMA_NETWORK_PREFIX}-sub-5" \
        --additional-node-network="network=${RDMA_NETWORK_PREFIX}-net,subnetwork=${RDMA_NETWORK_PREFIX}-sub-6" \
        --additional-node-network="network=${RDMA_NETWORK_PREFIX}-net,subnetwork=${RDMA_NETWORK_PREFIX}-sub-7" \
        --project="${PROJECT_ID}"
  4. Instala el instalador de RDMA de NCCL que se usa para los clústeres de Standard:

    kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/container-engine-accelerators/refs/heads/master/gpudirect-rdma/nccl-rdma-installer.yaml

A4X

  1. Crea un clúster de GKE y un grupo de nodos con el blueprint de Cluster Toolkitgke-a4x. El blueprint aprovisiona el clúster de GKE, incluido el grupo de nodos A4X vinculado a tu reserva, las redes de aceleradores (una gVNIC adicional y cuatro carriles RDMA) y el controlador DRANET administrado que expone las NIC CX-7 como dispositivos DRA.

    Usa las instrucciones de implementación del plano para configurar tus parámetros (como PROJECT_ID, CONTROL_PLANE_REGION, NODE_ZONE, reserva y NUM_GPU_NODES) y, luego, implementa el clúster. Como alternativa, puedes seguir la guía de creación de clúster de GKE A4X para crear un clúster de forma manual.

    1. Obtén credenciales para el clúster:
    gcloud container clusters get-credentials ${CLUSTER_NAME} --location=${CONTROL_PLANE_REGION}
    
  2. Verifica que el clúster exponga NIC de RDMA a través de DRA:

    kubectl get deviceclasses
    

    El resultado debe incluir mrdma.google.com.

  3. Verifica que los nodos A4X estén presentes:

    kubectl get nodes -l cloud.google.com/gke-accelerator=nvidia-gb200
    
  4. Instala el complemento NCCL de gIB (variante A4X):

    kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/container-engine-accelerators/master/gpudirect-rdma/nccl-rdma-installer-a4x.yaml
    
  5. Instala el controlador de DRA de NVIDIA, que proporciona canales ComputeDomain (IMEX) para NVLink de varios nodos:

    helm repo add nvidia https://helm.ngc.nvidia.com/nvidia && helm repo update
    kubectl create namespace nvidia-dra-driver-gpu
    kubectl apply -f - <<EOF
    apiVersion: v1
    kind: ResourceQuota
    metadata:
      name: nvidia-dra-driver-gpu-quota
      namespace: nvidia-dra-driver-gpu
    spec:
      hard:
        pods: "$((2 * NUM_GPU_NODES + 1))"
      scopeSelector:
        matchExpressions:
        - operator: In
          scopeName: PriorityClass
          values:
          - system-node-critical
          - system-cluster-critical
    EOF
    helm upgrade --install nvidia-dra-driver-gpu nvidia/nvidia-dra-driver-gpu \
      --version=25.3.1 --namespace nvidia-dra-driver-gpu \
      --set nvidiaDriverRoot=/home/kubernetes/bin/nvidia \
      --set resources.gpus.enabled=false \
      --set kubeletPlugin.tolerations[0].key=nvidia.com/gpu \
      --set kubeletPlugin.tolerations[0].operator=Exists \
      --set kubeletPlugin.tolerations[1].key=kubernetes.io/arch \
      --set kubeletPlugin.tolerations[1].operator=Exists
    
  6. Instala el operador de KubeRay, con alcance al espacio de nombres de la carga de trabajo:

    kubectl create namespace ${NAMESPACE}
    helm repo add kuberay https://ray-project.github.io/kuberay-helm/ && helm repo update
    helm upgrade --install kuberay-operator kuberay/kuberay-operator \
      --namespace ${NAMESPACE} \
      --set singleNamespaceInstall=true --set "watchNamespace={${NAMESPACE}}"
    

Configura asignaciones de red (solo GKE Standard: A4 y A3 Ultra)

A4 y A3 Ultra

Autopilot

Este paso es obligatorio para las configuraciones de GPU de GKE Standard (solo A4 y A3 Ultra). Si usas A4X (GB200), GKE administra las interfaces de red automáticamente, por lo que puedes omitir esta sección.

Estándar

  1. Inspecciona el manifiesto network-mapping.yaml:

    apiVersion: networking.gke.io/v1
    kind: GKENetworkParamSet
    metadata:
      name: gvnic-1
    spec:
      vpc: ${GVNIC_NETWORK_PREFIX}-net
      vpcSubnet: ${GVNIC_NETWORK_PREFIX}-sub
      deviceMode: NetDevice
    ---
    apiVersion: networking.gke.io/v1
    kind: Network
    metadata:
      name: gvnic-1
    spec:
      type: "Device"
      parametersRef:
        group: networking.gke.io
        kind: GKENetworkParamSet
        name: gvnic-1
    ---
    apiVersion: networking.gke.io/v1
    kind: GKENetworkParamSet
    metadata:
      name: rdma-0
    spec:
      vpc: ${RDMA_NETWORK_PREFIX}-net
      vpcSubnet: ${RDMA_NETWORK_PREFIX}-sub-0
      deviceMode: RDMA
    ---
    apiVersion: networking.gke.io/v1
    kind: Network
    metadata:
      name: rdma-0
    spec:
      type: "Device"
      parametersRef:
        group: networking.gke.io
        kind: GKENetworkParamSet
        name: rdma-0
    ---
    apiVersion: networking.gke.io/v1
    kind: GKENetworkParamSet
    metadata:
      name: rdma-1
    spec:
      vpc: ${RDMA_NETWORK_PREFIX}-net
      vpcSubnet: ${RDMA_NETWORK_PREFIX}-sub-1
      deviceMode: RDMA
    ---
    apiVersion: networking.gke.io/v1
    kind: Network
    metadata:
      name: rdma-1
    spec:
      type: "Device"
      parametersRef:
        group: networking.gke.io
        kind: GKENetworkParamSet
        name: rdma-1
    ---
    apiVersion: networking.gke.io/v1
    kind: GKENetworkParamSet
    metadata:
      name: rdma-2
    spec:
      vpc: ${RDMA_NETWORK_PREFIX}-net
      vpcSubnet: ${RDMA_NETWORK_PREFIX}-sub-2
      deviceMode: RDMA
    ---
    apiVersion: networking.gke.io/v1
    kind: Network
    metadata:
      name: rdma-2
    spec:
      type: "Device"
      parametersRef:
        group: networking.gke.io
        kind: GKENetworkParamSet
        name: rdma-2
    ---
    apiVersion: networking.gke.io/v1
    kind: GKENetworkParamSet
    metadata:
      name: rdma-3
    spec:
      vpc: ${RDMA_NETWORK_PREFIX}-net
      vpcSubnet: ${RDMA_NETWORK_PREFIX}-sub-3
      deviceMode: RDMA
    ---
    apiVersion: networking.gke.io/v1
    kind: Network
    metadata:
      name: rdma-3
    spec:
      type: "Device"
      parametersRef:
        group: networking.gke.io
        kind: GKENetworkParamSet
        name: rdma-3
    ---
    apiVersion: networking.gke.io/v1
    kind: GKENetworkParamSet
    metadata:
      name: rdma-4
    spec:
      vpc: ${RDMA_NETWORK_PREFIX}-net
      vpcSubnet: ${RDMA_NETWORK_PREFIX}-sub-4
      deviceMode: RDMA
    ---
    apiVersion: networking.gke.io/v1
    kind: Network
    metadata:
      name: rdma-4
    spec:
      type: "Device"
      parametersRef:
        group: networking.gke.io
        kind: GKENetworkParamSet
        name: rdma-4
    ---
    apiVersion: networking.gke.io/v1
    kind: GKENetworkParamSet
    metadata:
      name: rdma-5
    spec:
      vpc: ${RDMA_NETWORK_PREFIX}-net
      vpcSubnet: ${RDMA_NETWORK_PREFIX}-sub-5
      deviceMode: RDMA
    ---
    apiVersion: networking.gke.io/v1
    kind: Network
    metadata:
      name: rdma-5
    spec:
      type: "Device"
      parametersRef:
        group: networking.gke.io
        kind: GKENetworkParamSet
        name: rdma-5
    ---
    apiVersion: networking.gke.io/v1
    kind: GKENetworkParamSet
    metadata:
      name: rdma-6
    spec:
      vpc: ${RDMA_NETWORK_PREFIX}-net
      vpcSubnet: ${RDMA_NETWORK_PREFIX}-sub-6
      deviceMode: RDMA
    ---
    apiVersion: networking.gke.io/v1
    kind: Network
    metadata:
      name: rdma-6
    spec:
      type: "Device"
      parametersRef:
        group: networking.gke.io
        kind: GKENetworkParamSet
        name: rdma-6
    ---
    apiVersion: networking.gke.io/v1
    kind: GKENetworkParamSet
    metadata:
      name: rdma-7
    spec:
      vpc: ${RDMA_NETWORK_PREFIX}-net
      vpcSubnet: ${RDMA_NETWORK_PREFIX}-sub-7
      deviceMode: RDMA
    ---
    apiVersion: networking.gke.io/v1
    kind: Network
    metadata:
      name: rdma-7
    spec:
      type: "Device"
      parametersRef:
        group: networking.gke.io
        kind: GKENetworkParamSet
        name: rdma-7
  2. Aplica el manifiesto

    envsubst < network-mapping.yaml | kubectl apply -f -

A4X

Este paso es obligatorio para las configuraciones de GPU de GKE Standard (solo A4 y A3 Ultra). Si usas A4X (GB200), GKE administra las interfaces de red automáticamente, por lo que puedes omitir esta sección.

Prepara los datos y el almacenamiento

Configura los recursos de Cloud Storage y Kubernetes:

  1. Crea un bucket de Cloud Storage:

    gcloud storage buckets create "gs://${GS_BUCKET}" \
      --location="${CONTROL_PLANE_REGION}" \
      --project="${PROJECT_ID}" \
      --enable-hierarchical-namespace \
      --uniform-bucket-level-access
  2. Crea una cuenta de servicio de Kubernetes (KSA) y vincúlala al bucket:

    kubectl create serviceaccount ${KSA_NAME} -n ${NAMESPACE}
    gcloud storage buckets add-iam-policy-binding "gs://${GS_BUCKET}" \
      --member="principal://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${PROJECT_ID}.svc.id.goog/subject/ns/${NAMESPACE}/sa/${KSA_NAME}" \
      --role="roles/storage.objectUser"
  3. Crea el Secret para Hugging Face:

    kubectl create secret generic hf-secret --from-literal=hf_token=${HF_TOKEN}
  4. Inspecciona el manifiesto gcsfuse-storage.yaml:

    apiVersion: v1
    kind: PersistentVolume
    metadata:
      name: training-bucket-pv
    spec:
      accessModes:
      -   ReadWriteMany
      capacity:
        storage: 768Gi
      persistentVolumeReclaimPolicy: Delete
      storageClassName: gcsfuse-sc
      mountOptions:
      -   implicit-dirs
      -   metadata-cache:negative-ttl-secs:0
      -   metadata-cache:ttl-secs:0
      -   metadata-cache:stat-cache-max-size-mb:-1
      -   metadata-cache:type-cache-max-size-mb:-1
      -   file-cache:max-size-mb:-1
      -   file-cache:cache-file-for-range-read:true
      -   file-cache:enable-parallel-downloads:true
      -   read_ahead_kb=1024
      -   write:enable-streaming-writes:true
      -   write:global-max-blocks:200000
      csi:
        driver: gcsfuse.csi.storage.gke.io
        volumeHandle: ${GS_BUCKET}
        volumeAttributes:
          skipCSIBucketAccessCheck: "true"
          gcsfuseMetadataPrefetchOnMount: "true"
    ---
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: training-bucket-pvc
    spec:
      accessModes:
      -   ReadWriteMany
      resources:
        requests:
          storage: 768Gi
      storageClassName: gcsfuse-sc
  5. Aplica el manifiesto

    envsubst < gcsfuse-storage.yaml | kubectl apply -f - 

Configura DRANET

Configura tu DRANET:

A4 y A3 Ultra

Autopilot

  1. Crea el manifiesto de ComputeClass:

    echo "Generating computeclass-dranet.yaml..."
    cat <<EOF > computeclass-dranet.yaml
    apiVersion: cloud.google.com/v1
    kind: ComputeClass
    metadata:
      name: dranet-a4-computeclass-v3
    spec:
      nodePoolAutoCreation:
        enabled: true
      nodePoolConfig:
        dra:
          networking:
            enabled: true
      priorities:
      - machineType: ${MACHINE_TYPE}
        gpu:
          count: 8
          type: ${GPU_TYPE}
        acceleratorNetworkProfile: auto
    EOF
    
    if [ -n "${RESERVATION:-}" ]; then
      echo "Adding reservation affinity for ${RESERVATION} to ComputeClass..."
      cat <<EOF >> computeclass-dranet.yaml
        reservations:
          affinity: Specific
          specific:
          - name: ${RESERVATION}
            project: ${PROJECT_ID}
    EOF
    fi
  2. Aplica el manifiesto computeclass-dranet.yaml (creado en el paso anterior) y el manifiesto resourceclaim-dranet.yaml (incluido en el repositorio de muestra):

    echo "Applying ComputeClass..."
    kubectl apply -f computeclass-dranet.yaml
    
    echo "Applying ResourceClaimTemplate..."
    kubectl apply -f resourceclaim-dranet.yaml

Estándar

No se requiere configuración de DRANET. Puedes continuar directamente con la siguiente sección.

A4X

Cluster Toolkit establece DRANET. Puedes continuar directamente con la siguiente sección.

Prepara el modelo y los datos

Propaga tu bucket de Cloud Storage con los pesos y los conjuntos de datos del modelo. Puedes ejecutar estos comandos de forma local o en un Pod de GKE para completar el bucket:

A4 y A3 Ultra

Autopilot

  1. Inspecciona el manifiesto data-prep-job.yaml, que configura el trabajo de preparación de datos:

    apiVersion: batch/v1
    kind: Job
    metadata:
      name: data-prep-job
      namespace: ${NAMESPACE}
    spec:
      template:
        metadata:
          annotations:
            gke-gcsfuse/volumes: "true"
            gke-gcsfuse/cpu-limit: "4"
            gke-gcsfuse/memory-limit: "8Gi"
            gke-gcsfuse/ephemeral-storage-limit: "50Gi"
        spec:
          serviceAccountName: ${KSA_NAME}
          restartPolicy: OnFailure
          nodeSelector:
            cloud.google.com/compute-class: Performance
            cloud.google.com/machine-family: c3
          containers:
          - name: prep-data
            image: verlai/verl:vllm011.latest
            resources:
              requests:
                cpu: "8"
                memory: "16Gi"
                ephemeral-storage: "50Gi"
              limits:
                cpu: "8"
                memory: "16Gi"
                ephemeral-storage: "50Gi"
            env:
            - name: HF_TOKEN
              valueFrom:
                secretKeyRef:
                  name: hf-secret
                  key: hf_token
            - name: HF_HOME
              value: /data/.cache/huggingface
            - name: HF_HUB_DISABLE_XET
              value: "1"
            command: ["/bin/bash", "-c"]
            args:
            - |
              set -euo pipefail
    
              # Clone verl to GCS (for worker pods)
              if [ ! -d "/data/verl" ]; then
                echo "Cloning verl to GCS..."
                git clone --depth 1 --branch v0.6.1 https://github.com/volcengine/verl.git /data/verl
    
                echo "Installing verl on GCS to generate metadata..."
                pip3 install --no-deps -e /data/verl
              else
                echo "verl already exists in /data/verl"
              fi
    
              # Clone verl locally for fast installation
              echo "Cloning verl locally..."
              git clone --depth 1 --branch v0.6.1 https://github.com/volcengine/verl.git /tmp/verl
    
              # Install verl package from local clone
              echo "Installing verl package..."
              pip3 install --no-cache-dir --no-deps /tmp/verl
              rm -rf /tmp/verl
    
              # Preprocess GSM8K
              if [ ! -d "/data/gsm8k" ]; then
                echo "Preprocessing GSM8K..."
                python /data/verl/examples/data_preprocess/gsm8k.py --local_save_dir /data/gsm8k
              else
                echo "GSM8K data already exists in /data/gsm8k"
              fi
    
              # Download model sequentially to support GCS FUSE direct streaming writes
              if [ ! -d "/data/Qwen2.5-32B-Instruct" ]; then
                echo "Downloading Qwen2.5-32B-Instruct..."
                huggingface-cli download Qwen/Qwen2.5-32B-Instruct --local-dir /data/Qwen2.5-32B-Instruct --local-dir-use-symlinks False
              else
                echo "Model Qwen2.5-32B-Instruct already exists in /data/Qwen2.5-32B-Instruct"
              fi
    
              echo "Data preparation complete!"
            volumeMounts:
            - name: training-bucket-vol
              mountPath: /data
          volumes:
          - name: training-bucket-vol
            persistentVolumeClaim:
              claimName: training-bucket-pvc
  2. Inicia el trabajo:

    envsubst < "data-prep-job.yaml" | kubectl apply -f -
  3. Verifica el estado del Pod:

    watch kubectl get pods -n ${NAMESPACE}

    Después de que el Pod data-prep-job alcance el estado Running, presiona Control + C para dejar de supervisar.

  4. Supervisa el trabajo:

    kubectl logs -n ${NAMESPACE} -l job-name=data-prep-job -f

    El comando kubectl logs devuelve un resultado mínimo o nulo hasta que el Pod alcanza el estado Running.

Estándar

  1. Inspecciona el manifiesto data-prep-job.yaml, que configura el trabajo de preparación de datos:

    apiVersion: batch/v1
    kind: Job
    metadata:
      name: data-prep-job
      namespace: ${NAMESPACE}
    spec:
      template:
        metadata:
          annotations:
            gke-gcsfuse/volumes: "true"
            gke-gcsfuse/cpu-limit: "2"
            gke-gcsfuse/memory-limit: "4Gi"
            gke-gcsfuse/ephemeral-storage-limit: "20Gi"
        spec:
          serviceAccountName: ${KSA_NAME}
          restartPolicy: OnFailure
          nodeSelector:
            cloud.google.com/gke-nodepool: "default-pool"
          containers:
          - name: prep-data
            image: verlai/verl:vllm011.latest
            resources:
              requests:
                cpu: "4"
                memory: "8Gi"
                ephemeral-storage: "10Gi"
              limits:
                cpu: "4"
                memory: "8Gi"
                ephemeral-storage: "10Gi"
            env:
            - name: HF_TOKEN
              valueFrom:
                secretKeyRef:
                  name: hf-secret
                  key: hf_token
            - name: HF_HOME
              value: /data/.cache/huggingface
            - name: HF_HUB_DISABLE_XET
              value: "1"
            command: ["/bin/bash", "-c"]
            args:
            - |
              set -euo pipefail
    
              # Clone verl to GCS (for worker pods)
              if [ ! -d "/data/verl" ]; then
                echo "Cloning verl to GCS..."
                git clone --depth 1 --branch v0.6.1 https://github.com/volcengine/verl.git /data/verl
              else
                echo "verl already exists in /data/verl"
              fi
    
              # Clone verl locally for fast installation
              echo "Cloning verl locally..."
              git clone --depth 1 --branch v0.6.1 https://github.com/volcengine/verl.git /tmp/verl
    
              # Install verl package from local clone
              echo "Installing verl package..."
              pip3 install --no-cache-dir --no-deps /tmp/verl
              rm -rf /tmp/verl
    
              # Preprocess GSM8K
              if [ ! -d "/data/gsm8k" ]; then
                echo "Preprocessing GSM8K..."
                python /data/verl/examples/data_preprocess/gsm8k.py --local_save_dir /data/gsm8k
              else
                echo "GSM8K data already exists in /data/gsm8k"
              fi
    
              # Download model
              if [ ! -d "/data/Qwen2.5-32B-Instruct" ]; then
                echo "Downloading Qwen2.5-32B-Instruct..."
                huggingface-cli download Qwen/Qwen2.5-32B-Instruct --local-dir /data/Qwen2.5-32B-Instruct --local-dir-use-symlinks False
              else
                echo "Model Qwen2.5-32B-Instruct already exists in /data/Qwen2.5-32B-Instruct"
              fi
    
              echo "Data preparation complete!"
            volumeMounts:
            - name: training-bucket-vol
              mountPath: /data
          volumes:
          - name: training-bucket-vol
            persistentVolumeClaim:
              claimName: training-bucket-pvc
  2. Inicia el trabajo:

    envsubst < data-prep-job.yaml | kubectl apply -f -
    echo "Waiting for data-prep-job Pod to be created and pull image (this takes ~5 minutes)..."
    until [ -n "$(kubectl get pods -l job-name=data-prep-job -n "${NAMESPACE}" --no-headers 2>/dev/null)" ]; do
      sleep 2
    done
    kubectl wait --for=condition=ready pod -l job-name=data-prep-job -n "${NAMESPACE}" --timeout=600s
  3. Supervisa el trabajo:

    kubectl logs -n ${NAMESPACE} -l job-name=data-prep-job -f

A4X

  1. Clona el repositorio de verl, prepara el entorno virtual y procesa el conjunto de datos GSM8K:

    git clone https://github.com/volcengine/verl.git
    git -C verl checkout ${VERL_REF}
    
    VENV_DIR=.venv
    python3 -m venv $VENV_DIR
    source $VENV_DIR/bin/activate
    pip install verl
    
    python verl/examples/data_preprocess/gsm8k.py --local_save_dir ~/data/gsm8k
    
  2. Descarga el modelo Qwen2.5-32B-Instruct con la CLI de Hugging Face (esta descarga requiere alrededor de 66 GB de espacio en el disco):

    hf download Qwen/Qwen2.5-32B-Instruct --local-dir Qwen2.5-32B-Instruct
    
  3. Sube el modelo, los datos y el código de Verl a tu bucket de Cloud Storage:

    gcloud storage cp --recursive verl gs://${GS_BUCKET}/verl
    gcloud storage cp --recursive Qwen2.5-32B-Instruct gs://${GS_BUCKET}/Qwen2.5-32B-Instruct
    gcloud storage cp --recursive ~/data/gsm8k/* gs://${GS_BUCKET}/gsm8k/
    

Implementa el recurso personalizado RayCluster

Implementa un recurso personalizado RayCluster, que consta de un Pod principal del sistema y varios Pods de trabajador respaldados por GPU.

A4 y A3 Ultra

Selecciona el modo de clúster de GKE que usaste para crear tu clúster:

Autopilot

  1. Inspecciona la carga de trabajo de RayCluster:

    apiVersion: ray.io/v1
    kind: RayCluster
    metadata:
      name: b200-ray-cluster-dranet
    spec:
      rayVersion: '2.47.0'
      headGroupSpec:
        rayStartParams:
          dashboard-host: '0.0.0.0'
        template:
          metadata:
            annotations:
              gke-gcsfuse/volumes: "true"
          spec:
            serviceAccountName: ${KSA_NAME}
            nodeSelector:
              cloud.google.com/gke-spot: "true"
              cloud.google.com/machine-family: "c2"
              cloud.google.com/compute-class: Performance
            containers:
            - name: ray-head
              image: verlai/verl:vllm011.latest 
              ports:
                - containerPort: 6379
                  name: gcs-server
                - containerPort: 8265
                  name: dashboard
                - containerPort: 10001
                  name: client
              resources:
                limits:
                  cpu: "12"
                  memory: "32G"
                  ephemeral-storage: "9Gi"
                requests:
                  cpu: "12"
                  memory: "32G"
                  ephemeral-storage: "9Gi"
              volumeMounts:
                - mountPath: /tmp/ray
                  name: ray-logs
                - name: training-bucket-vol
                  mountPath: /data
            volumes:
              - name: ray-logs
                emptyDir: {}
              - name: training-bucket-vol
                persistentVolumeClaim:
                  claimName: training-bucket-pvc
      workerGroupSpecs:
      - replicas: 2
        minReplicas: 2
        maxReplicas: 2
        groupName: gpu-group
        rayStartParams:
          num-cpus: "220"
        template:
          metadata:
            annotations:
              gke-gcsfuse/volumes: "true"
          spec:
            resourceClaims:
              - name: rdma-claim
                resourceClaimTemplateName: all-mrdma
            initContainers:
            - name: verl-setup
              image: verlai/verl:vllm011.latest
              command: ["/bin/bash", "-c"]
              args:
                - |
                  echo "Installing GIB plugins..."
                  curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /etc/apt/trusted.gpg.d/cloud.google.gpg
                  echo 'deb https://packages.cloud.google.com/apt gpudirect-gib-apt main' | tee /etc/apt/sources.list.d/nccl-gib.list
                  apt-get update && apt-get install -y nccl-gib-plugins
    
                  echo "Local install skipped (handled by data prep)"
              volumeMounts:
              - name: training-bucket-vol
                mountPath: /data
              - name: gib
                mountPath: /usr/local/gib
            serviceAccountName: ${KSA_NAME}
            nodeSelector:
              cloud.google.com/compute-class: dranet-a4-computeclass-v3
            tolerations:
              - key: "nvidia.com/gpu"
                operator: "Exists"
                effect: "NoSchedule"
            containers:
            - name: ray-worker
              image: verlai/verl:vllm011.latest
              env:
               - name: LD_LIBRARY_PATH
                 value: /usr/local/nvidia/lib64:/usr/local/gib/lib64
              resources:
                limits:
                  cpu: "180"
                  memory: "2000Gi"
                  nvidia.com/gpu: "8"
                  ephemeral-storage: "1000Gi"
                requests:
                  cpu: "180"
                  memory: "2000Gi"
                  nvidia.com/gpu: "8"
                  ephemeral-storage: "1000Gi"
                claims:
                - name: rdma-claim
              volumeMounts:
              - name: gib
                mountPath: /usr/local/gib
                readOnly: true
              - name: shared-memory
                mountPath: /dev/shm
              - name: ray-tmp-storage
                mountPath: /tmp
              - name: training-bucket-vol
                mountPath: /data
            volumes:
            - name: gib
              emptyDir: {}
            - name: shared-memory
              emptyDir:
                medium: "Memory"
                sizeLimit: 250Gi 
            - name: ray-tmp-storage
              emptyDir: {}
            - name: training-bucket-vol
              persistentVolumeClaim:
                claimName: training-bucket-pvc
  2. Aplica el RayCluster:

    envsubst < "ray-cluster-auto-dranet.yaml" | kubectl apply -n "${NAMESPACE}" -f -

Estándar

  1. Inspecciona la carga de trabajo de RayCluster:

    apiVersion: ray.io/v1
    kind: RayCluster
    metadata:
      name: b200-ray-cluster
      annotations:
    spec:
      rayVersion: '2.47.0'
      headGroupSpec:
        rayStartParams:
          dashboard-host: '0.0.0.0'
        template:
          metadata:
            annotations:
              gke-gcsfuse/volumes: "true"
          spec:
            serviceAccountName: ${KSA_NAME}
            nodeSelector:
              cloud.google.com/gke-nodepool: "default-pool"
            containers:
            - name: ray-head
              image: verlai/verl:vllm011.latest 
              ports:
                - containerPort: 6379
                  name: gcs-server
                - containerPort: 8265
                  name: dashboard
                - containerPort: 10001
                  name: client
              resources:
                limits:
                  cpu: "12"
                  memory: "32G"
                  ephemeral-storage: "9Gi"
                requests:
                  cpu: "12"
                  memory: "32G"
                  ephemeral-storage: "9Gi"
              volumeMounts:
                - mountPath: /tmp/ray
                  name: ray-logs
                - name: training-bucket-vol
                  mountPath: /data
            volumes:
              - name: ray-logs
                emptyDir: {}
              - name: training-bucket-vol
                persistentVolumeClaim:
                  claimName: training-bucket-pvc
      workerGroupSpecs:
      - replicas: 2
        minReplicas: 2
        maxReplicas: 2
        groupName: gpu-group
        rayStartParams:
          num-cpus: "220"
        template:
          metadata:
            annotations:
              gke-gcsfuse/volumes: "true"
              networking.gke.io/default-interface: 'eth0'
              networking.gke.io/interfaces: |
                [
                  {"interfaceName":"eth0","network":"default"},
                  {"interfaceName":"eth1","network":"gvnic-1"},
                  {"interfaceName":"eth2","network":"rdma-0"},
                  {"interfaceName":"eth3","network":"rdma-1"},
                  {"interfaceName":"eth4","network":"rdma-2"},
                  {"interfaceName":"eth5","network":"rdma-3"},
                  {"interfaceName":"eth6","network":"rdma-4"},
                  {"interfaceName":"eth7","network":"rdma-5"},
                  {"interfaceName":"eth8","network":"rdma-6"},
                  {"interfaceName":"eth9","network":"rdma-7"}
                ]
          spec:
            serviceAccountName: ${KSA_NAME}
            nodeSelector:
              cloud.google.com/gke-accelerator: ${GPU_TYPE}
            tolerations:
              - key: "nvidia.com/gpu"
                operator: "Exists"
                effect: "NoSchedule"
            containers:
            - name: ray-worker
              image: verlai/verl:vllm011.latest
              env:
               - name: LD_LIBRARY_PATH
                 value: /usr/local/nvidia/lib64
              resources:
                limits:
                  cpu: "220"
                  memory: "2800Gi"
                  nvidia.com/gpu: "8"
                  ephemeral-storage: "1000Gi"
                requests:
                  cpu: "220"
                  memory: "2800Gi"
                  nvidia.com/gpu: "8"
                  ephemeral-storage: "1000Gi"
              volumeMounts:
              - name: nvidia
                mountPath: /usr/local/nvidia
              - name: gib
                mountPath: /usr/local/gib
              - name: shared-memory
                mountPath: /dev/shm
              - name: ray-tmp-storage
                mountPath: /tmp
              - name: training-bucket-vol
                mountPath: /data
            volumes:
            - name: gib
              hostPath:
                path: /home/kubernetes/bin/gib
            - name: nvidia
              hostPath:
                path: /home/kubernetes/bin/nvidia
            - name: lib64
              hostPath:
                path: /lib64
            - name: shared-memory
              emptyDir:
                medium: "Memory"
                sizeLimit: 250Gi 
            - name: sys
              hostPath:
                path: /sys
            - name: proc-sys
              hostPath:
                path: /proc/sys
            - name: ray-tmp-storage
              emptyDir: {}
            - name: training-bucket-vol
              persistentVolumeClaim:
                claimName: training-bucket-pvc
  2. Aplica el RayCluster:

    envsubst < "ray-cluster-standard.yaml" | kubectl apply -f -
    echo "Waiting for Ray head and GPU worker Pods to pull images and become Ready (this takes ~2–5 minutes)..."
    until [ -n "$(kubectl get pods -l ray.io/cluster=b200-ray-cluster -n "${NAMESPACE}" --no-headers 2>/dev/null)" ]; do
      sleep 2
    done
    kubectl wait --for=condition=ready pod -l ray.io/cluster=b200-ray-cluster -n "${NAMESPACE}" --timeout=1200s

A4X

  1. Crea el ResourceClaimTemplate de RDMA y el ComputeDomain de NVIDIA. Cada Pod de trabajador de GPU reclama cuatro NIC de RDMA (todos los rieles de su nodo) y un canal de IMEX. Guarda el siguiente manifiesto en compute-domain-a4x.yaml:

    apiVersion: resource.k8s.io/v1
    kind: ResourceClaimTemplate
    metadata:
      name: verl-rdma-nic
      namespace: ${NAMESPACE}
    spec:
      spec:
        devices:
          requests:
          - name: nic
            exactly:
              deviceClassName: mrdma.google.com
              allocationMode: ExactCount
              count: 1
    ---
    apiVersion: resource.nvidia.com/v1beta1
    kind: ComputeDomain
    metadata:
      name: verl-compute-domain
      namespace: ${NAMESPACE}
    spec:
      numNodes: ${NUM_GPU_NODES}
      channel:
        resourceClaimTemplate:
          name: verl-compute-domain-channel
    
  2. Aplica el manifiesto

    kubectl apply -f compute-domain-a4x.yaml
    
  3. Implementa el RayCluster. El Pod principal de Ray se ejecuta en un nodo A4X sin solicitar GPUs (ya que la imagen es solo para arm64). Guarda los siguientes parámetros de configuración en ray-cluster-a4x.yaml:

    apiVersion: ray.io/v1
    kind: RayCluster
    metadata:
      name: gb200-ray-cluster
      namespace: ${NAMESPACE}
    spec:
      rayVersion: '2.49.0'
      headGroupSpec:
        rayStartParams:
          dashboard-host: '0.0.0.0'
          num-cpus: "0"
        template:
          metadata:
            annotations:
              gke-gcsfuse/volumes: "true"
          spec:
            serviceAccountName: ${KSA_NAME}
            nodeSelector:
              cloud.google.com/gke-accelerator: nvidia-gb200
            tolerations:
            - key: nvidia.com/gpu
              operator: Exists
              effect: NoSchedule
            - key: kubernetes.io/arch
              operator: Exists
              effect: NoSchedule
            containers:
            - name: ray-head
              image: ${VERL_IMAGE}
              lifecycle:
                postStart:
                  exec:
                    command:
                    - /bin/bash
                    - -c
                    - pip3 install --quiet TransferQueue==0.1.8
              ports:
              - containerPort: 6379
                name: gcs-server
              - containerPort: 8265
                name: dashboard
              - containerPort: 10001
                name: client
              resources:
                limits:
                  cpu: "12"
                  memory: 32Gi
                  ephemeral-storage: 20Gi
                requests:
                  cpu: "12"
                  memory: 32Gi
                  ephemeral-storage: 20Gi
              volumeMounts:
              - mountPath: /tmp/ray
                name: ray-logs
              - name: training-bucket-vol
                mountPath: /data
            volumes:
            - name: ray-logs
              emptyDir: {}
            - name: training-bucket-vol
              persistentVolumeClaim:
                claimName: training-bucket-pvc
      workerGroupSpecs:
      - replicas: ${NUM_GPU_NODES}
        minReplicas: ${NUM_GPU_NODES}
        maxReplicas: ${NUM_GPU_NODES}
        groupName: gpu-group
        rayStartParams:
          num-cpus: "120"
        template:
          metadata:
            annotations:
              gke-gcsfuse/volumes: "true"
          spec:
            serviceAccountName: ${KSA_NAME}
            nodeSelector:
              cloud.google.com/gke-accelerator: nvidia-gb200
            affinity:
              podAntiAffinity:
                requiredDuringSchedulingIgnoredDuringExecution:
                - labelSelector:
                    matchLabels:
                      ray.io/group: gpu-group
                  topologyKey: kubernetes.io/hostname
            tolerations:
            - key: nvidia.com/gpu
              operator: Exists
              effect: NoSchedule
            - key: kubernetes.io/arch
              operator: Exists
              effect: NoSchedule
            containers:
            - name: ray-worker
              image: ${VERL_IMAGE}
              lifecycle:
                postStart:
                  exec:
                    command:
                    - /bin/bash
                    - -c
                    - pip3 install --quiet TransferQueue==0.1.8
              env:
              - name: LD_LIBRARY_PATH
                value: /usr/local/nvidia/lib64
              resources:
                limits:
                  cpu: "120"
                  memory: 600Gi
                  nvidia.com/gpu: "4"
                  ephemeral-storage: 500Gi
                requests:
                  cpu: "120"
                  memory: 600Gi
                  nvidia.com/gpu: "4"
                  ephemeral-storage: 500Gi
                claims:
                - name: rdma-nic-0
                - name: rdma-nic-1
                - name: rdma-nic-2
                - name: rdma-nic-3
                - name: compute-domain-channel
              volumeMounts:
              - name: nvidia
                mountPath: /usr/local/nvidia
              - name: gib
                mountPath: /usr/local/gib
              - name: shared-memory
                mountPath: /dev/shm
              - name: ray-tmp-storage
                mountPath: /tmp
              - name: training-bucket-vol
                mountPath: /data
            resourceClaims:
            - name: rdma-nic-0
              resourceClaimTemplateName: verl-rdma-nic
            - name: rdma-nic-1
              resourceClaimTemplateName: verl-rdma-nic
            - name: rdma-nic-2
              resourceClaimTemplateName: verl-rdma-nic
            - name: rdma-nic-3
              resourceClaimTemplateName: verl-rdma-nic
            - name: compute-domain-channel
              resourceClaimTemplateName: verl-compute-domain-channel
            volumes:
            - name: gib
              hostPath:
                path: /home/kubernetes/bin/gib
            - name: nvidia
              hostPath:
                path: /home/kubernetes/bin/nvidia
            - name: shared-memory
              emptyDir:
                medium: Memory
                sizeLimit: 200Gi
            - name: ray-tmp-storage
              emptyDir: {}
            - name: training-bucket-vol
              persistentVolumeClaim:
                claimName: training-bucket-pvc
    
  4. Aplica el manifiesto de RayCluster:

    envsubst < ray-cluster-a4x.yaml | kubectl apply -f -
    
  5. Espera hasta que un Pod principal y cuatro Pods de trabajador estén en el estado Running:

    kubectl get pods -w
    

Inicia el trabajo de GRPO

Configura y envía el trabajo de entrenamiento de aprendizaje por refuerzo:

A4 y A3 Ultra

Selecciona el modo de clúster de GKE que usaste para crear tu clúster:

Autopilot

  1. Configura el cliente de Ray:

    if [ ! -d "env" ]; then
      python3 -m venv env
    else
      echo "Found virtual environment env, not recreating"
    fi
    
    source env/bin/activate
    pip3 install --upgrade pip
    pip3 install ray[default]
  2. Recupera el servicio de encabezado de Ray:

    SVC_NAME="$(kubectl get svc -l "ray.io/node-type=head" -o jsonpath='{..metadata.name}')"
    echo "Ray head service name: ${SVC_NAME}"
  3. Configura la redirección de puertos al nodo del panel de Ray. El proceso se ejecuta en segundo plano, por lo que puedes seguir ejecutando comandos en la misma sesión de terminal.

    echo "Starting auto-restarting port-forwarding to ${SVC_NAME} on port 8265..."
    run_port_forward() {
        while true; do
            kubectl port-forward --address 0.0.0.0 svc/"${SVC_NAME}" 8265:8265 -n "${NAMESPACE}" >/dev/null 2>&1 || true
            sleep 1
        done
    }
    run_port_forward &
    PF_LOOP_PID=$!
    export RAY_ADDRESS="http://localhost:8265"

    Para detener el bucle de redirección de puertos, usa el comando kill ${PF_LOOP_PID}.

  4. Inspecciona el manifiesto runtime-env.yaml:

    py_modules: ["."]
    working_dir: "."
    excludes:
      - ".git"
      - "env"
      - "venv"
      - ".venv"
      - "__pycache__"
      - "*.pyc"
      - ".cache"
      - "*.log"
      - "*.tmp"
      - "build"
      - "dist"
    env_vars:
      PYTHONPATH: "/data/verl"
      LD_LIBRARY_PATH: "/usr/local/nvidia/lib64:/usr/local/gib/lib64"
      NCCL_DEBUG: "INFO"
      NCCL_ENV_PLUGIN: "gcp"
      NCCL_CONF_FILE: "/usr/local/gib/configs/nccl.a4.conf"
      NCCL_IB_GID_INDEX: "3"
      NCCL_NVLS_ENABLE: "0"
      NCCL_TUNER_CONFIG_PATH: "/usr/local/gib/configs"
      NUM_WORKERS: "2"
      CPUS_PER_WORKER: "192"
      GPUS_PER_WORKER: "8"
      HF_HOME: "/data/huggingface_cache"
      GLOO_SOCKET_IFNAME: "eth0"

    Si usas GPUs H200, cambia el valor de la variable de entorno NCCL_CONF_FILE por /usr/local/gib/configs/nccl.a3u.conf.

    El cliente de Ray usa este archivo. No es necesario que apliques este manifiesto al clúster.

  5. Envía el trabajo con el comando ray job submit:

    SUBMIT_OUT=$(ray job submit \
      --address "http://localhost:8265" \
      --no-wait \
      --runtime-env runtime-env.yaml \
      -- \
      bash -c "
          cd /data/verl && PYTHONUNBUFFERED=1 python3 -m verl.trainer.main_ppo \
          data.train_files=/data/gsm8k/train.parquet \
          data.val_files=/data/gsm8k/test.parquet \
          data.train_batch_size=256 \
          data.max_prompt_length=512 \
          data.max_response_length=512 \
          actor_rollout_ref.model.path=/data/Qwen2.5-32B-Instruct \
          actor_rollout_ref.actor.optim.lr=1e-5 \
          actor_rollout_ref.actor.ppo_mini_batch_size=256 \
          actor_rollout_ref.actor.ppo_micro_batch_size_per_gpu=16 \
          actor_rollout_ref.rollout.name=vllm \
          actor_rollout_ref.rollout.log_prob_micro_batch_size_per_gpu=8 \
          actor_rollout_ref.rollout.tensor_model_parallel_size=8 \
          actor_rollout_ref.rollout.gpu_memory_utilization=0.6 \
          actor_rollout_ref.ref.log_prob_micro_batch_size_per_gpu=4 \
          actor_rollout_ref.actor.strategy=fsdp2 \
          algorithm.kl_ctrl.kl_coef=0.001 \
          trainer.logger=console \
          trainer.val_before_train=False \
          trainer.n_gpus_per_node=8 \
          trainer.nnodes=2 \
          trainer.save_freq=10 \
          trainer.test_freq=10 \
          trainer.total_training_steps=10 \
          trainer.default_local_dir=/data/verl/checkpoints \
          algorithm.adv_estimator=grpo \
          actor_rollout_ref.rollout.n=8 \
          actor_rollout_ref.actor.use_torch_compile=False \
          actor_rollout_ref.actor.fsdp_config.use_torch_compile=False \
          actor_rollout_ref.ref.use_torch_compile=False \
          actor_rollout_ref.ref.fsdp_config.use_torch_compile=False \
          trainer.total_epochs=1")
    echo "${SUBMIT_OUT}"
    JOB_ID=$(echo "${SUBMIT_OUT}" | grep "submitted successfully" | awk -F"'" '{print $2}' || true)
  6. Supervisa los registros del trabajo de entrenamiento y busca la métrica critic/score/mean para ver si aumenta, lo que indica que el modelo está aprendiendo:

    • Panel de Ray:
      1. Abre http://localhost:8265 en tu navegador. Si usas Cloud Shell, haz clic en Vista previa en la Web, selecciona Cambiar puerto y, luego, ingresa 8265.
      2. Ve a la pestaña Trabajos.
      3. Haz clic en tu ID de tarea.
      4. Filtra los registros por la palabra clave critic para inspeccionar critic/score/mean.
    • Terminal: Debido a que el trabajo se envía de forma asíncrona con la marca --no-wait, transmite los registros del trabajo en tu terminal ejecutando el siguiente comando:

      ray job logs "${JOB_ID}" --address "http://localhost:8265" --follow
  7. Una vez que finalice el entrenamiento, verifica que los puntos de control del modelo se hayan guardado en tu bucket de Cloud Storage. Para ello, enumera el contenido del directorio:

    gcloud storage ls "gs://${GS_BUCKET}/verl/checkpoints/"

    Como alternativa, puedes ver los puntos de control en la consola de Google Cloud . Para ello, ve a Cloud Storage > Buckets, selecciona YOUR_GCS_BUCKET y abre la carpeta verl/checkpoints.

Estándar

  1. Configura el cliente de Ray:

    if [ ! -d "env" ]; then
      echo "Creating local virtual environment for Ray client..."
      python3 -m venv env
    else
      echo "Found virtual environment env, not recreating"
    fi
    source env/bin/activate
    pip3 install --upgrade pip
    pip3 install ray[default]
  2. Recupera el servicio de encabezado de Ray:

    SVC_NAME="$(kubectl get svc -l "ray.io/node-type=head" -o jsonpath='{..metadata.name}')"
    echo "Ray head service name: ${SVC_NAME}"
  3. Configura la redirección de puertos al nodo del panel de Ray. El proceso se ejecuta en segundo plano, por lo que puedes seguir ejecutando comandos en la misma sesión de terminal.

    echo "Starting port-forwarding loop to ${SVC_NAME} on port 8265..."
    run_port_forward() {
        while true; do
            kubectl port-forward --address 0.0.0.0 svc/"${SVC_NAME}" 8265:8265 -n "${NAMESPACE}" >/dev/null 2>&1 || true
            sleep 1
        done
    }
    run_port_forward &
    PF_PID=$!
    export RAY_ADDRESS="http://localhost:8265"
    sleep 3

    Para detener el bucle de reenvío de puertos, usa el comando kill ${PF_PID}.

  4. Inspecciona el manifiesto runtime-env.yaml:

    working_dir: "."
    excludes:
      - ".git"
      - "env"
      - ".cache"
      - ".zip"
      - ".log"
    env_vars:
      PYTHONPATH: "/data/verl"
      LD_LIBRARY_PATH: "/usr/local/nvidia/lib64"
      NCCL_DEBUG: "WARN"
      NCCL_NVLS_ENABLE: "0"
      NUM_WORKERS: "2"
      CPUS_PER_WORKER: "192"
      GPUS_PER_WORKER: "8"
      NCCL_NET_PLUGIN: "/usr/local/gib/lib64/libnccl-net_internal.so"
      NCCL_CROSS_NIC: "0"
      NCCL_NET_GDR_LEVEL: "PIX"
      NCCL_P2P_NET_CHUNKSIZE: "131072"
      NCCL_NVLS_CHUNKSIZE: "524288"
      NCCL_IB_ADAPTIVE_ROUTING: "1"
      NCCL_IB_QPS_PER_CONNECTION: "4"
      NCCL_IB_TC: "52"
      NCCL_IB_FIFO_TC: "84"
      NCCL_TUNER_CONFIG_PATH: "/usr/local/gib/configs/tuner_config_a4.txtpb"
      HF_HOME: "/data/huggingface_cache"
      GLOO_SOCKET_IFNAME: "eth0"

    Si usas GPUs H200, cambia el valor de la variable de entorno NCCL_TUNER_CONFIG_PATH por /usr/local/gib/configs/tuner_config_a3u.txtpb.

    El cliente de Ray usa este archivo. No es necesario que apliques este manifiesto al clúster.

  5. Envía el trabajo:

    SUBMIT_OUT=$(ray job submit \
      --address "http://localhost:8265" \
      --no-wait \
      --runtime-env runtime-env.yaml \
      -- \
      bash -c "
        cd /data/verl && PYTHONUNBUFFERED=1 python3 -m verl.trainer.main_ppo \
        data.train_files=/data/gsm8k/train.parquet \
        data.val_files=/data/gsm8k/test.parquet \
        data.train_batch_size=256 \
        data.max_prompt_length=512 \
        data.max_response_length=512 \
        actor_rollout_ref.model.path=/data/Qwen2.5-32B-Instruct \
        actor_rollout_ref.actor.optim.lr=1e-5 \
        actor_rollout_ref.actor.ppo_mini_batch_size=256 \
        actor_rollout_ref.actor.ppo_micro_batch_size_per_gpu=8 \
        actor_rollout_ref.rollout.name=vllm \
        actor_rollout_ref.rollout.log_prob_micro_batch_size_per_gpu=2 \
        actor_rollout_ref.rollout.tensor_model_parallel_size=8 \
        actor_rollout_ref.rollout.gpu_memory_utilization=0.5 \
        actor_rollout_ref.ref.log_prob_micro_batch_size_per_gpu=2 \
        actor_rollout_ref.actor.strategy=fsdp2 \
        algorithm.kl_ctrl.kl_coef=0.001 \
        trainer.logger=console \
        trainer.val_before_train=False \
        trainer.n_gpus_per_node=8 \
        trainer.nnodes=2 \
        trainer.save_freq=10 \
        trainer.test_freq=10 \
        trainer.total_training_steps=10 \
        trainer.default_local_dir=/data/verl/checkpoints \
        algorithm.adv_estimator=grpo \
        actor_rollout_ref.rollout.n=8 \
        trainer.total_epochs=1")
    echo "${SUBMIT_OUT}"
    JOB_ID=$(echo "${SUBMIT_OUT}" | grep "submitted successfully" | awk -F"'" '{print $2}' || true)
  6. Supervisa los registros del trabajo de entrenamiento y busca que aumente la métrica critic/score/mean, lo que indica que el modelo está aprendiendo:

    • Panel de Ray:
      1. Abre http://localhost:8265 en tu navegador. Si usas Cloud Shell, haz clic en Vista previa en la Web, selecciona Cambiar puerto y, luego, ingresa 8265.
      2. Ve a la pestaña Trabajos.
      3. Haz clic en tu ID de tarea.
      4. Filtra los registros por la palabra clave critic para inspeccionar la métrica critic/score/mean.
    • Resultado de la consola: Debido a que el trabajo se envía de forma asíncrona con la marca --no-wait, transmite los registros del trabajo en tu terminal ejecutando el siguiente comando:

      ray job logs "${JOB_ID}" --address "http://localhost:8265" --follow
  7. Una vez que finalice el entrenamiento, verifica que los puntos de control del modelo se hayan guardado en tu bucket de Cloud Storage. Para ello, enumera el contenido del directorio:

    gcloud storage ls "gs://${GS_BUCKET}/verl/checkpoints/"

    Como alternativa, consulta los puntos de control en la consola de Google Cloud en Cloud Storage > Buckets, selecciona YOUR_GCS_BUCKET y abre la carpeta verl/checkpoints.

A4X

  1. Obtén el nombre del Pod principal de Ray:

    export HEAD_POD=$(kubectl get pod -n ${NAMESPACE} -l ray.io/node-type=head -o jsonpath='{.items[0].metadata.name}')
    
  2. Configura el archivo del entorno de ejecución de Ray directamente en el Pod principal:

    kubectl exec ${HEAD_POD} -c ray-head -- bash -c 'mkdir -p /tmp/submit && cat > /tmp/submit/runtime-env.yaml <<EOF
    working_dir: "."
    env_vars:
      PYTHONPATH: "/data/verl"
      LD_LIBRARY_PATH: "/usr/local/nvidia/lib64:/usr/local/gib/lib64"
      NCCL_DEBUG: "INFO"
      NCCL_ENV_PLUGIN: "gcp"
      HF_HOME: "/data/huggingface_cache"
      GLOO_SOCKET_IFNAME: "eth0"
    EOF'
    
  3. Envía el trabajo de entrenamiento de GRPO ejecutándolo en el Pod principal de Ray:

    kubectl exec ${HEAD_POD} -c ray-head -- bash -c 'cd /tmp/submit && \
    ray job submit --runtime-env runtime-env.yaml --no-wait -- \
      python3 -m verl.trainer.main_ppo \
        algorithm.adv_estimator=grpo \
        data.train_files=/data/gsm8k/train.parquet \
        data.val_files=/data/gsm8k/test.parquet \
        data.train_batch_size=256 \
        data.max_prompt_length=512 \
        data.max_response_length=512 \
        actor_rollout_ref.model.path=/data/Qwen2.5-32B-Instruct \
        actor_rollout_ref.actor.optim.lr=1e-5 \
        actor_rollout_ref.actor.ppo_mini_batch_size=64 \
        actor_rollout_ref.actor.ppo_micro_batch_size_per_gpu=8 \
        actor_rollout_ref.actor.use_kl_loss=True \
        actor_rollout_ref.actor.strategy=fsdp2 \
        actor_rollout_ref.rollout.name=vllm \
        actor_rollout_ref.rollout.tensor_model_parallel_size=4 \
        actor_rollout_ref.rollout.gpu_memory_utilization=0.6 \
        actor_rollout_ref.rollout.n=8 \
        actor_rollout_ref.rollout.log_prob_micro_batch_size_per_gpu=16 \
        actor_rollout_ref.ref.log_prob_micro_batch_size_per_gpu=16 \
        algorithm.kl_ctrl.kl_coef=0.001 \
        trainer.logger=console \
        trainer.n_gpus_per_node=4 \
        trainer.nnodes=4 \
        trainer.save_freq=10 \
        trainer.test_freq=10 \
        trainer.total_epochs=2 \
        trainer.default_local_dir=/data/verl/checkpoints'
    
  4. Supervisa los registros del trabajo (con el ID único que muestra ray job submit):

    kubectl exec ${HEAD_POD} -c ray-head -- ray job logs <var>JOB_ID</var> --follow
    

    Reemplaza JOB_ID. Para confirmar que NVLink entre nodos esté activo, busca líneas de NCCL en los registros que contengan via P2P/MNNVL.

Realiza una limpieza

Para evitar que se generen cobros, borra los recursos:

A4 y A3 Ultra

Autopilot

  1. Si el bucle de reenvío de puertos aún se ejecuta en tu sesión en segundo plano, finalízalo:

    kill ${PF_LOOP_PID}
  2. Borra el clúster de Ray:

    envsubst < "ray-cluster-auto-dranet.yaml" | kubectl delete -f - --ignore-not-found=true || true
  3. Borra Cloud Storage FUSE:

    kubectl delete job data-prep-job -n "${NAMESPACE:-default}" --ignore-not-found=true || true
    envsubst < "gcsfuse-storage.yaml" | kubectl delete --timeout=30s -f - --ignore-not-found=true || true
  4. Borra los recursos de DRANET:

    kubectl delete -f "resourceclaim-dranet.yaml" --ignore-not-found=true 2>/dev/null || true
    kubectl delete -f "computeclass-dranet.yaml" --ignore-not-found=true 2>/dev/null || true
  5. Borra el bucket de Cloud Storage:

    gcloud storage rm -r "gs://${GS_BUCKET}" --project="${PROJECT_ID:-$(gcloud config get project)}" --quiet || {
      echo "First deletion attempt failed. Retrying in 5 seconds..."
      sleep 5
      gcloud storage rm -r "gs://${GS_BUCKET}" --project="${PROJECT_ID:-$(gcloud config get project)}" --quiet
    } || true
  6. Borra el clúster de GKE:

    gcloud container clusters delete "${CLUSTER_NAME}" --location="${CONTROL_PLANE_REGION}" --quiet || true

Estándar

  1. Si el bucle de redirección de puertos aún se está ejecutando en tu sesión en segundo plano, finalízalo:

    kill ${PF_PID}
  2. Borra el clúster de Ray:

    envsubst < "ray-cluster-standard.yaml" | kubectl delete --timeout=30s -f - --ignore-not-found=true || true
  3. Borra Cloud Storage FUSE:

    kubectl delete job data-prep-job -n ${NAMESPACE} --ignore-not-found=true || true
    envsubst < "gcsfuse-storage.yaml" | kubectl delete --timeout=30s -f - --ignore-not-found=true || true
  4. Borra el bucket de Cloud Storage:

    gcloud storage rm -r "gs://${GS_BUCKET}" --project="${PROJECT_ID}" || {
      echo "First deletion attempt failed. Retrying in 5 seconds..."
      sleep 5
      gcloud storage rm -r "gs://${GS_BUCKET}" --project="${PROJECT_ID}"
    } || true
  5. Borra el clúster de GKE:

    gcloud container clusters delete ${CLUSTER_NAME} --location=${CONTROL_PLANE_REGION} --project=${PROJECT_ID} --quiet || true
  6. Borra las redes y subredes de VPC:

    # Function to safely delete all firewall rules for a given network
    delete_network_firewalls() {
      local NET_NAME=$1
      echo "Deleting all firewall rules associated with network ${NET_NAME}..."
      for rule in $(gcloud compute firewall-rules list --filter="network:${NET_NAME}" --format="value(name)" --project=${PROJECT_ID} 2>/dev/null); do
        echo "Deleting firewall rule ${rule}..."
        gcloud compute firewall-rules delete ${rule} --project=${PROJECT_ID} --quiet || true
      done
    }
    
    # Delete Firewalls for both networks
    delete_network_firewalls "${RDMA_NETWORK_PREFIX}-net"
    delete_network_firewalls "${GVNIC_NETWORK_PREFIX}-net"
    
    # Delete RDMA subnets
    echo "Deleting RDMA subnets..."
    for N in $(seq 0 7); do
      if gcloud compute networks subnets describe ${RDMA_NETWORK_PREFIX}-sub-$N --region=${CONTROL_PLANE_REGION} --project=${PROJECT_ID} >/dev/null 2>&1; then
        echo "Deleting subnet ${RDMA_NETWORK_PREFIX}-sub-$N..."
        gcloud compute networks subnets delete ${RDMA_NETWORK_PREFIX}-sub-$N --region=${CONTROL_PLANE_REGION} --project=${PROJECT_ID} --quiet || true
      fi
    done
    
    # Delete GVNIC subnet
    if gcloud compute networks subnets describe ${GVNIC_NETWORK_PREFIX}-sub --region=${CONTROL_PLANE_REGION} --project=${PROJECT_ID} >/dev/null 2>&1; then
      echo "Deleting GVNIC subnet ${GVNIC_NETWORK_PREFIX}-sub..."
      gcloud compute networks subnets delete ${GVNIC_NETWORK_PREFIX}-sub --region=${CONTROL_PLANE_REGION} --project=${PROJECT_ID} --quiet || true
    fi
    
    # Function to delete VPC Network with retry and firewall re-purge
    delete_vpc_network_with_retry() {
      local NET_NAME=$1
      if gcloud compute networks describe ${NET_NAME} --project=${PROJECT_ID} >/dev/null 2>&1; then
        echo "Deleting network ${NET_NAME}..."
        for attempt in {1..6}; do
          delete_network_firewalls "${NET_NAME}"
          if gcloud compute networks delete ${NET_NAME} --project=${PROJECT_ID} --quiet; then
            echo "Successfully deleted network ${NET_NAME}"
            break
          fi
          echo "Failed to delete network ${NET_NAME} on attempt ${attempt}. Retrying in 10s..."
          sleep 10
        done
      fi
    }
    
    # Delete Networks safely with retry
    delete_vpc_network_with_retry "${RDMA_NETWORK_PREFIX}-net"
    delete_vpc_network_with_retry "${GVNIC_NETWORK_PREFIX}-net"

A4X

kubectl delete raycluster gb200-ray-cluster
kubectl delete computedomain verl-compute-domain
gcloud storage rm -r gs://${GS_BUCKET}
gcloud container clusters delete ${CLUSTER_NAME} --location=${CONTROL_PLANE_REGION}

¿Qué sigue?