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