Entrega un LLM con Ray Serve de varios clústeres y la puerta de enlace de inferencia de GKE

En este documento, se explica cómo administrar las solicitudes de inferencia en varios clústeres de Ray Serve en Google Kubernetes Engine (GKE) mediante la configuración de la API de Kubernetes Gateway y la puerta de enlace de inferencia de GKE. Esta configuración te permite centralizar la administración del tráfico para varios equipos, distribuir cargas de trabajo en diferentes regiones para obtener una mayor capacidad y, también, implementar el enrutamiento compatible con el modelo según el contenido del cuerpo de la solicitud.

Beneficios de usar la puerta de enlace de inferencia de GKE y Ray Serve

El uso de la puerta de enlace de inferencia de GKE y Ray Serve ofrece los siguientes beneficios:

  • Enrutamiento de rutas: Configura cada RayService con un prefijo de ruta y, luego, entrégalos con una puerta de enlace que enrute a varios servicios de Ray.
  • Enrutamiento compatible con el modelo: Elige un RayService al que enrutar según el cuerpo de la solicitud, por ejemplo, extrayendo el modelo solicitado de una solicitud JSON de la API de OpenAI.
  • Administración: Requiere claves de API para usar tu servicio o aplica cuotas para los usuarios con Apigee para la autenticación y la administración de APIs.
  • Multirregión: Divide el tráfico en varios clústeres de GKE con RayServices para lograr una mayor disponibilidad o capacidad con puertas de enlace de varios clústeres.
  • Separación de intereses: Usa RayServices independientes, que pueden ser administrados por equipos separados, seguir implementaciones separadas y ejecutarse en diferentes topologías.
  • Seguridad: Usa la puerta de enlace para que actúe como el terminador de SSL y te ayude a proteger el tráfico de los usuarios a través de Internet. Para obtener más información, consulta Seguridad de la puerta de enlace.

Para configurar el enrutamiento, debes implementar una puerta de enlace, una HTTPRoute y un RayService. KubeRay suele crear un servicio de Kubernetes para cada clúster de Ray de destino. Ray Serve distribuye la carga de solicitudes en el clúster, sin necesidad de crear un InferencePool ni un selector de extremos.

Enrutamiento compatible con el modelo para Ray Serve en GKE

El enrutamiento compatible con el modelo se habilita mediante una extensión de enrutamiento basada en el cuerpo. El enrutamiento basado en el cuerpo te permite dirigir el tráfico a diferentes RayServices en función del modelo que se menciona en la solicitud del usuario, lo que te permite tener un solo extremo que puede entregar muchos modelos alojados en varios clústeres de Ray. Tus usuarios tienen acceso simplificado y los desarrolladores de tu app tienen control sobre la configuración de cada extremo de Ray.

Para configurar el enrutamiento compatible con el modelo, debes implementar los siguientes componentes clave:

  • Una extensión de enrutador basada en el cuerpo para extraer nombres de modelos de cargas útiles JSON. Esta extensión de enrutador se implementa con Helm.
  • Una puerta de enlace de GKE (balanceador de cargas de aplicaciones interno regional de L7) para controlar el tráfico entrante.
  • Reglas de HTTPRoute para dirigir el tráfico al servicio de Ray correcto mediante encabezados propagados por la extensión del enrutador.
  • Varios clústeres de Ray Serve para administrar el ciclo de vida y el ajuste de escala automático de los modelos aislados.

Antes de comenzar

Antes de comenzar, asegúrate de haber realizado las siguientes tareas:

  • Habilita la API de Google Kubernetes Engine.
  • Habilitar la API de Google Kubernetes Engine
  • Si deseas usar Google Cloud CLI para esta tarea, instala y, luego, inicializa the gcloud CLI. Si ya instalaste gcloud CLI, ejecuta el comando gcloud components update para obtener la versión más reciente. Es posible que las versiones anteriores de gcloud CLI no admitan la ejecución de los comandos de este documento.

Prepara el entorno

Configura las variables de entorno:

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

Reemplaza YOUR_HUGGING_FACE_TOKEN por tu token de acceso de Hugging Face.

Prepara tu infraestructura

En esta sección, configurarás un clúster de GKE habilitado para Ray y para la puerta de enlace con GPUs L4.

  1. Crea un clúster con el operador de Ray y la API de Gateway habilitados:

    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 grupo de nodos de GPU para tus cargas de trabajo de modelos:

    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 subred de solo proxy para el balanceador de cargas de aplicaciones interno regional, que es necesario para el enrutamiento basado en el cuerpo:

    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. Implementa tu secreto de Hugging Face:

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

Implementa el enrutador basado en el cuerpo para el enrutamiento compatible con el modelo

La extensión del enrutador basado en el cuerpo intercepta las solicitudes, analiza el cuerpo JSON y extrae el campo del modelo en un encabezado X-Gateway-Model-Name.

  1. Crea un archivo llamado helm-values.yaml con el siguiente contenido:

    bbr:
      plugins:
        - type: "body-field-to-header"
          name: "openai-model-extractor"
          json:
            field_name: "model"
            header_name: "X-Gateway-Model-Name"
    
  2. Instala el enrutador basado en el cuerpo con 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
    

Implementa RayServices

Para implementar tus modelos, debes aplicar los manifiestos de RayService. Cada manifiesto define un clúster de Ray que ejecuta un LLM específico.

  1. Crea un archivo llamado gemma-2b-it.yaml con el siguiente contenido:

    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 archivo llamado qwen2.5-3b.yaml con el siguiente contenido:

    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. Implementa los modelos:

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

Configura las verificaciones de estado

Para garantizar que el balanceador de cargas supervise con precisión el estado del trabajador de Ray, debes aplicar el recurso HealthCheckPolicy.

  1. Crea un archivo llamado healthcheck-policy.yaml con el siguiente contenido:

    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. Aplica la política de verificación de estado:

    kubectl apply -f healthcheck-policy.yaml
    

Configura el enrutamiento

Para configurar el enrutamiento, debes aplicar los manifiestos de Gateway y HTTPRoute. HTTPRoute contiene reglas que coinciden con el encabezado X-Gateway-Model-Name (propagado por el enrutador basado en el cuerpo) para enrutar el tráfico al servicio de Ray adecuado.

  1. Crea un archivo llamado gateway.yaml con el siguiente contenido:

    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. Aplica la puerta de enlace y la ruta:

    kubectl apply -f gateway.yaml
    

Prueba la implementación

Una vez que se aprovisione la puerta de enlace y ambos clústeres de Ray estén listos, puedes probar el enrutamiento enviando solicitudes con diferentes nombres de modelos en el cuerpo JSON.

  1. Obtén la dirección IP de la puerta de enlace:

    kubectl get gateways ray-multi-model-gateway
    
  2. Inicia un shell en una red que pueda acceder a la dirección de la puerta de enlace. Puedes usar curl en uno de los pods del clúster de 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. Envía solicitudes probando el enrutamiento 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."}]
        }'
    

    Reemplaza GATEWAY_IP_ADDRESS por la dirección IP del paso anterior.

    El resultado es similar a este:

    {"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. Prueba el enrutamiento 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?"}]
        }'
    

    El resultado es similar a este:

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

El enrutador basado en el cuerpo extrae automáticamente el valor del campo model y garantiza que cada solicitud llegue al servicio de backend correcto configurado en el archivo gateway.yaml.

Limpia

Borra el clúster:

gcloud container clusters delete ${CLUSTER}

¿Qué sigue?