אופטימיזציה של ניצול משאבי GKE לעומסי עבודה מעורבים של אימון AI/ML והסקת מסקנות

במדריך הזה נסביר איך לשתף ביעילות משאבי האצה בין עומסי עבודה (workload) של אימון והסקת מסקנות באשכול יחיד של Google Kubernetes Engine ‏(GKE). כשמפיצים את עומסי העבודה המעורבים באשכול יחיד, משפרים את ניצול המשאבים, מפשטים את ניהול האשכולות, מצמצמים את הבעיות שנובעות ממגבלות על כמות המאיצים ומשפרים את היעילות הכוללת מבחינת עלויות.

במדריך הזה תיצרו פריסת מילוי בקשות בעדיפות גבוהה באמצעות מודל שפה גדול (LLM) Gemma 2 להסקת מסקנות, ומסגרת Hugging Face TGI (ממשק ליצירת טקסט) למילוי בקשות, יחד עם משימת כוונון עדין של LLM בעדיפות נמוכה. שתי עומסי העבודה פועלים באותו אשכול שמשתמש ב-GPU מסוג NVIDIA L4. אתם משתמשים ב-Kueue, מערכת בקוד פתוח לניהול ותזמון של עומסי עבודה (workload) ב-Kubernetes. בעזרת Kueue אפשר לתת עדיפות למשימות של הצגת מודלים ולבטל משימות אימון בעדיפות נמוכה יותר, כדי לבצע אופטימיזציה של ניצול המשאבים. כשהדרישות להצגת מודעות יורדות, אתם מקצים מחדש את המאיצים הפנויים כדי להמשיך את משימות האימון. אתם משתמשים ב-Kueue ובסיווגי עדיפות כדי לנהל את מכסות המשאבים לאורך התהליך.

המדריך הזה מיועד למהנדסי למידת מכונה (ML), לאדמינים ולמפעילים של פלטפורמות ולמומחים בתחום הנתונים וה-AI שרוצים לאמן ולארח מודל של למידת מכונה (ML) באשכול GKE, וגם רוצים לצמצם את העלויות ואת תקורה הניהול, במיוחד כשמדובר במספר מוגבל של מאיצים. מידע נוסף על תפקידים נפוצים ועל משימות לדוגמה שאנחנו מתייחסים אליהן בתוכן זמין במאמר תפקידים נפוצים של משתמשים ומשימות ב-GKE. Google Cloud

לפני שקוראים את הדף הזה, חשוב לוודא שמכירים את הנושאים הבאים:

מטרות

בסיום המדריך הזה, תוכלו לבצע את השלבים הבאים:

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

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

  • נכנסים לחשבון Google Cloud . אם אתם משתמשים חדשים ב- Google Cloud, צרו חשבון כדי שתוכלו להעריך את הביצועים של המוצרים שלנו בתרחישים מהעולם האמיתי. לקוחות חדשים מקבלים בחינם גם קרדיט בשווי 300$ להרצה, לבדיקה ולפריסה של עומסי העבודה.
  • In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator role (roles/resourcemanager.projectCreator), which contains the resourcemanager.projects.create permission. Learn how to grant roles.

    Go to project selector

  • Verify that billing is enabled for your Google Cloud project.

  • Enable the required APIs.

    Roles required to enable APIs

    To enable APIs, you need the Service Usage Admin IAM role (roles/serviceusage.serviceUsageAdmin), which contains the serviceusage.services.enable permission. Learn how to grant roles.

    Enable the APIs

  • In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator role (roles/resourcemanager.projectCreator), which contains the resourcemanager.projects.create permission. Learn how to grant roles.

    Go to project selector

  • Verify that billing is enabled for your Google Cloud project.

  • Enable the required APIs.

    Roles required to enable APIs

    To enable APIs, you need the Service Usage Admin IAM role (roles/serviceusage.serviceUsageAdmin), which contains the serviceusage.services.enable permission. Learn how to grant roles.

    Enable the APIs

  • צריך לוודא שיש לכם בפרויקט את התפקיד או התפקידים הבאים: roles/container.admin, roles/iam.serviceAccountAdmin

    בדיקת התפקידים

    1. נכנסים לדף IAM במסוף Google Cloud .

      כניסה לדף IAM
    2. בוחרים את הפרויקט.
    3. בעמודה Principal (חשבון המשתמש), מוצאים את כל השורות שבהן מופיע השם שלכם או של קבוצה שאתם נכללים בה. כדי לברר באילו קבוצות אתם נכללים, פנו לאדמין.

    4. בודקים את העמודה Role בכל השורות שבהן מצוין או מופיע השם שלכם, כדי לראות אם רשימת התפקידים כוללת את התפקידים הנדרשים.

    מתן התפקידים

    1. נכנסים לדף IAM במסוף Google Cloud .

      כניסה לדף IAM
    2. בוחרים את הפרויקט.
    3. לוחצים על Grant access.
    4. בשדה New principals, מזינים את מזהה המשתמש. ‫ בדרך כלל מזהה המשתמש הוא כתובת האימייל של חשבון Google.

    5. לוחצים על Select a role ומחפשים את התפקיד.
    6. כדי לתת עוד תפקידים, לוחצים על Add another role ומוסיפים אותם.
    7. לוחצים על Save.

