Gestisci un LLM con Ray Serve multi-cluster e GKE Inference Gateway

Questo documento spiega come gestire le richieste di inferenza in più cluster Ray Serve su Google Kubernetes Engine (GKE) configurando l'API Kubernetes Gateway e GKE Inference Gateway. Questa configurazione ti consente di centralizzare la gestione del traffico per più team, distribuire i workload tra le regioni per una maggiore capacità e implementare il routing basato su modello in base al contenuto del corpo della richiesta.

Vantaggi dell'utilizzo di GKE Inference Gateway e Ray Serve

L'utilizzo di GKE Inference Gateway e Ray Serve offre i seguenti vantaggi:

  • Routing basato sul percorso: configura ogni RayService con un prefisso del percorso, quindi pubblicalo con un gateway che esegue il routing a più Ray Services.
  • Routing basato sul modello: scegli un RayService a cui eseguire il routing in base al corpo della richiesta, ad esempio estraendo il modello richiesto da una richiesta JSON dell'API OpenAI.
  • Governance: richiedi chiavi API per utilizzare il tuo servizio o applica la quota per gli utenti utilizzando Apigee per l'autenticazione e la gestione delle API.
  • Multiregionale: dividi il traffico tra più cluster GKE con RayServices per ottenere una maggiore disponibilità o capacità con i gateway multicluster.
  • Separazione delle responsabilità: utilizza RayService separati, che possono essere amministrati da team separati, seguire implementazioni separate ed essere eseguiti su topologie diverse.
  • Sicurezza: utilizza il gateway come terminatore SSL per proteggere il traffico degli utenti su internet. Per ulteriori informazioni, vedi Sicurezza del gateway.

Per configurare il routing, devi eseguire il deployment di un gateway, di un HTTPRoute e di un RayService. Un servizio Kubernetes per ogni cluster Ray di destinazione viene in genere creato da KubeRay. Ray Serve distribuisce il carico delle richieste nel cluster, senza la necessità di creare un InferencePool o un selettore di endpoint.

Routing basato sul modello per Ray Serve su GKE

Il routing basato sul modello è abilitato da un'estensione di routing basata sul corpo. Il routing basato sul corpo consente di indirizzare il traffico a RayService diversi in base al modello indicato nella richiesta dell'utente, il che consente di avere un unico endpoint in grado di erogare molti modelli ospitati in più cluster Ray. I tuoi utenti hanno un accesso semplificato e gli sviluppatori di app hanno il controllo della configurazione di ogni endpoint Ray.

Per configurare il routing basato sul modello, devi eseguire il deployment dei seguenti componenti chiave:

  • Un'estensione del router basata sul corpo per estrarre i nomi dei modelli dai payload JSON. Il deployment di questa estensione del router viene eseguito utilizzando Helm.
  • Un gateway GKE (bilanciatore del carico delle applicazioni interno regionale di livello 7) per gestire il traffico in entrata.
  • Regole HTTPRoute per indirizzare il traffico al servizio Ray corretto utilizzando le intestazioni compilate dall'estensione del router.
  • Più cluster Ray Serve per gestire il ciclo di vita e la scalabilità automatica dei modelli in silos.

Prima di iniziare

Prima di iniziare, assicurati di aver eseguito le seguenti operazioni:

  • Attiva l'API Google Kubernetes Engine.
  • Attiva l'API Kubernetes Engine
  • Se vuoi utilizzare Google Cloud CLI per questa attività, installala e poi inizializza gcloud CLI. Se hai già installato gcloud CLI, scarica l'ultima versione eseguendo il comando gcloud components update. Le versioni precedenti di gcloud CLI potrebbero non supportare l'esecuzione dei comandi in questo documento.

prepara l'ambiente

Imposta le variabili di ambiente:

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

Sostituisci YOUR_HUGGING_FACE_TOKEN con il token di accesso Hugging Face.

Prepara l'infrastruttura

In questa sezione, configurerai un cluster GKE abilitato per Ray e Gateway con GPU L4.

  1. Crea un cluster con l'operatore Ray e l'API Gateway abilitati:

    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. Crea un pool di nodi GPU per i workload del modello:

    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. Crea una subnet solo proxy per il bilanciatore del carico delle applicazioni interno regionale, richiesta dal routing basato sul corpo:

    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. Esegui il deployment del segreto di Hugging Face:

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

Esegui il deployment del router basato sul corpo per il routing basato sul modello

L'estensione del router basato sul corpo intercetta le richieste, analizza il corpo JSON ed estrae il campo del modello in un'intestazione X-Gateway-Model-Name.

  1. Crea un file denominato helm-values.yaml con i seguenti contenuti:

    bbr:
      plugins:
        - type: "body-field-to-header"
          name: "openai-model-extractor"
          json:
            field_name: "model"
            header_name: "X-Gateway-Model-Name"
    
  2. Installa il router basato sul corpo utilizzando 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
    

Esegui il deployment di RayServices

Per eseguire il deployment dei modelli, devi applicare i manifest RayService. Ogni manifest definisce un cluster Ray che esegue un LLM specifico.

  1. Crea un file denominato gemma-2b-it.yaml con i seguenti contenuti:

    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. Crea un file denominato qwen2.5-3b.yaml con i seguenti contenuti:

    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. Esegui il deployment dei modelli:

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

Configura i controlli di integrità

Per assicurarti che il bilanciatore del carico monitori con precisione l'integrità del worker Ray, devi applicare la risorsa HealthCheckPolicy.

  1. Crea un file denominato healthcheck-policy.yaml con i seguenti contenuti:

    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. Applica il criterio di controllo di integrità:

    kubectl apply -f healthcheck-policy.yaml
    

Configura il routing

Per configurare il routing, devi applicare i manifest Gateway e HTTPRoute. HTTPRoute contiene regole che corrispondono all'intestazione X-Gateway-Model-Name (compilata dal router basato sul corpo) per instradare il traffico al servizio Ray appropriato.

  1. Crea un file denominato gateway.yaml con i seguenti contenuti:

    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. Applica il gateway e la route:

    kubectl apply -f gateway.yaml
    

Testa il deployment

Dopo il provisioning del gateway e la preparazione di entrambi i cluster Ray, puoi testare il routing inviando richieste con nomi di modelli diversi nel corpo JSON.

  1. Ottieni l'indirizzo IP del gateway:

    kubectl get gateways ray-multi-model-gateway
    
  2. Avvia una shell in una rete che può raggiungere l'indirizzo del gateway. Puoi utilizzare curl su uno dei pod del cluster 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. Invia richieste testando il routing a 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."}]
        }'
    

    Sostituisci GATEWAY_IP_ADDRESS con l'indirizzo IP del passaggio precedente.

    L'output è simile al seguente:

    {"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. Testa il routing a 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?"}]
        }'
    

    L'output è simile al seguente:

    {"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. [...]
    

Il router basato sul corpo estrae automaticamente il valore del campo model e garantisce che ogni richiesta raggiunga il servizio di backend corretto configurato nel file gateway.yaml.

Esegui la pulizia

Elimina il cluster:

gcloud container clusters delete ${CLUSTER}

Passaggi successivi