Affiner et mettre à l'échelle l'apprentissage par renforcement avec verl sur GKE

Ce tutoriel explique comment orchestrer un environnement d'entraînement distribué pour l'apprentissage par renforcement sur Google Kubernetes Engine (GKE). Vous utilisez Ray et le framework verl (Volcano Engine Reinforcement Learning) pour configurer un environnement d'entraînement distribué afin d'affiner un modèle Qwen2.5-32B-Instruct sur l'ensemble de données GSM8K.

Ce tutoriel se concentre sur le pipeline d'entraînement Group Relative Policy Optimization (GRPO) sur GKE avec Ray et verl. GRPO est un algorithme d'apprentissage par renforcement conçu pour améliorer la capacité de raisonnement d'un modèle. Cet algorithme économe en mémoire simplifie le processus d'apprentissage par renforcement (RL) en éliminant le Critic, ou modèle de valeur, et en utilisant à la place un calcul relatif basé sur des groupes.

Ce tutoriel est un bon point de départ si vous devez configurer un environnement d'entraînement distribué dans lequel les données, les pondérations du modèle et le moteur d'entraînement sont dissociés pour plus d'efficacité.

Ce tutoriel est compatible avec les architectures de GPU suivantes :

  • Nœuds GPU basés sur Intel ou AMD : configurez et mettez à l'échelle les GPU NVIDIA B200 ou H200 à l'aide de l'allocation dynamique des ressources (DRA) GKE pour le chemin Autopilot.
  • Nœuds A4X (GB200) basés sur Arm : configurez et mettez à l'échelle à l'aide des superchips NVIDIA GB200 Grace Blackwell, en utilisant l'allocation dynamique des ressources (DRA) et Multi-Node NVLink (IMEX) de GKE.

Arrière-plan

Les sections suivantes présentent brièvement les concepts utilisés dans ce tutoriel.

Apprentissage par renforcement

L'apprentissage par renforcement enseigne aux modèles par l'expérience, l'exploration et le retour d'informations plutôt que par l'imitation statique. Bien que le pré-entraînement apprenne à un modèle quoi dire, l'apprentissage par renforcement qui utilise le feedback humain (RLHF) lui apprend à être utile, sûr et logique. L'apprentissage par renforcement sert de pont entre un modèle de base et un modèle affiné pour un cas d'utilisation spécialisé.

Pour en savoir plus, consultez Qu'est-ce que l'apprentissage par renforcement ?

Optimisation des stratégies relatives aux groupes (GRPO)

GRPO, un algorithme popularisé par DeepSeek, offre une alternative économe en mémoire à l'optimisation de la stratégie proximale (PPO) pour l'alignement des LLM en supprimant le modèle Critic. Au lieu d'un réseau de critiques, GRPO génère un groupe de réponses pour la même invite et utilise la récompense moyenne de ce groupe comme référence.

Pour en savoir plus, consultez GRPO.

Volcano Engine Reinforcement Learning (verl)

verl est un framework hautes performances conçu pour gérer les modèles complexes de mémoire et de calcul du RL basé sur les LLM.

Pour en savoir plus, consultez verl.

Objectifs

Ce tutoriel vous explique comment configurer l'apprentissage par renforcement sur GKE avec verl en suivant les étapes suivantes :

  1. Configurez un cluster GKE avec des VM A4X (superchips GB200), A4 (GPU B200) ou A3 Ultra (GPU H200).
  2. Configurez KubeRay pour gérer un cluster Ray distribué.
  3. Utilisez Cloud Storage FUSE pour installer un bucket Cloud Storage sur tous les nœuds.
  4. Exécutez un job d'entraînement GRPO à l'aide de verl pour aligner le modèle Qwen2.5-32B-Instruct avec l'ensemble de données GSM8K.