הכנת הסביבה

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

גישה למודל

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

  1. חתימה על הסכם ההסכמה לרישיון. נכנסים אל דף ההסכמה לשימוש במודל, מאמתים את ההסכמה באמצעות חשבון Hugging Face ומאשרים את התנאים לשימוש במודל.
  2. יצירת אסימון גישה כדי לגשת למודל דרך Hugging Face, צריך טוקן של Hugging Face. אם עדיין אין לכם אסימון, אתם יכולים ליצור אסימון חדש באמצעות השלבים הבאים:

    1. לוחצים על הפרופיל שלך > הגדרות > טוקנים של גישה.
    2. בוחרים באפשרות New Token (טוקן חדש).
    3. מציינים שם לבחירתכם ותפקיד ברמה של Read לפחות.
    4. לוחצים על יצירת אסימון.
    5. מעתיקים את הטוקן שנוצר ללוח.

הפעלת Cloud Shell

במדריך הזה משתמשים ב-Cloud Shell כדי לנהל משאבים שמתארחים ב-Google Cloud. ב-Cloud Shell מותקן מראש התוכנה שדרושה לכם כדי לבצע את המדריך הזה, כולל kubectl, ה-CLI של gcloud ו-Terraform.

כדי להגדיר את הסביבה באמצעות Cloud Shell:

  1. ב Google Cloud מסוף, מפעילים סשן של Cloud Shell על ידי לחיצה על Activate Cloud Shell בGoogle Cloud מסוף.סמל ההפעלה של Cloud Shell תופעל סשן בחלונית התחתונה של Google Cloud המסוף.

  2. מגדירים את משתני הסביבה שמוגדרים כברירת מחדל:

    gcloud config set project PROJECT_ID
    export PROJECT_ID=$(gcloud config get project)
    

    מחליפים את PROJECT_ID ב Google Cloud מזהה הפרויקט.

  3. משכפלים את הקוד לדוגמה מ-GitHub. ב-Cloud Shell, מריצים את הפקודות הבאות:

    git clone https://github.com/GoogleCloudPlatform/kubernetes-engine-samples/
    cd kubernetes-engine-samples/ai-ml/mix-train-and-inference
    export EXAMPLE_HOME=$(pwd)
    

יצירת אשכול GKE

אתם יכולים להשתמש באשכול במצב Autopilot או באשכול רגיל בשביל עומסי העבודה המעורבים. כדי ליהנות מחוויית Kubernetes מנוהלת באופן מלא, מומלץ להשתמש באשכול Autopilot. כדי לבחור את מצב הפעולה של GKE שהכי מתאים לעומסי העבודה שלכם, אפשר לעיין במאמר בחירת מצב פעולה של GKE.

טייס אוטומטי

  1. מגדירים את משתני הסביבה שמוגדרים כברירת מחדל ב-Cloud Shell:

    export HF_TOKEN=HF_TOKEN
    export REGION=REGION
    export CLUSTER_NAME="llm-cluster"
    export PROJECT_NUMBER=$(gcloud projects list \
        --filter="$(gcloud config get-value project)" \
        --format="value(PROJECT_NUMBER)")
    export MODEL_BUCKET="model-bucket-$PROJECT_ID"
    

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

    • HF_TOKEN: האסימון של Hugging Face שיצרתם קודם.
    • REGION: אזור שתומך בסוג המאיץ שרוצים להשתמש בו, לדוגמה, us-central1 עבור GPU מסוג L4.

    אפשר לשנות את המשתנה MODEL_BUCKET, שמייצג את הקטגוריה של Cloud Storage שבה מאוחסנים משקלי המודל שאומן.

  2. יצירת אשכול Autopilot:

    gcloud container clusters create-auto ${CLUSTER_NAME} \
        --project=${PROJECT_ID} \
        --location=${REGION} \
        --release-channel=rapid
    
  3. יוצרים את הקטגוריה של Cloud Storage לעבודת הכוונון העדין:

    gcloud storage buckets create gs://${MODEL_BUCKET} \
        --location ${REGION} \
        --uniform-bucket-level-access
    
  4. כדי להעניק גישה לקטגוריה של Cloud Storage, מריצים את הפקודה הבאה:

    gcloud storage buckets add-iam-policy-binding "gs://$MODEL_BUCKET" \
        --role=roles/storage.objectAdmin \
        --member=principal://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$PROJECT_ID.svc.id.goog/subject/ns/llm/sa/default \
        --condition=None
    
  5. כדי לקבל פרטי כניסה לאימות עבור האשכול, מריצים את הפקודה הבאה:

    gcloud container clusters get-credentials llm-cluster \
        --location=$REGION \
        --project=$PROJECT_ID
    
  6. יוצרים מרחב שמות לפריסות. ב-Cloud Shell, מריצים את הפקודה הבאה:

    kubectl create ns llm
    

