193 lines
6.2 KiB
Python
193 lines
6.2 KiB
Python
import platform
|
|
import re
|
|
import sys
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
import ray
|
|
from ray._common.test_utils import wait_for_condition
|
|
from ray.cluster_utils import AutoscalingCluster
|
|
|
|
|
|
# Triggers the addition of a worker node.
|
|
@ray.remote(num_cpus=1)
|
|
class Actor:
|
|
def __init__(self):
|
|
self.data = []
|
|
|
|
def f(self):
|
|
pass
|
|
|
|
def recv(self, obj):
|
|
pass
|
|
|
|
def create(self, size):
|
|
return np.zeros(size)
|
|
|
|
|
|
# Tests that we scale down even if secondary copies of objects are present on
|
|
# idle nodes: https://github.com/ray-project/ray/issues/21870
|
|
@pytest.mark.skipif(platform.system() == "Windows", reason="Failing on Windows.")
|
|
@pytest.mark.parametrize("autoscaler_v2", [False, True], ids=["v1", "v2"])
|
|
def test_scaledown_shared_objects(autoscaler_v2, shutdown_only):
|
|
cluster = AutoscalingCluster(
|
|
head_resources={"CPU": 0},
|
|
worker_node_types={
|
|
"cpu_node": {
|
|
"resources": {
|
|
"CPU": 1,
|
|
"object_store_memory": 100 * 1024 * 1024,
|
|
},
|
|
"node_config": {},
|
|
"min_workers": 0,
|
|
"max_workers": 5,
|
|
},
|
|
},
|
|
idle_timeout_minutes=0.05,
|
|
autoscaler_v2=autoscaler_v2,
|
|
)
|
|
|
|
try:
|
|
cluster.start(_system_config={"scheduler_report_pinned_bytes_only": True})
|
|
ray.init("auto")
|
|
|
|
actors = [Actor.remote() for _ in range(5)]
|
|
ray.get([a.f.remote() for a in actors])
|
|
print("All five nodes launched")
|
|
|
|
# Verify scale-up.
|
|
wait_for_condition(lambda: ray.cluster_resources().get("CPU", 0) == 5)
|
|
|
|
data = actors[0].create.remote(1024 * 1024 * 5)
|
|
ray.get([a.recv.remote(data) for a in actors])
|
|
print("Data broadcast successfully, deleting actors.")
|
|
del actors
|
|
|
|
# Verify scale-down.
|
|
wait_for_condition(
|
|
lambda: ray.cluster_resources().get("CPU", 0) == 1, timeout=30
|
|
)
|
|
finally:
|
|
cluster.shutdown()
|
|
|
|
|
|
def check_memory(local_objs, num_spilled_objects=None, num_plasma_objects=None):
|
|
def ok():
|
|
s = ray._private.internal_api.memory_summary()
|
|
print(f"\n\nMemory Summary:\n{s}\n")
|
|
|
|
actual_objs = re.findall(r"LOCAL_REFERENCE[\s|\|]+([0-9a-f]+)", s)
|
|
if sorted(actual_objs) != sorted(local_objs):
|
|
raise RuntimeError(
|
|
f"Expect local objects={local_objs}, actual={actual_objs}"
|
|
)
|
|
|
|
if num_spilled_objects is not None:
|
|
m = re.search(r"Spilled (\d+) MiB, (\d+) objects", s)
|
|
if m is not None:
|
|
actual_spilled_objects = int(m.group(2))
|
|
if actual_spilled_objects < num_spilled_objects:
|
|
raise RuntimeError(
|
|
f"Expected spilled objects={num_spilled_objects} "
|
|
f"greater than actual={actual_spilled_objects}"
|
|
)
|
|
|
|
if num_plasma_objects is not None:
|
|
m = re.search(r"Plasma memory usage (\d+) MiB, (\d+) objects", s)
|
|
if m is None:
|
|
raise RuntimeError(
|
|
"Memory summary does not contain Plasma memory objects count"
|
|
)
|
|
actual_plasma_objects = int(m.group(2))
|
|
if actual_plasma_objects != num_plasma_objects:
|
|
raise RuntimeError(
|
|
f"Expected plasma objects={num_plasma_objects} not equal "
|
|
f"to actual={actual_plasma_objects}"
|
|
)
|
|
|
|
return True
|
|
|
|
wait_for_condition(ok, timeout=30, retry_interval_ms=5000)
|
|
|
|
|
|
# Tests that node with live spilled object does not get scaled down.
|
|
@pytest.mark.skipif(platform.system() == "Windows", reason="Failing on Windows.")
|
|
@pytest.mark.parametrize("autoscaler_v2", [False, True], ids=["v1", "v2"])
|
|
def test_no_scaledown_with_spilled_objects(autoscaler_v2, shutdown_only):
|
|
cluster = AutoscalingCluster(
|
|
head_resources={"CPU": 0},
|
|
worker_node_types={
|
|
"cpu_node": {
|
|
"resources": {
|
|
"CPU": 1,
|
|
"object_store_memory": 75 * 1024 * 1024,
|
|
},
|
|
"node_config": {},
|
|
"min_workers": 0,
|
|
"max_workers": 2,
|
|
},
|
|
},
|
|
idle_timeout_minutes=0.05,
|
|
autoscaler_v2=autoscaler_v2,
|
|
)
|
|
|
|
try:
|
|
cluster.start(
|
|
_system_config={
|
|
"scheduler_report_pinned_bytes_only": True,
|
|
"min_spilling_size": 0,
|
|
}
|
|
)
|
|
ray.init("auto")
|
|
|
|
actors = [Actor.remote() for _ in range(2)]
|
|
ray.get([a.f.remote() for a in actors])
|
|
|
|
# Verify scale-up.
|
|
wait_for_condition(lambda: ray.cluster_resources().get("CPU", 0) == 2)
|
|
print("All nodes launched")
|
|
|
|
# Put 10 x 80MiB objects into the object store with 75MiB memory limit.
|
|
obj_size = 10 * 1024 * 1024
|
|
objs = []
|
|
for i in range(10):
|
|
obj = actors[0].create.remote(obj_size)
|
|
ray.get(actors[1].recv.remote(obj))
|
|
objs.append(obj)
|
|
print(f"obj {i}={obj.hex()}")
|
|
del obj
|
|
|
|
# At least 9 out of the 10 objects should have spilled.
|
|
check_memory([obj.hex() for obj in objs], num_spilled_objects=9)
|
|
print("Objects spilled, deleting actors and object references.")
|
|
|
|
# Assume the 1st object always gets spilled.
|
|
spilled_obj = objs[0]
|
|
del objs
|
|
del actors
|
|
|
|
# Verify scale-down to 1 node.
|
|
def scaledown_to_one():
|
|
cpu = ray.cluster_resources().get("CPU", 0)
|
|
assert cpu > 0, "Scale-down should keep at least 1 node"
|
|
return cpu == 1
|
|
|
|
wait_for_condition(scaledown_to_one, timeout=30)
|
|
|
|
# Verify the spilled object still exists, and there is no object in the
|
|
# plasma store.
|
|
check_memory([spilled_obj.hex()], num_plasma_objects=0)
|
|
|
|
# Delete the spilled object, the remaining worker node should be scaled
|
|
# down.
|
|
del spilled_obj
|
|
wait_for_condition(lambda: ray.cluster_resources().get("CPU", 0) == 0)
|
|
check_memory([], num_plasma_objects=0)
|
|
finally:
|
|
cluster.shutdown()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(pytest.main(["-sv", __file__]))
|