Avant de commencer

  • Connectez-vous à votre compte Google Cloud . Si vous débutez sur Google Cloud, créez un compte pour évaluer les performances de nos produits en conditions réelles. Les nouveaux clients bénéficient également de 300 $ de crédits sans frais pour exécuter, tester et déployer des charges de travail.
  • Installez la Google Cloud CLI.

  • Si vous utilisez un fournisseur d'identité (IdP) externe, vous devez d'abord vous connecter à la gcloud CLI avec votre identité fédérée.

  • Pour initialiser la gcloud CLI, exécutez la commande suivante :

    gcloud init
  • Créez ou sélectionnez un projet Google Cloud .

    Rôles requis pour sélectionner ou créer un projet

    • Sélectionnez un projet : la sélection d'un projet ne nécessite pas de rôle IAM spécifique. Vous pouvez sélectionner n'importe quel projet pour lequel un rôle vous a été attribué.
    • Créer un projet : pour créer un projet, vous devez disposer du rôle Créateur de projet (roles/resourcemanager.projectCreator), qui contient l'autorisation resourcemanager.projects.create. Découvrez comment attribuer des rôles.
    • Créez un projet Google Cloud  :

      gcloud projects create PROJECT_ID

      Remplacez PROJECT_ID par le nom du projet Google Cloud que vous créez.

    • Sélectionnez le projet Google Cloud que vous avez créé :

      gcloud config set project PROJECT_ID

      Remplacez PROJECT_ID par le nom de votre projet Google Cloud .

  • Vérifiez que la facturation est activée pour votre projet Google Cloud .

  • Activez les API requises, si certaines ne le sont pas déjà :

    Rôles requis pour activer les API

    Pour activer les API, vous devez disposer de l'autorisation serviceusage.services.enable. Si vous avez créé le projet, vous disposez probablement déjà de cette autorisation grâce au rôle Propriétaire (roles/owner). Sinon, vous pouvez obtenir cette autorisation grâce au rôle Administrateur Service Usage (roles/serviceusage.serviceUsageAdmin). Découvrez comment attribuer des rôles.

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

  • Si vous utilisez un fournisseur d'identité (IdP) externe, vous devez d'abord vous connecter à la gcloud CLI avec votre identité fédérée.

  • Pour initialiser la gcloud CLI, exécutez la commande suivante :

    gcloud init
  • Créez ou sélectionnez un projet Google Cloud .

    Rôles requis pour sélectionner ou créer un projet

    • Sélectionnez un projet : la sélection d'un projet ne nécessite pas de rôle IAM spécifique. Vous pouvez sélectionner n'importe quel projet pour lequel un rôle vous a été attribué.
    • Créer un projet : pour créer un projet, vous devez disposer du rôle Créateur de projet (roles/resourcemanager.projectCreator), qui contient l'autorisation resourcemanager.projects.create. Découvrez comment attribuer des rôles.
    • Créez un projet Google Cloud  :

      gcloud projects create PROJECT_ID

      Remplacez PROJECT_ID par le nom du projet Google Cloud que vous créez.

    • Sélectionnez le projet Google Cloud que vous avez créé :

      gcloud config set project PROJECT_ID

      Remplacez PROJECT_ID par le nom de votre projet Google Cloud .

  • Vérifiez que la facturation est activée pour votre projet Google Cloud .

  • Activez les API requises, si certaines ne le sont pas déjà :

    Rôles requis pour activer les API

    Pour activer les API, vous devez disposer de l'autorisation serviceusage.services.enable. Si vous avez créé le projet, vous disposez probablement déjà de cette autorisation grâce au rôle Propriétaire (roles/owner). Sinon, vous pouvez obtenir cette autorisation grâce au rôle Administrateur Service Usage (roles/serviceusage.serviceUsageAdmin). Découvrez comment attribuer des rôles.

    gcloud services enable container.googleapis.com storage.googleapis.com compute.googleapis.com
  • Attribuez des rôles à votre compte utilisateur. Exécutez la commande suivante une fois pour chacun des rôles IAM suivants : 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

    Remplacez les éléments suivants :

    • PROJECT_ID : ID du projet.
    • USER_IDENTIFIER : identifiant de votre compte utilisateur . Par exemple, myemail@example.com.
    • ROLE : rôle IAM que vous accordez à votre compte utilisateur.

Préparer votre environnement

