在 GKE 上使用 Verl 微调和扩缩强化学习

本教程介绍了如何在 Google Kubernetes Engine (GKE) 上编排强化学习的分布式训练环境。您将使用 Ray 和 verl(Volcano Engine 强化学习)框架设置分布式训练环境,以便在 GSM8K 数据集上对 Qwen2.5-32B-Instruct 模型进行微调。

本教程重点介绍如何使用 Ray 和 verl 在 GKE 上运行组相对策略优化 (GRPO) 训练流水线。GRPO 是一种强化学习算法,旨在提高模型的推理能力。这种内存高效算法通过消除 Critic 或价值模型并改用基于组的相对计算,简化了强化学习 (RL) 过程。

如果您需要设置一个分布式训练环境,以便将数据、模型权重和训练引擎分离以提高效率,那么本教程是一个很好的起点。

本教程支持以下 GPU 架构:

  • 基于 Intel 或 AMD 的 GPU 节点:使用 NVIDIA B200 或 H200 GPU 设置和扩缩,并使用 GKE 动态资源分配 (DRA) 实现 Autopilot 路径。
  • 基于 Arm 的 A4X (GB200) 节点:使用 NVIDIA GB200 Grace Blackwell 超级芯片、GKE 动态资源分配 (DRA) 和多节点 NVLink (IMEX) 进行设置和扩缩。

背景

以下部分简要介绍了本教程中使用的概念。

强化学习 (RL)

RL 通过经验、探索和反馈来训练模型,而不是静态模仿。虽然预训练会教模型说什么,但基于人类反馈的强化学习 (RLHF) 会教模型如何提供帮助、安全和合乎逻辑。RL 是基础模型与针对特定应用场景进行微调的模型之间的桥梁。

如需了解详情,请参阅什么是强化学习?

群组相对政策优化 (GRPO)

GRPO 是一种由 DeepSeek 推广的算法,它通过移除 Critic 模型,为 LLM 对齐提供了一种比近端策略优化 (PPO) 更节省内存的替代方案。GRPO 不使用 Critic 网络,而是针对同一提示生成一组回答,并以该组回答的平均奖励作为基准。

如需了解详情,请参阅 GRPO。

Volcano Engine 强化学习 (verl)

verl 是一个高性能框架,旨在处理基于 LLM 的 RL 的复杂内存和计算模式。

如需了解详情,请参阅 verl。

目标

本教程将介绍如何通过完成以下步骤,在 GKE 上使用 verl 设置强化学习:

  1. 设置一个包含 A4X(GB200 超级芯片)、A4(B200 GPU)或 A3 Ultra(H200 GPU)的 GKE 集群。
  2. 配置 KubeRay 以管理分布式 Ray 集群。
  3. 使用 Cloud Storage FUSE 在所有节点上装载 Cloud Storage 存储桶。
  4. 使用 verl 运行 GRPO 训练作业,使 Qwen2.5-32B-Instruct 模型与 GSM8K 数据集保持一致。

