Stage 6 · Ray Serve 서빙 — 로컬 모델 배포 · 스케일/동시성

Ray Serve — localhost:8000으로 HTTP 요청을 받아 replica들에 분산하고 배칭·그래프로 조합 HTTP 클라이언트 POST / :8000 Ingress @serve.ingress(FastAPI) Replica 1 · 모델 (num_replicas) Replica 2 · 모델 배칭 (batch) · GPU 묶음 deployment graph · serve.run(app, route_prefix="/")
Ray Serve — HTTP 요청이 `localhost:8000`의 **Ingress**(FastAPI)로 들어와 `num_replicas`만큼의 replica(각자 모델 로드)에 분산되고, 벡터 연산은 **리퀘스트 배칭**으로 묶습니다. 여러 deployment를 **deployment graph**로 조합해 하나의 서비스를 만듭니다.

한눈에 보기

Stage 5에서 학습한 모델을 실제로 서비스하는 단계입니다. Ray Serve는 모델을 HTTP 엔드포인트로 배포하는 도구로, 공식적으로 “local first” — 배포 전에 로컬에서 전체 deployment graph를 테스트하라고 문서가 장려합니다. 한 대의 노트북에서 serve.run()이면 충분히 실제 서버가 뜹니다.

이번 포스트에서 다루는 핵심 질문은 세 가지입니다.

  1. @serve.deploymentserve.run으로 모델을 어떻게 HTTP로 배포하나?
  2. deployment graph와 FastAPI 인그레스로 여러 모델을 어떻게 조합하나?
  3. 로컬 스케일/동시성(복제·배칭)은 어떻게 제어하고, 주의점은?

Deployment 기본

@serve.deployment를 클래스에 붙이면 서빙 단위(배포)가 됩니다. bind()로 인자를 고정해 앱을 만들고, serve.run으로 띄웁니다. 기본 HTTP 포트는 8000입니다.

pip install -U "ray[serve]"
import ray
from ray import serve

@serve.deployment
class EchoModel:
    def __init__(self, suffix: str):
        self.suffix = suffix
    def __call__(self, request):
        name = request.query_params["name"]
        return {"result": f"hello {name} {self.suffix}"}

app = EchoModel.bind(suffix="!!")
serve.run(app, route_prefix="/")       # http://localhost:8000/

HTTP로 호출합니다:

curl "http://localhost:8000/?name=ray"
# {"result": "hello ray !!"}
  • @serve.deployment : 서빙할 클래스. __call__이 요청 핸들러.
  • bind() : 생성자 인자를 고정해 앱 조립.
  • serve.run(app, route_prefix=...) : 로컬에서 앱 시작. 데이터 복제/스케일이 가능.

실제 머신러닝 모델 배포

Hugging Face 파이프라인을 감싸는 전형적인 예시입니다.

@serve.deployment
class Sentiment:
    def __init__(self):
        from transformers import pipeline
        self._pipe = pipeline("sentiment-analysis")

    def __call__(self, request):
        text = request.query_params["text"]
        return self._pipe(text)

serve.run(Sentiment.bind(), route_prefix="/sentiment")

구성과 인그레스 — deployment graph

여러 모델 조합 — DeploymentHandle

여러 deployment를 조합하고 싶으면 DeploymentHandle 로 다른 deployment를 함수처럼 호출합니다. 이를 deployment graph라고 합니다.

@serve.deployment
class Upper:
    def __call__(self, text: str):
        return text.upper()

@serve.deployment
class Greeter:
    def __init__(self, upper):
        self._upper = upper
    def __call__(self, request):
        name = request.query_params["name"]
        return self._upper(name)          # DeploymentHandle 호출 (마치 함수)

app = Greeter.bind(Upper.bind())
serve.run(app, route_prefix="/")
  • 그래프로 여러 모델/전처리를 파이프라인처럼 연결합니다.
  • 각 deployment는 독립적으로 스케일(복제)할 수 있습니다.

FastAPI 인그레스 — @serve.ingress

복잡한 REST API가 필요하면 FastAPI 앱을 배포로 감쌉니다.

