Files
2026-07-13 13:17:40 +08:00

803 lines
24 KiB
Python

import os
import sys
import warnings
import pytest
import ray
from ray._private.test_utils import placement_group_assert_no_leak
from ray._private.utils import get_ray_doc_version
from ray.util.placement_group import (
NODE_ID_LABEL_KEY,
VALID_PLACEMENT_GROUP_STRATEGIES,
_validate_bundle_label_selector,
_validate_bundles,
validate_placement_group,
)
from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy
def are_pairwise_unique(g):
s = set()
for x in g:
if x in s:
return False
s.add(x)
return True
def test_placement_ready(ray_start_regular):
@ray.remote
class Actor:
def __init__(self):
pass
def v(self):
return 10
# kBundle_ResourceLabel is placement group reserved resources and
# can't be used in bundles
with pytest.raises(Exception):
ray.util.placement_group(bundles=[{"bundle": 1}])
# This test is to test the case that even there all resource in the
# bundle got allocated, we are still able to return from ready[I
# since ready use 0 CPU
pg = ray.util.placement_group(bundles=[{"CPU": 1}])
ray.get(pg.ready())
a = Actor.options(
num_cpus=1,
scheduling_strategy=PlacementGroupSchedulingStrategy(placement_group=pg),
).remote()
ray.get(a.v.remote())
ray.get(pg.ready())
with pytest.raises(ValueError):
a = Actor.options(
resources={"bundle": 1},
scheduling_strategy=PlacementGroupSchedulingStrategy(placement_group=pg),
).remote()
ray.get(a.v.remote())
placement_group_assert_no_leak([pg])
@pytest.mark.skipif(
ray._private.client_mode_hook.is_client_mode_enabled, reason="Fails w/ Ray Client."
)
def test_placement_group_invalid_resource_request(shutdown_only):
"""
Make sure exceptions are raised if
requested resources don't fit any bundles.
"""
ray.init(resources={"a": 1})
pg = ray.util.placement_group(bundles=[{"a": 1}])
#
# Test an actor with 0 cpu.
#
@ray.remote
class A:
def ready(self):
pass
# The actor cannot be scheduled with the default because
# it requires 1 cpu for the placement, but the pg doesn't have it.
with pytest.raises(ValueError):
a = A.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(placement_group=pg)
).remote()
# Shouldn't work with 1 CPU because pg doesn't contain CPUs.
with pytest.raises(ValueError):
a = A.options(
num_cpus=1,
scheduling_strategy=PlacementGroupSchedulingStrategy(placement_group=pg),
).remote()
# 0 CPU should work.
a = A.options(
num_cpus=0,
scheduling_strategy=PlacementGroupSchedulingStrategy(placement_group=pg),
).remote()
ray.get(a.ready.remote())
del a
#
# Test an actor with non-0 resources.
#
@ray.remote(resources={"a": 1})
class B:
def ready(self):
pass
# When resources are given to the placement group,
# it automatically adds 1 CPU to resources, so it should fail.
with pytest.raises(ValueError):
b = B.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(placement_group=pg)
).remote()
# If 0 cpu is given, it should work.
b = B.options(
num_cpus=0,
scheduling_strategy=PlacementGroupSchedulingStrategy(placement_group=pg),
).remote()
ray.get(b.ready.remote())
del b
# If resources are requested too much, it shouldn't work.
with pytest.raises(ValueError):
# The actor cannot be scheduled with no resource specified.
# Note that the default actor has 0 cpu.
B.options(
num_cpus=0,
resources={"a": 2},
schduling_strategy=PlacementGroupSchedulingStrategy(placement_group=pg),
).remote()
#
# Test a function with 1 CPU.
#
@ray.remote
def f():
pass
# 1 CPU shouldn't work because the pg doesn't have CPU bundles.
with pytest.raises(ValueError):
f.options(
schduling_strategy=PlacementGroupSchedulingStrategy(placement_group=pg)
).remote()
# 0 CPU should work.
ray.get(
f.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(placement_group=pg),
num_cpus=0,
).remote()
)
#
# Test a function with 0 CPU.
#
@ray.remote(num_cpus=0)
def g():
pass
# 0 CPU should work.
ray.get(
g.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(placement_group=pg)
).remote()
)
placement_group_assert_no_leak([pg])
@pytest.mark.parametrize(
"ray_start_cluster",
[
{
"include_dashboard": True,
}
],
indirect=True,
)
def test_placement_group_pack(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 i in range(num_nodes):
cluster.add_node(num_cpus=4)
ray.init(address=cluster.address)
placement_group = ray.util.placement_group(
name="name",
strategy="PACK",
bundles=[
{"CPU": 2, "GPU": 0}, # Test 0 resource spec doesn't break tests.
{"CPU": 2},
],
)
ray.get(placement_group.ready())
actor_1 = Actor.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=placement_group, placement_group_bundle_index=0
)
).remote()
actor_2 = Actor.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=placement_group, placement_group_bundle_index=1
)
).remote()
ray.get(actor_1.value.remote())
ray.get(actor_2.value.remote())
# Make sure all actors in counter_list are collocated in one node.
actor_info_1 = ray.util.state.get_actor(id=actor_1._actor_id.hex())
actor_info_2 = ray.util.state.get_actor(id=actor_2._actor_id.hex())
assert actor_info_1 and actor_info_2
node_of_actor_1 = actor_info_1.node_id
node_of_actor_2 = actor_info_2.node_id
assert node_of_actor_1 == node_of_actor_2
placement_group_assert_no_leak([placement_group])
@pytest.mark.parametrize(
"ray_start_cluster",
[
{
"include_dashboard": True,
}
],
indirect=True,
)
def test_placement_group_strict_pack(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)
placement_group = ray.util.placement_group(
name="name",
strategy="STRICT_PACK",
bundles=[
{
"memory": 50
* 1024
* 1024, # Test memory resource spec doesn't break tests.
"CPU": 2,
},
{"CPU": 2},
],
)
ray.get(placement_group.ready())
actor_1 = Actor.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=placement_group, placement_group_bundle_index=0
)
).remote()
actor_2 = Actor.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=placement_group, placement_group_bundle_index=1
)
).remote()
ray.get(actor_1.value.remote())
ray.get(actor_2.value.remote())
# Make sure all actors in counter_list are collocated in one node.
actor_info_1 = ray.util.state.get_actor(id=actor_1._actor_id.hex())
actor_info_2 = ray.util.state.get_actor(id=actor_2._actor_id.hex())
assert actor_info_1 and actor_info_2
node_of_actor_1 = actor_info_1.node_id
node_of_actor_2 = actor_info_2.node_id
assert node_of_actor_1 == node_of_actor_2
placement_group_assert_no_leak([placement_group])
@pytest.mark.parametrize(
"ray_start_cluster",
[
{
"include_dashboard": True,
}
],
indirect=True,
)
def test_placement_group_spread(ray_start_cluster):
@ray.remote
class Actor(object):
def __init__(self):
self.n = 0
def value(self):
return self.n
cluster = ray_start_cluster
num_nodes = 2
for i in range(num_nodes):
cluster.add_node(num_cpus=4)
ray.init(address=cluster.address)
placement_group = ray.util.placement_group(
name="name",
strategy="STRICT_SPREAD",
bundles=[{"CPU": 2}, {"CPU": 2}],
)
ray.get(placement_group.ready())
actors = [
Actor.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=placement_group, placement_group_bundle_index=i
),
num_cpus=2,
).remote()
for i in range(num_nodes)
]
[ray.get(actor.value.remote()) for actor in actors]
# Make sure all actors in counter_list are located in separate nodes.
actor_info_objs = [
ray.util.state.get_actor(id=actor._actor_id.hex()) for actor in actors
]
assert are_pairwise_unique([info_obj.node_id for info_obj in actor_info_objs])
placement_group_assert_no_leak([placement_group])
@pytest.mark.parametrize(
"ray_start_cluster",
[
{
"include_dashboard": True,
}
],
indirect=True,
)
def test_placement_group_strict_spread(ray_start_cluster):
@ray.remote
class Actor(object):
def __init__(self):
self.n = 0
def value(self):
return self.n
cluster = ray_start_cluster
num_nodes = 3
for i in range(num_nodes):
cluster.add_node(num_cpus=4)
ray.init(address=cluster.address)
placement_group = ray.util.placement_group(
name="name",
strategy="STRICT_SPREAD",
bundles=[{"CPU": 2}, {"CPU": 2}, {"CPU": 2}],
)
ray.get(placement_group.ready())
actors = [
Actor.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=placement_group, placement_group_bundle_index=i
),
num_cpus=1,
).remote()
for i in range(num_nodes)
]
[ray.get(actor.value.remote()) for actor in actors]
# Make sure all actors in counter_list are located in separate nodes.
actor_info_objs = [
ray.util.state.get_actor(id=actor._actor_id.hex()) for actor in actors
]
assert are_pairwise_unique([info_obj.node_id for info_obj in actor_info_objs])
actors_no_special_bundle = [
Actor.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=placement_group
),
num_cpus=1,
).remote()
for _ in range(num_nodes)
]
[ray.get(actor.value.remote()) for actor in actors_no_special_bundle]
actor_no_resource = Actor.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=placement_group
),
num_cpus=2,
).remote()
with pytest.raises(ray.exceptions.GetTimeoutError):
ray.get(actor_no_resource.value.remote(), timeout=0.5)
placement_group_assert_no_leak([placement_group])
def test_placement_group_actor_resource_ids(ray_start_cluster):
@ray.remote(num_cpus=1)
class F:
def f(self):
return ray.get_runtime_context().get_assigned_resources()
cluster = ray_start_cluster
num_nodes = 1
for _ in range(num_nodes):
cluster.add_node(num_cpus=4)
ray.init(address=cluster.address)
g1 = ray.util.placement_group([{"CPU": 2}])
a1 = F.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(placement_group=g1)
).remote()
resources = ray.get(a1.f.remote())
assert resources == {"CPU": 1}
placement_group_assert_no_leak([g1])
def test_placement_group_task_resource_ids(ray_start_cluster):
@ray.remote(num_cpus=1)
def f():
return ray.get_runtime_context().get_assigned_resources()
cluster = ray_start_cluster
num_nodes = 1
for _ in range(num_nodes):
cluster.add_node(num_cpus=4)
ray.init(address=cluster.address)
g1 = ray.util.placement_group([{"CPU": 2}])
o1 = f.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(placement_group=g1)
).remote()
resources = ray.get(o1)
assert resources == {"CPU": 1}
# Now retry with a bundle index constraint.
o1 = f.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=g1, placement_group_bundle_index=0
)
).remote()
resources = ray.get(o1)
assert resources == {"CPU": 1}
placement_group_assert_no_leak([g1])
def test_placement_group_hang(ray_start_cluster):
@ray.remote(num_cpus=1)
def f():
return ray.get_runtime_context().get_assigned_resources()
cluster = ray_start_cluster
num_nodes = 1
for _ in range(num_nodes):
cluster.add_node(num_cpus=4)
ray.init(address=cluster.address)
# Warm workers up, so that this triggers the hang rice.
ray.get(f.remote())
g1 = ray.util.placement_group([{"CPU": 2}])
# This will start out infeasible. The placement group will then be
# created and it transitions to feasible.
o1 = f.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(placement_group=g1)
).remote()
resources = ray.get(o1)
assert resources == {"CPU": 1}
placement_group_assert_no_leak([g1])
def test_placement_group_empty_bundle_error(ray_start_regular):
with pytest.raises(ValueError):
ray.util.placement_group([])
def test_placement_group_equal_hash(ray_start_regular):
from copy import copy
pg1 = ray.util.placement_group([{"CPU": 1}])
pg2 = copy(pg1)
# __eq__
assert pg1 == pg2
# __hash__
s = set()
s.add(pg1)
assert pg2 in s
# Compare in remote task
@ray.remote(num_cpus=0)
def same(a, b):
return a == b and b in {a}
assert ray.get(same.remote(pg1, pg2))
# Compare before/after object store
assert ray.get(ray.put(pg1)) == pg1
@pytest.mark.filterwarnings("default:placement_group parameter is deprecated")
def test_placement_group_scheduling_warning(ray_start_regular):
@ray.remote
class Foo:
def foo():
pass
pg = ray.util.placement_group(
name="bar",
strategy="PACK",
bundles=[
{"CPU": 1, "GPU": 0},
],
)
ray.get(pg.ready())
# Warning on using deprecated parameters.
with warnings.catch_warnings(record=True) as w:
Foo.options(placement_group=pg, placement_group_bundle_index=0).remote()
assert any(
"placement_group parameter is deprecated" in str(warning.message)
for warning in w
)
assert any(
f"docs.ray.io/en/{get_ray_doc_version()}" in str(warning.message)
for warning in w
)
# Pointing to the same doc version as ray.__version__.
ray.__version__ = "1.13.0"
with warnings.catch_warnings(record=True) as w:
Foo.options(placement_group=pg, placement_group_bundle_index=0).remote()
assert any(
"docs.ray.io/en/releases-1.13.0" in str(warning.message) for warning in w
)
# No warning when scheduling_strategy is specified.
with warnings.catch_warnings(record=True) as w:
Foo.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=pg, placement_group_bundle_index=0
),
).remote()
assert not w
@pytest.mark.skipif(
ray._private.client_mode_hook.is_client_mode_enabled, reason="Fails w/ Ray Client."
)
@pytest.mark.filterwarnings(
"default:Setting 'object_store_memory' for actors is deprecated"
)
@pytest.mark.filterwarnings(
"default:Setting 'object_store_memory' for bundles is deprecated"
)
def test_object_store_memory_deprecation_warning(ray_start_regular):
with warnings.catch_warnings(record=True) as w:
@ray.remote(object_store_memory=1)
class Actor:
pass
Actor.remote()
assert any(
"Setting 'object_store_memory' for actors is deprecated" in str(warning.message)
for warning in w
)
with warnings.catch_warnings(record=True) as w:
ray.util.placement_group([{"object_store_memory": 1}], strategy="STRICT_PACK")
assert any(
"Setting 'object_store_memory' for bundles is deprecated"
in str(warning.message)
for warning in w
)
def test_get_assigned_resources_in_pg(ray_start_cluster):
cluster = ray_start_cluster
cluster.add_node(num_cpus=3)
ray.init(address=cluster.address)
@ray.remote
def get_assigned_resources():
return ray.get_runtime_context().get_assigned_resources()
resources = ray.get(get_assigned_resources.options(num_cpus=1).remote())
assert resources == {"CPU": 1}
pg = ray.util.placement_group(bundles=[{"CPU": 3, "memory": 500}])
ray.get(pg.ready())
resources = ray.get(
get_assigned_resources.options(
num_cpus=1,
scheduling_strategy=PlacementGroupSchedulingStrategy(placement_group=pg),
).remote()
)
assert resources == {"CPU": 1}
resources = ray.get(
get_assigned_resources.options(
num_cpus=1,
memory=100,
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=pg, placement_group_bundle_index=0
),
).remote()
)
assert resources == {"CPU": 1, "memory": 100}
def test_omp_num_threads_in_pg(ray_start_cluster):
cluster = ray_start_cluster
cluster.add_node(num_cpus=3)
ray.init(address=cluster.address)
@ray.remote(num_cpus=3)
def test_omp_num_threads():
omp_threads = os.environ["OMP_NUM_THREADS"]
return int(omp_threads)
assert ray.get(test_omp_num_threads.remote()) == 3
pg = ray.util.placement_group(bundles=[{"CPU": 3}])
ray.get(pg.ready())
ref = test_omp_num_threads.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(placement_group=pg)
).remote()
assert ray.get(ref) == 3
ref = test_omp_num_threads.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=pg, placement_group_bundle_index=0
)
).remote()
assert ray.get(ref) == 3
class TestPlacementGroupValidation:
def test_strategy_validation(self):
"""Test strategy validation when creating a placement group."""
# Valid strategies should not raise an exception.
for strategy in VALID_PLACEMENT_GROUP_STRATEGIES:
validate_placement_group(bundles=[{"CPU": 1}], strategy=strategy)
# Any other strategy should raise a ValueError.
with pytest.raises(ValueError, match="Invalid placement group strategy"):
validate_placement_group(bundles=[{"CPU": 1}], strategy="invalid")
def test_topology_strategy_validation(self):
"""Test topology_strategy validation when creating a placement group."""
valid_topology_strategies = [
{NODE_ID_LABEL_KEY: "PACK"},
{NODE_ID_LABEL_KEY: "STRICT_SPREAD"},
{"ray.io/gpu-domain": "STRICT_PACK"},
{
NODE_ID_LABEL_KEY: "SPREAD",
"ray.io/gpu-domain": "STRICT_PACK",
},
]
for topology_strategy in valid_topology_strategies:
validate_placement_group(
bundles=[{"CPU": 1}], topology_strategy=topology_strategy
)
with pytest.raises(
ValueError, match="strategy` and `topology_strategy` cannot both"
):
validate_placement_group(
bundles=[{"CPU": 1}],
strategy="PACK",
topology_strategy={"ray.io/gpu-domain": "STRICT_PACK"},
)
with pytest.raises(ValueError, match="must be a dict"):
validate_placement_group(
bundles=[{"CPU": 1}],
topology_strategy=[{"ray.io/gpu-domain": "STRICT_PACK"}],
)
with pytest.raises(ValueError, match="keys must be non-empty strings"):
validate_placement_group(
bundles=[{"CPU": 1}], topology_strategy={"": "STRICT_PACK"}
)
with pytest.raises(ValueError, match="keys must be non-empty strings"):
validate_placement_group(
bundles=[{"CPU": 1}], topology_strategy={1: "STRICT_PACK"}
)
with pytest.raises(ValueError, match="Invalid topology strategy"):
validate_placement_group(
bundles=[{"CPU": 1}],
topology_strategy={NODE_ID_LABEL_KEY: "invalid"},
)
with pytest.raises(ValueError, match="only 'STRICT_PACK' is supported"):
validate_placement_group(
bundles=[{"CPU": 1}],
topology_strategy={"ray.io/gpu-domain": "SPREAD"},
)
with pytest.raises(ValueError, match="at most one topology label"):
validate_placement_group(
bundles=[{"CPU": 1}],
topology_strategy={
"ray.io/gpu-domain": "STRICT_PACK",
"ray.io/zone": "STRICT_PACK",
},
)
def test_bundle_validation(self):
"""Test _validate_bundle()."""
# Valid bundles should not raise an exception.
valid_bundles = [{"CPU": 1, "custom-resource": 2.2}, {"GPU": 0.75}]
_validate_bundles(valid_bundles)
# Non-list bundles should raise an exception.
with pytest.raises(ValueError, match="must be a list"):
_validate_bundles("not a list")
# Empty list bundles should raise an exception.
with pytest.raises(ValueError, match="must be a non-empty list"):
_validate_bundles([])
# List that doesn't contain dictionaries should raise an exception.
with pytest.raises(ValueError, match="resource dictionaries"):
_validate_bundles([{"CPU": 1}, "not a dict"])
# List with invalid dictionary entries should raise an exception.
with pytest.raises(ValueError, match="resource dictionaries"):
_validate_bundles([{8: 7}, {5: 3.5}])
with pytest.raises(ValueError, match="resource dictionaries"):
_validate_bundles([{"CPU": "6"}, {"GPU": "5"}])
# Bundles with resources that all have 0 values should raise an exception.
with pytest.raises(ValueError, match="only 0 values"):
_validate_bundles([{"CPU": 0, "GPU": 0}])
def test_bundle_label_selector_validation(self):
"""Test _validate_bundle_label_selector()."""
# Valid label selector list should not raise an exception.
valid_label_selectors = [
{"ray.io/market_type": "spot"},
{"ray.io/accelerator-type": "A100"},
]
_validate_bundle_label_selector(valid_label_selectors)
# Non-list input should raise an exception.
with pytest.raises(ValueError, match="must be a list"):
_validate_bundle_label_selector("not a list")
# Empty list should not raise (interpreted as no-op).
_validate_bundle_label_selector([])
# List with non-dictionary elements should raise an exception.
with pytest.raises(ValueError, match="must be a list of string dictionary"):
_validate_bundle_label_selector(["not a dict", {"valid": "label"}])
# Dictionary with non-string keys or values should raise an exception.
with pytest.raises(ValueError, match="must be a list of string dictionary"):
_validate_bundle_label_selector([{1: "value"}, {"key": "val"}])
with pytest.raises(ValueError, match="must be a list of string dictionary"):
_validate_bundle_label_selector([{"key": 123}, {"valid": "label"}])
# Invalid label key or value syntax (delegated to validate_label_selector).
with pytest.raises(ValueError, match="Invalid label selector provided"):
_validate_bundle_label_selector([{"INVALID key!": "value"}])
if __name__ == "__main__":
sys.exit(pytest.main(["-sv", __file__]))