chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:17:40 +08:00
commit f1825c8ceb
10096 changed files with 2364182 additions and 0 deletions
@@ -0,0 +1,47 @@
load("@rules_python//python:defs.bzl", "py_library", "py_test")
py_library(
name = "conftest",
srcs = ["conftest.py"],
deps = ["//python/ray/tests:conftest"],
)
# TODO(#54703): The tests in this file are being tagged
# as manual because they shouldn't be run as part of
# bazel test //python/ray/tests/...
py_test(
name = "test_resource_isolation_integration",
size = "medium",
srcs = ["test_resource_isolation_integration.py"],
tags = [
"cgroup",
"custom_setup",
"exclusive",
"no_windows",
"team:core",
],
target_compatible_with = [
"@platforms//os:linux",
],
deps = [
":conftest",
"//:ray_lib",
],
)
py_test(
name = "test_resource_isolation_config",
size = "medium",
srcs = ["test_resource_isolation_config.py"],
tags = [
"cgroup",
"custom_setup",
"exclusive",
"no_windows",
"team:core",
],
deps = [
":conftest",
"//:ray_lib",
],
)
@@ -0,0 +1 @@
from ray.tests.conftest import ray_start_cluster, maybe_setup_external_redis # noqa
@@ -0,0 +1,314 @@
import sys
import pytest
from ray._common import utils as common_utils
from ray._private import utils
from ray._private.resource_isolation_config import ResourceIsolationConfig
def test_resource_isolation_is_disabled_by_default():
resource_isolation_config = ResourceIsolationConfig()
assert not resource_isolation_config.is_enabled()
def test_disabled_resource_isolation_with_overrides_raises_value_error():
with pytest.raises(
ValueError,
match="cgroup_path cannot be set when resource isolation is not enabled",
):
ResourceIsolationConfig(
enable_resource_isolation=False, cgroup_path="/some/path"
)
with pytest.raises(
ValueError,
match="system_reserved_cpu cannot be set when resource isolation is not enabled",
):
ResourceIsolationConfig(enable_resource_isolation=False, system_reserved_cpu=1)
with pytest.raises(
ValueError,
match="system_reserved_cpu cannot be set when resource isolation is not enabled",
):
ResourceIsolationConfig(enable_resource_isolation=False, system_reserved_cpu=0)
with pytest.raises(
ValueError,
match="system_reserved_memory cannot be set when resource isolation is not enabled",
):
ResourceIsolationConfig(
enable_resource_isolation=False, system_reserved_memory=1024**3
)
with pytest.raises(
ValueError,
match="system_reserved_memory cannot be set when resource isolation is not enabled",
):
ResourceIsolationConfig(
enable_resource_isolation=False, system_reserved_memory=0
)
def test_enabled_resource_isolation_with_non_string_cgroup_path_raises_value_error():
with pytest.raises(ValueError, match="Invalid value.*for cgroup_path"):
ResourceIsolationConfig(enable_resource_isolation=True, cgroup_path=1)
with pytest.raises(ValueError, match="Invalid value.*for cgroup_path"):
ResourceIsolationConfig(enable_resource_isolation=True, cgroup_path=1.0)
def test_enabled_resource_isolation_with_non_number_reserved_cpu_raises_value_error():
with pytest.raises(ValueError, match="Invalid value.*for system_reserved_cpu."):
ResourceIsolationConfig(
enable_resource_isolation=True,
system_reserved_cpu="1",
)
def test_enabled_resource_isolation_with_non_number_reserved_memory_raises_value_error():
with pytest.raises(ValueError, match="Invalid value.*for system_reserved_memory."):
ResourceIsolationConfig(
enable_resource_isolation=True,
system_reserved_memory="1",
)
def test_enabled_default_config_with_insufficient_cpu_and_memory_raises_value_error(
monkeypatch,
):
# The following values in ray_constants define the minimum requirements for resource isolation
# 1) DEFAULT_MIN_SYSTEM_RESERVED_CPU_CORES
# 2) DEFAULT_MIN_SYSTEM_RESERVED_MEMORY_BYTES
# NOTE: if you change the DEFAULT_MIN_SYSTEM_* constants, you may need to modify this test.
monkeypatch.setattr(utils, "get_num_cpus", lambda *args, **kwargs: 0.5)
with pytest.raises(
ValueError, match="available number of cpu cores.*less than the minimum"
):
ResourceIsolationConfig(enable_resource_isolation=True)
monkeypatch.undo()
monkeypatch.setattr(
common_utils, "get_system_memory", lambda *args, **kwargs: 400 * (1024**2)
)
with pytest.raises(ValueError, match="available memory.*less than the minimum"):
ResourceIsolationConfig(enable_resource_isolation=True)
def test_enabled_resource_isolation_with_default_config_picks_min_values(monkeypatch):
# The following values in ray_constants define the minimum requirements for resource isolation
# 1) DEFAULT_MIN_SYSTEM_RESERVED_CPU_CORES
# 2) DEFAULT_MIN_SYSTEM_RESERVED_MEMORY_BYTES
# NOTE: if you change the DEFAULT_MIN_SYSTEM_* constants, you may need to modify this test.
# if the total number of cpus is between [1,19] the system cgroup will a weight that is equal to 1 cpu core.
# if the total amount of memory is between [0.5GB, 4.8GB] the system cgroup will get 0.5GB + object store memory.
monkeypatch.setattr(utils, "get_num_cpus", lambda *args, **kwargs: 2)
monkeypatch.setattr(
common_utils, "get_system_memory", lambda *args, **kwargs: 1024**3
)
config = ResourceIsolationConfig(enable_resource_isolation=True)
assert config.system_reserved_cpu_weight == 5000
assert config.system_reserved_memory == 500 * (1024**2)
monkeypatch.setattr(utils, "get_num_cpus", lambda *args, **kwargs: 19)
monkeypatch.setattr(
common_utils, "get_system_memory", lambda *args, **kwargs: 4.8 * (1024**3)
)
config = ResourceIsolationConfig(enable_resource_isolation=True)
assert config.system_reserved_cpu_weight == 526
assert config.system_reserved_memory == 500 * (1024**2)
def test_enabled_resource_isolation_with_default_config_values_scale_with_system(
monkeypatch,
):
# The following values in ray_constants define the default proportion for resource isolation
# 1) DEFAULT_SYSTEM_RESERVED_CPU_PROPORTION
# 2) DEFAULT_SYSTEM_RESERVED_MEMORY_PROPORTION
# NOTE: if you change the DEFAULT_SYSTEM_RESERVED_* constants, you may need to modify this test.
# if the number of cpus on the system is [20,60] the reserved cpu cores will scale proportionately.
# if the amount of memory on the system is [5GB, 100GB] the reserved system memory will scale proportionately.
monkeypatch.setattr(utils, "get_num_cpus", lambda *args, **kwargs: 20)
monkeypatch.setattr(
common_utils, "get_system_memory", lambda *args, **kwargs: 5 * (1024**3)
)
config = ResourceIsolationConfig(enable_resource_isolation=True)
assert config.system_reserved_cpu_weight == 500
assert config.system_reserved_memory == 512 * (1024**2)
monkeypatch.setattr(utils, "get_num_cpus", lambda *args, **kwargs: 59)
monkeypatch.setattr(
common_utils, "get_system_memory", lambda *args, **kwargs: 99 * (1024**3)
)
config = ResourceIsolationConfig(enable_resource_isolation=True)
assert config.system_reserved_cpu_weight == 500
assert config.system_reserved_memory == 10630044057 # 9.9GiB
def test_enabled_resource_isolation_with_default_config_picks_max_values(monkeypatch):
# The following values in ray_constants define the max reserved values for resource isolation
# 1) DEFAULT_MAX_SYSTEM_RESERVED_CPU_CORES
# 2) DEFAULT_MAX_SYSTEM_RESERVED_MEMORY_BYTES
# NOTE: if you change the DEFAULT_MAX_SYSTEM* constants, you may need to modify this test.
# if the number of cpus on the system >= 60 the reserved cpu cores will be DEFAULT_MAX_SYSTEM_RESERVED_CPU_CORES.
# if the amount of memory on the system >= 100GB the reserved memory will be DEFAULT_MAX_SYSTEM_RESERVED_MEMORY_BYTES.
monkeypatch.setattr(utils, "get_num_cpus", lambda *args, **kwargs: 61)
monkeypatch.setattr(
common_utils, "get_system_memory", lambda *args, **kwargs: 100 * (1024**3)
)
config = ResourceIsolationConfig(enable_resource_isolation=True)
assert config.system_reserved_cpu_weight == 491
assert config.system_reserved_memory == 10 * (1024**3)
monkeypatch.setattr(utils, "get_num_cpus", lambda *args, **kwargs: 128)
monkeypatch.setattr(
common_utils, "get_system_memory", lambda *args, **kwargs: 500 * (1024**3)
)
config = ResourceIsolationConfig(enable_resource_isolation=True)
assert config.system_reserved_cpu_weight == 234
assert config.system_reserved_memory == 10 * (1024**3)
def test_enabled_with_resource_overrides_less_than_minimum_defaults_raise_value_error():
# The following values in ray_constants define the min values needed to run ray with resource isolation.
# 1) DEFAULT_MIN_SYSTEM_RESERVED_CPU_CORES
# 2) DEFAULT_MIN_SYSTEM_RESERVED_MEMORY_BYTES
# NOTE: if you change the DEFAULT_MIN_SYSTEM* constants, you may need to modify this test.
with pytest.raises(
ValueError,
match="The requested system_reserved_cpu=0.5 is less than the minimum number of cpus that can be used for resource isolation.",
):
ResourceIsolationConfig(
enable_resource_isolation=True,
system_reserved_cpu=0.5,
)
with pytest.raises(
ValueError,
match="The requested system_reserved_cpu=0.0 is less than the minimum number of cpus that can be used for resource isolation.",
):
ResourceIsolationConfig(
enable_resource_isolation=True,
system_reserved_cpu=0,
)
with pytest.raises(
ValueError,
match="The requested system_reserved_memory 4194304 is less than the minimum number of bytes that can be used for resource isolation.",
):
ResourceIsolationConfig(
enable_resource_isolation=True,
system_reserved_memory=4 * (1024**2),
)
with pytest.raises(
ValueError,
match="The requested system_reserved_memory 0 is less than the minimum number of bytes that can be used for resource isolation.",
):
ResourceIsolationConfig(
enable_resource_isolation=True,
system_reserved_memory=0,
)
def test_enabled_with_resource_overrides_gte_than_available_resources_raise_value_error(
monkeypatch,
):
# The following values in ray_constants define the maximum reserved values to run ray with resource isolation.
# 1) DEFAULT_MAX_SYSTEM_RESERVED_CPU_CORES
# 2) DEFAULT_MAX_SYSTEM_RESERVED_MEMORY_BYTES
# NOTE: if you change the DEFAULT_MAX_SYSTEM* constants, you may need to modify this test.
monkeypatch.setattr(utils, "get_num_cpus", lambda *args, **kwargs: 32)
with pytest.raises(
ValueError,
match="The requested system_reserved_cpu=32.0 is greater than or equal to the number of cpus available=32",
):
ResourceIsolationConfig(
enable_resource_isolation=True,
system_reserved_cpu=32,
)
monkeypatch.setattr(
common_utils, "get_system_memory", lambda *args, **kwargs: 10 * (1024**3)
)
# 11GiB requested, 10GB available
with pytest.raises(
ValueError,
match=r"The total requested system_reserved_memory=11811160064 is greater than the amount of memory available=10737418240\.",
):
ResourceIsolationConfig(
enable_resource_isolation=True,
system_reserved_memory=11 * (1024**3),
)
def test_resource_isolation_enabled_with_partial_resource_overrides_and_defaults_happy_path(
monkeypatch,
):
# This is a happy path test where all overrides are specified with valid values.
# NOTE: if you change the DEFAULT_SYSTEM_RESERVED_CPU_PROPORTION, this test may fail.
monkeypatch.setattr(utils, "get_num_cpus", lambda *args, **kwargs: 32)
monkeypatch.setattr(
common_utils, "get_system_memory", lambda *args, **kwargs: 64 * (1024**3)
)
# Overriding cgroup_path while using default system_reserved_cpu and system_reserved_memory
override_cgroup_path_config: ResourceIsolationConfig = ResourceIsolationConfig(
enable_resource_isolation=True,
cgroup_path="/sys/fs/cgroup/ray",
)
assert override_cgroup_path_config.cgroup_path == "/sys/fs/cgroup/ray"
# (32 cpus * 0.05 (default))/10000 = 500
assert override_cgroup_path_config.system_reserved_cpu_weight == 500
# 64GB * 0.10 = 6.4GB
assert override_cgroup_path_config.system_reserved_memory == 6871947673 # 6.4GiB
# Overriding system_reserved_cpu while using default cgroup_path and system_reserved_memory
override_cpu_config: ResourceIsolationConfig = ResourceIsolationConfig(
enable_resource_isolation=True, system_reserved_cpu=1.5
)
assert override_cpu_config.system_reserved_cpu_weight == 468
# defaults to /sys/fs/cgroup
assert override_cpu_config.cgroup_path == "/sys/fs/cgroup"
# 64GB * 0.10 = 6.4GB
assert override_cpu_config.system_reserved_memory == 6871947673 # 6.4GiB
# Overriding system_reserved_memory while using default cgroup_path and system_reserved_cpu
override_memory_config: ResourceIsolationConfig = ResourceIsolationConfig(
enable_resource_isolation=True,
system_reserved_memory=5 * (1024**3),
)
assert override_memory_config.system_reserved_memory == 5368709120 # 5GiB
# defaults to /sys/fs/cgroup
assert override_memory_config.cgroup_path == "/sys/fs/cgroup"
# (32 cpus * 0.05 (default))/10000 = 500
assert override_memory_config.system_reserved_cpu_weight == 500
def test_resource_isolation_enabled_with_full_overrides_happy_path(monkeypatch):
monkeypatch.setattr(utils, "get_num_cpus", lambda *args, **kwargs: 32)
monkeypatch.setattr(
common_utils, "get_system_memory", lambda *args, **kwargs: 128 * (1024**3)
)
# The system_reserved_cpu is deliberately > the maximum default.
# The system_reserved_memory is deliberately > the maximum default.
override_config: ResourceIsolationConfig = ResourceIsolationConfig(
enable_resource_isolation=True,
cgroup_path="/sys/fs/cgroup/ray",
system_reserved_cpu=5.0,
system_reserved_memory=15 * 1024**3,
)
assert override_config.cgroup_path == "/sys/fs/cgroup/ray"
# int(5/32 * 10000)
assert override_config.system_reserved_cpu_weight == 1562
assert override_config.system_reserved_memory == 15 * (1024**3)
if __name__ == "__main__":
sys.exit(pytest.main(["-sv", __file__]))
@@ -0,0 +1,620 @@
import os
import platform
import subprocess
import sys
import textwrap
from pathlib import Path
from typing import Set
import pytest
from click.testing import CliRunner
import ray
import ray._common.utils as utils
import ray._private.ray_constants as ray_constants
import ray.scripts.scripts as scripts
from ray._common.test_utils import wait_for_condition
from ray._private.resource_isolation_config import ResourceIsolationConfig
# These tests are intended to run in CI inside a container.
#
# If you want to run this test locally, you will need to create a cgroup that
# the ray can manage and delegate to the correct user.
#
# Run these commands locally before running the test suite:
#
# sudo mkdir -p /sys/fs/cgroup/resource_isolation_test
# sudo chown -R $(whoami):$(whoami) /sys/fs/cgroup/resource_isolation_test/
# sudo chmod -R u+rwx /sys/fs/cgroup/resource_isolation_test/
# echo $$ | sudo tee /sys/fs/cgroup/resource_isolation_test/cgroup.procs
#
# Comment the following line out.
_ROOT_CGROUP = Path("/sys/fs/cgroup")
#
# To run locally, uncomment the following line.
# _ROOT_CGROUP = Path("/sys/fs/cgroup/resource_isolation_test")
# The integration tests assume that the _ROOT_CGROUP exists and that
# the process has read and write access.
#
# This test suite will create the following cgroup hierarchy for the tests
# starting with BASE_CGROUP.
#
# ROOT_CGROUP
# |
# BASE_CGROUP
# / \
# TEST_CGROUP LEAF_CGROUP
# |
# ray-node_<node_id>
# | |
# system user
# | | |
# leaf workers non-ray
#
# NOTE: The test suite does not assume that ROOT_CGROUP is the OS's root cgroup. Therefore,
# 1. setup will migrate all processes from the ROOT_CGROUP -> LEAF_CGROUP
# 2. teardown will migrate all processes from the LEAF_CGROUP -> ROOT_CGROUP
#
# NOTE: BASE_CGROUP will have a randomly generated name to isolate tests from each other.
#
# The test suite assumes that
# 1. cpu, memory controllers are available on ROOT_CGROUP i.e. in the ROOT_CGROUP/cgroup.controllers file.
# 2. All processes inside the base_cgroup can be migrated into the leaf_cgroup to avoid not violating
# the no internal processes contstraint.
#
# All python tests should only have access to the TEST_CGROUP and nothing outside of it.
_BASE_CGROUP = _ROOT_CGROUP / ("testing_" + utils.get_random_alphanumeric_string(5))
_TEST_CGROUP = _BASE_CGROUP / "test"
_LEAF_GROUP = _BASE_CGROUP / "leaf"
_MOUNT_FILE_PATH = "/proc/mounts"
# The names are here to help debug test failures. Tests should
# only use the size of this list. These processes are expected to be moved
# into the the system cgroup.
_EXPECTED_DASHBOARD_MODULES = [
"ray.dashboard.modules.usage_stats.usage_stats_head.UsageStatsHead",
"ray.dashboard.modules.metrics.metrics_head.MetricsHead",
"ray.dashboard.modules.data.data_head.DataHead",
"ray.dashboard.modules.event.event_head.EventHead",
"ray.dashboard.modules.job.job_head.JobHead",
"ray.dashboard.modules.node.node_head.NodeHead",
"ray.dashboard.modules.reporter.reporter_head.ReportHead",
"ray.dashboard.modules.serve.serve_head.ServeHead",
"ray.dashboard.modules.state.state_head.StateHead",
"ray.dashboard.modules.train.train_head.TrainHead",
]
# The list of processes expected to be started in the system cgroup
# with default params for 'ray start' and 'ray.init(...)'
_EXPECTED_SYSTEM_PROCESSES_RAY_START = [
ray_constants.PROCESS_TYPE_DASHBOARD,
ray_constants.PROCESS_TYPE_GCS_SERVER,
ray_constants.PROCESS_TYPE_MONITOR,
ray_constants.PROCESS_TYPE_LOG_MONITOR,
ray_constants.PROCESS_TYPE_RAY_CLIENT_SERVER,
ray_constants.PROCESS_TYPE_RAYLET,
ray_constants.PROCESS_TYPE_DASHBOARD_AGENT,
ray_constants.PROCESS_TYPE_RUNTIME_ENV_AGENT,
]
_EXPECTED_SYSTEM_PROCESSES_RAY_INIT = [
ray_constants.PROCESS_TYPE_DASHBOARD,
ray_constants.PROCESS_TYPE_GCS_SERVER,
ray_constants.PROCESS_TYPE_MONITOR,
ray_constants.PROCESS_TYPE_LOG_MONITOR,
ray_constants.PROCESS_TYPE_RAYLET,
ray_constants.PROCESS_TYPE_DASHBOARD_AGENT,
ray_constants.PROCESS_TYPE_RUNTIME_ENV_AGENT,
]
@pytest.fixture(scope="session", autouse=True)
def test_suite_fixture():
"""Setups up and tears down the cgroup hierachy for the test suite."""
setup_test_suite()
yield
cleanup_test_suite()
def setup_test_suite():
"""Creates the cgroup hierarchy and moves processes out of the _ROOT_CGROUP into the _LEAF_CGROUP.
The setup involves the following steps:
1) Check if the platform is Linux.
2) Check that cgroupv2 is mounted with read, write permissions in unified mode i.e. cgroupv1 is not mounted.
3) Check that the _ROOT_CGROUP exists and has [cpu, memory] controllers available.
4) Create the _BASE_CGROUP, _TEST_CGROUP, and _LEAF_CGROUP respectively.
5) Move processes from the _ROOT_CGROUP to the _LEAF_CGROUP because of the internal processes constraint.
6) Enable [cpu, memory] controllers in the _ROOT_CGROUP, _BASE_CGROUP, and _TEST_CGROUP respectively.
If any of the steps fail, teardown will be run. Teardown will perform a subset of these steps (not the checks), in reverse order.
"""
try:
# 1) If platform is not linux.
assert (
platform.system() == "Linux"
), f"Failed because resource isolation integration tests can only run on Linux and not on {platform.system()}."
# 2) Check that cgroupv2 is mounted in read-write mode in unified mode.
with open(_MOUNT_FILE_PATH, "r") as mount_file:
lines = mount_file.readlines()
found_cgroup_v1 = False
found_cgroup_v2 = False
for line in lines:
found_cgroup_v1 = found_cgroup_v1 or ("cgroup r" in line.strip())
found_cgroup_v2 = found_cgroup_v2 or ("cgroup2 rw" in line.strip())
assert found_cgroup_v2, (
"Failed because cgroupv2 is not mounted on the system in read-write mode."
" See the following documentation for how to enable cgroupv2 properly:"
" https://kubernetes.io/docs/concepts/architecture/cgroups/#linux-distribution-cgroup-v2-support"
)
assert not found_cgroup_v1, (
"Failed because cgroupv2 and cgroupv1 is mounted on this system."
" See the following documentation for how to enable cgroupv2 in properly in unified mode:"
" https://kubernetes.io/docs/concepts/architecture/cgroups/#linux-distribution-cgroup-v2-support"
)
# 3) Check that current user has read-write access to _BASE_CGROUP_PATH by attempting
# to write the current process into it.
root_cgroup_procs_file = _ROOT_CGROUP / "cgroup.procs"
with open(root_cgroup_procs_file, "w") as procs_file:
procs_file.write(str(os.getpid()))
procs_file.flush()
# 4) Check to see that _ROOT_CGROUP has the [cpu, memory] controllers are available.
root_cgroup_controllers_path = _ROOT_CGROUP / "cgroup.controllers"
expected_controllers = {"cpu", "memory"}
with open(root_cgroup_controllers_path, "r") as available_controllers_file:
available_controllers = set(
available_controllers_file.readline().strip().split(" ")
)
assert expected_controllers.issubset(available_controllers), (
f"Failed because the cpu and memory controllers are not available in {root_cgroup_controllers_path}."
" To enable a controller, you need to add it to the cgroup.controllers file of the parent cgroup of {_ROOT_CGROUP}."
" See: https://docs.kernel.org/admin-guide/cgroup-v2.html#enabling-and-disabling."
)
# 5) Create the leaf cgroup and move all processes from _BASE_CGROUP_PATH into it.
os.mkdir(_BASE_CGROUP)
os.mkdir(_TEST_CGROUP)
os.mkdir(_LEAF_GROUP)
# 6) Move all processes into the leaf cgroup.
with open(_ROOT_CGROUP / "cgroup.procs", "r") as root_procs_file, open(
_LEAF_GROUP / "cgroup.procs", "w"
) as leaf_procs_file:
root_cgroup_lines = root_procs_file.readlines()
for line in root_cgroup_lines:
leaf_procs_file.write(line.strip())
leaf_procs_file.flush()
# 7) Enable [cpu, memory] controllers on the base and test cgroup.
with open(
_ROOT_CGROUP / "cgroup.subtree_control", "w"
) as base_subtree_control_file:
base_subtree_control_file.write("+cpu +memory")
base_subtree_control_file.flush()
with open(
_BASE_CGROUP / "cgroup.subtree_control", "w"
) as base_subtree_control_file:
base_subtree_control_file.write("+cpu +memory")
base_subtree_control_file.flush()
with open(
_TEST_CGROUP / "cgroup.subtree_control", "w"
) as test_subtree_control_file:
test_subtree_control_file.write("+cpu +memory")
test_subtree_control_file.flush()
except Exception as e:
print(
f"Failed to setup the test suite with error {str(e)}. Attempting to run teardown."
)
cleanup_test_suite()
def cleanup_test_suite():
"""Cleans up the cgroup hierarchy and moves processes out of the _LEAF_CGROUP into the _ROOT_CGROUP.
The setup involves the following steps:
1) Disable [cpu, memory] controllers in the _ROOT_CGROUP, _BASE_CGROUP, and _TEST_CGROUP respectively.
2) Move processes from the _LEAF_CGROUP to the _ROOT_CGROUP so the hierarchy can be deleted.
3) Create the _BASE_CGROUP, _TEST_CGROUP, and _LEAF_CGROUP respectively.
If any of the steps fail, teardown will fail an assertion.
"""
# 1) Disable the controllers.
try:
with open(
_TEST_CGROUP / "cgroup.subtree_control", "w"
) as test_subtree_control_file:
test_subtree_control_file.write("-cpu -memory")
test_subtree_control_file.flush()
with open(
_BASE_CGROUP / "cgroup.subtree_control", "w"
) as base_subtree_control_file:
base_subtree_control_file.write("-cpu -memory")
base_subtree_control_file.flush()
with open(
_ROOT_CGROUP / "cgroup.subtree_control", "w"
) as base_subtree_control_file:
base_subtree_control_file.write("-cpu -memory")
base_subtree_control_file.flush()
# 2) Move processes back into the root cgroup.
with open(_ROOT_CGROUP / "cgroup.procs", "w") as root_procs_file, open(
_LEAF_GROUP / "cgroup.procs", "r"
) as leaf_procs_file:
leaf_cgroup_lines = leaf_procs_file.readlines()
for line in leaf_cgroup_lines:
root_procs_file.write(line.strip())
root_procs_file.flush()
# 3) Move the current process back into the _ROOT_CGROUP
with open(_ROOT_CGROUP / "cgroup.procs", "w") as root_procs_file, open(
_TEST_CGROUP / "cgroup.procs", "r"
) as test_procs_file:
test_cgroup_lines = test_procs_file.readlines()
for line in test_cgroup_lines:
root_procs_file.write(line.strip())
root_procs_file.flush()
# 3) Delete the cgroups.
os.rmdir(_LEAF_GROUP)
os.rmdir(_TEST_CGROUP)
os.rmdir(_BASE_CGROUP)
except Exception as e:
assert False, (
f"Failed to cleanup test suite's cgroup hierarchy because of {str(e)}."
"You may have to manually clean up the hierachy under ${_ROOT_CGROUP}"
)
@pytest.fixture
def cleanup_ray():
"""Shutdown all ray instances"""
yield
runner = CliRunner()
runner.invoke(scripts.stop)
ray.shutdown()
@pytest.fixture
def ray_shutdown():
yield
ray.shutdown()
def generate_node_id():
"""Returns a random node id."""
return ray.NodeID.from_random().hex()
def assert_cgroup_hierarchy_exists_for_node(
node_id: str, resource_isolation_config: ResourceIsolationConfig
):
"""Asserts that the cgroup hierarchy was created correctly for the node.
The cgroup hierarchy looks like:
_TEST_CGROUP
|
ray-node_<node_id>
| |
system user
| | |
leaf workers non-ray
Args:
node_id: used to find the path of the cgroup subtree
resource_isolation_config: used to verify constraints enabled on the system, workers, and user cgroups
"""
base_cgroup_for_node = resource_isolation_config.cgroup_path
node_cgroup = Path(base_cgroup_for_node) / f"ray-node_{node_id}"
system_cgroup = node_cgroup / "system"
system_leaf_cgroup = system_cgroup / "leaf"
user_cgroup = node_cgroup / "user"
workers_cgroup = user_cgroup / "workers"
non_ray_cgroup = user_cgroup / "non-ray"
# 1) Check that the cgroup hierarchy is created correctly for the node.
assert node_cgroup.is_dir()
assert system_cgroup.is_dir()
assert system_leaf_cgroup.is_dir()
assert workers_cgroup.is_dir()
assert user_cgroup.is_dir()
assert non_ray_cgroup.is_dir()
# 2) Verify the constraints are applied correctly.
total_memory = ray._common.utils.get_system_memory()
with open(user_cgroup / "memory.high", "r") as memory_high_file:
contents = memory_high_file.read().strip()
assert contents == str(
total_memory - resource_isolation_config.system_reserved_memory
)
with open(system_cgroup / "memory.low", "r") as memory_low_file:
contents = memory_low_file.read().strip()
assert contents == str(resource_isolation_config.system_reserved_memory)
with open(system_cgroup / "cpu.weight", "r") as cpu_weight_file:
contents = cpu_weight_file.read().strip()
assert contents == str(resource_isolation_config.system_reserved_cpu_weight)
with open(user_cgroup / "cpu.weight", "r") as cpu_weight_file:
contents = cpu_weight_file.read().strip()
assert contents == str(
10000 - resource_isolation_config.system_reserved_cpu_weight
)
def assert_process_in_not_moved_into_ray_cgroups(
node_id: str,
resource_isolation_config: ResourceIsolationConfig,
pid: str,
):
"""Asserts that the system processes were created in the correct cgroup.
Args:
node_id: used to construct the path of the cgroup subtree
resource_isolation_config: used to construct the path of the cgroup
subtree
pid:
"""
base_cgroup_for_node = resource_isolation_config.cgroup_path
node_cgroup = Path(base_cgroup_for_node) / f"ray-node_{node_id}"
cgroup_procs_file_paths = [
node_cgroup / "system" / "leaf" / "cgroup.procs",
node_cgroup / "user" / "non-ray" / "cgroup.procs",
node_cgroup / "user" / "workers" / "cgroup.procs",
]
found_pid = False
for file_path in cgroup_procs_file_paths:
with open(file_path, "r") as cgroup_procs_file:
lines = cgroup_procs_file.readlines()
for line in lines:
found_pid = found_pid or (line.strip() == pid)
assert not found_pid
def assert_system_processes_are_in_system_cgroup(
node_id: str,
resource_isolation_config: ResourceIsolationConfig,
expected_count: int,
):
"""Asserts that the system processes were created in the correct cgroup.
Args:
node_id: used to construct the path of the cgroup subtree
resource_isolation_config: used to construct the path of the cgroup
subtree
expected_count: the number of expected system processes.
"""
base_cgroup_for_node = resource_isolation_config.cgroup_path
node_cgroup = Path(base_cgroup_for_node) / f"ray-node_{node_id}"
system_cgroup = node_cgroup / "system"
system_leaf_cgroup = system_cgroup / "leaf"
# At least the raylet process is always moved.
with open(system_leaf_cgroup / "cgroup.procs", "r") as cgroup_procs_file:
lines = cgroup_procs_file.readlines()
assert (
len(lines) == expected_count
), f"Expected only system process passed into the raylet. Found {lines}. You may have added a new dashboard module in which case you need to update _EXPECTED_DASHBOARD_MODULES"
def assert_worker_processes_are_in_workers_cgroup(
node_id: str,
resource_isolation_config: ResourceIsolationConfig,
worker_pids: Set[str],
):
"""Asserts that the worker processes were created in the correct cgroup.
Args:
node_id: used to construct the path of the cgroup subtree
resource_isolation_config: used to construct the path of the cgroup
subtree
worker_pids: a set of pids that are expected inside the workers
leaf cgroup.
"""
base_cgroup_for_node = resource_isolation_config.cgroup_path
node_cgroup = Path(base_cgroup_for_node) / f"ray-node_{node_id}"
workers_cgroup_procs = node_cgroup / "user" / "workers" / "cgroup.procs"
with open(workers_cgroup_procs, "r") as cgroup_procs_file:
pids_in_cgroup = set()
lines = cgroup_procs_file.readlines()
for line in lines:
pids_in_cgroup.add(line.strip())
assert pids_in_cgroup == worker_pids
def assert_cgroup_hierarchy_cleaned_up_for_node(
node_id: str, resource_isolation_config: ResourceIsolationConfig
):
"""Asserts that the cgroup hierarchy was deleted correctly for the node.
Args:
node_id: used to construct the path of the cgroup subtree
resource_isolation_config: used to construct the path of the cgroup
subtree
"""
base_cgroup_for_node = resource_isolation_config.cgroup_path
node_cgroup = Path(base_cgroup_for_node) / f"ray-node_{node_id}"
# If the root cgroup is deleted, there's no need to check anything else.
assert (
not node_cgroup.is_dir()
), f"Root cgroup node at {node_cgroup} was not deleted. Cgroup cleanup failed. You may have to manually delete the cgroup subtree."
def create_driver_in_internal_namespace():
"""
Returns a driver process that is a part of the '_ray_internal_' namespace.
If the driver is part of the '_ray_internal_' namespace, it will NOT
be moved into the workers cgroup by the raylet when it registers.
The Dashboard ServeHead and JobHead modules are drivers that are
technically system processes and use the '_ray_internal_' namespace and therefore
must not be moved into the workers cgroup on registration.
"""
driver_code = textwrap.dedent(
"""
import ray
import time
ray.init(namespace='_ray_internal_')
time.sleep(3600)
"""
).strip()
second_driver_proc = subprocess.Popen(["python", "-c", driver_code])
return second_driver_proc
# The following tests check for cgroup setup and cleanup with the
# ray cli.
def test_ray_cli_start_invalid_resource_isolation_config(cleanup_ray):
runner = CliRunner()
result = runner.invoke(
scripts.start,
["--cgroup-path=/doesnt/matter"],
)
assert result.exit_code != 0
assert isinstance(result.exception, ValueError)
def test_ray_cli_start_resource_isolation_creates_cgroup_hierarchy_and_cleans_up(
cleanup_ray,
):
cgroup_path = str(_TEST_CGROUP)
object_store_memory = 1024**3
system_reserved_memory = 1024**3
num_cpus = 4
system_reserved_cpu = 1
resource_isolation_config = ResourceIsolationConfig(
cgroup_path=cgroup_path,
enable_resource_isolation=True,
system_reserved_cpu=system_reserved_cpu,
system_reserved_memory=system_reserved_memory,
)
node_id = ray.NodeID.from_random().hex()
os.environ["RAY_OVERRIDE_NODE_ID_FOR_TESTING"] = node_id
runner = CliRunner()
result = runner.invoke(
scripts.start,
[
"--head",
"--num-cpus",
num_cpus,
"--enable-resource-isolation",
"--cgroup-path",
cgroup_path,
"--system-reserved-cpu",
system_reserved_cpu,
"--system-reserved-memory",
system_reserved_memory,
"--object-store-memory",
object_store_memory,
],
)
assert result.exit_code == 0
assert_cgroup_hierarchy_exists_for_node(node_id, resource_isolation_config)
@ray.remote(num_cpus=1)
class Actor:
def __init__(self):
pass
def get_pid(self):
return os.getpid()
second_driver_proc = create_driver_in_internal_namespace()
actor_refs = []
for _ in range(num_cpus):
actor_refs.append(Actor.remote())
worker_pids = set()
worker_pids.add(str(os.getpid()))
for actor in actor_refs:
worker_pids.add(str(ray.get(actor.get_pid.remote())))
assert_system_processes_are_in_system_cgroup(
node_id,
resource_isolation_config,
len(_EXPECTED_SYSTEM_PROCESSES_RAY_START) + len(_EXPECTED_DASHBOARD_MODULES),
)
assert_worker_processes_are_in_workers_cgroup(
node_id, resource_isolation_config, worker_pids
)
assert_process_in_not_moved_into_ray_cgroups(
node_id, resource_isolation_config, second_driver_proc.pid
)
second_driver_proc.kill()
wait_for_condition(lambda: second_driver_proc.wait(), timeout=5)
runner.invoke(scripts.stop)
assert_cgroup_hierarchy_cleaned_up_for_node(node_id, resource_isolation_config)
# The following tests will test integration of resource isolation
# with the ray.init() function.
def test_ray_init_resource_isolation_disabled_by_default(ray_shutdown):
ray.init(address="local")
node = ray._private.worker._global_node
assert node is not None
assert not node.resource_isolation_config.is_enabled()
def test_ray_init_resource_isolation_creates_cgroup_hierarchy_and_cleans_up(
ray_shutdown,
):
cgroup_path = str(_TEST_CGROUP)
system_reserved_cpu = 1
system_reserved_memory = 1024**3
object_store_memory = 1024**3
num_cpus = 4
resource_isolation_config = ResourceIsolationConfig(
enable_resource_isolation=True,
cgroup_path=cgroup_path,
system_reserved_cpu=system_reserved_cpu,
system_reserved_memory=system_reserved_memory,
)
node_id = generate_node_id()
os.environ["RAY_OVERRIDE_NODE_ID_FOR_TESTING"] = node_id
ray.init(
address="local",
num_cpus=num_cpus,
enable_resource_isolation=True,
cgroup_path=cgroup_path,
system_reserved_cpu=system_reserved_cpu,
system_reserved_memory=system_reserved_memory,
object_store_memory=object_store_memory,
)
assert_cgroup_hierarchy_exists_for_node(node_id, resource_isolation_config)
@ray.remote(num_cpus=1)
class Actor:
def __init__(self):
pass
def get_pid(self):
return os.getpid()
actor_refs = []
for _ in range(num_cpus):
actor_refs.append(Actor.remote())
worker_pids = set()
worker_pids.add(str(os.getpid()))
for actor in actor_refs:
worker_pids.add(str(ray.get(actor.get_pid.remote())))
assert_system_processes_are_in_system_cgroup(
node_id,
resource_isolation_config,
len(_EXPECTED_SYSTEM_PROCESSES_RAY_INIT) + len(_EXPECTED_DASHBOARD_MODULES),
)
assert_worker_processes_are_in_workers_cgroup(
node_id, resource_isolation_config, worker_pids
)
ray.shutdown()
assert_cgroup_hierarchy_cleaned_up_for_node(node_id, resource_isolation_config)
if __name__ == "__main__":
sys.exit(pytest.main(["-sv", __file__]))