Dans ce tutoriel, vous allez utiliser Cloud Shell.

  1. Accédez à la consoleGoogle Cloud .

  2. En haut de la fenêtre de la console Google Cloud , cliquez sur le bouton Activer Cloud Shell.

  3. Définissez les variables d'environnement :

    A4 et 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"

    Standard

    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
    

    Remplacez les valeurs suivantes :

    • YOUR_REGION : région Compute Engine du plan de contrôle du cluster GKE.
    • YOUR_ZONE : zone où les nœuds sont réservés. Pour en savoir plus, consultez Disponibilité des GPU.
    • YOUR_CLUSTER_NAME : nom de votre cluster GKE.
    • YOUR_KSA_NAME : nom de votre compte de service Kubernetes.
    • YOUR_GCS_BUCKET : nom de base de votre bucket Cloud Storage. Vous n'avez pas besoin de spécifier le préfixe gs://.
    • YOUR_GPU_TYPE : accélérateur que vous avez réservé dans la réservation de capacité Compute Engine. Il doit s'agir de l'une des valeurs suivantes :
      • nvidia-gb200 : A4X (superchips GB200)
      • nvidia-b200 : A4 (GPU B200)
      • nvidia-h200-141gb : A3 Ultra (GPU H200)
    • YOUR_MACHINE_TYPE : type de machine à utiliser :
      • Pour les A4X (superchips GB200), utilisez a4x-highgpu-4g.
      • Pour les A4 (GPU B200), utilisez a4-highgpu-8g ou version ultérieure.
      • Pour les A3 Ultra (GPU H200), utilisez a3-ultragpu-8g ou une version ultérieure.
    • YOUR_CPU_MACHINE_TYPE : type de machine Compute Engine pour le pool de nœuds par défaut du processeur. Utilisez un type de machine highmem avec au moins 16 vCPU compatible avec la zone de votre cluster, comme n4-highmem-16 ou n2d-highmem-16.
    • YOUR_RESERVATION_NAME : nom de votre réservation de capacité.
    • YOUR_HF_TOKEN : votre jeton Hugging Face.
    • Utilisez les variables suivantes lorsque vous configurez votre environnement dans les clusters GKE Standard :
      • YOUR_GVNIC_NAME (A4 ou A3 Ultra uniquement) : préfixe du nom du réseau gVNIC. Vous pouvez utiliser le préfixe de votre choix.
      • YOUR_RDMA_NAME (A4 ou A3 Ultra uniquement) : préfixe du réseau d'accès direct à la mémoire à distance (RDMA). Vous pouvez utiliser le préfixe de votre choix.
  4. Clonez l'exemple de dépôt :

    git clone https://github.com/GoogleCloudSamples/AIHypercomputerSamples.git
  5. Accédez au répertoire de travail du mode GKE de votre choix :

    A4 et A3 Ultra

    Autopilot

    cd AIHypercomputerSamples/gpu/tuning/verl_rl_autopilot

    Standard

    cd AIHypercomputerSamples/gpu/tuning/verl_rl_standard

    A4X

    Aucun changement de répertoire n'est requis. Vous pouvez passer directement à la section suivante.

Configurer l'infrastructure

Dans cette section, vous allez créer des réseaux VPC standards et le cluster GKE.

Créer un réseau et des sous-réseaux RDMA (GKE Standard : A4 et A3 Ultra uniquement)

A4 et A3 Ultra

Autopilot

Cette section n'est obligatoire que pour les GPU GKE Standard A4 et A3 Ultra.

Si vous utilisez Autopilot, ignorez cette section et passez directement à Créer un cluster GKE. GKE provisionne automatiquement les réseaux et sous-réseaux VPC nécessaires, et utilise DRANET géré par GKE pour allouer ces ressources à vos pods. Vous n'avez pas besoin de créer manuellement d'infrastructure réseau.

Standard

  1. Créez un réseau VPC pour l'interface 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. Créez un réseau VPC pour RDMA :

    gcloud compute networks create ${RDMA_NETWORK_PREFIX}-net \
      --network-profile=${NODE_ZONE}-vpc-roce \
      --subnet-mode=custom \
      --project=${PROJECT_ID}
  3. Créez les huit sous-réseaux RDMA pour les huit GPU :

    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

Cette section n'est obligatoire que pour les GPU GKE Standard A4 et A3 Ultra.

Si vous utilisez des GPU A4X (GB200), ignorez cette section et passez directement à Créer un cluster GKE. Pour les GPU A4X (GB200) ou Autopilot, GKE crée automatiquement les réseaux lorsque le pool de nœuds utilise le profil réseau d'accélérateur auto. Le plan Cluster Toolkit active ce profil à l'aide de l'option enable_dranet:true.

Créer un cluster GKE

Créez un cluster GKE correspondant à votre architecture de GPU :

