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 solicitudes de inferencia en varios clústeres de Ray Serve en Google Kubernetes Engine (GKE) configurando la API de Gateway de Kubernetes y GKE Inference Gateway. Esta configuración te permite centralizar la administración del tráfico para varios equipos, distribuir las cargas de trabajo en diferentes regiones para aumentar la capacidad y, además, implementar el enrutamiento que tiene en cuenta el modelo según el contenido del cuerpo de la solicitud.

Beneficios de usar GKE Inference Gateway y Ray Serve

El uso de GKE Inference Gateway y Ray Serve ofrece los siguientes beneficios:

  • Enrutamiento de rutas de acceso: Configura cada RayService con un prefijo de ruta de acceso y, luego, proporciónalos con una puerta de enlace que enrute a varios servicios de Ray.
  • Enrutamiento que tiene en cuenta el modelo: Elige un RayService para enrutarlo según el cuerpo de la solicitud, por ejemplo, extrayendo el modelo solicitado de una solicitud JSON de la API de OpenAI.
  • Gobernanza: Exige 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 entre 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 responsabilidades: Usa RayServices separados, que pueden ser administrados por equipos separados, seguir lanzamientos separados y ejecutarse en diferentes topologías.
  • Seguridad: Usa Gateway para que actúe como el terminador de SSL y ayude a proteger el tráfico de los usuarios en Internet. Para obtener más información, consulta Seguridad de la puerta de enlace.

Para configurar el enrutamiento, debes implementar una Gateway, 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 o un selector de extremos.

Enrutamiento compatible con el modelo para Ray Serve en GKE

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

Para configurar el enrutamiento que tiene en cuenta el modelo, implementa los siguientes componentes clave:

  • Es una extensión del router basada en el cuerpo para extraer nombres de modelos de cargas útiles JSON. Esta extensión del router 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 con encabezados completados por la extensión del router.
  • 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 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 que se indican en este documento.

Prepara el entorno

Configure 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 Gateway 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 las cargas de trabajo de tu modelo:

    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 necesaria 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 Secret de Hugging Face:

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

Implementa el router basado en el cuerpo para el enrutamiento que tiene en cuenta el modelo

La extensión del router basada 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 router 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 de los trabajadores 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 Gateway y HTTPRoute. El HTTPRoute contiene reglas que coinciden con el encabezado X-Gateway-Model-Name (completado por el router 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

Después de 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 una 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 lo siguiente:

    {"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 lo siguiente:

    {"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 router 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 la configuración del servicio gateway.yaml.

Realiza una limpieza

Borra el clúster:

gcloud container clusters delete ${CLUSTER}

¿Qué sigue?