Ajustar e escalonar o aprendizado por reforço com o verl no GKE

Neste tutorial, mostramos como orquestrar um ambiente de treinamento distribuído para aprendizado por reforço no Google Kubernetes Engine (GKE). Você vai usar o Ray e o framework verl (Volcano Engine Reinforcement Learning) para configurar um ambiente de treinamento distribuído e ajustar um modelo Qwen2.5-32B-Instruct no conjunto de dados GSM8K.

Este tutorial se concentra no pipeline de treinamento da otimização de políticas relativas a grupos (GRPO) no GKE com Ray e verl. O GRPO é um algoritmo de aprendizado por reforço projetado para melhorar a capacidade de raciocínio de um modelo. Esse algoritmo com eficiência de memória simplifica o processo de aprendizado por reforço (RL) ao eliminar o Critic, ou modelo de valor, e usar um cálculo relativo baseado em grupo.

Este tutorial é um bom ponto de partida se você precisar configurar um ambiente de treinamento distribuído em que dados, pesos do modelo e o mecanismo de treinamento sejam dissociados para aumentar a eficiência.

Este tutorial é compatível com as seguintes arquiteturas de GPU:

  • Nós de GPU baseados em Intel ou AMD: configure e escalone usando GPUs NVIDIA B200 ou H200, com a alocação dinâmica de recursos (DRA, na sigla em inglês) do GKE para o caminho do Autopilot.
  • Nós A4X (GB200) baseados em Arm: configure e escalone usando os superchips NVIDIA GB200 Grace Blackwell, com a alocação dinâmica de recursos (DRA) do GKE e o NVLink de vários nós (IMEX).

Contexto

As seções a seguir oferecem uma breve visão geral dos conceitos usados neste tutorial.

Aprendizado por reforço (RL)

A RL ensina modelos por experiência, exploração e feedback, em vez de imitação estática. Embora o pré-treinamento ensine um modelo a falar, o aprendizado por reforço com feedback humano (RLHF) ensina como ser útil, seguro e lógico. O RL serve como ponte entre um modelo de base e um modelo refinado para um caso de uso especializado.

Para mais informações, consulte O que é aprendizado por reforço?

Otimização de política relativa a grupos (GRPO)

O GRPO, um algoritmo popularizado pela DeepSeek, oferece uma alternativa com eficiência de memória à otimização de política proximal (PPO) para o alinhamento de LLM ao remover o modelo de crítica. Em vez de uma rede de críticos, o GRPO gera um grupo de respostas para o mesmo comando e usa a recompensa média desse grupo como base.

Para mais informações, consulte GRPO.

Aprendizado por reforço do Volcano Engine (verl)

O verl é um framework de alto desempenho projetado para lidar com os padrões complexos de memória e computação da RL baseada em LLMs.

Para mais informações, consulte verl.

Objetivos

Neste tutorial, mostramos como configurar o aprendizado por reforço no GKE com o verl seguindo estas etapas:

  1. Configure um cluster do GKE com A4X (GB200 Superchips), A4 (GPUs B200) ou A3 Ultra (GPUs H200).
  2. Configure o KubeRay para gerenciar um cluster do Ray distribuído.
  3. Use o Cloud Storage FUSE para montar um bucket do Cloud Storage em todos os nós.
  4. Execute um job de treinamento de GRPO usando verl para alinhar o modelo Qwen2.5-32B-Instruct com o conjunto de dados GSM8K.