A4 et A3 Ultra

Sélectionnez le mode de cluster GKE que vous souhaitez utiliser :

Autopilot

  1. Créez un cluster Autopilot :

    gcloud container clusters create-auto ${CLUSTER_NAME} \
        --location=${CONTROL_PLANE_REGION} \
        --release-channel=rapid \
        --enable-ray-operator
  2. Obtenez les identifiants de votre cluster :

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

Standard

  1. Créez un cluster standard :

    gcloud container clusters create ${CLUSTER_NAME} \
        --location=${CONTROL_PLANE_REGION} \
        --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. Obtenez les identifiants de votre cluster :

    gcloud container clusters get-credentials ${CLUSTER_NAME} \
        --location=${CONTROL_PLANE_REGION} \
        --project=${PROJECT_ID}
  3. Créez le pool de nœuds GPU. Ces pools de nœuds utilisent votre réservation pour garantir la disponibilité. Vous commencez avec deux nœuds :

    CMD=(
      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"
      --enable-autoscaling
      --num-nodes=2
      --total-max-nodes=10
      --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}"
      --async
      --format="value(name)"
    )
    
    if [ -n "${RESERVATION:-}" ]; then
      CMD+=("--reservation-affinity=specific" "--reservation=${RESERVATION}")
    else
      CMD+=("--reservation-affinity=none")
    fi
    
    if ! OPERATION_ID="$("${CMD[@]}")" || [[ -z "${OPERATION_ID}" ]]; then
      echo "Error: Failed to initiate nodepool creation."
      exit 1
    fi
    
    # Wait for nodepool creation to complete
    if [[ "${NODEPOOL_TIMEOUT:-YOUR_NODEPOOL_TIMEOUT}" == "YOUR_NODEPOOL_TIMEOUT" ]]; then
      NODEPOOL_TIMEOUT="3600"
      echo "Debug: NODEPOOL_TIMEOUT is not set to a custom value, using default: ${NODEPOOL_TIMEOUT}"
    fi
    echo "Waiting for nodepool ${OPERATION_ID} to be created..."
    if ! timeout "${NODEPOOL_TIMEOUT}" gcloud container operations wait "${OPERATION_ID}" --location="${CONTROL_PLANE_REGION}" --project="${PROJECT_ID}"; then
      echo "Error: Nodepool creation timed out or failed after ${NODEPOOL_TIMEOUT} seconds."
    
      # Describe the operation to get more details
      gcloud container operations describe "${OPERATION_ID}" \
        --location="${CONTROL_PLANE_REGION}" \
        --project="${PROJECT_ID}"
      exit 1
    fi
    echo "Nodepool creation completed successfully."
  4. Installez le programme d'installation NCCL RDMA utilisé pour les clusters standards :

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

A4X

  1. Créez un cluster GKE et un pool de nœuds à l'aide du blueprint Cluster Toolkit gke-a4x. Le plan provisionne le cluster GKE, y compris le pool de nœuds A4X lié à votre réservation, les réseaux d'accélérateurs (un gVNIC supplémentaire et quatre rails RDMA) et le pilote DRANET géré qui expose les cartes réseau CX-7 en tant que périphériques DRA.

    Suivez les instructions de déploiement du blueprint pour configurer vos paramètres (tels que PROJECT_ID, CONTROL_PLANE_REGION, NODE_ZONE, la réservation et NUM_GPU_NODES), puis déployez le cluster. Vous pouvez également suivre le guide de création de cluster GKE pour A4X afin de créer un cluster manuellement.

    1. Obtenez les identifiants de votre cluster :
    gcloud container clusters get-credentials ${CLUSTER_NAME} --location=${CONTROL_PLANE_REGION}
    
  2. Vérifiez que le cluster expose les cartes d'interface réseau RDMA via DRA :

    kubectl get deviceclasses
    

    Le résultat doit inclure mrdma.google.com.

  3. Vérifiez que les nœuds A4X sont présents :

    kubectl get nodes -l cloud.google.com/gke-accelerator=nvidia-gb200
    
  4. Installez le plug-in gIB NCCL (variante A4X) :

    kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/container-engine-accelerators/master/gpudirect-rdma/nccl-rdma-installer-a4x.yaml
    
  5. Installez le pilote NVIDIA DRA, qui fournit des canaux ComputeDomain (IMEX) pour NVLink multinœud :

    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. Installez l'opérateur KubeRay, limité à l'espace de noms de la charge de travail :

    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}}"
    

