Diffuser un LLM avec Ray Serve multicluster et GKE Inference Gateway

Ce document explique comment gérer les requêtes d'inférence sur plusieurs clusters Ray Serve sur Google Kubernetes Engine (GKE) en configurant l'API Kubernetes Gateway et GKE Inference Gateway. Cette configuration vous permet de centraliser la gestion du trafic pour plusieurs équipes, de répartir les charges de travail entre les régions pour augmenter la capacité et d'implémenter un routage basé sur le modèle en fonction du contenu du corps de la requête.

Avantages de l'utilisation de GKE Inference Gateway et de Ray Serve

L'utilisation de GKE Inference Gateway et de Ray Serve offre les avantages suivants :

  • Routage basé sur le chemin d'accès : configurez chaque RayService avec un préfixe de chemin d'accès, puis mettez- les en service avec une passerelle qui achemine le trafic vers plusieurs services Ray.
    • Pour en savoir plus sur la configuration des règles de préfixe de chemin d'accès, consultez la documentation de l'API Gateway.
  • Routage basé sur le modèle : choisissez un RayService vers lequel effectuer le routage en fonction du corps de la requête, par exemple en extrayant le modèle demandé à partir d'une requête JSON de l'API OpenAI.
  • Gouvernance : exigez des clés API pour utiliser votre service ou appliquez un quota aux utilisateurs en utilisant Apigee pour l'authentification et la gestion des API.
  • Multirégion : répartissez le trafic sur plusieurs clusters GKE avec des RayServices pour obtenir une disponibilité ou une capacité plus élevées avec des passerelles multiclusters.
  • Séparation des préoccupations : utilisez des RayServices distincts, qui peuvent être administrés par des équipes distinctes, suivre des déploiements distincts et s'exécuter sur différentes topologies.
  • Sécurité : utilisez Gateway comme terminateur SSL pour sécuriser le trafic utilisateur sur Internet. Pour en savoir plus, consultez la section Sécurité des passerelles.

Pour configurer le routage, vous devez déployer une passerelle, une route HTTP et un RayService. Un service Kubernetes pour chaque cluster Ray cible est généralement créé par KubeRay. Ray Serve répartit la charge des requêtes dans le cluster, sans qu'il soit nécessaire de créer un InferencePool ou un sélecteur de point de terminaison.

Routage basé sur le modèle pour Ray Serve sur GKE

Le routage basé sur le modèle est activé par une extension de routage basée sur le corps. Le routage basé sur le corps vous permet de diriger le trafic vers différents RayServices en fonction uniquement du modèle nommé dans la requête de l'utilisateur. Vous pouvez ainsi disposer d'un point de terminaison unique capable de mettre en service de nombreux modèles hébergés dans plusieurs clusters Ray. Vos utilisateurs bénéficient d'un accès simplifié, et les développeurs de votre application peuvent contrôler la configuration de chaque point de terminaison Ray.

Pour configurer le routage basé sur le modèle, vous devez déployer les composants clés suivants :

  • Une extension de routeur basée sur le corps pour extraire les noms de modèles des charges utiles JSON. Cette extension de routeur est déployée à l'aide de Helm.
  • Une passerelle GKE (équilibreur de charge d'application interne régional de couche 7) pour gérer le trafic entrant.
  • Des règles HTTPRoute pour diriger le trafic vers le service Ray approprié à l'aide d'en-têtes renseignés par l'extension de routeur.
  • Plusieurs clusters Ray Serve pour gérer le cycle de vie et l'autoscaling des modèles isolés.

Avant de commencer

Avant de commencer, effectuez les tâches suivantes :

  • Activez l'API Google Kubernetes Engine.
  • Activer l'API Google Kubernetes Engine
  • Si vous souhaitez utiliser la Google Cloud CLI pour cette tâche, installez et initialisez la gcloud CLI. Si vous avez déjà installé la gcloud CLI, obtenez la dernière version en exécutant la commande gcloud components update. Il est possible que les versions antérieures de la gcloud CLI ne permettent pas d'exécuter les commandes de ce document.

Préparer votre environnement

Configurez des variables d'environnement :

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

Remplacez YOUR_HUGGING_FACE_TOKEN par votre jeton d'accès Hugging Face.

Préparer votre infrastructure

Dans cette section, vous allez configurer un cluster GKE compatible avec Ray et Gateway avec des GPU L4.

  1. Créez un cluster avec l'opérateur Ray et l'API Gateway activés :

    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. Créez un pool de nœuds GPU pour vos charges de travail de modèle :

    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. Créez un sous-réseau proxy réservé pour l'équilibreur de charge d'application interne régional, qui est requis par le routage basé sur le corps :

    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. Déployez votre secret Hugging Face :

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

Déployer le routeur basé sur le corps pour le routage basé sur le modèle

L'extension de routeur basée sur le corps intercepte les requêtes, analyse le corps JSON et extrait le champ du modèle dans un en-tête X-Gateway-Model-Name.

  1. Créez un fichier nommé helm-values.yaml avec le contenu suivant :

    bbr:
      plugins:
        - type: "body-field-to-header"
          name: "openai-model-extractor"
          json:
            field_name: "model"
            header_name: "X-Gateway-Model-Name"
    
  2. Installez le routeur basé sur le corps à l'aide de 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
    

Déployer des RayServices

Pour déployer vos modèles, vous devez appliquer les fichiers manifestes RayService. Chaque fichier manifeste définit un cluster Ray qui exécute un LLM spécifique.

  1. Créez un fichier nommé gemma-2b-it.yaml avec le contenu suivant :

    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. Créez un fichier nommé qwen2.5-3b.yaml avec le contenu suivant :

    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. Déployez les modèles :

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

Configurer les vérifications d'état

Pour vous assurer que l'équilibreur de charge surveille avec précision l'état des workers Ray, vous devez appliquer la ressource HealthCheckPolicy.

  1. Créez un fichier nommé healthcheck-policy.yaml avec le contenu suivant :

    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. Appliquez la règle de vérification de l'état :

    kubectl apply -f healthcheck-policy.yaml
    

Configurer le routage

Pour configurer le routage, vous devez appliquer les fichiers manifestes Gateway et HTTPRoute. HTTPRoute contient des règles qui correspondent à l'en-tête X-Gateway-Model-Name (renseigné par le routeur basé sur le corps) pour acheminer le trafic vers le service Ray approprié.

  1. Créez un fichier nommé gateway.yaml avec le contenu suivant :

    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. Appliquez la passerelle et la route :

    kubectl apply -f gateway.yaml
    

Tester le déploiement

Une fois la passerelle provisionnée et les deux clusters Ray prêts, vous pouvez tester le routage en envoyant des requêtes avec différents noms de modèles dans le corps JSON.

  1. Obtenez l'adresse IP de la passerelle :

    kubectl get gateways ray-multi-model-gateway
    
  2. Démarrez un shell dans un réseau pouvant atteindre l'adresse de la passerelle. Vous pouvez utiliser curl sur l'un des pods du 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. Envoyez des requêtes en testant le routage vers 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."}]
        }'
    

    Remplacez GATEWAY_IP_ADDRESS par l'adresse IP de l'étape précédente.

    Le résultat ressemble à ce qui suit :

    {"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. Testez le routage vers 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?"}]
        }'
    

    Le résultat ressemble à ce qui suit :

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

Le routeur basé sur le corps extrait automatiquement la valeur du champ model et s'assure que chaque requête atteint le service de backend approprié configuré dans le fichier gateway.yaml.

Libérer de l'espace

Supprimez le cluster à l'aide de la commande suivante :

gcloud container clusters delete ${CLUSTER}

Étape suivante