Files
ray-project--ray/python/ray/serve/tests/test_model_composition.py
T
2026-07-13 13:17:40 +08:00

330 lines
7.6 KiB
Python

import array
import asyncio
import os
import sys
from typing import Dict, Union
import httpx
import pytest
import starlette.requests
from ray import serve
from ray.serve._private.test_utils import get_application_url
from ray.serve.handle import DeploymentHandle
NESTED_HANDLE_KEY = "nested_handle"
@serve.deployment
class ClassHello:
def __init__(self):
pass
def hello(self):
return "hello"
@serve.deployment
class Model:
def __init__(self, weight: int, ratio: float = None):
self.weight = weight
self.ratio = ratio or 1
def forward(self, input: int):
return self.ratio * self.weight * input
def __call__(self, request):
input_data = request
return self.ratio * self.weight * input_data
@serve.deployment
class Combine:
def __init__(
self,
m1: DeploymentHandle,
m2: Union[DeploymentHandle, Dict[str, DeploymentHandle]],
):
self.m1 = m1
if isinstance(m2, dict):
self.m2 = m2.get(NESTED_HANDLE_KEY)
else:
self.m2 = m2
async def predict(self, req):
r1_ref = self.m1.forward.remote(req)
r2_ref = self.m2.forward.remote(req)
return sum(await asyncio.gather(r1_ref, r2_ref))
async def __call__(self, request: starlette.requests.Request):
req = await request.json()
return await self.predict(req)
@serve.deployment
class Counter:
def __init__(self, val):
self.val = val
def get(self):
return self.val
def inc(self, inc):
self.val += inc
@serve.deployment
def fn_hello():
return "hello"
@serve.deployment
def combine(m1_output, m2_output, kwargs_output=0):
return m1_output + m2_output + kwargs_output
def class_factory():
class MyInlineClass:
def __init__(self, val):
self.val = val
def get(self):
return self.val
def __call__(self):
return self.get()
return MyInlineClass
@serve.deployment
class Adder:
def __init__(self, increment: int):
self.increment = increment
def forward(self, inp: int) -> int:
print(f"Adder got {inp}")
return inp + self.increment
__call__ = forward
@serve.deployment
class NoargDriver:
def __init__(self, h: DeploymentHandle):
self._h = h
async def __call__(self):
return await self._h.remote()
def test_single_func_no_input(serve_instance):
dag = fn_hello.bind()
serve_dag = NoargDriver.bind(dag)
handle = serve.run(serve_dag)
url = get_application_url()
assert handle.remote().result() == "hello"
assert httpx.get(url).text == "hello"
async def json_resolver(request: starlette.requests.Request):
return await request.json()
def test_multi_instantiation_class_deployment_in_init_args(serve_instance):
m1 = Model.bind(2)
m2 = Model.bind(3)
serve_dag = Combine.bind(m1, m2=m2)
handle = serve.run(serve_dag)
url = get_application_url()
assert handle.predict.remote(1).result() == 5
assert httpx.post(url, json=1).json() == 5
def test_shared_deployment_handle(serve_instance):
m = Model.bind(2)
serve_dag = Combine.bind(m, m2=m)
handle = serve.run(serve_dag)
url = get_application_url()
assert handle.predict.remote(1).result() == 4
assert httpx.post(url, json=1).json() == 4
def test_multi_instantiation_class_nested_deployment_arg_dag(serve_instance):
m1 = Model.bind(2)
m2 = Model.bind(3)
serve_dag = Combine.bind(m1, m2={NESTED_HANDLE_KEY: m2})
handle = serve.run(serve_dag)
url = get_application_url()
assert handle.predict.remote(1).result() == 5
assert httpx.post(url, json=1).json() == 5
def test_class_factory(serve_instance):
serve_dag = serve.deployment(class_factory()).bind(3)
handle = serve.run(serve_dag)
url = get_application_url()
assert handle.get.remote().result() == 3
assert httpx.get(url).text == "3"
@serve.deployment
class Echo:
def __init__(self, s: str):
self._s = s
def __call__(self, *args):
return self._s
def test_single_node_deploy_success(serve_instance):
m1 = Adder.bind(1)
handle = serve.run(m1)
assert handle.remote(41).result() == 42
@serve.deployment
class TakeHandle:
def __init__(self, handle) -> None:
self.handle = handle
async def predict(self, inp):
return await self.handle.remote(inp)
async def __call__(self, request: starlette.requests.Request):
inp = await request.json()
return await self.predict(inp)
def test_passing_handle(serve_instance):
child = Adder.bind(1)
parent = TakeHandle.bind(child)
handle = serve.run(parent)
url = get_application_url()
assert handle.predict.remote(1).result() == 2
assert httpx.post(url, json=1).json() == 2
@serve.deployment
class DictParent:
def __init__(self, d: Dict[str, DeploymentHandle]):
self._d = d
async def __call__(self, key: str):
return await self._d[key].remote()
def test_passing_handle_in_obj(serve_instance):
child1 = Echo.bind("ed")
child2 = Echo.bind("simon")
parent = DictParent.bind({"child1": child1, "child2": child2})
handle = serve.run(parent)
assert handle.remote("child1").result() == "ed"
assert handle.remote("child2").result() == "simon"
@serve.deployment
class Child:
def __call__(self, *args):
return os.getpid()
@serve.deployment
class Parent:
def __init__(self, child):
self._child = child
async def __call__(self, *args):
return await self._child.remote()
@serve.deployment
class GrandParent:
def __init__(self, child, parent):
self._child = child
self._parent = parent
async def __call__(self, *args):
# Check that the grandparent and parent are talking to the same child.
assert await self._child.remote() == await self._parent.remote()
return "ok"
def test_pass_handle_to_multiple(serve_instance):
child = Child.bind()
parent = Parent.bind(child)
grandparent = GrandParent.bind(child, parent)
handle = serve.run(grandparent)
assert handle.remote().result() == "ok"
def test_run_non_json_serializable_args(serve_instance):
# Test that we can capture and bind non-json-serializable arguments.
arr1 = array.array("d", [1.0, 2.0, 3.0])
arr2 = array.array("d", [2.0, 3.0, 4.0])
arr3 = array.array("d", [3.0, 4.0, 5.0])
@serve.deployment
class A:
def __init__(self, arr1, *, arr2):
self.arr1 = arr1
self.arr2 = arr2
self.arr3 = arr3
def __call__(self, *args):
return self.arr1, self.arr2, self.arr3
handle = serve.run(A.bind(arr1, arr2=arr2))
ret1, ret2, ret3 = handle.remote().result()
assert all(
[
ret1 == arr1,
ret2 == arr2,
ret3 == arr3,
]
)
@serve.deployment
def func():
return 1
def test_single_functional_node_base_case(serve_instance):
# Base case should work
handle = serve.run(func.bind())
url = get_application_url()
assert handle.remote().result() == 1
assert httpx.get(url).text == "1"
def test_unsupported_remote():
@serve.deployment
class Actor:
def ping(self):
return "hello"
with pytest.raises(
AttributeError, match=r"\'Application\' object has no attribute \'remote\'"
):
_ = Actor.bind().remote()
@serve.deployment
def func():
return 1
with pytest.raises(
AttributeError, match=r"\'Application\' object has no attribute \'remote\'"
):
_ = func.bind().remote()
if __name__ == "__main__":
sys.exit(pytest.main(["-v", "-s", __file__]))