Ray Serve 핵심 개념

Ray Serve 핵심 개념

Ray Serve를 이해하려면 네 가지 추상화를 알아두면 편해요: Deployment(배포), Application(애플리케이션), DeploymentHandle, 그리고 Ingress(인그레스) 배포. 이 개념들이 어떻게 연결되는지 먼저 잡아 두면, LLM을 포함한 어떤 모델이든 Ray Serve로 띄우는 전체 흐름이 머릿속에 그려져요. 여기선 각 개념의 역할과 사용 예시를 담았어요.

출처: Ray Serve Key Concepts — Ray Docs

Deployment

Deployment는 Ray Serve에서 가장 중심이 되는 개념이에요. 들어오는 요청을 처리할 비즈니스 로직이나 ML 모델을 담고 있으며, Ray 클러스터 전체에 걸쳐 확장(스케일)할 수 있어요. 런타임에서 하나의 deployment는 여러 **replica(레플리카)**로 구성되는데, 각 레플리카는 클래스나 함수의 개별 복사본으로 별도의 Ray Actor(프로세스)에서 실행돼요. 레플리카 수는 요청 부하에 맞춰 늘리거나 줄일 수 있고, 자동 스케일링(autoscale)도 가능해요.

deployment를 정의하려면 Python 클래스(간단한 경우엔 함수)에 @serve.deployment 데코레이터를 붙여요. 그다음 deployment를 생성자 인자와 함께 bind해서 application을 정의하고, 마지막으로 serve.run(또는 동일한 serve run CLI)으로 배포해요.

from ray import serve
from ray.serve.handle import DeploymentHandle


@serve.deployment
class MyFirstDeployment:
    # Take the message to return as an argument to the constructor.
    def __init__(self, msg):
        self.msg = msg

    def __call__(self):
        return self.msg


my_first_deployment = MyFirstDeployment.bind("Hello world!")
handle: DeploymentHandle = serve.run(my_first_deployment)
assert handle.remote().result() == "Hello world!"

Application

Application은 Ray Serve 클러스터에서 업그레이드의 단위예요. 하나 이상의 deployment로 구성되며, 그중 하나가 들어오는 모든 트래픽을 처리하는 'ingress' 배포로 지정돼요. application은 지정된 route_prefix로 HTTP로 호출하거나, Python에서는 DeploymentHandle로 호출할 수 있어요.

DeploymentHandle (배포 간 구성)

Ray Serve는 여러 독립된 deployment가 서로를 호출할 수 있게 해서 유연한 모델 구성과 스케일링을 지원해요. deployment를 bind할 때 다른 bound deployment에 대한 참조를 포함할 수 있고, 런타임에서 이 인자들은 각각 DeploymentHandle로 변환돼요. 이 핸들로 Python 네이티브 API로 deployment를 호출할 수 있어요. 아래는 Ingress 배포가 두 개의 하위 모델을 호출하는 기본 예시예요. 더 자세한 내용은 모델 구성 가이드를 참고하세요.

from ray import serve
from ray.serve.handle import DeploymentHandle


@serve.deployment
class Hello:
    def __call__(self) -> str:
        return "Hello"


@serve.deployment
class World:
    def __call__(self) -> str:
        return " world!"


@serve.deployment
class Ingress:
    def __init__(self, hello_handle: DeploymentHandle, world_handle: DeploymentHandle):
        self._hello_handle = hello_handle
        self._world_handle = world_handle

    async def __call__(self) -> str:
        hello_response = self._hello_handle.remote()
        world_response = self._world_handle.remote()
        return (await hello_response) + (await world_response)


hello = Hello.bind()
world = World.bind()

# The deployments passed to the Ingress constructor are replaced with handles.
app = Ingress.bind(hello, world)

# Deploys Hello, World, and Ingress.
handle: DeploymentHandle = serve.run(app)

# `DeploymentHandle`s can also be used to call the ingress deployment of an application.
assert handle.remote().result() == "Hello world!"

Ingress deployment (HTTP 처리)

Serve application은 여러 deployment를 조합해 모델 구성이나 복잡한 비즈니스 로직을 만들 수 있어요. 그중 하나는 항상 serve.run에 전달되는 '최상위(top-level)' 배포로, application으로 들어오는 모든 트래픽의 진입점 역할을 한다고 해서 ingress 배포라고 불러요. 보통 이 배포가 DeploymentHandle API로 다른 배포에 라우팅하거나 호출하고, 그 결과를 조합해 사용자에게 돌려줘요.

ingress 배포는 application의 HTTP 처리 로직을 정의해요. 기본적으로 클래스의 __call__ 메서드가 호출되면서 Starlette request 객체를 받아요. 응답은 JSON으로 직렬화되지만, 다른 Starlette response 객체를 직접 반환할 수도 있어요.

import requests
from starlette.requests import Request

from ray import serve


@serve.deployment
class MostBasicIngress:
    async def __call__(self, request: Request) -> str:
        name = (await request.json())["name"]
        return f"Hello {name}!"


app = MostBasicIngress.bind()
serve.run(app)
assert (
    requests.get("http://127.0.0.1:8000/", json={"name": "Corey"}).text
    == "Hello Corey!"
)

더 표현력 있는 HTTP 처리를 원한다면 Ray Serve에 내장된 FastAPI 통합을 쓸 수 있어요. FastAPI의 풍부한 기능으로 더 복잡한 API를 정의할 수 있어요.

import requests
from fastapi import FastAPI
from fastapi.responses import PlainTextResponse

from ray import serve

fastapi_app = FastAPI()


@serve.deployment
@serve.ingress(fastapi_app)
class FastAPIIngress:
    @fastapi_app.get("/{name}")
    async def say_hi(self, name: str) -> str:
        return PlainTextResponse(f"Hello {name}!")


app = FastAPIIngress.bind()
serve.run(app)
assert requests.get("http://127.0.0.1:8000/Corey").text == "Hello Corey!"

더 알아보기