רגילה

  1. מגדירים את משתני הסביבה שמוגדרים כברירת מחדל ב-Cloud Shell:

    export HF_TOKEN=HF_TOKEN
    export REGION=REGION
    export CLUSTER_NAME="llm-cluster"
    export GPU_POOL_MACHINE_TYPE="g2-standard-24"
    export GPU_POOL_ACCELERATOR_TYPE="nvidia-l4"
    export PROJECT_NUMBER=$(gcloud projects list \
        --filter="$(gcloud config get-value project)" \
        --format="value(PROJECT_NUMBER)")
    export MODEL_BUCKET="model-bucket-$PROJECT_ID"
    

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

    • HF_TOKEN: האסימון של Hugging Face שיצרתם קודם.
    • REGION: האזור שתומך בסוג המאיץ שרוצים להשתמש בו. לדוגמה, us-central1 עבור GPU מסוג L4.

    אפשר לשנות את המשתנים האלה:

    • GPU_POOL_MACHINE_TYPE: סדרת המכונות של מאגר הצמתים שרוצים להשתמש בה באזור שנבחר. הערך הזה תלוי בסוג המאיץ שבחרתם. מידע נוסף זמין במאמר מגבלות השימוש ב-GPU ב-GKE. לדוגמה, במדריך הזה נעשה שימוש ב-g2-standard-24 עם שני GPU שמצורפים לכל צומת. רשימת מעבדי ה-GPU הזמינים העדכנית ביותר מופיעה במאמר מעבדי GPU לעומסי עבודה של מחשוב.
    • GPU_POOL_ACCELERATOR_TYPE: סוג המאיץ שנתמך באזור שבחרתם. לדוגמה, במדריך הזה נעשה שימוש ב-nvidia-l4. רשימת ה-GPU הזמינים העדכנית מופיעה במאמר מעבדי GPU לעומסי עבודה של מחשוב.
    • MODEL_BUCKET: הקטגוריה של Cloud Storage שבה מאחסנים את משקלי המודל שאומן.
  2. יצירת אשכול רגיל:

    gcloud container clusters create ${CLUSTER_NAME} \
        --project=${PROJECT_ID} \
        --location=${REGION} \
        --workload-pool=${PROJECT_ID}.svc.id.goog \
        --release-channel=rapid \
        --machine-type=e2-standard-4 \
        --addons GcsFuseCsiDriver \
        --num-nodes=1
    
  3. יוצרים את מאגר צמתי ה-GPU לעומסי עבודה של הסקת מסקנות ושינוי פרמטרים:

    gcloud container node-pools create gpupool \
        --accelerator type=${GPU_POOL_ACCELERATOR_TYPE},count=2,gpu-driver-version=latest \
        --project=${PROJECT_ID} \
        --location=${REGION} \
        --node-locations=${REGION}-a \
        --cluster=${CLUSTER_NAME} \
        --machine-type=${GPU_POOL_MACHINE_TYPE} \
        --num-nodes=3
    
  4. יוצרים את הקטגוריה של Cloud Storage לעבודת הכוונון העדין:

    gcloud storage buckets create gs://${MODEL_BUCKET} \
        --location ${REGION} \
        --uniform-bucket-level-access
    
  5. כדי להעניק גישה לקטגוריה של Cloud Storage, מריצים את הפקודה הבאה:

    gcloud storage buckets add-iam-policy-binding "gs://$MODEL_BUCKET" \
        --role=roles/storage.objectAdmin \
        --member=principal://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$PROJECT_ID.svc.id.goog/subject/ns/llm/sa/default \
        --condition=None
    
  6. כדי לקבל פרטי כניסה לאימות עבור האשכול, מריצים את הפקודה הבאה:

    gcloud container clusters get-credentials llm-cluster \
        --location=$REGION \
        --project=$PROJECT_ID
    
  7. יוצרים מרחב שמות לפריסות. ב-Cloud Shell, מריצים את הפקודה הבאה:

    kubectl create ns llm
    

יצירת סוד של Kubernetes לפרטי הכניסה של Hugging Face

כדי ליצור סוד של Kubernetes שמכיל את האסימון של Hugging Face, מריצים את הפקודה הבאה:

kubectl create secret generic hf-secret \
    --from-literal=hf_api_token=$HF_TOKEN \
    --dry-run=client -o yaml | kubectl apply --namespace=llm --filename=-

הגדרת Kueue

במדריך הזה, Kueue הוא מנהל המשאבים המרכזי, שמאפשר שיתוף יעיל של מעבדי GPU בין עומסי העבודה של האימון וההגשה. ‫Kueue עושה את זה על ידי הגדרת דרישות משאבים ('טעמים'), תעדוף של עומסי עבודה באמצעות תורים (עם תעדוף של משימות הגשה על פני אימון) והקצאה דינמית של משאבים על סמך הביקוש והעדיפות. במדריך הזה נעשה שימוש בסוג המשאב Workload כדי לקבץ את עומסי העבודה של ההסקה וההתאמה העדינה, בהתאמה.

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

