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

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__]))