Configurer les mappages réseau (GKE Standard – A4 et A3 Ultra uniquement)

A4 et A3 Ultra

Autopilot

Cette étape est requise pour les configurations GPU GKE Standard (A4 et A3 Ultra uniquement). Si vous utilisez A4X (GB200), GKE gère automatiquement les interfaces réseau. Vous pouvez donc ignorer cette section.

Standard

  1. Inspectez le fichier manifeste 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. Appliquez le fichier manifeste :

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

A4X

Cette étape est requise pour les configurations GPU GKE Standard (A4 et A3 Ultra uniquement). Si vous utilisez A4X (GB200), GKE gère automatiquement les interfaces réseau. Vous pouvez donc ignorer cette section.

Préparer les données et le stockage

Configurez les ressources Cloud Storage et Kubernetes :

  1. Créez un bucket Cloud Storage :

    gcloud storage buckets create "gs://${GS_BUCKET}" \
      --location="${CONTROL_PLANE_REGION}" \
      --project="${PROJECT_ID}" \
      --enable-hierarchical-namespace \
      --uniform-bucket-level-access
  2. Créez un compte de service Kubernetes (KSA) et associez-le au 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. Créez le Secret pour Hugging Face :

    kubectl create secret generic hf-secret --from-literal=hf_token=${HF_TOKEN}
  4. Inspectez le fichier manifeste 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. Appliquez le fichier manifeste :

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

Configurer DRANET

Configurez votre DRANET :

A4 et A3 Ultra

Autopilot

  1. Créez le fichier manifeste 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. Appliquez à la fois le fichier manifeste computeclass-dranet.yaml (créé à l'étape précédente) et le fichier manifeste resourceclaim-dranet.yaml (inclus dans l'exemple de dépôt) :

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

Standard

Aucune configuration DRANET n'est requise. Vous pouvez passer directement à la section suivante.

A4X

DRANET est défini par Cluster Toolkit. Vous pouvez passer directement à la section suivante.

Préparer le modèle et les données

Remplissez votre bucket Cloud Storage avec les pondérations et les ensembles de données du modèle. Vous pouvez exécuter ces commandes en local ou sur un pod GKE pour remplir le bucket :

A4 et A3 Ultra

Autopilot

  1. Inspectez le fichier manifeste data-prep-job.yaml, qui configure le job de préparation des données :

    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. Lancez la tâche :

    envsubst < "data-prep-job.yaml" | kubectl apply -f -
  3. Vérifiez l'état du pod :

    watch kubectl get pods -n ${NAMESPACE}

    Une fois que le pod data-prep-job a atteint l'état Running, appuyez sur Ctrl+C pour arrêter la vérification.

  4. Surveillez le job :

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

    La commande kubectl logs renvoie une sortie minimale ou nulle jusqu'à ce que le pod atteigne l'état Running.

Standard

  1. Inspectez le fichier manifeste data-prep-job.yaml, qui configure le job de préparation des données :

    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 --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 --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. Lancez le job :

    envsubst < data-prep-job.yaml | kubectl apply -f -
  3. Surveillez le job :

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

A4X

  1. Clonez le dépôt verl, préparez l'environnement virtuel et traitez l'ensemble de données 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. Téléchargez le modèle Qwen2.5-32B-Instruct à l'aide de la CLI Hugging Face (ce téléchargement nécessite environ 66 Go d'espace disque) :

    hf download Qwen/Qwen2.5-32B-Instruct --local-dir Qwen2.5-32B-Instruct
    
  3. Importez le modèle, les données et le code Verl dans votre bucket 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/
    

Déployer la ressource personnalisée RayCluster

Déployez une ressource personnalisée RayCluster, qui se compose d'un pod principal système et de plusieurs pods de nœuds de calcul compatibles avec les GPU.

A4 et A3 Ultra

Sélectionnez le mode de cluster GKE que vous avez utilisé pour créer votre cluster :

Autopilot

  1. Inspectez la charge de travail 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. Appliquez le RayCluster :

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

