用 Ray Serve 深度学习容器接管 TorchServe 之后的 GPU 推理栈

2026-09-09 28 预计阅读时间: 1 分钟
来源: aws.amazon.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:11 分钟

TorchServe 已不再维护,继续使用它意味着团队需要自行承担推理服务、GPU 运行时、依赖版本和故障排查的全部责任。对于已经运行在 Amazon EKS 上的团队,AWS Ray Serve Deep Learning Container(DLC)提供了一条更稳妥的迁移路径:把框架、GPU 驱动相关运行时和服务层预先组装在受支持、经过测试的容器中,再用 Ray Serve 部署视觉语言模型。

本文以单个 GPU 节点为假设,展示一套可以改造成实际项目的最小部署方式。示例中的 DLC 镜像地址和模型名称需要替换为你所在区域、账号权限和目标模型对应的值。

为什么不只是“换一个启动命令”

TorchServe 停止维护后,问题不只在于启动脚本过时。GPU 推理栈通常同时包含以下几层:

  • CUDA、GPU 驱动和 PyTorch 的兼容性;
  • Transformers、图像处理库以及模型权重的加载方式;
  • HTTP 服务、并发控制、批处理和健康检查;
  • Kubernetes 的 GPU 调度、滚动升级和故障恢复。

这些组件任意一层出现版本漂移,都可能导致“镜像能启动,但模型在 GPU 上运行失败”。Ray Serve DLC 的价值在于提供一个支持和预测试的基础容器,把常见的框架与服务运行时组合起来。团队仍然要负责模型代码、输入协议、资源配置和可观测性,但不必从零维护整套底层推理环境。

这并不代表 DLC 会自动解决所有问题。镜像版本、GPU 节点驱动、模型显存需求和区域可用性仍然需要在目标 EKS 集群中验证。

用一个轻量 Ray Serve 部署封装视觉语言模型

下面的示例使用 Transformers 的 image-to-text pipeline,接收图片二进制内容,并通过 X-Prompt 请求头传入提示词。它适合作为验证 DLC、GPU 调度和 HTTP 链路的最小样例;生产环境中通常需要根据具体视觉语言模型改写 processor 和生成参数。

把下面内容保存为 app.py。运行前,将 MODEL_ID 改成实际使用的模型,并确认基础镜像已经包含 ray[serve]transformerstorch 和 Pillow。

import io
import os

from fastapi import Request
from PIL import Image
from ray import serve
from transformers import pipeline

MODEL_ID = os.getenv("MODEL_ID", "Salesforce/blip-image-captioning-base")


@serve.deployment(
    num_replicas=1,
    ray_actor_options={"num_gpus": 1},
)
class VisionLanguageModel:
    def __init__(self):
        self.generator = pipeline(
            task="image-to-text",
            model=MODEL_ID,
            device=0,
        )

    async def __call__(self, request: Request):
        image_bytes = await request.body()
        if not image_bytes:
            return {"error": "request body must contain image bytes"}

        prompt = request.headers.get("X-Prompt", "")
        image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
        result = self.generator(
            image,
            prompt=prompt or None,
            max_new_tokens=128,
        )
        return {"model": MODEL_ID, "result": result}


app = VisionLanguageModel.bind()

num_gpus: 1 是关键约束:Ray 会为这个 deployment 申请一张 GPU。对于单 GPU 节点,这能避免多个副本意外争抢同一张卡。模型如果需要更大的显存,应改用更大规格的节点,或者调整模型量化、并行和副本策略,而不是简单增加 HTTP worker 数量。

构建镜像并部署到 EKS

DLC 通常作为基础镜像使用。由于不同区域、版本和访问权限对应的镜像地址可能不同,下面用环境变量表示镜像地址,不硬编码一个未经验证的 URI。

创建 Dockerfile

ARG BASE_IMAGE
FROM ${BASE_IMAGE}

WORKDIR /app
COPY app.py /app/app.py

# 如果基础 DLC 已经包含这些依赖,这一行可以删除。
# 不要在生产构建中无条件升级 torch 或 CUDA 相关包。
RUN python -m pip install --no-cache-dir fastapi pillow transformers

ENV MODEL_ID=Salesforce/blip-image-captioning-base
CMD ["serve", "run", "app:app", "--host", "0.0.0.0", "--port", "8000"]

构建时,把 BASE_IMAGE 替换成 AWS 提供、且与你的 CUDA/PyTorch 需求匹配的 Ray Serve DLC:

export BASE_IMAGE="<AWS_RAY_SERVE_DLC_IMAGE>"
export ECR_REPO="<ACCOUNT_ID>.dkr.ecr.<REGION>.amazonaws.com/ray-vlm"