Antes de começar

  • Faça login na sua conta do Google Cloud . Se você começou a usar o Google Cloudagora, crie uma conta para avaliar o desempenho dos nossos produtos em situações reais. Clientes novos também recebem US $300 em créditos para executar, testar e implantar cargas de trabalho.
  • Instale a CLI do Google Cloud.

  • Ao usar um provedor de identidade (IdP) externo, primeiro faça login na CLI gcloud com sua identidade federada.

  • Para inicializar a CLI gcloud, execute o seguinte comando:

    gcloud init
  • Crie ou selecione um Google Cloud projeto.

    Funções necessárias para selecionar ou criar um projeto

    • Selecionar um projeto: não é necessário um papel específico do IAM para selecionar um projeto. Você pode escolher qualquer projeto em que tenha recebido um papel.
    • Criar um projeto: para criar um projeto, é necessário ter o papel de Criador de projetos (roles/resourcemanager.projectCreator), que contém a permissão resourcemanager.projects.create. Saiba como conceder papéis.
    • Crie um projeto do Google Cloud :

      gcloud projects create PROJECT_ID

      Substitua PROJECT_ID por um nome para o projeto do Google Cloud que você está criando.

    • Selecione o projeto Google Cloud que você criou:

      gcloud config set project PROJECT_ID

      Substitua PROJECT_ID pelo nome do projeto do Google Cloud .

  • Verifique se o faturamento está ativado para o projeto do Google Cloud .

  • Ative as APIs necessárias, caso alguma ainda não esteja ativada:

    Funções necessárias para ativar APIs

    Para ativar APIs, você precisa da permissão serviceusage.services.enable. Se você criou o projeto, provavelmente já tem essa permissão pelo papel de Proprietário (roles/owner). Caso contrário, é possível receber essa permissão pelo papel de Administrador do Service Usage (roles/serviceusage.serviceUsageAdmin). Saiba como conceder papéis.

    gcloud services enable container.googleapis.com storage.googleapis.com compute.googleapis.com
  • Instale a CLI do Google Cloud.

  • Ao usar um provedor de identidade (IdP) externo, primeiro faça login na CLI gcloud com sua identidade federada.

  • Para inicializar a CLI gcloud, execute o seguinte comando:

    gcloud init
  • Crie ou selecione um Google Cloud projeto.

    Funções necessárias para selecionar ou criar um projeto

    • Selecionar um projeto: não é necessário um papel específico do IAM para selecionar um projeto. Você pode escolher qualquer projeto em que tenha recebido um papel.
    • Criar um projeto: para criar um projeto, é necessário ter o papel de Criador de projetos (roles/resourcemanager.projectCreator), que contém a permissão resourcemanager.projects.create. Saiba como conceder papéis.
    • Crie um projeto do Google Cloud :

      gcloud projects create PROJECT_ID

      Substitua PROJECT_ID por um nome para o projeto do Google Cloud que você está criando.

    • Selecione o projeto Google Cloud que você criou:

      gcloud config set project PROJECT_ID

      Substitua PROJECT_ID pelo nome do projeto do Google Cloud .

  • Verifique se o faturamento está ativado para o projeto do Google Cloud .

  • Ative as APIs necessárias, caso alguma ainda não esteja ativada:

    Funções necessárias para ativar APIs

    Para ativar APIs, você precisa da permissão serviceusage.services.enable. Se você criou o projeto, provavelmente já tem essa permissão pelo papel de Proprietário (roles/owner). Caso contrário, é possível receber essa permissão pelo papel de Administrador do Service Usage (roles/serviceusage.serviceUsageAdmin). Saiba como conceder papéis.

    gcloud services enable container.googleapis.com storage.googleapis.com compute.googleapis.com
  • Conceda papéis à sua conta de usuário. Execute uma vez o seguinte comando para cada um dos seguintes papéis do IAM: roles/container.admin, roles/iam.serviceAccountAdmin, roles/storage.admin, roles/iam.serviceAccountUser

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

    Substitua:

    • PROJECT_ID: o ID do projeto.
    • USER_IDENTIFIER: o identificador da sua conta de usuário . Por exemplo, myemail@example.com.
    • ROLE: o papel do IAM que você concede à sua conta de usuário.

Preparar o ambiente

