כוונון עדין והרחבה של למידת חיזוק עם NVIDIA NeMo RL ב-GKE

במדריך הזה תלמדו איך לתזמן סביבת אימון מבוזרת ללמידת חיזוק (RL) ב-Google Kubernetes Engine ‏ (GKE). אתם יכולים להשתמש ב-Ray ובמסגרת NVIDIA NeMo RL כדי להגדיר סביבת אימון מבוזרת לצורך כוונון עדין של מודל.

המדריך הזה מתמקד בצינור עיבוד הנתונים לאימון של Group Relative Policy Optimization (GRPO)‎ ב-GKE עם Ray ו-NeMo RL. GRPO הוא אלגוריתם ללמידה עם חיזוקים שנועד לשפר את יכולת ההסקה של מודל. האלגוריתם הזה חוסך בזיכרון ומפשט את תהליך ה-RL על ידי ביטול של רכיב ה-Critic, או מודל הערך, ושימוש בחישוב יחסי שמבוסס על קבוצה במקום זאת.

לפני שמריצים את המדריך הזה, צריך להשלים את המדריך Fine-tune and scale reinforcement learning with verl on GKE. במדריך הזה נעשה שימוש באותה הגדרת אשכול ובתצורה כמו במדריך בנושא כוונון עדין ושינוי קנה מידה של RL עם verl.

רקע

בקטעים הבאים מופיעה סקירה כללית קצרה של המושגים שבהם נעשה שימוש במדריך הזה.

למידת חיזוקים (RL)

ב-RL, המודלים לומדים מתוך ניסיון, מחקר ומשוב, ולא מתוך חיקוי סטטי. במהלך האימון המוקדם, המודל לומד מה לומר, אבל במהלך למידה ממשוב אנושי (RLHF), הוא לומד איך להיות מועיל, בטוח והגיוני. RL משמש כגשר בין מודל בסיסי לבין מודל מכוונן לשימוש ספציפי.

מידע נוסף זמין במאמר מה זה למידת חיזוק?

אופטימיזציה של מדיניות יחסית לקבוצה (GRPO)

GRPO, אלגוריתם שזכה לפופולריות בזכות DeepSeek, מציע חלופה חסכונית בזיכרון ל-Proximal Policy Optimization ‏ (PPO) להתאמת LLM, על ידי הסרת מודל ה-Critic. במקום רשת מבקרת, GRPO יוצרת קבוצת תגובות לאותה הנחיה ומשתמשת בתגמול הממוצע של הקבוצה הזו כנקודת בסיס.

מידע נוסף זמין במאמר בנושא GRPO.

NVIDIA NeMo RL

NeMo RL היא ספרייה של NVIDIA בקוד פתוח שנועדה להרחבת RL אחרי אימון. ‫NeMo RL הוא חלק ממערכת NeMo הרחבה, והוא מאפשר ניסויים בקנה מידה קטן ב-GPU יחיד ופריסות מרובות צמתים באלפי GPU.

מידע נוסף זמין במאמר בנושא NVIDIA NeMo RL.

מערך הנתונים GSM8k

במדריך הזה משתמשים במערך הנתונים GSM8k, שמכיל 8,500 בעיות מילוליות במתמטיקה ברמה של בית ספר יסודי, באיכות גבוהה ובמגוון לשוני.

באמצעות GSM8k ו-GRPO, המודל יוצר קבוצה של n תשובות שונות לאותה בעיה. המדד GRPO משווה את התשובות האלה לממוצע של הקבוצה. המודל מקבל תגמול גבוה יותר על נתיבים שהם נכונים ועקביים מבחינה לוגית בהשוואה לשאר הקבוצה. עם הזמן, המודל לומד שהדרך הכי אמינה למקסם את התגמול היא להסביר את השלבים בצורה ברורה, וכך הוא מצמצם את התגמול על תשובות עם ביצועים נמוכים.

מידע נוסף זמין במאמר בנושא GSM8k.

מטרות

במדריך הזה מוסבר איך להגדיר RL ב-GKE באמצעות NeMo RL. לשם כך, צריך לבצע את השלבים הבאים:

  1. מכינים את הסביבה.
  2. הגדרת אשכול GKE עם מעבדי GPU מסוג B200 או H200.
  3. הגדרת KubeRay לניהול אשכול Ray מבוזר.
  4. משתמשים ב-Managed Lustre לאחסון עם ביצועים גבוהים.
  5. מריצים משימת אימון של GRPO באמצעות NeMo RL.