כדי לשלוט בפריסת שרת ההסקות באמצעות Kueue, מפעילים את השילוב של pod ומגדירים את managedJobsNamespaceSelector כך שמרחבי השמות kube-system ו-kueue-system יוחרגו.

  1. במאגר /kueue, רואים את הקוד בkustomization.yaml. קובץ המניפסט הזה מתקין את מנהל המשאבים Kueue עם הגדרות בהתאמה אישית.

    apiVersion: kustomize.config.k8s.io/v1beta1
    kind: Kustomization
    resources:
    - https://github.com/kubernetes-sigs/kueue/releases/download/v0.12.3/manifests.yaml
    patches:
    - path: patch.yaml
      target:
        version: v1
        kind: ConfigMap
        name: kueue-manager-config
    
  2. במאגר /kueue, רואים את הקוד בpatch.yaml. ה-ConfigMap הזה מגדיר את Kueue כך שלא יכלול ניהול של Pods במרחבי השמות kube-system ו-kueue-system.

    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: kueue-manager-config
    data:
      controller_manager_config.yaml: |
        apiVersion: config.kueue.x-k8s.io/v1beta1
        kind: Configuration
        health:
          healthProbeBindAddress: :8081
        metrics:
          bindAddress: :8080
        # enableClusterQueueResources: true
        webhook:
          port: 9443
        leaderElection:
          leaderElect: true
          resourceName: c1f6bfd2.kueue.x-k8s.io
        controller:
          groupKindConcurrency:
            Job.batch: 5
            Pod: 5
            Workload.kueue.x-k8s.io: 5
            LocalQueue.kueue.x-k8s.io: 1
            ClusterQueue.kueue.x-k8s.io: 1
            ResourceFlavor.kueue.x-k8s.io: 1
        clientConnection:
          qps: 50
          burst: 100
        #pprofBindAddress: :8083
        #waitForPodsReady:
        #  enable: false
        #  timeout: 5m
        #  blockAdmission: false
        #  requeuingStrategy:
        #    timestamp: Eviction
        #    backoffLimitCount: null # null indicates infinite requeuing
        #    backoffBaseSeconds: 60
        #    backoffMaxSeconds: 3600
        #manageJobsWithoutQueueName: true
        managedJobsNamespaceSelector:
          matchExpressions:
            - key: kubernetes.io/metadata.name
              operator: NotIn
              values: [ kube-system, kueue-system ]
        #internalCertManagement:
        #  enable: false
        #  webhookServiceName: ""
        #  webhookSecretName: ""
        integrations:
          frameworks:
          - "batch/job"
          - "kubeflow.org/mpijob"
          - "ray.io/rayjob"
          - "ray.io/raycluster"
          - "jobset.x-k8s.io/jobset"
          - "kubeflow.org/paddlejob"
          - "kubeflow.org/pytorchjob"
          - "kubeflow.org/tfjob"
          - "kubeflow.org/xgboostjob"
          - "kubeflow.org/jaxjob"
          - "workload.codeflare.dev/appwrapper"
          - "pod"
        #  - "deployment" # requires enabling pod integration
        #  - "statefulset" # requires enabling pod integration
        #  - "leaderworkerset.x-k8s.io/leaderworkerset" # requires enabling pod integration
        #  externalFrameworks:
        #  - "Foo.v1.example.com"
        #fairSharing:
        #  enable: true
        #  preemptionStrategies: [LessThanOrEqualToFinalShare, LessThanInitialShare]
        #admissionFairSharing:
        #  usageHalfLifeTime: "168h" # 7 days
        #  usageSamplingInterval: "5m"
        #  resourceWeights: # optional, defaults to 1 for all resources if not specified
        #    cpu: 0    # if you want to completely ignore cpu usage
        #    memory: 0 # ignore completely memory usage
        #    example.com/gpu: 100 # and you care only about GPUs usage
        #resources:
        #  excludeResourcePrefixes: []
        #  transformations:
        #  - input: nvidia.com/mig-4g.5gb
        #    strategy: Replace | Retain
        #    outputs:
        #      example.com/accelerator-memory: 5Gi
        #      example.com/accelerator-gpc: 4
        #objectRetentionPolicies:
        #  workloads:
        #    afterFinished: null # null indicates infinite retention, 0s means no retention at all
        #    afterDeactivatedByKueue: null # null indicates infinite retention, 0s means no retention at all
    
  3. ב-Cloud Shell, מריצים את הפקודה הבאה כדי להתקין את Kueue:

    cd ${EXAMPLE_HOME}
    kubectl kustomize kueue |kubectl apply --server-side --filename=-
    

    מחכים עד שה-Pods של Kueue יהיו מוכנים:

    watch kubectl --namespace=kueue-system get pods
    

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

    NAME                                        READY   STATUS    RESTARTS   AGE
    kueue-controller-manager-bdc956fc4-vhcmx    1/1     Running   0          3m15s
    
  4. במאגר /workloads, צופים בקבצים flavors.yaml,‏ cluster-queue.yaml ו-local-queue.yaml. בקובצי המניפסט האלה מצוין איך Kueue מנהל את מכסות המשאבים:

    ResourceFlavor

    קובץ המניפסט הזה מגדיר ResourceFlavor כברירת מחדל ב-Kueue לניהול משאבים.

    apiVersion: kueue.x-k8s.io/v1beta1
    kind: ResourceFlavor
    metadata:
      name: default-flavor
    

    ClusterQueue

    קובץ המניפסט הזה מגדיר ClusterQueue של Kueue עם מגבלות משאבים למעבד (CPU), לזיכרון ולמעבד גרפי (GPU).

    במדריך הזה נעשה שימוש בצמתים עם שני מעבדי GPU מסוג Nvidia L4 שמצורפים, עם סוג הצומת המתאים g2-standard-24, שמציע 24 vCPU ו-96GB RAM. בדוגמת הקוד מוצג איך להגביל את השימוש במשאבים של עומס העבודה לשימוש מקסימלי של שש יחידות GPU.

    השדה preemption בהגדרת ClusterQueue מפנה אל PriorityClasses כדי לקבוע אילו פודים אפשר להפסיק לפני הזמן כשאין מספיק משאבים.

    apiVersion: kueue.x-k8s.io/v1beta1
    kind: ClusterQueue
    metadata:
      name: "cluster-queue"
    spec:
      namespaceSelector: {} # match all.
      preemption:
        reclaimWithinCohort: LowerPriority
        withinClusterQueue: LowerPriority
      resourceGroups:
      - coveredResources: [ "cpu", "memory", "nvidia.com/gpu", "ephemeral-storage" ]
        flavors:
        - name: default-flavor
          resources:
          - name: "cpu"
            nominalQuota: 72
          - name: "memory"
            nominalQuota: 288Gi
          - name: "nvidia.com/gpu"
            nominalQuota: 6
          - name: "ephemeral-storage"
            nominalQuota: 200Gi
    

    LocalQueue

    קובץ המניפסט הזה יוצר LocalQueue בשם lq במרחב השמות llm.

    apiVersion: kueue.x-k8s.io/v1beta1
    kind: LocalQueue
    metadata:
      namespace: llm # LocalQueue under llm namespace 
      name: lq
    spec:
      clusterQueue: cluster-queue # Point to the ClusterQueue
    
  5. צפייה בקבצים default-priorityclass.yaml, low-priorityclass.yaml ו-high-priorityclass.yaml. קבצי המניפסט האלה מגדירים את אובייקטי PriorityClass לתזמון ב-Kubernetes.

    עדיפות ברירת מחדל

    apiVersion: scheduling.k8s.io/v1
    kind: PriorityClass
    metadata:
      name: default-priority-nonpreempting
    value: 10
    preemptionPolicy: Never
    globalDefault: true
    description: "This priority class will not cause other pods to be preempted."
    

    עדיפות נמוכה

    apiVersion: scheduling.k8s.io/v1
    kind: PriorityClass
    metadata:
      name: low-priority-preempting
    value: 20
    preemptionPolicy: PreemptLowerPriority
    globalDefault: false
    description: "This priority class will cause pods with lower priority to be preempted."
    

    עדיפות גבוהה

    apiVersion: scheduling.k8s.io/v1
    kind: PriorityClass
    metadata:
      name: high-priority-preempting
    value: 30
    preemptionPolicy: PreemptLowerPriority
    globalDefault: false
    description: "This high priority class will cause other pods to be preempted."
    
  6. כדי ליצור את אובייקטי Kueue ו-Kubernetes, מריצים את הפקודות הבאות כדי להחיל את המניפסטים המתאימים.

    cd ${EXAMPLE_HOME}/workloads
    kubectl apply --filename=flavors.yaml
    kubectl apply --filename=default-priorityclass.yaml
    kubectl apply --filename=high-priorityclass.yaml
    kubectl apply --filename=low-priorityclass.yaml
    kubectl apply --filename=cluster-queue.yaml
    kubectl apply --filename=local-queue.yaml --namespace=llm
    