Standard

  1. Inspectez la charge de travail 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:
            initContainers:
            - name: verl-setup
              image: verlai/verl:vllm011.latest
              command: ["/bin/bash", "-c"]
              args:
                - |
                  echo "Performing local editable install..."
                  cd /data/verl && pip3 install --no-deps -e .
              volumeMounts:
              - name: training-bucket-vol
                mountPath: /data
            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. Appliquez le RayCluster :

    envsubst < "ray-cluster-standard.yaml" | kubectl apply -f -

A4X

  1. Créez le ResourceClaimTemplate RDMA et le ComputeDomain NVIDIA. Chaque pod de nœud de calcul GPU revendique quatre cartes d'interface réseau RDMA (tous les rails de son nœud) et un canal IMEX. Enregistrez le fichier manifeste suivant dans 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. Appliquez le fichier manifeste :

    kubectl apply -f compute-domain-a4x.yaml
    
  3. Déployez le RayCluster. Le pod principal Ray s'exécute sur un nœud A4X sans demander de GPU (car l'image est arm64 uniquement). Enregistrez les configurations suivantes dans 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. Appliquez le fichier manifeste RayCluster :

    envsubst < ray-cluster-a4x.yaml | kubectl apply -f -
    
  5. Attendez qu'un pod principal et quatre pods de nœuds de calcul soient à l'état Running :

    kubectl get pods -w
    

Lancer le job GRPO

Configurez et envoyez le job d'entraînement par apprentissage par renforcement :

A4 et A3 Ultra

Sélectionnez le mode de cluster GKE que vous avez utilisé pour créer votre cluster :

Autopilot

  1. Configurez le client 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. Récupérez le service Ray Head :

    SVC_NAME="$(kubectl get svc -l "ray.io/node-type=head" -o jsonpath='{..metadata.name}')"
    echo "Ray head service name: ${SVC_NAME}"
  3. Configurez le transfert de port vers le nœud du tableau de bord Ray. Le processus s'exécute en arrière-plan. Vous pouvez donc continuer à exécuter des commandes dans la même session de terminal.

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

    Pour arrêter la boucle de redirection de port, utilisez la commande kill ${PF_LOOP_PID}.

  4. Inspectez le fichier manifeste 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 vous utilisez des GPU H200, remplacez la valeur de la variable d'environnement NCCL_CONF_FILE par /usr/local/gib/configs/nccl.a3u.conf.

    Ce fichier est utilisé par le client Ray. Vous n'avez pas besoin d'appliquer ce fichier manifeste au cluster.

  5. Envoyez le job à l'aide de la commande 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. Surveillez les journaux du job d'entraînement et recherchez la métrique critic/score/mean qui augmente, ce qui indique que le modèle est en cours d'apprentissage :

    • Tableau de bord Ray :
      1. Ouvrez http://localhost:8265 dans votre navigateur. Si vous utilisez Cloud Shell, cliquez sur Aperçu sur le Web, sélectionnez Modifier le port, puis saisissez 8265.
      2. Accédez à l'onglet Tâches.
      3. Cliquez sur votre ID de tâche.
      4. Filtrez les journaux par le mot clé critic pour inspecter critic/score/mean.
    • Terminal : étant donné que le job est envoyé de manière asynchrone avec le flag --no-wait, diffusez les journaux de job dans votre terminal en exécutant la commande suivante :

      ray job logs "${JOB_ID}" --address "http://localhost:8265" --follow
  7. Une fois l'entraînement terminé, vérifiez que les points de contrôle du modèle ont été enregistrés dans votre bucket Cloud Storage en listant le contenu du répertoire :

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

    Vous pouvez également afficher les points de contrôle dans la console Google Cloud en accédant à Cloud Storage > Buckets, en sélectionnant YOUR_GCS_BUCKET et en ouvrant le dossier verl/checkpoints.

