כוונון עדין והרחבה של למידת חיזוק עם 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 ושינוי קנה מידה של 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.

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

  • נכנסים לחשבון Google Cloud . אם אתם משתמשים חדשים ב- Google Cloud, צרו חשבון כדי שתוכלו להעריך את הביצועים של המוצרים שלנו בתרחישים מהעולם האמיתי. לקוחות חדשים מקבלים בחינם גם קרדיט בשווי 300$ להרצה, לבדיקה ולפריסה של עומסי העבודה.
  • התקינו את ה-CLI של Google Cloud.

  • אם אתם משתמשים בספק זהויות חיצוני (IdP), קודם אתם צריכים להיכנס ל-CLI של gcloud באמצעות המאגר המאוחד לניהול זהויות.

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

    gcloud init
  • יוצרים או בוחרים 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 .

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

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

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

    כדי להפעיל ממשקי API, צריך את תפקיד ה-IAM 'אדמין של Service Usage' (roles/serviceusage.serviceUsageAdmin), שכולל את ההרשאה serviceusage.services.enable. איך מקצים תפקידים

    gcloud services enable container.googleapis.com storage.googleapis.com compute.googleapis.com
  • התקינו את ה-CLI של Google Cloud.

  • אם אתם משתמשים בספק זהויות חיצוני (IdP), קודם אתם צריכים להיכנס ל-CLI של gcloud באמצעות המאגר המאוחד לניהול זהויות.

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

    gcloud init
  • יוצרים או בוחרים 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 .

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

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

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

    כדי להפעיל ממשקי API, צריך את תפקיד ה-IAM 'אדמין של Service Usage' (roles/serviceusage.serviceUsageAdmin), שכולל את ההרשאה serviceusage.services.enable. איך מקצים תפקידים

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

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

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

    • PROJECT_ID: מזהה הפרויקט.
    • USER_IDENTIFIER: המזהה של חשבון המשתמש . לדוגמה, myemail@example.com.
    • ROLE: תפקיד ה-IAM שאתם מקצים לחשבון המשתמש.

הכנת הסביבה

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

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

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

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

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

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

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

    export NETWORK="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:

    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 > network-mapping-updated.yaml
    kubectl apply -f network-mapping-updated.yaml
    

הכנת האחסון

בקטע הזה תיצרו מכונה של 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 > lustre-pv-updated.yaml
      kubectl apply -f lustre-pv-updated.yaml
      
    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
    cd kubernetes-engine-samples
    
  2. עוברים לספריית העבודה:

    cd 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                3/3   Running 0      33h
    ray-cluster-kuberay-worker-grp-0-worker-gkbxw 3/3   Running 0      33h
    ray-cluster-kuberay-worker-grp-0-worker-kdg62 3/3   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. עורכים את הקובץ 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 רגיל, אותם מעבדים גרפיים מטפלים גם באימון וגם ביצירה. בהגדרה הזו של 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 tree בטרמינל של מאגר ray-head:

    apt update && apt install -y tree
    

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

     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
     ```
    

הסרת המשאבים

כדי להימנע מחיובים, מוחקים את המשאבים:

helm delete ray-cluster
gcloud container clusters delete ${CLUSTER_NAME} \
    --location=${CONTROL_PLANE_REGION} \
    --quiet
gcloud lustre instances delete ${LUSTRE_NAME} --location=${NODE_ZONE} --quiet
gcloud services vpc-peerings delete \
    --service=servicenetworking.googleapis.com \
    --network=${NETWORK}
gcloud compute addresses delete ${LUSTRE_NAME}-range --global --quiet
gcloud compute firewall-rules delete ${GVNIC_NETWORK_PREFIX}-internal --quiet
gcloud compute networks subnets delete ${GVNIC_NETWORK_PREFIX}-sub \
    --region=${CONTROL_PLANE_REGION} --quiet
gcloud compute networks delete ${GVNIC_NETWORK_PREFIX}-net --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
gcloud compute networks delete ${RDMA_NETWORK_PREFIX}-net --quiet
gcloud compute networks delete ${NETWORK} --quiet

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