הצגת LLM באמצעות Ray Serve מרובה אשכולות ו-GKE Inference Gateway

במאמר הזה מוסבר איך לנהל בקשות הסקה בכמה אשכולות של Ray Serve ב-Google Kubernetes Engine ‏ (GKE) באמצעות הגדרת Kubernetes Gateway API ו-GKE Inference Gateway. ההגדרה הזו מאפשרת לכם לרכז את ניהול התנועה של כמה צוותים, לפזר עומסי עבודה בין אזורים כדי להגדיל את הקיבולת וליישם ניתוב מודע-מודל על סמך תוכן גוף הבקשה.

יתרונות השימוש ב-GKE Inference Gateway וב-Ray Serve

היתרונות של שימוש ב-GKE Inference Gateway וב-Ray Serve:

  • ניתוב נתיבים: מגדירים לכל RayService קידומת נתיב, ואז מפעילים אותם עם Gateway אחד שמנתב לכמה Ray Services.
  • Model-aware routing: בחירה של RayService להפניה על סמך גוף הבקשה – לדוגמה, על ידי חילוץ המודל המבוקש מבקשת JSON של OpenAI-API.
  • ניהול מדיניות: אפשר לדרוש מפתחות API כדי להשתמש בשירות, או לאכוף מכסות למשתמשים באמצעות Apigee לאימות ולניהול API.
  • מספר אזורים: פיצול התנועה בין כמה אשכולות GKE באמצעות RayServices כדי להשיג זמינות או קיבולת גבוהות יותר באמצעות שערים מרובי אשכולות.
  • הפרדה בין נושאים: שימוש ב-RayServices נפרדים, שאפשר לנהל אותם באמצעות צוותים נפרדים, להשיק אותם בנפרד ולהפעיל אותם בטופולוגיות שונות.
  • אבטחה: אפשר להשתמש ב-Gateway ככלי לסיום SSL כדי לאבטח את תעבורת המשתמשים באינטרנט. מידע נוסף זמין במאמר בנושא אבטחת שערים.

כדי להגדיר ניתוב, צריך לפרוס Gateway,‏ HTTPRoute ו-RayService. בדרך כלל, KubeRay יוצר שירות Kubernetes לכל אשכול יעד של Ray. ‫Ray Serve מפזר את עומס הבקשות בתוך האשכול, בלי צורך ליצור InferencePool או Endpoint Picker.

ניתוב מודע למודל עבור Ray Serve ב-GKE

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

כדי להגדיר ניתוב מודע למודל, צריך לפרוס את רכיבי המפתח הבאים:

  • תוסף לניתוב מבוסס-גוף, לחילוץ שמות של מודלים ממטענים ייעודיים (payloads) של JSON. הפריסה של תוסף הנתב הזה מתבצעת באמצעות Helm.
  • שער GKE (מאזן עומסים פנימי אזורי של אפליקציות ברמה 7) לטיפול בתנועה הנכנסת.
  • כללי HTTPRoute לניתוב תעבורה לשירות Ray הנכון באמצעות כותרות שאוכלסו על ידי תוסף הנתב.
  • אשכולות Ray Serve מרובים לניהול מחזור החיים והתאמה אוטומטית של העומס (autoscaling) של מודלים מבודדים.

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

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

  • מפעילים את ממשק Google Kubernetes Engine API.
  • הפעלת Google Kubernetes Engine API
  • אם רוצים להשתמש ב-CLI של Google Cloud למשימה הזו, צריך להתקין ואז להפעיל את ה-CLI של gcloud. אם התקנתם בעבר את ה-CLI של gcloud, מריצים את הפקודה gcloud components update כדי לקבל את הגרסה העדכנית. יכול להיות שגרסאות קודמות של ה-CLI של gcloud לא יתמכו בהרצת הפקודות שמופיעות במסמך הזה.

הכנת הסביבה

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

export CLUSTER=$(whoami)-ray-bbr
export PROJECT_ID=$(gcloud config get-value project)
export LOCATION=us-central1-b
export REGION=us-central1
export HUGGING_FACE_TOKEN=YOUR_HUGGING_FACE_TOKEN

מחליפים את YOUR_HUGGING_FACE_TOKEN באסימון הגישה שלכם ב-Hugging Face.

הכנת התשתית