פריסת שרת ההיקשים TGI

בקטע הזה תפרסו את מאגר TGI כדי להפעיל את מודל Gemma 2.

  1. במאגר /workloads, צופים בקובץ tgi-gemma-2-9b-it-hp.yaml. קובץ המניפסט הזה מגדיר פריסת Kubernetes כדי לפרוס את זמן הריצה של TGI ואת מודל gemma-2-9B-it. פריסה היא אובייקט Kubernetes API שמאפשר להפעיל כמה רפליקות של Pods שמפוזרות בין הצמתים באשכול.

    הפריסה נותנת עדיפות למשימות של הסקת מסקנות ומשתמשת בשני מעבדי GPU עבור המודל. הוא משתמש במקביליות טנסורית על ידי הגדרת משתנה הסביבה NUM_SHARD כדי להתאים את המודל לזיכרון ה-GPU.

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: tgi-gemma-deployment
      labels:
        app: gemma-server
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: gemma-server
      template:
        metadata:
          labels:
            app: gemma-server
            ai.gke.io/model: gemma-2-9b-it
            ai.gke.io/inference-server: text-generation-inference
            examples.ai.gke.io/source: user-guide
            kueue.x-k8s.io/queue-name: lq
        spec:
          priorityClassName: high-priority-preempting
          containers:
          - name: inference-server
            image: us-docker.pkg.dev/deeplearning-platform-release/gcr.io/huggingface-text-generation-inference-cu121.2-1.ubuntu2204.py310
            resources:
              requests:
                cpu: "4"
                memory: "30Gi"
                ephemeral-storage: "30Gi"
                nvidia.com/gpu: "2"
              limits:
                cpu: "4"
                memory: "30Gi"
                ephemeral-storage: "30Gi"
                nvidia.com/gpu: "2"
            env:
            - name: AIP_HTTP_PORT
              value: '8000'
            - name: NUM_SHARD
              value: '2'
            - name: MODEL_ID
              value: google/gemma-2-9b-it
            - name: HUGGING_FACE_HUB_TOKEN
              valueFrom:
                secretKeyRef:
                  name: hf-secret
                  key: hf_api_token
            volumeMounts:
            - mountPath: /dev/shm
              name: dshm
          volumes:
          - name: dshm
            emptyDir:
              medium: Memory
          nodeSelector:
            cloud.google.com/gke-accelerator: "nvidia-l4"
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: llm-service
    spec:
      selector:
        app: gemma-server
      type: ClusterIP
      ports:
      - protocol: TCP
        port: 8000
        targetPort: 8000
    
  2. מריצים את הפקודה הבאה כדי להחיל את המניפסט:

    kubectl apply --filename=tgi-gemma-2-9b-it-hp.yaml --namespace=llm
    

    פעולת הפריסה תימשך כמה דקות.

  3. כדי לבדוק אם GKE יצר את הפריסה בהצלחה, מריצים את הפקודה הבאה:

    kubectl --namespace=llm get deployment
    

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

    NAME                   READY   UP-TO-DATE   AVAILABLE   AGE
    tgi-gemma-deployment   1/1     1            1           5m13s
    

