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

377 lines
12 KiB
Python

import logging
import sys
import time
import pytest
import ray
from ray._common.test_utils import SignalActor, wait_for_condition
from ray._private.test_utils import (
generate_system_config_map,
)
from ray.autoscaler._private.monitor import Monitor
from ray.autoscaler.sdk import request_resources
from ray.cluster_utils import Cluster
from ray.util.placement_group import placement_group, remove_placement_group
logger = logging.getLogger(__name__)
def test_cluster():
"""Basic test for adding and removing nodes in cluster."""
g = Cluster(initialize_head=False)
node = g.add_node()
node2 = g.add_node()
assert node.remaining_processes_alive()
assert node2.remaining_processes_alive()
g.remove_node(node2)
g.remove_node(node)
assert not any(n.any_processes_alive() for n in [node, node2])
g.shutdown()
def test_shutdown():
g = Cluster(initialize_head=False)
node = g.add_node()
node2 = g.add_node()
g.shutdown()
assert not any(n.any_processes_alive() for n in [node, node2])
@pytest.mark.parametrize(
"ray_start_cluster_head",
[
generate_system_config_map(
health_check_initial_delay_ms=0,
health_check_period_ms=1000,
health_check_failure_threshold=3,
object_timeout_milliseconds=12345,
),
],
indirect=True,
)
def test_system_config(ray_start_cluster_head):
"""Checks that the internal configuration setting works.
We set the cluster to timeout nodes after 2 seconds of no timeouts. We
then remove a node, wait for 1 second to check that the cluster is out
of sync, then wait another 2 seconds (giving 1 second of leeway) to check
that the client has timed out. We also check to see if the config is set.
"""
cluster = ray_start_cluster_head
worker = cluster.add_node()
cluster.wait_for_nodes()
@ray.remote
def f():
assert ray._config.object_timeout_milliseconds() == 12345
assert ray._config.health_check_initial_delay_ms() == 0
assert ray._config.health_check_failure_threshold() == 3
assert ray._config.health_check_period_ms() == 1000
ray.get([f.remote() for _ in range(5)])
cluster.remove_node(worker, allow_graceful=False)
time.sleep(1)
assert ray.cluster_resources()["CPU"] == 2
def _node_removed():
return ray.cluster_resources()["CPU"] == 1
wait_for_condition(_node_removed, timeout=3)
def setup_monitor(address):
monitor = Monitor(address, None)
return monitor
def assert_correct_pg(pg_response_data, pg_demands, strategy):
assert len(pg_response_data) == 1
pg_response_data = pg_response_data[0]
strategy_mapping_dict_protobuf = {
"PACK": 0,
"SPREAD": 1,
"STRICT_PACK": 2,
"STRICT_SPREAD": 3,
}
assert pg_response_data.strategy == strategy_mapping_dict_protobuf[strategy]
assert pg_response_data.creator_job_id
assert pg_response_data.creator_actor_id
assert pg_response_data.creator_actor_dead
assert pg_response_data.placement_group_id
for i, bundle in enumerate(pg_demands):
assert pg_response_data.bundles[i].unit_resources == bundle
assert pg_response_data.bundles[i].bundle_id.placement_group_id
# DO NOT CHANGE THIS VERIFICATION WITHOUT NOTIFYING (Eric/Ameer/Alex).
def verify_load_metrics(monitor, expected_resource_usage=None, timeout=30):
request_resources(num_cpus=42)
# add placement groups.
pg_demands = [{"GPU": 2}, {"extra_resource": 2}]
strategy = "STRICT_PACK"
pg = placement_group(pg_demands, strategy=strategy)
pg.ready()
time.sleep(2) # wait for placement groups to propagate.
# Disable event clearing for test.
monitor.event_summarizer.clear = lambda *a: None
visited_atleast_once = [set(), set()]
while True:
monitor.update_load_metrics()
monitor.update_resource_requests()
monitor.update_event_summary()
resource_usage = monitor.load_metrics._get_resource_usage()
# Check resource request propagation.
# For v1 autoscaler, resource_requests should be in old format (ResourceDict)
# because commands.py extracts resources field for v1 compatibility.
req = monitor.load_metrics.resource_requests
assert req == [{"CPU": 1}] * 42, req
# Call summary() to trigger freq_of_dicts() which will fail if resource_requests
# contains new format with nested dicts (label_selector).
# This ensures the fix in commands.py is working correctly.
summary = monitor.load_metrics.summary()
assert summary is not None
pg_response_data = monitor.load_metrics.pending_placement_groups
assert_correct_pg(pg_response_data, pg_demands, strategy)
if "memory" in resource_usage[0]:
del resource_usage[0]["memory"]
visited_atleast_once[0].add("memory")
if "object_store_memory" in resource_usage[0]:
del resource_usage[0]["object_store_memory"]
visited_atleast_once[0].add("object_store_memory")
if "memory" in resource_usage[1]:
del resource_usage[1]["memory"]
visited_atleast_once[1].add("memory")
if "object_store_memory" in resource_usage[1]:
del resource_usage[1]["object_store_memory"]
visited_atleast_once[1].add("object_store_memory")
for key in list(resource_usage[0].keys()):
if key.startswith("node:"):
del resource_usage[0][key]
visited_atleast_once[0].add("node:")
for key in list(resource_usage[1].keys()):
if key.startswith("node:"):
del resource_usage[1][key]
visited_atleast_once[1].add("node:")
if expected_resource_usage is None:
if all(x for x in resource_usage[0:]):
break
elif all(x == y for x, y in zip(resource_usage, expected_resource_usage)):
break
else:
timeout -= 1
time.sleep(1)
if timeout <= 0:
raise ValueError(
"Timeout. {} != {}".format(resource_usage, expected_resource_usage)
)
# Sanity check we emitted a resize event.
assert any("Resized to" in x for x in monitor.event_summarizer.summary())
assert visited_atleast_once[0] == {"memory", "object_store_memory", "node:"}
assert visited_atleast_once[0] == visited_atleast_once[1]
remove_placement_group(pg)
return resource_usage
@pytest.mark.parametrize(
"ray_start_cluster_head",
[
{
"num_cpus": 1,
},
{
"num_cpus": 2,
},
],
indirect=True,
)
def test_heartbeats_single(ray_start_cluster_head):
"""Unit test for `Cluster.wait_for_nodes`.
Test proper metrics.
"""
cluster = ray_start_cluster_head
monitor = setup_monitor(cluster.gcs_address)
total_cpus = ray._private.state.cluster_resources()["CPU"]
verify_load_metrics(monitor, ({"CPU": 0.0}, {"CPU": total_cpus}))
@ray.remote
def work(signal):
wait_signal = signal.wait.remote()
while True:
ready, not_ready = ray.wait([wait_signal], timeout=0)
if len(ready) == 1:
break
time.sleep(1)
signal = SignalActor.remote()
work_handle = work.remote(signal)
verify_load_metrics(monitor, ({"CPU": 1.0}, {"CPU": total_cpus}))
ray.get(signal.send.remote())
ray.get(work_handle)
@ray.remote(num_cpus=1)
class Actor:
def work(self, signal):
wait_signal = signal.wait.remote()
while True:
ready, not_ready = ray.wait([wait_signal], timeout=0)
if len(ready) == 1:
break
time.sleep(1)
signal = SignalActor.remote()
test_actor = Actor.remote()
work_handle = test_actor.work.remote(signal)
time.sleep(1) # Time for actor to get placed and the method to start.
verify_load_metrics(monitor, ({"CPU": 1.0}, {"CPU": total_cpus}))
ray.get(signal.send.remote())
ray.get(work_handle)
del monitor
def test_wait_for_nodes(ray_start_cluster_head):
"""Unit test for `Cluster.wait_for_nodes`.
Adds 4 workers, waits, then removes 4 workers, waits,
then adds 1 worker, waits, and removes 1 worker, waits.
"""
cluster = ray_start_cluster_head
workers = [cluster.add_node() for i in range(4)]
cluster.wait_for_nodes()
[cluster.remove_node(w) for w in workers]
cluster.wait_for_nodes()
assert ray.cluster_resources()["CPU"] == 1
worker2 = cluster.add_node()
cluster.wait_for_nodes()
cluster.remove_node(worker2)
cluster.wait_for_nodes()
assert ray.cluster_resources()["CPU"] == 1
@pytest.mark.parametrize(
"call_ray_start",
[
"ray start --head --ray-client-server-port 20000 "
+ "--min-worker-port=0 --max-worker-port=0 --port 0"
],
indirect=True,
)
def test_ray_client(call_ray_start):
from ray.util.client import ray as ray_client
ray.client("localhost:20000").connect()
@ray.remote
def f():
return "hello client"
assert ray_client.get(f.remote()) == "hello client"
def test_detached_actor_autoscaling(ray_start_cluster_head):
"""Make sure that a detached actor, which belongs to a dead job, can start
workers on nodes that were added after the job ended.
"""
cluster = ray_start_cluster_head
cluster.add_node(num_cpus=2)
cluster.wait_for_nodes(2)
@ray.remote(num_cpus=1)
class Actor:
def __init__(self):
self.handles = []
def start_actors(self, n):
self.handles.extend([Actor.remote() for _ in range(n)])
def get_children(self):
return self.handles
def ping(self):
pass
main_actor = Actor.options(lifetime="detached", name="main").remote()
ray.get(main_actor.ping.remote())
ray.shutdown()
ray.init(address=cluster.address, namespace="default_test_namespace")
main_actor = ray.get_actor("main")
num_to_start = int(ray.available_resources().get("CPU", 0) + 1)
print(f"Starting {num_to_start} actors")
ray.get(main_actor.start_actors.remote(num_to_start))
actor_handles = ray.get(main_actor.get_children.remote())
up, down = ray.wait(
[actor.ping.remote() for actor in actor_handles],
timeout=5,
num_returns=len(actor_handles),
)
assert len(up) == len(actor_handles) - 1
assert len(down) == 1
cluster.add_node(num_cpus=1)
cluster.wait_for_nodes(3)
up, down = ray.wait(
[actor.ping.remote() for actor in actor_handles],
timeout=5,
num_returns=len(actor_handles),
)
assert len(up) == len(actor_handles)
assert len(down) == 0
def test_multi_node_pgs(ray_start_cluster):
cluster = ray_start_cluster
cluster.add_node(num_cpus=2)
cluster.wait_for_nodes(2)
ray.init(address=cluster.address)
pgs = [ray.util.placement_group([{"CPU": 1}]) for _ in range(4)]
ready, not_ready = ray.wait([pg.ready() for pg in pgs], timeout=5, num_returns=4)
assert len(ready) == 2
assert len(not_ready) == 2
cluster.add_node(num_cpus=2)
cluster.wait_for_nodes(3)
ready, not_ready = ray.wait([pg.ready() for pg in pgs], timeout=5, num_returns=4)
assert len(ready) == 4
assert len(not_ready) == 0
for i in range(4, 10):
cluster.add_node(num_cpus=2)
cluster.wait_for_nodes(i)
print(".")
more_pgs = [ray.util.placement_group([{"CPU": 1}]) for _ in range(2)]
ready, not_ready = ray.wait(
[pg.ready() for pg in more_pgs], timeout=5, num_returns=2
)
assert len(ready) == 2
if __name__ == "__main__":
sys.exit(pytest.main(["-sv", __file__]))