במאמר הזה מוסבר איך לנהל בקשות הסקה בכמה אשכולות של 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.
- מידע נוסף על הגדרת כללים של קידומת נתיב זמין במאמרי העזרה של ה-API של Gateway.
- 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 לא יתמכו בהרצת הפקודות שמופיעות במסמך הזה.
- מוודאים ש-Helm מותקן.
- אם עדיין אין לכם חשבון Hugging Face, אתם צריכים ליצור חשבון.
- מוודאים שיש לכם טוקן של Hugging Face.
הכנת הסביבה
מגדירים משתני סביבה:
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.
יוצרים אשכול עם 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יוצרים מאגר צמתים של 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יוצרים רשת משנה (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פורסים את הסוד של Hugging Face:
kubectl create secret generic hf-secret \ --from-literal=hf_api_token=${HUGGING_FACE_TOKEN}
פריסת הנתב שמבוסס על גוף ההודעה לניתוב שמודע למודל
התוסף body-based router מיירט בקשות, מנתח את גוף ה-JSON ומחלץ את שדה המודל לכותרת X-Gateway-Model-Name.
יוצרים קובץ בשם
helm-values.yamlעם התוכן הבא:bbr: plugins: - type: "body-field-to-header" name: "openai-model-extractor" json: field_name: "model" header_name: "X-Gateway-Model-Name"מתקינים את הנתב מבוסס-הגוף באמצעות 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 ספציפי.
יוצרים קובץ בשם
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יוצרים קובץ בשם
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פריסת ��מודלים:
kubectl apply -f gemma-2b-it.yaml kubectl apply -f qwen2.5-3b.yaml
הגדרת בדיקות תקינות
כדי לוודא שמאזן העומסים עוקב בצורה מדויקת אחרי תקינות העובדים של Ray, צריך להחיל את משאב HealthCheckPolicy.
יוצרים קובץ בשם
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החלת מדיניות בדיקת התקינות:
kubectl apply -f healthcheck-policy.yaml
הגדרת ניתוב
כדי להגדיר ניתוב, צריך להחיל את המניפסטים Gateway ו-HTTPRoute.
HTTPRoute מכיל כללים שתואמים לכותרת X-Gateway-Model-Name (מאוכלס על ידי נתב מבוסס-גוף) כדי לנתב תנועה לשירות Ray המתאים.
יוצרים קובץ בשם
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החלת השער והמסלול:
kubectl apply -f gateway.yaml
בדיקת הפריסה
אחרי שה-Gateway מוקצה ושני אשכולי Ray מוכנים, אפשר לבדוק את הניתוב על ידי שליחת בקשות עם שמות מודלים שונים בגוף ה-JSON.
משיגים את כתובת ה-IP של השער:
kubectl get gateways ray-multi-model-gatewayמפעילים מעטפת ברשת שאפשר להגיע ממנה לכתובת השער. אפשר להשתמש ב-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שליחת בקשות על ידי בדיקת הניתוב אל 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 [...]בדיקת הניתוב ל-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}