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

788 lines
21 KiB
Python

# coding: utf-8
import array
import logging
import os
import random
import subprocess
import sys
import tempfile
import threading
import time
from unittest.mock import MagicMock, patch
import pytest
from ray._private.ray_constants import KV_NAMESPACE_FUNCTION_TABLE
from ray._private.test_utils import client_test_enabled
from ray.cluster_utils import Cluster, cluster_not_supported
from ray.exceptions import GetTimeoutError, RayTaskError
from ray.tests.client_test_utils import create_remote_signal_actor
if client_test_enabled():
from ray.util.client import ray
else:
import ray
logger = logging.getLogger(__name__)
def test_variable_number_of_args(shutdown_only):
ray.init(num_cpus=1)
@ray.remote
def varargs_fct1(*a):
return " ".join(map(str, a))
@ray.remote
def varargs_fct2(a, *b):
return " ".join(map(str, b))
x = varargs_fct1.remote(0, 1, 2)
assert ray.get(x) == "0 1 2"
x = varargs_fct2.remote(0, 1, 2)
assert ray.get(x) == "1 2"
@ray.remote
def f1(*args):
return args
@ray.remote
def f2(x, y, *args):
return x, y, args
assert ray.get(f1.remote()) == ()
assert ray.get(f1.remote(1)) == (1,)
assert ray.get(f1.remote(1, 2, 3)) == (1, 2, 3)
with pytest.raises(TypeError):
f2.remote()
with pytest.raises(TypeError):
f2.remote(1)
assert ray.get(f2.remote(1, 2)) == (1, 2, ())
assert ray.get(f2.remote(1, 2, 3)) == (1, 2, (3,))
assert ray.get(f2.remote(1, 2, 3, 4)) == (1, 2, (3, 4))
def testNoArgs(self):
@ray.remote
def no_op():
pass
self.ray_start()
ray.get(no_op.remote())
def test_defining_remote_functions(shutdown_only):
ray.init(num_cpus=3)
# Test that we can close over plain old data.
data = [
(1, 2, "a"),
[0.0, 1.0, 1 << 62],
1 << 60,
{"a": bytes(3)},
]
@ray.remote
def g():
return data
ray.get(g.remote())
# Test that we can close over modules.
@ray.remote
def h():
return array.array("d", [1.0, 2.0, 3.0])
assert ray.get(h.remote()) == array.array("d", [1.0, 2.0, 3.0])
@ray.remote
def j():
return time.time()
ray.get(j.remote())
# Test that we can define remote functions that call other remote
# functions.
@ray.remote
def k(x):
return x + 1
@ray.remote
def k2(x):
return ray.get(k.remote(x))
@ray.remote
def m(x):
return ray.get(k2.remote(x))
assert ray.get(k.remote(1)) == 2
assert ray.get(k2.remote(1)) == 2
assert ray.get(m.remote(1)) == 2
def test_redefining_remote_functions(shutdown_only):
ray.init(num_cpus=1)
# Test that we can define a remote function in the shell.
@ray.remote
def f(x):
return x + 1
assert ray.get(f.remote(0)) == 1
# Test that we can redefine the remote function.
@ray.remote
def f(x):
return x + 10
while True:
val = ray.get(f.remote(0))
assert val in [1, 10]
if val == 10:
break
else:
logger.info("Still using old definition of f, trying again.")
# Check that we can redefine functions even when the remote function source
# doesn't change (see https://github.com/ray-project/ray/issues/6130).
@ray.remote
def g():
return nonexistent()
with pytest.raises(RayTaskError, match="nonexistent"):
ray.get(g.remote())
def nonexistent():
return 1
# Redefine the function and make sure it succeeds.
@ray.remote
def g():
return nonexistent()
assert ray.get(g.remote()) == 1
# Check the same thing but when the redefined function is inside of another
# task.
@ray.remote
def h(i):
@ray.remote
def j():
return i
return j.remote()
for i in range(20):
assert ray.get(ray.get(h.remote(i))) == i
def test_call_matrix(shutdown_only):
ray.init(object_store_memory=1000 * 1024 * 1024)
@ray.remote
class Actor:
def small_value(self):
return 0
def large_value(self):
return bytes(80 * 1024 * 1024)
def echo(self, x):
if isinstance(x, list):
x = ray.get(x[0])
return x
@ray.remote
def small_value():
return 0
@ray.remote
def large_value():
return bytes(80 * 1024 * 1024)
@ray.remote
def echo(x):
if isinstance(x, list):
x = ray.get(x[0])
return x
def check(source_actor, dest_actor, is_large, out_of_band):
print(
"CHECKING",
"actor" if source_actor else "task",
"to",
"actor" if dest_actor else "task",
"large_object" if is_large else "small_object",
"out_of_band" if out_of_band else "in_band",
)
if source_actor:
a = Actor.remote()
if is_large:
x_id = a.large_value.remote()
else:
x_id = a.small_value.remote()
else:
if is_large:
x_id = large_value.remote()
else:
x_id = small_value.remote()
if out_of_band:
x_id = [x_id]
if dest_actor:
b = Actor.remote()
x = ray.get(b.echo.remote(x_id))
else:
x = ray.get(echo.remote(x_id))
if is_large:
assert isinstance(x, bytes)
else:
assert isinstance(x, int)
for is_large in [False, True]:
for source_actor in [False, True]:
for dest_actor in [False, True]:
for out_of_band in [False, True]:
check(source_actor, dest_actor, is_large, out_of_band)
def test_actor_call_order(shutdown_only):
ray.init(num_cpus=4)
@ray.remote
def small_value():
time.sleep(0.01 * random.randint(0, 10))
return 0
@ray.remote
class Actor:
def __init__(self):
self.count = 0
def inc(self, count, dependency):
assert count == self.count
self.count += 1
return count
a = Actor.remote()
assert ray.get([a.inc.remote(i, small_value.remote()) for i in range(100)]) == list(
range(100)
)
def test_actor_pass_by_ref_order_optimization(shutdown_only):
ray.init(num_cpus=4)
@ray.remote
class Actor:
def __init__(self):
pass
def f(self, x):
pass
a = Actor.remote()
@ray.remote
def fast_value():
print("fast value")
pass
@ray.remote
def slow_value():
print("start sleep")
time.sleep(30)
@ray.remote
def runner(f):
print("runner", a, f)
return ray.get(a.f.remote(f.remote()))
runner.remote(slow_value)
time.sleep(1)
x2 = runner.remote(fast_value)
start = time.time()
ray.get(x2)
delta = time.time() - start
assert delta < 10, "did not skip slow value"
@pytest.mark.parametrize(
"ray_start_cluster",
[
{
"num_cpus": 1,
"num_nodes": 1,
},
{
"num_cpus": 1,
"num_nodes": 2,
},
],
indirect=True,
)
def test_call_chain(ray_start_cluster):
@ray.remote
def g(x):
return x + 1
x = 0
for _ in range(100):
x = g.remote(x)
assert ray.get(x) == 100
@pytest.mark.xfail(cluster_not_supported, reason="cluster not supported")
@pytest.mark.skipif(client_test_enabled(), reason="init issue")
def test_system_config_when_connecting(ray_start_cluster):
config = {"object_timeout_milliseconds": 200}
cluster = Cluster()
cluster.add_node(_system_config=config, object_store_memory=100 * 1024 * 1024)
cluster.wait_for_nodes()
# Specifying _system_config when connecting to a cluster is disallowed.
with pytest.raises(ValueError):
ray.init(address=cluster.address, _system_config=config)
# Check that the config was picked up (object pinning is disabled).
ray.init(address=cluster.address)
obj_ref = ray.put(bytes(40 * 1024 * 1024))
for _ in range(5):
put_ref = ray.put(bytes(40 * 1024 * 1024))
del put_ref
ray.get(obj_ref)
def test_get_multiple(ray_start_regular_shared):
object_refs = [ray.put(i) for i in range(10)]
assert ray.get(object_refs) == list(range(10))
# Get a random choice of object refs with duplicates.
indices = [random.choice(range(10)) for i in range(5)]
indices += indices
results = ray.get([object_refs[i] for i in indices])
assert results == indices
def test_get_with_timeout(ray_start_regular_shared):
SignalActor = create_remote_signal_actor(ray)
signal = SignalActor.remote()
# Check that get() returns early if object is ready.
start = time.time()
ray.get(signal.wait.remote(should_wait=False), timeout=30)
assert time.time() - start < 30
# Check that get() raises a TimeoutError after the timeout if the object
# is not ready yet.
result_id = signal.wait.remote()
with pytest.raises(GetTimeoutError):
ray.get(result_id, timeout=0.1)
assert issubclass(GetTimeoutError, TimeoutError)
with pytest.raises(TimeoutError):
ray.get(result_id, timeout=0.1)
# timeout of 0 should raise an error
with pytest.raises(GetTimeoutError):
ray.get(result_id, timeout=0)
# Check that a subsequent get() returns early.
ray.get(signal.send.remote())
start = time.time()
ray.get(result_id, timeout=30)
assert time.time() - start < 30
# https://github.com/ray-project/ray/issues/6329
def test_call_actors_indirect_through_tasks(ray_start_regular_shared):
@ray.remote
class Counter:
def __init__(self, value):
self.value = int(value)
def increase(self, delta):
self.value += int(delta)
return self.value
@ray.remote
def foo(object):
return ray.get(object.increase.remote(1))
@ray.remote
def bar(object):
return ray.get(object.increase.remote(1))
@ray.remote
def zoo(object):
return ray.get(object[0].increase.remote(1))
c = Counter.remote(0)
for _ in range(0, 100):
ray.get(foo.remote(c))
ray.get(bar.remote(c))
ray.get(zoo.remote([c]))
def test_inline_arg_memory_corruption(ray_start_regular_shared):
@ray.remote
def f():
return bytes(1000)
@ray.remote
class Actor:
def __init__(self):
self.z = []
def add(self, x):
self.z.append(x)
for prev in self.z:
assert sum(prev) == 0, ("memory corruption detected", prev)
a = Actor.remote()
for i in range(100):
ray.get(a.add.remote(f.remote()))
@pytest.mark.skipif(client_test_enabled(), reason="internal api")
def test_skip_plasma(ray_start_regular_shared):
@ray.remote
class Actor:
def __init__(self):
pass
def f(self, x):
return x * 2
a = Actor.remote()
obj_ref = a.f.remote(1)
# it is not stored in plasma
assert not ray._private.worker.global_worker.core_worker.object_exists(obj_ref)
assert ray.get(obj_ref) == 2
@pytest.mark.skipif(client_test_enabled(), reason="internal api")
def test_actor_large_objects(ray_start_regular_shared):
@ray.remote
class Actor:
def __init__(self):
pass
def f(self):
time.sleep(1)
return bytes(80000000)
a = Actor.remote()
obj_ref = a.f.remote()
assert not ray._private.worker.global_worker.core_worker.object_exists(obj_ref)
done, _ = ray.wait([obj_ref])
assert len(done) == 1
assert ray._private.worker.global_worker.core_worker.object_exists(obj_ref)
assert isinstance(ray.get(obj_ref), bytes)
def test_actor_pass_by_ref(ray_start_regular_shared):
@ray.remote
class Actor:
def __init__(self):
pass
def f(self, x):
return x * 2
@ray.remote
def f(x):
return x
@ray.remote
def error():
sys.exit(0)
a = Actor.remote()
assert ray.get(a.f.remote(f.remote(1))) == 2
fut = [a.f.remote(f.remote(i)) for i in range(100)]
assert ray.get(fut) == [i * 2 for i in range(100)]
# propagates errors for pass by ref
with pytest.raises(Exception):
ray.get(a.f.remote(error.remote()))
def test_actor_recursive(ray_start_regular_shared):
@ray.remote
class Actor:
def __init__(self, delegate=None):
self.delegate = delegate
def f(self, x):
if self.delegate:
return ray.get(self.delegate.f.remote(x))
return x * 2
a = Actor.remote()
b = Actor.remote(a)
c = Actor.remote(b)
result = ray.get([c.f.remote(i) for i in range(100)])
assert result == [x * 2 for x in range(100)]
result, _ = ray.wait([c.f.remote(i) for i in range(100)], num_returns=100)
result = ray.get(result)
assert result == [x * 2 for x in range(100)]
def test_actor_concurrent(ray_start_regular_shared):
@ray.remote
class Batcher:
def __init__(self):
self.batch = []
self.event = threading.Event()
def add(self, x):
self.batch.append(x)
if len(self.batch) >= 3:
self.event.set()
else:
self.event.wait()
return sorted(self.batch)
a = Batcher.options(max_concurrency=3).remote()
x1 = a.add.remote(1)
x2 = a.add.remote(2)
x3 = a.add.remote(3)
r1 = ray.get(x1)
r2 = ray.get(x2)
r3 = ray.get(x3)
assert r1 == [1, 2, 3]
assert r1 == r2 == r3
def test_actor_max_concurrency(ray_start_regular_shared):
"""
Test that an actor of max_concurrency=N should only run
N tasks at most concurrently.
"""
CONCURRENCY = 3
@ray.remote
class ConcurentActor:
def __init__(self):
self.threads = set()
def call(self):
# Record the current thread that runs this function.
self.threads.add(threading.get_ident())
def get_num_threads(self):
return len(self.threads)
@ray.remote
def call(actor):
for _ in range(CONCURRENCY * 100):
ray.get(actor.call.remote())
return
actor = ConcurentActor.options(max_concurrency=CONCURRENCY).remote()
# Start many concurrent tasks that will call the actor many times.
ray.get([call.remote(actor) for _ in range(CONCURRENCY * 10)])
# Check that the number of threads shouldn't be greater than CONCURRENCY.
assert ray.get(actor.get_num_threads.remote()) <= CONCURRENCY
def test_duplicate_args(ray_start_regular_shared):
@ray.remote
def f(arg1, arg2, arg1_duplicate, kwarg1=None, kwarg2=None, kwarg1_duplicate=None):
assert arg1 == kwarg1
assert arg1 != arg2
assert arg1 == arg1_duplicate
assert kwarg1 != kwarg2
assert kwarg1 == kwarg1_duplicate
# Test by-value arguments.
arg1 = [1]
arg2 = [2]
ray.get(f.remote(arg1, arg2, arg1, kwarg1=arg1, kwarg2=arg2, kwarg1_duplicate=arg1))
# Test by-reference arguments.
arg1 = ray.put([1])
arg2 = ray.put([2])
ray.get(f.remote(arg1, arg2, arg1, kwarg1=arg1, kwarg2=arg2, kwarg1_duplicate=arg1))
# Test by-reference arguments on an actor task.
@ray.remote
class Actor:
def f(
self,
arg1,
arg2,
arg1_duplicate,
kwarg1=None,
kwarg2=None,
kwarg1_duplicate=None,
):
assert arg1 == kwarg1
assert arg1 != arg2
assert arg1 == arg1_duplicate
assert kwarg1 != kwarg2
assert kwarg1 == kwarg1_duplicate
actor = Actor.remote()
ray.get(
actor.f.remote(
arg1, arg2, arg1, kwarg1=arg1, kwarg2=arg2, kwarg1_duplicate=arg1
)
)
@pytest.mark.skipif(client_test_enabled(), reason="internal api")
def test_get_correct_node_ip():
with patch("ray._private.worker") as worker_mock:
node_mock = MagicMock()
node_mock.node_ip_address = "10.0.0.111"
worker_mock._global_node = node_mock
found_ip = ray.util.get_node_ip_address()
assert found_ip == "10.0.0.111"
def test_load_code_from_local(ray_start_regular_shared):
# This case writes a driver python file to a temporary directory.
#
# The driver starts a cluster with
# `ray.init(ray.job_config.JobConfig(code_search_path=<path list>))`,
# then creates a nested actor. The actor will be loaded from code in
# worker.
#
# This tests the following two cases when :
# 1) Load a nested class.
# 2) Load a class defined in the `__main__` module.
code_test = """
import os
import ray
class A:
@ray.remote
class B:
def get(self):
return "OK"
if __name__ == "__main__":
current_path = os.path.dirname(__file__)
job_config = ray.job_config.JobConfig(code_search_path=[current_path])
ray.init({}, job_config=job_config)
b = A.B.remote()
print(ray.get(b.get.remote()))
"""
# Test code search path contains space.
with tempfile.TemporaryDirectory(suffix="a b") as tmpdir:
test_driver = os.path.join(tmpdir, "test_load_code_from_local.py")
with open(test_driver, "w") as f:
f.write(code_test.format(repr(ray_start_regular_shared["address"])))
# Ray's handling of sys.path does not work with PYTHONSAFEPATH.
env = os.environ.copy()
if env.get("PYTHONSAFEPATH", "") != "":
env["PYTHONSAFEPATH"] = "" # Set to empty string to disable.
output = subprocess.check_output([sys.executable, test_driver], env=env)
assert b"OK" in output, f"Output has no 'OK': {output.decode()}"
def test_load_from_local_function(ray_start_regular_shared):
code_test = """
import ray
import os
@ray.remote
def wrapped_func():
return "OK"
def normal_func():
return "OK"
if __name__ == "__main__":
current_path = os.path.dirname(__file__)
job_config = ray.job_config.JobConfig(code_search_path=[current_path])
ray.init({}, job_config=job_config)
res1 = wrapped_func.remote()
wrapped_normal_func = ray.remote(normal_func)
res2 = wrapped_normal_func.remote()
s1 = ray.get(res1)
s2 = ray.get(res2)
print(s1+"+"+s2)
"""
# Test code search path contains space.
with tempfile.TemporaryDirectory(suffix="a b") as tmpdir:
test_driver = os.path.join(tmpdir, "test_load_code_from_local.py")
with open(test_driver, "w") as f:
f.write(code_test.format(repr(ray_start_regular_shared["address"])))
env = os.environ.copy()
if env.get("PYTHONSAFEPATH", "") != "":
env["PYTHONSAFEPATH"] = "" # Set to empty string to disable.
output = subprocess.check_output([sys.executable, test_driver], env=env)
assert b"OK+OK" in output, f"Output has no 'OK+OK': {output.decode()}"
@pytest.mark.skipif(
client_test_enabled(), reason="JobConfig doesn't work in client mode"
)
def test_use_dynamic_function_and_class():
# Test use dynamically defined functions
# and classes for remote tasks and actors.
# See https://github.com/ray-project/ray/issues/12834.
ray.shutdown()
current_path = os.path.dirname(__file__)
job_config = ray.job_config.JobConfig(code_search_path=[current_path])
ray.init(job_config=job_config)
def foo1():
@ray.remote
def foo2():
return "OK"
return foo2
@ray.remote
class Foo:
@ray.method(num_returns=1)
def foo(self):
return "OK"
f = foo1()
assert ray.get(f.remote()) == "OK"
# Check whether the dynamic function is exported to GCS.
# Note, the key format should be kept
# the same as in `FunctionActorManager.export`.
key_func = (
b"RemoteFunction:"
+ ray._private.worker.global_worker.current_job_id.hex().encode()
+ b":"
+ f._function_descriptor.function_id.binary()
)
assert ray._private.worker.global_worker.gcs_client.internal_kv_exists(
key_func, KV_NAMESPACE_FUNCTION_TABLE
)
foo_actor = Foo.remote()
assert ray.get(foo_actor.foo.remote()) == "OK"
# Check whether the dynamic class is exported to GCS.
# Note, the key format should be kept
# the same as in `FunctionActorManager.export_actor_class`.
key_cls = (
b"ActorClass:"
+ ray._private.worker.global_worker.current_job_id.hex().encode()
+ b":"
+ foo_actor._ray_actor_creation_function_descriptor.function_id.binary()
)
assert ray._private.worker.global_worker.gcs_client.internal_kv_exists(
key_cls, namespace=KV_NAMESPACE_FUNCTION_TABLE
)
if __name__ == "__main__":
import pytest
# Skip test_basic_2_client_mode for now- the test suite is breaking.
sys.exit(pytest.main(["-sv", __file__]))