אימות ניהול המכסות ב-Kueue

בקטע הזה, מאשרים שמכסת ה-GPU של הפריסה נאכפת בצורה נכונה על ידי Kueue.

  1. כדי לבדוק אם Kueue מודע לפריסה שלכם, מריצים את הפקודה הבאה כדי לאחזר את הסטטוס של אובייקטים של עומסי עבודה:

    kubectl --namespace=llm get workloads
    

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

    NAME                                              QUEUE   RESERVED IN     ADMITTED   FINISHED   AGE
    pod-tgi-gemma-deployment-6bf9ffdc9b-zcfrh-84f19   lq      cluster-queue   True                  8m23s
    
  2. כדי לבדוק את ההחלפה של מכסות, משנים את קנה המידה של הפריסה לארבעה עותקים:

    kubectl scale --replicas=4 deployment/tgi-gemma-deployment --namespace=llm
    
  3. מריצים את הפקודה הבאה כדי לראות את מספר הרפליקות ש-GKE פורס:

    kubectl get workloads --namespace=llm
    

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

    NAME                                              QUEUE   RESERVED IN     ADMITTED   FINISHED   AGE
    pod-tgi-gemma-deployment-6cb95cc7f5-5thgr-3f7d4   lq      cluster-queue   True                  14s
    pod-tgi-gemma-deployment-6cb95cc7f5-cbxg2-d9fe7   lq      cluster-queue   True                  5m41s
    pod-tgi-gemma-deployment-6cb95cc7f5-tznkl-80f6b   lq                                            13s
    pod-tgi-gemma-deployment-6cb95cc7f5-wd4q9-e4302   lq      cluster-queue   True                  13s
    

    בפלט אפשר לראות שרק שלושה פודים התקבלו בגלל מכסת המשאבים שאוכפת Kueue.

  4. מריצים את הפקודה הבאה כדי להציג את קבוצות ה-Pod במרחב השמות llm:

    kubectl get pod --namespace=llm
    

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

    NAME                                    READY   STATUS            RESTARTS   AGE
    tgi-gemma-deployment-7649884d64-6j256   1/1     Running           0          4m45s
    tgi-gemma-deployment-7649884d64-drpvc   0/1     SchedulingGated   0          7s
    tgi-gemma-deployment-7649884d64-thdkq   0/1     Pending           0          7s
    tgi-gemma-deployment-7649884d64-znvpb   0/1     Pending           0          7s
    
  5. עכשיו מצמצמים את הפריסה בחזרה ל-1. חובה לבצע את השלב הזה לפני שמפעילים את משימת הכוונון העדין, אחרת המשימה לא תתקבל כי למשימת ההסקה יש עדיפות.

    kubectl scale --replicas=1 deployment/tgi-gemma-deployment --namespace=llm
    

הסבר על ההתנהגות

