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

472 lines
14 KiB
Python

"""Reference counting tests that require their own custom fixture.
The other reference counting tests use a shared Ray instance across the test module
to reduce overheads & overall test runtime.
"""
# coding: utf-8
import logging
import platform
import random
import sys
import time
import numpy as np
import pytest
import ray
import ray.cluster_utils
from ray._common.test_utils import (
SignalActor,
fetch_prometheus_metrics,
wait_for_condition,
)
from ray._private.internal_api import memory_summary
logger = logging.getLogger(__name__)
def _fill_object_store_and_get(obj, succeed=True, object_MiB=20, num_objects=5):
for _ in range(num_objects):
ray.put(np.zeros(object_MiB * 1024 * 1024, dtype=np.uint8))
if type(obj) is bytes:
obj = ray.ObjectRef(obj)
if succeed:
wait_for_condition(
lambda: ray._private.worker.global_worker.core_worker.object_exists(obj)
)
else:
wait_for_condition(
lambda: not ray._private.worker.global_worker.core_worker.object_exists(obj)
)
@pytest.mark.skipif(platform.system() in ["Windows"], reason="Failing on Windows.")
def test_object_unpin(ray_start_cluster):
nodes = []
cluster = ray_start_cluster
head_node = cluster.add_node(
num_cpus=0,
object_store_memory=100 * 1024 * 1024,
_system_config={
"subscriber_timeout_ms": 100,
"health_check_initial_delay_ms": 0,
"health_check_period_ms": 1000,
"health_check_failure_threshold": 5,
},
)
ray.init(address=cluster.address)
# Add worker nodes.
for i in range(2):
nodes.append(
cluster.add_node(
num_cpus=1,
resources={f"node_{i}": 1},
object_store_memory=100 * 1024 * 1024,
)
)
cluster.wait_for_nodes()
one_mb_array = np.ones(1 * 1024 * 1024, dtype=np.uint8)
ten_mb_array = np.ones(10 * 1024 * 1024, dtype=np.uint8)
@ray.remote
class ObjectsHolder:
def __init__(self):
self.ten_mb_objs = []
self.one_mb_objs = []
def put_10_mb(self):
self.ten_mb_objs.append(ray.put(ten_mb_array))
def put_1_mb(self):
self.one_mb_objs.append(ray.put(one_mb_array))
def pop_10_mb(self):
if len(self.ten_mb_objs) == 0:
return False
self.ten_mb_objs.pop()
return True
def pop_1_mb(self):
if len(self.one_mb_objs) == 0:
return False
self.one_mb_objs.pop()
return True
# Head node contains 11MB of data.
one_mb_arrays = []
ten_mb_arrays = []
one_mb_arrays.append(ray.put(one_mb_array))
ten_mb_arrays.append(ray.put(ten_mb_array))
def check_memory(mb):
return f"Plasma memory usage {mb} MiB" in memory_summary(
address=head_node.address, stats_only=True
)
def wait_until_node_dead(node):
for n in ray.nodes():
if n["ObjectStoreSocketName"] == node.address_info["object_store_address"]:
return not n["Alive"]
return False
wait_for_condition(lambda: check_memory(11))
# Pop one mb array and see if it works.
one_mb_arrays.pop()
wait_for_condition(lambda: check_memory(10))
# Pop 10 MB.
ten_mb_arrays.pop()
wait_for_condition(lambda: check_memory(0))
# Put 11 MB for each actor.
# actor 1: 1MB + 10MB
# actor 2: 1MB + 10MB
actor_on_node_1 = ObjectsHolder.options(resources={"node_0": 1}).remote()
actor_on_node_2 = ObjectsHolder.options(resources={"node_1": 1}).remote()
ray.get(actor_on_node_1.put_1_mb.remote())
ray.get(actor_on_node_1.put_10_mb.remote())
ray.get(actor_on_node_2.put_1_mb.remote())
ray.get(actor_on_node_2.put_10_mb.remote())
wait_for_condition(lambda: check_memory(22))
# actor 1: 10MB
# actor 2: 1MB
ray.get(actor_on_node_1.pop_1_mb.remote())
ray.get(actor_on_node_2.pop_10_mb.remote())
wait_for_condition(lambda: check_memory(11))
# The second node is dead, and actor 2 is dead.
cluster.remove_node(nodes[1], allow_graceful=False)
wait_for_condition(lambda: wait_until_node_dead(nodes[1]))
wait_for_condition(lambda: check_memory(10))
# The first actor is dead, so object should be GC'ed.
ray.kill(actor_on_node_1)
wait_for_condition(lambda: check_memory(0))
@pytest.mark.skipif(platform.system() in ["Windows"], reason="Failing on Windows.")
def test_object_unpin_stress(ray_start_cluster):
nodes = []
cluster = ray_start_cluster
cluster.add_node(
num_cpus=1, resources={"head": 1}, object_store_memory=1000 * 1024 * 1024
)
ray.init(address=cluster.address)
# Add worker nodes.
for i in range(2):
nodes.append(
cluster.add_node(
num_cpus=1,
resources={f"node_{i}": 1},
object_store_memory=1000 * 1024 * 1024,
)
)
cluster.wait_for_nodes()
one_mb_array = np.ones(1 * 1024 * 1024, dtype=np.uint8)
ten_mb_array = np.ones(10 * 1024 * 1024, dtype=np.uint8)
@ray.remote
class ObjectsHolder:
def __init__(self):
self.ten_mb_objs = []
self.one_mb_objs = []
def put_10_mb(self):
self.ten_mb_objs.append(ray.put(ten_mb_array))
def put_1_mb(self):
self.one_mb_objs.append(ray.put(one_mb_array))
def pop_10_mb(self):
if len(self.ten_mb_objs) == 0:
return False
self.ten_mb_objs.pop()
return True
def pop_1_mb(self):
if len(self.one_mb_objs) == 0:
return False
self.one_mb_objs.pop()
return True
def get_obj_size(self):
return len(self.ten_mb_objs) * 10 + len(self.one_mb_objs)
actor_on_node_1 = ObjectsHolder.options(resources={"node_0": 1}).remote()
actor_on_node_2 = ObjectsHolder.options(resources={"node_1": 1}).remote()
actor_on_head_node = ObjectsHolder.options(resources={"head": 1}).remote()
ray.get(actor_on_node_1.get_obj_size.remote())
ray.get(actor_on_node_2.get_obj_size.remote())
ray.get(actor_on_head_node.get_obj_size.remote())
def random_ops(actors):
r = random.random()
for actor in actors:
if r <= 0.25:
actor.put_10_mb.remote()
elif r <= 0.5:
actor.put_1_mb.remote()
elif r <= 0.75:
actor.pop_10_mb.remote()
else:
actor.pop_1_mb.remote()
total_iter = 15
for _ in range(total_iter):
random_ops([actor_on_node_1, actor_on_node_2, actor_on_head_node])
# Simulate node dead.
cluster.remove_node(nodes[1])
for _ in range(total_iter):
random_ops([actor_on_node_1, actor_on_head_node])
total_size = sum(
[
ray.get(actor_on_node_1.get_obj_size.remote()),
ray.get(actor_on_head_node.get_obj_size.remote()),
]
)
wait_for_condition(
lambda: (
(f"Plasma memory usage {total_size} MiB") in memory_summary(stats_only=True)
)
)
@pytest.mark.parametrize("inline_args", [True, False])
def test_inlined_nested_refs(ray_start_cluster, inline_args):
cluster = ray_start_cluster
config = {}
if not inline_args:
config["max_direct_call_object_size"] = 0
cluster.add_node(
num_cpus=2, object_store_memory=100 * 1024 * 1024, _system_config=config
)
ray.init(address=cluster.address)
@ray.remote
class Actor:
def __init__(self):
return
def nested(self):
return ray.put("x")
@ray.remote
def nested_nested(a):
return a.nested.remote()
@ray.remote
def foo(ref):
time.sleep(1)
return ray.get(ref)
a = Actor.remote()
nested_nested_ref = nested_nested.remote(a)
# We get nested_ref's value directly from its owner.
nested_ref = ray.get(nested_nested_ref)
del nested_nested_ref
x = foo.remote(nested_ref)
del nested_ref
ray.get(x)
# https://github.com/ray-project/ray/issues/17553
@pytest.mark.parametrize("inline_args", [True, False])
def test_return_nested_ids(shutdown_only, inline_args):
config = dict()
if inline_args:
config["max_direct_call_object_size"] = 100 * 1024 * 1024
else:
config["max_direct_call_object_size"] = 0
ray.init(object_store_memory=100 * 1024 * 1024, _system_config=config)
class Nested:
def __init__(self, blocks):
self._blocks = blocks
@ray.remote
def echo(fn):
return fn()
@ray.remote
def create_nested():
refs = [ray.put(np.random.random(1024 * 1024)) for _ in range(10)]
return Nested(refs)
@ray.remote
def test():
ref = create_nested.remote()
result1 = ray.get(ref)
del ref
result = echo.remote(lambda: result1) # noqa
del result1
time.sleep(5)
block = ray.get(result)._blocks[0]
print(ray.get(block))
ray.get(test.remote())
def _check_refcounts(expected):
actual = ray._private.worker.global_worker.core_worker.get_all_reference_counts()
assert len(expected) == len(actual)
for object_ref, (local, submitted) in expected.items():
hex_id = object_ref.hex().encode("ascii")
assert hex_id in actual
assert local == actual[hex_id]["local"]
assert submitted == actual[hex_id]["submitted"]
def test_out_of_band_serialized_object_ref(ray_start_regular):
assert (
len(ray._private.worker.global_worker.core_worker.get_all_reference_counts())
== 0
)
obj_ref = ray.put("hello")
_check_refcounts({obj_ref: (1, 0)})
obj_ref_str = ray.cloudpickle.dumps(obj_ref)
_check_refcounts({obj_ref: (2, 0)})
del obj_ref
assert (
len(ray._private.worker.global_worker.core_worker.get_all_reference_counts())
== 1
)
assert ray.get(ray.cloudpickle.loads(obj_ref_str)) == "hello"
def test_captured_object_ref(ray_start_regular):
captured_id = ray.put(np.zeros(1024, dtype=np.uint8))
@ray.remote
def f(signal):
ray.get(signal.wait.remote())
ray.get(captured_id) # noqa: F821
signal = SignalActor.remote()
obj_ref = f.remote(signal)
# Delete local references.
del f
del captured_id
# Test that the captured object ref is pinned despite having no local
# references.
ray.get(signal.send.remote())
_fill_object_store_and_get(obj_ref)
captured_id = ray.put(np.zeros(1024, dtype=np.uint8))
@ray.remote
class Actor:
def get(self, signal):
ray.get(signal.wait.remote())
ray.get(captured_id) # noqa: F821
signal = SignalActor.remote()
actor = Actor.remote()
obj_ref = actor.get.remote(signal)
# Delete local references.
del Actor
del captured_id
# Test that the captured object ref is pinned despite having no local
# references.
ray.get(signal.send.remote())
_fill_object_store_and_get(obj_ref)
def test_borrowed_id_failure_while_pulling(ray_start_cluster):
"""The driver creates an object and passes the ref to actor A via an actor
task. That task passes the ref on to B, then A kills itself before
finishing the task, so the task never reports the borrower B to the
driver. The driver can therefore erase the ref while B is still pulling
the object, and B's get must then fail promptly instead of hanging.
"""
cluster = ray_start_cluster
cluster.add_node(
num_cpus=1,
resources={"head_node": 1},
object_store_memory=100 * 1024 * 1024,
_system_config={
"testing_asio_delay_us": (
"ObjectManagerService.grpc_server.Pull=5000000000:5000000000"
),
"metrics_report_interval_ms": 200,
},
)
ray.init(address=cluster.address)
cluster.add_node(
num_cpus=1,
resources={"worker_node": 1},
object_store_memory=100 * 1024 * 1024,
)
cluster.wait_for_nodes()
@ray.remote(resources={"head_node": 1})
class A:
def pass_ref(self, ref, b):
ray.get(b.receive_ref.remote(ref))
sys.exit(-1)
@ray.remote(resources={"worker_node": 1})
class B:
def __init__(self):
self.ref = None
def receive_ref(self, ref):
self.ref = ref[0]
def resolve_ref(self):
with pytest.raises(ray.exceptions.ObjectLostError):
ray.get(self.ref)
return True
def ping(self):
return
a = A.remote()
b = B.remote()
ray.get(b.ping.remote())
obj = ray.put(np.zeros(1024 * 1024, dtype=np.uint8))
with pytest.raises(ray.exceptions.RayActorError):
ray.get(a.pass_ref.remote([obj], b))
resolved = b.resolve_ref.remote()
def pull_in_flight():
(worker_node,) = [n for n in ray.nodes() if "worker_node" in n["Resources"]]
address = (
f"{worker_node['NodeManagerAddress']}:{worker_node['MetricsExportPort']}"
)
samples = fetch_prometheus_metrics([address]).get(
"ray_pull_manager_usage_bytes", []
)
return any(
s.labels.get("Type") == "BeingPulled" and s.value > 0 for s in samples
)
# make sure to only del the last ref to let ref count go to 0 after actor B's raylet starts pulling that object.
wait_for_condition(pull_in_flight, timeout=30)
del obj
assert ray.get(resolved, timeout=30)
if __name__ == "__main__":
sys.exit(pytest.main(["-sv", __file__]))