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 plutôt 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 politique proximale (PPO) pour l'alignement des LLM en supprimant le modèle Critic. Au lieu d'un réseau Critic, GRPO génère un groupe de réponses pour la même requête 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 GPU 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 :

    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 :

    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

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

    Remplacez les éléments suivants :

    • PROJECT_ID : ID de votre 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 utilisez 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 RESERVATION="YOUR_RESERVATION_NAME"
    export HF_TOKEN="YOUR_HF_TOKEN"
    
    export GVNIC_NETWORK_PREFIX="GVNIC_NAME"
    export RDMA_NETWORK_PREFIX="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 la section 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_RESERVATION_NAME : nom de votre réservation de capacité.
    • YOUR_HF_TOKEN : votre jeton Hugging Face.
    • Édition Standard de Google Kubernetes Engine (GKE) uniquement :
      • GVNIC_NAME (GKE Standard – A4 ou A3 Ultra uniquement) : préfixe du nom du réseau gVNIC. Vous pouvez utiliser le préfixe de votre choix.
      • RDMA_NAME (A4 ou A3 Ultra uniquement) : préfixe du réseau RDMA (Remote Direct Memory Access). 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 est obligatoire pour les GPU GKE Standard A4 et A3 Ultra uniquement.

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 beta 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 est obligatoire pour les GPU GKE Standard A4 et A3 Ultra uniquement.

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 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=c2-standard-16 \
        --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}"
    )
    
    if [ -n "${RESERVATION:-}" ]; then
      CMD+=("--reservation-affinity=specific" "--reservation=${RESERVATION}")
    else
      CMD+=("--reservation-affinity=none")
    fi
    
    "${CMD[@]}"
  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 blueprint 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 d'interface 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 A4X pour 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 NCCL gIB (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 de 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 de 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 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 de 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 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: "50Gi"
        spec:
          serviceAccountName: ${KSA_NAME}
          restartPolicy: OnFailure
          nodeSelector:
            cloud.google.com/compute-class: Performance
          containers:
          - name: prep-data
            image: verlai/verl:vllm011.latest
            resources:
              requests:
                cpu: "4"
                memory: "8Gi"
                ephemeral-storage: "50Gi"
              limits:
                cpu: "4"
                memory: "8Gi"
                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 --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 la tâche :

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

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

Standard

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

    envsubst < "${SCRIPT_DIR}/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 "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/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
              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: shared-memory
                mountPath: /dev/shm
              - name: ray-tmp-storage
                mountPath: /tmp
              - name: training-bucket-vol
                mountPath: /data
            volumes:
            - 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 -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

  1. Configurez le client Ray :

    if [ ! -d "env" ]; then
      virtualenv -p $(which python3) env
    else
      echo "Found virtual environment env, not recreating"
    fi
    source env/bin/activate
    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. Utilisez une fenêtre de terminal distincte pour cette étape, car cette commande bloque le terminal pendant son exécution. Utilisez Ctrl+C pour l'arrêter :

    echo "Starting port-forwarding to ${SVC_NAME} on port 8265..."
    kubectl port-forward svc/"${SVC_NAME}" 8265:8265 -n "${NAMESPACE}" &
  4. Inspectez le fichier manifeste runtime-env.yaml :

    py_modules: ["."]
    working_dir": "."
    py_executable": "uv run"
    setup_hook: runtime_env.uv_runtime_env_hook.hook 
    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" 
    pip:
      packages:
        - torch 
        - torchvision
        - TransferQueue

    Si vous utilisez des GPU H200, remplacez 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 le job à l'aide de ray job submit :

    ray job submit \
      --address "http://localhost:8265" \
      --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=64 \
            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.default_local_dir=/data/verl/checkpoints \
            algorithm.adv_estimator=grpo \
            actor_rollout_ref.rollout.n=8 \
            trainer.total_epochs=2"

    Surveillez les journaux dans le tableau de bord Ray ou dans la sortie de la console. Recherchez critic/score/mean pour voir si la valeur augmente, ce qui indique un apprentissage.

  6. Une fois l'entraînement terminé, les points de contrôle du modèle entraîné se trouvent dans gs://$GS_BUCKET/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 la tâche 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 inter-nœuds 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. Supprimez le cluster Ray :

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

    envsubst < gcsfuse-storage.yaml | kubectl delete -f - --ignore-not-found=true || true
  3. Supprimez les ressources DRANET :

    kubectl delete -f "resourceclaim-dranet.yaml" --ignore-not-found=true || true
    kubectl delete -f "computeclass-dranet.yaml" --ignore-not-found=true || true
  4. Supprimez le bucket Cloud Storage :

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

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

Standard

  1. Supprimez le cluster Ray :

    envsubst < "${SCRIPT_DIR}/ray-cluster-standard.yaml" | kubectl delete -f - --ignore-not-found=true || true
  2. Supprimez Cloud Storage FUSE :

    envsubst < "${SCRIPT_DIR}/gcsfuse-storage.yaml" | kubectl delete -f - --ignore-not-found=true || true
  3. Supprimez le bucket Cloud Storage :

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

    gcloud container clusters delete ${CLUSTER_NAME} --location=${CONTROL_PLANE_REGION} --project=${PROJECT_ID} --quiet || true
  5. Supprimez les réseaux et sous-réseaux VPC :

    # Delete RDMA subnets first
    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
        gcloud compute networks subnets delete ${RDMA_NETWORK_PREFIX}-sub-$N --region=${CONTROL_PLANE_REGION} --project=${PROJECT_ID} --quiet &
      fi
    done
    wait
    
    # Delete RDMA network
    if gcloud compute networks describe ${RDMA_NETWORK_PREFIX}-net --project=${PROJECT_ID} >/dev/null 2>&1; then
      echo "Deleting firewall rules for ${RDMA_NETWORK_PREFIX}-net..."
      for rule in $(gcloud compute firewall-rules list --filter="network:${RDMA_NETWORK_PREFIX}-net" --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
      echo "Deleting RDMA network ${RDMA_NETWORK_PREFIX}-net..."
      gcloud compute networks delete ${RDMA_NETWORK_PREFIX}-net --project=${PROJECT_ID} --quiet || true
    fi
    
    # Delete GVNIC Firewall
    if gcloud compute firewall-rules describe ${GVNIC_NETWORK_PREFIX}-internal --project=${PROJECT_ID} >/dev/null 2>&1; then
      echo "Deleting firewall rule ${GVNIC_NETWORK_PREFIX}-internal..."
      gcloud compute firewall-rules delete ${GVNIC_NETWORK_PREFIX}-internal --project=${PROJECT_ID} --quiet || true
    fi
    
    # 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
    
    # Delete GVNIC network
    if gcloud compute networks describe ${GVNIC_NETWORK_PREFIX}-net --project=${PROJECT_ID} >/dev/null 2>&1; then
      echo "Deleting firewall rules for ${GVNIC_NETWORK_PREFIX}-net..."
      for rule in $(gcloud compute firewall-rules list --filter="network:${GVNIC_NETWORK_PREFIX}-net" --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
      echo "Deleting GVNIC network ${GVNIC_NETWORK_PREFIX}-net..."
      gcloud compute networks delete ${GVNIC_NETWORK_PREFIX}-net --project=${PROJECT_ID} --quiet || true
    fi

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