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

460 lines
14 KiB
Python

import os
import sys
import time
import pytest
import ray
import ray.cluster_utils
from ray._common.test_utils import wait_for_condition
from ray._private.runtime_env.context import RuntimeEnvContext
from ray._private.runtime_env.plugin import RuntimeEnvPlugin
from ray._private.test_utils import (
get_other_nodes,
is_placement_group_removed,
placement_group_assert_no_leak,
)
from ray._raylet import PlacementGroupID
from ray.util.placement_group import PlacementGroup
from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy
MOCK_WORKER_STARTUP_SLOWLY_PLUGIN_CLASS_PATH = (
"ray.tests.test_placement_group_4.MockWorkerStartupSlowlyPlugin" # noqa
)
MOCK_WORKER_STARTUP_SLOWLY_PLUGIN_NAME = "MockWorkerStartupSlowlyPlugin"
class MockWorkerStartupSlowlyPlugin(RuntimeEnvPlugin):
name = MOCK_WORKER_STARTUP_SLOWLY_PLUGIN_NAME
def validate(runtime_env_dict: dict) -> str:
return "success"
@staticmethod
def create(uri: str, runtime_env_dict: dict, ctx: RuntimeEnvContext) -> float:
time.sleep(60)
return 0
def test_remove_placement_group(ray_start_cluster):
cluster = ray_start_cluster
cluster.add_node(num_cpus=4)
ray.init(address=cluster.address)
@ray.remote
def warmup():
pass
# warm up the cluster.
ray.get([warmup.remote() for _ in range(4)])
# First try to remove a placement group that doesn't
# exist. This should not do anything.
random_group_id = PlacementGroupID.from_random()
random_placement_group = PlacementGroup(random_group_id)
for _ in range(3):
ray.util.remove_placement_group(random_placement_group)
# Creating a placement group as soon as it is
# created should work.
placement_group = ray.util.placement_group([{"CPU": 2}, {"CPU": 2}])
assert placement_group.wait(10)
ray.util.remove_placement_group(placement_group)
wait_for_condition(lambda: is_placement_group_removed(placement_group))
# # Now let's create a placement group.
placement_group = ray.util.placement_group([{"CPU": 2}, {"CPU": 2}])
assert placement_group.wait(10)
# Create an actor that occupies resources.
@ray.remote(num_cpus=2)
class A:
def f(self):
return 3
# Currently, there's no way to prevent
# tasks to be retried for removed placement group.
# Set max_retries=0 for testing.
# TODO(sang): Handle this edge case.
@ray.remote(num_cpus=2, max_retries=0)
def long_running_task():
print(os.getpid())
time.sleep(50)
# Schedule a long running task and actor.
task_ref = long_running_task.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=placement_group
)
).remote()
a = A.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=placement_group
)
).remote()
assert ray.get(a.f.remote()) == 3
ray.util.remove_placement_group(placement_group)
# Subsequent remove request shouldn't do anything.
for _ in range(3):
ray.util.remove_placement_group(placement_group)
# Make sure placement group resources are
# released and we can schedule this task.
@ray.remote(num_cpus=4)
def f():
return 3
assert ray.get(f.remote()) == 3
# Since the placement group is removed,
# the actor should've been killed.
# That means this request should fail.
with pytest.raises(ray.exceptions.RayActorError, match="actor died"):
ray.get(a.f.remote(), timeout=3.0)
with pytest.raises(ray.exceptions.WorkerCrashedError):
ray.get(task_ref)
@pytest.mark.parametrize(
"set_runtime_env_plugins",
[
'[{"class":"' + MOCK_WORKER_STARTUP_SLOWLY_PLUGIN_CLASS_PATH + '"}]',
],
indirect=True,
)
def test_remove_placement_group_worker_startup_slowly(
set_runtime_env_plugins, ray_start_cluster
):
cluster = ray_start_cluster
cluster.add_node(num_cpus=4)
ray.init(address=cluster.address)
placement_group = ray.util.placement_group([{"CPU": 2}, {"CPU": 2}])
assert placement_group.wait(10)
@ray.remote(num_cpus=2)
class A:
def ready(self):
return "ok"
def hang(self):
time.sleep(60)
@ray.remote(num_cpus=2, max_retries=0)
def long_running_task():
time.sleep(60)
# Schedule a long-running task that uses
# runtime env to mock worker start up slowly.
task_ref = long_running_task.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=placement_group
),
runtime_env={MOCK_WORKER_STARTUP_SLOWLY_PLUGIN_NAME: {}},
).remote()
a = A.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=placement_group
)
).remote()
assert ray.get(a.ready.remote()) == "ok"
# Remove the PG, check that the actor and task are failed.
ray.util.remove_placement_group(placement_group)
with pytest.raises(ray.exceptions.RayActorError, match="actor died"):
ray.get(a.hang.remote(), timeout=10)
# The long-running task should still be in the state
# of leasing-worker bacause of the worker startup delay.
with pytest.raises(ray.exceptions.TaskPlacementGroupRemoved):
ray.get(task_ref)
def test_remove_pending_placement_group(ray_start_cluster):
cluster = ray_start_cluster
cluster.add_node(num_cpus=4)
ray.init(address=cluster.address)
# Create a placement group that cannot be scheduled now.
placement_group = ray.util.placement_group([{"GPU": 2}, {"CPU": 2}])
wait_for_condition(
lambda: (ray.util.placement_group_table(placement_group) or {}).get("state")
== "PENDING"
)
ray.util.remove_placement_group(placement_group)
wait_for_condition(lambda: is_placement_group_removed(placement_group))
@ray.remote(num_cpus=4)
def f():
return 3
# Make sure this task is still schedulable.
assert ray.get(f.remote()) == 3
placement_group_assert_no_leak([placement_group])
def test_placement_group_table(ray_start_cluster):
@ray.remote(num_cpus=2)
class Actor(object):
def __init__(self):
self.n = 0
def value(self):
return self.n
cluster = ray_start_cluster
num_nodes = 2
for _ in range(num_nodes):
cluster.add_node(num_cpus=4)
ray.init(address=cluster.address)
pgs_created = []
# Originally placement group creation should be pending because
# there are no resources.
name = "name"
strategy = "PACK"
bundles = [{"CPU": 2, "GPU": 1}, {"CPU": 2}]
placement_group = ray.util.placement_group(
name=name, strategy=strategy, bundles=bundles
)
pgs_created.append(placement_group)
result = ray.util.placement_group_table(placement_group)
assert result["name"] == name
assert result["strategy"] == strategy
for i in range(len(bundles)):
assert bundles[i] == result["bundles"][i]
assert result["state"] == "PENDING"
# Now the placement group should be scheduled.
cluster.add_node(num_cpus=5, num_gpus=1)
cluster.wait_for_nodes()
actor_1 = Actor.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=placement_group, placement_group_bundle_index=0
)
).remote()
ray.get(actor_1.value.remote())
result = ray.util.placement_group_table(placement_group)
assert result["state"] == "CREATED"
# Add tow more placement group for placement group table test.
second_strategy = "SPREAD"
pgs_created.append(
ray.util.placement_group(
name="second_placement_group", strategy=second_strategy, bundles=bundles
)
)
pgs_created.append(
ray.util.placement_group(
name="third_placement_group", strategy=second_strategy, bundles=bundles
)
)
placement_group_table = ray.util.placement_group_table()
assert len(placement_group_table) == 3
true_name_set = {"name", "second_placement_group", "third_placement_group"}
get_name_set = set()
for _, placement_group_data in placement_group_table.items():
get_name_set.add(placement_group_data["name"])
assert true_name_set == get_name_set
placement_group_assert_no_leak(pgs_created)
def test_placement_group_stats(ray_start_cluster):
cluster = ray_start_cluster
num_nodes = 1
for _ in range(num_nodes):
cluster.add_node(num_cpus=4, num_gpus=1)
ray.init(address=cluster.address)
# Test createable pgs.
pg = ray.util.placement_group(bundles=[{"CPU": 4, "GPU": 1}])
ray.get(pg.ready())
stats = ray.util.placement_group_table(pg)["stats"]
assert stats["scheduling_attempt"] == 1
assert stats["scheduling_state"] == "FINISHED"
assert stats["end_to_end_creation_latency_ms"] != 0
# Create a pending pg.
pg2 = ray.util.placement_group(bundles=[{"CPU": 4, "GPU": 1}])
def assert_scheduling_state():
stats = ray.util.placement_group_table(pg2)["stats"]
if stats["scheduling_attempt"] != 1:
return False
if stats["scheduling_state"] != "NO_RESOURCES":
return False
if stats["end_to_end_creation_latency_ms"] != 0:
return False
return True
wait_for_condition(assert_scheduling_state)
# Remove the first pg, and the second
# pg should be schedulable now.
ray.util.remove_placement_group(pg)
def assert_scheduling_state():
stats = ray.util.placement_group_table(pg2)["stats"]
if stats["scheduling_state"] != "FINISHED":
return False
if stats["end_to_end_creation_latency_ms"] == 0:
return False
return True
wait_for_condition(assert_scheduling_state)
# Infeasible pg.
pg3 = ray.util.placement_group(bundles=[{"CPU": 4, "a": 1}])
# TODO This is supposed to be infeasible, but it is printed
# as NO_RESOURCES. Fix the issue.
# def assert_scheduling_state():
# stats = ray.util.placement_group_table(pg3)["stats"]
# print(stats)
# if stats["scheduling_state"] != "INFEASIBLE":
# return False
# return True
# wait_for_condition(assert_scheduling_state)
ray.util.remove_placement_group(pg3)
def assert_scheduling_state():
stats = ray.util.placement_group_table(pg3)["stats"]
if stats["scheduling_state"] != "REMOVED":
return False
return True
wait_for_condition(assert_scheduling_state)
placement_group_assert_no_leak([pg2])
def test_cuda_visible_devices(ray_start_cluster):
@ray.remote(num_gpus=1)
def f():
return os.environ["CUDA_VISIBLE_DEVICES"]
cluster = ray_start_cluster
num_nodes = 1
for _ in range(num_nodes):
cluster.add_node(num_gpus=1)
ray.init(address=cluster.address)
g1 = ray.util.placement_group([{"CPU": 1, "GPU": 1}])
o1 = f.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(placement_group=g1)
).remote()
devices = ray.get(o1)
assert devices == "0", devices
placement_group_assert_no_leak([g1])
def test_placement_group_reschedule_when_node_dead(ray_start_cluster):
@ray.remote(num_cpus=1)
class Actor(object):
def __init__(self):
self.n = 0
def value(self):
return self.n
cluster = ray_start_cluster
cluster.add_node(num_cpus=4)
cluster.add_node(num_cpus=4)
cluster.add_node(num_cpus=4)
cluster.wait_for_nodes()
ray.init(address=cluster.address, namespace="default_test_namespace")
# Make sure both head and worker node are alive.
nodes = ray.nodes()
assert len(nodes) == 3
assert nodes[0]["alive"] and nodes[1]["alive"] and nodes[2]["alive"]
placement_group = ray.util.placement_group(
name="name", strategy="SPREAD", bundles=[{"CPU": 2}, {"CPU": 2}, {"CPU": 2}]
)
actor_1 = Actor.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=placement_group, placement_group_bundle_index=0
),
lifetime="detached",
).remote()
actor_2 = Actor.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=placement_group, placement_group_bundle_index=1
),
lifetime="detached",
).remote()
actor_3 = Actor.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=placement_group, placement_group_bundle_index=2
),
lifetime="detached",
).remote()
ray.get(actor_1.value.remote())
ray.get(actor_2.value.remote())
ray.get(actor_3.value.remote())
cluster.remove_node(get_other_nodes(cluster, exclude_head=True)[-1])
cluster.wait_for_nodes()
actor_4 = Actor.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=placement_group, placement_group_bundle_index=0
),
lifetime="detached",
).remote()
actor_5 = Actor.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=placement_group, placement_group_bundle_index=1
),
lifetime="detached",
).remote()
actor_6 = Actor.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=placement_group, placement_group_bundle_index=2
),
lifetime="detached",
).remote()
ray.get(actor_4.value.remote())
ray.get(actor_5.value.remote())
ray.get(actor_6.value.remote())
placement_group_assert_no_leak([placement_group])
def test_infeasible_pg(ray_start_cluster):
"""Test infeasible pgs are scheduled after new nodes are added."""
cluster = ray_start_cluster
cluster.add_node(num_cpus=2)
ray.init("auto")
bundle = {"CPU": 4, "GPU": 1}
pg = ray.util.placement_group([bundle], name="worker_1", strategy="STRICT_PACK")
# Placement group is infeasible.
with pytest.raises(ray.exceptions.GetTimeoutError):
ray.get(pg.ready(), timeout=3)
state = ray.util.placement_group_table()[pg.id.hex()]["stats"]["scheduling_state"]
assert state == "INFEASIBLE"
# Add a new node. PG can now be scheduled.
cluster.add_node(num_cpus=4, num_gpus=1)
assert ray.get(pg.ready(), timeout=10)
if __name__ == "__main__":
sys.exit(pytest.main(["-sv", __file__]))