Files
2026-07-13 13:17:40 +08:00

387 lines
10 KiB
Python

"""
Tests to ensure ray DAG can correctly mark its input(s) to take user
request, for all DAGNode types.
"""
from typing import Any, TypeVar
import pytest
import ray
from ray.dag.dag_node import DAGNode
from ray.dag.input_node import InputNode
RayHandleLike = TypeVar("RayHandleLike")
def test_no_args_to_input_node(shared_ray_instance):
@ray.remote
def f(input):
return input
with pytest.raises(
ValueError, match="InputNode should not take any args or kwargs"
):
with InputNode(0) as dag_input:
f.bind(dag_input)
with pytest.raises(
ValueError,
match="InputNode should not take any args or kwargs",
):
with InputNode(key=1) as dag_input:
f.bind(dag_input)
def test_simple_func(shared_ray_instance):
@ray.remote
def a(input: str):
return f"{input} -> a"
@ray.remote
def b(a: "RayHandleLike"):
# At runtime, a is replaced with execution result of a.
return f"{a} -> b"
# input -> a - > b -> ouput
with InputNode() as dag_input:
a_node = a.bind(dag_input)
dag = b.bind(a_node)
assert ray.get(dag.execute("input")) == "input -> a -> b"
assert ray.get(dag.execute("test")) == "test -> a -> b"
def test_func_dag(shared_ray_instance):
@ray.remote
def a(user_input):
return user_input
@ray.remote
def b(x):
return x * 2
@ray.remote
def c(x):
return x + 1
@ray.remote
def d(x, y):
return x + y
with InputNode() as dag_input:
a_ref = a.bind(dag_input)
b_ref = b.bind(a_ref)
c_ref = c.bind(a_ref)
d_ref = d.bind(b_ref, c_ref)
d1_ref = d.bind(d_ref, d_ref)
d2_ref = d.bind(d1_ref, d_ref)
dag = d.bind(d2_ref, d_ref)
# [(2*2 + 2+1) + (2*2 + 2+1)] + [(2*2 + 2+1) + (2*2 + 2+1)]
assert ray.get(dag.execute(2)) == 28
# [(3*2 + 3+1) + (3*2 + 3+1)] + [(3*2 + 3+1) + (3*2 + 3+1)]
assert ray.get(dag.execute(3)) == 40
def test_multi_input_func_dag(shared_ray_instance):
@ray.remote
def a(user_input):
return user_input * 2
@ray.remote
def b(user_input):
return user_input + 1
@ray.remote
def c(x, y):
return x + y
with InputNode() as dag_input:
a_ref = a.bind(dag_input)
b_ref = b.bind(dag_input)
dag = c.bind(a_ref, b_ref)
# (2*2) + (2*1)
assert ray.get(dag.execute(2)) == 7
# (3*2) + (3*1)
assert ray.get(dag.execute(3)) == 10
def test_invalid_input_node_as_class_constructor(shared_ray_instance):
@ray.remote
class Actor:
def __init__(self, val):
self.val = val
def get(self):
return self.val
with pytest.raises(
ValueError,
match=(
"InputNode handles user dynamic input the DAG, and "
"cannot be used as args, kwargs, or other_args_to_resolve "
"in ClassNode constructor because it is not available at "
"class construction or binding time."
),
):
with InputNode() as dag_input:
Actor.bind(dag_input)
def test_class_method_input(shared_ray_instance):
@ray.remote
class Model:
def __init__(self, weight: int):
self.weight = weight
def forward(self, input: "RayHandleLike"):
return self.weight * input
@ray.remote
class FeatureProcessor:
def __init__(self, scale):
self.scale = scale
def process(self, input: int):
return input * self.scale
with InputNode() as dag_input:
preprocess = FeatureProcessor.bind(0.5)
feature = preprocess.process.bind(dag_input)
model = Model.bind(4)
dag = model.forward.bind(feature)
# 2 * 0.5 * 4
assert ray.get(dag.execute(2)) == 4
# 6 * 0.5 * 4
assert ray.get(dag.execute(6)) == 12
def test_multi_class_method_input(shared_ray_instance):
"""
Test a multiple class methods can all be used as inputs in a dag.
"""
@ray.remote
class Model:
def __init__(self, weight: int):
self.weight = weight
def forward(self, input: int):
return self.weight * input
@ray.remote
def combine(m1: "RayHandleLike", m2: "RayHandleLike"):
return m1 + m2
with InputNode() as dag_input:
m1 = Model.bind(2)
m2 = Model.bind(3)
m1_output = m1.forward.bind(dag_input)
m2_output = m2.forward.bind(dag_input)
dag = combine.bind(m1_output, m2_output)
# 1*2 + 1*3
assert ray.get(dag.execute(1)) == 5
# 2*2 + 2*3
assert ray.get(dag.execute(2)) == 10
def test_func_class_mixed_input(shared_ray_instance):
"""
Test both class method and function are used as input in the
same dag.
"""
@ray.remote
class Model:
def __init__(self, weight: int):
self.weight = weight
def forward(self, input: int):
return self.weight * input
@ray.remote
def model_func(input: int):
return input * 2
@ray.remote
def combine(m1: "RayHandleLike", m2: "RayHandleLike"):
return m1 + m2
with InputNode() as dag_input:
m1 = Model.bind(3)
m1_output = m1.forward.bind(dag_input)
m2_output = model_func.bind(dag_input)
dag = combine.bind(m1_output, m2_output)
# 2*3 + 2*2
assert ray.get(dag.execute(2)) == 10
# 3*3 + 3*2
assert ray.get(dag.execute(3)) == 15
def test_input_attr_partial_access(shared_ray_instance):
@ray.remote
class Model:
def __init__(self, weight: int):
self.weight = weight
def forward(self, input: int):
return self.weight * input
@ray.remote
def combine(a, b, c, d=None):
if not d:
return a + b + c
else:
return a + b + c + d["deep"]["nested"]
# 1) Test default wrapping of args and kwargs into internal python object
with InputNode() as dag_input:
m1 = Model.bind(1)
m2 = Model.bind(2)
m1_output = m1.forward.bind(dag_input[0])
m2_output = m2.forward.bind(dag_input[1])
dag = combine.bind(m1_output, m2_output, dag_input.m3, dag_input.m4)
# 1*1 + 2*2 + 3 + 4 = 12
assert ray.get(dag.execute(1, 2, m3=3, m4={"deep": {"nested": 4}})) == 12
# 2) Test user passed data object as only input to the dag.execute()
class UserDataObj:
user_object_field_0: Any
user_object_field_1: Any
field_3: Any
def __init__(
self, user_object_field_0: Any, user_object_field_1: Any, field_3: Any
) -> None:
self.user_object_field_0 = user_object_field_0
self.user_object_field_1 = user_object_field_1
self.field_3 = field_3
with InputNode() as dag_input:
m1 = Model.bind(1)
m2 = Model.bind(2)
m1_output = m1.forward.bind(dag_input.user_object_field_0)
m2_output = m2.forward.bind(dag_input.user_object_field_1)
dag = combine.bind(m1_output, m2_output, dag_input.field_3)
# 1*1 + 2*2 + 3
assert ray.get(dag.execute(UserDataObj(1, 2, 3))) == 8
# 3) Test user passed only one list object with regular list index accessor
with InputNode() as dag_input:
m1 = Model.bind(1)
m2 = Model.bind(2)
m1_output = m1.forward.bind(dag_input[0])
m2_output = m2.forward.bind(dag_input[1])
dag = combine.bind(m1_output, m2_output, dag_input[2])
# 1*1 + 2*2 + 3 + 4 = 12
assert ray.get(dag.execute([1, 2, 3])) == 8
# 4) Test user passed only one dict object with key str accessor
with InputNode() as dag_input:
m1 = Model.bind(1)
m2 = Model.bind(2)
m1_output = m1.forward.bind(dag_input["m1"])
m2_output = m2.forward.bind(dag_input["m2"])
dag = combine.bind(m1_output, m2_output, dag_input["m3"])
# 1*1 + 2*2 + 3 + 4 = 12
assert ray.get(dag.execute({"m1": 1, "m2": 2, "m3": 3})) == 8
with pytest.raises(
AssertionError,
match="Please only use int index or str as first-level key",
):
with InputNode() as dag_input:
m1 = Model.bind(1)
dag = m1.forward.bind(dag_input[(1, 2)])
def test_ensure_in_context_manager(shared_ray_instance):
# No enforcement on creation given __enter__ executes after __init__
input = InputNode()
with pytest.raises(
AssertionError,
match=(
"InputNode is a singleton instance that should be only used "
"in context manager"
),
):
input.execute()
@ray.remote
def f(input):
return input
# No enforcement on creation given __enter__ executes after __init__
dag = f.bind(InputNode())
with pytest.raises(
AssertionError,
match=(
"InputNode is a singleton instance that should be only used "
"in context manager"
),
):
dag.execute()
def test_ensure_input_node_singleton(shared_ray_instance):
@ray.remote
def f(input):
return input
@ray.remote
def combine(a, b):
return a + b
with InputNode() as input_1:
a = f.bind(input_1)
with InputNode() as input_2:
b = f.bind(input_2)
dag = combine.bind(a, b)
with pytest.raises(
AssertionError, match="Each DAG should only have one unique InputNode"
):
_ = ray.get(dag.execute(2))
def test_apply_recursive_caching(shared_ray_instance):
@ray.remote
def f(input):
return input
input = InputNode()
f_node = f.bind(input)
a, b = input, f_node
for _ in range(10):
a, b = f.bind(a, b), f.bind(a, b)
counter = 0
original_apply_recursive = DAGNode.apply_recursive
def _apply_recursive_with_counter(self, fn):
nonlocal counter
counter += 1
return original_apply_recursive(self, fn)
DAGNode.apply_recursive = _apply_recursive_with_counter
a.apply_recursive(lambda node: node)
# Prior to #40337; count was 2559
assert counter == 40
DAGNode.apply_recursive = original_apply_recursive
if __name__ == "__main__":
import sys
sys.exit(pytest.main(["-v", __file__]))