Standard

  1. Configurez le client 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. Récupérez le service Ray Head :

    SVC_NAME="$(kubectl get svc -l "ray.io/node-type=head" -o jsonpath='{..metadata.name}')"
    echo "Ray head service name: ${SVC_NAME}"
  3. Configurez le transfert de port vers le nœud du tableau de bord Ray. Le processus s'exécute en arrière-plan. Vous pouvez donc continuer à exécuter des commandes dans la même session de terminal.

    echo "Starting port-forwarding to ${SVC_NAME} on port 8265..."
    kubectl port-forward svc/"${SVC_NAME}" 8265:8265 -n "${NAMESPACE}" &
    PF_PID=$!

    Pour arrêter la boucle de redirection de port, utilisez la commande kill ${PF_PID}.

  4. Inspectez le fichier manifeste 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: "INFO"
      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 vous utilisez des GPU H200, remplacez la valeur de la variable d'environnement NCCL_TUNER_CONFIG_PATH par /usr/local/gib/configs/tuner_config_a3u.txtpb.

    Ce fichier est utilisé par le client Ray. Vous n'avez pas besoin d'appliquer ce fichier manifeste au cluster.

  5. Envoyez la tâche :

    ray job submit \
    --address "http://localhost:8265" \
    --runtime-env runtime-env-local.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.default_local_dir=/data/verl/checkpoints \
        algorithm.adv_estimator=grpo \
        actor_rollout_ref.rollout.n=8 \
        trainer.total_epochs=2"
  6. Surveillez les journaux du job d'entraînement et recherchez la métrique critic/score/mean qui augmente, ce qui indique que le modèle est en cours d'apprentissage :

    • Tableau de bord Ray :
      1. Ouvrez http://localhost:8265 dans votre navigateur. Si vous utilisez Cloud Shell, cliquez sur Aperçu sur le Web, sélectionnez Modifier le port, puis saisissez 8265.
      2. Accédez à l'onglet Tâches.
      3. Cliquez sur votre ID de tâche.
      4. Filtrez les journaux par le mot clé critic pour inspecter la métrique critic/score/mean.
    • Sortie de la console : étant donné que le job est envoyé de manière asynchrone avec le flag --no-wait, diffusez les journaux du job dans votre terminal en exécutant la commande suivante :

      ray job logs "${JOB_ID}" --address "http://localhost:8265" --follow
  7. Une fois l'entraînement terminé, vérifiez que les points de contrôle du modèle ont été enregistrés dans votre bucket Cloud Storage en listant le contenu du répertoire :

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

    Vous pouvez également afficher les points de contrôle dans la console Google Cloud en accédant à Cloud Storage > Buckets, en sélectionnant YOUR_GCS_BUCKET et en ouvrant le dossier verl/checkpoints.

A4X

  1. Obtenez le nom du pod principal Ray :

    export HEAD_POD=$(kubectl get pod -n ${NAMESPACE} -l ray.io/node-type=head -o jsonpath='{.items[0].metadata.name}')
    
  2. Configurez le fichier d'environnement d'exécution Ray directement sur le 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. Envoyez le job d'entraînement GRPO en l'exécutant sur le pod principal 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. Surveillez les journaux des tâches (à l'aide de l'ID unique renvoyé par ray job submit) :

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

    Remplacez JOB_ID. Vérifiez que NVLink cross-nœud est actif en recherchant les lignes NCCL dans les journaux contenant via P2P/MNNVL.

Effectuer un nettoyage

Pour éviter que des frais ne vous soient facturés, supprimez les ressources :

A4 et A3 Ultra

Autopilot

  1. Si la boucle de transfert de port est toujours en cours d'exécution dans votre session en arrière-plan, mettez-y fin :

    kill ${PF_LOOP_PID}
  2. Supprimez le cluster Ray :

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

    envsubst < "gcsfuse-storage.yaml" | kubectl delete -f - --ignore-not-found=true || true
  4. Supprimez les ressources 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. Supprimez le bucket Cloud Storage :

    gcloud storage rm -r "gs://${GS_BUCKET}" --project="${PROJECT_ID:-$(gcloud config get project)}" --quiet || true
  6. Supprimez le cluster GKE :

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

Standard

  1. Si la boucle de transfert de port est toujours en cours d'exécution dans votre session en arrière-plan, mettez-y fin :

    kill ${PF_PID}
  2. Supprimez le cluster Ray :

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

    envsubst < "gcsfuse-storage.yaml" | kubectl delete --timeout=30s -f - --ignore-not-found=true || true
  4. Supprimez le bucket Cloud Storage :

    gcloud storage rm -r "gs://${GS_BUCKET}" --project="${PROJECT_ID}" || true
  5. Supprimez le cluster GKE :

    gcloud container clusters delete ${CLUSTER_NAME} --location=${CONTROL_PLANE_REGION} --project=${PROJECT_ID} --quiet || true
  6. Supprimez les réseaux et sous-réseaux 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}

Étapes suivantes