Neste tutorial, você vai usar o Cloud Shell.

  1. Acesse o console doGoogle Cloud .

  2. Na parte de cima da janela do console do Google Cloud , clique no botão Ativar o Cloud Shell.

  3. Defina as variáveis de ambiente:

    A4 e A3 Ultra

    Piloto automático

    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"

    Padrão

    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
    

    Substitua os seguintes valores:

    • YOUR_REGION: a região do Compute Engine para o plano de controle do cluster do GKE.
    • YOUR_ZONE: a zona em que os nós são reservados. Para mais informações, consulte Disponibilidade de GPU.
    • YOUR_CLUSTER_NAME: o nome do cluster do GKE.
    • YOUR_KSA_NAME: o nome da conta de serviço do Kubernetes.
    • YOUR_GCS_BUCKET: o nome base do bucket do Cloud Storage. Não é necessário especificar o prefixo gs://.
    • YOUR_GPU_TYPE: o acelerador que você reservou na reserva de capacidade do Compute Engine. Precisa ser um dos seguintes valores:
      • nvidia-gb200: A4X (superchips GB200)
      • nvidia-b200: A4 (GPUs B200)
      • nvidia-h200-141gb: A3 Ultra (GPUs H200)
    • YOUR_MACHINE_TYPE: o tipo de máquina a ser usado:
      • Para A4X (GB200 Superchips), use a4x-highgpu-4g.
      • Para A4 (GPUs B200), use a4-highgpu-8g ou mais recente.
      • Para A3 Ultra (GPUs H200), use a3-ultragpu-8g ou mais recente.
    • YOUR_CPU_MACHINE_TYPE: o tipo de máquina do Compute Engine para o pool de nós padrão da CPU. Use um tipo de máquina highmem com pelo menos 16 vCPUs compatível com a zona do cluster, como n4-highmem-16 ou n2d-highmem-16.
    • YOUR_RESERVATION_NAME: o nome da sua reserva de capacidade.
    • YOUR_HF_TOKEN: seu token do Hugging Face.
    • Use as seguintes variáveis ao configurar seu ambiente em clusters do GKE Standard:
      • YOUR_GVNIC_NAME (somente A4 ou A3 Ultra): o prefixo do nome da rede gVNIC. Você pode usar qualquer prefixo.
      • YOUR_RDMA_NAME (somente A4 ou A3 Ultra): o prefixo da rede de acesso direto à memória remota (RDMA). Você pode usar qualquer prefixo que quiser.
  4. Clone o repositório de amostra:

    git clone https://github.com/GoogleCloudSamples/AIHypercomputerSamples.git
  5. Navegue até o diretório de trabalho do modo do GKE escolhido:

    A4 e A3 Ultra

    Piloto automático

    cd AIHypercomputerSamples/gpu/tuning/verl_rl_autopilot

    Padrão

    cd AIHypercomputerSamples/gpu/tuning/verl_rl_standard

    A4X

    Não é necessário mudar o diretório. Você pode passar direto para a próxima seção.

Configurar a infraestrutura

Nesta seção, você vai criar redes VPC padrão e o cluster do GKE.

Criar rede e sub-redes RDMA (GKE Standard - somente A4 e A3 Ultra)

A4 e A3 Ultra

Piloto automático

Esta seção é obrigatória apenas para GPUs A4 e A3 Ultra do GKE Standard.

Se você usa o Autopilot, pule esta seção e vá direto para Criar um cluster do GKE. O GKE provisiona automaticamente as redes e sub-redes VPC necessárias e usa o DRANET gerenciado pelo GKE para alocar esses recursos aos seus pods. Não é necessário criar manualmente nenhuma infraestrutura de rede.

Padrão

  1. Crie uma rede VPC para a 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. Crie uma rede VPC para RDMA:

    gcloud compute networks create ${RDMA_NETWORK_PREFIX}-net \
      --network-profile=${NODE_ZONE}-vpc-roce \
      --subnet-mode=custom \
      --project=${PROJECT_ID}
  3. Crie as oito sub-redes RDMA para as oito GPUs:

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

A4X

Esta seção é obrigatória apenas para GPUs A4 e A3 Ultra do GKE Standard.

Se você usar GPUs A4X (GB200), pule esta seção e vá direto para Criar um cluster do GKE. Para GPUs A4X (GB200) ou Autopilot, o GKE cria as redes automaticamente quando o pool de nós usa o perfil de rede de aceleradores auto. O blueprint do Cluster Toolkit ativa esse perfil usando a flag enable_dranet:true.

Criar um cluster do GKE

Crie um cluster do GKE correspondente à sua arquitetura de GPU:

A4 e A3 Ultra

Selecione o modo de cluster do GKE que você quer usar:

Piloto automático

  1. Criar um cluster do Autopilot:

    gcloud container clusters create-auto ${CLUSTER_NAME} \
        --location=${CONTROL_PLANE_REGION} \
        --release-channel=rapid \
        --enable-ray-operator
  2. Receba as credenciais do cluster:

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