准备工作

  • 登录您的 Google Cloud 账号。如果您是 Google Cloud新手,请 创建一个账号来评估我们的产品在实际场景中的表现。新客户还可获享 $300 赠金,用于运行、测试和部署工作负载。
  • 安装 Google Cloud CLI。

  • 如果您使用的是外部身份提供方 (IdP),则必须先 使用联合身份登录 gcloud CLI。

  • 如需初始化 gcloud CLI,请运行以下命令:

    gcloud init
  • 创建或选择 Google Cloud 项目。

    选择或创建项目所需的角色

    • 选择项目:选择项目不需要特定的 IAM 角色,您可以选择已获授角色的任何项目。
    • 创建项目:如需创建项目,您需要拥有 Project Creator 角色 (roles/resourcemanager.projectCreator),该角色包含 resourcemanager.projects.create 权限。了解如何授予角色。
    • 创建 Google Cloud 项目:

      gcloud projects create PROJECT_ID

      将 PROJECT_ID 替换为您要创建的 Google Cloud 项目的名称。

    • 选择您创建的 Google Cloud 项目:

      gcloud config set project PROJECT_ID

      将 PROJECT_ID 替换为您的 Google Cloud 项目名称。

  • 验证您的 Google Cloud 项目是否已启用结算功能。

  • 启用所需的 API(如果尚未启用):

    启用 API 所需的角色

    如需启用 API,您需要拥有 serviceusage.services.enable 权限。如果您创建了项目,则可能已经通过 Owner 角色 (roles/owner) 拥有此权限。否则,您可以通过 Service Usage Admin 角色 (roles/serviceusage.serviceUsageAdmin) 获得此权限。了解如何授予角色。

    gcloud services enable container.googleapis.com storage.googleapis.com compute.googleapis.com
  • 安装 Google Cloud CLI。

  • 如果您使用的是外部身份提供方 (IdP),则必须先 使用联合身份登录 gcloud CLI。

  • 如需初始化 gcloud CLI,请运行以下命令:

    gcloud init
  • 创建或选择 Google Cloud 项目。

    选择或创建项目所需的角色

    • 选择项目:选择项目不需要特定的 IAM 角色,您可以选择已获授角色的任何项目。
    • 创建项目:如需创建项目,您需要拥有 Project Creator 角色 (roles/resourcemanager.projectCreator),该角色包含 resourcemanager.projects.create 权限。了解如何授予角色。
    • 创建 Google Cloud 项目:

      gcloud projects create PROJECT_ID

      将 PROJECT_ID 替换为您要创建的 Google Cloud 项目的名称。

    • 选择您创建的 Google Cloud 项目:

      gcloud config set project PROJECT_ID

      将 PROJECT_ID 替换为您的 Google Cloud 项目名称。

  • 验证您的 Google Cloud 项目是否已启用结算功能。

  • 启用所需的 API(如果尚未启用):

    启用 API 所需的角色

    如需启用 API,您需要拥有 serviceusage.services.enable 权限。如果您创建了项目,则可能已经通过 Owner 角色 (roles/owner) 拥有此权限。否则,您可以通过 Service Usage Admin 角色 (roles/serviceusage.serviceUsageAdmin) 获得此权限。了解如何授予角色。

    gcloud services enable container.googleapis.com storage.googleapis.com compute.googleapis.com
  • 向您的用户账号授予角色。对以下每个 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

    替换以下内容:

    • PROJECT_ID:您的项目 ID。
    • USER_IDENTIFIER:您的用户账号的标识符。 例如,myemail@example.com。
    • ROLE:您向用户账号授予的 IAM 角色。

准备环境

在本教程中,您将使用 Cloud Shell。

  1. 前往 Google Cloud 控制台。

  2. 在 Google Cloud 控制台窗口顶部,点击激活 Cloud Shell 按钮。

  3. 设置环境变量:

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

    标准

    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
    

    替换以下值:

    • YOUR_REGION:GKE 集群控制平面的 Compute Engine 区域。
    • YOUR_ZONE:预留节点的可用区。如需了解详情,请参阅 GPU 可用性。
    • YOUR_CLUSTER_NAME:GKE 集群的名称。此名称不得超过 40 个字符,且必须由小写字母数字字符和连字符组成。
    • YOUR_KSA_NAME:您的 Kubernetes 服务账号的名称。
    • YOUR_GCS_BUCKET:Cloud Storage 存储桶的基本名称。您无需指定 gs:// 前缀。
    • YOUR_GPU_TYPE:您在 Compute Engine 容量预留中预留的加速器。必须是下列值之一:
      • nvidia-gb200:A4X(GB200 超级芯片)
      • nvidia-b200:A4(B200 GPU)
      • nvidia-h200-141gb:A3 Ultra(H200 GPU)
    • YOUR_MACHINE_TYPE:要使用的机器类型:
      • 对于 A4X(GB200 超级芯片),请使用 a4x-highgpu-4g。
      • 对于 A4(B200 GPU),请使用 a4-highgpu-8g 或更高版本。
      • 对于 A3 Ultra(H200 GPU),请使用 a3-ultragpu-8g 或更高版本。
    • YOUR_CPU_MACHINE_TYPE:CPU 默认节点池的 Compute Engine 机器类型。使用集群可用区中支持的、至少具有 16 个 vCPU 的 highmem 机器类型,例如 n4-highmem-16 或 n2d-highmem-16。
    • YOUR_RESERVATION_NAME:容量预留的名称。
    • YOUR_HF_TOKEN:您的 Hugging Face 令牌。
    • 在 GKE Standard 集群中设置环境时,请使用以下变量:
      • YOUR_GVNIC_NAME(仅限 A4 或 A3 Ultra):gVNIC 网络名称的前缀。您可以使用所需的任何前缀。
      • YOUR_RDMA_NAME(仅限 A4 或 A3 Ultra):远程直接内存访问 (RDMA) 网络的前缀。您可以使用所需的任何前缀。
  4. 克隆示例代码库:

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

    在本教程的其余部分中,请在您克隆的示例代码库中查找需要检查或应用的具体文件。

  5. 前往所选 GKE 模式的工作目录:

    A4 和 A3 Ultra

    Autopilot

    cd AIHypercomputerSamples/gpu/tuning/verl_rl_autopilot

    标准

    cd AIHypercomputerSamples/gpu/tuning/verl_rl_standard

    A4X

    无需更改目录。您可以直接进入下一部分。