לפני שמתחילים

  1. התקינו את ה-CLI של Google Cloud.

  2. הגדירו שה-CLI של gcloud ישתמש בזהות המאוחדת שלכם.

    איך נכנסים ל-CLI של gcloud באמצעות הזהות המאוחדת?

  3. כדי לאתחל את ה-CLI של gcloud, הריצו את הפקודה הבאה:

    gcloud init
  4. יוצרים או בוחרים Google Cloud פרויקט.

    תפקידים שנדרשים כדי לבחור או ליצור פרויקט

    • Select a project (בחירת פרויקט): כדי לבחור פרויקט לא צריך תפקיד 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 .

  5. מוודאים שהחיוב מופעל בפרויקט Google Cloud .

  6. מפעילים את ממשקי ה-API הנדרשים:

    תפקידים שנדרשים להפעלת ממשקי API

    כדי להפעיל ממשקי API, נדרשת ההרשאה serviceusage.services.enable. אם יצרתם את הפרויקט, סביר להניח שכבר יש לכם את ההרשאה הזו דרך התפקיד 'בעלים' (roles/owner). אחרת, תוכלו לקבל את ההרשאה הזו דרך התפקיד 'אדמין של Service Usage' (roles/serviceusage.serviceUsageAdmin). איך מקצים תפקידים

    gcloud services enable container.googleapis.com storage.googleapis.com compute.googleapis.com
  7. מעניקים תפקידים לחשבון המשתמש. מריצים את הפקודה הבאה לכל אחד מהתפקידים הבאים ב-IAM: roles/container.admin, roles/iam.serviceAccountAdmin, roles/storage.admin

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

    מחליפים את מה שכתוב בשדות הבאים:

  8. יוצרים חשבון ב-Hugging Face, אם עדיין אין לכם חשבון.
  9. מוודאים שיש לכם טוקן של Hugging Face עם read access.
  10. אם אין לכם חשבון, אתם צריכים ליצור חשבון Weights & Biases (Wandb).
  11. יוצרים מפתח Wandb API.
  12. מוודאים שלפרויקט יש מכסה מספקת עבור מעבדי GPU מסוג B200 ו-H200. Google Cloud מידע נוסף זמין במאמרים תכנון מכסת GPU ומכסת GPU.

הכנת הסביבה

במדריך הזה משתמשים ב-Cloud Shell.

  1. עוברים אל Google Cloud המסוף.

  2. בחלק העליון של Google Cloud חלון המסוף, לוחצים על הלחצן Activate Cloud Shell (הפעלת Cloud Shell).

  3. מגדירים את משתני הסביבה הבאים:

    export CONTROL_PLANE_REGION="YOUR_REGION"
    export NODE_ZONE="YOUR_ZONE"
    export CLUSTER_NAME="YOUR_CLUSTER_NAME"
    export GPU_TYPE="YOUR_GPU_TYPE"
    export MACHINE_TYPE="YOUR_MACHINE_TYPE"
    export KSA_NAME="generic-ksa"
    export NAMESPACE="default"
    export RESERVATION="RESERVATION_NAME"
    export LUSTRE_NAME="CHOSEN_LUSTRE_NAME"
    export HF_TOKEN="YOUR_HF_TOKEN"
    export WANDB_API_KEY="YOUR_WANDB_API_KEY"
    
    export PROJECT_ID=$(gcloud config get project)
    export PROJECT_NUMBER=$(gcloud projects describe "${PROJECT_ID}" --format="value(projectNumber)")

    מחליפים את הערכים הבאים:

    • YOUR_REGION: האזור ב-Compute Engine של מישור הבקרה של אשכול GKE.
    • YOUR_NODE_ZONE: האזור של הצמתים. בוחרים אזור שבו זמינים מעבדי GPU מסוג NVIDIA B200 או H200.
    • YOUR_CLUSTER_NAME: השם של אשכול GKE.
    • YOUR_GPU_TYPE: המאיץ שהזמנתם בהזמנת הקיבולת של Compute Engine. הערך חייב להיות אחד מהערכים הבאים:
      • nvidia-b200: NVIDIA B200‏ (180 GB)
      • nvidia-h200-141gb: NVIDIA H200 ‏ (141 GB)
    • YOUR_MACHINE_TYPE: סוג המכונה לשימוש:
      • עבור יחידות GPU של NVIDIA B200 ‏ (180 GB), צריך להשתמש בגרסה a4-highgpu-8g ואילך.
      • למעבדי GPU מסוג NVIDIA H200 ‏ (141 GB), צריך להשתמש בגרסה a3-ultragpu-8g ואילך.
    • YOUR_RESERVATION_NAME: השם של הזמנת ה-GPU.
    • CHOSEN_LUSTRE_NAME: השם של מופע Lustre.
    • YOUR_HF_TOKEN: האסימון שלכם ב-Hugging Face.
    • YOUR_WANDB_API_KEY: מפתח ה-API של Wandb.
  4. יוצרים את משתני הסביבה הבאים לרשת:

    export NETWORK="YOUR_NETWORK_NAME"
    export GVNIC_NETWORK_PREFIX="GVNIC_NAME"
    export RDMA_NETWORK_PREFIX="RDMA_NAME"

    מחליפים את הערכים הבאים:

    • NETWORK_NAME: שם הרשת ב-GKE.
    • GVNIC_NAME: הקידומת של שם רשת gVNIC. אפשר להשתמש בכל קידומת שרוצים.
    • RDMA_NAME: הקידומת של רשת הגישה הישירה לזיכרון (RDMA) מרחוק. אפשר להשתמש בכל קידומת שרוצים.