Padrão

  1. Criar um cluster padrão

    gcloud container clusters create ${CLUSTER_NAME} \
        --location=${CONTROL_PLANE_REGION} \
        --node-locations=${NODE_ZONE} \
        --enable-dataplane-v2 \
        --workload-pool=${PROJECT_ID}.svc.id.goog \
        --enable-ip-alias \
        --enable-multi-networking \
        --addons=RayOperator,GcsFuseCsiDriver \
        --machine-type=${CPU_MACHINE_TYPE} \
        --num-nodes=1 \
        --min-nodes=1 \
        --max-nodes=5 \
        --enable-autoscaling \
        --project=${PROJECT_ID}
  2. Receba as credenciais do cluster:

    gcloud container clusters get-credentials ${CLUSTER_NAME} \
        --location=${CONTROL_PLANE_REGION} \
        --project=${PROJECT_ID}
  3. Crie o pool de nós de GPU. Esses pools de nós usam sua reserva para garantir a disponibilidade. Você começa com dois nós:

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

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

A4X

  1. Crie um cluster do GKE e um pool de nós usando o blueprint do Cluster Toolkit gke-a4x. O blueprint provisiona o cluster do GKE, incluindo o pool de nós A4X vinculado à sua reserva, as redes de aceleradores (uma gVNIC adicional mais quatro trilhos RDMA) e o driver DRANET gerenciado que expõe as NICs CX-7 como dispositivos DRA.

    Use as instruções de implantação do blueprint para configurar os parâmetros (como PROJECT_ID, CONTROL_PLANE_REGION, NODE_ZONE, reserva e NUM_GPU_NODES) e implante o cluster. Outra opção é seguir o guia de criação de cluster do GKE A4X para criar um cluster manualmente.

    1. Receba as credenciais do cluster:
    gcloud container clusters get-credentials ${CLUSTER_NAME} --location=${CONTROL_PLANE_REGION}
    
  2. Verifique se o cluster expõe NICs RDMA usando DRA:

    kubectl get deviceclasses
    

    A saída precisa incluir mrdma.google.com.

  3. Verifique se os nós A4X estão presentes:

    kubectl get nodes -l cloud.google.com/gke-accelerator=nvidia-gb200
    
  4. Instale o 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. Instale o driver NVIDIA DRA, que fornece canais ComputeDomain (IMEX) para NVLink de vários nós:

    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. Instale o operador KubeRay, com escopo para o namespace da carga de trabalho:

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

Configurar mapeamentos de rede (somente GKE Standard - A4 e A3 Ultra)

A4 e A3 Ultra

Piloto automático

Esta etapa é necessária para configurações de GPU do GKE Standard (somente A4 e A3 Ultra). Se você usa A4X (GB200), o GKE gerencia as interfaces de rede automaticamente. Portanto, pule esta seção.

Padrão

  1. Inspecione o manifesto 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. Aplique o manifesto:

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

A4X

Esta etapa é necessária para configurações de GPU do GKE Standard (somente A4 e A3 Ultra). Se você usa o A4X (GB200), o GKE gerencia as interfaces de rede automaticamente. Portanto, pule esta seção.

Preparar dados e armazenamento

Configure os recursos do Cloud Storage e do Kubernetes:

  1. Crie um bucket do Cloud Storage:

    gcloud storage buckets create "gs://${GS_BUCKET}" \
      --location="${CONTROL_PLANE_REGION}" \
      --project="${PROJECT_ID}" \
      --enable-hierarchical-namespace \
      --uniform-bucket-level-access
  2. Crie uma conta de serviço do Kubernetes (KSA) e vincule-a ao 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. Crie o secret para o Hugging Face:

    kubectl create secret generic hf-secret --from-literal=hf_token=${HF_TOKEN}
  4. Inspecione o manifesto 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. Aplique o manifesto:

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

Configurar a DRANET

Configure sua DRANET:

A4 e A3 Ultra

Piloto automático

  1. Crie o manifesto 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. Aplique o manifesto computeclass-dranet.yaml (criado na etapa anterior) e o manifesto resourceclaim-dranet.yaml (incluído no repositório de exemplo):

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

Padrão

Não é necessário configurar o DRANET. Você pode passar direto para a próxima seção.

A4X

O DRANET é definido pelo Cluster Toolkit. Você pode passar direto para a próxima seção.

Preparar modelo e dados