aws ecr get-login-password --region "<REGION>" \
  | docker login --username AWS --password-stdin "<ACCOUNT_ID>.dkr.ecr.<REGION>.amazonaws.com"

docker build \
  --build-arg BASE_IMAGE="${BASE_IMAGE}" \
  -t "${ECR_REPO}:v1" .

docker push "${ECR_REPO}:v1"

如果 DLC 已经带有完整的 Transformers 和图像处理依赖,建议移除 Dockerfile 中的 pip install,减少构建时的依赖漂移。尤其不要在一个已经验证过 CUDA 和 PyTorch 的镜像中随意升级 torch

下面是一个最小 Kubernetes Deployment 和 Service。它假设集群已经安装 NVIDIA device plugin,并且 GPU 节点带有 nvidia.com/gpu 资源。将 ${IMAGE} 替换为刚刚推送的镜像后再应用:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ray-vlm
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ray-vlm
  template:
    metadata:
      labels:
        app: ray-vlm
    spec:
      nodeSelector:
        eks.amazonaws.com/compute-type: ec2
      containers:
        - name: model
          image: ${IMAGE}
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 8000
          env:
            - name: MODEL_ID
              value: "${MODEL_ID}"
          resources:
            requests:
              cpu: "2"
              memory: "8Gi"
              nvidia.com/gpu: "1"
            limits:
              cpu: "4"
              memory: "16Gi"
              nvidia.com/gpu: "1"
          readinessProbe:
            httpGet:
              path: /
              port: http
            initialDelaySeconds: 30
            periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: ray-vlm
spec:
  selector:
    app: ray-vlm
  ports:
    - name: http
      port: 8000
      targetPort: http
  type: ClusterIP

实际部署:

export IMAGE="<ACCOUNT_ID>.dkr.ecr.<REGION>.amazonaws.com/ray-vlm:v1"
export MODEL_ID="Salesforce/blip-image-captioning-base"

envsubst < ray-vlm.yaml | kubectl apply -f -
kubectl rollout status deployment/ray-vlm
kubectl get pod -l app=ray-vlm -o wide
kubectl logs deployment/ray-vlm

这里的 readinessProbe 只是最小示例。模型加载期间容器可能需要较长时间,生产部署应增加合适的启动保护,或者提供一个明确的 /healthz 端点,避免把“进程已启动”误判为“模型已经可以接收请求”。

验证 GPU 和 HTTP 推理链路

先将 Service 转发到本地,再发送一张图片。请求体是图片二进制,提示词通过请求头传递:

kubectl port-forward service/ray-vlm 8000:8000

curl -X POST http://127.0.0.1:8000 \
  -H "Content-Type: image/jpeg" \
  -H "X-Prompt: describe this image" \
  --data-binary @sample.jpg

出现模型结果后,再检查 Pod 是否真的获得 GPU,而不是退回 CPU:

kubectl describe pod -l app=ray-vlm | grep -A5 -i "nvidia.com/gpu"
kubectl exec deploy/ray-vlm -- nvidia-smi

如果 Pod 一直处于 Pending,优先检查节点是否有可分配 GPU、NVIDIA device plugin 是否正常,以及 Deployment 的资源请求是否与节点标签匹配。如果容器启动后在模型加载阶段崩溃,则应查看显存、模型权重下载权限和 CUDA/PyTorch 兼容性,而不是先调大 HTTP 超时时间。

从单 GPU 验证走向生产的检查清单

单 GPU 部署适合验证镜像、模型和请求协议,但不能直接代表生产吞吐。继续推进时可以按下面的顺序收敛风险:

  1. 固定版本:锁定 DLC 标签、模型版本和 Python 依赖,不使用不可追踪的 latest
  2. 验证显存:记录模型加载峰值和推理峰值,给 Kubernetes 留出余量。
  3. 控制并发:先测单请求延迟,再逐步增加并发;不要因为 Ray Serve 能扩展副本,就忽略 GPU 显存限制。
  4. 完善探针:区分进程存活、模型加载完成和依赖服务可用这几种状态。
  5. 处理模型缓存:规划权重下载权限、缓存目录和重启后的恢复时间。
  6. 加入观测:至少记录请求延迟、错误率、模型加载时间、GPU 利用率和显存使用量。
  7. 再决定扩容策略:如果单模型无法在一张卡上稳定运行,应评估量化、模型并行或更大 GPU 节点,而不是盲目增加副本。

Ray Serve DLC 的核心价值不是替团队隐藏所有复杂性,而是把最容易发生兼容性问题的基础推理栈放进一个受支持的起点。对于 TorchServe 之后的迁移,先在单 GPU EKS 节点上跑通“模型加载—GPU 调度—HTTP 请求—滚动更新”这一条链路,再逐步加入批处理、监控和高可用,通常比一次性重写完整平台更容易控制风险。


相关推荐