בקטע הזה תגדירו אשכול GKE עם Ray ו-Gateway, עם GPUs ברמה L4.

  1. יוצרים אשכול עם Ray Operator ו-Gateway API מופעלים:

    gcloud container clusters create ${CLUSTER} \
        --project ${PROJECT_ID} \
        --location ${LOCATION} \
        --cluster-version 1.35 \
        --gateway-api standard \
        --addons HttpLoadBalancing,RayOperator \
        --enable-ray-cluster-logging \
        --enable-ray-cluster-monitoring \
        --machine-type e2-standard-4
    
  2. יוצרים מאגר צמתים של GPU לעומסי העבודה של המודל:

    gcloud container node-pools create gpu-pool \
        --cluster=${CLUSTER} \
        --location=${LOCATION} \
        --accelerator="type=nvidia-l4,count=1,gpu-driver-version=latest" \
        --machine-type=g2-standard-8 \
        --num-nodes=4
    
  3. יוצרים רשת משנה (subnet) לשרת proxy בלבד עבור מאזן עומסים של אפליקציות (ALB) פנימי אזורי, שנדרשת לניתוב מבוסס-גוף:

    gcloud compute networks subnets create bbr-proxy-only-subnet \
        --purpose=REGIONAL_MANAGED_PROXY \
        --role=ACTIVE \
        --region=${REGION} \
        --network=default \
        --range=192.168.10.0/24
    
  4. פורסים את הסוד של Hugging Face:

    kubectl create secret generic hf-secret \
        --from-literal=hf_api_token=${HUGGING_FACE_TOKEN}
    

פריסת הנתב שמבוסס על גוף ההודעה לניתוב שמודע למודל

התוסף body-based router מיירט בקשות, מנתח את גוף ה-JSON ומחלץ את שדה המודל לכותרת X-Gateway-Model-Name.

  1. יוצרים קובץ בשם helm-values.yaml עם התוכן הבא:

    bbr:
      plugins:
        - type: "body-field-to-header"
          name: "openai-model-extractor"
          json:
            field_name: "model"
            header_name: "X-Gateway-Model-Name"
    
  2. מתקינים את הנתב מבוסס-הגוף באמצעות Helm:

    helm install body-based-router \
        oci://registry.k8s.io/gateway-api-inference-extension/charts/body-based-routing \
        --version v1.4.0 \
        --set provider.name=gke \
        --set inferenceGateway.name=ray-multi-model-gateway \
        --values helm-values.yaml
    

Deploy RayServices