设置基础架构

在本部分中,您将创建标准 VPC 网络和 GKE 集群。

创建 RDMA 网络和子网(仅限 GKE Standard - A4 和 A3 Ultra)

A4 和 A3 Ultra

Autopilot

本部分仅适用于 GKE Standard A4 和 A3 Ultra GPU。

如果您使用 Autopilot,请跳过此部分,直接前往创建 GKE 集群。GKE 会自动预配必要的 VPC 网络和子网,并使用 GKE 托管的 DRANET 将这些资源分配给 Pod。您无需手动创建任何网络基础设施。

标准

  1. 为 gVNIC 接口创建 VPC 网络:

    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. 为 RDMA 创建 VPC 网络:

    gcloud compute networks create ${RDMA_NETWORK_PREFIX}-net \
      --network-profile=${NODE_ZONE}-vpc-roce \
      --subnet-mode=custom \
      --project=${PROJECT_ID}
  3. 为 8 个 GPU 创建 8 个 RDMA 子网:

    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

本部分仅适用于 GKE Standard A4 和 A3 Ultra GPU。

如果您使用 A4X (GB200) GPU,请跳过本部分,直接前往创建 GKE 集群。对于 A4X (GB200) GPU 或 Autopilot,当节点池使用 auto 加速器网络配置方案时,GKE 会自动创建网络。Cluster Toolkit 蓝图使用 enable_dranet:true 标志启用此配置文件。

创建 GKE 集群

创建与 GPU 架构对应的 GKE 集群:

A4 和 A3 Ultra

选择您要使用的 GKE 集群模式:

Autopilot

  1. 创建 Autopilot 集群:

    gcloud container clusters create-auto ${CLUSTER_NAME} \
        --location=${CONTROL_PLANE_REGION} \
        --release-channel=rapid \
        --enable-ray-operator
  2. 获取集群的凭据:

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

标准

  1. 创建 Standard 集群:

    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. 获取集群的凭据:

    gcloud container clusters get-credentials ${CLUSTER_NAME} \
        --location=${CONTROL_PLANE_REGION} \
        --project=${PROJECT_ID}
  3. 创建 GPU 节点池。这些节点池使用您的预留来确保可用性。您从两个节点开始:

    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. 安装用于标准集群的 NCCL RDMA 安装程序:

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

A4X

  1. 使用 Cluster Toolkit gke-a4x 蓝图创建 GKE 集群和节点池。此蓝图可预配 GKE 集群,包括绑定到预留的 A4X 节点池、加速器网络(一个额外的 gVNIC 加四个 RDMA 轨道)以及将 CX-7 NIC 作为 DRA 设备公开的受管 DRANET 驱动程序。

    使用蓝图的部署说明配置参数(例如 PROJECT_ID、CONTROL_PLANE_REGION、NODE_ZONE、预留和 NUM_GPU_NODES),然后部署集群。或者,您也可以按照 A4X GKE 集群创建指南手动创建集群。

    1. 获取集群的凭据:
    gcloud container clusters get-credentials ${CLUSTER_NAME} --location=${CONTROL_PLANE_REGION}
    
  2. 验证集群是否通过 DRA 公开 RDMA NIC:

    kubectl get deviceclasses
    

    输出必须包含 mrdma.google.com。

  3. 验证 A4X 节点是否存在:

    kubectl get nodes -l cloud.google.com/gke-accelerator=nvidia-gb200
    
  4. 安装 gIB NCCL 插件(A4X 变体):

    kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/container-engine-accelerators/master/gpudirect-rdma/nccl-rdma-installer-a4x.yaml
    
  5. 安装 NVIDIA DRA 驱动程序,该驱动程序可为多节点 NVLink 提供 ComputeDomain (IMEX) 通道:

    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. 安装 KubeRay operator,范围限定为工作负载命名空间:

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

配置网络映射(仅限 GKE Standard - A4 和 A3 Ultra)

A4 和 A3 Ultra

Autopilot

