כוונון עדין והתאמה לעומס של למידת חיזוק באמצעות Vertex AI ב-GKE

במדריך הזה תלמדו איך לתזמן סביבת אימון מבוזרת ללמידת חיזוקים ב-Google Kubernetes Engine ‏ (GKE). אתם משתמשים ב-Ray ובמסגרת verl (Volcano Engine Reinforcement Learning) כדי להגדיר סביבת אימון מבוזרת לצורך כוונון עדין של מודל Qwen2.5-32B-Instruct במערך הנתונים GSM8K.

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

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

המדריך הזה תומך בארכיטקטורות ה-GPU הבאות:

  • צמתי GPU מבוססי Intel או AMD: הגדרה ושינוי גודל באמצעות NVIDIA B200 או H200 GPUs, באמצעות הקצאת משאבים דינמית (DRA) של GKE לנתיב Autopilot.
  • צמתים מבוססי Arm‏ A4X‏ (GB200): הגדרה ושינוי גודל באמצעות NVIDIA GB200 Grace Blackwell Superchips, באמצעות הקצאת משאבים דינמית (DRA) של GKE ו-Multi-Node NVLink‏ (IMEX).

רקע

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

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

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

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

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

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

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

‫Volcano Engine Reinforcement Learning ‏ (verl)

verl הוא פריימוורק עתיר ביצועים שנועד לטפל בדפוסי זיכרון וחישוב מורכבים של RL מבוסס-LLM.

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

מטרות