כדי לפרוס את המודלים, צריך להחיל את קובצי המניפסט RayService. כל מניפסט מגדיר אשכול Ray שמריץ מודל LLM ספציפי.

  1. יוצרים קובץ בשם gemma-2b-it.yaml עם התוכן הבא:

    apiVersion: ray.io/v1
    kind: RayService
    metadata:
      name: gemma-2b-it
    spec:
      serveConfigV2: |
        applications:
        - name: llm_app
          route_prefix: "/"
          import_path: ray.serve.llm:build_openai_app
          args:
            llm_configs:
                - model_loading_config:
                    model_id: gemma-2b-it
                    model_source: google/gemma-2b-it
                  accelerator_type: L4
                  log_engine_metrics: true
                  deployment_config:
                    autoscaling_config:
                        min_replicas: 2
                        max_replicas: 2
                    health_check_period_s: 600
                    health_check_timeout_s: 300
      rayClusterConfig:
        headGroupSpec:
          rayStartParams:
            dashboard-host: "0.0.0.0"
            num-cpus: "0"
          template:
            spec:
              containers:
                - name: ray-head
                  image: rayproject/ray-llm:2.54.0-py311-cu128
                  resources:
                    limits:
                      memory: "8Gi"
                      ephemeral-storage: "32Gi"
                    requests:
                      cpu: "2"
                      memory: "8Gi"
                      ephemeral-storage: "32Gi"
                  ports:
                    - containerPort: 6379
                      name: gcs-server
                    - containerPort: 8265
                      name: dashboard
                    - containerPort: 10001
                      name: client
                    - containerPort: 8000
                      name: serve
                  env:
                    - name: RAY_SERVE_THROUGHPUT_OPTIMIZED
                      value: "1"
                    - name: RAY_SERVE_ENABLE_HA_PROXY
                      value: "1"
                    - name: HUGGING_FACE_HUB_TOKEN
                      valueFrom:
                        secretKeyRef:
                          name: hf-secret
                          key: hf_api_token
        rayVersion: 2.54.0
        workerGroupSpecs:
          - replicas: 2
            minReplicas: 2
            maxReplicas: 2
            groupName: gpu-group
            rayStartParams: {}
            template:
              spec:
                containers:
                  - name: llm
                    image: rayproject/ray-llm:2.54.0-py311-cu128
                    env:
                      - name: RAY_SERVE_THROUGHPUT_OPTIMIZED
                        value: "1"
                      - name: RAY_SERVE_ENABLE_HA_PROXY
                        value: "1"
                      - name: HUGGING_FACE_HUB_TOKEN
                        valueFrom:
                          secretKeyRef:
                            name: hf-secret
                            key: hf_api_token
                    resources:
                      limits:
                        nvidia.com/gpu: "1"
                        ephemeral-storage: "24Gi"
                      requests:
                        cpu: "6"
                        memory: "24Gi"
                        nvidia.com/gpu: "1"
                        ephemeral-storage: "24Gi"
                nodeSelector:
                  cloud.google.com/gke-accelerator: nvidia-l4
    
  2. יוצרים קובץ בשם qwen2.5-3b.yaml עם התוכן הבא:

    apiVersion: ray.io/v1
    kind: RayService
    metadata:
      name: qwen-25-3b
    spec:
      serveConfigV2: |
        applications:
        - name: llm_app
          route_prefix: "/"
          import_path: ray.serve.llm:build_openai_app
          args:
            llm_configs:
                - model_loading_config:
                    model_id: qwen-2.5-3b
                    model_source: Qwen/Qwen2.5-3B
                  accelerator_type: L4
                  log_engine_metrics: true
                  deployment_config:
                    autoscaling_config:
                        min_replicas: 2
                        max_replicas: 2
                    health_check_period_s: 600
                    health_check_timeout_s: 300
      rayClusterConfig:
        headGroupSpec:
          rayStartParams:
            dashboard-host: "0.0.0.0"
            num-cpus: "0"
          template:
            spec:
              containers:
                - name: ray-head
                  image: rayproject/ray-llm:2.54.0-py311-cu128
                  resources:
                    limits:
                      memory: "8Gi"
                      ephemeral-storage: "32Gi"
                    requests:
                      cpu: "2"
                      memory: "8Gi"
                      ephemeral-storage: "32Gi"
                  ports:
                    - containerPort: 6379
                      name: gcs-server
                    - containerPort: 8265
                      name: dashboard
                    - containerPort: 10001
                      name: client
                    - containerPort: 8000
                      name: serve
                  env:
                    - name: RAY_SERVE_THROUGHPUT_OPTIMIZED
                      value: "1"
                    - name: RAY_SERVE_ENABLE_HA_PROXY
                      value: "1"
                    - name: HUGGING_FACE_HUB_TOKEN
                      valueFrom:
                        secretKeyRef:
                          name: hf-secret
                          key: hf_api_token
        rayVersion: 2.54.0
        workerGroupSpecs:
          - replicas: 2
            minReplicas: 2
            maxReplicas: 2
            groupName: gpu-group
            rayStartParams: {}
            template:
              spec:
                containers:
                  - name: llm
                    image: rayproject/ray-llm:2.54.0-py311-cu128
                    env:
                      - name: RAY_SERVE_THROUGHPUT_OPTIMIZED
                        value: "1"
                      - name: RAY_SERVE_ENABLE_HA_PROXY
                        value: "1"
                      - name: HUGGING_FACE_HUB_TOKEN
                        valueFrom:
                          secretKeyRef:
                            name: hf-secret
                            key: hf_api_token
                    resources:
                      limits:
                        nvidia.com/gpu: "1"
                        ephemeral-storage: "24Gi"
                      requests:
                        cpu: "6"
                        memory: "24Gi"
                        nvidia.com/gpu: "1"
                        ephemeral-storage: "24Gi"
                nodeSelector:
                  cloud.google.com/gke-accelerator: nvidia-l4
    
  3. פריסת ��מודלים:

    kubectl apply -f gemma-2b-it.yaml
    kubectl apply -f qwen2.5-3b.yaml
    

הגדרת בדיקות תקינות

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

  1. יוצרים קובץ בשם healthcheck-policy.yaml עם התוכן הבא:

    apiVersion: networking.gke.io/v1
    kind: HealthCheckPolicy
    metadata:
      name: gemma-serve-healthcheck
      namespace: default
    spec:
      default:
        checkIntervalSec: 5
        timeoutSec: 5
        healthyThreshold: 2
        unhealthyThreshold: 2
        config:
          type: HTTP
          httpHealthCheck:
            port: 8000
            requestPath: /-/healthz
      targetRef:
        group: ""
        kind: Service
        name: gemma-2b-it-serve-svc
    ---
    apiVersion: networking.gke.io/v1
    kind: HealthCheckPolicy
    metadata:
      name: qwen-serve-healthcheck
      namespace: default
    spec:
      default:
        checkIntervalSec: 5
        timeoutSec: 5
        healthyThreshold: 2
        unhealthyThreshold: 2
        config:
          type: HTTP
          httpHealthCheck:
            port: 8000
            requestPath: /-/healthz
      targetRef:
        group: ""
        kind: Service
        name: qwen-25-3b-serve-svc
    
  2. החלת מדיניות בדיקת התקינות:

    kubectl apply -f healthcheck-policy.yaml
    