הגדרת התשתית

בקטע הזה יוצרים רשתות VPC ואשכול GKE.

יצירת רשת VPC

  1. יוצרים רשת VPC לממשק gVNIC:

    gcloud compute networks create ${NETWORK} --subnet-mode=auto
    
    gcloud compute networks create ${GVNIC_NETWORK_PREFIX}-net \
        --subnet-mode=custom
    
    gcloud compute networks subnets create ${GVNIC_NETWORK_PREFIX}-sub \
        --network=${GVNIC_NETWORK_PREFIX}-net \
        --region=${CONTROL_PLANE_REGION} \
        --range=192.168.0.0/24
    
    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
  2. יוצרים רשת VPC ורשתות משנה ל-RDMA שכוללות שמונה רשתות משנה לשמונה יחידות GPU:

    gcloud compute networks create ${RDMA_NETWORK_PREFIX}-net \
        --network-profile=${NODE_ZONE}-vpc-roce \
        --subnet-mode=custom
    
    for N in $(seq 0 7); do
      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 &
    done
    wait

יצירת אשכול GKE

אפשר להגדיר את NeMo RL באשכול GKE Standard.

  1. יצירת אשכול רגיל:

    gcloud container clusters create ${CLUSTER_NAME} \
        --location=${CONTROL_PLANE_REGION} \
        --workload-pool=${PROJECT_ID}.svc.id.goog \
        --enable-dataplane-v2 \
        --enable-ip-alias \
        --enable-multi-networking \
        --addons=RayOperator,LustreCsiDriver \
        --enable-legacy-lustre-port \
        --machine-type=n2-highmem-80 \
        --num-nodes=1 \
        --min-nodes=1 \
        --max-nodes=5 \
        --enable-autoscaling \
        --network=${NETWORK}
  2. קבלת פרטי הכניסה לאשכול:

    gcloud container clusters get-credentials $CLUSTER_NAME \
        --location=$CONTROL_PLANE_REGION
  3. יוצרים את מאגר הצמתים של ה-GPU:

    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 \
        --reservation-affinity=specific \
        --reservation=${RESERVATION} \
        --enable-autoscaling \
        --num-nodes=0 \
        --total-max-nodes=2 \
        --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
  4. מתקינים את NCCL RDMA installer:

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

הגדרת מיפויי רשת

  1. שומרים את קובץ המניפסט הבא בשם network-mapping.yaml:

    # Copyright 2026 Google LLC. All rights reserved.
    #
    # Licensed under the Apache License, Version 2.0 (the "License");
    # you may not use this file except in compliance with the License.
    # You may obtain a copy of the License at
    #
    #     http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS,
    # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    # See the License for the specific language governing permissions and
    # limitations under the License.
    
    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 -

הכנת האחסון