במקרה של דוגמת ההרחבה, נוצרות רק שלוש רפליקות (למרות ההרחבה לארבע) בגלל מגבלת מכסת ה-GPU שהגדרתם בהגדרות של ClusterQueue. בקטע spec.resourceGroups של ClusterQueue מוגדר nominalQuota עם הערך 6 עבור nvidia.com/gpu. הפריסה מציינת שכל Pod דורש 2 יחידות GPU. לכן, התור ClusterQueue יכול להכיל רק עד שלוש רפליקות של הפריסה בכל פעם (כי 3 רפליקות * 2 GPU לכל רפליקה = 6 GPU, שזה המכסה הכוללת).

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

פריסת משימת האימון

בקטע הזה מפרטים איך פורסים משימת כוונון עדין בעדיפות נמוכה יותר למודל Gemma 2 שדורש ארבע יחידות GPU בשני Pods. ב-Kubernetes, בקר משימות יוצר פוד אחד או יותר ועוזר לוודא שהם מבצעים משימה ספציפית בהצלחה.

למשימה הזו יידרש שאר מכסת ה-GPU ב-ClusterQueue. העבודה משתמשת בתמונה שנוצרה מראש ושומרת נקודות ביקורת כדי לאפשר הפעלה מחדש מתוצאות ביניים.

משימת הכוונון העדין משתמשת במערך הנתונים b-mc2/sql-create-context. אפשר למצוא את המקור של משימת הכוונון העדין במאגר.

  1. צפייה בקובץ fine-tune-l4.yaml. המניפסט הזה מגדיר את משימת הכוונון העדין.

    apiVersion: v1
    kind: Service
    metadata:
      name: headless-svc-l4
    spec:
      clusterIP: None # clusterIP must be None to create a headless service
      selector:
        job-name: finetune-gemma-l4 # must match Job name
    ---
    apiVersion: batch/v1
    kind: Job
    metadata:
      name: finetune-gemma-l4
      labels:
        kueue.x-k8s.io/queue-name: lq
    spec:
      backoffLimit: 4
      completions: 2
      parallelism: 2
      completionMode: Indexed
      suspend: true # Set to true to allow Kueue to control the Job when it starts
      template:
        metadata:
          labels:
            app: finetune-job
          annotations:
            gke-gcsfuse/volumes: "true"
            gke-gcsfuse/memory-limit: "35Gi"
        spec:
          priorityClassName: low-priority-preempting
          containers:
          - name: gpu-job
            imagePullPolicy: Always
            image: us-docker.pkg.dev/google-samples/containers/gke/gemma-fine-tuning:v1.0.0
            ports:
            - containerPort: 29500
            resources:
              requests:
                nvidia.com/gpu: "2"
              limits:
                nvidia.com/gpu: "2"
            command:
            - bash
            - -c
            - |
              accelerate launch \
              --config_file fsdp_config.yaml \
              --debug \
              --main_process_ip finetune-gemma-l4-0.headless-svc-l4 \
              --main_process_port 29500 \
              --machine_rank ${JOB_COMPLETION_INDEX} \
              --num_processes 4 \
              --num_machines 2 \
              fine_tune.py
            env:
            - name: "EXPERIMENT"
              value: "finetune-experiment"
            - name: MODEL_NAME
              value: "google/gemma-2-2b"
            - name: NEW_MODEL
              value: "gemma-ft"
            - name: MODEL_PATH
              value: "/model-data/model-gemma2/experiment"
            - name: DATASET_NAME
              value: "b-mc2/sql-create-context"
            - name: DATASET_LIMIT
              value: "5000"
            - name: EPOCHS
              value: "1"
            - name: GRADIENT_ACCUMULATION_STEPS
              value: "2"
            - name: CHECKPOINT_SAVE_STEPS
              value: "10"
            - name: HF_TOKEN
              valueFrom:
                secretKeyRef:
                  name: hf-secret
                  key: hf_api_token
            volumeMounts:
            - mountPath: /dev/shm
              name: dshm
            - name: gcs-fuse-csi-ephemeral
              mountPath: /model-data
              readOnly: false
          nodeSelector:
            cloud.google.com/gke-accelerator: nvidia-l4
          restartPolicy: OnFailure
          serviceAccountName: default
          subdomain: headless-svc-l4
          terminationGracePeriodSeconds: 60
          volumes:
          - name: dshm
            emptyDir:
              medium: Memory
          - name: gcs-fuse-csi-ephemeral
            csi:
              driver: gcsfuse.csi.storage.gke.io
              volumeAttributes:
                bucketName: <MODEL_BUCKET>
                mountOptions: "implicit-dirs"
                gcsfuseLoggingSeverity: warning
    
  2. מחילים את המניפסט כדי ליצור את משימת הכוונון העדין:

    cd ${EXAMPLE_HOME}/workloads
    
    sed -e "s/<MODEL_BUCKET>/$MODEL_BUCKET/g" \
        -e "s/<PROJECT_ID>/$PROJECT_ID/g" \
        -e "s/<REGION>/$REGION/g" \
        fine-tune-l4.yaml |kubectl apply --filename=- --namespace=llm
    
  3. מוודאים שהפריסות פועלות. כדי לבדוק את הסטטוס של אובייקטים של עומסי עבודה, מריצים את הפקודה הבאה:

    kubectl get workloads --namespace=llm
    

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

    NAME                                              QUEUE   RESERVED IN     ADMITTED   FINISHED   AGE
    job-finetune-gemma-l4-3316f                       lq      cluster-queue   True                  29m
    pod-tgi-gemma-deployment-6cb95cc7f5-cbxg2-d9fe7   lq      cluster-queue   True                  68m
    

    לאחר מכן, מציגים את ה-Pods במרחב השמות llm על ידי הרצת הפקודה הבאה:

    kubectl get pod --namespace=llm
    

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

    NAME                                    READY   STATUS    RESTARTS   AGE
    finetune-gemma-l4-0-vcxpz               2/2     Running   0          31m
    finetune-gemma-l4-1-9ppt9               2/2     Running   0          31m
    tgi-gemma-deployment-6cb95cc7f5-cbxg2   1/1     Running   0          70m
    

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

  4. מעיינים ביומני הפלט כדי לוודא שעבודת הכוונון העדין שומרת את נקודות הבדיקה בקטגוריה של Cloud Storage. תהליך הכוונון העדין נמשך כ-10 דקות לפני שהמערכת מתחילה לשמור את נקודת הבדיקה הראשונה.

    kubectl logs --namespace=llm --follow --selector=app=finetune-job
    

    הפלט של נקודת הבדיקה הראשונה שנשמרה אמור להיראות כך:

    {"name": "finetune", "thread": 133763559483200, "threadName": "MainThread", "processName": "MainProcess", "process": 33, "message": "Fine tuning started", "timestamp": 1731002351.0016131, "level": "INFO", "runtime": 451579.89835739136}
    …
    {"name": "accelerate.utils.fsdp_utils", "thread": 136658669348672, "threadName": "MainThread", "processName": "MainProcess", "process": 32, "message": "Saving model to /model-data/model-gemma2/experiment/checkpoint-10/pytorch_model_fsdp_0", "timestamp": 1731002386.1763802, "level": "INFO", "runtime": 486753.8924217224}
    