הגדרת ניתוב

כדי להגדיר ניתוב, צריך להחיל את המניפסטים Gateway ו-HTTPRoute. ‫HTTPRoute מכיל כללים שתואמים לכותרת X-Gateway-Model-Name (מאוכלס על ידי נתב מבוסס-גוף) כדי לנתב תנועה לשירות Ray המתאים.

  1. יוצרים קובץ בשם gateway.yaml עם התוכן הבא:

    apiVersion: gateway.networking.k8s.io/v1
    kind: Gateway
    metadata:
      name: ray-multi-model-gateway
      namespace: default
    spec:
      gatewayClassName: gke-l7-rilb
      listeners:
      - allowedRoutes:
          namespaces:
            from: Same
        name: http
        port: 80
        protocol: HTTP
    ---
    apiVersion: gateway.networking.k8s.io/v1
    kind: HTTPRoute
    metadata:
      name: ray-multi-model-route
    spec:
      parentRefs:
      - name: ray-multi-model-gateway
      rules:
      - matches:
        - headers:
          - type: Exact
            name: X-Gateway-Model-Name
            value: gemma-2b-it  # Must match model named in JSON request!
          path:
            type: PathPrefix
            value: /
        backendRefs:
        - name: gemma-2b-it-serve-svc  # Ray service name plus "-serve-svc".
          kind: Service
          port: 8000
    
      - matches:
        - headers:
          - type: Exact
            name: X-Gateway-Model-Name
            value: qwen-2.5-3b  # Matches another extracted model name
          path:
            type: PathPrefix
            value: /
        backendRefs:
        - name: qwen-25-3b-serve-svc  # Target Ray Service.
          kind: Service
          port: 8000
    
  2. החלת השער והמסלול:

    kubectl apply -f gateway.yaml
    

בדיקת הפריסה

אחרי שה-Gateway מוקצה ושני אשכולי Ray מוכנים, אפשר לבדוק את הניתוב על ידי שליחת בקשות עם שמות מודלים שונים בגוף ה-JSON.

  1. משיגים את כתובת ה-IP של השער:

    kubectl get gateways ray-multi-model-gateway
    
  2. מפעילים מעטפת ברשת שאפשר להגיע ממנה לכתובת השער. אפשר להשתמש ב-curl באחד מ-Pods של אשכול Ray:

    POD_NAME=$(kubectl get pods -l ray.io/node-type=head -o jsonpath='{.items[0].metadata.name}')
    kubectl exec -it $POD_NAME -- bash
    
  3. שליחת בקשות על ידי בדיקת הניתוב אל Gemma:

    curl http://GATEWAY_IP_ADDRESS/v1/chat/completions \
        --header 'Content-Type: application/json' \
        --data '{
        "model": "gemma-2b-it",
        "messages": [{"role": "user", "content": "Tell me about GKE."}]
        }'
    

    מחליפים את GATEWAY_IP_ADDRESS בכתובת ה-IP מהשלב הקודם.

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

    {"id":"chatcmpl-594f7cab-f991-4522-9829-acdbb65d9f67","object":"chat.completion","created":1776379509,"model":"gemma-2b-it","choices":[{"index":0,"message":{"role":"assistant","content":"**Google Kubernetes Engine (GKE)** is a fully managed container orchestration service for Kubernetes [...]
    
  4. בדיקת הניתוב ל-Qwen:

    curl http://GATEWAY_IP_ADDRESS/v1/chat/completions \
        --header 'Content-Type: application/json' \
        --data '{
        "model": "qwen-2.5-3b",
        "messages": [{"role": "user", "content": "How does Ray Serve work?"}]
        }'
    

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

    {"id":"chatcmpl-dfe3f3b7-45fc-481c-b53e-2fc09c033cdb","object":"chat.completion","created":1776380249,"model":"qwen-2.5-3b","choices":[{"index":0,"message":{"role":"assistant","content":"Ray Serve facilitates the hosting and deployment of scalable microservices. [...]
    

הנתב שמבוסס על גוף הבקשה מחלץ אוטומטית את הערך של השדה model ומוודא שכל בקשה מגיעה לשירות לקצה העורפי הנכון שהוגדר בקובץ gateway.yaml.

הסרת המשאבים

מחיקת האשכול:

gcloud container clusters delete ${CLUSTER}

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