Preencha o bucket do Cloud Storage com os pesos e conjuntos de dados do modelo. É possível executar esses comandos localmente ou em um pod do GKE para preencher o bucket:

A4 e A3 Ultra

Piloto automático

  1. Inspecione o manifesto data-prep-job.yaml, que configura o job de preparação de dados:

    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. Inicie o job:

    envsubst < "data-prep-job.yaml" | kubectl apply -f -
  3. Verifique o status do pod:

    watch kubectl get pods -n ${NAMESPACE}

    Depois que o pod data-prep-job atingir o status Running, pressione Control+C para parar de assistir.

  4. Monitore o job:

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

    O comando kubectl logs retorna uma saída mínima ou nenhuma saída até que o pod atinja o status Running.

Padrão

  1. Inspecione o manifesto data-prep-job.yaml, que configura o job de preparação de dados:

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

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

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

A4X

  1. Clone o repositório verl, prepare o ambiente virtual e processe o conjunto de dados 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. Faça o download do modelo Qwen2.5-32B-Instruct usando a CLI do Hugging Face (o download requer cerca de 66 GB de espaço em disco):

    hf download Qwen/Qwen2.5-32B-Instruct --local-dir Qwen2.5-32B-Instruct
    
  3. Faça upload do modelo, dos dados e do código verl para o bucket do 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/
    

Implantar o recurso personalizado RayCluster

Implante um recurso personalizado RayCluster, que consiste em um pod principal do sistema e vários pods de worker com suporte de GPU.

A4 e A3 Ultra

Selecione o modo de cluster do GKE que você usou para criar o cluster:

Piloto automático

  1. Inspecione a carga de trabalho do 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. Aplique o RayCluster:

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

Padrão

  1. Inspecione a carga de trabalho do RayCluster:

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

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

A4X

  1. Crie o ResourceClaimTemplate do RDMA e o ComputeDomain da NVIDIA. Cada pod de worker de GPU reivindica quatro NICs de RDMA (todos os rails do nó) e um canal IMEX. Salve o seguinte manifesto em 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. Aplique o manifesto:

    kubectl apply -f compute-domain-a4x.yaml
    
  3. Implante o RayCluster. O pod principal do Ray é executado em um nó A4X sem solicitar GPUs, já que a imagem é somente arm64. Salve as seguintes configurações em 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. Aplique o manifesto do RayCluster:

    envsubst < ray-cluster-a4x.yaml | kubectl apply -f -
    
  5. Aguarde até que um pod principal e quatro pods de worker estejam no estado Running:

    kubectl get pods -w
    

Iniciar o job do GRPO

Configure e envie o job de treinamento de aprendizado por reforço:

A4 e A3 Ultra

Selecione o modo de cluster do GKE que você usou para criar o cluster:

Piloto automático

  1. Configure o cliente do 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. Recupere o serviço do 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. Configure o encaminhamento de portas para o nó do painel do Ray. O processo é executado em segundo plano, para que você possa continuar executando comandos na mesma sessão do terminal.

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

    Para interromper o loop de encaminhamento de porta, use o comando kill ${PF_LOOP_PID}.

  4. Inspecione o manifesto 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"

    Se você estiver usando GPUs H200, mude o valor da variável de ambiente NCCL_CONF_FILE para /usr/local/gib/configs/nccl.a3u.conf.

    Esse arquivo é usado pelo cliente do Ray. Não é necessário aplicar esse manifesto ao cluster.

  5. Envie o job usando o comando ray job submit:

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

    • Painel do Ray:
      1. Abra http://localhost:8265 no navegador. Se você estiver usando o Cloud Shell, clique em Visualização da Web, selecione Alterar porta e digite 8265.
      2. Acesse a guia Jobs.
      3. Clique no código da tarefa.
      4. Filtre os registros pela palavra-chave critic para inspecionar critic/score/mean.
    • Terminal: como o job é enviado de forma assíncrona com a flag --no-wait, transmita os registros do job no terminal executando o seguinte comando:

      ray job logs "${JOB_ID}" --address "http://localhost:8265" --follow
  7. Depois que o treinamento terminar, verifique se os checkpoints do modelo foram salvos no bucket do Cloud Storage listando o conteúdo do diretório:

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

    Como alternativa, acesse os pontos de verificação no console Google Cloud em Cloud Storage > Buckets, selecione YOUR_GCS_BUCKET e abra a pasta verl/checkpoints.