בדיקה של הקצאה דינמית ושל דחיקה של משימות ב-Kueue בעומס עבודה מעורב

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

  1. מריצים את הפקודה הבאה כדי לשנות את קנה המידה של העותקים של שרת ההסקה לשניים:

    kubectl scale --replicas=2 deployment/tgi-gemma-deployment --namespace=llm
    
  2. בודקים את הסטטוס של אובייקטים של עומס עבודה:

    kubectl get workloads --namespace=llm
    

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

    NAME                                              QUEUE   RESERVED IN     ADMITTED   FINISHED   AGE
    job-finetune-gemma-l4-3316f                       lq                      False                 32m
    pod-tgi-gemma-deployment-6cb95cc7f5-cbxg2-d9fe7   lq      cluster-queue   True                  70m
    pod-tgi-gemma-deployment-6cb95cc7f5-p49sh-167de   lq      cluster-queue   True                  14s
    

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

  3. בודקים את הסטטוס של משימת הכוונון העדין:

    kubectl get job --namespace=llm
    

    הפלט אמור להיראות כך, ולציין שהסטטוס של משימת הדיוק העדין הוא עכשיו מושהה:

    NAME                STATUS      COMPLETIONS   DURATION   AGE
    finetune-gemma-l4   Suspended   0/2                      33m
    
  4. מריצים את הפקודה הבאה כדי לבדוק את ה-Pods:

    kubectl get pod --namespace=llm
    

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

    NAME                                    READY   STATUS              RESTARTS   AGE
    tgi-gemma-deployment-6cb95cc7f5-cbxg2   1/1     Running             0          72m
    tgi-gemma-deployment-6cb95cc7f5-p49sh   0/1     ContainerCreating   0          91s
    
  5. לאחר מכן, בודקים את התרחיש שבו העומס על שרת ההסקה יורד וה-Pods שלו מצטמצמים. מריצים את הפקודה הבאה:

    kubectl scale --replicas=1 deployment/tgi-gemma-deployment --namespace=llm
    

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

    kubectl get workloads --namespace=llm
    

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

    NAME                                              QUEUE   RESERVED IN     ADMITTED   FINISHED   AGE
    job-finetune-gemma-l4-3316f                       lq      cluster-queue   True                  37m
    pod-tgi-gemma-deployment-6cb95cc7f5-cbxg2-d9fe7   lq      cluster-queue   True                  75m
    
  6. מריצים את הפקודה הבאה כדי להציג את המשימות:

    kubectl get job --namespace=llm
    

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

    NAME                STATUS    COMPLETIONS   DURATION   AGE
    finetune-gemma-l4   Running   0/2           2m11s      38m
    

הסרת המשאבים

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

מחיקת המשאבים שנפרסו

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

gcloud storage rm --recursive gs://${MODEL_BUCKET}
gcloud container clusters delete ${CLUSTER_NAME} --location ${REGION}

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