בקטע הזה תיצרו מכונה של Managed Lustre, שמקצה את נפח האחסון הנדרש לביצועים גבוהים עבור עומס העבודה של RL.

  1. הקצאת טווח של כתובות IP לגישה לשירותים פרטיים:

    gcloud compute addresses create ${LUSTRE_NAME}-range \
        --global --purpose=VPC_PEERING \
        --prefix-length=20 --network=${NETWORK}
  2. מחברים את ה-peering:

    gcloud services vpc-peerings connect \
        --service=servicenetworking.googleapis.com \
        --ranges=${LUSTRE_NAME}-range \
        --network=${NETWORK}
  3. יצירת מכונה של Managed Lustre:

    gcloud lustre instances create ${LUSTRE_NAME} \
        --per-unit-storage-throughput=500 \
        --capacity-gib=18000 \
        --filesystem=lustrefs \
        --location=${NODE_ZONE} \
        --network=projects/${PROJECT_ID}/global/networks/${NETWORK} \
        --gke-support-enabled
  4. גישה למכונה קיימת ב-Managed Lustre באמצעות מנהל התקן ה-CSI של Managed Lustre.

    1. מחפשים את כתובת ה-IP של מכונת Managed Lustre.

      export LUSTRE_IP=$(gcloud lustre instances describe ${LUSTRE_NAME} \
          --location=$NODE_ZONE --format="value(mountPoint)" | awk -F'@' '{print $1}')
    2. בדיקת המניפסט של lustre-pv.yaml.

      # Copyright 2026 Google LLC
      #
      # Licensed under the Apache License, Version 2.0 (the "License");
      # you may not use this file except in compliance with the License.
      # You may obtain a copy of the License at
      #
      #     http://www.apache.org/licenses/LICENSE-2.0
      #
      # Unless required by applicable law or agreed to in writing, software
      # distributed under the License is distributed on an "AS IS" BASIS,
      # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      # See the License for the specific language governing permissions and
      # limitations under the License.
      
      apiVersion: v1
      kind: PersistentVolume
      metadata:
        name: lustre-pv
      spec:
        storageClassName: lustre-rwx-500mbps-per-tib
        capacity:
          storage: 18000Gi
        accessModes:
          - ReadWriteMany
        persistentVolumeReclaimPolicy: Retain
        volumeMode: Filesystem
        claimRef:
          namespace: default
          name: lustre-pvc
        csi:
          driver: lustre.csi.storage.gke.io
          volumeHandle: "${PROJECT_ID}/${NODE_ZONE}/${LUSTRE_NAME}"
          volumeAttributes:
            ip: ${LUSTRE_IP}
            filesystem: lustrefs
    3. החלת המניפסט:

      envsubst < lustre-pv.yaml | kubectl apply -f -
    4. בדיקת המניפסט של lustre-pvc.yaml.

      # Copyright 2026 Google LLC
      #
      # Licensed under the Apache License, Version 2.0 (the "License");
      # you may not use this file except in compliance with the License.
      # You may obtain a copy of the License at
      #
      #     http://www.apache.org/licenses/LICENSE-2.0
      #
      # Unless required by applicable law or agreed to in writing, software
      # distributed under the License is distributed on an "AS IS" BASIS,
      # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      # See the License for the specific language governing permissions and
      # limitations under the License.
      
      apiVersion: v1
      kind: PersistentVolumeClaim
      metadata:
        name: lustre-pvc
      spec:
        accessModes:
          - ReadWriteMany
        storageClassName: lustre-rwx-500mbps-per-tib
        volumeName: lustre-pv
        resources:
          requests:
            storage: 18000Gi
    5. החלת המניפסט:

      kubectl apply -f lustre-pvc.yaml

פריסת RayCluster