Padrão

  1. Configure o cliente do 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. Recupere o serviço do 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. Configure o encaminhamento de portas para o nó do painel do Ray. O processo é executado em segundo plano, para que você possa continuar executando comandos na mesma sessão do terminal.

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

    Para interromper o loop de encaminhamento de porta, use o comando kill ${PF_PID}.

  4. Inspecione o manifesto runtime-env.yaml:

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

    Se você estiver usando GPUs H200, mude o valor da variável de ambiente NCCL_TUNER_CONFIG_PATH para /usr/local/gib/configs/tuner_config_a3u.txtpb.

    Esse arquivo é usado pelo cliente do Ray. Não é necessário aplicar esse manifesto ao cluster.

  5. Envie o job:

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

    • Painel do Ray:
      1. Abra http://localhost:8265 no navegador. Se você estiver usando o Cloud Shell, clique em Visualização da Web, selecione Alterar porta e digite 8265.
      2. Acesse a guia Jobs.
      3. Clique no código da tarefa.
      4. Filtre os registros pela palavra-chave critic para inspecionar a métrica critic/score/mean.
    • Saída do console: como o job é enviado de forma assíncrona com a flag --no-wait, transmita os registros do job no terminal executando o seguinte comando:

      ray job logs "${JOB_ID}" --address "http://localhost:8265" --follow
  7. Depois que o treinamento terminar, verifique se os checkpoints do modelo foram salvos no bucket do Cloud Storage listando o conteúdo do diretório:

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

    Como alternativa, acesse os pontos de verificação no console Google Cloud em Cloud Storage > Buckets, selecione YOUR_GCS_BUCKET e abra a pasta verl/checkpoints.

A4X

  1. Consiga o nome do pod principal do Ray:

    export HEAD_POD=$(kubectl get pod -n ${NAMESPACE} -l ray.io/node-type=head -o jsonpath='{.items[0].metadata.name}')
    
  2. Configure o arquivo de ambiente de execução do Ray diretamente no 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. Envie o job de treinamento do GRPO executando-o no pod principal do 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. Monitore os registros do job (usando o ID exclusivo retornado por ray job submit):

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

    Substitua JOB_ID. Confirme se o NVLink entre nós está ativo procurando linhas do NCCL em registros que contenham via P2P/MNNVL.

Limpar

Para evitar cobranças, exclua os recursos:

A4 e A3 Ultra

Piloto automático

  1. Se o loop de encaminhamento de portas ainda estiver em execução na sessão em segundo plano, encerre-o:

    kill ${PF_LOOP_PID}
  2. Exclua o cluster do Ray:

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

    kubectl delete job data-prep-job -n "${NAMESPACE:-default}" --ignore-not-found=true || true
    envsubst < "gcsfuse-storage.yaml" | kubectl delete --timeout=30s -f - --ignore-not-found=true || true
  4. Exclua os recursos do 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. Exclua o bucket do Cloud Storage:

    gcloud storage rm -r "gs://${GS_BUCKET}" --project="${PROJECT_ID:-$(gcloud config get project)}" --quiet || {
      echo "First deletion attempt failed. Retrying in 5 seconds..."
      sleep 5
      gcloud storage rm -r "gs://${GS_BUCKET}" --project="${PROJECT_ID:-$(gcloud config get project)}" --quiet
    } || true
  6. Exclua o cluster do GKE:

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

Padrão

  1. Se o loop de encaminhamento de portas ainda estiver em execução na sessão em segundo plano, encerre-o:

    kill ${PF_PID}
  2. Exclua o cluster do Ray:

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

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

    gcloud storage rm -r "gs://${GS_BUCKET}" --project="${PROJECT_ID}" || {
      echo "First deletion attempt failed. Retrying in 5 seconds..."
      sleep 5
      gcloud storage rm -r "gs://${GS_BUCKET}" --project="${PROJECT_ID}"
    } || true
  5. Exclua o cluster do GKE:

    gcloud container clusters delete ${CLUSTER_NAME} --location=${CONTROL_PLANE_REGION} --project=${PROJECT_ID} --quiet || true
  6. Exclua as redes VPC e as sub-redes:

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

A seguir