במדריך הזה תלמדו איך להגדיר למידת חיזוק ב-GKE באמצעות verl. לשם כך, תצטרכו לבצע את השלבים הבאים:

  1. הגדרת אשכול GKE עם A4X (GB200 Superchips),‏ A4 (B200 GPUs) או A3 Ultra (H200 GPUs).
  2. הגדרת KubeRay לניהול אשכול Ray מבוזר.
  3. משתמשים ב-Cloud Storage FUSE כדי לטעון קטגוריה של Cloud Storage בכל הצמתים.
  4. מריצים משימת אימון של GRPO באמצעות verl כדי להתאים את מודל Qwen2.5-32B-Instruct למערך הנתונים GSM8K.

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

  • נכנסים לחשבון 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, נדרשת ההרשאה serviceusage.services.enable. אם יצרתם את הפרויקט, סביר להניח שכבר יש לכם את ההרשאה הזו דרך התפקיד 'בעלים' (roles/owner). אחרת, תוכלו לקבל את ההרשאה הזו דרך התפקיד 'אדמין של Service Usage' (roles/serviceusage.serviceUsageAdmin). איך מקצים תפקידים

    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, נדרשת ההרשאה serviceusage.services.enable. אם יצרתם את הפרויקט, סביר להניח שכבר יש לכם את ההרשאה הזו דרך התפקיד 'בעלים' (roles/owner). אחרת, תוכלו לקבל את ההרשאה הזו דרך התפקיד 'אדמין של Service Usage' (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

    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. מגדירים את משתני הסביבה:

    ‫A4 ו-A3 Ultra

    טייס אוטומטי

    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 RESERVATION="YOUR_RESERVATION_NAME"
    export HF_TOKEN="YOUR_HF_TOKEN"
    
    export GVNIC_NETWORK_PREFIX="GVNIC_NAME"
    export RDMA_NETWORK_PREFIX="RDMA_NAME"

    A4X

    export PROJECT_ID=$(gcloud config get project)
    export PROJECT_NUMBER=$(gcloud projects describe ${PROJECT_ID} --format="value(projectNumber)")
    export CONTROL_PLANE_REGION=YOUR_REGION
    export NODE_ZONE=YOUR_ZONE
    export CLUSTER_NAME=YOUR_CLUSTER_NAME
    export KSA_NAME=YOUR_KSA_NAME
    export GS_BUCKET=YOUR_GCS_BUCKET-${PROJECT_ID}
    export NAMESPACE=default
    export GPU_TYPE=YOUR_GPU_TYPE
    export MACHINE_TYPE=YOUR_MACINE_TYPE
    export RESERVATION=YOUR_RESERVATION_NAME
    export HF_TOKEN=YOUR_HF_TOKEN
    
    # A4X (GB200 Superchips) only variables
    export NUM_GPU_NODES=4
    export VERL_IMAGE=verlai/verl:vllm023.aarch64.dev1
    export VERL_REF=ddbcdb7
    

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

    • YOUR_REGION: האזור ב-Compute Engine של מישור הבקרה של אשכול GKE.
    • YOUR_ZONE: האזור שבו הצמתים מוזמנים. מידע נוסף מופיע במאמר בנושא זמינות של GPU.
    • YOUR_CLUSTER_NAME: השם של אשכול GKE.
    • YOUR_KSA_NAME: השם של חשבון השירות של Kubernetes.
    • YOUR_GCS_BUCKET: שם הבסיס של הקטגוריה ב-Cloud Storage. אין צורך לציין את הקידומת gs://.
    • YOUR_GPU_TYPE: המאיץ שהזמנתם בהזמנת הקיבולת של Compute Engine. הערך חייב להיות אחד מהערכים הבאים:
      • nvidia-gb200: A4X (GB200 Superchips)
      • nvidia-b200: A4 (יחידות GPU מסוג B200)
      • nvidia-h200-141gb: A3 Ultra (מעבדי GPU מדגם H200)
    • YOUR_MACHINE_TYPE: סוג המכונה לשימוש:
      • ב-A4X (GB200 Superchips), משתמשים ב-a4x-highgpu-4g.
      • ל-A4 (יחידות GPU מסוג B200), צריך להשתמש בגרסה a4-highgpu-8g ואילך.
      • ב-A3 Ultra (יחידות GPU מדגם H200), צריך להשתמש בגרסה a3-ultragpu-8g ואילך.
    • YOUR_RESERVATION_NAME: השם של הזמנת הקיבולת.
    • YOUR_HF_TOKEN: האסימון שלכם ב-Hugging Face.
    • מהדורת Standard של Google Kubernetes Engine‏ (GKE) בלבד:
      • GVNIC_NAME (GKE Standard – A4 או A3 Ultra בלבד): התחילית של שם רשת gVNIC. אפשר להשתמש בכל קידומת שרוצים.
      • RDMA_NAME (A4 או A3 Ultra בלבד): הקידומת של רשת הגישה הישירה לזיכרון (RDMA) מרחוק. אפשר להשתמש בכל קידומת שרוצים.
  4. משכפלים את המאגר לדוגמה:

    git clone https://github.com/GoogleCloudSamples/AIHypercomputerSamples.git
    
  5. עוברים לספריית העבודה של מצב ה-GKE שבחרתם:

    ‫A4 ו-A3 Ultra

    טייס אוטומטי

    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

טייס אוטומטי

הקטע הזה נדרש רק עבור מעבדי GKE Standard A4 ו-A3 Ultra GPU.

אם אתם משתמשים ב-Autopilot, דלגו על הקטע הזה ועברו ישירות אל יצירת אשכול GKE. ‫GKE מקצה אוטומטית את רשתות ה-VPC ורשתות המשנה הנדרשות, ומשתמש ב-DRANET מנוהל של GKE כדי להקצות את המשאבים האלה ל-Pods. לא צריך ליצור ידנית תשתית רשת.

רגילה

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

    gcloud compute networks create ${GVNIC_NETWORK_PREFIX}-net \
      --subnet-mode=custom \
      --project=${PROJECT_ID}
    
    gcloud compute networks subnets create ${GVNIC_NETWORK_PREFIX}-sub \
      --network=${GVNIC_NETWORK_PREFIX}-net \
      --region=${CONTROL_PLANE_REGION} \
      --range=192.168.0.0/24 \
      --project=${PROJECT_ID}
    
    gcloud compute firewall-rules create ${GVNIC_NETWORK_PREFIX}-internal \
      --network=${GVNIC_NETWORK_PREFIX}-net \
      --action=ALLOW \
      --rules=tcp:0-65535,udp:0-65535,icmp \
      --source-ranges=192.168.0.0/16 \
      --project=${PROJECT_ID}
  2. יוצרים רשת VPC ל-RDMA:

    gcloud beta compute networks create ${RDMA_NETWORK_PREFIX}-net \
      --network-profile=${NODE_ZONE}-vpc-roce \
      --subnet-mode=custom \
      --project=${PROJECT_ID}
  3. יוצרים את 8 רשתות המשנה של RDMA עבור 8 יחידות ה-GPU:

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

A4X

הקטע הזה נדרש רק עבור מעבדי GKE Standard A4 ו-A3 Ultra GPU.

אם אתם משתמשים ביחידות GPU מסוג A4X ‏ (GB200), דלגו על הקטע הזה ועברו ישירות אל יצירת אשכול GKE. ב-GPU מסוג A4X ‏ (GB200) או ב-Autopilot, ‏ GKE יוצר את הרשתות באופן אוטומטי כשמאגר הצמתים משתמש בפרופיל רשת המאיצים auto. התוכנית של Cluster Toolkit מאפשרת את הפרופיל הזה באמצעות הדגל enable_dranet:true.

יצירת אשכול GKE

יוצרים אשכול GKE שתואם לארכיטקטורת ה-GPU:

‫A4 ו-A3 Ultra

בוחרים את מצב אשכול GKE שרוצים להשתמש בו:

טייס אוטומטי

  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. יצירת אשכול רגיל:

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

    gcloud container clusters get-credentials ${CLUSTER_NAME} \
        --location=${CONTROL_PLANE_REGION} \
        --project=${PROJECT_ID}
  3. יוצרים את מאגר הצמתים של ה-GPU. מאגרי הצמתים האלה משתמשים בהזמנה שלכם כדי להבטיח זמינות. מתחילים עם שני צמתים:

    CMD=(
      gcloud container node-pools create gpu-pool
      --cluster="${CLUSTER_NAME}"
      --location="${CONTROL_PLANE_REGION}"
      --node-locations="${NODE_ZONE}"
      --machine-type="${MACHINE_TYPE}"
      --accelerator="type=${GPU_TYPE},count=8,gpu-driver-version=DEFAULT"
      --enable-autoscaling
      --num-nodes=2
      --total-max-nodes=10
      --additional-node-network="network=${GVNIC_NETWORK_PREFIX}-net,subnetwork=${GVNIC_NETWORK_PREFIX}-sub"
      --additional-node-network="network=${RDMA_NETWORK_PREFIX}-net,subnetwork=${RDMA_NETWORK_PREFIX}-sub-0"
      --additional-node-network="network=${RDMA_NETWORK_PREFIX}-net,subnetwork=${RDMA_NETWORK_PREFIX}-sub-1"
      --additional-node-network="network=${RDMA_NETWORK_PREFIX}-net,subnetwork=${RDMA_NETWORK_PREFIX}-sub-2"
      --additional-node-network="network=${RDMA_NETWORK_PREFIX}-net,subnetwork=${RDMA_NETWORK_PREFIX}-sub-3"
      --additional-node-network="network=${RDMA_NETWORK_PREFIX}-net,subnetwork=${RDMA_NETWORK_PREFIX}-sub-4"
      --additional-node-network="network=${RDMA_NETWORK_PREFIX}-net,subnetwork=${RDMA_NETWORK_PREFIX}-sub-5"
      --additional-node-network="network=${RDMA_NETWORK_PREFIX}-net,subnetwork=${RDMA_NETWORK_PREFIX}-sub-6"
      --additional-node-network="network=${RDMA_NETWORK_PREFIX}-net,subnetwork=${RDMA_NETWORK_PREFIX}-sub-7"
      --project="${PROJECT_ID}"
    )
    
    if [ -n "${RESERVATION:-}" ]; then
      CMD+=("--reservation-affinity=specific" "--reservation=${RESERVATION}")
    else
      CMD+=("--reservation-affinity=none")
    fi
    
    "${CMD[@]}"
  4. מתקינים את NCCL RDMA installer שמשמש לאשכולות רגילים:

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

A4X

  1. יוצרים אשכול GKE ומאגר צמתים באמצעות Cluster Toolkit gke-a4x blueprint. תוכנית האב-טיפוס מספקת את אשכול GKE, כולל מאגר הצמתים A4X שקשור להזמנה שלכם, רשתות המאיצים (gVNIC נוסף וארבע מסילות RDMA) ומנהל ההתקן המנוהל של DRANET שחושף את כרטיסי ה-NIC של CX-7 כמכשירי DRA.

    משתמשים בהוראות הפריסה של התוכנית כדי להגדיר את הפרמטרים (כמו PROJECT_ID,‏ CONTROL_PLANE_REGION,‏ NODE_ZONE, הזמנה ו-NUM_GPU_NODES), ואז פורסים את האשכול. לחלופין, אפשר ליצור אשכול באופן ידני לפי המדריך ליצירת אשכול GKE ב-A4X.

    1. קבלת פרטי הכניסה לאשכול:
    gcloud container clusters get-credentials ${CLUSTER_NAME} --location=${CONTROL_PLANE_REGION}
    
  2. מוודאים שהאשכול חושף כרטיסי רשת של RDMA דרך DRA:

    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, שמספק ערוצי ComputeDomain (IMEX) ל-NVLink מרובה צמתים:

    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, בהיקף של מרחב השמות של עומס העבודה:

    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

טייס אוטומטי

השלב הזה נדרש להגדרות GPU ב-GKE Standard (רק 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

השלב הזה נדרש להגדרות GPU ב-GKE Standard (רק 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. יוצרים את ה-Secret עבור Hugging Face:

    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

טייס אוטומטי

  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 במשקלים של המודל ובמערכי הנתונים. אפשר להריץ את הפקודות האלה באופן מקומי או ב-Pod של GKE כדי לאכלס את הדלי:

‫A4 ו-A3 Ultra

טייס אוטומטי

  1. בודקים את משימת הכנת הנתונים:

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

    envsubst < "data-prep-job.yaml" | kubectl apply -f -
  3. עוקבים אחרי העבודה:

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

רגילה

  1. בודקים את משימת הכנת הנתונים:

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

    envsubst < "${SCRIPT_DIR}/data-prep-job.yaml" | kubectl apply -f -
  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. מורידים את המודל Qwen2.5-32B-Instruct באמצעות Hugging Face CLI (ההורדה הזו דורשת כ-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 אחד של ראש המערכת ומכמה Pods של עובדים עם תמיכה ב-GPU.

‫A4 ו-A3 Ultra

בוחרים את מצב האשכול של GKE שבו השתמשתם כדי ליצור את האשכול:

טייס אוטומטי

  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 "Performing local editable install..."
                  cd /data/verl && pip3 install --no-deps -e .
              volumeMounts:
              - name: training-bucket-vol
                mountPath: /data
            serviceAccountName: ${KSA_NAME}
            nodeSelector:
              cloud.google.com/compute-class: dranet-a4-computeclass-v3
            tolerations:
              - key: "nvidia.com/gpu"
                operator: "Exists"
                effect: "NoSchedule"
            containers:
            - name: ray-worker
              image: verlai/verl:vllm011.latest
              env:
               - name: LD_LIBRARY_PATH
                 value: /usr/local/nvidia/lib64
              resources:
                limits:
                  cpu: "180"
                  memory: "2000Gi"
                  nvidia.com/gpu: "8"
                  ephemeral-storage: "1000Gi"
                requests:
                  cpu: "180"
                  memory: "2000Gi"
                  nvidia.com/gpu: "8"
                  ephemeral-storage: "1000Gi"
                claims:
                - name: rdma-claim
              volumeMounts:
              - name: shared-memory
                mountPath: /dev/shm
              - name: ray-tmp-storage
                mountPath: /tmp
              - name: training-bucket-vol
                mountPath: /data
            volumes:
            - name: shared-memory
              emptyDir:
                medium: "Memory"
                sizeLimit: 250Gi 
            - name: ray-tmp-storage
              emptyDir: {}
            - name: training-bucket-vol
              persistentVolumeClaim:
                claimName: training-bucket-pvc
  2. החלת ה-RayCluster:

    envsubst < "ray-cluster-auto-dranet.yaml" | kubectl apply -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:
            initContainers:
            - name: verl-setup
              image: verlai/verl:vllm011.latest
              command: ["/bin/bash", "-c"]
              args:
                - |
                  echo "Performing local editable install..."
                  cd /data/verl && pip3 install --no-deps -e .
              volumeMounts:
              - name: training-bucket-vol
                mountPath: /data
            serviceAccountName: ${KSA_NAME}
            nodeSelector:
              cloud.google.com/gke-accelerator: ${GPU_TYPE}
            tolerations:
              - key: "nvidia.com/gpu"
                operator: "Exists"
                effect: "NoSchedule"
            containers:
            - name: ray-worker
              image: verlai/verl:vllm011.latest
              env:
               - name: LD_LIBRARY_PATH
                 value: /usr/local/nvidia/lib64
              resources:
                limits:
                  cpu: "220"
                  memory: "2800Gi"
                  nvidia.com/gpu: "8"
                  ephemeral-storage: "1000Gi"
                requests:
                  cpu: "220"
                  memory: "2800Gi"
                  nvidia.com/gpu: "8"
                  ephemeral-storage: "1000Gi"
              volumeMounts:
              - name: nvidia
                mountPath: /usr/local/nvidia
              - name: gib
                mountPath: /usr/local/gib
              - name: shared-memory
                mountPath: /dev/shm
              - name: ray-tmp-storage
                mountPath: /tmp
              - name: training-bucket-vol
                mountPath: /data
            volumes:
            - name: gib
              hostPath:
                path: /home/kubernetes/bin/gib
            - name: nvidia
              hostPath:
                path: /home/kubernetes/bin/nvidia
            - name: lib64
              hostPath:
                path: /lib64
            - name: shared-memory
              emptyDir:
                medium: "Memory"
                sizeLimit: 250Gi 
            - name: sys
              hostPath:
                path: /sys
            - name: proc-sys
              hostPath:
                path: /proc/sys
            - name: ray-tmp-storage
              emptyDir: {}
            - name: training-bucket-vol
              persistentVolumeClaim:
                claimName: training-bucket-pvc
  2. החלת ה-RayCluster:

    envsubst < "ray-cluster-standard.yaml" | kubectl apply -f -

A4X

  1. יוצרים את RDMA ResourceClaimTemplate ואת NVIDIA ComputeDomain. כל Pod של GPU worker תופס ארבעה כרטיסי 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. ה-Pod של Ray head פועל בצומת 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 ראשי אחד וארבעה Pods של עובדים יהיה Running:

    kubectl get pods -w
    

הפעלת משימת GRPO

מגדירים את משימת ההדרכה של למידת חיזוקים ושולחים אותה:

‫A4 ו-A3 Ultra

  1. מגדירים את Ray Client:

    if [ ! -d "env" ]; then
      virtualenv -p $(which python3) env
    else
      echo "Found virtual environment env, not recreating"
    fi
    source env/bin/activate
    pip3 install ray[default]
  2. שחזור של שירות 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. צריך להשתמש בחלון Terminal נפרד בשביל השלב הזה, כי הפקודה הזו חוסמת את ה-Terminal בזמן שהיא פועלת. משתמשים ב-Control+C כדי לעצור את התהליך:

    echo "Starting port-forwarding to ${SVC_NAME} on port 8265..."
    kubectl port-forward svc/"${SVC_NAME}" 8265:8265 -n "${NAMESPACE}" &
  4. בודקים את קובץ המניפסט runtime-env.yaml:

    py_modules: ["."]
    working_dir": "."
    py_executable": "uv run"
    setup_hook: runtime_env.uv_runtime_env_hook.hook 
    env_vars:
      PYTHONPATH: "/data/verl"
      LD_LIBRARY_PATH: "/usr/local/nvidia/lib64"
      NCCL_DEBUG: "INFO"
      NUM_WORKERS: "2"
      CPUS_PER_WORKER: "192"
      GPUS_PER_WORKER: "8"
      NCCL_NET_PLUGIN: "/usr/local/gib/lib64/libnccl-net_internal.so"
      NCCL_CROSS_NIC: "0"
      NCCL_NET_GDR_LEVEL: "PIX"
      NCCL_P2P_NET_CHUNKSIZE: "131072"
      NCCL_NVLS_CHUNKSIZE: "524288"
      NCCL_IB_ADAPTIVE_ROUTING: "1"
      NCCL_IB_QPS_PER_CONNECTION: "4"
      NCCL_IB_TC: "52"
      NCCL_IB_FIFO_TC: "84"
      NCCL_TUNER_CONFIG_PATH: "/usr/local/gib/configs/tuner_config_a4.txtpb" 
      HF_HOME: "/data/huggingface_cache"
      GLOO_SOCKET_IFNAME: "eth0" 
    pip:
      packages:
        - torch 
        - torchvision
        - TransferQueue

    אם אתם משתמשים ב-GPU מסוג H200, צריך לשנות את NCCL_TUNER_CONFIG_PATH ל-/usr/local/gib/configs/tuner_config_a3u.txtpb.

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

  5. שולחים את המשימה באמצעות ray job submit:

    ray job submit \
      --address "http://localhost:8265" \
      --runtime-env runtime-env.yaml \
        -- \
        bash -c "
            cd /data/verl && PYTHONUNBUFFERED=1 python3 -m verl.trainer.main_ppo \
            data.train_files=/data/gsm8k/train.parquet \
            data.val_files=/data/gsm8k/test.parquet \
            data.train_batch_size=256 \
            data.max_prompt_length=512 \
            data.max_response_length=512 \
            actor_rollout_ref.model.path=/data/Qwen2.5-32B-Instruct \
            actor_rollout_ref.actor.optim.lr=1e-5 \
            actor_rollout_ref.actor.ppo_mini_batch_size=256 \
            actor_rollout_ref.actor.ppo_micro_batch_size_per_gpu=64 \
            actor_rollout_ref.rollout.name=vllm \
            actor_rollout_ref.rollout.log_prob_micro_batch_size_per_gpu=8 \
            actor_rollout_ref.rollout.tensor_model_parallel_size=8 \
            actor_rollout_ref.rollout.gpu_memory_utilization=0.6 \
            actor_rollout_ref.ref.log_prob_micro_batch_size_per_gpu=4 \
            actor_rollout_ref.actor.strategy=fsdp2 \
            algorithm.kl_ctrl.kl_coef=0.001 \
            trainer.logger=console \
            trainer.val_before_train=False \
            trainer.n_gpus_per_node=8 \
            trainer.nnodes=2 \
            trainer.save_freq=10 \
            trainer.test_freq=10 \
            trainer.default_local_dir=/data/verl/checkpoints \
            algorithm.adv_estimator=grpo \
            actor_rollout_ref.rollout.n=8 \
            trainer.total_epochs=2"

    עוקבים אחרי היומנים בלוח הבקרה של Ray או בפלט של המסוף. מחפשים את הערך critic/score/mean כדי לראות אם יש עלייה, שמצביעה על למידה.

  6. אחרי שהאימון מסתיים, אפשר למצוא את נקודות הבקרה של המודל שאומן ב-gs://$GS_BUCKET/verl/checkpoints.

A4X

  1. אחזור שם ה-Pod של Ray head:

    export HEAD_POD=$(kubectl get pod -n ${NAMESPACE} -l ray.io/node-type=head -o jsonpath='{.items[0].metadata.name}')
    
  2. מגדירים את קובץ סביבת זמן הריצה של Ray ישירות ב-Pod הראשי:

    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. שולחים את משימת האימון של GRPO על ידי הרצה ב-Ray head Pod:

    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):

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

    החלפה של JOB_ID. כדי לוודא ש-NVLink בין צמתים פעיל, מחפשים ביומנים שורות של NCCL שמכילות via P2P/MNNVL.

הסרת המשאבים

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

‫A4 ו-A3 Ultra

טייס אוטומטי

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

    envsubst < ray-cluster-auto-dranet.yaml | kubectl delete -f - --ignore-not-found=true || true
  2. מחיקת Cloud Storage FUSE:

    envsubst < gcsfuse-storage.yaml | kubectl delete -f - --ignore-not-found=true || true
  3. מוחקים את משאבי DRANET:

    kubectl delete -f "resourceclaim-dranet.yaml" --ignore-not-found=true || true
    kubectl delete -f "computeclass-dranet.yaml" --ignore-not-found=true || true
  4. מוחקים את הקטגוריה של Cloud Storage:

    gcloud storage rm -r "gs://${GS_BUCKET}" || true
  5. מחיקת אשכול GKE:

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

רגילה

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

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

    envsubst < "${SCRIPT_DIR}/gcsfuse-storage.yaml" | kubectl delete -f - --ignore-not-found=true || true
  3. מוחקים את הקטגוריה של Cloud Storage:

    gcloud storage rm -r "gs://${GS_BUCKET}" --project="${PROJECT_ID}" || true
  4. מחיקת אשכול GKE:

    gcloud container clusters delete ${CLUSTER_NAME} --location=${CONTROL_PLANE_REGION} --project=${PROJECT_ID} --quiet || true
  5. מחיקת רשתות VPC ורשתות משנה:

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

A4X

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

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