בקטע הזה משכפלים את מאגר הדוגמאות, מכינים את קובצי המניפסט ומפריסים את אשכול Ray:

  1. משכפלים את המאגר לדוגמה:

    git clone https://github.com/GoogleCloudPlatform/kubernetes-engine-samples.git
  2. עוברים לספריית העבודה:

    cd kubernetes-engine-samples/ai-ml/nemo-rl-on-gke/nemoRL
  3. בודקים את קובץ המניפסט values.yaml:

    # Copyright 2026 Google LLC
    #
    # Licensed under the Apache License, Version 2.0 (the "License");
    # you may not use this file except in compliance with the License.
    # You may obtain a copy of the License at
    #
    #     http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS,
    # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    # See the License for the specific language governing permissions and
    # limitations under the License.
    
    image:
      repository: "nvcr.io/nvidia/nemo-rl"
      tag: "v0.5.0" 
      pullPolicy: Always
    
    nameOverride: "kuberay"
    fullnameOverride: ""
    
    common:
      containerEnv: {}
    
    configMap:
      fluentbit:
        data:
          fluent-bit.conf: |
            [INPUT]
                Name              tail
                Path              /tmp/ray/session_latest/logs/worker-*
                Tag               ray-worker
            [INPUT]
                Name              tail
                Path              /tmp/ray/session_latest/logs/raylet*
                Tag               raylet
            [INPUT]
                Name              tail
                Path              /tmp/ray/session_latest/logs/*
                Exclude_Path      /tmp/ray/session_latest/logs/debug_state.txt,/tmp/ray/session_latest/logs/raylet*,/tmp/ray/session_latest/logs/worker-*
                Tag               ray-misc
            [OUTPUT]
                Name              stackdriver
                Match             *
                resource          gce_instance
                labels_key        labels
    
    # --- Head Node Configuration ---
    head:
      enableInTreeAutoscaling: false
      serviceAccountName: ""
      rayStartParams:
        dashboard-host: '0.0.0.0'
      template:
        metadata:
          annotations:
            gke-gcsfuse/volumes: "true"
            networking.gke.io/default-interface: 'eth0'
      containerEnv:
      - name: RAY_GROUP
        value: "head"
      nodeSelector:
        cloud.google.com/gke-nodepool: default-pool
      resources:
        limits:
          cpu: "64"
          memory: "500G"
          nvidia.com/gpu: 0
        requests:
          cpu: "64"
          memory: "500G"
          nvidia.com/gpu: 0
      tolerations:
        # - operator: "Exists"
        #   key: "components.gke.io/gke-managed-components"
        # - key: "nvidia.com/gpu"
        #   operator: "Exists"
        #   effect: "NoSchedule"
      volumeMounts:
        - mountPath: /data
          name: lustre-data
    
      volumes:
        - name: log-volume
          emptyDir: {}
        - name: fluentbit-config-volume
          configMap:
            name: "ray-cluster-kuberay-fluentbit-config"
        - name: lustre-data
          persistentVolumeClaim:
            claimName: lustre-pvc
      sidecarContainers:
        - name: fluent-bit
          image: fluent/fluent-bit:latest
          env:
          - name: RAY_GROUP
            value: "head"
          volumeMounts:
            - name: fluentbit-config-volume
              mountPath: /fluent-bit/etc/
            - mountPath: /tmp/ray
              name: log-volume
    
      # --- HEAD POD STARTUP SCRIPT ---
      command:
        - "bash"
        - "-c"
        - |
          set -ex
          echo "--- Head Pod Setup ---"
          apt-get update
          apt-get install -y sudo netcat-openbsd pciutils
          cd /opt/nemo-rl
          /usr/bin/python -m pip install uv
          /usr/bin/python -m uv venv
          echo "Head pod setup complete. Starting Ray..."
    
          exec ${KUBERAY_GEN_RAY_START_CMD}
    
      args: []
      headService: {}
      # nodeSelector:
      #   cloud.google.com/gke-accelerator: nvidia-b200 #cloud.google.com/gke-nodepool: cpu-node-pool-llama #cpu-node-pool
    
    # --- Default Worker (Disabled) ---
    worker:
      disabled: true
    
    # --- A4 GPU Worker Groups ---
    additionalWorkerGroups:
      worker-grp-0:
        disabled: false
        replicas: 4
        annotations:
          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"}
            ]
        containerEnv:
          - name: RAY_GROUP
            valueFrom:
              fieldRef:
                fieldPath: metadata.labels['ray.io/group']
          - name: NCCL_NET  
            value: "gIB"
          - name: NCCL_IB_GID_INDEX
            value: "3"   
          - name: GLOO_SOCKET_IFNAME
            value: "eth0"
          - name: NCCL_CROSS_NIC
            value: "0"
          - name: NCCL_SOCKET_IFNAME
            value: "eth0"
          - name: TP_SOCKET_IFNAME # Specific to DTensor/PyTorch Distributed
            value: "eth0"
          - name: NCCL_TUNER_CONFIG_PATH
            value: "/usr/local/gib/configs/tuner_config_a4.txtpb"
          - name: NCCL_NET_GDR_LEVEL
            value: "PIX"
          - name: LD_LIBRARY_PATH
            value: /usr/local/nvidia/lib64
        resources:
          limits:
            nvidia.com/gpu: 8
            cpu: "206"
            memory: "2400Gi"
          requests:
            nvidia.com/gpu: 8
            cpu: "206"
            memory: "2400Gi"
    
        nodeSelector:
          cloud.google.com/gke-accelerator: nvidia-b200
        tolerations:
          - operator: "Exists"
            key: "nvidia.com/gpu"
          - operator: "Exists"
            key: "cloud.google.com/impending-node-termination"
          - operator: "Exists"
            key: "user-workload"
        securityContext:
          privileged: true
        volumes:
          - name: log-volume
            emptyDir: {}
          - name: shared-memory
            emptyDir:
              medium: "Memory"
              sizeLimit: 240Gi
          - name: ray-tmp
            emptyDir:
              medium: "Memory"
          - name: fluentbit-config-volume
            configMap:
              name: "ray-cluster-kuberay-fluentbit-config"
          - name: nvidia-install-dir-host
            hostPath:
              path: /home/kubernetes/bin/nvidia
          - name: gib-nccl-plugin-volume
            hostPath: 
              path: /home/kubernetes/bin/gib
          - name: lustre-data
            persistentVolumeClaim:
              claimName: lustre-pvc
        volumeMounts:
          - mountPath: /tmp/ray
            name: log-volume
          - name: shared-memory
            mountPath: /dev/shm
          - name: nvidia-install-dir-host
            mountPath: /usr/local/nvidia
          - name: gib-nccl-plugin-volume
            mountPath: /usr/local/gib
          - mountPath: /data
            name: lustre-data   
        # --- WORKER POD STARTUP SCRIPT ---
        command:
          - "bash"
          - "-c"
          - |
            set -ex
    
            echo "--- Worker Pod Setup ---"
            apt-get update
            apt-get install -y sudo netcat-openbsd pciutils
            cd /opt/nemo-rl
            /usr/bin/python -m pip install uv
            /usr/bin/python -m uv venv
    
            ldconfig /usr/local/nvidia/lib64/
            ldconfig -p | grep libcuda | sed 's/^/  /'
            export LD_LIBRARY_PATH="/usr/local/gib/lib64:$LD_LIBRARY_PATH"
            source /usr/local/gib/scripts/set_nccl_env.sh
    
            echo "Worker pod setup complete. Starting Ray..."
    
            exec ${KUBERAY_GEN_RAY_START_CMD}
    
    
        sidecarContainers:
          - name: fluent-bit
            env:
              - name: RAY_GROUP
                valueFrom:
                  fieldRef:
                    fieldPath: metadata.labels['ray.io/group']
            image: fluent/fluent-bit:latest
            volumeMounts:
              - name: fluentbit-config-volume
                mountPath: /fluent-bit/etc/
              - mountPath: /tmp/ray
                name: log-volume
    
    # --- Service Config ---
    service:
      type: ClusterIP
    

    מחליפים את NCCL_TUNER_CONFIG_PATH באחד מהערכים הבאים, בהתאם למאיץ שבו משתמשים במדריך הזה:

    • NVIDIA B200 ‏ (180 GB): /usr/local/gib/configs/tuner_config_a4.txtpb
    • NVIDIA H200 (141 GB): /usr/local/gib/configs/tuner_config_a3u.txtpb

    במניפסט הזה, צומת הראש מנהל את העבודה ומארח את לוח הבקרה של Ray. צמתי העובדים מריצים את משימות האימון.

  4. פורסים את אשכול Ray:

    export REPLICA_COUNT=2
    helm install ray-cluster . \
      --set additionalWorkerGroups.worker-grp-0.replicas=$REPLICA_COUNT

    במדריך הזה משתמשים בשני צמתי עובדים. כדי לשנות את מספר צמתי העובדים, משנים את הערך של REPLICA_COUNT.

  5. מוודאים שצומתי העובד והראש פועלים:

    kubectl get pods

    הפלט אמור להיראות כך:

    NAME                                          READY STATUS RESTARTS AGE
    ray-cluster-kuberay-head-sw7dp                2/2   Running 0      33h
    ray-cluster-kuberay-worker-grp-0-worker-gkbxw 2/2   Running 0      33h
    ray-cluster-kuberay-worker-grp-0-worker-kdg62 2/2   Running 0      33h
    
  6. מוודאים שאשכול Ray פועל:

    kubectl ray get cluster

    הפלט אמור להיראות כך:

    NAME                 NAMESPACE DESIRED WORKERS AVAILABLE WORKERS CPUS GPUS TPUS MEMORY CONDITION STATUS AGE
    ray-cluster-kuberay  default   2       2           618     17   0    1573741824k RayClusterProvisioned ready 33h
    

הפעלת משימת GRPO

אחרי שקלאסטר Ray מוכן, אפשר לשלוח Ray Job לקלאסטר Ray שפועל ב-GKE. המודל יורד אוטומטית במהלך ההרצה של משימת אימון ה-RL.

כדי לשלוח Ray Job, צריך להתחיל סשן אינטראקטיבי כדי להריץ את ה-Job.

  1. כדי ליצור חיבור מקומי לאשכול Ray, מריצים את הפקודה הבאה:

    kubectl ray session ray-cluster-kuberay

    הפקודה הזו מפעילה העברת יציאות בין המכונה המקומית לבין צומת הראש של Ray באשכול GKE. שימו לב: הטרמינל יהיה תפוס בזמן שהסשן פעיל. כדי להמשיך, צריך לפתוח מופע נפרד של הטרמינל.

  2. בטרמינל נפרד, עוברים אל kubernetes-engine-samples/ai-ml/nemo-rl-on-gke/nemoRL/gemma3-27b-it ועורכים את הקובץ gemma3-27b-gsm8k.sh:

    # Copyright 2026 Google LLC
    #
    # Licensed under the Apache License, Version 2.0 (the "License");
    # you may not use this file except in compliance with the License.
    # You may obtain a copy of the License at
    #
    #     http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS,
    # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    # See the License for the specific language governing permissions and
    # limitations under the License.
    
    #!/bin/bash
    WANDB_API_KEY='YOUR_WANDB_API_KEY' # Update this with your WANDB API key
    HF_TOKEN='YOUR_HF_TOKEN' # Update this with your HF token
    WORLD_SIZE=16
    
    # --- Step 1: Find the Ray Head Pod ---
    echo "Finding Ray head pod..."
    export HEAD_POD_NAME=$(kubectl get pods --selector=ray.io/node-type=head -o jsonpath='{.items[0].metadata.name}')
    if [ -z "$HEAD_POD_NAME" ]; then
        echo "Error: No running Ray head pod found. Please check your cluster."
        exit 1
    fi
    echo "Found head pod: $HEAD_POD_NAME"
    echo ""
    
    # --- Step 2: Define the Job Script to Run ---
    # This is the script that will be executed *inside* the head pod.
    # It assumes the 'uv venv' setup from the values.yaml is already done.
    JOB_SCRIPT=$(cat <<EOF
    set -ex
    
    echo "--- Running on Ray Head Pod ($HOSTNAME) ---"
    cd /opt/nemo-rl
    
    git pull && git checkout main
    
    sed -i 's/subset: Optional\[str\] = None/subset: Optional[str] = "main"/' /opt/nemo-rl/nemo_rl/data/datasets/response_datasets/response_dataset.py
    sed -i 's/raw_dataset = load_dataset(data_path)/raw_dataset = load_dataset(data_path, "main")/' /opt/nemo-rl/nemo_rl/data/datasets/utils.py
    
    echo "Setting environment variables..."
    export WANDB_API_KEY=$WANDB_API_KEY
    export HF_TOKEN=$HF_TOKEN
    export HF_HOME=/opt/nemo-rl/
    
    ###-----Example to launch Gemma3-27B on 2 nodes (16 GPUs)----------
    uv run python examples/run_grpo_math.py \
      --config examples/configs/recipes/llm/grpo-gemma3-27b-it-8n4g-fsdp2tp4-actckpt-long.yaml \
      cluster.num_nodes=2 \
      cluster.gpus_per_node=8 \
      grpo.max_num_steps=10 \
      checkpointing.checkpoint_dir=/data/nemo_rl_gemma3_27b_3_17 \
      data.dataset_name=ResponseDataset \
      +data.train_data_path=openai/gsm8k \
      +data.val_data_path=openai/gsm8k \
      +data.val_split=test \
      +data.train_split=train \
      +data.subset="main" \
      +data.input_key="question" \
      +data.output_key="answer" \
      logger.tensorboard_enabled=False \
      logger.wandb_enabled=True \
      logger.wandb.name='nemo_rl_gemma3_27b_3_17' \
      grpo.num_prompts_per_step=16 \
      grpo.num_generations_per_prompt=32 \
      policy.generation.colocated.enabled=False \
      policy.generation.colocated.resources.num_nodes=1 \
      policy.generation.colocated.resources.gpus_per_node=8 \
      policy.generation.vllm_cfg.tensor_parallel_size=8 \
      policy.generation.vllm_cfg.gpu_memory_utilization=0.9 \
      policy.dtensor_cfg.tensor_parallel_size=8
    
    echo "--- Job Finished ---"
    EOF
    )
    
    # --- Step 3: Execute the Job ---
    echo "Submitting job to $HEAD_POD_NAME..."
    echo "$JOB_SCRIPT" | tr -d '\r' | kubectl exec -i $HEAD_POD_NAME -c ray-head -- /bin/bash
    
    echo ""
    echo "Job submission complete."
    

    מחליפים את הערכים הבאים בקובץ gemma3-27b-gsm8k.sh:

    • YOUR_WANDB_API_KEY: מפתח ה-API של WandB.
    • YOUR_HF_TOKEN: האסימון שלכם ב-Hugging Face.

    בקובץ הזה אפשר לראות את ההגדרה להפעלת משימה עם מודל gemma3-27b-it במערך הנתונים GSM8k. כדי להשלים את צינור ההדרכה של GRPO, הסקריפט הזה מגדיר את הפרמטרים הבאים:

    • num_prompts_per_step: 16 ו-num_generations_per_prompt: 32: מודל Gemma3-27b-it יוצר קבוצה גדולה של תשובות לכל הנחיה. במקרה כזה, המודל יפיק 512 תשובות (16 × 32 = 512).
    • policy.generation.colocated.enabled=False: הפרמטר הזה משבית את התכונה של יצירת תוכן במיקום משותף, כלומר המודל לא יוצר תגובות באותו צומת כמו תהליך האימון. ב-RL רגיל, אותם מעבדי GPU מטפלים גם באימון וגם ביצירה. בהגדרה הזו של NeMo RL, מקצים צמתים ספציפיים (שמנוהלים באמצעות הפרמטר policy.generation.colocated.resources) רק להסקת מסקנות של vLLM, בעוד ששאר האשכול מתמקד במתמטיקה של האימון. הפרדה בין עומסי העבודה האלה מונעת תחרות על משאבים בין מאגרי האימון שדורשים הרבה זיכרון לבין עומסי העבודה של ההסקה שדורשים הרבה משאבי מחשוב.
  3. כדי לשלוח את המשימה, מריצים את הפקודה הבאה:

    bash gemma3-27b-it/gemma3-27b-gsm8k.sh

    במהלך הפעלת המשימה, הפלט מציג את תוצאות האימון, התזמון ומדדי הביצועים.

מעקב אחר תקינות משימת ה-GRPO

אחרי ש-Ray מסיים את העבודה, NeMo RL שומר את נקודות הבדיקה בנתיב שהוגדר.

  1. כדי לבדוק את הפלט של עבודת ה-GRPO, יוצרים סשן SSH לקונטיינר ray-head:

    kubectl exec -it $(kubectl get pods -l ray.io/node-type=head -o name) -c ray-head -- bash
  2. מתקינים את כלי העץ apt בטרמינל של מאגר התגים ray-head:

    apt update && apt install -y tree
  3. מציגים את מבנה הספרייה של מאגר ray-head:

    tree /data/nemo_rl_gemma3_27b_3_17/

    הפלט אמור להיראות כך:

    root@ray-cluster-kuberay-worker-grp-0-worker-gkbxw:/opt/nemo-rl# tree /data/nemo_rl_gemma3_27b_3_17/
    /data/nemo_rl_gemma3_27b_3_17/
    `-- step_10
        |-- config.yaml
        |-- policy
        |   |-- optimizer
        |   |   |-- __0_0.distcp
        |   |   |-- __10_0.distcp
        |   |   |-- __11_0.distcp
        |   |   |-- __12_0.distcp
        |   |   |-- __13_0.distcp
        |   |   |-- __14_0.distcp
        |   |   |-- __15_0.distcp
        |   |   |-- __1_0.distcp
        |   |   |-- __2_0.distcp
        |   |   |-- __3_0.distcp
        |   |   |-- __4_0.distcp
        |   |   |-- __5_0.distcp
        |   |   |-- __6_0.distcp
        |   |   |-- __7_0.distcp
        |   |   |-- __8_0.distcp
        |   |   `-- __9_0.distcp
        |   |-- tokenizer
        |   |   |-- chat_template.jinja
        |   |   |-- special_tokens_map.json
        |   |   |-- tokenizer.json
        |   |   `-- tokenizer_config.json
        |   `-- weights
        |       |-- __0_0.distcp
        |       |-- __10_0.distcp
        |       |-- __11_0.distcp
        |       |-- __12_0.distcp
        |       |-- __13_0.distcp
        |       |-- __14_0.distcp
        |       |-- __15_0.distcp
        |       |-- __1_0.distcp
        |       |-- __2_0.distcp
        |       |-- __3_0.distcp
        |       |-- __4_0.distcp
        |       |-- __5_0.distcp
        |       |-- __6_0.distcp
        |       |-- __7_0.distcp
        |       |-- __8_0.distcp
        |       `-- __9_0.distcp
        |-- train_dataloader.pt
        `-- training_info.json
    
    6 directories, 39 files
    

הסרת המשאבים

כדי להימנע מחיובים בחשבון Google Cloud על המשאבים שבהם השתמשתם במדריך הזה, אתם יכולים למחוק את המשאבים הספציפיים או את הפרויקט שמכיל אותם.

מחיקת המשאבים

  1. מחיקת אשכול Slurm:

    helm delete ray-cluster
  2. מחיקת אשכול GKE:

    gcloud container clusters delete ${CLUSTER_NAME} \
        --location=${CONTROL_PLANE_REGION} \
        --quiet
  3. מחיקת מערכת הקבצים של Lustre:

    gcloud lustre instances delete ${LUSTRE_NAME} --location=${NODE_ZONE} --quiet
  4. מחיקת קישור בין רשתות VPC שכנות (peering):

    gcloud services vpc-peerings delete \
        --service=servicenetworking.googleapis.com \
        --network=${NETWORK}
  5. מוחקים את טווח כתובות ה-IP הפרטיות של Lustre:

    gcloud compute addresses delete ${LUSTRE_NAME}-range --global --quiet
  6. מחיקת תת-רשתות של RDMA ו-gVNIC:

    gcloud compute networks subnets delete ${GVNIC_NETWORK_PREFIX}-sub \
        --region=${CONTROL_PLANE_REGION} --quiet
    
    for N in $(seq 0 7); do
      gcloud compute networks subnets delete ${RDMA_NETWORK_PREFIX}-sub-$N \
        --region=${CONTROL_PLANE_REGION} --quiet &
    done
    wait
  7. מחיקת הכללים של חומת האש והרשתות:

    echo "[$(date)] ========== Deleting firewall rules and networks =========="
    
    NETWORKS=(
        "${RDMA_NETWORK_PREFIX}-net"
        "${GVNIC_NETWORK_PREFIX}-net"
        "${NETWORK}"
    )
    
    for NW in "${NETWORKS[@]}"; do
    
      echo "========== Deleting firewall rules for ${NW} =========="
      while true; do
          rules=$(gcloud compute firewall-rules list \
            --filter="network:${NW}" \
            --format="value(name)" \
            --project="${PROJECT_ID}")
    
            if [[ -z "${rules}" ]]; then
              echo "No firewall rules remain for ${NW}"
              break
            fi
    
            for rule in ${rules}; do
              echo "Deleting firewall rule ${rule}..."
              gcloud compute firewall-rules delete "${rule}" --project="${PROJECT_ID}" --quiet || true
            done
    
            sleep 3
          done
    
          echo "[$(date)] ========== Deleting network ${NW} =========="
          gcloud compute networks delete ${NW} --quiet || true
    done

מחיקת פרויקט

כדי למחוק פרויקט Google Cloud :

gcloud projects delete PROJECT_ID

המאמרים הבאים