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

204 lines
5.2 KiB
Python

import pytest
import ray
from ray._common.test_utils import wait_for_condition
from ray.dag.input_node import InputNode
from ray.dag.output_node import MultiOutputNode
from ray.util.state import list_tasks
def test_output_node(shared_ray_instance):
@ray.remote
def f(input):
return input
with pytest.raises(ValueError):
with InputNode() as input_data:
dag = MultiOutputNode(f.bind(input_data))
with InputNode() as input_data:
dag = MultiOutputNode([f.bind(input_data)])
assert ray.get(dag.execute(1)) == [1]
assert ray.get(dag.execute(2)) == [2]
with InputNode() as input_data:
dag = MultiOutputNode([f.bind(input_data["x"]), f.bind(input_data["y"])])
refs = dag.execute({"x": 1, "y": 2})
assert len(refs) == 2
assert ray.get(refs) == [1, 2]
with InputNode() as input_data:
dag = MultiOutputNode(
[f.bind(input_data["x"]), f.bind(input_data["y"]), f.bind(input_data["x"])]
)
refs = dag.execute({"x": 1, "y": 2})
assert len(refs) == 3
assert ray.get(refs) == [1, 2, 1]
def test_dag_with_actor_handle(shared_ray_instance):
"""Verify DAG API works with actor created by .remote"""
@ray.remote
class Worker:
def __init__(self):
self.forward_called = 0
self.init_called = 0
def forward(self, input):
print("forward")
self.forward_called += 1
return input
def initialize(self, input):
print("initialize")
self.init_called += 1
return input
def get(self):
return (self.forward_called, self.init_called)
worker = Worker.remote()
with InputNode() as input_node:
init_dag = worker.initialize.bind(input_node)
with InputNode() as input_node:
forward_dag = worker.forward.bind(input_node)
assert ray.get(init_dag.execute(1)) == 1
assert ray.get(forward_dag.execute(2)) == 2
# Make sure both forward/initialize called only once
assert ray.get(worker.get.remote()) == (1, 1)
# Double check the actor is resued.
assert ray.get(init_dag.execute(1)) == 1
assert ray.get(worker.get.remote()) == (1, 2)
def test_dag_with_alive_actors_chained(shared_ray_instance):
"""Verify we can have multiple DAGs to the
same actor that are chained.
"""
@ray.remote
class Actor:
def __init__(self, init_value):
self.i = init_value
def add(self, x):
return self.i + x
@ray.remote
def combine(x, y):
return x + y
a1 = Actor.remote(10)
a1_dag = a1.add.bind(a1.add.bind(2)) # 22
a1_dag_2 = a1.add.bind(a1.add.bind(6)) # 26
dag = combine.bind(a1_dag, a1_dag_2)
assert ray.get(dag.execute()) == 48
def test_tensor_parallel_dag(shared_ray_instance):
"""Simulate the TP DAG with N workers.
Input -> forward -> MultiOutput
"""
@ray.remote
class Worker:
def __init__(self, rank):
self.rank = rank
self.forwarded = 0
def forward(self, input_data: int):
print(input_data)
self.forwarded += 1
return self.rank + input_data
def initialize(self):
pass
def get_forwarded(self):
return self.forwarded
NUM_WORKERS = 4
workers = [Worker.remote(i) for i in range(NUM_WORKERS)]
# Init multiple times.
for _ in range(4):
ray.get([worker.initialize.remote() for worker in workers])
with InputNode() as input_data:
dag = MultiOutputNode([worker.forward.bind(input_data) for worker in workers])
# Run DAG repetitively.
ITER = 4
assert ITER > 1
for i in range(ITER):
ref = dag.execute(i)
all_outputs = ray.get(ref)
assert len(all_outputs) == NUM_WORKERS
assert all_outputs == [i + j for j in range(NUM_WORKERS)]
forwarded = ray.get([worker.get_forwarded.remote() for worker in workers])
assert forwarded == [ITER for _ in range(NUM_WORKERS)]
def test_shared_output(shared_ray_instance):
"""Verify when an upstream task output is shared by
multi output, the upstream task runs only once.
"""
@ray.remote
def shared_f():
return 1
@ray.remote
def g(input):
return input + 1
@ray.remote
def h(input):
return input + 2
x = shared_f.bind()
dag = MultiOutputNode([g.bind(x), h.bind(x)])
assert ray.get(dag.execute()) == [2, 3]
# Verify f ran only once.
def verify():
tasks = list_tasks(filters=[("name", "=", "shared_f")])
return len(tasks) == 1
wait_for_condition(verify)
def test_bind_survives_handle_deletion(shared_ray_instance):
"""Verify that .bind().execute() still works even if the original handle was dropped."""
@ray.remote
class A:
def f(self):
return 1
# Grab the handle and the bound method node
actor = A.remote()
method_node = actor.f.bind()
# Destroy the only Python variable reference and force collection
del actor
# Executing should now succeed because the node holds the ref
result = ray.get(method_node.execute())
assert result == 1
if __name__ == "__main__":
import sys
sys.exit(pytest.main(["-v", __file__]))