298 lines
9.0 KiB
Python
298 lines
9.0 KiB
Python
import sys
|
|
import threading
|
|
import time
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
import ray
|
|
import ray._common.test_utils
|
|
import ray._private.test_utils as test_utils
|
|
from ray._private.state import available_resources
|
|
|
|
|
|
def ensure_cpu_returned(expected_cpus):
|
|
ray._common.test_utils.wait_for_condition(
|
|
lambda: (available_resources().get("CPU", 0) == expected_cpus)
|
|
)
|
|
|
|
|
|
def test_threaded_actor_basic(ray_start_cluster):
|
|
"""Test the basic threaded actor."""
|
|
ray.init(num_cpus=1)
|
|
|
|
@ray.remote(num_cpus=1)
|
|
class ThreadedActor:
|
|
def __init__(self):
|
|
self.received = []
|
|
self.lock = threading.Lock()
|
|
|
|
def add(self, seqno):
|
|
with self.lock:
|
|
self.received.append(seqno)
|
|
|
|
def get_all(self):
|
|
with self.lock:
|
|
return self.received
|
|
|
|
a = ThreadedActor.options(max_concurrency=10).remote()
|
|
max_seq = 50
|
|
ray.get([a.add.remote(seqno) for seqno in range(max_seq)])
|
|
seqnos = ray.get(a.get_all.remote())
|
|
# Currently, the caller submission order is not guaranteed
|
|
# when the threaded actor is used.
|
|
assert sorted(seqnos) == list(range(max_seq))
|
|
ray.kill(a)
|
|
ensure_cpu_returned(1)
|
|
|
|
|
|
def test_threaded_actor_api_thread_safe(ray_start_cluster):
|
|
"""Test if Ray APIs are thread safe
|
|
when they are used within threaded actor.
|
|
"""
|
|
ray.init(
|
|
num_cpus=8,
|
|
# from 1024 bytes, the return obj will go to the plasma store.
|
|
_system_config={"max_direct_call_object_size": 1024},
|
|
)
|
|
|
|
@ray.remote
|
|
def in_memory_return(i):
|
|
return i
|
|
|
|
@ray.remote
|
|
def plasma_return(i):
|
|
arr = np.zeros(8 * 1024 * i, dtype=np.uint8) # 8 * i KB
|
|
return arr
|
|
|
|
@ray.remote(num_cpus=1)
|
|
class ThreadedActor:
|
|
def __init__(self):
|
|
self.received = []
|
|
self.lock = threading.Lock()
|
|
|
|
def in_memory_return_test(self, i):
|
|
self._add(i)
|
|
return ray.get(in_memory_return.remote(i))
|
|
|
|
def plasma_return_test(self, i):
|
|
self._add(i)
|
|
return ray.get(plasma_return.remote(i))
|
|
|
|
def _add(self, seqno):
|
|
with self.lock:
|
|
self.received.append(seqno)
|
|
|
|
def get_all(self):
|
|
with self.lock:
|
|
return self.received
|
|
|
|
a = ThreadedActor.options(max_concurrency=10).remote()
|
|
max_seq = 50
|
|
|
|
# Test in-memory return obj
|
|
seqnos = ray.get(
|
|
[a.in_memory_return_test.remote(seqno) for seqno in range(max_seq)]
|
|
)
|
|
assert sorted(seqnos) == list(range(max_seq))
|
|
|
|
# Test plasma return obj
|
|
real = ray.get([a.plasma_return_test.remote(seqno) for seqno in range(max_seq)])
|
|
expected = [np.zeros(8 * 1024 * i, dtype=np.uint8) for i in range(max_seq)]
|
|
for r, e in zip(real, expected):
|
|
assert np.array_equal(r, e)
|
|
|
|
ray.kill(a)
|
|
ensure_cpu_returned(8)
|
|
|
|
|
|
def test_threaded_actor_creation_and_kill(ray_start_cluster):
|
|
"""Test the scenario where the threaded actors are created and killed."""
|
|
cluster = ray_start_cluster
|
|
NUM_CPUS_PER_NODE = 3
|
|
NUM_NODES = 2
|
|
for _ in range(NUM_NODES):
|
|
cluster.add_node(num_cpus=NUM_CPUS_PER_NODE)
|
|
ray.init(address=cluster.address)
|
|
|
|
@ray.remote(num_cpus=0)
|
|
class ThreadedActor:
|
|
def __init__(self):
|
|
self.received = []
|
|
self.lock = threading.Lock()
|
|
|
|
def add(self, seqno):
|
|
time.sleep(1)
|
|
with self.lock:
|
|
self.received.append(seqno)
|
|
|
|
def get_all(self):
|
|
with self.lock:
|
|
return self.received
|
|
|
|
def ready(self):
|
|
pass
|
|
|
|
def terminate(self):
|
|
ray.actor.exit_actor()
|
|
|
|
# - Create threaded actors
|
|
# - Submit many tasks.
|
|
# - Ungracefully kill them in the middle.
|
|
for _ in range(10):
|
|
actors = [
|
|
ThreadedActor.options(max_concurrency=10).remote()
|
|
for _ in range(NUM_NODES * NUM_CPUS_PER_NODE)
|
|
]
|
|
ray.get([actor.ready.remote() for actor in actors])
|
|
|
|
for _ in range(10):
|
|
for actor in actors:
|
|
actor.add.remote(1)
|
|
time.sleep(0.5)
|
|
for actor in actors:
|
|
ray.kill(actor)
|
|
ensure_cpu_returned(NUM_NODES * NUM_CPUS_PER_NODE)
|
|
|
|
# - Create threaded actors
|
|
# - Submit many tasks.
|
|
# - Gracefully kill them in the middle.
|
|
for _ in range(10):
|
|
actors = [
|
|
ThreadedActor.options(max_concurrency=10).remote()
|
|
for _ in range(NUM_NODES * NUM_CPUS_PER_NODE)
|
|
]
|
|
ray.get([actor.ready.remote() for actor in actors])
|
|
for _ in range(10):
|
|
for actor in actors:
|
|
actor.add.remote(1)
|
|
|
|
time.sleep(0.5)
|
|
for actor in actors:
|
|
actor.terminate.remote()
|
|
ensure_cpu_returned(NUM_NODES * NUM_CPUS_PER_NODE)
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform == "win32", reason="Failing on Windows.")
|
|
@pytest.mark.parametrize("ray_start_cluster_head", [{"num_cpus": 2}], indirect=True)
|
|
def test_threaded_actor_integration_test_stress(
|
|
ray_start_cluster_head, log_pubsub, error_pubsub
|
|
):
|
|
"""This is a sanity test that checks threaded actors are
|
|
working with the nightly stress test.
|
|
"""
|
|
cluster = ray_start_cluster_head
|
|
p = log_pubsub
|
|
e = error_pubsub
|
|
|
|
# Prepare the config
|
|
num_remote_nodes = 4
|
|
num_parents = 6
|
|
num_children = 6
|
|
death_probability = 0.95
|
|
max_concurrency = 10
|
|
|
|
for _ in range(num_remote_nodes):
|
|
cluster.add_node(num_cpus=2)
|
|
|
|
@ray.remote
|
|
class Child(object):
|
|
def __init__(self, death_probability):
|
|
self.death_probability = death_probability
|
|
|
|
def ping(self):
|
|
# Exit process with some probability.
|
|
exit_chance = np.random.rand()
|
|
if exit_chance > self.death_probability:
|
|
sys.exit(-1)
|
|
|
|
@ray.remote
|
|
class Parent(object):
|
|
def __init__(self, num_children, death_probability=0.95):
|
|
self.death_probability = death_probability
|
|
self.children = [
|
|
Child.options(max_concurrency=max_concurrency).remote(death_probability)
|
|
for _ in range(num_children)
|
|
]
|
|
|
|
def ping(self, num_pings):
|
|
children_outputs = []
|
|
for _ in range(num_pings):
|
|
children_outputs += [child.ping.remote() for child in self.children]
|
|
try:
|
|
ray.get(children_outputs)
|
|
except Exception:
|
|
# Replace the children if one of them died.
|
|
self.__init__(len(self.children), self.death_probability)
|
|
|
|
def kill(self):
|
|
# Clean up children.
|
|
ray.get([child.__ray_terminate__.remote() for child in self.children])
|
|
|
|
parents = [
|
|
Parent.options(max_concurrency=max_concurrency).remote(
|
|
num_children, death_probability
|
|
)
|
|
for _ in range(num_parents)
|
|
]
|
|
|
|
start = time.time()
|
|
loop_times = []
|
|
for _ in range(10):
|
|
loop_start = time.time()
|
|
ray.get([parent.ping.remote(10) for parent in parents])
|
|
|
|
# Kill a parent actor with some probability.
|
|
exit_chance = np.random.rand()
|
|
if exit_chance > death_probability:
|
|
parent_index = np.random.randint(len(parents))
|
|
parents[parent_index].kill.remote()
|
|
parents[parent_index] = Parent.options(
|
|
max_concurrency=max_concurrency
|
|
).remote(num_children, death_probability)
|
|
loop_times.append(time.time() - loop_start)
|
|
result = {}
|
|
print("Finished in: {}s".format(time.time() - start))
|
|
print("Average iteration time: {}s".format(np.mean(loop_times)))
|
|
print("Max iteration time: {}s".format(max(loop_times)))
|
|
print("Min iteration time: {}s".format(min(loop_times)))
|
|
result["total_time"] = time.time() - start
|
|
result["avg_iteration_time"] = np.mean(loop_times)
|
|
result["max_iteration_time"] = max(loop_times)
|
|
result["min_iteration_time"] = min(loop_times)
|
|
result["success"] = 1
|
|
print(result)
|
|
ensure_cpu_returned(10)
|
|
del parents
|
|
|
|
# Make sure parents are still scheduleable.
|
|
parents = [
|
|
Parent.options(max_concurrency=max_concurrency).remote(
|
|
num_children, death_probability
|
|
)
|
|
for _ in range(num_parents)
|
|
]
|
|
ray.get([parent.ping.remote(10) for parent in parents])
|
|
"""
|
|
Make sure there are not SIGSEGV, SIGBART, or other odd check failures.
|
|
"""
|
|
# Get all logs for 20 seconds.
|
|
logs = test_utils.get_log_message(p, timeout=20)
|
|
for log in logs:
|
|
assert "SIG" not in log, "There's the segfault or SIGBART reported."
|
|
assert "Check failed" not in log, "There's the check failure reported."
|
|
|
|
# Get error messages for 10 seconds.
|
|
errors = test_utils.get_error_message(e, timeout=10)
|
|
for error in errors:
|
|
print(error)
|
|
assert (
|
|
"You can ignore this message if" not in error["error_message"]
|
|
), "Resource deadlock warning shouldn't be printed, but it did."
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
# Test suite is timing out. Disable on windows for now.
|
|
sys.exit(pytest.main(["-sv", __file__]))
|