from fastapi import FastAPI
from ray import serve

app_fastapi = FastAPI()

@serve.deployment
@serve.ingress(app_fastapi)
class MyAPI:
    @app_fastapi.get("/ping")
    def ping(self):
        return {"msg": "pong"}

    @app_fastapi.post("/predict")
    async def predict(self, body: dict):
        return {"pred": body["x"] * 2}

serve.run(MyAPI.bind(), route_prefix="/")
  • @serve.ingress(app) 로 FastAPI 라우트를 deployment에 노출.
  • async 핸들러는 replica 내부에서 이벤트 루프로 처리됩니다.

로컬 스케일/동시성 — 복제·배칭·주의점

num_replicas — 확장

@serve.deployment(num_replicas=2)   # 프로세스 복제
class Sentiment:
    ...

# 또는 오토스케일
@serve.deployment(autoscaling_config={
    "min_replicas": 1, "max_replicas": 4,
    "target_num_ongoing_requests_per_replica": 2.0,
})
class Sentiment:
    ...
  • num_replicas=k : 같은 모델을 k개 프로세스로 복제해 병렬 처리. 로컬에선 물리 코어 이내로.

리퀘스트 배칭 — 벡터 연산 묶기

GPU 추론은 개별 요청 처리보다 배치로 묶는 게 효율적입니다.

import numpy as np

@serve.deployment
class BatchPredictor:
    def __init__(self):
        self._batch = []
    @serve.batch(max_batch_size=16, batch_wait_timeout_s=0.05)
    def predict_batch(self, batches):          # list[list] 묶음
        return [sum(b) for b in batches]
    async def __call__(self, request):
        x = request.query_params["x"]
        return await self.predict_batch([int(x)])

serve.run(BatchPredictor.bind(), route_prefix="/")
  • @serve.batch : 다중 요청을 최대 max_batch_size개/batch_wait_timeout_s초 동안 모아 한 번에 처리.

⚠️ 로컬 주의점

  • 동시성 기본값: 서빙 deployment의 동시 요청 한도(max_concurrent_queries, 과거 max_ongoing_requests) 기본값은 Ray 2.x 사이에서 변경됐습니다. 설치 버전의 기본값을 확인하세요.
  • 복제 수 vs 코어: num_replicas를 코어보다 크게 하면 오버서브스크라이빙으로 오히려 느려집니다.
  • 상태 보관 위치: replica __init__에서 모델을 로드하면 replica별로 메모리가 중복됩니다. 상태를 공유해야 하면 액터(Stage 2) 패턴을 섞습니다.

로컬 활용 전략 — 실전 요약

  1. 학습 → 서빙 전환: Stage 5의 체크포인트를 RayTrainPredictor(또는 직접 로드)로 @serve.deployment에 넣습니다.
  2. 로컬에서 전체 그래프 테스트: serve.run(app)으로 배포 전에 검증 → Serve의 공식 권장 워크플로.
  3. 배칭으로 GPU 효율: 벡터 연산 모델엔 @serve.batch.
  4. FastAPI 인그레스: /predict, /health 등 REST 엔드포인트로 클라이언트와 계약.

Summary

  • Ray Serve@serve.deployment + serve.run으로 모델을 localhost:8000 HTTP 서비스로 배포합니다 — local first.
  • deployment graph(DeploymentHandle)와 FastAPI 인그레스(@serve.ingress)로 여러 모델을 조합합니다.
  • 로컬 스케일/동시성은 num_replicas, @serve.batch 배칭, 동시 요청 한도로 제어하며, 기본값이 버전에 따라 바뀌는 점과 코어 한도를 주의합니다.

다음 학습 (Next Learning)

  • Stage 7 · 실전 통합 — Data→Train→Tune→Serve를 하나의 파이프라인으로 조립하고 concurrency를 종합합니다.
  • Stage 5 · Ray Train 분산 학습 — 서빙할 모델을 분산 학습·체크포인트로 준비.
  • Stage 4 · Ray Tune 튜닝 — 튜닝된 하이퍼파라미터를 서빙 deployment에 반영.