对于 GKE Standard GPU 设置(仅限 A4 和 A3 Ultra),此步骤是必需的。如果您使用 A4X (GB200),GKE 会自动管理网络接口,因此请跳过此部分。

标准

  1. 检查 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. 应用清单:

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

A4X

对于 GKE Standard GPU 设置(仅限 A4 和 A3 Ultra),此步骤是必需的。 如果您使用 A4X (GB200),GKE 会自动管理网络接口,因此请跳过此部分。

准备数据和存储空间

配置 Cloud Storage 和 Kubernetes 资源:

  1. 创建 Cloud Storage 存储桶,请运行以下命令:

    gcloud storage buckets create "gs://${GS_BUCKET}" \
      --location="${CONTROL_PLANE_REGION}" \
      --project="${PROJECT_ID}" \
      --enable-hierarchical-namespace \
      --uniform-bucket-level-access
  2. 创建一个 Kubernetes 服务账号 (KSA) 并将其绑定到存储桶:

    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. 为 Hugging Face 创建 Secret:

    kubectl create secret generic hf-secret --from-literal=hf_token=${HF_TOKEN}
  4. 检查 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. 应用清单:

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

设置 DRANET

配置 DRANET:

A4 和 A3 Ultra

Autopilot

  1. 创建 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. 应用 computeclass-dranet.yaml 清单(在上一步中创建)和 resourceclaim-dranet.yaml 清单(包含在示例代码库中):

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

标准

无需设置 DRANET。您可以直接进入下一部分。

A4X

DRANET 由 Cluster Toolkit 设置。您可以直接进入下一部分。

准备模型和数据

使用模型权重和数据集填充 Cloud Storage 存储桶。 您可以在本地或在 GKE Pod 上运行以下命令,以填充存储桶:

A4 和 A3 Ultra

Autopilot

  1. 检查 data-prep-job.yaml 清单,该清单用于配置数据准备作业:

    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. 启动作业:

    envsubst < "data-prep-job.yaml" | kubectl apply -f -
  3. 检查 Pod 状态:

    watch kubectl get pods -n ${NAMESPACE}

    当 data-prep-job Pod 达到 Running 状态后,按 Control+C 停止观看。

  4. 监控作业:

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

    在 Pod 达到 Running 状态之前,kubectl logs 命令只会返回极少的输出或不返回任何输出。

标准

  1. 检查 data-prep-job.yaml 清单,该清单用于配置数据准备作业:

    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. 启动作业:

    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. 监控作业:

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

A4X

  1. 克隆 verl 代码库,准备虚拟环境,并处理 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. 使用 Hugging Face CLI 下载 Qwen2.5-32B-Instruct 模型(此下载需要大约 66 GB 的磁盘可用空间):

    hf download Qwen/Qwen2.5-32B-Instruct --local-dir Qwen2.5-32B-Instruct
    
  3. 将模型、数据和 verl 代码上传到 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/
    

部署 RayCluster 自定义资源

部署 RayCluster 自定义资源,该资源由一个系统头 Pod 和多个 GPU 支持的工作器 Pod 组成。

A4 和 A3 Ultra

选择您用于创建集群的 GKE 集群模式:

Autopilot

  1. 检查 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. 应用 RayCluster:

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

标准

  1. 检查 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. 应用 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. 创建 RDMA ResourceClaimTemplate 和 NVIDIA ComputeDomain。 每个 GPU 工作器 Pod 声明了 4 个 RDMA NIC(其节点的所有轨道)和一个 IMEX 通道。将以下清单保存到 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. 应用清单:

    kubectl apply -f compute-domain-a4x.yaml
    
  3. 部署 RayCluster。Ray 头 Pod 在 A4X 节点上运行,无需请求 GPU(因为该映像仅支持 arm64)。将以下配置保存到 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. 应用 RayCluster 清单:

    envsubst < ray-cluster-a4x.yaml | kubectl apply -f -
    
  5. 等待一个头 Pod 和四个工作器 Pod 处于 Running 状态:

    kubectl get pods -w
    

启动 GRPO 作业

配置并提交强化学习训练作业:

A4 和 A3 Ultra

选择您用于创建集群的 GKE 集群模式:

Autopilot

  1. 设置 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. 恢复 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. 设置到 Ray 信息中心节点的端口转发。该进程在后台运行,因此您可以继续在同一终端会话中运行命令。

    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"

    如需停止端口转发循环,请使用 kill ${PF_LOOP_PID} 命令。

  4. 检查 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"

    如果您使用的是 H200 GPU,请将 NCCL_CONF_FILE 环境变量的值更改为 /usr/local/gib/configs/nccl.a3u.conf。

    Ray 客户端会使用此文件。您无需将此清单应用于集群。

  5. 使用 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. 监控训练作业日志,并查看 critic/score/mean 指标是否增加,这表示模型正在学习:

    • Ray 仪表板:
      1. 在浏览器中打开 http://localhost:8265。如果您使用的是 Cloud Shell,请点击 Web 预览,选择更改端口,然后输入 8265。
      2. 前往作业标签页。
      3. 点击您的任务 ID。
      4. 按关键字 critic 过滤日志,以检查 critic/score/mean。
    • 终端:由于作业是使用 --no-wait 标志异步提交的,因此请在终端中运行以下命令,以流式传输作业日志:

      ray job logs "${JOB_ID}" --address "http://localhost:8265" --follow
  7. 训练完成后,列出目录内容,验证模型检查点是否已保存到 Cloud Storage 存储桶:

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

    或者,您也可以在 Google Cloud 控制台中查看检查点,方法是前往 Cloud Storage > 存储桶,选择 YOUR_GCS_BUCKET,然后打开 verl/checkpoints 文件夹。

标准

  1. 设置 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. 恢复 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. 设置到 Ray 信息中心节点的端口转发。该进程在后台运行,因此您可以继续在同一终端会话中运行命令。

    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

    如需停止端口转发循环,请使用 kill ${PF_PID} 命令。

  4. 检查 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"

    如果您使用的是 H200 GPU,请将 NCCL_TUNER_CONFIG_PATH 环境变量的值更改为 /usr/local/gib/configs/tuner_config_a3u.txtpb。

    Ray 客户端会使用此文件。您无需将此清单应用于集群。

  5. 提交作业:

    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. 监控训练作业日志,并查找要增加的 critic/score/mean 指标,该指标表示模型正在学习:

    • Ray 仪表板:
      1. 在浏览器中打开 http://localhost:8265。如果您使用的是 Cloud Shell,请点击 Web 预览,选择更改端口,然后输入 8265。
      2. 前往作业标签页。
      3. 点击您的任务 ID。
      4. 按 critic 关键字过滤日志,以检查 critic/score/mean 指标。
    • 控制台输出:由于作业是使用 --no-wait 标志异步提交的,因此请在终端中运行以下命令,以流式传输作业日志:

      ray job logs "${JOB_ID}" --address "http://localhost:8265" --follow
  7. 训练结束后,通过列出目录内容来验证模型检查点是否已保存到 Cloud Storage 存储桶:

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

    或者,您也可以在 Google Cloud 控制台中查看检查点,方法是前往 Cloud Storage > 存储桶,选择 YOUR_GCS_BUCKET,然后打开 verl/checkpoints 文件夹。

A4X

  1. 获取 Ray 头 Pod 名称:

    export HEAD_POD=$(kubectl get pod -n ${NAMESPACE} -l ray.io/node-type=head -o jsonpath='{.items[0].metadata.name}')
    
  2. 直接在头 Pod 上配置 Ray 运行时环境文件:

    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. 通过在 Ray 头节点 Pod 上运行 GRPO 训练作业来提交该作业:

    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. 监控作业日志(使用 ray job submit 返回的唯一 ID):

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

    替换 JOB_ID。通过在包含 via P2P/MNNVL 的日志中查找 NCCL 行,确认跨节点 NVLink 处于活跃状态。

清理

为避免产生费用,请删除以下资源:

A4 和 A3 Ultra

Autopilot

  1. 如果端口转发循环仍在后台会话中运行,请将其终止:

    kill ${PF_LOOP_PID}
  2. 删除 Ray 集群:

    envsubst < "ray-cluster-auto-dranet.yaml" | kubectl delete -f - --ignore-not-found=true || true
  3. 删除 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. 删除 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. 删除 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. 删除 GKE 集群:

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

标准

  1. 如果端口转发循环仍在后台会话中运行,请将其终止:

    kill ${PF_PID}
  2. 删除 Ray 集群:

    envsubst < "ray-cluster-standard.yaml" | kubectl delete --timeout=30s -f - --ignore-not-found=true || true
  3. 删除 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. 删除 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. 删除 GKE 集群:

    gcloud container clusters delete ${CLUSTER_NAME} --location=${CONTROL_PLANE_REGION} --project=${PROJECT_ID} --quiet || true
  6. 删除 VPC 网络和子网:

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

A4X

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

后续步骤