788 lines
24 KiB
Python
788 lines
24 KiB
Python
import os
|
|
import sys
|
|
import time
|
|
from typing import List
|
|
|
|
import pytest
|
|
|
|
import ray
|
|
import ray.cluster_utils
|
|
import ray.experimental.internal_kv as internal_kv
|
|
from ray import ObjectRef
|
|
from ray._common.test_utils import (
|
|
run_string_as_driver,
|
|
wait_for_condition,
|
|
)
|
|
from ray._private.ray_constants import (
|
|
DEBUG_AUTOSCALING_ERROR,
|
|
DEBUG_AUTOSCALING_STATUS,
|
|
)
|
|
from ray._private.test_utils import (
|
|
generate_system_config_map,
|
|
is_placement_group_removed,
|
|
kill_actor_and_wait_for_failure,
|
|
reset_autoscaler_v2_enabled_cache,
|
|
)
|
|
from ray.autoscaler._private.commands import debug_status
|
|
from ray.autoscaler._private.constants import AUTOSCALER_UPDATE_INTERVAL_S
|
|
from ray.exceptions import RaySystemError
|
|
from ray.util.placement_group import placement_group, remove_placement_group
|
|
from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy
|
|
|
|
|
|
def _get_status_section(
|
|
status_output: str, headers: List[str], next_headers: List[str]
|
|
) -> str:
|
|
lines = status_output.splitlines()
|
|
start_index = None
|
|
header_set = set(headers)
|
|
next_header_set = set(next_headers)
|
|
|
|
for index, line in enumerate(lines):
|
|
if line.strip() in header_set:
|
|
start_index = index + 1
|
|
break
|
|
|
|
if start_index is None:
|
|
return ""
|
|
|
|
section_lines = []
|
|
for line in lines[start_index:]:
|
|
if line.strip() in next_header_set:
|
|
break
|
|
if not section_lines and not line.strip():
|
|
continue
|
|
section_lines.append(line.rstrip())
|
|
|
|
return "\n".join(section_lines).strip()
|
|
|
|
|
|
def get_ray_status_output(address):
|
|
gcs_client = ray._raylet.GcsClient(address=address)
|
|
internal_kv._initialize_internal_kv(gcs_client)
|
|
status = internal_kv._internal_kv_get(DEBUG_AUTOSCALING_STATUS)
|
|
error = internal_kv._internal_kv_get(DEBUG_AUTOSCALING_ERROR)
|
|
status_output = debug_status(status, error, address=address)
|
|
print(status_output)
|
|
return {
|
|
"demand": _get_status_section(
|
|
status_output,
|
|
headers=["Demands:", "Pending Demands:"],
|
|
next_headers=[],
|
|
),
|
|
"usage": _get_status_section(
|
|
status_output,
|
|
headers=["Usage:", "Total Usage:"],
|
|
next_headers=["Demands:", "Pending Demands:"],
|
|
),
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"ray_start_cluster_head_with_external_redis",
|
|
[
|
|
generate_system_config_map(
|
|
health_check_initial_delay_ms=0,
|
|
health_check_failure_threshold=10,
|
|
)
|
|
],
|
|
indirect=True,
|
|
)
|
|
def test_create_placement_group_during_gcs_server_restart(
|
|
ray_start_cluster_head_with_external_redis,
|
|
):
|
|
cluster = ray_start_cluster_head_with_external_redis
|
|
cluster.add_node(num_cpus=200)
|
|
cluster.wait_for_nodes()
|
|
|
|
# Create placement groups during gcs server restart.
|
|
placement_groups = []
|
|
for i in range(0, 100):
|
|
placement_group = ray.util.placement_group([{"CPU": 1}, {"CPU": 1}])
|
|
placement_groups.append(placement_group)
|
|
|
|
cluster.head_node.kill_gcs_server()
|
|
cluster.head_node.start_gcs_server()
|
|
|
|
for i in range(0, 100):
|
|
ray.get(placement_groups[i].ready())
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"ray_start_cluster_head_with_external_redis",
|
|
[
|
|
generate_system_config_map(
|
|
health_check_initial_delay_ms=0,
|
|
health_check_failure_threshold=10,
|
|
)
|
|
],
|
|
indirect=True,
|
|
)
|
|
def test_placement_group_wait_api(ray_start_cluster_head_with_external_redis):
|
|
cluster = ray_start_cluster_head_with_external_redis
|
|
cluster.add_node(num_cpus=2)
|
|
cluster.add_node(num_cpus=2)
|
|
cluster.wait_for_nodes()
|
|
|
|
# Create placement group 1 successfully.
|
|
placement_group1 = ray.util.placement_group([{"CPU": 1}, {"CPU": 1}])
|
|
assert placement_group1.wait(10)
|
|
|
|
# Restart gcs server.
|
|
cluster.head_node.kill_gcs_server()
|
|
cluster.head_node.start_gcs_server()
|
|
|
|
# Create placement group 2 successfully.
|
|
placement_group2 = ray.util.placement_group([{"CPU": 1}, {"CPU": 1}])
|
|
assert placement_group2.wait(10)
|
|
|
|
# Remove placement group 1.
|
|
ray.util.remove_placement_group(placement_group1)
|
|
|
|
# Wait for placement group 1 after it is removed.
|
|
with pytest.raises(Exception):
|
|
placement_group1.wait(10)
|
|
|
|
|
|
def test_placement_group_wait_api_timeout(shutdown_only):
|
|
"""Make sure the wait API timeout works
|
|
|
|
https://github.com/ray-project/ray/issues/27287
|
|
"""
|
|
ray.init(num_cpus=1)
|
|
pg = ray.util.placement_group(bundles=[{"CPU": 2}])
|
|
start = time.time()
|
|
assert not pg.wait(5)
|
|
assert 5 <= time.time() - start
|
|
|
|
|
|
def test_schedule_placement_groups_at_the_same_time(shutdown_only):
|
|
ray.init(num_cpus=4)
|
|
|
|
pgs = [placement_group([{"CPU": 2}]) for _ in range(6)]
|
|
|
|
wait_pgs = {pg.ready(): pg for pg in pgs}
|
|
|
|
def is_all_placement_group_removed():
|
|
ready, _ = ray.wait(list(wait_pgs.keys()), timeout=0.5)
|
|
if ready:
|
|
ready_pg = wait_pgs[ready[0]]
|
|
remove_placement_group(ready_pg)
|
|
del wait_pgs[ready[0]]
|
|
|
|
if len(wait_pgs) == 0:
|
|
return True
|
|
return False
|
|
|
|
wait_for_condition(is_all_placement_group_removed)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"ray_start_cluster",
|
|
[
|
|
{
|
|
"include_dashboard": True,
|
|
}
|
|
],
|
|
indirect=True,
|
|
)
|
|
def test_detached_placement_group(ray_start_cluster):
|
|
cluster = ray_start_cluster
|
|
for _ in range(2):
|
|
cluster.add_node(num_cpus=3)
|
|
cluster.wait_for_nodes()
|
|
info = ray.init(address=cluster.address)
|
|
|
|
# Make sure detached placement group will alive when job dead.
|
|
driver_code = f"""
|
|
import ray
|
|
from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy
|
|
|
|
ray.init(address="{info["address"]}")
|
|
|
|
pg = ray.util.placement_group(
|
|
[{{"CPU": 1}} for _ in range(2)],
|
|
strategy="STRICT_SPREAD", lifetime="detached")
|
|
ray.get(pg.ready())
|
|
|
|
@ray.remote(num_cpus=1)
|
|
class Actor:
|
|
def ready(self):
|
|
return True
|
|
|
|
for bundle_index in range(2):
|
|
actor = Actor.options(lifetime="detached",
|
|
scheduling_strategy=PlacementGroupSchedulingStrategy(placement_group=pg,
|
|
placement_group_bundle_index=bundle_index)).remote()
|
|
ray.get(actor.ready.remote())
|
|
|
|
ray.shutdown()
|
|
"""
|
|
|
|
run_string_as_driver(driver_code)
|
|
|
|
# Wait until the driver is reported as dead by GCS.
|
|
def is_job_done():
|
|
jobs = ray._private.state.jobs()
|
|
for job in jobs:
|
|
if job["IsDead"]:
|
|
return True
|
|
return False
|
|
|
|
def assert_alive_num_pg(expected_num_pg):
|
|
alive_num_pg = 0
|
|
for _, placement_group_info in ray.util.placement_group_table().items():
|
|
if placement_group_info["state"] == "CREATED":
|
|
alive_num_pg += 1
|
|
return alive_num_pg == expected_num_pg
|
|
|
|
def assert_alive_num_actor(expected_num_actor):
|
|
alive_num_actor = 0
|
|
for actor_info in ray.util.state.list_actors():
|
|
if actor_info.state == "ALIVE":
|
|
alive_num_actor += 1
|
|
return alive_num_actor == expected_num_actor
|
|
|
|
wait_for_condition(is_job_done)
|
|
|
|
assert assert_alive_num_pg(1)
|
|
assert assert_alive_num_actor(2)
|
|
|
|
# Make sure detached placement group will alive when its creator which
|
|
# is detached actor dead.
|
|
# Test actors first.
|
|
@ray.remote(num_cpus=1)
|
|
class NestedActor:
|
|
def ready(self):
|
|
return True
|
|
|
|
@ray.remote(num_cpus=1)
|
|
class Actor:
|
|
def __init__(self):
|
|
self.actors = []
|
|
|
|
def ready(self):
|
|
return True
|
|
|
|
def schedule_nested_actor_with_detached_pg(self):
|
|
# Create placement group which is detached.
|
|
pg = ray.util.placement_group(
|
|
[{"CPU": 1} for _ in range(2)],
|
|
strategy="STRICT_SPREAD",
|
|
lifetime="detached",
|
|
name="detached_pg",
|
|
)
|
|
ray.get(pg.ready())
|
|
# Schedule nested actor with the placement group.
|
|
for bundle_index in range(2):
|
|
actor = NestedActor.options(
|
|
scheduling_strategy=PlacementGroupSchedulingStrategy(
|
|
placement_group=pg, placement_group_bundle_index=bundle_index
|
|
),
|
|
lifetime="detached",
|
|
).remote()
|
|
ray.get(actor.ready.remote())
|
|
self.actors.append(actor)
|
|
|
|
a = Actor.options(lifetime="detached").remote()
|
|
ray.get(a.ready.remote())
|
|
# 1 parent actor and 2 children actor.
|
|
ray.get(a.schedule_nested_actor_with_detached_pg.remote())
|
|
|
|
# Kill an actor and wait until it is killed.
|
|
kill_actor_and_wait_for_failure(a)
|
|
with pytest.raises(ray.exceptions.RayActorError):
|
|
ray.get(a.ready.remote())
|
|
|
|
# We should have 2 alive pgs and 4 alive actors.
|
|
assert assert_alive_num_pg(2)
|
|
assert assert_alive_num_actor(4)
|
|
|
|
|
|
def test_named_placement_group(ray_start_cluster):
|
|
cluster = ray_start_cluster
|
|
for _ in range(2):
|
|
cluster.add_node(num_cpus=3)
|
|
cluster.wait_for_nodes()
|
|
info = ray.init(address=cluster.address, namespace="default_test_namespace")
|
|
global_placement_group_name = "named_placement_group"
|
|
|
|
# Create a detached placement group with name.
|
|
driver_code = f"""
|
|
import ray
|
|
|
|
ray.init(address="{info["address"]}", namespace="default_test_namespace")
|
|
|
|
pg = ray.util.placement_group(
|
|
[{{"CPU": 1}} for _ in range(2)],
|
|
strategy="STRICT_SPREAD",
|
|
name="{global_placement_group_name}",
|
|
lifetime="detached")
|
|
ray.get(pg.ready())
|
|
|
|
ray.shutdown()
|
|
"""
|
|
|
|
run_string_as_driver(driver_code)
|
|
|
|
# Wait until the driver is reported as dead by GCS.
|
|
def is_job_done():
|
|
jobs = ray._private.state.jobs()
|
|
for job in jobs:
|
|
if job["IsDead"]:
|
|
return True
|
|
return False
|
|
|
|
wait_for_condition(is_job_done)
|
|
|
|
@ray.remote(num_cpus=1)
|
|
class Actor:
|
|
def ping(self):
|
|
return "pong"
|
|
|
|
# Get the named placement group and schedule a actor.
|
|
placement_group = ray.util.get_placement_group(global_placement_group_name)
|
|
assert placement_group is not None
|
|
assert placement_group.wait(5)
|
|
actor = Actor.options(
|
|
scheduling_strategy=PlacementGroupSchedulingStrategy(
|
|
placement_group=placement_group, placement_group_bundle_index=0
|
|
)
|
|
).remote()
|
|
|
|
ray.get(actor.ping.remote())
|
|
|
|
# Create another placement group and make sure its creation will failed.
|
|
error_creation_count = 0
|
|
try:
|
|
ray.util.placement_group(
|
|
[{"CPU": 1} for _ in range(2)],
|
|
strategy="STRICT_SPREAD",
|
|
name=global_placement_group_name,
|
|
)
|
|
except RaySystemError:
|
|
error_creation_count += 1
|
|
assert error_creation_count == 1
|
|
|
|
# Remove a named placement group and make sure the second creation
|
|
# will successful.
|
|
ray.util.remove_placement_group(placement_group)
|
|
same_name_pg = ray.util.placement_group(
|
|
[{"CPU": 1} for _ in range(2)],
|
|
strategy="STRICT_SPREAD",
|
|
name=global_placement_group_name,
|
|
)
|
|
assert same_name_pg.wait(10)
|
|
|
|
# Get a named placement group with a name that doesn't exist
|
|
# and make sure it will raise ValueError correctly.
|
|
error_count = 0
|
|
try:
|
|
ray.util.get_placement_group("inexistent_pg")
|
|
except ValueError:
|
|
error_count = error_count + 1
|
|
assert error_count == 1
|
|
|
|
|
|
def test_placement_group_synchronous_registration(ray_start_cluster):
|
|
cluster = ray_start_cluster
|
|
# One node which only has one CPU.
|
|
cluster.add_node(num_cpus=1)
|
|
cluster.wait_for_nodes()
|
|
ray.init(address=cluster.address)
|
|
|
|
# Create a placement group that has two bundles and `STRICT_PACK`
|
|
# strategy so its registration will successful but scheduling failed.
|
|
placement_group = ray.util.placement_group(
|
|
name="name",
|
|
strategy="STRICT_PACK",
|
|
bundles=[
|
|
{
|
|
"CPU": 1,
|
|
},
|
|
{"CPU": 1},
|
|
],
|
|
)
|
|
# Make sure we can properly remove it immediately
|
|
# as its registration is synchronous.
|
|
ray.util.remove_placement_group(placement_group)
|
|
|
|
wait_for_condition(lambda: is_placement_group_removed(placement_group))
|
|
|
|
|
|
def test_placement_group_gpu_set(ray_start_cluster):
|
|
cluster = ray_start_cluster
|
|
# One node which only has one CPU.
|
|
cluster.add_node(num_cpus=1, num_gpus=1)
|
|
cluster.add_node(num_cpus=1, num_gpus=1)
|
|
cluster.wait_for_nodes()
|
|
ray.init(address=cluster.address)
|
|
|
|
placement_group = ray.util.placement_group(
|
|
name="name",
|
|
strategy="PACK",
|
|
bundles=[{"CPU": 1, "GPU": 1}, {"CPU": 1, "GPU": 1}],
|
|
)
|
|
|
|
@ray.remote(num_gpus=1)
|
|
def get_gpus():
|
|
return ray.get_gpu_ids()
|
|
|
|
result = get_gpus.options(
|
|
scheduling_strategy=PlacementGroupSchedulingStrategy(
|
|
placement_group=placement_group, placement_group_bundle_index=0
|
|
)
|
|
).remote()
|
|
result = ray.get(result)
|
|
assert result == [0]
|
|
|
|
result = get_gpus.options(
|
|
scheduling_strategy=PlacementGroupSchedulingStrategy(
|
|
placement_group=placement_group, placement_group_bundle_index=1
|
|
)
|
|
).remote()
|
|
result = ray.get(result)
|
|
assert result == [0]
|
|
|
|
|
|
def test_placement_group_gpu_assigned(ray_start_cluster):
|
|
cluster = ray_start_cluster
|
|
cluster.add_node(num_gpus=2)
|
|
ray.init(address=cluster.address)
|
|
gpu_ids_res = set()
|
|
|
|
@ray.remote(num_gpus=1, num_cpus=0)
|
|
def f():
|
|
return os.environ["CUDA_VISIBLE_DEVICES"]
|
|
|
|
pg1 = ray.util.placement_group([{"GPU": 1}])
|
|
pg2 = ray.util.placement_group([{"GPU": 1}])
|
|
|
|
assert pg1.wait(10)
|
|
assert pg2.wait(10)
|
|
|
|
gpu_ids_res.add(
|
|
ray.get(
|
|
f.options(
|
|
scheduling_strategy=PlacementGroupSchedulingStrategy(
|
|
placement_group=pg1
|
|
)
|
|
).remote()
|
|
)
|
|
)
|
|
gpu_ids_res.add(
|
|
ray.get(
|
|
f.options(
|
|
scheduling_strategy=PlacementGroupSchedulingStrategy(
|
|
placement_group=pg2
|
|
)
|
|
).remote()
|
|
)
|
|
)
|
|
|
|
assert len(gpu_ids_res) == 2
|
|
|
|
|
|
def test_incremental_pg_and_actor_scheduling(ray_start_cluster):
|
|
"""Tests that actors in pending PGs are scheduled as resources become available.
|
|
|
|
For more detailed information please refer to:
|
|
https://github.com/ray-project/ray/issues/15801.
|
|
"""
|
|
cluster = ray_start_cluster
|
|
cluster.add_node(num_cpus=0)
|
|
ray.init(address=cluster.address)
|
|
|
|
@ray.remote(num_cpus=1)
|
|
class A:
|
|
def ready(self):
|
|
pass
|
|
|
|
# Schedule a large number of placement groups and actors that should be placed in
|
|
# those groups. Initially, none are schedulable.
|
|
pgs = [ray.util.placement_group([{"CPU": 1}]) for _ in range(1000)]
|
|
pg_refs = [pg.ready() for pg in pgs]
|
|
actors = [
|
|
A.options(
|
|
scheduling_strategy=PlacementGroupSchedulingStrategy(placement_group=pg)
|
|
).remote()
|
|
for pg in pgs
|
|
]
|
|
actor_refs = [actor.ready.remote() for actor in actors]
|
|
|
|
ready_pgs, _ = ray.wait(pg_refs, timeout=0.1)
|
|
assert len(ready_pgs) == 0
|
|
ready_actors, _ = ray.wait(actor_refs, timeout=0.1)
|
|
assert len(ready_actors) == 0
|
|
|
|
def check_num_refs_ready(refs: List[ObjectRef], expected: int) -> bool:
|
|
ready, _ = ray.wait(refs, num_returns=expected, timeout=1)
|
|
return len(ready) == expected
|
|
|
|
# Iteratively add nodes to the cluster so that some of the placement groups (and
|
|
# therefore actors) can be scheduled. Verify that the PGs and actors are scheduled
|
|
# incrementally as their required resources become available.
|
|
for i in range(5):
|
|
cluster.add_node(num_cpus=1)
|
|
wait_for_condition(lambda: check_num_refs_ready(pg_refs, i + 1), timeout=30)
|
|
wait_for_condition(lambda: check_num_refs_ready(actor_refs, i + 1), timeout=30)
|
|
|
|
|
|
def test_placement_group_gpu_unique_assigned(ray_start_cluster):
|
|
cluster = ray_start_cluster
|
|
cluster.add_node(num_gpus=4, num_cpus=4)
|
|
ray.init(address=cluster.address)
|
|
gpu_ids_res = set()
|
|
|
|
# Create placement group with 4 bundles using 1 GPU each.
|
|
num_gpus = 4
|
|
bundles = [{"GPU": 1, "CPU": 1} for _ in range(num_gpus)]
|
|
pg = placement_group(bundles)
|
|
ray.get(pg.ready())
|
|
|
|
# Actor using 1 GPU that has a method to get
|
|
# $CUDA_VISIBLE_DEVICES env variable.
|
|
@ray.remote(num_gpus=1, num_cpus=1)
|
|
class Actor:
|
|
def get_gpu(self):
|
|
import os
|
|
|
|
return os.environ["CUDA_VISIBLE_DEVICES"]
|
|
|
|
# Create actors out of order.
|
|
actors = []
|
|
actors.append(
|
|
Actor.options(
|
|
scheduling_strategy=PlacementGroupSchedulingStrategy(
|
|
placement_group=pg, placement_group_bundle_index=0
|
|
)
|
|
).remote()
|
|
)
|
|
actors.append(
|
|
Actor.options(
|
|
scheduling_strategy=PlacementGroupSchedulingStrategy(
|
|
placement_group=pg, placement_group_bundle_index=3
|
|
)
|
|
).remote()
|
|
)
|
|
actors.append(
|
|
Actor.options(
|
|
scheduling_strategy=PlacementGroupSchedulingStrategy(
|
|
placement_group=pg, placement_group_bundle_index=2
|
|
)
|
|
).remote()
|
|
)
|
|
actors.append(
|
|
Actor.options(
|
|
scheduling_strategy=PlacementGroupSchedulingStrategy(
|
|
placement_group=pg, placement_group_bundle_index=1
|
|
)
|
|
).remote()
|
|
)
|
|
|
|
for actor in actors:
|
|
gpu_ids = ray.get(actor.get_gpu.remote())
|
|
assert len(gpu_ids) == 1
|
|
gpu_ids_res.add(gpu_ids)
|
|
|
|
assert len(gpu_ids_res) == 4
|
|
|
|
|
|
@pytest.mark.parametrize("enable_v2", [True, False])
|
|
def test_placement_group_status_no_bundle_demand(ray_start_cluster, enable_v2):
|
|
reset_autoscaler_v2_enabled_cache()
|
|
cluster = ray_start_cluster
|
|
cluster.add_node(num_cpus=4, _system_config={"enable_autoscaler_v2": enable_v2})
|
|
ray.init(address=cluster.address)
|
|
|
|
@ray.remote
|
|
def f():
|
|
pass
|
|
|
|
pg = ray.util.placement_group([{"CPU": 1}])
|
|
ray.get(pg.ready())
|
|
ray.util.remove_placement_group(pg)
|
|
wait_for_condition(lambda: is_placement_group_removed(pg))
|
|
# Create a ready task after the placement group is removed.
|
|
# This shouldn't be reported to the resource demand.
|
|
r = pg.ready() # noqa
|
|
|
|
# Wait until the usage is updated, which is
|
|
# when the demand is also updated.
|
|
def is_usage_updated():
|
|
demand_output = get_ray_status_output(cluster.address)
|
|
return demand_output["usage"] != ""
|
|
|
|
wait_for_condition(is_usage_updated)
|
|
# The output shouldn't include the pg.ready task demand.
|
|
demand_output = get_ray_status_output(cluster.address)
|
|
assert demand_output["demand"] == "(no resource demands)"
|
|
|
|
|
|
@pytest.mark.parametrize("enable_v2", [True, False])
|
|
def test_placement_group_status(ray_start_cluster, enable_v2):
|
|
cluster = ray_start_cluster
|
|
cluster.add_node(num_cpus=4, _system_config={"enable_autoscaler_v2": enable_v2})
|
|
ray.init(cluster.address)
|
|
|
|
@ray.remote(num_cpus=1)
|
|
class A:
|
|
def ready(self):
|
|
pass
|
|
|
|
pg = ray.util.placement_group([{"CPU": 1}])
|
|
ray.get(pg.ready())
|
|
|
|
# Wait until the usage is updated to the expected, which is
|
|
# when the demand is also updated.
|
|
def is_usage_updated():
|
|
demand_output = get_ray_status_output(cluster.address)
|
|
cpu_usage = demand_output["usage"]
|
|
if cpu_usage == "":
|
|
return False
|
|
cpu_usage = cpu_usage.split("\n")[0]
|
|
expected = "0.0/4.0 CPU (0.0 used of 1.0 reserved in placement groups)"
|
|
if cpu_usage != expected:
|
|
assert cpu_usage == "0.0/4.0 CPU"
|
|
return False
|
|
return True
|
|
|
|
wait_for_condition(
|
|
is_usage_updated,
|
|
timeout=3 * AUTOSCALER_UPDATE_INTERVAL_S,
|
|
retry_interval_ms=1000,
|
|
)
|
|
|
|
# 2 CPU + 1 PG CPU == 3.0/4.0 CPU (1 used by pg)
|
|
actors = [A.remote() for _ in range(2)]
|
|
actors_in_pg = [
|
|
A.options(
|
|
scheduling_strategy=PlacementGroupSchedulingStrategy(placement_group=pg)
|
|
).remote()
|
|
for _ in range(1)
|
|
]
|
|
|
|
ray.get([actor.ready.remote() for actor in actors])
|
|
ray.get([actor.ready.remote() for actor in actors_in_pg])
|
|
|
|
def is_pg_usage_propagated():
|
|
demand_output = get_ray_status_output(cluster.address)
|
|
cpu_usage = demand_output["usage"].split("\n")[0]
|
|
return cpu_usage == "3.0/4.0 CPU (1.0 used of 1.0 reserved in placement groups)"
|
|
|
|
wait_for_condition(
|
|
is_pg_usage_propagated,
|
|
timeout=3 * AUTOSCALER_UPDATE_INTERVAL_S,
|
|
retry_interval_ms=1000,
|
|
)
|
|
|
|
|
|
def test_placement_group_removal_leak_regression(ray_start_cluster):
|
|
"""Related issue:
|
|
https://github.com/ray-project/ray/issues/19131
|
|
"""
|
|
cluster = ray_start_cluster
|
|
cluster.add_node(num_cpus=5)
|
|
ray.init(address=cluster.address)
|
|
|
|
TOTAL_CPUS = 8
|
|
bundles = [{"CPU": 1, "GPU": 1}]
|
|
bundles += [{"CPU": 1} for _ in range(TOTAL_CPUS - 1)]
|
|
|
|
pg = placement_group(bundles, strategy="PACK")
|
|
# Here, we simulate that the ready task is queued and
|
|
# the new node is up. As soon as the new node is up,
|
|
# the ready task is scheduled.
|
|
# See https://github.com/ray-project/ray/pull/19138
|
|
# for more details about the test.
|
|
o = pg.ready()
|
|
# Add an artificial delay until the new node is up.
|
|
time.sleep(3)
|
|
cluster.add_node(num_cpus=5, num_gpus=1)
|
|
ray.get(o)
|
|
bundle_resource_name = f"bundle_group_{pg.id.hex()}"
|
|
expected_bundle_wildcard_val = TOTAL_CPUS * 1000
|
|
|
|
# This should fail if there's a leakage
|
|
# because the bundle resources are never returned properly.
|
|
def check_bundle_leaks():
|
|
bundle_resources = ray.available_resources()[bundle_resource_name]
|
|
return expected_bundle_wildcard_val == bundle_resources
|
|
|
|
wait_for_condition(check_bundle_leaks)
|
|
|
|
|
|
def test_placement_group_local_resource_view(monkeypatch, ray_start_cluster):
|
|
"""Please refer to https://github.com/ray-project/ray/pull/19911
|
|
for more details.
|
|
"""
|
|
with monkeypatch.context() as m:
|
|
# Increase broadcasting interval so that node resource will arrive
|
|
# at raylet after local resource all being allocated.
|
|
m.setenv("RAY_raylet_report_resources_period_milliseconds", "2000")
|
|
cluster = ray_start_cluster
|
|
|
|
cluster.add_node(num_cpus=16, object_store_memory=1e9)
|
|
cluster.wait_for_nodes()
|
|
# We need to init here so that we can make sure it's connecting to
|
|
# the raylet where it only has cpu resources.
|
|
# This is a hacky way to prevent scheduling hanging which will
|
|
# schedule <CPU:1> job to the node with GPU and for <GPU:1, CPU:1> task
|
|
# there is no node has this resource.
|
|
ray.init(address="auto")
|
|
cluster.add_node(num_cpus=16, num_gpus=1)
|
|
cluster.wait_for_nodes()
|
|
NUM_CPU_BUNDLES = 30
|
|
|
|
@ray.remote(num_cpus=1)
|
|
class Worker(object):
|
|
def __init__(self, i):
|
|
self.i = i
|
|
|
|
def work(self):
|
|
time.sleep(0.1)
|
|
print("work ", self.i)
|
|
|
|
@ray.remote(num_cpus=1, num_gpus=1)
|
|
class Trainer(object):
|
|
def __init__(self, i):
|
|
self.i = i
|
|
|
|
def train(self):
|
|
time.sleep(0.2)
|
|
print("train ", self.i)
|
|
|
|
bundles = [{"CPU": 1, "GPU": 1}]
|
|
bundles += [{"CPU": 1} for _ in range(NUM_CPU_BUNDLES)]
|
|
pg = placement_group(bundles, strategy="PACK")
|
|
ray.get(pg.ready())
|
|
|
|
# Local resource will be allocated and here we are to ensure
|
|
# local view is consistent and node resouce updates are discarded
|
|
workers = [
|
|
Worker.options(
|
|
scheduling_strategy=PlacementGroupSchedulingStrategy(placement_group=pg)
|
|
).remote(i)
|
|
for i in range(NUM_CPU_BUNDLES)
|
|
]
|
|
trainer = Trainer.options(
|
|
scheduling_strategy=PlacementGroupSchedulingStrategy(placement_group=pg)
|
|
).remote(0)
|
|
ray.get([workers[i].work.remote() for i in range(NUM_CPU_BUNDLES)])
|
|
ray.get(trainer.train.remote())
|
|
|
|
|
|
def test_fractional_resources_handle_correct(ray_start_cluster):
|
|
cluster = ray_start_cluster
|
|
cluster.add_node(num_cpus=1000)
|
|
ray.init(address=cluster.address)
|
|
|
|
bundles = [{"CPU": 0.01} for _ in range(5)]
|
|
pg = placement_group(bundles, strategy="SPREAD")
|
|
|
|
ray.get(pg.ready(), timeout=10)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(pytest.main(["-sv", __file__]))
|