使用多集群 Ray Serve 和 GKE Inference Gateway 提供 LLM

本文档介绍了如何通过配置 Kubernetes Gateway API 和 GKE Inference Gateway,管理 Google Kubernetes Engine (GKE) 上多个 Ray Serve 集群的推理请求。通过此配置,您可以集中管理多个团队的流量,在不同区域分配工作负载以提高容量,并根据请求正文内容实现模型感知路由。

使用 GKE Inference Gateway 和 Ray Serve 的优势

使用 GKE Inference Gateway 和 Ray Serve 具有以下优势:

如需配置路由,您需要部署网关、HTTPRoute 和 RayService。通常,KubeRay 会为每个目标 Ray 集群创建一个 Kubernetes 服务。 Ray Serve 会在集群内分散请求负载,而无需创建 InferencePool 或端点选择器。

在 GKE 上为 Ray Serve 实现模型感知路由

模型感知路由由基于正文的路由扩展程序启用。基于正文的路由可让您仅根据用户请求中命名的模型将流量定向到不同的 RayService,从而让您拥有一个可以为多个 Ray 集群中托管的多个模型提供服务的端点。您的用户可以简化访问,而您的应用开发者可以控制每个 Ray 端点的配置。

如需配置模型感知路由,请部署以下关键组件:

  • 一个基于正文的路由器扩展程序,用于从 JSON 载荷中提取模型名称。 此路由器扩展程序使用 Helm 部署。
  • 一个 GKE 网关(L7 区域级内部应用负载平衡器),用于处理传入流量。
  • HTTPRoute 规则,用于使用路由器扩展程序填充的标头将流量定向到正确的 Ray 服务。
  • 多个 Ray Serve 集群,用于管理孤立模型的生命周期和自动扩缩。

准备工作

在开始之前,请确保您已执行以下任务:

  • 启用 Google Kubernetes Engine API。
  • 启用 Google Kubernetes Engine API
  • 如果您要使用 Google Cloud CLI 执行此任务,请安装初始化 gcloud CLI。 如果您之前安装了 gcloud CLI,请通过运行 gcloud components update 命令来获取最新版本。较早版本的 gcloud CLI 可能不支持运行本文档中的命令。

准备环境

设置环境变量:

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 访问令牌。

准备基础架构

在本部分中,您将设置一个启用 Ray 和网关的 GKE 集群,其中包含 L4 GPU。

  1. 创建一个启用了 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
    
  2. 为模型工作负载创建 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
    
  3. 为区域级内部应用负载平衡器创建一个代理专用子网,这是基于正文的路由所必需的:

    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. 部署 Hugging Face Secret:

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

部署基于正文的路由器以实现模型感知路由

基于正文的路由器扩展程序会拦截请求、解析 JSON 正文,并将模型字段提取到 X-Gateway-Model-Name 标头中。

  1. 创建一个名为 helm-values.yaml 的文件,其中包含以下内容:

    bbr:
      plugins:
        - type: "body-field-to-header"
          name: "openai-model-extractor"
          json:
            field_name: "model"
            header_name: "X-Gateway-Model-Name"
    
  2. 使用 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
    

部署 RayService

如需部署模型,您必须应用 RayService 清单。每个清单定义一个运行特定 LLM 的 Ray 集群。

  1. 创建一个名为 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
    
  2. 创建一个名为 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
    
  3. 部署模型:

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

配置健康检查

为帮助确保负载均衡器准确监控 Ray 工作器健康状况,您必须应用 HealthCheckPolicy 资源。

  1. 创建一个名为 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
    
  2. 应用健康检查政策:

    kubectl apply -f healthcheck-policy.yaml
    

配置路由

如需配置路由,您必须应用 GatewayHTTPRoute 清单。 HTTPRoute 包含与 X-Gateway-Model-Name 标头(由基于正文的路由器填充)匹配的规则,以将流量路由到相应的 Ray 服务。

  1. 创建一个名为 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
    
  2. 应用网关和路由:

    kubectl apply -f gateway.yaml
    

测试部署

预配网关且两个 Ray 集群都准备就绪后,您可以通过在 JSON 正文中发送具有不同模型名称的请求来测试路由。

  1. 获取网关 IP 地址:

    kubectl get gateways ray-multi-model-gateway
    
  2. 在可以访问网关地址的网络中启动 shell。 您可以在其中一个 Ray 集群 Pod 上使用 curl:

    POD_NAME=$(kubectl get pods -l ray.io/node-type=head -o jsonpath='{.items[0].metadata.name}')
    kubectl exec -it $POD_NAME -- bash
    
  3. 通过测试到 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 [...]
    
  4. 测试到 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}

后续步骤